Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:31:17 +08:00

3102 lines
98 KiB
Go

package planparserv2
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
"github.com/antlr4-go/antlr/v4"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
parser "github.com/milvus-io/milvus/internal/parser/planparserv2/generated"
"github.com/milvus-io/milvus/pkg/v3/proto/planpb"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
"github.com/milvus-io/milvus/pkg/v3/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
type ParserVisitorArgs struct {
Timezone string
}
// int64OverflowError is a special error type used to handle the case where
// 9223372036854775808 (which exceeds int64 max) is used with unary minus
// to represent -9223372036854775808 (int64 minimum value).
// This happens because ANTLR parses -9223372036854775808 as Unary(SUB, Integer(9223372036854775808)),
// causing the integer literal to exceed int64 range before the unary minus is applied.
type int64OverflowError struct {
literal string
}
func (e *int64OverflowError) Error() string {
return fmt.Sprintf("int64 overflow: %s", e.literal)
}
func isInt64OverflowError(err error) bool {
_, ok := err.(*int64OverflowError)
return ok
}
type ParserVisitor struct {
parser.BasePlanVisitor
schema *typeutil.SchemaHelper
args *ParserVisitorArgs
// currentStructArrayField stores the struct array field name when processing ElementFilter
currentStructArrayField string
}
func NewParserVisitor(schema *typeutil.SchemaHelper, args *ParserVisitorArgs) *ParserVisitor {
return &ParserVisitor{schema: schema, args: args}
}
// VisitParens unpack the parentheses.
func (v *ParserVisitor) VisitParens(ctx *parser.ParensContext) interface{} {
return ctx.Expr().Accept(v)
}
// errNullLiteral rejects a bare `null`/`NULL` used as an identifier. `NULL` is
// lexed as an ordinary identifier (it is not a grammar token), so a literal NULL
// where a column is expected — inside an `in [...]` list, a comparison/range
// operand, a function argument (`array_length(NULL)`), an `is null` target, a
// JSON/array subscript base (`NULL["x"]`), etc. — reaches a field lookup. Without
// a dynamic field it fails an opaque "field NULL not exist"; with a dynamic field
// it is silently mistaken for a JSON key named NULL. Treat bare `null`/`NULL` as a
// reserved word (issue #50882).
//
// The guard is schema-aware for backward compatibility: "null" only became a
// create-time keyword in this change, 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. A strict GetFieldFromName (NOT the
// DefaultJSON variant, whose dynamic-field fallback is exactly what produced
// the original misparse) decides: a real declared field resolves as before,
// anything else is rejected. A JSON key literally named "null" additionally
// stays reachable via quoting, e.g. `field["null"]` / `$meta["null"]`, whose
// base identifier is the field name, not "null".
func errNullLiteral() error {
return merr.WrapErrParameterInvalidMsg(
"NULL literal is not supported in expressions; use '<field> is null' or '<field> is not null' instead")
}
func (v *ParserVisitor) translateIdentifier(identifier string) (*ExprWithType, error) {
return v.translateIdentifierWithText(identifier, false)
}
func (v *ParserVisitor) translateIdentifierWithText(identifier string, allowText bool) (*ExprWithType, error) {
identifier = decodeUnicode(identifier)
if strings.EqualFold(identifier, "null") {
// Schema-aware: honor a legacy declared field literally named "null";
// strict lookup so a dynamic-field collection still rejects
// bare-null-as-JSON-key. See errNullLiteral.
if _, err := v.schema.GetFieldFromName(identifier); err != nil {
return nil, errNullLiteral()
}
}
field, err := v.schema.GetFieldFromNameDefaultJSON(identifier)
if err != nil {
return nil, err
}
var nestedPath []string
if identifier != field.Name {
nestedPath = append(nestedPath, identifier)
}
if field.DataType == schemapb.DataType_Text && !allowText {
return nil, merr.WrapErrParameterInvalidMsg("filter on text field (%s) is not supported yet", field.Name)
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_ColumnExpr{
ColumnExpr: &planpb.ColumnExpr{
Info: &planpb.ColumnInfo{
FieldId: field.FieldID,
DataType: field.DataType,
IsPrimaryKey: field.IsPrimaryKey,
IsAutoID: field.AutoID,
NestedPath: nestedPath,
IsPartitionKey: field.IsPartitionKey,
IsClusteringKey: field.IsClusteringKey,
ElementType: field.GetElementType(),
Nullable: field.GetNullable(),
},
},
},
},
dataType: field.DataType,
nodeDependent: true,
}, nil
}
// VisitIdentifier translates expr to column plan.
func (v *ParserVisitor) VisitIdentifier(ctx *parser.IdentifierContext) interface{} {
identifier := ctx.GetText()
expr, err := v.translateIdentifier(identifier)
if err != nil {
return err
}
return expr
}
// VisitBoolean translates expr to GenericValue.
func (v *ParserVisitor) VisitBoolean(ctx *parser.BooleanContext) interface{} {
literal := ctx.BooleanConstant().GetText()
b, err := strconv.ParseBool(literal)
if err != nil {
return err
}
return &ExprWithType{
dataType: schemapb.DataType_Bool,
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: NewBool(b),
},
},
},
nodeDependent: true,
}
}
// VisitInteger translates expr to GenericValue.
func (v *ParserVisitor) VisitInteger(ctx *parser.IntegerContext) interface{} {
literal := ctx.IntegerConstant().GetText()
i, err := strconv.ParseInt(literal, 0, 64)
if err != nil {
// Special case: 9223372036854775808 is out of int64 range,
// but -9223372036854775808 is valid (int64 minimum value).
// This happens because ANTLR parses -9223372036854775808 as:
// Unary(SUB, Integer(9223372036854775808))
// The integer literal 9223372036854775808 exceeds int64 max (9223372036854775807)
// before the unary minus is applied. We handle this in VisitUnary.
if literal == "9223372036854775808" {
return &int64OverflowError{literal: literal}
}
return err
}
return &ExprWithType{
dataType: schemapb.DataType_Int64,
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: NewInt(i),
},
},
},
nodeDependent: true,
}
}
// VisitFloating translates expr to GenericValue.
func (v *ParserVisitor) VisitFloating(ctx *parser.FloatingContext) interface{} {
literal := ctx.FloatingConstant().GetText()
f, err := strconv.ParseFloat(literal, 64)
if err != nil {
return err
}
return &ExprWithType{
dataType: schemapb.DataType_Double,
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: NewFloat(f),
},
},
},
nodeDependent: true,
}
}
// VisitString translates expr to GenericValue.
func (v *ParserVisitor) VisitString(ctx *parser.StringContext) interface{} {
pattern, err := convertEscapeSingle(ctx.GetText())
if err != nil {
return err
}
return &ExprWithType{
dataType: schemapb.DataType_VarChar,
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: NewString(pattern),
},
},
},
nodeDependent: true,
}
}
// VisitRawString handles raw string literals (r"..." / R'...'). Unlike
// VisitString, a backslash is NOT an escape character here: the content between
// the quotes is taken verbatim, so no convertEscapeSingle/strconv.Unquote pass
// runs. This removes one layer of backslash halving — e.g. matching a literal
// '\' in a LIKE pattern is r"\\" instead of "\\\\", aligning with the raw
// string literals of BigQuery / Spark SQL. The LIKE/regex escape layer still
// applies on the resulting value (r"\%" -> literal %, same as BigQuery r'\%').
func (v *ParserVisitor) VisitRawString(ctx *parser.RawStringContext) interface{} {
text := ctx.GetText()
// text is r"..." or R'...'; drop the one-byte prefix + the surrounding quotes.
content := text[2 : len(text)-1]
return &ExprWithType{
dataType: schemapb.DataType_VarChar,
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: NewString(content),
},
},
},
nodeDependent: true,
}
}
func (v *ParserVisitor) parseStringLiteralOrTemplate(ctx parser.IExprContext, argName string) (string, string, bool, error) {
if ctx == nil {
return "", "", false, merr.WrapErrParameterInvalidMsg("%s is missing", argName)
}
parsed := ctx.Accept(v)
if err := getError(parsed); err != nil {
return "", "", false, err
}
valueExpr := getValueExpr(parsed)
if valueExpr == nil {
return "", "", false, merr.WrapErrParameterInvalidMsg("%s should be a string literal or template variable, got: %s", argName, ctx.GetText())
}
if isTemplateExpr(valueExpr) {
return "", valueExpr.GetTemplateVariableName(), true, nil
}
value := valueExpr.GetValue()
if value == nil || !IsString(value) {
return "", "", false, merr.WrapErrParameterInvalidMsg("%s should be a string literal or template variable, got: %s", argName, ctx.GetText())
}
return value.GetStringVal(), "", false, nil
}
func (v *ParserVisitor) parseRegexPatternOrTemplate(ctx parser.IExprContext, argName string) (string, string, bool, error) {
if ctx == nil {
return "", "", false, merr.WrapErrParameterInvalidMsg("%s is missing", argName)
}
if _, ok := ctx.(*parser.StringContext); ok {
pattern, err := extractRegexPattern(ctx.GetText())
return pattern, "", false, err
}
if raw, ok := ctx.(*parser.RawStringContext); ok {
// Raw string: the regex pattern is the content verbatim, no escape
// processing — backslashes (\d, \., \\) reach the engine as written.
text := raw.GetText()
return text[2 : len(text)-1], "", false, nil
}
parsed := ctx.Accept(v)
if err := getError(parsed); err != nil {
return "", "", false, err
}
valueExpr := getValueExpr(parsed)
if valueExpr == nil || !isTemplateExpr(valueExpr) {
return "", "", false, merr.WrapErrParameterInvalidMsg("%s should be a string literal or template variable, got: %s", argName, ctx.GetText())
}
return "", valueExpr.GetTemplateVariableName(), true, nil
}
func checkDirectComparisonBinaryField(columnInfo *planpb.ColumnInfo) error {
if typeutil.IsArrayType(columnInfo.GetDataType()) && len(columnInfo.GetNestedPath()) == 0 && !columnInfo.GetIsElementLevel() {
return merr.WrapErrQueryPlanMsg("can not comparisons array fields directly")
}
return nil
}
// VisitAddSub translates expr to arithmetic plan.
func (v *ParserVisitor) VisitAddSub(ctx *parser.AddSubContext) interface{} {
var err error
left := ctx.Expr(0).Accept(v)
if err = getError(left); err != nil {
return err
}
right := ctx.Expr(1).Accept(v)
if err = getError(right); err != nil {
return err
}
leftValueExpr, rightValueExpr := getValueExpr(left), getValueExpr(right)
if leftValueExpr != nil && rightValueExpr != nil {
if isTemplateExpr(leftValueExpr) || isTemplateExpr(rightValueExpr) {
return merr.WrapErrParameterInvalidMsg("placeholder was not supported between two constants with operator: %s", ctx.GetOp().GetText())
}
leftValue, rightValue := leftValueExpr.GetValue(), rightValueExpr.GetValue()
switch ctx.GetOp().GetTokenType() {
case parser.PlanParserADD:
n, err := Add(leftValue, rightValue)
if err != nil {
return err
}
return n
case parser.PlanParserSUB:
n, err := Subtract(leftValue, rightValue)
if err != nil {
return err
}
return n
default:
return merr.WrapErrParameterInvalidMsg("unexpected op: %s", ctx.GetOp().GetText())
}
}
leftExpr, rightExpr := getExpr(left), getExpr(right)
reverse := leftValueExpr != nil
if leftExpr == nil || rightExpr == nil {
return merr.WrapErrParameterInvalidMsg("invalid arithmetic expression, left: %s, op: %s, right: %s", ctx.Expr(0).GetText(), ctx.GetOp(), ctx.Expr(1).GetText())
}
if err = checkDirectComparisonBinaryField(toColumnInfo(leftExpr)); err != nil {
return err
}
if err = checkDirectComparisonBinaryField(toColumnInfo(rightExpr)); err != nil {
return err
}
var dataType schemapb.DataType
if leftExpr.expr.GetIsTemplate() {
dataType = rightExpr.dataType
} else if rightExpr.expr.GetIsTemplate() {
dataType = leftExpr.dataType
} else {
if err := canArithmetic(leftExpr.dataType, getArrayElementType(leftExpr), rightExpr.dataType, getArrayElementType(rightExpr), reverse); err != nil {
return merr.WrapErrParameterInvalidMsg("'%s' %s", arithNameMap[ctx.GetOp().GetTokenType()], err.Error())
}
dataType, err = calcDataType(leftExpr, rightExpr, reverse)
if err != nil {
return err
}
}
expr := &planpb.Expr{
Expr: &planpb.Expr_BinaryArithExpr{
BinaryArithExpr: &planpb.BinaryArithExpr{
Left: leftExpr.expr,
Right: rightExpr.expr,
Op: arithExprMap[ctx.GetOp().GetTokenType()],
},
},
IsTemplate: leftExpr.expr.GetIsTemplate() || rightExpr.expr.GetIsTemplate(),
}
return &ExprWithType{
expr: expr,
dataType: dataType,
nodeDependent: true,
}
}
// VisitMulDivMod translates expr to arithmetic plan.
func (v *ParserVisitor) VisitMulDivMod(ctx *parser.MulDivModContext) interface{} {
var err error
left := ctx.Expr(0).Accept(v)
if err := getError(left); err != nil {
return err
}
right := ctx.Expr(1).Accept(v)
if err := getError(right); err != nil {
return err
}
leftValueExpr, rightValueExpr := getValueExpr(left), getValueExpr(right)
if leftValueExpr != nil && rightValueExpr != nil {
if isTemplateExpr(leftValueExpr) || isTemplateExpr(rightValueExpr) {
return merr.WrapErrParameterInvalidMsg("placeholder was not supported between two constants with operator: %s", ctx.GetOp().GetText())
}
leftValue, rightValue := getGenericValue(left), getGenericValue(right)
switch ctx.GetOp().GetTokenType() {
case parser.PlanParserMUL:
n, err := Multiply(leftValue, rightValue)
if err != nil {
return err
}
return n
case parser.PlanParserDIV:
n, err := Divide(leftValue, rightValue)
if err != nil {
return err
}
return n
case parser.PlanParserMOD:
n, err := Modulo(leftValue, rightValue)
if err != nil {
return err
}
return n
default:
return merr.WrapErrParameterInvalidMsg("unexpected op: %s", ctx.GetOp().GetText())
}
}
leftExpr, rightExpr := getExpr(left), getExpr(right)
reverse := leftValueExpr != nil
if leftExpr == nil || rightExpr == nil {
return merr.WrapErrParameterInvalidMsg("invalid arithmetic expression, left: %s, op: %s, right: %s", ctx.Expr(0).GetText(), ctx.GetOp(), ctx.Expr(1).GetText())
}
if err := checkDirectComparisonBinaryField(toColumnInfo(leftExpr)); err != nil {
return err
}
if err := checkDirectComparisonBinaryField(toColumnInfo(rightExpr)); err != nil {
return err
}
var dataType schemapb.DataType
if leftExpr.expr.GetIsTemplate() {
dataType = rightExpr.dataType
} else if rightExpr.expr.GetIsTemplate() {
dataType = leftExpr.dataType
} else {
if err := canArithmetic(leftExpr.dataType, getArrayElementType(leftExpr), rightExpr.dataType, getArrayElementType(rightExpr), reverse); err != nil {
return merr.WrapErrParameterInvalidMsg("'%s' %s", arithNameMap[ctx.GetOp().GetTokenType()], err.Error())
}
if err = checkValidModArith(arithExprMap[ctx.GetOp().GetTokenType()], leftExpr.dataType, getArrayElementType(leftExpr), rightExpr.dataType, getArrayElementType(rightExpr)); err != nil {
return err
}
dataType, err = calcDataType(leftExpr, rightExpr, reverse)
if err != nil {
return err
}
}
expr := &planpb.Expr{
Expr: &planpb.Expr_BinaryArithExpr{
BinaryArithExpr: &planpb.BinaryArithExpr{
Left: leftExpr.expr,
Right: rightExpr.expr,
Op: arithExprMap[ctx.GetOp().GetTokenType()],
},
},
IsTemplate: leftExpr.expr.GetIsTemplate() || rightExpr.expr.GetIsTemplate(),
}
return &ExprWithType{
expr: expr,
dataType: dataType,
nodeDependent: true,
}
}
// VisitEquality translates expr to compare/range plan.
func (v *ParserVisitor) VisitEquality(ctx *parser.EqualityContext) interface{} {
left := ctx.Expr(0).Accept(v)
if err := getError(left); err != nil {
return err
}
right := ctx.Expr(1).Accept(v)
if err := getError(right); err != nil {
return err
}
leftValueExpr, rightValueExpr := getValueExpr(left), getValueExpr(right)
if leftValueExpr != nil && rightValueExpr != nil {
if isTemplateExpr(leftValueExpr) || isTemplateExpr(rightValueExpr) {
return merr.WrapErrParameterInvalidMsg("placeholder was not supported between two constants with operator: %s", ctx.GetOp().GetText())
}
leftValue, rightValue := leftValueExpr.GetValue(), rightValueExpr.GetValue()
var ret *ExprWithType
switch ctx.GetOp().GetTokenType() {
case parser.PlanParserEQ:
ret = Equal(leftValue, rightValue)
case parser.PlanParserNE:
ret = NotEqual(leftValue, rightValue)
default:
return merr.WrapErrParameterInvalidMsg("unexpected op: %s", ctx.GetOp().GetText())
}
if ret == nil {
return merr.WrapErrParameterInvalidMsg("comparison operations cannot be applied to two incompatible operands: %s", ctx.GetText())
}
return ret
}
leftExpr, rightExpr := getExpr(left), getExpr(right)
expr, err := HandleCompare(ctx.GetOp().GetTokenType(), leftExpr, rightExpr)
if err != nil {
return err
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
// VisitRelational translates expr to range/compare plan.
func (v *ParserVisitor) VisitRelational(ctx *parser.RelationalContext) interface{} {
left := ctx.Expr(0).Accept(v)
if err := getError(left); err != nil {
return err
}
right := ctx.Expr(1).Accept(v)
if err := getError(right); err != nil {
return err
}
leftValueExpr, rightValueExpr := getValueExpr(left), getValueExpr(right)
if leftValueExpr != nil && rightValueExpr != nil {
if isTemplateExpr(leftValueExpr) || isTemplateExpr(rightValueExpr) {
return merr.WrapErrParameterInvalidMsg("placeholder was not supported between two constants with operator: %s", ctx.GetOp().GetText())
}
leftValue, rightValue := getGenericValue(left), getGenericValue(right)
var ret *ExprWithType
switch ctx.GetOp().GetTokenType() {
case parser.PlanParserLT:
ret = Less(leftValue, rightValue)
case parser.PlanParserLE:
ret = LessEqual(leftValue, rightValue)
case parser.PlanParserGT:
ret = Greater(leftValue, rightValue)
case parser.PlanParserGE:
ret = GreaterEqual(leftValue, rightValue)
default:
return merr.WrapErrParameterInvalidMsg("unexpected op: %s", ctx.GetOp().GetText())
}
if ret == nil {
return merr.WrapErrParameterInvalidMsg("comparison operations cannot be applied to two incompatible operands: %s", ctx.GetText())
}
return ret
}
leftExpr, rightExpr := getExpr(left), getExpr(right)
if err := checkDirectComparisonBinaryField(toColumnInfo(leftExpr)); err != nil {
return err
}
if err := checkDirectComparisonBinaryField(toColumnInfo(rightExpr)); err != nil {
return err
}
expr, err := HandleCompare(ctx.GetOp().GetTokenType(), leftExpr, rightExpr)
if err != nil {
return err
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
// VisitLike handles match operations.
func (v *ParserVisitor) VisitLike(ctx *parser.LikeContext) interface{} {
left := ctx.Expr(0).Accept(v)
if err := getError(left); err != nil {
return err
}
leftExpr := getExpr(left)
if leftExpr == nil {
return merr.WrapErrQueryPlanMsg("the left operand of like is invalid")
}
column := toColumnInfo(leftExpr)
if column == nil {
return merr.WrapErrQueryPlanMsg("like operation on complicated expr is unsupported")
}
if err := checkDirectComparisonBinaryField(column); err != nil {
return err
}
if !typeutil.IsStringType(leftExpr.dataType) && !typeutil.IsJSONType(leftExpr.dataType) &&
(!typeutil.IsArrayType(leftExpr.dataType) || !typeutil.IsStringType(column.GetElementType())) {
return merr.WrapErrQueryPlanMsg("like operation on non-string or no-json field is unsupported")
}
pattern, placeholder, isTemplate, err := v.parseStringLiteralOrTemplate(ctx.Expr(1), "like pattern")
if err != nil {
return err
}
op := planpb.OpType_Match
var value *planpb.GenericValue
if isTemplate {
value = nil
} else {
operand := ""
op, operand, err = translatePatternMatch(pattern)
if err != nil {
return err
}
value = NewString(operand)
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: column,
Op: op,
Value: value,
TemplateVariableName: placeholder,
},
},
IsTemplate: isTemplate,
},
dataType: schemapb.DataType_Bool,
}
}
// extractRegexPattern extracts a regex pattern from an ANTLR string literal
// token. Unlike convertEscapeSingle which uses strconv.Unquote (which rejects
// regex escapes like \d, \., \p), this function preserves all backslash
// sequences as-is, only processing quote delimiters.
func extractRegexPattern(literal string) (string, error) {
if len(literal) < 2 {
return "", merr.WrapErrQueryPlanMsg("invalid string literal: %s", literal)
}
quote := literal[0]
if (quote != '"' && quote != '\'') || literal[len(literal)-1] != quote {
return "", merr.WrapErrQueryPlanMsg("invalid string literal: %s", literal)
}
// Strip surrounding quotes, preserve all escape sequences as-is
inner := literal[1 : len(literal)-1]
var result strings.Builder
result.Grow(len(inner))
for i := 0; i < len(inner); i++ {
if inner[i] == '\\' && i+1 < len(inner) {
next := inner[i+1]
switch next {
case quote:
// Escaped quote → literal quote character
result.WriteByte(quote)
i++
case '\\':
// Escaped backslash → single backslash
result.WriteByte('\\')
i++
default:
// All other escapes: pass through as-is (e.g. \d, \., \p, \n)
result.WriteByte('\\')
result.WriteByte(next)
i++
}
} else {
result.WriteByte(inner[i])
}
}
return result.String(), nil
}
// tryOptimizeRegexToLike attempts to convert anchored simple regex patterns to
// more efficient LIKE operations. Returns (op, operand, true) if optimization is
// possible, or (_, _, false) if the pattern must remain as RegexMatch.
//
// Only patterns composed entirely of literal characters and ^/$ anchors are
// converted. Any regex metacharacter causes the function to return false.
func tryOptimizeRegexToLike(pattern string) (planpb.OpType, string, bool) {
if len(pattern) == 0 {
// Empty pattern matches everything in PartialMatch — cannot be
// simplified to a single LIKE op.
return 0, "", false
}
hasStart := false
hasEnd := false
inner := pattern
if inner[0] == '^' {
hasStart = true
inner = inner[1:]
}
if len(inner) > 0 && inner[len(inner)-1] == '$' {
// Make sure the $ is not escaped
if len(inner) < 2 || inner[len(inner)-2] != '\\' {
hasEnd = true
inner = inner[:len(inner)-1]
}
}
// Check that the remaining string is purely literal (no metacharacters).
// Walk character by character, handling escape sequences.
var literal []byte
for i := 0; i < len(inner); i++ {
c := inner[i]
if c == '\\' && i+1 < len(inner) {
next := inner[i+1]
// Only escaped metacharacters produce a literal character.
// Shorthand classes (\d, \w, \s, etc.) are not literal.
switch next {
case '.', '+', '*', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\':
literal = append(literal, next)
i++ // skip next
default:
// \d, \w, \s, \b, etc. — not purely literal
return 0, "", false
}
} else if c == '.' || c == '+' || c == '*' || c == '?' ||
c == '{' || c == '}' || c == '(' || c == ')' ||
c == '|' || c == '[' || c == ']' || c == '^' || c == '$' {
// Unescaped metacharacter — cannot optimize
return 0, "", false
} else {
literal = append(literal, c)
}
}
lit := string(literal)
if len(lit) == 0 {
// After stripping anchors, nothing left (e.g., "^$" matches only "")
if hasStart && hasEnd {
return planpb.OpType_Equal, "", true
}
return 0, "", false
}
switch {
case hasStart && hasEnd:
return planpb.OpType_Equal, lit, true
case hasStart:
return planpb.OpType_PrefixMatch, lit, true
case hasEnd:
return planpb.OpType_PostfixMatch, lit, true
default:
// Keep unanchored literal regex as RegexMatch. RE2's literal
// PartialMatch path is faster than Milvus InnerMatch in current
// growing-segment benchmarks.
return 0, "", false
}
}
func validateAndOptimizeRegexPattern(pattern string) (planpb.OpType, string, error) {
if _, err := regexp.Compile(pattern); err != nil {
return 0, "", merr.WrapErrQueryPlan(err, "invalid regex pattern")
}
op := planpb.OpType_RegexMatch
operand := pattern
if optOp, optOperand, ok := tryOptimizeRegexToLike(pattern); ok {
op = optOp
operand = optOperand
}
return op, operand, nil
}
func isRegexMatchSupportedType(dataType schemapb.DataType, elementType schemapb.DataType) bool {
return typeutil.IsStringType(dataType) ||
typeutil.IsJSONType(dataType) ||
(typeutil.IsArrayType(dataType) && typeutil.IsStringType(elementType))
}
// VisitRegexMatch handles =~ regex match operations.
func (v *ParserVisitor) VisitRegexMatch(ctx *parser.RegexMatchContext) interface{} {
left := ctx.Expr(0).Accept(v)
if err := getError(left); err != nil {
return err
}
leftExpr := getExpr(left)
if leftExpr == nil {
return merr.WrapErrQueryPlanMsg("the left operand of =~ is invalid")
}
column := toColumnInfo(leftExpr)
if column == nil {
return merr.WrapErrQueryPlanMsg("regex match on complicated expr is unsupported")
}
if err := checkDirectComparisonBinaryField(column); err != nil {
return err
}
if !isRegexMatchSupportedType(leftExpr.dataType, column.GetElementType()) {
return merr.WrapErrQueryPlanMsg("regex match on non-string or non-json field is unsupported")
}
pattern, placeholder, isTemplate, err := v.parseRegexPatternOrTemplate(ctx.Expr(1), "regex pattern")
if err != nil {
return err
}
op := planpb.OpType_RegexMatch
var value *planpb.GenericValue
if !isTemplate {
operand := ""
op, operand, err = validateAndOptimizeRegexPattern(pattern)
if err != nil {
return err
}
value = NewString(operand)
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: column,
Op: op,
Value: value,
TemplateVariableName: placeholder,
},
},
IsTemplate: isTemplate,
},
dataType: schemapb.DataType_Bool,
}
}
// VisitRegexNotMatch handles !~ regex not match operations.
func (v *ParserVisitor) VisitRegexNotMatch(ctx *parser.RegexNotMatchContext) interface{} {
left := ctx.Expr(0).Accept(v)
if err := getError(left); err != nil {
return err
}
leftExpr := getExpr(left)
if leftExpr == nil {
return merr.WrapErrQueryPlanMsg("the left operand of !~ is invalid")
}
column := toColumnInfo(leftExpr)
if column == nil {
return merr.WrapErrQueryPlanMsg("regex match on complicated expr is unsupported")
}
if err := checkDirectComparisonBinaryField(column); err != nil {
return err
}
if !isRegexMatchSupportedType(leftExpr.dataType, column.GetElementType()) {
return merr.WrapErrQueryPlanMsg("regex match on non-string or non-json field is unsupported")
}
pattern, placeholder, isTemplate, err := v.parseRegexPatternOrTemplate(ctx.Expr(1), "regex pattern")
if err != nil {
return err
}
op := planpb.OpType_RegexMatch
var value *planpb.GenericValue
if !isTemplate {
operand := ""
op, operand, err = validateAndOptimizeRegexPattern(pattern)
if err != nil {
return err
}
value = NewString(operand)
}
innerExpr := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: column,
Op: op,
Value: value,
TemplateVariableName: placeholder,
},
},
IsTemplate: isTemplate,
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryExpr{
UnaryExpr: &planpb.UnaryExpr{
Op: planpb.UnaryExpr_Not,
Child: innerExpr,
},
},
IsTemplate: isTemplate,
},
dataType: schemapb.DataType_Bool,
}
}
// parseTextMatchOperand runs the shared prologue of the text_match /
// text_match_fuzzy / phrase_match visitors: resolve the field, require a
// text-match-enabled string column, and parse the query literal or template.
func (v *ParserVisitor) parseTextMatchOperand(identifier string, queryExpr parser.IExprContext, opName string, argName string) (*planpb.ColumnInfo, *planpb.GenericValue, string, bool, error) {
column, err := v.translateIdentifierWithText(identifier, true)
if err != nil {
return nil, nil, "", false, err
}
columnInfo := toColumnInfo(column)
if !typeutil.IsStringType(column.dataType) {
return nil, nil, "", false, merr.WrapErrQueryPlanMsg("%s operation on non-string is unsupported", opName)
}
if !v.schema.IsFieldTextMatchEnabled(columnInfo.FieldId) {
return nil, nil, "", false, merr.WrapErrParameterInvalidMsg("field \"%s\" does not enable match", identifier)
}
queryText, placeholder, isTemplate, err := v.parseStringLiteralOrTemplate(queryExpr, argName)
if err != nil {
return nil, nil, "", false, err
}
var value *planpb.GenericValue
if !isTemplate {
value = NewString(queryText)
}
return columnInfo, value, placeholder, isTemplate, nil
}
func (v *ParserVisitor) VisitTextMatch(ctx *parser.TextMatchContext) interface{} {
identifier := ctx.Identifier().GetText()
columnInfo, value, placeholder, isTemplate, err := v.parseTextMatchOperand(
identifier, ctx.Expr(), "text match", "text_match query")
if err != nil {
return err
}
// Handle optional min_should_match parameter
var extraValues []*planpb.GenericValue
if ctx.TextMatchOption() != nil {
minShouldMatchExpr := ctx.TextMatchOption().Accept(v)
if err, ok := minShouldMatchExpr.(error); ok {
return err
}
extraVal, err := validateAndExtractMinShouldMatch(minShouldMatchExpr)
if err != nil {
return err
}
extraValues = extraVal
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: columnInfo,
Op: planpb.OpType_TextMatch,
Value: value,
TemplateVariableName: placeholder,
ExtraValues: extraValues,
},
},
IsTemplate: isTemplate,
},
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitTextMatchFuzzy(ctx *parser.TextMatchFuzzyContext) interface{} {
identifier := ctx.Identifier(0).GetText()
columnInfo, value, placeholder, isTemplate, err := v.parseTextMatchOperand(
identifier, ctx.Expr(), "text match fuzzy", "text_match_fuzzy query")
if err != nil {
return err
}
// The option name is a soft keyword (a plain identifier) so that a scalar
// field literally named "max_edit_distance" is still usable elsewhere in a
// filter; only accept the expected option name here.
optionName := ctx.Identifier(1).GetText()
if !strings.EqualFold(optionName, "max_edit_distance") {
return merr.WrapErrParameterInvalidMsg(
"invalid option %q for text_match_fuzzy, expected max_edit_distance", optionName)
}
// tantivy's fuzzy automaton only supports an edit distance of 0, 1 or 2.
distanceText := ctx.IntegerConstant().GetText()
maxEditDistance, err := strconv.ParseInt(distanceText, 0, 64)
if err != nil {
return merr.WrapErrParameterInvalidMsg("invalid max_edit_distance value: %s", distanceText)
}
if maxEditDistance < 0 || maxEditDistance > 2 {
return merr.WrapErrParameterInvalidMsg("max_edit_distance should be in [0, 2], got %d", maxEditDistance)
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: columnInfo,
Op: planpb.OpType_TextMatchFuzzy,
Value: value,
TemplateVariableName: placeholder,
ExtraValues: []*planpb.GenericValue{NewInt(maxEditDistance)},
},
},
IsTemplate: isTemplate,
},
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitTextMatchOption(ctx *parser.TextMatchOptionContext) interface{} {
// Parse the integer constant for minimum_should_match
integerConstant := ctx.IntegerConstant().GetText()
value, err := strconv.ParseInt(integerConstant, 0, 64)
if err != nil {
return merr.WrapErrParameterInvalidMsg("invalid minimum_should_match value: %s", integerConstant)
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: NewInt(value),
},
},
},
dataType: schemapb.DataType_Int64,
}
}
func (v *ParserVisitor) VisitPhraseMatch(ctx *parser.PhraseMatchContext) interface{} {
identifier := ctx.Identifier().GetText()
columnInfo, value, placeholder, isTemplate, err := v.parseTextMatchOperand(
identifier, ctx.Expr(0), "phrase match", "phrase_match query")
if err != nil {
return err
}
var slop int64 = 0
if ctx.Expr(1) != nil {
slopExpr := ctx.Expr(1).Accept(v)
slopValueExpr := getValueExpr(slopExpr)
if slopValueExpr == nil || slopValueExpr.GetValue() == nil {
return merr.WrapErrParameterInvalidMsg("\"slop\" should be a const integer expression with \"uint32\" value. \"slop\" expression passed: %s", ctx.Expr(1).GetText())
}
slop = slopValueExpr.GetValue().GetInt64Val()
if slop < 0 {
return merr.WrapErrParameterInvalidMsg("\"slop\" should not be a negative interger. \"slop\" passed: %s", ctx.Expr(1).GetText())
}
if slop > math.MaxUint32 {
return merr.WrapErrParameterInvalidMsg("\"slop\" exceeds the range of \"uint32\". \"slop\" expression passed: %s", ctx.Expr(1).GetText())
}
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: columnInfo,
Op: planpb.OpType_PhraseMatch,
Value: value,
TemplateVariableName: placeholder,
ExtraValues: []*planpb.GenericValue{NewInt(slop)},
},
},
IsTemplate: isTemplate,
},
dataType: schemapb.DataType_Bool,
}
}
func isRandomSampleExpr(expr *ExprWithType) bool {
return expr.expr.GetRandomSampleExpr() != nil
}
func isElementFilterExpr(expr *ExprWithType) bool {
return expr.expr.GetElementFilterExpr() != nil
}
const EPSILON = 1e-10
func (v *ParserVisitor) VisitRandomSample(ctx *parser.RandomSampleContext) interface{} {
if ctx.Expr() == nil {
return merr.WrapErrParameterInvalidMsg("sample factor missed: %s", ctx.GetText())
}
floatExpr := ctx.Expr().Accept(v)
if err := getError(floatExpr); err != nil {
return merr.WrapErrParameterInvalidMsg("cannot parse expression: %s, error: %s", ctx.Expr().GetText(), err)
}
floatValueExpr := getValueExpr(floatExpr)
if floatValueExpr == nil || floatValueExpr.GetValue() == nil {
return merr.WrapErrParameterInvalidMsg("\"float factor\" should be a const float expression: \"float factor\" passed: %s", ctx.Expr().GetText())
}
sampleFactor := floatValueExpr.GetValue().GetFloatVal()
if sampleFactor <= 0+EPSILON || sampleFactor >= 1-EPSILON {
return merr.WrapErrParameterInvalidMsg("the sample factor should be between 0 and 1 and not too close to 0 or 1(the difference should be larger than 1e-10), but got %s", ctx.Expr().GetText())
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_RandomSampleExpr{
RandomSampleExpr: &planpb.RandomSampleExpr{
SampleFactor: float32(sampleFactor),
Predicate: nil,
},
},
},
dataType: schemapb.DataType_Bool,
}
}
// VisitTerm translates expr to term plan.
func (v *ParserVisitor) VisitTerm(ctx *parser.TermContext) interface{} {
child := ctx.Expr(0).Accept(v)
if err := getError(child); err != nil {
return err
}
if childValue := getGenericValue(child); childValue != nil {
return merr.WrapErrParameterInvalidMsg("'term' can only be used on non-const expression, but got: %s", ctx.Expr(0).GetText())
}
childExpr := getExpr(child)
columnInfo := toColumnInfo(childExpr)
if columnInfo == nil {
return merr.WrapErrParameterInvalidMsg("'term' can only be used on single field, but got: %s", ctx.Expr(0).GetText())
}
dataType := columnInfo.GetDataType()
// Use element type for IN operation in two cases:
// 1. Array with nested path (e.g., arr[0] IN [1, 2, 3])
// 2. Array with element level flag (e.g., $[intField] IN [1, 2] in MATCH_ALL/ElementFilter)
if typeutil.IsArrayType(dataType) && (len(columnInfo.GetNestedPath()) != 0 || columnInfo.GetIsElementLevel()) {
dataType = columnInfo.GetElementType()
}
term := ctx.Expr(1).Accept(v)
if getError(term) != nil {
return term
}
valueExpr := getValueExpr(term)
var placeholder string
var isTemplate bool
var values []*planpb.GenericValue
if valueExpr.GetValue() == nil && valueExpr.GetTemplateVariableName() != "" {
placeholder = valueExpr.GetTemplateVariableName()
values = nil
isTemplate = true
} else {
elementValue := valueExpr.GetValue()
if elementValue == nil {
return merr.WrapErrParameterInvalidMsg("value '%s' in list cannot be a non-const expression", ctx.Expr(1).GetText())
}
if !IsArray(elementValue) {
return merr.WrapErrParameterInvalidMsg("the right-hand side of 'in' must be a list, but got: %s", ctx.Expr(1).GetText())
}
array := elementValue.GetArrayVal().GetArray()
values = make([]*planpb.GenericValue, len(array))
for i, e := range array {
castedValue, err := castValue(dataType, e)
if err != nil {
return merr.WrapErrParameterInvalidMsg("value '%s' in list cannot be casted to %s", e.String(), dataType.String())
}
values[i] = castedValue
}
// For JSON type, ensure all numeric values have consistent type.
// If there's a mix of integers and floats, convert all to floats.
if typeutil.IsJSONType(dataType) && len(values) > 0 {
hasInt := false
hasFloat := false
for _, val := range values {
if IsInteger(val) {
hasInt = true
} else if IsFloating(val) {
hasFloat = true
}
}
// If we have both int and float, convert all ints to floats
if hasInt && hasFloat {
for i, val := range values {
if IsInteger(val) {
values[i] = NewFloat(float64(val.GetInt64Val()))
}
}
}
}
}
expr := &planpb.Expr{
Expr: &planpb.Expr_TermExpr{
TermExpr: &planpb.TermExpr{
ColumnInfo: columnInfo,
Values: values,
TemplateVariableName: placeholder,
},
},
IsTemplate: isTemplate,
}
if ctx.GetOp() != nil {
expr = &planpb.Expr{
Expr: &planpb.Expr_UnaryExpr{
UnaryExpr: &planpb.UnaryExpr{
Op: planpb.UnaryExpr_Not,
Child: expr,
},
},
IsTemplate: isTemplate,
}
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
func isValidStructSubField(tokenText string) bool {
return len(tokenText) >= 4 && tokenText[:2] == "$[" && tokenText[len(tokenText)-1] == ']'
}
func (v *ParserVisitor) getColumnInfoFromStructSubField(tokenText string) (*planpb.ColumnInfo, error) {
if !isValidStructSubField(tokenText) {
return nil, merr.WrapErrParameterInvalidMsg("invalid struct sub-field syntax: %s", tokenText)
}
// Remove "$[" prefix and "]" suffix
fieldName := tokenText[2 : len(tokenText)-1]
// Check if we're inside an ElementFilter context
if v.currentStructArrayField == "" {
return nil, merr.WrapErrParameterInvalidMsg("$[%s] syntax can only be used inside ElementFilter", fieldName)
}
// Construct full field name for struct array field
fullFieldName := typeutil.ConcatStructFieldName(v.currentStructArrayField, fieldName)
// Get the struct array field info
field, err := v.schema.GetFieldFromName(fullFieldName)
if err != nil {
return nil, merr.WrapErrParameterInvalidMsg("array field not found: %s, error: %s", fullFieldName, err)
}
// In element-level context, data_type should be the element type
elementType := field.GetElementType()
nullable := field.GetNullable()
if structField := v.schema.GetStructArrayFieldFromName(v.currentStructArrayField); structField != nil {
nullable = nullable || structField.GetNullable()
}
return &planpb.ColumnInfo{
FieldId: field.FieldID,
DataType: elementType, // Use element type, not storage type
IsPrimaryKey: field.IsPrimaryKey,
IsAutoID: field.AutoID,
IsPartitionKey: field.IsPartitionKey,
IsClusteringKey: field.IsClusteringKey,
ElementType: elementType,
Nullable: nullable,
IsElementLevel: true, // Mark as element-level access
}, nil
}
func (v *ParserVisitor) getColumnInfoFromStructIndexField(identifier string) (*planpb.ColumnInfo, error) {
// Parse "struct_arr[0][sub_field]" -> fieldName="struct_arr", index="0", subField="sub_field"
parts := strings.SplitN(identifier, "[", 3)
if len(parts) != 3 {
return nil, merr.WrapErrParameterInvalidMsg("invalid struct index field identifier: %s", identifier)
}
fieldName := parts[0]
index := strings.TrimSuffix(parts[1], "]")
subField := strings.TrimSuffix(parts[2], "]")
structFieldName := fieldName + "[" + subField + "]"
field, err := v.schema.GetFieldFromName(structFieldName)
if err != nil {
return nil, merr.WrapErrParameterInvalidMsg("struct field not found: %s, error: %s", structFieldName, err)
}
nullable := field.GetNullable()
if structField := v.schema.GetStructArrayFieldFromName(fieldName); structField != nil {
nullable = nullable || structField.GetNullable()
}
return &planpb.ColumnInfo{
FieldId: field.FieldID,
DataType: field.DataType,
NestedPath: []string{index},
ElementType: field.GetElementType(),
Nullable: nullable,
}, nil
}
func (v *ParserVisitor) getChildColumnInfo(identifier, child, structSubField, structIndexField antlr.TerminalNode) (*planpb.ColumnInfo, error) {
if identifier != nil {
childExpr, err := v.translateIdentifier(identifier.GetText())
if err != nil {
return nil, err
}
return toColumnInfo(childExpr), nil
}
if structSubField != nil {
return v.getColumnInfoFromStructSubField(structSubField.GetText())
}
if structIndexField != nil {
return v.getColumnInfoFromStructIndexField(structIndexField.GetText())
}
return v.getColumnInfoFromJSONIdentifier(child.GetText())
}
func (v *ParserVisitor) getStructArrayParentColumnInfo(fieldName string) (*planpb.ColumnInfo, bool, error) {
fieldName = decodeUnicode(fieldName)
if _, err := v.schema.GetFieldFromName(fieldName); err == nil {
return nil, false, nil
}
structField := v.schema.GetStructArrayFieldFromName(fieldName)
if structField == nil {
return nil, false, nil
}
subFields := structField.GetFields()
if len(subFields) == 0 {
return nil, true, merr.WrapErrParameterInvalidMsg(
"struct array field %s has no sub-fields", fieldName)
}
subField := subFields[0]
return &planpb.ColumnInfo{
FieldId: subField.GetFieldID(),
DataType: subField.GetDataType(),
ElementType: subField.GetElementType(),
Nullable: structField.GetNullable() || subField.GetNullable(),
}, true, nil
}
func (v *ParserVisitor) getNullExprColumnInfo(identifier, child antlr.TerminalNode) (*planpb.ColumnInfo, error) {
if identifier != nil {
// try struct first
if columnInfo, ok, err := v.getStructArrayParentColumnInfo(identifier.GetText()); ok || err != nil {
return columnInfo, err
}
}
return v.getChildColumnInfo(identifier, child, nil, nil)
}
func isUnsupportedNullExprVectorType(dataType schemapb.DataType) bool {
return typeutil.IsVectorType(dataType) && !typeutil.IsVectorArrayType(dataType)
}
// VisitCall parses the expr to call plan.
func (v *ParserVisitor) VisitCall(ctx *parser.CallContext) interface{} {
functionName := strings.ToLower(ctx.Identifier().GetText())
numParams := len(ctx.AllExpr())
funcParameters := make([]*planpb.Expr, 0, numParams)
for _, param := range ctx.AllExpr() {
paramExpr := param.Accept(v)
if err := getError(paramExpr); err != nil {
return err
}
funcParameters = append(funcParameters, getExpr(param.Accept(v)).expr)
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_CallExpr{
CallExpr: &planpb.CallExpr{
FunctionName: functionName,
FunctionParameters: funcParameters,
},
},
},
dataType: schemapb.DataType_Bool,
}
}
// VisitRange translates expr to range plan.
func (v *ParserVisitor) VisitRange(ctx *parser.RangeContext) interface{} {
columnInfo, err := v.getChildColumnInfo(
ctx.Identifier(),
ctx.JSONIdentifier(),
ctx.StructSubFieldIdentifier(),
ctx.StructIndexFieldIdentifier(),
)
if err != nil {
return err
}
if columnInfo == nil {
return merr.WrapErrParameterInvalidMsg("range operations are only supported on single fields now, got: %s", ctx.Expr(1).GetText())
}
if err := checkDirectComparisonBinaryField(columnInfo); err != nil {
return err
}
lower := ctx.Expr(0).Accept(v)
upper := ctx.Expr(1).Accept(v)
if err := getError(lower); err != nil {
return err
}
if err := getError(upper); err != nil {
return err
}
lowerValueExpr, upperValueExpr := getValueExpr(lower), getValueExpr(upper)
if lowerValueExpr == nil {
return merr.WrapErrParameterInvalidMsg("lowerbound cannot be a non-const expression: %s", ctx.Expr(0).GetText())
}
if upperValueExpr == nil {
return merr.WrapErrParameterInvalidMsg("upperbound cannot be a non-const expression: %s", ctx.Expr(1).GetText())
}
fieldDataType := columnInfo.GetDataType()
if typeutil.IsArrayType(columnInfo.GetDataType()) {
fieldDataType = columnInfo.GetElementType()
}
lowerValue := lowerValueExpr.GetValue()
upperValue := upperValueExpr.GetValue()
if !isTemplateExpr(lowerValueExpr) {
if lowerValue, err = castRangeValue(fieldDataType, lowerValue); err != nil {
return err
}
}
if !isTemplateExpr(upperValueExpr) {
if upperValue, err = castRangeValue(fieldDataType, upperValue); err != nil {
return err
}
}
lowerInclusive := ctx.GetOp1().GetTokenType() == parser.PlanParserLE
upperInclusive := ctx.GetOp2().GetTokenType() == parser.PlanParserLE
if !isTemplateExpr(lowerValueExpr) && !isTemplateExpr(upperValueExpr) {
if !lowerInclusive || !upperInclusive {
if getGenericValue(GreaterEqual(lowerValue, upperValue)).GetBoolVal() {
return merr.WrapErrQueryPlanMsg("invalid range: lowerbound is greater than upperbound")
}
} else {
if getGenericValue(Greater(lowerValue, upperValue)).GetBoolVal() {
return merr.WrapErrQueryPlanMsg("invalid range: lowerbound is greater than upperbound")
}
}
}
expr := &planpb.Expr{
Expr: &planpb.Expr_BinaryRangeExpr{
BinaryRangeExpr: &planpb.BinaryRangeExpr{
ColumnInfo: columnInfo,
LowerInclusive: lowerInclusive,
UpperInclusive: upperInclusive,
LowerValue: lowerValue,
UpperValue: upperValue,
LowerTemplateVariableName: lowerValueExpr.GetTemplateVariableName(),
UpperTemplateVariableName: upperValueExpr.GetTemplateVariableName(),
},
},
IsTemplate: isTemplateExpr(lowerValueExpr) || isTemplateExpr(upperValueExpr),
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
// VisitReverseRange parses the expression like "1 > a > 0".
func (v *ParserVisitor) VisitReverseRange(ctx *parser.ReverseRangeContext) interface{} {
columnInfo, err := v.getChildColumnInfo(
ctx.Identifier(),
ctx.JSONIdentifier(),
ctx.StructSubFieldIdentifier(),
ctx.StructIndexFieldIdentifier(),
)
if err != nil {
return err
}
if columnInfo == nil {
return merr.WrapErrParameterInvalidMsg("range operations are only supported on single fields now, got: %s", ctx.Expr(1).GetText())
}
if err := checkDirectComparisonBinaryField(columnInfo); err != nil {
return err
}
lower := ctx.Expr(1).Accept(v)
upper := ctx.Expr(0).Accept(v)
if err := getError(lower); err != nil {
return err
}
if err := getError(upper); err != nil {
return err
}
lowerValueExpr, upperValueExpr := getValueExpr(lower), getValueExpr(upper)
if lowerValueExpr == nil {
return merr.WrapErrParameterInvalidMsg("lowerbound cannot be a non-const expression: %s", ctx.Expr(0).GetText())
}
if upperValueExpr == nil {
return merr.WrapErrParameterInvalidMsg("upperbound cannot be a non-const expression: %s", ctx.Expr(1).GetText())
}
fieldDataType := columnInfo.GetDataType()
if typeutil.IsArrayType(columnInfo.GetDataType()) {
fieldDataType = columnInfo.GetElementType()
}
lowerValue := lowerValueExpr.GetValue()
upperValue := upperValueExpr.GetValue()
if !isTemplateExpr(lowerValueExpr) {
if lowerValue, err = castRangeValue(fieldDataType, lowerValue); err != nil {
return err
}
}
if !isTemplateExpr(upperValueExpr) {
if upperValue, err = castRangeValue(fieldDataType, upperValue); err != nil {
return err
}
}
lowerInclusive := ctx.GetOp2().GetTokenType() == parser.PlanParserGE
upperInclusive := ctx.GetOp1().GetTokenType() == parser.PlanParserGE
if !isTemplateExpr(lowerValueExpr) && !isTemplateExpr(upperValueExpr) {
if !lowerInclusive || !upperInclusive {
if getGenericValue(GreaterEqual(lowerValue, upperValue)).GetBoolVal() {
return merr.WrapErrQueryPlanMsg("invalid range: lowerbound is greater than upperbound")
}
} else {
if getGenericValue(Greater(lowerValue, upperValue)).GetBoolVal() {
return merr.WrapErrQueryPlanMsg("invalid range: lowerbound is greater than upperbound")
}
}
}
expr := &planpb.Expr{
Expr: &planpb.Expr_BinaryRangeExpr{
BinaryRangeExpr: &planpb.BinaryRangeExpr{
ColumnInfo: columnInfo,
LowerInclusive: lowerInclusive,
UpperInclusive: upperInclusive,
LowerValue: lowerValue,
UpperValue: upperValue,
LowerTemplateVariableName: lowerValueExpr.GetTemplateVariableName(),
UpperTemplateVariableName: upperValueExpr.GetTemplateVariableName(),
},
},
IsTemplate: isTemplateExpr(lowerValueExpr) || isTemplateExpr(upperValueExpr),
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
// VisitUnary unpack the +expr to expr.
func (v *ParserVisitor) VisitUnary(ctx *parser.UnaryContext) interface{} {
child := ctx.Expr().Accept(v)
if err := getError(child); err != nil {
// Special case: handle -9223372036854775808
// ANTLR parses -9223372036854775808 as Unary(SUB, Integer(9223372036854775808)).
// The integer literal 9223372036854775808 exceeds int64 max, but when combined
// with unary minus, it represents the valid int64 minimum value.
if isInt64OverflowError(err) && ctx.GetOp().GetTokenType() == parser.PlanParserSUB {
return &ExprWithType{
dataType: schemapb.DataType_Int64,
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: NewInt(math.MinInt64),
},
},
},
nodeDependent: true,
}
}
return err
}
childValue := getGenericValue(child)
if childValue != nil {
switch ctx.GetOp().GetTokenType() {
case parser.PlanParserADD:
return child
case parser.PlanParserSUB:
return Negative(childValue)
case parser.PlanParserNOT:
n, err := Not(childValue)
if err != nil {
return err
}
return n
default:
return merr.WrapErrParameterInvalidMsg("unexpected op: %s", ctx.GetOp().GetText())
}
}
childExpr := getExpr(child)
if childExpr == nil {
return merr.WrapErrQueryPlanMsg("failed to parse unary expressions")
}
if isRandomSampleExpr(childExpr) {
return merr.WrapErrQueryPlanMsg("random sample expression cannot be used in unary expression")
}
if isElementFilterExpr(childExpr) {
return merr.WrapErrQueryPlanMsg("element filter expression cannot be used in unary expression")
}
if err := checkDirectComparisonBinaryField(toColumnInfo(childExpr)); err != nil {
return err
}
switch ctx.GetOp().GetTokenType() {
case parser.PlanParserADD:
return childExpr
case parser.PlanParserNOT:
if !canBeExecuted(childExpr) {
return merr.WrapErrParameterInvalidMsg("%s op can only be applied on boolean expression", unaryLogicalNameMap[parser.PlanParserNOT])
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryExpr{
UnaryExpr: &planpb.UnaryExpr{
Op: unaryLogicalOpMap[parser.PlanParserNOT],
Child: childExpr.expr,
},
},
IsTemplate: childExpr.expr.GetIsTemplate(),
},
dataType: schemapb.DataType_Bool,
}
default:
return merr.WrapErrParameterInvalidMsg("unexpected op: %s", ctx.GetOp().GetText())
}
}
// VisitLogicalOr apply logical or to two boolean expressions.
func (v *ParserVisitor) VisitLogicalOr(ctx *parser.LogicalOrContext) interface{} {
left := ctx.Expr(0).Accept(v)
if err := getError(left); err != nil {
return err
}
right := ctx.Expr(1).Accept(v)
if err := getError(right); err != nil {
return err
}
leftValue, rightValue := getGenericValue(left), getGenericValue(right)
if leftValue != nil && rightValue != nil {
n, err := Or(leftValue, rightValue)
if err != nil {
return err
}
return n
}
// One side is a boolean literal, the other is an expression: short-circuit fold.
// true or expr → AlwaysTrueExpr; false or expr → expr (and symmetric cases).
if leftValue != nil || rightValue != nil {
boolLiteral := leftValue
otherExpr := getExpr(right)
if boolLiteral == nil {
boolLiteral = rightValue
otherExpr = getExpr(left)
}
if !IsBool(boolLiteral) {
return merr.WrapErrQueryPlanMsg("'or' can only be used between boolean expressions")
}
if boolLiteral.GetBoolVal() {
// true or expr → always true
return &ExprWithType{
expr: alwaysTrueExpr(),
dataType: schemapb.DataType_Bool,
}
}
// false or expr → expr
if otherExpr == nil || !canBeExecuted(otherExpr) {
return merr.WrapErrQueryPlanMsg("'or' can only be used between boolean expressions")
}
return otherExpr
}
var leftExpr *ExprWithType
var rightExpr *ExprWithType
leftExpr = getExpr(left)
rightExpr = getExpr(right)
if isRandomSampleExpr(leftExpr) || isRandomSampleExpr(rightExpr) {
return merr.WrapErrQueryPlanMsg("random sample expression cannot be used in logical and expression")
}
if isElementFilterExpr(leftExpr) {
return merr.WrapErrQueryPlanMsg("element filter expression can only be the last expression in the logical or expression")
}
if !canBeExecuted(leftExpr) || !canBeExecuted(rightExpr) {
return merr.WrapErrQueryPlanMsg("'or' can only be used between boolean expressions")
}
expr := &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: leftExpr.expr,
Right: rightExpr.expr,
Op: planpb.BinaryExpr_LogicalOr,
},
},
IsTemplate: leftExpr.expr.GetIsTemplate() || rightExpr.expr.GetIsTemplate(),
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
// VisitLogicalAnd apply logical and to two boolean expressions.
func (v *ParserVisitor) VisitLogicalAnd(ctx *parser.LogicalAndContext) interface{} {
left := ctx.Expr(0).Accept(v)
if err := getError(left); err != nil {
return err
}
right := ctx.Expr(1).Accept(v)
if err := getError(right); err != nil {
return err
}
leftValue, rightValue := getGenericValue(left), getGenericValue(right)
if leftValue != nil && rightValue != nil {
n, err := And(leftValue, rightValue)
if err != nil {
return err
}
return n
}
// One side is a boolean literal, the other is an expression: short-circuit fold.
// false and expr → AlwaysFalseExpr; true and expr → expr (and symmetric cases).
if leftValue != nil || rightValue != nil {
boolLiteral := leftValue
otherExpr := getExpr(right)
if boolLiteral == nil {
boolLiteral = rightValue
otherExpr = getExpr(left)
}
if !IsBool(boolLiteral) {
return merr.WrapErrQueryPlanMsg("'and' can only be used between boolean expressions")
}
if !boolLiteral.GetBoolVal() {
// false and expr → always false
return &ExprWithType{
expr: alwaysFalseExpr(),
dataType: schemapb.DataType_Bool,
}
}
// true and expr → expr
if otherExpr == nil || !canBeExecuted(otherExpr) {
return merr.WrapErrQueryPlanMsg("'and' can only be used between boolean expressions")
}
return otherExpr
}
var leftExpr *ExprWithType
var rightExpr *ExprWithType
leftExpr = getExpr(left)
rightExpr = getExpr(right)
if isRandomSampleExpr(leftExpr) {
return merr.WrapErrQueryPlanMsg("random sample expression can only be the last expression in the logical and expression")
}
if isElementFilterExpr(leftExpr) {
return merr.WrapErrQueryPlanMsg("element filter expression can only be the last expression in the logical and expression")
}
if !canBeExecuted(leftExpr) || !canBeExecuted(rightExpr) {
return merr.WrapErrQueryPlanMsg("'and' can only be used between boolean expressions")
}
var expr *planpb.Expr
if isRandomSampleExpr(rightExpr) {
randomSampleExpr := rightExpr.expr.GetRandomSampleExpr()
randomSampleExpr.Predicate = leftExpr.expr
expr = &planpb.Expr{
Expr: &planpb.Expr_RandomSampleExpr{
RandomSampleExpr: randomSampleExpr,
},
}
} else if isElementFilterExpr(rightExpr) {
// Similar to RandomSampleExpr, extract doc-level predicate
elementFilterExpr := rightExpr.expr.GetElementFilterExpr()
elementFilterExpr.Predicate = leftExpr.expr
expr = &planpb.Expr{
Expr: &planpb.Expr_ElementFilterExpr{
ElementFilterExpr: elementFilterExpr,
},
}
} else {
expr = &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: leftExpr.expr,
Right: rightExpr.expr,
Op: planpb.BinaryExpr_LogicalAnd,
},
},
IsTemplate: leftExpr.expr.GetIsTemplate() || rightExpr.expr.GetIsTemplate(),
}
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
// visitBitwiseBinaryOp is the shared implementation for VisitBitAnd/VisitBitOr/VisitBitXor.
func (v *ParserVisitor) visitBitwiseBinaryOp(leftCtx, rightCtx parser.IExprContext, tokenType int, text string) interface{} {
var err error
left := leftCtx.Accept(v)
if err = getError(left); err != nil {
return err
}
right := rightCtx.Accept(v)
if err = getError(right); err != nil {
return err
}
leftValueExpr, rightValueExpr := getValueExpr(left), getValueExpr(right)
if leftValueExpr != nil && rightValueExpr != nil {
if isTemplateExpr(leftValueExpr) || isTemplateExpr(rightValueExpr) {
return merr.WrapErrParameterInvalidMsg("placeholder was not supported between two constants with operator: %s", text)
}
leftValue, rightValue := getGenericValue(left), getGenericValue(right)
switch tokenType {
case parser.PlanParserBAND:
n, err := BitAnd(leftValue, rightValue)
if err != nil {
return err
}
return n
case parser.PlanParserBOR:
n, err := BitOr(leftValue, rightValue)
if err != nil {
return err
}
return n
case parser.PlanParserBXOR:
n, err := BitXor(leftValue, rightValue)
if err != nil {
return err
}
return n
default:
return merr.WrapErrParameterInvalidMsg("unexpected bitwise op: %s", text)
}
}
leftExpr, rightExpr := getExpr(left), getExpr(right)
reverse := leftValueExpr != nil
if leftExpr == nil || rightExpr == nil {
return merr.WrapErrParameterInvalidMsg("invalid bitwise expression, left: %s, op: %s, right: %s", leftCtx.GetText(), text, rightCtx.GetText())
}
if err = checkDirectComparisonBinaryField(toColumnInfo(leftExpr)); err != nil {
return err
}
if err = checkDirectComparisonBinaryField(toColumnInfo(rightExpr)); err != nil {
return err
}
var dataType schemapb.DataType
if leftExpr.expr.GetIsTemplate() {
dataType = rightExpr.dataType
} else if rightExpr.expr.GetIsTemplate() {
dataType = leftExpr.dataType
} else {
if err = canArithmetic(leftExpr.dataType, getArrayElementType(leftExpr), rightExpr.dataType, getArrayElementType(rightExpr), reverse); err != nil {
return merr.WrapErrParameterInvalidMsg("'%s' %s", arithNameMap[tokenType], err.Error())
}
if err = checkValidModArith(arithExprMap[tokenType], leftExpr.dataType, getArrayElementType(leftExpr), rightExpr.dataType, getArrayElementType(rightExpr)); err != nil {
return err
}
dataType, err = calcDataType(leftExpr, rightExpr, reverse)
if err != nil {
return err
}
}
expr := &planpb.Expr{
Expr: &planpb.Expr_BinaryArithExpr{
BinaryArithExpr: &planpb.BinaryArithExpr{
Left: leftExpr.expr,
Right: rightExpr.expr,
Op: arithExprMap[tokenType],
},
},
IsTemplate: leftExpr.expr.GetIsTemplate() || rightExpr.expr.GetIsTemplate(),
}
return &ExprWithType{
expr: expr,
dataType: dataType,
nodeDependent: true,
}
}
// VisitBitXor translates bitwise XOR expression to arithmetic plan.
func (v *ParserVisitor) VisitBitXor(ctx *parser.BitXorContext) interface{} {
return v.visitBitwiseBinaryOp(ctx.Expr(0), ctx.Expr(1), parser.PlanParserBXOR, ctx.GetText())
}
// VisitBitAnd translates bitwise AND expression to arithmetic plan.
func (v *ParserVisitor) VisitBitAnd(ctx *parser.BitAndContext) interface{} {
return v.visitBitwiseBinaryOp(ctx.Expr(0), ctx.Expr(1), parser.PlanParserBAND, ctx.GetText())
}
// VisitPower parses power expression.
func (v *ParserVisitor) VisitPower(ctx *parser.PowerContext) interface{} {
left := ctx.Expr(0).Accept(v)
if err := getError(left); err != nil {
return err
}
right := ctx.Expr(1).Accept(v)
if err := getError(right); err != nil {
return err
}
leftValue, rightValue := getGenericValue(left), getGenericValue(right)
if leftValue != nil && rightValue != nil {
return Power(leftValue, rightValue)
}
return merr.WrapErrParameterInvalidMsg("power can only apply on constants: %s", ctx.GetText())
}
// VisitShift unsupported.
func (v *ParserVisitor) VisitShift(ctx *parser.ShiftContext) interface{} {
return merr.WrapErrParameterInvalidMsg("shift is not supported: %s", ctx.GetText())
}
// VisitBitOr translates bitwise OR expression to arithmetic plan.
func (v *ParserVisitor) VisitBitOr(ctx *parser.BitOrContext) interface{} {
return v.visitBitwiseBinaryOp(ctx.Expr(0), ctx.Expr(1), parser.PlanParserBOR, ctx.GetText())
}
// getColumnInfoFromJSONIdentifier parse JSON field name and JSON nested path.
// input: user["name"]["first"],
// output: if user is JSON field name, and fieldID is 102
/*
&planpb.ColumnInfo{
FieldId: 102,
DataType: JSON,
NestedPath: []string{"name", "first"},
}, nil
*/
// if user is not JSON field name, and $SYS_META fieldID is 102:
/*
&planpb.ColumnInfo{
FieldId: 102,
DataType: JSON,
NestedPath: []string{"user", "name", "first"},
}, nil
*/
// input: user,
// output: if user is JSON field name, return error.
// if user is not JSON field name, and $SYS_META fieldID is 102:
/*
&planpb.ColumnInfo{
FieldId: 102,
DataType: JSON,
NestedPath: []string{"user"},
}, nil
*/
// More tests refer to plan_parser_v2_test.go::Test_JSONExpr
func (v *ParserVisitor) getColumnInfoFromJSONIdentifier(identifier string) (*planpb.ColumnInfo, error) {
// Do NOT decodeUnicode the whole identifier up front: a raw-string key
// (r"..." / R'...') is verbatim, so its \uXXXX must survive untouched. Decode
// the field name and normal (non-raw) keys individually below instead.
rawFieldName := strings.Split(identifier, "[")[0]
fieldName := decodeUnicode(rawFieldName)
// Reject a bare `null`/`NULL` base (e.g. `NULL["x"]`, `NULL[0]`) here too —
// this lookup bypasses translateIdentifierWithText. Schema-aware like the
// guard there: a legacy field literally named "null" resolves. See
// errNullLiteral.
if strings.EqualFold(fieldName, "null") {
if _, err := v.schema.GetFieldFromName(fieldName); err != nil {
return nil, errNullLiteral()
}
}
nestedPath := make([]string, 0)
field, err := v.schema.GetFieldFromNameDefaultJSON(fieldName)
if err != nil {
return nil, err
}
if field.GetDataType() != schemapb.DataType_JSON &&
field.GetDataType() != schemapb.DataType_Array {
errMsg := fmt.Sprintf("%s data type not supported accessed with []", field.GetDataType())
return nil, merr.WrapErrParameterInvalidMsg("%s", errMsg)
}
if fieldName != field.Name {
nestedPath = append(nestedPath, fieldName)
}
jsonKeyStr := identifier[len(rawFieldName):]
ss := strings.Split(jsonKeyStr, "][")
for i := 0; i < len(ss); i++ {
path := strings.Trim(ss[i], "[]")
if path == "" {
return nil, merr.WrapErrParameterInvalidMsg("invalid identifier: %s", identifier)
}
// A raw-string key (r"..." / R'...'): drop the r/R prefix and take the
// content verbatim — no decodeUnicode, so a literal \uXXXX in the key is
// the key, not its decoded rune (issue #43864).
isRaw := len(path) >= 2 && (path[0] == 'r' || path[0] == 'R') &&
(path[1] == '"' || path[1] == '\'')
if isRaw {
path = path[1:]
}
if (strings.HasPrefix(path, "\"") && strings.HasSuffix(path, "\"")) ||
(strings.HasPrefix(path, "'") && strings.HasSuffix(path, "'")) {
path = path[1 : len(path)-1]
if path == "" {
return nil, merr.WrapErrParameterInvalidMsg("invalid identifier: %s", identifier)
}
if typeutil.IsArrayType(field.DataType) {
return nil, merr.WrapErrQueryPlanMsg("can only access array field with integer index")
}
if !isRaw {
// Normal keys keep the historical \uXXXX decoding behavior.
path = decodeUnicode(path)
}
} else if _, err := strconv.ParseInt(path, 10, 64); err != nil {
return nil, merr.WrapErrParameterInvalidMsg("json key must be enclosed in double quotes or single quotes: \"%s\"", path)
}
nestedPath = append(nestedPath, path)
}
return &planpb.ColumnInfo{
FieldId: field.FieldID,
DataType: field.DataType,
NestedPath: nestedPath,
ElementType: field.GetElementType(),
Nullable: field.GetNullable(),
}, nil
}
func (v *ParserVisitor) VisitJSONIdentifier(ctx *parser.JSONIdentifierContext) interface{} {
field, err := v.getColumnInfoFromJSONIdentifier(ctx.JSONIdentifier().GetText())
if err != nil {
return err
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_ColumnExpr{
ColumnExpr: &planpb.ColumnExpr{
Info: &planpb.ColumnInfo{
FieldId: field.GetFieldId(),
DataType: field.GetDataType(),
NestedPath: field.GetNestedPath(),
ElementType: field.GetElementType(),
Nullable: field.GetNullable(),
},
},
},
},
dataType: field.GetDataType(),
nodeDependent: true,
}
}
// VisitStructField handles struct_array[sub_field] syntax for struct sub-field access.
func (v *ParserVisitor) VisitStructField(ctx *parser.StructFieldContext) interface{} {
// Get the full identifier text, e.g., "struct_array[sub_int]"
identifier := ctx.StructFieldIdentifier().GetText()
// Look up the field directly by its full name
field, err := v.schema.GetFieldFromName(identifier)
if err != nil {
return merr.WrapErrParameterInvalidMsg("struct field not found: %s, error: %s", identifier, err)
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_ColumnExpr{
ColumnExpr: &planpb.ColumnExpr{
Info: &planpb.ColumnInfo{
FieldId: field.FieldID,
DataType: field.DataType,
ElementType: field.GetElementType(),
},
},
},
},
dataType: field.DataType,
nodeDependent: true,
}
}
// VisitStructIndexField handles struct_arr[index][sub_field] syntax for accessing
// a specific element's sub-field in a struct array.
func (v *ParserVisitor) VisitStructIndexField(ctx *parser.StructIndexFieldContext) interface{} {
identifier := ctx.StructIndexFieldIdentifier().GetText()
columnInfo, err := v.getColumnInfoFromStructIndexField(identifier)
if err != nil {
return err
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_ColumnExpr{
ColumnExpr: &planpb.ColumnExpr{
Info: columnInfo,
},
},
},
dataType: columnInfo.GetDataType(),
nodeDependent: true,
}
}
func (v *ParserVisitor) VisitExists(ctx *parser.ExistsContext) interface{} {
child := ctx.Expr().Accept(v)
if err := getError(child); err != nil {
return err
}
columnInfo := toColumnInfo(child.(*ExprWithType))
if columnInfo == nil {
return merr.WrapErrParameterInvalidMsg(
"exists operations are only supported on single fields now, got: %s", ctx.Expr().GetText())
}
if columnInfo.GetDataType() != schemapb.DataType_JSON {
return merr.WrapErrParameterInvalidMsg(
"exists operations are only supportted on json field, got:%s", columnInfo.GetDataType())
}
if len(columnInfo.GetNestedPath()) == 0 {
return merr.WrapErrParameterInvalidMsg(
"exists operations are only supportted on json key")
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_ExistsExpr{
ExistsExpr: &planpb.ExistsExpr{
Info: &planpb.ColumnInfo{
FieldId: columnInfo.GetFieldId(),
DataType: columnInfo.GetDataType(),
NestedPath: columnInfo.GetNestedPath(),
},
},
},
},
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitArray(ctx *parser.ArrayContext) interface{} {
allExpr := ctx.AllExpr()
array := make([]*planpb.GenericValue, len(allExpr))
dType := schemapb.DataType_None
sameType := true
for i := 0; i < len(allExpr); i++ {
element := allExpr[i].Accept(v)
if err := getError(element); err != nil {
return err
}
elementValue := getGenericValue(element)
if elementValue == nil {
return merr.WrapErrParameterInvalidMsg("array element type must be generic value, but got: %s", allExpr[i].GetText())
}
array[i] = elementValue
if dType == schemapb.DataType_None {
dType = element.(*ExprWithType).dataType
} else if dType != element.(*ExprWithType).dataType {
sameType = false
}
}
if !sameType {
dType = schemapb.DataType_None
}
return &ExprWithType{
dataType: schemapb.DataType_Array,
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: &planpb.GenericValue{
Val: &planpb.GenericValue_ArrayVal{
ArrayVal: &planpb.Array{
Array: array,
SameType: sameType,
ElementType: dType,
},
},
},
},
},
},
nodeDependent: true,
}
}
func (v *ParserVisitor) VisitEmptyArray(ctx *parser.EmptyArrayContext) interface{} {
return &ExprWithType{
dataType: schemapb.DataType_Array,
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: &planpb.GenericValue{
Val: &planpb.GenericValue_ArrayVal{
ArrayVal: &planpb.Array{
Array: nil,
SameType: true,
ElementType: schemapb.DataType_None,
},
},
},
},
},
},
nodeDependent: true,
}
}
func (v *ParserVisitor) VisitIsNotNull(ctx *parser.IsNotNullContext) interface{} {
column, err := v.getNullExprColumnInfo(ctx.Identifier(), ctx.JSONIdentifier())
if err != nil {
return err
}
if isUnsupportedNullExprVectorType(column.DataType) {
return merr.WrapErrParameterInvalidMsg("IsNull/IsNotNull operations are not supported on vector fields")
}
if len(column.NestedPath) != 0 {
if typeutil.IsArrayType(column.GetDataType()) {
return merr.WrapErrParameterInvalidMsg("IsNull/IsNotNull operations are not supported on array element access, got: %s", ctx.GetText())
}
// convert json not null expr to exists expr, eg: json['a'] is not null -> exists json['a']
expr := &planpb.Expr{
Expr: &planpb.Expr_ExistsExpr{
ExistsExpr: &planpb.ExistsExpr{
Info: &planpb.ColumnInfo{
FieldId: column.FieldId,
DataType: column.DataType,
NestedPath: column.NestedPath,
},
},
},
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
expr := &planpb.Expr{
Expr: &planpb.Expr_NullExpr{
NullExpr: &planpb.NullExpr{
ColumnInfo: column,
Op: planpb.NullExpr_IsNotNull,
},
},
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitIsNull(ctx *parser.IsNullContext) interface{} {
column, err := v.getNullExprColumnInfo(ctx.Identifier(), ctx.JSONIdentifier())
if err != nil {
return err
}
if isUnsupportedNullExprVectorType(column.DataType) {
return merr.WrapErrParameterInvalidMsg("IsNull/IsNotNull operations are not supported on vector fields")
}
if len(column.NestedPath) != 0 {
if typeutil.IsArrayType(column.GetDataType()) {
return merr.WrapErrParameterInvalidMsg("IsNull/IsNotNull operations are not supported on array element access, got: %s", ctx.GetText())
}
// convert json is null expr to not exists expr, eg: json['a'] is null -> not exists json['a']
expr := &planpb.Expr{
Expr: &planpb.Expr_ExistsExpr{
ExistsExpr: &planpb.ExistsExpr{
Info: &planpb.ColumnInfo{
FieldId: column.FieldId,
DataType: column.DataType,
NestedPath: column.NestedPath,
},
},
},
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryExpr{
UnaryExpr: &planpb.UnaryExpr{
Op: unaryLogicalOpMap[parser.PlanParserNOT],
Child: expr,
},
},
},
dataType: schemapb.DataType_Bool,
}
}
expr := &planpb.Expr{
Expr: &planpb.Expr_NullExpr{
NullExpr: &planpb.NullExpr{
ColumnInfo: column,
Op: planpb.NullExpr_IsNull,
},
},
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitJSONContains(ctx *parser.JSONContainsContext) interface{} {
field := ctx.Expr(0).Accept(v)
if err := getError(field); err != nil {
return err
}
columnInfo := toColumnInfo(field.(*ExprWithType))
if columnInfo == nil ||
(!typeutil.IsJSONType(columnInfo.GetDataType()) && !typeutil.IsArrayType(columnInfo.GetDataType())) {
return merr.WrapErrParameterInvalidMsg(
"contains operation are only supported on json or array fields now, got: %s", ctx.Expr(0).GetText())
}
element := ctx.Expr(1).Accept(v)
if err := getError(element); err != nil {
return err
}
elementExpr := getValueExpr(element)
if elementExpr == nil {
return merr.WrapErrParameterInvalidMsg(
"contains operation are only supported explicitly specified element, got: %s", ctx.Expr(1).GetText())
}
var elements []*planpb.GenericValue
if !isTemplateExpr(elementExpr) {
elements = make([]*planpb.GenericValue, 1)
elementValue := elementExpr.GetValue()
if err := checkContainsElement(field.(*ExprWithType), planpb.JSONContainsExpr_Contains, elementValue); err != nil {
return err
}
elements[0] = elementValue
}
expr := &planpb.Expr{
Expr: &planpb.Expr_JsonContainsExpr{
JsonContainsExpr: &planpb.JSONContainsExpr{
ColumnInfo: columnInfo,
Elements: elements,
Op: planpb.JSONContainsExpr_Contains,
ElementsSameType: true,
TemplateVariableName: elementExpr.GetTemplateVariableName(),
},
},
IsTemplate: isTemplateExpr(elementExpr),
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitJSONContainsAll(ctx *parser.JSONContainsAllContext) interface{} {
field := ctx.Expr(0).Accept(v)
if err := getError(field); err != nil {
return err
}
columnInfo := toColumnInfo(field.(*ExprWithType))
if columnInfo == nil ||
(!typeutil.IsJSONType(columnInfo.GetDataType()) && !typeutil.IsArrayType(columnInfo.GetDataType())) {
return merr.WrapErrParameterInvalidMsg(
"contains_all operation are only supported on json or array fields now, got: %s", ctx.Expr(0).GetText())
}
element := ctx.Expr(1).Accept(v)
if err := getError(element); err != nil {
return err
}
elementExpr := getValueExpr(element)
if elementExpr == nil {
return merr.WrapErrParameterInvalidMsg(
"contains_all operation are only supported explicitly specified element, got: %s", ctx.Expr(1).GetText())
}
var elements []*planpb.GenericValue
var sameType bool
if !isTemplateExpr(elementExpr) {
elementValue := elementExpr.GetValue()
if err := checkContainsElement(field.(*ExprWithType), planpb.JSONContainsExpr_ContainsAll, elementValue); err != nil {
return err
}
elements = elementValue.GetArrayVal().GetArray()
sameType = elementValue.GetArrayVal().GetSameType()
}
expr := &planpb.Expr{
Expr: &planpb.Expr_JsonContainsExpr{
JsonContainsExpr: &planpb.JSONContainsExpr{
ColumnInfo: columnInfo,
Elements: elements,
Op: planpb.JSONContainsExpr_ContainsAll,
ElementsSameType: sameType,
TemplateVariableName: elementExpr.GetTemplateVariableName(),
},
},
IsTemplate: isTemplateExpr(elementExpr),
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitJSONContainsAny(ctx *parser.JSONContainsAnyContext) interface{} {
field := ctx.Expr(0).Accept(v)
if err := getError(field); err != nil {
return err
}
columnInfo := toColumnInfo(field.(*ExprWithType))
if columnInfo == nil ||
(!typeutil.IsJSONType(columnInfo.GetDataType()) && !typeutil.IsArrayType(columnInfo.GetDataType())) {
return merr.WrapErrParameterInvalidMsg(
"contains_any operation are only supported on json or array fields now, got: %s", ctx.Expr(0).GetText())
}
element := ctx.Expr(1).Accept(v)
if err := getError(element); err != nil {
return err
}
valueExpr := getValueExpr(element)
if valueExpr == nil {
return merr.WrapErrParameterInvalidMsg(
"contains_any operation are only supported explicitly specified element, got: %s", ctx.Expr(1).GetText())
}
var elements []*planpb.GenericValue
var sameType bool
if !isTemplateExpr(valueExpr) {
elementValue := valueExpr.GetValue()
if err := checkContainsElement(field.(*ExprWithType), planpb.JSONContainsExpr_ContainsAny, elementValue); err != nil {
return err
}
elements = elementValue.GetArrayVal().GetArray()
sameType = elementValue.GetArrayVal().GetSameType()
}
expr := &planpb.Expr{
Expr: &planpb.Expr_JsonContainsExpr{
JsonContainsExpr: &planpb.JSONContainsExpr{
ColumnInfo: columnInfo,
Elements: elements,
Op: planpb.JSONContainsExpr_ContainsAny,
ElementsSameType: sameType,
TemplateVariableName: valueExpr.GetTemplateVariableName(),
},
},
IsTemplate: isTemplateExpr(valueExpr),
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitArrayLength(ctx *parser.ArrayLengthContext) interface{} {
var columnInfo *planpb.ColumnInfo
var err error
if ctx.StructFieldIdentifier() != nil {
// Handle struct_arr[sub_field] syntax: look up the full field name directly
identifier := ctx.StructFieldIdentifier().GetText()
field, fieldErr := v.schema.GetFieldFromName(identifier)
if fieldErr != nil {
return merr.WrapErrParameterInvalidMsg("struct field not found: %s, error: %s", identifier, fieldErr)
}
columnInfo = &planpb.ColumnInfo{
FieldId: field.FieldID,
DataType: field.DataType,
ElementType: field.GetElementType(),
Nullable: field.GetNullable(),
}
} else {
if ctx.Identifier() != nil {
if parentColumnInfo, ok, parentErr := v.getStructArrayParentColumnInfo(ctx.Identifier().GetText()); ok || parentErr != nil {
columnInfo = parentColumnInfo
err = parentErr
}
}
if columnInfo == nil && err == nil {
columnInfo, err = v.getChildColumnInfo(ctx.Identifier(), ctx.JSONIdentifier(), nil, nil)
}
if err != nil {
return err
}
}
if columnInfo == nil ||
(!typeutil.IsJSONType(columnInfo.GetDataType()) &&
!typeutil.IsArrayType(columnInfo.GetDataType()) &&
!typeutil.IsVectorArrayType(columnInfo.GetDataType())) {
return merr.WrapErrParameterInvalidMsg(
"array_length operation are only supported on json, array or array-of-vector fields now, got: %s", ctx.GetText())
}
expr := &planpb.Expr{
Expr: &planpb.Expr_BinaryArithExpr{
BinaryArithExpr: &planpb.BinaryArithExpr{
Left: &planpb.Expr{
Expr: &planpb.Expr_ColumnExpr{
ColumnExpr: &planpb.ColumnExpr{
Info: columnInfo,
},
},
},
Right: nil,
Op: planpb.ArithOpType_ArrayLength,
},
},
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Int64,
nodeDependent: true,
}
}
func (v *ParserVisitor) VisitTemplateVariable(ctx *parser.TemplateVariableContext) interface{} {
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_ValueExpr{
ValueExpr: &planpb.ValueExpr{
Value: nil,
TemplateVariableName: ctx.Identifier().GetText(),
},
},
IsTemplate: true,
},
}
}
func (v *ParserVisitor) VisitSpatialBinary(ctx *parser.SpatialBinaryContext) interface{} {
childExpr, err := v.translateIdentifier(ctx.Identifier().GetText())
if err != nil {
return err
}
columnInfo := toColumnInfo(childExpr)
if columnInfo == nil ||
(!typeutil.IsGeometryType(columnInfo.GetDataType())) {
return merr.WrapErrParameterInvalidMsg(
"spatial operation are only supported on geometry fields now, got: %s", ctx.GetText())
}
wktString, placeholder, isTemplate, err := v.parseStringLiteralOrTemplate(ctx.Expr(), "WKT string")
if err != nil {
return err
}
if isTemplate {
wktString = placeholder
} else {
if err := checkValidWKT(wktString); err != nil {
return err
}
}
// Map token type to GIS operation
var op planpb.GISFunctionFilterExpr_GISOp
switch ctx.GetOp().GetTokenType() {
case parser.PlanParserSTEuqals:
op = planpb.GISFunctionFilterExpr_Equals
case parser.PlanParserSTTouches:
op = planpb.GISFunctionFilterExpr_Touches
case parser.PlanParserSTOverlaps:
op = planpb.GISFunctionFilterExpr_Overlaps
case parser.PlanParserSTCrosses:
op = planpb.GISFunctionFilterExpr_Crosses
case parser.PlanParserSTContains:
op = planpb.GISFunctionFilterExpr_Contains
case parser.PlanParserSTIntersects:
op = planpb.GISFunctionFilterExpr_Intersects
case parser.PlanParserSTWithin:
op = planpb.GISFunctionFilterExpr_Within
default:
return merr.WrapErrParameterInvalidMsg("unhandled spatial operator: %s", ctx.GetOp().GetText())
}
expr := &planpb.Expr{
Expr: &planpb.Expr_GisfunctionFilterExpr{
GisfunctionFilterExpr: &planpb.GISFunctionFilterExpr{
ColumnInfo: columnInfo,
WktString: wktString,
Op: op,
},
},
IsTemplate: isTemplate,
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitSTIsValid(ctx *parser.STIsValidContext) interface{} {
childExpr, err := v.translateIdentifier(ctx.Identifier().GetText())
if err != nil {
return err
}
columnInfo := toColumnInfo(childExpr)
if columnInfo == nil ||
(!typeutil.IsGeometryType(columnInfo.GetDataType())) {
return merr.WrapErrParameterInvalidMsg(
"STIsValid operation are only supported on geometry fields now, got: %s", ctx.GetText())
}
expr := &planpb.Expr{
Expr: &planpb.Expr_GisfunctionFilterExpr{
GisfunctionFilterExpr: &planpb.GISFunctionFilterExpr{
ColumnInfo: columnInfo,
Op: planpb.GISFunctionFilterExpr_STIsValid,
},
},
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
func (v *ParserVisitor) VisitSTDWithin(ctx *parser.STDWithinContext) interface{} {
// Process the geometry field identifier
childExpr, err := v.translateIdentifier(ctx.Identifier().GetText())
if err != nil {
return err
}
columnInfo := toColumnInfo(childExpr)
if columnInfo == nil ||
(!typeutil.IsGeometryType(columnInfo.GetDataType())) {
return merr.WrapErrParameterInvalidMsg(
"ST_DWITHIN operation are only supported on geometry fields now, got: %s", ctx.GetText())
}
wktString, placeholder, isTemplate, err := v.parseStringLiteralOrTemplate(ctx.Expr(0), "WKT string")
if err != nil {
return err
}
if isTemplate {
wktString = placeholder
} else {
if err = checkValidPoint(wktString); err != nil {
return err
}
}
// Process the distance expression (can be int or float)
distanceExpr := ctx.Expr(1).Accept(v)
if err := getError(distanceExpr); err != nil {
return err
}
// Extract distance value - must be a constant expression
distanceValueExpr := getValueExpr(distanceExpr)
if distanceValueExpr == nil {
return merr.WrapErrParameterInvalidMsg("distance parameter must be a constant numeric value, got: %s", ctx.Expr(1).GetText())
}
var distance float64
genericValue := distanceValueExpr.GetValue()
if genericValue == nil {
return merr.WrapErrParameterInvalidMsg("invalid distance value: %s", ctx.Expr(1).GetText())
}
// Handle both integer and floating point values using type assertion
switch val := genericValue.GetVal().(type) {
case *planpb.GenericValue_Int64Val:
distance = float64(val.Int64Val)
case *planpb.GenericValue_FloatVal:
distance = val.FloatVal
default:
return merr.WrapErrParameterInvalidMsg("distance parameter must be a numeric value (int or float), got: %s", ctx.Expr(1).GetText())
}
if distance < 0 {
return merr.WrapErrParameterInvalidMsg("distance parameter must be non-negative, got: %f", distance)
}
// Create the GIS function expression using the bounding box
expr := &planpb.Expr{
Expr: &planpb.Expr_GisfunctionFilterExpr{
GisfunctionFilterExpr: &planpb.GISFunctionFilterExpr{
ColumnInfo: columnInfo,
WktString: wktString, // Use bounding box instead of original point
Op: planpb.GISFunctionFilterExpr_DWithin,
Distance: distance, // Keep distance for reference
},
},
IsTemplate: isTemplate,
}
return &ExprWithType{
expr: expr,
dataType: schemapb.DataType_Bool,
}
}
// VisitTimestamptzCompareForward handles comparison expressions where the column
// is on the left side of the operator.
// Syntax example: column > '2025-01-01' [ + INTERVAL 'P1D' ]
//
// Optimization Logic:
// 1. Quick Path: If no INTERVAL is provided, it generates a UnaryRangeExpr
// to enable index-based scan performance in Milvus.
// 2. Slow Path: If an INTERVAL exists, it generates a TimestamptzArithCompareExpr
// for specialized arithmetic evaluation.
func (v *ParserVisitor) VisitTimestamptzCompareForward(ctx *parser.TimestamptzCompareForwardContext) interface{} {
colExpr, err := v.translateIdentifier(ctx.Identifier().GetText())
identifier := ctx.Identifier().Accept(v)
if err != nil {
return merr.WrapErrParameterInvalidMsg("can not translate identifier: %s", identifier)
}
if colExpr.dataType != schemapb.DataType_Timestamptz {
return merr.WrapErrParameterInvalidMsg("field '%s' is not a timestamptz datatype", identifier)
}
compareOp := cmpOpMap[ctx.GetOp2().GetTokenType()]
rawCompareStr := ctx.GetCompare_string().GetText()
unquotedCompareStr, err := convertEscapeSingle(rawCompareStr)
if err != nil {
return merr.WrapErrParameterInvalidMsg("can not convert compare string: %s", rawCompareStr)
}
timestamptzInt64, err := timestamptz.ValidateAndReturnUnixMicroTz(unquotedCompareStr, v.args.Timezone)
if err != nil {
return err
}
if ctx.GetOp1() == nil {
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: toColumnInfo(colExpr),
Op: compareOp,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: timestamptzInt64}},
},
},
},
dataType: schemapb.DataType_Bool,
}
}
arithOp := arithExprMap[ctx.GetOp1().GetTokenType()]
rawIntervalStr := ctx.GetInterval_string().GetText()
unquotedIntervalStr, err := convertEscapeSingle(rawIntervalStr)
if err != nil {
return merr.WrapErrParameterInvalidMsg("can not convert interval string: %s", rawIntervalStr)
}
interval, err := parseISODuration(unquotedIntervalStr)
if err != nil {
return err
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_TimestamptzArithCompareExpr{
TimestamptzArithCompareExpr: &planpb.TimestamptzArithCompareExpr{
TimestamptzColumn: toColumnInfo(colExpr),
ArithOp: arithOp,
Interval: interval,
CompareOp: compareOp,
CompareValue: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: timestamptzInt64}},
},
},
},
dataType: schemapb.DataType_Bool,
}
}
// VisitTimestamptzCompareReverse handles comparison expressions where the column
// is on the right side of the operator.
// Syntax example: '2025-01-01' [ + INTERVAL 'P1D' ] > column
//
// Optimization and Normalization Logic:
// 1. Operator Reversal: The comparison operator is flipped (e.g., '>' to '<')
// to normalize the expression into a column-centric format.
// 2. Quick Path: For simple comparisons without INTERVAL, it generates a
// UnaryRangeExpr with the reversed operator to leverage indexing.
// 3. Slow Path: For complex expressions involving INTERVAL, it produces a
// TimestamptzArithCompareExpr with the reversed operator.
func (v *ParserVisitor) VisitTimestamptzCompareReverse(ctx *parser.TimestamptzCompareReverseContext) interface{} {
colExpr, err := v.translateIdentifier(ctx.Identifier().GetText())
identifier := ctx.Identifier().GetText()
if err != nil {
return merr.WrapErrParameterInvalidMsg("can not translate identifier: %s", identifier)
}
if colExpr.dataType != schemapb.DataType_Timestamptz {
return merr.WrapErrParameterInvalidMsg("field '%s' is not a timestamptz datatype", identifier)
}
rawCompareStr := ctx.GetCompare_string().GetText()
unquotedCompareStr, err := convertEscapeSingle(rawCompareStr)
if err != nil {
return merr.WrapErrParameterInvalidMsg("can not convert compare string: %s", rawCompareStr)
}
originalCompareOp := cmpOpMap[ctx.GetOp2().GetTokenType()]
compareOp := reverseCompareOp(originalCompareOp)
if compareOp == planpb.OpType_Invalid && originalCompareOp != planpb.OpType_Invalid {
return merr.WrapErrParameterInvalidMsg("unsupported comparison operator for reverse Timestamptz: %s", ctx.GetOp2().GetText())
}
timestamptzInt64, err := timestamptz.ValidateAndReturnUnixMicroTz(unquotedCompareStr, v.args.Timezone)
if err != nil {
return err
}
// Quick Path: No arithmetic operation. Use UnaryRangeExpr for index optimization.
if ctx.GetOp1() == nil {
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: toColumnInfo(colExpr),
Op: compareOp,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: timestamptzInt64}},
},
},
},
dataType: schemapb.DataType_Bool,
}
}
// Slow Path: Handle arithmetic with TimestamptzArithCompareExpr.
arithOp := arithExprMap[ctx.GetOp1().GetTokenType()]
rawIntervalStr := ctx.GetInterval_string().GetText()
unquotedIntervalStr, err := convertEscapeSingle(rawIntervalStr)
if err != nil {
return merr.WrapErrParameterInvalidMsg("can not convert interval string: %s", rawIntervalStr)
}
interval, err := parseISODuration(unquotedIntervalStr)
if err != nil {
return err
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_TimestamptzArithCompareExpr{
TimestamptzArithCompareExpr: &planpb.TimestamptzArithCompareExpr{
TimestamptzColumn: toColumnInfo(colExpr),
ArithOp: arithOp,
Interval: interval,
CompareOp: compareOp,
CompareValue: &planpb.GenericValue{
Val: &planpb.GenericValue_Int64Val{Int64Val: timestamptzInt64},
},
},
},
},
dataType: schemapb.DataType_Bool,
}
}
func reverseCompareOp(op planpb.OpType) planpb.OpType {
switch op {
case planpb.OpType_LessThan:
return planpb.OpType_GreaterThan
case planpb.OpType_LessEqual:
return planpb.OpType_GreaterEqual
case planpb.OpType_GreaterThan:
return planpb.OpType_LessThan
case planpb.OpType_GreaterEqual:
return planpb.OpType_LessEqual
case planpb.OpType_Equal:
return planpb.OpType_Equal
case planpb.OpType_NotEqual:
return planpb.OpType_NotEqual
default:
return planpb.OpType_Invalid
}
}
func validateAndExtractMinShouldMatch(minShouldMatchExpr interface{}) ([]*planpb.GenericValue, error) {
if minShouldMatchValue, ok := minShouldMatchExpr.(*ExprWithType); ok {
valueExpr := getValueExpr(minShouldMatchValue)
if valueExpr == nil || valueExpr.GetValue() == nil {
return nil, merr.WrapErrParameterInvalidMsg("minimum_should_match should be a const integer expression")
}
minShouldMatch := valueExpr.GetValue().GetInt64Val()
if minShouldMatch < 1 {
return nil, merr.WrapErrParameterInvalidMsg("minimum_should_match should be >= 1, got %d", minShouldMatch)
}
if minShouldMatch > 1000 {
return nil, merr.WrapErrParameterInvalidMsg("minimum_should_match should be <= 1000, got %d", minShouldMatch)
}
return []*planpb.GenericValue{NewInt(minShouldMatch)}, nil
}
return nil, nil
}
// VisitElementFilter handles ElementFilter(structArrayField, elementExpr) syntax.
func (v *ParserVisitor) VisitElementFilter(ctx *parser.ElementFilterContext) interface{} {
// Check for nested ElementFilter - not allowed
if v.currentStructArrayField != "" {
return merr.WrapErrParameterInvalidMsg("nested ElementFilter is not supported, already inside ElementFilter for field: %s", v.currentStructArrayField)
}
// Get struct array field name (first parameter)
arrayFieldName := ctx.Identifier().GetText()
// Set current context for element expression parsing
v.currentStructArrayField = arrayFieldName
defer func() { v.currentStructArrayField = "" }()
elementExpr := ctx.Expr().Accept(v)
if err := getError(elementExpr); err != nil {
return merr.WrapErrParameterInvalidMsg("cannot parse element expression: %s, error: %s", ctx.Expr().GetText(), err)
}
exprWithType := getExpr(elementExpr)
if exprWithType == nil {
return merr.WrapErrParameterInvalidMsg("invalid element expression: %s", ctx.Expr().GetText())
}
// Build ElementFilterExpr proto
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_ElementFilterExpr{
ElementFilterExpr: &planpb.ElementFilterExpr{
ElementExpr: exprWithType.expr,
StructName: arrayFieldName,
},
},
},
dataType: schemapb.DataType_Bool,
}
}
// VisitStructSubField handles $[fieldName] syntax within ElementFilter.
func (v *ParserVisitor) VisitStructSubField(ctx *parser.StructSubFieldContext) interface{} {
// Extract the field name from $[fieldName]
tokenText := ctx.StructSubFieldIdentifier().GetText()
if !isValidStructSubField(tokenText) {
return merr.WrapErrParameterInvalidMsg("invalid struct sub-field syntax: %s", tokenText)
}
// Remove "$[" prefix and "]" suffix
fieldName := tokenText[2 : len(tokenText)-1]
// Check if we're inside an ElementFilter or MATCH_* context
if v.currentStructArrayField == "" {
return merr.WrapErrParameterInvalidMsg("$[%s] syntax can only be used inside ElementFilter or MATCH_*", fieldName)
}
// Construct full field name for struct array field
fullFieldName := typeutil.ConcatStructFieldName(v.currentStructArrayField, fieldName)
// Get the struct array field info
field, err := v.schema.GetFieldFromName(fullFieldName)
if err != nil {
return merr.WrapErrParameterInvalidMsg("array field not found: %s, error: %s", fullFieldName, err)
}
// In element-level context, use Array as storage type, element type for operations
elementType := field.GetElementType()
nullable := field.GetNullable()
if structField := v.schema.GetStructArrayFieldFromName(v.currentStructArrayField); structField != nil {
nullable = nullable || structField.GetNullable()
}
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_ColumnExpr{
ColumnExpr: &planpb.ColumnExpr{
Info: &planpb.ColumnInfo{
FieldId: field.FieldID,
DataType: schemapb.DataType_Array, // Storage type is Array
IsPrimaryKey: field.IsPrimaryKey,
IsAutoID: field.AutoID,
IsPartitionKey: field.IsPartitionKey,
IsClusteringKey: field.IsClusteringKey,
ElementType: elementType, // Element type for operations
Nullable: nullable,
IsElementLevel: true, // Mark as element-level access
},
},
},
},
dataType: elementType, // Expression evaluates to element type
nodeDependent: true,
}
}
// parseMatchExpr is a helper function for parsing match expressions
// matchType: the type of match operation (MatchAll, MatchAny, MatchLeast, MatchMost)
// count: for MatchLeast/MatchMost, the count parameter (N); for MatchAll/MatchAny, this is ignored (0)
func (v *ParserVisitor) parseMatchExpr(structArrayFieldName string, exprCtx parser.IExprContext, matchType planpb.MatchType, count int64, funcName string) interface{} {
// Check for nested match expression - not allowed
if v.currentStructArrayField != "" {
return merr.WrapErrParameterInvalidMsg("nested %s is not supported, already inside match expression for field: %s", funcName, v.currentStructArrayField)
}
// Set current context for element expression parsing
v.currentStructArrayField = structArrayFieldName
defer func() { v.currentStructArrayField = "" }()
// Parse the predicate expression
predicate := exprCtx.Accept(v)
if err := getError(predicate); err != nil {
return merr.WrapErrParameterInvalidMsg("cannot parse predicate expression: %s, error: %s", exprCtx.GetText(), err)
}
predicateExpr := getExpr(predicate)
if predicateExpr == nil {
return merr.WrapErrParameterInvalidMsg("invalid predicate expression in %s: %s", funcName, exprCtx.GetText())
}
// Build MatchExpr proto
return &ExprWithType{
expr: &planpb.Expr{
Expr: &planpb.Expr_MatchExpr{
MatchExpr: &planpb.MatchExpr{
StructName: structArrayFieldName,
Predicate: predicateExpr.expr,
MatchType: matchType,
Count: count,
},
},
},
dataType: schemapb.DataType_Bool,
}
}
// VisitMatchSimple handles MATCH_ALL and MATCH_ANY expressions
// Syntax: MATCH_ALL/MATCH_ANY(structArrayField, $[intField] == 1 && $[strField] == "aaa")
func (v *ParserVisitor) VisitMatchSimple(ctx *parser.MatchSimpleContext) interface{} {
structArrayFieldName := ctx.Identifier().GetText()
var matchType planpb.MatchType
var opName string
switch ctx.GetOp().GetTokenType() {
case parser.PlanParserMATCH_ALL:
matchType = planpb.MatchType_MatchAll
opName = "MATCH_ALL"
case parser.PlanParserMATCH_ANY:
matchType = planpb.MatchType_MatchAny
opName = "MATCH_ANY"
default:
return merr.WrapErrParameterInvalidMsg("unhandled match operator: %s", ctx.GetOp().GetText())
}
return v.parseMatchExpr(structArrayFieldName, ctx.Expr(), matchType, 0, opName)
}
// VisitMatchThreshold handles MATCH_LEAST, MATCH_MOST, and MATCH_EXACT expressions
// Syntax: MATCH_LEAST/MATCH_MOST/MATCH_EXACT(structArrayField, $[intField] == 1, threshold=N)
func (v *ParserVisitor) VisitMatchThreshold(ctx *parser.MatchThresholdContext) interface{} {
structArrayFieldName := ctx.Identifier().GetText()
countStr := ctx.IntegerConstant().GetText()
count, err := strconv.ParseInt(countStr, 10, 64)
if err != nil {
return merr.WrapErrParameterInvalidMsg("invalid count: %s", countStr)
}
var matchType planpb.MatchType
var opName string
switch ctx.GetOp().GetTokenType() {
case parser.PlanParserMATCH_LEAST:
matchType = planpb.MatchType_MatchLeast
opName = "MATCH_LEAST"
if count <= 0 {
return merr.WrapErrParameterInvalidMsg("count in MATCH_LEAST must be positive, got: %d", count)
}
case parser.PlanParserMATCH_MOST:
matchType = planpb.MatchType_MatchMost
opName = "MATCH_MOST"
if count < 0 {
return merr.WrapErrParameterInvalidMsg("count in MATCH_MOST cannot be negative, got: %d", count)
}
case parser.PlanParserMATCH_EXACT:
matchType = planpb.MatchType_MatchExact
opName = "MATCH_EXACT"
if count < 0 {
return merr.WrapErrParameterInvalidMsg("count in MATCH_EXACT cannot be negative, got: %d", count)
}
default:
return merr.WrapErrParameterInvalidMsg("unhandled match threshold operator: %s", ctx.GetOp().GetText())
}
return v.parseMatchExpr(structArrayFieldName, ctx.Expr(), matchType, count, opName)
}