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

github-actions[bot] pushed a commit to branch auto-pick-65982-branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/auto-pick-65982-branch-4.1 by 
this push:
     new ce253e5e358 [fix](fd)drop Function dependencies from join outer side 
(#65982)
ce253e5e358 is described below

commit ce253e5e35881f643a8c8da71c5d92faeac9203c
Author: minghong <[email protected]>
AuthorDate: Fri Aug 14 16:34:23 2026 +0800

    [fix](fd)drop Function dependencies from join outer side (#65982)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A (no issue linked)
    
    Related PR: N/A
    
    Problem Summary:
    
    Nereids derives functional dependencies (FDs) from each operator's
    children via `DataTrait`, and rewrite rules such as
    `EliminateGroupByKey`, `EliminateGroupByKeyByUniform`,
    `EliminateOrderByKey` and `ConstantPropagation` consume these FDs to
    drop functionally-determined grouping/ordering keys. If an FD is derived
    incorrectly, those rules may produce wrong query results.
    
    `LogicalJoin.computeFd()` and `PhysicalHashJoin.computeFd()` previously
    propagated FDs from both join inputs, only excluding the semi/anti-join
    side:
    
    ```java
    if (!joinType.isLeftSemiOrAntiJoin()) {
        builder.addFuncDepsDG(right().getLogicalProperties().getTrait());
    }
    if (!joinType.isRightSemiOrAntiJoin()) {
        builder.addFuncDepsDG(left().getLogicalProperties().getTrait());
    }
    ```
    
    For outer joins the nullable side is null-extended: unmatched rows are
    padded with NULLs, which invalidates FDs from that side. For example, in
    `t1 LEFT OUTER JOIN t2`, if the right side has the FD `t2.a -> t2.b` and
    `a` is nullable, a matched row with `a = NULL, b = 1` and an unmatched
    row `(a = NULL, b = NULL)` together violate `a -> b` on the join output.
    The old code still propagated such FDs from the nullable side for `LEFT
    OUTER JOIN` (right side), `RIGHT OUTER JOIN` (left side) and `FULL OUTER
    JOIN` (both sides), so a downstream rule could remove a group-by key
    that is not actually functionally determined and change the query
    result.
    
    This PR fixes the FD derivation on join outputs:
    
    1. `computeFd()` in `LogicalJoin` and `PhysicalHashJoin` is rewritten
    with an explicit switch over join types:
       - inner / cross joins: propagate FDs from both sides;
       - semi / anti joins: propagate FDs only from the output side;
    - outer joins: propagate FDs from the preserved side, and from the
    nullable side only the FDs whose determinant is NOT NULL in the child —
    matched rows then always carry a non-null determinant, so they cannot
    collide with the `(NULL, NULL)` null-extension of unmatched rows;
    - full outer join: keep only the NOT-NULL-determinant FDs from both
    sides.
    2. A new `DataTrait.Builder.addFuncDepsDGForOuterJoinNullableSide()` /
    `FuncDepsDG.Builder.addDepsForOuterJoinNullableSide()` implements the
    NOT-NULL-determinant filter.
    3. The nullability check is performed against the *current* child output
    rather than the slot stored in the FD graph: slots are keyed by ExprId
    and may carry a stale `nullable` flag (e.g. after
    `LogicalSubQueryAliasToLogicalProject` inlining), so a determinant that
    became nullable in the immediate child is dropped.
    
    Tests in `FdTest` are updated (FOJ/LOJ/ROJ no longer propagate
    nullable-side FDs, while NOT-NULL-determinant FDs from the nullable side
    are kept), and a new `testNestedOuterJoinNullableDeterminant` covers the
    nested outer-join case where the determinant's stale non-nullable flag
    must not leak through, verified on both the logical and the physical
    join paths.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    
    - Behavior changed:
        - [x] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [x] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 .../apache/doris/nereids/properties/DataTrait.java |  10 ++
 .../doris/nereids/properties/FuncDepsDG.java       |  38 +++++++
 .../nereids/trees/plans/logical/LogicalJoin.java   |  49 ++++++++-
 .../trees/plans/physical/PhysicalHashJoin.java     |  49 ++++++++-
 .../apache/doris/nereids/properties/FdTest.java    | 119 ++++++++++++++++++++-
 5 files changed, 252 insertions(+), 13 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/DataTrait.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/DataTrait.java
index 2e367dc8838..4b9409f553a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/DataTrait.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/DataTrait.java
@@ -237,6 +237,16 @@ public class DataTrait {
             fdDgBuilder.addDeps(fd.fdDg);
         }
 
+        /**
+         * Add FDs from the nullable side of an outer join, filtering out 
edges whose
+         * determinant may be NULL in the immediate child's current output — 
those would
+         * be invalidated by null-extension of unmatched rows. Determinants 
are canonicalized
+         * against childOutput (by ExprId) before the nullability check.
+         */
+        public void addFuncDepsDGForOuterJoinNullableSide(DataTrait fd, 
List<Slot> childOutput) {
+            fdDgBuilder.addDepsForOuterJoinNullableSide(fd.fdDg, childOutput);
+        }
+
         /**add Dependency relation for dominate and dependency*/
         public void addDeps(Set<Slot> dominate, Set<Slot> dependency) {
             if (dominate.isEmpty() || dependency.isEmpty()) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/FuncDepsDG.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/FuncDepsDG.java
index 09bc85e084d..425273fda35 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/FuncDepsDG.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/FuncDepsDG.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.nereids.properties;
 
+import org.apache.doris.nereids.trees.expressions.ExprId;
 import org.apache.doris.nereids.trees.expressions.Slot;
 
 import com.google.common.collect.ImmutableList;
@@ -219,6 +220,43 @@ public class FuncDepsDG {
             }
         }
 
+        /**
+         * Add FD edges from the nullable side of an outer join. Only keep 
edges whose
+         * determinant slots are all NOT NULL in the immediate child's current 
output:
+         * matched rows always carry a non-null determinant, while unmatched 
rows contribute
+         * (NULL, NULL), so they cannot collide. Edges with a nullable 
determinant are dropped
+         * because a matched row with determinant=NULL could conflict with an 
unmatched (NULL, NULL).
+         * The determinant slots stored in the graph may carry a stale 
nullable flag (slot
+         * equality/hash use only ExprId and getOrCreateNode never replaces 
the stored object),
+         * so each determinant is canonicalized against the child's current 
output before the
+         * nullability check.
+         */
+        public void addDepsForOuterJoinNullableSide(FuncDepsDG funcDepsDG, 
List<Slot> childOutput) {
+            Map<ExprId, Slot> outputSlotMap = new HashMap<>();
+            for (Slot slot : childOutput) {
+                outputSlotMap.put(slot.getExprId(), slot);
+            }
+            for (DGItem dgItem : funcDepsDG.dgItems) {
+                Set<Slot> canonicalSlots = new HashSet<>();
+                boolean allNotNull = true;
+                for (Slot slot : dgItem.slots) {
+                    Slot outputSlot = outputSlotMap.get(slot.getExprId());
+                    // a determinant not in the child's output cannot be 
trusted; drop the edge
+                    if (outputSlot == null || outputSlot.nullable()) {
+                        allNotNull = false;
+                        break;
+                    }
+                    canonicalSlots.add(outputSlot);
+                }
+                if (!allNotNull) {
+                    continue;
+                }
+                for (int childIdx : dgItem.children) {
+                    addDeps(canonicalSlots, 
funcDepsDG.dgItems.get(childIdx).slots);
+                }
+            }
+        }
+
         public void replace(Map<Slot, Slot> replaceSlotMap) {
             for (DGItem item : dgItems) {
                 item.replace(replaceSlotMap);
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 b1ccabc52db..abcca92ef48 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
@@ -711,11 +711,50 @@ public class LogicalJoin<LEFT_CHILD_TYPE extends Plan, 
RIGHT_CHILD_TYPE extends
 
     @Override
     public void computeFd(Builder builder) {
-        if (!joinType.isLeftSemiOrAntiJoin()) {
-            builder.addFuncDepsDG(right().getLogicalProperties().getTrait());
-        }
-        if (!joinType.isRightSemiOrAntiJoin()) {
-            builder.addFuncDepsDG(left().getLogicalProperties().getTrait());
+        switch (joinType) {
+            case INNER_JOIN:
+            case ASOF_LEFT_INNER_JOIN:
+            case ASOF_RIGHT_INNER_JOIN:
+            case CROSS_JOIN:
+                
builder.addFuncDepsDG(left().getLogicalProperties().getTrait());
+                
builder.addFuncDepsDG(right().getLogicalProperties().getTrait());
+                break;
+            case LEFT_SEMI_JOIN:
+            case LEFT_ANTI_JOIN:
+            case NULL_AWARE_LEFT_ANTI_JOIN:
+                // Semi/anti joins only output the left side; right-side FDs 
are irrelevant.
+                
builder.addFuncDepsDG(left().getLogicalProperties().getTrait());
+                break;
+            case LEFT_OUTER_JOIN:
+            case ASOF_LEFT_OUTER_JOIN:
+                // Left side preserved; right side nullable — keep only FDs 
whose
+                // determinant is NOT NULL in the right child's current output.
+                
builder.addFuncDepsDG(left().getLogicalProperties().getTrait());
+                builder.addFuncDepsDGForOuterJoinNullableSide(
+                        right().getLogicalProperties().getTrait(), 
right().getOutput());
+                break;
+            case RIGHT_SEMI_JOIN:
+            case RIGHT_ANTI_JOIN:
+                // Semi/anti joins only output the right side; left-side FDs 
are irrelevant.
+                
builder.addFuncDepsDG(right().getLogicalProperties().getTrait());
+                break;
+            case RIGHT_OUTER_JOIN:
+            case ASOF_RIGHT_OUTER_JOIN:
+                // Right side preserved; left side nullable — keep only FDs 
whose
+                // determinant is NOT NULL in the left child's current output.
+                
builder.addFuncDepsDG(right().getLogicalProperties().getTrait());
+                builder.addFuncDepsDGForOuterJoinNullableSide(
+                        left().getLogicalProperties().getTrait(), 
left().getOutput());
+                break;
+            case FULL_OUTER_JOIN:
+                // Both sides are nullable; keep only FDs whose determinant is 
NOT NULL.
+                builder.addFuncDepsDGForOuterJoinNullableSide(
+                        left().getLogicalProperties().getTrait(), 
left().getOutput());
+                builder.addFuncDepsDGForOuterJoinNullableSide(
+                        right().getLogicalProperties().getTrait(), 
right().getOutput());
+                break;
+            default:
+                break;
         }
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalHashJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalHashJoin.java
index 9145b6bfbf4..d870aa0bca4 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalHashJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalHashJoin.java
@@ -357,11 +357,50 @@ public class PhysicalHashJoin<
 
     @Override
     public void computeFd(DataTrait.Builder builder) {
-        if (!joinType.isLeftSemiOrAntiJoin()) {
-            builder.addFuncDepsDG(right().getLogicalProperties().getTrait());
-        }
-        if (!joinType.isRightSemiOrAntiJoin()) {
-            builder.addFuncDepsDG(left().getLogicalProperties().getTrait());
+        switch (joinType) {
+            case INNER_JOIN:
+            case ASOF_LEFT_INNER_JOIN:
+            case ASOF_RIGHT_INNER_JOIN:
+            case CROSS_JOIN:
+                
builder.addFuncDepsDG(left().getLogicalProperties().getTrait());
+                
builder.addFuncDepsDG(right().getLogicalProperties().getTrait());
+                break;
+            case LEFT_SEMI_JOIN:
+            case LEFT_ANTI_JOIN:
+            case NULL_AWARE_LEFT_ANTI_JOIN:
+                // Semi/anti joins only output the left side; right-side FDs 
are irrelevant.
+                
builder.addFuncDepsDG(left().getLogicalProperties().getTrait());
+                break;
+            case LEFT_OUTER_JOIN:
+            case ASOF_LEFT_OUTER_JOIN:
+                // Left side preserved; right side nullable — keep only FDs 
whose
+                // determinant is NOT NULL in the right child's current output.
+                
builder.addFuncDepsDG(left().getLogicalProperties().getTrait());
+                builder.addFuncDepsDGForOuterJoinNullableSide(
+                        right().getLogicalProperties().getTrait(), 
right().getOutput());
+                break;
+            case RIGHT_SEMI_JOIN:
+            case RIGHT_ANTI_JOIN:
+                // Semi/anti joins only output the right side; left-side FDs 
are irrelevant.
+                
builder.addFuncDepsDG(right().getLogicalProperties().getTrait());
+                break;
+            case RIGHT_OUTER_JOIN:
+            case ASOF_RIGHT_OUTER_JOIN:
+                // Right side preserved; left side nullable — keep only FDs 
whose
+                // determinant is NOT NULL in the left child's current output.
+                
builder.addFuncDepsDG(right().getLogicalProperties().getTrait());
+                builder.addFuncDepsDGForOuterJoinNullableSide(
+                        left().getLogicalProperties().getTrait(), 
left().getOutput());
+                break;
+            case FULL_OUTER_JOIN:
+                // Both sides are nullable; keep only FDs whose determinant is 
NOT NULL.
+                builder.addFuncDepsDGForOuterJoinNullableSide(
+                        left().getLogicalProperties().getTrait(), 
left().getOutput());
+                builder.addFuncDepsDGForOuterJoinNullableSide(
+                        right().getLogicalProperties().getTrait(), 
right().getOutput());
+                break;
+            default:
+                break;
         }
     }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/FdTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/FdTest.java
index ba72b0b2d59..da7cb8e940f 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/FdTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/FdTest.java
@@ -19,6 +19,10 @@ package org.apache.doris.nereids.properties;
 
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
 import org.apache.doris.nereids.util.PlanChecker;
 import org.apache.doris.utframe.TestWithFeService;
 
@@ -27,6 +31,7 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import java.util.Set;
+import java.util.function.Predicate;
 
 class FdTest extends TestWithFeService {
     @Override
@@ -46,6 +51,13 @@ class FdTest extends TestWithFeService {
                 + "UNIQUE KEY(id)\n"
                 + "distributed by hash(id) buckets 10\n"
                 + "properties('replication_num' = '1');");
+        createTable("create table test.nullable_uni (\n"
+                + "id int,\n"
+                + "id2 int not null,\n"
+                + "name varchar(128) not null)\n"
+                + "UNIQUE KEY(id)\n"
+                + "distributed by hash(id) buckets 10\n"
+                + "properties('replication_num' = '1');");
         connectContext.setDatabase("test");
         
connectContext.getSessionVariable().setDisableNereidsRules("PRUNE_EMPTY_PARTITION");
     }
@@ -147,38 +159,139 @@ class FdTest extends TestWithFeService {
         Assertions.assertTrue(plan.getLogicalProperties().getTrait()
                 .isDependent(ImmutableSet.of(plan.getOutput().get(1)), 
ImmutableSet.of(plan.getOutput().get(2))));
 
-        // foj
+        // foj: both sides nullable — keep FDs with NOT NULL determinants
         plan = PlanChecker.from(connectContext)
                 .analyze("select t1.id, t1.id2, t2.id, t2.id2 "
                         + "from uni as t1 full outer join uni as t2 on t1.id2 
= t2.id2")
                 .rewrite()
                 .getPlan();
+        // t1.id is NOT NULL, so {t1.id} -> {t1.id2} survives null extension
         Assertions.assertTrue(plan.getLogicalProperties().getTrait()
                 .isDependent(ImmutableSet.of(plan.getOutput().get(0)), 
ImmutableSet.of(plan.getOutput().get(1))));
+        // t2.id is NOT NULL, so {t2.id} -> {t2.id2} survives null extension
         Assertions.assertTrue(plan.getLogicalProperties().getTrait()
                 .isDependent(ImmutableSet.of(plan.getOutput().get(2)), 
ImmutableSet.of(plan.getOutput().get(3))));
 
-        // loj
+        // loj: left side preserved, right side nullable — only NOT 
NULL-determinant FDs from right propagate
         plan = PlanChecker.from(connectContext)
                 .analyze("select t1.id, t1.id2, t2.id, t2.id2 "
                         + "from uni as t1 left outer join uni as t2 on t1.id2 
= t2.id2")
                 .rewrite()
                 .getPlan();
+        // t1.id is NOT NULL, left side always preserved
         Assertions.assertTrue(plan.getLogicalProperties().getTrait()
                 .isDependent(ImmutableSet.of(plan.getOutput().get(0)), 
ImmutableSet.of(plan.getOutput().get(1))));
+        // t2.id is NOT NULL, so {t2.id} -> {t2.id2} survives null extension
         Assertions.assertTrue(plan.getLogicalProperties().getTrait()
                 .isDependent(ImmutableSet.of(plan.getOutput().get(2)), 
ImmutableSet.of(plan.getOutput().get(3))));
 
-        // roj
+        // roj: right side preserved, left side nullable — only NOT 
NULL-determinant FDs from left propagate
         plan = PlanChecker.from(connectContext)
                 .analyze("select t1.id, t1.id2, t2.id, t2.id2 "
                         + "from uni as t1 right outer join uni as t2 on t1.id2 
= t2.id2")
                 .rewrite()
                 .getPlan();
+        // t1.id is NOT NULL, so {t1.id} -> {t1.id2} survives null extension
         Assertions.assertTrue(plan.getLogicalProperties().getTrait()
                 .isDependent(ImmutableSet.of(plan.getOutput().get(0)), 
ImmutableSet.of(plan.getOutput().get(1))));
+        // t2.id is NOT NULL, right side always preserved
         Assertions.assertTrue(plan.getLogicalProperties().getTrait()
                 .isDependent(ImmutableSet.of(plan.getOutput().get(2)), 
ImmutableSet.of(plan.getOutput().get(3))));
+
+        // loj with nullable determinant: FD should be dropped
+        plan = PlanChecker.from(connectContext)
+                .analyze("select t1.id, t1.id2, t2.id, t2.id2 "
+                        + "from uni as t1 left outer join nullable_uni as t2 
on t1.id2 = t2.id2")
+                .rewrite()
+                .getPlan();
+        // t1 side preserved
+        Assertions.assertTrue(plan.getLogicalProperties().getTrait()
+                .isDependent(ImmutableSet.of(plan.getOutput().get(0)), 
ImmutableSet.of(plan.getOutput().get(1))));
+        // t2.id is nullable, so {t2.id} -> {t2.id2} should be dropped
+        Assertions.assertFalse(plan.getLogicalProperties().getTrait()
+                .isDependent(ImmutableSet.of(plan.getOutput().get(2)), 
ImmutableSet.of(plan.getOutput().get(3))));
+
+        // foj with nullable determinant on one side
+        plan = PlanChecker.from(connectContext)
+                .analyze("select t1.id, t1.id2, t2.id, t2.id2 "
+                        + "from uni as t1 full outer join nullable_uni as t2 
on t1.id2 = t2.id2")
+                .rewrite()
+                .getPlan();
+        // t1.id is NOT NULL, so {t1.id} -> {t1.id2} survives
+        Assertions.assertTrue(plan.getLogicalProperties().getTrait()
+                .isDependent(ImmutableSet.of(plan.getOutput().get(0)), 
ImmutableSet.of(plan.getOutput().get(1))));
+        // t2.id is nullable, so {t2.id} -> {t2.id2} should be dropped
+        Assertions.assertFalse(plan.getLogicalProperties().getTrait()
+                .isDependent(ImmutableSet.of(plan.getOutput().get(2)), 
ImmutableSet.of(plan.getOutput().get(3))));
+    }
+
+    @Test
+    void testNestedOuterJoinNullableDeterminant() {
+        // Reduced failing tree from review "Check determinant nullability 
against the current child output":
+        //   Aggregate(group by r_id, c)
+        //     RightOuterJoin
+        //       Project(l_id, r_id, coalesce(r_id, 1) AS c)
+        //         LeftOuterJoin
+        //           Scan L
+        //           Scan R(r_id NOT NULL UNIQUE)
+        //       Scan V
+        // r_id is NOT NULL in R but becomes nullable at the inner LOJ output; 
the Project derives
+        // r_id -> c from the expression. At the outer join output this FD 
must be dropped:
+        // unmatched V rows inject (r_id=NULL, c=NULL), which collides with 
the Project's own
+        // (r_id=NULL, c=1). After rewrite the sub-query alias is inlined into 
a plain project
+        // (LogicalSubQueryAliasToLogicalProject) whose trait keeps the stale 
non-nullable r_id,
+        // so the outer join must still be checked against the immediate 
child's current output.
+        // c is kept in the select list so that it is not pruned away before 
the trait check.
+        // Disable join reorder to keep the join tree stable (v LEFT OUTER 
JOIN p as written).
+        connectContext.getSessionVariable().setDisableJoinReorder(true);
+        String sql = "select p.id, p.c, count(*) "
+                + "from uni as v "
+                + "left outer join ("
+                + "select l.id2, r.id, coalesce(r.id, 1) as c "
+                + "from agg as l left outer join uni as r on l.id2 = r.id2) p "
+                + "on v.id2 = p.id2 "
+                + "group by p.id, p.c";
+
+        LogicalAggregate<?> aggregate = (LogicalAggregate<?>) findNode(
+                PlanChecker.from(connectContext).analyze(sql).getPlan(), n -> 
n instanceof LogicalAggregate);
+        Assertions.assertNotNull(aggregate);
+        // group by (r_id, c); both are plain slots after subquery inlining
+        Slot rId = (Slot) aggregate.getGroupByExpressions().get(0);
+        Slot c = (Slot) aggregate.getGroupByExpressions().get(1);
+
+        // logical path: the outer join's trait must not contain r_id -> c
+        Plan rewritten = 
PlanChecker.from(connectContext).analyze(sql).rewrite().getPlan();
+        LogicalJoin<?, ?> outerJoin = (LogicalJoin<?, ?>) findNode(rewritten, 
n -> n instanceof LogicalJoin);
+        Assertions.assertNotNull(outerJoin, "rewritten plan: " + 
rewritten.treeString());
+        Assertions.assertFalse(outerJoin.getLogicalProperties().getTrait()
+                        .isDependent(ImmutableSet.of(rId), ImmutableSet.of(c)),
+                "r_id -> c must be dropped at the outer join since r_id is 
nullable on the outer side");
+
+        // physical path: PhysicalHashJoin must drop r_id -> c as well; pick 
the outer join
+        // (its subtree contains the inner join). implement() applies the 
implementation rules
+        // directly (no CBO), so no table statistics are required.
+        PhysicalPlan physicalPlan = PlanChecker.from(connectContext)
+                .analyze(sql).rewrite().implement().getPhysicalPlan();
+        PhysicalHashJoin<?, ?> physicalOuterJoin = (PhysicalHashJoin<?, ?>) 
findNode(physicalPlan,
+                n -> n instanceof PhysicalHashJoin
+                        && n.anyMatch(p -> p instanceof PhysicalHashJoin && p 
!= n));
+        Assertions.assertNotNull(physicalOuterJoin, "physical plan: " + 
physicalPlan.treeString());
+        
Assertions.assertFalse(physicalOuterJoin.getLogicalProperties().getTrait()
+                        .isDependent(ImmutableSet.of(rId), ImmutableSet.of(c)),
+                "physical join must also drop r_id -> c since r_id is nullable 
on the outer side");
+    }
+
+    private Plan findNode(Plan plan, Predicate<Plan> predicate) {
+        if (predicate.test(plan)) {
+            return plan;
+        }
+        for (Plan child : plan.children()) {
+            Plan found = findNode(child, predicate);
+            if (found != null) {
+                return found;
+            }
+        }
+        return null;
     }
 
     @Test


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

Reply via email to