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]