From a60dc5759854e259bef3d387bd4fdd8975fd5c8d Mon Sep 17 00:00:00 2001 From: tidwall Date: Fri, 15 Feb 2019 08:26:55 -0700 Subject: [PATCH] Isolated fields type --- internal/collection/collection.go | 86 +++++++-------- internal/collection/collection_test.go | 141 ++++++++++++++----------- internal/collection/fields.go | 40 +++++++ internal/collection/item/item.go | 63 ++++++++--- internal/collection/item/item_test.go | 71 +++++++++++-- internal/server/aofshrink.go | 12 ++- internal/server/crud.go | 14 ++- internal/server/fence.go | 5 +- internal/server/scan.go | 5 +- internal/server/scanner.go | 50 ++++----- internal/server/search.go | 67 +++++++----- internal/server/server.go | 26 ++--- 12 files changed, 375 insertions(+), 205 deletions(-) create mode 100644 internal/collection/fields.go diff --git a/internal/collection/collection.go b/internal/collection/collection.go index 5167e70f..0827dcef 100644 --- a/internal/collection/collection.go +++ b/internal/collection/collection.go @@ -119,39 +119,39 @@ func (c *Collection) delItem(item *item.Item) { func (c *Collection) Set( id string, obj geojson.Object, fields []string, values []float64, ) ( - oldObj geojson.Object, oldFields []float64, newFields []float64, + oldObj geojson.Object, oldFields *Fields, newFields *Fields, ) { // create the new item - item := item.New(id, obj) + newItem := item.New(id, obj) // add the new item to main btree and remove the old one if needed - oldItemV, ok := c.items.Set(item) + var oldItem *item.Item + oldItemV, ok := c.items.Set(newItem) if ok { - oldItem := oldItemV + oldItem = oldItemV oldObj = oldItem.Obj() // remove old item from indexes c.delItem(oldItem) - if len(oldItem.Fields()) > 0 { + if oldItem.HasFields() { // merge old and new fields - oldFields = oldItem.Fields() - item.CopyOverFields(oldFields) + newItem.CopyOverFields(oldItem) } } if fields == nil && len(values) > 0 { // directly set the field values, from copy - item.CopyOverFields(values) + newItem.CopyOverFields(values) } else if len(fields) > 0 { // add new field to new item - c.setFields(item, fields, values, false) + c.setFields(newItem, fields, values, false) } // add new item to indexes - c.addItem(item) + c.addItem(newItem) // fmt.Printf("!!! %#v\n", oldObj) - return oldObj, oldFields, item.Fields() + return oldObj, itemFields(oldItem), itemFields(newItem) } func (c *Collection) setFields( @@ -192,7 +192,7 @@ func (c *Collection) setField( // Delete removes an object and returns it. // If the object does not exist then the 'ok' return value will be false. func (c *Collection) Delete(id string) ( - obj geojson.Object, fields []float64, ok bool, + obj geojson.Object, fields *Fields, ok bool, ) { oldItemV, ok := c.items.Delete(id) if !ok { @@ -202,13 +202,13 @@ func (c *Collection) Delete(id string) ( c.delItem(oldItem) - return oldItem.Obj(), oldItem.Fields(), true + return oldItem.Obj(), itemFields(oldItem), true } // Get returns an object. // If the object does not exist then the 'ok' return value will be false. func (c *Collection) Get(id string) ( - obj geojson.Object, fields []float64, ok bool, + obj geojson.Object, fields *Fields, ok bool, ) { itemV, ok := c.items.Get(id) if !ok { @@ -216,13 +216,13 @@ func (c *Collection) Get(id string) ( } item := itemV - return item.Obj(), item.Fields(), true + return item.Obj(), itemFields(item), true } // SetField set a field value for an object and returns that object. // If the object does not exist then the 'ok' return value will be false. func (c *Collection) SetField(id, fieldName string, fieldValue float64) ( - obj geojson.Object, fields []float64, updated bool, ok bool, + obj geojson.Object, fields *Fields, updated bool, ok bool, ) { itemV, ok := c.items.Get(id) if !ok { @@ -230,13 +230,13 @@ func (c *Collection) SetField(id, fieldName string, fieldValue float64) ( } item := itemV updated = c.setField(item, fieldName, fieldValue, true) - return item.Obj(), item.Fields(), updated, true + return item.Obj(), itemFields(item), updated, true } // SetFields is similar to SetField, just setting multiple fields at once func (c *Collection) SetFields( id string, fieldNames []string, fieldValues []float64, -) (obj geojson.Object, fields []float64, updatedCount int, ok bool) { +) (obj geojson.Object, fields *Fields, updatedCount int, ok bool) { itemV, ok := c.items.Get(id) if !ok { return nil, nil, 0, false @@ -245,7 +245,7 @@ func (c *Collection) SetFields( updatedCount = c.setFields(item, fieldNames, fieldValues, true) - return item.Obj(), item.Fields(), updatedCount, true + return item.Obj(), itemFields(item), updatedCount, true } // FieldMap return a maps of the field names. @@ -264,7 +264,7 @@ func (c *Collection) FieldArr() []string { // Scan iterates though the collection ids. func (c *Collection) Scan(desc bool, cursor Cursor, - iterator func(id string, obj geojson.Object, fields []float64) bool, + iterator func(id string, obj geojson.Object, fields *Fields) bool, ) bool { var keepon = true var count uint64 @@ -281,7 +281,7 @@ func (c *Collection) Scan(desc bool, cursor Cursor, if cursor != nil { cursor.Step(1) } - keepon = iterator(item.ID(), item.Obj(), item.Fields()) + keepon = iterator(item.ID(), item.Obj(), itemFields(item)) return keepon } if desc { @@ -294,7 +294,7 @@ func (c *Collection) Scan(desc bool, cursor Cursor, // ScanRange iterates though the collection starting with specified id. func (c *Collection) ScanRange(start, end string, desc bool, cursor Cursor, - iterator func(id string, obj geojson.Object, fields []float64) bool, + iterator func(id string, obj geojson.Object, fields *Fields) bool, ) bool { var keepon = true var count uint64 @@ -320,7 +320,7 @@ func (c *Collection) ScanRange(start, end string, desc bool, cursor Cursor, return false } } - keepon = iterator(item.ID(), item.Obj(), item.Fields()) + keepon = iterator(item.ID(), item.Obj(), itemFields(item)) return keepon } @@ -334,7 +334,7 @@ func (c *Collection) ScanRange(start, end string, desc bool, cursor Cursor, // SearchValues iterates though the collection values. func (c *Collection) SearchValues(desc bool, cursor Cursor, - iterator func(id string, obj geojson.Object, fields []float64) bool, + iterator func(id string, obj geojson.Object, fields *Fields) bool, ) bool { var keepon = true var count uint64 @@ -352,7 +352,7 @@ func (c *Collection) SearchValues(desc bool, cursor Cursor, cursor.Step(1) } iitm := v.(*item.Item) - keepon = iterator(iitm.ID(), iitm.Obj(), iitm.Fields()) + keepon = iterator(iitm.ID(), iitm.Obj(), itemFields(iitm)) return keepon } if desc { @@ -366,7 +366,7 @@ func (c *Collection) SearchValues(desc bool, cursor Cursor, // SearchValuesRange iterates though the collection values. func (c *Collection) SearchValuesRange(start, end string, desc bool, cursor Cursor, - iterator func(id string, obj geojson.Object, fields []float64) bool, + iterator func(id string, obj geojson.Object, fields *Fields) bool, ) bool { var keepon = true var count uint64 @@ -384,7 +384,7 @@ func (c *Collection) SearchValuesRange(start, end string, desc bool, cursor.Step(1) } iitm := v.(*item.Item) - keepon = iterator(iitm.ID(), iitm.Obj(), iitm.Fields()) + keepon = iterator(iitm.ID(), iitm.Obj(), itemFields(iitm)) return keepon } if desc { @@ -402,7 +402,7 @@ func (c *Collection) SearchValuesRange(start, end string, desc bool, // ScanGreaterOrEqual iterates though the collection starting with specified id. func (c *Collection) ScanGreaterOrEqual(id string, desc bool, cursor Cursor, - iterator func(id string, obj geojson.Object, fields []float64) bool, + iterator func(id string, obj geojson.Object, fields *Fields) bool, ) bool { var keepon = true var count uint64 @@ -419,7 +419,7 @@ func (c *Collection) ScanGreaterOrEqual(id string, desc bool, if cursor != nil { cursor.Step(1) } - keepon = iterator(item.ID(), item.Obj(), item.Fields()) + keepon = iterator(item.ID(), item.Obj(), itemFields(item)) return keepon } if desc { @@ -432,7 +432,7 @@ func (c *Collection) ScanGreaterOrEqual(id string, desc bool, func (c *Collection) geoSearch( rect geometry.Rect, - iter func(id string, obj geojson.Object, fields []float64) bool, + iter func(id string, obj geojson.Object, fields *Fields) bool, ) bool { alive := true c.index.Search( @@ -440,7 +440,7 @@ func (c *Collection) geoSearch( []float64{rect.Max.X, rect.Max.Y}, func(_, _ []float64, itemv *item.Item) bool { item := itemv - alive = iter(item.ID(), item.Obj(), item.Fields()) + alive = iter(item.ID(), item.Obj(), itemFields(item)) return alive }, ) @@ -449,12 +449,12 @@ func (c *Collection) geoSearch( func (c *Collection) geoSparse( obj geojson.Object, sparse uint8, - iter func(id string, obj geojson.Object, fields []float64) (match, ok bool), + iter func(id string, obj geojson.Object, fields *Fields) (match, ok bool), ) bool { matches := make(map[string]bool) alive := true c.geoSparseInner(obj.Rect(), sparse, - func(id string, o geojson.Object, fields []float64) ( + func(id string, o geojson.Object, fields *Fields) ( match, ok bool, ) { ok = true @@ -471,7 +471,7 @@ func (c *Collection) geoSparse( } func (c *Collection) geoSparseInner( rect geometry.Rect, sparse uint8, - iter func(id string, obj geojson.Object, fields []float64) (match, ok bool), + iter func(id string, obj geojson.Object, fields *Fields) (match, ok bool), ) bool { if sparse > 0 { w := rect.Max.X - rect.Min.X @@ -503,7 +503,7 @@ func (c *Collection) geoSparseInner( } alive := true c.geoSearch(rect, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { match, ok := iter(id, obj, fields) if !ok { alive = false @@ -521,7 +521,7 @@ func (c *Collection) Within( obj geojson.Object, sparse uint8, cursor Cursor, - iter func(id string, obj geojson.Object, fields []float64) bool, + iter func(id string, obj geojson.Object, fields *Fields) bool, ) bool { var count uint64 var offset uint64 @@ -531,7 +531,7 @@ func (c *Collection) Within( } if sparse > 0 { return c.geoSparse(obj, sparse, - func(id string, o geojson.Object, fields []float64) ( + func(id string, o geojson.Object, fields *Fields) ( match, ok bool, ) { count++ @@ -549,7 +549,7 @@ func (c *Collection) Within( ) } return c.geoSearch(obj.Rect(), - func(id string, o geojson.Object, fields []float64) bool { + func(id string, o geojson.Object, fields *Fields) bool { count++ if count <= offset { return true @@ -571,7 +571,7 @@ func (c *Collection) Intersects( obj geojson.Object, sparse uint8, cursor Cursor, - iter func(id string, obj geojson.Object, fields []float64) bool, + iter func(id string, obj geojson.Object, fields *Fields) bool, ) bool { var count uint64 var offset uint64 @@ -581,7 +581,7 @@ func (c *Collection) Intersects( } if sparse > 0 { return c.geoSparse(obj, sparse, - func(id string, o geojson.Object, fields []float64) ( + func(id string, o geojson.Object, fields *Fields) ( match, ok bool, ) { count++ @@ -599,7 +599,7 @@ func (c *Collection) Intersects( ) } return c.geoSearch(obj.Rect(), - func(id string, o geojson.Object, fields []float64) bool { + func(id string, o geojson.Object, fields *Fields) bool { count++ if count <= offset { return true @@ -619,7 +619,7 @@ func (c *Collection) Intersects( func (c *Collection) Nearby( target geojson.Object, cursor Cursor, - iter func(id string, obj geojson.Object, fields []float64) bool, + iter func(id string, obj geojson.Object, fields *Fields) bool, ) bool { // First look to see if there's at least one candidate in the circle's // outer rectangle. This is a fast-fail operation. @@ -665,7 +665,7 @@ func (c *Collection) Nearby( cursor.Step(1) } item := itemv - alive = iter(item.ID(), item.Obj(), item.Fields()) + alive = iter(item.ID(), item.Obj(), itemFields(item)) return alive }, ) diff --git a/internal/collection/collection_test.go b/internal/collection/collection_test.go index 6627bde1..54539df0 100644 --- a/internal/collection/collection_test.go +++ b/internal/collection/collection_test.go @@ -104,7 +104,7 @@ func TestCollectionNewCollection(t *testing.T) { Min: geometry.Point{X: -180, Y: -90}, Max: geometry.Point{X: 180, Y: 90}, } - c.geoSearch(bbox, func(id string, obj geojson.Object, field []float64) bool { + c.geoSearch(bbox, func(id string, obj geojson.Object, field *Fields) bool { count++ return true }) @@ -124,8 +124,8 @@ func TestCollectionSet(t *testing.T) { str1 := String("hello") oldObject, oldFields, newFields := c.Set("str", str1, nil, nil) expect(t, oldObject == nil) - expect(t, len(oldFields) == 0) - expect(t, len(newFields) == 0) + expect(t, fieldLen(oldFields) == 0) + expect(t, fieldLen(newFields) == 0) }) t.Run("UpdateString", func(t *testing.T) { c := New() @@ -133,20 +133,20 @@ func TestCollectionSet(t *testing.T) { str2 := String("world") oldObject, oldFields, newFields := c.Set("str", str1, nil, nil) expect(t, oldObject == nil) - expect(t, len(oldFields) == 0) - expect(t, len(newFields) == 0) + expect(t, fieldLen(oldFields) == 0) + expect(t, fieldLen(newFields) == 0) oldObject, oldFields, newFields = c.Set("str", str2, nil, nil) expect(t, oldObject == str1) - expect(t, len(oldFields) == 0) - expect(t, len(newFields) == 0) + expect(t, fieldLen(oldFields) == 0) + expect(t, fieldLen(newFields) == 0) }) t.Run("AddPoint", func(t *testing.T) { c := New() point1 := PO(-112.1, 33.1) oldObject, oldFields, newFields := c.Set("point", point1, nil, nil) expect(t, oldObject == nil) - expect(t, len(oldFields) == 0) - expect(t, len(newFields) == 0) + expect(t, fieldLen(oldFields) == 0) + expect(t, fieldLen(newFields) == 0) }) t.Run("UpdatePoint", func(t *testing.T) { c := New() @@ -154,12 +154,12 @@ func TestCollectionSet(t *testing.T) { point2 := PO(-112.2, 33.2) oldObject, oldFields, newFields := c.Set("point", point1, nil, nil) expect(t, oldObject == nil) - expect(t, len(oldFields) == 0) - expect(t, len(newFields) == 0) + expect(t, fieldLen(oldFields) == 0) + expect(t, fieldLen(newFields) == 0) oldObject, oldFields, newFields = c.Set("point", point2, nil, nil) expect(t, oldObject == point1) - expect(t, len(oldFields) == 0) - expect(t, len(newFields) == 0) + expect(t, fieldLen(oldFields) == 0) + expect(t, fieldLen(newFields) == 0) }) t.Run("Fields", func(t *testing.T) { c := New() @@ -170,23 +170,23 @@ func TestCollectionSet(t *testing.T) { fValues := []float64{1, 2, 3} oldObj, oldFlds, newFlds := c.Set("str", str1, fNames, fValues) expect(t, oldObj == nil) - expect(t, len(oldFlds) == 0) - expect(t, reflect.DeepEqual(newFlds, fValues)) + expect(t, fieldLen(oldFlds) == 0) + expect(t, fieldIterEquals(newFlds, fValues)) } { fNames := []string{"d", "e", "f"} fValues := []float64{4, 5, 6} oldObj, oldFlds, newFlds := c.Set("str", str2, fNames, fValues) expect(t, oldObj == str1) - expect(t, reflect.DeepEqual(oldFlds, []float64{1, 2, 3})) - expect(t, reflect.DeepEqual(newFlds, []float64{1, 2, 3, 4, 5, 6})) + expect(t, fieldIterEquals(oldFlds, []float64{1, 2, 3})) + expect(t, fieldIterEquals(newFlds, []float64{1, 2, 3, 4, 5, 6})) } { fValues := []float64{7, 8, 9, 10, 11, 12} oldObj, oldFlds, newFlds := c.Set("str", str1, nil, fValues) expect(t, oldObj == str2) - expect(t, reflect.DeepEqual(oldFlds, []float64{1, 2, 3, 4, 5, 6})) - expect(t, reflect.DeepEqual(newFlds, []float64{7, 8, 9, 10, 11, 12})) + expect(t, fieldIterEquals(oldFlds, []float64{1, 2, 3, 4, 5, 6})) + expect(t, fieldIterEquals(newFlds, []float64{7, 8, 9, 10, 11, 12})) } }) t.Run("Delete", func(t *testing.T) { @@ -204,7 +204,7 @@ func TestCollectionSet(t *testing.T) { Max: geometry.Point{X: 1, Y: 2}}) var v geojson.Object var ok bool - var flds []float64 + var flds *Fields var updated bool var updateCount int @@ -226,24 +226,24 @@ func TestCollectionSet(t *testing.T) { v, flds, updated, ok = c.SetField("3", "hello", 123) expect(t, ok) - expect(t, reflect.DeepEqual(flds, []float64{123})) + expect(t, fieldIterEquals(flds, []float64{123})) expect(t, updated) expect(t, c.FieldMap()["hello"] == 0) v, flds, updated, ok = c.SetField("3", "hello", 1234) expect(t, ok) - expect(t, reflect.DeepEqual(flds, []float64{1234})) + expect(t, fieldIterEquals(flds, []float64{1234})) expect(t, updated) v, flds, updated, ok = c.SetField("3", "hello", 1234) expect(t, ok) - expect(t, reflect.DeepEqual(flds, []float64{1234})) + expect(t, fieldIterEquals(flds, []float64{1234})) expect(t, !updated) v, flds, updateCount, ok = c.SetFields("3", []string{"planet", "world"}, []float64{55, 66}) expect(t, ok) - expect(t, reflect.DeepEqual(flds, []float64{1234, 55, 66})) + expect(t, fieldIterEquals(flds, []float64{1234, 55, 66})) expect(t, updateCount == 2) expect(t, c.FieldMap()["hello"] == 0) expect(t, c.FieldMap()["planet"] == 1) @@ -277,6 +277,29 @@ func TestCollectionSet(t *testing.T) { }) } +func fieldLen(fields *Fields) int { + var idx int + fields.ForEach(-1, func(value float64) bool { + idx++ + return true + }) + return idx +} + +func fieldIterEquals(fields *Fields, values []float64) bool { + ok := true + var idx int + fields.ForEach(len(values), func(value float64) bool { + if value != values[idx] { + ok = false + return false + } + idx++ + return true + }) + return ok +} + func TestCollectionScan(t *testing.T) { N := 256 c := New() @@ -286,22 +309,22 @@ func TestCollectionScan(t *testing.T) { } var n int var prevID string - c.Scan(false, nil, func(id string, obj geojson.Object, fields []float64) bool { + c.Scan(false, nil, func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, id > prevID) } - expect(t, id == fmt.Sprintf("%04d", int(fields[0]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(0)))) n++ prevID = id return true }) expect(t, n == c.Count()) n = 0 - c.Scan(true, nil, func(id string, obj geojson.Object, fields []float64) bool { + c.Scan(true, nil, func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, id < prevID) } - expect(t, id == fmt.Sprintf("%04d", int(fields[0]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(0)))) n++ prevID = id return true @@ -310,11 +333,11 @@ func TestCollectionScan(t *testing.T) { n = 0 c.ScanRange("0060", "0070", false, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, id > prevID) } - expect(t, id == fmt.Sprintf("%04d", int(fields[0]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(0)))) n++ prevID = id return true @@ -323,11 +346,11 @@ func TestCollectionScan(t *testing.T) { n = 0 c.ScanRange("0070", "0060", true, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, id < prevID) } - expect(t, id == fmt.Sprintf("%04d", int(fields[0]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(0)))) n++ prevID = id return true @@ -336,11 +359,11 @@ func TestCollectionScan(t *testing.T) { n = 0 c.ScanGreaterOrEqual("0070", true, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, id < prevID) } - expect(t, id == fmt.Sprintf("%04d", int(fields[0]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(0)))) n++ prevID = id return true @@ -349,11 +372,11 @@ func TestCollectionScan(t *testing.T) { n = 0 c.ScanGreaterOrEqual("0070", false, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, id > prevID) } - expect(t, id == fmt.Sprintf("%04d", int(fields[0]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(0)))) n++ prevID = id return true @@ -373,22 +396,22 @@ func TestCollectionSearch(t *testing.T) { } var n int var prevValue string - c.SearchValues(false, nil, func(id string, obj geojson.Object, fields []float64) bool { + c.SearchValues(false, nil, func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, obj.String() > prevValue) } - expect(t, id == fmt.Sprintf("%04d", int(fields[1]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(1)))) n++ prevValue = obj.String() return true }) expect(t, n == c.Count()) n = 0 - c.SearchValues(true, nil, func(id string, obj geojson.Object, fields []float64) bool { + c.SearchValues(true, nil, func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, obj.String() < prevValue) } - expect(t, id == fmt.Sprintf("%04d", int(fields[1]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(1)))) n++ prevValue = obj.String() return true @@ -397,11 +420,11 @@ func TestCollectionSearch(t *testing.T) { n = 0 c.SearchValuesRange("0060", "0070", false, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, obj.String() > prevValue) } - expect(t, id == fmt.Sprintf("%04d", int(fields[1]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(1)))) n++ prevValue = obj.String() return true @@ -410,11 +433,11 @@ func TestCollectionSearch(t *testing.T) { n = 0 c.SearchValuesRange("0070", "0060", true, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { if n > 0 { expect(t, obj.String() < prevValue) } - expect(t, id == fmt.Sprintf("%04d", int(fields[1]))) + expect(t, id == fmt.Sprintf("%04d", int(fields.Get(1)))) n++ prevValue = obj.String() return true @@ -493,7 +516,7 @@ func TestSpatialSearch(t *testing.T) { n = 0 c.Within(q1, 0, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { n++ return true }, @@ -502,7 +525,7 @@ func TestSpatialSearch(t *testing.T) { n = 0 c.Within(q2, 0, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { n++ return true }, @@ -511,7 +534,7 @@ func TestSpatialSearch(t *testing.T) { n = 0 c.Within(q3, 0, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { n++ return true }, @@ -520,7 +543,7 @@ func TestSpatialSearch(t *testing.T) { n = 0 c.Intersects(q1, 0, nil, - func(_ string, _ geojson.Object, _ []float64) bool { + func(_ string, _ geojson.Object, _ *Fields) bool { n++ return true }, @@ -529,7 +552,7 @@ func TestSpatialSearch(t *testing.T) { n = 0 c.Intersects(q2, 0, nil, - func(_ string, _ geojson.Object, _ []float64) bool { + func(_ string, _ geojson.Object, _ *Fields) bool { n++ return true }, @@ -538,7 +561,7 @@ func TestSpatialSearch(t *testing.T) { n = 0 c.Intersects(q3, 0, nil, - func(_ string, _ geojson.Object, _ []float64) bool { + func(_ string, _ geojson.Object, _ *Fields) bool { n++ return true }, @@ -547,7 +570,7 @@ func TestSpatialSearch(t *testing.T) { n = 0 c.Intersects(q3, 0, nil, - func(_ string, _ geojson.Object, _ []float64) bool { + func(_ string, _ geojson.Object, _ *Fields) bool { n++ return n <= 1 }, @@ -559,7 +582,7 @@ func TestSpatialSearch(t *testing.T) { r2, p1, p4, r1, p3, r3, p2, } c.Nearby(q4, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { items = append(items, obj) return true }, @@ -585,7 +608,7 @@ func TestCollectionSparse(t *testing.T) { var n int n = 0 c.Within(rect, 1, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { n++ return true }, @@ -594,7 +617,7 @@ func TestCollectionSparse(t *testing.T) { n = 0 c.Within(rect, 2, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { n++ return true }, @@ -603,7 +626,7 @@ func TestCollectionSparse(t *testing.T) { n = 0 c.Within(rect, 3, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { n++ return true }, @@ -612,7 +635,7 @@ func TestCollectionSparse(t *testing.T) { n = 0 c.Within(rect, 3, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *Fields) bool { n++ return n <= 30 }, @@ -621,7 +644,7 @@ func TestCollectionSparse(t *testing.T) { n = 0 c.Intersects(rect, 3, nil, - func(id string, _ geojson.Object, _ []float64) bool { + func(id string, _ geojson.Object, _ *Fields) bool { n++ return true }, @@ -630,7 +653,7 @@ func TestCollectionSparse(t *testing.T) { n = 0 c.Intersects(rect, 3, nil, - func(id string, _ geojson.Object, _ []float64) bool { + func(id string, _ geojson.Object, _ *Fields) bool { n++ return n <= 30 }, @@ -683,7 +706,7 @@ func TestManyCollections(t *testing.T) { Min: geometry.Point{X: -180, Y: 30}, Max: geometry.Point{X: 34, Y: 100}, } - col.geoSearch(bbox, func(id string, obj geojson.Object, fields []float64) bool { + col.geoSearch(bbox, func(id string, obj geojson.Object, fields *Fields) bool { //println(id) return true }) diff --git a/internal/collection/fields.go b/internal/collection/fields.go new file mode 100644 index 00000000..c4871e39 --- /dev/null +++ b/internal/collection/fields.go @@ -0,0 +1,40 @@ +package collection + +import "github.com/tidwall/tile38/internal/collection/item" + +// // FieldIter ... +// type FieldIter interface { +// ForEachField(count int, iter func(value float64) bool) +// GetField(index int) float64 +// HasFields() bool +// } + +// Fields ... +type Fields struct { + item *item.Item +} + +// ForEach iterates over each field. The count param is the number of +// iterations. When count is less than zero, then all fields are returns. +func (fields *Fields) ForEach(count int, iter func(value float64) bool) { + if fields == nil || fields.item == nil { + return + } + fields.item.ForEachField(count, iter) +} + +// Get returns the value for a field at index. If there is no field at index, +// then zero is returned. +func (fields *Fields) Get(index int) float64 { + if fields == nil || fields.item == nil { + return 0 + } + return fields.item.GetField(index) +} + +func itemFields(item *item.Item) *Fields { + if item == nil || !item.HasFields() { + return nil + } + return &Fields{item: item} +} diff --git a/internal/collection/item/item.go b/internal/collection/item/item.go index 8177da46..5463d8a6 100644 --- a/internal/collection/item/item.go +++ b/internal/collection/item/item.go @@ -39,7 +39,7 @@ func (item *Item) ID() string { } // Fields returns the field values -func (item *Item) Fields() []float64 { +func (item *Item) fields() []float64 { return *(*[]float64)((unsafe.Pointer)(&reflect.SliceHeader{ Data: uintptr(unsafe.Pointer(item.data)), Len: int(item.fieldsLen) / 8, @@ -104,8 +104,15 @@ func (item *Item) Less(other btree.Item, ctx interface{}) bool { return item.ID() < other.(*Item).ID() } -// CopyOverFields overwriting previous fields -func (item *Item) CopyOverFields(values []float64) { +// CopyOverFields overwriting previous fields. Accepts an *Item or []float64 +func (item *Item) CopyOverFields(from interface{}) { + var values []float64 + switch from := from.(type) { + case *Item: + values = from.fields() + case []float64: + values = from + } fieldBytes := floatsToBytes(values) oldData := item.dataBytes() newData := make([]byte, len(fieldBytes)+int(item.idLen)) @@ -153,15 +160,6 @@ func (item *Item) SetField(index int, value float64) (updated bool) { return true } -// GetField returns the value for a field at index. -func (item *Item) GetField(index int) float64 { - numFields := int(item.fieldsLen / 8) - if index < numFields { - return getFieldAt(item.data, index) - } - return 0 -} - func (item *Item) dataBytes() []byte { return *(*[]byte)((unsafe.Pointer)(&reflect.SliceHeader{ Data: uintptr(unsafe.Pointer(item.data)), @@ -177,3 +175,44 @@ func floatsToBytes(f []float64) []byte { Cap: len(f) * 8, })) } + +// ForEachField iterates over each field. The count param is the number of +// iterations. When count is less than zero, then all fields are returns. +func (item *Item) ForEachField(count int, iter func(value float64) bool) { + if item == nil { + return + } + fields := item.fields() + var n int + if count < 0 { + n = len(fields) + } else { + n = count + } + for i := 0; i < n; i++ { + var field float64 + if i < len(fields) { + field = fields[i] + } + if !iter(field) { + return + } + } +} + +// GetField returns the value for a field at index. +func (item *Item) GetField(index int) float64 { + if item == nil { + return 0 + } + numFields := int(item.fieldsLen / 8) + if index < numFields { + return getFieldAt(item.data, index) + } + return 0 +} + +// HasFields returns true when item has fields +func (item *Item) HasFields() bool { + return item != nil && item.fieldsLen > 0 +} diff --git a/internal/collection/item/item_test.go b/internal/collection/item/item_test.go index 6160dde6..f0237a54 100644 --- a/internal/collection/item/item_test.go +++ b/internal/collection/item/item_test.go @@ -19,7 +19,12 @@ func testRandItem(t *testing.T) { for i := range values { values[i] = rand.Float64() } - item := New(key, geojson.NewPoint(geometry.Point{X: 1, Y: 2})) + var item *Item + if rand.Int()%2 == 0 { + item = New(key, geojson.NewSimplePoint(geometry.Point{X: 1, Y: 2})) + } else { + item = New(key, geojson.NewPoint(geometry.Point{X: 1, Y: 2})) + } if item.ID() != key { t.Fatalf("expected '%v', got '%v'", key, item.ID()) } @@ -38,7 +43,7 @@ func testRandItem(t *testing.T) { t.Fatalf("expected '%v', got '%v'", values[i], item.GetField(i)) } } - fields := item.Fields() + fields := item.fields() for i := 0; i < len(fields); i++ { for _, j := range setValues { if i == j { @@ -65,6 +70,42 @@ func testRandItem(t *testing.T) { t.Fatal("expected false") } } + var fvalues []float64 + item.ForEachField(len(values), func(value float64) bool { + fvalues = append(fvalues, value) + return true + }) + if !reflect.DeepEqual(values, fvalues) { + t.Fatalf("expected '%v', got '%v'", values, fvalues) + } + + fvalues = nil + item.ForEachField(len(values), func(value float64) bool { + if len(fvalues) == 1 { + return false + } + fvalues = append(fvalues, value) + return true + }) + if len(values) > 0 && len(fvalues) != 1 { + t.Fatalf("expected '%v', got '%v'", 1, len(fvalues)) + } + + fvalues = nil + item.ForEachField(-1, func(value float64) bool { + fvalues = append(fvalues, value) + return true + }) + if !reflect.DeepEqual(values, fvalues) { + t.Fatalf("expected '%v', got '%v'", 1, len(fvalues)) + } + + // should not fail, must allow nil receiver + (*Item)(nil).ForEachField(1, nil) + if (*Item)(nil).GetField(1) != 0 { + t.Fatalf("expected '%v', got '%v'", 0, (*Item)(nil).GetField(1)) + } + if item.ID() != key { t.Fatalf("expected '%v', got '%v'", key, item.ID()) } @@ -79,8 +120,22 @@ func testRandItem(t *testing.T) { if points != 1 { t.Fatalf("expected '%v', got '%v'", 1, points) } - if !reflect.DeepEqual(item.Fields(), values) { - t.Fatalf("expected '%v', got '%v'", values, item.Fields()) + if !reflect.DeepEqual(item.fields(), values) { + t.Fatalf("expected '%v', got '%v'", values, item.fields()) + } + item.CopyOverFields(item) + weight, points = item.WeightAndPoints() + if weight != len(values)*8+len(key)+points*16 { + t.Fatalf("expected '%v', got '%v'", len(values)*8+len(key)+points*16, weight) + } + if points != 1 { + t.Fatalf("expected '%v', got '%v'", 1, points) + } + if !reflect.DeepEqual(item.fields(), values) { + t.Fatalf("expected '%v', got '%v'", values, item.fields()) + } + if !item.HasFields() { + t.Fatal("expected true") } item.CopyOverFields(nil) @@ -91,12 +146,16 @@ func testRandItem(t *testing.T) { if points != 1 { t.Fatalf("expected '%v', got '%v'", 1, points) } - if len(item.Fields()) != 0 { - t.Fatalf("expected '%#v', got '%#v'", 0, len(item.Fields())) + if len(item.fields()) != 0 { + t.Fatalf("expected '%#v', got '%#v'", 0, len(item.fields())) } if item.ID() != key { t.Fatalf("expected '%v', got '%v'", key, item.ID()) } + if item.HasFields() { + t.Fatal("expected false") + } + } func TestItem(t *testing.T) { diff --git a/internal/server/aofshrink.go b/internal/server/aofshrink.go index efc1c1c1..03ca574f 100644 --- a/internal/server/aofshrink.go +++ b/internal/server/aofshrink.go @@ -96,8 +96,9 @@ func (server *Server) aofshrink() { var exm = server.expires[keys[0]] // the expiration map var now = time.Now() // used for expiration var count = 0 // the object count + col.ScanGreaterOrEqual(nextid, false, nil, - func(id string, obj geojson.Object, fields []float64) bool { + func(id string, obj geojson.Object, fields *collection.Fields) bool { if count == maxids { // we reached the max number of ids for one batch nextid = id @@ -110,13 +111,16 @@ func (server *Server) aofshrink() { values = append(values, "set") values = append(values, keys[0]) values = append(values, id) - for i, fvalue := range fields { + var fidx int + fields.ForEach(len(fnames), func(fvalue float64) bool { if fvalue != 0 { values = append(values, "field") - values = append(values, fnames[i]) + values = append(values, fnames[fidx]) values = append(values, strconv.FormatFloat(fvalue, 'f', -1, 64)) } - } + fidx++ + return true + }) if exm != nil { at, ok := exm[id] if ok { diff --git a/internal/server/crud.go b/internal/server/crud.go index e7aa5b03..ad680ab5 100644 --- a/internal/server/crud.go +++ b/internal/server/crud.go @@ -34,16 +34,14 @@ func (a byField) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func orderFields(fmap map[string]int, fields []float64) []fvt { +func orderFields(fmap map[string]int, fields *collection.Fields) []fvt { var fv fvt fvs := make([]fvt, 0, len(fmap)) for field, idx := range fmap { - if idx < len(fields) { - fv.field = field - fv.value = fields[idx] - if fv.value != 0 { - fvs = append(fvs, fv) - } + fv.field = field + fv.value = fields.Get(idx) + if fv.value != 0 { + fvs = append(fvs, fv) } } sort.Sort(byField(fvs)) @@ -352,7 +350,7 @@ func (server *Server) cmdPdel(msg *Message) (res resp.Value, d commandDetails, e return } now := time.Now() - iter := func(id string, o geojson.Object, fields []float64) bool { + iter := func(id string, o geojson.Object, fields *collection.Fields) bool { if match, _ := glob.Match(d.pattern, id); match { d.children = append(d.children, &commandDetails{ command: "del", diff --git a/internal/server/fence.go b/internal/server/fence.go index 67c041a0..e5d514a2 100644 --- a/internal/server/fence.go +++ b/internal/server/fence.go @@ -9,6 +9,7 @@ import ( "github.com/tidwall/geojson/geo" "github.com/tidwall/geojson/geometry" "github.com/tidwall/gjson" + "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/glob" ) @@ -284,7 +285,7 @@ func extendRoamMessage( } pattern := match.id + fence.roam.scan iterator := func( - oid string, o geojson.Object, fields []float64, + oid string, o geojson.Object, fields *collection.Fields, ) bool { if oid == match.id { return true @@ -371,7 +372,7 @@ func fenceMatchRoam( Max: geometry.Point{X: maxLon, Y: maxLat}, } col.Intersects(geojson.NewRect(rect), 0, nil, func( - id string, obj2 geojson.Object, fields []float64, + id string, obj2 geojson.Object, fields *collection.Fields, ) bool { if c.hasExpired(fence.roam.key, id) { return true diff --git a/internal/server/scan.go b/internal/server/scan.go index c19894b2..33bd4fc0 100644 --- a/internal/server/scan.go +++ b/internal/server/scan.go @@ -7,6 +7,7 @@ import ( "github.com/tidwall/geojson" "github.com/tidwall/resp" + "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/glob" ) @@ -67,7 +68,7 @@ func (c *Server) cmdScan(msg *Message) (res resp.Value, err error) { g := glob.Parse(sw.globPattern, s.desc) if g.Limits[0] == "" && g.Limits[1] == "" { sw.col.Scan(s.desc, sw, - func(id string, o geojson.Object, fields []float64) bool { + func(id string, o geojson.Object, fields *collection.Fields) bool { return sw.writeObject(ScanWriterParams{ id: id, o: o, @@ -77,7 +78,7 @@ func (c *Server) cmdScan(msg *Message) (res resp.Value, err error) { ) } else { sw.col.ScanRange(g.Limits[0], g.Limits[1], s.desc, sw, - func(id string, o geojson.Object, fields []float64) bool { + func(id string, o geojson.Object, fields *collection.Fields) bool { return sw.writeObject(ScanWriterParams{ id: id, o: o, diff --git a/internal/server/scanner.go b/internal/server/scanner.go index 7aa4434e..2ecb6c3b 100644 --- a/internal/server/scanner.go +++ b/internal/server/scanner.go @@ -64,7 +64,7 @@ type scanWriter struct { type ScanWriterParams struct { id string o geojson.Object - fields []float64 + fields *collection.Fields distance float64 noLock bool ignoreGlobMatch bool @@ -194,7 +194,9 @@ func (sw *scanWriter) writeFoot() { } } -func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []float64, match bool) { +func (sw *scanWriter) fieldMatch( + fields *collection.Fields, o geojson.Object, +) (fvals []float64, match bool) { var z float64 var gotz bool fvals = sw.fvals @@ -212,9 +214,7 @@ func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []fl var value float64 idx, ok := sw.fmap[where.field] if ok { - if len(fields) > idx { - value = fields[idx] - } + value = fields.Get(idx) } if !where.match(value) { return @@ -224,9 +224,7 @@ func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []fl var value float64 idx, ok := sw.fmap[wherein.field] if ok { - if len(fields) > idx { - value = fields[idx] - } + value = fields.Get(idx) } if !wherein.match(value) { return @@ -235,11 +233,7 @@ func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []fl for _, whereval := range sw.whereevals { fieldsWithNames := make(map[string]float64) for field, idx := range sw.fmap { - if idx < len(fields) { - fieldsWithNames[field] = fields[idx] - } else { - fieldsWithNames[field] = 0 - } + fieldsWithNames[field] = fields.Get(idx) } if !whereval.match(fieldsWithNames) { return @@ -247,11 +241,7 @@ func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []fl } } else { for idx := range sw.farr { - var value float64 - if len(fields) > idx { - value = fields[idx] - } - sw.fvals[idx] = value + sw.fvals[idx] = fields.Get(idx) } for _, where := range sw.wheres { if where.field == "z" { @@ -285,11 +275,7 @@ func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []fl for _, whereval := range sw.whereevals { fieldsWithNames := make(map[string]float64) for field, idx := range sw.fmap { - if idx < len(fields) { - fieldsWithNames[field] = fields[idx] - } else { - fieldsWithNames[field] = 0 - } + fieldsWithNames[field] = fields.Get(idx) } if !whereval.match(fieldsWithNames) { return @@ -333,7 +319,10 @@ func (sw *scanWriter) Step(n uint64) { // ok is whether the object passes the test and should be written // keepGoing is whether there could be more objects to test -func (sw *scanWriter) testObject(id string, o geojson.Object, fields []float64, ignoreGlobMatch bool) ( +func (sw *scanWriter) testObject( + id string, o geojson.Object, + fields *collection.Fields, ignoreGlobMatch bool, +) ( ok, keepGoing bool, fieldVals []float64) { if !ignoreGlobMatch { match, kg := sw.globMatch(id, o) @@ -384,14 +373,13 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool { jsfields = `,"fields":{` var i int for field, idx := range sw.fmap { - if len(opts.fields) > idx { - if opts.fields[idx] != 0 { - if i > 0 { - jsfields += `,` - } - jsfields += jsonString(field) + ":" + strconv.FormatFloat(opts.fields[idx], 'f', -1, 64) - i++ + fvalue := opts.fields.Get(idx) + if fvalue != 0 { + if i > 0 { + jsfields += `,` } + jsfields += jsonString(field) + ":" + strconv.FormatFloat(fvalue, 'f', -1, 64) + i++ } } jsfields += `}` diff --git a/internal/server/search.go b/internal/server/search.go index 9315d77a..88646e8a 100644 --- a/internal/server/search.go +++ b/internal/server/search.go @@ -14,6 +14,7 @@ import ( "github.com/tidwall/geojson/geometry" "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/bing" + "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/glob" ) @@ -370,7 +371,12 @@ func (server *Server) cmdNearby(msg *Message) (res resp.Value, err error) { } sw.writeHead() if sw.col != nil { - iter := func(id string, o geojson.Object, fields []float64, dist float64) bool { + iter := func( + id string, + o geojson.Object, + fields *collection.Fields, + dist float64, + ) bool { meters := 0.0 if s.distance { meters = geo.DistanceFromHaversine(dist) @@ -398,35 +404,38 @@ func (server *Server) cmdNearby(msg *Message) (res resp.Value, err error) { type iterItem struct { id string o geojson.Object - fields []float64 + fields *collection.Fields dist float64 } func (server *Server) nearestNeighbors( s *liveFenceSwitches, sw *scanWriter, target *geojson.Circle, - iter func(id string, o geojson.Object, fields []float64, dist float64, + iter func( + id string, o geojson.Object, fields *collection.Fields, dist float64, ) bool) { maxDist := target.Haversine() limit := int(sw.limit) var items []iterItem - sw.col.Nearby(target, sw, func(id string, o geojson.Object, fields []float64) bool { - if server.hasExpired(s.key, id) { - return true - } - ok, keepGoing, _ := sw.testObject(id, o, fields, true) - if !ok { - return true - } - dist := target.HaversineTo(o.Center()) - if maxDist > 0 && dist > maxDist { - return false - } - items = append(items, iterItem{id: id, o: o, fields: fields, dist: dist}) - if !keepGoing { - return false - } - return len(items) < limit - }) + sw.col.Nearby( + target, sw, + func(id string, o geojson.Object, fields *collection.Fields) bool { + if server.hasExpired(s.key, id) { + return true + } + ok, keepGoing, _ := sw.testObject(id, o, fields, true) + if !ok { + return true + } + dist := target.HaversineTo(o.Center()) + if maxDist > 0 && dist > maxDist { + return false + } + items = append(items, iterItem{id: id, o: o, fields: fields, dist: dist}) + if !keepGoing { + return false + } + return len(items) < limit + }) sort.Slice(items, func(i, j int) bool { return items[i].dist < items[j].dist }) @@ -481,7 +490,7 @@ func (server *Server) cmdWithinOrIntersects(cmd string, msg *Message) (res resp. if sw.col != nil { if cmd == "within" { sw.col.Within(s.obj, s.sparse, sw, func( - id string, o geojson.Object, fields []float64, + id string, o geojson.Object, fields *collection.Fields, ) bool { if server.hasExpired(s.key, id) { return true @@ -497,7 +506,7 @@ func (server *Server) cmdWithinOrIntersects(cmd string, msg *Message) (res resp. sw.col.Intersects(s.obj, s.sparse, sw, func( id string, o geojson.Object, - fields []float64, + fields *collection.Fields, ) bool { if server.hasExpired(s.key, id) { return true @@ -579,7 +588,11 @@ func (server *Server) cmdSearch(msg *Message) (res resp.Value, err error) { g := glob.Parse(sw.globPattern, s.desc) if g.Limits[0] == "" && g.Limits[1] == "" { sw.col.SearchValues(s.desc, sw, - func(id string, o geojson.Object, fields []float64) bool { + func( + id string, + o geojson.Object, + fields *collection.Fields, + ) bool { return sw.writeObject(ScanWriterParams{ id: id, o: o, @@ -593,7 +606,11 @@ func (server *Server) cmdSearch(msg *Message) (res resp.Value, err error) { // globSingle is only for ID matches, not values. sw.globSingle = false sw.col.SearchValuesRange(g.Limits[0], g.Limits[1], s.desc, sw, - func(id string, o geojson.Object, fields []float64) bool { + func( + id string, + o geojson.Object, + fields *collection.Fields, + ) bool { return sw.writeObject(ScanWriterParams{ id: id, o: o, diff --git a/internal/server/server.go b/internal/server/server.go index a31140a9..08fa1468 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -47,19 +47,19 @@ const ( // commandDetails is detailed information about a mutable command. It's used // for geofence formulas. type commandDetails struct { - command string // client command, like "SET" or "DEL" - key, id string // collection key and object id of object - newKey string // new key, for RENAME command - fmap map[string]int // map of field names to value indexes - obj geojson.Object // new object - fields []float64 // array of field values - oldObj geojson.Object // previous object, if any - oldFields []float64 // previous object field values - updated bool // object was updated - timestamp time.Time // timestamp when the update occured - parent bool // when true, only children are forwarded - pattern string // PDEL key pattern - children []*commandDetails // for multi actions such as "PDEL" + command string // client command, like "SET" or "DEL" + key, id string // collection key and object id of object + newKey string // new key, for RENAME command + obj geojson.Object // new object + fmap map[string]int // map of field names to value indexes + fields *collection.Fields // array of field values + oldObj geojson.Object // previous object, if any + oldFields *collection.Fields // previous object field values + updated bool // object was updated + timestamp time.Time // timestamp when the update occured + parent bool // when true, only children are forwarded + pattern string // PDEL key pattern + children []*commandDetails // for multi actions such as "PDEL" } // Server is a tile38 controller