tile38/internal/field/field.go
tidwall cf7f49fd9b 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
2022-10-20 11:17:01 -07:00

233 lines
4.7 KiB
Go

package field
import (
"math"
"strconv"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/pretty"
)
var ZeroValue = Value{kind: Number, data: "0", num: 0}
var ZeroField = Field{name: "", value: ZeroValue}
type Kind byte
const (
Null = Kind(gjson.Null)
False = Kind(gjson.False)
Number = Kind(gjson.Number)
String = Kind(gjson.String)
True = Kind(gjson.True)
JSON = Kind(gjson.JSON)
)
type Value struct {
kind Kind
data string
num float64
}
func (v Value) IsZero() bool {
return (v.kind == Number && v.data == "0" && v.num == 0) || v == (Value{})
}
func (v Value) Equals(b Value) bool {
return !v.Less(b) && !b.Less(v)
}
func (v Value) Kind() Kind {
return v.kind
}
func (v Value) Data() string {
return v.data
}
func (v Value) Num() float64 {
return v.num
}
func (v Value) JSON() string {
switch v.Kind() {
case Number:
switch v.Data() {
case "NaN":
return `"NaN"`
case "+Inf":
return `"+Inf"`
case "-Inf":
return `"-Inf"`
default:
return v.Data()
}
case String:
return string(gjson.AppendJSONString(nil, v.Data()))
case True:
return "true"
case False:
return "false"
case Null:
if v != (Value{}) {
return "null"
}
case JSON:
return v.Data()
}
return "0"
}
func stringLessInsensitive(a, b string) bool {
for i := 0; i < len(a) && i < len(b); i++ {
if a[i] >= 'A' && a[i] <= 'Z' {
if b[i] >= 'A' && b[i] <= 'Z' {
// both are uppercase, do nothing
if a[i] < b[i] {
return true
} else if a[i] > b[i] {
return false
}
} else {
// a is uppercase, convert a to lowercase
if a[i]+32 < b[i] {
return true
} else if a[i]+32 > b[i] {
return false
}
}
} else if b[i] >= 'A' && b[i] <= 'Z' {
// b is uppercase, convert b to lowercase
if a[i] < b[i]+32 {
return true
} else if a[i] > b[i]+32 {
return false
}
} else {
// neither are uppercase
if a[i] < b[i] {
return true
} else if a[i] > b[i] {
return false
}
}
}
return len(a) < len(b)
}
// Less return true if a value is less than another value.
// The caseSensitive paramater is used when the value are Strings.
// The order when comparing two different kinds is:
//
// Null < False < Number < String < True < JSON
//
// Pulled from github.com/tidwall/gjson
func (v Value) LessCase(b Value, caseSensitive bool) bool {
if v.kind < b.kind {
return true
}
if v.kind > b.kind {
return false
}
if v.kind == Number {
return v.num < b.num
}
if v.kind == String {
if caseSensitive {
return v.data < b.data
}
return stringLessInsensitive(v.data, b.data)
}
return v.data < b.data
}
// Less return true if a value is less than another value.
//
// Null < False < Number < String < True < JSON
//
// Pulled from github.com/tidwall/gjson
func (v Value) Less(b Value) bool {
return v.LessCase(b, false)
}
type Field struct {
name string
value Value
}
func (f Field) Name() string {
return f.name
}
func (f Field) Value() Value {
return f.value
}
func (f Field) Weight() int {
return len(f.name) + 8 + len(f.value.data)
}
var nan = math.NaN()
var pinf = math.Inf(+1)
var ninf = math.Inf(-1)
func ValueOf(data string) Value {
data = strings.TrimSpace(data)
num, err := strconv.ParseFloat(data, 64)
if err == nil {
if math.IsInf(num, 0) {
if math.IsInf(num, +1) {
return Value{kind: Number, data: "+Inf", num: pinf}
} else {
return Value{kind: Number, data: "-Inf", num: ninf}
}
} else if math.IsNaN(num) {
return Value{kind: Number, data: "NaN", num: nan}
}
return Value{kind: Number, data: data, num: num}
}
if gjson.Valid(data) {
data = strings.TrimSpace(data)
r := gjson.Parse(data)
switch r.Type {
case gjson.Null:
return Value{kind: Null, data: "null"}
case gjson.JSON:
return Value{kind: JSON, data: string(pretty.Ugly([]byte(data)))}
case gjson.True:
return Value{kind: True, data: "true"}
case gjson.False:
return Value{kind: False, data: "false"}
case gjson.Number:
// Ignore. Numbers will always be picked up by the ParseFloat above.
case gjson.String:
// Ignore. Strings fallthrough by default
}
// Extract String from JSON
data = r.String()
}
// Check if string is NaN, Inf(inity), +Inf(inity), -Inf(inity)
if len(data) >= 3 && len(data) <= 9 {
switch data[0] {
case '-', '+', 'I', 'i', 'N', 'n':
switch strings.ToLower(data) {
case "nan":
return Value{kind: Number, data: "NaN", num: nan}
case "inf", "+inf", "infinity", "+infinity":
return Value{kind: Number, data: "+Inf", num: pinf}
case "-inf", "-infinity":
return Value{kind: Number, data: "-Inf", num: ninf}
}
}
}
return Value{kind: String, data: data}
}
func Make(name, data string) Field {
return Field{
strings.ToLower(strings.TrimSpace(name)),
ValueOf(data),
}
}