This is an automated email from the ASF dual-hosted git repository.

morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 5f107ef5b50 [fix](rbo) Preserve semantics in predicate inference 
(#67919)
5f107ef5b50 is described below

commit 5f107ef5b508d6fd77881cfb141498177f97720c
Author: feiniaofeiafei <[email protected]>
AuthorDate: Wed Sep 23 16:10:53 2026 +0800

    [fix](rbo) Preserve semantics in predicate inference (#67919)
    
    ### What problem does this PR solve?
    
    Problem Summary:
    
    Fix two predicate inference errors that change query results:
    
    - `chooseInputPredicates` records a retained `GT`/`GTE` predicate as
    `EQ`. The false equality can make another necessary predicate appear
    redundant. For example, `a > b AND rn > b AND a > rn` can lose `a > rn`
    and return extra rows in a QUALIFY query. Record the actual relation in
    the working graph.
    - Comparison-equal operands are not interchangeable inside arbitrary
    expressions. DATE and DATETIME values can compare equal but have
    different string lengths; negative and positive floating-point zero
    compare equal but have different SIGNBIT results. Substitution can
    therefore add filters that discard matching rows. Restrict expression
    substitution to exactly matching supported scalar types with
    value-preserving equality. Preserve direct comparisons, IN and their
    negations on the unwrapped operand, along with the existing determinism
    and cast-analysis checks.
    
    The fixes are in separate commits. Shared cast extraction is unchanged,
    and existing regression expectations are unchanged.
    
    ### Release note
    
    Fix extra or missing rows caused by incorrect predicate inference,
    including QUALIFY inequality chains and substitutions inside
    type-sensitive or representation-sensitive expressions.
---
 .../rules/rewrite/CollectFilterAboveConsumer.java  |   2 +-
 .../rules/rewrite/InferPredicateByReplace.java     |  34 ++++-
 .../nereids/rules/rewrite/InferPredicates.java     |   4 +-
 ...ProjectOtherJoinConditionForNestedLoopJoin.java |   2 +-
 .../rewrite/PushDownFilterThroughAggregation.java  |   2 +-
 .../rewrite/PushDownFilterThroughGenerate.java     |   2 +-
 .../rules/rewrite/PushDownFilterThroughJoin.java   |   4 +-
 .../PushDownFilterThroughPartitionTopN.java        |   2 +-
 .../rewrite/PushDownFilterThroughProject.java      |   2 +-
 .../rewrite/PushDownFilterThroughSetOperation.java |   2 +-
 .../rules/rewrite/PushDownFilterThroughWindow.java |   2 +-
 .../rules/rewrite/PushDownJoinOtherCondition.java  |   2 +-
 .../rules/rewrite/PushFilterInsideJoin.java        |   2 +-
 .../doris/nereids/rules/rewrite/ReorderJoin.java   |   7 +-
 .../rules/rewrite/UnequalPredicateInfer.java       |   8 +-
 .../functions/scalar/ToBitmapWithCheck.java        |   3 +-
 .../doris/nereids/util/PredicateInferUtils.java    |  38 +----
 .../rules/rewrite/InferPredicateByReplaceTest.java | 157 +++++++++++++++++++++
 .../rules/rewrite/UnequalPredicateInferTest.java   |  99 +++++++++++++
 .../apache/doris/nereids/types/DataTypeTest.java   |  15 ++
 .../infer_none_movable_predicate.out               |   5 +
 .../infer_predicate_reverse_relation.out           |  11 ++
 .../infer_predicate/infer_timestamptz_cast.out     |  13 ++
 .../nereids_rules_p0/infer_predicate_qualify.out   |  15 ++
 .../infer_predicate_replace_type.out               |  36 +++++
 .../infer_none_movable_predicate.groovy            |  40 ++++++
 .../infer_predicate_reverse_relation.groovy        |  50 +++++++
 .../infer_predicate/infer_timestamptz_cast.groovy  |  69 +++++++++
 .../infer_predicate_qualify.groovy                 |  54 +++++++
 .../infer_predicate_replace_type.groovy            |  89 ++++++++++++
 30 files changed, 712 insertions(+), 59 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectFilterAboveConsumer.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectFilterAboveConsumer.java
index 25fa5d2ccc8..91b003e0647 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectFilterAboveConsumer.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectFilterAboveConsumer.java
@@ -38,7 +38,7 @@ public class CollectFilterAboveConsumer extends 
OneRewriteRuleFactory {
             LogicalCTEConsumer cteConsumer = filter.child();
             Set<Expression> exprs = filter.getConjuncts();
             for (Expression expr : exprs) {
-                if (expr.containsVolatileExpression()) {
+                if (expr.containsVolatileOrNoneMovableExpression()) {
                     continue;
                 }
                 Expression rewrittenExpr = expr.rewriteUp(e -> {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferPredicateByReplace.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferPredicateByReplace.java
index c2ca99f0b61..9003b802476 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferPredicateByReplace.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferPredicateByReplace.java
@@ -32,9 +32,11 @@ import org.apache.doris.nereids.trees.expressions.Not;
 import org.apache.doris.nereids.trees.expressions.Or;
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.functions.ExpressionTrait;
+import 
org.apache.doris.nereids.trees.expressions.functions.NoneMovableFunction;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.types.DataType;
 import org.apache.doris.nereids.types.DecimalV2Type;
 import org.apache.doris.nereids.types.DecimalV3Type;
 import org.apache.doris.nereids.util.ExpressionUtils;
@@ -153,12 +155,15 @@ public class InferPredicateByReplace {
         ExpressionAnalyzer analyzer = new ReplaceAnalyzer(null, new 
Scope(ImmutableList.of()), null, false, false);
         Set<Expression> res = new LinkedHashSet<>();
         for (T equals : equalSet) {
-            Map<Expression, Expression> replaceMap = new HashMap<>();
-            replaceMap.put(equals, replaceToThis);
             if (!exprPredicates.containsKey(equals)) {
                 continue;
             }
+            Map<Expression, Expression> replaceMap = new HashMap<>();
+            replaceMap.put(equals, replaceToThis);
             for (Expression predicate : exprPredicates.get(equals)) {
+                if (!canReplace(equals, replaceToThis, predicate)) {
+                    continue;
+                }
                 Expression newPredicates = ExpressionUtils.replace(predicate, 
replaceMap);
                 try {
                     Expression analyzed = analyzer.analyze(newPredicates);
@@ -171,6 +176,27 @@ public class InferPredicateByReplace {
         return res;
     }
 
+    private static boolean canReplace(Expression source, Expression target, 
Expression predicate) {
+        Expression comparison = predicate instanceof Not ? predicate.child(0) 
: predicate;
+        // Direct comparisons observe comparison equality rather than a 
value's type or representation.
+        // Do not descend through functions, casts or OR to apply this 
exception.
+        if ((comparison instanceof ComparisonPredicate || comparison 
instanceof InPredicate)
+                && comparison.child(0).equals(source)) {
+            return true;
+        }
+        DataType type = source.getDataType();
+        // Comparison equality across types does not preserve type-sensitive 
expressions such as CAST to STRING.
+        if (!type.equals(target.getDataType())) {
+            return false;
+        }
+        // Only substitute types whose equality preserves the value observed 
by enclosing expressions.
+        // In particular, FLOAT/DOUBLE equality cannot distinguish signed 
zero, but SIGNBIT can.
+        // Comparisons can still be propagated separately by 
UnequalPredicateInfer.
+        return type.isBooleanType() || type.isIntegralType() || 
type.isDecimalLikeType()
+                || type.isStringLikeType() || type.isIPType()
+                || (type.isDateLikeType() && !type.isTimeStampTzType());
+    }
+
     /* Extract the equivalence relationship a=b, and when case (d_tinyint as 
int)=d_int is encountered,
     remove the cast and extract d_tinyint=d_int
     EqualPairs is the output parameter and the equivalent pair of predicate 
derivation input,
@@ -210,7 +236,9 @@ public class InferPredicateByReplace {
         }
         Map<Expression, Set<Expression>> exprPredicates = new HashMap<>();
         for (Expression input : inputs) {
-            if (input.anyMatch(expr -> !((ExpressionTrait) 
expr).isDeterministic())
+            // Inference can evaluate a predicate on rows that never reach its 
original filter.
+            if (input.anyMatch(expr -> expr instanceof NoneMovableFunction
+                    || !((ExpressionTrait) expr).isDeterministic())
                     || input.getInputSlots().size() != 1) {
                 continue;
             }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferPredicates.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferPredicates.java
index 8688db6a7d1..4ac9da39a7e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferPredicates.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferPredicates.java
@@ -218,7 +218,7 @@ public class InferPredicates extends 
DefaultPlanRewriter<JobContext> implements
         Set<Expression> predicates = new LinkedHashSet<>();
         Set<Slot> planOutputs = plan.getOutputSet();
         for (Expression expr : expressions) {
-            if (expr.containsVolatileExpression()) {
+            if (expr.containsVolatileOrNoneMovableExpression()) {
                 // Volatile expressions (e.g. rand(), uuid()) must not be 
cloned into
                 // subtrees that did not already evaluate them. Otherwise, 
callers that perform
                 // slot substitution (e.g. SetOp visitors below) would 
introduce a fresh
@@ -250,7 +250,7 @@ public class InferPredicates extends 
DefaultPlanRewriter<JobContext> implements
         Set<Expression> predicates = new LinkedHashSet<>();
         Set<Slot> planOutputs = plan.getOutputSet();
         for (Expression expr : expressions) {
-            if (expr.containsVolatileExpression()) {
+            if (expr.containsVolatileOrNoneMovableExpression()) {
                 // See inferNewPredicate for rationale: never clone volatile
                 // predicates into a subtree that did not already evaluate 
them.
                 continue;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
index 2d0a032fd9c..22606cf86d7 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
@@ -124,7 +124,7 @@ public class ProjectOtherJoinConditionForNestedLoopJoin 
extends OneRewriteRuleFa
             // pair" to "per row of that child", which silently changes 
results. Keep such
             // expressions inline in otherJoinConjuncts, but still recurse to 
extract deterministic
             // child expressions.
-            if (expression.containsVolatileExpression()) {
+            if (expression.containsVolatileOrNoneMovableExpression()) {
                 return super.visit(expression, ctx);
             }
             if (ctx.leftSlots.containsAll(input)) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughAggregation.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughAggregation.java
index 0945162f6d0..c61f5b28d42 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughAggregation.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughAggregation.java
@@ -69,7 +69,7 @@ public class PushDownFilterThroughAggregation extends 
OneRewriteRuleFactory {
                 // 2. if the conjunct contains unique function, it should not 
be pushed down;
                 //    e.g. 'select a, sum(a) from t group by a having a + 
random() >  10'
                 //    not equals 'select a, sum(a) from t where a + random() > 
10 group by a'
-                if (!conjunct.containsVolatileExpression()
+                if (!conjunct.containsVolatileOrNoneMovableExpression()
                         && !conjunctSlots.isEmpty() && 
canPushDownSlots.containsAll(conjunctSlots)) {
                     pushDownPredicates.add(conjunct);
                 } else {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughGenerate.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughGenerate.java
index 85de47ab127..a7e885a501f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughGenerate.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughGenerate.java
@@ -50,7 +50,7 @@ public class PushDownFilterThroughGenerate extends 
OneRewriteRuleFactory {
             filter.getConjuncts().forEach(conjunct -> {
                 Set<Slot> conjunctSlots = conjunct.getInputSlots();
                 if (!conjunctSlots.isEmpty() && 
childOutputs.containsAll(conjunctSlots)
-                        && !conjunct.containsVolatileExpression()) {
+                        && 
!conjunct.containsVolatileOrNoneMovableExpression()) {
                     pushDownPredicates.add(conjunct);
                 } else {
                     remainPredicates.add(conjunct);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughJoin.java
index ddcff70759f..7e3532c1f10 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughJoin.java
@@ -120,7 +120,7 @@ public class PushDownFilterThroughJoin extends 
OneRewriteRuleFactory {
             Set<Expression> rightPredicates = Sets.newLinkedHashSet();
             Set<Expression> remainingPredicates = Sets.newLinkedHashSet();
             for (Expression p : filterPredicates) {
-                if (p.containsVolatileExpression()) {
+                if (p.containsVolatileOrNoneMovableExpression()) {
                     remainingPredicates.add(p);
                     continue;
                 }
@@ -162,7 +162,7 @@ public class PushDownFilterThroughJoin extends 
OneRewriteRuleFactory {
         if (!(predicate instanceof EqualTo)) {
             return false;
         }
-        if (predicate.containsVolatileExpression()) {
+        if (predicate.containsVolatileOrNoneMovableExpression()) {
             return false;
         }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughPartitionTopN.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughPartitionTopN.java
index 5c5275730a1..62d3e139b9e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughPartitionTopN.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughPartitionTopN.java
@@ -77,7 +77,7 @@ public class PushDownFilterThroughPartitionTopN extends 
OneRewriteRuleFactory {
                 // top-N", and the surviving rows of every partition would no 
longer be the true
                 // top-N. Empty-input-slot predicates like `rand() > 0.5` 
would also bypass the
                 // `containsAll` check otherwise.
-                if (!expr.containsVolatileExpression() && 
partitionKeySlots.containsAll(exprInputSlots)) {
+                if (!expr.containsVolatileOrNoneMovableExpression() && 
partitionKeySlots.containsAll(exprInputSlots)) {
                     bottomConjunctsBuilder.add(expr);
                 } else {
                     upperConjunctsBuilder.add(expr);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughProject.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughProject.java
index 1a46b51a246..0f3478e0642 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughProject.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughProject.java
@@ -125,7 +125,7 @@ public class PushDownFilterThroughProject implements 
RewriteRuleFactory {
             // `project(b + random(1, 10) as a) -> filter(b + random(1, 10) > 
1)`, it contains two distinct RANDOM.
             if (childOutputs.containsAll(conjunctSlots)
                     && 
conjunctSlots.stream().map(childAlias::get).filter(Objects::nonNull)
-                            
.noneMatch(Expression::containsVolatileExpression)) {
+                            
.noneMatch(Expression::containsVolatileOrNoneMovableExpression)) {
                 pushDownPredicates.add(conjunct);
             } else {
                 remainPredicates.add(conjunct);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughSetOperation.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughSetOperation.java
index 85d78be1aef..b5f0c243d4f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughSetOperation.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughSetOperation.java
@@ -87,7 +87,7 @@ public class PushDownFilterThroughSetOperation extends 
OneRewriteRuleFactory {
                     pushableConjuncts = new LinkedHashSet<>();
                     Set<Expression> kept = new LinkedHashSet<>();
                     for (Expression c : origFilter.getConjuncts()) {
-                        if (c.containsVolatileExpression()) {
+                        if (c.containsVolatileOrNoneMovableExpression()) {
                             kept.add(c);
                         } else {
                             pushableConjuncts.add(c);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughWindow.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughWindow.java
index 3fc7c0b8dfa..cc64a338666 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughWindow.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughWindow.java
@@ -97,7 +97,7 @@ public class PushDownFilterThroughWindow extends 
OneRewriteRuleFactory {
         // changes the value of every window function (row_number, rank, sum, 
...). In addition,
         // a predicate like `rand() > 0.5` has empty input slots, so 
`containsAll(emptySet)`
         // would otherwise wrongly return true.
-        return !conjunct.containsVolatileExpression()
+        return !conjunct.containsVolatileOrNoneMovableExpression()
                 && commonPartitionKeys.containsAll(conjunct.getInputSlots());
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOtherCondition.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOtherCondition.java
index 098175ff462..e5e89742e1b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOtherCondition.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOtherCondition.java
@@ -78,7 +78,7 @@ public class PushDownJoinOtherCondition extends 
OneRewriteRuleFactory {
                         // child changes their evaluation granularity from per 
joined row to per
                         // input row. Repeated volatile occurrences are 
materialized later by
                         // AddProjectForVolatileExpression.
-                        if (otherConjunct.containsVolatileExpression()) {
+                        if 
(otherConjunct.containsVolatileOrNoneMovableExpression()) {
                             remainingOther.add(otherConjunct);
                         } else if 
(PUSH_DOWN_LEFT_VALID_TYPE.contains(join.getJoinType())
                                 && allCoveredBy(otherConjunct, 
join.left().getOutputSet())) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushFilterInsideJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushFilterInsideJoin.java
index 7c529ee6669..0fcc24bae21 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushFilterInsideJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushFilterInsideJoin.java
@@ -59,7 +59,7 @@ public class PushFilterInsideJoin extends 
OneRewriteRuleFactory {
                     List<Expression> otherConditions = 
Lists.newArrayListWithExpectedSize(
                             filter.getConjuncts().size() + 
join.getOtherJoinConjuncts().size());
                     for (Expression expr : filter.getConjuncts()) {
-                        if (expr.containsVolatileExpression()) {
+                        if (expr.containsVolatileOrNoneMovableExpression()) {
                             remainConditions.add(expr);
                         } else {
                             otherConditions.add(expr);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ReorderJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ReorderJoin.java
index 3f5c520b27f..d71af183080 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ReorderJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ReorderJoin.java
@@ -100,7 +100,7 @@ public class ReorderJoin extends OneRewriteRuleFactory {
                 for (Expression conjunct : filter.getConjuncts()) {
                     // after reorder and push down the random() down to lower 
join,
                     // the rewritten sql may have less rows() than the origin 
sql
-                    if (conjunct.containsVolatileExpression()) {
+                    if (conjunct.containsVolatileOrNoneMovableExpression()) {
                         uniqueExprConjuncts.add(conjunct);
                     } else {
                         nonUniqueExprConjuncts.add(conjunct);
@@ -153,7 +153,7 @@ public class ReorderJoin extends OneRewriteRuleFactory {
                 // (t1 join t2) join t3 where t1.a = t3.x + random()
                 // if reorder, then may have ((t1 join t3) on t1.a = t3.x + 
random()) join t2,
                 // then the reorder result will less rows than origin.
-                if (conjunct.containsVolatileExpression()) {
+                if (conjunct.containsVolatileOrNoneMovableExpression()) {
                     return plan;
                 }
             }
@@ -163,7 +163,8 @@ public class ReorderJoin extends OneRewriteRuleFactory {
             join = (LogicalJoin<?, ?>) plan;
         }
 
-        if (join.isMarkJoin() || join.getJoinType().isAsofJoin()) {
+        if (join.isMarkJoin() || join.getJoinType().isAsofJoin()
+                || 
join.getExpressions().stream().anyMatch(Expression::containsVolatileExpression))
 {
             return plan;
         }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnequalPredicateInfer.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnequalPredicateInfer.java
index adf369221e1..bf3402eb88a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnequalPredicateInfer.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnequalPredicateInfer.java
@@ -396,7 +396,8 @@ public class UnequalPredicateInfer {
 
         private void clear(Relation[][] graph, int left, int right, Relation 
type) {
             graph[left][right] = Relation.UNDEFINED;
-            if (type == Relation.EQ) {
+            // A reverse inequality is a separate constraint, not the 
duplicate of this equality.
+            if (type == Relation.EQ && graph[right][left] == Relation.EQ) {
                 graph[right][left] = Relation.UNDEFINED;
             }
         }
@@ -448,7 +449,8 @@ public class UnequalPredicateInfer {
                     clear(chosen, left, right, type);
                 } else if (deduced[left][right] != type) {
                     keep[i] = true;
-                    set(deduced, left, right, Relation.EQ);
+                    // Preserve the relation of the retained predicate; an 
inequality is not an equality.
+                    set(deduced, left, right, type);
                     expandGraph(deduced, left, right);
                     if (type == Relation.EQ) {
                         expandGraph(deduced, right, left);
@@ -568,7 +570,7 @@ public class UnequalPredicateInfer {
             return inputs;
         }
         inferGraph.deduce(inferGraph.graph);
-        Set<Expression> newPredicates = new LinkedHashSet<>();
+        Set<Expression> newPredicates = new LinkedHashSet<>(inputs);
         newPredicates.addAll(inferGraph.generatePredicates(inferGraph.graph));
         newPredicates.addAll(inferGraph.otherPredicates);
         return newPredicates;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToBitmapWithCheck.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToBitmapWithCheck.java
index d238ad782e6..8805e5c55df 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToBitmapWithCheck.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToBitmapWithCheck.java
@@ -21,6 +21,7 @@ import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.functions.AlwaysNotNullable;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
+import 
org.apache.doris.nereids.trees.expressions.functions.NoneMovableFunction;
 import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.BigIntType;
@@ -37,7 +38,7 @@ import java.util.List;
  * ScalarFunction 'to_bitmap_with_check'. This class is generated by 
GenerateFunction.
  */
 public class ToBitmapWithCheck extends ScalarFunction
-        implements UnaryExpression, ExplicitlyCastableSignature, 
AlwaysNotNullable {
+        implements UnaryExpression, ExplicitlyCastableSignature, 
AlwaysNotNullable, NoneMovableFunction {
 
     public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             
FunctionSignature.ret(BitmapType.INSTANCE).args(BigIntType.INSTANCE),
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/PredicateInferUtils.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/PredicateInferUtils.java
index 2a3ac016c49..96b20606cdb 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/PredicateInferUtils.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/PredicateInferUtils.java
@@ -26,10 +26,6 @@ import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
 import org.apache.doris.nereids.types.DataType;
-import org.apache.doris.nereids.types.DateTimeType;
-import org.apache.doris.nereids.types.DateTimeV2Type;
-import org.apache.doris.nereids.types.DateType;
-import org.apache.doris.nereids.types.DateV2Type;
 import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.DateLikeType;
 import org.apache.doris.nereids.types.coercion.IntegralType;
@@ -128,37 +124,9 @@ public class PredicateInferUtils {
         Expression child = cast.child();
         DataType dataType = cast.getDataType();
         DataType childType = child.getDataType();
-        if (inferType == InferType.INTEGRAL) {
-            if (dataType instanceof IntegralType) {
-                IntegralType integralType = (IntegralType) dataType;
-                if (childType instanceof IntegralType && 
integralType.widerThan((IntegralType) childType)) {
-                    return validForInfer(((Cast) expression).child(), 
inferType);
-                }
-            }
-        } else if (inferType == InferType.DATE) {
-            // avoid lost precision
-            if (dataType instanceof DateType) {
-                if (childType instanceof DateV2Type || childType instanceof 
DateType) {
-                    return validForInfer(child, inferType);
-                }
-            } else if (dataType instanceof DateV2Type) {
-                if (childType instanceof DateType || childType instanceof 
DateV2Type) {
-                    return validForInfer(child, inferType);
-                }
-            } else if (dataType instanceof DateTimeType) {
-                if (childType.isTimeStampNsType()) {
-                    return Optional.empty();
-                }
-                if (!(childType instanceof DateTimeV2Type)) {
-                    return validForInfer(child, inferType);
-                }
-            } else if (dataType instanceof DateTimeV2Type) {
-                if (childType.isTimeStampNsType()) {
-                    return Optional.empty();
-                }
-                if (!(childType instanceof DateTimeV2Type) || 
childType.isInjectiveCastTo(dataType)) {
-                    return validForInfer(child, inferType);
-                }
+        if (inferType == InferType.INTEGRAL || inferType == InferType.DATE) {
+            if (childType.isInjectiveCastTo(dataType)) {
+                return validForInfer(child, inferType);
             }
         } else if (inferType == InferType.STRING) {
             // avoid substring cast such as cast(char(3) as char(2))
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferPredicateByReplaceTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferPredicateByReplaceTest.java
index 5c174cb6348..f9bd8835bda 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferPredicateByReplaceTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferPredicateByReplaceTest.java
@@ -28,30 +28,68 @@ import org.apache.doris.nereids.trees.expressions.Not;
 import org.apache.doris.nereids.trees.expressions.Or;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Abs;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.AssertTrue;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.DateTrunc;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Length;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.SignBit;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.DateTimeLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
+import org.apache.doris.nereids.types.ArrayType;
 import org.apache.doris.nereids.types.BigIntType;
+import org.apache.doris.nereids.types.BooleanType;
+import org.apache.doris.nereids.types.CharType;
+import org.apache.doris.nereids.types.DataType;
 import org.apache.doris.nereids.types.DateTimeType;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateType;
+import org.apache.doris.nereids.types.DateV2Type;
+import org.apache.doris.nereids.types.DecimalV3Type;
+import org.apache.doris.nereids.types.DoubleType;
+import org.apache.doris.nereids.types.FloatType;
+import org.apache.doris.nereids.types.IPv4Type;
+import org.apache.doris.nereids.types.IPv6Type;
 import org.apache.doris.nereids.types.IntegerType;
 import org.apache.doris.nereids.types.StringType;
 import org.apache.doris.nereids.types.TimeStampNsType;
+import org.apache.doris.nereids.types.TimeStampTzType;
 import org.apache.doris.nereids.types.TinyIntType;
+import org.apache.doris.nereids.types.VarcharType;
 import org.apache.doris.nereids.util.PredicateInferUtils;
+import org.apache.doris.nereids.util.TypeCoercionUtils;
 
 import com.google.common.collect.ImmutableList;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Set;
+import java.util.stream.Stream;
 
 public class InferPredicateByReplaceTest {
+    @Test
+    public void testDoNotInferNoneMovablePredicateInsideOr() {
+        SlotReference a = new SlotReference("a", IntegerType.INSTANCE);
+        SlotReference b = new SlotReference("b", IntegerType.INSTANCE);
+        Expression predicate = new Or(
+                new AssertTrue(new GreaterThan(a, new IntegerLiteral(0)), new 
StringLiteral("bad")),
+                new GreaterThan(a, new IntegerLiteral(10)));
+        Set<Expression> inputs = new LinkedHashSet<>(ImmutableList.of(new 
EqualTo(a, b), predicate));
+
+        Assertions.assertEquals(inputs, InferPredicateByReplace.infer(inputs));
+        Assertions.assertEquals(inputs, 
PredicateInferUtils.inferAllPredicate(inputs));
+    }
+
     @Test
     public void testInferWithEqualTo() {
         SlotReference a = new SlotReference("a", IntegerType.INSTANCE);
@@ -231,6 +269,23 @@ public class InferPredicateByReplaceTest {
         
Assertions.assertFalse(PredicateInferUtils.getPairFromCast(legacyEqualTo).isPresent());
     }
 
+    @Test
+    public void testTimestampTzCastIsNotRemovedForPredicateInference() {
+        for (int sourceScale : new int[] {0, 3, 6}) {
+            SlotReference timestampTz = new SlotReference("tz", 
TimeStampTzType.of(sourceScale));
+            for (DataType target : ImmutableList.of(DateTimeType.INSTANCE,
+                    DateTimeV2Type.of(0), DateTimeV2Type.of(3), 
DateTimeV2Type.of(6))) {
+                SlotReference localTime = new SlotReference("dt", target);
+                Cast cast = new Cast(timestampTz, target);
+                Assertions.assertFalse(PredicateInferUtils.getPairFromCast(new 
EqualTo(cast, localTime)).isPresent());
+                Assertions.assertFalse(PredicateInferUtils.getPairFromCast(new 
GreaterThan(cast, localTime)).isPresent());
+                Assertions.assertFalse(PredicateInferUtils.getPairFromCast(
+                        new EqualTo(new Cast(cast, DateTimeV2Type.of(6)),
+                                new Cast(localTime, 
DateTimeV2Type.of(6)))).isPresent());
+            }
+        }
+    }
+
     @Test
     public void testNotInferWithTransitiveEqualitySameTable() {
         // a = b, b = c
@@ -245,4 +300,106 @@ public class InferPredicateByReplaceTest {
         Set<Expression> result = InferPredicateByReplace.infer(inputs);
         Assertions.assertEquals(2, result.size());
     }
+
+    static Stream<Arguments> replacementTypes() {
+        List<Arguments> cases = new ArrayList<>();
+        for (DataType type : ImmutableList.of(BooleanType.INSTANCE, 
IntegerType.INSTANCE, BigIntType.INSTANCE,
+                StringType.INSTANCE, DateV2Type.INSTANCE, 
DateTimeV2Type.of(0), DateTimeV2Type.of(6),
+                DecimalV3Type.createDecimalV3Type(9, 2), 
TimeStampNsType.INSTANCE,
+                CharType.createCharType(10), VarcharType.createVarcharType(10),
+                IPv4Type.INSTANCE, IPv6Type.INSTANCE)) {
+            cases.add(Arguments.of(type, type, true));
+        }
+        for (List<DataType> pair : ImmutableList.<List<DataType>>of(
+                ImmutableList.of(DateV2Type.INSTANCE, DateTimeV2Type.of(0)),
+                ImmutableList.of(DateTimeV2Type.of(0), DateTimeV2Type.of(6)),
+                ImmutableList.of(IntegerType.INSTANCE, BigIntType.INSTANCE),
+                ImmutableList.of(DecimalV3Type.createDecimalV3Type(9, 2),
+                        DecimalV3Type.createDecimalV3Type(9, 3)))) {
+            cases.add(Arguments.of(pair.get(0), pair.get(1), false));
+            cases.add(Arguments.of(pair.get(1), pair.get(0), false));
+        }
+        cases.add(Arguments.of(FloatType.INSTANCE, FloatType.INSTANCE, false));
+        cases.add(Arguments.of(DoubleType.INSTANCE, DoubleType.INSTANCE, 
false));
+        cases.add(Arguments.of(ArrayType.of(DoubleType.INSTANCE), 
ArrayType.of(DoubleType.INSTANCE), false));
+        return cases.stream();
+    }
+
+    @ParameterizedTest(name = "{0} -> {1}, replace={2}")
+    @MethodSource("replacementTypes")
+    public void testTypeSensitiveReplacement(DataType sourceType, DataType 
targetType, boolean canReplace) {
+        SlotReference a = new SlotReference("a", sourceType);
+        SlotReference b = new SlotReference("b", targetType);
+        Expression equality = TypeCoercionUtils.processComparisonPredicate(new 
EqualTo(a, b));
+        Expression predicate = new EqualTo(new Length(new Cast(a, 
StringType.INSTANCE)), new IntegerLiteral(10));
+        Set<Expression> inputs = new 
LinkedHashSet<>(ImmutableList.of(equality, predicate));
+        Set<Expression> result = InferPredicateByReplace.infer(inputs);
+        if (canReplace) {
+            Expression expected = new EqualTo(new Length(new Cast(b, 
StringType.INSTANCE)), new IntegerLiteral(10));
+            Assertions.assertTrue(result.contains(expected), () -> "Missing " 
+ expected + " in " + result);
+        } else {
+            Assertions.assertEquals(inputs, result);
+        }
+    }
+
+    @Test
+    public void testSignedZeroInOr() {
+        SlotReference x = new SlotReference("x", DoubleType.INSTANCE);
+        SlotReference y = new SlotReference("y", DoubleType.INSTANCE);
+        Expression predicate = new Or(new SignBit(x), new GreaterThan(x, new 
DoubleLiteral(1.0)));
+        Set<Expression> inputs = new LinkedHashSet<>(ImmutableList.of(new 
EqualTo(x, y), predicate));
+        // x = -0.0 and y = +0.0 satisfy the input, but not the predicate with 
x replaced by y.
+        Assertions.assertEquals(inputs, InferPredicateByReplace.infer(inputs));
+        Assertions.assertEquals(inputs, 
PredicateInferUtils.inferPredicate(inputs));
+    }
+
+    @Test
+    public void testSafeComparisonPropagation() {
+        SlotReference x = new SlotReference("x", DoubleType.INSTANCE, true, 
ImmutableList.of("left"));
+        SlotReference y = new SlotReference("y", DoubleType.INSTANCE, true, 
ImmutableList.of("right"));
+        Set<Expression> floating = new LinkedHashSet<>(ImmutableList.of(new 
EqualTo(x, y),
+                new GreaterThan(x, new DoubleLiteral(1.0))));
+        Assertions.assertTrue(PredicateInferUtils.inferPredicate(floating)
+                .contains(new GreaterThan(y, new DoubleLiteral(1.0))));
+
+        SlotReference small = new SlotReference("small", IntegerType.INSTANCE, 
true, ImmutableList.of("left"));
+        SlotReference wide = new SlotReference("wide", BigIntType.INSTANCE, 
true, ImmutableList.of("right"));
+        Set<Expression> integers = new LinkedHashSet<>(ImmutableList.of(
+                new EqualTo(new Cast(small, BigIntType.INSTANCE), wide),
+                new GreaterThan(small, new IntegerLiteral(1))));
+        
Assertions.assertTrue(PredicateInferUtils.inferPredicate(integers).stream()
+                .anyMatch(p -> p instanceof GreaterThan && 
p.child(0).equals(wide)
+                        && p.child(1).equals(new BigIntLiteral(1))));
+    }
+
+    @Test
+    public void testDirectComparisonReplacement() {
+        SlotReference small = new SlotReference("small", IntegerType.INSTANCE);
+        SlotReference wide = new SlotReference("wide", BigIntType.INSTANCE);
+        Expression equality = new EqualTo(new Cast(small, 
BigIntType.INSTANCE), wide);
+        List<Expression> predicates = ImmutableList.of(
+                new Not(new EqualTo(small, new IntegerLiteral(10))),
+                new InPredicate(small, ImmutableList.of(new 
IntegerLiteral(10), new IntegerLiteral(20))),
+                new Not(new InPredicate(small,
+                        ImmutableList.of(new IntegerLiteral(10), new 
IntegerLiteral(20)))));
+        List<Expression> expected = ImmutableList.of(
+                new Not(new EqualTo(wide, new BigIntLiteral(10))),
+                new InPredicate(wide, ImmutableList.of(new BigIntLiteral(10), 
new BigIntLiteral(20))),
+                new Not(new InPredicate(wide, ImmutableList.of(new 
BigIntLiteral(10), new BigIntLiteral(20)))));
+        for (int i = 0; i < predicates.size(); i++) {
+            Set<Expression> inputs = new 
LinkedHashSet<>(ImmutableList.of(equality, predicates.get(i)));
+            
Assertions.assertTrue(InferPredicateByReplace.infer(inputs).contains(expected.get(i)));
+        }
+    }
+
+    @Test
+    public void testSameTypeBehindWideningCasts() {
+        SlotReference a = new SlotReference("a", IntegerType.INSTANCE);
+        SlotReference b = new SlotReference("b", IntegerType.INSTANCE);
+        Expression equality = new EqualTo(new Cast(a, BigIntType.INSTANCE), 
new Cast(b, BigIntType.INSTANCE));
+        Expression predicate = new EqualTo(new Length(new Cast(a, 
StringType.INSTANCE)), new IntegerLiteral(1));
+        Set<Expression> inputs = new 
LinkedHashSet<>(ImmutableList.of(equality, predicate));
+        Expression expected = new EqualTo(new Length(new Cast(b, 
StringType.INSTANCE)), new IntegerLiteral(1));
+        
Assertions.assertTrue(InferPredicateByReplace.infer(inputs).contains(expected));
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/UnequalPredicateInferTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/UnequalPredicateInferTest.java
index 7bd43c98929..7115af40297 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/UnequalPredicateInferTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/UnequalPredicateInferTest.java
@@ -36,14 +36,18 @@ import org.apache.doris.nereids.types.DateV2Type;
 import org.apache.doris.nereids.types.IntegerType;
 import org.apache.doris.nereids.util.PredicateInferUtils;
 
+import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public class UnequalPredicateInferTest {
@@ -685,4 +689,99 @@ public class UnequalPredicateInferTest {
         EqualTo expected = new EqualTo(a, b);
         Assertions.assertTrue(result.contains(expected) || 
result.contains(expected.commute()), "Expected to find a = b in the result.");
     }
+
+    @Test
+    public void testInputPredicateSemantics() {
+        SlotReference a = new SlotReference("a", IntegerType.INSTANCE, false, 
ImmutableList.of("t"));
+        SlotReference b = new SlotReference("b", IntegerType.INSTANCE, false, 
ImmutableList.of("t"));
+        List<List<Relation>> relations = new ArrayList<>();
+        for (Relation first : ImmutableList.of(Relation.GT, Relation.GTE)) {
+            for (Relation second : ImmutableList.of(Relation.GT, Relation.GTE, 
Relation.EQ)) {
+                for (Relation third : ImmutableList.of(Relation.GT, 
Relation.GTE)) {
+                    relations.add(ImmutableList.of(first, second, third));
+                }
+            }
+        }
+        relations.add(ImmutableList.of(Relation.EQ, Relation.EQ, Relation.EQ));
+        // Window outputs have no table qualifier. Also cover same-table and 
cross-table slots.
+        for (List<String> qualifier : 
ImmutableList.of(ImmutableList.<String>of(),
+                ImmutableList.of("t"), ImmutableList.of("other"))) {
+            SlotReference rn = new SlotReference("rn", IntegerType.INSTANCE, 
false, qualifier);
+            for (List<Relation> types : relations) {
+                List<Expression> predicates = ImmutableList.of(comparison(a, 
b, types.get(0)),
+                        comparison(rn, b, types.get(1)), comparison(a, rn, 
types.get(2)));
+                for (List<Expression> permutation : 
Collections2.permutations(predicates)) {
+                    Set<Expression> inputs = new LinkedHashSet<>(permutation);
+                    Set<? extends Expression> inferred = 
UnequalPredicateInfer.inferUnequalPredicates(inputs);
+                    assertPredicateSemantics(inputs, inferred, a, b, rn);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testStrictReverseRelationWithEquality() {
+        SlotReference a = new SlotReference("a", IntegerType.INSTANCE, false, 
ImmutableList.of("t"));
+        SlotReference b = new SlotReference("b", IntegerType.INSTANCE, false, 
ImmutableList.of("t"));
+        for (List<String> qualifier : 
ImmutableList.of(ImmutableList.<String>of(),
+                ImmutableList.of("t"), ImmutableList.of("other"))) {
+            SlotReference c = new SlotReference("c", IntegerType.INSTANCE, 
false, qualifier);
+            List<Expression> predicates = ImmutableList.of(new EqualTo(a, c), 
new GreaterThan(c, a),
+                    new GreaterThanEqual(c, b), new GreaterThan(b, c), new 
EqualTo(b, c));
+            // Check the reported order first, then all permutations. Clearing 
a chosen equality must
+            // not discard a distinct reverse inequality needed to keep the 
contradiction.
+            Set<Expression> inputs = new LinkedHashSet<>(predicates);
+            assertPredicateSemantics(inputs, 
UnequalPredicateInfer.inferUnequalPredicates(inputs), a, b, c);
+            for (List<Expression> permutation : 
Collections2.permutations(predicates)) {
+                inputs = new LinkedHashSet<>(permutation);
+                assertPredicateSemantics(inputs, 
UnequalPredicateInfer.inferUnequalPredicates(inputs), a, b, c);
+                assertPredicateSemantics(inputs, 
UnequalPredicateInfer.inferAllPredicates(inputs), a, b, c);
+            }
+        }
+    }
+
+    private static void assertPredicateSemantics(Set<Expression> inputs, Set<? 
extends Expression> inferred,
+            SlotReference a, SlotReference b, SlotReference c) {
+        for (int av = 0; av <= 3; av++) {
+            for (int bv = 0; bv <= 3; bv++) {
+                for (int cv = 0; cv <= 3; cv++) {
+                    Map<Expression, Integer> values = ImmutableMap.of(a, av, 
b, bv, c, cv);
+                    boolean expected = inputs.stream().allMatch(p -> 
evaluateComparison(p, values));
+                    boolean actual = inferred.stream().allMatch(p -> 
evaluateComparison(p, values));
+                    Assertions.assertEquals(expected, actual,
+                            () -> "inputs=" + inputs + ", inferred=" + 
inferred + ", values=" + values);
+                }
+            }
+        }
+    }
+
+    private static Expression comparison(Expression left, Expression right, 
Relation relation) {
+        switch (relation) {
+            case GT:
+                return new GreaterThan(left, right);
+            case GTE:
+                return new GreaterThanEqual(left, right);
+            case EQ:
+                return new EqualTo(left, right);
+            default:
+                throw new AssertionError("Unexpected relation: " + relation);
+        }
+    }
+
+    private static boolean evaluateComparison(Expression expression, 
Map<Expression, Integer> values) {
+        int left = values.get(expression.child(0));
+        int right = values.get(expression.child(1));
+        if (expression instanceof GreaterThan) {
+            return left > right;
+        } else if (expression instanceof GreaterThanEqual) {
+            return left >= right;
+        } else if (expression instanceof LessThan) {
+            return left < right;
+        } else if (expression instanceof LessThanEqual) {
+            return left <= right;
+        } else if (expression instanceof EqualTo) {
+            return left == right;
+        }
+        throw new AssertionError("Unexpected comparison: " + expression);
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/types/DataTypeTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/types/DataTypeTest.java
index 2195b5e4b49..6dadaf7e8f2 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/nereids/types/DataTypeTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/types/DataTypeTest.java
@@ -314,6 +314,21 @@ public class DataTypeTest {
         assertSafeCast(v1, anotherV1);
     }
 
+    @Test
+    public void testIsInjectiveCastToForDateTypes() {
+        for (DataType source : ImmutableList.of(DateType.INSTANCE, 
DateV2Type.INSTANCE)) {
+            assertSafeCast(source, DateType.INSTANCE);
+            assertSafeCast(source, DateV2Type.INSTANCE);
+            assertSafeCast(source, DateTimeType.INSTANCE);
+            for (int scale = 0; scale <= DateTimeV2Type.MAX_SCALE; scale++) {
+                assertSafeCast(source, DateTimeV2Type.of(scale));
+            }
+            assertUnsafeCast(source, TimeStampTzType.MAX);
+            assertUnsafeCast(source, TimeStampNsType.INSTANCE);
+            assertUnsafeCast(DateTimeV2Type.MAX, source);
+        }
+    }
+
     @Test
     public void testIsInjectiveCastToForComplexTypes() {
         assertSafeCast(ArrayType.of(IntegerType.INSTANCE), 
ArrayType.of(BigIntType.INSTANCE));
diff --git 
a/regression-test/data/nereids_rules_p0/infer_predicate/infer_none_movable_predicate.out
 
b/regression-test/data/nereids_rules_p0/infer_predicate/infer_none_movable_predicate.out
new file mode 100644
index 00000000000..6a145a72a41
--- /dev/null
+++ 
b/regression-test/data/nereids_rules_p0/infer_predicate/infer_none_movable_predicate.out
@@ -0,0 +1,5 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !assert_in_or --
+1      1
+11     11
+
diff --git 
a/regression-test/data/nereids_rules_p0/infer_predicate/infer_predicate_reverse_relation.out
 
b/regression-test/data/nereids_rules_p0/infer_predicate/infer_predicate_reverse_relation.out
new file mode 100644
index 00000000000..35ba57417f0
--- /dev/null
+++ 
b/regression-test/data/nereids_rules_p0/infer_predicate/infer_predicate_reverse_relation.out
@@ -0,0 +1,11 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !strict --
+
+-- !reordered --
+
+-- !commuted --
+
+-- !non_strict --
+1      1       1       1
+2      2       2       2
+
diff --git 
a/regression-test/data/nereids_rules_p0/infer_predicate/infer_timestamptz_cast.out
 
b/regression-test/data/nereids_rules_p0/infer_predicate/infer_timestamptz_cast.out
new file mode 100644
index 00000000000..9469cb5a353
--- /dev/null
+++ 
b/regression-test/data/nereids_rules_p0/infer_predicate/infer_timestamptz_cast.out
@@ -0,0 +1,13 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !dst_not_equal --
+1      10
+
+-- !dst_greater_than --
+1      10
+
+-- !scale_not_equal --
+2      20
+
+-- !scale_less_than --
+2      20
+
diff --git a/regression-test/data/nereids_rules_p0/infer_predicate_qualify.out 
b/regression-test/data/nereids_rules_p0/infer_predicate_qualify.out
new file mode 100644
index 00000000000..141f1db1e8d
--- /dev/null
+++ b/regression-test/data/nereids_rules_p0/infer_predicate_qualify.out
@@ -0,0 +1,15 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !strict --
+
+-- !non_strict --
+1      2       1       1
+2      2       1       2
+
+-- !mixed --
+1      2       1       1
+
+-- !reordered --
+
+-- !matching_row --
+3      4       1       3
+
diff --git 
a/regression-test/data/nereids_rules_p0/infer_predicate_replace_type.out 
b/regression-test/data/nereids_rules_p0/infer_predicate_replace_type.out
new file mode 100644
index 00000000000..18d06474878
--- /dev/null
+++ b/regression-test/data/nereids_rules_p0/infer_predicate_replace_type.out
@@ -0,0 +1,36 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !date_length --
+1      10
+
+-- !datetime_length --
+1      10
+
+-- !date_same_type --
+1      1
+
+-- !date_comparison --
+1      10
+
+-- !signed_zero_facts --
+1      true    true    false
+2      true    false   true
+3      true    false   false
+4      true    false   false
+5      true    true    true
+
+-- !signed_zero_or --
+1
+3
+5
+
+-- !signed_zero_or_reversed --
+2
+3
+5
+
+-- !float_comparison --
+3
+
+-- !decimal_scale --
+1      10
+
diff --git 
a/regression-test/suites/nereids_rules_p0/infer_predicate/infer_none_movable_predicate.groovy
 
b/regression-test/suites/nereids_rules_p0/infer_predicate/infer_none_movable_predicate.groovy
new file mode 100644
index 00000000000..0c6d53c96b1
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/infer_predicate/infer_none_movable_predicate.groovy
@@ -0,0 +1,40 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("infer_none_movable_predicate", "p0") {
+    sql "DROP TABLE IF EXISTS infer_none_movable_l"
+    sql "DROP TABLE IF EXISTS infer_none_movable_r"
+    sql """
+        CREATE TABLE infer_none_movable_l (a INT NOT NULL)
+        DUPLICATE KEY(a) DISTRIBUTED BY HASH(a) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql """
+        CREATE TABLE infer_none_movable_r (b INT NOT NULL)
+        DUPLICATE KEY(b) DISTRIBUTED BY HASH(b) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql "INSERT INTO infer_none_movable_l VALUES (1), (11)"
+    sql "INSERT INTO infer_none_movable_r VALUES (-1), (1), (11)"
+    // The unmatched negative row must never evaluate the left-side assertion.
+    order_qt_assert_in_or """
+        SELECT l.a, r.b
+        FROM (SELECT a FROM infer_none_movable_l
+              WHERE assert_true(a > 0, 'bad') OR a > 10) l
+        JOIN infer_none_movable_r r ON l.a = r.b
+    """
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/infer_predicate/infer_predicate_reverse_relation.groovy
 
b/regression-test/suites/nereids_rules_p0/infer_predicate/infer_predicate_reverse_relation.groovy
new file mode 100644
index 00000000000..459dd6dc2aa
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/infer_predicate/infer_predicate_reverse_relation.groovy
@@ -0,0 +1,50 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("infer_predicate_reverse_relation") {
+    sql "drop table if exists infer_predicate_reverse_relation_input"
+    sql """
+        create table infer_predicate_reverse_relation_input (
+            k int not null, a int null, b int null, c int null
+        ) duplicate key(k) distributed by hash(k) buckets 1
+        properties("replication_num"="1")
+    """
+    sql """
+        insert into infer_predicate_reverse_relation_input values
+        (1,1,1,1),(2,2,2,2),(3,3,2,1),(4,1,2,3),
+        (5,null,1,1),(6,1,null,1),(7,1,1,null),(8,null,null,null)
+    """
+
+    // Both strict comparisons contradict the equalities. Inference must not 
admit a = b = c.
+    order_qt_strict """
+        select * from infer_predicate_reverse_relation_input
+        where a = c and c > a and c >= b and b > c and b = c
+    """
+    order_qt_reordered """
+        select * from infer_predicate_reverse_relation_input
+        where b = c and b > c and c >= b and c > a and a = c
+    """
+    order_qt_commuted """
+        select * from infer_predicate_reverse_relation_input
+        where c = a and a < c and b <= c and c < b and c = b
+    """
+    // Equal non-null rows do satisfy the non-strict variant.
+    order_qt_non_strict """
+        select * from infer_predicate_reverse_relation_input
+        where a = c and c >= a and c >= b and b >= c and b = c
+    """
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/infer_predicate/infer_timestamptz_cast.groovy
 
b/regression-test/suites/nereids_rules_p0/infer_predicate/infer_timestamptz_cast.groovy
new file mode 100644
index 00000000000..6325686623b
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/infer_predicate/infer_timestamptz_cast.groovy
@@ -0,0 +1,69 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("infer_timestamptz_cast", "p0") {
+    sql "DROP TABLE IF EXISTS infer_timestamptz_l"
+    sql "DROP TABLE IF EXISTS infer_timestamptz_r"
+    sql """
+        CREATE TABLE infer_timestamptz_l (id INT, tz TIMESTAMPTZ(6))
+        DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql """
+        CREATE TABLE infer_timestamptz_r (id INT, dt DATETIMEV2(3))
+        DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql """
+        INSERT INTO infer_timestamptz_l VALUES
+        (1, CAST('2024-11-03 06:30:00 +00:00' AS TIMESTAMPTZ(6))),
+        (2, CAST('2024-01-01 00:00:00.123600 +00:00' AS TIMESTAMPTZ(6)))
+    """
+    sql """
+        INSERT INTO infer_timestamptz_r VALUES
+        (10, '2024-11-03 01:30:00'), (20, '2024-01-01 00:00:00.124')
+    """
+    def originalTimeZone = sql "SELECT @@time_zone"
+    try {
+        sql "SET time_zone = 'America/New_York'"
+        // 05:30Z and 06:30Z both map to 01:30 during the fall-back overlap.
+        order_qt_dst_not_equal """
+            SELECT l.id, r.id FROM infer_timestamptz_l l JOIN 
infer_timestamptz_r r
+              ON CAST(l.tz AS DATETIMEV2(0)) = r.dt
+            WHERE NOT (l.tz = CAST('2024-11-03 05:30:00 +00:00' AS 
TIMESTAMPTZ(6)))
+        """
+        order_qt_dst_greater_than """
+            SELECT l.id, r.id FROM infer_timestamptz_l l JOIN 
infer_timestamptz_r r
+              ON CAST(l.tz AS DATETIMEV2(0)) = r.dt
+            WHERE l.tz > CAST('2024-11-03 05:30:00 +00:00' AS TIMESTAMPTZ(6))
+        """
+        sql "SET time_zone = '+00:00'"
+        // .123600 and .124000 become equal after rounding to milliseconds.
+        order_qt_scale_not_equal """
+            SELECT l.id, r.id FROM infer_timestamptz_l l JOIN 
infer_timestamptz_r r
+              ON CAST(l.tz AS DATETIMEV2(3)) = r.dt
+            WHERE NOT (l.tz = CAST('2024-01-01 00:00:00.124000 +00:00' AS 
TIMESTAMPTZ(6)))
+        """
+        order_qt_scale_less_than """
+            SELECT l.id, r.id FROM infer_timestamptz_l l JOIN 
infer_timestamptz_r r
+              ON CAST(l.tz AS DATETIMEV2(3)) = r.dt
+            WHERE l.tz < CAST('2024-01-01 00:00:00.124000 +00:00' AS 
TIMESTAMPTZ(6))
+        """
+    } finally {
+        sql "SET time_zone = '${originalTimeZone[0][0]}'"
+    }
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/infer_predicate_qualify.groovy 
b/regression-test/suites/nereids_rules_p0/infer_predicate_qualify.groovy
new file mode 100644
index 00000000000..e29879738d8
--- /dev/null
+++ b/regression-test/suites/nereids_rules_p0/infer_predicate_qualify.groovy
@@ -0,0 +1,54 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("infer_predicate_qualify") {
+    sql "drop table if exists infer_predicate_qualify_input"
+    sql """
+        create table infer_predicate_qualify_input (k bigint not null, a 
bigint not null, b bigint not null)
+        duplicate key(k) distributed by hash(k) buckets 1
+        properties("replication_num"="1")
+    """
+    sql "insert into infer_predicate_qualify_input values (1,2,1),(2,2,1)"
+
+    order_qt_strict """
+        select t.k, t.a, t.b, row_number() over (order by t.k) as rn
+        from infer_predicate_qualify_input t
+        qualify t.a > t.b and rn > t.b and t.a > rn
+    """
+    order_qt_non_strict """
+        select t.k, t.a, t.b, row_number() over (order by t.k) as rn
+        from infer_predicate_qualify_input t
+        qualify t.a >= t.b and rn >= t.b and t.a >= rn
+    """
+    order_qt_mixed """
+        select t.k, t.a, t.b, row_number() over (order by t.k) as rn
+        from infer_predicate_qualify_input t
+        qualify t.a >= t.b and rn >= t.b and t.a > rn
+    """
+    order_qt_reordered """
+        select t.k, t.a, t.b, row_number() over (order by t.k) as rn
+        from infer_predicate_qualify_input t
+        qualify t.a > rn and rn > t.b and t.a > t.b
+    """
+    // Include a row satisfying all strict comparisons, so preserving every 
row is also detected.
+    sql "insert into infer_predicate_qualify_input values (3,4,1)"
+    order_qt_matching_row """
+        select t.k, t.a, t.b, row_number() over (order by t.k) as rn
+        from infer_predicate_qualify_input t
+        qualify t.a > t.b and rn > t.b and t.a > rn
+    """
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/infer_predicate_replace_type.groovy 
b/regression-test/suites/nereids_rules_p0/infer_predicate_replace_type.groovy
new file mode 100644
index 00000000000..ada35647762
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/infer_predicate_replace_type.groovy
@@ -0,0 +1,89 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("infer_predicate_replace_type") {
+    sql "drop table if exists infer_replace_date_l"
+    sql """
+        create table infer_replace_date_l (id int not null, d date not null)
+        duplicate key(id) distributed by hash(id) buckets 1 
properties("replication_num"="1")
+    """
+    sql "drop table if exists infer_replace_date_r"
+    sql """
+        create table infer_replace_date_r (id int not null, ts datetime(0) not 
null)
+        duplicate key(id) distributed by hash(id) buckets 1 
properties("replication_num"="1")
+    """
+    sql "insert into infer_replace_date_l values (1,'2024-01-01')"
+    sql "insert into infer_replace_date_r values (10,'2024-01-01 00:00:00')"
+
+    order_qt_date_length """
+        select l.id, r.id from infer_replace_date_l l join 
infer_replace_date_r r on l.d = r.ts
+        where length(cast(l.d as string)) = 10
+    """
+    order_qt_datetime_length """
+        select l.id, r.id from infer_replace_date_l l join 
infer_replace_date_r r on l.d = r.ts
+        where length(cast(r.ts as string)) = 19
+    """
+    order_qt_date_same_type """
+        select l.id, r.id from infer_replace_date_l l join 
infer_replace_date_l r on l.d = r.d
+        where length(cast(l.d as string)) = 10
+    """
+    order_qt_date_comparison """
+        select l.id, r.id from infer_replace_date_l l join 
infer_replace_date_r r on l.d = r.ts
+        where l.d > cast('2023-12-31' as date)
+    """
+
+    sql "drop table if exists infer_replace_fp"
+    sql """
+        create table infer_replace_fp (id bigint not null, x double not null, 
y double not null)
+        duplicate key(id) distributed by hash(id) buckets 1 
properties("replication_num"="1")
+    """
+    sql """
+        insert into infer_replace_fp values
+        (1,cast('-0.0' as double),cast('0.0' as double)),
+        (2,cast('0.0' as double),cast('-0.0' as double)),
+        (3,2,2),(4,0,0),(5,-2,-2)
+    """
+    order_qt_signed_zero_facts """
+        select id, x = y, signbit(x), signbit(y) from infer_replace_fp
+    """
+    order_qt_signed_zero_or """
+        select id from infer_replace_fp where x = y and (signbit(x) or x > 
cast(1 as double))
+    """
+    order_qt_signed_zero_or_reversed """
+        select id from infer_replace_fp where x = y and (signbit(y) or y > 
cast(1 as double))
+    """
+    order_qt_float_comparison """
+        select id from infer_replace_fp where x = y and x > cast(1 as double)
+    """
+
+    sql "drop table if exists infer_replace_decimal_l"
+    sql """
+        create table infer_replace_decimal_l (id int not null, d decimal(9,2) 
not null)
+        duplicate key(id) distributed by hash(id) buckets 1 
properties("replication_num"="1")
+    """
+    sql "drop table if exists infer_replace_decimal_r"
+    sql """
+        create table infer_replace_decimal_r (id int not null, d decimal(9,3) 
not null)
+        duplicate key(id) distributed by hash(id) buckets 1 
properties("replication_num"="1")
+    """
+    sql "insert into infer_replace_decimal_l values (1,1.20)"
+    sql "insert into infer_replace_decimal_r values (10,1.200)"
+    order_qt_decimal_scale """
+        select l.id, r.id from infer_replace_decimal_l l join 
infer_replace_decimal_r r on l.d = r.d
+        where length(cast(l.d as string)) = 4
+    """
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to