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 b4945e5be7a [fix](mark join) Fix invalid semi-join transpose when the 
bottom join is a mark join (#66574)
b4945e5be7a is described below

commit b4945e5be7a86845363bd2d0649f47223b159f3d
Author: starocean999 <[email protected]>
AuthorDate: Mon Aug 10 19:43:07 2026 +0800

    [fix](mark join) Fix invalid semi-join transpose when the bottom join is a 
mark join (#66574)
    
    ### What problem does this PR solve?
    
    Problem Summary:
    A query with a subquery inside the ON condition of an anti join failed at
    physical planning with "A expression contains slot not from children", e.g.:
    
    ```sql
    SELECT t1.* FROM t1 LEFT ANTI JOIN t2
        ON t1.k2 = t2.k3 AND t1.k1 NOT IN (SELECT t3.k1 FROM t3 WHERE t1.k2 = 
t3.k2);
    ```
    
    The subquery is unnested into a mark join `(t1 LEFT ANTI JOIN t3)` that 
produces
    a mark slot, and the outer anti join references the mark slot in its 
conjuncts.
    Root cause: the exploration rule `SemiJoinSemiJoinTransposeProject` 
transposes
    two nested left semi/anti joins `(A ⟕̸ B) ⟕̸ C` into `(A ⟕̸ C) ⟕̸ B`.
    When the bottom join `(A ⟕̸ B)` is a mark join, the transposed plan builds
    `newBottomSemi = (A ⟕̸ C)` from the top join, inheriting the top join's 
conjuncts
    that reference the bottom mark slot, while the mark slot is now produced 
above by
    the new top mark join. The mark slot is therefore referenced by a join whose
    children don't output it, and physical planning fails with "slot not from 
children".
    
    The fix rejects the transpose when the bottom semi join is a mark join and 
the
    top semi join references the bottom mark slot in its conjuncts, so the mark 
join
    is always kept below the join that consumes the mark slot. After the fix the
    query above executes correctly and returns the expected result.
---
 .../join/SemiJoinSemiJoinTransposeProject.java     | 30 +++++++++++-
 .../join/SemiJoinSemiJoinTransposeProjectTest.java | 55 ++++++++++++++++++++++
 2 files changed, 84 insertions(+), 1 deletion(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/join/SemiJoinSemiJoinTransposeProject.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/join/SemiJoinSemiJoinTransposeProject.java
index 359d6e13552..8d1d67af5c0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/join/SemiJoinSemiJoinTransposeProject.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/join/SemiJoinSemiJoinTransposeProject.java
@@ -68,7 +68,15 @@ public class SemiJoinSemiJoinTransposeProject extends 
OneExplorationRuleFactory
                 .when(this::typeChecker)
                 .when(topSemi -> InnerJoinLAsscomProject.checkReorder(topSemi, 
topSemi.left().child(), false))
                 .whenNot(join -> join.hasDistributeHint() || 
join.left().child().hasDistributeHint())
-                .when(join -> join.left().isAllSlots()))
+                .when(join -> join.left().isAllSlots())
+                // the transpose swaps the bottom semi join to the top, so the 
mark slot
+                // produced by the bottom mark join would be produced by the 
new top semi
+                // join. if the top semi join references the mark slot in its 
conjuncts,
+                // those conjuncts would be moved to the new bottom semi join 
whose children
+                // don't output the mark slot, which makes the mark slot 
dangling and fails
+                // physical planning with "slot not from children", so the 
transpose must be
+                // rejected in this case
+                .whenNot(this::isMarkSlotReferencedByTopJoin))
                 .then(topProject -> {
                     LogicalJoin<LogicalProject<LogicalJoin<GroupPlan, 
GroupPlan>>, GroupPlan> topSemi
                             = topProject.child();
@@ -119,4 +127,24 @@ public class SemiJoinSemiJoinTransposeProject extends 
OneExplorationRuleFactory
     public boolean 
typeChecker(LogicalJoin<LogicalProject<LogicalJoin<GroupPlan, GroupPlan>>, 
GroupPlan> topJoin) {
         return VALID_TYPE_PAIR_SET.contains(Pair.of(topJoin.getJoinType(), 
topJoin.left().child().getJoinType()));
     }
+
+    /**
+     * check whether the top semi join references the mark slot produced by 
the bottom mark
+     * join in its conjuncts. in the transposed plan the mark slot is produced 
by the new
+     * top semi join (built from the bottom semi join), while the top semi 
join becomes the
+     * new bottom semi join whose children are A and C, which don't output the 
mark slot.
+     * so if the top semi join's conjuncts reference the mark slot, the 
transpose would make
+     * the mark slot dangling and must be rejected.
+     */
+    private boolean isMarkSlotReferencedByTopJoin(
+            LogicalJoin<LogicalProject<LogicalJoin<GroupPlan, GroupPlan>>, 
GroupPlan> topSemi) {
+        LogicalJoin<GroupPlan, GroupPlan> bottomSemi = topSemi.left().child();
+        if (!bottomSemi.isMarkJoin()) {
+            return false;
+        }
+        ExprId markSlotExprId = 
bottomSemi.getMarkJoinSlotReference().get().getExprId();
+        return topSemi.getExpressions().stream()
+                .flatMap(expr -> expr.getInputSlotExprIds().stream())
+                .anyMatch(markSlotExprId::equals);
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/join/SemiJoinSemiJoinTransposeProjectTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/join/SemiJoinSemiJoinTransposeProjectTest.java
index d37be0a1a13..99a0c452138 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/join/SemiJoinSemiJoinTransposeProjectTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/join/SemiJoinSemiJoinTransposeProjectTest.java
@@ -18,6 +18,7 @@
 package org.apache.doris.nereids.rules.exploration.join;
 
 import org.apache.doris.common.Pair;
+import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.plans.JoinType;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
@@ -95,4 +96,58 @@ public class SemiJoinSemiJoinTransposeProjectTest implements 
MemoPatternMatchSup
                         )
                 );
     }
+
+    @Test
+    public void 
testSemiProjectSemiCommuteRejectedWhenTopJoinReferencesBottomMarkSlot() {
+        /*
+         * the transpose must be rejected when the bottom semi join is a mark 
join and the
+         * top semi join references the mark slot in its conjuncts. otherwise 
the transposed
+         * plan would move the conjuncts that reference the mark slot to a 
join whose
+         * children don't output the mark slot, which fails physical planning 
with
+         * "slot not from children".
+         *
+         *        topJoin(references mark)        the transpose is rejected, 
the plan
+         *        /       \                       keeps the original order:
+         *    abProject    t3                     topJoin
+         *      |                                  /      \
+         * bottomMarkJoin(t1 anti t2)        abProject   t3
+         *    /      \                          |
+         *   t1      t2                   bottomMarkJoin
+         *                                       /      \
+         *                                      t1      t2
+         */
+        // bottom mark join: t1 left anti t2, markJoinConjuncts = (t1#0 = 
t2#0),
+        // output = [t1#0, t1#1, markSlot]
+        LogicalPlan bottomMarkJoin = new LogicalPlanBuilder(scan1)
+                .markJoinWithMarkConjuncts(scan2, JoinType.LEFT_ANTI_JOIN, 
Pair.of(0, 0))
+                .build();
+        // project exposes [t1#0, markSlot]
+        LogicalPlan abProject = new LogicalPlanBuilder(bottomMarkJoin)
+                .project(ImmutableList.of(0, 2))
+                .build();
+        // top anti join on t3 whose other conjunct references the mark slot 
of the bottom
+        // mark join, this is exactly the plan shape that used to trigger the 
bug
+        Slot markSlot = abProject.getOutput().get(1);
+        LogicalPlan topJoin = new LogicalPlanBuilder(abProject)
+                .join(scan3, JoinType.LEFT_ANTI_JOIN, ImmutableList.of(), 
ImmutableList.of(markSlot))
+                .projectAll()
+                .build();
+        // the transpose is rejected, so the plan keeps the original order and 
the
+        // mark join still produces the mark slot below the top anti join
+        PlanChecker.from(MemoTestUtils.createConnectContext(), topJoin)
+                
.applyExploration(SemiJoinSemiJoinTransposeProject.INSTANCE.build())
+                .matches(
+                        logicalProject(
+                                logicalJoin(
+                                        logicalProject(
+                                                logicalJoin(
+                                                        
logicalOlapScan().when(s -> s.getTable().getName().equals("t1")),
+                                                        
logicalOlapScan().when(s -> s.getTable().getName().equals("t2"))
+                                                ).when(join -> 
join.getJoinType() == JoinType.LEFT_ANTI_JOIN)
+                                        ),
+                                        logicalOlapScan().when(s -> 
s.getTable().getName().equals("t3"))
+                                ).when(join -> join.getJoinType() == 
JoinType.LEFT_ANTI_JOIN)
+                        )
+                );
+    }
 }


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

Reply via email to