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 3d4c1d391b3 [fix](join) Bound outer join null-reject inference to 
nullable outputs (#65250)
3d4c1d391b3 is described below

commit 3d4c1d391b37d60900c405f0713c565e32c3e6e8
Author: foxtail463 <[email protected]>
AuthorDate: Sat Aug 1 11:56:58 2026 +0800

    [fix](join) Bound outer join null-reject inference to nullable outputs 
(#65250)
    
    related PR: #63318
    
    Problem Summary:
    Outer join elimination uses fold-based null-reject inference to decide 
whether
    nullable-side rows from the current outer join can be filtered away. This 
check
    only needs slots from that join's nullable-side outputs, but the previous 
flow
    could also test unrelated predicate inputs. When a filter predicate 
references a
    mark slot produced by another join, folding that slot is useless for 
eliminating
    the current outer join and can be costly when the fixed-point rewrite batch
    revisits the same predicate multiple times.
    
    Solution:
    Add an API to infer null-rejecting slots only for a given target slot set, 
and
    use the current join's nullable-side outputs as that target in
    EliminateOuterJoin. Skip mark-join slots as inference targets, preserve the
    existing expression complexity limits, and avoid rewriting when the join 
type
    does not change.
    
    ---------
    
    Co-authored-by: yangtao555 <[email protected]>
---
 .../nereids/rules/rewrite/EliminateOuterJoin.java  |  35 +++---
 .../nereids/rules/rewrite/InferAggNotNull.java     |   3 +-
 .../apache/doris/nereids/trees/plans/JoinType.java |  14 +++
 .../nereids/trees/plans/logical/LogicalJoin.java   |  16 +++
 .../apache/doris/nereids/util/ExpressionUtils.java |  87 ++++++---------
 .../rules/rewrite/EliminateOuterJoinTest.java      | 122 +++++++++++++++++++++
 .../nereids/rules/rewrite/InferAggNotNullTest.java |  55 ++++++++++
 .../filter_push_down/push_filter_through.out       |   8 +-
 .../eliminate_outer_join/eliminate_outer_join.out  |  19 ++--
 .../eliminate_outer_join.groovy                    |   2 +-
 10 files changed, 273 insertions(+), 88 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateOuterJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateOuterJoin.java
index 435d280ccf6..ac03ef50514 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateOuterJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateOuterJoin.java
@@ -28,17 +28,14 @@ import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.plans.JoinType;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.util.ExpressionUtils;
 import org.apache.doris.nereids.util.JoinUtils;
-import org.apache.doris.nereids.util.TypeUtils;
 import org.apache.doris.nereids.util.Utils;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSet.Builder;
 import com.google.common.collect.Sets;
 
 import java.util.Collection;
-import java.util.HashSet;
-import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -50,19 +47,11 @@ public class EliminateOuterJoin extends 
OneRewriteRuleFactory {
     public Rule build() {
         return logicalFilter(
                 logicalJoin().when(join -> join.getJoinType().isOuterJoin() || 
join.getJoinType().isAsofOuterJoin())
-        ).then(filter -> {
-            LogicalJoin<Plan, Plan> join = filter.child();
+        ).thenApply(ctx -> {
+            LogicalJoin<Plan, Plan> join = ctx.root.child();
 
-            Builder<Expression> conjunctsBuilder = ImmutableSet.builder();
-            Set<Slot> notNullSlots = new HashSet<>();
-            for (Expression predicate : filter.getConjuncts()) {
-                Optional<Slot> notNullSlot = TypeUtils.isNotNull(predicate);
-                if (notNullSlot.isPresent()) {
-                    notNullSlots.add(notNullSlot.get());
-                } else {
-                    conjunctsBuilder.add(predicate);
-                }
-            }
+            Set<Slot> notNullSlots = ExpressionUtils.inferNotNullSlots(
+                    ctx.root.getConjuncts(), join.getNullableSideOutput(), 
ctx.cascadesContext);
             boolean canFilterLeftNull = 
Utils.isIntersecting(join.left().getOutputSet(), notNullSlots);
             boolean canFilterRightNull = 
Utils.isIntersecting(join.right().getOutputSet(), notNullSlots);
             if (!canFilterRightNull && !canFilterLeftNull) {
@@ -70,8 +59,14 @@ public class EliminateOuterJoin extends 
OneRewriteRuleFactory {
             }
 
             JoinType newJoinType = tryEliminateOuterJoin(join.getJoinType(), 
canFilterLeftNull, canFilterRightNull);
+            // Nothing changed: avoid adding redundant generated `IS NOT NULL` 
markers and
+            // returning a structurally-equivalent plan, which would otherwise 
cause pointless
+            // re-rewrite churn inside the PUSH_DOWN_FILTERS fixed-point.
+            if (newJoinType == join.getJoinType()) {
+                return null;
+            }
             Set<Expression> conjuncts = Sets.newHashSet();
-            conjuncts.addAll(filter.getConjuncts());
+            conjuncts.addAll(ctx.root.getConjuncts());
             boolean conjunctsChanged = false;
             if (!notNullSlots.isEmpty()) {
                 for (Slot slot : notNullSlots) {
@@ -86,8 +81,6 @@ public class EliminateOuterJoin extends OneRewriteRuleFactory 
{
                  * by which the left outer join could be eliminated. Finally, 
the join transformed to
                  * (A join B on A.a=B.b) join C on B.x=C.x.
                  * This elimination can be processed recursively.
-                 *
-                 * TODO: is_not_null can also be inferred from A < B and so on
                  */
                 conjunctsChanged |= join.getEqualToConjuncts().stream()
                         .map(EqualTo.class::cast)
@@ -105,10 +98,10 @@ public class EliminateOuterJoin extends 
OneRewriteRuleFactory {
                         .anyMatch(equalTo -> 
createIsNotNullIfNecessary(equalTo, conjuncts));
             }
             if (conjunctsChanged) {
-                return 
filter.withConjuncts(conjuncts.stream().collect(ImmutableSet.toImmutableSet()))
+                return 
ctx.root.withConjuncts(conjuncts.stream().collect(ImmutableSet.toImmutableSet()))
                         .withChildren(join.withJoinTypeAndContext(newJoinType, 
join.getJoinReorderContext()));
             }
-            return 
filter.withChildren(join.withJoinTypeAndContext(newJoinType, 
join.getJoinReorderContext()));
+            return 
ctx.root.withChildren(join.withJoinTypeAndContext(newJoinType, 
join.getJoinReorderContext()));
         }).toRule(RuleType.ELIMINATE_OUTER_JOIN);
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNull.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNull.java
index 4daf320811a..e27a7b8cfad 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNull.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNull.java
@@ -108,8 +108,7 @@ public class InferAggNotNull extends OneRewriteRuleFactory {
 
     private boolean canInferFunctionNotNull(AggregateFunction 
aggregateFunction) {
         return isSupportedAggregateFunction(aggregateFunction)
-                && !aggregateFunction.children().isEmpty()
-                && 
ExpressionUtils.isCheapEnoughToInferNotNull(aggregateFunction.children());
+                && !aggregateFunction.children().isEmpty();
     }
 
     private boolean isSupportedAggregateFunction(AggregateFunction 
aggregateFunction) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/JoinType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/JoinType.java
index b1fcf9bd460..9d91d3fc87a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/JoinType.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/JoinType.java
@@ -253,6 +253,20 @@ public enum JoinType {
         return this == ASOF_LEFT_OUTER_JOIN || this == ASOF_RIGHT_OUTER_JOIN;
     }
 
+    /**
+     * Whether this join may null-extend slots from its left child.
+     */
+    public final boolean isLeftSideNullable() {
+        return isRightOuterJoin() || isAsofRightOuterJoin() || 
isFullOuterJoin();
+    }
+
+    /**
+     * Whether this join may null-extend slots from its right child.
+     */
+    public final boolean isRightSideNullable() {
+        return isLeftOuterJoin() || isAsofLeftOuterJoin() || isFullOuterJoin();
+    }
+
     public final boolean isAsofLeftJoin() {
         return this == ASOF_LEFT_INNER_JOIN || this == ASOF_LEFT_OUTER_JOIN;
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalJoin.java
index 666ac5b95dd..5321b9c097a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalJoin.java
@@ -287,6 +287,22 @@ public class LogicalJoin<LEFT_CHILD_TYPE extends Plan, 
RIGHT_CHILD_TYPE extends
         return joinReorderContext;
     }
 
+    /**
+     * Output slots of the null-extended side(s) of an outer join, i.e. the 
slots that
+     * this join may turn into NULL. Rejecting NULL on them lets an outer join 
be eliminated
+     * or weakened. Returns empty for non-outer joins.
+     */
+    public Set<Slot> getNullableSideOutput() {
+        ImmutableSet.Builder<Slot> nullableSide = ImmutableSet.builder();
+        if (joinType.isLeftSideNullable()) {
+            nullableSide.addAll(left().getOutputSet());
+        }
+        if (joinType.isRightSideNullable()) {
+            nullableSide.addAll(right().getOutputSet());
+        }
+        return nullableSide.build();
+    }
+
     @Override
     public List<Slot> computeOutput() {
         return ImmutableList.<Slot>builder()
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java
index fd7e761bc6e..474909db546 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java
@@ -752,62 +752,59 @@ public class ExpressionUtils {
      * infer notNulls slot from predicate
      */
     public static Set<Slot> inferNotNullSlots(Set<Expression> predicates, 
CascadesContext cascadesContext) {
-        ImmutableSet.Builder<Slot> notNullSlots = 
ImmutableSet.builderWithExpectedSize(predicates.size());
-        for (Expression predicate : 
filterCheapPredicatesForNotNull(predicates)) {
+        Set<Slot> targetSlots = new HashSet<>();
+        for (Expression predicate : predicates) {
             for (Slot slot : predicate.getInputSlots()) {
-                Map<Expression, Expression> replaceMap = new HashMap<>();
-                Literal nullLiteral = new NullLiteral(slot.getDataType());
-                replaceMap.put(slot, nullLiteral);
-                Expression evalExpr = FoldConstantRule.evaluate(
-                        ExpressionUtils.replace(predicate, replaceMap),
-                        new ExpressionRewriteContext(cascadesContext));
-                if (evalExpr.isNullLiteral() || 
BooleanLiteral.FALSE.equals(evalExpr)) {
-                    notNullSlots.add(slot);
+                if (!(slot instanceof MarkJoinSlotReference)) {
+                    targetSlots.add(slot);
                 }
             }
         }
-        return notNullSlots.build();
+        return inferNotNullSlots(predicates, targetSlots, cascadesContext);
     }
 
     /**
-     * Return whether all predicates are cheap enough for not-null inference.
+     * infer notNulls slot from predicate but these slots must be in the given 
target slots.
      */
-    public static boolean isCheapEnoughToInferNotNull(Collection<? extends 
Expression> predicates) {
+    public static Set<Slot> inferNotNullSlots(Set<Expression> predicates, 
Set<Slot> targetSlots,
+            CascadesContext cascadesContext) {
+        ImmutableSet.Builder<Slot> notNullSlots = 
ImmutableSet.builderWithExpectedSize(targetSlots.size());
         Set<Slot> inputSlots = new HashSet<>();
         for (Expression predicate : predicates) {
-            Optional<Set<Slot>> mergedInputSlots = 
mergeInputSlotsIfCheap(predicate, inputSlots);
-            if (!mergedInputSlots.isPresent()) {
-                return false;
+            if (predicate.getWidth() > MAX_INFER_NOT_NULL_EXPR_WIDTH
+                    || predicate.getDepth() > MAX_INFER_NOT_NULL_EXPR_DEPTH) {
+                continue;
             }
-            inputSlots = mergedInputSlots.get();
-        }
-        return true;
-    }
-
-    /**
-     * Filter predicates that are cheap enough for not-null inference.
-     */
-    public static Set<Expression> filterCheapPredicatesForNotNull(
-            Collection<? extends Expression> predicates) {
-        Set<Slot> inputSlots = new HashSet<>();
-        Set<Expression> cheapPredicates = Sets.newLinkedHashSet();
-        for (Expression predicate : predicates) {
-            Optional<Set<Slot>> mergedInputSlots = 
mergeInputSlotsIfCheap(predicate, inputSlots);
+            Set<Slot> predicateInputSlots = predicate.getInputSlots();
+            Set<Slot> candidateSlots = Sets.intersection(predicateInputSlots, 
targetSlots);
+            if (candidateSlots.isEmpty()) {
+                continue;
+            }
+            Optional<Set<Slot>> mergedInputSlots = 
mergeInputSlotsWithinLimit(inputSlots, predicateInputSlots);
             if (!mergedInputSlots.isPresent()) {
                 continue;
             }
             inputSlots = mergedInputSlots.get();
-            cheapPredicates.add(predicate);
+            for (Slot slot : candidateSlots) {
+                if (isNullRejecting(predicate, slot, cascadesContext)) {
+                    notNullSlots.add(slot);
+                }
+            }
         }
-        return cheapPredicates;
+        return notNullSlots.build();
     }
 
-    private static Optional<Set<Slot>> mergeInputSlotsIfCheap(Expression 
predicate, Set<Slot> inputSlots) {
-        if (predicate.getWidth() > MAX_INFER_NOT_NULL_EXPR_WIDTH
-                || predicate.getDepth() > MAX_INFER_NOT_NULL_EXPR_DEPTH) {
-            return Optional.empty();
-        }
-        Set<Slot> predicateInputSlots = predicate.getInputSlots();
+    private static boolean isNullRejecting(Expression predicate, Slot slot, 
CascadesContext cascadesContext) {
+        Map<Expression, Expression> replaceMap = new HashMap<>();
+        Literal nullLiteral = new NullLiteral(slot.getDataType());
+        replaceMap.put(slot, nullLiteral);
+        Expression evalExpr = FoldConstantRule.evaluate(
+                ExpressionUtils.replace(predicate, replaceMap),
+                new ExpressionRewriteContext(cascadesContext));
+        return evalExpr.isNullLiteral() || 
BooleanLiteral.FALSE.equals(evalExpr);
+    }
+
+    private static Optional<Set<Slot>> mergeInputSlotsWithinLimit(Set<Slot> 
inputSlots, Set<Slot> predicateInputSlots) {
         if (predicateInputSlots.size() > MAX_INFER_NOT_NULL_INPUT_SLOTS) {
             return Optional.empty();
         }
@@ -830,20 +827,6 @@ public class ExpressionUtils {
         return newPredicates.build();
     }
 
-    /**
-     * infer notNulls slot from predicate but these slots must be in the given 
slots.
-     */
-    public static Set<Expression> inferNotNull(Set<Expression> predicates, 
Set<Slot> slots,
-            CascadesContext cascadesContext) {
-        ImmutableSet.Builder<Expression> newPredicates = 
ImmutableSet.builderWithExpectedSize(predicates.size());
-        for (Slot slot : inferNotNullSlots(predicates, cascadesContext)) {
-            if (slots.contains(slot)) {
-                newPredicates.add(new Not(new IsNull(slot), true));
-            }
-        }
-        return newPredicates.build();
-    }
-
     public static boolean isGeneratedNotNull(Expression expression) {
         return expression instanceof Not
                 && ((Not) expression).isGeneratedIsNotNull()
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOuterJoinTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOuterJoinTest.java
index 850c31a14ab..c16fbd870f1 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOuterJoinTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOuterJoinTest.java
@@ -20,7 +20,11 @@ package org.apache.doris.nereids.rules.rewrite;
 import org.apache.doris.common.Pair;
 import org.apache.doris.nereids.trees.expressions.And;
 import org.apache.doris.nereids.trees.expressions.GreaterThan;
+import org.apache.doris.nereids.trees.expressions.InPredicate;
+import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.Or;
 import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
+import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
 import org.apache.doris.nereids.trees.plans.JoinType;
@@ -32,6 +36,7 @@ import org.apache.doris.nereids.util.MemoTestUtils;
 import org.apache.doris.nereids.util.PlanChecker;
 import org.apache.doris.nereids.util.PlanConstructor;
 
+import com.google.common.collect.ImmutableList;
 import org.junit.jupiter.api.Test;
 
 import java.util.Objects;
@@ -53,6 +58,24 @@ class EliminateOuterJoinTest implements 
MemoPatternMatchSupported {
         testEliminateLeftHelper(JoinType.ASOF_LEFT_OUTER_JOIN);
     }
 
+    @Test
+    void testEliminateLeftByInPredicateOrFalse() {
+        LogicalPlan plan = new LogicalPlanBuilder(scan1)
+                .join(scan2, JoinType.LEFT_OUTER_JOIN, Pair.of(0, 0))  // 
t1.id = t2.id
+                .filter(new Or(new InPredicate(scan2.getOutput().get(0),
+                        ImmutableList.of(new IntegerLiteral(1), new 
IntegerLiteral(2))), BooleanLiteral.FALSE))
+                .build();
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
+                .applyTopDown(new EliminateOuterJoin())
+                .applyTopDown(new EliminateNotNull())
+                .matchesFromRoot(
+                        logicalFilter(
+                                logicalJoin().when(join -> 
join.getJoinType().isInnerJoin()))
+                                .when(filter -> filter.getConjuncts().size() 
== 1)
+                );
+    }
+
     private void testEliminateLeftHelper(JoinType joinType) {
         LogicalPlan plan = new LogicalPlanBuilder(scan1)
                 .join(scan2, joinType, Pair.of(0, 0))  // t1.id = t2.id
@@ -119,4 +142,103 @@ class EliminateOuterJoinTest implements 
MemoPatternMatchSupported {
                         ).when(filter -> filter.getConjuncts().size() == 2)
                 );
     }
+
+    /**
+     * FULL OUTER -> LEFT OUTER when only the left side is null-rejecting.
+     */
+    @Test
+    void testFullOuterDegradeToLeft() {
+        LogicalPlan plan = new LogicalPlanBuilder(scan1)
+                .join(scan2, JoinType.FULL_OUTER_JOIN, Pair.of(0, 0))
+                .filter(new GreaterThan(scan1.getOutput().get(0), new 
IntegerLiteral(1)))
+                .build();
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
+                .applyTopDown(new EliminateOuterJoin())
+                .matchesFromRoot(
+                        logicalFilter(
+                                logicalJoin().when(join -> 
join.getJoinType().isLeftOuterJoin()))
+                );
+    }
+
+    /**
+     * FULL OUTER -> RIGHT OUTER when only the right side is null-rejecting.
+     */
+    @Test
+    void testFullOuterDegradeToRight() {
+        LogicalPlan plan = new LogicalPlanBuilder(scan1)
+                .join(scan2, JoinType.FULL_OUTER_JOIN, Pair.of(0, 0))
+                .filter(new GreaterThan(scan2.getOutput().get(0), new 
IntegerLiteral(1)))
+                .build();
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
+                .applyTopDown(new EliminateOuterJoin())
+                .matchesFromRoot(
+                        logicalFilter(
+                                logicalJoin().when(join -> 
join.getJoinType().isRightOuterJoin()))
+                );
+    }
+
+    /**
+     * LEFT OUTER stays LEFT OUTER when the filter only constrains the 
preserved (left) side.
+     * Without the early-return guard, the rule would still rewrite the plan 
with redundant
+     * generated IS NOT NULL markers and trigger pointless re-rewrite churn.
+     */
+    @Test
+    void testLeftOuterNotChangedByLeftSidePredicate() {
+        LogicalPlan plan = new LogicalPlanBuilder(scan1)
+                .join(scan2, JoinType.LEFT_OUTER_JOIN, Pair.of(0, 0))
+                .filter(new GreaterThan(scan1.getOutput().get(0), new 
IntegerLiteral(1)))
+                .build();
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
+                .applyTopDown(new EliminateOuterJoin())
+                .matchesFromRoot(
+                        logicalFilter(
+                                logicalJoin().when(join -> 
join.getJoinType().isLeftOuterJoin()))
+                                .when(filter -> filter.getConjuncts().size() 
== 1)
+                );
+    }
+
+    /**
+     * `t2.b IS NULL OR t2.b > 0` is NOT null-rejecting on t2.b (the IS NULL 
branch keeps NULLs),
+     * so LEFT OUTER must stay LEFT OUTER.
+     */
+    @Test
+    void testLeftOuterNotEliminatedByIsNullOr() {
+        LogicalPlan plan = new LogicalPlanBuilder(scan1)
+                .join(scan2, JoinType.LEFT_OUTER_JOIN, Pair.of(0, 0))
+                .filter(new Or(
+                        new IsNull(scan2.getOutput().get(0)),
+                        new GreaterThan(scan2.getOutput().get(0), new 
IntegerLiteral(0))))
+                .build();
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
+                .applyTopDown(new EliminateOuterJoin())
+                .matchesFromRoot(
+                        logicalFilter(
+                                logicalJoin().when(join -> 
join.getJoinType().isLeftOuterJoin()))
+                );
+    }
+
+    /**
+     * A predicate that mixes both sides via OR (`t1.a > 0 OR t2.b > 0`) is 
NOT null-rejecting
+     * on either side individually, because one branch may stay TRUE when the 
other side is NULL.
+     */
+    @Test
+    void testLeftOuterNotEliminatedByCrossSideOr() {
+        LogicalPlan plan = new LogicalPlanBuilder(scan1)
+                .join(scan2, JoinType.LEFT_OUTER_JOIN, Pair.of(0, 0))
+                .filter(new Or(
+                        new GreaterThan(scan1.getOutput().get(0), new 
IntegerLiteral(0)),
+                        new GreaterThan(scan2.getOutput().get(0), new 
IntegerLiteral(0))))
+                .build();
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
+                .applyTopDown(new EliminateOuterJoin())
+                .matchesFromRoot(
+                        logicalFilter(
+                                logicalJoin().when(join -> 
join.getJoinType().isLeftOuterJoin()))
+                );
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNullTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNullTest.java
index 23b108c8347..feea96e0f4d 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNullTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferAggNotNullTest.java
@@ -17,12 +17,21 @@
 
 package org.apache.doris.nereids.rules.rewrite;
 
+import org.apache.doris.catalog.AggregateType;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.KeysType;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.PartitionInfo;
+import org.apache.doris.catalog.Type;
 import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.IsNull;
 import org.apache.doris.nereids.trees.expressions.Not;
 import 
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Avg;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
+import org.apache.doris.nereids.trees.plans.RelationId;
 import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
@@ -31,11 +40,14 @@ import 
org.apache.doris.nereids.util.MemoPatternMatchSupported;
 import org.apache.doris.nereids.util.MemoTestUtils;
 import org.apache.doris.nereids.util.PlanChecker;
 import org.apache.doris.nereids.util.PlanConstructor;
+import org.apache.doris.thrift.TStorageType;
 
 import com.google.common.collect.ImmutableList;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 class InferAggNotNullTest implements MemoPatternMatchSupported {
@@ -130,6 +142,37 @@ class InferAggNotNullTest implements 
MemoPatternMatchSupported {
                 );
     }
 
+    @Test
+    void testInferPartialWhenArgsExceedSlotLimit() {
+        // count(distinct c0..c32) has 33 nullable arguments. inferNotNull 
merges input slots up to
+        // a 32-slot limit, so the first 32 arguments get a generated IS NOT 
NULL and the 33rd is
+        // skipped. This partial-inference path is only reachable after the 
all-children cheapness
+        // gate was removed from InferAggNotNull.
+        LogicalOlapScan wideScan = newWideNullableScan(33);
+        List<Expression> args = new ArrayList<>(wideScan.getOutput());
+
+        LogicalPlan plan = new LogicalPlanBuilder(wideScan)
+                .aggGroupUsingIndex(ImmutableList.of(),
+                        ImmutableList.of(new Alias(
+                                new Count(true, args.get(0), args.subList(1, 
33).toArray(new Expression[0])),
+                                "cnt")))
+                .build();
+
+        PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
+                .applyTopDown(new InferAggNotNull())
+                .matches(
+                        logicalAggregate(
+                                logicalFilter().when(filter -> {
+                                    Set<Expression> conjuncts = 
filter.getConjuncts();
+                                    return conjuncts.size() == 32
+                                            && conjuncts.stream().allMatch(e 
-> e instanceof Not
+                                                    && ((Not) 
e).isGeneratedIsNotNull()
+                                                    && ((Not) e).child() 
instanceof IsNull);
+                                })
+                        )
+                );
+    }
+
     @Test
     void testGetAggregateFunctionsStopsAtAggregateFunction() {
         // Use different agg function types for inner (Avg) and outer (Count),
@@ -147,4 +190,16 @@ class InferAggNotNullTest implements 
MemoPatternMatchSupported {
         Assertions.assertTrue(aggregateFunctions.stream().allMatch(f -> f 
instanceof Count),
                 "should collect only the outer Count, got: " + 
aggregateFunctions);
     }
+
+    private LogicalOlapScan newWideNullableScan(int columnCount) {
+        List<Column> columns = new ArrayList<>(columnCount);
+        for (int i = 0; i < columnCount; i++) {
+            columns.add(new Column("c" + i, Type.INT, false, 
AggregateType.NONE, true, "", ""));
+        }
+        OlapTable table = new OlapTable(100L, "wide", columns,
+                KeysType.DUP_KEYS, new PartitionInfo(), null);
+        table.setIndexMeta(-1, "wide", table.getFullSchema(), 0, 0, (short) 0,
+                TStorageType.COLUMN, KeysType.DUP_KEYS);
+        return new LogicalOlapScan(RelationId.createGenerator().getNextId(), 
table, ImmutableList.of("db"));
+    }
 }
diff --git 
a/regression-test/data/nereids_rules_p0/filter_push_down/push_filter_through.out
 
b/regression-test/data/nereids_rules_p0/filter_push_down/push_filter_through.out
index d04e95189da..f7d67dc0b87 100644
--- 
a/regression-test/data/nereids_rules_p0/filter_push_down/push_filter_through.out
+++ 
b/regression-test/data/nereids_rules_p0/filter_push_down/push_filter_through.out
@@ -169,11 +169,13 @@ PhysicalResultSink
 
 -- !filter_mixed_inner_left --
 PhysicalResultSink
---filter((t3.id = 2))
-----hashJoin[LEFT_OUTER_JOIN] hashCondition=((t1.id = t3.id)) otherCondition=()
-------hashJoin[INNER_JOIN] hashCondition=((t1.id = t2.id)) otherCondition=()
+--hashJoin[INNER_JOIN] hashCondition=((t1.id = t3.id)) otherCondition=()
+----hashJoin[INNER_JOIN] hashCondition=((t1.id = t2.id)) otherCondition=()
+------filter(( not t1.id IS NULL) and (t1.id = 2))
 --------PhysicalOlapScan[t1]
+------filter((t2.id = 2))
 --------PhysicalOlapScan[t2]
+----filter(( not t3.id IS NULL) and (t3.id = 2))
 ------PhysicalOlapScan[t3]
 
 -- !filter_multi_left --
diff --git 
a/regression-test/data/query_p0/eliminate_outer_join/eliminate_outer_join.out 
b/regression-test/data/query_p0/eliminate_outer_join/eliminate_outer_join.out
index 6a94a1cf100..51b6ca56bc1 100644
--- 
a/regression-test/data/query_p0/eliminate_outer_join/eliminate_outer_join.out
+++ 
b/regression-test/data/query_p0/eliminate_outer_join/eliminate_outer_join.out
@@ -20,11 +20,12 @@ SyntaxError:
 -- !2 --
 PhysicalResultSink
 --hashJoin[RIGHT_OUTER_JOIN] hashCondition=((partsupp.ps_suppkey = 
supplier.s_suppkey)) otherCondition=()
-----filter((supplier.s_suppkey > 1))
-------hashJoin[FULL_OUTER_JOIN] hashCondition=((nation.n_nationkey = 
supplier.s_suppkey)) otherCondition=()
---------hashJoin[FULL_OUTER_JOIN] hashCondition=((region.r_regionkey = 
nation.n_regionkey)) otherCondition=()
-----------PhysicalOlapScan[region]
+----hashJoin[RIGHT_OUTER_JOIN] hashCondition=((nation.n_nationkey = 
supplier.s_suppkey)) otherCondition=()
+------hashJoin[RIGHT_OUTER_JOIN] hashCondition=((region.r_regionkey = 
nation.n_regionkey)) otherCondition=()
+--------PhysicalOlapScan[region]
+--------filter(( not nation.n_nationkey IS NULL) and (nation.n_nationkey > 1))
 ----------PhysicalOlapScan[nation]
+------filter(( not supplier.s_suppkey IS NULL) and (supplier.s_suppkey > 1))
 --------PhysicalOlapScan[supplier]
 ----filter(( not partsupp.ps_suppkey IS NULL) and (partsupp.ps_suppkey > 1))
 ------PhysicalOlapScan[partsupp]
@@ -37,11 +38,12 @@ SyntaxError:
 -- !3 --
 PhysicalResultSink
 --hashJoin[RIGHT_OUTER_JOIN] hashCondition=((partsupp.ps_suppkey = 
supplier.s_suppkey)) otherCondition=()
-----filter((supplier.s_suppkey > 1))
-------hashJoin[LEFT_OUTER_JOIN] hashCondition=((nation.n_nationkey = 
supplier.s_suppkey)) otherCondition=()
---------hashJoin[FULL_OUTER_JOIN] hashCondition=((region.r_regionkey = 
nation.n_regionkey)) otherCondition=()
-----------PhysicalOlapScan[region]
+----hashJoin[INNER_JOIN] hashCondition=((nation.n_nationkey = 
supplier.s_suppkey)) otherCondition=()
+------hashJoin[RIGHT_OUTER_JOIN] hashCondition=((region.r_regionkey = 
nation.n_regionkey)) otherCondition=()
+--------PhysicalOlapScan[region]
+--------filter(( not nation.n_nationkey IS NULL) and (nation.n_nationkey > 1))
 ----------PhysicalOlapScan[nation]
+------filter(( not supplier.s_suppkey IS NULL) and (supplier.s_suppkey > 1))
 --------PhysicalOlapScan[supplier]
 ----filter(( not partsupp.ps_suppkey IS NULL) and (partsupp.ps_suppkey > 1))
 ------PhysicalOlapScan[partsupp]
@@ -143,4 +145,3 @@ Hint log:
 Used: [broadcast]_1
 UnUsed:
 SyntaxError:
-
diff --git 
a/regression-test/suites/query_p0/eliminate_outer_join/eliminate_outer_join.groovy
 
b/regression-test/suites/query_p0/eliminate_outer_join/eliminate_outer_join.groovy
index 9f83b850fe8..177496d1a00 100644
--- 
a/regression-test/suites/query_p0/eliminate_outer_join/eliminate_outer_join.groovy
+++ 
b/regression-test/suites/query_p0/eliminate_outer_join/eliminate_outer_join.groovy
@@ -123,7 +123,7 @@ suite("eliminate_outer_join") {
     where ps_suppkey > 1
     '''
 
-    // full join ps => right join ps, other outer joins are not eliminated
+    // The filter on partsupp null-rejects the right side of each cascading 
full join.
     qt_2 '''
     explain shape plan
     select * 


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

Reply via email to