featurebase/pql/pqlpeg_test.go
2022-09-02 13:23:39 -07:00

832 lines
16 KiB
Go
Raw Permalink Blame History

// Copyright 2022 Molecula Corp. (DBA FeatureBase).
// SPDX-License-Identifier: Apache-2.0
package pql
import (
"fmt"
"reflect"
"strconv"
"testing"
"github.com/pkg/errors"
)
func TestPEG(t *testing.T) {
p := PQL{Buffer: `
SetBit(Union(Zitmap(row==4), Intersect(Qitmap(blah>4), Ritmap(field="http://zoo9.com=\\'hello' and \"hello\"")), Hitmap(row=ag-bee)), a="4z", b=5) Count(Union(Witmap(row=5.73, frame=.10), Row(zztop><[2, 9]))) TopN(blah, fields=["hello", "goodbye", "zero"])`[1:]}
err := p.Init()
if err != nil {
t.Fatal(errors.Wrap(err, "creating parser"))
}
err = p.Parse()
if err != nil {
t.Fatalf("parse error: %v", err)
}
p.Execute()
q, err := ParseString("TopN(blah, Bitmap(id==other), field=f, n=0)")
if err != nil {
t.Fatalf("should have parsed: %v", err)
}
if q.String() != `TopN(Bitmap(id=="other"), _field="blah", field="f", n=0)` {
t.Fatalf("Failed, got: %s", q)
}
_, err = ParseString("Row(a=falsen0)")
if err != nil {
t.Fatalf("falsen0 should have been parsed as a string")
}
q, err = ParseString("Bitmap(row=4, did==other)")
if err != nil {
t.Fatalf("should have parsed: %v", err)
}
if q.String() != `Bitmap(did=="other", row=4)` {
t.Fatalf("got %s", q)
}
}
func TestOldPQL(t *testing.T) {
_, err := ParseString(`SetBit(f=11, col=1)`)
if err != nil {
t.Fatalf("should have parsed: %v", err)
}
}
func TestPEGWorking(t *testing.T) {
tests := []struct {
name string
input string
ncalls int
}{
{
name: "Empty",
input: "",
ncalls: 0},
{
name: "Set",
input: "Set(2, f=10)",
ncalls: 1},
{
name: "SetWithColKeySingleQuote",
input: `Set('foo', f=10)`,
ncalls: 1},
{
name: "SetWithColKeyDoubleQuote",
input: `Set("foo", f=10)`,
ncalls: 1},
{
name: "SetTime",
input: "Set(2, f=1, 1999-12-31T00:00)",
ncalls: 1},
{
name: "DoubleSet",
input: "Set(1, a=4)Set(2, a=4)",
ncalls: 2},
{
name: "DoubleSetSpc",
input: "Set(1, a=4) Set(2, a=4)",
ncalls: 2},
{
name: "DoubleSetNewline",
input: "Set(1, a=4) \n Set(2, a=4)",
ncalls: 2},
{
name: "SetWithArbCall",
input: "Set(1, a=4)Row(z=ha)",
ncalls: 2},
{
name: "SetArbSet",
input: "Set(1, a=4)Row(z=ha)Set(2, z=99)",
ncalls: 3},
{
name: "ArbSetArb",
input: "Row(q=1, a=4)Set(1, z=9)Row(z=99)",
ncalls: 3},
{
name: "SetStringArg",
input: "Set(1, a=zoom)",
ncalls: 1},
{
name: "SetManyArgs",
input: "Set(1, a=4, b=5)",
ncalls: 1},
{
name: "SetManyMixedArgs",
input: "Set(1, a=4, bsd=haha)",
ncalls: 1},
{
name: "SetTimestamp",
input: "Set(1, a=4, 2017-04-03T19:34)",
ncalls: 1},
{
name: "SetTimestampField",
input: "Set(1, a='2017-04-03T19:34:00Z')",
ncalls: 1},
{
name: "SetTimestampTZField",
input: "Set(1, a='2017-04-03T19:34:00-07:00')",
ncalls: 1},
{
name: "SetTimestampTZField",
input: "Set(1, a='2017-04-03T19:34:00+07:00')",
ncalls: 1},
{
name: "SetTimestampNanoField",
input: "Set(1, a='2017-04-03T19:34:00.000000Z')",
ncalls: 1},
{
name: "Union()",
input: "Union()",
ncalls: 1},
{
name: "UnionOneRow",
input: "Union(Row(a=1))",
ncalls: 1},
{
name: "UnionTwoRows",
input: "Union(Row(a=1), Row(z=44))",
ncalls: 1},
{
name: "UnionNested",
input: "Union(Intersect(Row(), Union(Row(), Row())), Row())",
ncalls: 1},
{
name: "TopN no args",
input: "TopN(boondoggle)",
ncalls: 1},
{
name: "TopN with args",
input: "TopN(boon, doggle=9)",
ncalls: 1},
{
name: "double quoted args",
input: `Row(a="zm''e")`,
ncalls: 1},
{
name: "single quoted args",
input: `Row(a='zm""e')`,
ncalls: 1},
{
name: "Clear",
input: "Clear(1, a=53)",
ncalls: 1},
{
name: "Clear2args",
input: "Clear(1, a=53, b=33)",
ncalls: 1},
{
name: "TopN",
input: "TopN(myfield, n=44)",
ncalls: 1},
{
name: "TopNBitmap",
input: "TopN(myfield, Row(a=47), n=10)",
ncalls: 1},
{
name: "RangeLT",
input: "Row(a < 4)",
ncalls: 1},
{
name: "RangeGT",
input: "Row(a > 4)",
ncalls: 1},
{
name: "RangeLTE",
input: "Row(a <= 4)",
ncalls: 1},
{
name: "RangeGTE",
input: "Row(a >= 4)",
ncalls: 1},
{
name: "RangeEQ",
input: "Row(a == 4)",
ncalls: 1},
{
name: "RangeEQNULL",
input: "Row(a == null)",
ncalls: 1},
{
name: "RangeNEQ",
input: "Row(a != 4)",
ncalls: 1},
{
name: "RangeNEQNull",
input: "Row(a != null)",
ncalls: 1},
{
name: "RangeLTLT",
input: "Row(4 < a < 9)",
ncalls: 1},
{
name: "RangeLTLTE",
input: "Row(4 < a <= 9)",
ncalls: 1},
{
name: "RangeLTELT",
input: "Row(4 <= a < 9)",
ncalls: 1},
{
name: "RangeLTELTE",
input: "Row(4 <= a <= 9)",
ncalls: 1},
{
name: "RangeTime",
input: "Row(a=4, from=2010-07-04T00:00, to=2010-08-04T00:00)",
ncalls: 1},
{
name: "RangeTimeQuotes",
input: `Row(a=4, from='2010-07-04T00:00', to="2010-08-04T00:00")`,
ncalls: 1},
{
name: "RangeTimeFromQuotes",
input: `Row(a=4, from='2010-07-04T00:00')`,
ncalls: 1},
{
name: "RangeTimeToQuotes",
input: `Row(a=4, to="2010-08-04T00:00")`,
ncalls: 1},
{
name: "Dashed Frame",
input: "Set(1, my-frame=9)",
ncalls: 1},
{
name: "newlines",
input: `Set(
1,
my-frame
=9)`,
ncalls: 1},
{
name: "OldRange",
input: "Range(blah=1, 2019-04-07T00:00, 2019-08-07T00:00)",
ncalls: 1},
}
for i, test := range tests {
t.Run(test.name+strconv.Itoa(i), func(t *testing.T) {
q, err := ParseString(test.input)
if err != nil {
t.Fatalf("parsing query '%s': %v", test.input, err)
}
if len(q.Calls) != test.ncalls {
t.Fatalf("wrong number of calls for '%s': %#v", test.input, q.Calls)
}
})
}
}
func TestPEGErrors(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "SetNoParens",
input: "Set"},
{
name: "SetBadTimestamp",
input: "Set(1, a=4, 2017-94-03T19:34)"},
{
name: "SetTimestampNoArg",
input: "Set(1, 2017-04-03T19:34)"},
{
name: "SetStartingComma",
input: "Set(, 1, a=4)"},
{
name: "StartinCommaArb",
input: "Row(, a=4)"},
{
name: "Clear0args",
input: "Clear(9)"},
{
name: "RangeTimeGT",
input: "Row(a>4, 2010-07-04T00:00, 2010-08-04T00:00)"},
{
name: "RangeTimeOneStamp",
input: "Row(a=4, 2010-07-04T00:00)"},
{
name: "ArgOutOfBounds",
input: "Row(a=9223372036854775808)"},
{
name: "ArgOutOfBoundsNeg",
input: "Row(a=-9223372036854775809)"},
}
for i, test := range tests {
t.Run(test.name+strconv.Itoa(i), func(t *testing.T) {
q, err := ParseString(test.input)
if err == nil {
t.Fatalf("parsing query '%s' - expected error, got: %s", test.input, q)
}
})
}
}
func TestPQLDeepEquality(t *testing.T) {
tests := []struct {
name string
call string
exp *Call
}{
{
name: "Set",
call: "Set(1, a=7, 2010-07-08T14:44)",
exp: &Call{
Name: "Set",
Args: map[string]interface{}{
"a": int64(7),
"_col": int64(1),
"_timestamp": "2010-07-08T14:44",
},
}},
{
name: "SetWithUnicode",
call: `Set(0, unicode="Æ<>漢д ☮♬ ♞🜻💣")`,
exp: &Call{
Name: "Set",
Args: map[string]interface{}{
"_col": int64(0),
"unicode": `Æ<EFBFBD>漢д ☮♬ ♞🜻💣`,
},
}},
{
name: "RowWithUnicode",
call: `Row(unicode="Æ<>漢д ☮♬ ♞🜻💣")`,
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"unicode": `Æ<EFBFBD>漢д ☮♬ ♞🜻💣`,
},
}},
{
name: "RowsWithUnicode",
call: `Rows(job, previous="💣")`,
exp: &Call{
Name: "Rows",
Args: map[string]interface{}{
"_field": "job",
"previous": `💣`,
},
}},
{
name: "TopNWithUnicode",
call: `TopN(stargazer, Row(unicode="Æ<>漢д ☮♬ ♞🜻💣"), a="∑")`,
exp: &Call{
Name: "TopN",
Args: map[string]interface{}{
"_field": "stargazer",
"a": "∑",
},
Children: []*Call{
{Name: "Row", Args: map[string]interface{}{"unicode": <>漢д ☮♬ ♞🜻💣"}},
},
}},
{
name: "TopK",
call: "TopK(myfield, Row(), k=7)",
exp: &Call{
Name: "TopK",
Args: map[string]interface{}{
"_field": "myfield",
"k": int64(7),
},
Children: []*Call{
{Name: "Row"},
},
}},
{
name: "TopKWithField=",
call: "TopK(field=myfield, Row(), k=7)",
exp: &Call{
Name: "TopK",
Args: map[string]interface{}{
"_field": "myfield",
"k": int64(7),
},
Children: []*Call{
{Name: "Row"},
},
}},
{
name: "Rows",
call: "Rows(myfield)",
exp: &Call{
Name: "Rows",
Args: map[string]interface{}{
"_field": "myfield",
},
}},
{
name: "RowsWithField=",
call: "Rows(field=myfield)",
exp: &Call{
Name: "Rows",
Args: map[string]interface{}{
"_field": "myfield",
},
}},
{
name: "Clear",
call: "Clear(1, a=7)",
exp: &Call{
Name: "Clear",
Args: map[string]interface{}{
"a": int64(7),
"_col": int64(1),
},
}},
{
name: "TopN",
call: "TopN(myfield, Row(), a=7)",
exp: &Call{
Name: "TopN",
Args: map[string]interface{}{
"a": int64(7),
"_field": "myfield",
},
Children: []*Call{
{Name: "Row"},
},
}},
{
name: "TopNwithField=",
call: "TopN(field=myfield, Row(), a=7)",
exp: &Call{
Name: "TopN",
Args: map[string]interface{}{
"a": int64(7),
"_field": "myfield",
},
Children: []*Call{
{Name: "Row"},
},
}},
{
name: "RangeEQ",
call: "Row(a==7)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: EQ,
Value: int64(7),
},
},
}},
{
name: "RangeLT",
call: "Row(a<7)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: LT,
Value: int64(7),
},
},
}},
{
name: "RangeLTE",
call: "Row(a<=7)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: LTE,
Value: int64(7),
},
},
}},
{
name: "RangeGTE",
call: "Row(a>=7)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: GTE,
Value: int64(7),
},
},
}},
{
name: "RangeGT",
call: "Row(a>7)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: GT,
Value: int64(7),
},
},
}},
{
name: "RangeNEQ",
call: "Row(a!=null)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: NEQ,
Value: nil,
},
},
}},
{
name: "RangeLTELT",
call: "Row(4 <= a < 9)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: BTWN_LTE_LT,
Value: []interface{}{int64(4), int64(9)},
},
},
}},
{
name: "RangeLTLT",
call: "Row(4 < a < 9)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: BTWN_LT_LT,
Value: []interface{}{int64(4), int64(9)},
},
},
}},
{
name: "RangeLTELTE",
call: "Row(4 <= a <= 9)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: BETWEEN,
Value: []interface{}{int64(4), int64(9)},
},
},
}},
{
name: "RangeLTLTE",
call: "Row(4 < a <= 9)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: BTWN_LT_LTE,
Value: []interface{}{int64(4), int64(9)},
},
},
}},
{
name: "Sum",
call: "Sum(f)",
exp: &Call{
Name: "Sum",
Args: map[string]interface{}{
"_field": "f",
},
}},
{
name: "Sum",
call: "Sum(field=f)",
exp: &Call{
Name: "Sum",
Args: map[string]interface{}{
"_field": "f",
},
}},
{
name: "Max",
call: "Max(f)",
exp: &Call{
Name: "Max",
Args: map[string]interface{}{
"_field": "f",
},
}},
{
name: "Max",
call: "Max(field=f)",
exp: &Call{
Name: "Max",
Args: map[string]interface{}{
"_field": "f",
},
}},
{
name: "Min",
call: "Min(f)",
exp: &Call{
Name: "Min",
Args: map[string]interface{}{
"_field": "f",
},
}},
{
name: "Min",
call: "Min(field=f)",
exp: &Call{
Name: "Min",
Args: map[string]interface{}{
"_field": "f",
},
}},
{
name: "Weird dash",
call: "Count(dashy-=f)",
exp: &Call{
Name: "Count",
Args: map[string]interface{}{
"dashy-": "f",
},
}},
{
name: "SumChild",
call: "Sum(Row(), field=f)",
exp: &Call{
Name: "Sum",
Args: map[string]interface{}{
"field": "f",
},
Children: []*Call{
{Name: "Row"},
},
}},
{
name: "SumChild",
call: "Sum(f, Row())",
exp: &Call{
Name: "Sum",
Args: map[string]interface{}{
"_field": "f",
},
Children: []*Call{
{Name: "Row"},
},
}},
{
name: "MinChild",
call: "Min(Row(), field=f)",
exp: &Call{
Name: "Min",
Args: map[string]interface{}{
"field": "f",
},
Children: []*Call{
{Name: "Row"},
},
}},
{
name: "MaxChild",
call: "Max(Row(), field=f)",
exp: &Call{
Name: "Max",
Args: map[string]interface{}{
"field": "f",
},
Children: []*Call{
{Name: "Row"},
},
}},
{
name: "OptionsWrapper",
call: "Options(Row(f1=123), shards=[1,2,3])",
exp: &Call{
Name: "Options",
Args: map[string]interface{}{
"shards": []interface{}{
int64(1),
int64(2),
int64(3),
},
},
Children: []*Call{
{
Name: "Row",
Args: map[string]interface{}{
"f1": int64(123),
},
},
},
}},
{
name: "GroupBy",
call: "GroupBy(Rows(), filter=Row(a=1))",
exp: &Call{
Name: "GroupBy",
Args: map[string]interface{}{
"filter": &Call{
Name: "Row",
Args: map[string]interface{}{
"a": int64(1),
},
},
},
Children: []*Call{
{Name: "Rows"},
},
}},
{
name: "GroupByFilterRangeLTLT",
call: "GroupBy(Rows(), filter=Row(4 < a < 9))",
exp: &Call{
Name: "GroupBy",
Args: map[string]interface{}{
"filter": &Call{
Name: "Row",
Args: map[string]interface{}{
"a": &Condition{
Op: BTWN_LT_LT,
Value: []interface{}{int64(4), int64(9)},
},
},
},
},
Children: []*Call{
{Name: "Rows"},
},
}},
{
name: "Variable",
call: "Row(f=$my_VAR123)",
exp: &Call{
Name: "Row",
Args: map[string]interface{}{
"f": &Variable{Name: "my_VAR123"},
},
}},
{
name: "RowsWithVariable",
call: `Rows($var)`,
exp: &Call{
Name: "Rows",
Args: map[string]interface{}{
"_field": &Variable{Name: "var"},
},
}},
}
for i, test := range tests {
t.Run(test.name+strconv.Itoa(i), func(t *testing.T) {
q, err := ParseString(test.call)
if err != nil {
t.Fatalf("parsing query '%s': %v", test.call, err)
}
if !reflect.DeepEqual(test.exp, q.Calls[0]) {
t.Fatalf("unexpected call:\n%s\ninstead of:\n%s\n'%#v'\ninstead of:\n'%#v'", q.Calls[0], test.exp, q.Calls[0], test.exp)
}
})
}
}
func TestDuplicateArgError(t *testing.T) {
tests := []struct {
name string
call string
}{
// case 1
{
name: "StringConditional",
call: "Row(a==foo, a==bar)",
},
// case 2
{
name: "StringValue",
call: "Row(a=foo, a=bar)",
},
// case 3
{
name: "IntConditional",
call: "Row(a>5, a>6)",
},
// case 4
{
name: "IntValue",
call: "Row(a=7, a=8)",
},
// case 5
{
name: "List",
call: "Row(a=[7], a=[7,8])",
},
}
for i, test := range tests {
t.Run(test.name+strconv.Itoa(i), func(t *testing.T) {
_, err := ParseString(test.call)
expErr := fmt.Sprintf("%s: a", duplicateArgErrorMessage)
if err == nil {
t.Fatalf("expected error for duplicate argument: %s", test.call)
} else if err.Error() != expErr {
t.Fatalf("expected error: %s, but got: %v", expErr, err.Error())
}
})
}
}