mirror of
				https://github.com/chaisql/chai.git
				synced 2025-10-31 19:02:48 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			259 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			259 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package parser
 | |
| 
 | |
| import (
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/genjidb/genji/document"
 | |
| 	"github.com/genjidb/genji/sql/query/expr"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| )
 | |
| 
 | |
| func parsePath(t testing.TB, ref string) document.ValuePath {
 | |
| 	t.Helper()
 | |
| 
 | |
| 	vp, err := ParsePath(ref)
 | |
| 	require.NoError(t, err)
 | |
| 	return vp
 | |
| }
 | |
| 
 | |
| func TestParserExpr(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name     string
 | |
| 		s        string
 | |
| 		expected expr.Expr
 | |
| 		fails    bool
 | |
| 	}{
 | |
| 		// integers
 | |
| 		{"+int8", "10", expr.IntegerValue(10), false},
 | |
| 		{"-int8", "-10", expr.IntegerValue(-10), false},
 | |
| 		{"+int16", "1000", expr.IntegerValue(1000), false},
 | |
| 		{"-int16", "-1000", expr.IntegerValue(-1000), false},
 | |
| 		{"+int32", "10000000", expr.IntegerValue(10000000), false},
 | |
| 		{"-int32", "-10000000", expr.IntegerValue(-10000000), false},
 | |
| 		{"+int64", "10000000000", expr.IntegerValue(10000000000), false},
 | |
| 		{"-int64", "-10000000000", expr.IntegerValue(-10000000000), false},
 | |
| 		{"> max int64 -> float64", "10000000000000000000", expr.DoubleValue(10000000000000000000), false},
 | |
| 		{"< min int64 -> float64", "-10000000000000000000", expr.DoubleValue(-10000000000000000000), false},
 | |
| 		{"very large int", "100000000000000000000000000000000000000000000000", expr.DoubleValue(100000000000000000000000000000000000000000000000), false},
 | |
| 
 | |
| 		// floats
 | |
| 		{"+float64", "10.0", expr.DoubleValue(10), false},
 | |
| 		{"-float64", "-10.0", expr.DoubleValue(-10), false},
 | |
| 
 | |
| 		// strings
 | |
| 		{"double quoted string", `"10.0"`, expr.TextValue("10.0"), false},
 | |
| 		{"single quoted string", "'-10.0'", expr.TextValue("-10.0"), false},
 | |
| 
 | |
| 		// documents
 | |
| 		{"empty document", `{}`, expr.KVPairs(nil), 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{
 | |
| 				expr.KVPair{K: "a", V: expr.IntegerValue(1)},
 | |
| 				expr.KVPair{K: "b", V: expr.DoubleValue(1)},
 | |
| 				expr.KVPair{K: "c", V: expr.BoolValue(true)},
 | |
| 				expr.KVPair{K: "d", V: expr.TextValue("string")},
 | |
| 				expr.KVPair{K: "e", V: expr.TextValue("string")},
 | |
| 				expr.KVPair{K: "f", V: expr.KVPairs{
 | |
| 					expr.KVPair{K: "foo", V: expr.TextValue("bar")},
 | |
| 				}},
 | |
| 				expr.KVPair{K: "g", V: expr.FieldSelector(parsePath(t, "h.i.j"))},
 | |
| 				expr.KVPair{K: "k", V: expr.LiteralExprList{expr.IntegerValue(1), expr.IntegerValue(2), expr.IntegerValue(3)}},
 | |
| 			},
 | |
| 			false},
 | |
| 		{"document keys", `{a: 1, "foo bar __&&))": 1, 'ola ': 1}`,
 | |
| 			expr.KVPairs{
 | |
| 				expr.KVPair{K: "a", V: expr.IntegerValue(1)},
 | |
| 				expr.KVPair{K: "foo bar __&&))", V: expr.IntegerValue(1)},
 | |
| 				expr.KVPair{K: "ola ", V: expr.IntegerValue(1)},
 | |
| 			},
 | |
| 			false},
 | |
| 		{"document keys: same key", `{a: 1, a: 2, "a": 3}`,
 | |
| 			expr.KVPairs{
 | |
| 				expr.KVPair{K: "a", V: expr.IntegerValue(1)},
 | |
| 				expr.KVPair{K: "a", V: expr.IntegerValue(2)},
 | |
| 				expr.KVPair{K: "a", V: expr.IntegerValue(3)},
 | |
| 			},
 | |
| 			false},
 | |
| 		{"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: expr.IntegerValue(1),
 | |
| 			}, false},
 | |
| 		{"parentheses: expr", `(1 + true * (4 + 3))`,
 | |
| 			expr.Parentheses{
 | |
| 				E: expr.Add(
 | |
| 					expr.IntegerValue(1),
 | |
| 					expr.Mul(
 | |
| 						expr.BoolValue(true),
 | |
| 						expr.Parentheses{
 | |
| 							E: expr.Add(
 | |
| 								expr.IntegerValue(4),
 | |
| 								expr.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{
 | |
| 				expr.IntegerValue(1),
 | |
| 				expr.BoolValue(true),
 | |
| 				expr.KVPairs{expr.KVPair{K: "a", V: expr.IntegerValue(1)}},
 | |
| 				expr.FieldSelector(parsePath(t, "a.b.c")),
 | |
| 				expr.Parentheses{E: expr.IntegerValue(-1)},
 | |
| 				expr.LiteralExprList{expr.IntegerValue(-1)},
 | |
| 			}, false},
 | |
| 		{"list with brackets: missing bracket", `[1, true, {a: 1}, a.b.c, (-1), [-1]`, nil, true},
 | |
| 
 | |
| 		// operators
 | |
| 		{"=", "age = 10", expr.Eq(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"!=", "age != 10", expr.Neq(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{">", "age > 10", expr.Gt(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{">=", "age >= 10", expr.Gte(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"<", "age < 10", expr.Lt(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"<=", "age <= 10", expr.Lte(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"+", "age + 10", expr.Add(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"-", "age - 10", expr.Sub(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"*", "age * 10", expr.Mul(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"/", "age / 10", expr.Div(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"%", "age % 10", expr.Mod(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"&", "age & 10", expr.BitwiseAnd(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)), false},
 | |
| 		{"IN", "age IN ages", expr.In(expr.FieldSelector(parsePath(t, "age")), expr.FieldSelector(parsePath(t, "ages"))), false},
 | |
| 		{"IS", "age IS NULL", expr.Is(expr.FieldSelector(parsePath(t, "age")), expr.NullValue()), false},
 | |
| 		{"IS NOT", "age IS NOT NULL", expr.IsNot(expr.FieldSelector(parsePath(t, "age")), expr.NullValue()), false},
 | |
| 		{"precedence", "4 > 1 + 2", expr.Gt(
 | |
| 			expr.IntegerValue(4),
 | |
| 			expr.Add(
 | |
| 				expr.IntegerValue(1),
 | |
| 				expr.IntegerValue(2),
 | |
| 			),
 | |
| 		), false},
 | |
| 		{"AND", "age = 10 AND age <= 11",
 | |
| 			expr.And(
 | |
| 				expr.Eq(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)),
 | |
| 				expr.Lte(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(11)),
 | |
| 			), false},
 | |
| 		{"OR", "age = 10 OR age = 11",
 | |
| 			expr.Or(
 | |
| 				expr.Eq(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)),
 | |
| 				expr.Eq(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(11)),
 | |
| 			), false},
 | |
| 		{"AND then OR", "age >= 10 AND age > $age OR age < 10.4",
 | |
| 			expr.Or(
 | |
| 				expr.And(
 | |
| 					expr.Gte(expr.FieldSelector(parsePath(t, "age")), expr.IntegerValue(10)),
 | |
| 					expr.Gt(expr.FieldSelector(parsePath(t, "age")), expr.NamedParam("age")),
 | |
| 				),
 | |
| 				expr.Lt(expr.FieldSelector(parsePath(t, "age")), expr.DoubleValue(10.4)),
 | |
| 			), false},
 | |
| 		{"with NULL", "age > NULL", expr.Gt(expr.FieldSelector(parsePath(t, "age")), expr.NullValue()), false},
 | |
| 		{"pk() function", "pk()", &expr.PKFunc{}, false},
 | |
| 		{"count(expr) function", "count(a)", &expr.CountFunc{Expr: expr.FieldSelector(parsePath(t, "a"))}, false},
 | |
| 		{"count(*) function", "count(*)", &expr.CountFunc{Wildcard: true}, false},
 | |
| 		{"CAST", "CAST(a.b[1][0] AS TEXT)", expr.CastFunc{Expr: expr.FieldSelector(parsePath(t, "a.b[1][0]")), CastAs: document.TextValue}, false},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			ex, lit, err := NewParser(strings.NewReader(test.s)).ParseExpr()
 | |
| 			if test.fails {
 | |
| 				require.Error(t, err)
 | |
| 			} else {
 | |
| 				require.NoError(t, err)
 | |
| 				require.EqualValues(t, test.expected, ex)
 | |
| 				require.Equal(t, test.s, lit)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestParserPath(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name     string
 | |
| 		s        string
 | |
| 		expected document.ValuePath
 | |
| 		fails    bool
 | |
| 	}{
 | |
| 		{"one fragment", `a`, document.ValuePath{
 | |
| 			document.ValuePathFragment{FieldName: "a"},
 | |
| 		}, false},
 | |
| 		{"one fragment with quotes", "`    \"a\"`", document.ValuePath{
 | |
| 			document.ValuePathFragment{FieldName: "    \"a\""},
 | |
| 		}, false},
 | |
| 		{"multiple fragments", `a.b[100].c[1][2]`, document.ValuePath{
 | |
| 			document.ValuePathFragment{FieldName: "a"},
 | |
| 			document.ValuePathFragment{FieldName: "b"},
 | |
| 			document.ValuePathFragment{ArrayIndex: 100},
 | |
| 			document.ValuePathFragment{FieldName: "c"},
 | |
| 			document.ValuePathFragment{ArrayIndex: 1},
 | |
| 			document.ValuePathFragment{ArrayIndex: 2},
 | |
| 		}, false},
 | |
| 		{"with quotes", "`some ident`.` with`[5].`  \"quotes`", document.ValuePath{
 | |
| 			document.ValuePathFragment{FieldName: "some ident"},
 | |
| 			document.ValuePathFragment{FieldName: " with"},
 | |
| 			document.ValuePathFragment{ArrayIndex: 5},
 | |
| 			document.ValuePathFragment{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 := ParsePath(test.s)
 | |
| 			if test.fails {
 | |
| 				require.Error(t, err)
 | |
| 			} else {
 | |
| 				require.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(expr.FieldSelector(parsePath(t, "age")), expr.PositionalParam(1)), false},
 | |
| 		{"multiple positional", "age = ? AND age <= ?",
 | |
| 			expr.And(
 | |
| 				expr.Eq(expr.FieldSelector(parsePath(t, "age")), expr.PositionalParam(1)),
 | |
| 				expr.Lte(expr.FieldSelector(parsePath(t, "age")), expr.PositionalParam(2)),
 | |
| 			), false},
 | |
| 		{"one named", "age = $age", expr.Eq(expr.FieldSelector(parsePath(t, "age")), expr.NamedParam("age")), false},
 | |
| 		{"multiple named", "age = $foo OR age = $bar",
 | |
| 			expr.Or(
 | |
| 				expr.Eq(expr.FieldSelector(parsePath(t, "age")), expr.NamedParam("foo")),
 | |
| 				expr.Eq(expr.FieldSelector(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, lit, err := NewParser(strings.NewReader(test.s)).ParseExpr()
 | |
| 			if test.errored {
 | |
| 				require.Error(t, err)
 | |
| 			} else {
 | |
| 				require.NoError(t, err)
 | |
| 				require.EqualValues(t, test.expected, ex)
 | |
| 				require.Equal(t, test.s, lit)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | 
