diff --git a/go.mod b/go.mod index ed1846cd..b6e17547 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/tidwall/redbench v0.1.0 github.com/tidwall/redcon v1.4.4 github.com/tidwall/resp v0.1.1 - github.com/tidwall/rtree v1.9.1 + github.com/tidwall/rtree v1.9.2 github.com/tidwall/sjson v1.2.4 github.com/xdg/scram v1.0.5 github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da diff --git a/go.sum b/go.sum index f4ce2ce0..9905964d 100644 --- a/go.sum +++ b/go.sum @@ -383,8 +383,8 @@ github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYg github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= github.com/tidwall/rtree v1.3.1/go.mod h1:S+JSsqPTI8LfWA4xHBo5eXzie8WJLVFeppAutSegl6M= -github.com/tidwall/rtree v1.9.1 h1:UIPtvE09nLKZRnMNEwRZxu9jRAkzROAZDR+NPS/9IRs= -github.com/tidwall/rtree v1.9.1/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ= +github.com/tidwall/rtree v1.9.2 h1:6HiSU/bf4a7l2smEC+fEum/WloHMFCIQKWHjahm0Do8= +github.com/tidwall/rtree v1.9.2/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ= github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= diff --git a/internal/collection/collection.go b/internal/collection/collection.go index 0b7ff539..1c3f0e9b 100644 --- a/internal/collection/collection.go +++ b/internal/collection/collection.go @@ -5,11 +5,11 @@ import ( "github.com/tidwall/btree" "github.com/tidwall/geojson" - "github.com/tidwall/geojson/geo" "github.com/tidwall/geojson/geometry" "github.com/tidwall/rtree" "github.com/tidwall/tile38/internal/deadline" "github.com/tidwall/tile38/internal/field" + "github.com/tidwall/tile38/internal/object" ) // yieldStep forces the iterator to yield goroutine every 256 steps. @@ -21,20 +21,13 @@ type Cursor interface { Step(count uint64) } -type itemT struct { - id string - obj geojson.Object - expires int64 // unix nano expiration - fields field.List +func byID(a, b *object.Object) bool { + return a.ID() < b.ID() } -func byID(a, b *itemT) bool { - return a.id < b.id -} - -func byValue(a, b *itemT) bool { - value1 := a.obj.String() - value2 := b.obj.String() +func byValue(a, b *object.Object) bool { + value1 := a.String() + value2 := b.String() if value1 < value2 { return true } @@ -45,30 +38,23 @@ func byValue(a, b *itemT) bool { return byID(a, b) } -func byExpires(a, b *itemT) bool { - if a.expires < b.expires { +func byExpires(a, b *object.Object) bool { + if a.Expires() < b.Expires() { return true } - if a.expires > b.expires { + if a.Expires() > b.Expires() { return false } // the values match so we'll compare IDs, which are always unique. return byID(a, b) } -func (item *itemT) Rect() geometry.Rect { - if item.obj != nil { - return item.obj.Rect() - } - return geometry.Rect{} -} - // Collection represents a collection of geojson objects. type Collection struct { - items *btree.BTreeG[*itemT] // items sorted by id - spatial *rtree.RTreeGN[float32, *itemT] // items geospatially indexed - values *btree.BTreeG[*itemT] // items sorted by value+id - expires *btree.BTreeG[*itemT] // items sorted by ex+id + items *btree.BTreeG[*object.Object] // sorted by id + spatial *rtree.RTreeGN[float32, *object.Object] // geospatially indexed + values *btree.BTreeG[*object.Object] // sorted by value+id + expires *btree.BTreeG[*object.Object] // sorted by ex+id weight int points int objects int // geometry count @@ -83,7 +69,7 @@ func New() *Collection { items: btree.NewBTreeGOptions(byID, optsNoLock), values: btree.NewBTreeGOptions(byValue, optsNoLock), expires: btree.NewBTreeGOptions(byExpires, optsNoLock), - spatial: &rtree.RTreeGN[float32, *itemT]{}, + spatial: &rtree.RTreeGN[float32, *object.Object]{}, } return col } @@ -121,31 +107,14 @@ func (c *Collection) Bounds() (minX, minY, maxX, maxY float64) { right.Rect().Max.X, top.Rect().Max.Y } -func objIsSpatial(obj geojson.Object) bool { - _, ok := obj.(geojson.Spatial) - return ok -} - -func (c *Collection) objWeight(item *itemT) int { - var weight int - weight += len(item.id) - if objIsSpatial(item.obj) { - weight += item.obj.NumPoints() * 16 - } else { - weight += len(item.obj.String()) - } - weight += item.fields.Weight() - return weight -} - -func (c *Collection) indexDelete(item *itemT) { - if !item.obj.Empty() { +func (c *Collection) indexDelete(item *object.Object) { + if !item.Geo().Empty() { c.spatial.Delete(rtreeItem(item)) } } -func (c *Collection) indexInsert(item *itemT) { - if !item.obj.Empty() { +func (c *Collection) indexInsert(item *object.Object) { + if !item.Geo().Empty() { c.spatial.Insert(rtreeItem(item)) } } @@ -176,7 +145,7 @@ func rtreeValueUp(d float64) float32 { return f } -func rtreeItem(item *itemT) (min, max [2]float32, data *itemT) { +func rtreeItem(item *object.Object) (min, max [2]float32, data *object.Object) { min, max = rtreeRect(item.Rect()) return min, max, item } @@ -193,105 +162,68 @@ func rtreeRect(rect geometry.Rect) (min, max [2]float32) { // Set adds or replaces an object in the collection and returns the fields // array. -func (c *Collection) Set(id string, obj geojson.Object, fields field.List, ex int64) ( - oldObject geojson.Object, oldFields, newFields field.List, -) { - newItem := &itemT{ - id: id, - obj: obj, - expires: ex, - fields: fields, - } - - // add the new item to main btree and remove the old one if needed - oldItem, ok := c.items.Set(newItem) - if ok { - // the old item was removed, now let's remove it from the rtree/btree. - if objIsSpatial(oldItem.obj) { - c.indexDelete(oldItem) +func (c *Collection) Set(obj *object.Object) (prev *object.Object) { + prev, _ = c.items.Set(obj) + if prev != nil { + if prev.IsSpatial() { + c.indexDelete(prev) c.objects-- } else { - c.values.Delete(oldItem) + c.values.Delete(prev) c.nobjects-- } - // delete old item from the expires queue - if oldItem.expires != 0 { - c.expires.Delete(oldItem) + if prev.Expires() != 0 { + c.expires.Delete(prev) } - - // decrement the point count - c.points -= oldItem.obj.NumPoints() - - // decrement the weights - c.weight -= c.objWeight(oldItem) + c.points -= prev.Geo().NumPoints() + c.weight -= prev.Weight() } - - // insert the new item into the rtree or strings tree. - if objIsSpatial(newItem.obj) { - c.indexInsert(newItem) + if obj.IsSpatial() { + c.indexInsert(obj) c.objects++ } else { - c.values.Set(newItem) + c.values.Set(obj) c.nobjects++ } - // insert item into expires queue. - if newItem.expires != 0 { - c.expires.Set(newItem) + if obj.Expires() != 0 { + c.expires.Set(obj) } - - // increment the point count - c.points += newItem.obj.NumPoints() - - // add the new weights - c.weight += c.objWeight(newItem) - - if oldItem != nil { - return oldItem.obj, oldItem.fields, newItem.fields - } - return nil, field.List{}, newItem.fields + c.points += obj.Geo().NumPoints() + c.weight += obj.Weight() + return prev } // 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 field.List, ok bool, -) { - oldItem, ok := c.items.Delete(&itemT{id: id}) - if !ok { - return nil, field.List{}, false +func (c *Collection) Delete(id string) (prev *object.Object) { + key := object.New(id, nil, 0, 0, field.List{}) + prev, _ = c.items.Delete(key) + if prev == nil { + return nil } - if objIsSpatial(oldItem.obj) { - if !oldItem.obj.Empty() { - c.indexDelete(oldItem) + if prev.IsSpatial() { + if !prev.Geo().Empty() { + c.indexDelete(prev) } c.objects-- } else { - c.values.Delete(oldItem) + c.values.Delete(prev) c.nobjects-- } - // delete old item from expires queue - if oldItem.expires != 0 { - c.expires.Delete(oldItem) + if prev.Expires() != 0 { + c.expires.Delete(prev) } - c.weight -= c.objWeight(oldItem) - c.points -= oldItem.obj.NumPoints() - - return oldItem.obj, oldItem.fields, true + c.points -= prev.Geo().NumPoints() + c.weight -= prev.Weight() + return prev } // 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 field.List, - ex int64, - ok bool, -) { - item, ok := c.items.Get(&itemT{id: id}) - if !ok { - return nil, field.List{}, 0, false - } - return item.obj, item.fields, item.expires, true +func (c *Collection) Get(id string) *object.Object { + key := object.New(id, nil, 0, 0, field.List{}) + obj, _ := c.items.Get(key) + return obj } // Scan iterates though the collection ids. @@ -299,7 +231,7 @@ func (c *Collection) Scan( desc bool, cursor Cursor, deadline *deadline.Deadline, - iterator func(id string, obj geojson.Object, fields field.List) bool, + iterator func(obj *object.Object) bool, ) bool { var keepon = true var count uint64 @@ -308,13 +240,13 @@ func (c *Collection) Scan( offset = cursor.Offset() cursor.Step(offset) } - iter := func(item *itemT) bool { + iter := func(obj *object.Object) bool { count++ if count <= offset { return true } nextStep(count, cursor, deadline) - keepon = iterator(item.id, item.obj, item.fields) + keepon = iterator(obj) return keepon } if desc { @@ -331,7 +263,7 @@ func (c *Collection) ScanRange( desc bool, cursor Cursor, deadline *deadline.Deadline, - iterator func(id string, obj geojson.Object, fields field.List) bool, + iterator func(o *object.Object) bool, ) bool { var keepon = true var count uint64 @@ -340,29 +272,30 @@ func (c *Collection) ScanRange( offset = cursor.Offset() cursor.Step(offset) } - iter := func(item *itemT) bool { + iter := func(o *object.Object) bool { count++ if count <= offset { return true } nextStep(count, cursor, deadline) if !desc { - if item.id >= end { + if o.ID() >= end { return false } } else { - if item.id <= end { + if o.ID() <= end { return false } } - keepon = iterator(item.id, item.obj, item.fields) + keepon = iterator(o) return keepon } + pstart := object.New(start, nil, 0, 0, field.List{}) if desc { - c.items.Descend(&itemT{id: start}, iter) + c.items.Descend(pstart, iter) } else { - c.items.Ascend(&itemT{id: start}, iter) + c.items.Ascend(pstart, iter) } return keepon } @@ -372,7 +305,7 @@ func (c *Collection) SearchValues( desc bool, cursor Cursor, deadline *deadline.Deadline, - iterator func(id string, obj geojson.Object, fields field.List) bool, + iterator func(o *object.Object) bool, ) bool { var keepon = true var count uint64 @@ -381,13 +314,13 @@ func (c *Collection) SearchValues( offset = cursor.Offset() cursor.Step(offset) } - iter := func(item *itemT) bool { + iter := func(o *object.Object) bool { count++ if count <= offset { return true } nextStep(count, cursor, deadline) - keepon = iterator(item.id, item.obj, item.fields) + keepon = iterator(o) return keepon } if desc { @@ -402,7 +335,7 @@ func (c *Collection) SearchValues( func (c *Collection) SearchValuesRange(start, end string, desc bool, cursor Cursor, deadline *deadline.Deadline, - iterator func(id string, obj geojson.Object, fields field.List) bool, + iterator func(o *object.Object) bool, ) bool { var keepon = true var count uint64 @@ -411,38 +344,39 @@ func (c *Collection) SearchValuesRange(start, end string, desc bool, offset = cursor.Offset() cursor.Step(offset) } - iter := func(item *itemT) bool { + iter := func(o *object.Object) bool { count++ if count <= offset { return true } nextStep(count, cursor, deadline) - keepon = iterator(item.id, item.obj, item.fields) + keepon = iterator(o) return keepon } - pstart := &itemT{obj: String(start)} - pend := &itemT{obj: String(end)} + + pstart := object.New("", String(start), 0, 0, field.List{}) + pend := object.New("", String(end), 0, 0, field.List{}) if desc { // descend range - c.values.Descend(pstart, func(item *itemT) bool { + c.values.Descend(pstart, func(item *object.Object) bool { return bGT(c.values, item, pend) && iter(item) }) } else { - c.values.Ascend(pstart, func(item *itemT) bool { + c.values.Ascend(pstart, func(item *object.Object) bool { return bLT(c.values, item, pend) && iter(item) }) } return keepon } -func bLT(tr *btree.BTreeG[*itemT], a, b *itemT) bool { return tr.Less(a, b) } -func bGT(tr *btree.BTreeG[*itemT], a, b *itemT) bool { return tr.Less(b, a) } +func bLT(tr *btree.BTreeG[*object.Object], a, b *object.Object) bool { return tr.Less(a, b) } +func bGT(tr *btree.BTreeG[*object.Object], a, b *object.Object) bool { return tr.Less(b, a) } // ScanGreaterOrEqual iterates though the collection starting with specified id. func (c *Collection) ScanGreaterOrEqual(id string, desc bool, cursor Cursor, deadline *deadline.Deadline, - iterator func(id string, obj geojson.Object, fields field.List, ex int64) bool, + iterator func(o *object.Object) bool, ) bool { var keepon = true var count uint64 @@ -451,33 +385,34 @@ func (c *Collection) ScanGreaterOrEqual(id string, desc bool, offset = cursor.Offset() cursor.Step(offset) } - iter := func(item *itemT) bool { + iter := func(o *object.Object) bool { count++ if count <= offset { return true } nextStep(count, cursor, deadline) - keepon = iterator(item.id, item.obj, item.fields, item.expires) + keepon = iterator(o) return keepon } + pstart := object.New(id, nil, 0, 0, field.List{}) if desc { - c.items.Descend(&itemT{id: id}, iter) + c.items.Descend(pstart, iter) } else { - c.items.Ascend(&itemT{id: id}, iter) + c.items.Ascend(pstart, iter) } return keepon } func (c *Collection) geoSearch( rect geometry.Rect, - iter func(id string, obj geojson.Object, fields field.List) bool, + iter func(o *object.Object) bool, ) bool { alive := true min, max := rtreeRect(rect) c.spatial.Search( min, max, - func(_, _ [2]float32, item *itemT) bool { - alive = iter(item.id, item.obj, item.fields) + func(_, _ [2]float32, o *object.Object) bool { + alive = iter(o) return alive }, ) @@ -486,29 +421,25 @@ func (c *Collection) geoSearch( func (c *Collection) geoSparse( obj geojson.Object, sparse uint8, - iter func(id string, obj geojson.Object, fields field.List) (match, ok bool), + iter func(o *object.Object) (match, ok bool), ) bool { matches := make(map[string]bool) alive := true - c.geoSparseInner(obj.Rect(), sparse, - func(id string, o geojson.Object, fields field.List) ( - match, ok bool, - ) { - ok = true - if !matches[id] { - match, ok = iter(id, o, fields) - if match { - matches[id] = true - } + c.geoSparseInner(obj.Rect(), sparse, func(o *object.Object) (match, ok bool) { + ok = true + if !matches[o.ID()] { + match, ok = iter(o) + if match { + matches[o.ID()] = true } - return match, ok - }, - ) + } + return match, ok + }) return alive } func (c *Collection) geoSparseInner( rect geometry.Rect, sparse uint8, - iter func(id string, obj geojson.Object, fields field.List) (match, ok bool), + iter func(o *object.Object) (match, ok bool), ) bool { if sparse > 0 { w := rect.Max.X - rect.Min.X @@ -539,16 +470,14 @@ func (c *Collection) geoSparseInner( return true } alive := true - c.geoSearch(rect, - func(id string, obj geojson.Object, fields field.List) bool { - match, ok := iter(id, obj, fields) - if !ok { - alive = false - return false - } - return !match - }, - ) + c.geoSearch(rect, func(o *object.Object) bool { + match, ok := iter(o) + if !ok { + alive = false + return false + } + return !match + }) return alive } @@ -559,7 +488,7 @@ func (c *Collection) Within( sparse uint8, cursor Cursor, deadline *deadline.Deadline, - iter func(id string, obj geojson.Object, fields field.List) bool, + iter func(o *object.Object) bool, ) bool { var count uint64 var offset uint64 @@ -568,45 +497,39 @@ func (c *Collection) Within( cursor.Step(offset) } if sparse > 0 { - return c.geoSparse(obj, sparse, - func(id string, o geojson.Object, fields field.List) ( - match, ok bool, - ) { - count++ - if count <= offset { - return false, true - } - nextStep(count, cursor, deadline) - if match = o.Within(obj); match { - ok = iter(id, o, fields) - } - return match, ok - }, - ) - } - return c.geoSearch(obj.Rect(), - func(id string, o geojson.Object, fields field.List) bool { + return c.geoSparse(obj, sparse, func(o *object.Object) (match, ok bool) { count++ if count <= offset { - return true + return false, true } nextStep(count, cursor, deadline) - if o.Within(obj) { - return iter(id, o, fields) + if match = o.Geo().Within(obj); match { + ok = iter(o) } + return match, ok + }) + } + return c.geoSearch(obj.Rect(), func(o *object.Object) bool { + count++ + if count <= offset { return true - }, - ) + } + nextStep(count, cursor, deadline) + if o.Geo().Within(obj) { + return iter(o) + } + return true + }) } // Intersects returns all object that are intersect an object or bounding box. // Set obj to nil in order to use the bounding box. func (c *Collection) Intersects( - obj geojson.Object, + gobj geojson.Object, sparse uint8, cursor Cursor, deadline *deadline.Deadline, - iter func(id string, obj geojson.Object, fields field.List) bool, + iter func(o *object.Object) bool, ) bool { var count uint64 var offset uint64 @@ -615,34 +538,29 @@ func (c *Collection) Intersects( cursor.Step(offset) } if sparse > 0 { - return c.geoSparse(obj, sparse, - func(id string, o geojson.Object, fields field.List) ( - match, ok bool, - ) { - count++ - if count <= offset { - return false, true - } - nextStep(count, cursor, deadline) - if match = o.Intersects(obj); match { - ok = iter(id, o, fields) - } - return match, ok - }, - ) - } - return c.geoSearch(obj.Rect(), - func(id string, o geojson.Object, fields field.List) bool { + return c.geoSparse(gobj, sparse, func(o *object.Object) (match, ok bool) { count++ if count <= offset { - return true + return false, true } nextStep(count, cursor, deadline) - if o.Intersects(obj) { - return iter(id, o, fields) + if match = o.Geo().Intersects(gobj); match { + ok = iter(o) } + return match, ok + }) + } + return c.geoSearch(gobj.Rect(), func(o *object.Object) bool { + count++ + if count <= offset { return true - }, + } + nextStep(count, cursor, deadline) + if o.Geo().Intersects(gobj) { + return iter(o) + } + return true + }, ) } @@ -651,41 +569,8 @@ func (c *Collection) Nearby( target geojson.Object, cursor Cursor, deadline *deadline.Deadline, - iter func(id string, obj geojson.Object, fields field.List, dist float64) bool, + iter func(o *object.Object, dist float64) 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. - if circle, ok := target.(*geojson.Circle); ok { - meters := circle.Meters() - if meters > 0 { - center := circle.Center() - minLat, minLon, maxLat, maxLon := - geo.RectFromCenter(center.Y, center.X, meters) - var exists bool - min, max := rtreeRect(geometry.Rect{ - Min: geometry.Point{ - X: minLon, - Y: minLat, - }, - Max: geometry.Point{ - X: maxLon, - Y: maxLat, - }, - }) - c.spatial.Search( - min, max, - func(_, _ [2]float32, item *itemT) bool { - exists = true - return false - }, - ) - if !exists { - // no candidates - return true - } - } - } - // do the kNN operation alive := true center := target.Center() var count uint64 @@ -694,22 +579,22 @@ func (c *Collection) Nearby( offset = cursor.Offset() cursor.Step(offset) } - distFn := geodeticDistAlgo[*itemT]([2]float64{center.X, center.Y}) + distFn := geodeticDistAlgo([2]float64{center.X, center.Y}) c.spatial.Nearby( - func(min, max [2]float32, data *itemT, item bool) float32 { - return float32(distFn( + func(min, max [2]float32, data *object.Object, item bool) float64 { + return distFn( [2]float64{float64(min[0]), float64(min[1])}, [2]float64{float64(max[0]), float64(max[1])}, data, item, - )) + ) }, - func(_, _ [2]float32, item *itemT, dist float32) bool { + func(_, _ [2]float32, o *object.Object, dist float64) bool { count++ if count <= offset { return true } nextStep(count, cursor, deadline) - alive = iter(item.id, item.obj, item.fields, float64(dist)) + alive = iter(o, dist) return alive }, ) @@ -727,8 +612,6 @@ func nextStep(step uint64, cursor Cursor, deadline *deadline.Deadline) { } // ScanExpires returns a list of all objects that have expired. -func (c *Collection) ScanExpires(iter func(id string, expires int64) bool) { - c.expires.Scan(func(item *itemT) bool { - return iter(item.id, item.expires) - }) +func (c *Collection) ScanExpires(iter func(o *object.Object) bool) { + c.expires.Scan(iter) } diff --git a/internal/collection/collection_test.go b/internal/collection/collection_test.go index 6a4e0592..dc4a9aec 100644 --- a/internal/collection/collection_test.go +++ b/internal/collection/collection_test.go @@ -12,6 +12,7 @@ import ( "github.com/tidwall/geojson/geometry" "github.com/tidwall/gjson" "github.com/tidwall/tile38/internal/field" + "github.com/tidwall/tile38/internal/object" ) func PO(x, y float64) *geojson.Point { @@ -47,14 +48,14 @@ func TestCollectionNewCollection(t *testing.T) { id := strconv.FormatInt(int64(i), 10) obj := PO(rand.Float64()*360-180, rand.Float64()*180-90) objs[id] = obj - c.Set(id, obj, field.List{}, 0) + c.Set(object.New(id, obj, 0, 0, field.List{})) } count := 0 bbox := geometry.Rect{ Min: geometry.Point{X: -180, Y: -90}, Max: geometry.Point{X: 180, Y: 90}, } - c.geoSearch(bbox, func(id string, obj geojson.Object, _ field.List) bool { + c.geoSearch(bbox, func(o *object.Object) bool { count++ return true }) @@ -80,44 +81,32 @@ func TestCollectionSet(t *testing.T) { t.Run("AddString", func(t *testing.T) { c := New() str1 := String("hello") - oldObject, oldFields, newFields := c.Set("str", str1, field.List{}, 0) - expect(t, oldObject == nil) - expect(t, oldFields.Len() == 0) - expect(t, newFields.Len() == 0) + old := c.Set(object.New("str", str1, 0, 0, field.List{})) + expect(t, old == nil) }) t.Run("UpdateString", func(t *testing.T) { c := New() str1 := String("hello") str2 := String("world") - oldObject, oldFields, newFields := c.Set("str", str1, field.List{}, 0) - expect(t, oldObject == nil) - expect(t, oldFields.Len() == 0) - expect(t, newFields.Len() == 0) - oldObject, oldFields, newFields = c.Set("str", str2, field.List{}, 0) - expect(t, oldObject == str1) - expect(t, oldFields.Len() == 0) - expect(t, newFields.Len() == 0) + old := c.Set(object.New("str", str1, 0, 0, field.List{})) + expect(t, old == nil) + old = c.Set(object.New("str", str2, 0, 0, field.List{})) + expect(t, old.Geo() == str1) }) t.Run("AddPoint", func(t *testing.T) { c := New() point1 := PO(-112.1, 33.1) - oldObject, oldFields, newFields := c.Set("point", point1, field.List{}, 0) - expect(t, oldObject == nil) - expect(t, oldFields.Len() == 0) - expect(t, newFields.Len() == 0) + old := c.Set(object.New("point", point1, 0, 0, field.List{})) + expect(t, old == nil) }) t.Run("UpdatePoint", func(t *testing.T) { c := New() point1 := PO(-112.1, 33.1) point2 := PO(-112.2, 33.2) - oldObject, oldFields, newFields := c.Set("point", point1, field.List{}, 0) - expect(t, oldObject == nil) - expect(t, oldFields.Len() == 0) - expect(t, newFields.Len() == 0) - oldObject, oldFields, newFields = c.Set("point", point2, field.List{}, 0) - expect(t, oldObject == point1) - expect(t, oldFields.Len() == 0) - expect(t, newFields.Len() == 0) + old := c.Set(object.New("point", point1, 0, 0, field.List{})) + expect(t, old == nil) + old = c.Set(object.New("point", point2, 0, 0, field.List{})) + expect(t, old.Geo().Center() == point1.Base()) }) t.Run("Fields", func(t *testing.T) { c := New() @@ -126,11 +115,8 @@ func TestCollectionSet(t *testing.T) { fNames := []string{"a", "b", "c"} fValues := []string{"1", "2", "3"} fields1 := toFields(fNames, fValues) - oldObj, oldFlds, newFlds := c.Set("str", str1, fields1, 0) - - expect(t, oldObj == nil) - expect(t, oldFlds.Len() == 0) - expect(t, reflect.DeepEqual(newFlds, fields1)) + old := c.Set(object.New("str", str1, 0, 0, fields1)) + expect(t, old == nil) str2 := String("hello") @@ -138,25 +124,23 @@ func TestCollectionSet(t *testing.T) { fValues = []string{"4", "5", "6"} fields2 := toFields(fNames, fValues) - oldObj, oldFlds, newFlds = c.Set("str", str2, fields2, 0) - expect(t, oldObj == str1) - expect(t, reflect.DeepEqual(oldFlds, fields1)) - expect(t, reflect.DeepEqual(newFlds, fields2)) + old = c.Set(object.New("str", str2, 0, 0, fields2)) + expect(t, old.Geo() == str1) + expect(t, reflect.DeepEqual(old.Fields(), fields1)) fNames = []string{"a", "b", "c", "d", "e", "f"} fValues = []string{"7", "8", "9", "10", "11", "12"} fields3 := toFields(fNames, fValues) - oldObj, oldFlds, newFlds = c.Set("str", str1, fields3, 0) - expect(t, oldObj == str2) - expect(t, reflect.DeepEqual(oldFlds, fields2)) - expect(t, reflect.DeepEqual(newFlds, fields3)) + old = c.Set(object.New("str", str1, 0, 0, fields3)) + expect(t, old.Geo() == str2) + expect(t, reflect.DeepEqual(old.Fields(), fields2)) }) t.Run("Delete", func(t *testing.T) { c := New() - c.Set("1", String("1"), field.List{}, 0) - c.Set("2", String("2"), field.List{}, 0) - c.Set("3", PO(1, 2), field.List{}, 0) + c.Set(object.New("1", String("1"), 0, 0, field.List{})) + c.Set(object.New("2", String("2"), 0, 0, field.List{})) + c.Set(object.New("3", PO(1, 2), 0, 0, field.List{})) expect(t, c.Count() == 3) expect(t, c.StringCount() == 2) @@ -164,78 +148,30 @@ func TestCollectionSet(t *testing.T) { expect(t, bounds(c) == geometry.Rect{ Min: geometry.Point{X: 1, Y: 2}, Max: geometry.Point{X: 1, Y: 2}}) - var v geojson.Object - var ok bool - // var flds []float64 - // var updated bool - // var updateCount int + var prev *object.Object - v, _, ok = c.Delete("2") - expect(t, v.String() == "2") - expect(t, ok) + prev = c.Delete("2") + expect(t, prev.Geo().String() == "2") expect(t, c.Count() == 2) expect(t, c.StringCount() == 1) expect(t, c.PointCount() == 1) - v, _, ok = c.Delete("1") - expect(t, v.String() == "1") - expect(t, ok) + prev = c.Delete("1") + expect(t, prev.Geo().String() == "1") expect(t, c.Count() == 1) expect(t, c.StringCount() == 0) expect(t, c.PointCount() == 1) - // expect(t, len(c.FieldMap()) == 0) - - // _, flds, updated, ok = c.SetField("3", "hello", 123) - // expect(t, ok) - // expect(t, reflect.DeepEqual(flds, []float64{123})) - // expect(t, updated) - // expect(t, c.FieldMap()["hello"] == 0) - - // _, flds, updated, ok = c.SetField("3", "hello", 1234) - // expect(t, ok) - // expect(t, reflect.DeepEqual(flds, []float64{1234})) - // expect(t, updated) - - // _, flds, updated, ok = c.SetField("3", "hello", 1234) - // expect(t, ok) - // expect(t, reflect.DeepEqual(flds, []float64{1234})) - // expect(t, !updated) - - // _, 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, updateCount == 2) - // expect(t, c.FieldMap()["hello"] == 0) - // expect(t, c.FieldMap()["planet"] == 1) - // expect(t, c.FieldMap()["world"] == 2) - - v, _, ok = c.Delete("3") - expect(t, v.String() == `{"type":"Point","coordinates":[1,2]}`) - expect(t, ok) + prev = c.Delete("3") + expect(t, prev.Geo().String() == `{"type":"Point","coordinates":[1,2]}`) expect(t, c.Count() == 0) expect(t, c.StringCount() == 0) expect(t, c.PointCount() == 0) - v, _, ok = c.Delete("3") - expect(t, v == nil) - expect(t, !ok) + prev = c.Delete("3") + expect(t, prev == nil) expect(t, c.Count() == 0) expect(t, bounds(c) == geometry.Rect{}) - v, _, _, ok = c.Get("3") - expect(t, v == nil) - expect(t, !ok) - // _, _, _, ok = c.SetField("3", "hello", 123) - // expect(t, !ok) - // _, _, _, ok = c.SetFields("3", []string{"hello"}, []float64{123}) - // expect(t, !ok) - // expect(t, c.TotalWeight() == 0) - // expect(t, c.FieldMap()["hello"] == 0) - // expect(t, c.FieldMap()["planet"] == 1) - // expect(t, c.FieldMap()["world"] == 2) - // expect(t, reflect.DeepEqual( - // c.FieldArr(), []string{"hello", "planet", "world"}), - // ) + expect(t, c.Get("3") == nil) }) } @@ -260,82 +196,82 @@ func TestCollectionScan(t *testing.T) { c := New() for _, i := range rand.Perm(N) { id := fmt.Sprintf("%04d", i) - c.Set(id, String(id), makeFields( + c.Set(object.New(id, String(id), 0, 0, makeFields( field.Make("ex", id), - ), 0) + ))) } var n int var prevID string - c.Scan(false, nil, nil, func(id string, obj geojson.Object, fields field.List) bool { + c.Scan(false, nil, nil, func(o *object.Object) bool { if n > 0 { - expect(t, id > prevID) + expect(t, o.ID() > prevID) } - expect(t, id == fieldValueAt(fields, 0)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 0)) n++ - prevID = id + prevID = o.ID() return true }) expect(t, n == c.Count()) n = 0 - c.Scan(true, nil, nil, func(id string, obj geojson.Object, fields field.List) bool { + c.Scan(true, nil, nil, func(o *object.Object) bool { if n > 0 { - expect(t, id < prevID) + expect(t, o.ID() < prevID) } - expect(t, id == fieldValueAt(fields, 0)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 0)) n++ - prevID = id + prevID = o.ID() return true }) expect(t, n == c.Count()) n = 0 c.ScanRange("0060", "0070", false, nil, nil, - func(id string, obj geojson.Object, fields field.List) bool { + func(o *object.Object) bool { if n > 0 { - expect(t, id > prevID) + expect(t, o.ID() > prevID) } - expect(t, id == fieldValueAt(fields, 0)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 0)) n++ - prevID = id + prevID = o.ID() return true }) expect(t, n == 10) n = 0 c.ScanRange("0070", "0060", true, nil, nil, - func(id string, obj geojson.Object, fields field.List) bool { + func(o *object.Object) bool { if n > 0 { - expect(t, id < prevID) + expect(t, o.ID() < prevID) } - expect(t, id == fieldValueAt(fields, 0)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 0)) n++ - prevID = id + prevID = o.ID() return true }) expect(t, n == 10) n = 0 c.ScanGreaterOrEqual("0070", true, nil, nil, - func(id string, obj geojson.Object, fields field.List, ex int64) bool { + func(o *object.Object) bool { if n > 0 { - expect(t, id < prevID) + expect(t, o.ID() < prevID) } - expect(t, id == fieldValueAt(fields, 0)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 0)) n++ - prevID = id + prevID = o.ID() return true }) expect(t, n == 71) n = 0 c.ScanGreaterOrEqual("0070", false, nil, nil, - func(id string, obj geojson.Object, fields field.List, ex int64) bool { + func(o *object.Object) bool { if n > 0 { - expect(t, id > prevID) + expect(t, o.ID() > prevID) } - expect(t, id == fieldValueAt(fields, 0)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 0)) n++ - prevID = id + prevID = o.ID() return true }) expect(t, n == c.Count()-70) @@ -356,58 +292,59 @@ func TestCollectionSearch(t *testing.T) { for i, j := range rand.Perm(N) { id := fmt.Sprintf("%04d", j) ex := fmt.Sprintf("%04d", i) - c.Set(id, String(ex), + c.Set(object.New(id, String(ex), + 0, 0, makeFields( field.Make("i", ex), field.Make("j", id), - ), 0) + ))) } var n int var prevValue string - c.SearchValues(false, nil, nil, func(id string, obj geojson.Object, fields field.List) bool { + c.SearchValues(false, nil, nil, func(o *object.Object) bool { if n > 0 { - expect(t, obj.String() > prevValue) + expect(t, o.Geo().String() > prevValue) } - expect(t, id == fieldValueAt(fields, 1)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 1)) n++ - prevValue = obj.String() + prevValue = o.Geo().String() return true }) expect(t, n == c.Count()) n = 0 - c.SearchValues(true, nil, nil, func(id string, obj geojson.Object, fields field.List) bool { + c.SearchValues(true, nil, nil, func(o *object.Object) bool { if n > 0 { - expect(t, obj.String() < prevValue) + expect(t, o.Geo().String() < prevValue) } - expect(t, id == fieldValueAt(fields, 1)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 1)) n++ - prevValue = obj.String() + prevValue = o.Geo().String() return true }) expect(t, n == c.Count()) n = 0 c.SearchValuesRange("0060", "0070", false, nil, nil, - func(id string, obj geojson.Object, fields field.List) bool { + func(o *object.Object) bool { if n > 0 { - expect(t, obj.String() > prevValue) + expect(t, o.Geo().String() > prevValue) } - expect(t, id == fieldValueAt(fields, 1)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 1)) n++ - prevValue = obj.String() + prevValue = o.Geo().String() return true }) expect(t, n == 10) n = 0 c.SearchValuesRange("0070", "0060", true, nil, nil, - func(id string, obj geojson.Object, fields field.List) bool { + func(o *object.Object) bool { if n > 0 { - expect(t, obj.String() < prevValue) + expect(t, o.Geo().String() < prevValue) } - expect(t, id == fieldValueAt(fields, 1)) + expect(t, o.ID() == fieldValueAt(o.Fields(), 1)) n++ - prevValue = obj.String() + prevValue = o.Geo().String() return true }) expect(t, n == 10) @@ -415,41 +352,37 @@ func TestCollectionSearch(t *testing.T) { func TestCollectionWeight(t *testing.T) { c := New() - c.Set("1", String("1"), field.List{}, 0) + c.Set(object.New("1", String("1"), 0, 0, field.List{})) expect(t, c.TotalWeight() > 0) c.Delete("1") expect(t, c.TotalWeight() == 0) - c.Set("1", String("1"), + c.Set(object.New("1", String("1"), 0, 0, toFields( []string{"a", "b", "c"}, []string{"1", "2", "3"}, ), - 0, - ) + )) expect(t, c.TotalWeight() > 0) c.Delete("1") expect(t, c.TotalWeight() == 0) - c.Set("1", String("1"), + c.Set(object.New("1", String("1"), 0, 0, toFields( []string{"a", "b", "c"}, []string{"1", "2", "3"}, ), - 0, - ) - c.Set("2", String("2"), + )) + c.Set(object.New("2", String("2"), 0, 0, toFields( []string{"d", "e", "f"}, []string{"4", "5", "6"}, ), - 0, - ) - c.Set("1", String("1"), + )) + c.Set(object.New("1", String("1"), 0, 0, toFields( []string{"d", "e", "f"}, []string{"4", "5", "6"}, ), - 0, - ) + )) c.Delete("1") c.Delete("2") expect(t, c.TotalWeight() == 0) @@ -484,77 +417,63 @@ func TestSpatialSearch(t *testing.T) { q4, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q4"]`).Raw, nil) c := New() - c.Set("p1", p1, field.List{}, 0) - c.Set("p2", p2, field.List{}, 0) - c.Set("p3", p3, field.List{}, 0) - c.Set("p4", p4, field.List{}, 0) - c.Set("r1", r1, field.List{}, 0) - c.Set("r2", r2, field.List{}, 0) - c.Set("r3", r3, field.List{}, 0) + c.Set(object.New("p1", p1, 0, 0, field.List{})) + c.Set(object.New("p2", p2, 0, 0, field.List{})) + c.Set(object.New("p3", p3, 0, 0, field.List{})) + c.Set(object.New("p4", p4, 0, 0, field.List{})) + c.Set(object.New("r1", r1, 0, 0, field.List{})) + c.Set(object.New("r2", r2, 0, 0, field.List{})) + c.Set(object.New("r3", r3, 0, 0, field.List{})) var n int n = 0 - c.Within(q1, 0, nil, nil, - func(id string, obj geojson.Object, _ field.List) bool { - n++ - return true - }, - ) + c.Within(q1, 0, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 3) n = 0 - c.Within(q2, 0, nil, nil, - func(id string, obj geojson.Object, _ field.List) bool { - n++ - return true - }, - ) + c.Within(q2, 0, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 7) n = 0 - c.Within(q3, 0, nil, nil, - func(id string, obj geojson.Object, _ field.List) bool { - n++ - return true - }, - ) + c.Within(q3, 0, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 4) n = 0 - c.Intersects(q1, 0, nil, nil, - func(_ string, _ geojson.Object, _ field.List) bool { - n++ - return true - }, - ) + c.Intersects(q1, 0, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 4) n = 0 - c.Intersects(q2, 0, nil, nil, - func(_ string, _ geojson.Object, _ field.List) bool { - n++ - return true - }, - ) + c.Intersects(q2, 0, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 7) n = 0 - c.Intersects(q3, 0, nil, nil, - func(_ string, _ geojson.Object, _ field.List) bool { - n++ - return true - }, - ) + c.Intersects(q3, 0, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 5) n = 0 - c.Intersects(q3, 0, nil, nil, - func(_ string, _ geojson.Object, _ field.List) bool { - n++ - return n <= 1 - }, - ) + c.Intersects(q3, 0, nil, nil, func(o *object.Object) bool { + n++ + return n <= 1 + }) expect(t, n == 2) var items []geojson.Object @@ -564,15 +483,13 @@ func TestSpatialSearch(t *testing.T) { lastDist := float64(-1) distsMonotonic := true - c.Nearby(q4, nil, nil, - func(id string, obj geojson.Object, fields field.List, dist float64) bool { - if dist < lastDist { - distsMonotonic = false - } - items = append(items, obj) - return true - }, - ) + c.Nearby(q4, nil, nil, func(o *object.Object, dist float64) bool { + if dist < lastDist { + distsMonotonic = false + } + items = append(items, o.Geo()) + return true + }) expect(t, len(items) == 7) expect(t, distsMonotonic) expect(t, reflect.DeepEqual(items, exitems)) @@ -590,72 +507,60 @@ func TestCollectionSparse(t *testing.T) { x := (r.Max.X-r.Min.X)*rand.Float64() + r.Min.X y := (r.Max.Y-r.Min.Y)*rand.Float64() + r.Min.Y point := PO(x, y) - c.Set(fmt.Sprintf("%d", i), point, field.List{}, 0) + c.Set(object.New(fmt.Sprintf("%d", i), point, 0, 0, field.List{})) } var n int n = 0 - c.Within(rect, 1, nil, nil, - func(id string, obj geojson.Object, fields field.List) bool { - n++ - return true - }, - ) + c.Within(rect, 1, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 4) n = 0 - c.Within(rect, 2, nil, nil, - func(id string, obj geojson.Object, fields field.List) bool { - n++ - return true - }, - ) + c.Within(rect, 2, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 16) n = 0 - c.Within(rect, 3, nil, nil, - func(id string, obj geojson.Object, fields field.List) bool { - n++ - return true - }, - ) + c.Within(rect, 3, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 64) n = 0 - c.Within(rect, 3, nil, nil, - func(id string, obj geojson.Object, fields field.List) bool { - n++ - return n <= 30 - }, - ) + c.Within(rect, 3, nil, nil, func(o *object.Object) bool { + n++ + return n <= 30 + }) expect(t, n == 31) n = 0 - c.Intersects(rect, 3, nil, nil, - func(id string, _ geojson.Object, _ field.List) bool { - n++ - return true - }, - ) + c.Intersects(rect, 3, nil, nil, func(o *object.Object) bool { + n++ + return true + }) expect(t, n == 64) n = 0 - c.Intersects(rect, 3, nil, nil, - func(id string, _ geojson.Object, _ field.List) bool { - n++ - return n <= 30 - }, - ) + c.Intersects(rect, 3, nil, nil, func(o *object.Object) bool { + n++ + return n <= 30 + }) expect(t, n == 31) } func testCollectionVerifyContents(t *testing.T, c *Collection, objs map[string]geojson.Object) { for id, o2 := range objs { - o1, _, _, ok := c.Get(id) - if !ok { + o := c.Get(id) + if o == nil { t.Fatalf("ok[%s] = false, expect true", id) } - j1 := string(o1.AppendJSON(nil)) + j1 := string(o.Geo().AppendJSON(nil)) j2 := string(o2.AppendJSON(nil)) if j1 != j2 { t.Fatalf("j1 == %s, expect %s", j1, j2) @@ -682,7 +587,7 @@ func TestManyCollections(t *testing.T) { col = New() colsM[key] = col } - col.Set(id, obj, field.List{}, 0) + col.Set(object.New(id, obj, 0, 0, field.List{})) k++ } } @@ -693,7 +598,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 field.List) bool { + col.geoSearch(bbox, func(o *object.Object) bool { //println(id) return true }) @@ -736,7 +641,7 @@ func benchmarkInsert(t *testing.B, nFields int) { col := New() t.ResetTimer() for i := 0; i < t.N; i++ { - col.Set(items[i].id, items[i].object, items[i].fields, 0) + col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields)) } } @@ -760,12 +665,12 @@ func benchmarkReplace(t *testing.B, nFields int) { } col := New() for i := 0; i < t.N; i++ { - col.Set(items[i].id, items[i].object, items[i].fields, 0) + col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields)) } t.ResetTimer() for _, i := range rand.Perm(t.N) { - o, _, _ := col.Set(items[i].id, items[i].object, field.List{}, 0) - if o != items[i].object { + o := col.Set(object.New(items[i].id, items[i].object, 0, 0, field.List{})) + if o.Geo() != items[i].object { t.Fatal("shoot!") } } @@ -791,12 +696,12 @@ func benchmarkGet(t *testing.B, nFields int) { } col := New() for i := 0; i < t.N; i++ { - col.Set(items[i].id, items[i].object, items[i].fields, 0) + col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields)) } t.ResetTimer() for _, i := range rand.Perm(t.N) { - o, _, _, _ := col.Get(items[i].id) - if o != items[i].object { + o := col.Get(items[i].id) + if o.Geo() != items[i].object { t.Fatal("shoot!") } } @@ -822,12 +727,12 @@ func benchmarkRemove(t *testing.B, nFields int) { } col := New() for i := 0; i < t.N; i++ { - col.Set(items[i].id, items[i].object, items[i].fields, 0) + col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields)) } t.ResetTimer() for _, i := range rand.Perm(t.N) { - o, _, _ := col.Delete(items[i].id) - if o != items[i].object { + prev := col.Delete(items[i].id) + if prev.Geo() != items[i].object { t.Fatal("shoot!") } } @@ -853,12 +758,12 @@ func benchmarkScan(t *testing.B, nFields int) { } col := New() for i := 0; i < t.N; i++ { - col.Set(items[i].id, items[i].object, items[i].fields, 0) + col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields)) } t.ResetTimer() for i := 0; i < t.N; i++ { var scanIteration int - col.Scan(true, nil, nil, func(id string, obj geojson.Object, fields field.List) bool { + col.Scan(true, nil, nil, func(o *object.Object) bool { scanIteration++ return scanIteration <= 500 }) diff --git a/internal/collection/geodesic.go b/internal/collection/geodesic.go index b6330bcf..ed83b110 100644 --- a/internal/collection/geodesic.go +++ b/internal/collection/geodesic.go @@ -1,12 +1,23 @@ package collection -import "math" +import ( + "math" -func geodeticDistAlgo[T any](center [2]float64) ( - algo func(min, max [2]float64, data T, item bool) (dist float64), + "github.com/tidwall/tile38/internal/object" +) + +func geodeticDistAlgo(center [2]float64) ( + algo func(min, max [2]float64, obj *object.Object, item bool) (dist float64), ) { const earthRadius = 6371e3 - return func(min, max [2]float64, data T, item bool) (dist float64) { + return func(min, max [2]float64, obj *object.Object, item bool) (dist float64) { + if item { + r := obj.Rect() + min[0] = r.Min.X + min[1] = r.Min.Y + max[0] = r.Max.X + max[1] = r.Max.Y + } return earthRadius * pointRectDistGeodeticDeg( center[1], center[0], min[1], min[0], diff --git a/internal/field/list_binary.go b/internal/field/list_binary.go index f41bcf42..596a00b8 100644 --- a/internal/field/list_binary.go +++ b/internal/field/list_binary.go @@ -151,6 +151,9 @@ func (fields List) Set(field Field) List { func delfield(b []byte, s, e int) *byte { totallen := s + (len(b) - e) + if totallen == 0 { + return nil + } var psz [10]byte pn := binary.PutUvarint(psz[:], uint64(totallen)) plen := pn + totallen @@ -347,3 +350,12 @@ func (fields List) Weight() int { x, n := uvarint(*(*[]byte)(unsafe.Pointer(&bytes{fields.p, 10, 10}))) return x + n } + +// Bytes returns the raw bytes (including the header) +func (fields List) Bytes() []byte { + if fields.p == nil { + return nil + } + x, n := uvarint(*(*[]byte)(unsafe.Pointer(&bytes{fields.p, 10, 10}))) + return (*(*[]byte)(unsafe.Pointer(&bytes{fields.p, 0, n + x}))) +} diff --git a/internal/field/list_array.go b/internal/field/list_struct.go similarity index 100% rename from internal/field/list_array.go rename to internal/field/list_struct.go diff --git a/internal/object/object.go b/internal/object/object.go new file mode 100644 index 00000000..cf6cce8e --- /dev/null +++ b/internal/object/object.go @@ -0,0 +1,96 @@ +package object + +import ( + "github.com/tidwall/geojson" + "github.com/tidwall/geojson/geometry" + "github.com/tidwall/tile38/internal/field" +) + +type Object struct { + id string + geo geojson.Object + created int64 // unix nano created + expires int64 // unix nano expiration + fields field.List +} + +func (o *Object) ID() string { + if o == nil { + return "" + } + return o.id +} + +func (o *Object) Fields() field.List { + if o == nil { + return field.List{} + } + return o.fields +} + +func (o *Object) Created() int64 { + if o == nil { + return 0 + } + return o.created +} + +func (o *Object) Expires() int64 { + if o == nil { + return 0 + } + return o.expires +} + +func (o *Object) Rect() geometry.Rect { + if o == nil || o.geo == nil { + return geometry.Rect{} + } + return o.geo.Rect() +} + +func (o *Object) Geo() geojson.Object { + if o == nil || o.geo == nil { + return nil + } + return o.geo +} + +func (o *Object) String() string { + if o == nil || o.geo == nil { + return "" + } + return o.geo.String() +} + +func (o *Object) IsSpatial() bool { + _, ok := o.geo.(geojson.Spatial) + return ok +} + +func (o *Object) Weight() int { + if o == nil { + return 0 + } + var weight int + weight += len(o.ID()) + if o.IsSpatial() { + weight += o.Geo().NumPoints() * 16 + } else { + weight += len(o.Geo().String()) + } + weight += o.Fields().Weight() + return weight +} + +func New(id string, geo geojson.Object, created, expires int64, + fields field.List, +) *Object { + return &Object{ + id: id, + geo: geo, + created: created, + expires: expires, + fields: fields, + } +} diff --git a/internal/object/object_test.go b/internal/object/object_test.go new file mode 100644 index 00000000..4d60d3e7 --- /dev/null +++ b/internal/object/object_test.go @@ -0,0 +1,18 @@ +package object + +import ( + "testing" + + "github.com/tidwall/assert" + "github.com/tidwall/geojson" + "github.com/tidwall/geojson/geometry" + "github.com/tidwall/tile38/internal/field" +) + +func P(x, y float64) geojson.Object { + return geojson.NewSimplePoint(geometry.Point{X: 10, Y: 20}) +} +func TestObject(t *testing.T) { + o := New("hello", P(10, 20), 0, 99, field.List{}) + assert.Assert(o.ID() == "hello") +} diff --git a/internal/server/aof.go b/internal/server/aof.go index f90ab6e6..6d083e11 100644 --- a/internal/server/aof.go +++ b/internal/server/aof.go @@ -226,8 +226,8 @@ func (s *Server) getQueueCandidates(d *commandDetails) []*Hook { return true }) // look for candidates that might "cross" geofences - if d.oldObj != nil && d.obj != nil && s.hookCross.Len() > 0 { - r1, r2 := d.oldObj.Rect(), d.obj.Rect() + if d.old != nil && d.obj != nil && s.hookCross.Len() > 0 { + r1, r2 := d.old.Rect(), d.obj.Rect() s.hookCross.Search( [2]float64{ math.Min(r1.Min.X, r2.Min.X), @@ -246,8 +246,8 @@ func (s *Server) getQueueCandidates(d *commandDetails) []*Hook { }) } // look for candidates that overlap the old object - if d.oldObj != nil { - r1 := d.oldObj.Rect() + if d.old != nil { + r1 := d.old.Rect() s.hookTree.Search( [2]float64{r1.Min.X, r1.Min.Y}, [2]float64{r1.Max.X, r1.Max.Y}, diff --git a/internal/server/aofshrink.go b/internal/server/aofshrink.go index 980f0dc4..c47693c1 100644 --- a/internal/server/aofshrink.go +++ b/internal/server/aofshrink.go @@ -8,11 +8,11 @@ import ( "time" "github.com/tidwall/btree" - "github.com/tidwall/geojson" "github.com/tidwall/tile38/core" "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/field" "github.com/tidwall/tile38/internal/log" + "github.com/tidwall/tile38/internal/object" ) const maxkeys = 8 @@ -97,10 +97,10 @@ func (s *Server) aofshrink() { var now = time.Now().UnixNano() // used for expiration var count = 0 // the object count col.ScanGreaterOrEqual(nextid, false, nil, nil, - func(id string, obj geojson.Object, fields field.List, ex int64) bool { + func(o *object.Object) bool { if count == maxids { // we reached the max number of ids for one batch - nextid = id + nextid = o.ID() idsdone = false return false } @@ -108,8 +108,8 @@ func (s *Server) aofshrink() { values = values[:0] values = append(values, "set") values = append(values, keys[0]) - values = append(values, id) - fields.Scan(func(f field.Field) bool { + values = append(values, o.ID()) + o.Fields().Scan(func(f field.Field) bool { if !f.Value().IsZero() { values = append(values, "field") values = append(values, f.Name()) @@ -117,8 +117,8 @@ func (s *Server) aofshrink() { } return true }) - if ex != 0 { - ttl := math.Floor(float64(ex-now)/float64(time.Second)*10) / 10 + if o.Expires() != 0 { + ttl := math.Floor(float64(o.Expires()-now)/float64(time.Second)*10) / 10 if ttl < 0.1 { // always leave a little bit of ttl. ttl = 0.1 @@ -126,12 +126,12 @@ func (s *Server) aofshrink() { values = append(values, "ex") values = append(values, strconv.FormatFloat(ttl, 'f', -1, 64)) } - if objIsSpatial(obj) { + if objIsSpatial(o.Geo()) { values = append(values, "object") - values = append(values, string(obj.AppendJSON(nil))) + values = append(values, string(o.Geo().AppendJSON(nil))) } else { values = append(values, "string") - values = append(values, obj.String()) + values = append(values, o.Geo().String()) } // append the values to the aof buffer diff --git a/internal/server/crud.go b/internal/server/crud.go index 4dd33b9e..5de8affb 100644 --- a/internal/server/crud.go +++ b/internal/server/crud.go @@ -14,30 +14,9 @@ import ( "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/field" "github.com/tidwall/tile38/internal/glob" + "github.com/tidwall/tile38/internal/object" ) -// type fvt struct { -// field string -// value float64 -// } - -// func orderFields(fmap map[string]int, farr []string, fields []float64) []fvt { -// var fv fvt -// var idx int -// fvs := make([]fvt, 0, len(fmap)) -// for _, field := range farr { -// idx = fmap[field] -// if idx < len(fields) { -// fv.field = field -// fv.value = fields[idx] -// if fv.value != 0 { -// fvs = append(fvs, fv) -// } -// } -// } -// return fvs -// } - func (s *Server) cmdBounds(msg *Message) (resp.Value, error) { start := time.Now() vs := msg.Args[1:] @@ -150,7 +129,8 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) { } return NOMessage, errKeyNotFound } - o, fields, _, ok := col.Get(id) + o := col.Get(id) + ok = o != nil if !ok { if msg.OutputType == RESP { return resp.NullValue(), nil @@ -174,17 +154,17 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) { case "object": if msg.OutputType == JSON { buf.WriteString(`,"object":`) - buf.WriteString(string(o.AppendJSON(nil))) + buf.WriteString(string(o.Geo().AppendJSON(nil))) } else { - vals = append(vals, resp.StringValue(o.String())) + vals = append(vals, resp.StringValue(o.Geo().String())) } case "point": if msg.OutputType == JSON { buf.WriteString(`,"point":`) - buf.Write(appendJSONSimplePoint(nil, o)) + buf.Write(appendJSONSimplePoint(nil, o.Geo())) } else { - point := o.Center() - z := extractZCoordinate(o) + point := o.Geo().Center() + z := extractZCoordinate(o.Geo()) if z != 0 { vals = append(vals, resp.ArrayValue([]resp.Value{ resp.StringValue(strconv.FormatFloat(point.Y, 'f', -1, 64)), @@ -209,7 +189,7 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) { if err != nil || precision < 1 || precision > 12 { return NOMessage, errInvalidArgument(sprecision) } - center := o.Center() + center := o.Geo().Center() p := geohash.EncodeWithPrecision(center.Y, center.X, uint(precision)) if msg.OutputType == JSON { buf.WriteString(`"` + p + `"`) @@ -219,7 +199,7 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) { case "bounds": if msg.OutputType == JSON { buf.WriteString(`,"bounds":`) - buf.Write(appendJSONSimpleBounds(nil, o)) + buf.Write(appendJSONSimpleBounds(nil, o.Geo())) } else { bbox := o.Rect() vals = append(vals, resp.ArrayValue([]resp.Value{ @@ -239,14 +219,14 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) { return NOMessage, errInvalidNumberOfArguments } if withfields { - nfields := fields.Len() + nfields := o.Fields().Len() if nfields > 0 { fvals := make([]resp.Value, 0, nfields*2) if msg.OutputType == JSON { buf.WriteString(`,"fields":{`) } var i int - fields.Scan(func(f field.Field) bool { + o.Fields().Scan(func(f field.Field) bool { if msg.OutputType == JSON { if i > 0 { buf.WriteString(`,`) @@ -282,57 +262,64 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) { return NOMessage, nil } -func (s *Server) cmdDel(msg *Message) (res resp.Value, d commandDetails, err error) { +// DEL key id [ERRON404] +func (s *Server) cmdDel(msg *Message) (resp.Value, commandDetails, error) { start := time.Now() - vs := msg.Args[1:] - var ok bool - if vs, d.key, ok = tokenval(vs); !ok || d.key == "" { - err = errInvalidNumberOfArguments - return - } - if vs, d.id, ok = tokenval(vs); !ok || d.id == "" { - err = errInvalidNumberOfArguments - return + + // >> Args + + args := msg.Args + if len(args) < 3 { + return retwerr(errInvalidNumberOfArguments) } + key := args[1] + id := args[2] erron404 := false - if len(vs) > 0 { - _, arg, ok := tokenval(vs) - if ok && strings.ToLower(arg) == "erron404" { + for i := 3; i < len(args); i++ { + switch strings.ToLower(args[i]) { + case "erron404": erron404 = true - vs = vs[1:] - } else { - err = errInvalidArgument(arg) - return + default: + return retwerr(errInvalidArgument(args[i])) } } - if len(vs) != 0 { - err = errInvalidNumberOfArguments - return - } - found := false - col, _ := s.cols.Get(d.key) + + // >> Operation + + updated := false + var old *object.Object + col, _ := s.cols.Get(key) if col != nil { - d.obj, d.fields, ok = col.Delete(d.id) - if ok { + old = col.Delete(id) + if old != nil { if col.Count() == 0 { - s.cols.Delete(d.key) + s.cols.Delete(key) } - found = true + updated = true } else if erron404 { - err = errIDNotFound - return + return retwerr(errIDNotFound) } } else if erron404 { - err = errKeyNotFound - return + return retwerr(errKeyNotFound) } - s.groupDisconnectObject(d.key, d.id) + s.groupDisconnectObject(key, id) + + // >> Response + + var d commandDetails + d.command = "del" - d.updated = found + d.key = key + d.obj = old + d.updated = updated d.timestamp = time.Now() + + var res resp.Value + switch msg.OutputType { case JSON: - res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}") + res = resp.StringValue(`{"ok":true,"elapsed":"` + + time.Since(start).String() + "\"}") case RESP: if d.updated { res = resp.IntegerValue(1) @@ -340,7 +327,7 @@ func (s *Server) cmdDel(msg *Message) (res resp.Value, d commandDetails, err err res = resp.IntegerValue(0) } } - return + return res, d, nil } func (s *Server) cmdPdel(msg *Message) (res resp.Value, d commandDetails, err error) { @@ -360,14 +347,14 @@ func (s *Server) cmdPdel(msg *Message) (res resp.Value, d commandDetails, err er return } now := time.Now() - iter := func(id string, o geojson.Object, fields field.List) bool { - if match, _ := glob.Match(d.pattern, id); match { + iter := func(o *object.Object) bool { + if match, _ := glob.Match(d.pattern, o.ID()); match { d.children = append(d.children, &commandDetails{ command: "del", updated: true, timestamp: now, key: d.key, - id: id, + obj: o, }) } return true @@ -384,14 +371,15 @@ func (s *Server) cmdPdel(msg *Message) (res resp.Value, d commandDetails, err er } var atLeastOneNotDeleted bool for i, dc := range d.children { - dc.obj, dc.fields, ok = col.Delete(dc.id) - if !ok { + old := col.Delete(dc.obj.ID()) + if old == nil { d.children[i].command = "?" atLeastOneNotDeleted = true } else { + dc.obj = old d.children[i] = dc } - s.groupDisconnectObject(dc.key, dc.id) + s.groupDisconnectObject(dc.key, dc.obj.ID()) } if atLeastOneNotDeleted { var nchildren []*commandDetails @@ -565,7 +553,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) { var ex int64 var xx bool var nx bool - var obj geojson.Object + var oobj geojson.Object args := msg.Args if len(args) < 3 { @@ -614,7 +602,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) { } str := args[i+1] i += 1 - obj = collection.String(str) + oobj = collection.String(str) case "point": if i+2 >= len(args) { return retwerr(errInvalidNumberOfArguments) @@ -642,9 +630,9 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) { return retwerr(errInvalidArgument(slon)) } if !hasZ { - obj = geojson.NewPoint(geometry.Point{X: x, Y: y}) + oobj = geojson.NewPoint(geometry.Point{X: x, Y: y}) } else { - obj = geojson.NewPointZ(geometry.Point{X: x, Y: y}, z) + oobj = geojson.NewPointZ(geometry.Point{X: x, Y: y}, z) } case "bounds": if i+4 >= len(args) { @@ -659,7 +647,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) { } } i += 4 - obj = geojson.NewRect(geometry.Rect{ + oobj = geojson.NewRect(geometry.Rect{ Min: geometry.Point{X: vals[1], Y: vals[0]}, Max: geometry.Point{X: vals[3], Y: vals[2]}, }) @@ -670,7 +658,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) { shash := args[i+1] i += 1 lat, lon := geohash.Decode(shash) - obj = geojson.NewPoint(geometry.Point{X: lon, Y: lat}) + oobj = geojson.NewPoint(geometry.Point{X: lon, Y: lat}) case "object": if i+1 >= len(args) { return retwerr(errInvalidNumberOfArguments) @@ -678,7 +666,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) { json := args[i+1] i += 1 var err error - obj, err = geojson.Parse(json, &s.geomParseOpts) + oobj, err = geojson.Parse(json, &s.geomParseOpts) if err != nil { return retwerr(err) } @@ -702,7 +690,10 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) { var ofields field.List if !nada { - _, ofields, _, ok = col.Get(id) + o := col.Get(id) + if o != nil { + ofields = o.Fields() + } if xx || nx { if (nx && ok) || (xx && !ok) { nada = true @@ -730,18 +721,16 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) { ofields = ofields.Set(f) } - oldObj, oldFields, newFields := col.Set(id, obj, ofields, ex) + obj := object.New(id, oobj, 0, ex, ofields) + old := col.Set(obj) // >> Response var d commandDetails d.command = "set" d.key = key - d.id = id d.obj = obj - d.oldObj = oldObj - d.oldFields = oldFields - d.fields = newFields + d.old = old d.updated = true // perhaps we should do a diff on the previous object? d.timestamp = time.Now() @@ -811,12 +800,14 @@ func (s *Server) cmdFSET(msg *Message) (resp.Value, commandDetails, error) { if !ok { return retwerr(errKeyNotFound) } - obj, ofields, ex, ok := col.Get(id) + o := col.Get(id) + ok = o != nil if !(ok || xx) { return retwerr(errIDNotFound) } if ok { + ofields := o.Fields() for _, f := range fields { prev := ofields.Get(f.Name()) if !prev.Value().Equals(f.Value()) { @@ -824,11 +815,11 @@ func (s *Server) cmdFSET(msg *Message) (resp.Value, commandDetails, error) { updateCount++ } } - col.Set(id, obj, ofields, ex) - d.obj = obj + obj := object.New(id, o.Geo(), 0, o.Expires(), ofields) + col.Set(obj) d.command = "fset" d.key = key - d.id = id + d.obj = obj d.timestamp = time.Now() d.updated = updateCount > 0 } @@ -861,21 +852,22 @@ func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) { return retwerr(errInvalidArgument(svalue)) } var ok bool + var obj *object.Object col, _ := s.cols.Get(key) if col != nil { // replace the expiration by getting the old objec ex := time.Now().Add(time.Duration(float64(time.Second) * value)).UnixNano() - var obj geojson.Object - var fields field.List - obj, fields, _, ok = col.Get(id) + o := col.Get(id) + ok = o != nil if ok { - col.Set(id, obj, fields, ex) + obj = object.New(id, o.Geo(), 0, ex, o.Fields()) + col.Set(obj) } } var d commandDetails if ok { d.key = key - d.id = id + d.obj = obj d.command = "expire" d.updated = true d.timestamp = time.Now() @@ -909,41 +901,35 @@ func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) { return retwerr(errInvalidNumberOfArguments) } key, id := args[1], args[2] - var cleared bool - var ok bool col, _ := s.cols.Get(key) - if col != nil { - var ex int64 - _, _, ex, ok = col.Get(id) - if ok && ex != 0 { - var obj geojson.Object - var fields field.List - obj, fields, _, ok = col.Get(id) - if ok { - col.Set(id, obj, fields, 0) - } - if ok { - cleared = true - } - } - } - - if !ok { + if col == nil { if msg.OutputType == RESP { return resp.IntegerValue(0), commandDetails{}, nil } - if col == nil { - return retwerr(errKeyNotFound) + return retwerr(errKeyNotFound) + } + o := col.Get(id) + if o == nil { + if msg.OutputType == RESP { + return resp.IntegerValue(0), commandDetails{}, nil } return retwerr(errIDNotFound) } + var obj *object.Object + var cleared bool + if o.Expires() != 0 { + obj = object.New(id, o.Geo(), 0, 0, o.Fields()) + col.Set(obj) + cleared = true + } + var res resp.Value var d commandDetails - d.key = key - d.id = id d.command = "persist" + d.key = key + d.obj = obj d.updated = cleared d.timestamp = time.Now() @@ -973,15 +959,15 @@ func (s *Server) cmdTTL(msg *Message) (resp.Value, error) { var ok2 bool col, _ := s.cols.Get(key) if col != nil { - var ex int64 - _, _, ex, ok = col.Get(id) + o := col.Get(id) + ok = o != nil if ok { - if ex != 0 { + if o.Expires() != 0 { now := start.UnixNano() - if now > ex { + if now > o.Expires() { ok2 = false } else { - v = float64(ex-now) / float64(time.Second) + v = float64(o.Expires()-now) / float64(time.Second) if v < 0 { v = 0 } diff --git a/internal/server/expire.go b/internal/server/expire.go index ede82f1a..cd67fd79 100644 --- a/internal/server/expire.go +++ b/internal/server/expire.go @@ -5,6 +5,7 @@ import ( "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/log" + "github.com/tidwall/tile38/internal/object" ) const bgExpireDelay = time.Second / 10 @@ -31,11 +32,11 @@ func (s *Server) backgroundExpireObjects(now time.Time) { nano := now.UnixNano() var msgs []*Message s.cols.Scan(func(key string, col *collection.Collection) bool { - col.ScanExpires(func(id string, expires int64) bool { - if nano < expires { + col.ScanExpires(func(o *object.Object) bool { + if nano < o.Expires() { return false } - msgs = append(msgs, &Message{Args: []string{"del", key, id}}) + msgs = append(msgs, &Message{Args: []string{"del", key, o.ID()}}) return true }) return true diff --git a/internal/server/fence.go b/internal/server/fence.go index 58d2d9b1..8a0c80a2 100644 --- a/internal/server/fence.go +++ b/internal/server/fence.go @@ -12,6 +12,7 @@ import ( "github.com/tidwall/gjson" "github.com/tidwall/tile38/internal/field" "github.com/tidwall/tile38/internal/glob" + "github.com/tidwall/tile38/internal/object" ) // FenceMatch executes a fence match returns back json messages for fence detection. @@ -81,10 +82,13 @@ func fenceMatch( `,"time":` + jsonTimeFormat(details.timestamp) + `}`, } } - if !multiGlobMatch(fence.globs, details.id) { + if details.obj == nil { return nil } - if details.obj == nil || !objIsSpatial(details.obj) { + if !multiGlobMatch(fence.globs, details.obj.ID()) { + return nil + } + if !objIsSpatial(details.obj.Geo()) { return nil } if details.command == "fset" { @@ -97,7 +101,7 @@ func fenceMatch( return []string{ `{"command":"del"` + hookJSONString(hookName, metas) + `,"key":` + jsonString(details.key) + - `,"id":` + jsonString(details.id) + + `,"id":` + jsonString(details.obj.ID()) + `,"time":` + jsonTimeFormat(details.timestamp) + `}`, } } @@ -107,8 +111,7 @@ func fenceMatch( if fence.roam.on { if details.command == "set" { roamNearbys, roamFaraways = - fenceMatchRoam(sw.s, fence, details.id, - details.oldObj, details.obj) + fenceMatchRoam(sw.s, fence, details.obj, details.old) if len(roamNearbys) == 0 && len(roamFaraways) == 0 { return nil } @@ -117,14 +120,14 @@ func fenceMatch( } else { var nocross bool // not using roaming - match1 := fenceMatchObject(fence, details.oldObj) + match1 := fenceMatchObject(fence, details.old) if match1 { - match1, _, _ = sw.testObject(details.id, details.oldObj, details.oldFields) + match1, _, _ = sw.testObject(details.old) nocross = !match1 } match2 := fenceMatchObject(fence, details.obj) if match2 { - match2, _, _ = sw.testObject(details.id, details.obj, details.fields) + match2, _, _ = sw.testObject(details.obj) nocross = !match2 } if match1 && match2 { @@ -140,11 +143,11 @@ func fenceMatch( if details.command != "fset" { // Maybe the old object and new object create a line that crosses the fence. // Must detect for that possibility. - if !nocross && details.oldObj != nil { + if !nocross && details.old != nil { ls := geojson.NewLineString(geometry.NewLine( []geometry.Point{ - details.oldObj.Center(), - details.obj.Center(), + details.old.Geo().Center(), + details.obj.Geo().Center(), }, nil)) temp := false if fence.cmd == "within" { @@ -153,7 +156,8 @@ func fenceMatch( fence.cmd = "intersects" temp = true } - if fenceMatchObject(fence, ls) { + lso := object.New("", ls, 0, 0, field.List{}) + if fenceMatchObject(fence, lso) { detect = "cross" } if temp { @@ -185,18 +189,15 @@ func fenceMatch( } var distance float64 if fence.distance && fence.obj != nil { - distance = details.obj.Distance(fence.obj) + distance = details.obj.Geo().Distance(fence.obj) } - // TODO: fields - // sw.fmap = details.fmap + sw.fullFields = true sw.msg.OutputType = JSON sw.writeObject(ScanWriterParams{ - id: details.id, - o: details.obj, - fields: details.fields, + obj: details.obj, noTest: true, - distance: distance, + dist: distance, distOutput: fence.distance, }) @@ -215,14 +216,14 @@ func fenceMatch( var group string if detect == "enter" { - group = sw.s.groupConnect(hookName, details.key, details.id) + group = sw.s.groupConnect(hookName, details.key, details.obj.ID()) } else if detect == "cross" { - sw.s.groupDisconnect(hookName, details.key, details.id) - group = sw.s.groupConnect(hookName, details.key, details.id) + sw.s.groupDisconnect(hookName, details.key, details.obj.ID()) + group = sw.s.groupConnect(hookName, details.key, details.obj.ID()) } else { - group = sw.s.groupGet(hookName, details.key, details.id) + group = sw.s.groupGet(hookName, details.key, details.obj.ID()) if group == "" { - group = sw.s.groupConnect(hookName, details.key, details.id) + group = sw.s.groupConnect(hookName, details.key, details.obj.ID()) } } var msgs []string @@ -287,26 +288,24 @@ func extendRoamMessage( nmsg = append(nmsg, `,"scan":[`...) col, _ := sw.s.cols.Get(fence.roam.key) if col != nil { - obj, _, _, ok := col.Get(match.id) - if ok { + o := col.Get(match.id) + if o != nil { nmsg = append(nmsg, `{"id":`...) nmsg = appendJSONString(nmsg, match.id) nmsg = append(nmsg, `,"self":true,"object":`...) - nmsg = obj.AppendJSON(nmsg) + nmsg = o.Geo().AppendJSON(nmsg) nmsg = append(nmsg, '}') } pattern := match.id + fence.roam.scan - iterator := func( - oid string, o geojson.Object, fields field.List, - ) bool { - if oid == match.id { + iterator := func(o *object.Object) bool { + if o.ID() == match.id { return true } - if matched, _ := glob.Match(pattern, oid); matched { + if matched, _ := glob.Match(pattern, o.ID()); matched { nmsg = append(nmsg, `,{"id":`...) - nmsg = appendJSONString(nmsg, oid) + nmsg = appendJSONString(nmsg, o.ID()) nmsg = append(nmsg, `,"object":`...) - nmsg = o.AppendJSON(nmsg) + nmsg = o.Geo().AppendJSON(nmsg) nmsg = append(nmsg, '}') } return true @@ -345,8 +344,8 @@ func makemsg( return string(buf) } -func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool { - if obj == nil { +func fenceMatchObject(fence *liveFenceSwitches, o *object.Object) bool { + if o == nil { return false } if fence.roam.on { @@ -356,18 +355,18 @@ func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool { switch fence.cmd { case "nearby": // nearby is an INTERSECT on a Circle - return obj.Intersects(fence.obj) + return o.Geo().Intersects(fence.obj) case "within": - return obj.Within(fence.obj) + return o.Geo().Within(fence.obj) case "intersects": - return obj.Intersects(fence.obj) + return o.Geo().Intersects(fence.obj) } return false } func fenceMatchNearbys( s *Server, fence *liveFenceSwitches, - id string, obj geojson.Object, + obj *object.Object, ) (nearbys []roamMatch) { if obj == nil { return nil @@ -376,49 +375,49 @@ func fenceMatchNearbys( if col == nil { return nil } - center := obj.Center() + center := obj.Geo().Center() minLat, minLon, maxLat, maxLon := geo.RectFromCenter(center.Y, center.X, fence.roam.meters) rect := geometry.Rect{ Min: geometry.Point{X: minLon, Y: minLat}, Max: geometry.Point{X: maxLon, Y: maxLat}, } - col.Intersects(geojson.NewRect(rect), 0, nil, nil, func( - id2 string, obj2 geojson.Object, fields field.List, - ) bool { - var idMatch bool - if id2 == id { - return true // skip self - } - meters := obj.Distance(obj2) - if meters > fence.roam.meters { - return true // skip outside radius - } - if fence.roam.pattern { - idMatch, _ = glob.Match(fence.roam.id, id2) - } else { - idMatch = fence.roam.id == id2 - } - if !idMatch { - return true // skip non-id match - } - match := roamMatch{ - id: id2, - obj: obj2, - meters: obj.Distance(obj2), - } - nearbys = append(nearbys, match) - return true - }) + col.Intersects(geojson.NewRect(rect), 0, nil, nil, + func(o *object.Object) bool { + var idMatch bool + if o.ID() == obj.ID() { + return true // skip self + } + meters := o.Geo().Distance(o.Geo()) + if meters > fence.roam.meters { + return true // skip outside radius + } + if fence.roam.pattern { + idMatch, _ = glob.Match(fence.roam.id, o.ID()) + } else { + idMatch = fence.roam.id == o.ID() + } + if !idMatch { + return true // skip non-id match + } + match := roamMatch{ + id: o.ID(), + obj: o.Geo(), + meters: obj.Geo().Distance(o.Geo()), + } + nearbys = append(nearbys, match) + return true + }, + ) return nearbys } func fenceMatchRoam( s *Server, fence *liveFenceSwitches, - id string, old, obj geojson.Object, + obj, old *object.Object, ) (nearbys, faraways []roamMatch) { - oldNearbys := fenceMatchNearbys(s, fence, id, old) - newNearbys := fenceMatchNearbys(s, fence, id, obj) + oldNearbys := fenceMatchNearbys(s, fence, old) + newNearbys := fenceMatchNearbys(s, fence, obj) // Go through all matching objects in new-nearbys and old-nearbys. for i := 0; i < len(oldNearbys); i++ { var match bool @@ -444,7 +443,7 @@ func fenceMatchRoam( faraways, nearbys = oldNearbys, newNearbys // ensure the faraways distances are to the new object for i := 0; i < len(faraways); i++ { - faraways[i].meters = faraways[i].obj.Distance(obj) + faraways[i].meters = faraways[i].obj.Distance(obj.Geo()) } sortRoamMatches(faraways) sortRoamMatches(nearbys) diff --git a/internal/server/json.go b/internal/server/json.go index b9181ef9..8a137e91 100644 --- a/internal/server/json.go +++ b/internal/server/json.go @@ -12,6 +12,8 @@ import ( "github.com/tidwall/resp" "github.com/tidwall/sjson" "github.com/tidwall/tile38/internal/collection" + "github.com/tidwall/tile38/internal/field" + "github.com/tidwall/tile38/internal/object" ) func appendJSONString(b []byte, s string) []byte { @@ -191,8 +193,8 @@ func (s *Server) cmdJget(msg *Message) (resp.Value, error) { } return NOMessage, errKeyNotFound } - o, _, _, ok := col.Get(id) - if !ok { + o := col.Get(id) + if o == nil { if msg.OutputType == RESP { return resp.NullValue(), nil } @@ -200,9 +202,9 @@ func (s *Server) cmdJget(msg *Message) (resp.Value, error) { } var res gjson.Result if doget { - res = gjson.Get(o.String(), path) + res = gjson.Get(o.Geo().String(), path) } else { - res = gjson.Parse(o.String()) + res = gjson.Parse(o.Geo().String()) } var val string if raw { @@ -270,10 +272,12 @@ func (s *Server) cmdJset(msg *Message) (res resp.Value, d commandDetails, err er } var json string var geoobj bool - o, fields, _, ok := col.Get(id) - if ok { - geoobj = objIsSpatial(o) - json = o.String() + var fields field.List + o := col.Get(id) + if o != nil { + geoobj = objIsSpatial(o.Geo()) + json = o.Geo().String() + fields = o.Fields() } if raw { // set as raw block @@ -295,14 +299,15 @@ func (s *Server) cmdJset(msg *Message) (res resp.Value, d commandDetails, err er if createcol { s.cols.Set(key, col) } + var oobj geojson.Object = collection.String(json) + obj := object.New(id, oobj, 0, 0, fields) + col.Set(obj) d.key = key - d.id = id - d.obj = collection.String(json) + d.obj = obj d.timestamp = time.Now() d.updated = true - col.Set(d.id, d.obj, fields, 0) switch msg.OutputType { case JSON: var buf bytes.Buffer @@ -335,10 +340,12 @@ func (s *Server) cmdJdel(msg *Message) (res resp.Value, d commandDetails, err er var json string var geoobj bool - o, fields, _, ok := col.Get(id) - if ok { - geoobj = objIsSpatial(o) - json = o.String() + var fields field.List + o := col.Get(id) + if o != nil { + geoobj = objIsSpatial(o.Geo()) + json = o.Geo().String() + fields = o.Fields() } njson, err := sjson.Delete(json, path) if err != nil { @@ -361,12 +368,14 @@ func (s *Server) cmdJdel(msg *Message) (res resp.Value, d commandDetails, err er return s.cmdSET(&nmsg) } + var oobj geojson.Object = collection.String(json) + obj := object.New(id, oobj, 0, 0, fields) + col.Set(obj) + d.key = key - d.id = id - d.obj = collection.String(json) + d.obj = obj d.timestamp = time.Now() d.updated = true - col.Set(d.id, d.obj, fields, 0) switch msg.OutputType { case JSON: var buf bytes.Buffer diff --git a/internal/server/scan.go b/internal/server/scan.go index 02c36c0a..1eea7a1b 100644 --- a/internal/server/scan.go +++ b/internal/server/scan.go @@ -5,9 +5,8 @@ import ( "errors" "time" - "github.com/tidwall/geojson" "github.com/tidwall/resp" - "github.com/tidwall/tile38/internal/field" + "github.com/tidwall/tile38/internal/object" ) func (s *Server) cmdScanArgs(vs []string) ( @@ -70,11 +69,9 @@ func (s *Server) cmdScan(msg *Message) (res resp.Value, err error) { if limits[0] == "" && limits[1] == "" { sw.col.Scan(args.desc, sw, msg.Deadline, - func(id string, o geojson.Object, fields field.List) bool { + func(o *object.Object) bool { keepGoing, err := sw.pushObject(ScanWriterParams{ - id: id, - o: o, - fields: fields, + obj: o, }) if err != nil { ierr = err @@ -86,11 +83,9 @@ func (s *Server) cmdScan(msg *Message) (res resp.Value, err error) { } else { sw.col.ScanRange(limits[0], limits[1], args.desc, sw, msg.Deadline, - func(id string, o geojson.Object, fields field.List) bool { + func(o *object.Object) bool { keepGoing, err := sw.pushObject(ScanWriterParams{ - id: id, - o: o, - fields: fields, + obj: o, }) if err != nil { ierr = err diff --git a/internal/server/scanner.go b/internal/server/scanner.go index ef5733dd..6a348b0c 100644 --- a/internal/server/scanner.go +++ b/internal/server/scanner.go @@ -14,6 +14,7 @@ import ( "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/field" "github.com/tidwall/tile38/internal/glob" + "github.com/tidwall/tile38/internal/object" ) const limitItems = 100 @@ -60,10 +61,8 @@ type scanWriter struct { } type ScanWriterParams struct { - id string - o geojson.Object - fields field.List - distance float64 + obj *object.Object + dist float64 distOutput bool // query or fence requested distance output noTest bool ignoreGlobMatch bool @@ -201,34 +200,37 @@ func extractZCoordinate(o geojson.Object) float64 { } } -func getFieldValue(o geojson.Object, fields field.List, name string) field.Value { +func getFieldValue(o *object.Object, name string) field.Value { if name == "z" { - return field.ValueOf(strconv.FormatFloat(extractZCoordinate(o), 'f', -1, 64)) + z := extractZCoordinate(o.Geo()) + return field.ValueOf(strconv.FormatFloat(z, 'f', -1, 64)) } - f := fields.Get(name) - return f.Value() + return o.Fields().Get(name).Value() } -func (sw *scanWriter) fieldMatch(o geojson.Object, fields field.List) (bool, error) { +func (sw *scanWriter) fieldMatch(o *object.Object) (bool, error) { for _, where := range sw.wheres { - if !where.match(getFieldValue(o, fields, where.name)) { + if !where.match(getFieldValue(o, where.name)) { return false, nil } } for _, wherein := range sw.whereins { - if !wherein.match(getFieldValue(o, fields, wherein.name)) { + if !wherein.match(getFieldValue(o, wherein.name)) { return false, nil } } if len(sw.whereevals) > 0 { - fieldsWithNames := make(map[string]field.Value) - fieldsWithNames["z"] = field.ValueOf(strconv.FormatFloat(extractZCoordinate(o), 'f', -1, 64)) - fields.Scan(func(f field.Field) bool { - fieldsWithNames[f.Name()] = f.Value() + fieldNames := make(map[string]field.Value) + if objIsSpatial(o.Geo()) { + z := extractZCoordinate(o.Geo()) + fieldNames["z"] = field.ValueOf(strconv.FormatFloat(z, 'f', -1, 64)) + } + o.Fields().Scan(func(f field.Field) bool { + fieldNames[f.Name()] = f.Value() return true }) for _, whereval := range sw.whereevals { - match, err := whereval.match(fieldsWithNames) + match, err := whereval.match(fieldNames) if err != nil { return false, err } @@ -240,7 +242,7 @@ func (sw *scanWriter) fieldMatch(o geojson.Object, fields field.List) (bool, err return true, nil } -func (sw *scanWriter) globMatch(id string, o geojson.Object) (ok, keepGoing bool) { +func (sw *scanWriter) globMatch(o *object.Object) (ok, keepGoing bool) { if sw.globEverything { return true, true } @@ -248,7 +250,7 @@ func (sw *scanWriter) globMatch(id string, o geojson.Object) (ok, keepGoing bool if sw.matchValues { val = o.String() } else { - val = id + val = o.ID() } for _, pattern := range sw.globs { ok, _ := glob.Match(pattern, val) @@ -270,13 +272,13 @@ 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 field.List, +func (sw *scanWriter) testObject(o *object.Object, ) (ok, keepGoing bool, err error) { - match, kg := sw.globMatch(id, o) + match, kg := sw.globMatch(o) if !match { return false, kg, nil } - ok, err = sw.fieldMatch(o, fields) + ok, err = sw.fieldMatch(o) if err != nil { return false, false, err } @@ -288,7 +290,7 @@ func (sw *scanWriter) pushObject(opts ScanWriterParams) (keepGoing bool, err err if !opts.noTest { var ok bool var err error - ok, keepGoing, err = sw.testObject(opts.id, opts.o, opts.fields) + ok, keepGoing, err = sw.testObject(opts.obj) if err != nil { return false, err } @@ -301,10 +303,17 @@ func (sw *scanWriter) pushObject(opts ScanWriterParams) (keepGoing bool, err err return sw.count < sw.limit, nil } if opts.clip != nil { - opts.o = clip.Clip(opts.o, opts.clip, &sw.s.geomIndexOpts) + // create a newly clipped object + opts.obj = object.New( + opts.obj.ID(), + clip.Clip(opts.obj.Geo(), opts.clip, &sw.s.geomIndexOpts), + 0, opts.obj.Expires(), + opts.obj.Fields(), + ) } + if !sw.fullFields { - opts.fields.Scan(func(f field.Field) bool { + opts.obj.Fields().Scan(func(f field.Field) bool { sw.fkeys.Insert(f.Name()) return true }) @@ -339,10 +348,10 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) { } fieldsOutput := sw.hasFieldsOutput() if fieldsOutput && sw.fullFields { - if opts.fields.Len() > 0 { + if opts.obj.Fields().Len() > 0 { jsfields = `,"fields":{` var i int - opts.fields.Scan(func(f field.Field) bool { + opts.obj.Fields().Scan(func(f field.Field) bool { if !f.Value().IsZero() { if i > 0 { jsfields += `,` @@ -361,7 +370,7 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) { if i > 0 { jsfields += `,` } - f := opts.fields.Get(name) + f := opts.obj.Fields().Get(name) jsfields += f.Value().JSON() i++ return true @@ -369,29 +378,29 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) { jsfields += `]` } if sw.output == outputIDs { - if opts.distOutput || opts.distance > 0 { - wr.WriteString(`{"id":` + jsonString(opts.id) + - `,"distance":` + strconv.FormatFloat(opts.distance, 'f', -1, 64) + "}") + if opts.distOutput || opts.dist > 0 { + wr.WriteString(`{"id":` + jsonString(opts.obj.ID()) + + `,"distance":` + strconv.FormatFloat(opts.dist, 'f', -1, 64) + "}") } else { - wr.WriteString(jsonString(opts.id)) + wr.WriteString(jsonString(opts.obj.ID())) } } else { - wr.WriteString(`{"id":` + jsonString(opts.id)) + wr.WriteString(`{"id":` + jsonString(opts.obj.ID())) switch sw.output { case outputObjects: - wr.WriteString(`,"object":` + string(opts.o.AppendJSON(nil))) + wr.WriteString(`,"object":` + string(opts.obj.Geo().AppendJSON(nil))) case outputPoints: - wr.WriteString(`,"point":` + string(appendJSONSimplePoint(nil, opts.o))) + wr.WriteString(`,"point":` + string(appendJSONSimplePoint(nil, opts.obj.Geo()))) case outputHashes: - center := opts.o.Center() + center := opts.obj.Geo().Center() p := geohash.EncodeWithPrecision(center.Y, center.X, uint(sw.precision)) wr.WriteString(`,"hash":"` + p + `"`) case outputBounds: - wr.WriteString(`,"bounds":` + string(appendJSONSimpleBounds(nil, opts.o))) + wr.WriteString(`,"bounds":` + string(appendJSONSimpleBounds(nil, opts.obj.Geo()))) } wr.WriteString(jsfields) - if opts.distOutput || opts.distance > 0 { - wr.WriteString(`,"distance":` + strconv.FormatFloat(opts.distance, 'f', -1, 64)) + if opts.distOutput || opts.dist > 0 { + wr.WriteString(`,"distance":` + strconv.FormatFloat(opts.dist, 'f', -1, 64)) } wr.WriteString(`}`) @@ -399,10 +408,10 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) { sw.wr.Write(wr.Bytes()) case RESP: vals := make([]resp.Value, 1, 3) - vals[0] = resp.StringValue(opts.id) + vals[0] = resp.StringValue(opts.obj.ID()) if sw.output == outputIDs { - if opts.distOutput || opts.distance > 0 { - vals = append(vals, resp.FloatValue(opts.distance)) + if opts.distOutput || opts.dist > 0 { + vals = append(vals, resp.FloatValue(opts.dist)) sw.values = append(sw.values, resp.ArrayValue(vals)) } else { sw.values = append(sw.values, vals[0]) @@ -410,10 +419,10 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) { } else { switch sw.output { case outputObjects: - vals = append(vals, resp.StringValue(opts.o.String())) + vals = append(vals, resp.StringValue(opts.obj.String())) case outputPoints: - point := opts.o.Center() - z := extractZCoordinate(opts.o) + point := opts.obj.Geo().Center() + z := extractZCoordinate(opts.obj.Geo()) if z != 0 { vals = append(vals, resp.ArrayValue([]resp.Value{ resp.FloatValue(point.Y), @@ -427,11 +436,11 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) { })) } case outputHashes: - center := opts.o.Center() + center := opts.obj.Geo().Center() p := geohash.EncodeWithPrecision(center.Y, center.X, uint(sw.precision)) vals = append(vals, resp.StringValue(p)) case outputBounds: - bbox := opts.o.Rect() + bbox := opts.obj.Rect() vals = append(vals, resp.ArrayValue([]resp.Value{ resp.ArrayValue([]resp.Value{ resp.FloatValue(bbox.Min.Y), @@ -444,23 +453,22 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) { })) } if sw.hasFieldsOutput() { - if opts.fields.Len() > 0 { - var fvals []resp.Value - var i int - opts.fields.Scan(func(f field.Field) bool { - if !f.Value().IsZero() { - fvals = append(fvals, resp.StringValue(f.Name()), resp.StringValue(f.Value().Data())) - i++ - } - return true - }) + var fvals []resp.Value + var i int + opts.obj.Fields().Scan(func(f field.Field) bool { + if !f.Value().IsZero() { + fvals = append(fvals, resp.StringValue(f.Name()), resp.StringValue(f.Value().Data())) + i++ + } + return true + }) + if len(fvals) > 0 { vals = append(vals, resp.ArrayValue(fvals)) } } - if opts.distOutput || opts.distance > 0 { - vals = append(vals, resp.FloatValue(opts.distance)) + if opts.distOutput || opts.dist > 0 { + vals = append(vals, resp.FloatValue(opts.dist)) } - sw.values = append(sw.values, resp.ArrayValue(vals)) } } diff --git a/internal/server/scanner_test.go b/internal/server/scanner_test.go index aecb51f0..9cc47f9d 100644 --- a/internal/server/scanner_test.go +++ b/internal/server/scanner_test.go @@ -10,6 +10,7 @@ import ( "github.com/tidwall/geojson" "github.com/tidwall/geojson/geometry" "github.com/tidwall/tile38/internal/field" + "github.com/tidwall/tile38/internal/object" ) type testPointItem struct { @@ -47,7 +48,7 @@ func BenchmarkFieldMatch(t *testing.B) { for i := 0; i < t.N; i++ { // one call is super fast, measurements are not reliable, let's do 100 for ix := 0; ix < 100; ix++ { - sw.fieldMatch(items[i].object, items[i].fields) + sw.fieldMatch(object.New("", items[i].object, 0, 0, items[i].fields)) } } } diff --git a/internal/server/search.go b/internal/server/search.go index 4af4d774..0fc6a23d 100644 --- a/internal/server/search.go +++ b/internal/server/search.go @@ -16,8 +16,8 @@ import ( "github.com/tidwall/tile38/internal/bing" "github.com/tidwall/tile38/internal/buffer" "github.com/tidwall/tile38/internal/clip" - "github.com/tidwall/tile38/internal/field" "github.com/tidwall/tile38/internal/glob" + "github.com/tidwall/tile38/internal/object" ) const defaultCircleSteps = 64 @@ -376,11 +376,12 @@ func (s *Server) cmdSearchArgs( err = errKeyNotFound return } - lfs.obj, _, _, ok = col.Get(id) - if !ok { + o := col.Get(id) + if o == nil { err = errIDNotFound return } + lfs.obj = o.Geo() case "roam": lfs.roam.on = true if vs, lfs.roam.key, ok = tokenval(vs); !ok || lfs.roam.key == "" { @@ -499,12 +500,10 @@ func (s *Server) cmdNearby(msg *Message) (res resp.Value, err error) { } var ierr error if sw.col != nil { - iterStep := func(id string, o geojson.Object, fields field.List, meters float64) bool { + iterStep := func(o *object.Object, dist float64) bool { keepGoing, err := sw.pushObject(ScanWriterParams{ - id: id, - o: o, - fields: fields, - distance: meters, + obj: o, + dist: dist, distOutput: sargs.distance, ignoreGlobMatch: true, skipTesting: true, @@ -523,16 +522,16 @@ func (s *Server) cmdNearby(msg *Message) (res resp.Value, err error) { errors.New("cannot use SPARSE without a point distance") } // An intersects operation is required for SPARSE - iter := func(id string, o geojson.Object, fields field.List) bool { - var meters float64 + iter := func(o *object.Object) bool { + var dist float64 if sargs.distance { - meters = o.Distance(sargs.obj) + dist = o.Geo().Distance(sargs.obj) } - return iterStep(id, o, fields, meters) + return iterStep(o, dist) } sw.col.Intersects(sargs.obj, sargs.sparse, sw, msg.Deadline, iter) } else { - iter := func(id string, o geojson.Object, fields field.List, dist float64) bool { + iter := func(o *object.Object, dist float64) bool { if maxDist > 0 && dist > maxDist { return false } @@ -540,7 +539,7 @@ func (s *Server) cmdNearby(msg *Message) (res resp.Value, err error) { if sargs.distance { meters = dist } - return iterStep(id, o, fields, meters) + return iterStep(o, meters) } sw.col.Nearby(sargs.obj, sw, msg.Deadline, iter) } @@ -599,41 +598,31 @@ func (s *Server) cmdWITHINorINTERSECTS(cmd string, msg *Message) (res resp.Value var ierr error if sw.col != nil { if cmd == "within" { - sw.col.Within(sargs.obj, sargs.sparse, sw, msg.Deadline, func( - id string, o geojson.Object, fields field.List, - ) bool { - keepGoing, err := sw.pushObject(ScanWriterParams{ - id: id, - o: o, - fields: fields, - }) - if err != nil { - ierr = err - return false - } - return keepGoing - }) + sw.col.Within(sargs.obj, sargs.sparse, sw, msg.Deadline, + func(o *object.Object) bool { + keepGoing, err := sw.pushObject(ScanWriterParams{obj: o}) + if err != nil { + ierr = err + return false + } + return keepGoing + }, + ) } else if cmd == "intersects" { - sw.col.Intersects(sargs.obj, sargs.sparse, sw, msg.Deadline, func( - id string, - o geojson.Object, - fields field.List, - ) bool { - params := ScanWriterParams{ - id: id, - o: o, - fields: fields, - } - if sargs.clip { - params.clip = sargs.obj - } - keepGoing, err := sw.pushObject(params) - if err != nil { - ierr = err - return false - } - return keepGoing - }) + sw.col.Intersects(sargs.obj, sargs.sparse, sw, msg.Deadline, + func(o *object.Object) bool { + params := ScanWriterParams{obj: o} + if sargs.clip { + params.clip = sargs.obj + } + keepGoing, err := sw.pushObject(params) + if err != nil { + ierr = err + return false + } + return keepGoing + }, + ) } } if ierr != nil { @@ -732,11 +721,9 @@ func (s *Server) cmdSearch(msg *Message) (res resp.Value, err error) { limits := multiGlobParse(sw.globs, sargs.desc) if limits[0] == "" && limits[1] == "" { sw.col.SearchValues(sargs.desc, sw, msg.Deadline, - func(id string, o geojson.Object, fields field.List) bool { + func(o *object.Object) bool { keepGoing, err := sw.pushObject(ScanWriterParams{ - id: id, - o: o, - fields: fields, + obj: o, }) if err != nil { ierr = err @@ -750,11 +737,9 @@ func (s *Server) cmdSearch(msg *Message) (res resp.Value, err error) { // globSingle is only for ID matches, not values. sw.col.SearchValuesRange(limits[0], limits[1], sargs.desc, sw, msg.Deadline, - func(id string, o geojson.Object, fields field.List) bool { + func(o *object.Object) bool { keepGoing, err := sw.pushObject(ScanWriterParams{ - id: id, - o: o, - fields: fields, + obj: o, }) if err != nil { ierr = err diff --git a/internal/server/server.go b/internal/server/server.go index b7239248..66a116a1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -36,8 +36,8 @@ import ( "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/deadline" "github.com/tidwall/tile38/internal/endpoint" - "github.com/tidwall/tile38/internal/field" "github.com/tidwall/tile38/internal/log" + "github.com/tidwall/tile38/internal/object" ) var errOOM = errors.New("OOM command not allowed when used memory > 'maxmemory'") @@ -55,14 +55,11 @@ const ( // for geofence formulas. type commandDetails struct { command string // client command, like "SET" or "DEL" - key, id string // collection key and object id of object + key string // collection key newKey string // new key, for RENAME command - obj geojson.Object // new object - fields field.List // array of field values - - oldObj geojson.Object // previous object, if any - oldFields field.List // previous object field values + obj *object.Object // target object + old *object.Object // previous object, if any updated bool // object was updated timestamp time.Time // timestamp when the update occured diff --git a/internal/server/test.go b/internal/server/test.go index 1c9c230f..53e0d889 100644 --- a/internal/server/test.go +++ b/internal/server/test.go @@ -278,11 +278,12 @@ func (s *Server) parseArea(ovs []string, doClip bool) (vs []string, o geojson.Ob err = errKeyNotFound return } - o, _, _, ok = col.Get(id) - if !ok { + obj := col.Get(id) + if obj == nil { err = errIDNotFound return } + o = obj.Geo() } return } diff --git a/tests/keys_search_test.go b/tests/keys_search_test.go index b368a59d..e68b9236 100644 --- a/tests/keys_search_test.go +++ b/tests/keys_search_test.go @@ -46,7 +46,7 @@ func keys_KNN_basic_test(mc *mockServer) error { {"NEARBY", "mykey", "LIMIT", 10, "POINTS", "POINT", 20, 20}, { "[0 [[2 [19 19]] [3 [12 19]] [5 [33 21]] [1 [5 5]] [4 [-5 5]] [6 [52 13]]]]"}, {"NEARBY", "mykey", "LIMIT", 10, "IDS", "POINT", 20, 20, 4000000}, {"[0 [2 3 5 1 4 6]]"}, - {"NEARBY", "mykey", "LIMIT", 10, "DISTANCE", "IDS", "POINT", 20, 20, 1500000}, {"[0 [[2 152808.671875] [3 895945.125] [5 1448929.625]]]"}, + {"NEARBY", "mykey", "LIMIT", 10, "DISTANCE", "IDS", "POINT", 20, 20, 1500000}, {"[0 [[2 152808.67164037024] [3 895945.1409106688] [5 1448929.5916252395]]]"}, {"NEARBY", "mykey", "LIMIT", 10, "DISTANCE", "POINT", 52, 13, 100}, {`[0 [[6 {"type":"Point","coordinates":[13,52]} 0]]]`}, {"NEARBY", "mykey", "LIMIT", 10, "POINT", 52.1, 13.1, 100000}, {`[0 [[6 {"type":"Point","coordinates":[13,52]}]]]`}, {"OUTPUT", "json"}, {func(res string) bool { return gjson.Get(res, "ok").Bool() }},