milvus/internal/parser/planparserv2/fill_expression_value_test.go
Buqian Zheng 724598d231
fix: handle mixed int64/float types in BinaryRangeExpr for JSON fields (#46681)
test: add unit tests for mixed int64/float types in BinaryRangeExpr

When processing binary range expressions (e.g., `x > 499 && x <= 512.0`)
on JSON/dynamic fields with expression templates, the lower and upper
bounds could have different numeric types (int64 vs float64). This
caused an assertion failure in GetValueFromProto when the template type
didn't match the actual proto value type.

Fixes:
1. Go side (fill_expression_value.go): Normalize numeric types for JSON
fields - if either bound is float and the other is int, convert the int
to float.

2. C++ side (BinaryRangeExpr.cpp):
   - Check both lower_val and upper_val types when dispatching
   - Use double template when either bound is float
- Use GetValueWithCastNumber instead of GetValueFromProto to safely
handle int64->double conversion

issue: #46588

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
- Core invariant: JSON field binary-range expressions must present
numeric bounds to the evaluator with a consistent numeric type; if
either bound is floating-point, both bounds must be treated as double to
avoid proto-type mismatches during template instantiation.
- Bug fix (issue #46588 & concrete change): mixed int64/float bounds
could dispatch the wrong template (e.g.,
ExecRangeVisitorImplForJson<int64_t>) and trigger assertions in
GetValueFromProto. Fixes: (1) Go parser (FillBinaryRangeExpressionValue
in fill_expression_value.go) normalizes mixed JSON numeric bounds by
promoting the int bound to float; (2) C++ evaluator
(PhyBinaryRangeFilterExpr::Eval in BinaryRangeExpr.cpp) inspects both
lower_type and upper_type, sets use_double when either is float, selects
ExecRangeVisitorImplForJson<double> for mixed numeric cases, and
replaces GetValueFromProto with GetValueWithCastNumber so int64→double
conversions are handled safely.
- Removed / simplified logic: the previous evaluator branched on only
the lower bound's proto type and had separate index/non-index handling
for int64 vs float; that per-bound branching is replaced by unified
numeric handling (convert to double when needed) and a single numeric
path for index use — eliminating redundant, error-prone branches that
assumed homogeneous bound types.
- No data loss or regression: changes only promote int→double for
JSON-range comparisons when the other bound is float; integer-only and
float-only paths remain unchanged. Promotion uses IEEE double (C++
double and Go float64) and only affects template dispatch and
value-extraction paths; GetValueWithCastNumber safely converts int64 to
double and index/non-index code paths both normalize consistently,
preserving semantics for comparisons and avoiding assertion failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
2025-12-31 11:52:24 +08:00

582 lines
23 KiB
Go

package planparserv2
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
type FillExpressionValueSuite struct {
suite.Suite
}
func TestFillExpressionValue(t *testing.T) {
suite.Run(t, new(FillExpressionValueSuite))
}
type testcase struct {
expr string
values map[string]*schemapb.TemplateValue
}
func (s *FillExpressionValueSuite) assertValidExpr(helper *typeutil.SchemaHelper, exprStr string, templateValues map[string]*schemapb.TemplateValue) {
expr, err := ParseExpr(helper, exprStr, templateValues)
s.NoError(err, exprStr)
s.NotNil(expr, exprStr)
ShowExpr(expr)
}
func (s *FillExpressionValueSuite) assertInvalidExpr(helper *typeutil.SchemaHelper, exprStr string, templateValues map[string]*schemapb.TemplateValue) {
expr, err := ParseExpr(helper, exprStr, templateValues)
s.Error(err, exprStr)
s.Nil(expr, exprStr)
}
func (s *FillExpressionValueSuite) TestTermExpr() {
s.Run("normal case", func() {
testcases := []testcase{
{`Int64Field in {age}`, map[string]*schemapb.TemplateValue{
"age": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3), int64(4)})),
}},
{`FloatField in {age}`, map[string]*schemapb.TemplateValue{
"age": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Double, []float64{1.1, 2.2, 3.3, 4.4})),
}},
{`A in {list}`, map[string]*schemapb.TemplateValue{
"list": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_JSON, [][]byte{
generateJSONData(int64(1)),
generateJSONData(2.2),
generateJSONData("abc"),
generateJSONData(false),
})),
}},
{`ArrayField in {list}`, map[string]*schemapb.TemplateValue{
"list": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Array,
[]*schemapb.TemplateArrayValue{
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3)}),
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(4), int64(5), int64(6)}),
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(7), int64(8), int64(9)}),
})),
}},
{`ArrayField[0] in {list}`, map[string]*schemapb.TemplateValue{
"list": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3)})),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertValidExpr(schemaH, c.expr, c.values)
}
})
s.Run("failed case", func() {
testcases := []testcase{
{`Int64Field in {age}`, map[string]*schemapb.TemplateValue{
"age": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_String, []string{"abc", "def"})),
}},
{`StringField in {list}`, map[string]*schemapb.TemplateValue{
"list": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_JSON, [][]byte{
generateJSONData(int64(1)),
generateJSONData("abc"),
generateJSONData(2.2),
generateJSONData(false),
})),
}},
{"ArrayField[0] in {list}", map[string]*schemapb.TemplateValue{
"list": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_JSON, [][]byte{
generateJSONData(int64(1)),
generateJSONData("abc"),
generateJSONData(3.2),
})),
}},
{"Int64Field not in {not_list}", map[string]*schemapb.TemplateValue{
"not_list": generateTemplateValue(schemapb.DataType_Int64, int64(33)),
}},
{"Int64Field not in {not_list}", map[string]*schemapb.TemplateValue{
"age": generateTemplateValue(schemapb.DataType_Int64, int64(33)),
}},
{`Int64Field in {empty_list}`, map[string]*schemapb.TemplateValue{
"empty_list": generateTemplateValue(schemapb.DataType_Array, &schemapb.TemplateArrayValue{}),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertInvalidExpr(schemaH, c.expr, c.values)
}
})
}
func (s *FillExpressionValueSuite) TestUnaryRange() {
s.Run("normal case", func() {
testcases := []testcase{
{`Int64Field == 10`, nil},
{`Int64Field > {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
}},
{`FloatField < {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Double, float64(12.3)),
}},
{`DoubleField != {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Double, float64(3.5)),
}},
{`ArrayField[0] >= {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Int64, int64(3)),
}},
{`BoolField == {bool}`, map[string]*schemapb.TemplateValue{
"bool": generateTemplateValue(schemapb.DataType_Bool, false),
}},
{`{str} != StringField`, map[string]*schemapb.TemplateValue{
"str": generateTemplateValue(schemapb.DataType_String, "abc"),
}},
{`{target} > Int64Field`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertValidExpr(schemaH, c.expr, c.values)
}
})
s.Run("failed case", func() {
testcases := []testcase{
{`Int64Field == 10.5`, nil},
{`Int64Field > {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Double, 11.2),
}},
{`FloatField < {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_String, "abc"),
}},
{`DoubleField != {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Bool, false),
}},
{`ArrayField[0] >= {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Double, 3.5),
}},
{`BoolField == {bool}`, map[string]*schemapb.TemplateValue{
"bool": generateTemplateValue(schemapb.DataType_String, "abc"),
}},
{`{str} != StringField`, map[string]*schemapb.TemplateValue{
"str": generateTemplateValue(schemapb.DataType_Int64, int64(5)),
}},
{`{int} != StringField`, map[string]*schemapb.TemplateValue{
"int": generateTemplateValue(schemapb.DataType_Int64, int64(5)),
}},
{`{str} != StringField`, map[string]*schemapb.TemplateValue{
"int": generateTemplateValue(schemapb.DataType_Int64, int64(5)),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertInvalidExpr(schemaH, c.expr, c.values)
}
})
}
func (s *FillExpressionValueSuite) TestBinaryRange() {
s.Run("normal case", func() {
testcases := []testcase{
{`10 < Int64Field < 20`, nil},
{`{max} > Int64Field > {min}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
"max": generateTemplateValue(schemapb.DataType_Int64, int64(22)),
}},
{`{min} <= FloatField <= {max}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Float, float64(11)),
"max": generateTemplateValue(schemapb.DataType_Float, float64(22)),
}},
{`{min} < DoubleField < {max}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Double, float64(11)),
"max": generateTemplateValue(schemapb.DataType_Double, float64(22)),
}},
{`{max} >= ArrayField[0] >= {min}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
"max": generateTemplateValue(schemapb.DataType_Int64, int64(22)),
}},
{`{max} > Int64Field >= 10`, map[string]*schemapb.TemplateValue{
"max": generateTemplateValue(schemapb.DataType_Int64, int64(22)),
}},
{`30 >= Int64Field > {min}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
}},
{`10 < Int64Field <= {max}`, map[string]*schemapb.TemplateValue{
"max": generateTemplateValue(schemapb.DataType_Int64, int64(22)),
}},
{`{min} <= Int64Field < 20`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertValidExpr(schemaH, c.expr, c.values)
}
})
s.Run("failed case", func() {
testcases := []testcase{
{`10 < Int64Field < 20.5`, nil},
{`{max} > Int64Field > {min}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
"max": generateTemplateValue(schemapb.DataType_Double, 22.5),
}},
{`{min} <= FloatField <= {max}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_String, "abc"),
"max": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
}},
{`{min} < DoubleField < {max}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(33)),
"max": generateTemplateValue(schemapb.DataType_Int64, int64(22)),
}},
{`{max} >= ArrayField[0] >= {min}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Double, 11.5),
"max": generateTemplateValue(schemapb.DataType_Int64, int64(22)),
}},
{`{max} >= Int64Field >= {min}`, map[string]*schemapb.TemplateValue{
"max": generateTemplateValue(schemapb.DataType_Int64, int64(22)),
}},
{`{max} > Int64Field`, map[string]*schemapb.TemplateValue{
"max": generateTemplateValue(schemapb.DataType_Bool, false),
}},
{`{$meta} > Int64Field`, map[string]*schemapb.TemplateValue{
"$meta": generateTemplateValue(schemapb.DataType_Int64, int64(22)),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertInvalidExpr(schemaH, c.expr, c.values)
}
})
}
func (s *FillExpressionValueSuite) TestBinaryArithOpEvalRange() {
s.Run("normal case", func() {
testcases := []testcase{
{`Int64Field + 5 == 10`, nil},
{`Int64Field - {offset} >= {target}`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Int64, int64(3)),
"target": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
}},
{`Int64Field * 3 <= {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Int64, int64(11)),
}},
{`Int64Field / {offset} > 11`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Int64, int64(3)),
}},
{`FloatField / {offset} > 11.3`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Double, 3.3),
}},
{`ArrayField[0] % {offset} < 11`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Int64, int64(3)),
}},
{`array_length(ArrayField) == {length}`, map[string]*schemapb.TemplateValue{
"length": generateTemplateValue(schemapb.DataType_Int64, int64(3)),
}},
{`array_length(ArrayField) > {length}`, map[string]*schemapb.TemplateValue{
"length": generateTemplateValue(schemapb.DataType_Int64, int64(3)),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertValidExpr(schemaH, c.expr, c.values)
}
})
s.Run("failed case", func() {
testcases := []testcase{
{`Int64Field + 6 == 12.5`, nil},
{`Int64Field - {offset} == {target}`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Int64, int64(4)),
"target": generateTemplateValue(schemapb.DataType_Double, 13.5),
}},
{`Int64Field * 6 == {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Double, 13.5),
}},
{`Int64Field / {offset} == 11.5`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Int64, int64(6)),
}},
{`Int64Field % {offset} < 11`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Double, 3.5),
}},
{`Int64Field + {offset} < {target}`, map[string]*schemapb.TemplateValue{
"target": generateTemplateValue(schemapb.DataType_Double, 3.5),
}},
{`Int64Field + {offset} < {target}`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_String, "abc"),
"target": generateTemplateValue(schemapb.DataType_Int64, int64(15)),
}},
{`Int64Field + {offset} < {target}`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Int64, int64(15)),
"target": generateTemplateValue(schemapb.DataType_String, "def"),
}},
{`ArrayField + {offset} < {target}`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Double, 3.5),
"target": generateTemplateValue(schemapb.DataType_Int64, int64(5)),
}},
{`ArrayField[0] + {offset} < {target}`, map[string]*schemapb.TemplateValue{
"offset": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3)})),
"target": generateTemplateValue(schemapb.DataType_Int64, int64(5)),
}},
{`array_length(ArrayField) == {length}`, map[string]*schemapb.TemplateValue{
"length": generateTemplateValue(schemapb.DataType_String, "abc"),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertInvalidExpr(schemaH, c.expr, c.values)
}
})
}
func (s *FillExpressionValueSuite) TestJSONContainsExpression() {
s.Run("normal case", func() {
testcases := []testcase{
{`json_contains(A, 5)`, nil},
{`json_contains(A, {age})`, map[string]*schemapb.TemplateValue{
"age": generateTemplateValue(schemapb.DataType_Int64, int64(18)),
}},
{`json_contains(A, {str})`, map[string]*schemapb.TemplateValue{
"str": generateTemplateValue(schemapb.DataType_String, "abc"),
}},
{`json_contains(A, {bool})`, map[string]*schemapb.TemplateValue{
"bool": generateTemplateValue(schemapb.DataType_Bool, false),
}},
{`json_contains_any(JSONField, {array})`, map[string]*schemapb.TemplateValue{
"array": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_JSON, [][]byte{
generateJSONData(int64(1)),
generateJSONData("abc"),
generateJSONData(2.2),
generateJSONData(false),
})),
}},
{`json_contains_any(JSONField, {array})`, map[string]*schemapb.TemplateValue{
"array": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3), int64(4)})),
}},
{`json_contains_any(JSONField["A"], {array})`, map[string]*schemapb.TemplateValue{
"array": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3), int64(4)})),
}},
{`json_contains_all(JSONField["A"], {array})`, map[string]*schemapb.TemplateValue{
"array": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_JSON, [][]byte{
generateJSONData(int64(1)),
generateJSONData("abc"),
generateJSONData(2.2),
generateJSONData(false),
})),
}},
{`json_contains_all(JSONField["A"], {array})`, map[string]*schemapb.TemplateValue{
"array": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Array, []*schemapb.TemplateArrayValue{
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3)}),
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(4), int64(5), int64(6)}),
})),
}},
{`json_contains(ArrayField, {int})`, map[string]*schemapb.TemplateValue{
"int": generateTemplateValue(schemapb.DataType_Int64, int64(1)),
}},
{`json_contains_any(ArrayField, {list})`, map[string]*schemapb.TemplateValue{
"list": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3), int64(4)})),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertValidExpr(schemaH, c.expr, c.values)
}
})
s.Run("failed case", func() {
testcases := []testcase{
{`json_contains(ArrayField[0], {str})`, map[string]*schemapb.TemplateValue{
"str": generateTemplateValue(schemapb.DataType_String, "abc"),
}},
{`json_contains_any(JSONField, {not_array})`, map[string]*schemapb.TemplateValue{
"not_array": generateTemplateValue(schemapb.DataType_Int64, int64(1)),
}},
{`json_contains_all(JSONField, {not_array})`, map[string]*schemapb.TemplateValue{
"not_array": generateTemplateValue(schemapb.DataType_Int64, int64(1)),
}},
{`json_contains_all(JSONField, {not_array})`, map[string]*schemapb.TemplateValue{
"array": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3)})),
}},
{`json_contains_all(ArrayField, {array})`, map[string]*schemapb.TemplateValue{
"array": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_JSON, [][]byte{
generateJSONData(int64(1)),
generateJSONData("abc"),
generateJSONData(2.2),
generateJSONData(false),
})),
}},
{`json_contains(ArrayField, {array})`, map[string]*schemapb.TemplateValue{
"array": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_Int64, []int64{int64(1), int64(2), int64(3)})),
}},
{`json_contains_any(ArrayField, {array})`, map[string]*schemapb.TemplateValue{
"array": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_JSON, [][]byte{
generateJSONData(int64(1)),
generateJSONData("abc"),
generateJSONData(2.2),
generateJSONData(false),
})),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertInvalidExpr(schemaH, c.expr, c.values)
}
})
}
func (s *FillExpressionValueSuite) TestBinaryExpression() {
s.Run("normal case", func() {
testcases := []testcase{
{`Int64Field > {int} && StringField in {list}`, map[string]*schemapb.TemplateValue{
"int": generateTemplateValue(schemapb.DataType_Int64, int64(10)),
"list": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_String, []string{"abc", "def", "ghi"})),
}},
{`{max} > FloatField >= {min} or BoolField == {bool}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(10)),
"max": generateTemplateValue(schemapb.DataType_Float, 22.22),
"bool": generateTemplateValue(schemapb.DataType_Bool, true),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertValidExpr(schemaH, c.expr, c.values)
}
})
s.Run("failed case", func() {
testcases := []testcase{
{`Int64Field > {int} && StringField in {list}`, map[string]*schemapb.TemplateValue{
"int": generateTemplateValue(schemapb.DataType_String, "abc"),
"list": generateTemplateValue(schemapb.DataType_Array,
generateTemplateArrayValue(schemapb.DataType_JSON, [][]byte{
generateJSONData("abc"),
generateJSONData(int64(10)),
generateJSONData("ghi"),
})),
}},
{`{max} > FloatField >= {min} or BoolField == {bool}`, map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(10)),
"bool": generateTemplateValue(schemapb.DataType_Bool, true),
}},
}
schemaH := newTestSchemaHelper(s.T())
for _, c := range testcases {
s.assertInvalidExpr(schemaH, c.expr, c.values)
}
})
}
func (s *FillExpressionValueSuite) TestBinaryRangeWithMixedNumericTypesForJSON() {
// Test that mixed int64/float types are normalized to float for JSON fields.
// This prevents assertion failures in C++ expression execution.
// Related issue: https://github.com/milvus-io/milvus/issues/46588
schemaH := newTestSchemaHelper(s.T())
s.Run("lower int64 upper float should normalize to float", func() {
// A is a dynamic field (JSON type)
exprStr := `{min} < A < {max}`
templateValues := map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(499)),
"max": generateTemplateValue(schemapb.DataType_Double, float64(512.0)),
}
expr, err := ParseExpr(schemaH, exprStr, templateValues)
s.NoError(err)
s.NotNil(expr)
// Verify both bounds are normalized to float type
bre := expr.GetBinaryRangeExpr()
s.NotNil(bre, "expected BinaryRangeExpr")
s.Equal(float64(499), bre.GetLowerValue().GetFloatVal())
s.Equal(float64(512.0), bre.GetUpperValue().GetFloatVal())
})
s.Run("lower float upper int64 should normalize to float", func() {
exprStr := `{min} < A < {max}`
templateValues := map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Double, float64(10.5)),
"max": generateTemplateValue(schemapb.DataType_Int64, int64(100)),
}
expr, err := ParseExpr(schemaH, exprStr, templateValues)
s.NoError(err)
s.NotNil(expr)
// Verify both bounds are normalized to float type
bre := expr.GetBinaryRangeExpr()
s.NotNil(bre, "expected BinaryRangeExpr")
s.Equal(float64(10.5), bre.GetLowerValue().GetFloatVal())
s.Equal(float64(100), bre.GetUpperValue().GetFloatVal())
})
s.Run("both int64 should remain int64", func() {
exprStr := `{min} < A < {max}`
templateValues := map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Int64, int64(10)),
"max": generateTemplateValue(schemapb.DataType_Int64, int64(100)),
}
expr, err := ParseExpr(schemaH, exprStr, templateValues)
s.NoError(err)
s.NotNil(expr)
// Verify both bounds remain int64 type
bre := expr.GetBinaryRangeExpr()
s.NotNil(bre, "expected BinaryRangeExpr")
s.Equal(int64(10), bre.GetLowerValue().GetInt64Val())
s.Equal(int64(100), bre.GetUpperValue().GetInt64Val())
})
s.Run("both float should remain float", func() {
exprStr := `{min} < A < {max}`
templateValues := map[string]*schemapb.TemplateValue{
"min": generateTemplateValue(schemapb.DataType_Double, float64(10.5)),
"max": generateTemplateValue(schemapb.DataType_Double, float64(100.5)),
}
expr, err := ParseExpr(schemaH, exprStr, templateValues)
s.NoError(err)
s.NotNil(expr)
// Verify both bounds remain float type
bre := expr.GetBinaryRangeExpr()
s.NotNil(bre, "expected BinaryRangeExpr")
s.Equal(float64(10.5), bre.GetLowerValue().GetFloatVal())
s.Equal(float64(100.5), bre.GetUpperValue().GetFloatVal())
})
}