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

morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 52c60df9539 [fix](mtmv) Avoid invalid slot cast in MV null-reject 
compensation (#66613)
52c60df9539 is described below

commit 52c60df9539f8305dad738d6e43354f5d42c8508
Author: seawinde <[email protected]>
AuthorDate: Thu Aug 27 11:07:06 2026 +0800

    [fix](mtmv) Avoid invalid slot cast in MV null-reject compensation (#66613)
    
    ### What problem does this PR solve?
    
    Related PR: #43539, #62492, #63268
    
    Problem Summary:
    
    When an INNER JOIN query is matched against a LEFT OUTER JOIN
    materialized
    view, the rewrite must prove that the nullable side is null-rejected.
    The MV
    rule shuttles the nullable-side output Slots through the view plan
    lineage to
    normalize Project and Alias outputs before selecting an `IS NOT NULL`
    compensation Slot.
    
    **Root cause:**
    `AbstractMaterializedViewRule.getShuttledRequireNoNullableViewSlots()`
    assumed that `ExpressionUtils.shuttleExpressionWithLineage()` always returns
    `Slot` values and unconditionally used `Slot.class::cast`. The API returns
    general `Expression` values. Expression JOIN keys such as CAST equality can
    introduce helper projections whose lineage expands to `Cast`, causing a
    `ClassCastException` during MV rewrite. The unsafe assumption was introduced
    by #43539. PR #62492 added INNER JoinEdge null-reject inference, and #63268
    materialized that evidence as compensation, making this path more readily 
reachable.
    
    **Current limitation:** This is a conservative crash fix, not transparent
    rewrite support for CAST or arbitrary derived expressions. If no usable Slot
    remains after lineage expansion, the existing proof checks return invalid 
and
    the MV rewrite safely falls back to base tables. The CAST JOIN case covered 
by
    the test therefore still does not use the MV. Using an expression's input 
Slots
    as compensation evidence is not generally sound because functions and casts
    can change nullability semantics; supporting such expressions requires an
    explicit nullability-preserving proof.
    
    ### Release note
    
    Fixed an internal `ClassCastException` during materialized view rewrite for
    expression-based join keys. Unsupported derived-expression lineage now falls
    back safely.
---
 .../mv/AbstractMaterializedViewRule.java           |  4 +-
 .../exploration/mv/MvExplorationSuiteTest.java     | 47 ++++++++++++++++++++++
 2 files changed, 50 insertions(+), 1 deletion(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java
index 5b24abedbbb..39ccd89558b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/AbstractMaterializedViewRule.java
@@ -965,7 +965,9 @@ public abstract class AbstractMaterializedViewRule 
implements ExplorationRuleFac
         for (Set<Slot> requireNullableSlots : requireNoNullableViewSlot) {
             shuttledRequireNoNullableViewSlot.add(
                     ExpressionUtils.shuttleExpressionWithLineage(new 
ArrayList<>(requireNullableSlots),
-                                    
viewStructInfo.getTopPlan()).stream().map(Slot.class::cast)
+                                    viewStructInfo.getTopPlan()).stream()
+                            .filter(Slot.class::isInstance)
+                            .map(Slot.class::cast)
                             .collect(Collectors.toSet()));
         }
         return shuttledRequireNoNullableViewSlot;
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MvExplorationSuiteTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MvExplorationSuiteTest.java
index c3ce56772a4..b59982b082b 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MvExplorationSuiteTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MvExplorationSuiteTest.java
@@ -926,6 +926,53 @@ public class MvExplorationSuiteTest extends SqlTestBase {
                 .anyMatch(expression -> isNotNullOnSlot(expression, 
"o_orderdate")));
     }
 
+    @Test
+    void testNullRejectCompensationWithCastJoinConditionFallsBack() {
+        
connectContext.getSessionVariable().setDisableNereidsRules("INFER_PREDICATES,PRUNE_EMPTY_PARTITION");
+        CascadesContext queryContext = createCascadesContext(
+                "select lineitem.l_orderkey, orders.o_orderkey, 
orders.o_orderdate from lineitem "
+                        + "inner join orders on cast(lineitem.l_orderkey as 
bigint) "
+                        + "= cast(orders.o_orderkey as bigint)",
+                connectContext
+        );
+        Plan queryPlan = PlanChecker.from(queryContext)
+                .analyze()
+                .rewrite()
+                .applyExploration(RuleSet.BUSHY_TREE_JOIN_REORDER)
+                .getAllPlan().get(0).child(0);
+
+        CascadesContext viewContext = createCascadesContext(
+                "select lineitem.l_orderkey, orders.o_orderkey, 
orders.o_orderdate from lineitem "
+                        + "left outer join orders on cast(lineitem.l_orderkey 
as bigint) "
+                        + "= cast(orders.o_orderkey as bigint)",
+                connectContext
+        );
+        Plan viewPlan = PlanChecker.from(viewContext)
+                .analyze()
+                .rewrite()
+                .applyExploration(RuleSet.BUSHY_TREE_JOIN_REORDER)
+                .getAllPlan().get(0).child(0);
+
+        StructInfo queryStructInfo = StructInfo.of(queryPlan, queryPlan, 
queryContext);
+        StructInfo viewStructInfo = StructInfo.of(viewPlan, viewPlan, 
viewContext);
+        RelationMapping relationMapping = RelationMapping.generate(
+                queryStructInfo.getRelations(), viewStructInfo.getRelations(), 
8).get(0);
+        SlotMapping queryToView = SlotMapping.generate(relationMapping);
+        SlotMapping viewToQuery = queryToView.inverse();
+        LogicalCompatibilityContext compatibilityContext = 
LogicalCompatibilityContext.from(
+                relationMapping, viewToQuery, queryStructInfo, viewStructInfo);
+        ComparisonResult comparisonResult = StructInfo.isGraphLogicalEquals(
+                queryStructInfo, viewStructInfo, compatibilityContext);
+
+        Assertions.assertFalse(comparisonResult.isInvalid());
+        
Assertions.assertFalse(comparisonResult.getViewNoNullableSlot().isEmpty());
+
+        SplitPredicate compensatePredicates = Assertions.assertDoesNotThrow(
+                () -> TEST_RULE.predicatesCompensateForTest(
+                        queryStructInfo, viewStructInfo, viewToQuery, 
comparisonResult, queryContext));
+        Assertions.assertTrue(compensatePredicates.isInvalid());
+    }
+
     private static boolean isNotNullOnSlot(Expression expression, String 
slotName) {
         if (!(expression instanceof Not) || ((Not) 
expression).isGeneratedIsNotNull()
                 || !(((Not) expression).child() instanceof IsNull)) {


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

Reply via email to