Immutable Object type

This commit is contained in:
tidwall 2022-09-20 14:20:53 -07:00
parent ba9a767988
commit 2c643996e7
22 changed files with 819 additions and 912 deletions

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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)
}

View File

@ -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
})

View File

@ -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],

View File

@ -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})))
}

96
internal/object/object.go Normal file
View File

@ -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,
}
}

View File

@ -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")
}

View File

@ -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},

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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))
}
}

View File

@ -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))
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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() }},