mirror of https://github.com/tidwall/tile38.git
Add field path queries for json and comparison operators
It's now possible to query a JSON field using a GJSON path. SET fleet truck1 FIELD props '{"speed":58,"name":"Andy"}' POINT 33 -112 You can then use the GJSON type path to return the objects that match the WHERE. SCAN fleet WHERE props.speed 50 inf SCAN fleet WHERE props.name Andy Andy Included in this commit is support for '==', '<', '>', '<=', '>=', and '!='. The previous queries could be written like: SCAN fleet WHERE props.speed > 50 SCAN fleet WHERE props.name == Andy
This commit is contained in:
parent
6b310cebb5
commit
cf7f49fd9b
|
@ -34,7 +34,7 @@ func (v Value) IsZero() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v Value) Equals(b Value) bool {
|
func (v Value) Equals(b Value) bool {
|
||||||
return v.kind == b.kind && v.data == b.data
|
return !v.Less(b) && !b.Less(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v Value) Kind() Kind {
|
func (v Value) Kind() Kind {
|
||||||
|
|
|
@ -3,8 +3,11 @@ package field
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tidwall/pretty"
|
||||||
"github.com/tidwall/tile38/internal/sstring"
|
"github.com/tidwall/tile38/internal/sstring"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -250,6 +253,15 @@ func putfield(b []byte, f Field, s, e int) *byte {
|
||||||
|
|
||||||
// Get a field from the list. Or returns ZeroField if not found.
|
// Get a field from the list. Or returns ZeroField if not found.
|
||||||
func (fields List) Get(name string) Field {
|
func (fields List) Get(name string) Field {
|
||||||
|
var isj bool
|
||||||
|
var jname string
|
||||||
|
var jpath string
|
||||||
|
dot := strings.IndexByte(name, '.')
|
||||||
|
if dot != -1 {
|
||||||
|
isj = true
|
||||||
|
jname = name[:dot]
|
||||||
|
jpath = name[dot+1:]
|
||||||
|
}
|
||||||
b := ptob(fields.p)
|
b := ptob(fields.p)
|
||||||
var i int
|
var i int
|
||||||
for {
|
for {
|
||||||
|
@ -274,11 +286,23 @@ func (fields List) Get(name string) Field {
|
||||||
data = btoa(b[i+n : i+n+x])
|
data = btoa(b[i+n : i+n+x])
|
||||||
i += n + x
|
i += n + x
|
||||||
}
|
}
|
||||||
if name < fname {
|
if kind == JSON && isj {
|
||||||
break
|
if jname < fname {
|
||||||
}
|
break
|
||||||
if fname == name {
|
}
|
||||||
return bfield(fname, kind, data)
|
if fname == jname {
|
||||||
|
res := gjson.Get(data, jpath)
|
||||||
|
if res.Exists() {
|
||||||
|
return bfield(name, Kind(res.Type), res.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if name < fname {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if fname == name {
|
||||||
|
return bfield(name, kind, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ZeroField
|
return ZeroField
|
||||||
|
@ -360,3 +384,21 @@ func MakeList(fields []Field) List {
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fields List) String() string {
|
||||||
|
var dst []byte
|
||||||
|
dst = append(dst, '{')
|
||||||
|
var i int
|
||||||
|
fields.Scan(func(f Field) bool {
|
||||||
|
if i > 0 {
|
||||||
|
dst = append(dst, ',')
|
||||||
|
}
|
||||||
|
dst = gjson.AppendJSONString(dst, f.Name())
|
||||||
|
dst = append(dst, ':')
|
||||||
|
dst = append(dst, f.Value().JSON()...)
|
||||||
|
i++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
dst = append(dst, '}')
|
||||||
|
return string(pretty.UglyInPlace(dst))
|
||||||
|
}
|
||||||
|
|
|
@ -179,3 +179,54 @@ func TestRandom(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJSONGet(t *testing.T) {
|
||||||
|
|
||||||
|
var list List
|
||||||
|
list = list.Set(Make("hello", "world"))
|
||||||
|
list = list.Set(Make("hello", `"world"`))
|
||||||
|
list = list.Set(Make("jello", "planet"))
|
||||||
|
list = list.Set(Make("telly", `{"a":[1,2,3],"b":null,"c":true,"d":false}`))
|
||||||
|
list = list.Set(Make("belly", `{"a":{"b":{"c":"fancy"}}}`))
|
||||||
|
json := list.String()
|
||||||
|
exp := `{"belly":{"a":{"b":{"c":"fancy"}}},"hello":"world","jello":` +
|
||||||
|
`"planet","telly":{"a":[1,2,3],"b":null,"c":true,"d":false}}`
|
||||||
|
if json != exp {
|
||||||
|
t.Fatalf("expected '%s', got '%s'", exp, json)
|
||||||
|
}
|
||||||
|
data := list.Get("hello").Value().Data()
|
||||||
|
if data != "world" {
|
||||||
|
t.Fatalf("expected '%s', got '%s'", "world", data)
|
||||||
|
}
|
||||||
|
data = list.Get("telly").Value().Data()
|
||||||
|
if data != `{"a":[1,2,3],"b":null,"c":true,"d":false}` {
|
||||||
|
t.Fatalf("expected '%s', got '%s'",
|
||||||
|
`{"a":[1,2,3],"b":null,"c":true,"d":false}`, data)
|
||||||
|
}
|
||||||
|
data = list.Get("belly").Value().Data()
|
||||||
|
if data != `{"a":{"b":{"c":"fancy"}}}` {
|
||||||
|
t.Fatalf("expected '%s', got '%s'",
|
||||||
|
`{"a":{"b":{"c":"fancy"}}}`, data)
|
||||||
|
}
|
||||||
|
data = list.Get("belly.a").Value().Data()
|
||||||
|
if data != `{"b":{"c":"fancy"}}` {
|
||||||
|
t.Fatalf("expected '%s', got '%s'",
|
||||||
|
`{"b":{"c":"fancy"}}`, data)
|
||||||
|
}
|
||||||
|
data = list.Get("belly.a.b").Value().Data()
|
||||||
|
if data != `{"c":"fancy"}` {
|
||||||
|
t.Fatalf("expected '%s', got '%s'",
|
||||||
|
`{"c":"fancy"}`, data)
|
||||||
|
}
|
||||||
|
data = list.Get("belly.a.b.c").Value().Data()
|
||||||
|
if data != `fancy` {
|
||||||
|
t.Fatalf("expected '%s', got '%s'",
|
||||||
|
`fancy`, data)
|
||||||
|
}
|
||||||
|
// Tile38 defaults non-existent fields to zero.
|
||||||
|
data = list.Get("belly.a.b.c.d").Value().Data()
|
||||||
|
if data != `0` {
|
||||||
|
t.Fatalf("expected '%s', got '%s'",
|
||||||
|
`0`, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -77,6 +77,20 @@ func mGTE(a, b field.Value) bool { return !mLT(a, b) }
|
||||||
func mEQ(a, b field.Value) bool { return a.Equals(b) }
|
func mEQ(a, b field.Value) bool { return a.Equals(b) }
|
||||||
|
|
||||||
func (where whereT) match(value field.Value) bool {
|
func (where whereT) match(value field.Value) bool {
|
||||||
|
switch where.min.Data() {
|
||||||
|
case "<":
|
||||||
|
return mLT(value, where.max)
|
||||||
|
case "<=":
|
||||||
|
return mLTE(value, where.max)
|
||||||
|
case ">":
|
||||||
|
return mGT(value, where.max)
|
||||||
|
case ">=":
|
||||||
|
return mGTE(value, where.max)
|
||||||
|
case "==":
|
||||||
|
return mEQ(value, where.max)
|
||||||
|
case "!=":
|
||||||
|
return !mEQ(value, where.max)
|
||||||
|
}
|
||||||
if !where.minx {
|
if !where.minx {
|
||||||
if mLT(value, where.min) { // if value < where.min {
|
if mLT(value, where.min) { // if value < where.min {
|
||||||
return false
|
return false
|
||||||
|
@ -276,18 +290,17 @@ func (s *Server) parseSearchScanBaseTokens(
|
||||||
}
|
}
|
||||||
var minx, maxx bool
|
var minx, maxx bool
|
||||||
smin = strings.ToLower(smin)
|
smin = strings.ToLower(smin)
|
||||||
if smin == "-inf" {
|
smax = strings.ToLower(smax)
|
||||||
smin = "-inf"
|
if smax == "+inf" || smax == "inf" {
|
||||||
} else {
|
smax = "inf"
|
||||||
|
}
|
||||||
|
switch smin {
|
||||||
|
case "<", "<=", ">", ">=", "==", "!=":
|
||||||
|
default:
|
||||||
if strings.HasPrefix(smin, "(") {
|
if strings.HasPrefix(smin, "(") {
|
||||||
minx = true
|
minx = true
|
||||||
smin = smin[1:]
|
smin = smin[1:]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
smax = strings.ToLower(smax)
|
|
||||||
if smax == "+inf" || smax == "inf" {
|
|
||||||
smax = "inf"
|
|
||||||
} else {
|
|
||||||
if strings.HasPrefix(smax, "(") {
|
if strings.HasPrefix(smax, "(") {
|
||||||
maxx = true
|
maxx = true
|
||||||
smax = smax[1:]
|
smax = smax[1:]
|
||||||
|
|
|
@ -459,6 +459,22 @@ func keys_FIELDS_test(mc *mockServer) error {
|
||||||
Do("GET", "fleet", "truck1", "WITHFIELDS").JSON().Str(`{"ok":true,"object":{"type":"Point","coordinates":[33,-112]}}`),
|
Do("GET", "fleet", "truck1", "WITHFIELDS").JSON().Str(`{"ok":true,"object":{"type":"Point","coordinates":[33,-112]}}`),
|
||||||
Do("SET", "fleet", "truck1", "FIELD", "speed", "2", "POINT", "-112", "33").JSON().OK(),
|
Do("SET", "fleet", "truck1", "FIELD", "speed", "2", "POINT", "-112", "33").JSON().OK(),
|
||||||
Do("GET", "fleet", "truck1", "WITHFIELDS").JSON().Str(`{"ok":true,"object":{"type":"Point","coordinates":[33,-112]},"fields":{"speed":2}}`),
|
Do("GET", "fleet", "truck1", "WITHFIELDS").JSON().Str(`{"ok":true,"object":{"type":"Point","coordinates":[33,-112]},"fields":{"speed":2}}`),
|
||||||
|
|
||||||
|
// Do some GJSON queries.
|
||||||
|
Do("SET", "fleet", "truck2", "FIELD", "hello", `{"world":"tom"}`, "POINT", "-112", "33").JSON().OK(),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello", `{"world":"tom"}`, `{"world":"tom"}`, "COUNT").JSON().Str(`{"ok":true,"count":1,"cursor":0}`),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world", `tom`, `tom`, "COUNT").JSON().Str(`{"ok":true,"count":1,"cursor":0}`),
|
||||||
|
// The next scan does not match on anything, but since we're matching
|
||||||
|
// on zeros, which is the default, then all (two) objects are returned.
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world.1", `0`, `0`, "IDS").JSON().Str(`{"ok":true,"ids":["truck1","truck2"],"count":2,"cursor":0}`),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world", ">", `tom`, "IDS").JSON().Str(`{"ok":true,"ids":[],"count":0,"cursor":0}`),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world", ">=", `Tom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck2"],"count":1,"cursor":0}`),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world", ">=", `tom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck2"],"count":1,"cursor":0}`),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world", "==", `tom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck2"],"count":1,"cursor":0}`),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world", "<", `tom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck1"],"count":1,"cursor":0}`),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world", "<=", `tom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck1","truck2"],"count":2,"cursor":0}`),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world", "<", `uom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck1","truck2"],"count":2,"cursor":0}`),
|
||||||
|
Do("SCAN", "fleet", "WHERE", "hello.world", "!=", `tom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck1"],"count":1,"cursor":0}`),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue