blob: 93947b22609c937f8554a14d7ca887330825d263 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package query_checker_test
import (
"errors"
"fmt"
"reflect"
"testing"
"v.io/syncbase/v23/syncbase/nosql/internal/query/query_checker"
"v.io/syncbase/v23/syncbase/nosql/internal/query/query_parser"
"v.io/syncbase/v23/syncbase/nosql/query_db"
"v.io/syncbase/v23/syncbase/nosql/syncql"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/verror"
_ "v.io/x/ref/runtime/factories/generic"
"v.io/x/ref/test"
"strings"
)
// oneOf represents an expectation that more than one type of error may be
// returned by the corresponding test.
type oneOf []interface{}
func (o oneOf) matches(err error) bool {
for _, e := range o {
switch e := e.(type) {
case error:
if e.Error() == err.Error() {
return true
}
case vErrorContaining:
vErr, ok := err.(verror.E)
if !ok {
continue
}
if e.matches(vErr) {
return true
}
}
}
return false
}
// containing represents an expectation that an error will be of the given
// VError type and will contain the given string.
type vErrorContaining struct {
id verror.ID
message string
}
func newVErrorContaining(id verror.ID, message string) vErrorContaining {
return vErrorContaining{
id: id,
message: message,
}
}
func (v vErrorContaining) matches(err verror.E) bool {
if err.ID != v.id {
return false
}
if !strings.Contains(err.Error(), v.message) {
return false
}
return true
}
func (v vErrorContaining) String() string {
return fmt.Sprintf("VError containing ID=%s, message=%s", v.id, v.message)
}
type mockDB struct {
ctx *context.T
}
func (db *mockDB) GetContext() *context.T {
return db.ctx
}
type customerTable struct {
}
type invoiceTable struct {
}
func init() {
var shutdown v23.Shutdown
db.ctx, shutdown = test.V23Init()
defer shutdown()
}
func (t invoiceTable) Scan(keyRanges query_db.KeyRanges) (query_db.KeyValueStream, error) {
return nil, errors.New("unimplemented")
}
func (t customerTable) Scan(keyRanges query_db.KeyRanges) (query_db.KeyValueStream, error) {
return nil, errors.New("unimplemented")
}
func (db *mockDB) GetTable(table string) (query_db.Table, error) {
if table == "Customer" {
var t customerTable
return t, nil
} else if table == "Invoice" {
var t invoiceTable
return t, nil
}
return nil, errors.New(fmt.Sprintf("No such table: %s", table))
}
var db mockDB
type checkSelectTest struct {
query string
}
type keyRangesTest struct {
query string
keyRanges *query_db.KeyRanges
}
type regularExpressionsTest struct {
query string
regex string
matches []string
nonMatches []string
}
type parseSelectErrorTest struct {
query string
err interface{}
}
func TestQueryChecker(t *testing.T) {
basic := []checkSelectTest{
{"select k, v from Customer"},
{"select k, v.name from Customer"},
{"select k, v.name from Customer limit 200"},
{"select k, v.name from Customer offset 100"},
{"select k, v.name from Customer where k = \"foo\""},
{"select v.z from Customer where k = v.y"},
{"select v.z from Customer where k <> v.y"},
{"select v.z from Customer where k < v.y"},
{"select v.z from Customer where k <= v.y"},
{"select v.z from Customer where k > v.y"},
{"select v.z from Customer where k >= v.y"},
{"select v from Customer where k is nil"},
{"select v from Customer where k is not nil"},
{"select k, v.name from Customer where \"foo\" = k"},
{"select v.z from Customer where v.y = k"},
{"select v.z from Customer where v.y <> k"},
{"select v.z from Customer where v.y < k"},
{"select v.z from Customer where v.y <= k"},
{"select v.z from Customer where v.y > k"},
{"select v.z from Customer where v.y >= k"},
{"select v.z from Customer where \"abc%\" = k"},
{"select v from Customer where k is nil"},
{"select v from Customer where k is not nil"},
{"select v from Customer where Type(v) = \"Foo.Bar\""},
{"select v from Customer where Type(v) <> \"Foo.Bar\""},
{"select v from Customer where Type(v) < \"Foo.Bar\""},
{"select v from Customer where Type(v) <= \"Foo.Bar\""},
{"select v from Customer where Type(v) > \"Foo.Bar\""},
{"select v from Customer where Type(v) >= \"Foo.Bar\""},
{"select v from Customer where Type(v) like \"%.Foo.Bar\""},
{"select v from Customer where Type(v) not like \"%.Foo.Bar\""},
{"select v from Customer where Type(v) is nil"},
{"select v from Customer where Type(v) is not nil"},
{"select v from Customer where \"Foo.Bar\" = Type(v)"},
{"select v from Customer where \"Foo.Bar\" <> Type(v)"},
{"select v from Customer where \"Foo.Bar\" < Type(v)"},
{"select v from Customer where \"Foo.Bar\" > Type(v)"},
{"select v from Customer where \"Foo.Bar\" <= Type(v)"},
{"select v from Customer where \"Foo.Bar\" >= Type(v)"},
{"select v.z from Customer where Type(v) = 2"},
{"select v.z from Customer where Type(v) <> \"foo\""},
{"select v.z from Customer where Type(v) < \"foo\""},
{"select v.z from Customer where Type(v) <= \"foo\""},
{"select v.z from Customer where Type(v) > \"foo\""},
{"select v.z from Customer where Type(v) >= \"foo\""},
{"select v.z from Customer where \"foo\" = Type(v)"},
{"select k, v from Customer where Type(v) = \"Foo.Bar\" and k like \"abc%\" limit 100 offset 200"},
{"select v from Customer where Type(v) is nil"},
{"select v from Customer where Type(v) is not nil"},
{"select v.z from Customer where k not like \"foo\""},
{"select v.z from Customer where k not like \"foo%\""},
{"select v from Customer where v.A = true"},
{"select v from Customer where v.A <> true"},
{"select v from Customer where false = v.A"},
{"select v from Customer where false = false"},
{"select v from Customer where true = true"},
{"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"},
{"select v from Customer where Now() < 10"},
{"select Now() from Customer"},
{"select Time(\"2006-01-02 MST\", \"2015-06-01 PST\"), Time(\"2006-01-02 15:04:05 MST\", \"2015-06-01 12:34:56 PST\"), Year(Now(), \"America/Los_Angeles\") from Customer"},
}
for _, test := range basic {
s, syntaxErr := query_parser.Parse(&db, test.query)
if syntaxErr != nil {
t.Errorf("query: %s; unexpected error: got %v, want nil", test.query, syntaxErr)
}
err := query_checker.Check(&db, s)
if err != nil {
t.Errorf("query: %s; got %v, want: nil", test.query, err)
}
}
}
func appendZeroByte(start string) string {
limit := []byte(start)
limit = append(limit, 0)
return string(limit)
}
func TestKeyRanges(t *testing.T) {
basic := []keyRangesTest{
{
"select k, v from Customer",
&query_db.KeyRanges{
query_db.KeyRange{Start: "", Limit: ""},
},
},
{
"select k, v from Customer where k = \"abc\" or k = \"def\"",
&query_db.KeyRanges{
query_db.KeyRange{Start: "abc", Limit: appendZeroByte("abc")},
query_db.KeyRange{Start: "def", Limit: appendZeroByte("def")},
},
},
{
"select k, v from Customer where \"abc\" = k or \"def\" = k",
&query_db.KeyRanges{
query_db.KeyRange{Start: "abc", Limit: appendZeroByte("abc")},
query_db.KeyRange{Start: "def", Limit: appendZeroByte("def")},
},
},
{
"select k, v from Customer where k >= \"foo\" and k < \"goo\"",
&query_db.KeyRanges{
query_db.KeyRange{Start: "foo", Limit: "goo"},
},
},
{
"select k, v from Customer where \"foo\" <= k and \"goo\" >= k",
&query_db.KeyRanges{
query_db.KeyRange{Start: "foo", Limit: appendZeroByte("goo")},
},
},
{
"select k, v from Customer where k <> \"foo\"",
&query_db.KeyRanges{
query_db.KeyRange{Start: "", Limit: "foo"},
query_db.KeyRange{Start: appendZeroByte("foo"), Limit: ""},
},
},
{
"select k, v from Customer where k <> \"foo\" and k > \"bar\"",
&query_db.KeyRanges{
query_db.KeyRange{Start: appendZeroByte("bar"), Limit: "foo"},
query_db.KeyRange{Start: appendZeroByte("foo"), Limit: ""},
},
},
{
"select k, v from Customer where k <> \"foo\" or k > \"bar\"",
&query_db.KeyRanges{
query_db.KeyRange{Start: "", Limit: ""},
},
},
{
"select k, v from Customer where k <> \"bar\" or k > \"foo\"",
&query_db.KeyRanges{
query_db.KeyRange{Start: "", Limit: "bar"},
query_db.KeyRange{Start: appendZeroByte("bar"), Limit: ""},
},
},
{
"select v from Customer where Type(v) = \"Foo.Bar\" and k >= \"100\" and k < \"200\" and v.foo > 50 and v.bar <= 1000 and v.baz <> -20.7",
&query_db.KeyRanges{
query_db.KeyRange{Start: "100", Limit: "200"},
},
},
{
"select k, v from Customer where k = \"abc\" and k = \"def\"",
&query_db.KeyRanges{},
},
{
"select k, v from Customer where Type(v) = \"Foo.Bar\" and k like \"abc%\" limit 100 offset 200",
&query_db.KeyRanges{
query_db.KeyRange{Start: "abc", Limit: "abd"},
},
},
{
"select k, v from \n Customer where k like \"002%\" or k like \"001%\" or k like \"%\"",
&query_db.KeyRanges{
query_db.KeyRange{Start: "", Limit: ""},
},
},
{
"select k, v from Customer where k = \"Foo.Bar\" and k like \"abc%\" limit 100 offset 200",
&query_db.KeyRanges{},
},
{
"select k, v from Customer where k like \"foo%\" and k like \"bar%\"",
&query_db.KeyRanges{},
},
{
"select k, v from Customer where k like \"foo%\" or k like \"bar%\"",
&query_db.KeyRanges{
query_db.KeyRange{Start: "bar", Limit: "bas"},
query_db.KeyRange{Start: "foo", Limit: "fop"},
},
},
{
// Note: 'like "Foo"' is optimized to '= "Foo"
"select k, v from Customer where k = \"Foo.Bar\" or k like \"Foo\" or k like \"abc%\" limit 100 offset 200",
&query_db.KeyRanges{
query_db.KeyRange{Start: "Foo", Limit: appendZeroByte("Foo")},
query_db.KeyRange{Start: "Foo.Bar", Limit: appendZeroByte("Foo.Bar")},
query_db.KeyRange{Start: "abc", Limit: "abd"},
},
},
{
"select k, v from Customer where k like \"Foo\\%Bar\" or k like \"abc%\" limit 100 offset 200",
&query_db.KeyRanges{
query_db.KeyRange{Start: "Foo%Bar", Limit: appendZeroByte("Foo%Bar")},
query_db.KeyRange{Start: "abc", Limit: "abd"},
},
},
{
"select k, v from Customer where k like \"Foo\\\\%Bar\" or k like \"abc%\" limit 100 offset 200",
&query_db.KeyRanges{
query_db.KeyRange{Start: "Foo\\", Limit: "Foo]"},
query_db.KeyRange{Start: "abc", Limit: "abd"},
},
},
{
"select k, v from Customer where k like \"Foo\\\\\\%Bar\" or k like \"abc%\" limit 100 offset 200",
&query_db.KeyRanges{
query_db.KeyRange{Start: "Foo\\%Bar", Limit: appendZeroByte("Foo\\%Bar")},
query_db.KeyRange{Start: "abc", Limit: "abd"},
},
},
{
"select k, v from Customer where k not like \"002%\"",
&query_db.KeyRanges{
query_db.KeyRange{Start: "", Limit: "002"},
query_db.KeyRange{Start: "003", Limit: ""},
},
},
}
for _, test := range basic {
s, syntaxErr := query_parser.Parse(&db, test.query)
if syntaxErr != nil {
t.Errorf("query: %s; unexpected error: got %v, want nil", test.query, syntaxErr)
}
err := query_checker.Check(&db, s)
if err != nil {
t.Errorf("query: %s; got %v, want: nil", test.query, err)
}
switch sel := (*s).(type) {
case query_parser.SelectStatement:
keyRanges := query_checker.CompileKeyRanges(sel.Where)
if !reflect.DeepEqual(test.keyRanges, keyRanges) {
t.Errorf("query: %s;\nGOT %v\nWANT %v", test.query, keyRanges, test.keyRanges)
}
default:
t.Errorf("query: %s;\nGOT %v\nWANT query_parser.SelectStatement", test.query, *s)
}
}
}
func TestRegularExpressions(t *testing.T) {
basic := []regularExpressionsTest{
{
"select v from Customer where v like \"abc%\"",
"^abc.*?$",
[]string{"abc", "abcd", "abcabc"},
[]string{"xabcd"},
},
{
"select v from Customer where v like \"abc_\"",
"^abc.$",
[]string{"abcd", "abc1"},
[]string{"abc", "xabcd", "abcde"},
},
{
"select v from Customer where v like \"abc_efg\"",
"^abc.efg$",
[]string{"abcdefg"},
[]string{"abc", "xabcd", "abcde", "abcdefgh"},
},
{
"select v from Customer where v like \"abc\\\\efg\"",
"^abc\\\\efg$",
[]string{"abc\\efg"},
[]string{"abc\\", "xabc\\efg", "abc\\de", "abc\\defgh"},
},
{
"select v from Customer where v like \"abc%def\"",
"^abc.*?def$",
[]string{"abcdefdef", "abcdef", "abcdefghidef"},
[]string{"abcdefg", "abcdefde"},
},
{
"select v from Customer where v like \"[0-9]*abc%def\"",
"^\\[0-9\\]\\*abc.*?def$",
[]string{"[0-9]*abcdefdef", "[0-9]*abcdef", "[0-9]*abcdefghidef"},
[]string{"0abcdefg", "9abcdefde", "[0-9]abcdefg", "[0-9]abcdefg", "[0-9]abcdefg"},
},
{
"select v from Customer where v like \"[0-9]*a\\\\b\\\\c%def\"",
"^\\[0-9\\]\\*a\\\\b\\\\c.*?def$",
[]string{"[0-9]*a\\b\\cdefdef", "[0-9]*a\\b\\cdef", "[0-9]*a\\b\\cdefghidef"},
[]string{"0a\\b\\cdefg", "9a\\\b\\cdefde", "[0-9]a\\\b\\cdefg", "[0-9]a\\b\\cdefg", "[0-9]a\\b\\cdefg"},
},
}
for _, test := range basic {
s, syntaxErr := query_parser.Parse(&db, test.query)
if syntaxErr != nil {
t.Errorf("query: %s; unexpected error: got %v, want nil", test.query, syntaxErr)
}
err := query_checker.Check(&db, s)
if err != nil {
t.Errorf("query: %s; got %v, want: nil", test.query, err)
}
switch sel := (*s).(type) {
case query_parser.SelectStatement:
// We know there is exactly one like expression and operand2 contains
// a regex and compiled regex.
if sel.Where.Expr.Operand2.Regex != test.regex {
t.Errorf("query: %s;\nGOT %s\nWANT %s", test.query, sel.Where.Expr.Operand2.Regex, test.regex)
}
regexp := sel.Where.Expr.Operand2.CompRegex
// Make sure all matches actually match
for _, m := range test.matches {
if !regexp.MatchString(m) {
t.Errorf("query: %s;Expected match: %s; \nGOT false\nWANT true", test.query, m)
}
}
// Make sure all nonMatches actually don't match
for _, n := range test.nonMatches {
if regexp.MatchString(n) {
t.Errorf("query: %s;Expected nonMatch: %s; \nGOT true\nWANT false", test.query, n)
}
}
}
}
}
func TestQueryCheckerErrors(t *testing.T) {
basic := []parseSelectErrorTest{
{"select a from Customer", syncql.NewErrInvalidSelectField(db.GetContext(), 7)},
{"select v from Bob", syncql.NewErrTableCantAccess(db.GetContext(), 14, "Bob", errors.New("No such table: Bob"))},
{"select k.a from Customer", syncql.NewErrDotNotationDisallowedForKey(db.GetContext(), 9)},
{"select k from Customer where t.a = \"Foo.Bar\"", syncql.NewErrBadFieldInWhere(db.GetContext(), 29)},
{"select v from Customer where a=1", syncql.NewErrBadFieldInWhere(db.GetContext(), 29)},
{"select v from Customer limit 0", syncql.NewErrLimitMustBeGe0(db.GetContext(), 29)},
{"select v.z from Customer where v.x like v.y", syncql.NewErrLikeExpressionsRequireRhsString(db.GetContext(), 31)},
{"select v.z from Customer where k like \"a\\bc%\"", syncql.NewErrInvalidEscapedChar(db.GetContext(), 38)},
{"select v from Customer where v.A > false", syncql.NewErrBoolInvalidExpression(db.GetContext(), 33)},
{"select v from Customer where true <= v.A", syncql.NewErrBoolInvalidExpression(db.GetContext(), 34)},
{"select v from Customer where Foo(\"2015/07/22\", true, 3.14157) = true", syncql.NewErrFunctionNotFound(db.GetContext(), 29, "Foo")},
{"select v from Customer where nil is v.ZipCode", syncql.NewErrIsIsNotRequireLhsValue(db.GetContext(), 29)},
{"select v from Customer where v.ZipCode is \"94303\"", syncql.NewErrIsIsNotRequireRhsNil(db.GetContext(), 42)},
{"select v from Customer where v.ZipCode is 94303", syncql.NewErrIsIsNotRequireRhsNil(db.GetContext(), 42)},
{"select v from Customer where v.ZipCode is true", syncql.NewErrIsIsNotRequireRhsNil(db.GetContext(), 42)},
{"select v from Customer where v.ZipCode is 943.03", syncql.NewErrIsIsNotRequireRhsNil(db.GetContext(), 42)},
{"select v from Customer where nil is not v.ZipCode", syncql.NewErrIsIsNotRequireLhsValue(db.GetContext(), 29)},
{"select v from Customer where v.ZipCode is not \"94303\"", syncql.NewErrIsIsNotRequireRhsNil(db.GetContext(), 46)},
{"select v from Customer where v.ZipCode is not 94303", syncql.NewErrIsIsNotRequireRhsNil(db.GetContext(), 46)},
{"select v from Customer where v.ZipCode is not true", syncql.NewErrIsIsNotRequireRhsNil(db.GetContext(), 46)},
{"select v from Customer where v.ZipCode is not 943.03", syncql.NewErrIsIsNotRequireRhsNil(db.GetContext(), 46)},
// TODO(jkline): check offset for the 1.5 tests.
{"select v from Customer where Type(v) = \"Customer\" and Year(v.InvoiceDate, \"ABC\") = 2015", oneOf([]interface{}{
// Go1.4
syncql.NewErrLocationConversionError(db.GetContext(), 74, errors.New("unknown time zone ABC")),
// Go1.5
newVErrorContaining(syncql.ErrLocationConversionError.ID, "cannot find ABC in zip file"),
})},
{"select v from Customer where Type(v) = \"Customer\" and Month(v.InvoiceDate, \"ABC\") = 2015", oneOf([]interface{} {
// Go1.4
syncql.NewErrLocationConversionError(db.GetContext(), 75, errors.New("unknown time zone ABC")),
// Go1.5
newVErrorContaining(syncql.ErrLocationConversionError.ID, "cannot find ABC in zip file"),
})},
{"select v from Customer where Type(v) = \"Customer\" and Day(v.InvoiceDate, \"ABC\") = 2015", oneOf([]interface{} {
// Go1.4
syncql.NewErrLocationConversionError(db.GetContext(), 73, errors.New("unknown time zone ABC")),
// Go1.5
newVErrorContaining(syncql.ErrLocationConversionError.ID, "cannot find ABC in zip file"),
})},
{"select v from Customer where Type(v) = \"Customer\" and Hour(v.InvoiceDate, \"ABC\") = 2015", oneOf([]interface{} {
// Go1.4
syncql.NewErrLocationConversionError(db.GetContext(), 74, errors.New("unknown time zone ABC")),
// Go1.5
newVErrorContaining(syncql.ErrLocationConversionError.ID, "cannot find ABC in zip file"),
})},
{"select v from Customer where Type(v) = \"Customer\" and Minute(v.InvoiceDate, \"ABC\") = 2015", oneOf([]interface{} {
// Go1.4
syncql.NewErrLocationConversionError(db.GetContext(), 76, errors.New("unknown time zone ABC")),
// Go1.5
newVErrorContaining(syncql.ErrLocationConversionError.ID, "cannot find ABC in zip file"),
})},
{"select v from Customer where Type(v) = \"Customer\" and Second(v.InvoiceDate, \"ABC\") = 2015", oneOf([]interface{} {
// Go1.4
syncql.NewErrLocationConversionError(db.GetContext(), 76, errors.New("unknown time zone ABC")),
// Go1.5
newVErrorContaining(syncql.ErrLocationConversionError.ID, "cannot find ABC in zip file"),
})},
{"select v from Customer where Type(v) = \"Customer\" and Now(v.InvoiceDate, \"ABC\") = 2015", syncql.NewErrFunctionArgCount(db.GetContext(), 54, "Now", 0, 2)},
{"select v from Customer where Type(v) = \"Customer\" and Lowercase(v.Name, 2) = \"smith\"", syncql.NewErrFunctionArgCount(db.GetContext(), 54, "Lowercase", 1, 2)},
{"select v from Customer where Type(v) = \"Customer\" and Uppercase(v.Name, 2) = \"SMITH\"", syncql.NewErrFunctionArgCount(db.GetContext(), 54, "Uppercase", 1, 2)},
{"select Time() from Customer", syncql.NewErrFunctionArgCount(db.GetContext(), 7, "Time", 2, 0)},
{"select Year(v.InvoiceDate, \"Foo\") from Customer where Type(v) = \"Invoice\"", oneOf([]interface{}{
// Go1.4
syncql.NewErrLocationConversionError(db.GetContext(), 27, errors.New("unknown time zone Foo")),
// Go1.5
newVErrorContaining(syncql.ErrLocationConversionError.ID, "cannot find Foo in zip file"),
})},
{"select K from Customer where Type(v) = \"Invoice\"", syncql.NewErrDidYouMeanLowercaseK(db.GetContext(), 7)},
{"select V from Customer where Type(v) = \"Invoice\"", syncql.NewErrDidYouMeanLowercaseV(db.GetContext(), 7)},
{"select k from Customer where K = \"001\"", syncql.NewErrDidYouMeanLowercaseK(db.GetContext(), 29)},
{"select v from Customer where Type(V) = \"Invoice\"", syncql.NewErrDidYouMeanLowercaseV(db.GetContext(), 34)},
{"select K, V from Customer where Type(V) = \"Invoice\" and K = \"001\"", syncql.NewErrDidYouMeanLowercaseK(db.GetContext(), 7)},
}
for _, test := range basic {
s, syntaxErr := query_parser.Parse(&db, test.query)
if syntaxErr != nil {
t.Errorf("query: %s; unexpected error: got %v, want nil", test.query, syntaxErr)
} else {
err := query_checker.Check(&db, s)
// Test both that the IDs compare and the text compares (since the offset needs to match).
switch e := test.err.(type) {
case error:
if verror.ErrorID(err) != verror.ErrorID(e) || err.Error() != e.Error() {
t.Errorf("query: %s; got %v, want %v", test.query, err, test.err)
}
case oneOf:
if !e.matches(err) {
t.Errorf("query: %s; got %v, want one of %s", test.query, err, e)
}
}
}
}
}