enhance: implement 6 expression rewriter optimizations for issue #47992

1. Contradiction detection (equal vs equal):
   (a == 1) AND (a == 2) → false

2. Contradiction detection (range vs equal):
   (a > 10) AND (a == 5) → false
   (a > 10) AND (a == 15) → a == 15

3. Tautology detection (complementary ranges, NULL-aware):
   (a > 10) OR (a <= 10) → true (non-nullable columns only)

4. Absorption law:
   P OR (P AND Q) → P
   P AND (P OR Q) → P

5. OR multi-interval merge (N intervals):
   (10<x<20) OR (15<x<25) OR (22<x<30) → (10<x<30)

6. OR bounded+unbounded interval merge:
   (x > 10) OR (5 < x < 15) → (x > 5)
   (x < 20) OR (10 < x < 30) → (x < 30)

Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
This commit is contained in:
Buqian Zheng
2026-04-23 06:15:37 +00:00
parent e91e8825f7
commit b721d5bb89
9 changed files with 1579 additions and 247 deletions
@@ -0,0 +1,215 @@
package rewriter
import (
"reflect"
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
)
// combineOrAbsorption implements the absorption law for OR:
// P OR (P AND Q) → P
// If any OR-level part is structurally identical to any AND-child of another
// OR-level part, the compound part (P AND Q) is absorbed.
func (v *visitor) combineOrAbsorption(parts []*planpb.Expr) []*planpb.Expr {
if len(parts) < 2 {
return parts
}
absorbed := make([]bool, len(parts))
for i := 0; i < len(parts); i++ {
if absorbed[i] {
continue
}
for j := 0; j < len(parts); j++ {
if i == j || absorbed[j] {
continue
}
// Check if parts[i] is a sub-expression of parts[j]'s AND tree.
// If parts[j] = (A AND B AND ...) and parts[i] equals any child,
// then parts[j] is absorbed by parts[i].
if containsInAnd(parts[j], parts[i]) {
absorbed[j] = true
}
}
}
out := make([]*planpb.Expr, 0, len(parts))
for i, e := range parts {
if !absorbed[i] {
out = append(out, e)
}
}
if len(out) == len(parts) {
return parts
}
return out
}
// combineAndAbsorption implements the absorption law for AND:
// P AND (P OR Q) → P
// If any AND-level part is structurally identical to any OR-child of another
// AND-level part, the compound part (P OR Q) is absorbed.
func (v *visitor) combineAndAbsorption(parts []*planpb.Expr) []*planpb.Expr {
if len(parts) < 2 {
return parts
}
absorbed := make([]bool, len(parts))
for i := 0; i < len(parts); i++ {
if absorbed[i] {
continue
}
for j := 0; j < len(parts); j++ {
if i == j || absorbed[j] {
continue
}
// Check if parts[i] is a sub-expression of parts[j]'s OR tree.
if containsInOr(parts[j], parts[i]) {
absorbed[j] = true
}
}
}
out := make([]*planpb.Expr, 0, len(parts))
for i, e := range parts {
if !absorbed[i] {
out = append(out, e)
}
}
if len(out) == len(parts) {
return parts
}
return out
}
// containsInAnd returns true if expr is an AND tree and needle matches
// one of its leaf children structurally.
func containsInAnd(expr, needle *planpb.Expr) bool {
be := expr.GetBinaryExpr()
if be == nil || be.GetOp() != planpb.BinaryExpr_LogicalAnd {
return false
}
// Collect AND children
children := make([]*planpb.Expr, 0, 4)
collectAnd(expr, &children)
for _, child := range children {
if exprStructEqual(child, needle) {
return true
}
}
return false
}
// containsInOr returns true if expr is an OR tree and needle matches
// one of its leaf children structurally.
func containsInOr(expr, needle *planpb.Expr) bool {
be := expr.GetBinaryExpr()
if be == nil || be.GetOp() != planpb.BinaryExpr_LogicalOr {
return false
}
children := make([]*planpb.Expr, 0, 4)
collectOr(expr, &children)
for _, child := range children {
if exprStructEqual(child, needle) {
return true
}
}
return false
}
// exprStructEqual compares two expressions for structural equality.
// Uses protobuf reflection for a deep comparison.
func exprStructEqual(a, b *planpb.Expr) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
// Fast path: check type first
aType := reflect.TypeOf(a.GetExpr())
bType := reflect.TypeOf(b.GetExpr())
if aType != bType {
return false
}
switch aExpr := a.GetExpr().(type) {
case *planpb.Expr_UnaryRangeExpr:
bExpr := b.GetUnaryRangeExpr()
if bExpr == nil {
return false
}
aa, bb := aExpr.UnaryRangeExpr, bExpr
return aa.GetOp() == bb.GetOp() &&
columnKey(aa.GetColumnInfo()) == columnKey(bb.GetColumnInfo()) &&
equalsGenericSafe(aa.GetValue(), bb.GetValue())
case *planpb.Expr_BinaryRangeExpr:
bExpr := b.GetBinaryRangeExpr()
if bExpr == nil {
return false
}
aa, bb := aExpr.BinaryRangeExpr, bExpr
return columnKey(aa.GetColumnInfo()) == columnKey(bb.GetColumnInfo()) &&
aa.GetLowerInclusive() == bb.GetLowerInclusive() &&
aa.GetUpperInclusive() == bb.GetUpperInclusive() &&
equalsGenericSafe(aa.GetLowerValue(), bb.GetLowerValue()) &&
equalsGenericSafe(aa.GetUpperValue(), bb.GetUpperValue())
case *planpb.Expr_TermExpr:
bExpr := b.GetTermExpr()
if bExpr == nil {
return false
}
aa, bb := aExpr.TermExpr, bExpr
if columnKey(aa.GetColumnInfo()) != columnKey(bb.GetColumnInfo()) {
return false
}
aVals, bVals := aa.GetValues(), bb.GetValues()
if len(aVals) != len(bVals) {
return false
}
for i := range aVals {
if !equalsGeneric(aVals[i], bVals[i]) {
return false
}
}
return true
case *planpb.Expr_BinaryExpr:
bExpr := b.GetBinaryExpr()
if bExpr == nil {
return false
}
return aExpr.BinaryExpr.GetOp() == bExpr.GetOp() &&
exprStructEqual(aExpr.BinaryExpr.GetLeft(), bExpr.GetLeft()) &&
exprStructEqual(aExpr.BinaryExpr.GetRight(), bExpr.GetRight())
case *planpb.Expr_UnaryExpr:
bExpr := b.GetUnaryExpr()
if bExpr == nil {
return false
}
return aExpr.UnaryExpr.GetOp() == bExpr.GetOp() &&
exprStructEqual(aExpr.UnaryExpr.GetChild(), bExpr.GetChild())
case *planpb.Expr_AlwaysTrueExpr:
return b.GetAlwaysTrueExpr() != nil
default:
return false
}
}
// equalsGenericSafe handles nil values.
func equalsGenericSafe(a, b *planpb.GenericValue) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return equalsGeneric(a, b)
}
@@ -0,0 +1,253 @@
package rewriter
import (
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
)
// combineAndEqualWithEqual detects contradictory equalities on the same column.
// (a == 1) AND (a == 2) → false
// (a == 1) AND (a == 1) → a == 1
func (v *visitor) combineAndEqualWithEqual(parts []*planpb.Expr) []*planpb.Expr {
type group struct {
col *planpb.ColumnInfo
values []*planpb.GenericValue
origIndices []int
}
groups := map[string]*group{}
others := []int{}
for idx, e := range parts {
u := e.GetUnaryRangeExpr()
if u == nil || u.GetOp() != planpb.OpType_Equal || u.GetValue() == nil {
others = append(others, idx)
continue
}
col := u.GetColumnInfo()
if col == nil {
others = append(others, idx)
continue
}
key := columnKey(col)
g, ok := groups[key]
if !ok {
g = &group{col: col}
groups[key] = g
}
g.values = append(g.values, u.GetValue())
g.origIndices = append(g.origIndices, idx)
}
used := make([]bool, len(parts))
out := make([]*planpb.Expr, 0, len(parts))
for _, idx := range others {
out = append(out, parts[idx])
used[idx] = true
}
for _, g := range groups {
if len(g.origIndices) <= 1 {
continue
}
// Check if all values are the same
first := g.values[0]
allSame := true
for i := 1; i < len(g.values); i++ {
if !equalsGeneric(first, g.values[i]) {
allSame = false
break
}
}
for _, idx := range g.origIndices {
used[idx] = true
}
if allSame {
out = append(out, newUnaryRangeExpr(g.col, planpb.OpType_Equal, first))
} else {
out = append(out, newAlwaysFalseExpr())
}
}
for i := range parts {
if !used[i] {
out = append(out, parts[i])
}
}
return out
}
// combineAndRangeWithEqual checks equality values against range bounds.
// (a > 10) AND (a == 5) → false (5 not > 10)
// (a > 10) AND (a == 15) → a == 15
// Also handles BinaryRangeExpr: (10 < a < 50) AND (a == 30) → a == 30
func (v *visitor) combineAndRangeWithEqual(parts []*planpb.Expr) []*planpb.Expr {
type rangeIndex struct {
lower *planpb.GenericValue
lowerInc bool
upper *planpb.GenericValue
upperInc bool
indices []int
}
type eqEntry struct {
value *planpb.GenericValue
index int
}
type group struct {
col *planpb.ColumnInfo
ranges []rangeIndex
eqs []eqEntry
}
groups := map[string]*group{}
others := []int{}
for idx, e := range parts {
if u := e.GetUnaryRangeExpr(); u != nil && u.GetColumnInfo() != nil && u.GetValue() != nil {
col := u.GetColumnInfo()
key := columnKey(col)
op := u.GetOp()
if op == planpb.OpType_Equal {
g := groups[key]
if g == nil {
g = &group{col: col}
groups[key] = g
}
g.eqs = append(g.eqs, eqEntry{value: u.GetValue(), index: idx})
continue
}
if op == planpb.OpType_GreaterThan || op == planpb.OpType_GreaterEqual ||
op == planpb.OpType_LessThan || op == planpb.OpType_LessEqual {
g := groups[key]
if g == nil {
g = &group{col: col}
groups[key] = g
}
ri := rangeIndex{indices: []int{idx}}
isLower := op == planpb.OpType_GreaterThan || op == planpb.OpType_GreaterEqual
inc := op == planpb.OpType_GreaterEqual || op == planpb.OpType_LessEqual
if isLower {
ri.lower = u.GetValue()
ri.lowerInc = inc
} else {
ri.upper = u.GetValue()
ri.upperInc = inc
}
g.ranges = append(g.ranges, ri)
continue
}
}
if br := e.GetBinaryRangeExpr(); br != nil && br.GetColumnInfo() != nil {
col := br.GetColumnInfo()
key := columnKey(col)
g := groups[key]
if g == nil {
g = &group{col: col}
groups[key] = g
}
g.ranges = append(g.ranges, rangeIndex{
lower: br.GetLowerValue(),
lowerInc: br.GetLowerInclusive(),
upper: br.GetUpperValue(),
upperInc: br.GetUpperInclusive(),
indices: []int{idx},
})
continue
}
others = append(others, idx)
}
used := make([]bool, len(parts))
out := make([]*planpb.Expr, 0, len(parts))
for _, idx := range others {
out = append(out, parts[idx])
used[idx] = true
}
for _, g := range groups {
if len(g.eqs) == 0 || len(g.ranges) == 0 {
continue
}
dt := effectiveDataType(g.col)
if !isSupportedScalarForRange(dt) {
continue
}
// Collect tightest bounds from all ranges
var bestLower *planpb.GenericValue
var bestLowerInc bool
var bestUpper *planpb.GenericValue
var bestUpperInc bool
for _, ri := range g.ranges {
if ri.lower != nil {
if bestLower == nil {
bestLower = ri.lower
bestLowerInc = ri.lowerInc
} else {
c := cmpGeneric(dt, ri.lower, bestLower)
if c > 0 || (c == 0 && !ri.lowerInc && bestLowerInc) {
bestLower = ri.lower
bestLowerInc = ri.lowerInc
}
}
}
if ri.upper != nil {
if bestUpper == nil {
bestUpper = ri.upper
bestUpperInc = ri.upperInc
} else {
c := cmpGeneric(dt, ri.upper, bestUpper)
if c < 0 || (c == 0 && !ri.upperInc && bestUpperInc) {
bestUpper = ri.upper
bestUpperInc = ri.upperInc
}
}
}
}
// Check each equality value against the tightest range
contradiction := false
for _, eq := range g.eqs {
if bestLower != nil && !satisfiesLower(dt, eq.value, bestLower, bestLowerInc) {
contradiction = true
break
}
if bestUpper != nil && !satisfiesUpper(dt, eq.value, bestUpper, bestUpperInc) {
contradiction = true
break
}
}
if contradiction {
// Mark everything as used, emit false
for _, eq := range g.eqs {
used[eq.index] = true
}
for _, ri := range g.ranges {
for _, idx := range ri.indices {
used[idx] = true
}
}
out = append(out, newAlwaysFalseExpr())
} else {
// Equality is stricter than range → drop range predicates, keep equalities
for _, ri := range g.ranges {
for _, idx := range ri.indices {
used[idx] = true
}
}
// Equalities are not used, will be appended from the unused loop
}
}
for i := range parts {
if !used[i] {
out = append(out, parts[i])
}
}
return out
}
@@ -52,23 +52,28 @@ func (v *visitor) visitBinaryExpr(expr *planpb.BinaryExpr) interface{} {
if v.optimizeEnabled {
parts = v.combineOrEqualsToIn(parts)
parts = v.combineOrTextMatchToMerged(parts)
parts = v.combineOrComplementaryRanges(parts)
parts = v.combineOrRangePredicates(parts)
parts = v.combineOrBinaryRanges(parts)
parts = v.combineOrInWithNotEqual(parts)
parts = v.combineOrInWithIn(parts)
parts = v.combineOrInWithEqual(parts)
parts = v.combineOrAbsorption(parts)
}
return foldBinary(planpb.BinaryExpr_LogicalOr, parts)
case planpb.BinaryExpr_LogicalAnd:
parts := flattenAnd(left, right)
if v.optimizeEnabled {
parts = v.combineAndEqualWithEqual(parts)
parts = v.combineAndRangePredicates(parts)
parts = v.combineAndBinaryRanges(parts)
parts = v.combineAndRangeWithEqual(parts)
parts = v.combineAndInWithIn(parts)
parts = v.combineAndInWithNotEqual(parts)
parts = v.combineAndInWithRange(parts)
parts = v.combineAndInWithEqual(parts)
parts = v.combineAndNotEqualsToNotIn(parts)
parts = v.combineAndAbsorption(parts)
}
return foldBinary(planpb.BinaryExpr_LogicalAnd, parts)
default:
@@ -0,0 +1,648 @@
package rewriter_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
parser "github.com/milvus-io/milvus/internal/parser/planparserv2"
"github.com/milvus-io/milvus/internal/parser/planparserv2/rewriter"
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
// ============================================================
// 1. Contradiction Detection: Equal vs Equal
// ============================================================
func TestRewrite_Contradiction_EqualEqual_Int64(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
expr, err := parser.ParseExpr(helper, `Int64Field == 1 and Int64Field == 2`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_Contradiction_EqualEqual_String(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
expr, err := parser.ParseExpr(helper, `VarCharField == "a" and VarCharField == "b"`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_Contradiction_EqualEqual_SameValue_Dedup(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// a == 5 AND a == 5 → a == 5
expr, err := parser.ParseExpr(helper, `Int64Field == 5 and Int64Field == 5`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure)
require.Equal(t, planpb.OpType_Equal, ure.GetOp())
require.Equal(t, int64(5), ure.GetValue().GetInt64Val())
}
func TestRewrite_Contradiction_EqualEqual_ThreeValues(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
expr, err := parser.ParseExpr(helper, `Int64Field == 1 and Int64Field == 2 and Int64Field == 3`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_Contradiction_EqualEqual_DifferentColumns_NoOptimize(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// Different columns → no contradiction
expr, err := parser.ParseExpr(helper, `Int64Field == 1 and VarCharField == "a"`, nil)
require.NoError(t, err)
be := expr.GetBinaryExpr()
require.NotNil(t, be, "different columns should not trigger contradiction")
}
func TestRewrite_Contradiction_EqualEqual_WithOtherConditions(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// a == 1 AND a == 2 AND b > 10 → false (contradiction on a kills entire AND)
expr, err := parser.ParseExpr(helper, `Int64Field == 1 and Int64Field == 2 and FloatField > 10`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
// ============================================================
// 2. Contradiction Detection: Range vs Equal
// ============================================================
func TestRewrite_Contradiction_RangeEqual_LowerBound(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// a > 10 AND a == 5 → false
expr, err := parser.ParseExpr(helper, `Int64Field > 10 and Int64Field == 5`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_Contradiction_RangeEqual_UpperBound(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// a < 10 AND a == 20 → false
expr, err := parser.ParseExpr(helper, `Int64Field < 10 and Int64Field == 20`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_Contradiction_RangeEqual_ExclusiveBoundary(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// a > 10 AND a == 10 → false (exclusive >)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 and Int64Field == 10`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_Contradiction_RangeEqual_InclusiveBoundary(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// a >= 10 AND a == 10 → a == 10
expr, err := parser.ParseExpr(helper, `Int64Field >= 10 and Int64Field == 10`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure)
require.Equal(t, planpb.OpType_Equal, ure.GetOp())
require.Equal(t, int64(10), ure.GetValue().GetInt64Val())
}
func TestRewrite_Contradiction_RangeEqual_InRange(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// a > 10 AND a == 15 → a == 15
expr, err := parser.ParseExpr(helper, `Int64Field > 10 and Int64Field == 15`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure)
require.Equal(t, planpb.OpType_Equal, ure.GetOp())
require.Equal(t, int64(15), ure.GetValue().GetInt64Val())
}
func TestRewrite_Contradiction_BinaryRangeEqual(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (10 < a < 50) AND a == 30 → a == 30
expr, err := parser.ParseExpr(helper, `(Int64Field > 10 and Int64Field < 50) and Int64Field == 30`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure)
require.Equal(t, planpb.OpType_Equal, ure.GetOp())
require.Equal(t, int64(30), ure.GetValue().GetInt64Val())
}
func TestRewrite_Contradiction_BinaryRangeEqual_Outside(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (10 < a < 50) AND a == 5 → false
expr, err := parser.ParseExpr(helper, `(Int64Field > 10 and Int64Field < 50) and Int64Field == 5`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_Contradiction_RangeEqual_Float(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// FloatField > 10.5 AND FloatField == 5.0 → false
expr, err := parser.ParseExpr(helper, `FloatField > 10.5 and FloatField == 5.0`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_Contradiction_RangeEqual_String(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// VarCharField > "m" AND VarCharField == "abc" → false
expr, err := parser.ParseExpr(helper, `VarCharField > "m" and VarCharField == "abc"`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
// ============================================================
// 3. Tautology Detection
// ============================================================
func buildSchemaHelperNonNullableT(t *testing.T) *typeutil.SchemaHelper {
fields := []*schemapb.FieldSchema{
{FieldID: 101, Name: "Int64Field", DataType: schemapb.DataType_Int64, Nullable: false},
{FieldID: 102, Name: "VarCharField", DataType: schemapb.DataType_VarChar, Nullable: false},
{FieldID: 103, Name: "FloatField", DataType: schemapb.DataType_Double, Nullable: false},
}
schema := &schemapb.CollectionSchema{
Name: "rewrite_non_nullable_test",
AutoID: false,
Fields: fields,
}
helper, err := typeutil.CreateSchemaHelper(schema)
require.NoError(t, err)
return helper
}
func buildSchemaHelperNullableT(t *testing.T) *typeutil.SchemaHelper {
fields := []*schemapb.FieldSchema{
{FieldID: 101, Name: "Int64Field", DataType: schemapb.DataType_Int64, Nullable: true},
{FieldID: 102, Name: "VarCharField", DataType: schemapb.DataType_VarChar, Nullable: true},
}
schema := &schemapb.CollectionSchema{
Name: "rewrite_nullable_test",
AutoID: false,
Fields: fields,
}
helper, err := typeutil.CreateSchemaHelper(schema)
require.NoError(t, err)
return helper
}
func TestRewrite_Tautology_GT_LE_NonNullable(t *testing.T) {
helper := buildSchemaHelperNonNullableT(t)
// a > 10 OR a <= 10 → true (non-nullable)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 or Int64Field <= 10`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysTrueExpr(expr))
}
func TestRewrite_Tautology_GE_LT_NonNullable(t *testing.T) {
helper := buildSchemaHelperNonNullableT(t)
// a >= 10 OR a < 10 → true
expr, err := parser.ParseExpr(helper, `Int64Field >= 10 or Int64Field < 10`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysTrueExpr(expr))
}
func TestRewrite_Tautology_String_NonNullable(t *testing.T) {
helper := buildSchemaHelperNonNullableT(t)
// VarCharField >= "m" OR VarCharField < "m" → true
expr, err := parser.ParseExpr(helper, `VarCharField >= "m" or VarCharField < "m"`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysTrueExpr(expr))
}
func TestRewrite_Tautology_Float_NonNullable(t *testing.T) {
helper := buildSchemaHelperNonNullableT(t)
expr, err := parser.ParseExpr(helper, `FloatField > 3.14 or FloatField <= 3.14`, nil)
require.NoError(t, err)
require.True(t, rewriter.IsAlwaysTrueExpr(expr))
}
func TestRewrite_Tautology_Nullable_NoOptimize(t *testing.T) {
helper := buildSchemaHelperNullableT(t)
// a > 10 OR a <= 10 → NOT tautology for nullable (NULL values are excluded)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 or Int64Field <= 10`, nil)
require.NoError(t, err)
require.False(t, rewriter.IsAlwaysTrueExpr(expr), "nullable columns should not produce tautology")
be := expr.GetBinaryExpr()
require.NotNil(t, be, "should remain as OR for nullable column")
}
func TestRewrite_Tautology_NotComplement_NoOptimize(t *testing.T) {
helper := buildSchemaHelperNonNullableT(t)
// a > 10 OR a < 10 → NOT a tautology (missing == 10)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 or Int64Field < 10`, nil)
require.NoError(t, err)
require.False(t, rewriter.IsAlwaysTrueExpr(expr))
}
func TestRewrite_Tautology_DifferentValues_NoOptimize(t *testing.T) {
helper := buildSchemaHelperNonNullableT(t)
// a > 10 OR a <= 20 → NOT a tautology (different values)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 or Int64Field <= 20`, nil)
require.NoError(t, err)
// This isn't a complementary pair (different values), not optimized by tautology detection
// (although it does cover everything, detecting this is more complex)
require.False(t, rewriter.IsAlwaysTrueExpr(expr))
}
// ============================================================
// 4. Absorption Law
// ============================================================
func TestRewrite_Absorption_OR_Simple(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (a > 10) OR ((a > 10) AND (b > 20)) → (a > 10)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 or (Int64Field > 10 and FloatField > 20)`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure, "absorption should simplify to just a > 10")
require.Equal(t, planpb.OpType_GreaterThan, ure.GetOp())
require.Equal(t, int64(10), ure.GetValue().GetInt64Val())
}
func TestRewrite_Absorption_OR_Reversed(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// ((a > 10) AND (b > 20)) OR (a > 10) → (a > 10)
expr, err := parser.ParseExpr(helper, `(Int64Field > 10 and FloatField > 20) or Int64Field > 10`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure, "absorption should simplify to just a > 10")
require.Equal(t, planpb.OpType_GreaterThan, ure.GetOp())
}
func TestRewrite_Absorption_AND_Simple(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (a > 10) AND ((a > 10) OR (b > 20)) → (a > 10)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 and (Int64Field > 10 or FloatField > 20)`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure, "absorption should simplify to just a > 10")
require.Equal(t, planpb.OpType_GreaterThan, ure.GetOp())
}
func TestRewrite_Absorption_NoMatch_NoOptimize(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (a > 10) OR ((a > 20) AND (b > 30)) → no absorption (a > 10 != a > 20)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 or (Int64Field > 20 and FloatField > 30)`, nil)
require.NoError(t, err)
be := expr.GetBinaryExpr()
require.NotNil(t, be, "no absorption should happen")
}
func TestRewrite_Absorption_OR_Equality(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (a == 5) OR ((a == 5) AND (b < 10)) → (a == 5)
expr, err := parser.ParseExpr(helper, `Int64Field == 5 or (Int64Field == 5 and FloatField < 10)`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure)
require.Equal(t, planpb.OpType_Equal, ure.GetOp())
require.Equal(t, int64(5), ure.GetValue().GetInt64Val())
}
// ============================================================
// 5. OR Multi-Interval Merge (3+ intervals)
// ============================================================
func TestRewrite_OR_ThreeOverlapping_Int(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (10 < x < 20) OR (15 < x < 25) OR (22 < x < 30) → (10 < x < 30)
expr, err := parser.ParseExpr(helper, `(Int64Field > 10 and Int64Field < 20) or (Int64Field > 15 and Int64Field < 25) or (Int64Field > 22 and Int64Field < 30)`, nil)
require.NoError(t, err)
bre := expr.GetBinaryRangeExpr()
require.NotNil(t, bre)
require.Equal(t, int64(10), bre.GetLowerValue().GetInt64Val())
require.Equal(t, int64(30), bre.GetUpperValue().GetInt64Val())
}
func TestRewrite_OR_FourAdjacentInclusive(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (10 < x <= 20) OR (20 < x <= 30) OR (30 < x <= 40) OR (40 < x <= 50) → (10 < x <= 50)
expr, err := parser.ParseExpr(helper, `(Int64Field > 10 and Int64Field <= 20) or (Int64Field > 20 and Int64Field <= 30) or (Int64Field > 30 and Int64Field <= 40) or (Int64Field > 40 and Int64Field <= 50)`, nil)
require.NoError(t, err)
bre := expr.GetBinaryRangeExpr()
require.NotNil(t, bre)
require.Equal(t, false, bre.GetLowerInclusive())
require.Equal(t, true, bre.GetUpperInclusive())
require.Equal(t, int64(10), bre.GetLowerValue().GetInt64Val())
require.Equal(t, int64(50), bre.GetUpperValue().GetInt64Val())
}
func TestRewrite_OR_ThreeContained(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (10 < x < 100) OR (20 < x < 50) OR (30 < x < 40) → (10 < x < 100)
expr, err := parser.ParseExpr(helper, `(Int64Field > 10 and Int64Field < 100) or (Int64Field > 20 and Int64Field < 50) or (Int64Field > 30 and Int64Field < 40)`, nil)
require.NoError(t, err)
bre := expr.GetBinaryRangeExpr()
require.NotNil(t, bre)
require.Equal(t, int64(10), bre.GetLowerValue().GetInt64Val())
require.Equal(t, int64(100), bre.GetUpperValue().GetInt64Val())
}
func TestRewrite_OR_ThreeDisjoint_NoMerge(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (1 < x < 5) OR (10 < x < 15) OR (20 < x < 25) → remains as OR
expr, err := parser.ParseExpr(helper, `(Int64Field > 1 and Int64Field < 5) or (Int64Field > 10 and Int64Field < 15) or (Int64Field > 20 and Int64Field < 25)`, nil)
require.NoError(t, err)
// Should not merge to single range (disjoint)
bre := expr.GetBinaryRangeExpr()
require.Nil(t, bre, "disjoint intervals should NOT merge to single range")
}
// ============================================================
// 6. OR Bounded + Unbounded Merge
// ============================================================
func TestRewrite_OR_UnboundedLower_Bounded_Overlapping(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (x > 10) OR (5 < x < 15) → (x > 5)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 or (Int64Field > 5 and Int64Field < 15)`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure)
require.Equal(t, planpb.OpType_GreaterThan, ure.GetOp())
require.Equal(t, int64(5), ure.GetValue().GetInt64Val())
}
func TestRewrite_OR_UnboundedUpper_Bounded_Overlapping(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (x < 20) OR (10 < x < 30) → (x < 30)
expr, err := parser.ParseExpr(helper, `Int64Field < 20 or (Int64Field > 10 and Int64Field < 30)`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure)
require.Equal(t, planpb.OpType_LessThan, ure.GetOp())
require.Equal(t, int64(30), ure.GetValue().GetInt64Val())
}
func TestRewrite_OR_UnboundedLower_Bounded_NotOverlapping(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (x > 100) OR (5 < x < 10) → no merge (disjoint)
expr, err := parser.ParseExpr(helper, `Int64Field > 100 or (Int64Field > 5 and Int64Field < 10)`, nil)
require.NoError(t, err)
be := expr.GetBinaryExpr()
require.NotNil(t, be, "disjoint unbounded + bounded should not merge")
}
func TestRewrite_OR_UnboundedLower_MultipleBounded(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (x > 20) OR (5 < x < 10) OR (8 < x < 25) → (x > 5)
expr, err := parser.ParseExpr(helper, `Int64Field > 20 or (Int64Field > 5 and Int64Field < 10) or (Int64Field > 8 and Int64Field < 25)`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure)
require.Equal(t, planpb.OpType_GreaterThan, ure.GetOp())
require.Equal(t, int64(5), ure.GetValue().GetInt64Val())
}
func TestRewrite_OR_UnboundedUpper_Inclusive(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (x <= 20) OR (15 <= x <= 30) → (x <= 30)
expr, err := parser.ParseExpr(helper, `Int64Field <= 20 or (Int64Field >= 15 and Int64Field <= 30)`, nil)
require.NoError(t, err)
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure)
require.Equal(t, planpb.OpType_LessEqual, ure.GetOp())
require.Equal(t, int64(30), ure.GetValue().GetInt64Val())
}
// ============================================================
// NULL-awareness: Direct protobuf construction tests
// ============================================================
func TestRewrite_Direct_Contradiction_EqualEqual(t *testing.T) {
col := &planpb.ColumnInfo{FieldId: 101, DataType: schemapb.DataType_Int64}
left := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: col,
Op: planpb.OpType_Equal,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 1}},
},
},
}
right := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: col,
Op: planpb.OpType_Equal,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 2}},
},
},
}
input := &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: left, Right: right, Op: planpb.BinaryExpr_LogicalAnd,
},
},
}
result := rewriter.RewriteExprWithConfig(input, true)
require.True(t, rewriter.IsAlwaysFalseExpr(result))
}
func TestRewrite_Direct_Tautology_NonNullable(t *testing.T) {
col := &planpb.ColumnInfo{FieldId: 101, DataType: schemapb.DataType_Int64, Nullable: false}
left := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: col,
Op: planpb.OpType_GreaterThan,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 10}},
},
},
}
right := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: col,
Op: planpb.OpType_LessEqual,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 10}},
},
},
}
input := &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: left, Right: right, Op: planpb.BinaryExpr_LogicalOr,
},
},
}
result := rewriter.RewriteExprWithConfig(input, true)
require.True(t, rewriter.IsAlwaysTrueExpr(result))
}
func TestRewrite_Direct_Tautology_Nullable_NotOptimized(t *testing.T) {
col := &planpb.ColumnInfo{FieldId: 101, DataType: schemapb.DataType_Int64, Nullable: true}
left := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: col,
Op: planpb.OpType_GreaterThan,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 10}},
},
},
}
right := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: col,
Op: planpb.OpType_LessEqual,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 10}},
},
},
}
input := &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: left, Right: right, Op: planpb.BinaryExpr_LogicalOr,
},
},
}
result := rewriter.RewriteExprWithConfig(input, true)
require.False(t, rewriter.IsAlwaysTrueExpr(result), "nullable column should not be tautology")
}
func TestRewrite_Direct_Absorption_OR(t *testing.T) {
colA := &planpb.ColumnInfo{FieldId: 101, DataType: schemapb.DataType_Int64}
colB := &planpb.ColumnInfo{FieldId: 104, DataType: schemapb.DataType_Double}
// P = (a > 10)
p := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: colA,
Op: planpb.OpType_GreaterThan,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 10}},
},
},
}
// Q = (b > 20)
q := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: colB,
Op: planpb.OpType_GreaterThan,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_FloatVal{FloatVal: 20}},
},
},
}
// P AND Q
pAndQ := &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: p, Right: q, Op: planpb.BinaryExpr_LogicalAnd,
},
},
}
// P2 = same as P (a > 10) — must construct separately since pointers differ
p2 := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: colA,
Op: planpb.OpType_GreaterThan,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 10}},
},
},
}
// P2 OR (P AND Q) → P2
input := &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: p2, Right: pAndQ, Op: planpb.BinaryExpr_LogicalOr,
},
},
}
result := rewriter.RewriteExprWithConfig(input, true)
ure := result.GetUnaryRangeExpr()
require.NotNil(t, ure, "absorption should produce single range")
require.Equal(t, planpb.OpType_GreaterThan, ure.GetOp())
require.Equal(t, int64(10), ure.GetValue().GetInt64Val())
}
func TestRewrite_Direct_Absorption_AND(t *testing.T) {
colA := &planpb.ColumnInfo{FieldId: 101, DataType: schemapb.DataType_Int64}
colB := &planpb.ColumnInfo{FieldId: 104, DataType: schemapb.DataType_Double}
// P = (a > 10), Q = (b > 20)
p := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: colA,
Op: planpb.OpType_GreaterThan,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 10}},
},
},
}
q := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: colB,
Op: planpb.OpType_GreaterThan,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_FloatVal{FloatVal: 20}},
},
},
}
// P OR Q
pOrQ := &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: p, Right: q, Op: planpb.BinaryExpr_LogicalOr,
},
},
}
p2 := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: colA,
Op: planpb.OpType_GreaterThan,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 10}},
},
},
}
// P2 AND (P OR Q) → P2
input := &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: p2, Right: pOrQ, Op: planpb.BinaryExpr_LogicalAnd,
},
},
}
result := rewriter.RewriteExprWithConfig(input, true)
ure := result.GetUnaryRangeExpr()
require.NotNil(t, ure, "AND absorption should produce single range")
require.Equal(t, planpb.OpType_GreaterThan, ure.GetOp())
require.Equal(t, int64(10), ure.GetValue().GetInt64Val())
}
// ============================================================
// Optimization disabled: verify no optimization happens
// ============================================================
func TestRewrite_OptimizationDisabled_NoContradiction(t *testing.T) {
col := &planpb.ColumnInfo{FieldId: 101, DataType: schemapb.DataType_Int64}
left := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: col,
Op: planpb.OpType_Equal,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 1}},
},
},
}
right := &planpb.Expr{
Expr: &planpb.Expr_UnaryRangeExpr{
UnaryRangeExpr: &planpb.UnaryRangeExpr{
ColumnInfo: col,
Op: planpb.OpType_Equal,
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 2}},
},
},
}
input := &planpb.Expr{
Expr: &planpb.Expr_BinaryExpr{
BinaryExpr: &planpb.BinaryExpr{
Left: left, Right: right, Op: planpb.BinaryExpr_LogicalAnd,
},
},
}
result := rewriter.RewriteExprWithConfig(input, false)
require.False(t, rewriter.IsAlwaysFalseExpr(result), "optimization disabled should not detect contradiction")
}
+291 -171
View File
@@ -2,6 +2,7 @@ package rewriter
import (
"fmt"
"sort"
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
@@ -517,21 +518,19 @@ func (v *visitor) combineAndBinaryRanges(parts []*planpb.Expr) []*planpb.Expr {
inc := op == planpb.OpType_GreaterEqual || op == planpb.OpType_LessEqual
if isLower {
g.intervals = append(g.intervals, interval{
lower: ure.GetValue(),
lowerInc: inc,
upper: nil,
upperInc: false,
exprIndex: idx,
isBinaryRange: false,
lower: ure.GetValue(),
lowerInc: inc,
upper: nil,
upperInc: false,
exprIndex: idx,
})
} else {
g.intervals = append(g.intervals, interval{
lower: nil,
lowerInc: false,
upper: ure.GetValue(),
upperInc: inc,
exprIndex: idx,
isBinaryRange: false,
lower: nil,
lowerInc: false,
upper: ure.GetValue(),
upperInc: inc,
exprIndex: idx,
})
}
continue
@@ -646,21 +645,23 @@ func (v *visitor) combineAndBinaryRanges(parts []*planpb.Expr) []*planpb.Expr {
return out
}
// orInterval represents an interval in OR merging with original expression index.
type orInterval struct {
lower *planpb.GenericValue
lowerInc bool
upper *planpb.GenericValue
upperInc bool
exprIndex int
}
// combineOrBinaryRanges merges BinaryRangeExpr nodes with OR semantics (union if overlapping/adjacent).
// Also handles mixing BinaryRangeExpr with UnaryRangeExpr.
// Supports N intervals (sort-and-sweep) and bounded+unbounded merging.
func (v *visitor) combineOrBinaryRanges(parts []*planpb.Expr) []*planpb.Expr {
type interval struct {
lower *planpb.GenericValue
lowerInc bool
upper *planpb.GenericValue
upperInc bool
exprIndex int
isBinaryRange bool
}
type group struct {
col *planpb.ColumnInfo
effDt schemapb.DataType
intervals []interval
intervals []orInterval
}
groups := map[string]*group{}
others := []int{}
@@ -692,13 +693,12 @@ func (v *visitor) combineOrBinaryRanges(parts []*planpb.Expr) []*planpb.Expr {
g = &group{col: col, effDt: effDt}
groups[key] = g
}
g.intervals = append(g.intervals, interval{
lower: bre.GetLowerValue(),
lowerInc: bre.GetLowerInclusive(),
upper: bre.GetUpperValue(),
upperInc: bre.GetUpperInclusive(),
exprIndex: idx,
isBinaryRange: true,
g.intervals = append(g.intervals, orInterval{
lower: bre.GetLowerValue(),
lowerInc: bre.GetLowerInclusive(),
upper: bre.GetUpperValue(),
upperInc: bre.GetUpperInclusive(),
exprIndex: idx,
})
continue
}
@@ -735,22 +735,20 @@ func (v *visitor) combineOrBinaryRanges(parts []*planpb.Expr) []*planpb.Expr {
isLower := op == planpb.OpType_GreaterThan || op == planpb.OpType_GreaterEqual
inc := op == planpb.OpType_GreaterEqual || op == planpb.OpType_LessEqual
if isLower {
g.intervals = append(g.intervals, interval{
lower: ure.GetValue(),
lowerInc: inc,
upper: nil,
upperInc: false,
exprIndex: idx,
isBinaryRange: false,
g.intervals = append(g.intervals, orInterval{
lower: ure.GetValue(),
lowerInc: inc,
upper: nil,
upperInc: false,
exprIndex: idx,
})
} else {
g.intervals = append(g.intervals, interval{
lower: nil,
lowerInc: false,
upper: ure.GetValue(),
upperInc: inc,
exprIndex: idx,
isBinaryRange: false,
g.intervals = append(g.intervals, orInterval{
lower: nil,
lowerInc: false,
upper: ure.GetValue(),
upperInc: inc,
exprIndex: idx,
})
}
continue
@@ -768,148 +766,42 @@ func (v *visitor) combineOrBinaryRanges(parts []*planpb.Expr) []*planpb.Expr {
}
for _, g := range groups {
if len(g.intervals) == 0 {
continue
}
if len(g.intervals) == 1 {
// Single interval, keep as is
if len(g.intervals) <= 1 {
continue
}
// For OR, try to merge overlapping/adjacent intervals
// If any interval is unbounded on one side, check if it subsumes others
// For simplicity, we'll handle the common cases:
// 1. All bounded intervals: try to merge if overlapping/adjacent
// 2. Mix of bounded/unbounded: merge unbounded with compatible bounds
merged := mergeOrIntervals(g.effDt, g.intervals)
if merged == nil {
continue
}
var hasUnboundedLower, hasUnboundedUpper bool
var unboundedLowerVal *planpb.GenericValue
var unboundedLowerInc bool
var unboundedUpperVal *planpb.GenericValue
var unboundedUpperInc bool
// Check for unbounded intervals
// Mark all original intervals as used
for _, iv := range g.intervals {
if iv.lower != nil && iv.upper == nil {
// Lower bound only (x > a)
if !hasUnboundedLower {
hasUnboundedLower = true
unboundedLowerVal = iv.lower
unboundedLowerInc = iv.lowerInc
} else {
// Multiple lower-only bounds: take weakest (minimum)
c := cmpGeneric(g.effDt, iv.lower, unboundedLowerVal)
if c < 0 || (c == 0 && iv.lowerInc && !unboundedLowerInc) {
unboundedLowerVal = iv.lower
unboundedLowerInc = iv.lowerInc
}
}
}
if iv.lower == nil && iv.upper != nil {
// Upper bound only (x < b)
if !hasUnboundedUpper {
hasUnboundedUpper = true
unboundedUpperVal = iv.upper
unboundedUpperInc = iv.upperInc
} else {
// Multiple upper-only bounds: take weakest (maximum)
c := cmpGeneric(g.effDt, iv.upper, unboundedUpperVal)
if c > 0 || (c == 0 && iv.upperInc && !unboundedUpperInc) {
unboundedUpperVal = iv.upper
unboundedUpperInc = iv.upperInc
}
}
}
used[iv.exprIndex] = true
}
// Case 1: Have both unbounded lower and upper → entire domain (always true, but we can't express that simply)
// For now, keep them separate
// Case 2: Have one unbounded side → merge with compatible bounded intervals
// Case 3: All bounded → try to merge overlapping/adjacent
if hasUnboundedLower && hasUnboundedUpper {
// Both unbounded sides - this likely covers most values
// Keep as separate predicates for now (more advanced merging could be done)
continue
}
if hasUnboundedLower || hasUnboundedUpper {
// Merge unbounded with bounded intervals where applicable
// For unbounded lower (x > a): can merge with binary ranges that have compatible upper bounds
// For unbounded upper (x < b): can merge with binary ranges that have compatible lower bounds
// This is complex, so for now we'll keep it simple and just skip merging
// In practice, unbounded intervals often dominate
continue
}
// All bounded intervals: try to merge overlapping/adjacent ones
// This requires sorting and checking overlap
// For simplicity in this initial implementation, we'll check if there are exactly 2 intervals
// and try to merge them if they overlap or are adjacent
if len(g.intervals) == 2 {
iv1, iv2 := g.intervals[0], g.intervals[1]
if iv1.lower == nil || iv1.upper == nil || iv2.lower == nil || iv2.upper == nil {
// One is not fully bounded, skip
// Emit merged intervals
for _, m := range merged {
if m.lower == nil && m.upper == nil {
// Unbounded on both sides shouldn't happen from merge, skip
continue
}
// Check if they overlap or are adjacent
// They overlap if: iv1.lower <= iv2.upper AND iv2.lower <= iv1.upper
// They are adjacent if: iv1.upper == iv2.lower (or vice versa) with at least one inclusive
// Determine order: which has smaller lower bound
var first, second interval
c := cmpGeneric(g.effDt, iv1.lower, iv2.lower)
if c <= 0 {
first, second = iv1, iv2
if m.lower != nil && m.upper != nil {
out = append(out, newBinaryRangeExpr(g.col, m.lowerInc, m.upperInc, m.lower, m.upper))
} else if m.lower != nil {
op := planpb.OpType_GreaterThan
if m.lowerInc {
op = planpb.OpType_GreaterEqual
}
out = append(out, newUnaryRangeExpr(g.col, op, m.lower))
} else {
first, second = iv2, iv1
}
// Check if they can be merged
// Overlap: first.upper >= second.lower
cmpUpperLower := cmpGeneric(g.effDt, first.upper, second.lower)
canMerge := false
if cmpUpperLower > 0 {
// Overlap
canMerge = true
} else if cmpUpperLower == 0 {
// Adjacent: at least one bound must be inclusive
if first.upperInc || second.lowerInc {
canMerge = true
op := planpb.OpType_LessThan
if m.upperInc {
op = planpb.OpType_LessEqual
}
}
if canMerge {
// Merge: take min lower and max upper
mergedLower := first.lower
mergedLowerInc := first.lowerInc
mergedUpper := second.upper
mergedUpperInc := second.upperInc
// Upper bound: take maximum
cmpUppers := cmpGeneric(g.effDt, first.upper, second.upper)
if cmpUppers > 0 {
mergedUpper = first.upper
mergedUpperInc = first.upperInc
} else if cmpUppers == 0 {
// Same value: prefer inclusive
if first.upperInc {
mergedUpperInc = true
}
}
// Mark both as used
used[first.exprIndex] = true
used[second.exprIndex] = true
// Emit merged interval
out = append(out, newBinaryRangeExpr(g.col, mergedLowerInc, mergedUpperInc, mergedLower, mergedUpper))
out = append(out, newUnaryRangeExpr(g.col, op, m.upper))
}
}
// For more than 2 intervals, we'd need more sophisticated merging logic
// For now, we'll leave them separate
}
// Add unused parts
@@ -921,3 +813,231 @@ func (v *visitor) combineOrBinaryRanges(parts []*planpb.Expr) []*planpb.Expr {
return out
}
// mergedInterval is a result of OR interval merging.
type mergedInterval struct {
lower *planpb.GenericValue
lowerInc bool
upper *planpb.GenericValue
upperInc bool
}
// mergeOrIntervals performs a sort-and-sweep merge of OR intervals.
// Returns nil if no merging happened (callers should keep originals).
func mergeOrIntervals(dt schemapb.DataType, intervals []orInterval) []mergedInterval {
if len(intervals) < 2 {
return nil
}
// Separate into bounded, lower-only (x > a), and upper-only (x < b)
type bounded struct {
lower *planpb.GenericValue
lowerInc bool
upper *planpb.GenericValue
upperInc bool
}
var boundedList []bounded
var lowerOnlyList []bounded // lower set, upper nil
var upperOnlyList []bounded // lower nil, upper set
for _, iv := range intervals {
b := bounded{
lower: iv.lower, lowerInc: iv.lowerInc,
upper: iv.upper, upperInc: iv.upperInc,
}
switch {
case iv.lower != nil && iv.upper != nil:
boundedList = append(boundedList, b)
case iv.lower != nil && iv.upper == nil:
lowerOnlyList = append(lowerOnlyList, b)
case iv.lower == nil && iv.upper != nil:
upperOnlyList = append(upperOnlyList, b)
}
}
// Merge multiple lower-only into weakest: x > min(a1, a2, ...)
var mergedLowerOnly *bounded
if len(lowerOnlyList) > 0 {
best := lowerOnlyList[0]
for i := 1; i < len(lowerOnlyList); i++ {
c := cmpGeneric(dt, lowerOnlyList[i].lower, best.lower)
if c < 0 || (c == 0 && lowerOnlyList[i].lowerInc && !best.lowerInc) {
best = lowerOnlyList[i]
}
}
mergedLowerOnly = &best
}
// Merge multiple upper-only into weakest: x < max(b1, b2, ...)
var mergedUpperOnly *bounded
if len(upperOnlyList) > 0 {
best := upperOnlyList[0]
for i := 1; i < len(upperOnlyList); i++ {
c := cmpGeneric(dt, upperOnlyList[i].upper, best.upper)
if c > 0 || (c == 0 && upperOnlyList[i].upperInc && !best.upperInc) {
best = upperOnlyList[i]
}
}
mergedUpperOnly = &best
}
// Try to extend unbounded intervals by absorbing overlapping bounded intervals.
// (x > a) OR (b < x < c) where b <= a → x > min(a, b) (if overlapping/adjacent)
// (x < b) OR (a < x < c) where c >= b → x < max(b, c) (if overlapping/adjacent)
if mergedLowerOnly != nil {
// Absorb bounded intervals that overlap with the lower-only interval
remaining := make([]bounded, 0, len(boundedList))
for _, bnd := range boundedList {
// The lower-only covers [a, +∞). The bounded covers (bnd.lower, bnd.upper).
// They overlap/adjacent if bnd.upper >= a (considering inclusivity)
cmpBndUpperVsLower := cmpGeneric(dt, bnd.upper, mergedLowerOnly.lower)
overlaps := cmpBndUpperVsLower > 0 ||
(cmpBndUpperVsLower == 0 && (bnd.upperInc || mergedLowerOnly.lowerInc))
if overlaps {
// Extend: lower bound = min(a, bnd.lower)
c := cmpGeneric(dt, bnd.lower, mergedLowerOnly.lower)
if c < 0 || (c == 0 && bnd.lowerInc && !mergedLowerOnly.lowerInc) {
mergedLowerOnly.lower = bnd.lower
mergedLowerOnly.lowerInc = bnd.lowerInc
}
} else {
remaining = append(remaining, bnd)
}
}
boundedList = remaining
}
if mergedUpperOnly != nil {
remaining := make([]bounded, 0, len(boundedList))
for _, bnd := range boundedList {
// Upper-only covers (-∞, b). Bounded covers (bnd.lower, bnd.upper).
// They overlap/adjacent if bnd.lower <= b (considering inclusivity)
cmpBndLowerVsUpper := cmpGeneric(dt, bnd.lower, mergedUpperOnly.upper)
overlaps := cmpBndLowerVsUpper < 0 ||
(cmpBndLowerVsUpper == 0 && (bnd.lowerInc || mergedUpperOnly.upperInc))
if overlaps {
// Extend: upper bound = max(b, bnd.upper)
c := cmpGeneric(dt, bnd.upper, mergedUpperOnly.upper)
if c > 0 || (c == 0 && bnd.upperInc && !mergedUpperOnly.upperInc) {
mergedUpperOnly.upper = bnd.upper
mergedUpperOnly.upperInc = bnd.upperInc
}
} else {
remaining = append(remaining, bnd)
}
}
boundedList = remaining
}
// Note: if lower-only and upper-only together overlap, they form a tautology
// for non-nullable columns. We leave that to combineOrComplementaryRanges.
// Sort-and-sweep merge of remaining bounded intervals
if len(boundedList) > 1 {
// Sort by lower bound
sort.Slice(boundedList, func(i, j int) bool {
c := cmpGeneric(dt, boundedList[i].lower, boundedList[j].lower)
if c != 0 {
return c < 0
}
// Equal lower bounds: inclusive first (covers more)
return boundedList[i].lowerInc && !boundedList[j].lowerInc
})
// Sweep merge
merged := []bounded{boundedList[0]}
for i := 1; i < len(boundedList); i++ {
cur := &merged[len(merged)-1]
next := boundedList[i]
// Check overlap or adjacency
cmpCurUpperNextLower := cmpGeneric(dt, cur.upper, next.lower)
canMerge := cmpCurUpperNextLower > 0 ||
(cmpCurUpperNextLower == 0 && (cur.upperInc || next.lowerInc))
if canMerge {
// Extend upper bound to max
cmpUppers := cmpGeneric(dt, next.upper, cur.upper)
if cmpUppers > 0 {
cur.upper = next.upper
cur.upperInc = next.upperInc
} else if cmpUppers == 0 && next.upperInc {
cur.upperInc = true
}
} else {
merged = append(merged, next)
}
}
boundedList = merged
}
// Also try to absorb remaining bounded intervals into unbounded ones (post-bounded-merge)
if mergedLowerOnly != nil && len(boundedList) > 0 {
remaining := make([]bounded, 0, len(boundedList))
for _, bnd := range boundedList {
cmpBndUpperVsLower := cmpGeneric(dt, bnd.upper, mergedLowerOnly.lower)
overlaps := cmpBndUpperVsLower > 0 ||
(cmpBndUpperVsLower == 0 && (bnd.upperInc || mergedLowerOnly.lowerInc))
if overlaps {
c := cmpGeneric(dt, bnd.lower, mergedLowerOnly.lower)
if c < 0 || (c == 0 && bnd.lowerInc && !mergedLowerOnly.lowerInc) {
mergedLowerOnly.lower = bnd.lower
mergedLowerOnly.lowerInc = bnd.lowerInc
}
} else {
remaining = append(remaining, bnd)
}
}
boundedList = remaining
}
if mergedUpperOnly != nil && len(boundedList) > 0 {
remaining := make([]bounded, 0, len(boundedList))
for _, bnd := range boundedList {
cmpBndLowerVsUpper := cmpGeneric(dt, bnd.lower, mergedUpperOnly.upper)
overlaps := cmpBndLowerVsUpper < 0 ||
(cmpBndLowerVsUpper == 0 && (bnd.lowerInc || mergedUpperOnly.upperInc))
if overlaps {
c := cmpGeneric(dt, bnd.upper, mergedUpperOnly.upper)
if c > 0 || (c == 0 && bnd.upperInc && !mergedUpperOnly.upperInc) {
mergedUpperOnly.upper = bnd.upper
mergedUpperOnly.upperInc = bnd.upperInc
}
} else {
remaining = append(remaining, bnd)
}
}
boundedList = remaining
}
// Count total result intervals
totalResults := len(boundedList)
if mergedLowerOnly != nil {
totalResults++
}
if mergedUpperOnly != nil {
totalResults++
}
// Only return merged results if we actually reduced the count
if totalResults >= len(intervals) {
return nil
}
result := make([]mergedInterval, 0, totalResults)
if mergedUpperOnly != nil {
result = append(result, mergedInterval{
upper: mergedUpperOnly.upper, upperInc: mergedUpperOnly.upperInc,
})
}
for _, b := range boundedList {
result = append(result, mergedInterval(b))
}
if mergedLowerOnly != nil {
result = append(result, mergedInterval{
lower: mergedLowerOnly.lower, lowerInc: mergedLowerOnly.lowerInc,
})
}
return result
}
@@ -297,100 +297,81 @@ func TestRewrite_BinaryRange_AND_JSON_DifferentPaths(t *testing.T) {
}
// Test BinaryRangeExpr OR with 3 overlapping intervals
// NOTE: Current implementation limitation - only merges 2 intervals at a time
func TestRewrite_BinaryRange_OR_ThreeOverlapping_CurrentLimitation(t *testing.T) {
func TestRewrite_BinaryRange_OR_ThreeOverlapping(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (10 < x < 20) OR (15 < x < 25) OR (22 < x < 30)
// Ideally should merge to (10 < x < 30)
// Currently: may only partially merge due to limitation
// (10 < x < 20) OR (15 < x < 25) OR (22 < x < 30) → (10 < x < 30)
expr, err := parser.ParseExpr(helper, `(Int64Field > 10 and Int64Field < 20) or (Int64Field > 15 and Int64Field < 25) or (Int64Field > 22 and Int64Field < 30)`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
// Due to current limitation, result may vary depending on tree structure
// This test documents the current behavior rather than ideal behavior
// When enhancement is implemented, this test should be updated to verify (10 < x < 30)
// For now, just verify it doesn't crash and produces valid output
require.NotNil(t, expr)
// Could be BinaryRangeExpr (if some merged) or BinaryExpr OR (if not merged)
// We document that 3+ intervals are NOT fully optimized yet
bre := expr.GetBinaryRangeExpr()
require.NotNil(t, bre, "3 overlapping intervals should merge to single range")
require.Equal(t, int64(10), bre.GetLowerValue().GetInt64Val())
require.Equal(t, int64(30), bre.GetUpperValue().GetInt64Val())
}
// Test BinaryRangeExpr OR with 3 fully overlapping intervals
func TestRewrite_BinaryRange_OR_ThreeFullyOverlapping(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (10 < x < 30) OR (12 < x < 28) OR (15 < x < 25)
// The second and third are fully contained in the first
// Ideally should merge to (10 < x < 30)
// (10 < x < 30) OR (12 < x < 28) OR (15 < x < 25) → (10 < x < 30)
expr, err := parser.ParseExpr(helper, `(Int64Field > 10 and Int64Field < 30) or (Int64Field > 12 and Int64Field < 28) or (Int64Field > 15 and Int64Field < 25)`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
// Current limitation: may not fully optimize
// This test documents that 3+ interval merging is not yet complete
require.NotNil(t, expr)
bre := expr.GetBinaryRangeExpr()
require.NotNil(t, bre, "fully contained intervals should merge")
require.Equal(t, int64(10), bre.GetLowerValue().GetInt64Val())
require.Equal(t, int64(30), bre.GetUpperValue().GetInt64Val())
}
// Test BinaryRangeExpr OR with 4 adjacent intervals
func TestRewrite_BinaryRange_OR_FourAdjacent_CurrentLimitation(t *testing.T) {
func TestRewrite_BinaryRange_OR_FourAdjacent(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (10 < x <= 20) OR (20 < x <= 30) OR (30 < x <= 40) OR (40 < x <= 50)
// Ideally should merge to (10 < x <= 50)
// (10 < x <= 20) OR (20 < x <= 30) OR (30 < x <= 40) OR (40 < x <= 50) → (10 < x <= 50)
expr, err := parser.ParseExpr(helper, `(Int64Field > 10 and Int64Field <= 20) or (Int64Field > 20 and Int64Field <= 30) or (Int64Field > 30 and Int64Field <= 40) or (Int64Field > 40 and Int64Field <= 50)`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
// Current limitation: only pairs of adjacent intervals may merge
// Full chain merging not implemented
require.NotNil(t, expr)
bre := expr.GetBinaryRangeExpr()
require.NotNil(t, bre, "4 adjacent intervals should merge to single range")
require.Equal(t, false, bre.GetLowerInclusive())
require.Equal(t, true, bre.GetUpperInclusive())
require.Equal(t, int64(10), bre.GetLowerValue().GetInt64Val())
require.Equal(t, int64(50), bre.GetUpperValue().GetInt64Val())
}
// Test OR with unbounded lower + bounded interval
// NOTE: Current implementation limitation - unbounded intervals not merged with bounded
func TestRewrite_BinaryRange_OR_UnboundedLower_Bounded_CurrentLimitation(t *testing.T) {
func TestRewrite_BinaryRange_OR_UnboundedLower_Bounded(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (x > 10) OR (5 < x < 15)
// Ideally should merge to (x > 5)
// Currently: both predicates remain separate
// (x > 10) OR (5 < x < 15) → (x > 5)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 or (Int64Field > 5 and Int64Field < 15)`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
// Current limitation: unbounded + bounded intervals not optimized
// Result should be a BinaryExpr OR with both predicates
be := expr.GetBinaryExpr()
require.NotNil(t, be, "should remain as OR (not merged)")
require.Equal(t, planpb.BinaryExpr_LogicalOr, be.GetOp())
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure, "should merge to single unbounded range")
require.Equal(t, planpb.OpType_GreaterThan, ure.GetOp())
require.Equal(t, int64(5), ure.GetValue().GetInt64Val())
}
// Test OR with unbounded upper + bounded interval
func TestRewrite_BinaryRange_OR_UnboundedUpper_Bounded_CurrentLimitation(t *testing.T) {
func TestRewrite_BinaryRange_OR_UnboundedUpper_Bounded(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (x < 20) OR (10 < x < 30)
// Ideally should merge to (x < 30)
// Currently: both predicates remain separate
// (x < 20) OR (10 < x < 30) → (x < 30)
expr, err := parser.ParseExpr(helper, `Int64Field < 20 or (Int64Field > 10 and Int64Field < 30)`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
// Current limitation: unbounded + bounded intervals not optimized
be := expr.GetBinaryExpr()
require.NotNil(t, be, "should remain as OR (not merged)")
require.Equal(t, planpb.BinaryExpr_LogicalOr, be.GetOp())
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure, "should merge to single unbounded range")
require.Equal(t, planpb.OpType_LessThan, ure.GetOp())
require.Equal(t, int64(30), ure.GetValue().GetInt64Val())
}
// Test OR with unbounded lower + unbounded upper
func TestRewrite_BinaryRange_OR_UnboundedBoth_CurrentLimitation(t *testing.T) {
// Test OR with unbounded lower + unbounded upper (overlapping)
func TestRewrite_BinaryRange_OR_UnboundedBoth_Overlapping(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
// (x > 10) OR (x < 20)
// This covers most values (gap only between 10 and 20 if both exclusive)
// Currently: both predicates remain separate
// (x > 10) OR (x < 20) — overlapping range, covers everything
// Keep as separate predicates (tautology detection handles non-nullable case)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 or Int64Field < 20`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
// Current limitation: unbounded intervals in OR not merged
// Both unbounded sides remain separate since we can't express "everything" as single range
be := expr.GetBinaryExpr()
require.NotNil(t, be, "should remain as OR")
require.Equal(t, planpb.BinaryExpr_LogicalOr, be.GetOp())
@@ -0,0 +1,112 @@
package rewriter
import (
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
)
// combineOrComplementaryRanges detects complementary range pairs in OR.
// (a > 10) OR (a <= 10) → true (for non-nullable columns)
// (a >= 10) OR (a < 10) → true (for non-nullable columns)
// For nullable columns, NULL values evaluate to false for all comparisons,
// so we cannot simplify to AlwaysTrue.
func (v *visitor) combineOrComplementaryRanges(parts []*planpb.Expr) []*planpb.Expr {
type rangeEntry struct {
op planpb.OpType
value *planpb.GenericValue
index int
col *planpb.ColumnInfo
}
type group struct {
col *planpb.ColumnInfo
entries []rangeEntry
}
groups := map[string]*group{}
others := []int{}
for idx, e := range parts {
u := e.GetUnaryRangeExpr()
if u == nil || u.GetColumnInfo() == nil || u.GetValue() == nil {
others = append(others, idx)
continue
}
op := u.GetOp()
if op != planpb.OpType_GreaterThan && op != planpb.OpType_GreaterEqual &&
op != planpb.OpType_LessThan && op != planpb.OpType_LessEqual {
others = append(others, idx)
continue
}
col := u.GetColumnInfo()
// Skip nullable columns: NULL comparisons evaluate to false,
// so (a > v) OR (a <= v) is NOT true when a is NULL.
if col.GetNullable() {
others = append(others, idx)
continue
}
key := columnKey(col)
g := groups[key]
if g == nil {
g = &group{col: col}
groups[key] = g
}
g.entries = append(g.entries, rangeEntry{op: op, value: u.GetValue(), index: idx, col: col})
}
used := make([]bool, len(parts))
out := make([]*planpb.Expr, 0, len(parts))
for _, idx := range others {
out = append(out, parts[idx])
used[idx] = true
}
for _, g := range groups {
if len(g.entries) < 2 {
continue
}
dt := effectiveDataType(g.col)
if !isSupportedScalarForRange(dt) {
continue
}
// Check all pairs for complementary ranges
foundTautology := false
for i := 0; i < len(g.entries) && !foundTautology; i++ {
for j := i + 1; j < len(g.entries) && !foundTautology; j++ {
a, b := g.entries[i], g.entries[j]
if cmpGeneric(dt, a.value, b.value) != 0 {
continue
}
// Same value, check if they are complementary
if isComplement(a.op, b.op) {
foundTautology = true
for _, e := range g.entries {
used[e.index] = true
}
out = append(out, newAlwaysTrueExpr())
}
}
}
}
for i := range parts {
if !used[i] {
out = append(out, parts[i])
}
}
return out
}
// isComplement returns true if op1 and op2 together cover all values at the same point.
// (> v, <= v), (>= v, < v), and their reverses.
func isComplement(a, b planpb.OpType) bool {
switch a {
case planpb.OpType_GreaterThan:
return b == planpb.OpType_LessEqual
case planpb.OpType_GreaterEqual:
return b == planpb.OpType_LessThan
case planpb.OpType_LessThan:
return b == planpb.OpType_GreaterEqual
case planpb.OpType_LessEqual:
return b == planpb.OpType_GreaterThan
}
return false
}
@@ -458,53 +458,51 @@ func TestRewrite_Or_In_Or_NotEqual_VNotInSet_ToNotEqual(t *testing.T) {
}
// Test contradictory equals: (a == 1) AND (a == 2) → false
// NOTE: This is a known limitation - currently NOT optimized
func TestRewrite_And_Equal_And_Equal_Contradiction_CurrentLimitation(t *testing.T) {
func TestRewrite_And_Equal_And_Equal_Contradiction(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
expr, err := parser.ParseExpr(helper, `Int64Field == 1 and Int64Field == 2`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
be := expr.GetBinaryExpr()
require.NotNil(t, be, "should remain as AND (not optimized)")
require.Equal(t, planpb.BinaryExpr_LogicalAnd, be.GetOp())
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_And_Equal_ThreeWay_Contradiction_CurrentLimitation(t *testing.T) {
// Test contradictory equals with three values
func TestRewrite_And_Equal_ThreeWay_Contradiction(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
expr, err := parser.ParseExpr(helper, `Int64Field == 1 and Int64Field == 2 and Int64Field == 3`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
be := expr.GetBinaryExpr()
require.NotNil(t, be, "should remain as AND chain (not optimized)")
require.Equal(t, planpb.BinaryExpr_LogicalAnd, be.GetOp())
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_And_Range_And_Equal_Contradiction_CurrentLimitation(t *testing.T) {
// Test range + contradictory equal: (a > 10) AND (a == 5) → false
func TestRewrite_And_Range_And_Equal_Contradiction(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 and Int64Field == 5`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
_ = expr
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func TestRewrite_And_Range_And_Equal_NonContradiction_CurrentLimitation(t *testing.T) {
// Test non-contradictory range + equal: (a > 10) AND (a == 15) → a == 15
func TestRewrite_And_Range_And_Equal_NonContradiction(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
expr, err := parser.ParseExpr(helper, `Int64Field > 10 and Int64Field == 15`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
be := expr.GetBinaryExpr()
require.NotNil(t, be, "should remain as AND (not optimized)")
require.Equal(t, planpb.BinaryExpr_LogicalAnd, be.GetOp())
ure := expr.GetUnaryRangeExpr()
require.NotNil(t, ure, "should simplify to equality")
require.Equal(t, planpb.OpType_Equal, ure.GetOp())
require.Equal(t, int64(15), ure.GetValue().GetInt64Val())
}
func TestRewrite_And_Equal_String_Contradiction_CurrentLimitation(t *testing.T) {
// Test string contradictory equals
func TestRewrite_And_Equal_String_Contradiction(t *testing.T) {
helper := buildSchemaHelperForRewriteT(t)
expr, err := parser.ParseExpr(helper, `VarCharField == "apple" and VarCharField == "banana"`, nil)
require.NoError(t, err)
require.NotNil(t, expr)
be := expr.GetBinaryExpr()
require.NotNil(t, be, "should remain as AND (not optimized)")
require.Equal(t, planpb.BinaryExpr_LogicalAnd, be.GetOp())
require.True(t, rewriter.IsAlwaysFalseExpr(expr))
}
func buildSchemaWithTimestamptz(t *testing.T) *typeutil.SchemaHelper {
+2 -2
View File
@@ -264,12 +264,12 @@ func TestValidatePartitionKeyIsolation(t *testing.T) {
{
name: "partition key isolation equal AND with same field equal diff",
expr: "key_field == 10 && key_field == 20",
expectedErrorString: "",
expectedErrorString: "partition key not found in expr",
},
{
name: "partition key isolation equal AND with same field equal 3",
expr: "key_field == 10 && key_field == 11 && key_field == 12",
expectedErrorString: "",
expectedErrorString: "partition key not found in expr",
},
{
name: "partition key isolation equal AND with varchar field equal",