From ca3cfd2577f7df214b41861fad6e294f189ffb39 Mon Sep 17 00:00:00 2001 From: GreatRiver Date: Mon, 6 Jul 2026 13:49:34 +0800 Subject: [PATCH 1/2] fix nullable in list runtime semantics --- .../colexec/evalExpression_zonemap_test.go | 105 ++++++++++++++++++ pkg/sql/plan/function/operator_in.go | 64 +++++++++-- pkg/sql/plan/function/operator_in_test.go | 57 ++++++++++ pkg/sql/plan/rule/constant_fold.go | 5 +- pkg/sql/plan/utils.go | 8 +- pkg/vm/engine/readutil/expr_filter.go | 5 +- pkg/vm/engine/tae/index/zm.go | 25 +++++ pkg/vm/engine/tae/index/zm_test.go | 53 +++++++++ 8 files changed, 307 insertions(+), 15 deletions(-) create mode 100644 pkg/sql/plan/function/operator_in_test.go diff --git a/pkg/sql/colexec/evalExpression_zonemap_test.go b/pkg/sql/colexec/evalExpression_zonemap_test.go index c4e42d52457be..66548db648e05 100644 --- a/pkg/sql/colexec/evalExpression_zonemap_test.go +++ b/pkg/sql/colexec/evalExpression_zonemap_test.go @@ -18,6 +18,7 @@ import ( "context" "testing" + "github.com/matrixorigin/matrixone/pkg/container/batch" "github.com/matrixorigin/matrixone/pkg/container/types" "github.com/matrixorigin/matrixone/pkg/container/vector" "github.com/matrixorigin/matrixone/pkg/objectio" @@ -27,6 +28,7 @@ import ( "github.com/matrixorigin/matrixone/pkg/sql/plan/function" "github.com/matrixorigin/matrixone/pkg/testutil" "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/index" + "github.com/matrixorigin/matrixone/pkg/vm/process" "github.com/stretchr/testify/require" ) @@ -42,6 +44,46 @@ func TestEvaluateFilterByZoneMapNullableInListIsConservative(t *testing.T) { require.True(t, selected, plan2.FormatExpr(expr, plan2.FormatOption{})) } +func TestEvaluateFilterByZoneMapNullableInVecIsConservative(t *testing.T) { + proc := testutil.NewProcess(t) + ctx := proc.Ctx + + expr := makeVarcharInVecExpr(t, ctx, proc, "keep", true) + meta := makeVarcharBlockMeta("key", "keep") + zms, vecs := makeZoneMapEvalScratch(expr) + + selected := colexec.EvaluateFilterByZoneMap(ctx, proc, expr, meta, map[int]int{0: 0}, zms, vecs) + require.True(t, selected, plan2.FormatExpr(expr, plan2.FormatOption{})) +} + +func TestFoldedNullableInExprKeepsMatchAndNullsMiss(t *testing.T) { + proc := testutil.NewProcess(t) + ctx := proc.Ctx + + expr := makeVarcharInExpr(t, ctx, "keep", true) + folded, err := plan2.ConstantFold(batch.EmptyForConstFoldBatch, plan2.DeepCopyExpr(expr), proc, true, true) + require.NoError(t, err) + + result := evalVarcharPredicate(t, proc, folded, "keep", "key", "") + requireBoolValue(t, result, 0, true, false) + requireBoolValue(t, result, 1, false, true) + requireBoolValue(t, result, 2, false, true) +} + +func TestFoldedNullableNotInExprNullsMiss(t *testing.T) { + proc := testutil.NewProcess(t) + ctx := proc.Ctx + + expr := makeVarcharNotInExpr(t, ctx, "keep", true) + folded, err := plan2.ConstantFold(batch.EmptyForConstFoldBatch, plan2.DeepCopyExpr(expr), proc, true, true) + require.NoError(t, err) + + result := evalVarcharPredicate(t, proc, folded, "keep", "key", "") + requireBoolValue(t, result, 0, false, false) + requireBoolValue(t, result, 1, false, true) + requireBoolValue(t, result, 2, false, true) +} + func TestEvaluateFilterByZoneMapStillPrunesFalseEquality(t *testing.T) { proc := testutil.NewProcess(t) ctx := proc.Ctx @@ -286,6 +328,32 @@ func makeVarcharInExpr(t *testing.T, ctx context.Context, value string, withNull return expr } +func makeVarcharInVecExpr(t *testing.T, ctx context.Context, proc *process.Process, value string, withNull bool) *plan.Expr { + t.Helper() + + vec := vector.NewVec(types.T_varchar.ToType()) + require.NoError(t, vector.AppendBytes(vec, []byte(value), false, proc.Mp())) + if withNull { + require.NoError(t, vector.AppendBytes(vec, nil, true, proc.Mp())) + } + data, err := vec.MarshalBinary() + require.NoError(t, err) + vec.Free(proc.Mp()) + + vecLen := int32(1) + if withNull { + vecLen = 2 + } + vecExpr := &plan.Expr{ + Typ: makeVarcharColExpr().Typ, + Expr: &plan.Expr_Vec{Vec: &plan.LiteralVec{Len: vecLen, Data: data}}, + } + return makeNativeVarcharInExpr(t, ctx, []*plan.Expr{ + makeVarcharColExpr(), + vecExpr, + }) +} + func makeNativeVarcharInExpr(t *testing.T, ctx context.Context, args []*plan.Expr) *plan.Expr { t.Helper() @@ -335,6 +403,43 @@ func makeVarcharNotInExpr(t *testing.T, ctx context.Context, value string, withN return expr } +func evalVarcharPredicate(t *testing.T, proc *process.Process, expr *plan.Expr, values ...string) *vector.Vector { + t.Helper() + + input := batch.NewWithSize(1) + defer input.Clean(proc.Mp()) + vec := vector.NewVec(types.T_varchar.ToType()) + input.Vecs[0] = vec + for _, value := range values { + require.NoError(t, vector.AppendBytes(vec, []byte(value), value == "", proc.Mp())) + } + input.SetRowCount(len(values)) + + executor, err := colexec.NewExpressionExecutor(proc, expr) + require.NoError(t, err) + defer executor.Free() + + result, err := executor.Eval(proc, []*batch.Batch{input}, nil) + require.NoError(t, err) + dup, err := result.Dup(proc.Mp()) + require.NoError(t, err) + t.Cleanup(func() { + dup.Free(proc.Mp()) + }) + return dup +} + +func requireBoolValue(t *testing.T, vec *vector.Vector, row uint64, expected bool, expectedNull bool) { + t.Helper() + + param := vector.GenerateFunctionFixedTypeParameter[bool](vec) + actual, isNull := param.GetValue(row) + require.Equal(t, expectedNull, isNull) + if !expectedNull { + require.Equal(t, expected, actual) + } +} + func makeVarcharColExpr() *plan.Expr { return makeVarcharColExprAt(0) } diff --git a/pkg/sql/plan/function/operator_in.go b/pkg/sql/plan/function/operator_in.go index 84948f6225f54..7e57b1158940b 100644 --- a/pkg/sql/plan/function/operator_in.go +++ b/pkg/sql/plan/function/operator_in.go @@ -27,13 +27,15 @@ type TGenericOfIn interface { } type opOperatorFixedIn[T TGenericOfIn] struct { - ready bool - mp map[T]bool + ready bool + hasNull bool + mp map[T]bool } type opOperatorStrIn struct { - ready bool - mp map[string]bool + ready bool + hasNull bool + mp map[string]bool } func newOpOperatorFixedIn[T TGenericOfIn]() *opOperatorFixedIn[T] { @@ -50,15 +52,22 @@ func newOpOperatorStrIn() *opOperatorStrIn { func (op *opOperatorFixedIn[T]) init(tuple *vector.Vector) { op.ready = true + op.hasNull = false if tuple.IsConstNull() { + op.hasNull = true op.mp = make(map[T]bool) return } p := vector.GenerateFunctionFixedTypeParameter[T](tuple) if tuple.IsConst() { - v, _ := p.GetValue(0) + v, null := p.GetValue(0) + if null { + op.hasNull = true + op.mp = make(map[T]bool) + return + } op.mp = make(map[T]bool, 1) op.mp[v] = true return @@ -67,23 +76,32 @@ func (op *opOperatorFixedIn[T]) init(tuple *vector.Vector) { op.mp = make(map[T]bool, tuple.Length()) for i := uint64(0); i < uint64(tuple.Length()); i++ { v, null := p.GetValue(i) - if !null { - op.mp[v] = true + if null { + op.hasNull = true + continue } + op.mp[v] = true } } func (op *opOperatorStrIn) init(tuple *vector.Vector) { op.ready = true + op.hasNull = false if tuple.IsConstNull() { + op.hasNull = true op.mp = make(map[string]bool) return } p := vector.GenerateFunctionStrParameter(tuple) if tuple.IsConst() { - v, _ := p.GetStrValue(0) + v, null := p.GetStrValue(0) + if null { + op.hasNull = true + op.mp = make(map[string]bool) + return + } op.mp = make(map[string]bool, 1) op.mp[string(v)] = true return @@ -92,9 +110,11 @@ func (op *opOperatorStrIn) init(tuple *vector.Vector) { op.mp = make(map[string]bool) for i := uint64(0); i < uint64(tuple.Length()); i++ { v, null := p.GetStrValue(i) - if !null { - op.mp[string(v)] = true + if null { + op.hasNull = true + continue } + op.mp[string(v)] = true } } @@ -113,6 +133,12 @@ func (op *opOperatorFixedIn[T]) operatorIn(parameters []*vector.Vector, result v } } else { _, ok := op.mp[v] + if !ok && op.hasNull { + if err := rs.Append(false, true); err != nil { + return err + } + continue + } if err := rs.Append(ok, false); err != nil { return err } @@ -136,6 +162,12 @@ func (op *opOperatorFixedIn[T]) operatorNotIn(parameters []*vector.Vector, resul } } else { _, ok := op.mp[v] + if !ok && op.hasNull { + if err := rs.Append(false, true); err != nil { + return err + } + continue + } if err := rs.Append(!ok, false); err != nil { return err } @@ -159,6 +191,12 @@ func (op *opOperatorStrIn) operatorIn(parameters []*vector.Vector, result vector } } else { _, ok := op.mp[string(v)] + if !ok && op.hasNull { + if err := rs.Append(false, true); err != nil { + return err + } + continue + } if err := rs.Append(ok, false); err != nil { return err } @@ -182,6 +220,12 @@ func (op *opOperatorStrIn) operatorNotIn(parameters []*vector.Vector, result vec } } else { _, ok := op.mp[string(v)] + if !ok && op.hasNull { + if err := rs.Append(false, true); err != nil { + return err + } + continue + } if err := rs.Append(!ok, false); err != nil { return err } diff --git a/pkg/sql/plan/function/operator_in_test.go b/pkg/sql/plan/function/operator_in_test.go new file mode 100644 index 0000000000000..6f90b400541a9 --- /dev/null +++ b/pkg/sql/plan/function/operator_in_test.go @@ -0,0 +1,57 @@ +// Copyright 2026 Matrix Origin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package function + +import ( + "testing" + + "github.com/matrixorigin/matrixone/pkg/container/types" + "github.com/matrixorigin/matrixone/pkg/testutil" + "github.com/stretchr/testify/require" +) + +func TestOperatorInNullableListUsesThreeValuedLogic(t *testing.T) { + proc := testutil.NewProcess(t) + + tc := NewFunctionTestCase( + proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{"keep", "key", ""}, []bool{false, false, true}), + NewFunctionTestInput(types.T_varchar.ToType(), []string{"keep", ""}, []bool{false, true}), + }, + NewFunctionTestResult(types.T_bool.ToType(), false, []bool{true, false, false}, []bool{false, true, true}), + newOpOperatorStrIn().operatorIn, + ) + + ok, errInfo := tc.Run() + require.True(t, ok, errInfo) +} + +func TestOperatorNotInNullableListUsesThreeValuedLogic(t *testing.T) { + proc := testutil.NewProcess(t) + + tc := NewFunctionTestCase( + proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{"keep", "key", ""}, []bool{false, false, true}), + NewFunctionTestInput(types.T_varchar.ToType(), []string{"keep", ""}, []bool{false, true}), + }, + NewFunctionTestResult(types.T_bool.ToType(), false, []bool{false, false, false}, []bool{false, true, true}), + newOpOperatorStrIn().operatorNotIn, + ) + + ok, errInfo := tc.Run() + require.True(t, ok, errInfo) +} diff --git a/pkg/sql/plan/rule/constant_fold.go b/pkg/sql/plan/rule/constant_fold.go index e5ffdc9975d61..17527cc5e97f4 100644 --- a/pkg/sql/plan/rule/constant_fold.go +++ b/pkg/sql/plan/rule/constant_fold.go @@ -154,7 +154,10 @@ func (r *ConstantFold) constantFold(expr *plan.Expr, proc *process.Process) *pla } defer vec.Free(proc.Mp()) - vec.InplaceSortAndCompact() + // Nullable IN-lists must keep their null bitmap aligned with values. + if !vec.IsConstNull() && !vec.GetNulls().Any() { + vec.InplaceSortAndCompact() + } data, err := vec.MarshalBinary() if err != nil { diff --git a/pkg/sql/plan/utils.go b/pkg/sql/plan/utils.go index 00c0385b48d22..8e9232ee30193 100644 --- a/pkg/sql/plan/utils.go +++ b/pkg/sql/plan/utils.go @@ -1484,7 +1484,10 @@ func ConstantFold(bat *batch.Batch, expr *plan.Expr, proc *process.Process, varA } defer vec.Free(proc.Mp()) - vec.InplaceSortAndCompact() + // Nullable IN-lists must keep their null bitmap aligned with values. + if !vec.IsConstNull() && !vec.GetNulls().Any() { + vec.InplaceSortAndCompact() + } data, err := vec.MarshalBinary() if err != nil { return nil, err @@ -3260,7 +3263,8 @@ func EvalFoldExpr(proc *process.Process, expr *Expr, executors *[]colexec.Expres if err != nil { return err } - if !vec.IsConstNull() { + // Nullable folded lists must keep their null bitmap aligned with values. + if !vec.IsConstNull() && !vec.GetNulls().Any() { vec.InplaceSortAndCompact() } data, err = vec.MarshalBinary() diff --git a/pkg/vm/engine/readutil/expr_filter.go b/pkg/vm/engine/readutil/expr_filter.go index 39d722f24ac90..f1229885aa3d1 100644 --- a/pkg/vm/engine/readutil/expr_filter.go +++ b/pkg/vm/engine/readutil/expr_filter.go @@ -945,8 +945,9 @@ func CompileFilterExpr( dataMeta := meta.MustDataMeta() return dataMeta.MustGetColumn(uint16(seqNum)).ZoneMap().AnyIn(vec), nil } + vecHasNull := vec.IsConstNull() || vec.GetNulls().Any() var maxVal []byte - if vec.Length() > 0 { + if vec.Length() > 0 && !vecHasNull { maxVal = vec.GetRawBytesAt(vec.Length() - 1) } blockFilterOp = func( @@ -972,7 +973,7 @@ func CompileFilterExpr( } return false, true, nil } - if isSorted && vec.Length() > 0 { + if isSorted && vec.Length() > 0 && !vecHasNull { minVal := vec.GetRawBytesAt(0) seekOp = func(meta objectio.ObjectDataMeta) int { blockCnt := int(meta.BlockCount()) diff --git a/pkg/vm/engine/tae/index/zm.go b/pkg/vm/engine/tae/index/zm.go index cc641f23bfd61..8cedb23d6d009 100644 --- a/pkg/vm/engine/tae/index/zm.go +++ b/pkg/vm/engine/tae/index/zm.go @@ -747,6 +747,12 @@ func (zm ZM) PrefixIn(vec *vector.Vector) bool { // anyIn has been called, so there must be a subvector in this zonemap // return lower bound and upper bound func (zm ZM) SubVecIn(vec *vector.Vector) (int, int) { + if vec.IsConstNull() { + return 0, 0 + } + if vec.GetNulls().Any() { + return 0, vec.Length() + } if vec.Length() <= 3 { return 0, vec.Length() } @@ -1040,6 +1046,12 @@ func (zm ZM) SubVecIn(vec *vector.Vector) (int, int) { } func (zm ZM) AnyIn(vec *vector.Vector) bool { + if vec.IsConstNull() { + return false + } + if vec.GetNulls().Any() { + return zm.anyInNullableVec(vec) + } switch vec.GetType().Oid { case types.T_bool: col := vector.MustFixedColNoTypeCheck[bool](vec) @@ -1280,6 +1292,19 @@ func (zm ZM) AnyIn(vec *vector.Vector) bool { } } +func (zm ZM) anyInNullableVec(vec *vector.Vector) bool { + for i := 0; i < vec.Length(); i++ { + if vec.IsNull(uint64(i)) { + continue + } + value := vec.GetRawBytesAt(i) + if zm.AnyLEByValue(value) && zm.AnyGEByValue(value) { + return true + } + } + return false +} + // max = v1.max+v2.max // min = v1.min+v2.min func ZMPlus(v1, v2, res ZM) ZM { diff --git a/pkg/vm/engine/tae/index/zm_test.go b/pkg/vm/engine/tae/index/zm_test.go index c07435291deca..093d26778d08a 100644 --- a/pkg/vm/engine/tae/index/zm_test.go +++ b/pkg/vm/engine/tae/index/zm_test.go @@ -319,6 +319,59 @@ func TestVectorZM(t *testing.T) { require.Zero(t, m.CurrNB()) } +func TestZoneMapAnyInSkipsNulls(t *testing.T) { + mp := mpool.MustNewZero() + vec := vector.NewVec(types.T_varchar.ToType()) + defer vec.Free(mp) + + require.NoError(t, vector.AppendBytes(vec, []byte("aaa"), false, mp)) + require.NoError(t, vector.AppendBytes(vec, []byte("key"), false, mp)) + require.NoError(t, vector.AppendBytes(vec, []byte("keep"), false, mp)) + require.NoError(t, vector.AppendBytes(vec, nil, true, mp)) + + zm := NewZM(types.T_varchar, 0) + UpdateZM(zm, []byte("key")) + UpdateZM(zm, []byte("keep")) + + require.True(t, zm.AnyIn(vec)) + lower, upper := zm.SubVecIn(vec) + require.Equal(t, 0, lower) + require.Equal(t, vec.Length(), upper) +} + +func TestZoneMapAnyInSkipsNullsForFixedTypes(t *testing.T) { + mp := mpool.MustNewZero() + vec := vector.NewVec(types.T_int32.ToType()) + defer vec.Free(mp) + + require.NoError(t, vector.AppendFixed(vec, int32(1), false, mp)) + require.NoError(t, vector.AppendFixed(vec, int32(2), false, mp)) + require.NoError(t, vector.AppendFixed(vec, int32(0), true, mp)) + + minVal, maxVal := int32(2), int32(4) + zm := NewZM(types.T_int32, 0) + UpdateZM(zm, types.EncodeInt32(&minVal)) + UpdateZM(zm, types.EncodeInt32(&maxVal)) + + require.True(t, zm.AnyIn(vec)) + lower, upper := zm.SubVecIn(vec) + require.Equal(t, 0, lower) + require.Equal(t, vec.Length(), upper) +} + +func TestZoneMapAnyInAllNulls(t *testing.T) { + mp := mpool.MustNewZero() + vec := vector.NewVec(types.T_varchar.ToType()) + defer vec.Free(mp) + + require.NoError(t, vector.AppendBytes(vec, nil, true, mp)) + + zm := NewZM(types.T_varchar, 0) + UpdateZM(zm, []byte("key")) + + require.False(t, zm.AnyIn(vec)) +} + func TestZMArray(t *testing.T) { zm := NewZM(types.T_array_float32, 0) zm.Update(types.ArrayToBytes[float32]([]float32{1, 1, 1})) From 57b5d9d7f1b062cb60efab158b40fac71a81180a Mon Sep 17 00:00:00 2001 From: GreatRiver Date: Mon, 6 Jul 2026 16:50:43 +0800 Subject: [PATCH 2/2] add coverage for nullable in list paths --- pkg/sql/plan/function/operator_in_test.go | 85 +++++++++++++++++++++++ pkg/vm/engine/tae/index/zm_test.go | 14 ++++ 2 files changed, 99 insertions(+) diff --git a/pkg/sql/plan/function/operator_in_test.go b/pkg/sql/plan/function/operator_in_test.go index 6f90b400541a9..7f33e692d0a90 100644 --- a/pkg/sql/plan/function/operator_in_test.go +++ b/pkg/sql/plan/function/operator_in_test.go @@ -55,3 +55,88 @@ func TestOperatorNotInNullableListUsesThreeValuedLogic(t *testing.T) { ok, errInfo := tc.Run() require.True(t, ok, errInfo) } + +func TestOperatorFixedInConstListUsesThreeValuedLogic(t *testing.T) { + proc := testutil.NewProcess(t) + + tc := NewFunctionTestCase( + proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_int32.ToType(), []int32{1, 2, 0}, []bool{false, false, true}), + NewFunctionTestConstInput(types.T_int32.ToType(), []int32{1}, []bool{false}), + }, + NewFunctionTestResult(types.T_bool.ToType(), false, []bool{true, false, false}, []bool{false, false, true}), + newOpOperatorFixedIn[int32]().operatorIn, + ) + + ok, errInfo := tc.Run() + require.True(t, ok, errInfo) +} + +func TestOperatorFixedInConstNullListUsesThreeValuedLogic(t *testing.T) { + proc := testutil.NewProcess(t) + + tc := NewFunctionTestCase( + proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_int32.ToType(), []int32{1, 2}, []bool{false, false}), + NewFunctionTestConstInput(types.T_int32.ToType(), []int32{0}, []bool{true}), + }, + NewFunctionTestResult(types.T_bool.ToType(), false, []bool{false, false}, []bool{true, true}), + newOpOperatorFixedIn[int32]().operatorIn, + ) + + ok, errInfo := tc.Run() + require.True(t, ok, errInfo) +} + +func TestOperatorFixedNotInNullableListUsesThreeValuedLogic(t *testing.T) { + proc := testutil.NewProcess(t) + + tc := NewFunctionTestCase( + proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_int32.ToType(), []int32{1, 2, 0}, []bool{false, false, true}), + NewFunctionTestInput(types.T_int32.ToType(), []int32{1, 0}, []bool{false, true}), + }, + NewFunctionTestResult(types.T_bool.ToType(), false, []bool{false, false, false}, []bool{false, true, true}), + newOpOperatorFixedIn[int32]().operatorNotIn, + ) + + ok, errInfo := tc.Run() + require.True(t, ok, errInfo) +} + +func TestOperatorStrInConstListUsesThreeValuedLogic(t *testing.T) { + proc := testutil.NewProcess(t) + + tc := NewFunctionTestCase( + proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{"keep", "key", ""}, []bool{false, false, true}), + NewFunctionTestConstInput(types.T_varchar.ToType(), []string{"keep"}, []bool{false}), + }, + NewFunctionTestResult(types.T_bool.ToType(), false, []bool{true, false, false}, []bool{false, false, true}), + newOpOperatorStrIn().operatorIn, + ) + + ok, errInfo := tc.Run() + require.True(t, ok, errInfo) +} + +func TestOperatorStrInConstNullListUsesThreeValuedLogic(t *testing.T) { + proc := testutil.NewProcess(t) + + tc := NewFunctionTestCase( + proc, + []FunctionTestInput{ + NewFunctionTestInput(types.T_varchar.ToType(), []string{"keep", "key"}, []bool{false, false}), + NewFunctionTestConstInput(types.T_varchar.ToType(), []string{""}, []bool{true}), + }, + NewFunctionTestResult(types.T_bool.ToType(), false, []bool{false, false}, []bool{true, true}), + newOpOperatorStrIn().operatorIn, + ) + + ok, errInfo := tc.Run() + require.True(t, ok, errInfo) +} diff --git a/pkg/vm/engine/tae/index/zm_test.go b/pkg/vm/engine/tae/index/zm_test.go index 093d26778d08a..e71d6bff52bb3 100644 --- a/pkg/vm/engine/tae/index/zm_test.go +++ b/pkg/vm/engine/tae/index/zm_test.go @@ -372,6 +372,20 @@ func TestZoneMapAnyInAllNulls(t *testing.T) { require.False(t, zm.AnyIn(vec)) } +func TestZoneMapAnyInConstNull(t *testing.T) { + mp := mpool.MustNewZero() + vec := vector.NewConstNull(types.T_varchar.ToType(), 1, mp) + defer vec.Free(mp) + + zm := NewZM(types.T_varchar, 0) + UpdateZM(zm, []byte("key")) + + require.False(t, zm.AnyIn(vec)) + lower, upper := zm.SubVecIn(vec) + require.Equal(t, 0, lower) + require.Equal(t, 0, upper) +} + func TestZMArray(t *testing.T) { zm := NewZM(types.T_array_float32, 0) zm.Update(types.ArrayToBytes[float32]([]float32{1, 1, 1}))