mirror of
https://github.com/minio/minio.git
synced 2025-01-11 23:13:23 -05:00
select: add MISSING operator support (#14406)
Probably not full support, but for regular checks it should work. Fixes #14358
This commit is contained in:
parent
e43cc316ff
commit
88fd1cba71
@ -59,6 +59,7 @@ func (r *Record) Get(name string) (*sql.Value, error) {
|
|||||||
}
|
}
|
||||||
return sql.FromBytes([]byte(r.csvRecord[idx])), nil
|
return sql.FromBytes([]byte(r.csvRecord[idx])), nil
|
||||||
}
|
}
|
||||||
|
// TODO: Return Missing?
|
||||||
return nil, fmt.Errorf("column %v not found", name)
|
return nil, fmt.Errorf("column %v not found", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,6 +89,8 @@ func (r *Record) Set(name string, value *sql.Value) (sql.Record, error) {
|
|||||||
v = s
|
v = s
|
||||||
} else if value.IsNull() {
|
} else if value.IsNull() {
|
||||||
v = nil
|
v = nil
|
||||||
|
} else if value.IsMissing() {
|
||||||
|
return r, nil
|
||||||
} else if b, ok := value.ToBytes(); ok {
|
} else if b, ok := value.ToBytes(); ok {
|
||||||
// This can either be raw json or a CSV value.
|
// This can either be raw json or a CSV value.
|
||||||
// Only treat objects and arrays as JSON.
|
// Only treat objects and arrays as JSON.
|
||||||
|
@ -479,6 +479,89 @@ func TestJSONQueries(t *testing.T) {
|
|||||||
query: `select * from s3object[*] as s where s.request.header['User-Agent'] is not null`,
|
query: `select * from s3object[*] as s where s.request.header['User-Agent'] is not null`,
|
||||||
wantResult: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}`,
|
wantResult: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}`,
|
||||||
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-not-missing",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header['User-Agent'] is not missing`,
|
||||||
|
wantResult: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}`,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-not-missing-2",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header is not missing`,
|
||||||
|
wantResult: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-not-missing-null",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header is not missing`,
|
||||||
|
wantResult: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":null}}`,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":null}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-not-missing",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header['User-Agent'] is not missing`,
|
||||||
|
wantResult: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}`,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-not-missing-2",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header is not missing`,
|
||||||
|
wantResult: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-not-missing-null",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header is not missing`,
|
||||||
|
wantResult: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":null}}`,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":null}}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "is-missing",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header['User-Agent'] is missing`,
|
||||||
|
wantResult: `{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-missing-2",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header is missing`,
|
||||||
|
wantResult: ``,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-missing",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header['User-Agent'] = missing`,
|
||||||
|
wantResult: `{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-missing-null",
|
||||||
|
query: `select * from s3object[*] as s where s.request.header is missing`,
|
||||||
|
wantResult: ``,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
|
{"request":{"uri":"/2","header":null}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is-missing-select",
|
||||||
|
query: `select s.request.header['User-Agent'] as h from s3object[*] as s`,
|
||||||
|
wantResult: `{"h":"test"}
|
||||||
|
{}`,
|
||||||
|
withJSON: `{"request":{"uri":"/1","header":{"User-Agent":"test"}}}
|
||||||
{"request":{"uri":"/2","header":{}}}`,
|
{"request":{"uri":"/2","header":{}}}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -449,6 +449,8 @@ func jsonToValue(result interface{}) (*Value, error) {
|
|||||||
return FromArray(rval), nil
|
return FromArray(rval), nil
|
||||||
case nil:
|
case nil:
|
||||||
return FromNull(), nil
|
return FromNull(), nil
|
||||||
|
case Missing:
|
||||||
|
return FromMissing(), nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("Unhandled value type: %T", result)
|
return nil, fmt.Errorf("Unhandled value type: %T", result)
|
||||||
}
|
}
|
||||||
@ -493,6 +495,8 @@ func (e *LitValue) evalNode(_ Record) (res *Value, err error) {
|
|||||||
return FromString(string(*e.String)), nil
|
return FromString(string(*e.String)), nil
|
||||||
case e.Boolean != nil:
|
case e.Boolean != nil:
|
||||||
return FromBool(bool(*e.Boolean)), nil
|
return FromBool(bool(*e.Boolean)), nil
|
||||||
|
case e.Missing:
|
||||||
|
return FromMissing(), nil
|
||||||
}
|
}
|
||||||
return FromNull(), nil
|
return FromNull(), nil
|
||||||
}
|
}
|
||||||
|
@ -52,12 +52,12 @@ func jsonpathEval(p []*JSONPathElement, v interface{}) (r interface{}, flat bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Key not found - return nil result
|
// Key not found - return nil result
|
||||||
return nil, false, nil
|
return Missing{}, false, nil
|
||||||
case simdjson.Object:
|
case simdjson.Object:
|
||||||
elem := kvs.FindKey(key, nil)
|
elem := kvs.FindKey(key, nil)
|
||||||
if elem == nil {
|
if elem == nil {
|
||||||
// Key not found - return nil result
|
// Key not found - return nil result
|
||||||
return nil, false, nil
|
return Missing{}, false, nil
|
||||||
}
|
}
|
||||||
val, err := IterToValue(elem.Iter)
|
val, err := IterToValue(elem.Iter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -66,32 +66,34 @@ func TestJsonpathEval(t *testing.T) {
|
|||||||
{"s.authorInfo.yearRange", []interface{}{[]interface{}{1890.0, 1976.0}, []interface{}{1920.0, 1992.0}, []interface{}{1881.0, 1975.0}}},
|
{"s.authorInfo.yearRange", []interface{}{[]interface{}{1890.0, 1976.0}, []interface{}{1920.0, 1992.0}, []interface{}{1881.0, 1975.0}}},
|
||||||
{"s.authorInfo.name", []interface{}{"Agatha Christie", "Isaac Asimov", "P. G. Wodehouse"}},
|
{"s.authorInfo.name", []interface{}{"Agatha Christie", "Isaac Asimov", "P. G. Wodehouse"}},
|
||||||
{"s.authorInfo.yearRange[0]", []interface{}{1890.0, 1920.0, 1881.0}},
|
{"s.authorInfo.yearRange[0]", []interface{}{1890.0, 1920.0, 1881.0}},
|
||||||
{"s.publicationHistory[0].pages", []interface{}{256.0, 336.0, nil}},
|
{"s.publicationHistory[0].pages", []interface{}{256.0, 336.0, Missing{}}},
|
||||||
}
|
}
|
||||||
for i, tc := range cases {
|
for i, tc := range cases {
|
||||||
jp := JSONPath{}
|
t.Run(tc.str, func(t *testing.T) {
|
||||||
err := p.ParseString(tc.str, &jp)
|
jp := JSONPath{}
|
||||||
// fmt.Println(jp)
|
err := p.ParseString(tc.str, &jp)
|
||||||
if err != nil {
|
// fmt.Println(jp)
|
||||||
t.Fatalf("parse failed!: %d %v %s", i, err, tc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read only the first json object from the file
|
|
||||||
recs, err := getJSONStructs(b)
|
|
||||||
if err != nil || len(recs) != 3 {
|
|
||||||
t.Fatalf("%v or length was not 3", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for j, rec := range recs {
|
|
||||||
// fmt.Println(rec)
|
|
||||||
r, _, err := jsonpathEval(jp.PathExpr, rec)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error: %d %d %v", i, j, err)
|
t.Fatalf("parse failed!: %d %v %s", i, err, tc)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(r, tc.res[j]) {
|
|
||||||
fmt.Printf("%#v (%v) != %v (%v)\n", r, reflect.TypeOf(r), tc.res[j], reflect.TypeOf(tc.res[j]))
|
// Read only the first json object from the file
|
||||||
t.Errorf("case: %d %d failed", i, j)
|
recs, err := getJSONStructs(b)
|
||||||
|
if err != nil || len(recs) != 3 {
|
||||||
|
t.Fatalf("%v or length was not 3", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
for j, rec := range recs {
|
||||||
|
// fmt.Println(rec)
|
||||||
|
r, _, err := jsonpathEval(jp.PathExpr, rec)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error: %d %d %v", i, j, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(r, tc.res[j]) {
|
||||||
|
fmt.Printf("%#v (%v) != %v (%v)\n", r, reflect.TypeOf(r), tc.res[j], reflect.TypeOf(tc.res[j]))
|
||||||
|
t.Errorf("case: %d %d failed", i, j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -340,7 +340,8 @@ type LitValue struct {
|
|||||||
Int *float64 `parser:" | @Int"` // To avoid value out of range, use float64 instead
|
Int *float64 `parser:" | @Int"` // To avoid value out of range, use float64 instead
|
||||||
String *LiteralString `parser:" | @LitString"`
|
String *LiteralString `parser:" | @LitString"`
|
||||||
Boolean *Boolean `parser:" | @(\"TRUE\" | \"FALSE\")"`
|
Boolean *Boolean `parser:" | @(\"TRUE\" | \"FALSE\")"`
|
||||||
Null bool `parser:" | @\"NULL\")"`
|
Null bool `parser:" | @\"NULL\""`
|
||||||
|
Missing bool `parser:" | @\"MISSING\")"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identifier represents a parsed identifier
|
// Identifier represents a parsed identifier
|
||||||
@ -352,7 +353,7 @@ type Identifier struct {
|
|||||||
var (
|
var (
|
||||||
sqlLexer = lexer.Must(lexer.Regexp(`(\s+)` +
|
sqlLexer = lexer.Must(lexer.Regexp(`(\s+)` +
|
||||||
`|(?P<Timeword>(?i)\b(?:YEAR|MONTH|DAY|HOUR|MINUTE|SECOND|TIMEZONE_HOUR|TIMEZONE_MINUTE)\b)` +
|
`|(?P<Timeword>(?i)\b(?:YEAR|MONTH|DAY|HOUR|MINUTE|SECOND|TIMEZONE_HOUR|TIMEZONE_MINUTE)\b)` +
|
||||||
`|(?P<Keyword>(?i)\b(?:SELECT|FROM|TOP|DISTINCT|ALL|WHERE|GROUP|BY|HAVING|UNION|MINUS|EXCEPT|INTERSECT|ORDER|LIMIT|OFFSET|TRUE|FALSE|NULL|IS|NOT|ANY|SOME|BETWEEN|AND|OR|LIKE|ESCAPE|AS|IN|BOOL|INT|INTEGER|STRING|FLOAT|DECIMAL|NUMERIC|TIMESTAMP|AVG|COUNT|MAX|MIN|SUM|COALESCE|NULLIF|CAST|DATE_ADD|DATE_DIFF|EXTRACT|TO_STRING|TO_TIMESTAMP|UTCNOW|CHAR_LENGTH|CHARACTER_LENGTH|LOWER|SUBSTRING|TRIM|UPPER|LEADING|TRAILING|BOTH|FOR)\b)` +
|
`|(?P<Keyword>(?i)\b(?:SELECT|FROM|TOP|DISTINCT|ALL|WHERE|GROUP|BY|HAVING|UNION|MINUS|EXCEPT|INTERSECT|ORDER|LIMIT|OFFSET|TRUE|FALSE|NULL|IS|NOT|ANY|SOME|BETWEEN|AND|OR|LIKE|ESCAPE|AS|IN|BOOL|INT|INTEGER|STRING|FLOAT|DECIMAL|NUMERIC|TIMESTAMP|AVG|COUNT|MAX|MIN|SUM|COALESCE|NULLIF|CAST|DATE_ADD|DATE_DIFF|EXTRACT|TO_STRING|TO_TIMESTAMP|UTCNOW|CHAR_LENGTH|CHARACTER_LENGTH|LOWER|SUBSTRING|TRIM|UPPER|LEADING|TRAILING|BOTH|FOR|MISSING)\b)` +
|
||||||
`|(?P<Ident>[a-zA-Z_][a-zA-Z0-9_]*)` +
|
`|(?P<Ident>[a-zA-Z_][a-zA-Z0-9_]*)` +
|
||||||
`|(?P<QuotIdent>"([^"]*("")?)*")` +
|
`|(?P<QuotIdent>"([^"]*("")?)*")` +
|
||||||
`|(?P<Float>\d*\.\d+([eE][-+]?\d+)?)` +
|
`|(?P<Float>\d*\.\d+([eE][-+]?\d+)?)` +
|
||||||
|
@ -49,6 +49,9 @@ type Value struct {
|
|||||||
value interface{}
|
value interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Missing is used to indicate a non-existing value.
|
||||||
|
type Missing struct{}
|
||||||
|
|
||||||
// MarshalJSON provides json marshaling of values.
|
// MarshalJSON provides json marshaling of values.
|
||||||
func (v Value) MarshalJSON() ([]byte, error) {
|
func (v Value) MarshalJSON() ([]byte, error) {
|
||||||
if b, ok := v.ToBytes(); ok {
|
if b, ok := v.ToBytes(); ok {
|
||||||
@ -76,6 +79,8 @@ func (v Value) GetTypeString() string {
|
|||||||
return "BYTES"
|
return "BYTES"
|
||||||
case []Value:
|
case []Value:
|
||||||
return "ARRAY"
|
return "ARRAY"
|
||||||
|
case Missing:
|
||||||
|
return "MISSING"
|
||||||
}
|
}
|
||||||
return "--"
|
return "--"
|
||||||
}
|
}
|
||||||
@ -104,6 +109,8 @@ func (v Value) Repr() string {
|
|||||||
}
|
}
|
||||||
s.WriteString("]:ARRAY")
|
s.WriteString("]:ARRAY")
|
||||||
return s.String()
|
return s.String()
|
||||||
|
case Missing:
|
||||||
|
return ":MISSING"
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("%v:INVALID", v.value)
|
return fmt.Sprintf("%v:INVALID", v.value)
|
||||||
}
|
}
|
||||||
@ -139,6 +146,11 @@ func FromNull() *Value {
|
|||||||
return &Value{value: nil}
|
return &Value{value: nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromMissing creates a Value with Missing value
|
||||||
|
func FromMissing() *Value {
|
||||||
|
return &Value{value: Missing{}}
|
||||||
|
}
|
||||||
|
|
||||||
// FromBytes creates a Value from a []byte
|
// FromBytes creates a Value from a []byte
|
||||||
func FromBytes(b []byte) *Value {
|
func FromBytes(b []byte) *Value {
|
||||||
return &Value{value: b}
|
return &Value{value: b}
|
||||||
@ -239,6 +251,12 @@ func (v Value) IsNull() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsMissing - checks if value is missing.
|
||||||
|
func (v Value) IsMissing() bool {
|
||||||
|
_, ok := v.value.(Missing)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// IsArray returns whether the value is an array.
|
// IsArray returns whether the value is an array.
|
||||||
func (v Value) IsArray() (ok bool) {
|
func (v Value) IsArray() (ok bool) {
|
||||||
_, ok = v.value.([]Value)
|
_, ok = v.value.([]Value)
|
||||||
@ -283,7 +301,7 @@ func (v Value) String() string {
|
|||||||
// CSVString - convert to string for CSV serialization
|
// CSVString - convert to string for CSV serialization
|
||||||
func (v Value) CSVString() string {
|
func (v Value) CSVString() string {
|
||||||
switch x := v.value.(type) {
|
switch x := v.value.(type) {
|
||||||
case nil:
|
case nil, Missing:
|
||||||
return ""
|
return ""
|
||||||
case bool:
|
case bool:
|
||||||
if x {
|
if x {
|
||||||
@ -390,13 +408,21 @@ func (v *Value) compareOp(op string, a *Value) (res bool, err error) {
|
|||||||
switch op {
|
switch op {
|
||||||
case opIs:
|
case opIs:
|
||||||
if a.IsNull() {
|
if a.IsNull() {
|
||||||
return v.IsNull(), nil
|
// Missing is null
|
||||||
|
return v.IsNull() || v.IsMissing(), nil
|
||||||
|
}
|
||||||
|
if a.IsMissing() {
|
||||||
|
return v.IsMissing(), nil
|
||||||
}
|
}
|
||||||
// Forward to Equal
|
// Forward to Equal
|
||||||
op = opEq
|
op = opEq
|
||||||
case opIsNot:
|
case opIsNot:
|
||||||
if a.IsNull() {
|
if a.IsNull() {
|
||||||
return !v.IsNull(), nil
|
// Missing is not null
|
||||||
|
return !v.IsNull() && !v.IsMissing(), nil
|
||||||
|
}
|
||||||
|
if a.IsMissing() {
|
||||||
|
return !v.IsMissing(), nil
|
||||||
}
|
}
|
||||||
// Forward to not equal.
|
// Forward to not equal.
|
||||||
op = opIneq
|
op = opIneq
|
||||||
@ -416,6 +442,11 @@ func (v *Value) compareOp(op string, a *Value) (res bool, err error) {
|
|||||||
return boolCompare(op, v.IsNull(), a.IsNull())
|
return boolCompare(op, v.IsNull(), a.IsNull())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.IsMissing() || v.IsMissing() {
|
||||||
|
// If one is, both must be.
|
||||||
|
return boolCompare(op, v.IsMissing(), a.IsMissing())
|
||||||
|
}
|
||||||
|
|
||||||
// Check array values
|
// Check array values
|
||||||
aArr, aOK := a.ToArray()
|
aArr, aOK := a.ToArray()
|
||||||
vArr, vOK := v.ToArray()
|
vArr, vOK := v.ToArray()
|
||||||
|
Loading…
Reference in New Issue
Block a user