Files
chaisql/internal/sql/parser/expr_test.go
Jean Hadrien Chabran 4a6e68439a Refactor to handle errors with internal/errors (#432)
All new error handling code now rely on internal/errors package
which provides a compilation time toggle that enables to capture
stacktraces for easier debugging while developing.

It also comes with a new testutil/assert package which replaces the require
package when it comes to checking or comparing errors and printing the
stack traces if needed.

Finally, the test target of the Makefile uses the debug build tag by default. 
A testnodebug target is also provided for convenience and to make sure no
tests are broken due to not having used the internal/errors or testutil/assert package.

See #431 for more details
2021-08-22 11:47:54 +03:00

266 lines
11 KiB
Go

package parser_test
import (
"strings"
"testing"
"github.com/genjidb/genji/document"
"github.com/genjidb/genji/internal/expr"
"github.com/genjidb/genji/internal/expr/functions"
"github.com/genjidb/genji/internal/sql/parser"
"github.com/genjidb/genji/internal/testutil"
"github.com/genjidb/genji/internal/testutil/assert"
"github.com/genjidb/genji/types"
"github.com/stretchr/testify/require"
)
func TestParserExpr(t *testing.T) {
tests := []struct {
name string
s string
expected expr.Expr
fails bool
}{
// integers
{"int", "10", testutil.IntegerValue(10), false},
{"-int", "-10", testutil.IntegerValue(-10), false},
{"+int", "+10", testutil.IntegerValue(10), false},
{"> max int64 -> float64", "10000000000000000000", testutil.DoubleValue(10000000000000000000), false},
{"< min int64 -> float64", "-10000000000000000000", testutil.DoubleValue(-10000000000000000000), false},
{"very large int", "100000000000000000000000000000000000000000000000", testutil.DoubleValue(100000000000000000000000000000000000000000000000), false},
// floats
{"+float64", "10.0", testutil.DoubleValue(10), false},
{"-float64", "-10.0", testutil.DoubleValue(-10), false},
// strings
{"double quoted string", `"10.0"`, testutil.TextValue("10.0"), false},
{"single quoted string", "'-10.0'", testutil.TextValue("-10.0"), false},
// blobs
{"blob as hex string", `'\xff'`, testutil.BlobValue([]byte{255}), false},
{"invalid blob hex string", `'\xzz'`, nil, true},
// documents
{"empty document", `{}`, &expr.KVPairs{SelfReferenced: true}, false},
{"document values", `{a: 1, b: 1.0, c: true, d: 'string', e: "string", f: {foo: 'bar'}, g: h.i.j, k: [1, 2, 3]}`,
&expr.KVPairs{SelfReferenced: true, Pairs: []expr.KVPair{
{K: "a", V: testutil.IntegerValue(1)},
{K: "b", V: testutil.DoubleValue(1)},
{K: "c", V: testutil.BoolValue(true)},
{K: "d", V: testutil.TextValue("string")},
{K: "e", V: testutil.TextValue("string")},
{K: "f", V: &expr.KVPairs{SelfReferenced: true, Pairs: []expr.KVPair{
{K: "foo", V: testutil.TextValue("bar")},
}}},
{K: "g", V: testutil.ParsePath(t, "h.i.j")},
{K: "k", V: expr.LiteralExprList{testutil.IntegerValue(1), testutil.IntegerValue(2), testutil.IntegerValue(3)}},
}},
false},
{"document keys", `{a: 1, "foo bar __&&))": 1, 'ola ': 1}`,
&expr.KVPairs{SelfReferenced: true, Pairs: []expr.KVPair{
{K: "a", V: testutil.IntegerValue(1)},
{K: "foo bar __&&))", V: testutil.IntegerValue(1)},
{K: "ola ", V: testutil.IntegerValue(1)},
}},
false},
{"bad document keys: same key", `{a: 1, a: 2, "a": 3}`, nil, true},
{"bad document keys: param", `{?: 1}`, nil, true},
{"bad document keys: dot", `{a.b: 1}`, nil, true},
{"bad document keys: space", `{a b: 1}`, nil, true},
{"bad document: missing right bracket", `{a: 1`, nil, true},
{"bad document: missing colon", `{a: 1, 'b'}`, nil, true},
// parentheses
{"parentheses: empty", "()", nil, true},
{"parentheses: values", `(1)`,
expr.Parentheses{
E: testutil.IntegerValue(1),
}, false},
{"parentheses: expr", `(1 + true * (4 + 3))`,
expr.Parentheses{
E: expr.Add(
testutil.IntegerValue(1),
expr.Mul(
testutil.BoolValue(true),
expr.Parentheses{
E: expr.Add(
testutil.IntegerValue(4),
testutil.IntegerValue(3),
),
},
),
),
}, false},
{"list with brackets: empty", "[]", expr.LiteralExprList(nil), false},
{"list with brackets: values", `[1, true, {a: 1}, a.b.c, (-1), [-1]]`,
expr.LiteralExprList{
testutil.IntegerValue(1),
testutil.BoolValue(true),
&expr.KVPairs{SelfReferenced: true, Pairs: []expr.KVPair{{K: "a", V: testutil.IntegerValue(1)}}},
testutil.ParsePath(t, "a.b.c"),
expr.Parentheses{E: testutil.IntegerValue(-1)},
expr.LiteralExprList{testutil.IntegerValue(-1)},
}, false},
{"list with brackets: missing bracket", `[1, true, {a: 1}, a.b.c, (-1), [-1]`, nil, true},
// operators
{"=", "age = 10", expr.Eq(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"!=", "age != 10", expr.Neq(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{">", "age > 10", expr.Gt(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{">=", "age >= 10", expr.Gte(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"<", "age < 10", expr.Lt(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"<=", "age <= 10", expr.Lte(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"BETWEEN", "1 BETWEEN 10 AND 11", expr.Between(testutil.IntegerValue(10))(testutil.IntegerValue(1), testutil.IntegerValue(11)), false},
{"+", "age + 10", expr.Add(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"-", "age - 10", expr.Sub(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"*", "age * 10", expr.Mul(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"/", "age / 10", expr.Div(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"%", "age % 10", expr.Mod(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"&", "age & 10", expr.BitwiseAnd(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)), false},
{"||", "name || 'foo'", expr.Concat(testutil.ParsePath(t, "name"), testutil.TextValue("foo")), false},
{"IN", "age IN ages", expr.In(testutil.ParsePath(t, "age"), testutil.ParsePath(t, "ages")), false},
{"NOT IN", "age NOT IN ages", expr.NotIn(testutil.ParsePath(t, "age"), testutil.ParsePath(t, "ages")), false},
{"IS", "age IS NULL", expr.Is(testutil.ParsePath(t, "age"), testutil.NullValue()), false},
{"IS NOT", "age IS NOT NULL", expr.IsNot(testutil.ParsePath(t, "age"), testutil.NullValue()), false},
{"LIKE", "name LIKE 'foo'", expr.Like(testutil.ParsePath(t, "name"), testutil.TextValue("foo")), false},
{"NOT LIKE", "name NOT LIKE 'foo'", expr.NotLike(testutil.ParsePath(t, "name"), testutil.TextValue("foo")), false},
{"NOT =", "name NOT = 'foo'", nil, true},
{"precedence", "4 > 1 + 2", expr.Gt(
testutil.IntegerValue(4),
expr.Add(
testutil.IntegerValue(1),
testutil.IntegerValue(2),
),
), false},
{"AND", "age = 10 AND age <= 11",
expr.And(
expr.Eq(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)),
expr.Lte(testutil.ParsePath(t, "age"), testutil.IntegerValue(11)),
), false},
{"OR", "age = 10 OR age = 11",
expr.Or(
expr.Eq(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)),
expr.Eq(testutil.ParsePath(t, "age"), testutil.IntegerValue(11)),
), false},
{"AND then OR", "age >= 10 AND age > $age OR age < 10.4",
expr.Or(
expr.And(
expr.Gte(testutil.ParsePath(t, "age"), testutil.IntegerValue(10)),
expr.Gt(testutil.ParsePath(t, "age"), expr.NamedParam("age")),
),
expr.Lt(testutil.ParsePath(t, "age"), testutil.DoubleValue(10.4)),
), false},
{"with NULL", "age > NULL", expr.Gt(testutil.ParsePath(t, "age"), testutil.NullValue()), false},
// unary operators
{"CAST", "CAST(a.b[1][0] AS TEXT)", expr.Cast{Expr: testutil.ParsePath(t, "a.b[1][0]"), CastAs: types.TextValue}, false},
{"NOT", "NOT 10", expr.Not(testutil.IntegerValue(10)), false},
{"NOT", "NOT NOT", nil, true},
{"NOT", "NOT NOT 10", expr.Not(expr.Not(testutil.IntegerValue(10))), false},
{"NEXT VALUE FOR", "NEXT VALUE FOR hello", expr.NextValueFor{SeqName: "hello"}, false},
{"NEXT VALUE FOR", "NEXT VALUE FOR `good morning`", expr.NextValueFor{SeqName: "good morning"}, false},
{"NEXT VALUE FOR", "NEXT VALUE FOR 10", nil, true},
// functions
{"pk() function", "pk()", &functions.PK{}, false},
{"count(expr) function", "count(a)", &functions.Count{Expr: testutil.ParsePath(t, "a")}, false},
{"count(*) function", "count(*)", &functions.Count{Wildcard: true}, false},
{"packaged function", "math.floor(1.2)", testutil.FunctionExpr(t, "math.floor", testutil.DoubleValue(1.2)), false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ex, err := parser.NewParser(strings.NewReader(test.s)).ParseExpr()
if test.fails {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if !expr.Equal(test.expected, ex) {
require.EqualValues(t, test.expected, ex)
}
}
})
}
}
func TestParsePath(t *testing.T) {
tests := []struct {
name string
s string
expected document.Path
fails bool
}{
{"one fragment", `a`, document.Path{
document.PathFragment{FieldName: "a"},
}, false},
{"one fragment with quotes", "` \"a\"`", document.Path{
document.PathFragment{FieldName: " \"a\""},
}, false},
{"multiple fragments", `a.b[100].c[1][2]`, document.Path{
document.PathFragment{FieldName: "a"},
document.PathFragment{FieldName: "b"},
document.PathFragment{ArrayIndex: 100},
document.PathFragment{FieldName: "c"},
document.PathFragment{ArrayIndex: 1},
document.PathFragment{ArrayIndex: 2},
}, false},
{"with quotes", "`some ident`.` with`[5].` \"quotes`", document.Path{
document.PathFragment{FieldName: "some ident"},
document.PathFragment{FieldName: " with"},
document.PathFragment{ArrayIndex: 5},
document.PathFragment{FieldName: " \"quotes"},
}, false},
{"negative index", `a.b[-100].c`, nil, true},
{"with spaces", `a. b[100]. c`, nil, true},
{"starting with array", `[10].a`, nil, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
vp, err := parser.ParsePath(test.s)
if test.fails {
assert.Error(t, err)
} else {
assert.NoError(t, err)
require.EqualValues(t, test.expected, vp)
}
})
}
}
func TestParserParams(t *testing.T) {
tests := []struct {
name string
s string
expected expr.Expr
errored bool
}{
{"one positional", "age = ?", expr.Eq(testutil.ParsePath(t, "age"), expr.PositionalParam(1)), false},
{"multiple positional", "age = ? AND age <= ?",
expr.And(
expr.Eq(testutil.ParsePath(t, "age"), expr.PositionalParam(1)),
expr.Lte(testutil.ParsePath(t, "age"), expr.PositionalParam(2)),
), false},
{"one named", "age = $age", expr.Eq(testutil.ParsePath(t, "age"), expr.NamedParam("age")), false},
{"multiple named", "age = $foo OR age = $bar",
expr.Or(
expr.Eq(testutil.ParsePath(t, "age"), expr.NamedParam("foo")),
expr.Eq(testutil.ParsePath(t, "age"), expr.NamedParam("bar")),
), false},
{"mixed", "age >= ? AND age > $foo OR age < ?", nil, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ex, err := parser.NewParser(strings.NewReader(test.s)).ParseExpr()
if test.errored {
assert.Error(t, err)
} else {
assert.NoError(t, err)
require.EqualValues(t, test.expected, ex)
}
})
}
}