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

englefly 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 67019ff97ed [fix](topnFilter)Fix TopN filter probe expressions to wrap 
nullable slots when pushed through outer joins. (#59074)
67019ff97ed is described below

commit 67019ff97ed8f31dc96334d69a3257f45a7e3e41
Author: minghong <[email protected]>
AuthorDate: Fri Feb 13 14:57:50 2026 +0800

    [fix](topnFilter)Fix TopN filter probe expressions to wrap nullable slots 
when pushed through outer joins. (#59074)
    
    ### What problem does this PR solve?
    Fix TopN filter probe expressions to wrap nullable slots when pushed 
through outer joins. (#59074)
---
 .../processor/post/TopnFilterPushDownVisitor.java  | 32 +++++++++++--
 .../nereids/postprocess/TopNRuntimeFilterTest.java | 53 ++++++++++++++++++++++
 2 files changed, 82 insertions(+), 3 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterPushDownVisitor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterPushDownVisitor.java
index 101b29d91f9..bf21d5baf9e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterPushDownVisitor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterPushDownVisitor.java
@@ -21,6 +21,8 @@ import 
org.apache.doris.nereids.processor.post.TopnFilterPushDownVisitor.PushDow
 import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Nullable;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitors;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.algebra.TopN;
@@ -42,6 +44,7 @@ import 
org.apache.doris.nereids.trees.plans.physical.PhysicalSetOperation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalWindow;
 import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.util.ExpressionUtils;
 import org.apache.doris.qe.ConnectContext;
 
 import com.google.common.collect.Maps;
@@ -169,6 +172,26 @@ public class TopnFilterPushDownVisitor extends 
PlanVisitor<Boolean, PushDownCont
         return false;
     }
 
+    private PushDownContext adjustProbeExprNullableThroughOuterJoin(Plan 
leftChild, PushDownContext ctx) {
+        Expression probeExpr = ctx.probeExpr;
+        Map<Expression, Expression> replaceMap = Maps.newHashMap();
+        boolean changed = false;
+        for (Slot probSlot : probeExpr.getInputSlots()) {
+            for (Slot childSlot : leftChild.getOutput()) {
+                if (probSlot.getExprId().asInt() == 
childSlot.getExprId().asInt()
+                        && probSlot.nullable() && !childSlot.nullable()) {
+                    replaceMap.put(probSlot, new Nullable(childSlot));
+                    changed = true;
+                    break;
+                }
+            }
+        }
+        if (changed) {
+            return 
ctx.withNewProbeExpression(ExpressionUtils.replace(probeExpr, replaceMap));
+        }
+        return ctx;
+    }
+
     @Override
     public Boolean visitPhysicalHashJoin(PhysicalHashJoin<? extends Plan, ? 
extends Plan> join,
             PushDownContext ctx) {
@@ -180,7 +203,8 @@ public class TopnFilterPushDownVisitor extends 
PlanVisitor<Boolean, PushDownCont
             return false;
         }
         if 
(join.left().getOutputSet().containsAll(ctx.probeExpr.getInputSlots())) {
-            return join.left().accept(this, ctx);
+            PushDownContext childPushDownContext = 
adjustProbeExprNullableThroughOuterJoin(join.left(), ctx);
+            return join.left().accept(this, childPushDownContext);
         }
         if 
(join.right().getOutputSet().containsAll(ctx.probeExpr.getInputSlots())) {
             // expand expr to the other side of hash join condition:
@@ -189,8 +213,10 @@ public class TopnFilterPushDownVisitor extends 
PlanVisitor<Boolean, PushDownCont
             for (Expression conj : join.getHashJoinConjuncts()) {
                 if (ctx.probeExpr.equals(conj.child(1))) {
                     // push to left child. right child is blocking operator, 
do not need topn-filter
-                    PushDownContext newCtx = 
ctx.withNewProbeExpression(conj.child(0));
-                    return join.left().accept(this, newCtx);
+                    PushDownContext ctxOnLeftChild = 
ctx.withNewProbeExpression(conj.child(0));
+                    PushDownContext childPushDownContext = 
adjustProbeExprNullableThroughOuterJoin(join.left(),
+                            ctxOnLeftChild);
+                    return join.left().accept(this, childPushDownContext);
                 }
             }
         }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopNRuntimeFilterTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopNRuntimeFilterTest.java
index 261ac8a813e..41c7001f792 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopNRuntimeFilterTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/TopNRuntimeFilterTest.java
@@ -19,16 +19,26 @@ package org.apache.doris.nereids.postprocess;
 
 import org.apache.doris.nereids.datasets.ssb.SSBTestBase;
 import org.apache.doris.nereids.processor.post.PlanPostProcessors;
+import org.apache.doris.nereids.processor.post.TopnFilterContext;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Nullable;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Substring;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.SortPhase;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRelation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
+import org.apache.doris.nereids.trees.plans.physical.TopnFilter;
 import org.apache.doris.nereids.util.MemoPatternMatchSupported;
 import org.apache.doris.nereids.util.PlanChecker;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.Map;
+
 public class TopNRuntimeFilterTest extends SSBTestBase implements 
MemoPatternMatchSupported {
     @Override
     public void runBeforeAll() throws Exception {
@@ -78,4 +88,47 @@ public class TopNRuntimeFilterTest extends SSBTestBase 
implements MemoPatternMat
         Assertions.assertTrue(localTopN.getSortPhase().isLocal());
         
Assertions.assertFalse(checker.getCascadesContext().getTopnFilterContext().isTopnFilterSource(localTopN));
     }
+
+    @Test
+    public void testProbeExprNullableThroughRightOuterJoin() {
+        // topn node push down filter value to scan node.
+        // the filter value is nullable.
+        // but c_name in scan node is not nullable. so we use
+        // substring(nullable(c_name), 1, 5) as probe expr on scan node
+        String sql = "select substring(c_name, 1, 5) "
+                + "from customer c right outer join lineorder l "
+                + "on c.c_custkey = l.lo_custkey "
+                + "order by substring(c_name, 1, 5) nulls last limit 3";
+        connectContext.getSessionVariable().setDisableJoinReorder(true);
+        PlanChecker checker = PlanChecker.from(connectContext)
+                .analyze(sql)
+                .rewrite()
+                .implement();
+        PhysicalPlan plan = checker.getPhysicalPlan();
+        plan = new 
PlanPostProcessors(checker.getCascadesContext()).process(plan);
+
+        TopnFilterContext ctx = 
checker.getCascadesContext().getTopnFilterContext();
+        Assertions.assertFalse(ctx.getTopnFilters().isEmpty(), "topn filter 
should be created");
+
+        TopnFilter filter = ctx.getTopnFilters().stream()
+                .filter(f -> f.targets.values().stream().anyMatch(expr -> expr 
instanceof Substring))
+                .findFirst()
+                .orElseThrow(() -> new AssertionError("topn filter with 
substring probe not found"));
+
+        Map.Entry<PhysicalRelation, Expression> target = 
filter.targets.entrySet().stream()
+                .filter(entry -> entry.getValue() instanceof Substring)
+                .findFirst()
+                .orElseThrow(() -> new AssertionError("substring probe target 
not found"));
+        PhysicalRelation relation = target.getKey();
+        Expression probeExpr = target.getValue();
+
+        Slot cNameSlot = relation.getOutput().stream()
+                .filter(slot -> slot.getName().equalsIgnoreCase("c_name"))
+                .findFirst()
+                .orElseThrow(() -> new IllegalStateException("c_name slot not 
found"));
+
+        Expression expectedProbeExpr = new Substring(new Nullable(cNameSlot), 
new IntegerLiteral(1),
+                new IntegerLiteral(5));
+        Assertions.assertEquals(expectedProbeExpr, probeExpr);
+    }
 }


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

Reply via email to