github-actions[bot] commented on code in PR #65467:
URL: https://github.com/apache/doris/pull/65467#discussion_r3569370579


##########
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.

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);

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).

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]

Reply via email to