From a97548f25a81900174e2aa9eeba4782a57e352c5 Mon Sep 17 00:00:00 2001 From: seawinde Date: Thu, 27 Aug 2026 11:07:06 +0800 Subject: [PATCH] [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. `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. 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. ### Release note Fixed an internal `ClassCastException` during materialized view rewrite for expression-based join keys. Unsupported derived-expression lineage now falls back safely. ### Check List (For Author) - Test: Unit Test - `NullRejectInferenceTest` - Behavior changed: Yes, unsupported derived-expression lineage falls back instead of throwing - Does this need documentation: No --- .../mv/AbstractMaterializedViewRule.java | 4 +- .../mv/NullRejectInferenceTest.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 34eacfd90edd09..f1bfe8d89e33a0 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 @@ -973,7 +973,9 @@ private Set> getShuttledRequireNoNullableViewSlots(Set> requ for (Set 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/NullRejectInferenceTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/NullRejectInferenceTest.java index 5fd7628096b5b5..2f7e8c869bc8fa 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/NullRejectInferenceTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/NullRejectInferenceTest.java @@ -184,6 +184,53 @@ void testNullRejectCompensationForInnerJoinFullJoinRewriteOnRightSide() { .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)) {