Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for array and document indexing #199

Merged
merged 9 commits into from
Sep 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions database/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,6 @@ func (t *Table) Insert(d document.Document) ([]byte, error) {
v = document.NewNullValue()
}

// arrays and documents are not indexed.
if v.Type == document.ArrayValue || v.Type == document.DocumentValue {
continue
}

err = idx.Set(v, key)
if err != nil {
if err == index.ErrDuplicate {
Expand Down
4 changes: 2 additions & 2 deletions document/array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ func TestSortArray(t *testing.T) {
{"empty array", `[]`, `[]`},
{"numbers", `[1.4,3,2.1,-5]`, `[-5,1.4,2.1,3]`},
{"text", `["foo","bar",""]`, `["","bar","foo"]`},
{"arrays", `[[1, 2],[-1,10],[]]`, `[[1,2],[-1,10],[]]`},
{"documents", `[{"z":10},{"a":40},{}]`, `[{"z":10},{"a":40},{}]`},
{"arrays", `[[1, 2],[-1,10],[]]`, `[[],[-1,10],[1,2]]`},
{"documents", `[{"z":10},{"a":40},{}]`, `[{},{"a":40},{"z":10}]`},
{"mixed", `["foo",["a"],{},null,true,10]`, `[null,true,10,"foo",["a"],{}]`},
}

Expand Down
189 changes: 183 additions & 6 deletions document/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (op operator) String() string {

// IsEqual returns true if v is equal to the given value.
func (v Value) IsEqual(other Value) (bool, error) {
return compare(operatorEq, v, other)
return compare(operatorEq, v, other, false)
}

// IsNotEqual returns true if v is not equal to the given value.
Expand All @@ -50,25 +50,25 @@ func (v Value) IsNotEqual(other Value) (bool, error) {

// IsGreaterThan returns true if v is greather than the given value.
func (v Value) IsGreaterThan(other Value) (bool, error) {
return compare(operatorGt, v, other)
return compare(operatorGt, v, other, false)
}

// IsGreaterThanOrEqual returns true if v is greather than or equal to the given value.
func (v Value) IsGreaterThanOrEqual(other Value) (bool, error) {
return compare(operatorGte, v, other)
return compare(operatorGte, v, other, false)
}

// IsLesserThan returns true if v is lesser than the given value.
func (v Value) IsLesserThan(other Value) (bool, error) {
return compare(operatorLt, v, other)
return compare(operatorLt, v, other, false)
}

// IsLesserThanOrEqual returns true if v is lesser than or equal to the given value.
func (v Value) IsLesserThanOrEqual(other Value) (bool, error) {
return compare(operatorLte, v, other)
return compare(operatorLte, v, other, false)
}

func compare(op operator, l, r Value) (bool, error) {
func compare(op operator, l, r Value, compareDifferentTypes bool) (bool, error) {
switch {
// deal with nil
case l.Type == NullValue || r.Type == NullValue:
Expand Down Expand Up @@ -97,6 +97,25 @@ func compare(op operator, l, r Value) (bool, error) {
// compare durations together
case l.Type == DurationValue && r.Type == DurationValue:
return compareIntegers(op, int64(l.V.(time.Duration)), int64(r.V.(time.Duration))), nil

// compare arrays together
case l.Type == ArrayValue && r.Type == ArrayValue:
return compareArrays(op, l.V.(Array), r.V.(Array))

// compare documents together
case l.Type == DocumentValue && r.Type == DocumentValue:
return compareDocuments(op, l.V.(Document), r.V.(Document))
}

if compareDifferentTypes {
switch op {
case operatorEq:
return false, nil
case operatorGt, operatorGte:
return l.Type > r.Type, nil
case operatorLt, operatorLte:
return l.Type < r.Type, nil
}
}

return false, nil
Expand Down Expand Up @@ -213,3 +232,161 @@ func compareNumbers(op operator, l, r Value) (bool, error) {

return ok, nil
}

func compareArrays(op operator, l Array, r Array) (bool, error) {
var i, j int

for {
lv, lerr := l.GetByIndex(i)
rv, rerr := r.GetByIndex(j)
if lerr == nil {
i++
}
if rerr == nil {
j++
}
if lerr != nil || rerr != nil {
break
}
isEq, err := compare(operatorEq, lv, rv, true)
if err != nil {
return false, err
}
if !isEq && op != operatorEq {
return compare(op, lv, rv, true)
}
if !isEq {
return false, nil
}
}

switch {
case i > j:
switch op {
case operatorEq, operatorLt, operatorLte:
return false, nil
default:
return true, nil
}
case i < j:
switch op {
case operatorEq, operatorGt, operatorGte:
return false, nil
default:
return true, nil
}
default:
switch op {
case operatorEq, operatorGte, operatorLte:
return true, nil
default:
return false, nil
}
}
}

func compareDocuments(op operator, l, r Document) (bool, error) {
lf, err := Fields(l)
if err != nil {
return false, err
}
rf, err := Fields(r)
if err != nil {
return false, err
}

if len(lf) == 0 && len(rf) > 0 {
switch op {
case operatorEq:
return false, nil
case operatorGt:
return false, nil
case operatorGte:
return false, nil
case operatorLt:
return true, nil
case operatorLte:
return true, nil
}
}

if len(rf) == 0 && len(lf) > 0 {
switch op {
case operatorEq:
return false, nil
case operatorGt:
return true, nil
case operatorGte:
return true, nil
case operatorLt:
return false, nil
case operatorLte:
return false, nil
}
}

var i, j int

for i < len(lf) && j < len(rf) {
if cmp := strings.Compare(lf[i], rf[j]); cmp != 0 {
switch op {
case operatorEq:
return false, nil
case operatorGt:
return cmp > 0, nil
case operatorGte:
return cmp >= 0, nil
case operatorLt:
return cmp < 0, nil
case operatorLte:
return cmp <= 0, nil
}
}

lv, lerr := l.GetByField(lf[i])
rv, rerr := r.GetByField(rf[j])
if lerr == nil {
i++
}
if rerr == nil {
j++
}
if lerr != nil || rerr != nil {
break
}
isEq, err := compare(operatorEq, lv, rv, true)
if err != nil {
return false, err
}
if !isEq && op != operatorEq {
return compare(op, lv, rv, true)
}
if !isEq {
return false, nil
}
}

switch {
case i > j:
switch op {
case operatorEq, operatorLt, operatorLte:
return false, nil
default:
return true, nil
}
case i < j:
switch op {
case operatorEq, operatorGt, operatorGte:
return false, nil
default:
return true, nil
}
default:
switch op {
case operatorEq, operatorGte, operatorLte:
return true, nil
default:
return false, nil
}
}
}
72 changes: 63 additions & 9 deletions document/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,19 +182,73 @@ func TestCompare(t *testing.T) {
{"<=", "b", "b", true, toBlob},

// array
{"=", `[1]`, `[1]`, false, jsonToArray},
{"!=", `[1]`, `[1]`, false, toBlob},
{">", `[1]`, `[1]`, false, jsonToArray},
{">=", `[1]`, `[1]`, false, jsonToArray},
{"<", `[1]`, `[1]`, false, jsonToArray},
{"<=", `[1]`, `[1]`, false, jsonToArray},
{"=", `[]`, `[]`, true, jsonToArray},
{"=", `[1]`, `[1]`, true, jsonToArray},
{"=", `[1]`, `[]`, false, jsonToArray},
{"=", `[1.0, 2]`, `[1, 2]`, true, jsonToArray},
{"=", `[1,2,3]`, `[1,2,3]`, true, jsonToArray},
{"!=", `[1]`, `[5]`, true, jsonToArray},
{"!=", `[1]`, `[1, 1]`, true, jsonToArray},
{"!=", `[1,2,3]`, `[1,2,3]`, false, jsonToArray},
{"!=", `[1]`, `[]`, true, jsonToArray},
{">", `[2]`, `[1]`, true, jsonToArray},
{">", `[2]`, `[1, 1000]`, true, jsonToArray},
{">", `[1]`, `[1, 1000]`, false, jsonToArray},
{">", `[1, 2]`, `[1, 1000]`, false, jsonToArray},
{">", `[1, 10]`, `[1, true]`, true, jsonToArray},
{">", `[1, true]`, `[1, 10]`, false, jsonToArray},
{">", `[2, 1000]`, `[1]`, true, jsonToArray},
{">", `[2, 1000]`, `[2]`, true, jsonToArray},
{">", `[1,2,3]`, `[1,2,3]`, false, jsonToArray},
{">", `[1,2,3]`, `[]`, true, jsonToArray},
{">=", `[2]`, `[1]`, true, jsonToArray},
{">=", `[2]`, `[2]`, true, jsonToArray},
{">=", `[2]`, `[1, 1000]`, true, jsonToArray},
{">=", `[1]`, `[1, 1000]`, false, jsonToArray},
{">=", `[1, 2]`, `[1, 2]`, true, jsonToArray},
{">=", `[1, 2]`, `[1, 1000]`, false, jsonToArray},
{">=", `[1, 10]`, `[1, true]`, true, jsonToArray},
{">=", `[1, true]`, `[1, 10]`, false, jsonToArray},
{">=", `[2, 1000]`, `[1]`, true, jsonToArray},
{">=", `[2, 1000]`, `[2]`, true, jsonToArray},
{">=", `[1,2,3]`, `[1,2,3]`, true, jsonToArray},
{">=", `[1,2,3]`, `[]`, true, jsonToArray},
{"<", `[1]`, `[2]`, true, jsonToArray},
{"<", `[1,2,3]`, `[1,2]`, false, jsonToArray},
{"<", `[1,2,3]`, `[1,2,3]`, false, jsonToArray},
{"<", `[1,2]`, `[1,2,3]`, true, jsonToArray},
{"<", `[1, 1000]`, `[2]`, true, jsonToArray},
{"<", `[2]`, `[2, 1000]`, true, jsonToArray},
{"<", `[1,2,3]`, `[]`, false, jsonToArray},
{"<", `[]`, `[1,2,3]`, true, jsonToArray},
{"<", `[1, 10]`, `[1, true]`, false, jsonToArray},
{"<", `[1, true]`, `[1, 10]`, true, jsonToArray},
{"<=", `[1]`, `[2]`, true, jsonToArray},
{"<=", `[1, 1000]`, `[2]`, true, jsonToArray},
{"<=", `[1,2,3]`, `[1,2]`, false, jsonToArray},
{">=", `[2]`, `[1]`, true, jsonToArray},
{">=", `[2]`, `[2]`, true, jsonToArray},
{">=", `[2]`, `[1, 1000]`, true, jsonToArray},
{">=", `[2, 1000]`, `[1]`, true, jsonToArray},
{"<=", `[1,2,3]`, `[1,2,3]`, true, jsonToArray},
{"<=", `[]`, `[]`, true, jsonToArray},
{"<=", `[]`, `[1,2,3]`, true, jsonToArray},

// document
{"=", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
{"=", `{}`, `{}`, true, jsonToDocument},
{"=", `{"a": 1}`, `{"a": 1}`, true, jsonToDocument},
{"=", `{"a": 1, "b": 2}`, `{"b": 2, "a": 1}`, true, jsonToDocument},
{"=", `{"a": 1, "b": {"a": 1}}`, `{"b": {"a": 1}, "a": 1}`, true, jsonToDocument},
{">", `{"a": 2}`, `{"a": 1}`, true, jsonToDocument},
{">", `{"b": 1}`, `{"a": 1}`, true, jsonToDocument},
{">", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
{">=", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
{">", `{"a": 1}`, `{"a": true}`, true, jsonToDocument},
{"<", `{"a": 1}`, `{"a": 2}`, true, jsonToDocument},
{"<", `{"a": 1}`, `{"b": 1}`, true, jsonToDocument},
{"<", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
{"<=", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
{"<", `{"a": 1}`, `{"a": true}`, false, jsonToDocument},
{">=", `{"a": 1}`, `{"a": 1}`, true, jsonToDocument},
{"<=", `{"a": 1}`, `{"a": 1}`, true, jsonToDocument},
}

for _, test := range tests {
Expand Down
33 changes: 33 additions & 0 deletions document/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package document

import (
"errors"
"sort"
"strconv"
"strings"
)
Expand Down Expand Up @@ -41,6 +42,26 @@ func Length(d Document) (int, error) {
return len, err
}

// Fields returns a list of all the fields at the root of the document
// sorted lexicographically.
func Fields(d Document) ([]string, error) {
if fb, ok := d.(*FieldBuffer); ok {
return fb.Fields(), nil
}

var fields []string
err := d.Iterate(func(f string, _ Value) error {
fields = append(fields, f)
return nil
})
if err != nil {
return nil, err
}

sort.Strings(fields)
return fields, nil
}

// FieldBuffer stores a group of fields in memory. It implements the Document interface.
type FieldBuffer struct {
fields []fieldValue
Expand Down Expand Up @@ -260,6 +281,18 @@ func (fb *FieldBuffer) Key() []byte {
return fb.key
}

// Fields returns a sorted list of root field names.
func (fb *FieldBuffer) Fields() []string {
fields := make([]string, len(fb.fields))

for i := range fb.fields {
fields[i] = fb.fields[i].Field
}

sort.Strings(fields)
return fields
}

// A ValuePath represents the path to a particular value within a document.
type ValuePath []ValuePathFragment

Expand Down
Loading