synbase: query: add "is nil" and "is not nil" expressions.
Change-Id: Idb02b8a471a20f207d577822c43c14f5788c7722
diff --git a/v23/syncbase/nosql/internal/query/eval.go b/v23/syncbase/nosql/internal/query/eval.go
index e14cee7..e923c60 100644
--- a/v23/syncbase/nosql/internal/query/eval.go
+++ b/v23/syncbase/nosql/internal/query/eval.go
@@ -37,6 +37,16 @@
func evalComparisonOperators(k string, v interface{}, e *query_parser.Expression) bool {
lhsValue := resolveOperand(k, v, e.Operand1)
+ // Check for an is nil epression (i.e., v[.<field>...] is nil).
+ // These expressions evaluate to true if the field cannot be resolved.
+ if e.Operator.Type == query_parser.Is && e.Operand2.Type == query_parser.TypNil {
+ return lhsValue == nil
+ }
+ if e.Operator.Type == query_parser.IsNot && e.Operand2.Type == query_parser.TypNil {
+ return lhsValue != nil
+ }
+ // For anything but "is[not] nil" (which is handled above), an unresolved operator
+ // results in the expression evaluating to false.
if lhsValue == nil {
return false
}
diff --git a/v23/syncbase/nosql/internal/query/query_checker/query_checker.go b/v23/syncbase/nosql/internal/query/query_checker/query_checker.go
index fb1ba20..301bbc1 100644
--- a/v23/syncbase/nosql/internal/query/query_checker/query_checker.go
+++ b/v23/syncbase/nosql/internal/query/query_checker/query_checker.go
@@ -119,6 +119,16 @@
e.Operand2.CompRegex = compRegex
}
+ // Is/IsNot expressions require operand1 to be a value and operand2 to be nil.
+ if e.Operator.Type == query_parser.Is || e.Operator.Type == query_parser.IsNot {
+ if !IsField(e.Operand1) {
+ return Error(e.Operand1.Off, "'Is/is not' expressions require left operand to be a value operand.")
+ }
+ if e.Operand2.Type != query_parser.TypNil {
+ return Error(e.Operand2.Off, "'Is/is not' expressions require right operand to be nil.")
+ }
+ }
+
// type as an operand must be the first operand, the operator must be = and the 2nd operand must be string literal.
if (IsType(e.Operand1) && (e.Operator.Type != query_parser.Equal || e.Operand2.Type != query_parser.TypStr)) || IsType(e.Operand2) {
return Error(e.Off, "Type expressions must be 't = <string-literal>'.")
diff --git a/v23/syncbase/nosql/internal/query/query_checker/query_checker_test.go b/v23/syncbase/nosql/internal/query/query_checker/query_checker_test.go
index b819c9d..e51e7e0 100644
--- a/v23/syncbase/nosql/internal/query/query_checker/query_checker_test.go
+++ b/v23/syncbase/nosql/internal/query/query_checker/query_checker_test.go
@@ -83,6 +83,10 @@
{"select v from Customer where false = true"},
{"select v from Customer where true = false"},
{"select v from Customer where false <> true"},
+ {"select v from Customer where v.ZipCode is nil"},
+ {"select v from Customer where v.ZipCode Is Nil"},
+ {"select v from Customer where v.ZipCode is not nil"},
+ {"select v from Customer where v.ZipCode IS NOT NIL"},
}
for _, test := range basic {
@@ -254,6 +258,20 @@
{"select v from Customer where true <= v.A", query_checker.Error(34, "Boolean operands may only be used in equals and not equals expressions.")},
{"select v from Customer where Now() < Date(\"2015/07/22\")", query_checker.Error(29, "Functions are not yet supported. Stay tuned.")},
{"select v from Customer where Foo(\"2015/07/22\", true, 3.14157) = true", query_checker.Error(29, "Functions are not yet supported. Stay tuned.")},
+ {"select v from Customer where t is nil", query_checker.Error(29, "Type expressions must be 't = <string-literal>'.")},
+ {"select v from Customer where k is nil", query_checker.Error(29, "Key (i.e., 'k') expressions must be of form 'k like|= <string-literal>'.")},
+ {"select v from Customer where nil is v.ZipCode", query_checker.Error(29, "'Is/is not' expressions require left operand to be a value operand.")},
+ {"select v from Customer where v.ZipCode is \"94303\"", query_checker.Error(42, "'Is/is not' expressions require right operand to be nil.")},
+ {"select v from Customer where v.ZipCode is 94303", query_checker.Error(42, "'Is/is not' expressions require right operand to be nil.")},
+ {"select v from Customer where v.ZipCode is true", query_checker.Error(42, "'Is/is not' expressions require right operand to be nil.")},
+ {"select v from Customer where v.ZipCode is 943.03", query_checker.Error(42, "'Is/is not' expressions require right operand to be nil.")},
+ {"select v from Customer where t is not nil", query_checker.Error(29, "Type expressions must be 't = <string-literal>'.")},
+ {"select v from Customer where k is not nil", query_checker.Error(29, "Key (i.e., 'k') expressions must be of form 'k like|= <string-literal>'.")},
+ {"select v from Customer where nil is not v.ZipCode", query_checker.Error(29, "'Is/is not' expressions require left operand to be a value operand.")},
+ {"select v from Customer where v.ZipCode is not \"94303\"", query_checker.Error(46, "'Is/is not' expressions require right operand to be nil.")},
+ {"select v from Customer where v.ZipCode is not 94303", query_checker.Error(46, "'Is/is not' expressions require right operand to be nil.")},
+ {"select v from Customer where v.ZipCode is not true", query_checker.Error(46, "'Is/is not' expressions require right operand to be nil.")},
+ {"select v from Customer where v.ZipCode is not 943.03", query_checker.Error(46, "'Is/is not' expressions require right operand to be nil.")},
}
for _, test := range basic {
diff --git a/v23/syncbase/nosql/internal/query/query_parser/query_parser.go b/v23/syncbase/nosql/internal/query/query_parser/query_parser.go
index 61cce96..502fe43 100644
--- a/v23/syncbase/nosql/internal/query/query_parser/query_parser.go
+++ b/v23/syncbase/nosql/internal/query/query_parser/query_parser.go
@@ -80,6 +80,8 @@
Equal
GreaterThan
GreaterThanOrEqual
+ Is
+ IsNot
LessThan
LessThanOrEqual
Like
@@ -104,6 +106,7 @@
TypFloat
TypFunction
TypInt
+ TypNil
TypStr
TypObject // Only as the result of a ResolveOperand.
TypUint // Only as a result of a ResolveOperand
@@ -535,12 +538,14 @@
field.Segments = append(field.Segments, segment)
token = scanToken(s)
- // Check for true/false. If so, change this operand to a bool.
- // If the next token is not a period, check for true and false operands.
+ // If the next token is not a period, check for true/false/nil.
+ // If true/false or nil, change this operand to a bool or nil, respectively.
// Also, check for function call. If so, change to a function operand.
- if token.Tok != TokPERIOD && strings.ToLower(segment.Value) == "true" || strings.ToLower(segment.Value) == "false" {
+ if token.Tok != TokPERIOD && (strings.ToLower(segment.Value) == "true" || strings.ToLower(segment.Value) == "false") {
operand.Type = TypBool
operand.Bool = strings.ToLower(segment.Value) == "true"
+ } else if token.Tok != TokPERIOD && strings.ToLower(segment.Value) == "nil" {
+ operand.Type = TypNil
} else if token.Tok == TokLEFTPAREN {
operand.Type = TypFunction
var function Function
@@ -651,8 +656,18 @@
switch strings.ToLower(token.Value) {
case "equal":
operator.Type = Equal
+ token = scanToken(s)
+ case "is":
+ operator.Type = Is
+ token = scanToken(s)
+ // if the next token is "not", change to IsNot
+ if token.Tok != TokEOF && strings.ToLower(token.Value) == "not" {
+ operator.Type = IsNot
+ token = scanToken(s)
+ }
case "like":
operator.Type = Like
+ token = scanToken(s)
case "not":
token = scanToken(s)
if token.Tok == TokEOF || (strings.ToLower(token.Value) != "equal" && strings.ToLower(token.Value) != "like") {
@@ -664,10 +679,10 @@
default: //case "like":
operator.Type = NotLike
}
+ token = scanToken(s)
default:
return nil, nil, Error(token.Off, fmt.Sprintf("Expected operator ('like', 'not like', '=', '<>', '<', '<=', '>', '>=', 'equal' or 'not equal', found '%s'.", token.Value))
}
- token = scanToken(s)
} else {
switch token.Tok {
case TokEQUAL:
@@ -909,6 +924,8 @@
case TypExpr:
val += "(expr)"
val += o.Expr.String()
+ case TypNil:
+ val += "<nil>"
case TypObject:
val += "(object)"
val += fmt.Sprintf("%v", o.Object)
@@ -930,6 +947,10 @@
val += ">"
case GreaterThanOrEqual:
val += ">="
+ case Is:
+ val += "IS"
+ case IsNot:
+ val += "IS NOT"
case LessThan:
val += "<"
case LessThanOrEqual:
diff --git a/v23/syncbase/nosql/internal/query/query_parser/query_parser_test.go b/v23/syncbase/nosql/internal/query/query_parser/query_parser_test.go
index 78a6075..8053e43 100644
--- a/v23/syncbase/nosql/internal/query/query_parser/query_parser_test.go
+++ b/v23/syncbase/nosql/internal/query/query_parser/query_parser_test.go
@@ -368,6 +368,124 @@
nil,
},
{
+ "select v from Customer where v.ZipCode is nil",
+ query_parser.SelectStatement{
+ Select: &query_parser.SelectClause{
+ Columns: []query_parser.Field{
+ query_parser.Field{
+ Segments: []query_parser.Segment{
+ query_parser.Segment{
+ Value: "v",
+ Node: query_parser.Node{Off: 7},
+ },
+ },
+ Node: query_parser.Node{Off: 7},
+ },
+ },
+ Node: query_parser.Node{Off: 0},
+ },
+ From: &query_parser.FromClause{
+ Table: query_parser.TableEntry{
+ Name: "Customer",
+ Node: query_parser.Node{Off: 14},
+ },
+ Node: query_parser.Node{Off: 9},
+ },
+ Where: &query_parser.WhereClause{
+ Expr: &query_parser.Expression{
+ Operand1: &query_parser.Operand{
+ Type: query_parser.TypField,
+ Column: &query_parser.Field{
+ Segments: []query_parser.Segment{
+ query_parser.Segment{
+ Value: "v",
+ Node: query_parser.Node{Off: 29},
+ },
+ query_parser.Segment{
+ Value: "ZipCode",
+ Node: query_parser.Node{Off: 31},
+ },
+ },
+ Node: query_parser.Node{Off: 29},
+ },
+ Node: query_parser.Node{Off: 29},
+ },
+ Operator: &query_parser.BinaryOperator{
+ Type: query_parser.Is,
+ Node: query_parser.Node{Off: 39},
+ },
+ Operand2: &query_parser.Operand{
+ Type: query_parser.TypNil,
+ Node: query_parser.Node{Off: 42},
+ },
+ Node: query_parser.Node{Off: 29},
+ },
+ Node: query_parser.Node{Off: 23},
+ },
+ Node: query_parser.Node{Off: 0},
+ },
+ nil,
+ },
+ {
+ "select v from Customer where v.ZipCode is not nil",
+ query_parser.SelectStatement{
+ Select: &query_parser.SelectClause{
+ Columns: []query_parser.Field{
+ query_parser.Field{
+ Segments: []query_parser.Segment{
+ query_parser.Segment{
+ Value: "v",
+ Node: query_parser.Node{Off: 7},
+ },
+ },
+ Node: query_parser.Node{Off: 7},
+ },
+ },
+ Node: query_parser.Node{Off: 0},
+ },
+ From: &query_parser.FromClause{
+ Table: query_parser.TableEntry{
+ Name: "Customer",
+ Node: query_parser.Node{Off: 14},
+ },
+ Node: query_parser.Node{Off: 9},
+ },
+ Where: &query_parser.WhereClause{
+ Expr: &query_parser.Expression{
+ Operand1: &query_parser.Operand{
+ Type: query_parser.TypField,
+ Column: &query_parser.Field{
+ Segments: []query_parser.Segment{
+ query_parser.Segment{
+ Value: "v",
+ Node: query_parser.Node{Off: 29},
+ },
+ query_parser.Segment{
+ Value: "ZipCode",
+ Node: query_parser.Node{Off: 31},
+ },
+ },
+ Node: query_parser.Node{Off: 29},
+ },
+ Node: query_parser.Node{Off: 29},
+ },
+ Operator: &query_parser.BinaryOperator{
+ Type: query_parser.IsNot,
+ Node: query_parser.Node{Off: 39},
+ },
+ Operand2: &query_parser.Operand{
+ Type: query_parser.TypNil,
+ Node: query_parser.Node{Off: 46},
+ },
+ Node: query_parser.Node{Off: 29},
+ },
+ Node: query_parser.Node{Off: 23},
+ },
+ Node: query_parser.Node{Off: 0},
+ },
+ nil,
+ },
+ {
"select v from Customer where v.Value = false",
query_parser.SelectStatement{
Select: &query_parser.SelectClause{
@@ -2118,6 +2236,8 @@
{"select v from Customer where Foo(,1) = true", query_parser.Error(33, "Expected operand, found ','.")},
{"select v from Customer where Foo(1, 2.0 = true", query_parser.Error(40, "Expected right paren or comma.")},
{"select v from Customer where Foo(1, 2.0 limit 100", query_parser.Error(40, "Expected right paren or comma.")},
+ {"select v from Customer where v is", query_parser.Error(33, "Unexpected end of statement, expected operand.")},
+ {"select v from Customer where v = 1.0 is k = \"abc\"", query_parser.Error(37, "Unexpected: 'is'.")},
}
for _, test := range basic {
diff --git a/v23/syncbase/nosql/internal/query/query_test.go b/v23/syncbase/nosql/internal/query/query_test.go
index dced5d7..a703e17 100644
--- a/v23/syncbase/nosql/internal/query/query_test.go
+++ b/v23/syncbase/nosql/internal/query/query_test.go
@@ -231,6 +231,85 @@
},
},
{
+ // Select values where v.InvoiceNum is nil
+ // Since InvoiceNum does not exist for Invoice,
+ // this will return just customers.
+ "select v from Customer where v.InvoiceNum is nil",
+ []interface{}{
+ []interface{}{customerRows[0].value},
+ []interface{}{customerRows[4].value},
+ },
+ },
+ {
+ // Select values where v.InvoiceNum is nil
+ // or v.Name is nil This will select all customers
+ // with the former and all invoices with the latter.
+ // Hence, all records are returned.
+ "select v from Customer where v.InvoiceNum is nil or v.Name is nil",
+ []interface{}{
+ []interface{}{customerRows[0].value},
+ []interface{}{customerRows[1].value},
+ []interface{}{customerRows[2].value},
+ []interface{}{customerRows[3].value},
+ []interface{}{customerRows[4].value},
+ []interface{}{customerRows[5].value},
+ []interface{}{customerRows[6].value},
+ []interface{}{customerRows[7].value},
+ []interface{}{customerRows[8].value},
+ },
+ },
+ {
+ // Select values where v.InvoiceNum is nil
+ // and v.Name is nil. Expect nothing returned.
+ "select v from Customer where v.InvoiceNum is nil and v.Name is nil",
+ []interface{}{},
+ },
+ {
+ // Select values where v.InvoiceNum is not nil
+ // This will return just invoices.
+ "select v from Customer where v.InvoiceNum is not nil",
+ []interface{}{
+ []interface{}{customerRows[1].value},
+ []interface{}{customerRows[2].value},
+ []interface{}{customerRows[3].value},
+ []interface{}{customerRows[5].value},
+ []interface{}{customerRows[6].value},
+ []interface{}{customerRows[7].value},
+ []interface{}{customerRows[8].value},
+ },
+ },
+ {
+ // Select values where v.InvoiceNum is not nil
+ // or v.Name is not nil. All records are returned.
+ "select v from Customer where v.InvoiceNum is not nil or v.Name is not nil",
+ []interface{}{
+ []interface{}{customerRows[0].value},
+ []interface{}{customerRows[1].value},
+ []interface{}{customerRows[2].value},
+ []interface{}{customerRows[3].value},
+ []interface{}{customerRows[4].value},
+ []interface{}{customerRows[5].value},
+ []interface{}{customerRows[6].value},
+ []interface{}{customerRows[7].value},
+ []interface{}{customerRows[8].value},
+ },
+ },
+ {
+ // Select values where v.InvoiceNum is nil and v.Name is not nil.
+ // All customers are returned.
+ "select v from Customer where v.InvoiceNum is nil and v.Name is not nil",
+ []interface{}{
+ []interface{}{customerRows[0].value},
+ []interface{}{customerRows[4].value},
+ },
+ },
+ {
+ // Select values where v.InvoiceNum is not nil
+ // and v.Name is not nil. Expect nothing returned.
+ "select v from Customer where v.InvoiceNum is not nil and v.Name is not nil",
+ []interface{}{},
+ },
+ {
// Select keys & values for all customer records.
"select k, v from Customer where t = \"Customer\"",
[]interface{}{
@@ -389,6 +468,18 @@
},
},
{
+ // Select records where v.Foo.FooBarBaz.Baz is 84 and v.InvoiceNum is not nil.
+ "select v from Customer where v.Foo.FooBarBaz.Baz = 84 and v.InvoiceNum is not nil",
+ []interface{}{},
+ },
+ {
+ // Select records where v.Foo.FooBarBaz.Baz is 84 and v.InvoiceNum is nil.
+ "select v from Customer where v.Foo.FooBarBaz.Baz = 84 and v.InvoiceNum is nil",
+ []interface{}{
+ []interface{}{customerRows[4].value},
+ },
+ },
+ {
// Select customer name for customer ID (i.e., key) "001".
"select v.Name from Customer where t = \"Customer\" and k = \"001\"",
[]interface{}{
@@ -449,6 +540,22 @@
[]interface{}{customerRows[5].value},
},
},
+ {
+ // Select records where v.Foo.FooBarBaz.Baz is 84 or (type is Invoice and v.InvoiceNum is not nil).
+ // Limit 3 Offset 2
+ "select v from Customer where v.Foo.FooBarBaz.Baz = 84 or (t = \"Invoice\" and v.InvoiceNum is not nil) limit 3 offset 2",
+ []interface{}{
+ []interface{}{customerRows[3].value},
+ []interface{}{customerRows[4].value},
+ []interface{}{customerRows[5].value},
+ },
+ },
+ {
+ // Select records where v.Foo.FooBarBaz.Baz is 84 or (type is Invoice and v.InvoiceNum is nil).
+ // Limit 3 Offset 2
+ "select v from Customer where v.Foo.FooBarBaz.Baz = 84 or (t = \"Invoice\" and v.InvoiceNum is nil) limit 3 offset 2",
+ []interface{}{},
+ },
}
for _, test := range basic {