From 026488d5baaa5287f553b59ea81136a58bfa7943 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 1 May 2022 10:04:58 +0200 Subject: [PATCH 01/45] Use gob instead of json to serialize documents --- storage.go | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/storage.go b/storage.go index dc618bd..e176d35 100644 --- a/storage.go +++ b/storage.go @@ -1,7 +1,8 @@ package clover import ( - "encoding/json" + "bytes" + "encoding/gob" "errors" "log" "sort" @@ -92,6 +93,7 @@ func (s *storageImpl) Open(path string) error { s.db = db if err == nil { s.startGC() + gob.Register(map[string]interface{}{}) } return err } @@ -146,9 +148,16 @@ func (s *storageImpl) FindAll(q *Query) ([]*Document, error) { return docs, err } -func readDoc(data []byte) (*Document, error) { +func decodeDoc(data []byte) (*Document, error) { doc := NewDocument() - return doc, json.Unmarshal(data, &doc.fields) + err := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&doc.fields) + return doc, err +} + +func encodeDoc(doc *Document) ([]byte, error) { + var buf bytes.Buffer + err := gob.NewEncoder(&buf).Encode(doc.fields) + return buf.Bytes(), err } func (s *storageImpl) FindById(collectionName string, id string) (*Document, error) { @@ -171,7 +180,7 @@ func (s *storageImpl) FindById(collectionName string, id string) (*Document, err var doc *Document err = item.Value(func(data []byte) error { - d, err := readDoc(data) + d, err := decodeDoc(data) doc = d return err }) @@ -196,7 +205,7 @@ func (s *storageImpl) Insert(collection string, docs ...*Document) error { } for _, doc := range docs { - data, err := json.Marshal(doc.fields) + data, err := encodeDoc(doc) if err != nil { return err } @@ -248,7 +257,7 @@ func (s *storageImpl) replaceDocs(txn *badger.Txn, q *Query, updater docUpdater) return err } } else { - data, err := json.Marshal(newDoc.fields) + data, err := encodeDoc(newDoc) if err != nil { return err } @@ -315,7 +324,7 @@ func (s *storageImpl) UpdateById(collectionName string, docId string, updater fu var doc *Document err = item.Value(func(value []byte) error { - d, err := readDoc(value) + d, err := decodeDoc(value) doc = d return err }) @@ -325,11 +334,11 @@ func (s *storageImpl) UpdateById(collectionName string, docId string, updater fu } updatedDoc := updater(doc) - jsonDoc, err := json.Marshal(updatedDoc.fields) + encodedDoc, err := encodeDoc(updatedDoc) if err != nil { return err } - return txn.Set([]byte(docKey), jsonDoc) + return txn.Set([]byte(docKey), encodedDoc) }) } @@ -381,7 +390,7 @@ func (s *storageImpl) iterateDocs(txn *badger.Txn, q *Query, consumer docConsume for n := 0; (q.limit < 0 || n < q.limit) && it.ValidForPrefix(prefix); it.Next() { err := it.Item().Value(func(data []byte) error { - doc, err := readDoc(data) + doc, err := decodeDoc(data) if err != nil { return err } From 13e063d3bbbeb773433ee621656284018cb6e823 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 4 May 2022 22:38:31 +0200 Subject: [PATCH 02/45] Implement comparison total ordering and normalization to new internal types --- compare.go | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++ criteria.go | 40 +++++-------- db_test.go | 20 ++++--- document.go | 8 +-- storage.go | 1 + util.go | 134 +++++++++++++++++++++++++++++++++---------- 6 files changed, 294 insertions(+), 70 deletions(-) create mode 100644 compare.go diff --git a/compare.go b/compare.go new file mode 100644 index 0000000..47f0497 --- /dev/null +++ b/compare.go @@ -0,0 +1,161 @@ +package clover + +import ( + "math/big" + "reflect" + "sort" + "strings" + "time" +) + +var typesMap map[string]int = map[string]int{ + "nil": 0, + "number": 1, + "string": 2, + "map": 3, + "slice": 4, + "boolean": 5, + "time": 6, +} + +func getTypeName(v interface{}) string { + if isNumber(v) { + return "number" + } + + switch v.(type) { + case nil: + return "null" + case time.Time: + return "time" + } + + return reflect.TypeOf(v).Kind().String() +} + +func compareTypes(v1 interface{}, v2 interface{}) int { + t1 := getTypeName(v1) + t2 := getTypeName(v2) + return typesMap[t1] - typesMap[t2] +} + +func compareSlices(s1 []interface{}, s2 []interface{}) int { + if len(s1) < len(s2) { + return -1 + } else if len(s1) > len(s2) { + return 1 + } + + for i := 0; i < len(s1); i++ { + if res := compareValues(s1[i], s2[i]); res != 0 { + return res + } + } + return 0 +} + +func compareNumbers(v1 interface{}, v2 interface{}) int { + _, isV1Float := v1.(float64) + _, isV2Float := v2.(float64) + + if isV1Float || isV2Float { + v1Float := toFloat64(v1) + v2Float := toFloat64(v2) + return big.NewFloat(v1Float).Cmp(big.NewFloat(v2Float)) + } + + _, isV1Int64 := v1.(int64) + _, isV2Int64 := v1.(int64) + + if isV1Int64 || isV2Int64 { + v1Int64 := toUint64(v1) + v2Int64 := toUint64(v2) + return int(v1Int64 - v2Int64) + } + + v1Uint64 := v1.(uint64) + v2Uint64 := v2.(uint64) + return int(v1Uint64 - v2Uint64) +} + +func compareValues(v1 interface{}, v2 interface{}) int { + if res := compareTypes(v1, v2); res != 0 { + return res + } + + if isNumber(v1) && isNumber(v2) { + return compareNumbers(v1, v2) + } + + v1Str, isStr := v1.(string) + if isStr { + v2Str := v2.(string) + return strings.Compare(v1Str, v2Str) + } + + v1Bool, isBool := v1.(bool) + if isBool { + v2Bool := v2.(bool) + return boolToInt(v1Bool) - boolToInt(v2Bool) + } + + v1Time, isTime := v1.(time.Time) + if isTime { + v2Time := v2.(time.Time) + return int(v1Time.UnixNano() - v2Time.UnixNano()) + } + + v1Slice, isSlice := v1.([]interface{}) + if isSlice { + return compareSlices(v1Slice, v2.([]interface{})) + } + + if v1 == nil { + return 0 + } + + return compareObjects(v1.(map[string]interface{}), v2.(map[string]interface{})) +} + +func getKeys(m map[string]interface{}) []string { + keys := make([]string, len(m)) + for key := range m { + keys = append(keys, key) + } + + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + return keys +} + +func compareObjects(m1 map[string]interface{}, m2 map[string]interface{}) int { + if len(m1) < len(m2) { + return -1 + } + + if len(m1) > len(m2) { + return 1 + } + + m1Keys := getKeys(m1) + m2Keys := getKeys(m2) + + for i := 0; i < len(m1Keys); i++ { + k1 := m1Keys[i] + k2 := m2Keys[i] + + if res := strings.Compare(k1, k2); res != 0 { + return res + } + + v1 := m1[k1] + v2 := m2[k2] + + if res := compareValues(v1, v2); res != 0 { + return res + } + } + return 0 +} diff --git a/criteria.go b/criteria.go index 3f5aaf3..bf87569 100644 --- a/criteria.go +++ b/criteria.go @@ -1,7 +1,6 @@ package clover import ( - "reflect" "regexp" ) @@ -55,9 +54,10 @@ func (f *field) IsNilOrNotExists() *Criteria { } func (f *field) Eq(value interface{}) *Criteria { + normalizedValue, err := normalize(value) + return &Criteria{ p: func(doc *Document) bool { - normValue, err := normalize(value) if err != nil { return false } @@ -65,7 +65,12 @@ func (f *field) Eq(value interface{}) *Criteria { if !doc.Has(f.name) { return false } - return reflect.DeepEqual(doc.Get(f.name), normValue) + + fieldValue, err := normalize(doc.Get(f.name)) + if err != nil { + return false + } + return compareValues(fieldValue, normalizedValue) == 0 }, } } @@ -77,11 +82,7 @@ func (f *field) Gt(value interface{}) *Criteria { if err != nil { return false } - v, ok := compareValues(doc.Get(f.name), normValue) - if !ok { - return false - } - return v > 0 + return compareValues(doc.Get(f.name), normValue) > 0 }, } } @@ -93,11 +94,7 @@ func (f *field) GtEq(value interface{}) *Criteria { if err != nil { return false } - v, ok := compareValues(doc.Get(f.name), normValue) - if !ok { - return false - } - return v >= 0 + return compareValues(doc.Get(f.name), normValue) >= 0 }, } } @@ -109,11 +106,7 @@ func (f *field) Lt(value interface{}) *Criteria { if err != nil { return false } - v, ok := compareValues(doc.Get(f.name), normValue) - if !ok { - return false - } - return v < 0 + return compareValues(doc.Get(f.name), normValue) < 0 }, } } @@ -125,18 +118,13 @@ func (f *field) LtEq(value interface{}) *Criteria { if err != nil { return false } - v, ok := compareValues(doc.Get(f.name), normValue) - if !ok { - return false - } - return v <= 0 + return compareValues(doc.Get(f.name), normValue) <= 0 }, } } func (f *field) Neq(value interface{}) *Criteria { - c := f.Eq(value) - return c.Not() + return f.Eq(value).Not() } func (f *field) In(values ...interface{}) *Criteria { @@ -146,7 +134,7 @@ func (f *field) In(values ...interface{}) *Criteria { for _, value := range values { normValue, err := normalize(value) if err == nil { - if reflect.DeepEqual(normValue, docValue) { + if compareValues(normValue, docValue) == 0 { return true } } diff --git a/db_test.go b/db_test.go index 2451f28..4d10121 100644 --- a/db_test.go +++ b/db_test.go @@ -187,7 +187,7 @@ func TestInsertAndGet(t *testing.T) { n, err = db.Query("myCollection").MatchPredicate(func(doc *c.Document) bool { require.True(t, doc.Has("myField")) - v, _ := doc.Get("myField").(float64) + v, _ := doc.Get("myField").(int64) return int(v)%2 == 0 }).Count() require.NoError(t, err) @@ -492,11 +492,11 @@ func TestCompareWithWrongType(t *testing.T) { n, err = db.Query("todos").Where(c.Field("completed").Lt("true")).Count() require.NoError(t, err) - require.Equal(t, n, 0) + require.Equal(t, n, 200) n, err = db.Query("todos").Where(c.Field("completed").LtEq("true")).Count() require.NoError(t, err) - require.Equal(t, n, 0) + require.Equal(t, n, 200) }) } @@ -546,11 +546,13 @@ func TestEqCriteriaWithDifferentTypes(t *testing.T) { count10, err := db.Query("todos").Where(c.Field("userId").Eq(uint64(1))).Count() require.NoError(t, err) - count11, err := db.Query("todos").Where(c.Field("userId").Eq(float32(1))).Count() - require.NoError(t, err) + /* + count11, err := db.Query("todos").Where(c.Field("userId").Eq(float32(1))).Count() + require.NoError(t, err) - count12, err := db.Query("todos").Where(c.Field("userId").Eq(float64(1))).Count() - require.NoError(t, err) + count12, err := db.Query("todos").Where(c.Field("userId").Eq(float64(1))).Count() + require.NoError(t, err) + */ require.Greater(t, count1, 0) @@ -563,8 +565,8 @@ func TestEqCriteriaWithDifferentTypes(t *testing.T) { require.Equal(t, count1, count8) require.Equal(t, count1, count9) require.Equal(t, count1, count10) - require.Equal(t, count1, count11) - require.Equal(t, count1, count12) + //require.Equal(t, count1, count11) + //require.Equal(t, count1, count12) }) } diff --git a/document.go b/document.go index f7eadff..0605e65 100644 --- a/document.go +++ b/document.go @@ -119,11 +119,9 @@ func compareDocuments(first *Document, second *Document, sortOpts []SortOption) } if firstHas && secondHas { - res, canCompare := compareValues(first.Get(field), second.Get(field)) - if canCompare { - if res != 0 { - return res * direction - } + res := compareValues(first.Get(field), second.Get(field)) + if res != 0 { + return res * direction } } } diff --git a/storage.go b/storage.go index e176d35..8089899 100644 --- a/storage.go +++ b/storage.go @@ -94,6 +94,7 @@ func (s *storageImpl) Open(path string) error { if err == nil { s.startGC() gob.Register(map[string]interface{}{}) + gob.Register(time.Time{}) } return err } diff --git a/util.go b/util.go index 87c9447..9894f65 100644 --- a/util.go +++ b/util.go @@ -1,10 +1,10 @@ package clover import ( - "encoding/json" - "math/big" + "fmt" "os" - "strings" + "reflect" + "time" ) const defaultPermDir = 0777 @@ -29,16 +29,6 @@ func copyMap(m map[string]interface{}) map[string]interface{} { return mapCopy } -func normalize(value interface{}) (interface{}, error) { - var normalized interface{} - bytes, err := json.Marshal(value) - if err != nil { - return nil, err - } - err = json.Unmarshal(bytes, &normalized) - return normalized, err -} - func boolToInt(v bool) int { if v { return 1 @@ -46,30 +36,114 @@ func boolToInt(v bool) int { return 0 } -func compareValues(v1 interface{}, v2 interface{}) (int, bool) { - v1Float, isFloat := v1.(float64) - if isFloat { - v2Float, isFloat := v2.(float64) - if isFloat { - return big.NewFloat(v1Float).Cmp(big.NewFloat(v2Float)), true +func convertMap(mapValue reflect.Value) (map[string]interface{}, error) { + // check if type is map (this is intended to be used directly) + + if mapValue.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("map key type must be a string") + } + + m := make(map[string]interface{}) + for _, key := range mapValue.MapKeys() { + value := mapValue.MapIndex(key) + + normalized, err := normalize(value.Interface()) + if err != nil { + return nil, err } + m[key.String()] = normalized } + return m, nil +} + +func convertStruct(structValue reflect.Value) (map[string]interface{}, error) { + m := make(map[string]interface{}) + for i := 0; i < structValue.NumField(); i++ { + fieldName := structValue.Type().Field(i).Name + fieldValue := structValue.Field(i) - v1Str, isStr := v1.(string) - if isStr { - v2Str, isStr := v2.(string) - if isStr { - return strings.Compare(v1Str, v2Str), true + if fieldValue.CanInterface() { + normalized, err := normalize(structValue.Field(i).Interface()) + if err != nil { + return nil, err + } + m[fieldName] = normalized } } + return m, nil +} - v1Bool, isBool := v1.(bool) - if isBool { - v2Bool, isBool := v2.(bool) - if isBool { - return boolToInt(v1Bool) - boolToInt(v2Bool), true +func convertSlice(sliceValue reflect.Value) ([]interface{}, error) { + s := make([]interface{}, 01) + for i := 0; i < sliceValue.Len(); i++ { + v, err := normalize(sliceValue.Index(i).Interface()) + if err != nil { + return nil, err } + s = append(s, v) + } + return s, nil +} + +func normalize(value interface{}) (interface{}, error) { + if value == nil { + return nil, nil } - return 0, false + if _, isTime := value.(time.Time); isTime { + return value, nil + } + + rValue := reflect.ValueOf(value) + rType := reflect.TypeOf(value) + + switch rType.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return rValue.Convert(reflect.TypeOf(int64(0))).Interface(), nil + case reflect.Float32, reflect.Float64: + return rValue.Convert(reflect.TypeOf(float64(0))).Interface(), nil + case reflect.Struct: + return convertStruct(rValue) + case reflect.Map: + return convertMap(rValue) + case reflect.String, reflect.Bool: + return value, nil + case reflect.Slice: + return convertSlice(rValue) + } + + return nil, fmt.Errorf("invalid dtype %s", rType.Name()) +} + +func isNumber(v interface{}) bool { + switch v.(type) { + case int, uint, uint8, uint16, uint32, uint64, + int8, int16, int32, int64, float32, float64: + return true + default: + return false + } +} + +func toFloat64(v interface{}) float64 { + switch vType := v.(type) { + case uint64: + return float64(vType) + case int64: + return float64(vType) + case float64: + return vType + } + panic("not a number") +} + +func toUint64(v interface{}) int64 { + switch vType := v.(type) { + case uint64: + return int64(vType) + case int64: + return vType + } + panic("not a number") } From f7b3f59640f88ecac5775d8b18480046a11b1d8c Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 4 May 2022 22:55:47 +0200 Subject: [PATCH 03/45] Cache normalized value outsize predicate function --- criteria.go | 70 +++++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/criteria.go b/criteria.go index bf87569..ebd342e 100644 --- a/criteria.go +++ b/criteria.go @@ -10,6 +10,12 @@ const ( type predicate func(doc *Document) bool +var falseCriteria Criteria = Criteria{ + p: func(_ *Document) bool { + return false + }, +} + // Criteria represents a predicate for selecting documents. // It follows a fluent API style so that you can easily chain together multiple criteria. type Criteria struct { @@ -55,69 +61,67 @@ func (f *field) IsNilOrNotExists() *Criteria { func (f *field) Eq(value interface{}) *Criteria { normalizedValue, err := normalize(value) + if err != nil { + return &falseCriteria + } return &Criteria{ p: func(doc *Document) bool { - if err != nil { - return false - } - if !doc.Has(f.name) { return false } - - fieldValue, err := normalize(doc.Get(f.name)) - if err != nil { - return false - } - return compareValues(fieldValue, normalizedValue) == 0 + return compareValues(doc.Get(f.name), normalizedValue) == 0 }, } } func (f *field) Gt(value interface{}) *Criteria { + normValue, err := normalize(value) + if err != nil { + return &falseCriteria + } + return &Criteria{ p: func(doc *Document) bool { - normValue, err := normalize(value) - if err != nil { - return false - } return compareValues(doc.Get(f.name), normValue) > 0 }, } } func (f *field) GtEq(value interface{}) *Criteria { + normValue, err := normalize(value) + if err != nil { + return &falseCriteria + } + return &Criteria{ p: func(doc *Document) bool { - normValue, err := normalize(value) - if err != nil { - return false - } return compareValues(doc.Get(f.name), normValue) >= 0 }, } } func (f *field) Lt(value interface{}) *Criteria { + normValue, err := normalize(value) + if err != nil { + return &falseCriteria + } + return &Criteria{ p: func(doc *Document) bool { - normValue, err := normalize(value) - if err != nil { - return false - } return compareValues(doc.Get(f.name), normValue) < 0 }, } } func (f *field) LtEq(value interface{}) *Criteria { + normValue, err := normalize(value) + if err != nil { + return &falseCriteria + } + return &Criteria{ p: func(doc *Document) bool { - normValue, err := normalize(value) - if err != nil { - return false - } return compareValues(doc.Get(f.name), normValue) <= 0 }, } @@ -128,15 +132,17 @@ func (f *field) Neq(value interface{}) *Criteria { } func (f *field) In(values ...interface{}) *Criteria { + normValues, err := normalize(values) + if err != nil || normValues == nil { + return &falseCriteria + } + return &Criteria{ p: func(doc *Document) bool { docValue := doc.Get(f.name) - for _, value := range values { - normValue, err := normalize(value) - if err == nil { - if compareValues(normValue, docValue) == 0 { - return true - } + for _, value := range normValues.([]interface{}) { + if compareValues(value, docValue) == 0 { + return true } } return false From 12ad2be17751139bb12cb2119ca9daee39397b2e Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 10:23:04 +0200 Subject: [PATCH 04/45] Fix wrong function name --- compare.go | 4 ++-- util.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compare.go b/compare.go index 47f0497..78b5c09 100644 --- a/compare.go +++ b/compare.go @@ -68,8 +68,8 @@ func compareNumbers(v1 interface{}, v2 interface{}) int { _, isV2Int64 := v1.(int64) if isV1Int64 || isV2Int64 { - v1Int64 := toUint64(v1) - v2Int64 := toUint64(v2) + v1Int64 := toInt64(v1) + v2Int64 := toInt64(v2) return int(v1Int64 - v2Int64) } diff --git a/util.go b/util.go index 9894f65..c7041b9 100644 --- a/util.go +++ b/util.go @@ -138,7 +138,7 @@ func toFloat64(v interface{}) float64 { panic("not a number") } -func toUint64(v interface{}) int64 { +func toInt64(v interface{}) int64 { switch vType := v.(type) { case uint64: return int64(vType) From d375ee0a861f000fa59079373961743ecef0f989 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 10:23:34 +0200 Subject: [PATCH 05/45] Return falseCriteria in case of error when compiling pattern --- criteria.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/criteria.go b/criteria.go index ebd342e..1342cfd 100644 --- a/criteria.go +++ b/criteria.go @@ -152,13 +152,12 @@ func (f *field) In(values ...interface{}) *Criteria { func (f *field) Like(pattern string) *Criteria { expr, err := regexp.Compile(pattern) + if err != nil { + return &falseCriteria + } return &Criteria{ p: func(doc *Document) bool { - if err != nil { - return false - } - s, isString := doc.Get(f.name).(string) if !isString { return false From 9ffae1e5375515fdc8a98000b866cf730d20ac99 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 10:24:18 +0200 Subject: [PATCH 06/45] Set initial slice len to 0 --- compare.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compare.go b/compare.go index 78b5c09..847d2fd 100644 --- a/compare.go +++ b/compare.go @@ -118,7 +118,7 @@ func compareValues(v1 interface{}, v2 interface{}) int { } func getKeys(m map[string]interface{}) []string { - keys := make([]string, len(m)) + keys := make([]string, 0, len(m)) for key := range m { keys = append(keys, key) } From c7b6845b68d8c2c09fe35aca7d6d5c464a7b7aec Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 10:28:17 +0200 Subject: [PATCH 07/45] Convert unsigned values to uint64 --- util.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/util.go b/util.go index c7041b9..ce754e5 100644 --- a/util.go +++ b/util.go @@ -98,8 +98,9 @@ func normalize(value interface{}) (interface{}, error) { rType := reflect.TypeOf(value) switch rType.Kind() { - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return rValue.Convert(reflect.TypeOf(uint64(0))).Interface(), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return rValue.Convert(reflect.TypeOf(int64(0))).Interface(), nil case reflect.Float32, reflect.Float64: return rValue.Convert(reflect.TypeOf(float64(0))).Interface(), nil From 5479927e659a4fa94dd3789fd59be1f10961e89c Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 10:34:50 +0200 Subject: [PATCH 08/45] Extract method registerGobTypes --- storage.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/storage.go b/storage.go index 8089899..a52664e 100644 --- a/storage.go +++ b/storage.go @@ -88,13 +88,17 @@ func (s *storageImpl) stopGC() { close(s.chQuit) } +func registerGobTypes() { + gob.Register(map[string]interface{}{}) + gob.Register(time.Time{}) +} + func (s *storageImpl) Open(path string) error { db, err := badger.Open(badger.DefaultOptions(path).WithLoggingLevel(badger.ERROR)) s.db = db if err == nil { + registerGobTypes() s.startGC() - gob.Register(map[string]interface{}{}) - gob.Register(time.Time{}) } return err } From aa94bca87f778f6e2aef5f6e753298f247eec413 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 11:03:37 +0200 Subject: [PATCH 09/45] Normalize document fields inside Set() --- db_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++- document.go | 7 ++++-- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/db_test.go b/db_test.go index 4d10121..bd24369 100644 --- a/db_test.go +++ b/db_test.go @@ -882,10 +882,69 @@ func TestDocument(t *testing.T) { fieldName := genRandomFieldName() doc.Set(fieldName, i) require.True(t, doc.Has(fieldName)) - require.Equal(t, doc.Get(fieldName), i) + require.Equal(t, doc.Get(fieldName), int64(i)) } } +func TestDocumentSetUint(t *testing.T) { + doc := c.NewDocument() + + // test uint64 conversion + doc.Set("uint", uint(0)) + require.IsType(t, uint64(0), doc.Get("uint")) + + doc.Set("uint8", uint8(0)) + require.IsType(t, uint64(0), doc.Get("uint8")) + + doc.Set("uint16", uint16(0)) + require.IsType(t, uint64(0), doc.Get("uint16")) + + doc.Set("uint32", uint16(0)) + require.IsType(t, uint64(0), doc.Get("uint32")) + + doc.Set("uint64", uint16(0)) + require.IsType(t, uint64(0), doc.Get("uint64")) +} + +func TestDocumentSetInt(t *testing.T) { + doc := c.NewDocument() + + // test int64 conversion + doc.Set("int", int(0)) + require.IsType(t, int64(0), doc.Get("int")) + + doc.Set("int8", int8(0)) + require.IsType(t, int64(0), doc.Get("int8")) + + doc.Set("int16", int16(0)) + require.IsType(t, int64(0), doc.Get("int16")) + + doc.Set("int32", int16(0)) + require.IsType(t, int64(0), doc.Get("int32")) + + doc.Set("int64", int16(0)) + require.IsType(t, int64(0), doc.Get("int64")) +} + +func TestDocumentSetFloat(t *testing.T) { + doc := c.NewDocument() + + // test float64 conversion + doc.Set("float32", float32(0)) + require.IsType(t, float64(0), doc.Get("float32")) + + doc.Set("float64", float64(0)) + require.IsType(t, float64(0), doc.Get("float64")) +} + +func TestDocumentSetInvalidType(t *testing.T) { + doc := c.NewDocument() + + // try setting an invalid type + doc.Set("chan", make(chan struct{})) + require.Nil(t, doc.Get("chan")) +} + func TestDocumentUnmarshal(t *testing.T) { runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").FindAll() diff --git a/document.go b/document.go index 0605e65..2167c00 100644 --- a/document.go +++ b/document.go @@ -82,8 +82,11 @@ func (doc *Document) Get(name string) interface{} { // Set maps a field to a value. Nested fields can be accessed using dot. func (doc *Document) Set(name string, value interface{}) { - m, _, fieldName := lookupField(name, doc.fields, true) - m[fieldName] = value + normalizedValue, err := normalize(value) + if err == nil { + m, _, fieldName := lookupField(name, doc.fields, true) + m[fieldName] = normalizedValue + } } // SetAll sets each field specified in the input map to the corresponding value. Nested fields can be accessed using dot. From ed4f0d714c73c7ed3e2879048e48a7030194d09f Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 11:04:18 +0200 Subject: [PATCH 10/45] Avoid to normalize documents inside Insert(), since fields are already normalized inside Set() --- db.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/db.go b/db.go index bf314ae..756e5e0 100644 --- a/db.go +++ b/db.go @@ -56,25 +56,13 @@ func isValidObjectId(id string) bool { // Insert adds the supplied documents to a collection. func (db *DB) Insert(collectionName string, docs ...*Document) error { - insertDocs := make([]*Document, 0, len(docs)) for _, doc := range docs { - insertDoc := NewDocument() - fields, err := normalize(doc.fields) - if err != nil { - return err - } - insertDoc.fields = fields.(map[string]interface{}) - if !isValidObjectId(doc.ObjectId()) { objectId := NewObjectId() - - insertDoc.Set(objectIdField, objectId) doc.Set(objectIdField, objectId) } - - insertDocs = append(insertDocs, insertDoc) } - return db.engine.Insert(collectionName, insertDocs...) + return db.engine.Insert(collectionName, docs...) } // Save or update a document From 68c65cb98b0dc57d67873a71bcb67b2e5ea50872 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 11:38:13 +0200 Subject: [PATCH 11/45] Fix wrong initialization of slice --- util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util.go b/util.go index ce754e5..19ab3f5 100644 --- a/util.go +++ b/util.go @@ -74,7 +74,7 @@ func convertStruct(structValue reflect.Value) (map[string]interface{}, error) { } func convertSlice(sliceValue reflect.Value) ([]interface{}, error) { - s := make([]interface{}, 01) + s := make([]interface{}, 0) for i := 0; i < sliceValue.Len(); i++ { v, err := normalize(sliceValue.Index(i).Interface()) if err != nil { From a98082b880ae43e6f6cebdb4c34d167e71ebd8e1 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 11:38:31 +0200 Subject: [PATCH 12/45] Register gob type []interface{} --- storage.go | 1 + 1 file changed, 1 insertion(+) diff --git a/storage.go b/storage.go index a52664e..e528e1d 100644 --- a/storage.go +++ b/storage.go @@ -90,6 +90,7 @@ func (s *storageImpl) stopGC() { func registerGobTypes() { gob.Register(map[string]interface{}{}) + gob.Register([]interface{}{}) gob.Register(time.Time{}) } From 474b3b1f2b708b03ecfc3c8c22c282657767f2dc Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 11:39:08 +0200 Subject: [PATCH 13/45] Add earthquakes data and test for slice comparison --- db_test.go | 19 +++ test/data/earthquakes.json | 322 +++++++++++++++++++++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 test/data/earthquakes.json diff --git a/db_test.go b/db_test.go index bd24369..faee5ac 100644 --- a/db_test.go +++ b/db_test.go @@ -17,6 +17,7 @@ import ( const ( airlinesPath = "test/data/airlines.json" todosPath = "test/data/todos.json" + earthquakes = "test/data/earthquakes.json" ) func runCloverTest(t *testing.T, jsonPath string, test func(t *testing.T, db *c.DB)) { @@ -1029,3 +1030,21 @@ func TestExportAndImportCollection(t *testing.T) { } }) } + +func TestSliceCompare(t *testing.T) { + runCloverTest(t, earthquakes, func(t *testing.T, db *c.DB) { + coords := []interface{}{127.1311, 6.5061, 26.2} + + n, err := db.Query("earthquakes").Where(c.Field("geometry.coordinates").Eq(coords)).Count() + require.NoError(t, err) + require.Equal(t, 1, n) + + n, err = db.Query("earthquakes").Where(c.Field("geometry.coordinates").GtEq(coords)).Count() + require.NoError(t, err) + require.Equal(t, 1, n) + + n, err = db.Query("earthquakes").Where(c.Field("geometry.coordinates").Lt(coords)).Count() + require.NoError(t, err) + require.Equal(t, 7, n) + }) +} diff --git a/test/data/earthquakes.json b/test/data/earthquakes.json new file mode 100644 index 0000000..80874fd --- /dev/null +++ b/test/data/earthquakes.json @@ -0,0 +1,322 @@ +[ + { + "type": "Feature", + "properties": { + "mag": 0.43, + "place": "9km NE of Aguanga, CA", + "time": 1651742164000, + "updated": 1651742373412, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40252688", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/ci40252688.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "automatic", + "tsunami": 0, + "sig": 3, + "net": "ci", + "code": "40252688", + "ids": ",ci40252688,", + "sources": ",ci,", + "types": ",nearby-cities,origin,phase-data,scitech-link,", + "nst": 14, + "dmin": 0.08271, + "rms": 0.31, + "gap": 114, + "magType": "ml", + "type": "earthquake", + "title": "M 0.4 - 9km NE of Aguanga, CA" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -116.788, + 33.4941667, + 14.73 + ] + }, + "id": "ci40252688" + }, + { + "type": "Feature", + "properties": { + "mag": 1.90999997, + "place": "4 km E of Pāhala, Hawaii", + "time": 1651741077860, + "updated": 1651741263230, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/hv73003457", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/hv73003457.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "automatic", + "tsunami": 0, + "sig": 56, + "net": "hv", + "code": "73003457", + "ids": ",hv73003457,", + "sources": ",hv,", + "types": ",origin,phase-data,", + "nst": 33, + "dmin": null, + "rms": 0.109999999, + "gap": 141, + "magType": "md", + "type": "earthquake", + "title": "M 1.9 - 4 km E of Pāhala, Hawaii" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -155.433502197266, + 19.204833984375, + 31.8299999237061 + ] + }, + "id": "hv73003457" + }, + { + "type": "Feature", + "properties": { + "mag": 1.1, + "place": "32 km SE of Mina, Nevada", + "time": 1651740832067, + "updated": 1651741064918, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/nn00838379", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/nn00838379.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "automatic", + "tsunami": 0, + "sig": 19, + "net": "nn", + "code": "00838379", + "ids": ",nn00838379,", + "sources": ",nn,", + "types": ",origin,phase-data,", + "nst": 9, + "dmin": 0.09, + "rms": 0.1808, + "gap": 182.89, + "magType": "ml", + "type": "earthquake", + "title": "M 1.1 - 32 km SE of Mina, Nevada" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -117.8573, + 38.1792, + 3.8 + ] + }, + "id": "nn00838379" + }, + { + "type": "Feature", + "properties": { + "mag": 1.6, + "place": "31 km NE of Paxson, Alaska", + "time": 1651740604282, + "updated": 1651740843535, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ak0225qv8ko3", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/ak0225qv8ko3.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "automatic", + "tsunami": 0, + "sig": 39, + "net": "ak", + "code": "0225qv8ko3", + "ids": ",ak0225qv8ko3,", + "sources": ",ak,", + "types": ",origin,phase-data,", + "nst": null, + "dmin": null, + "rms": 1.04, + "gap": null, + "magType": "ml", + "type": "earthquake", + "title": "M 1.6 - 31 km NE of Paxson, Alaska" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -145.104, + 63.2543, + 0 + ] + }, + "id": "ak0225qv8ko3" + }, + { + "type": "Feature", + "properties": { + "mag": 1.4, + "place": "25 km SSE of Manley Hot Springs, Alaska", + "time": 1651740047653, + "updated": 1651740941735, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ak0225qv6iue", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/ak0225qv6iue.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "automatic", + "tsunami": 0, + "sig": 30, + "net": "ak", + "code": "0225qv6iue", + "ids": ",ak0225qv6iue,", + "sources": ",ak,", + "types": ",origin,phase-data,", + "nst": null, + "dmin": null, + "rms": 0.7, + "gap": null, + "magType": "ml", + "type": "earthquake", + "title": "M 1.4 - 25 km SSE of Manley Hot Springs, Alaska" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -150.3704, + 64.8061, + 4.5 + ] + }, + "id": "ak0225qv6iue" + }, + { + "type": "Feature", + "properties": { + "mag": 1.5, + "place": "33 km NNW of Petersville, Alaska", + "time": 1651740000314, + "updated": 1651740246415, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ak0225qv6c3y", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/ak0225qv6c3y.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "automatic", + "tsunami": 0, + "sig": 35, + "net": "ak", + "code": "0225qv6c3y", + "ids": ",ak0225qv6c3y,", + "sources": ",ak,", + "types": ",origin,phase-data,", + "nst": null, + "dmin": null, + "rms": 0.96, + "gap": null, + "magType": "ml", + "type": "earthquake", + "title": "M 1.5 - 33 km NNW of Petersville, Alaska" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -150.9951, + 62.7736, + 107.6 + ] + }, + "id": "ak0225qv6c3y" + }, + { + "type": "Feature", + "properties": { + "mag": 1.42, + "place": "40km ENE of Olancha, CA", + "time": 1651738909910, + "updated": 1651739121545, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/ci40252680", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/ci40252680.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "automatic", + "tsunami": 0, + "sig": 31, + "net": "ci", + "code": "40252680", + "ids": ",ci40252680,", + "sources": ",ci,", + "types": ",nearby-cities,origin,phase-data,scitech-link,", + "nst": 14, + "dmin": 0.1471, + "rms": 0.15, + "gap": 110, + "magType": "ml", + "type": "earthquake", + "title": "M 1.4 - 40km ENE of Olancha, CA" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -117.5981667, + 36.4188333, + -0.12 + ] + }, + "id": "ci40252680" + }, + { + "type": "Feature", + "properties": { + "mag": 5.9, + "place": "96 km SE of Lukatan, Philippines", + "time": 1651738877171, + "updated": 1651741886073, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/us7000h781", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/us7000h781.geojson", + "felt": 7, + "cdi": 4.6, + "mmi": 4.022, + "alert": "green", + "status": "reviewed", + "tsunami": 0, + "sig": 539, + "net": "us", + "code": "7000h781", + "ids": ",us7000h781,", + "sources": ",us,", + "types": ",dyfi,losspager,origin,phase-data,shakemap,", + "nst": null, + "dmin": 1.639, + "rms": 0.93, + "gap": 52, + "magType": "mww", + "type": "earthquake", + "title": "M 5.9 - 96 km SE of Lukatan, Philippines" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 127.1311, + 6.5061, + 26.2 + ] + }, + "id": "us7000h781" + } +] \ No newline at end of file From 65970365624f8f57e4b8e2c0a0513e4b2080a65a Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 11:51:40 +0200 Subject: [PATCH 14/45] Add TestComparison() --- db_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/db_test.go b/db_test.go index faee5ac..4df4610 100644 --- a/db_test.go +++ b/db_test.go @@ -1048,3 +1048,33 @@ func TestSliceCompare(t *testing.T) { require.Equal(t, 7, n) }) } + +func TestObjectComparison(t *testing.T) { + runCloverTest(t, "", func(t *testing.T, db *c.DB) { + err := db.CreateCollection("myCollection") + require.NoError(t, err) + + data := map[string]interface{}{ + "SomeNumber": float64(0), + "SomeString": "aString", + } + + dataAsStruct := struct { + SomeNumber int + SomeString string + }{0, "aString"} + + doc := c.NewDocument() + doc.SetAll(map[string]interface{}{ + "data": data, + }) + + _, err = db.InsertOne("myCollection", doc) + require.NoError(t, err) + + queryDoc, err := db.Query("myCollection").Where(c.Field("data").Eq(dataAsStruct)).FindFirst() + require.NoError(t, err) + + require.Equal(t, doc, queryDoc) + }) +} From d2918eb31cb735f3c6488004acceb1d822bde227 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 14:03:21 +0200 Subject: [PATCH 15/45] Fix Conversion issue. Handle pointer types --- db_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ util.go | 25 ++++++++++++++++++++----- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/db_test.go b/db_test.go index 4df4610..f3ef343 100644 --- a/db_test.go +++ b/db_test.go @@ -938,6 +938,47 @@ func TestDocumentSetFloat(t *testing.T) { require.IsType(t, float64(0), doc.Get("float64")) } +func TestDocumentSetPointer(t *testing.T) { + doc := c.NewDocument() + + var x int = 100 + ptr := &x + dPtr := &ptr + + doc.Set("*int", ptr) + + v := doc.Get("*int") + + require.NotEqual(t, &v, ptr) + require.Equal(t, v, int64(100)) + + doc.Set("**int", dPtr) + v1 := doc.Get("**int") + require.NotEqual(t, &v1, dPtr) + + require.Equal(t, doc.Get("**int"), int64(100)) + + var intPtr *int = nil + doc.Set("intPtr", intPtr) + require.True(t, doc.Has("intPtr")) + require.Nil(t, doc.Get("intPtr")) + + s := "hello" + var sPtr *string = &s + + doc.Set("string", &sPtr) + + s = "clover" // this statement should not affect the document field + + require.Equal(t, "hello", doc.Get("string")) + + sPtr = nil + doc.Set("string", sPtr) + + require.True(t, doc.Has("string")) + require.Nil(t, doc.Get("string")) +} + func TestDocumentSetInvalidType(t *testing.T) { doc := c.NewDocument() diff --git a/util.go b/util.go index 19ab3f5..b32ed7b 100644 --- a/util.go +++ b/util.go @@ -85,18 +85,31 @@ func convertSlice(sliceValue reflect.Value) ([]interface{}, error) { return s, nil } +func getElemValueAndType(v interface{}) (reflect.Value, reflect.Type) { + rv := reflect.ValueOf(v) + rt := reflect.TypeOf(v) + + for rt.Kind() == reflect.Ptr && !rv.IsNil() { + rt = rt.Elem() + rv = rv.Elem() + } + return rv, rt +} + func normalize(value interface{}) (interface{}, error) { if value == nil { return nil, nil } + rValue, rType := getElemValueAndType(value) + if rType.Kind() == reflect.Ptr { + return nil, nil + } + if _, isTime := value.(time.Time); isTime { return value, nil } - rValue := reflect.ValueOf(value) - rType := reflect.TypeOf(value) - switch rType.Kind() { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return rValue.Convert(reflect.TypeOf(uint64(0))).Interface(), nil @@ -108,8 +121,10 @@ func normalize(value interface{}) (interface{}, error) { return convertStruct(rValue) case reflect.Map: return convertMap(rValue) - case reflect.String, reflect.Bool: - return value, nil + case reflect.String: + return rValue.String(), nil + case reflect.Bool: + return rValue.Bool(), nil case reflect.Slice: return convertSlice(rValue) } From 4e01aa0600ada493a430e5120da9fb9c5cd75dfc Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 5 May 2022 14:40:00 +0200 Subject: [PATCH 16/45] Remove commented lines --- db_test.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/db_test.go b/db_test.go index f3ef343..f506b4e 100644 --- a/db_test.go +++ b/db_test.go @@ -547,13 +547,11 @@ func TestEqCriteriaWithDifferentTypes(t *testing.T) { count10, err := db.Query("todos").Where(c.Field("userId").Eq(uint64(1))).Count() require.NoError(t, err) - /* - count11, err := db.Query("todos").Where(c.Field("userId").Eq(float32(1))).Count() - require.NoError(t, err) + count11, err := db.Query("todos").Where(c.Field("userId").Eq(float32(1))).Count() + require.NoError(t, err) - count12, err := db.Query("todos").Where(c.Field("userId").Eq(float64(1))).Count() - require.NoError(t, err) - */ + count12, err := db.Query("todos").Where(c.Field("userId").Eq(float64(1))).Count() + require.NoError(t, err) require.Greater(t, count1, 0) @@ -566,8 +564,8 @@ func TestEqCriteriaWithDifferentTypes(t *testing.T) { require.Equal(t, count1, count8) require.Equal(t, count1, count9) require.Equal(t, count1, count10) - //require.Equal(t, count1, count11) - //require.Equal(t, count1, count12) + require.Equal(t, count1, count11) + require.Equal(t, count1, count12) }) } From 6ced290b68750b92dd77d57372b15f06e17e12f0 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 10:10:32 +0200 Subject: [PATCH 17/45] Add module encoding for encoding specific functions --- criteria.go | 14 +-- db_test.go | 187 ++++++++++++++++++++++----------- document.go | 19 ++-- encoding/encoding.go | 243 +++++++++++++++++++++++++++++++++++++++++++ storage.go | 19 +--- util.go | 99 ------------------ 6 files changed, 389 insertions(+), 192 deletions(-) create mode 100644 encoding/encoding.go diff --git a/criteria.go b/criteria.go index 1342cfd..0b00573 100644 --- a/criteria.go +++ b/criteria.go @@ -2,6 +2,8 @@ package clover import ( "regexp" + + "github.com/ostafen/clover/encoding" ) const ( @@ -60,7 +62,7 @@ func (f *field) IsNilOrNotExists() *Criteria { } func (f *field) Eq(value interface{}) *Criteria { - normalizedValue, err := normalize(value) + normalizedValue, err := encoding.Normalize(value) if err != nil { return &falseCriteria } @@ -76,7 +78,7 @@ func (f *field) Eq(value interface{}) *Criteria { } func (f *field) Gt(value interface{}) *Criteria { - normValue, err := normalize(value) + normValue, err := encoding.Normalize(value) if err != nil { return &falseCriteria } @@ -89,7 +91,7 @@ func (f *field) Gt(value interface{}) *Criteria { } func (f *field) GtEq(value interface{}) *Criteria { - normValue, err := normalize(value) + normValue, err := encoding.Normalize(value) if err != nil { return &falseCriteria } @@ -102,7 +104,7 @@ func (f *field) GtEq(value interface{}) *Criteria { } func (f *field) Lt(value interface{}) *Criteria { - normValue, err := normalize(value) + normValue, err := encoding.Normalize(value) if err != nil { return &falseCriteria } @@ -115,7 +117,7 @@ func (f *field) Lt(value interface{}) *Criteria { } func (f *field) LtEq(value interface{}) *Criteria { - normValue, err := normalize(value) + normValue, err := encoding.Normalize(value) if err != nil { return &falseCriteria } @@ -132,7 +134,7 @@ func (f *field) Neq(value interface{}) *Criteria { } func (f *field) In(values ...interface{}) *Criteria { - normValues, err := normalize(values) + normValues, err := encoding.Normalize(values) if err != nil || normValues == nil { return &falseCriteria } diff --git a/db_test.go b/db_test.go index f506b4e..cbf9df9 100644 --- a/db_test.go +++ b/db_test.go @@ -6,9 +6,11 @@ import ( "math/rand" "os" "path/filepath" + "reflect" "sort" "strings" "testing" + "time" c "github.com/ostafen/clover" "github.com/stretchr/testify/require" @@ -20,7 +22,16 @@ const ( earthquakes = "test/data/earthquakes.json" ) -func runCloverTest(t *testing.T, jsonPath string, test func(t *testing.T, db *c.DB)) { +type TodoModel struct { + Title string `json:"title" clover:"title"` + Completed bool `json:"completed,omitempty" clover:"completed"` + Id int `json:"id" clover:"id"` + UserId int `json:"userId" clover:"userId"` + CompletedDate *time.Time `json:"completed_date,omitempty" clover:"completed_date,omitempty"` + Notes *string `json:"notes,omitempty" clover:"notes,omitempty"` +} + +func runCloverTest(t *testing.T, jsonPath string, dataModel interface{}, test func(t *testing.T, db *c.DB)) { dir, err := ioutil.TempDir("", "clover-test") require.NoError(t, err) @@ -30,8 +41,8 @@ func runCloverTest(t *testing.T, jsonPath string, test func(t *testing.T, db *c. require.NoError(t, err) if jsonPath != "" { - require.NoError(t, loadFromJson(inMemDb, jsonPath)) - require.NoError(t, loadFromJson(db, jsonPath)) + require.NoError(t, loadFromJson(inMemDb, jsonPath, dataModel)) + require.NoError(t, loadFromJson(db, jsonPath, dataModel)) } defer func() { @@ -44,7 +55,7 @@ func runCloverTest(t *testing.T, jsonPath string, test func(t *testing.T, db *c. } func TestErrCollectionNotExist(t *testing.T) { - runCloverTest(t, "", func(t *testing.T, db *c.DB) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { q := db.Query("myCollection") _, err := q.Count() require.Equal(t, c.ErrCollectionNotExist, err) @@ -70,7 +81,7 @@ func TestErrCollectionNotExist(t *testing.T) { } func TestCreateCollectionAndDrop(t *testing.T) { - runCloverTest(t, "", func(t *testing.T, db *c.DB) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { err := db.CreateCollection("myCollection") require.NoError(t, err) has, err := db.HasCollection("myCollection") @@ -93,7 +104,7 @@ func TestCreateCollectionAndDrop(t *testing.T) { } func TestInsertOneAndDelete(t *testing.T) { - runCloverTest(t, "", func(t *testing.T, db *c.DB) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { err := db.CreateCollection("myCollection") require.NoError(t, err) @@ -126,7 +137,7 @@ func TestInsertOneAndDelete(t *testing.T) { } func TestInsert(t *testing.T) { - runCloverTest(t, "", func(t *testing.T, db *c.DB) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { err := db.CreateCollection("myCollection") require.NoError(t, err) @@ -139,7 +150,7 @@ func TestInsert(t *testing.T) { } func TestSaveDocument(t *testing.T) { - runCloverTest(t, "", func(t *testing.T, db *c.DB) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { err := db.CreateCollection("myCollection") require.NoError(t, err) @@ -168,7 +179,7 @@ func TestSaveDocument(t *testing.T) { } func TestInsertAndGet(t *testing.T) { - runCloverTest(t, "", func(t *testing.T, db *c.DB) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { err := db.CreateCollection("myCollection") require.NoError(t, err) @@ -197,8 +208,8 @@ func TestInsertAndGet(t *testing.T) { }) } -func loadFromJson(db *c.DB, filename string) error { - objects := make([]map[string]interface{}, 0) +func loadFromJson(db *c.DB, filename string, model interface{}) error { + var objects []interface{} data, err := ioutil.ReadFile(filename) if err != nil { @@ -216,15 +227,30 @@ func loadFromJson(db *c.DB, filename string) error { docs := make([]*c.Document, 0) for _, obj := range objects { - doc := c.NewDocumentOf(obj) + data, err := json.Marshal(obj) + if err != nil { + return err + } + + var fields interface{} + if model == nil { + fields = make(map[string]interface{}) + } else { + fields = reflect.New(reflect.TypeOf(model)).Interface() + } + + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + + doc := c.NewDocumentOf(fields) docs = append(docs, doc) } - return db.Insert(collectionName, docs...) } func TestUpdateCollection(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { criteria := c.Field("completed").Eq(true) updates := make(map[string]interface{}) updates["completed"] = false @@ -249,7 +275,7 @@ func TestUpdateCollection(t *testing.T) { } func TestUpdateById(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { doc, err := db.Query("todos").FindFirst() require.NoError(t, err) @@ -270,7 +296,7 @@ func TestUpdateById(t *testing.T) { } func TestReplaceById(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { doc, err := db.Query("todos").FindFirst() require.NoError(t, err) @@ -295,7 +321,7 @@ func TestReplaceById(t *testing.T) { } func TestInsertAndDelete(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { criteria := c.Field("completed").Eq(true) err := db.Query("todos").Where(criteria).Delete() require.NoError(t, err) @@ -318,7 +344,7 @@ func TestOpenExisting(t *testing.T) { db, err := c.Open(dir) require.NoError(t, err) - require.NoError(t, loadFromJson(db, todosPath)) + require.NoError(t, loadFromJson(db, todosPath, nil)) require.NoError(t, db.Close()) db, err = c.Open(dir) @@ -366,7 +392,7 @@ func TestReloadIndex(t *testing.T) { } func TestInvalidCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("completed").Eq(func() {})).FindAll() require.NoError(t, err) require.Equal(t, len(docs), 0) @@ -398,7 +424,7 @@ func TestInvalidCriteria(t *testing.T) { } func TestExistsCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Where(c.Field("completed_date").Exists()).Count() require.NoError(t, err) m, err := db.Query("todos").Where(c.Field("completed").IsTrue()).Count() @@ -407,7 +433,7 @@ func TestExistsCriteria(t *testing.T) { } func TestNotExistsCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Where(c.Field("completed_date").NotExists()).Count() require.NoError(t, err) @@ -417,7 +443,7 @@ func TestNotExistsCriteria(t *testing.T) { } func TestIsNil(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, nil, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Where(c.Field("notes").IsNil()).Count() require.NoError(t, err) require.Equal(t, n, 1) @@ -425,7 +451,7 @@ func TestIsNil(t *testing.T) { } func TestIsTrue(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Where(c.Field("completed").Eq(true)).Count() require.NoError(t, err) @@ -437,7 +463,7 @@ func TestIsTrue(t *testing.T) { } func TestIsFalse(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Where(c.Field("completed").Eq(false)).Count() require.NoError(t, err) @@ -449,7 +475,7 @@ func TestIsFalse(t *testing.T) { } func TestIsNilOrNotExist(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Where(c.Field("completed_date").IsNilOrNotExists()).Count() require.NoError(t, err) m, err := db.Query("todos").Where(c.Field("completed").IsFalse()).Count() @@ -458,7 +484,7 @@ func TestIsNilOrNotExist(t *testing.T) { } func TestEqCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("completed").Eq(true)).FindAll() require.NoError(t, err) require.Greater(t, len(docs), 0) @@ -471,7 +497,7 @@ func TestEqCriteria(t *testing.T) { } func TestBoolCompare(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Where(c.Field("completed").Eq(true)).Count() require.NoError(t, err) m, err := db.Query("todos").Where(c.Field("completed").Gt(false)).Count() @@ -482,7 +508,7 @@ func TestBoolCompare(t *testing.T) { } func TestCompareWithWrongType(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Where(c.Field("completed").Gt("true")).Count() require.NoError(t, err) require.Equal(t, n, 0) @@ -502,7 +528,7 @@ func TestCompareWithWrongType(t *testing.T) { } func TestCompareString(t *testing.T) { - runCloverTest(t, airlinesPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, airlinesPath, nil, func(t *testing.T, db *c.DB) { docs, err := db.Query("airlines").Where(c.Field("Airport.Code").Gt("CLT")).FindAll() require.NoError(t, err) require.Greater(t, len(docs), 0) @@ -516,7 +542,7 @@ func TestCompareString(t *testing.T) { } func TestEqCriteriaWithDifferentTypes(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { count1, err := db.Query("todos").Where(c.Field("userId").Eq(int(1))).Count() require.NoError(t, err) @@ -570,7 +596,7 @@ func TestEqCriteriaWithDifferentTypes(t *testing.T) { } func TestNeqCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("userId").Neq(7)).FindAll() require.NoError(t, err) require.Greater(t, len(docs), 0) @@ -583,46 +609,46 @@ func TestNeqCriteria(t *testing.T) { } func TestGtCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("userId").Gt(4)).FindAll() require.NoError(t, err) require.Greater(t, len(docs), 0) for _, doc := range docs { require.NotNil(t, doc.Get("userId")) - require.Greater(t, doc.Get("userId"), float64(4)) + require.Greater(t, doc.Get("userId"), int64(4)) } }) } func TestGtEqCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("userId").GtEq(4)).FindAll() require.NoError(t, err) require.Greater(t, len(docs), 0) for _, doc := range docs { require.NotNil(t, doc.Get("userId")) - require.GreaterOrEqual(t, doc.Get("userId"), float64(4)) + require.GreaterOrEqual(t, doc.Get("userId"), int64(4)) } }) } func TestLtCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("userId").Lt(4)).FindAll() require.NoError(t, err) require.Greater(t, len(docs), 0) for _, doc := range docs { require.NotNil(t, doc.Get("userId")) - require.Less(t, doc.Get("userId"), float64(4)) + require.Less(t, doc.Get("userId"), int64(4)) } }) } func TestLtEqCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("userId").LtEq(4)).FindAll() require.NoError(t, err) @@ -630,13 +656,13 @@ func TestLtEqCriteria(t *testing.T) { for _, doc := range docs { require.NotNil(t, doc.Get("userId")) - require.LessOrEqual(t, doc.Get("userId"), float64(4)) + require.LessOrEqual(t, doc.Get("userId"), int64(4)) } }) } func TestInCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("userId").In(5, 8)).FindAll() require.NoError(t, err) @@ -646,7 +672,7 @@ func TestInCriteria(t *testing.T) { userId := doc.Get("userId") require.NotNil(t, userId) - if userId != float64(5) && userId != float64(8) { + if userId != int64(5) && userId != int64(8) { require.Fail(t, "userId is not in the correct range") } } @@ -654,7 +680,7 @@ func TestInCriteria(t *testing.T) { } func TestChainedWhere(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("completed").Eq(true)).Where(c.Field("userId").Gt(2)).FindAll() require.NoError(t, err) @@ -663,13 +689,13 @@ func TestChainedWhere(t *testing.T) { require.NotNil(t, doc.Get("completed")) require.NotNil(t, doc.Get("userId")) require.Equal(t, doc.Get("completed"), true) - require.Greater(t, doc.Get("userId"), float64(2)) + require.Greater(t, doc.Get("userId"), int64(2)) } }) } func TestAndCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { criteria := c.Field("completed").Eq(true).And(c.Field("userId").Gt(2)) docs, err := db.Query("todos").Where(criteria).FindAll() require.NoError(t, err) @@ -679,13 +705,13 @@ func TestAndCriteria(t *testing.T) { require.NotNil(t, doc.Get("completed")) require.NotNil(t, doc.Get("userId")) require.Equal(t, doc.Get("completed"), true) - require.Greater(t, doc.Get("userId"), float64(2)) + require.Greater(t, doc.Get("userId"), int64(2)) } }) } func TestOrCriteria(t *testing.T) { - runCloverTest(t, airlinesPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, airlinesPath, nil, func(t *testing.T, db *c.DB) { criteria := c.Field("Statistics.Flights.Cancelled").Gt(100).Or(c.Field("Statistics.Flights.Total").GtEq(1000)) docs, err := db.Query("airlines").Where(criteria).FindAll() require.NoError(t, err) @@ -704,7 +730,7 @@ func TestOrCriteria(t *testing.T) { } func TestLikeCriteria(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { likeCriteria := c.Field("title").Like(".*est.*") docs, err := db.Query("todos").Where(likeCriteria).FindAll() @@ -731,8 +757,42 @@ func TestLikeCriteria(t *testing.T) { }) } +func TestTimeRangeQuery(t *testing.T) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { + start := time.Date(2020, 06, 10, 0, 0, 0, 0, time.UTC) + end := time.Date(2021, 03, 20, 0, 0, 0, 0, time.UTC) + + allDocs, err := db.Query("todos").FindAll() + require.NoError(t, err) + + n := 0 + for _, doc := range allDocs { + date := doc.Get("completed_date") + if date == nil { + continue + } + require.IsType(t, time.Time{}, date) + + completedDate := date.(time.Time) + if completedDate.Unix() >= start.Unix() && completedDate.Unix() <= end.Unix() { + n++ + } + } + + docs, err := db.Query("todos").Where(c.Field("completed_date").GtEq(start).And(c.Field("completed_date").Lt(end))).FindAll() + require.NoError(t, err) + require.Len(t, docs, n) + + for _, doc := range docs { + date := doc.Get("completed_date") + require.GreaterOrEqual(t, date, start) + require.Less(t, date, end) + } + }) +} + func TestLimit(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Count() require.NoError(t, err) @@ -745,7 +805,7 @@ func TestLimit(t *testing.T) { } func TestSkip(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { sortOption := c.SortOption{ Field: "id", Direction: 1, @@ -763,7 +823,7 @@ func TestSkip(t *testing.T) { } func TestLimitAndSkip(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { sortOption := c.SortOption{ Field: "id", Direction: 1, @@ -781,7 +841,7 @@ func TestLimitAndSkip(t *testing.T) { } func TestFindFirst(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { doc, err := db.Query("todos").Where(c.Field("completed").Eq(true)).FindFirst() require.NoError(t, err) require.NotNil(t, doc) @@ -791,7 +851,7 @@ func TestFindFirst(t *testing.T) { } func TestExists(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { exists, err := db.Query("todos").Where(c.Field("completed").IsTrue()).Exists() require.NoError(t, err) require.True(t, exists) @@ -803,7 +863,7 @@ func TestExists(t *testing.T) { } func TestForEach(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n, err := db.Query("todos").Where(c.Field("completed").IsTrue()).Count() require.NoError(t, err) @@ -818,7 +878,7 @@ func TestForEach(t *testing.T) { } func TestSort(t *testing.T) { - runCloverTest(t, airlinesPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, airlinesPath, nil, func(t *testing.T, db *c.DB) { sortOpts := []c.SortOption{{"Statistics.Flights.Total", 1}, {"Statistics.Flights.Cancelled", -1}} docs, err := db.Query("airlines").Sort(sortOpts...).FindAll() @@ -847,7 +907,7 @@ func TestSort(t *testing.T) { } func TestForEachStop(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { n := 0 err := db.Query("todos").ForEach(func(doc *c.Document) bool { if n < 100 { @@ -986,14 +1046,15 @@ func TestDocumentSetInvalidType(t *testing.T) { } func TestDocumentUnmarshal(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").FindAll() require.NoError(t, err) todo := &struct { - Completed bool `json:"completed"` - Title string `json:"title"` - UserId int `json:"userId"` + Completed bool `clover:"completed"` + Title string `clover:"title"` + UserId float32 `clover:"userId"` + CompletedDate *time.Time `clover:"completed_date"` }{} require.Greater(t, len(docs), 0) @@ -1005,7 +1066,7 @@ func TestDocumentUnmarshal(t *testing.T) { } func TestListCollections(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { collections, err := db.ListCollections() require.NoError(t, err) require.Equal(t, 1, len(collections)) @@ -1044,7 +1105,7 @@ func TestListCollections(t *testing.T) { } func TestExportAndImportCollection(t *testing.T) { - runCloverTest(t, todosPath, func(t *testing.T, db *c.DB) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { exportPath, err := ioutil.TempDir("", "export-dir") require.NoError(t, err) defer os.RemoveAll(exportPath) @@ -1071,7 +1132,7 @@ func TestExportAndImportCollection(t *testing.T) { } func TestSliceCompare(t *testing.T) { - runCloverTest(t, earthquakes, func(t *testing.T, db *c.DB) { + runCloverTest(t, earthquakes, nil, func(t *testing.T, db *c.DB) { coords := []interface{}{127.1311, 6.5061, 26.2} n, err := db.Query("earthquakes").Where(c.Field("geometry.coordinates").Eq(coords)).Count() @@ -1089,7 +1150,7 @@ func TestSliceCompare(t *testing.T) { } func TestObjectComparison(t *testing.T) { - runCloverTest(t, "", func(t *testing.T, db *c.DB) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { err := db.CreateCollection("myCollection") require.NoError(t, err) diff --git a/document.go b/document.go index 2167c00..45af5dc 100644 --- a/document.go +++ b/document.go @@ -1,8 +1,9 @@ package clover import ( - "encoding/json" "strings" + + "github.com/ostafen/clover/encoding" ) // Document represents a document as a map. @@ -27,7 +28,13 @@ func NewDocument() *Document { } // NewDocumentOf creates a new document and initializes it with the content of the provided map. -func NewDocumentOf(fields map[string]interface{}) *Document { +func NewDocumentOf(o interface{}) *Document { + normalized, _ := encoding.Normalize(o) + fields, _ := normalized.(map[string]interface{}) + if fields == nil { + return nil + } + return &Document{ fields: fields, } @@ -82,7 +89,7 @@ func (doc *Document) Get(name string) interface{} { // Set maps a field to a value. Nested fields can be accessed using dot. func (doc *Document) Set(name string, value interface{}) { - normalizedValue, err := normalize(value) + normalizedValue, err := encoding.Normalize(value) if err == nil { m, _, fieldName := lookupField(name, doc.fields, true) m[fieldName] = normalizedValue @@ -98,11 +105,7 @@ func (doc *Document) SetAll(values map[string]interface{}) { // Unmarshal stores the document in the value pointed by v. func (doc *Document) Unmarshal(v interface{}) error { - bytes, err := json.Marshal(doc.fields) - if err != nil { - return err - } - return json.Unmarshal(bytes, v) + return encoding.Convert(doc.fields, v) } func compareDocuments(first *Document, second *Document, sortOpts []SortOption) int { diff --git a/encoding/encoding.go b/encoding/encoding.go new file mode 100644 index 0000000..425f96f --- /dev/null +++ b/encoding/encoding.go @@ -0,0 +1,243 @@ +package encoding + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + "reflect" + "strings" + "time" +) + +func init() { + gob.Register(map[string]interface{}{}) + gob.Register([]interface{}{}) + gob.Register(time.Time{}) +} + +func processTag(tagStr string) (string, bool) { + tags := strings.Split(tagStr, ",") + if len(tags) == 0 { + return "", false + } + name := tags[0] + omitempty := len(tags) > 1 && tags[1] == "omitempty" + return name, omitempty +} + +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Pointer: + return v.IsNil() + } + return false +} + +func convertStruct(structValue reflect.Value) (map[string]interface{}, error) { + m := make(map[string]interface{}) + for i := 0; i < structValue.NumField(); i++ { + fieldType := structValue.Type().Field(i) + fieldValue := structValue.Field(i) + + if fieldValue.CanInterface() { + fieldName := fieldType.Name + + cloverTag := fieldType.Tag.Get("clover") + name, omitempty := processTag(cloverTag) + if name != "" { + fieldName = name + } + + if !omitempty || !isEmptyValue(fieldValue) { + normalized, err := Normalize(structValue.Field(i).Interface()) + if err != nil { + return nil, err + } + m[fieldName] = normalized + } + } + } + + return m, nil +} + +func convertSlice(sliceValue reflect.Value) ([]interface{}, error) { + s := make([]interface{}, 0) + for i := 0; i < sliceValue.Len(); i++ { + v, err := Normalize(sliceValue.Index(i).Interface()) + if err != nil { + return nil, err + } + s = append(s, v) + } + return s, nil +} + +func getElemValueAndType(v interface{}) (reflect.Value, reflect.Type) { + rv := reflect.ValueOf(v) + rt := reflect.TypeOf(v) + + for rt.Kind() == reflect.Ptr && !rv.IsNil() { + rt = rt.Elem() + rv = rv.Elem() + } + return rv, rt +} + +func convertMap(mapValue reflect.Value) (map[string]interface{}, error) { + if mapValue.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("map key type must be a string") + } + + m := make(map[string]interface{}) + for _, key := range mapValue.MapKeys() { + value := mapValue.MapIndex(key) + + normalized, err := Normalize(value.Interface()) + if err != nil { + return nil, err + } + m[key.String()] = normalized + } + return m, nil +} + +func Normalize(value interface{}) (interface{}, error) { + if value == nil { + return nil, nil + } + + rValue, rType := getElemValueAndType(value) + if rType.Kind() == reflect.Ptr { + return nil, nil + } + + if _, isTime := rValue.Interface().(time.Time); isTime { + return rValue.Interface(), nil + } + + switch rType.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return rValue.Uint(), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return rValue.Int(), nil + case reflect.Float32, reflect.Float64: + return rValue.Float(), nil + case reflect.Struct: + return convertStruct(rValue) + case reflect.Map: + return convertMap(rValue) + case reflect.String: + return rValue.String(), nil + case reflect.Bool: + return rValue.Bool(), nil + case reflect.Slice: + return convertSlice(rValue) + } + return nil, fmt.Errorf("invalid dtype %s", rType.Name()) +} + +func buildRenameMap(rv reflect.Value) map[string]string { + renameMap := make(map[string]string) + for i := 0; i < rv.NumField(); i++ { + fieldType := rv.Type().Field(i) + + tagStr, found := fieldType.Tag.Lookup("clover") + if found { + name, _ := processTag(tagStr) + renameMap[name] = fieldType.Name + } + } + return renameMap +} + +func rename(fields map[string]interface{}, v interface{}) map[string]interface{} { + rv := reflect.ValueOf(v) + if rv.Type().Kind() != reflect.Struct { + return nil + } + + renameMap := buildRenameMap(rv) + m := make(map[string]interface{}) + for key, value := range fields { + renamedFieldName := renameMap[key] + if renamedFieldName != "" { + m[renamedFieldName] = value + delete(m, key) + } else { + m[key] = value + } + } + return m +} + +func getElemType(rt reflect.Type) reflect.Type { + for rt.Kind() == reflect.Pointer { + rt = rt.Elem() + } + return rt +} + +func getFieldName(m map[string]interface{}, sf reflect.StructField) string { + name := sf.Name + + tagStr, found := sf.Tag.Lookup("clover") + if found { + name, _ = processTag(tagStr) + } + + return name +} + +func renameMap(m map[string]interface{}, v interface{}) map[string]interface{} { + rv, rt := getElemValueAndType(v) + if rt.Kind() != reflect.Struct { + return m + } + + renamed := rename(m, rv.Interface()) + for i := 0; i < rv.NumField(); i++ { + sf := rv.Type().Field(i) + fv := renamed[sf.Name] + ft := getElemType(sf.Type) + + fMap, isMap := fv.(map[string]interface{}) + if isMap && ft.Kind() == reflect.Struct { + converted := renameMap(fMap, rv.Field(i).Interface()) + renamed[sf.Name] = converted + } + } + return renamed +} + +func Encode(v interface{}) ([]byte, error) { + var buf bytes.Buffer + err := gob.NewEncoder(&buf).Encode(v) + return buf.Bytes(), err +} + +func Decode(data []byte, v interface{}) error { + buf := bytes.NewBuffer(data) + return gob.NewDecoder(buf).Decode(v) +} + +func Convert(m map[string]interface{}, v interface{}) error { + renamed := renameMap(m, v) + + b, err := json.Marshal(renamed) + if err != nil { + return err + } + return json.Unmarshal(b, v) +} diff --git a/storage.go b/storage.go index e528e1d..661b117 100644 --- a/storage.go +++ b/storage.go @@ -1,8 +1,6 @@ package clover import ( - "bytes" - "encoding/gob" "errors" "log" "sort" @@ -12,6 +10,7 @@ import ( "time" badger "github.com/dgraph-io/badger/v3" + "github.com/ostafen/clover/encoding" ) var ErrDocumentNotExist = errors.New("no such document") @@ -88,19 +87,9 @@ func (s *storageImpl) stopGC() { close(s.chQuit) } -func registerGobTypes() { - gob.Register(map[string]interface{}{}) - gob.Register([]interface{}{}) - gob.Register(time.Time{}) -} - func (s *storageImpl) Open(path string) error { db, err := badger.Open(badger.DefaultOptions(path).WithLoggingLevel(badger.ERROR)) s.db = db - if err == nil { - registerGobTypes() - s.startGC() - } return err } @@ -156,14 +145,12 @@ func (s *storageImpl) FindAll(q *Query) ([]*Document, error) { func decodeDoc(data []byte) (*Document, error) { doc := NewDocument() - err := gob.NewDecoder(bytes.NewBuffer(data)).Decode(&doc.fields) + err := encoding.Decode(data, &doc.fields) return doc, err } func encodeDoc(doc *Document) ([]byte, error) { - var buf bytes.Buffer - err := gob.NewEncoder(&buf).Encode(doc.fields) - return buf.Bytes(), err + return encoding.Encode(doc.fields) } func (s *storageImpl) FindById(collectionName string, id string) (*Document, error) { diff --git a/util.go b/util.go index b32ed7b..2fdd9ce 100644 --- a/util.go +++ b/util.go @@ -1,10 +1,7 @@ package clover import ( - "fmt" "os" - "reflect" - "time" ) const defaultPermDir = 0777 @@ -36,102 +33,6 @@ func boolToInt(v bool) int { return 0 } -func convertMap(mapValue reflect.Value) (map[string]interface{}, error) { - // check if type is map (this is intended to be used directly) - - if mapValue.Type().Key().Kind() != reflect.String { - return nil, fmt.Errorf("map key type must be a string") - } - - m := make(map[string]interface{}) - for _, key := range mapValue.MapKeys() { - value := mapValue.MapIndex(key) - - normalized, err := normalize(value.Interface()) - if err != nil { - return nil, err - } - m[key.String()] = normalized - } - return m, nil -} - -func convertStruct(structValue reflect.Value) (map[string]interface{}, error) { - m := make(map[string]interface{}) - for i := 0; i < structValue.NumField(); i++ { - fieldName := structValue.Type().Field(i).Name - fieldValue := structValue.Field(i) - - if fieldValue.CanInterface() { - normalized, err := normalize(structValue.Field(i).Interface()) - if err != nil { - return nil, err - } - m[fieldName] = normalized - } - } - return m, nil -} - -func convertSlice(sliceValue reflect.Value) ([]interface{}, error) { - s := make([]interface{}, 0) - for i := 0; i < sliceValue.Len(); i++ { - v, err := normalize(sliceValue.Index(i).Interface()) - if err != nil { - return nil, err - } - s = append(s, v) - } - return s, nil -} - -func getElemValueAndType(v interface{}) (reflect.Value, reflect.Type) { - rv := reflect.ValueOf(v) - rt := reflect.TypeOf(v) - - for rt.Kind() == reflect.Ptr && !rv.IsNil() { - rt = rt.Elem() - rv = rv.Elem() - } - return rv, rt -} - -func normalize(value interface{}) (interface{}, error) { - if value == nil { - return nil, nil - } - - rValue, rType := getElemValueAndType(value) - if rType.Kind() == reflect.Ptr { - return nil, nil - } - - if _, isTime := value.(time.Time); isTime { - return value, nil - } - - switch rType.Kind() { - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return rValue.Convert(reflect.TypeOf(uint64(0))).Interface(), nil - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return rValue.Convert(reflect.TypeOf(int64(0))).Interface(), nil - case reflect.Float32, reflect.Float64: - return rValue.Convert(reflect.TypeOf(float64(0))).Interface(), nil - case reflect.Struct: - return convertStruct(rValue) - case reflect.Map: - return convertMap(rValue) - case reflect.String: - return rValue.String(), nil - case reflect.Bool: - return rValue.Bool(), nil - case reflect.Slice: - return convertSlice(rValue) - } - - return nil, fmt.Errorf("invalid dtype %s", rType.Name()) -} - func isNumber(v interface{}) bool { switch v.(type) { case int, uint, uint8, uint16, uint32, uint64, From 2248085ec7a34c5980038904e5bc36d1e1b7ac50 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 10:13:38 +0200 Subject: [PATCH 18/45] Change assert in TestImportAndExportCollection --- db_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/db_test.go b/db_test.go index cbf9df9..f9c0bbd 100644 --- a/db_test.go +++ b/db_test.go @@ -1126,7 +1126,13 @@ func TestExportAndImportCollection(t *testing.T) { require.Equal(t, len(docs), len(importDocs)) for i := 0; i < len(docs); i++ { - require.Equal(t, docs[i], importDocs[i]) + todo1 := &TodoModel{} + todo2 := &TodoModel{} + + require.NoError(t, docs[i].Unmarshal(todo1)) + require.NoError(t, importDocs[i].Unmarshal(todo2)) + + require.Equal(t, todo1, todo2) } }) } From 10711107c9b78de00bc581c11738220cc264e399 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 10:23:23 +0200 Subject: [PATCH 19/45] Fix error in compareNumbers --- compare.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compare.go b/compare.go index 847d2fd..87ef8f5 100644 --- a/compare.go +++ b/compare.go @@ -65,7 +65,7 @@ func compareNumbers(v1 interface{}, v2 interface{}) int { } _, isV1Int64 := v1.(int64) - _, isV2Int64 := v1.(int64) + _, isV2Int64 := v2.(int64) if isV1Int64 || isV2Int64 { v1Int64 := toInt64(v1) From b94aaf299522c9268b2fa2ab0fb1b6125d30e5cc Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 12:14:22 +0200 Subject: [PATCH 20/45] Add TestCompareUint64 --- db_test.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/db_test.go b/db_test.go index f9c0bbd..d923c11 100644 --- a/db_test.go +++ b/db_test.go @@ -25,7 +25,7 @@ const ( type TodoModel struct { Title string `json:"title" clover:"title"` Completed bool `json:"completed,omitempty" clover:"completed"` - Id int `json:"id" clover:"id"` + Id uint `json:"id" clover:"id"` UserId int `json:"userId" clover:"userId"` CompletedDate *time.Time `json:"completed_date,omitempty" clover:"completed_date,omitempty"` Notes *string `json:"notes,omitempty" clover:"notes,omitempty"` @@ -595,6 +595,17 @@ func TestEqCriteriaWithDifferentTypes(t *testing.T) { }) } +func TestCompareUint64(t *testing.T) { + runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { + docs, err := db.Query("todos").Where(c.Field("id").Gt(uint64(4))).FindAll() + require.NoError(t, err) + + for _, doc := range docs { + require.Greater(t, doc.Get("id"), uint64(4)) + } + }) +} + func TestNeqCriteria(t *testing.T) { runCloverTest(t, todosPath, &TodoModel{}, func(t *testing.T, db *c.DB) { docs, err := db.Query("todos").Where(c.Field("userId").Neq(7)).FindAll() From d0000aabf210120ca84fd59382c0fcd2f671f883 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 12:54:27 +0200 Subject: [PATCH 21/45] Fix a bug related with side effect when updating document after querying --- mem.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mem.go b/mem.go index 9694443..f6445b1 100644 --- a/mem.go +++ b/mem.go @@ -127,7 +127,7 @@ func (e *memEngine) Insert(collection string, docs ...*Document) error { } for _, d := range docs { - c[d.ObjectId()] = d + c[d.ObjectId()] = d.Copy() } return nil @@ -144,7 +144,7 @@ func (e *memEngine) iterateDocs(q *Query, consumer docConsumer) error { for _, d := range c { if q.satisfy(d) { - allDocs = append(allDocs, d) + allDocs = append(allDocs, d.Copy()) } } From 300c2ba63b83f7ce1904ddbdfced90c43fe02620 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 12:57:42 +0200 Subject: [PATCH 22/45] Fix wrong slice compare --- compare.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/compare.go b/compare.go index 87ef8f5..54fa615 100644 --- a/compare.go +++ b/compare.go @@ -40,18 +40,12 @@ func compareTypes(v1 interface{}, v2 interface{}) int { } func compareSlices(s1 []interface{}, s2 []interface{}) int { - if len(s1) < len(s2) { - return -1 - } else if len(s1) > len(s2) { - return 1 - } - - for i := 0; i < len(s1); i++ { + for i := 0; i < len(s1) && i < len(s2); i++ { if res := compareValues(s1[i], s2[i]); res != 0 { return res } } - return 0 + return len(s1) - len(s2) } func compareNumbers(v1 interface{}, v2 interface{}) int { From 554e9e4f99ae63db89a50e1609f551457de0a7eb Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 12:58:45 +0200 Subject: [PATCH 23/45] Make TestSliceCompare robust by checking if it really sort lexicographically --- db_test.go | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/db_test.go b/db_test.go index d923c11..a6c58a0 100644 --- a/db_test.go +++ b/db_test.go @@ -1149,20 +1149,43 @@ func TestExportAndImportCollection(t *testing.T) { } func TestSliceCompare(t *testing.T) { - runCloverTest(t, earthquakes, nil, func(t *testing.T, db *c.DB) { - coords := []interface{}{127.1311, 6.5061, 26.2} - - n, err := db.Query("earthquakes").Where(c.Field("geometry.coordinates").Eq(coords)).Count() + runCloverTest(t, todosPath, nil, func(t *testing.T, db *c.DB) { + allDocs, err := db.Query("todos").FindAll() require.NoError(t, err) - require.Equal(t, 1, n) - n, err = db.Query("earthquakes").Where(c.Field("geometry.coordinates").GtEq(coords)).Count() + require.NoError(t, db.CreateCollection("todos.copy")) + + for _, doc := range allDocs { + title, _ := doc.Get("title").(string) + if title != "" { + s := make([]int, len(title)) + for i := 0; i < len(title); i++ { + s[i] = int(byte(title[i])) + } + doc.Set("title", s) + } + } + err = db.Insert("todos.copy", allDocs...) require.NoError(t, err) - require.Equal(t, 1, n) - n, err = db.Query("earthquakes").Where(c.Field("geometry.coordinates").Lt(coords)).Count() + sort1, err := db.Query("todos").Sort(c.SortOption{Field: "title"}).FindAll() + + sort2, err := db.Query("todos.copy").Sort(c.SortOption{Field: "title"}).FindAll() require.NoError(t, err) - require.Equal(t, 7, n) + + require.Equal(t, len(sort1), len(sort2)) + + for i := 0; i < len(sort1); i++ { + doc1 := sort1[i] + doc2 := sort2[i] + + title := "" + sTitle := doc2.Get("title").([]interface{}) + for j := 0; j < len(sTitle); j++ { + title += string(byte(sTitle[j].(int64))) + } + require.Equal(t, title, doc1.Get("title")) + } }) } From 960b94b17ec732de8790479e49614e97286725b6 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 13:03:19 +0200 Subject: [PATCH 24/45] Fix compareObjects --- compare.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/compare.go b/compare.go index 54fa615..1799698 100644 --- a/compare.go +++ b/compare.go @@ -125,14 +125,6 @@ func getKeys(m map[string]interface{}) []string { } func compareObjects(m1 map[string]interface{}, m2 map[string]interface{}) int { - if len(m1) < len(m2) { - return -1 - } - - if len(m1) > len(m2) { - return 1 - } - m1Keys := getKeys(m1) m2Keys := getKeys(m2) @@ -151,5 +143,5 @@ func compareObjects(m1 map[string]interface{}, m2 map[string]interface{}) int { return res } } - return 0 + return len(m1) - len(m2) } From 8fb98b47c81e2009b725fe5aec00286c5057a6f9 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 13:11:43 +0200 Subject: [PATCH 25/45] Add additional test for object comparison --- db_test.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/db_test.go b/db_test.go index a6c58a0..d5a05d0 100644 --- a/db_test.go +++ b/db_test.go @@ -1189,7 +1189,7 @@ func TestSliceCompare(t *testing.T) { }) } -func TestObjectComparison(t *testing.T) { +func TestCompareObjects1(t *testing.T) { runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { err := db.CreateCollection("myCollection") require.NoError(t, err) @@ -1218,3 +1218,33 @@ func TestObjectComparison(t *testing.T) { require.Equal(t, doc, queryDoc) }) } + +func TestCompareObjects2(t *testing.T) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { + err := db.CreateCollection("myCollection") + require.NoError(t, err) + + doc1 := c.NewDocumentOf(map[string]interface{}{ + "data": map[string]interface{}{ + "SomeNumber": float64(0), + "SomeString": "aString", + }, + }) + + doc2 := c.NewDocumentOf(map[string]interface{}{ + "data": map[string]interface{}{ + "SomeNumber": float64(0), + "SomeStr": "aString", + }, + }) + + err = db.Insert("myCollection", doc1, doc2) + require.NoError(t, err) + + docs, err := db.Query("myCollection").Sort(c.SortOption{Field: "data"}).FindAll() + require.NoError(t, err) + require.Len(t, docs, 2) + + require.True(t, docs[0].Has("data.SomeStr")) + }) +} From 748571f74d3cfd12f84904a935ab34b5a8baa555 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 13:13:28 +0200 Subject: [PATCH 26/45] Add additional object comparison test --- db_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/db_test.go b/db_test.go index d5a05d0..9fec06d 100644 --- a/db_test.go +++ b/db_test.go @@ -1248,3 +1248,33 @@ func TestCompareObjects2(t *testing.T) { require.True(t, docs[0].Has("data.SomeStr")) }) } + +func TestCompareObjects3(t *testing.T) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { + err := db.CreateCollection("myCollection") + require.NoError(t, err) + + doc1 := c.NewDocumentOf(map[string]interface{}{ + "data": map[string]interface{}{ + "SomeNumber": float64(0), + "SomeString": "aString", + }, + }) + + doc2 := c.NewDocumentOf(map[string]interface{}{ + "data": map[string]interface{}{ + "SomeNumber": float64(0), + "SomeString": "aStr", + }, + }) + + err = db.Insert("myCollection", doc1, doc2) + require.NoError(t, err) + + docs, err := db.Query("myCollection").Sort(c.SortOption{Field: "data"}).FindAll() + require.NoError(t, err) + require.Len(t, docs, 2) + + require.Equal(t, docs[0].Get("data.SomeString"), "aStr") + }) +} From 7c36bae174bc234ef469745665c9c9a11ade3f74 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 13:19:48 +0200 Subject: [PATCH 27/45] Update description of NewDocumentOf() --- document.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/document.go b/document.go index 45af5dc..dac5be3 100644 --- a/document.go +++ b/document.go @@ -27,7 +27,8 @@ func NewDocument() *Document { } } -// NewDocumentOf creates a new document and initializes it with the content of the provided map. +// NewDocumentOf creates a new document and initializes it with the content of the provided object. +// It returns nil if the object cannot be converted to a valid Document. func NewDocumentOf(o interface{}) *Document { normalized, _ := encoding.Normalize(o) fields, _ := normalized.(map[string]interface{}) From 5462f77c6ba37eff1e8148a04dc03c339dc4050b Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 13:21:04 +0200 Subject: [PATCH 28/45] Rename some functions --- encoding/encoding.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/encoding/encoding.go b/encoding/encoding.go index 425f96f..737f882 100644 --- a/encoding/encoding.go +++ b/encoding/encoding.go @@ -16,7 +16,7 @@ func init() { gob.Register(time.Time{}) } -func processTag(tagStr string) (string, bool) { +func processStructTag(tagStr string) (string, bool) { tags := strings.Split(tagStr, ",") if len(tags) == 0 { return "", false @@ -44,7 +44,7 @@ func isEmptyValue(v reflect.Value) bool { return false } -func convertStruct(structValue reflect.Value) (map[string]interface{}, error) { +func normalizeStruct(structValue reflect.Value) (map[string]interface{}, error) { m := make(map[string]interface{}) for i := 0; i < structValue.NumField(); i++ { fieldType := structValue.Type().Field(i) @@ -54,7 +54,7 @@ func convertStruct(structValue reflect.Value) (map[string]interface{}, error) { fieldName := fieldType.Name cloverTag := fieldType.Tag.Get("clover") - name, omitempty := processTag(cloverTag) + name, omitempty := processStructTag(cloverTag) if name != "" { fieldName = name } @@ -72,7 +72,7 @@ func convertStruct(structValue reflect.Value) (map[string]interface{}, error) { return m, nil } -func convertSlice(sliceValue reflect.Value) ([]interface{}, error) { +func normalizeSlice(sliceValue reflect.Value) ([]interface{}, error) { s := make([]interface{}, 0) for i := 0; i < sliceValue.Len(); i++ { v, err := Normalize(sliceValue.Index(i).Interface()) @@ -95,7 +95,7 @@ func getElemValueAndType(v interface{}) (reflect.Value, reflect.Type) { return rv, rt } -func convertMap(mapValue reflect.Value) (map[string]interface{}, error) { +func normalizeMap(mapValue reflect.Value) (map[string]interface{}, error) { if mapValue.Type().Key().Kind() != reflect.String { return nil, fmt.Errorf("map key type must be a string") } @@ -135,27 +135,27 @@ func Normalize(value interface{}) (interface{}, error) { case reflect.Float32, reflect.Float64: return rValue.Float(), nil case reflect.Struct: - return convertStruct(rValue) + return normalizeStruct(rValue) case reflect.Map: - return convertMap(rValue) + return normalizeMap(rValue) case reflect.String: return rValue.String(), nil case reflect.Bool: return rValue.Bool(), nil case reflect.Slice: - return convertSlice(rValue) + return normalizeSlice(rValue) } return nil, fmt.Errorf("invalid dtype %s", rType.Name()) } -func buildRenameMap(rv reflect.Value) map[string]string { +func createRenameMap(rv reflect.Value) map[string]string { renameMap := make(map[string]string) for i := 0; i < rv.NumField(); i++ { fieldType := rv.Type().Field(i) tagStr, found := fieldType.Tag.Lookup("clover") if found { - name, _ := processTag(tagStr) + name, _ := processStructTag(tagStr) renameMap[name] = fieldType.Name } } @@ -168,7 +168,7 @@ func rename(fields map[string]interface{}, v interface{}) map[string]interface{} return nil } - renameMap := buildRenameMap(rv) + renameMap := createRenameMap(rv) m := make(map[string]interface{}) for key, value := range fields { renamedFieldName := renameMap[key] @@ -194,13 +194,13 @@ func getFieldName(m map[string]interface{}, sf reflect.StructField) string { tagStr, found := sf.Tag.Lookup("clover") if found { - name, _ = processTag(tagStr) + name, _ = processStructTag(tagStr) } return name } -func renameMap(m map[string]interface{}, v interface{}) map[string]interface{} { +func renameMapKeys(m map[string]interface{}, v interface{}) map[string]interface{} { rv, rt := getElemValueAndType(v) if rt.Kind() != reflect.Struct { return m @@ -214,7 +214,7 @@ func renameMap(m map[string]interface{}, v interface{}) map[string]interface{} { fMap, isMap := fv.(map[string]interface{}) if isMap && ft.Kind() == reflect.Struct { - converted := renameMap(fMap, rv.Field(i).Interface()) + converted := renameMapKeys(fMap, rv.Field(i).Interface()) renamed[sf.Name] = converted } } @@ -233,7 +233,7 @@ func Decode(data []byte, v interface{}) error { } func Convert(m map[string]interface{}, v interface{}) error { - renamed := renameMap(m, v) + renamed := renameMapKeys(m, v) b, err := json.Marshal(renamed) if err != nil { From e324810c040dac9374dd20c6b5f6ed6ad1e63da7 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 14:39:44 +0200 Subject: [PATCH 29/45] Add "Data Types" section --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a738816..e60d514 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,9 @@ The `FindAll()` method is used to retrieve all documents satisfying a given quer docs, _ := db.Query("myCollection").FindAll() todo := &struct { - Completed bool `json:"completed"` - Title string `json:"title"` - UserId int `json:"userId"` + Completed bool `clover:"completed"` + Title string `clover:"title"` + UserId int `clover:"userId"` }{} for _, doc := range docs { @@ -188,6 +188,48 @@ db.Query("todos").UpdateById(docId, map[string]interface{}{"completed": true}) db.Query("todos").DeleteById(docId) ``` +## Data Types + +Internally, CloverDB supports the following primitive data types: **int64**, **uint64**, **float64** and **time.Time**. When possible, values having different types are silently converted to one of the internal types: signed integer values get converted to int64, while unsigned ones to uint64. Float32 values are extended to float64. + +For example, consider the following snippet, which sets an uint8 value on a given document field: + +```go +doc := c.NewDocument() +doc.Set("myField", uint8(10)) // "myField" is automatically promoted to uint64 + +fmt.Println(doc.Get("myField").(uint64)) +``` + +Pointer values are dereferenced until either **nil** or a **non-pointer** value is found: + +``` go +var x int = 10 +var ptr *int = &x +var ptr1 **int = &ptr + +doc.Set("ptr", ptr) +doc.Set("ptr1", ptr1) + +fmt.Println(doc.Get("ptr").(int64) == 10) +fmt.Println(doc.Get("ptr1").(int64) == 10) + +ptr = nil + +doc.Set("ptr1", ptr1) +// ptr1 is not nil, but it points to the nil "ptr" pointer, so the field is set to nil +fmt.Println(doc.Get("ptr1") == nil) +``` + +Invalid types leaves the document untouched: + +```go +doc := c.NewDocument() +doc.Set("myField", make(chan struct{})) + +log.Println(doc.Has("myField")) // will output false +``` + ## Contributing **CloverDB** is actively developed. Any contribution, in the form of a suggestion, bug report or pull request, is well accepted :blush: From e4c90ee0b98cde29b459488eb2138e7f6d9c1fc0 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sat, 7 May 2022 16:56:18 +0200 Subject: [PATCH 30/45] Use IsExported() to check if a struct field is exported --- encoding/encoding.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encoding/encoding.go b/encoding/encoding.go index 737f882..a0c083e 100644 --- a/encoding/encoding.go +++ b/encoding/encoding.go @@ -50,7 +50,7 @@ func normalizeStruct(structValue reflect.Value) (map[string]interface{}, error) fieldType := structValue.Type().Field(i) fieldValue := structValue.Field(i) - if fieldValue.CanInterface() { + if fieldType.IsExported() { fieldName := fieldType.Name cloverTag := fieldType.Tag.Get("clover") From 9dc0e6d01c622afd767311960579e32bf7fdebad Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 10:57:54 +0200 Subject: [PATCH 31/45] Do not convert []uint8 to []interface{} --- encoding/encoding.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/encoding/encoding.go b/encoding/encoding.go index a0c083e..c5fa664 100644 --- a/encoding/encoding.go +++ b/encoding/encoding.go @@ -72,7 +72,11 @@ func normalizeStruct(structValue reflect.Value) (map[string]interface{}, error) return m, nil } -func normalizeSlice(sliceValue reflect.Value) ([]interface{}, error) { +func normalizeSlice(sliceValue reflect.Value) (interface{}, error) { + if sliceValue.Type().Elem().Kind() == reflect.Uint8 { + return sliceValue.Interface(), nil + } + s := make([]interface{}, 0) for i := 0; i < sliceValue.Len(); i++ { v, err := Normalize(sliceValue.Index(i).Interface()) From a2fdab62f71e9912400c4ae87c043a026a440eb0 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 11:59:56 +0200 Subject: [PATCH 32/45] Add test for encoding module --- encoding/encoding_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 encoding/encoding_test.go diff --git a/encoding/encoding_test.go b/encoding/encoding_test.go new file mode 100644 index 0000000..8c5dc8c --- /dev/null +++ b/encoding/encoding_test.go @@ -0,0 +1,26 @@ +package encoding + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNormalize(t *testing.T) { + s := &struct { + IntField int + UintField uint + StringField string + }{} + + ns, err := Normalize(s) + require.NoError(t, err) + + require.IsType(t, ns, map[string]interface{}{}) + + m := ns.(map[string]interface{}) + + require.Equal(t, m["IntField"], int64(0)) + require.Equal(t, m["UintField"], uint64(0)) + require.Equal(t, m["StringField"], "") +} From 49c6a3560371cfec88f6d2b4bdfc2276cff3348a Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:11:58 +0200 Subject: [PATCH 33/45] More robust test --- encoding/encoding_test.go | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/encoding/encoding_test.go b/encoding/encoding_test.go index 8c5dc8c..22d6c69 100644 --- a/encoding/encoding_test.go +++ b/encoding/encoding_test.go @@ -2,25 +2,39 @@ package encoding import ( "testing" + "time" "github.com/stretchr/testify/require" ) +type TestStruct struct { + IntField int + UintField uint + StringField string + TimeField time.Time + SliceField []int + MapField map[string]interface{} +} + func TestNormalize(t *testing.T) { - s := &struct { - IntField int - UintField uint - StringField string - }{} + date := time.Date(2020, 01, 1, 0, 0, 0, 0, time.UTC) + + s := &TestStruct{ + TimeField: date, + SliceField: []int{1, 2, 3, 4}, + MapField: map[string]interface{}{ + "hello": "clover", + }, + } ns, err := Normalize(s) require.NoError(t, err) require.IsType(t, ns, map[string]interface{}{}) - m := ns.(map[string]interface{}) + s1 := &TestStruct{} + err = Convert(ns.(map[string]interface{}), s1) + require.NoError(t, err) - require.Equal(t, m["IntField"], int64(0)) - require.Equal(t, m["UintField"], uint64(0)) - require.Equal(t, m["StringField"], "") + require.Equal(t, s, s1) } From 5579ccb71c99015e7f91d6183930c8b16e734579 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:14:39 +0200 Subject: [PATCH 34/45] Add TestEncodeDecode --- encoding/encoding_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/encoding/encoding_test.go b/encoding/encoding_test.go index 22d6c69..d17fbdf 100644 --- a/encoding/encoding_test.go +++ b/encoding/encoding_test.go @@ -38,3 +38,23 @@ func TestNormalize(t *testing.T) { require.Equal(t, s, s1) } + +func TestEncodeDecode(t *testing.T) { + date := time.Date(2020, 01, 1, 0, 0, 0, 0, time.UTC) + + s := &TestStruct{ + TimeField: date, + SliceField: []int{1, 2, 3, 4}, + MapField: map[string]interface{}{ + "hello": "clover", + }, + } + + data, err := Encode(s) + require.NoError(t, err) + + s1 := &TestStruct{} + require.NoError(t, Decode(data, s1)) + + require.Equal(t, s, s1) +} From f8ecdc577e7ef783e540aea72e5e0ffd5d974446 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:16:30 +0200 Subject: [PATCH 35/45] Test struct tags --- encoding/encoding_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/encoding/encoding_test.go b/encoding/encoding_test.go index d17fbdf..ad76367 100644 --- a/encoding/encoding_test.go +++ b/encoding/encoding_test.go @@ -8,8 +8,8 @@ import ( ) type TestStruct struct { - IntField int - UintField uint + IntField int `clover:"int"` + UintField uint `clover:"uint,omitempty"` StringField string TimeField time.Time SliceField []int @@ -31,6 +31,8 @@ func TestNormalize(t *testing.T) { require.NoError(t, err) require.IsType(t, ns, map[string]interface{}{}) + m := ns.(map[string]interface{}) + require.Nil(t, m["uint"]) // testing omitempty s1 := &TestStruct{} err = Convert(ns.(map[string]interface{}), s1) From 1f176fe3fe4327c4b99e7a14ea7f41da62fb7c29 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:21:34 +0200 Subject: [PATCH 36/45] Add some fields to TestStruct. Add TestNormalize2 --- encoding/encoding_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/encoding/encoding_test.go b/encoding/encoding_test.go index ad76367..9c7f844 100644 --- a/encoding/encoding_test.go +++ b/encoding/encoding_test.go @@ -11,9 +11,12 @@ type TestStruct struct { IntField int `clover:"int"` UintField uint `clover:"uint,omitempty"` StringField string + FloatField float32 + BoolField bool TimeField time.Time SliceField []int MapField map[string]interface{} + Data []byte } func TestNormalize(t *testing.T) { @@ -21,7 +24,11 @@ func TestNormalize(t *testing.T) { s := &TestStruct{ TimeField: date, + IntField: 10, + FloatField: 0.1, + BoolField: true, SliceField: []int{1, 2, 3, 4}, + Data: []byte("hello, clover!"), MapField: map[string]interface{}{ "hello": "clover", }, @@ -34,6 +41,8 @@ func TestNormalize(t *testing.T) { m := ns.(map[string]interface{}) require.Nil(t, m["uint"]) // testing omitempty + require.IsType(t, m["Data"], []byte{}) + s1 := &TestStruct{} err = Convert(ns.(map[string]interface{}), s1) require.NoError(t, err) @@ -41,6 +50,15 @@ func TestNormalize(t *testing.T) { require.Equal(t, s, s1) } +func TestNormalize2(t *testing.T) { + norm, err := Normalize(nil) + require.NoError(t, err) + require.Nil(t, norm) + + _, err = Normalize(make(chan struct{})) + require.Error(t, err) +} + func TestEncodeDecode(t *testing.T) { date := time.Date(2020, 01, 1, 0, 0, 0, 0, time.UTC) From 5c93cf75dadc84d5aa85c0fd9d661f856c4f94db Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:23:18 +0200 Subject: [PATCH 37/45] Remove unused function --- encoding/encoding.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/encoding/encoding.go b/encoding/encoding.go index c5fa664..623de30 100644 --- a/encoding/encoding.go +++ b/encoding/encoding.go @@ -193,17 +193,6 @@ func getElemType(rt reflect.Type) reflect.Type { return rt } -func getFieldName(m map[string]interface{}, sf reflect.StructField) string { - name := sf.Name - - tagStr, found := sf.Tag.Lookup("clover") - if found { - name, _ = processStructTag(tagStr) - } - - return name -} - func renameMapKeys(m map[string]interface{}, v interface{}) map[string]interface{} { rv, rt := getElemValueAndType(v) if rt.Kind() != reflect.Struct { From 851945671d010fc4e7431e9f40cff64a736678c2 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:33:46 +0200 Subject: [PATCH 38/45] Add TestNormalize3 --- encoding/encoding_test.go | 73 ++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/encoding/encoding_test.go b/encoding/encoding_test.go index 9c7f844..a04dc54 100644 --- a/encoding/encoding_test.go +++ b/encoding/encoding_test.go @@ -8,27 +8,32 @@ import ( ) type TestStruct struct { - IntField int `clover:"int"` - UintField uint `clover:"uint,omitempty"` - StringField string - FloatField float32 - BoolField bool - TimeField time.Time - SliceField []int - MapField map[string]interface{} - Data []byte + IntField int `clover:"int,omitempty"` + UintField uint `clover:"uint,omitempty"` + StringField string `clover:",omitempty"` + FloatField float32 `clover:",omitempty"` + BoolField bool `clover:",omitempty"` + TimeField time.Time `clover:",omitempty"` + IntPtr *int `clover:",omitempty"` + SliceField []int `clover:",omitempty"` + MapField map[string]interface{} `clover:",omitempty"` + Data []byte `clover:",omitempty"` } func TestNormalize(t *testing.T) { date := time.Date(2020, 01, 1, 0, 0, 0, 0, time.UTC) + var x int = 100 + s := &TestStruct{ - TimeField: date, - IntField: 10, - FloatField: 0.1, - BoolField: true, - SliceField: []int{1, 2, 3, 4}, - Data: []byte("hello, clover!"), + TimeField: date, + IntField: 10, + FloatField: 0.1, + StringField: "aString", + BoolField: true, + IntPtr: &x, + SliceField: []int{1, 2, 3, 4}, + Data: []byte("hello, clover!"), MapField: map[string]interface{}{ "hello": "clover", }, @@ -38,11 +43,13 @@ func TestNormalize(t *testing.T) { require.NoError(t, err) require.IsType(t, ns, map[string]interface{}{}) - m := ns.(map[string]interface{}) - require.Nil(t, m["uint"]) // testing omitempty + m := ns.(map[string]interface{}) require.IsType(t, m["Data"], []byte{}) + require.Nil(t, m["uint"]) // testing omitempty + require.Equal(t, m["IntPtr"], int64(100)) + s1 := &TestStruct{} err = Convert(ns.(map[string]interface{}), s1) require.NoError(t, err) @@ -59,6 +66,38 @@ func TestNormalize2(t *testing.T) { require.Error(t, err) } +func TestNormalize3(t *testing.T) { + date := time.Date(2020, 01, 1, 0, 0, 0, 0, time.UTC) + + s := &TestStruct{ + TimeField: date, + UintField: 0, + IntField: 0, + FloatField: 0, + StringField: "", + BoolField: false, + IntPtr: nil, + SliceField: []int{}, + Data: nil, + MapField: map[string]interface{}{}, + } + + ns, err := Normalize(s) + require.NoError(t, err) + + require.IsType(t, ns, map[string]interface{}{}) + m := ns.(map[string]interface{}) + + require.Nil(t, m["int"]) + require.Nil(t, m["uint"]) + require.Nil(t, m["FloatField"]) + require.Nil(t, m["BoolField"]) + require.Nil(t, m["SliceField"]) + require.Nil(t, m["Data"]) + require.Nil(t, m["MapField"]) + require.Nil(t, m["IntPtr"]) +} + func TestEncodeDecode(t *testing.T) { date := time.Date(2020, 01, 1, 0, 0, 0, 0, time.UTC) From 03910c186a3768f208face5dc8bd7cf592dd7e18 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:38:43 +0200 Subject: [PATCH 39/45] Add some require checks --- encoding/encoding_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/encoding/encoding_test.go b/encoding/encoding_test.go index a04dc54..e519479 100644 --- a/encoding/encoding_test.go +++ b/encoding/encoding_test.go @@ -51,10 +51,13 @@ func TestNormalize(t *testing.T) { require.Equal(t, m["IntPtr"], int64(100)) s1 := &TestStruct{} - err = Convert(ns.(map[string]interface{}), s1) + err = Convert(m, s1) require.NoError(t, err) require.Equal(t, s, s1) + + err = Convert(m, 10) + require.Error(t, err) } func TestNormalize2(t *testing.T) { @@ -64,6 +67,9 @@ func TestNormalize2(t *testing.T) { _, err = Normalize(make(chan struct{})) require.Error(t, err) + + _, err = Normalize(make(map[int]interface{})) + require.Error(t, err) } func TestNormalize3(t *testing.T) { From 54cc7906734ce654da136a197901843e7db11990 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:42:02 +0200 Subject: [PATCH 40/45] Simplify processTags --- encoding/encoding.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/encoding/encoding.go b/encoding/encoding.go index 623de30..64f341d 100644 --- a/encoding/encoding.go +++ b/encoding/encoding.go @@ -18,10 +18,7 @@ func init() { func processStructTag(tagStr string) (string, bool) { tags := strings.Split(tagStr, ",") - if len(tags) == 0 { - return "", false - } - name := tags[0] + name := tags[0] // when tagStr is "", tags[0] will also be "" omitempty := len(tags) > 1 && tags[1] == "omitempty" return name, omitempty } From 6f9122a41dd382e15f8c1cbd50fa34460257a403 Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:50:25 +0200 Subject: [PATCH 41/45] Add primitive types to description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e60d514..9f7a56b 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ db.Query("todos").DeleteById(docId) ## Data Types -Internally, CloverDB supports the following primitive data types: **int64**, **uint64**, **float64** and **time.Time**. When possible, values having different types are silently converted to one of the internal types: signed integer values get converted to int64, while unsigned ones to uint64. Float32 values are extended to float64. +Internally, CloverDB supports the following primitive data types: **int64**, **uint64**, **float64**, **string**, **bool** and **time.Time**. When possible, values having different types are silently converted to one of the internal types: signed integer values get converted to int64, while unsigned ones to uint64. Float32 values are extended to float64. For example, consider the following snippet, which sets an uint8 value on a given document field: From 60b1eac8a8a768a0f08bef4905d2efeea71a818e Mon Sep 17 00:00:00 2001 From: Stefano Date: Sun, 8 May 2022 12:53:35 +0200 Subject: [PATCH 42/45] Integrate Contains() from main --- criteria.go | 3 ++- db_test.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/criteria.go b/criteria.go index 9888852..f1189cf 100644 --- a/criteria.go +++ b/criteria.go @@ -1,6 +1,7 @@ package clover import ( + "reflect" "regexp" "github.com/ostafen/clover/encoding" @@ -164,7 +165,7 @@ func (f *field) Contains(elems ...interface{}) *Criteria { for _, elem := range elems { found := false - normElem, err := normalize(elem) + normElem, err := encoding.Normalize(elem) if err == nil { for _, val := range slice { diff --git a/db_test.go b/db_test.go index d2842c9..5b7fad1 100644 --- a/db_test.go +++ b/db_test.go @@ -692,7 +692,7 @@ func TestInCriteria(t *testing.T) { } func TestContainsCriteria(t *testing.T) { - runCloverTest(t, "", func(t *testing.T, db *c.DB) { + runCloverTest(t, "", nil, func(t *testing.T, db *c.DB) { err := db.CreateCollection("myCollection") require.NoError(t, err) @@ -727,7 +727,7 @@ func TestContainsCriteria(t *testing.T) { found := false for _, elem := range myField { - if elem.(float64) == 4 { + if elem.(int64) == 4 { found = true break } From 3532516f57a7f3091235a20e84f2bc15ed7c182e Mon Sep 17 00:00:00 2001 From: Stefano Date: Mon, 9 May 2022 01:05:17 +0200 Subject: [PATCH 43/45] Use compareValues instead of reflect.DeepEqual() --- criteria.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/criteria.go b/criteria.go index f1189cf..30594cc 100644 --- a/criteria.go +++ b/criteria.go @@ -1,7 +1,6 @@ package clover import ( - "reflect" "regexp" "github.com/ostafen/clover/encoding" @@ -169,7 +168,7 @@ func (f *field) Contains(elems ...interface{}) *Criteria { if err == nil { for _, val := range slice { - if reflect.DeepEqual(normElem, val) { + if compareValues(normElem, val) == 0 { found = true break } From 1ae32d7359cdf3789ee125250636cfc2b13d3b5d Mon Sep 17 00:00:00 2001 From: Stefano Date: Mon, 9 May 2022 22:22:58 +0200 Subject: [PATCH 44/45] Issue #43: fix error inside InsertOne() --- db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db.go b/db.go index 756e5e0..b20bec4 100644 --- a/db.go +++ b/db.go @@ -76,7 +76,7 @@ func (db *DB) Save(collectionName string, doc *Document) error { // InsertOne inserts a single document to an existing collection. It returns the id of the inserted document. func (db *DB) InsertOne(collectionName string, doc *Document) (string, error) { err := db.Insert(collectionName, doc) - return doc.Get(objectIdField).(string), err + return doc.ObjectId(), err } // Open opens a new clover database on the supplied path. If such a folder doesn't exist, it is automatically created. From c271ca94b5e8755e478c2e7424bdeac4295ba8b2 Mon Sep 17 00:00:00 2001 From: segfault99 <30441063+segfault99@users.noreply.github.com> Date: Tue, 10 May 2022 21:22:47 +0300 Subject: [PATCH 45/45] Feature/document field comparison (#52) --- criteria.go | 70 ++++++++++++++++++++++++++++------------------------- db_test.go | 38 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/criteria.go b/criteria.go index 30594cc..0a248c5 100644 --- a/criteria.go +++ b/criteria.go @@ -2,6 +2,7 @@ package clover import ( "regexp" + "strings" "github.com/ostafen/clover/encoding" ) @@ -62,13 +63,12 @@ func (f *field) IsNilOrNotExists() *Criteria { } func (f *field) Eq(value interface{}) *Criteria { - normalizedValue, err := encoding.Normalize(value) - if err != nil { - return &falseCriteria - } - return &Criteria{ p: func(doc *Document) bool { + normalizedValue, err := encoding.Normalize(getFieldOrValue(doc, value)) + if err != nil { + return false + } if !doc.Has(f.name) { return false } @@ -78,52 +78,48 @@ func (f *field) Eq(value interface{}) *Criteria { } func (f *field) Gt(value interface{}) *Criteria { - normValue, err := encoding.Normalize(value) - if err != nil { - return &falseCriteria - } - return &Criteria{ p: func(doc *Document) bool { + normValue, err := encoding.Normalize(getFieldOrValue(doc, value)) + if err != nil { + return false + } return compareValues(doc.Get(f.name), normValue) > 0 }, } } func (f *field) GtEq(value interface{}) *Criteria { - normValue, err := encoding.Normalize(value) - if err != nil { - return &falseCriteria - } - return &Criteria{ p: func(doc *Document) bool { + normValue, err := encoding.Normalize(getFieldOrValue(doc, value)) + if err != nil { + return false + } return compareValues(doc.Get(f.name), normValue) >= 0 }, } } func (f *field) Lt(value interface{}) *Criteria { - normValue, err := encoding.Normalize(value) - if err != nil { - return &falseCriteria - } - return &Criteria{ p: func(doc *Document) bool { + normValue, err := encoding.Normalize(getFieldOrValue(doc, value)) + if err != nil { + return false + } return compareValues(doc.Get(f.name), normValue) < 0 }, } } func (f *field) LtEq(value interface{}) *Criteria { - normValue, err := encoding.Normalize(value) - if err != nil { - return &falseCriteria - } - return &Criteria{ p: func(doc *Document) bool { + normValue, err := encoding.Normalize(getFieldOrValue(doc, value)) + if err != nil { + return false + } return compareValues(doc.Get(f.name), normValue) <= 0 }, } @@ -134,16 +130,12 @@ func (f *field) Neq(value interface{}) *Criteria { } func (f *field) In(values ...interface{}) *Criteria { - normValues, err := encoding.Normalize(values) - if err != nil || normValues == nil { - return &falseCriteria - } - return &Criteria{ p: func(doc *Document) bool { docValue := doc.Get(f.name) - for _, value := range normValues.([]interface{}) { - if compareValues(value, docValue) == 0 { + for _, v := range values { + normValue, err := encoding.Normalize(getFieldOrValue(doc, v)) + if err == nil && compareValues(normValue, docValue) == 0 { return true } } @@ -164,7 +156,7 @@ func (f *field) Contains(elems ...interface{}) *Criteria { for _, elem := range elems { found := false - normElem, err := encoding.Normalize(elem) + normElem, err := encoding.Normalize(getFieldOrValue(doc, elem)) if err == nil { for _, val := range slice { @@ -241,3 +233,15 @@ func (c *Criteria) Not() *Criteria { p: negatePredicate(c.p), } } + +// getFieldOrValue returns dereferenced value if value denotes another document field, +// otherwise returns the value itself directly +func getFieldOrValue(doc *Document, value interface{}) interface{} { + if cmpField, ok := value.(*field); ok { + value = doc.Get(cmpField.name) + } else if fStr, ok := value.(string); ok && strings.HasPrefix(fStr, "$") { + fieldName := strings.TrimLeft(fStr, "$") + value = doc.Get(fieldName) + } + return value +} diff --git a/db_test.go b/db_test.go index 5b7fad1..c7f5e50 100644 --- a/db_test.go +++ b/db_test.go @@ -688,6 +688,17 @@ func TestInCriteria(t *testing.T) { require.Fail(t, "userId is not in the correct range") } } + + criteria := c.Field("userId").In(c.Field("id"), 6) + docs, err = db.Query("todos").Where(criteria).FindAll() + require.NoError(t, err) + + require.Greater(t, len(docs), 0) + for _, doc := range docs { + userId := doc.Get("userId").(int64) + id := doc.Get("id").(uint64) + require.True(t, uint64(userId) == id || userId == 6) + } }) } @@ -1327,3 +1338,30 @@ func TestCompareObjects3(t *testing.T) { require.Equal(t, docs[0].Get("data.SomeString"), "aStr") }) } + +func TestCompareDocumentFields(t *testing.T) { + runCloverTest(t, airlinesPath, nil, func(t *testing.T, db *c.DB) { + criteria := c.Field("Statistics.Flights.Diverted").Gt(c.Field("Statistics.Flights.Cancelled")) + docs, err := db.Query("airlines").Where(criteria).FindAll() + require.NoError(t, err) + + require.Greater(t, len(docs), 0) + for _, doc := range docs { + diverted := doc.Get("Statistics.Flights.Diverted").(float64) + cancelled := doc.Get("Statistics.Flights.Cancelled").(float64) + require.Greater(t, diverted, cancelled) + } + + //alternative syntax using $ + criteria = c.Field("Statistics.Flights.Diverted").Gt("$Statistics.Flights.Cancelled") + docs, err = db.Query("airlines").Where(criteria).FindAll() + require.NoError(t, err) + + require.Greater(t, len(docs), 0) + for _, doc := range docs { + diverted := doc.Get("Statistics.Flights.Diverted").(float64) + cancelled := doc.Get("Statistics.Flights.Cancelled").(float64) + require.Greater(t, diverted, cancelled) + } + }) +}