mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-21 10:15:43 +00:00
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:
@@ -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")
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user