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]