github-actions[bot] commented on code in PR #65467:
URL: https://github.com/apache/doris/pull/65467#discussion_r3569379448
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/hint/LeadingHint.java:
##########
@@ -576,39 +658,91 @@ public Plan generateLeadingJoinPlan() {
}
LogicalJoin finalJoin = (LogicalJoin) stack.pop();
- // we want all filters been removed
- if (Utils.enableAssert && !filters.isEmpty()) {
- throw new IllegalStateException(
- "Leading hint process failed: filter should be empty, but
meet: " + filters
- );
+ // Any filter that was put back due to incompatible join type but never
+ // consumed by a later join means the leading hint cannot preserve the
+ // original semantics. Fail unconditionally.
+ if (!filters.isEmpty()) {
+ this.setStatus(HintStatus.UNUSED);
+ this.setErrorMessage("leading plan cannot consume all join
predicates, leftover: "
+ + filters);
+ return null;
}
if (finalJoin != null) {
this.setStatus(HintStatus.SUCCESS);
}
return finalJoin;
}
- private List<Expression> getJoinConditions(List<Pair<Long, Expression>>
filters,
- LogicalPlan left, LogicalPlan
right) {
- List<Expression> joinConditions = new ArrayList<>();
+ /** Collect filter entries whose bitmap is a subset of the given join's
tables.
+ * Removes them from the global filter list; the caller must put back any
entry
+ * whose original type is incompatible with the final join type.
+ *
+ * Filters from an INNER/CROSS join that reference the nullable side of a
+ * pending outer join (RIGHT or LEFT) are deferred — consuming them before
+ * the outer join is built would change NULL semantics. For example, with
+ * (a RIGHT JOIN b) INNER JOIN c AND leading(a c b), the {a,c} inner
predicate
+ * must not be consumed at the {a,c} level because a is the nullable side
of
+ * the pending RIGHT JOIN. */
+ private List<FilterEntry> collectJoinConditions(List<FilterEntry> filters,
+ LogicalPlan left,
LogicalPlan right) {
+ List<FilterEntry> collected = new ArrayList<>();
+ Long tablesBitMap = LongBitmap.or(getBitmap(left), getBitmap(right));
for (int i = filters.size() - 1; i >= 0; i--) {
- Pair<Long, Expression> filterPair = filters.get(i);
- Long tablesBitMap = LongBitmap.or(getBitmap(left),
getBitmap(right));
- // left one is smaller set
- if (LongBitmap.isSubset(filterPair.first, tablesBitMap)) {
- joinConditions.add(filterPair.second);
+ FilterEntry entry = filters.get(i);
+ if (LongBitmap.isSubset(entry.bitmap, tablesBitMap)) {
+ // Defer inner/cross predicates that reference the nullable
side
+ // of a pending outer join whose counterpart is not yet
present.
+ if (entry.originalType.isInnerOrCrossJoin()
+ && isBlockedByPendingOuterJoin(entry.bitmap,
tablesBitMap)) {
+ continue;
+ }
+ collected.add(entry);
filters.remove(i);
}
}
- return joinConditions;
+ return collected;
}
- private LogicalPlan makeFilterPlanIfExist(List<Pair<Long, Expression>>
filters, LogicalPlan scan) {
+ /** Check whether an inner/cross predicate whose column bitmap is
+ * {@code filterBitmap} should be deferred because it references the
+ * nullable side of a pending outer join whose other side is not yet
+ * in the current join ({@code joinTableBitmap}). */
+ private boolean isBlockedByPendingOuterJoin(long filterBitmap, long
joinTableBitmap) {
+ for (JoinConstraint constraint : joinConstraintList) {
+ // RIGHT OUTER JOIN: leftHand is the nullable side.
+ // The predicate must wait until the preserved rightHand arrives.
+ if (constraint.getJoinType().isRightOuterJoin()) {
+ if (LongBitmap.isOverlap(filterBitmap,
constraint.getLeftHand())
+ && !LongBitmap.isSubset(constraint.getRightHand(),
joinTableBitmap)) {
+ return true;
+ }
+ }
+ // LEFT OUTER JOIN: rightHand is the nullable side.
+ // The predicate must wait until the preserved leftHand arrives.
+ if (constraint.getJoinType().isLeftOuterJoin()) {
Review Comment:
`isBlockedByPendingOuterJoin()` still ignores full outer joins, so upper
inner predicates over either side of a pending full outer join can be consumed
before the full outer join exists. A reduced tree is:
```text
InnerJoin ON a.v > 0
FullOuterJoin ON a.k = b.k
a
b
c
```
With `leading(a b c)`, `CollectJoinConstraint` records the upper `a.v > 0`
as an `INNER_JOIN` entry with bitmap `{a}`. When scan `a` is visited,
`shouldDeferFromScan()` only blocks left/right outer constraints through this
helper, so the predicate is pushed into `Filter(a.v > 0, a)` and removed. The
generated plan can then build the exact full outer join, cross join `c`, find
no leftover filters, and mark the hint successful.
That changes results: if an `a` row with `a.v <= 0` matches a `b` row, the
original upper inner predicate eliminates the matched row. After scan pushdown,
the `a` row is gone before the full outer join, so the `b` row is emitted as
unmatched/null-extended and then survives the cross join. Please treat full
outer joins as pending nullable-side constraints for inner/cross deferral until
the full outer join can be built, and cover this through the real leading-hint
plan path.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/hint/LeadingHint.java:
##########
@@ -576,39 +658,91 @@ public Plan generateLeadingJoinPlan() {
}
LogicalJoin finalJoin = (LogicalJoin) stack.pop();
- // we want all filters been removed
- if (Utils.enableAssert && !filters.isEmpty()) {
- throw new IllegalStateException(
- "Leading hint process failed: filter should be empty, but
meet: " + filters
- );
+ // Any filter that was put back due to incompatible join type but never
+ // consumed by a later join means the leading hint cannot preserve the
+ // original semantics. Fail unconditionally.
+ if (!filters.isEmpty()) {
+ this.setStatus(HintStatus.UNUSED);
+ this.setErrorMessage("leading plan cannot consume all join
predicates, leftover: "
+ + filters);
+ return null;
}
if (finalJoin != null) {
this.setStatus(HintStatus.SUCCESS);
}
return finalJoin;
}
- private List<Expression> getJoinConditions(List<Pair<Long, Expression>>
filters,
- LogicalPlan left, LogicalPlan
right) {
- List<Expression> joinConditions = new ArrayList<>();
+ /** Collect filter entries whose bitmap is a subset of the given join's
tables.
+ * Removes them from the global filter list; the caller must put back any
entry
+ * whose original type is incompatible with the final join type.
+ *
+ * Filters from an INNER/CROSS join that reference the nullable side of a
+ * pending outer join (RIGHT or LEFT) are deferred — consuming them before
+ * the outer join is built would change NULL semantics. For example, with
+ * (a RIGHT JOIN b) INNER JOIN c AND leading(a c b), the {a,c} inner
predicate
+ * must not be consumed at the {a,c} level because a is the nullable side
of
+ * the pending RIGHT JOIN. */
+ private List<FilterEntry> collectJoinConditions(List<FilterEntry> filters,
+ LogicalPlan left,
LogicalPlan right) {
+ List<FilterEntry> collected = new ArrayList<>();
+ Long tablesBitMap = LongBitmap.or(getBitmap(left), getBitmap(right));
for (int i = filters.size() - 1; i >= 0; i--) {
- Pair<Long, Expression> filterPair = filters.get(i);
- Long tablesBitMap = LongBitmap.or(getBitmap(left),
getBitmap(right));
- // left one is smaller set
- if (LongBitmap.isSubset(filterPair.first, tablesBitMap)) {
- joinConditions.add(filterPair.second);
+ FilterEntry entry = filters.get(i);
+ if (LongBitmap.isSubset(entry.bitmap, tablesBitMap)) {
+ // Defer inner/cross predicates that reference the nullable
side
+ // of a pending outer join whose counterpart is not yet
present.
+ if (entry.originalType.isInnerOrCrossJoin()
Review Comment:
This deferral is too broad for predicates that are wholly inside the pending
outer join's original nullable-side subtree. For example:
```text
LeftOuterJoin ON a.k = b.k
a
InnerJoin ON b.x = d.x
b
d
```
With `leading(b d a)`, the left-outer constraint has nullable
`rightHand={b,d}`, and the `b.x = d.x` predicate is collected as an
`INNER_JOIN` entry with bitmap `{b,d}`. At the `{b,d}` join, this line defers
the predicate because it overlaps the nullable right hand and `{a}` is absent.
The lower join is then built as `b CROSS d`; at the top join the predicate is
incompatible with the generated `RIGHT_OUTER_JOIN`, gets put back, and the
final leftover check marks the hint `UNUSED`.
That leading order is legal: `{b,d}` is the original nullable-side child,
not an unrelated table mixed into it. Please let `collectJoinConditions()`
consume inner/cross predicates at a join wholly contained in the pending outer
join's original nullable hand, while keeping scan-level deferral in
`shouldDeferFromScan()`, and continue to block nullable-side joins that include
tables outside that hand before the preserved side arrives.
##########
fe/fe-core/src/test/java/org/apache/doris/nereids/hint/LeadingHintTest.java:
##########
@@ -138,6 +140,420 @@ public void
testCompositeRightSemiAndAntiJoinRetainedSideCanUseMinHand() {
assertCompositeRetainedSideCanUseMinHand(JoinType.RIGHT_ANTI_JOIN);
}
+ @Test
+ public void testIsJoinTypeCompatible() {
+ // Exact match
+
Assertions.assertTrue(LeadingHint.isJoinTypeCompatible(JoinType.INNER_JOIN,
JoinType.INNER_JOIN));
+
Assertions.assertTrue(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_SEMI_JOIN,
JoinType.LEFT_SEMI_JOIN));
+
Assertions.assertTrue(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_ANTI_JOIN,
JoinType.LEFT_ANTI_JOIN));
+
Assertions.assertTrue(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_OUTER_JOIN,
JoinType.LEFT_OUTER_JOIN));
+
+ // One-side outer joins are interchangeable
+
Assertions.assertTrue(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_OUTER_JOIN,
JoinType.RIGHT_OUTER_JOIN));
+
Assertions.assertTrue(LeadingHint.isJoinTypeCompatible(JoinType.RIGHT_OUTER_JOIN,
JoinType.LEFT_OUTER_JOIN));
+
+ // Semi joins are compatible with each other
+
Assertions.assertTrue(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_SEMI_JOIN,
JoinType.RIGHT_SEMI_JOIN));
+
+ // Anti joins are compatible with each other
+
Assertions.assertTrue(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_ANTI_JOIN,
JoinType.RIGHT_ANTI_JOIN));
+
+ // Incompatible pairs
+
Assertions.assertFalse(LeadingHint.isJoinTypeCompatible(JoinType.INNER_JOIN,
JoinType.LEFT_SEMI_JOIN));
+
Assertions.assertFalse(LeadingHint.isJoinTypeCompatible(JoinType.INNER_JOIN,
JoinType.LEFT_ANTI_JOIN));
+
Assertions.assertFalse(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_SEMI_JOIN,
JoinType.INNER_JOIN));
+
Assertions.assertFalse(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_ANTI_JOIN,
JoinType.INNER_JOIN));
+
Assertions.assertFalse(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_SEMI_JOIN,
JoinType.LEFT_ANTI_JOIN));
+
Assertions.assertFalse(LeadingHint.isJoinTypeCompatible(JoinType.LEFT_OUTER_JOIN,
JoinType.INNER_JOIN));
+
+ // Full outer join is only compatible with itself
+
Assertions.assertTrue(LeadingHint.isJoinTypeCompatible(JoinType.FULL_OUTER_JOIN,
JoinType.FULL_OUTER_JOIN));
+
Assertions.assertFalse(LeadingHint.isJoinTypeCompatible(JoinType.FULL_OUTER_JOIN,
JoinType.INNER_JOIN));
+
Assertions.assertFalse(LeadingHint.isJoinTypeCompatible(JoinType.INNER_JOIN,
JoinType.FULL_OUTER_JOIN));
+
Assertions.assertFalse(LeadingHint.isJoinTypeCompatible(JoinType.FULL_OUTER_JOIN,
JoinType.LEFT_OUTER_JOIN));
+ }
+
+ @Test
+ public void testFilterEntryKeepsOriginalJoinType() {
+ // Same expression "a.v > 0" collected from two different joins:
+ // LeftAntiJoin(a,b) → bitmap={0,1}, type=LEFT_ANTI_JOIN
+ // InnerJoin(c) → bitmap={0}, type=INNER_JOIN
+ // The inner-join occurrence should NOT be consumed by the anti join.
+ LeadingHint leading = new LeadingHint("Leading");
+ Expression expr = new IntegerLiteral(1);
+
+ long a = LongBitmap.newBitmap(0);
+ long b = LongBitmap.newBitmap(1);
+ long ab = LongBitmap.newBitmapUnion(a, b);
+
+ // Simulate CollectJoinConstraint bottom-up:
+ // 1. LeftAntiJoin(a,b): records expr with bitmap={a,b},
type=LEFT_ANTI_JOIN
+ leading.addFilter(ab, expr, JoinType.LEFT_ANTI_JOIN);
+ // 2. InnerJoin((a,b),c): records same expr with bitmap={a},
type=INNER_JOIN
+ leading.addFilter(a, expr, JoinType.INNER_JOIN);
+
+ // Verify both entries are present with correct types
+ Assertions.assertEquals(2, leading.getFilters().size());
+ Assertions.assertEquals(ab, leading.getFilters().get(0).bitmap);
+ Assertions.assertEquals(JoinType.LEFT_ANTI_JOIN,
leading.getFilters().get(0).originalType);
+ Assertions.assertEquals(a, leading.getFilters().get(1).bitmap);
+ Assertions.assertEquals(JoinType.INNER_JOIN,
leading.getFilters().get(1).originalType);
+ }
+
+ @Test
+ public void testFullOuterJoinConstraintRequiresExactChildMatch() {
+ // (a FULL OUTER JOIN b ON a.k = b.k) JOIN c
+ // Leading(a c b) → the full outer constraint requires exact child
match.
+ // When children don't match exactly (left={a,c}, right={b}), the full
+ // outer constraint continues (no match), and computeJoinType falls
back
+ // to INNER_JOIN. The FULL_OUTER_JOIN predicate would then be rejected
by
+ // isJoinTypeCompatible and put back, causing a leftover filter
failure.
+ //
+ // This test verifies the constraint matching layer: full outer join
only
+ // matches when children exactly equal original leftHand/rightHand.
+ LeadingHint leading = new LeadingHint("Leading");
+ long a = LongBitmap.newBitmap(0);
+ long b = LongBitmap.newBitmap(1);
+ long c = LongBitmap.newBitmap(2);
+
+ // Full outer join: leftHand={a}, rightHand={b}
+ addJoinConstraint(leading, a, b, a, b, JoinType.FULL_OUTER_JOIN);
+
+ long ab = LongBitmap.newBitmapUnion(a, b);
+ long ac = LongBitmap.newBitmapUnion(a, c);
+ long abc = LongBitmap.newBitmapUnion(ab, c);
+
+ // Exact children match → constraint matches
+ Pair<JoinConstraint, Boolean> exactMatch =
leading.getJoinConstraint(ab, a, b);
+ Assertions.assertTrue(exactMatch.second, "exact children should match
full outer constraint");
+
+ // Extra table mixed in left child → constraint does not match,
+ // falls back to (null, true) = inner join (no constraint matched).
+ // The full-outer predicate loss is caught later by the
leftover-filter check.
+ Pair<JoinConstraint, Boolean> mixedLeft =
leading.getJoinConstraint(abc, ac, b);
+ Assertions.assertNull(mixedLeft.first, "full outer constraint should
not match with extra table");
+ Assertions.assertTrue(mixedLeft.second, "no constraint matched → inner
join is legal at this level");
+ }
+
+ @Test
+ public void testRightOuterJoinBlocksPrematureInnerPredicateConsumption() {
+ // (a RIGHT OUTER JOIN b ON a.k = b.k) INNER JOIN c ON a.x = c.x
+ // Leading(a c b) → the inner predicate a.x = c.x references the
nullable
+ // left side {a} of the RIGHT OUTER JOIN. It must NOT be consumed at
the
+ // premature {a,c} join level because the outer join's preserved side
{b}
+ // has not arrived yet. Consuming it early would produce:
+ // (a INNER JOIN c) RIGHT OUTER JOIN b
+ // which preserves b rows when a is empty — not equivalent to the
original.
+ //
+ // At the {a,c} level the right-outer constraint is not applicable
+ // (minRightHand={b} is absent), so getJoinConstraint returns
(null,true).
+ LeadingHint leading = new LeadingHint("Leading");
+ long a = LongBitmap.newBitmap(0);
+ long b = LongBitmap.newBitmap(1);
+ long c = LongBitmap.newBitmap(2);
+
+ // RIGHT OUTER JOIN constraint: leftHand={a} (nullable), rightHand={b}
(preserved)
+ addJoinConstraint(leading, a, b, a, b, JoinType.RIGHT_OUTER_JOIN);
+
+ long ac = LongBitmap.newBitmapUnion(a, c);
+ long abc = LongBitmap.newBitmapUnion(a, b, c);
+
+ // Level {a,c}: nullable side {a} mixed with unrelated {c} while
+ // preserved side {b} is absent → violated.
+ Pair<JoinConstraint, Boolean> levelAC = leading.getJoinConstraint(ac,
a, c);
+ Assertions.assertNull(levelAC.first,
+ "right outer constraint should not match at {a,c} level");
+ Assertions.assertFalse(levelAC.second,
+ "nullable side {a} mixed with unrelated {c} before preserved
{b} → violated");
+
+ // Level {a,b,c}: constraint matches — children {a,c} and {b}
+ // Wait, the constraint requires leftHand={a} on left and
rightHand={b} on right.
+ // With left={a,c} and right={b}: leftHand={a}⊆{a,c}? That's the
minLeftHand check
+ // in the general matching, not the full-outer exact match.
+ // For RIGHT OUTER JOIN, the constraint uses the general
minLeftHand/minRightHand
+ // matching (not exact match like FULL OUTER JOIN).
+ // minLeftHand={a} ⊆ {a,c}=left? Yes. minRightHand={b} ⊆ {b}=right?
Yes.
+ // → constraint matches as RIGHT_OUTER_JOIN.
+ Pair<JoinConstraint, Boolean> levelABC =
leading.getJoinConstraint(abc, ac, b);
+ Assertions.assertNotNull(levelABC.first, "right outer constraint
should match at {a,b,c} level");
+ Assertions.assertTrue(levelABC.second, "constraint matched");
+ Assertions.assertEquals(JoinType.RIGHT_OUTER_JOIN,
levelABC.first.getJoinType());
+ }
+
+ @Test
+ public void testLeftOuterJoinBlocksPrematureInnerPredicateConsumption() {
+ // (a LEFT OUTER JOIN b ON a.k = b.k) INNER JOIN c ON b.x = c.x
+ // Leading(b c a) → the inner predicate b.x = c.x references the
nullable
+ // right side {b} of the LEFT OUTER JOIN. It must NOT be consumed at
the
+ // premature {b,c} join level before the preserved left side {a}
arrives.
+ LeadingHint leading = new LeadingHint("Leading");
+ long a = LongBitmap.newBitmap(0);
+ long b = LongBitmap.newBitmap(1);
+ long c = LongBitmap.newBitmap(2);
+
+ // LEFT OUTER JOIN constraint: leftHand={a} (preserved), rightHand={b}
(nullable)
+ addJoinConstraint(leading, a, b, a, b, JoinType.LEFT_OUTER_JOIN);
+
+ long bc = LongBitmap.newBitmapUnion(b, c);
+ long abc = LongBitmap.newBitmapUnion(a, b, c);
+
+ // Level {b,c}: the nullable side {b} is mixed with unrelated table {c}
+ // while the preserved side {a} is absent → violated (not skippable).
+ Pair<JoinConstraint, Boolean> levelBC = leading.getJoinConstraint(bc,
b, c);
+ Assertions.assertNull(levelBC.first,
+ "left outer constraint should not match at {b,c} level");
+ Assertions.assertFalse(levelBC.second,
+ "mixing nullable side {b} with unrelated {c} before preserved
{a} arrives → violated");
+
+ // Level {a,b,c}: constraint matches
+ Pair<JoinConstraint, Boolean> levelABC =
leading.getJoinConstraint(abc, a, bc);
+ Assertions.assertNotNull(levelABC.first, "left outer constraint should
match at {a,b,c} level");
+ Assertions.assertTrue(levelABC.second, "constraint matched");
+ Assertions.assertEquals(JoinType.LEFT_OUTER_JOIN,
levelABC.first.getJoinType());
+ }
+
+ @Test
+ public void testLeftOuterJoinNullableSideCannotMixWithUnrelatedTables() {
+ // (a LEFT OUTER JOIN b ON a.k = b.k) CROSS JOIN c
+ // leading(b c a): the nullable side {b} must not CROSS JOIN with {c}
+ // before the preserved side {a} arrives.
+ // Original: (a LEFT JOIN b) CROSS JOIN c
+ // When c is empty: produces 0 rows.
+ // Generated without guard: (b CROSS JOIN c) RIGHT JOIN a
+ // When c is empty: preserves all a rows with NULL c — not
equivalent!
+ LeadingHint leading = new LeadingHint("Leading");
+ long a = LongBitmap.newBitmap(0);
+ long b = LongBitmap.newBitmap(1);
+ long c = LongBitmap.newBitmap(2);
+
+ addJoinConstraint(leading, a, b, a, b, JoinType.LEFT_OUTER_JOIN);
+
+ long bc = LongBitmap.newBitmapUnion(b, c);
+ long abc = LongBitmap.newBitmapUnion(a, b, c);
+
+ // Level {b,c}: violated — nullable side {b} mixed with {c}
+ Pair<JoinConstraint, Boolean> levelBC = leading.getJoinConstraint(bc,
b, c);
+ Assertions.assertFalse(levelBC.second,
+ "{b,c} mixes nullable side with unrelated table → violated");
+
+ // Level {a,b,c}: constraint matches as reversed (LEFT → RIGHT)
+ Pair<JoinConstraint, Boolean> levelABC =
leading.getJoinConstraint(abc, bc, a);
+ Assertions.assertNotNull(levelABC.first, "constraint should match at
{a,b,c}");
+ Assertions.assertTrue(levelABC.second);
+ Assertions.assertTrue(levelABC.first.isReversed(),
+ "should be reversed: left={b,c}, right={a} for LEFT OUTER
JOIN");
+ }
+
+ @Test
+ public void testRightOuterJoinNullableSideCannotMixWithUnrelatedTables() {
+ // (a RIGHT OUTER JOIN b ON a.k = b.k) CROSS JOIN c
+ // leading(a c b): the nullable side {a} must not CROSS JOIN with {c}
+ // before the preserved side {b} arrives.
+ LeadingHint leading = new LeadingHint("Leading");
+ long a = LongBitmap.newBitmap(0);
+ long b = LongBitmap.newBitmap(1);
+ long c = LongBitmap.newBitmap(2);
+
+ addJoinConstraint(leading, a, b, a, b, JoinType.RIGHT_OUTER_JOIN);
+
+ long ac = LongBitmap.newBitmapUnion(a, c);
+ long abc = LongBitmap.newBitmapUnion(a, b, c);
+
+ // For RIGHT OUTER JOIN at {a,c}: leftHand={a} (nullable) overlaps
{a,c},
+ // minRightHand={b} (preserved) is absent, and {a,c} ⊄ leftHand={a} →
+ // violated: nullable side mixed with unrelated table before preserved
arrives.
+ Pair<JoinConstraint, Boolean> levelAC = leading.getJoinConstraint(ac,
a, c);
+ Assertions.assertNull(levelAC.first, "right outer constraint should
not match at {a,c}");
+ Assertions.assertFalse(levelAC.second,
+ "nullable side {a} mixed with unrelated {c} before preserved
{b} → violated");
+
+ // Level {a,b,c}: constraint matches
+ Pair<JoinConstraint, Boolean> levelABC =
leading.getJoinConstraint(abc, ac, b);
+ Assertions.assertNotNull(levelABC.first, "constraint should match at
{a,b,c}");
+ Assertions.assertTrue(levelABC.second);
+ }
+
+ @Test
+ public void testFullOuterJoinPredicateNotConsumedAsScanFilter() {
+ // a FULL OUTER JOIN b ON a.v > 0 — the predicate a.v > 0 belongs to
+ // the full outer join's ON clause. It must NOT be pushed down as a
+ // scan filter on a because rows where a.v <= 0 should produce
+ // NULL-extended rows in the full outer join.
+ //
+ // Verifies: predicates with outer-join originalType are deferred from
+ // scan-level consumption by shouldDeferFromScan.
+ LeadingHint leading = new LeadingHint("Leading");
+ Expression expr = new IntegerLiteral(1);
+ long a = LongBitmap.newBitmap(0);
+
+ // Simulate: FULL_OUTER_JOIN(a,b) records predicate a.v > 0 with
bitmap={a}
+ leading.addFilter(a, expr, JoinType.FULL_OUTER_JOIN);
+ Assertions.assertEquals(JoinType.FULL_OUTER_JOIN,
+ leading.getFilters().get(0).originalType,
+ "full outer predicate should carry FULL_OUTER_JOIN type");
+ }
+
+ @Test
+ public void
testRightOuterJoinUpperPredicateNotConsumedBeforeNullableSide() {
+ // (a LEFT OUTER JOIN b ON a.k = b.k) INNER JOIN c ON b.v > 0
+ // leading(b a c): scan b is visited first. The predicate b.v > 0
+ // has type=INNER_JOIN (from the upper join) and bitmap={b} which is
+ // the nullable right side of the LEFT OUTER JOIN. It must NOT be
+ // pushed as a scan filter on b — it must wait for the outer join.
+ //
+ // Verifies: isBlockedByPendingOuterJoin blocks INNER predicates that
+ // reference the nullable side when the preserved side is absent.
+ LeadingHint leading = new LeadingHint("Leading");
+ long a = LongBitmap.newBitmap(0);
+ long b = LongBitmap.newBitmap(1);
+
+ // LEFT OUTER JOIN constraint: leftHand={a} (preserved), rightHand={b}
(nullable)
+ addJoinConstraint(leading, a, b, a, b, JoinType.LEFT_OUTER_JOIN);
+
+ // The INNER JOIN predicate b.v > 0 has bitmap={b}, type=INNER_JOIN
+ // Pretend we're at scan b: joinTableBitmap={b}
+ // isBlockedByPendingOuterJoin({b}, {b}):
+ // LEFT OUTER JOIN: isOverlap({b}, rightHand={b})=true
+ // && !isSubset(leftHand={a}, {b})=true → blocked!
+ //
+ // getJoinConstraint at {b} level: minRightHand={b} overlaps {b}=true
+ // → the else-branch mustBeLeftjoin guard we added skips because
+ // minLeftHand={a} is not in {b}. So getJoinConstraint returns
(null,true).
+ Pair<JoinConstraint, Boolean> levelB = leading.getJoinConstraint(b, b,
0L);
+ Assertions.assertNull(levelB.first,
+ "left outer constraint should not match at scan {b} (a
absent)");
+ Assertions.assertTrue(levelB.second,
+ "inner join is legal at {b} level — predicate deferral happens
in collectJoinConditions/makeFilterPlanIfExist");
Review Comment:
This added line is 128 characters, but FE checkstyle applies to test sources
and sets `LineLength` to 120. Please wrap the message so `mvn
validate`/`build.sh --fe` does not fail.
```suggestion
"inner join is legal at {b} level - predicate deferral
happens in "
+ "collectJoinConditions/makeFilterPlanIfExist");
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]