mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-06 00:25:55 +00:00
queries can become arbitrarily long when variables are used. This is especially the case when a variable is defined as 'select distinct field from table' and a user wants to use all the values in a Row call (which is effectively disabling any condition on the field). This change allows users to select no values for a variable associated with a Row call to disable the condition. If that variable is the only condition (query expands to nothing) then it interprets it as an All call.
259 lines
8.3 KiB
Go
259 lines
8.3 KiB
Go
// Copyright 2021 Molecula Corp. All rights reserved.
|
|
package pql_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/molecula/featurebase/v3/pql"
|
|
)
|
|
|
|
// Ensure call can be converted into a string.
|
|
func TestCall_String(t *testing.T) {
|
|
t.Run("Empty", func(t *testing.T) {
|
|
c := &pql.Call{Name: "Bitmap"}
|
|
if s := c.String(); s != `Bitmap()` {
|
|
t.Fatalf("unexpected string: %s", s)
|
|
}
|
|
})
|
|
t.Run("With Args", func(t *testing.T) {
|
|
c := &pql.Call{
|
|
Name: "Range",
|
|
Args: map[string]interface{}{
|
|
"other": "f",
|
|
"field0": &pql.Condition{Op: pql.GTE, Value: 10},
|
|
},
|
|
}
|
|
if s := c.String(); s != `Range(field0>=10, other="f")` {
|
|
t.Fatalf("unexpected string: %s", s)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Ensure condition string with subject is correct.
|
|
func TestCondition_StringWithSubj(t *testing.T) {
|
|
subj := "subj"
|
|
for _, tt := range []struct {
|
|
op pql.Token
|
|
val interface{}
|
|
exp string
|
|
}{
|
|
{pql.BETWEEN, []interface{}{int64(4), int64(8)}, "4<=subj<=8"},
|
|
{pql.BETWEEN, []interface{}{uint64(5), uint64(9)}, "5<=subj<=9"},
|
|
{pql.BETWEEN, []interface{}{pql.Decimal{Value: -401, Scale: 2}, pql.Decimal{Value: 802, Scale: 1}}, "-4.01<=subj<=80.2"},
|
|
{pql.EQ, nil, "subj==null"},
|
|
{pql.NEQ, nil, "subj!=null"},
|
|
} {
|
|
c := &pql.Condition{
|
|
Op: tt.op,
|
|
Value: tt.val,
|
|
}
|
|
if sws := c.StringWithSubj(subj); sws != tt.exp {
|
|
t.Fatalf("invalid between string. expected: %s, got %s", tt.exp, sws)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuery_ExpandVars(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
output string
|
|
vars map[string]interface{}
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "ExpandRowEQInterior",
|
|
input: `count(row(animal=$var1))`,
|
|
output: `Count(Union(Row(animal="cat"), Row(animal="dog"), Row(animal="pig")))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{"cat", "dog", "pig"}},
|
|
},
|
|
{
|
|
name: "ExpandRowEQInterior-NoValues",
|
|
input: `count(row(animal=$var1))`,
|
|
output: `Count(All())`,
|
|
vars: map[string]interface{}{"var1": []interface{}{}},
|
|
},
|
|
{
|
|
name: "ExpandRowEQExterior",
|
|
input: `row(animal=$var1)`,
|
|
output: `Union(Row(animal="cat"))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{"cat"}},
|
|
},
|
|
{
|
|
name: "ExpandRowEQExterior-NoValues",
|
|
input: `row(animal=$var1)`,
|
|
output: ``,
|
|
vars: map[string]interface{}{"var1": []interface{}{}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ExpandRowGT",
|
|
input: `count(row(num>$var1))`,
|
|
output: `Count(Union(Row(num>5), Row(num>10)))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{5, 10}},
|
|
},
|
|
{
|
|
name: "ExpandRowGT-NoValues",
|
|
input: `count(row(num>$var1))`,
|
|
output: `Count(All())`,
|
|
vars: map[string]interface{}{"var1": []interface{}{}},
|
|
},
|
|
{
|
|
name: "ExpandRowNOT",
|
|
input: `count(row(num!=$var1))`,
|
|
output: `Count(Union(Row(num!=5), Row(num!=10)))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{5, 10}},
|
|
},
|
|
{
|
|
name: "ExpandRowLTString",
|
|
input: `count(row(num<$var1))`,
|
|
output: `Count(Union(Row(num<"cat"), Row(num<"dog")))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{"cat", "dog"}},
|
|
},
|
|
{
|
|
name: "ExpandRowsInterior",
|
|
input: `GroupBy(rows($var1), limit=5)`,
|
|
output: `GroupBy(Rows(_field="cat"), Rows(_field="dog"), limit=5)`,
|
|
vars: map[string]interface{}{"var1": []interface{}{"cat", "dog"}},
|
|
},
|
|
{
|
|
name: "ExpandRowsInterior-NoValues",
|
|
input: `GroupBy(rows($var1), limit=5)`,
|
|
output: `GroupBy(), limit=5)`,
|
|
vars: map[string]interface{}{"var1": []interface{}{}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ExpandRowsExterior",
|
|
input: `rows($var1)`,
|
|
output: `Rows(_field="cat")` + "\n" + `Rows(_field="dog")`,
|
|
vars: map[string]interface{}{"var1": []interface{}{"cat", "dog"}},
|
|
},
|
|
{
|
|
name: "ExpandRowsExterior-NoValues",
|
|
input: `rows($var1)`,
|
|
output: ``,
|
|
vars: map[string]interface{}{"var1": []interface{}{}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ExpandRowAndRows",
|
|
input: `GroupBy(Rows($animal), limit=7, filter=Row(size=$size))`,
|
|
output: `GroupBy(Rows(_field="cat"), Rows(_field="dog"), filter=Union(Row(size="lg"), Row(size="md")), limit=7)`,
|
|
vars: map[string]interface{}{"animal": []interface{}{"cat", "dog"}, "size": []interface{}{"lg", "md"}},
|
|
},
|
|
{
|
|
name: "ExpandRowAndRows-NoValues1",
|
|
input: `GroupBy(Rows($animal), limit=7, filter=Row(size=$size))`,
|
|
output: `GroupBy(filter=All(), limit=7)`,
|
|
vars: map[string]interface{}{"animal": []interface{}{}, "size": []interface{}{}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ExpandRowAndRows-NoValues2",
|
|
input: `GroupBy(Rows($animal), limit=7, filter=Row(size=$size))`,
|
|
output: `GroupBy(Rows(_field="cat"), Rows(_field="dog"), filter=All(), limit=7)`,
|
|
vars: map[string]interface{}{"animal": []interface{}{"cat", "dog"}, "size": []interface{}{}},
|
|
},
|
|
{
|
|
name: "ExpandRowAndRows-NoValues3",
|
|
input: `GroupBy(Rows($animal), limit=7, filter=Row(size=$size))`,
|
|
output: `GroupBy(filter=Union(Row(size="lg"), Row(size="md")), limit=7)`,
|
|
vars: map[string]interface{}{"animal": []interface{}{}, "size": []interface{}{"lg", "md"}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ExpandBad",
|
|
input: `$animal`,
|
|
vars: map[string]interface{}{"animal": []interface{}{"cat", "dog"}, "columns": []interface{}{5, 10}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ExpandBad2",
|
|
input: `GroupBy($animal)`,
|
|
output: `Intersect(ConstRow(columns=[5, 10]), Union(Row(animal="cat"), Row(animal="dog")))`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ExpandAsCSV",
|
|
input: `Intersect(ConstRow(columns=$var2), Row(animal=$var1))`,
|
|
output: `Intersect(ConstRow(columns=[5,10]), Union(Row(animal="cat"), Row(animal="dog")))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{"cat", "dog"}, "var2": []interface{}{5, 10}},
|
|
},
|
|
{
|
|
name: "ExpandAsCSV-NoValues",
|
|
input: `Intersect(ConstRow(columns=$var2), Row(animal=$var1))`,
|
|
output: `Intersect(ConstRow(columns=[]))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{}, "var2": []interface{}{}},
|
|
},
|
|
{
|
|
name: "ExpandPercentile",
|
|
input: `Percentile(field="bytes", nth=99.0, filter=Row(level=$animal))`,
|
|
output: `Percentile(field="bytes", filter=Union(Row(level="cat"), Row(level="dog")), nth=99)`,
|
|
vars: map[string]interface{}{"animal": []interface{}{"cat", "dog"}, "columns": []interface{}{5, 10}},
|
|
},
|
|
{
|
|
name: "ExpandPercentile-NoValues",
|
|
input: `Percentile(field="bytes", nth=99.0, filter=Row(level=$animal))`,
|
|
output: `Percentile(field="bytes", filter=All(), nth=99)`,
|
|
vars: map[string]interface{}{"animal": []interface{}{}, "columns": []interface{}{}},
|
|
},
|
|
{
|
|
name: "Union-NoValues-1",
|
|
input: `Count(Union(row(x=$var1), row(y=$var2)), limit=5)`,
|
|
output: `Count(Union(Union(Row(y="cat"), Row(y="dog"))), limit=5)`,
|
|
vars: map[string]interface{}{"var1": []interface{}{}, "var2": []interface{}{"cat", "dog"}},
|
|
},
|
|
{
|
|
name: "Rows-NoValues-1",
|
|
input: `groupby(rows($var1))`,
|
|
output: `GroupBy()`,
|
|
vars: map[string]interface{}{"var1": []interface{}{}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Extract-NoValues-1",
|
|
input: `Extract(Limit(Row(animals=$var1), limit=1000), Rows($var2))`,
|
|
output: `Extract(Limit(All(), limit=1000))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{}, "var2": []interface{}{}},
|
|
},
|
|
{
|
|
name: "Extract-NoValues-2",
|
|
input: `Extract(Limit(Row(animals=$var1), limit=1000), Rows($var2))`,
|
|
output: `Extract(Limit(Union(Row(animals="a"), Row(animals="b")), limit=1000))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{"a", "b"}, "var2": []interface{}{}},
|
|
},
|
|
{
|
|
name: "Extract-NoValues-3",
|
|
input: `Extract(Limit(Row(animals=$var1), limit=1000), Rows($var2))`,
|
|
output: `Extract(Limit(All(), limit=1000), Rows(_field="a"), Rows(_field="b"))`,
|
|
vars: map[string]interface{}{"var1": []interface{}{}, "var2": []interface{}{"a", "b"}},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
q, err := pql.NewParser(strings.NewReader(tt.input)).Parse()
|
|
if err != nil {
|
|
if !tt.wantErr {
|
|
t.Errorf("Parse error = %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
got, err := q.ExpandVars(tt.vars)
|
|
if err != nil {
|
|
if !tt.wantErr {
|
|
t.Errorf("Query.ExpandVars() error = %v", err)
|
|
}
|
|
return
|
|
}
|
|
if tt.output != got.String() && !tt.wantErr {
|
|
t.Errorf("got %v, want %v", got, tt.output)
|
|
}
|
|
if tt.wantErr {
|
|
t.Error("test succeeded, but expected error")
|
|
}
|
|
})
|
|
}
|
|
}
|