498b235461
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
4502 lines
147 KiB
Go
4502 lines
147 KiB
Go
package planparserv2
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/antlr4-go/antlr/v4"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
|
|
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
|
|
"github.com/milvus-io/milvus/internal/util/function/rerank"
|
|
"github.com/milvus-io/milvus/pkg/v3/common"
|
|
"github.com/milvus-io/milvus/pkg/v3/proto/planpb"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
|
|
)
|
|
|
|
func newTestSchema(EnableDynamicField bool) *schemapb.CollectionSchema {
|
|
fields := []*schemapb.FieldSchema{
|
|
{FieldID: 0, Name: "FieldID", IsPrimaryKey: false, Description: "field no.1", DataType: schemapb.DataType_Int64},
|
|
}
|
|
|
|
for name, value := range schemapb.DataType_value {
|
|
dataType := schemapb.DataType(value)
|
|
newField := &schemapb.FieldSchema{
|
|
FieldID: int64(100 + value), Name: name + "Field", IsPrimaryKey: false, Description: "", DataType: dataType,
|
|
}
|
|
if dataType == schemapb.DataType_Array {
|
|
newField.ElementType = schemapb.DataType_Int64
|
|
}
|
|
fields = append(fields, newField)
|
|
}
|
|
if EnableDynamicField {
|
|
fields = append(fields, &schemapb.FieldSchema{
|
|
FieldID: 130, Name: common.MetaFieldName, IsPrimaryKey: false, Description: "dynamic field", DataType: schemapb.DataType_JSON,
|
|
IsDynamic: true,
|
|
})
|
|
}
|
|
|
|
fields = append(fields, &schemapb.FieldSchema{
|
|
FieldID: 131, Name: "StringArrayField", IsPrimaryKey: false, Description: "string array field",
|
|
DataType: schemapb.DataType_Array,
|
|
ElementType: schemapb.DataType_VarChar,
|
|
})
|
|
|
|
structArrayField := &schemapb.StructArrayFieldSchema{
|
|
FieldID: 132, Name: "struct_array", Fields: []*schemapb.FieldSchema{
|
|
{
|
|
FieldID: 133, Name: "struct_array[sub_str]", IsPrimaryKey: false, Description: "sub struct array field for string",
|
|
DataType: schemapb.DataType_Array,
|
|
ElementType: schemapb.DataType_VarChar,
|
|
},
|
|
{
|
|
FieldID: 134, Name: "struct_array[sub_int]", IsPrimaryKey: false, Description: "sub struct array field for int",
|
|
DataType: schemapb.DataType_Array,
|
|
ElementType: schemapb.DataType_Int32,
|
|
},
|
|
},
|
|
}
|
|
|
|
return &schemapb.CollectionSchema{
|
|
Name: "test",
|
|
Description: "schema for test used",
|
|
AutoID: true,
|
|
Fields: fields,
|
|
StructArrayFields: []*schemapb.StructArrayFieldSchema{structArrayField},
|
|
EnableDynamicField: EnableDynamicField,
|
|
}
|
|
}
|
|
|
|
func enableMatch(schema *schemapb.CollectionSchema) {
|
|
for _, field := range schema.Fields {
|
|
if typeutil.IsStringType(field.DataType) {
|
|
field.TypeParams = append(field.TypeParams, &commonpb.KeyValuePair{
|
|
Key: "enable_match", Value: "True",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func newTestSchemaHelper(t *testing.T) *typeutil.SchemaHelper {
|
|
schema := newTestSchema(true)
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(t, err)
|
|
return schemaHelper
|
|
}
|
|
|
|
func assertValidExpr(t *testing.T, helper *typeutil.SchemaHelper, exprStr string) {
|
|
expr, err := ParseExpr(helper, exprStr, nil)
|
|
assert.NoError(t, err, exprStr)
|
|
// fmt.Printf("expr: %s\n", exprStr)
|
|
assert.NotNil(t, expr, exprStr)
|
|
ShowExpr(expr)
|
|
}
|
|
|
|
func assertInvalidExpr(t *testing.T, helper *typeutil.SchemaHelper, exprStr string) {
|
|
_, err := ParseExpr(helper, exprStr, nil)
|
|
assert.Error(t, err, exprStr)
|
|
}
|
|
|
|
func TestExpr_Term(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`BoolField in [true, false]`,
|
|
`Int8Field in [1, 2]`,
|
|
`Int16Field in [3, 4]`,
|
|
`Int32Field in [5, 6]`,
|
|
`Int64Field in [7, 8]`,
|
|
`FloatField in [9.0, 10.0]`,
|
|
`DoubleField in [11.0, 12.0]`,
|
|
`StringField in ["str13", "str14"]`,
|
|
`VarCharField in ["str15", "str16"]`,
|
|
`FloatField in [1373, 115]`,
|
|
`Int64Field in [17]`,
|
|
`Int64Field in []`,
|
|
`Int64Field not in []`,
|
|
`JSONField["A"] in [1, 10]`,
|
|
`JSONField["A"] in []`,
|
|
`$meta["A"] in [1, 10]`,
|
|
`$meta["A"] in []`,
|
|
`A in [1, 10]`,
|
|
`A in []`,
|
|
`A in ["abc", "def"]`,
|
|
`A in ["1", "2", "abc", "def"]`,
|
|
`A in ["1", 2, "abc", 2.2]`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
// assertNullLiteralRejected checks that the expression is rejected specifically
|
|
// because of the bare-NULL reserved-word guard, i.e. it returns the actionable
|
|
// message rather than the misleading "field NULL not exist" (issue #50882).
|
|
func assertNullLiteralRejected(t *testing.T, helper *typeutil.SchemaHelper, exprStr string) {
|
|
_, err := ParseExpr(helper, exprStr, nil)
|
|
if assert.Error(t, err, exprStr) {
|
|
assert.Contains(t, err.Error(), "NULL literal is not supported in expressions", exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_NullLiteral(t *testing.T) {
|
|
// NULL is not a value literal. It is lexed as a bare identifier, so it must be
|
|
// rejected wherever it appears in column/value position (issue #50882). The
|
|
// behavior must be identical whether or not a dynamic field is present: without
|
|
// one a bare NULL fails a field lookup; with one it would otherwise be silently
|
|
// mistaken for a dynamic JSON key named "NULL". Use `<field> is null` /
|
|
// `is not null` to compare against null instead.
|
|
rejected := []string{
|
|
// inside an `in [...]` value list (the exact shape from the issue)
|
|
`Int64Field in [6560, NULL, 6722, -7856, -6757]`,
|
|
`Int64Field in [null]`,
|
|
`Int64Field in [NULL]`,
|
|
`Int64Field not in [1, Null]`,
|
|
`VarCharField in ["a", null, "b"]`,
|
|
`(not (not ((Int64Field is not null) and (Int64Field in [1, NULL, 2]))))`,
|
|
// NULL as the tested column on the left of `in`
|
|
`NULL in [1, 2]`,
|
|
// binary comparison, either side
|
|
`Int64Field == NULL`,
|
|
`NULL == Int64Field`,
|
|
`NULL > 5`,
|
|
`Int64Field != NULL`,
|
|
// range comparison
|
|
`1 < NULL < 5`,
|
|
`Int64Field < NULL`,
|
|
// logical / unary operands
|
|
`NULL and Int64Field > 0`,
|
|
`Int64Field > 0 or NULL`,
|
|
`not NULL`,
|
|
// function arguments
|
|
`array_length(NULL) > 0`,
|
|
`array_contains(NULL, 1)`,
|
|
// `is null` / `is not null` with NULL as the target (nonsensical)
|
|
`NULL is null`,
|
|
`NULL is not null`,
|
|
// NULL as a JSON / array subscript base — a separate lookup path
|
|
// (getColumnInfoFromJSONIdentifier) that bypasses translateIdentifier
|
|
`NULL["x"] == 1`,
|
|
`NULL[0] > 1`,
|
|
// case-insensitive: any casing of the reserved word is rejected
|
|
`Int64Field == Null`,
|
|
`Int64Field == nUlL`,
|
|
`Int64Field == NuLL`,
|
|
}
|
|
|
|
// Valid expressions that must NOT be affected by the guard.
|
|
validBoth := []string{
|
|
// the real "is null" predicates the guard points users to
|
|
`Int64Field is null`,
|
|
`Int64Field is not null`,
|
|
`JSONField["a"] is null`,
|
|
// a JSON key literally named "null" stays reachable via quoting: the base
|
|
// identifier is the field name, not "null"
|
|
`JSONField["null"] == 1`,
|
|
`JSONField['null'] == 1`,
|
|
// the string literal "null" is a value, not an identifier
|
|
`VarCharField == "null"`,
|
|
`VarCharField in ["null", "NULL"]`,
|
|
}
|
|
// Only valid when the dynamic field exists.
|
|
validDynamicOnly := []string{
|
|
`$meta["null"] == 1`,
|
|
`A == 1`, // sanity: an arbitrary dynamic key still resolves
|
|
}
|
|
|
|
for _, dynamic := range []bool{true, false} {
|
|
t.Run(fmt.Sprintf("dynamic=%v", dynamic), func(t *testing.T) {
|
|
helper, err := typeutil.CreateSchemaHelper(newTestSchema(dynamic))
|
|
assert.NoError(t, err)
|
|
|
|
for _, exprStr := range rejected {
|
|
assertNullLiteralRejected(t, helper, exprStr)
|
|
}
|
|
for _, exprStr := range validBoth {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
if dynamic {
|
|
for _, exprStr := range validDynamicOnly {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExpr_NullLiteral_LegacyNullField locks the schema-aware side of the
|
|
// bare-NULL guard: "null" only became a create-time keyword together with this
|
|
// guard, so a legacy collection may own a field literally named "null", and the
|
|
// bare identifier is the ONLY syntax that can reference a top-level scalar
|
|
// field (quoting like field["null"] reaches JSON sub-keys only). Such a field
|
|
// must stay queryable; the strict GetFieldFromName check makes it resolve while
|
|
// everything else keeps the reserved-word rejection (see errNullLiteral).
|
|
func TestExpr_NullLiteral_LegacyNullField(t *testing.T) {
|
|
withNullField := func(dataType schemapb.DataType) *typeutil.SchemaHelper {
|
|
schema := newTestSchema(true)
|
|
schema.Fields = append(schema.Fields, &schemapb.FieldSchema{
|
|
FieldID: 199, Name: "null", Description: "legacy field literally named null", DataType: dataType,
|
|
})
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(t, err)
|
|
return helper
|
|
}
|
|
|
|
t.Run("scalar field named null", func(t *testing.T) {
|
|
helper := withNullField(schemapb.DataType_Int64)
|
|
|
|
// The bare identifier resolves to the declared field, as before the guard.
|
|
assertValidExpr(t, helper, `null > 5`)
|
|
assertValidExpr(t, helper, `null in [1, 2]`)
|
|
assertValidExpr(t, helper, `Int64Field == null`)
|
|
// The guard previously rejected these outright; schema-aware turns them
|
|
// into valid "is the null-named field NULL?" predicates.
|
|
assertValidExpr(t, helper, `null is null`)
|
|
assertValidExpr(t, helper, `null is not null`)
|
|
// Strict lookup is exact-case: a differently-cased NULL does not match the
|
|
// declared field and keeps the reserved-word rejection (pre-guard it would
|
|
// have been misparsed as the dynamic JSON key $meta["NULL"]).
|
|
assertNullLiteralRejected(t, helper, `NULL > 5`)
|
|
assertNullLiteralRejected(t, helper, `Int64Field == Null`)
|
|
})
|
|
|
|
t.Run("json field named null", func(t *testing.T) {
|
|
helper := withNullField(schemapb.DataType_JSON)
|
|
|
|
// The subscript base resolves via the same schema-aware guard in
|
|
// getColumnInfoFromJSONIdentifier.
|
|
assertValidExpr(t, helper, `null["x"] == 1`)
|
|
assertValidExpr(t, helper, `null["x"] is null`)
|
|
assertNullLiteralRejected(t, helper, `NULL["x"] == 1`)
|
|
})
|
|
}
|
|
|
|
func TestExpr_Call(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
testcases := []struct {
|
|
CallExpr string
|
|
FunctionName string
|
|
ParameterNum int
|
|
}{
|
|
{`hello123()`, "hello123", 0},
|
|
{`lt(Int32Field)`, "lt", 1},
|
|
// test parens
|
|
{`lt((((Int32Field))))`, "lt", 1},
|
|
{`empty(VarCharField,)`, "empty", 1},
|
|
{`f2(Int64Field)`, "f2", 1},
|
|
{`f2(Int64Field, 4)`, "f2", 2},
|
|
{`f3(JSON_FIELD["A"], Int32Field)`, "f3", 2},
|
|
{`f5(3+3, Int32Field)`, "f5", 2},
|
|
}
|
|
for _, testcase := range testcases {
|
|
expr, err := ParseExpr(helper, testcase.CallExpr, nil)
|
|
assert.NoError(t, err, testcase)
|
|
assert.Equal(t, testcase.FunctionName, expr.GetCallExpr().FunctionName, testcase)
|
|
assert.Equal(t, testcase.ParameterNum, len(expr.GetCallExpr().FunctionParameters), testcase)
|
|
ShowExpr(expr)
|
|
}
|
|
|
|
expr, err := ParseExpr(helper, "xxx(1+1, !true, f(10+10))", nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "xxx", expr.GetCallExpr().FunctionName)
|
|
assert.Equal(t, 3, len(expr.GetCallExpr().FunctionParameters))
|
|
assert.Equal(t, int64(2), expr.GetCallExpr().GetFunctionParameters()[0].GetValueExpr().GetValue().GetInt64Val())
|
|
assert.Equal(t, false, expr.GetCallExpr().GetFunctionParameters()[1].GetValueExpr().GetValue().GetBoolVal())
|
|
assert.Equal(t, int64(20), expr.GetCallExpr().GetFunctionParameters()[2].GetCallExpr().GetFunctionParameters()[0].GetValueExpr().GetValue().GetInt64Val())
|
|
|
|
expr, err = ParseExpr(helper, "ceil(pow(1.5*Int32Field,0.58))", nil)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, expr)
|
|
}
|
|
|
|
func TestExpr_Compare(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`Int8Field < Int16Field`,
|
|
`Int16Field <= Int32Field`,
|
|
`Int32Field > Int64Field`,
|
|
`Int64Field >= FloatField`,
|
|
`FloatField == DoubleField`,
|
|
`StringField != VarCharField`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
exprStrs = []string{
|
|
`BoolField == false + true`,
|
|
`StringField == "1" + "2"`,
|
|
`BoolField == false - true`,
|
|
`StringField == "1" - "2"`,
|
|
`BoolField == false * true`,
|
|
`StringField == "1" * "2"`,
|
|
`BoolField == false / true`,
|
|
`StringField == "1" / "2"`,
|
|
`BoolField == false % true`,
|
|
`StringField == "1" % "2"`,
|
|
}
|
|
|
|
for _, exprStr := range exprStrs {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_UnaryRange(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`Int8Field < 0`,
|
|
`Int16Field <= 1`,
|
|
`Int32Field > 2`,
|
|
`Int64Field >= 3`,
|
|
`FloatField == 4.0`,
|
|
`FloatField == 2`,
|
|
`DoubleField != 5.0`,
|
|
`StringField > "str6"`,
|
|
`VarCharField <= "str7"`,
|
|
`JSONField["A"] > 10`,
|
|
`$meta["A"] > 10`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_Like(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
expr := `A like "8\\_0%"`
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
fmt.Println(plan)
|
|
assert.Equal(t, planpb.OpType_PrefixMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, "8_0", plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
expr = `A like "8_\\_0%"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
fmt.Println(plan)
|
|
assert.Equal(t, planpb.OpType_Match, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, `8_\_0%`, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
expr = `A like "8\\%-0%"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
fmt.Println(plan)
|
|
assert.Equal(t, planpb.OpType_PrefixMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, `8%-0`, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
expr = `A like "abc"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_Equal, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, "abc", plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
}
|
|
|
|
func TestExpr_RawString(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
check := func(expr string, wantOp planpb.OpType, wantVal string) {
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan, expr)
|
|
ur := plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr()
|
|
assert.Equal(t, wantOp, ur.GetOp(), expr)
|
|
assert.Equal(t, wantVal, ur.GetValue().GetStringVal(), expr)
|
|
}
|
|
|
|
// In a raw string the backslash is NOT a string-literal escape: the content
|
|
// is taken verbatim (no \n/\b/\\ processing), so == sees exactly what's typed.
|
|
check(`A == r"a\b"`, planpb.OpType_Equal, `a\b`)
|
|
check(`A == r"a\nb"`, planpb.OpType_Equal, `a\nb`)
|
|
check(`A == r"a\\b"`, planpb.OpType_Equal, `a\\b`)
|
|
|
|
// LIKE: the raw content still passes through the LIKE escape layer (\% -> %),
|
|
// exactly like BigQuery's r'\%', but needs far fewer backslashes because the
|
|
// string-literal Unquote layer is gone.
|
|
check(`A like r"abc%"`, planpb.OpType_PrefixMatch, `abc`)
|
|
check(`A like r"%abc%"`, planpb.OpType_InnerMatch, `abc`)
|
|
check(`A like r"\%"`, planpb.OpType_Equal, `%`) // literal %
|
|
check(`A like r"a\_b%"`, planpb.OpType_PrefixMatch, `a_b`)
|
|
check(`A like r"\\%"`, planpb.OpType_PrefixMatch, `\`) // prefix of one literal backslash
|
|
check(`A like r"a\\b"`, planpb.OpType_Equal, `a\b`) // literal a\b
|
|
check(`A like r'\\%'`, planpb.OpType_PrefixMatch, `\`) // single-quoted raw string
|
|
|
|
// uppercase R prefix works too
|
|
check(`A like R"abc%"`, planpb.OpType_PrefixMatch, `abc`)
|
|
// empty raw string
|
|
check(`A == r""`, planpb.OpType_Equal, ``)
|
|
// the opposite quote needs no escaping inside a raw string
|
|
check(`A == r"a'b"`, planpb.OpType_Equal, `a'b`)
|
|
check(`A == r'a"b'`, planpb.OpType_Equal, `a"b`)
|
|
|
|
// IN list accepts raw strings, taken verbatim
|
|
inPlan, err := CreateSearchPlan(helper, `A in [r"a\b", r"c\d"]`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
vals := inPlan.GetVectorAnns().GetPredicates().GetTermExpr().GetValues()
|
|
assert.Len(t, vals, 2)
|
|
assert.Equal(t, `a\b`, vals[0].GetStringVal())
|
|
assert.Equal(t, `c\d`, vals[1].GetStringVal())
|
|
|
|
// a raw string cannot end with an odd number of backslashes (unterminated)
|
|
assertInvalidExpr(t, helper, `A == r"x\"`)
|
|
|
|
// raw string works as a JSON path key; JSON keys are already verbatim, so a
|
|
// raw key yields the same nested path as a normal key.
|
|
jPlan, err := CreateSearchPlan(helper, `JSONField[r"a\b"] == 1`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
jNorm, err := CreateSearchPlan(helper, `JSONField["a\b"] == 1`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{`a\b`}, jPlan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath())
|
|
assert.Equal(t,
|
|
jNorm.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath(),
|
|
jPlan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath())
|
|
|
|
// Raw needs 2 backslashes where a normal literal needs 4 — same final operand.
|
|
rawPlan, err := CreateSearchPlan(helper, `A like r"\\%"`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
normPlan, err := CreateSearchPlan(helper, `A like "\\\\%"`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t,
|
|
normPlan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal(),
|
|
rawPlan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
// Raw string in a regex (=~) reaches the engine verbatim (no escape pass).
|
|
rePlan, err := CreateSearchPlan(helper, `A =~ r"\d+"`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
reUR := rePlan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr()
|
|
assert.Equal(t, planpb.OpType_RegexMatch, reUR.GetOp())
|
|
assert.Equal(t, `\d+`, reUR.GetValue().GetStringVal())
|
|
|
|
// --- raw strings must stay VERBATIM even for content the parser otherwise
|
|
// rewrites before/around the raw handling: CJK runes (convertHanToASCII runs
|
|
// pre-lex) and \uXXXX escapes (decodeUnicode runs on JSON paths). Issue
|
|
// #43864 follow-up — without the raw-span exemption these silently leak. ---
|
|
|
|
// CJK in a raw value reaches the matcher verbatim, not as \uXXXX.
|
|
check(`A == r"中文"`, planpb.OpType_Equal, `中文`)
|
|
check(`A like r"中%"`, planpb.OpType_PrefixMatch, `中`)
|
|
|
|
// CJK in a raw regex reaches the engine verbatim too.
|
|
check(`A =~ r"中文"`, planpb.OpType_RegexMatch, `中文`)
|
|
|
|
// A raw JSON key with a literal A is NOT unicode-decoded: the key is the
|
|
// 6 verbatim bytes, not the decoded "A".
|
|
jUni, err := CreateSearchPlan(helper, `JSONField[r"\u0041"] == 1`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{`\u0041`}, jUni.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath())
|
|
|
|
// A raw JSON key with CJK reaches the plan verbatim (regression guard: today
|
|
// this only works because decodeUnicode reverses convertHanToASCII; it must
|
|
// keep working once both passes skip raw spans).
|
|
jHan, err := CreateSearchPlan(helper, `JSONField[r"中"] == 1`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{`中`}, jHan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath())
|
|
// A raw VALUE with a literal A stays verbatim too (the value path has no
|
|
// unicode decoding either) — locks the value/key consistency.
|
|
check(`A == r"\u0041"`, planpb.OpType_Equal, `\u0041`)
|
|
|
|
// Mixed raw + normal keys in one JSON path: each segment is classified
|
|
// independently — the raw segment is verbatim, the normal segment decodes.
|
|
jMix1, err := CreateSearchPlan(helper, `JSONField[r"中"]["b"] == 1`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{`中`, `b`}, jMix1.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath())
|
|
|
|
jMix2, err := CreateSearchPlan(helper, `JSONField["a"][r"\u0041"] == 1`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{`a`, `\u0041`}, jMix2.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath())
|
|
|
|
// Single-quoted raw JSON key behaves the same as the double-quoted form.
|
|
jSq, err := CreateSearchPlan(helper, `JSONField[r'\u0041'] == 1`, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{`\u0041`}, jSq.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath())
|
|
}
|
|
|
|
// TestExpr_RawString_LikeEscapeModel exercises the LIKE escape model (issue
|
|
// #43864) end-to-end through the raw-string literal r"...". Because a raw string
|
|
// drops the string-literal Unquote layer, a backslash reaches the LIKE pattern
|
|
// layer verbatim, so these read with the same single backslash the C++ canonical
|
|
// matcher (RegexQuery.cpp) uses — no doubled/quadrupled backslashes. Each case
|
|
// asserts the optimized op and the literal operand the executor must match
|
|
// verbatim.
|
|
func TestExpr_RawString_LikeEscapeModel(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
check := func(expr string, wantOp planpb.OpType, wantVal string) {
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan, expr)
|
|
ur := plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr()
|
|
assert.Equal(t, wantOp, ur.GetOp(), expr)
|
|
assert.Equal(t, wantVal, ur.GetValue().GetStringVal(), expr)
|
|
}
|
|
|
|
// An escaped wildcard is a LITERAL byte in the operand: the optimized op
|
|
// carries a literal '%'/'_', which the C++ side matches verbatim and must NOT
|
|
// re-interpret as a wildcard.
|
|
check(`A like r"a\%bc"`, planpb.OpType_Equal, `a%bc`)
|
|
check(`A like r"a\_bc"`, planpb.OpType_Equal, `a_bc`)
|
|
check(`A like r"abc\%%"`, planpb.OpType_PrefixMatch, `abc%`)
|
|
check(`A like r"%abc\%"`, planpb.OpType_PostfixMatch, `abc%`)
|
|
check(`A like r"%abc\%%"`, planpb.OpType_InnerMatch, `abc%`)
|
|
|
|
// A literal '\%' and an UNescaped '%' coexist: the literal lands in the
|
|
// operand verbatim, while the bare '%' is the prefix/postfix/inner boundary
|
|
// the C++ matcher expands to an ANY-length span.
|
|
check(`A like r"abc\%def%"`, planpb.OpType_PrefixMatch, `abc%def`)
|
|
check(`A like r"%abc\%def"`, planpb.OpType_PostfixMatch, `abc%def`)
|
|
check(`A like r"%abc\%def%"`, planpb.OpType_InnerMatch, `abc%def`)
|
|
|
|
// A backslash escapes ANY next byte, not only wildcards: r"\a" -> literal "a".
|
|
check(`A like r"\a"`, planpb.OpType_Equal, `a`)
|
|
|
|
// A raw "\\" collapses to one literal backslash at the pattern layer.
|
|
check(`A like r"a\\b"`, planpb.OpType_Equal, `a\b`)
|
|
check(`A like r"%a\\b%"`, planpb.OpType_InnerMatch, `a\b`)
|
|
|
|
// A dangling trailing backslash cannot be written as a raw string at all — a
|
|
// raw string may not end in an odd number of backslashes (it would not
|
|
// terminate). So the unterminated raw form is a parse error, and the literal
|
|
// trailing-backslash pattern can only be expressed via a normal string, where
|
|
// it is not optimizable and falls back to OpType_Match (the C++ matcher then
|
|
// raises ExprInvalid at execution).
|
|
assertInvalidExpr(t, helper, `A like r"abc\"`)
|
|
check(`A like "abc\\"`, planpb.OpType_Match, `abc\`)
|
|
}
|
|
|
|
func TestExpr_RegexMatch(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
// --- Regex-to-LIKE optimization tests ---
|
|
|
|
// Pure literal "abc" stays RegexMatch. RE2's literal PartialMatch path is
|
|
// faster than Milvus InnerMatch in current growing-segment benchmarks.
|
|
expr := `A =~ "abc"`
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_RegexMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, "abc", plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
// "^abc" → PrefixMatch
|
|
expr = `A =~ "^abc"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_PrefixMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, "abc", plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
// "abc$" → PostfixMatch
|
|
expr = `A =~ "abc$"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_PostfixMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, "abc", plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
// "^abc$" → Equal
|
|
expr = `A =~ "^abc$"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_Equal, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, "abc", plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
// Escaped metacharacter without anchors stays RegexMatch
|
|
expr = `A =~ "file\\.txt"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_RegexMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, `file\.txt`, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
// --- Patterns that stay as RegexMatch ---
|
|
|
|
// Regex with metacharacters → stays RegexMatch
|
|
expr = `A =~ "a.*b"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_RegexMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, "a.*b", plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
// Character class → stays RegexMatch
|
|
expr = `A =~ "[0-9]+"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_RegexMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
|
|
// Empty pattern → stays RegexMatch (matches everything)
|
|
expr = `A =~ ""`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_RegexMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
|
|
// --- Patterns that MUST stay RegexMatch (never downgrade to LIKE) ---
|
|
|
|
// Unicode property \p{...}
|
|
expr = `A =~ "\\p{Han}+"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0, MetricType: "", SearchParams: "", RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_RegexMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
|
|
// Named group (?P<name>...)
|
|
expr = `A =~ "(?P<user>[a-z]+)@host"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0, MetricType: "", SearchParams: "", RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_RegexMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
|
|
// Inline flag (?m)
|
|
expr = `A =~ "(?m)^start"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0, MetricType: "", SearchParams: "", RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_RegexMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
|
|
// Shorthand class \d
|
|
expr = `A =~ "\\d{3}-\\d{4}"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0, MetricType: "", SearchParams: "", RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, planpb.OpType_RegexMatch, plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp())
|
|
|
|
// --- Error cases ---
|
|
|
|
// Invalid regex pattern — unclosed bracket
|
|
expr = `A =~ "[unclosed"`
|
|
assertInvalidExpr(t, helper, expr)
|
|
|
|
// Non-string field — should error
|
|
expr = `Int64Field =~ "abc"`
|
|
assertInvalidExpr(t, helper, expr)
|
|
|
|
// --- Negation ---
|
|
|
|
// !~ with pure literal → NOT(RegexMatch)
|
|
expr = `A !~ "abc"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
predicates := plan.GetVectorAnns().GetPredicates()
|
|
assert.Equal(t, planpb.UnaryExpr_Not, predicates.GetUnaryExpr().GetOp())
|
|
assert.Equal(t, planpb.OpType_RegexMatch, predicates.GetUnaryExpr().GetChild().GetUnaryRangeExpr().GetOp())
|
|
assert.Equal(t, "abc", predicates.GetUnaryExpr().GetChild().GetUnaryRangeExpr().GetValue().GetStringVal())
|
|
|
|
// !~ with metacharacters → NOT(RegexMatch)
|
|
expr = `A !~ "a.*b"`
|
|
plan, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
predicates = plan.GetVectorAnns().GetPredicates()
|
|
assert.Equal(t, planpb.UnaryExpr_Not, predicates.GetUnaryExpr().GetOp())
|
|
assert.Equal(t, planpb.OpType_RegexMatch, predicates.GetUnaryExpr().GetChild().GetUnaryRangeExpr().GetOp())
|
|
|
|
// --- JSON and other field types ---
|
|
validExprs := []string{
|
|
`JSONField["A"] =~ "abc"`,
|
|
`VarCharField =~ "^prefix"`,
|
|
}
|
|
for _, exprStr := range validExprs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
// --- Comprehensive tryOptimizeRegexToLike edge cases ---
|
|
|
|
// Helper to check op type for =~ expressions
|
|
checkOp := func(t *testing.T, exprStr string, expectedOp planpb.OpType) {
|
|
t.Helper()
|
|
p, e := CreateSearchPlan(helper, exprStr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0, MetricType: "", SearchParams: "", RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, e, exprStr)
|
|
assert.NotNil(t, p, exprStr)
|
|
assert.Equal(t, expectedOp,
|
|
p.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetOp(),
|
|
"wrong OpType for: %s", exprStr)
|
|
}
|
|
checkVal := func(t *testing.T, exprStr string, expectedOp planpb.OpType, expectedVal string) {
|
|
t.Helper()
|
|
p, e := CreateSearchPlan(helper, exprStr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0, MetricType: "", SearchParams: "", RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, e, exprStr)
|
|
assert.NotNil(t, p, exprStr)
|
|
ure := p.GetVectorAnns().GetPredicates().GetUnaryRangeExpr()
|
|
assert.Equal(t, expectedOp, ure.GetOp(), "wrong OpType for: %s", exprStr)
|
|
assert.Equal(t, expectedVal, ure.GetValue().GetStringVal(), "wrong value for: %s", exprStr)
|
|
}
|
|
|
|
// Optimizable patterns
|
|
checkVal(t, `A =~ "hello"`, planpb.OpType_RegexMatch, "hello")
|
|
checkVal(t, `A =~ "^hello"`, planpb.OpType_PrefixMatch, "hello")
|
|
checkVal(t, `A =~ "hello$"`, planpb.OpType_PostfixMatch, "hello")
|
|
checkVal(t, `A =~ "^hello$"`, planpb.OpType_Equal, "hello")
|
|
checkVal(t, `A =~ "^$"`, planpb.OpType_Equal, "")
|
|
checkVal(t, `A =~ "hello world"`, planpb.OpType_RegexMatch, "hello world")
|
|
checkVal(t, `A =~ "file\\.txt"`, planpb.OpType_RegexMatch, `file\.txt`)
|
|
checkVal(t, `A =~ "^file\\.txt$"`, planpb.OpType_Equal, "file.txt")
|
|
checkVal(t, `A =~ "a\\(b\\)"`, planpb.OpType_RegexMatch, `a\(b\)`)
|
|
checkVal(t, `A =~ "price\\$10"`, planpb.OpType_RegexMatch, `price\$10`)
|
|
checkVal(t, `A =~ "back\\\\slash"`, planpb.OpType_RegexMatch, `back\\slash`)
|
|
|
|
// Non-optimizable: metacharacters → stay RegexMatch
|
|
checkOp(t, `A =~ "a.*b"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "a.b"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "[a-z]"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "a+"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "a?"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "a{2}"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "(abc)"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "a|b"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ ""`, planpb.OpType_RegexMatch)
|
|
|
|
// Non-optimizable: shorthand classes, control chars, special escapes
|
|
checkOp(t, `A =~ "\\d+"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "\\w+"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "\\s"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "\\b"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "\\n"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "\\t"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "\\x41"`, planpb.OpType_RegexMatch)
|
|
|
|
// Non-optimizable: Unicode property, named groups, inline flags
|
|
checkOp(t, `A =~ "\\p{Han}"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "(?P<name>[a-z]+)"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "(?i)hello"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "(?m)^start"`, planpb.OpType_RegexMatch)
|
|
checkOp(t, `A =~ "(?s)a.b"`, planpb.OpType_RegexMatch)
|
|
|
|
// Edge: escaped $ at end should NOT be treated as anchor
|
|
checkVal(t, `A =~ "price\\$"`, planpb.OpType_RegexMatch, `price\$`)
|
|
checkVal(t, `A =~ "^price\\$"`, planpb.OpType_PrefixMatch, "price$")
|
|
}
|
|
|
|
func TestExpr_TextMatch(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`text_match(VarCharField, "query")`,
|
|
`text_match(TextField, "query")`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
enableMatch(schema)
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
unsupported := []string{
|
|
`text_match(not_exist, "query")`,
|
|
`text_match(BoolField, "query")`,
|
|
}
|
|
for _, exprStr := range unsupported {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_TextMatchFuzzy(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
// the field must enable match first, otherwise fuzzy match is rejected.
|
|
assertInvalidExpr(t, helper, `text_match_fuzzy(VarCharField, "query", max_edit_distance=1)`)
|
|
enableMatch(schema)
|
|
|
|
for _, v := range []int64{0, 1, 2} {
|
|
expr := fmt.Sprintf(`text_match_fuzzy(VarCharField, "query", max_edit_distance=%d)`, v)
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 10,
|
|
MetricType: "L2",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
|
|
ure := plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr()
|
|
assert.NotNil(t, ure)
|
|
assert.Equal(t, planpb.OpType_TextMatchFuzzy, ure.GetOp())
|
|
assert.Equal(t, "query", ure.GetValue().GetStringVal())
|
|
extra := ure.GetExtraValues()
|
|
assert.Equal(t, 1, len(extra))
|
|
assert.Equal(t, v, extra[0].GetInt64Val())
|
|
}
|
|
|
|
{
|
|
// a templated query is filled at plan time and the edit distance survives.
|
|
expr := `text_match_fuzzy(VarCharField, {q}, max_edit_distance=2)`
|
|
mv := map[string]*schemapb.TemplateValue{
|
|
"q": generateTemplateValue(schemapb.DataType_VarChar, "hello"),
|
|
}
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 10,
|
|
MetricType: "L2",
|
|
}, mv, nil)
|
|
assert.NoError(t, err, expr)
|
|
ure := plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr()
|
|
assert.NotNil(t, ure)
|
|
assert.Equal(t, planpb.OpType_TextMatchFuzzy, ure.GetOp())
|
|
assert.Equal(t, "hello", ure.GetValue().GetStringVal())
|
|
extra := ure.GetExtraValues()
|
|
assert.Equal(t, 1, len(extra))
|
|
assert.Equal(t, int64(2), extra[0].GetInt64Val())
|
|
}
|
|
|
|
{
|
|
expr := `text_match_fuzzy(VarCharField, "query", max_edit_distance=3)`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "max_edit_distance should be in [0, 2]")
|
|
}
|
|
|
|
{
|
|
// the distance argument is required by the grammar.
|
|
expr := `text_match_fuzzy(VarCharField, "query")`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
{
|
|
expr := `text_match_fuzzy(VarCharField, "query", max_edit_distance=1.5)`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
{
|
|
expr := `text_match_fuzzy(VarCharField, "query", max_edit_distance=9223372036854775808)`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid max_edit_distance value")
|
|
}
|
|
|
|
unsupported := []string{
|
|
`text_match_fuzzy(not_exist, "query", max_edit_distance=1)`,
|
|
`text_match_fuzzy(BoolField, "query", max_edit_distance=1)`,
|
|
}
|
|
for _, exprStr := range unsupported {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_TextMatchFuzzy_SoftKeyword(t *testing.T) {
|
|
// A wrong option name is rejected: the option name is a soft keyword, so
|
|
// only "max_edit_distance" (any case) is accepted.
|
|
{
|
|
schema := newTestSchema(true)
|
|
enableMatch(schema)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
expr := `text_match_fuzzy(VarCharField, "query", fuzziness=1)`
|
|
_, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "expected max_edit_distance")
|
|
}
|
|
|
|
// The option name is case-insensitive.
|
|
{
|
|
schema := newTestSchema(true)
|
|
enableMatch(schema)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
expr := `text_match_fuzzy(VarCharField, "query", MAX_EDIT_DISTANCE=1)`
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 10, MetricType: "L2",
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
}
|
|
|
|
// max_edit_distance is a soft keyword, not a reserved word: a scalar field
|
|
// literally named "max_edit_distance" stays usable in an ordinary filter
|
|
// (issue #51058 — hard-keywording it would break such collections).
|
|
{
|
|
schema := newTestSchema(false)
|
|
schema.Fields = append(schema.Fields, &schemapb.FieldSchema{
|
|
FieldID: 9999, Name: "max_edit_distance", DataType: schemapb.DataType_Int64,
|
|
})
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
expr := `max_edit_distance > 1`
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 10, MetricType: "L2",
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
}
|
|
}
|
|
|
|
func TestExpr_TextMatch_MinShouldMatch(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
enableMatch(schema)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
for _, v := range []int64{1, 2, 1000} {
|
|
expr := fmt.Sprintf(`text_match(VarCharField, "query", minimum_should_match=%d)`, v)
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 10,
|
|
MetricType: "L2",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
assert.NotNil(t, plan)
|
|
|
|
predicates := plan.GetVectorAnns().GetPredicates()
|
|
assert.NotNil(t, predicates)
|
|
ure := predicates.GetUnaryRangeExpr()
|
|
assert.NotNil(t, ure)
|
|
assert.Equal(t, planpb.OpType_TextMatch, ure.GetOp())
|
|
assert.Equal(t, "query", ure.GetValue().GetStringVal())
|
|
extra := ure.GetExtraValues()
|
|
assert.Equal(t, 1, len(extra))
|
|
assert.Equal(t, v, extra[0].GetInt64Val())
|
|
}
|
|
|
|
{
|
|
expr := `text_match(VarCharField, "query", minimum_should_match=0)`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "minimum_should_match should be >= 1")
|
|
}
|
|
|
|
{
|
|
expr := `text_match(VarCharField, "query", minimum_should_match=1001)`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "minimum_should_match should be <= 1000")
|
|
}
|
|
|
|
{
|
|
expr := `text_match(VarCharField, "query", minimum_should_match=1.5)`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
{
|
|
expr := `text_match(VarCharField, "query", minimum_should_match={min})`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
// grammar rejects placeholder before visitor; accept either parse error or visitor error
|
|
errMsg := err.Error()
|
|
assert.True(t, strings.Contains(errMsg, "mismatched input") || strings.Contains(errMsg, "minimum_should_match should be a const integer expression"), errMsg)
|
|
}
|
|
|
|
{
|
|
expr := `text_match(VarCharField, "query", minimum_should_match=9223372036854775808)`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid minimum_should_match value")
|
|
}
|
|
|
|
{
|
|
expr := `text_match(VarCharField, "\中国")`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
{
|
|
expr := `text_match(VarCharField, "query", minimum_should_match=9223372036854775808)`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid minimum_should_match value")
|
|
}
|
|
|
|
{
|
|
expr := `text_match(VarCharField, "\中国")`
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
}
|
|
|
|
func TestExpr_TextMatch_MinShouldMatch_NilValue_Coverage(t *testing.T) {
|
|
// This test is specifically to cover the error case in validateAndExtractMinShouldMatch
|
|
// which handles the edge case where minShouldMatchExpr is an ExprWithType
|
|
|
|
// Test case 1: ExprWithType with a ColumnExpr
|
|
// This will make getValueExpr return nil
|
|
exprWithColumnExpr := &ExprWithType{
|
|
expr: &planpb.Expr{
|
|
Expr: &planpb.Expr_ColumnExpr{
|
|
ColumnExpr: &planpb.ColumnExpr{
|
|
Info: &planpb.ColumnInfo{
|
|
FieldId: 100,
|
|
DataType: schemapb.DataType_Int64,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dataType: schemapb.DataType_Int64,
|
|
}
|
|
|
|
_, err := validateAndExtractMinShouldMatch(exprWithColumnExpr)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "minimum_should_match should be a const integer expression")
|
|
|
|
// Test case 2: ExprWithType with a ValueExpr but nil Value
|
|
// This will make getValueExpr return a non-nil ValueExpr but GetValue() returns nil
|
|
exprWithNilValue := &ExprWithType{
|
|
expr: &planpb.Expr{
|
|
Expr: &planpb.Expr_ValueExpr{
|
|
ValueExpr: &planpb.ValueExpr{
|
|
Value: nil,
|
|
},
|
|
},
|
|
},
|
|
dataType: schemapb.DataType_Int64,
|
|
}
|
|
|
|
_, err = validateAndExtractMinShouldMatch(exprWithNilValue)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "minimum_should_match should be a const integer expression")
|
|
|
|
// Test case 3: Valid ExprWithType with proper value
|
|
validExpr := &ExprWithType{
|
|
expr: &planpb.Expr{
|
|
Expr: &planpb.Expr_ValueExpr{
|
|
ValueExpr: &planpb.ValueExpr{
|
|
Value: NewInt(10),
|
|
},
|
|
},
|
|
},
|
|
dataType: schemapb.DataType_Int64,
|
|
}
|
|
|
|
extraVals, err := validateAndExtractMinShouldMatch(validExpr)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, extraVals)
|
|
assert.Equal(t, 1, len(extraVals))
|
|
assert.Equal(t, int64(10), extraVals[0].GetInt64Val())
|
|
}
|
|
|
|
func TestExpr_TextMatch_MinShouldMatch_Omitted(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
enableMatch(schema)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
expr := `text_match(VarCharField, "query")`
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 10,
|
|
MetricType: "L2",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, plan)
|
|
|
|
predicates := plan.GetVectorAnns().GetPredicates()
|
|
assert.NotNil(t, predicates)
|
|
ure := predicates.GetUnaryRangeExpr()
|
|
assert.NotNil(t, ure)
|
|
assert.Equal(t, planpb.OpType_TextMatch, ure.GetOp())
|
|
assert.Equal(t, "query", ure.GetValue().GetStringVal())
|
|
// When omitted, ExtraValues should be empty
|
|
assert.Equal(t, 0, len(ure.GetExtraValues()))
|
|
}
|
|
|
|
func TestExpr_TextMatch_MinShouldMatch_IntegerConstant(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
enableMatch(schema)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
expr := `text_match(VarCharField, "query", minimum_should_match=10)`
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 10,
|
|
MetricType: "L2",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, plan)
|
|
|
|
predicates := plan.GetVectorAnns().GetPredicates()
|
|
assert.NotNil(t, predicates)
|
|
ure := predicates.GetUnaryRangeExpr()
|
|
assert.NotNil(t, ure)
|
|
assert.Equal(t, planpb.OpType_TextMatch, ure.GetOp())
|
|
assert.Equal(t, "query", ure.GetValue().GetStringVal())
|
|
extra := ure.GetExtraValues()
|
|
assert.Equal(t, 1, len(extra))
|
|
assert.Equal(t, int64(10), extra[0].GetInt64Val())
|
|
}
|
|
|
|
func TestExpr_TextMatch_MinShouldMatch_NameTypos(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
enableMatch(schema)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
invalid := []string{
|
|
`text_match(VarCharField, "q", minimum_shouldmatch=1)`,
|
|
`text_match(VarCharField, "q", min_should_match=1)`,
|
|
`text_match(VarCharField, "q", minimumShouldMatch=1)`,
|
|
`text_match(VarCharField, "q", minimum-should-match=1)`,
|
|
`text_match(VarCharField, "q", minimum_should_matchx=1)`,
|
|
}
|
|
for _, expr := range invalid {
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_TextMatch_MinShouldMatch_InvalidValueTypes(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
enableMatch(schema)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
invalid := []string{
|
|
`text_match(VarCharField, "q", minimum_should_match=10*10)`,
|
|
`text_match(VarCharField, "q", minimum_should_match=nil)`,
|
|
`text_match(VarCharField, "q", minimum_should_match=)`,
|
|
`text_match(VarCharField, "q", minimum_should_match="10")`,
|
|
`text_match(VarCharField, "q", minimum_should_match=true)`,
|
|
`text_match(VarCharField, "q", minimum_should_match=a)`,
|
|
`text_match(VarCharField, "q", minimum_should_match={min})`,
|
|
`text_match(VarCharField, "q", minimum_should_match=1.0)`,
|
|
`text_match(VarCharField, "q", minimum_should_match=-1)`,
|
|
}
|
|
for _, expr := range invalid {
|
|
_, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_TextMatch_MinShouldMatch_LeadingZerosAndOctal(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
enableMatch(schema)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
{
|
|
expr := `text_match(VarCharField, "query", minimum_should_match=001)`
|
|
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{Topk: 10}, nil, nil)
|
|
assert.NoError(t, err)
|
|
ure := plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr()
|
|
extra := ure.GetExtraValues()
|
|
assert.Equal(t, 1, len(extra))
|
|
assert.Equal(t, int64(1), extra[0].GetInt64Val())
|
|
}
|
|
}
|
|
|
|
func TestExpr_TextMatch_MinShouldMatch_DuplicateOption(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
enableMatch(schema)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
expr := `text_match(VarCharField, "query", minimum_should_match=2, minimum_should_match=3)`
|
|
_, err = CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestExpr_PhraseMatch(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
enableMatch(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`phrase_match(VarCharField, "phrase")`,
|
|
`phrase_match(TextField, "phrase")`,
|
|
`phrase_match(StringField, "phrase")`,
|
|
`phrase_match(StringField, "phrase", 1)`,
|
|
`phrase_match(VarCharField, "phrase", 11223)`,
|
|
`phrase_match(StringField, "phrase", 0)`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
unsupported := []string{
|
|
`phrase_match(not_exist, "phrase")`,
|
|
`phrase_match(BoolField, "phrase")`,
|
|
`phrase_match(StringField, "phrase", -1)`,
|
|
}
|
|
for _, exprStr := range unsupported {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
unsupported = []string{
|
|
`phrase_match(StringField, "phrase", -1)`,
|
|
`phrase_match(StringField, "phrase", a)`,
|
|
`phrase_match(StringField, "phrase", -a)`,
|
|
`phrase_match(StringField, "phrase", 4294967296)`,
|
|
}
|
|
errMsgs := []string{
|
|
`"slop" should not be a negative interger. "slop" passed: -1`,
|
|
`"slop" should be a const integer expression with "uint32" value. "slop" expression passed: a`,
|
|
`"slop" should be a const integer expression with "uint32" value. "slop" expression passed: -a`,
|
|
`"slop" exceeds the range of "uint32". "slop" expression passed: 4294967296`,
|
|
}
|
|
for i, exprStr := range unsupported {
|
|
_, err := ParseExpr(helper, exprStr, nil)
|
|
assert.True(t, strings.Contains(err.Error(), errMsgs[i]), fmt.Sprintf("Error expected: %v, actual %v", errMsgs[i], err.Error()))
|
|
}
|
|
}
|
|
|
|
func TestExpr_TextField(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
invalidExprs := []string{
|
|
`TextField == "query"`,
|
|
}
|
|
|
|
for _, exprStr := range invalidExprs {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_IsNull(t *testing.T) {
|
|
schema := newTestSchema(false)
|
|
schema.EnableDynamicField = false
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`VarCharField is null`,
|
|
`VarCharField IS NULL`,
|
|
`ArrayField is null`,
|
|
`StringArrayField IS NULL`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
unsupported := []string{
|
|
`not_exist is null`,
|
|
`FloatVectorField is null`,
|
|
`BinaryVectorField is null`,
|
|
`Float16VectorField is null`,
|
|
`BFloat16VectorField is null`,
|
|
`SparseFloatVectorField is null`,
|
|
`Int8VectorField is null`,
|
|
// issue #48904: array element access with IS NULL should be
|
|
// rejected at parse time rather than raising an internal error
|
|
// at execution time.
|
|
`ArrayField[0] is null`,
|
|
`ArrayField[1] IS NULL`,
|
|
`StringArrayField[0] is null`,
|
|
}
|
|
for _, exprStr := range unsupported {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_StructArrayParentIsNull(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
schema.StructArrayFields[0].Nullable = true
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(t, err)
|
|
|
|
expr, err := ParseExpr(helper, `struct_array is null`, nil)
|
|
require.NoError(t, err)
|
|
nullExpr := expr.GetNullExpr()
|
|
require.NotNil(t, nullExpr)
|
|
assert.Equal(t, planpb.NullExpr_IsNull, nullExpr.GetOp())
|
|
assert.Equal(t, int64(133), nullExpr.GetColumnInfo().GetFieldId())
|
|
assert.Equal(t, schemapb.DataType_Array, nullExpr.GetColumnInfo().GetDataType())
|
|
assert.Equal(t, schemapb.DataType_VarChar, nullExpr.GetColumnInfo().GetElementType())
|
|
assert.True(t, nullExpr.GetColumnInfo().GetNullable())
|
|
|
|
expr, err = ParseExpr(helper, `struct_array is not null`, nil)
|
|
require.NoError(t, err)
|
|
nullExpr = expr.GetNullExpr()
|
|
require.NotNil(t, nullExpr)
|
|
assert.Equal(t, planpb.NullExpr_IsNotNull, nullExpr.GetOp())
|
|
assert.Equal(t, int64(133), nullExpr.GetColumnInfo().GetFieldId())
|
|
assert.Equal(t, schemapb.DataType_Array, nullExpr.GetColumnInfo().GetDataType())
|
|
assert.True(t, nullExpr.GetColumnInfo().GetNullable())
|
|
}
|
|
|
|
func TestExpr_VectorArrayOnlyStructParentIsNull(t *testing.T) {
|
|
schema := newTestSchema(false)
|
|
schema.StructArrayFields = append(schema.StructArrayFields, &schemapb.StructArrayFieldSchema{
|
|
FieldID: 10000,
|
|
Name: "vector_struct",
|
|
Nullable: true,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{
|
|
FieldID: 10001,
|
|
Name: "vector_struct[embeddings]",
|
|
DataType: schemapb.DataType_ArrayOfVector,
|
|
ElementType: schemapb.DataType_FloatVector,
|
|
Nullable: true,
|
|
TypeParams: []*commonpb.KeyValuePair{
|
|
{Key: common.DimKey, Value: "4"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(t, err)
|
|
|
|
for _, testcase := range []struct {
|
|
expr string
|
|
op planpb.NullExpr_NullOp
|
|
}{
|
|
{expr: `vector_struct is null`, op: planpb.NullExpr_IsNull},
|
|
{expr: `vector_struct is not null`, op: planpb.NullExpr_IsNotNull},
|
|
} {
|
|
expr, err := ParseExpr(helper, testcase.expr, nil)
|
|
require.NoError(t, err, testcase.expr)
|
|
nullExpr := expr.GetNullExpr()
|
|
require.NotNil(t, nullExpr, testcase.expr)
|
|
assert.Equal(t, testcase.op, nullExpr.GetOp(), testcase.expr)
|
|
assert.Equal(t, int64(10001), nullExpr.GetColumnInfo().GetFieldId(), testcase.expr)
|
|
assert.Equal(t, schemapb.DataType_ArrayOfVector, nullExpr.GetColumnInfo().GetDataType(), testcase.expr)
|
|
assert.Equal(t, schemapb.DataType_FloatVector, nullExpr.GetColumnInfo().GetElementType(), testcase.expr)
|
|
assert.True(t, nullExpr.GetColumnInfo().GetNullable(), testcase.expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_IsNotNull(t *testing.T) {
|
|
schema := newTestSchema(false)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`VarCharField is not null`,
|
|
`VarCharField IS NOT NULL`,
|
|
`ArrayField is not null`,
|
|
`StringArrayField IS NOT NULL`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
unsupported := []string{
|
|
`not_exist is not null`,
|
|
`FloatVectorField is not null`,
|
|
`BinaryVectorField is not null`,
|
|
`Float16VectorField is not null`,
|
|
`BFloat16VectorField is not null`,
|
|
`SparseFloatVectorField is not null`,
|
|
`Int8VectorField is not null`,
|
|
// issue #48904: array element access with IS NOT NULL should be
|
|
// rejected at parse time rather than raising an internal error
|
|
// at execution time.
|
|
`ArrayField[0] is not null`,
|
|
`ArrayField[1] IS NOT NULL`,
|
|
`StringArrayField[0] is not null`,
|
|
}
|
|
for _, exprStr := range unsupported {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_BinaryRange(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`1 < Int8Field < 2`,
|
|
`3 <= Int16Field < 4`,
|
|
`5 <= Int32Field <= 6`,
|
|
`7 < Int64Field <= 8`,
|
|
`9.0 < FloatField < 10.0`,
|
|
`11.0 < DoubleField < 12.0`,
|
|
`"str13" < StringField < "str14"`,
|
|
`"str15" < VarCharField < "str16"`,
|
|
`17 < DoubleField < 18`,
|
|
`10 < A < 25`,
|
|
|
|
`2 > Int8Field > 1`,
|
|
`4 >= Int16Field >= 3`,
|
|
`6 >= Int32Field >= 5`,
|
|
`8 >= Int64Field > 7`,
|
|
`10.0 > FloatField > 9.0`,
|
|
`12.0 > DoubleField > 11.0`,
|
|
`"str14" > StringField > "str13"`,
|
|
`"str16" > VarCharField > "str15"`,
|
|
`18 > DoubleField > 17`,
|
|
`100 > B > 14`,
|
|
`1 < JSONField < 3`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
invalidExprs := []string{
|
|
`1 < ArrayField < 3`,
|
|
`1 < A+B < 3`,
|
|
}
|
|
|
|
for _, exprStr := range invalidExprs {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_castValue(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStr := `Int64Field + 1.1 == 2.1`
|
|
expr, err := ParseExpr(helper, exprStr, nil)
|
|
assert.Error(t, err, exprStr)
|
|
assert.Nil(t, expr, exprStr)
|
|
|
|
exprStr = `FloatField +1 == 2`
|
|
expr, err = ParseExpr(helper, exprStr, nil)
|
|
assert.NoError(t, err, exprStr)
|
|
assert.NotNil(t, expr, exprStr)
|
|
assert.NotNil(t, expr.GetBinaryArithOpEvalRangeExpr())
|
|
assert.NotNil(t, expr.GetBinaryArithOpEvalRangeExpr().GetRightOperand().GetFloatVal())
|
|
assert.NotNil(t, expr.GetBinaryArithOpEvalRangeExpr().GetValue().GetFloatVal())
|
|
}
|
|
|
|
func TestExpr_BinaryArith(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`Int64Field % 10 == 9`,
|
|
`Int64Field % 10 != 9`,
|
|
`FloatField + 1.1 == 2.1`,
|
|
`A % 10 != 2`,
|
|
`Int8Field + 1 < 2`,
|
|
`Int16Field - 3 <= 4`,
|
|
`Int32Field * 5 > 6`,
|
|
`Int64Field / 7 >= 8`,
|
|
`FloatField + 11 < 12`,
|
|
`DoubleField - 13 <= 14`,
|
|
`A * 15 > 16`,
|
|
`JSONField['A'] / 17 >= 18`,
|
|
`ArrayField[0] % 19 >= 20`,
|
|
`JSONField + 15 == 16`,
|
|
`15 + JSONField == 16`,
|
|
`Int64Field + (2**3) > 0`,
|
|
`1 + FloatField > 100`,
|
|
// bitwise operators on integer fields
|
|
`(Int64Field & 4) == 4`,
|
|
`(Int64Field & 4) != 0`,
|
|
`(Int32Field | 2) == 3`,
|
|
`(Int32Field | 2) != 0`,
|
|
`(Int64Field ^ 7) == 0`,
|
|
`(Int64Field ^ 7) != 5`,
|
|
`(Int8Field & 1) == 1`,
|
|
`(Int16Field | 8) >= 8`,
|
|
`(Int32Field ^ 15) < 16`,
|
|
// bitwise on a JSON dynamic field is allowed: the value type is only
|
|
// known at runtime, so the parser cannot reject it (the executor casts
|
|
// to int64 and treats non-numeric / missing values as non-matching).
|
|
`(JSONField["A"] & 4) == 4`,
|
|
`(JSONField["B"] | 2) != 0`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
// TODO: enable these after execution backend is ready.
|
|
unsupported := []string{
|
|
`ArrayField + 15 == 16`,
|
|
`15 + ArrayField == 16`,
|
|
`Int64Field + 1.1 = 2.1`,
|
|
`Int64Field == 2.1`,
|
|
`Int64Field >= 2.1`,
|
|
`3 > Int64Field >= 2.1`,
|
|
`Int64Field + (2**-1) > 0`,
|
|
`Int64Field / 0 == 1`,
|
|
`Int64Field % 0 == 1`,
|
|
`FloatField / 0 == 1`,
|
|
`FloatField % 0 == 1`,
|
|
// bitwise ops on non-integer types are invalid
|
|
`(FloatField & 1) == 1`,
|
|
`(DoubleField | 2) == 3`,
|
|
`(FloatField ^ 4) == 0`,
|
|
// folding a bitwise op over float literals is invalid (integer-only)
|
|
`Int64Field == (1.5 & 1)`,
|
|
`(2.5 | 1) == Int64Field`,
|
|
// bitwise ops between two fields are unsupported, consistent with how
|
|
// +, -, *, /, % reject field-to-field arithmetic (right operand must be
|
|
// a constant in the BinaryArithOpEvalRange model).
|
|
`(Int64Field & Int32Field) == 4`,
|
|
`(Int64Field | Int32Field) != 0`,
|
|
`(Int64Field ^ Int32Field) == 0`,
|
|
}
|
|
for _, exprStr := range unsupported {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
// TestExpr_BitwiseArith asserts the generated plan structure for bitwise
|
|
// operators, not merely that the expression parses. A bitwise op over a field
|
|
// must fuse into a BinaryArithOpEvalRangeExpr carrying the matching ArithOpType,
|
|
// right_operand (the mask) and comparison value; a bitwise op over two integer
|
|
// literals must constant-fold into a plain comparison.
|
|
func TestExpr_BitwiseArith(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
type bitwiseCase struct {
|
|
expr string
|
|
arithOp planpb.ArithOpType
|
|
cmpOp planpb.OpType
|
|
mask int64
|
|
value int64
|
|
}
|
|
cases := []bitwiseCase{
|
|
{`(Int64Field & 4) == 4`, planpb.ArithOpType_BitAnd, planpb.OpType_Equal, 4, 4},
|
|
{`(Int64Field & 6) != 0`, planpb.ArithOpType_BitAnd, planpb.OpType_NotEqual, 6, 0},
|
|
{`(Int32Field | 2) == 3`, planpb.ArithOpType_BitOr, planpb.OpType_Equal, 2, 3},
|
|
{`(Int32Field | 8) >= 8`, planpb.ArithOpType_BitOr, planpb.OpType_GreaterEqual, 8, 8},
|
|
{`(Int64Field ^ 7) == 0`, planpb.ArithOpType_BitXor, planpb.OpType_Equal, 7, 0},
|
|
{`(Int64Field ^ 7) > 5`, planpb.ArithOpType_BitXor, planpb.OpType_GreaterThan, 7, 5},
|
|
{`(Int16Field & 1) < 1`, planpb.ArithOpType_BitAnd, planpb.OpType_LessThan, 1, 1},
|
|
{`(Int8Field | 3) <= 7`, planpb.ArithOpType_BitOr, planpb.OpType_LessEqual, 3, 7},
|
|
// reverse form (constant on the left) for the symmetric == / != ops
|
|
{`4 == (Int64Field & 4)`, planpb.ArithOpType_BitAnd, planpb.OpType_Equal, 4, 4},
|
|
{`0 != (Int32Field | 2)`, planpb.ArithOpType_BitOr, planpb.OpType_NotEqual, 2, 0},
|
|
}
|
|
for _, c := range cases {
|
|
expr, err := ParseExpr(helper, c.expr, nil)
|
|
assert.NoError(t, err, c.expr)
|
|
bao := expr.GetBinaryArithOpEvalRangeExpr()
|
|
assert.NotNil(t, bao, c.expr)
|
|
if bao == nil {
|
|
continue
|
|
}
|
|
assert.Equal(t, c.arithOp, bao.GetArithOp(), c.expr)
|
|
assert.Equal(t, c.cmpOp, bao.GetOp(), c.expr)
|
|
assert.Equal(t, c.mask, bao.GetRightOperand().GetInt64Val(), c.expr)
|
|
assert.Equal(t, c.value, bao.GetValue().GetInt64Val(), c.expr)
|
|
}
|
|
|
|
// Constant folding: a bitwise op over two integer literals collapses to a
|
|
// constant, so the comparison degrades to a plain UnaryRange on the field.
|
|
type foldCase struct {
|
|
expr string
|
|
expected int64
|
|
}
|
|
foldCases := []foldCase{
|
|
{`Int64Field == (7 & 3)`, 3}, // 7 & 3 = 3
|
|
{`Int64Field == (5 | 2)`, 7}, // 5 | 2 = 7
|
|
{`Int64Field == (6 ^ 3)`, 5}, // 6 ^ 3 = 5
|
|
}
|
|
for _, c := range foldCases {
|
|
expr, err := ParseExpr(helper, c.expr, nil)
|
|
assert.NoError(t, err, c.expr)
|
|
ure := expr.GetUnaryRangeExpr()
|
|
assert.NotNil(t, ure, c.expr)
|
|
if ure == nil {
|
|
continue
|
|
}
|
|
assert.Equal(t, planpb.OpType_Equal, ure.GetOp(), c.expr)
|
|
assert.Equal(t, c.expected, ure.GetValue().GetInt64Val(), c.expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_Value(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`1`,
|
|
`2.0`,
|
|
`true`,
|
|
`false`,
|
|
`"str"`,
|
|
`3 > 2`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
expr := handleExpr(helper, exprStr)
|
|
assert.NotNil(t, getExpr(expr).expr, exprStr)
|
|
// fmt.Printf("expr: %s\n", exprStr)
|
|
// ShowExpr(getExpr(expr).expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_Identifier(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`BoolField`,
|
|
`Int8Field`,
|
|
`Int16Field`,
|
|
`Int32Field`,
|
|
`Int64Field`,
|
|
`FloatField`,
|
|
`DoubleField`,
|
|
`StringField`,
|
|
`VarCharField`,
|
|
`JSONField["A"]`,
|
|
`$meta["A"]`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
expr := handleExpr(helper, exprStr)
|
|
assert.NotNil(t, getExpr(expr).expr, exprStr)
|
|
|
|
// fmt.Printf("expr: %s\n", exprStr)
|
|
// ShowExpr(getExpr(expr).expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_Constant(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
// ------------------- arithmetic operations ----------------
|
|
`1.0 + 2.0`,
|
|
`1.0 + 2`,
|
|
`1 + 2.0`,
|
|
`1 + 2`,
|
|
`1.0 - 2.0`,
|
|
`1.0 - 2`,
|
|
`1 - 2.0`,
|
|
`1 - 2`,
|
|
`1.0 * 2.0`,
|
|
`1.0 * 2`,
|
|
`1 * 2.0`,
|
|
`1 * 2`,
|
|
`1.0 / 2.0`,
|
|
`1.0 / 2`,
|
|
`1 / 2.0`,
|
|
`1 / 2`,
|
|
`1 % 2`,
|
|
// ------------------- logical operations ----------------
|
|
`true and false`,
|
|
`true or false`,
|
|
`!true`,
|
|
`!false`,
|
|
// ------------------- relational operations ----------------
|
|
`"1" < "2"`,
|
|
`1.0 < 2.0`,
|
|
`1.0 < 2`,
|
|
`1 < 2.0`,
|
|
`1 < 2`,
|
|
`"1" <= "2"`,
|
|
`1.0 <= 2.0`,
|
|
`1.0 <= 2`,
|
|
`1 <= 2.0`,
|
|
`1 <= 2`,
|
|
`"1" > "2"`,
|
|
`1.0 > 2.0`,
|
|
`1.0 > 2`,
|
|
`1 > 2.0`,
|
|
`1 > 2`,
|
|
`"1" >= "2"`,
|
|
`1.0 >= 2.0`,
|
|
`1.0 >= 2`,
|
|
`1 >= 2.0`,
|
|
`1 >= 2`,
|
|
`"1" == "2"`,
|
|
`1.0 == 2.0`,
|
|
`1.0 == 2`,
|
|
`1 == 2.0`,
|
|
`1 == 2`,
|
|
`true == false`,
|
|
`"1" != "2"`,
|
|
`1.0 != 2.0`,
|
|
`1.0 != 2`,
|
|
`1 != 2.0`,
|
|
`1 != 2`,
|
|
`true != false`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
expr := handleExpr(helper, exprStr)
|
|
assert.NotNil(t, getExpr(expr).expr, exprStr)
|
|
|
|
// fmt.Printf("expr: %s\n", exprStr)
|
|
// ShowExpr(getExpr(expr).expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_Combinations(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`not (Int8Field + 1 == 2)`,
|
|
`(Int16Field - 3 == 4) and (Int32Field * 5 != 6)`,
|
|
`(Int16Field - 3 == 4) AND (Int32Field * 5 != 6)`,
|
|
`(Int64Field / 7 != 8) or (Int64Field % 10 == 9)`,
|
|
`(Int64Field / 7 != 8) OR (Int64Field % 10 == 9)`,
|
|
`Int64Field > 0 && VarCharField > "0"`,
|
|
`Int64Field < 0 && VarCharField < "0"`,
|
|
`A > 50 or B < 40`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestCreateRetrievePlan(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
_, err := CreateRetrievePlan(schema, "Int64Field > 0", nil)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = CreateRetrievePlan(schema, "id > -9223372036854775808", nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCreateSearchPlan(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
_, err := CreateSearchPlan(schema, `$meta["A"] != 10`, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCreateFloat16SearchPlan(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
_, err := CreateSearchPlan(schema, `$meta["A"] != 10`, "Float16VectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCreateBFloat16earchPlan(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
_, err := CreateSearchPlan(schema, `$meta["A"] != 10`, "BFloat16VectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCreateSparseFloatVectorSearchPlan(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
_, err := CreateSearchPlan(schema, `$meta["A"] != 10`, "SparseFloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestExpr_Invalid(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`invalid expression`,
|
|
`"constant"`,
|
|
// -------------- identifier not in schema --------------
|
|
`not_in_schema`,
|
|
// ------------------------ Add/Sub ---------------------
|
|
`not_in_schema + 1`,
|
|
`1 - not_in_schema`,
|
|
`true + false`,
|
|
`"str" + "text"`,
|
|
`true + "str"`,
|
|
`true - false`,
|
|
`"str" - "text"`,
|
|
`true - "str"`,
|
|
`StringField + VarCharField`,
|
|
`StringField - 2`,
|
|
`2 + StringField`,
|
|
// ------------------------ Mul/Div/Mod ---------------------
|
|
`not_in_schema * 1`,
|
|
`1 / not_in_schema`,
|
|
`1 % not_in_schema`,
|
|
`true * false`,
|
|
`true / false`,
|
|
`true % false`,
|
|
`"str" * "text"`,
|
|
`"str" / "text"`,
|
|
`"str" % "text"`,
|
|
`2 / 0`,
|
|
`2 % 0`,
|
|
`StringField % VarCharField`,
|
|
`StringField * 2`,
|
|
`2 / StringField`,
|
|
//`JSONField / 2 == 1`,
|
|
`2 % JSONField == 1`,
|
|
`2 % Int64Field == 1`,
|
|
`ArrayField / 2 == 1`,
|
|
`2 / ArrayField == 1`,
|
|
// ----------------------- ==/!= -------------------------
|
|
//`not_in_schema != 1`, // maybe in json
|
|
//`1 == not_in_schema`, // maybe in json
|
|
`true == "str"`,
|
|
`"str" != false`,
|
|
`VarCharField != FloatField`,
|
|
`FloatField == VarCharField`,
|
|
`A == -9223372036854775809`,
|
|
// ---------------------- relational --------------------
|
|
//`not_in_schema < 1`, // maybe in json
|
|
//`1 <= not_in_schema`, // maybe in json
|
|
`true <= "str"`,
|
|
`"str" >= false`,
|
|
`VarCharField < FloatField`,
|
|
`FloatField > VarCharField`,
|
|
//`JSONField > 1`,
|
|
//`1 < JSONField`,
|
|
`ArrayField > 2`,
|
|
`2 < ArrayField`,
|
|
// https://github.com/milvus-io/milvus/issues/34139
|
|
"\"Int64Field\" > 500 && \"Int64Field\" < 1000",
|
|
"\"Int64Field\" == 500 || \"Int64Field\" != 1000",
|
|
`"str" < 100`,
|
|
`"str" <= 100`,
|
|
`"str" > 100`,
|
|
`"str" >= 100`,
|
|
`"str" == 100`,
|
|
`"str" != 100`,
|
|
// ------------------------ like ------------------------
|
|
`(VarCharField % 2) like "prefix%"`,
|
|
`FloatField like "prefix%"`,
|
|
//`value like "prefix%"`, // maybe in json
|
|
// ------------------------ term ------------------------
|
|
//`not_in_schema in [1, 2, 3]`, // maybe in json
|
|
`1 in [1, 2, 3]`,
|
|
`(Int8Field + 8) in [1, 2, 3]`,
|
|
`Int8Field in [(true + 1)]`,
|
|
`Int8Field in [Int16Field]`,
|
|
`BoolField in [4.0]`,
|
|
`VarCharField in [4.0]`,
|
|
`Int32Field in [4.0]`,
|
|
`FloatField in [5, 6.0, true]`,
|
|
`1 in A`,
|
|
// ----------------------- range -------------------------
|
|
//`1 < not_in_schema < 2`, // maybe in json
|
|
`1 < 3 < 2`,
|
|
`1 < (Int8Field + Int16Field) < 2`,
|
|
`(invalid_lower) < Int32Field < 2`,
|
|
`1 < Int32Field < (invalid_upper)`,
|
|
`(Int8Field) < Int32Field < 2`,
|
|
`1 < Int32Field < (Int16Field)`,
|
|
`1 < StringField < 2`,
|
|
`1 < BoolField < 2`,
|
|
`1.0 < Int32Field < 2.0`,
|
|
`true < FloatField < false`,
|
|
// `2 <= Int32Field <= 1`,
|
|
`2 = Int32Field = 1`,
|
|
`true = BoolField = false`,
|
|
// ----------------------- unary ------------------------
|
|
`-true`,
|
|
`!"str"`,
|
|
`!(not_in_schema)`,
|
|
`-Int32Field`,
|
|
`!(Int32Field)`,
|
|
// ----------------------- or/and ------------------------
|
|
`false or not_in_schema`,
|
|
`"str" or false`,
|
|
`BoolField OR false`,
|
|
`Int32Field OR Int64Field`,
|
|
`not_in_schema and true`,
|
|
`"str" and false`,
|
|
`Int32Field AND Int64Field`,
|
|
// -------------------- unsupported ----------------------
|
|
`1 ^ 2`,
|
|
`1 & 2`,
|
|
`1 ** 2`,
|
|
`1 << 2`,
|
|
`1 | 2`,
|
|
// -------------------- cannot be independent ----------------------
|
|
`BoolField`,
|
|
`Int64Field > 100 and BoolField`,
|
|
`!BoolField`,
|
|
// -------------------- array ----------------------
|
|
//`A == [1, 2, 3]`,
|
|
`Int64Field == [1, 2, 3]`,
|
|
`Int64Field > [1, 2, 3]`,
|
|
`Int64Field + [1, 2, 3] == 10`,
|
|
`Int64Field % [1, 2, 3] == 10`,
|
|
`[1, 2, 3] < Int64Field < [4, 5, 6]`,
|
|
`Int64Field["A"] == 123`,
|
|
`[1,2,3] == [4,5,6]`,
|
|
`[1,2,3] == 1`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
_, err := ParseExpr(helper, exprStr, nil)
|
|
assert.Error(t, err, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestCreateRetrievePlan_Invalid(t *testing.T) {
|
|
t.Run("invalid expr", func(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
_, err := CreateRetrievePlan(schema, "invalid expression", nil)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestCreateSearchPlan_Invalid(t *testing.T) {
|
|
t.Run("invalid expr", func(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
_, err := CreateSearchPlan(schema, "invalid expression", "", nil, nil, nil)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("invalid vector field", func(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
_, err := CreateSearchPlan(schema, "Int64Field > 0", "not_exist", nil, nil, nil)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("not vector type", func(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
_, err := CreateSearchPlan(schema, "Int64Field > 0", "VarCharField", nil, nil, nil)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
var listenerCnt int
|
|
|
|
type errorListenerTest struct {
|
|
antlr.DefaultErrorListener
|
|
}
|
|
|
|
func (l *errorListenerTest) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) {
|
|
listenerCnt += 1
|
|
}
|
|
|
|
func (l *errorListenerTest) ReportAmbiguity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, exact bool, ambigAlts *antlr.BitSet, configs *antlr.ATNConfigSet) {
|
|
listenerCnt += 1
|
|
}
|
|
|
|
func (l *errorListenerTest) Error() error {
|
|
return nil
|
|
}
|
|
|
|
func Test_FixErrorListenerNotRemoved(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
normal := "1 < Int32Field < (Int16Field)"
|
|
for i := 0; i < 10; i++ {
|
|
err := handleExpr(schemaHelper, normal)
|
|
err1, ok := err.(error)
|
|
assert.True(t, ok)
|
|
assert.Error(t, err1)
|
|
}
|
|
assert.True(t, listenerCnt <= 10)
|
|
}
|
|
|
|
func Test_handleExpr(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
ret1 := handleExpr(schemaHelper, "1 < Int32Field < (Int16Field)")
|
|
err1, ok := ret1.(error)
|
|
assert.True(t, ok)
|
|
assert.Error(t, err1)
|
|
}
|
|
|
|
func Test_handleExpr_empty(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
ret1 := handleExpr(schemaHelper, "")
|
|
assert.True(t, isAlwaysTrueExpr(getExpr(ret1).expr))
|
|
assert.Equal(t, schemapb.DataType_Bool, getExpr(ret1).dataType)
|
|
}
|
|
|
|
// test if handleExpr is thread-safe.
|
|
func Test_handleExpr_17126_26662(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
normal := `VarCharField == "abcd\"defg"`
|
|
|
|
n := 400
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < n; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
ret := handleExpr(schemaHelper, normal)
|
|
_, ok := ret.(error)
|
|
assert.False(t, ok)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func Test_JSONExpr(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
// search
|
|
exprs := []string{
|
|
`$meta["A"] > 90`,
|
|
`JSONField["A"] > 90`,
|
|
`A < 10`,
|
|
`JSONField["A"] <= 5`,
|
|
`$meta["A"] <= 5`,
|
|
`$meta["A"] >= 95`,
|
|
`$meta["A"] == 5`,
|
|
`$meta["A"] != 95`,
|
|
`$meta["A"] > 90 && $meta["B"] < 5`,
|
|
`$meta["A"] > 95 || $meta["B"] < 5`,
|
|
`A > 95 || $meta["B"] < 5`,
|
|
`not ($meta["A"] == 95)`,
|
|
`$meta["A"] in [90, 91, 95, 97]`,
|
|
`$meta["A"] not in [90, 91, 95, 97]`,
|
|
`$meta["C"]["0"] in [90, 91, 95, 97]`,
|
|
`$meta["C"]["0"] not in [90, 91, 95, 97]`,
|
|
`C["0"] not in [90, 91, 95, 97]`,
|
|
`C[0] in [90, 91, 95, 97]`,
|
|
`C["0"] > 90`,
|
|
`C["0"] < 90`,
|
|
`C["0"] == 90`,
|
|
`10 < C["0"] < 90`,
|
|
`100 > C["0"] > 90`,
|
|
`0 <= $meta["A"] < 5`,
|
|
`0 <= A < 5`,
|
|
`$meta["A"] + 5 == 10`,
|
|
`$meta["A"] > 10 + 5`,
|
|
`100 - 5 < $meta["A"]`,
|
|
`100 == $meta["A"] + 6`,
|
|
`exists $meta["A"]`,
|
|
`exists $meta["A"]["B"]["C"] `,
|
|
`exists $meta["A"] || exists JSONField["A"]`,
|
|
`exists $meta["A"] && exists JSONField["A"]`,
|
|
`A["B"][0] > 100`,
|
|
`$meta[0] > 100`,
|
|
`A["\"\"B\"\""] > 10`,
|
|
`A["[\"B\"]"] == "abc\"bbb\"cc"`,
|
|
`A['B'] == "abc\"bbb\"cc"`,
|
|
`A['B'] == 'abc"cba'`,
|
|
`A['B'] == 'abc\"cba'`,
|
|
`A == [1,2,3]`,
|
|
`A + 1.2 == 3.3`,
|
|
`A + 1 == 2`,
|
|
`JSONField > 0`,
|
|
`JSONField == 0`,
|
|
`JSONField < 100`,
|
|
`0 < JSONField < 100`,
|
|
`20 > JSONField > 0`,
|
|
`JSONField + 5 > 0`,
|
|
`JSONField > 2 + 5`,
|
|
`JSONField * 2 > 5`,
|
|
`JSONField / 2 > 5`,
|
|
`JSONField % 10 > 5`,
|
|
}
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func Test_InvalidExprOnJSONField(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
// search
|
|
exprs := []string{
|
|
`exists $meta`,
|
|
`exists JSONField`,
|
|
`exists ArrayField`,
|
|
`exists $meta["A"] > 10 `,
|
|
`exists Int64Field`,
|
|
`A[[""B""]] > 10`,
|
|
`A["[""B""]"] > 10`,
|
|
`A[[""B""]] > 10`,
|
|
`A[B] > 10`,
|
|
`A + B == 3.3`,
|
|
}
|
|
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err, expr)
|
|
}
|
|
}
|
|
|
|
func Test_InvalidExprWithoutJSONField(t *testing.T) {
|
|
fields := []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "id", IsPrimaryKey: true, Description: "id", DataType: schemapb.DataType_Int64},
|
|
{FieldID: 101, Name: "vector", IsPrimaryKey: false, Description: "vector", DataType: schemapb.DataType_FloatVector},
|
|
}
|
|
|
|
schema := &schemapb.CollectionSchema{
|
|
Name: "test",
|
|
Description: "schema for test used",
|
|
AutoID: true,
|
|
Fields: fields,
|
|
}
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(t, err)
|
|
expr := ""
|
|
|
|
exprs := []string{
|
|
`A == 0`,
|
|
`JSON["A"] > 0`,
|
|
`A < 100`,
|
|
`0 < JSON["A"] < 100`,
|
|
`0 < A < 100`,
|
|
`100 > JSON["A"] > 0`,
|
|
`100 > A > 0`,
|
|
}
|
|
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schemaHelper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
}
|
|
|
|
func Test_InvalidExprWithMultipleJSONField(t *testing.T) {
|
|
fields := []*schemapb.FieldSchema{
|
|
{FieldID: 100, Name: "id", IsPrimaryKey: true, Description: "id", DataType: schemapb.DataType_Int64},
|
|
{FieldID: 101, Name: "vector", IsPrimaryKey: false, Description: "vector", DataType: schemapb.DataType_FloatVector},
|
|
{FieldID: 102, Name: "json1", IsPrimaryKey: false, Description: "json field 1", DataType: schemapb.DataType_JSON},
|
|
{FieldID: 103, Name: "json2", IsPrimaryKey: false, Description: "json field 2", DataType: schemapb.DataType_JSON},
|
|
}
|
|
|
|
schema := &schemapb.CollectionSchema{
|
|
Name: "test",
|
|
Description: "schema for test used",
|
|
AutoID: true,
|
|
Fields: fields,
|
|
}
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(t, err)
|
|
|
|
expr := ""
|
|
exprs := []string{
|
|
`A == 0`,
|
|
`A in [1, 2, 3]`,
|
|
`A not in [1, 2, 3]`,
|
|
`"1" in A`,
|
|
`"1" not in A`,
|
|
}
|
|
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schemaHelper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
}
|
|
|
|
func Test_exprWithSingleQuotes(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
exprs := []string{
|
|
`'abc' < StringField < "def"`,
|
|
`'ab"c' < StringField < "d'ef"`,
|
|
`'ab\"c' < StringField < "d\'ef"`,
|
|
`'ab\'c' < StringField < "d\"ef"`,
|
|
}
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
invalidExprs := []string{
|
|
`'abc'd' < StringField < "def"`,
|
|
`'abc' < StringField < "def"g"`,
|
|
}
|
|
|
|
for _, expr = range invalidExprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
}
|
|
|
|
func Test_JSONContains(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
exprs := []string{
|
|
`json_contains(A, 10)`,
|
|
`not json_contains(A, 10)`,
|
|
`json_contains(A, 10.5)`,
|
|
`not json_contains(A, 10.5)`,
|
|
`json_contains(A, "10")`,
|
|
`not json_contains(A, "10")`,
|
|
`json_contains($meta["A"], 10)`,
|
|
`not json_contains($meta["A"], 10)`,
|
|
`json_contains(JSONField["x"], 5)`,
|
|
`not json_contains(JSONField["x"], 5)`,
|
|
`JSON_CONTAINS(JSONField["x"], 5)`,
|
|
`json_Contains(JSONField, 5)`,
|
|
`JSON_contains(JSONField, 5)`,
|
|
`json_contains(A, [1,2,3])`,
|
|
`array_contains(A, [1,2,3])`,
|
|
`array_contains(ArrayField, 1)`,
|
|
`json_contains(JSONField, 5)`,
|
|
`json_contains($meta, 1)`,
|
|
}
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func Test_InvalidJSONContains(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
exprs := []string{
|
|
`json_contains(10, A)`,
|
|
`json_contains(1, [1,2,3])`,
|
|
`json_contains([1,2,3], 1)`,
|
|
`json_contains([1,2,3], [1,2,3])`,
|
|
`json_contains([1,2,3], [1,2])`,
|
|
`json_contains(A, B)`,
|
|
`not json_contains(A, B)`,
|
|
`json_contains(A, B > 5)`,
|
|
`json_contains(StringField, "a")`,
|
|
`json_contains(A, StringField > 5)`,
|
|
`json_contains(A)`,
|
|
`json_contains(A, 5, C)`,
|
|
`json_contains(ArrayField, "abc")`,
|
|
`json_contains(ArrayField, [1,2])`,
|
|
}
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
}
|
|
|
|
func Test_isEmptyExpression(t *testing.T) {
|
|
type args struct {
|
|
s string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want bool
|
|
}{
|
|
{
|
|
args: args{s: ""},
|
|
want: true,
|
|
},
|
|
{
|
|
args: args{s: " "},
|
|
want: true,
|
|
},
|
|
{
|
|
args: args{s: "not empty"},
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equalf(t, tt.want, isEmptyExpression(tt.args.s), "isEmptyExpression(%v)", tt.args.s)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_EscapeString(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
exprs := []string{
|
|
`A == "\"" || B == '\"'`,
|
|
`A == "\n" || B == '\n'`,
|
|
`A == "\367" || B == '\367'`,
|
|
`A == "\3678" || B == '\3678'`,
|
|
`A == "ab'c\'d" || B == 'abc"de\"'`,
|
|
`A == "'" || B == '"'`,
|
|
`A == "\'" || B == '\"' || C == '\''`,
|
|
`A == "\\'" || B == '\\"' || C == '\''`,
|
|
`A == "\\\'" || B == '\\\"' || C == '\\\''`,
|
|
`A == "\\\\'" || B == '\\\\"' || C == '\\\''`,
|
|
`A == "\\\\\'" || B == '\\\\\"' || C == '\\\\\''`,
|
|
`A == "\\\\\\'" || B == '\\\\\\"' || C == '\\\\\''`,
|
|
`str2 like 'abc\"def-%'`,
|
|
`str2 like 'abc"def-%'`,
|
|
`str4 like "abc\367-%"`,
|
|
`str4 like "中国"`,
|
|
`tag == '"blue"'`,
|
|
}
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
invalidExprs := []string{
|
|
`A == "ab
|
|
c" || B == 'ab
|
|
c'`,
|
|
`A == "\423" || B == '\378'`,
|
|
`A == "\中国"`,
|
|
}
|
|
for _, expr = range invalidExprs {
|
|
plan, err := CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err)
|
|
fmt.Println(plan)
|
|
}
|
|
}
|
|
|
|
// todo add null test
|
|
|
|
func Test_JSONContainsAll(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
var plan *planpb.PlanNode
|
|
|
|
exprs := []string{
|
|
`json_contains_all(A, [1,2,3])`,
|
|
`json_contains_all(A, [1,"2",3.0])`,
|
|
`JSON_CONTAINS_ALL(A, [1,"2",3.0])`,
|
|
`array_contains_all(ArrayField, [1,2,3])`,
|
|
`array_contains_all(ArrayField, [1])`,
|
|
`array_contains_all(ArrayField, [1,2,3])`,
|
|
}
|
|
for _, expr = range exprs {
|
|
plan, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, plan.GetVectorAnns().GetPredicates().GetJsonContainsExpr())
|
|
assert.Equal(t, planpb.JSONContainsExpr_ContainsAll, plan.GetVectorAnns().GetPredicates().GetJsonContainsExpr().GetOp())
|
|
}
|
|
|
|
invalidExprs := []string{
|
|
`JSON_CONTAINS_ALL(A, 1)`,
|
|
`JSON_CONTAINS_ALL(A, [abc])`,
|
|
`JSON_CONTAINS_ALL(A, [2>a])`,
|
|
`JSON_CONTAINS_ALL(A, [2>>a])`,
|
|
`JSON_CONTAINS_ALL(A[""], [1,2,3])`,
|
|
`JSON_CONTAINS_ALL(Int64Field, [1,2,3])`,
|
|
`JSON_CONTAINS_ALL(A, B)`,
|
|
`JSON_CONTAINS_ALL(ArrayField, [[1,2,3]])`,
|
|
}
|
|
for _, expr = range invalidExprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
}
|
|
|
|
func Test_JSONContainsAny(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
var plan *planpb.PlanNode
|
|
|
|
exprs := []string{
|
|
`json_contains_any(A, [1,2,3])`,
|
|
`json_contains_any(A, [1,"2",3.0])`,
|
|
`JSON_CONTAINS_ANY(A, [1,"2",3.0])`,
|
|
`JSON_CONTAINS_ANY(ArrayField, [1,2,3])`,
|
|
}
|
|
for _, expr = range exprs {
|
|
plan, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, plan.GetVectorAnns().GetPredicates().GetJsonContainsExpr())
|
|
assert.Equal(t, planpb.JSONContainsExpr_ContainsAny, plan.GetVectorAnns().GetPredicates().GetJsonContainsExpr().GetOp())
|
|
}
|
|
|
|
invalidExprs := []string{
|
|
`JSON_CONTAINS_ANY(A, 1)`,
|
|
`JSON_CONTAINS_ANY(A, [abc])`,
|
|
`JSON_CONTAINS_ANY(A, [2>a])`,
|
|
`JSON_CONTAINS_ANY(A, [2>>a])`,
|
|
`JSON_CONTAINS_ANY(A[""], [1,2,3])`,
|
|
`JSON_CONTAINS_ANY(Int64Field, [1,2,3])`,
|
|
`JSON_CONTAINS_ANY(ArrayField, [[1,2,3]])`,
|
|
`JSON_CONTAINS_ANY(A, B)`,
|
|
}
|
|
for _, expr = range invalidExprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err)
|
|
}
|
|
}
|
|
|
|
func Test_ArrayExpr(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
|
|
exprs := []string{
|
|
`ArrayField == [1,2,3,4]`,
|
|
`ArrayField[0] == 1`,
|
|
`ArrayField[0] > 1`,
|
|
`1 < ArrayField[0] < 3`,
|
|
`StringArrayField[0] == "abc"`,
|
|
`StringArrayField[0] < "abc"`,
|
|
`"abc" < StringArrayField[0] < "efg"`,
|
|
`array_contains(ArrayField, 1)`,
|
|
`not ARRAY_CONTAINS(ArrayField, 1)`,
|
|
`array_contains_all(ArrayField, [1,2,3,4])`,
|
|
`not ARRAY_CONTAINS_ALL(ArrayField, [1,2,3,4])`,
|
|
`array_contains_any(ArrayField, [1,2,3,4])`,
|
|
`not ARRAY_CONTAINS_ANY(ArrayField, [1,2,3,4])`,
|
|
`array_contains(StringArrayField, "abc")`,
|
|
`not ARRAY_CONTAINS(StringArrayField, "abc")`,
|
|
`array_contains_all(StringArrayField, ["a", "b", "c", "d"])`,
|
|
`not ARRAY_CONTAINS_ALL(StringArrayField, ["a", "b", "c", "d"])`,
|
|
`array_contains_any(StringArrayField, ["a", "b", "c", "d"])`,
|
|
`not ARRAY_CONTAINS_ANY(StringArrayField, ["a", "b", "c", "d"])`,
|
|
`StringArrayField[0] like "abd%"`,
|
|
`+ArrayField[0] == 1`,
|
|
`ArrayField[0] % 3 == 1`,
|
|
`ArrayField[0] + 3 == 1`,
|
|
`ArrayField[0] in [1,2,3]`,
|
|
`ArrayField[0] in []`,
|
|
`0 < ArrayField[0] < 100`,
|
|
`100 > ArrayField[0] > 0`,
|
|
`ArrayField[0] > 1`,
|
|
`ArrayField[0] == 1`,
|
|
`ArrayField in []`,
|
|
}
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
}
|
|
|
|
invalidExprs := []string{
|
|
`ArrayField == ["abc", "def"]`,
|
|
`"abc" < ArrayField[0] < "def"`,
|
|
`ArrayField[0] == "def"`,
|
|
`ArrayField["0"] == 1`,
|
|
`array_contains(ArrayField, "a")`,
|
|
`array_contains(StringArrayField, 1)`,
|
|
`array_contains_all(StringArrayField, ["abc", 123])`,
|
|
`array_contains_any(StringArrayField, ["abc", 123])`,
|
|
`StringArrayField like "abd%"`,
|
|
`+ArrayField == 1`,
|
|
`ArrayField % 3 == 1`,
|
|
`ArrayField + 3 == 1`,
|
|
`ArrayField in [1,2,3]`,
|
|
`ArrayField[0] in [1, "abc",3.3]`,
|
|
`0 < ArrayField < 100`,
|
|
`100 > ArrayField > 0`,
|
|
`ArrayField > 1`,
|
|
`ArrayField == 1`,
|
|
`ArrayField[] == 1`,
|
|
`A[] == 1`,
|
|
`ArrayField[0] + ArrayField[1] == 1`,
|
|
`ArrayField == []`,
|
|
}
|
|
for _, expr = range invalidExprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err, expr)
|
|
}
|
|
}
|
|
|
|
func Test_ArrayLength(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
expr := ""
|
|
var err error
|
|
|
|
exprs := []string{
|
|
`array_length(ArrayField) == 10`,
|
|
`array_length(A) != 10`,
|
|
`array_length(StringArrayField) == 1`,
|
|
`array_length(B) != 1`,
|
|
`not (array_length(C[0]) == 1)`,
|
|
`not (array_length(C["D"]) != 1)`,
|
|
`array_length(StringArrayField) < 1`,
|
|
`array_length(StringArrayField) <= 1`,
|
|
`array_length(StringArrayField) > 5`,
|
|
`array_length(StringArrayField) >= 5`,
|
|
// struct array sub-field
|
|
`array_length(struct_array) == 2`,
|
|
`array_length(struct_array[sub_str]) == 3`,
|
|
`array_length(struct_array[sub_int]) > 1`,
|
|
`array_length(struct_array[sub_int]) <= 10`,
|
|
}
|
|
for _, expr = range exprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
}
|
|
|
|
invalidExprs := []string{
|
|
`array_length(a > b) == 0`,
|
|
`array_length(a, b) == 1`,
|
|
`array_length(A)`,
|
|
`array_length("A") / 10 == 2`,
|
|
`array_length(Int64Field) == 2`,
|
|
`array_length(a-b) == 2`,
|
|
`0 < array_length(a-b) < 2`,
|
|
`0 < array_length(StringArrayField) < 1`,
|
|
`100 > array_length(ArrayField) > 10`,
|
|
`array_length(A) % 10 == 2`,
|
|
`array_length(A) / 10 == 2`,
|
|
`array_length(A) + 1 == 2`,
|
|
`array_length(JSONField) + 1 == 2`,
|
|
`array_length(A) == 2.2`,
|
|
}
|
|
for _, expr = range invalidExprs {
|
|
_, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.Error(t, err, expr)
|
|
}
|
|
}
|
|
|
|
// Test randome sample with all other predicate expressions.
|
|
func TestRandomSampleWithFilter(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
enableMatch(schema)
|
|
assert.NoError(t, err)
|
|
|
|
exprStrs := []string{
|
|
`random_sample(0.01)`,
|
|
`random_sample(0.9999)`,
|
|
`BoolField in [true, false] && random_sample(0.01)`,
|
|
`Int8Field < Int16Field && random_sample(0.01)`,
|
|
`Int8Field < Int16Field && Int16Field <= 1 && random_sample(0.01)`,
|
|
`(Int8Field < Int16Field || Int16Field <= 1) && random_sample(0.01)`,
|
|
`Int16Field <= 1 && random_sample(0.01)`,
|
|
`VarCharField like "prefix%" && random_sample(0.01)`,
|
|
`VarCharField is null && random_sample(0.01)`,
|
|
`VarCharField IS NOT NULL && random_sample(0.01)`,
|
|
`11.0 < DoubleField < 12.0 && random_sample(0.01)`,
|
|
`1 < JSONField < 3 && random_sample(0.01)`,
|
|
`Int64Field + 1 == 2 && random_sample(0.01)`,
|
|
`Int64Field % 10 != 9 && random_sample(0.01)`,
|
|
`A * 15 > 16 && random_sample(0.01)`,
|
|
`(Int16Field - 3 == 4) and (Int32Field * 5 != 6) && random_sample(0.01)`,
|
|
}
|
|
for _, exprStr := range exprStrs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
exprStrsInvalid := []string{
|
|
`random_sample(a)`,
|
|
`random_sample(1)`,
|
|
`random_sample(1.1)`,
|
|
`random_sample(0)`,
|
|
`random_sample(-1)`,
|
|
`random_sample(0.01, Int8Field < Int16Field) + 1`,
|
|
`random_sample(0.01, Int8Field < Int16Field) ** 2 == 1`,
|
|
`not random_sample(0.01, Int8Field < Int16Field)`,
|
|
`(Int16Field - 3 == 4) || random_sample(0.01)`,
|
|
`random_sample(0.01) and (Int16Field - 3 == 4)`,
|
|
`random_sample(0.01) or (Int16Field - 3 == 4)`,
|
|
`random_sample(0.01, FloatField)`,
|
|
`random_sample(0.01, 1.0 + 2.0)`,
|
|
`random_sample(0.01, false)`,
|
|
`random_sample(0.01, text_match(VarCharField, "query"))`,
|
|
`random_sample(0.01, phrase_match(VarCharField, "query something"))`,
|
|
`Int8Field < Int16Field || Int16Field <= 1 && random_sample(0.01)`,
|
|
}
|
|
for _, exprStr := range exprStrsInvalid {
|
|
assertInvalidExpr(t, helper, exprStr)
|
|
}
|
|
}
|
|
|
|
func Test_SegmentScorers(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// helper to build a boost segment scorer function
|
|
makeBoostRanker := func(filter string, weight string) *schemapb.FunctionSchema {
|
|
params := []*commonpb.KeyValuePair{
|
|
{Key: rerank.WeightKey, Value: weight},
|
|
{Key: "reranker", Value: rerank.BoostName},
|
|
}
|
|
if filter != "" {
|
|
params = append(params, &commonpb.KeyValuePair{Key: rerank.FilterKey, Value: filter})
|
|
}
|
|
return &schemapb.FunctionSchema{
|
|
Params: params,
|
|
}
|
|
}
|
|
|
|
t.Run("ok - single boost scorer", func(t *testing.T) {
|
|
fs := &schemapb.FunctionScore{
|
|
Functions: []*schemapb.FunctionSchema{
|
|
makeBoostRanker("Int64Field > 0", "1.5"),
|
|
},
|
|
}
|
|
plan, err := CreateSearchPlan(schema, "", "FloatVectorField", &planpb.QueryInfo{GroupByFieldId: -1}, nil, fs)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, 1, len(plan.Scorers))
|
|
// filter should be parsed into Expr when provided
|
|
assert.NotNil(t, plan.Scorers[0])
|
|
})
|
|
|
|
t.Run("ok - multiple boost scorers", func(t *testing.T) {
|
|
fs := &schemapb.FunctionScore{
|
|
Functions: []*schemapb.FunctionSchema{
|
|
makeBoostRanker("Int64Field > 0", "1.0"),
|
|
makeBoostRanker("", "2.0"),
|
|
},
|
|
}
|
|
plan, err := CreateSearchPlan(schema, "", "FloatVectorField", &planpb.QueryInfo{GroupByFieldId: -1}, nil, fs)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, 2, len(plan.Scorers))
|
|
})
|
|
|
|
t.Run("error - not segment scorer flag", func(t *testing.T) {
|
|
fs := &schemapb.FunctionScore{
|
|
Functions: []*schemapb.FunctionSchema{{Params: []*commonpb.KeyValuePair{{Key: "reranker", Value: "weighted"}}}},
|
|
}
|
|
plan, err := CreateSearchPlan(schema, "", "FloatVectorField", &planpb.QueryInfo{GroupByFieldId: -1}, nil, fs)
|
|
assert.NoError(t, err)
|
|
// not segment scorer means ignored
|
|
assert.NotNil(t, plan)
|
|
assert.Equal(t, 0, len(plan.Scorers))
|
|
})
|
|
|
|
t.Run("error - missing weight", func(t *testing.T) {
|
|
// missing weight should cause CreateSearchScorer to fail
|
|
fs := &schemapb.FunctionScore{
|
|
Functions: []*schemapb.FunctionSchema{
|
|
{Params: []*commonpb.KeyValuePair{
|
|
{Key: "reranker", Value: rerank.BoostName},
|
|
// no weight
|
|
}},
|
|
},
|
|
}
|
|
_, err := CreateSearchPlan(schema, "", "FloatVectorField", &planpb.QueryInfo{}, nil, fs)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("error - invalid weight format", func(t *testing.T) {
|
|
fs := &schemapb.FunctionScore{
|
|
Functions: []*schemapb.FunctionSchema{
|
|
makeBoostRanker("", "invalid_float"),
|
|
},
|
|
}
|
|
_, err := CreateSearchPlan(schema, "", "FloatVectorField", &planpb.QueryInfo{}, nil, fs)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("error - scorer with group_by", func(t *testing.T) {
|
|
fs := &schemapb.FunctionScore{
|
|
Functions: []*schemapb.FunctionSchema{
|
|
makeBoostRanker("", "1.0"),
|
|
},
|
|
}
|
|
_, err := CreateSearchPlan(schema, "", "FloatVectorField", &planpb.QueryInfo{GroupByFieldId: 100}, nil, fs)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("error - scorer with search_iterator_v2", func(t *testing.T) {
|
|
fs := &schemapb.FunctionScore{
|
|
Functions: []*schemapb.FunctionSchema{
|
|
makeBoostRanker("", "1.0"),
|
|
},
|
|
}
|
|
_, err := CreateSearchPlan(schema, "", "FloatVectorField", &planpb.QueryInfo{SearchIteratorV2Info: &planpb.SearchIteratorV2Info{}}, nil, fs)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestConcurrency(t *testing.T) {
|
|
schemaHelper := newTestSchemaHelper(t)
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(10)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 10; j++ {
|
|
r := handleExpr(schemaHelper, fmt.Sprintf("array_length(ArrayField) == %d", j))
|
|
err := getError(r)
|
|
assert.NoError(t, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func BenchmarkPlanCache(b *testing.B) {
|
|
schema := newTestSchema(true)
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(b, err)
|
|
|
|
b.ResetTimer()
|
|
|
|
b.Run("cached", func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
r := handleExpr(schemaHelper, "array_length(ArrayField) == 10")
|
|
err := getError(r)
|
|
assert.NoError(b, err)
|
|
}
|
|
})
|
|
|
|
b.Run("uncached", func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
r := handleExpr(schemaHelper, fmt.Sprintf("array_length(ArrayField) == %d", i))
|
|
err := getError(r)
|
|
assert.NoError(b, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func randomChineseString(length int) string {
|
|
min := 0x4e00
|
|
max := 0x9fa5
|
|
|
|
result := make([]rune, length)
|
|
for i := 0; i < length; i++ {
|
|
result[i] = rune(rand.Intn(max-min+1) + min)
|
|
}
|
|
|
|
return string(result)
|
|
}
|
|
|
|
func BenchmarkWithString(b *testing.B) {
|
|
schema := newTestSchema(true)
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(b, err)
|
|
|
|
expr := ""
|
|
for i := 0; i < 100; i++ {
|
|
expr += fmt.Sprintf(`"%s",`, randomChineseString(rand.Intn(100)))
|
|
}
|
|
expr = "StringField in [" + expr + "]"
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
plan, err := CreateSearchPlan(schemaHelper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(b, err)
|
|
assert.NotNil(b, plan)
|
|
}
|
|
}
|
|
|
|
func Test_convertHanToASCII(t *testing.T) {
|
|
type testcase struct {
|
|
source string
|
|
target string
|
|
}
|
|
testcases := []testcase{
|
|
{`A in ["中国"]`, `A in ["\u4e2d\u56fd"]`},
|
|
{`A in ["\中国"]`, `A in ["\中国"]`},
|
|
{`A in ["\\中国"]`, `A in ["\\\u4e2d\u56fd"]`},
|
|
}
|
|
|
|
for _, c := range testcases {
|
|
assert.Equal(t, c.target, convertHanToASCII(c.source))
|
|
}
|
|
}
|
|
|
|
func BenchmarkTemplateWithString(b *testing.B) {
|
|
schema := newTestSchema(true)
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(b, err)
|
|
|
|
elements := make([]interface{}, 100)
|
|
for i := 0; i < 100; i++ {
|
|
elements[i] = generateTemplateValue(schemapb.DataType_String, fmt.Sprintf(`"%s",`, randomChineseString(rand.Intn(100))))
|
|
}
|
|
expr := "StringField in {list}"
|
|
|
|
mv := map[string]*schemapb.TemplateValue{
|
|
"list": generateTemplateValue(schemapb.DataType_Array, elements),
|
|
}
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
plan, err := CreateSearchPlan(schemaHelper, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, mv, nil)
|
|
assert.NoError(b, err)
|
|
assert.NotNil(b, plan)
|
|
}
|
|
}
|
|
|
|
func TestNestedPathWithChinese(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
expr := `A["姓名"] == "小明"`
|
|
plan, err := CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
paths := plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath()
|
|
assert.NotNil(t, paths)
|
|
assert.Equal(t, 2, len(paths))
|
|
assert.Equal(t, "A", paths[0])
|
|
assert.Equal(t, "姓名", paths[1])
|
|
|
|
expr = `A["年份"]["月份"] == "九月"`
|
|
plan, err = CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
paths = plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath()
|
|
assert.NotNil(t, paths)
|
|
assert.Equal(t, 3, len(paths))
|
|
assert.Equal(t, "A", paths[0])
|
|
assert.Equal(t, "年份", paths[1])
|
|
assert.Equal(t, "月份", paths[2])
|
|
}
|
|
|
|
func Test_JSONPathNullExpr(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
exprPairs := [][]string{
|
|
{`A["a"] is null`, `not exists A["a"]`},
|
|
{`A["a"] is not null`, `exists A["a"]`},
|
|
{`dyn_field is null`, `not exists dyn_field`},
|
|
{`dyn_field is not null`, `exists dyn_field`},
|
|
}
|
|
|
|
for _, expr := range exprPairs {
|
|
plan, err := CreateSearchPlan(schema, expr[0], "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, plan)
|
|
|
|
plan2, err := CreateSearchPlan(schema, expr[1], "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, plan2)
|
|
|
|
planStr, err := proto.Marshal(plan)
|
|
assert.NoError(t, err)
|
|
plan2Str, err := proto.Marshal(plan2)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, planStr, plan2Str)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// GIS Functions Tests
|
|
// ============================================================================
|
|
|
|
func TestExpr_GISFunctions(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test valid GIS function expressions
|
|
validExprs := []string{
|
|
// ST_EQUALS tests
|
|
`st_equals(GeometryField, "POINT(0 0)")`,
|
|
`ST_EQUALS(GeometryField, "POINT(1.5 2.3)")`,
|
|
`st_equals(GeometryField, "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))")`,
|
|
`st_equals(GeometryField, "LINESTRING(0 0, 1 1, 2 2)")`,
|
|
`st_equals(GeometryField, "MULTIPOINT((0 0), (1 1))")`,
|
|
|
|
// ST_INTERSECTS tests
|
|
`st_intersects(GeometryField, "POINT(0 0)")`,
|
|
`ST_INTERSECTS(GeometryField, "POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))")`,
|
|
`st_intersects(GeometryField, "LINESTRING(-1 -1, 1 1)")`,
|
|
|
|
// ST_CONTAINS tests
|
|
`st_contains(GeometryField, "POINT(0.5 0.5)")`,
|
|
`ST_CONTAINS(GeometryField, "POLYGON((-1 -1, 1 -1, 1 1, -1 1, -1 -1))")`,
|
|
|
|
// ST_WITHIN tests
|
|
`st_within(GeometryField, "POLYGON((-2 -2, 2 -2, 2 2, -2 2, -2 -2))")`,
|
|
`ST_WITHIN(GeometryField, "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))")`,
|
|
|
|
// ST_TOUCHES tests
|
|
`st_touches(GeometryField, "POINT(1 1)")`,
|
|
`ST_TOUCHES(GeometryField, "LINESTRING(0 0, 1 0)")`,
|
|
|
|
// ST_OVERLAPS tests
|
|
`st_overlaps(GeometryField, "POLYGON((0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, 0.5 0.5))")`,
|
|
`ST_OVERLAPS(GeometryField, "POLYGON((-0.5 -0.5, 0.5 -0.5, 0.5 0.5, -0.5 0.5, -0.5 -0.5))")`,
|
|
|
|
// ST_CROSSES tests
|
|
`st_crosses(GeometryField, "LINESTRING(-1 0, 1 0)")`,
|
|
`ST_CROSSES(GeometryField, "LINESTRING(0 -1, 0 1)")`,
|
|
|
|
// ST_DWITHIN tests
|
|
`st_dwithin(GeometryField, "POINT(0 0)", 1.0)`,
|
|
`ST_DWITHIN(GeometryField, "POINT(1 1)", 5)`,
|
|
`st_dwithin(GeometryField, "POINT(2.5 3.7)", 10.5)`,
|
|
`ST_DWITHIN(GeometryField, "POINT(0.5 0.5)", 2.0)`,
|
|
`st_dwithin(GeometryField, "POINT(1.0 1.0)", 1)`,
|
|
|
|
// ST_ISVALID tests
|
|
`st_isvalid(GeometryField)`,
|
|
`ST_ISVALID(GeometryField)`,
|
|
|
|
// Case insensitive tests
|
|
`St_Equals(GeometryField, "POINT(0 0)")`,
|
|
`sT_iNtErSeCts(GeometryField, "POINT(1 1)")`,
|
|
`St_DWithin(GeometryField, "POINT(0 0)", 5.0)`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_GISFunctionsInvalidExpressions(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test invalid GIS function expressions
|
|
invalidExprs := []string{
|
|
// Invalid field type
|
|
`st_equals(Int64Field, "POINT(0 0)")`,
|
|
`st_intersects(StringField, "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))")`,
|
|
`st_dwithin(BoolField, "POINT(0 0)", 1.0)`,
|
|
|
|
// Invalid WKT strings
|
|
`st_equals(GeometryField, "INVALID WKT")`,
|
|
`st_intersects(GeometryField, "POINT()")`,
|
|
`st_contains(GeometryField, "POLYGON((0 0, 1 0))")`, // Unclosed polygon
|
|
`st_within(GeometryField, "LINESTRING(0)")`, // Incomplete linestring
|
|
|
|
// Missing parameters
|
|
`st_equals(GeometryField)`,
|
|
`st_intersects()`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)")`, // Missing distance parameter
|
|
`st_contains(GeometryField, "POINT(0 0)", 1.0)`, // Extra parameter
|
|
|
|
// Invalid distance parameter for ST_DWITHIN
|
|
`st_dwithin(GeometryField, "POINT(0 0)", "abc")`, // String parameter
|
|
`st_dwithin(GeometryField, "POINT(0 0)", "invalid")`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", -1.0)`, // Negative distance
|
|
`st_dwithin(GeometryField, "POINT(0 0)", true)`, // Boolean instead of number
|
|
|
|
// Non-existent fields
|
|
`st_equals(NonExistentField, "POINT(0 0)")`,
|
|
`st_dwithin(UnknownGeometryField, "POINT(0 0)", 5.0)`,
|
|
|
|
// ST_ISVALID invalid usage
|
|
`st_isvalid(Int64Field)`,
|
|
`st_isvalid()`,
|
|
`st_isvalid(GeometryField, 1)`,
|
|
}
|
|
|
|
for _, expr := range invalidExprs {
|
|
assertInvalidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_GISFunctionsComplexExpressions(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test complex GIS expressions with logical operators
|
|
complexExprs := []string{
|
|
// AND combinations
|
|
`st_equals(GeometryField, "POINT(0 0)") and st_intersects(GeometryField, "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))")`,
|
|
`st_contains(GeometryField, "POINT(0.5 0.5)") AND st_within(GeometryField, "POLYGON((-1 -1, 1 -1, 1 1, -1 1, -1 -1))")`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", 5.0) and Int64Field > 100`,
|
|
`st_isvalid(GeometryField) and Int64Field > 0`,
|
|
|
|
// OR combinations
|
|
`st_equals(GeometryField, "POINT(0 0)") or st_equals(GeometryField, "POINT(1 1)")`,
|
|
`st_intersects(GeometryField, "POINT(0 0)") OR st_touches(GeometryField, "POINT(1 1)")`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", 1.0) or st_dwithin(GeometryField, "POINT(5 5)", 2.0)`,
|
|
|
|
// NOT combinations
|
|
`not st_equals(GeometryField, "POINT(0 0)")`,
|
|
`!(st_intersects(GeometryField, "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))"))`,
|
|
`not (st_dwithin(GeometryField, "POINT(0 0)", 1.0))`,
|
|
`not st_isvalid(GeometryField)`,
|
|
// Mixed with other field types
|
|
`st_contains(GeometryField, "POINT(0 0)") and StringField == "test"`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", 5.0) or Int32Field in [1, 2, 3]`,
|
|
`st_within(GeometryField, "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))") and FloatField > 0.5`,
|
|
|
|
// Nested expressions
|
|
`(st_equals(GeometryField, "POINT(0 0)") and Int64Field > 0) or (st_intersects(GeometryField, "POINT(1 1)") and StringField != "")`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", 5.0) and (Int32Field > 10 or BoolField == true)`,
|
|
}
|
|
|
|
for _, expr := range complexExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_GISFunctionsWithDifferentGeometryTypes(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test different WKT geometry types
|
|
geometryTests := []struct {
|
|
gisFunc string
|
|
geometryWKT string
|
|
description string
|
|
}{
|
|
// Point geometries
|
|
{"st_equals", "POINT(0 0)", "Simple point"},
|
|
{"st_intersects", "POINT(1.5 2.3)", "Point with decimals"},
|
|
{"st_dwithin", "POINT(-1 -1)", "Point with negative coordinates"},
|
|
|
|
// LineString geometries
|
|
{"st_intersects", "LINESTRING(0 0, 1 1)", "Simple linestring"},
|
|
{"st_crosses", "LINESTRING(-1 0, 1 0)", "Horizontal linestring"},
|
|
{"st_contains", "LINESTRING(0 0, 1 1, 2 2)", "Multi-segment linestring"},
|
|
|
|
// Polygon geometries
|
|
{"st_within", "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))", "Simple polygon"},
|
|
{"st_overlaps", "POLYGON((-1 -1, 1 -1, 1 1, -1 1, -1 -1))", "Centered polygon"},
|
|
{"st_contains", "POLYGON((0 0, 2 0, 2 2, 0 2, 0 0), (0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, 0.5 0.5))", "Polygon with hole"},
|
|
|
|
// Multi geometries
|
|
{"st_intersects", "MULTIPOINT((0 0), (1 1), (2 2))", "Multiple points"},
|
|
{"st_crosses", "MULTILINESTRING((0 0, 1 0), (1 1, 2 1))", "Multiple linestrings"},
|
|
{"st_overlaps", "MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)))", "Multiple polygons"},
|
|
|
|
// Collection geometries
|
|
{"st_intersects", "GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(1 1, 2 2))", "Mixed geometry collection"},
|
|
}
|
|
|
|
for _, test := range geometryTests {
|
|
exprStr := fmt.Sprintf(`%s(GeometryField, "%s")`, test.gisFunc, test.geometryWKT)
|
|
if test.gisFunc == "st_dwithin" {
|
|
exprStr = fmt.Sprintf(`%s(GeometryField, "%s", 5.0)`, test.gisFunc, test.geometryWKT)
|
|
}
|
|
|
|
assertValidExpr(t, schema, exprStr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_GISFunctionsWithVariousDistances(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test ST_DWITHIN with various distance values
|
|
distanceTests := []struct {
|
|
distance interface{}
|
|
shouldPass bool
|
|
description string
|
|
}{
|
|
// Valid distances (including zero)
|
|
{0, true, "Zero distance (integer)"},
|
|
{0.0, true, "Zero distance (float)"},
|
|
{1, true, "Integer distance"},
|
|
{1.0, true, "Float distance"},
|
|
{0.5, true, "Small decimal distance"},
|
|
{1000.0, true, "Large distance"},
|
|
{99999999.999, true, "Very large distance"},
|
|
{0.000001, true, "Very small distance"},
|
|
|
|
// Valid distance expressions as strings that should be parsed
|
|
{"0", true, "String zero integer"},
|
|
{"0.0", true, "String zero float"},
|
|
{"1", true, "String integer"},
|
|
{"1.5", true, "String float"},
|
|
}
|
|
|
|
for _, test := range distanceTests {
|
|
var exprStr string
|
|
switch v := test.distance.(type) {
|
|
case int:
|
|
exprStr = fmt.Sprintf(`st_dwithin(GeometryField, "POINT(0 0)", %d)`, v)
|
|
case float64:
|
|
exprStr = fmt.Sprintf(`st_dwithin(GeometryField, "POINT(0 0)", %g)`, v)
|
|
case string:
|
|
exprStr = fmt.Sprintf(`st_dwithin(GeometryField, "POINT(0 0)", %s)`, v)
|
|
}
|
|
|
|
if test.shouldPass {
|
|
assertValidExpr(t, schema, exprStr)
|
|
} else {
|
|
assertInvalidExpr(t, schema, exprStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExpr_GISFunctionsPlanGeneration(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test that GIS expressions can be used in search plans
|
|
gisExprs := []string{
|
|
`st_equals(GeometryField, "POINT(0 0)")`,
|
|
`st_intersects(GeometryField, "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))")`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", 5.0)`,
|
|
`st_contains(GeometryField, "POINT(0.5 0.5)") and Int64Field > 100`,
|
|
`st_within(GeometryField, "POLYGON((-1 -1, 1 -1, 1 1, -1 1, -1 -1))") or StringField == "test"`,
|
|
}
|
|
|
|
for _, expr := range gisExprs {
|
|
plan, err := CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 10,
|
|
MetricType: "L2",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, "Failed to create plan for expression: %s", expr)
|
|
assert.NotNil(t, plan, "Plan should not be nil for expression: %s", expr)
|
|
assert.NotNil(t, plan.GetVectorAnns(), "Vector annotations should not be nil for expression: %s", expr)
|
|
|
|
if plan.GetVectorAnns().GetPredicates() != nil {
|
|
// Verify that the plan contains GIS function filter expressions
|
|
// This ensures that the GIS expressions are properly parsed and converted to plan nodes
|
|
predicates := plan.GetVectorAnns().GetPredicates()
|
|
assert.NotNil(t, predicates, "Predicates should not be nil for GIS expression: %s", expr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExpr_GISFunctionsWithJSONFields(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test invalid usage with JSON fields - GIS functions should only work with geometry fields
|
|
invalidJSONGISExprs := []string{
|
|
`st_equals(JSONField, "POINT(0 0)")`,
|
|
`st_intersects($meta["geometry"], "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))")`,
|
|
`st_dwithin(A, "POINT(0 0)", 5.0)`, // Dynamic field
|
|
`st_contains(JSONField["geom"], "POINT(0.5 0.5)")`,
|
|
}
|
|
|
|
for _, expr := range invalidJSONGISExprs {
|
|
assertInvalidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_GISFunctionsZeroDistance(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test zero distance specifically for ST_DWITHIN
|
|
zeroDistanceExprs := []string{
|
|
// Integer zero
|
|
`st_dwithin(GeometryField, "POINT(0 0)", 0)`,
|
|
// Float zero
|
|
`st_dwithin(GeometryField, "POINT(1 1)", 0.0)`,
|
|
// Zero in complex expressions
|
|
`st_dwithin(GeometryField, "POINT(0 0)", 0) and Int64Field > 10`,
|
|
}
|
|
|
|
for _, expr := range zeroDistanceExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
|
|
// Test that zero distance expressions can generate valid search plans
|
|
for _, expr := range zeroDistanceExprs {
|
|
plan, err := CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 10,
|
|
MetricType: "L2",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, "Failed to create plan for zero distance expression: %s", expr)
|
|
assert.NotNil(t, plan, "Plan should not be nil for zero distance expression: %s", expr)
|
|
}
|
|
|
|
// Test that negative distances are still invalid
|
|
invalidNegativeExprs := []string{
|
|
`st_dwithin(GeometryField, "POINT(0 0)", -1)`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", -0.1)`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", -100.5)`,
|
|
}
|
|
|
|
for _, expr := range invalidNegativeExprs {
|
|
assertInvalidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_GISFunctionsInvalidParameterTypes(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test various invalid parameter types for ST_DWITHIN distance parameter
|
|
invalidTypeExprs := []string{
|
|
// String parameters (should be rejected)
|
|
`st_dwithin(GeometryField, "POINT(0 0)", "abc")`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", "123")`, // Numeric string
|
|
`st_dwithin(GeometryField, "POINT(0 0)", "123.45")`, // Float string
|
|
`st_dwithin(GeometryField, "POINT(0 0)", "0")`, // Zero string
|
|
`st_dwithin(GeometryField, "POINT(0 0)", "-5")`, // Negative string
|
|
|
|
// Boolean parameters (should be rejected)
|
|
`st_dwithin(GeometryField, "POINT(0 0)", true)`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", false)`,
|
|
|
|
// Array/complex parameters (should be rejected)
|
|
`st_dwithin(GeometryField, "POINT(0 0)", [1, 2, 3])`,
|
|
`st_dwithin(GeometryField, "POINT(0 0)", GeometryField)`, // Field reference instead of literal
|
|
}
|
|
|
|
for _, expr := range invalidTypeExprs {
|
|
assertInvalidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_ElementFilter(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
// Valid expressions
|
|
validExprs := []string{
|
|
`element_filter(struct_array, 2 > $[sub_int] > 1)`,
|
|
`element_filter(struct_array, $[sub_int] > 1)`,
|
|
`element_filter(struct_array, $[sub_int] == 100)`,
|
|
`element_filter(struct_array, $[sub_int] >= 0)`,
|
|
`element_filter(struct_array, $[sub_int] <= 1000)`,
|
|
`element_filter(struct_array, $[sub_int] != 0)`,
|
|
|
|
`element_filter(struct_array, $[sub_str] == "1")`,
|
|
`element_filter(struct_array, $[sub_str] != "")`,
|
|
|
|
`element_filter(struct_array, $[sub_str] == "1" || $[sub_int] > 1)`,
|
|
`element_filter(struct_array, $[sub_str] == "1" && $[sub_int] > 1)`,
|
|
`element_filter(struct_array, $[sub_int] > 0 && $[sub_int] < 100)`,
|
|
|
|
`element_filter(struct_array, ($[sub_int] > 0 && $[sub_int] < 100) || $[sub_str] == "default")`,
|
|
`element_filter(struct_array, !($[sub_int] < 0))`,
|
|
|
|
`Int64Field > 0 && element_filter(struct_array, $[sub_int] > 1)`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, helper, expr)
|
|
}
|
|
|
|
// Invalid expressions
|
|
invalidExprs := []string{
|
|
`element_filter(struct_array, element_filter(struct_array, $[sub_int] > 1))`,
|
|
`element_filter(struct_array, $[sub_int] > 1 && element_filter(struct_array, $[sub_str] == "1"))`,
|
|
|
|
`$[sub_int] > 1`,
|
|
`Int64Field > 0 && $[sub_int] > 1`,
|
|
|
|
`element_filter(struct_array, $[non_existent_field] > 1)`,
|
|
`element_filter(non_existent_array, $[sub_int] > 1)`,
|
|
|
|
`element_filter(struct_array)`, // missing element expression
|
|
`element_filter()`, // missing all parameters
|
|
|
|
`element_filter(struct_array, $[sub_int] > 1) || element_filter(struct_array, $[sub_str] == "test")`,
|
|
`element_filter(struct_array, $[sub_int] > 1) && Int64Field > 0`,
|
|
|
|
`not element_filter(struct_array, $[sub_int] > 1)`,
|
|
`!element_filter(struct_array, $[sub_int] > 1)`,
|
|
}
|
|
|
|
for _, expr := range invalidExprs {
|
|
assertInvalidExpr(t, helper, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_Match(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
// Valid MATCH_ALL expressions
|
|
validExprs := []string{
|
|
// MATCH_ALL: all elements must match
|
|
`MATCH_ALL(struct_array, $[sub_int] > 1)`,
|
|
`MATCH_ALL(struct_array, $[sub_int] == 100)`,
|
|
`MATCH_ALL(struct_array, $[sub_str] == "aaa")`,
|
|
`MATCH_ALL(struct_array, $[sub_str] == "aaa" && $[sub_int] > 100)`,
|
|
`MATCH_ALL(struct_array, $[sub_str] != "" || $[sub_int] >= 0)`,
|
|
|
|
// MATCH_ANY: at least one element must match
|
|
`MATCH_ANY(struct_array, $[sub_int] > 1)`,
|
|
`MATCH_ANY(struct_array, $[sub_int] == 100)`,
|
|
`MATCH_ANY(struct_array, $[sub_str] == "aaa")`,
|
|
`MATCH_ANY(struct_array, $[sub_str] == "aaa" && $[sub_int] > 100)`,
|
|
|
|
// MATCH_LEAST: at least N elements must match
|
|
`MATCH_LEAST(struct_array, $[sub_int] > 1, threshold=3)`,
|
|
`MATCH_LEAST(struct_array, $[sub_str] == "aaa", threshold=1)`,
|
|
`MATCH_LEAST(struct_array, $[sub_str] == "aaa" && $[sub_int] > 100, threshold=2)`,
|
|
|
|
// MATCH_MOST: at most N elements must match
|
|
`MATCH_MOST(struct_array, $[sub_int] > 1, threshold=3)`,
|
|
`MATCH_MOST(struct_array, $[sub_str] == "aaa", threshold=0)`,
|
|
`MATCH_MOST(struct_array, $[sub_str] == "aaa" && $[sub_int] > 100, threshold=5)`,
|
|
|
|
// MATCH_EXACT: exactly N elements must match
|
|
`MATCH_EXACT(struct_array, $[sub_int] > 1, threshold=2)`,
|
|
`MATCH_EXACT(struct_array, $[sub_str] == "aaa", threshold=0)`,
|
|
`MATCH_EXACT(struct_array, $[sub_str] == "aaa" && $[sub_int] > 100, threshold=3)`,
|
|
|
|
// Combined with other expressions (match must be last)
|
|
`Int64Field > 0 && MATCH_ALL(struct_array, $[sub_int] > 1)`,
|
|
`Int64Field > 0 && MATCH_ANY(struct_array, $[sub_str] == "test")`,
|
|
`Int64Field > 0 && MATCH_LEAST(struct_array, $[sub_int] > 1, threshold=2)`,
|
|
|
|
// Complex predicates
|
|
`MATCH_ALL(struct_array, ($[sub_int] > 0 && $[sub_int] < 100) || $[sub_str] == "default")`,
|
|
`MATCH_ANY(struct_array, !($[sub_int] < 0))`,
|
|
|
|
// Case insensitivity
|
|
`match_all(struct_array, $[sub_int] > 1)`,
|
|
`match_any(struct_array, $[sub_int] > 1)`,
|
|
`match_least(struct_array, $[sub_int] > 1, threshold=2)`,
|
|
`match_most(struct_array, $[sub_int] > 1, threshold=2)`,
|
|
`match_exact(struct_array, $[sub_int] > 1, threshold=2)`,
|
|
|
|
// Multiple match expressions with logical operators
|
|
`MATCH_ALL(struct_array, $[sub_int] > 1) || MATCH_ANY(struct_array, $[sub_str] == "test")`,
|
|
`MATCH_ALL(struct_array, $[sub_int] > 1) && MATCH_ANY(struct_array, $[sub_str] == "test")`,
|
|
`MATCH_ANY(struct_array, $[sub_int] > 1) || Int64Field > 0`,
|
|
`MATCH_ALL(struct_array, $[sub_int] > 1) && Int64Field > 0`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, helper, expr)
|
|
}
|
|
|
|
// Test proto structure assertions
|
|
t.Run("MatchAll_Proto", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, `MATCH_ALL(struct_array, $[sub_int] > 1)`, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, expr.GetMatchExpr())
|
|
assert.Equal(t, "struct_array", expr.GetMatchExpr().GetStructName())
|
|
assert.Equal(t, planpb.MatchType_MatchAll, expr.GetMatchExpr().GetMatchType())
|
|
assert.Equal(t, int64(0), expr.GetMatchExpr().GetCount())
|
|
})
|
|
|
|
t.Run("MatchAny_Proto", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, `MATCH_ANY(struct_array, $[sub_str] == "aaa")`, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, expr.GetMatchExpr())
|
|
assert.Equal(t, "struct_array", expr.GetMatchExpr().GetStructName())
|
|
assert.Equal(t, planpb.MatchType_MatchAny, expr.GetMatchExpr().GetMatchType())
|
|
assert.Equal(t, int64(0), expr.GetMatchExpr().GetCount())
|
|
})
|
|
|
|
t.Run("MatchLeast_Proto", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, `MATCH_LEAST(struct_array, $[sub_int] > 1, threshold=3)`, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, expr.GetMatchExpr())
|
|
assert.Equal(t, "struct_array", expr.GetMatchExpr().GetStructName())
|
|
assert.Equal(t, planpb.MatchType_MatchLeast, expr.GetMatchExpr().GetMatchType())
|
|
assert.Equal(t, int64(3), expr.GetMatchExpr().GetCount())
|
|
})
|
|
|
|
t.Run("MatchMost_Proto", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, `MATCH_MOST(struct_array, $[sub_str] == "aaa", threshold=5)`, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, expr.GetMatchExpr())
|
|
assert.Equal(t, "struct_array", expr.GetMatchExpr().GetStructName())
|
|
assert.Equal(t, planpb.MatchType_MatchMost, expr.GetMatchExpr().GetMatchType())
|
|
assert.Equal(t, int64(5), expr.GetMatchExpr().GetCount())
|
|
})
|
|
|
|
t.Run("MatchExact_Proto", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, `MATCH_EXACT(struct_array, $[sub_int] == 100, threshold=2)`, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, expr.GetMatchExpr())
|
|
assert.Equal(t, "struct_array", expr.GetMatchExpr().GetStructName())
|
|
assert.Equal(t, planpb.MatchType_MatchExact, expr.GetMatchExpr().GetMatchType())
|
|
assert.Equal(t, int64(2), expr.GetMatchExpr().GetCount())
|
|
})
|
|
|
|
// Invalid expressions
|
|
invalidExprs := []string{
|
|
// Nested match expressions not allowed
|
|
`MATCH_ALL(struct_array, MATCH_ANY(struct_array, $[sub_int] > 1))`,
|
|
`MATCH_ANY(struct_array, $[sub_int] > 1 && MATCH_ALL(struct_array, $[sub_str] == "1"))`,
|
|
|
|
// $[field] syntax outside match context
|
|
`$[sub_int] > 1`,
|
|
`Int64Field > 0 && $[sub_int] > 1`,
|
|
|
|
// Non-existent fields
|
|
`MATCH_ALL(struct_array, $[non_existent_field] > 1)`,
|
|
`MATCH_ALL(non_existent_array, $[sub_int] > 1)`,
|
|
|
|
// Missing parameters
|
|
`MATCH_ALL(struct_array)`,
|
|
`MATCH_ALL()`,
|
|
`MATCH_ANY(struct_array)`,
|
|
`MATCH_ANY()`,
|
|
`MATCH_LEAST(struct_array, $[sub_int] > 1)`, // missing count
|
|
`MATCH_MOST(struct_array, $[sub_int] > 1)`, // missing count
|
|
`MATCH_EXACT(struct_array, $[sub_int] > 1)`, // missing count
|
|
|
|
// MATCH_ALL/MATCH_ANY should not have count parameter
|
|
`MATCH_ALL(struct_array, $[sub_int] > 1, 3)`,
|
|
`MATCH_ANY(struct_array, $[sub_int] > 1, 2)`,
|
|
|
|
// Invalid count values
|
|
`MATCH_LEAST(struct_array, $[sub_int] > 1, threshold=0)`, // count must be positive for MATCH_LEAST
|
|
`MATCH_LEAST(struct_array, $[sub_int] > 1, threshold=-1)`, // negative count
|
|
`MATCH_MOST(struct_array, $[sub_int] > 1, threshold=-1)`, // negative count
|
|
`MATCH_EXACT(struct_array, $[sub_int] > 1, threshold=-1)`, // negative count
|
|
}
|
|
|
|
for _, expr := range invalidExprs {
|
|
assertInvalidExpr(t, helper, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_ArrayContains(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
// Valid ArrayContains expressions
|
|
validExprs := []string{
|
|
`array_contains(struct_array[sub_int], 1)`,
|
|
`array_contains(struct_array[sub_int], 1) && array_contains(struct_array[sub_int], 2)`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, helper, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_StructIndexField(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
assert.NoError(t, err)
|
|
|
|
// Valid struct_arr[index][sub_field] expressions
|
|
validExprs := []string{
|
|
// comparison
|
|
`struct_array[0][sub_int] > 50`,
|
|
`struct_array[0][sub_int] == 100`,
|
|
`struct_array[1][sub_str] == "apple"`,
|
|
`struct_array[0][sub_str] != "banana"`,
|
|
// range via &&
|
|
`struct_array[0][sub_int] >= 30 && struct_array[0][sub_int] <= 70`,
|
|
// IN / NOT IN
|
|
`struct_array[0][sub_int] in [10, 20, 30, 40, 50]`,
|
|
`struct_array[0][sub_int] not in [0, 1, 2, 3]`,
|
|
`struct_array[0][sub_str] in ["apple", "banana"]`,
|
|
`struct_array[0][sub_str] not in ["apple", "banana"]`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, helper, expr)
|
|
}
|
|
|
|
// Invalid expressions
|
|
invalidExprs := []string{
|
|
// non-existent parent or sub-field
|
|
`non_existent[0][sub_int] > 50`,
|
|
`struct_array[0][non_existent] > 50`,
|
|
// unsupported form: sub-field first then index
|
|
`struct_array[sub_int][0] > 50`,
|
|
`struct_array[sub_str][0] == "apple"`,
|
|
`struct_array[sub_int][0] in [10, 20, 30]`,
|
|
}
|
|
|
|
for _, expr := range invalidExprs {
|
|
assertInvalidExpr(t, helper, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_StructIndexField_PlanShape(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
plan, err := CreateSearchPlan(schema, `struct_array[0][sub_int] == 100`, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
predicates := plan.GetVectorAnns().GetPredicates()
|
|
require.NotNil(t, predicates)
|
|
|
|
ure := predicates.GetUnaryRangeExpr()
|
|
require.NotNil(t, ure)
|
|
assert.Equal(t, planpb.OpType_Equal, ure.GetOp())
|
|
assert.Equal(t, int64(100), ure.GetValue().GetInt64Val())
|
|
|
|
columnInfo := ure.GetColumnInfo()
|
|
require.NotNil(t, columnInfo)
|
|
assert.Equal(t, int64(134), columnInfo.GetFieldId())
|
|
assert.Equal(t, schemapb.DataType_Array, columnInfo.GetDataType())
|
|
assert.Equal(t, schemapb.DataType_Int32, columnInfo.GetElementType())
|
|
assert.Equal(t, []string{"0"}, columnInfo.GetNestedPath())
|
|
|
|
ret := handleExpr(schema, `struct_array[1][sub_str] in ["apple", "banana"]`)
|
|
ewt, ok := ret.(*ExprWithType)
|
|
require.True(t, ok, "handleExpr should return *ExprWithType for term expression")
|
|
|
|
te := ewt.expr.GetTermExpr()
|
|
require.NotNil(t, te)
|
|
assert.Len(t, te.GetValues(), 2)
|
|
assert.Equal(t, "apple", te.GetValues()[0].GetStringVal())
|
|
assert.Equal(t, "banana", te.GetValues()[1].GetStringVal())
|
|
|
|
columnInfo = te.GetColumnInfo()
|
|
require.NotNil(t, columnInfo)
|
|
assert.Equal(t, int64(133), columnInfo.GetFieldId())
|
|
assert.Equal(t, schemapb.DataType_Array, columnInfo.GetDataType())
|
|
assert.Equal(t, schemapb.DataType_VarChar, columnInfo.GetElementType())
|
|
assert.Equal(t, []string{"1"}, columnInfo.GetNestedPath())
|
|
}
|
|
|
|
func TestExpr_StructFieldArrayLength(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
ret := handleExpr(schema, `array_length(struct_array)`)
|
|
ewt, ok := ret.(*ExprWithType)
|
|
require.True(t, ok, "handleExpr should return *ExprWithType")
|
|
assert.Equal(t, schemapb.DataType_Int64, ewt.dataType)
|
|
|
|
bae := ewt.expr.GetBinaryArithExpr()
|
|
require.NotNil(t, bae)
|
|
assert.Equal(t, planpb.ArithOpType_ArrayLength, bae.GetOp())
|
|
require.Nil(t, bae.GetRight())
|
|
|
|
columnInfo := bae.GetLeft().GetColumnExpr().GetInfo()
|
|
require.NotNil(t, columnInfo)
|
|
assert.Equal(t, int64(133), columnInfo.GetFieldId())
|
|
assert.Equal(t, schemapb.DataType_Array, columnInfo.GetDataType())
|
|
assert.Equal(t, schemapb.DataType_VarChar, columnInfo.GetElementType())
|
|
assert.Empty(t, columnInfo.GetNestedPath())
|
|
|
|
ret = handleExpr(schema, `array_length(struct_array[sub_int])`)
|
|
ewt, ok = ret.(*ExprWithType)
|
|
require.True(t, ok, "handleExpr should return *ExprWithType")
|
|
assert.Equal(t, schemapb.DataType_Int64, ewt.dataType)
|
|
|
|
bae = ewt.expr.GetBinaryArithExpr()
|
|
require.NotNil(t, bae)
|
|
assert.Equal(t, planpb.ArithOpType_ArrayLength, bae.GetOp())
|
|
require.Nil(t, bae.GetRight())
|
|
|
|
columnInfo = bae.GetLeft().GetColumnExpr().GetInfo()
|
|
require.NotNil(t, columnInfo)
|
|
assert.Equal(t, int64(134), columnInfo.GetFieldId())
|
|
assert.Equal(t, schemapb.DataType_Array, columnInfo.GetDataType())
|
|
assert.Equal(t, schemapb.DataType_Int32, columnInfo.GetElementType())
|
|
assert.Empty(t, columnInfo.GetNestedPath())
|
|
|
|
validExprs := []string{
|
|
`array_length(struct_array) == 10`,
|
|
`array_length(struct_array[sub_int]) == 10`,
|
|
`array_length(struct_array[sub_str]) > 0`,
|
|
}
|
|
for _, expr := range validExprs {
|
|
_, err := CreateSearchPlan(schema, expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
assert.NoError(t, err, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_VectorArrayOnlyStructParentArrayLength(t *testing.T) {
|
|
schema := newTestSchema(false)
|
|
schema.StructArrayFields = append(schema.StructArrayFields, &schemapb.StructArrayFieldSchema{
|
|
FieldID: 10000,
|
|
Name: "vector_struct",
|
|
Nullable: true,
|
|
Fields: []*schemapb.FieldSchema{
|
|
{
|
|
FieldID: 10001,
|
|
Name: "vector_struct[embeddings]",
|
|
DataType: schemapb.DataType_ArrayOfVector,
|
|
ElementType: schemapb.DataType_FloatVector,
|
|
Nullable: true,
|
|
TypeParams: []*commonpb.KeyValuePair{
|
|
{Key: common.DimKey, Value: "4"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(t, err)
|
|
|
|
for _, expr := range []string{
|
|
`array_length(vector_struct)`,
|
|
`array_length(vector_struct[embeddings])`,
|
|
} {
|
|
ret := handleExpr(helper, expr)
|
|
ewt, ok := ret.(*ExprWithType)
|
|
require.True(t, ok, "handleExpr should return *ExprWithType")
|
|
assert.Equal(t, schemapb.DataType_Int64, ewt.dataType)
|
|
|
|
bae := ewt.expr.GetBinaryArithExpr()
|
|
require.NotNil(t, bae)
|
|
assert.Equal(t, planpb.ArithOpType_ArrayLength, bae.GetOp())
|
|
|
|
columnInfo := bae.GetLeft().GetColumnExpr().GetInfo()
|
|
require.NotNil(t, columnInfo)
|
|
assert.Equal(t, int64(10001), columnInfo.GetFieldId())
|
|
assert.Equal(t, schemapb.DataType_ArrayOfVector, columnInfo.GetDataType())
|
|
assert.Equal(t, schemapb.DataType_FloatVector, columnInfo.GetElementType())
|
|
assert.True(t, columnInfo.GetNullable())
|
|
}
|
|
|
|
_, err = CreateSearchPlan(helper, `array_length(vector_struct) == 2`, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestExpr_StructIndexField_RangeForms(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
testCases := []struct {
|
|
expr string
|
|
lower int64
|
|
upper int64
|
|
lowerInclusive bool
|
|
upperInclusive bool
|
|
}{
|
|
{
|
|
expr: `1 < struct_array[0][sub_int] < 3`,
|
|
lower: 1,
|
|
upper: 3,
|
|
lowerInclusive: false,
|
|
upperInclusive: false,
|
|
},
|
|
{
|
|
expr: `0 <= struct_array[0][sub_int] <= 100`,
|
|
lower: 0,
|
|
upper: 100,
|
|
lowerInclusive: true,
|
|
upperInclusive: true,
|
|
},
|
|
{
|
|
expr: `100 > struct_array[0][sub_int] > 0`,
|
|
lower: 0,
|
|
upper: 100,
|
|
lowerInclusive: false,
|
|
upperInclusive: false,
|
|
},
|
|
{
|
|
expr: `100 >= struct_array[0][sub_int] >= 0`,
|
|
lower: 0,
|
|
upper: 100,
|
|
lowerInclusive: true,
|
|
upperInclusive: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
plan, err := CreateSearchPlan(schema, tc.expr, "FloatVectorField", &planpb.QueryInfo{
|
|
Topk: 0,
|
|
MetricType: "",
|
|
SearchParams: "",
|
|
RoundDecimal: 0,
|
|
}, nil, nil)
|
|
require.NoError(t, err, tc.expr)
|
|
|
|
predicates := plan.GetVectorAnns().GetPredicates()
|
|
require.NotNil(t, predicates, tc.expr)
|
|
|
|
bre := predicates.GetBinaryRangeExpr()
|
|
require.NotNil(t, bre, tc.expr)
|
|
assert.Equal(t, tc.lower, bre.GetLowerValue().GetInt64Val(), tc.expr)
|
|
assert.Equal(t, tc.upper, bre.GetUpperValue().GetInt64Val(), tc.expr)
|
|
assert.Equal(t, tc.lowerInclusive, bre.GetLowerInclusive(), tc.expr)
|
|
assert.Equal(t, tc.upperInclusive, bre.GetUpperInclusive(), tc.expr)
|
|
|
|
columnInfo := bre.GetColumnInfo()
|
|
require.NotNil(t, columnInfo, tc.expr)
|
|
assert.Equal(t, int64(134), columnInfo.GetFieldId(), tc.expr)
|
|
assert.Equal(t, schemapb.DataType_Array, columnInfo.GetDataType(), tc.expr)
|
|
assert.Equal(t, schemapb.DataType_Int32, columnInfo.GetElementType(), tc.expr)
|
|
assert.Equal(t, []string{"0"}, columnInfo.GetNestedPath(), tc.expr)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Timestamptz Expression Tests
|
|
// These tests cover VisitTimestamptzCompareForward and VisitTimestamptzCompareReverse
|
|
// which are used for optimized timestamptz comparisons with optional INTERVAL arithmetic
|
|
// ============================================================================
|
|
|
|
func newTestSchemaWithTimestamptz(t *testing.T) *typeutil.SchemaHelper {
|
|
// Create schema with Timestamptz field for testing
|
|
// The newTestSchema already includes all DataType values including Timestamptz
|
|
schema := newTestSchema(true)
|
|
schemaHelper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(t, err)
|
|
return schemaHelper
|
|
}
|
|
|
|
func TestExpr_TimestamptzCompareForward(t *testing.T) {
|
|
schema := newTestSchemaWithTimestamptz(t)
|
|
|
|
// Test valid timestamptz forward comparisons (column op ISO value)
|
|
// Format: TimestamptzField [+|- INTERVAL 'duration'] <op> ISO 'timestamp'
|
|
// Note: ISO keyword is required before the timestamp string literal
|
|
validExprs := []string{
|
|
// Simple comparisons without INTERVAL (quick path)
|
|
`TimestamptzField > ISO '2025-01-01T00:00:00Z'`,
|
|
`TimestamptzField >= ISO '2025-01-01T00:00:00Z'`,
|
|
`TimestamptzField < ISO '2025-12-31T23:59:59Z'`,
|
|
`TimestamptzField <= ISO '2025-06-15T12:00:00Z'`,
|
|
`TimestamptzField == ISO '2025-03-20T10:30:00Z'`,
|
|
`TimestamptzField != ISO '2025-08-10T08:00:00Z'`,
|
|
|
|
// Comparisons with INTERVAL (slow path with arithmetic)
|
|
`TimestamptzField + INTERVAL 'P1D' > ISO '2025-01-01T00:00:00Z'`,
|
|
`TimestamptzField - INTERVAL 'P1D' < ISO '2025-12-31T23:59:59Z'`,
|
|
`TimestamptzField + INTERVAL 'PT1H' >= ISO '2025-06-15T12:00:00Z'`,
|
|
`TimestamptzField - INTERVAL 'PT30M' <= ISO '2025-03-20T10:30:00Z'`,
|
|
`TimestamptzField + INTERVAL 'P1Y' == ISO '2026-01-01T00:00:00Z'`,
|
|
`TimestamptzField - INTERVAL 'P6M' != ISO '2024-06-01T00:00:00Z'`,
|
|
|
|
// Complex INTERVAL durations
|
|
`TimestamptzField + INTERVAL 'P1Y2M3D' > ISO '2025-01-01T00:00:00Z'`,
|
|
`TimestamptzField + INTERVAL 'PT10H30M15S' < ISO '2025-12-31T23:59:59Z'`,
|
|
`TimestamptzField - INTERVAL 'P1Y2M3DT4H5M6S' >= ISO '2024-01-01T00:00:00Z'`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_TimestamptzCompareReverse(t *testing.T) {
|
|
schema := newTestSchemaWithTimestamptz(t)
|
|
|
|
// Test valid timestamptz reverse comparisons (ISO value op column)
|
|
// Format: ISO 'timestamp' <op> TimestamptzField [+|- INTERVAL 'duration']
|
|
// Note: ISO keyword is required before the timestamp string
|
|
// Note: Operator gets reversed internally (e.g., '>' becomes '<')
|
|
validExprs := []string{
|
|
// Simple reverse comparisons without INTERVAL (quick path)
|
|
`ISO '2025-01-01T00:00:00Z' < TimestamptzField`,
|
|
`ISO '2025-01-01T00:00:00Z' <= TimestamptzField`,
|
|
`ISO '2025-12-31T23:59:59Z' > TimestamptzField`,
|
|
`ISO '2025-06-15T12:00:00Z' >= TimestamptzField`,
|
|
`ISO '2025-03-20T10:30:00Z' == TimestamptzField`,
|
|
`ISO '2025-08-10T08:00:00Z' != TimestamptzField`,
|
|
|
|
// Reverse comparisons with INTERVAL after field (slow path with arithmetic)
|
|
`ISO '2025-01-01T00:00:00Z' < TimestamptzField + INTERVAL 'P1D'`,
|
|
`ISO '2025-12-31T23:59:59Z' > TimestamptzField - INTERVAL 'P1D'`,
|
|
`ISO '2025-06-15T12:00:00Z' <= TimestamptzField + INTERVAL 'PT1H'`,
|
|
`ISO '2025-03-20T10:30:00Z' >= TimestamptzField - INTERVAL 'PT30M'`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_TimestamptzCompareInvalid(t *testing.T) {
|
|
schema := newTestSchemaWithTimestamptz(t)
|
|
|
|
// Test invalid timestamptz expressions
|
|
// Note: ISO keyword is required for timestamptz comparisons
|
|
invalidExprs := []string{
|
|
// Invalid field type for timestamptz operations (non-timestamptz field with INTERVAL)
|
|
`Int64Field + INTERVAL 'P1D' > ISO '2025-01-01T00:00:00Z'`,
|
|
`VarCharField + INTERVAL 'P1D' < ISO '2025-01-01T00:00:00Z'`,
|
|
|
|
// Invalid timestamp format with ISO
|
|
`TimestamptzField > ISO 'invalid-timestamp'`,
|
|
`TimestamptzField < ISO '2025-13-01T00:00:00Z'`, // Invalid month
|
|
`TimestamptzField > ISO '2025-01-32T00:00:00Z'`, // Invalid day
|
|
|
|
// Invalid interval format
|
|
`TimestamptzField + INTERVAL 'invalid' > ISO '2025-01-01T00:00:00Z'`,
|
|
`TimestamptzField + INTERVAL '1D' > ISO '2025-01-01T00:00:00Z'`, // Missing P prefix
|
|
}
|
|
|
|
for _, expr := range invalidExprs {
|
|
assertInvalidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Power Expression Tests
|
|
// These tests cover VisitPower for constant power operations
|
|
// ============================================================================
|
|
|
|
func TestExpr_Power(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test valid power expressions with constants
|
|
validExprs := []string{
|
|
// Integer powers
|
|
`2 ** 3 == 8`,
|
|
`3 ** 2 == 9`,
|
|
`10 ** 0 == 1`,
|
|
|
|
// Float powers
|
|
`2.0 ** 3.0 == 8.0`,
|
|
`4.0 ** 0.5 > 1.0`,
|
|
|
|
// Negative exponents
|
|
`2 ** -1 == 0.5`,
|
|
|
|
// Used in arithmetic expressions
|
|
`Int64Field + (2 ** 3) > 0`,
|
|
`Int64Field * (10 ** 2) < 1000`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
|
|
// Test invalid power expressions - power requires constant operands
|
|
invalidExprs := []string{
|
|
// Power with field operands (not allowed)
|
|
`Int64Field ** 2 == 100`,
|
|
`2 ** Int64Field == 8`,
|
|
`Int64Field ** Int64Field == 1`,
|
|
}
|
|
|
|
for _, expr := range invalidExprs {
|
|
assertInvalidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Error Handling Tests
|
|
// These tests cover the int64OverflowError type and error handling paths
|
|
// ============================================================================
|
|
|
|
func TestInt64OverflowError(t *testing.T) {
|
|
// Test int64OverflowError.Error() method - covers the Error() method at 0% coverage
|
|
err := &int64OverflowError{literal: "9223372036854775808"}
|
|
assert.Contains(t, err.Error(), "int64 overflow")
|
|
assert.Contains(t, err.Error(), "9223372036854775808")
|
|
|
|
// Test isInt64OverflowError helper function
|
|
assert.True(t, isInt64OverflowError(err))
|
|
assert.False(t, isInt64OverflowError(fmt.Errorf("some other error")))
|
|
assert.False(t, isInt64OverflowError(nil))
|
|
}
|
|
|
|
// ============================================================================
|
|
// reverseCompareOp Tests
|
|
// This function is used internally to reverse comparison operators
|
|
// ============================================================================
|
|
|
|
func Test_reverseCompareOp(t *testing.T) {
|
|
// Test all comparison operator reversals
|
|
// This covers the reverseCompareOp function at 0% coverage
|
|
tests := []struct {
|
|
input planpb.OpType
|
|
expected planpb.OpType
|
|
}{
|
|
{planpb.OpType_LessThan, planpb.OpType_GreaterThan},
|
|
{planpb.OpType_LessEqual, planpb.OpType_GreaterEqual},
|
|
{planpb.OpType_GreaterThan, planpb.OpType_LessThan},
|
|
{planpb.OpType_GreaterEqual, planpb.OpType_LessEqual},
|
|
{planpb.OpType_Equal, planpb.OpType_Equal},
|
|
{planpb.OpType_NotEqual, planpb.OpType_NotEqual},
|
|
{planpb.OpType_Invalid, planpb.OpType_Invalid},
|
|
{planpb.OpType_PrefixMatch, planpb.OpType_Invalid}, // Unknown ops return Invalid
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := reverseCompareOp(tt.input)
|
|
assert.Equal(t, tt.expected, result, "reverseCompareOp(%v)", tt.input)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Additional Coverage Tests for Edge Cases
|
|
// ============================================================================
|
|
|
|
func TestExpr_AdditionalEdgeCases(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test valid edge case expressions
|
|
validExprs := []string{
|
|
// Floating point edge cases
|
|
`FloatField > 1e10`,
|
|
`DoubleField < 1e-10`,
|
|
`FloatField == 3.14159265358979`,
|
|
|
|
// Boolean expressions
|
|
`true == true`,
|
|
`false != true`,
|
|
|
|
// Empty string comparison
|
|
`StringField == ""`,
|
|
`VarCharField != ""`,
|
|
|
|
// JSON with complex nested paths
|
|
`JSONField["level1"]["level2"]["level3"] > 0`,
|
|
|
|
// Array length operations
|
|
`array_length(ArrayField) > 0`,
|
|
`array_length(ArrayField) == 10`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
func TestExpr_InvalidOperatorCombinations(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Test invalid operator combinations that should fail
|
|
// These test the error paths in various Visit methods
|
|
invalidExprs := []string{
|
|
// Shift operations not supported
|
|
`Int64Field << 2`,
|
|
`Int64Field >> 2`,
|
|
|
|
// Bitwise operations not supported
|
|
`Int64Field & 0xFF`,
|
|
`Int64Field | 0xFF`,
|
|
`Int64Field ^ 0xFF`,
|
|
|
|
// Type mismatches
|
|
`"string" + 1`,
|
|
`BoolField + 1`,
|
|
}
|
|
|
|
for _, expr := range invalidExprs {
|
|
assertInvalidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
// TestExpr_VisitBooleanEdgeCases tests edge cases in VisitBoolean
|
|
// Boolean literals must be used in comparison expressions, not as standalone values
|
|
func TestExpr_VisitBooleanEdgeCases(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Valid boolean comparison expressions
|
|
// Note: Standalone boolean values or fields are not valid filter expressions
|
|
// They must be used in comparisons
|
|
validExprs := []string{
|
|
`true == true`,
|
|
`false == false`,
|
|
`true != false`,
|
|
`BoolField == true`,
|
|
`BoolField != false`,
|
|
`BoolField == BoolField`,
|
|
`not (BoolField == true)`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
// TestExpr_VisitFloatingEdgeCases tests edge cases in VisitFloating
|
|
func TestExpr_VisitFloatingEdgeCases(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Valid floating point literal expressions
|
|
validExprs := []string{
|
|
`FloatField > 0.0`,
|
|
`FloatField < 1.0e10`,
|
|
`FloatField >= -1.0e-10`,
|
|
`FloatField <= 3.14159265`,
|
|
`DoubleField == 2.718281828`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
// TestExpr_VisitRangeEdgeCases tests edge cases in VisitRange and VisitReverseRange
|
|
func TestExpr_VisitRangeEdgeCases(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Valid range expressions
|
|
validExprs := []string{
|
|
// Forward range: lower < field < upper
|
|
`1 < Int64Field < 10`,
|
|
`0.0 < FloatField < 1.0`,
|
|
`"a" < StringField < "z"`,
|
|
|
|
// Forward range with equal
|
|
`1 <= Int64Field < 10`,
|
|
`1 < Int64Field <= 10`,
|
|
`1 <= Int64Field <= 10`,
|
|
|
|
// Reverse range: upper > field > lower
|
|
`10 > Int64Field > 1`,
|
|
`1.0 > FloatField > 0.0`,
|
|
`"z" > StringField > "a"`,
|
|
|
|
// Reverse range with equal
|
|
`10 >= Int64Field > 1`,
|
|
`10 > Int64Field >= 1`,
|
|
`10 >= Int64Field >= 1`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
|
|
// Invalid range expressions
|
|
invalidExprs := []string{
|
|
// Range on bool type is invalid
|
|
`true < BoolField < false`,
|
|
|
|
// Non-const bounds
|
|
`Int64Field < Int32Field < Int64Field`,
|
|
}
|
|
|
|
for _, expr := range invalidExprs {
|
|
assertInvalidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
// TestExpr_VisitUnaryEdgeCases tests edge cases in VisitUnary
|
|
// Unary operators (not/!) must produce boolean expressions for filter predicates
|
|
func TestExpr_VisitUnaryEdgeCases(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Valid unary expressions - must produce boolean filter predicates
|
|
validExprs := []string{
|
|
`not (Int64Field > 0)`,
|
|
`!(Int64Field < 10)`,
|
|
`not (BoolField == true)`,
|
|
`not (true == false)`,
|
|
`!(FloatField >= 1.0)`,
|
|
// Unary negation used in comparison context
|
|
`Int64Field > -1`,
|
|
`Int64Field < -(-5)`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
// TestExpr_ConstantFolding tests constant folding in arithmetic expressions
|
|
func TestExpr_ConstantFolding(t *testing.T) {
|
|
schema := newTestSchemaHelper(t)
|
|
|
|
// Expressions where constants can be folded
|
|
validExprs := []string{
|
|
// Add/Sub constant folding
|
|
`Int64Field > (1 + 2)`,
|
|
`Int64Field < (10 - 5)`,
|
|
`Int64Field == (1 + 2 + 3)`,
|
|
|
|
// Mul/Div/Mod constant folding
|
|
`Int64Field > (2 * 3)`,
|
|
`Int64Field < (10 / 2)`,
|
|
`Int64Field == (10 % 3)`,
|
|
|
|
// Mixed operations
|
|
`Int64Field > (2 * 3 + 4)`,
|
|
`Int64Field < (10 - 2 * 3)`,
|
|
|
|
// Float constant folding
|
|
`FloatField > (1.0 + 2.0)`,
|
|
`FloatField < (10.0 / 2.0)`,
|
|
}
|
|
|
|
for _, expr := range validExprs {
|
|
assertValidExpr(t, schema, expr)
|
|
}
|
|
}
|
|
|
|
// TestExpr_BooleanLiteral verifies how standalone "true"/"false" literals
|
|
// are parsed by the proxy expression parser.
|
|
//
|
|
// Key behavior:
|
|
// - Standalone "true" is converted to AlwaysTrueExpr
|
|
// - Standalone "false" is converted to AlwaysFalseExpr (UnaryExpr(Not, AlwaysTrueExpr))
|
|
// - Combined expressions like "BoolField == true" or "1==1" work fine
|
|
// - After rewriting, "1==1" becomes AlwaysTrueExpr, "1==2" becomes AlwaysFalseExpr
|
|
func TestExpr_BooleanLiteral(t *testing.T) {
|
|
schema := newTestSchema(true)
|
|
helper, err := typeutil.CreateSchemaHelper(schema)
|
|
require.NoError(t, err)
|
|
|
|
// Case 1: standalone "true" variants → AlwaysTrueExpr
|
|
for _, exprStr := range []string{"true", "True", "TRUE"} {
|
|
expr, err := ParseExpr(helper, exprStr, nil)
|
|
require.NoError(t, err, "standalone %q should succeed", exprStr)
|
|
assert.NotNil(t, expr.GetAlwaysTrueExpr(),
|
|
"standalone %q should become AlwaysTrueExpr", exprStr)
|
|
}
|
|
|
|
// Case 1b: standalone "false" variants → AlwaysFalseExpr
|
|
for _, exprStr := range []string{"false", "False", "FALSE"} {
|
|
expr, err := ParseExpr(helper, exprStr, nil)
|
|
require.NoError(t, err, "standalone %q should succeed", exprStr)
|
|
ue := expr.GetUnaryExpr()
|
|
require.NotNil(t, ue, "standalone %q should be AlwaysFalseExpr", exprStr)
|
|
assert.Equal(t, planpb.UnaryExpr_Not, ue.GetOp())
|
|
assert.NotNil(t, ue.GetChild().GetAlwaysTrueExpr())
|
|
}
|
|
|
|
// Case 2: verify that handleExpr (internal) parses them into ValueExpr with Bool
|
|
for _, exprStr := range []string{"true", "false"} {
|
|
ret := handleExpr(helper, exprStr)
|
|
ewt, ok := ret.(*ExprWithType)
|
|
require.True(t, ok, "handleExpr(%q) should return *ExprWithType", exprStr)
|
|
assert.Equal(t, schemapb.DataType_Bool, ewt.dataType)
|
|
assert.True(t, ewt.nodeDependent, "boolean literal should be nodeDependent")
|
|
|
|
ve := ewt.expr.GetValueExpr()
|
|
require.NotNil(t, ve, "should be ValueExpr for %q", exprStr)
|
|
if exprStr == "true" {
|
|
assert.True(t, ve.GetValue().GetBoolVal())
|
|
} else {
|
|
assert.False(t, ve.GetValue().GetBoolVal())
|
|
}
|
|
}
|
|
|
|
// Case 3: boolean literals in valid combined expressions
|
|
// These all produce executable boolean predicates
|
|
validBoolExprs := []string{
|
|
"BoolField == true",
|
|
"BoolField == false",
|
|
"BoolField != true",
|
|
"BoolField != false",
|
|
"BoolField in [true, false]",
|
|
}
|
|
for _, exprStr := range validBoolExprs {
|
|
assertValidExpr(t, helper, exprStr)
|
|
}
|
|
|
|
// Case 4: constant-folded expressions become AlwaysTrueExpr / AlwaysFalseExpr
|
|
// "1==1" constant-folds to ValueExpr(true), then rewriter converts to AlwaysTrueExpr
|
|
exprTrue, err := ParseExpr(helper, "1==1", nil)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, exprTrue.GetAlwaysTrueExpr(),
|
|
"1==1 should be rewritten to AlwaysTrueExpr")
|
|
|
|
// "1==2" constant-folds to ValueExpr(false), then rewriter converts to AlwaysFalseExpr
|
|
// AlwaysFalseExpr is represented as UnaryExpr(Not, AlwaysTrueExpr)
|
|
exprFalse, err := ParseExpr(helper, "1==2", nil)
|
|
require.NoError(t, err)
|
|
ue := exprFalse.GetUnaryExpr()
|
|
require.NotNil(t, ue, "1==2 should be rewritten to UnaryExpr(Not, AlwaysTrueExpr)")
|
|
assert.Equal(t, planpb.UnaryExpr_Not, ue.GetOp())
|
|
assert.NotNil(t, ue.GetChild().GetAlwaysTrueExpr())
|
|
|
|
// Case 5: empty expression becomes AlwaysTrueExpr (special case in handleExprInternal)
|
|
exprEmpty, err := ParseExpr(helper, "", nil)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, exprEmpty.GetAlwaysTrueExpr(),
|
|
"empty expression should be AlwaysTrueExpr")
|
|
|
|
// Case 6: "true and false" / "true or false" — two boolean literals connected by logical operators
|
|
// Both sides are GenericValue (from VisitBoolean), so VisitLogicalAnd calls And() which
|
|
// constant-folds to ValueExpr(BoolVal = true && false = false) with nodeDependent=false (default).
|
|
// Since nodeDependent=false, canBeExecuted() passes, then rewriter converts to AlwaysFalseExpr.
|
|
t.Run("true_and_false", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, "true and false", nil)
|
|
require.NoError(t, err, "\"true and false\" should be valid")
|
|
// And(true, false) = false → rewriter → AlwaysFalseExpr = UnaryExpr(Not, AlwaysTrueExpr)
|
|
ue := expr.GetUnaryExpr()
|
|
require.NotNil(t, ue, "should be AlwaysFalseExpr (UnaryExpr Not)")
|
|
assert.Equal(t, planpb.UnaryExpr_Not, ue.GetOp())
|
|
assert.NotNil(t, ue.GetChild().GetAlwaysTrueExpr())
|
|
})
|
|
|
|
t.Run("true_and_true", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, "true and true", nil)
|
|
require.NoError(t, err, "\"true and true\" should be valid")
|
|
// And(true, true) = true → rewriter → AlwaysTrueExpr
|
|
assert.NotNil(t, expr.GetAlwaysTrueExpr(),
|
|
"\"true and true\" should become AlwaysTrueExpr")
|
|
})
|
|
|
|
t.Run("true_or_false", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, "true or false", nil)
|
|
require.NoError(t, err, "\"true or false\" should be valid")
|
|
// Or(true, false) = true → rewriter → AlwaysTrueExpr
|
|
assert.NotNil(t, expr.GetAlwaysTrueExpr(),
|
|
"\"true or false\" should become AlwaysTrueExpr")
|
|
})
|
|
|
|
t.Run("false_or_false", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, "false or false", nil)
|
|
require.NoError(t, err, "\"false or false\" should be valid")
|
|
// Or(false, false) = false → rewriter → AlwaysFalseExpr
|
|
ue := expr.GetUnaryExpr()
|
|
require.NotNil(t, ue, "should be AlwaysFalseExpr")
|
|
assert.Equal(t, planpb.UnaryExpr_Not, ue.GetOp())
|
|
assert.NotNil(t, ue.GetChild().GetAlwaysTrueExpr())
|
|
})
|
|
|
|
t.Run("false_and_false", func(t *testing.T) {
|
|
expr, err := ParseExpr(helper, "false and false", nil)
|
|
require.NoError(t, err, "\"false and false\" should be valid")
|
|
// And(false, false) = false → rewriter → AlwaysFalseExpr
|
|
ue := expr.GetUnaryExpr()
|
|
require.NotNil(t, ue, "should be AlwaysFalseExpr")
|
|
assert.Equal(t, planpb.UnaryExpr_Not, ue.GetOp())
|
|
assert.NotNil(t, ue.GetChild().GetAlwaysTrueExpr())
|
|
})
|
|
|
|
// Case 7: boolean literal mixed with expression (issue #48443)
|
|
t.Run("true_or_expr", func(t *testing.T) {
|
|
// true or (Int64Field > 50) → AlwaysTrueExpr (short-circuit)
|
|
expr, err := ParseExpr(helper, "true or (Int64Field > 50)", nil)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, expr.GetAlwaysTrueExpr(),
|
|
"\"true or expr\" should become AlwaysTrueExpr")
|
|
})
|
|
|
|
t.Run("expr_or_true", func(t *testing.T) {
|
|
// (Int64Field > 50) or true → AlwaysTrueExpr (short-circuit)
|
|
expr, err := ParseExpr(helper, "(Int64Field > 50) or true", nil)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, expr.GetAlwaysTrueExpr(),
|
|
"\"expr or true\" should become AlwaysTrueExpr")
|
|
})
|
|
|
|
t.Run("false_or_expr", func(t *testing.T) {
|
|
// false or (Int64Field > 50) → Int64Field > 50
|
|
expr, err := ParseExpr(helper, "false or (Int64Field > 50)", nil)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, expr.GetUnaryRangeExpr(),
|
|
"\"false or expr\" should return the expr itself")
|
|
})
|
|
|
|
t.Run("true_and_expr", func(t *testing.T) {
|
|
// true and (Int64Field > 50) → Int64Field > 50
|
|
expr, err := ParseExpr(helper, "true and (Int64Field > 50)", nil)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, expr.GetUnaryRangeExpr(),
|
|
"\"true and expr\" should return the expr itself")
|
|
})
|
|
|
|
t.Run("expr_and_true", func(t *testing.T) {
|
|
// (Int64Field > 50) and true → Int64Field > 50
|
|
expr, err := ParseExpr(helper, "(Int64Field > 50) and true", nil)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, expr.GetUnaryRangeExpr(),
|
|
"\"expr and true\" should return the expr itself")
|
|
})
|
|
|
|
t.Run("false_and_expr", func(t *testing.T) {
|
|
// false and (Int64Field > 50) → AlwaysFalseExpr (short-circuit)
|
|
expr, err := ParseExpr(helper, "false and (Int64Field > 50)", nil)
|
|
require.NoError(t, err)
|
|
ue := expr.GetUnaryExpr()
|
|
require.NotNil(t, ue, "\"false and expr\" should become AlwaysFalseExpr")
|
|
assert.Equal(t, planpb.UnaryExpr_Not, ue.GetOp())
|
|
assert.NotNil(t, ue.GetChild().GetAlwaysTrueExpr())
|
|
})
|
|
|
|
t.Run("expr_and_false", func(t *testing.T) {
|
|
// (Int64Field > 50) and false → AlwaysFalseExpr (short-circuit)
|
|
expr, err := ParseExpr(helper, "(Int64Field > 50) and false", nil)
|
|
require.NoError(t, err)
|
|
ue := expr.GetUnaryExpr()
|
|
require.NotNil(t, ue, "\"expr and false\" should become AlwaysFalseExpr")
|
|
assert.Equal(t, planpb.UnaryExpr_Not, ue.GetOp())
|
|
assert.NotNil(t, ue.GetChild().GetAlwaysTrueExpr())
|
|
})
|
|
|
|
// Case 8: non-boolean literal with logical operators should still fail
|
|
t.Run("int_or_expr", func(t *testing.T) {
|
|
_, err := ParseExpr(helper, "1 or (Int64Field > 50)", nil)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("string_and_expr", func(t *testing.T) {
|
|
_, err := ParseExpr(helper, "\"hello\" and (Int64Field > 50)", nil)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|