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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 9488297efce branch-4.1: [fix](fe) Backport null-reject MV rewrite 
fixes (#62492, #63268) (#63593)
9488297efce is described below

commit 9488297efce7a58bf5b29df54c4c37bdf6de5bf8
Author: seawinde <[email protected]>
AuthorDate: Thu Jun 4 15:38:46 2026 +0800

    branch-4.1: [fix](fe) Backport null-reject MV rewrite fixes (#62492, 
#63268) (#63593)
    
    pr: #62492, #63268
    commitId: 673b028a, 76bbe581
    
    Backport #62492 and its correctness follow-up #63268 together to
    branch-4.1.
    
    #62492 infers null-reject evidence from INNER JoinEdge for multi-hop
    outer join MV rewrite.
    #63268 adds the required real IS NOT NULL compensation when INNER
    JoinEdge proof is needed for OUTER JOIN MV rewrite, preventing extra
    null-padded rows.
    
    Tests:
    - git diff --check upstream/branch-4.1..HEAD
    - Tried ./run-fe-ut.sh --run
    org.apache.doris.nereids.rules.exploration.mv.NullRejectInferenceTest
    twice. The first run failed during fe-core compilation because generated
    thrift classes such as TIcebergPartitionField/TRecCTENode/TMCCommitData
    were not available from fe-common classes. After generated thrift
    sources were refreshed, the second run failed before tests during
    fe-core generate-patterns with NoClassDefFoundError:
    org/apache/doris/nereids/pattern/generator/ExpressionTypeMappingGenerator.
    No test case was executed locally.
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
    Co-authored-by: seawinde <[email protected]>
---
 .../mv/AbstractMaterializedViewRule.java           | 184 +++++++++++++----
 .../exploration/mv/NullRejectInferenceTest.java    | 209 ++++++++++++++++++++
 .../mv/dimension/dimension_1.groovy                |   2 +-
 .../mv/dimension/dimension_2_left_join.groovy      |   8 +-
 .../mv/dimension/dimension_2_right_join.groovy     |   8 +-
 .../mv/dimension/dimension_self_conn.groovy        |   5 +-
 .../mv/dimension_predicate/left_join_filter.groovy |   4 +-
 .../dimension_predicate/right_join_filter.groovy   |   4 +-
 .../outer_join_two_hop_null_reject.groovy          | 138 +++++++++++++
 .../join_elim_line_pattern.groovy                  |   4 +-
 .../join_elim_star_pattern.groovy                  |  16 +-
 .../inner_join_infer_and_derive.groovy             |  24 +--
 .../inner_join_null_reject_compensation.groovy     | 217 +++++++++++++++++++++
 .../left_join_infer_and_derive.groovy              |   6 +-
 .../right_join_infer_and_derive.groovy             |   6 +-
 15 files changed, 750 insertions(+), 85 deletions(-)

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 9f76c242dcf..9429ac30645 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
@@ -18,6 +18,7 @@
 package org.apache.doris.nereids.rules.exploration.mv;
 
 import org.apache.doris.catalog.MTMV;
+import org.apache.doris.catalog.TableIf;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.Id;
 import org.apache.doris.common.Pair;
@@ -29,6 +30,7 @@ import org.apache.doris.mtmv.MTMVRelatedTableIf;
 import org.apache.doris.nereids.CascadesContext;
 import org.apache.doris.nereids.StatementContext;
 import org.apache.doris.nereids.jobs.executor.Rewriter;
+import org.apache.doris.nereids.jobs.joinorder.hypergraph.edge.JoinEdge;
 import org.apache.doris.nereids.properties.LogicalProperties;
 import org.apache.doris.nereids.properties.OrderKey;
 import org.apache.doris.nereids.rules.exploration.ExplorationRuleFactory;
@@ -43,6 +45,7 @@ import 
org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
 import org.apache.doris.nereids.rules.rewrite.MergeProjectable;
 import org.apache.doris.nereids.trees.expressions.ComparisonPredicate;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.IsNull;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
 import org.apache.doris.nereids.trees.expressions.Not;
 import org.apache.doris.nereids.trees.expressions.Slot;
@@ -821,21 +824,28 @@ public abstract class AbstractMaterializedViewRule 
implements ExplorationRuleFac
         Set<Set<Slot>> requireNoNullableViewSlot = 
comparisonResult.getViewNoNullableSlot();
         // check query is use the null reject slot which view comparison need
         if (!requireNoNullableViewSlot.isEmpty()) {
+            // Required null-reject slots are recorded on the view side. Map 
query slots to view slots
+            // before checking whether query predicates or INNER JoinEdges can 
reject those null rows.
             SlotMapping queryToViewMapping = viewToQuerySlotMapping.inverse();
-            // try to use
-            boolean valid = containsNullRejectSlot(requireNoNullableViewSlot,
-                    queryStructInfo.getPredicates().getPulledUpPredicates(), 
queryToViewMapping, queryStructInfo,
-                    viewStructInfo, cascadesContext);
-            if (!valid) {
+            Optional<Set<Expression>> 
queryBasedNullRejectCompensationPredicates =
+                    getQueryBasedNullRejectCompensationPredicates(
+                            requireNoNullableViewSlot,
+                            
queryStructInfo.getPredicates().getPulledUpPredicates(), queryToViewMapping,
+                            queryStructInfo, viewStructInfo, 
viewToQuerySlotMapping, cascadesContext);
+            if (!queryBasedNullRejectCompensationPredicates.isPresent()) {
                 queryStructInfo = 
queryStructInfo.withPredicates(queryStructInfo.getPredicates()
                         
.mergePulledUpPredicates(comparisonResult.getQueryAllPulledUpExpressions()));
-                valid = containsNullRejectSlot(requireNoNullableViewSlot,
-                        
queryStructInfo.getPredicates().getPulledUpPredicates(), queryToViewMapping,
-                        queryStructInfo, viewStructInfo, cascadesContext);
+                queryBasedNullRejectCompensationPredicates = 
getQueryBasedNullRejectCompensationPredicates(
+                        requireNoNullableViewSlot, 
queryStructInfo.getPredicates().getPulledUpPredicates(),
+                        queryToViewMapping, queryStructInfo, viewStructInfo, 
viewToQuerySlotMapping, cascadesContext);
             }
-            if (!valid) {
+            if (!queryBasedNullRejectCompensationPredicates.isPresent()) {
                 return SplitPredicate.INVALID_INSTANCE;
             }
+            if (!queryBasedNullRejectCompensationPredicates.get().isEmpty()) {
+                queryStructInfo = 
queryStructInfo.withPredicates(queryStructInfo.getPredicates()
+                        
.mergePulledUpPredicates(queryBasedNullRejectCompensationPredicates.get()));
+            }
         }
         // compensate couldNot PulledUp Conjunctions
         Map<Expression, ExpressionInfo> couldNotPulledUpCompensateConjunctions 
=
@@ -863,46 +873,106 @@ public abstract class AbstractMaterializedViewRule 
implements ExplorationRuleFac
     }
 
     /**
-     * Check the queryPredicates contains the required nullable slot
+     * Check whether query-side null-reject evidence covers each required 
view-side slot set.
+     *
+     * <p>The check is view-based because the required null-reject slots come 
from the MV join graph.
+     * The returned compensation predicates are query-based because they will 
be merged into queryStructInfo.
+     *
+     * <p>Return meanings:
+     * Optional.empty(): no valid proof, or no safe output slot can carry the 
compensation predicate.
+     * Optional.of(emptySet()): existing query predicates already provide the 
required null-reject.
+     * Optional.of(nonEmptySet): INNER JoinEdge proof must be materialized as 
these IS NOT NULL predicates.
      */
-    private boolean containsNullRejectSlot(Set<Set<Slot>> 
requireNoNullableViewSlot,
+    private Optional<Set<Expression>> 
getQueryBasedNullRejectCompensationPredicates(
+            Set<Set<Slot>> requireNoNullableViewSlot,
             Set<Expression> queryPredicates,
             SlotMapping queryToViewMapping,
             StructInfo queryStructInfo,
             StructInfo viewStructInfo,
+            SlotMapping viewToQueryMapping,
             CascadesContext cascadesContext) {
-        Set<Expression> queryPulledUpPredicates = queryPredicates.stream()
-                .flatMap(expr -> 
ExpressionUtils.extractConjunction(expr).stream())
-                .map(expr -> {
-                    // NOTICE inferNotNull generate Not with 
isGeneratedIsNotNull = false,
-                    //  so, we need set this flag to false before comparison.
-                    if (expr instanceof Not) {
-                        return ((Not) expr).withGeneratedIsNotNull(false);
-                    }
-                    return expr;
-                })
+        Set<Slot> predicateNullRejectViewSlots = getViewBasedNullRejectSlots(
+                getPredicateNullRejectSlots(queryPredicates, cascadesContext), 
queryToViewMapping, queryStructInfo);
+        Set<Slot> innerJoinNullRejectViewSlots = getViewBasedNullRejectSlots(
+                getInnerJoinNullRejectSlots(queryStructInfo, cascadesContext), 
queryToViewMapping, queryStructInfo);
+        Set<Slot> allNullRejectViewSlots = new 
HashSet<>(predicateNullRejectViewSlots);
+        allNullRejectViewSlots.addAll(innerJoinNullRejectViewSlots);
+        if (allNullRejectViewSlots.isEmpty()) {
+            return Optional.empty();
+        }
+        Set<Slot> viewOutputSlots = 
viewStructInfo.getPlanOutputShuttledExpressions().stream()
+                .filter(Slot.class::isInstance)
+                .map(Slot.class::cast)
                 .collect(Collectors.toSet());
-        Set<Expression> queryNullRejectPredicates =
-                ExpressionUtils.inferNotNull(queryPulledUpPredicates, 
cascadesContext);
-        if (queryPulledUpPredicates.containsAll(queryNullRejectPredicates)) {
-            // Query has no null reject predicates, return
-            return false;
+        Map<SlotReference, SlotReference> viewToQuerySlotReferenceMap = 
viewToQueryMapping.toSlotReferenceMap();
+        Set<Expression> compensationPredicates = new HashSet<>();
+        for (Set<Slot> requiredViewSlots : 
getShuttledRequireNoNullableViewSlots(
+                requireNoNullableViewSlot, viewStructInfo)) {
+            if (Sets.intersection(requiredViewSlots, 
allNullRejectViewSlots).isEmpty()) {
+                return Optional.empty();
+            }
+            if (!Sets.intersection(requiredViewSlots, 
predicateNullRejectViewSlots).isEmpty()) {
+                continue;
+            }
+            Optional<Slot> compensationViewSlot = findCompensationViewSlot(
+                    requiredViewSlots, viewOutputSlots, 
innerJoinNullRejectViewSlots);
+            if (!compensationViewSlot.isPresent()) {
+                return Optional.empty();
+            }
+            Slot querySlot = 
viewToQuerySlotReferenceMap.get(compensationViewSlot.get());
+            if (querySlot == null) {
+                return Optional.empty();
+            }
+            compensationPredicates.add(new Not(new IsNull(querySlot), false));
+        }
+        return Optional.of(compensationPredicates);
+    }
+
+    private Set<Slot> getPredicateNullRejectSlots(Set<Expression> 
queryPredicates, CascadesContext cascadesContext) {
+        Set<Slot> nullRejectSlots = new HashSet<>();
+        for (Expression queryPredicate : queryPredicates) {
+            
TypeUtils.isNotNull(queryPredicate).ifPresent(nullRejectSlots::add);
+        }
+        for (Expression inferredNotNull : 
ExpressionUtils.inferNotNull(queryPredicates, cascadesContext)) {
+            
TypeUtils.isNotNull(inferredNotNull).ifPresent(nullRejectSlots::add);
         }
-        // Get query null reject predicate slots
-        Set<Expression> queryNullRejectSlotSet = new HashSet<>();
-        for (Expression queryNullRejectPredicate : queryNullRejectPredicates) {
-            Optional<Slot> notNullSlot = 
TypeUtils.isNotNull(queryNullRejectPredicate);
-            if (!notNullSlot.isPresent()) {
+        return nullRejectSlots;
+    }
+
+    private Set<Slot> getInnerJoinNullRejectSlots(StructInfo queryStructInfo, 
CascadesContext cascadesContext) {
+        Set<Slot> nullRejectSlots = new HashSet<>();
+        // INNER JOIN conditions guarantee NOT NULL on join-key slots.
+        // After EliminateOuterJoin converts LEFT to INNER, the JoinEdge 
objects in the HyperGraph
+        // retain the INNER type even though EliminateNotNull removes 
filter-level NOT NULL predicates.
+        for (JoinEdge joinEdge : 
queryStructInfo.getHyperGraph().getJoinEdges()) {
+            if (joinEdge.getJoinType().isInnerJoin()) {
+                nullRejectSlots.addAll(ExpressionUtils.inferNotNullSlots(
+                        ImmutableSet.copyOf(joinEdge.getExpressions()), 
cascadesContext));
+            }
+        }
+        return nullRejectSlots;
+    }
+
+    private Set<Slot> getViewBasedNullRejectSlots(Set<Slot> 
queryNullRejectSlots,
+            SlotMapping queryToViewMapping, StructInfo queryStructInfo) {
+        Set<Slot> viewBasedSlots = new HashSet<>();
+        for (Slot queryNullRejectSlot : queryNullRejectSlots) {
+            Expression shuttledQuerySlot = 
ExpressionUtils.shuttleExpressionWithLineage(
+                    queryNullRejectSlot, queryStructInfo.getTopPlan());
+            if (!(shuttledQuerySlot instanceof Slot)) {
                 continue;
             }
-            queryNullRejectSlotSet.add(notNullSlot.get());
+            Expression viewSlot = ExpressionUtils.replace(shuttledQuerySlot,
+                    queryToViewMapping.toSlotReferenceMap());
+            if (viewSlot instanceof Slot) {
+                viewBasedSlots.add((Slot) viewSlot);
+            }
         }
-        // query slot need shuttle to use table slot, avoid alias influence
-        Set<Expression> queryUsedNeedRejectNullSlotsViewBased = 
ExpressionUtils.shuttleExpressionWithLineage(
-                        new ArrayList<>(queryNullRejectSlotSet), 
queryStructInfo.getTopPlan()).stream()
-                .map(expr -> ExpressionUtils.replace(expr, 
queryToViewMapping.toSlotReferenceMap()))
-                .collect(Collectors.toSet());
-        // view slot need shuttle to use table slot, avoid alias influence
+        return viewBasedSlots;
+    }
+
+    private Set<Set<Slot>> 
getShuttledRequireNoNullableViewSlots(Set<Set<Slot>> requireNoNullableViewSlot,
+            StructInfo viewStructInfo) {
         Set<Set<Slot>> shuttledRequireNoNullableViewSlot = new HashSet<>();
         for (Set<Slot> requireNullableSlots : requireNoNullableViewSlot) {
             shuttledRequireNoNullableViewSlot.add(
@@ -910,9 +980,41 @@ public abstract class AbstractMaterializedViewRule 
implements ExplorationRuleFac
                                     
viewStructInfo.getTopPlan()).stream().map(Slot.class::cast)
                             .collect(Collectors.toSet()));
         }
-        // query pulledUp predicates should have null reject predicates and 
contains any require noNullable slot
-        return 
shuttledRequireNoNullableViewSlot.stream().noneMatch(viewRequiredNullSlotSet ->
-                Sets.intersection(viewRequiredNullSlotSet, 
queryUsedNeedRejectNullSlotsViewBased).isEmpty());
+        return shuttledRequireNoNullableViewSlot;
+    }
+
+    private Optional<Slot> findCompensationViewSlot(Set<Slot> 
requiredViewSlots, Set<Slot> viewOutputSlots,
+            Set<Slot> innerJoinNullRejectViewSlots) {
+        Set<Slot> outputRequiredSlots = Sets.intersection(requiredViewSlots, 
viewOutputSlots);
+        Optional<Slot> compensationViewSlot = outputRequiredSlots.stream()
+                .filter(innerJoinNullRejectViewSlots::contains)
+                .findFirst();
+        if (compensationViewSlot.isPresent()) {
+            return compensationViewSlot;
+        }
+        return outputRequiredSlots.stream()
+                .filter(slot -> 
isOriginalNonNullableSlotOnInnerJoinProofTable(slot, 
innerJoinNullRejectViewSlots))
+                .findFirst();
+    }
+
+    private boolean isOriginalNonNullableSlotOnInnerJoinProofTable(Slot slot, 
Set<Slot> innerJoinNullRejectViewSlots) {
+        if (!(slot instanceof SlotReference)) {
+            return false;
+        }
+        SlotReference slotReference = (SlotReference) slot;
+        if (!slotReference.getOriginalColumn().map(column -> 
!column.isAllowNull()).orElse(!slot.nullable())) {
+            return false;
+        }
+        Optional<TableIf> originalTable = slotReference.getOriginalTable();
+        if (!originalTable.isPresent()) {
+            return false;
+        }
+        return innerJoinNullRejectViewSlots.stream()
+                .filter(SlotReference.class::isInstance)
+                .map(SlotReference.class::cast)
+                .map(SlotReference::getOriginalTable)
+                .anyMatch(referenceTable -> referenceTable.isPresent()
+                        && referenceTable.get().equals(originalTable.get()));
     }
 
     /**
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
new file mode 100644
index 00000000000..5fd7628096b
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/NullRejectInferenceTest.java
@@ -0,0 +1,209 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.exploration.mv;
+
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.rules.Rule;
+import org.apache.doris.nereids.rules.RuleSet;
+import org.apache.doris.nereids.rules.exploration.mv.Predicates.SplitPredicate;
+import org.apache.doris.nereids.rules.exploration.mv.mapping.RelationMapping;
+import org.apache.doris.nereids.rules.exploration.mv.mapping.SlotMapping;
+import org.apache.doris.nereids.sqltest.SqlTestBase;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.Not;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.util.PlanChecker;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+class NullRejectInferenceTest extends SqlTestBase {
+
+    private static final TestMaterializedViewRule TEST_RULE = new 
TestMaterializedViewRule();
+
+    @Test
+    void testTwoHopNullRejectFromInnerJoinConditions() {
+        
connectContext.getSessionVariable().setDisableNereidsRules("INFER_PREDICATES,PRUNE_EMPTY_PARTITION");
+        CascadesContext queryContext = createCascadesContext(
+                "select lineitem.l_orderkey, supplier.s_name, nation.n_name 
from lineitem "
+                        + "inner join supplier on lineitem.l_suppkey = 
supplier.s_suppkey "
+                        + "inner join nation on supplier.s_nationkey = 
nation.n_nationkey "
+                        + "where nation.n_name = 'CHINA'",
+                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, supplier.s_name, nation.n_name 
from lineitem "
+                        + "left outer join supplier on lineitem.l_suppkey = 
supplier.s_suppkey "
+                        + "left outer join nation on supplier.s_nationkey = 
nation.n_nationkey",
+                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 = 
TEST_RULE.predicatesCompensateForTest(
+                queryStructInfo, viewStructInfo, viewToQuery, 
comparisonResult, queryContext);
+        Assertions.assertFalse(compensatePredicates.isInvalid());
+        Assertions.assertTrue(compensatePredicates.toList().stream()
+                .anyMatch(expression -> isNotNullOnSlot(expression, 
"s_name")));
+    }
+
+    @Test
+    void testNullRejectCompensationForInnerJoinFullJoinRewrite() {
+        
connectContext.getSessionVariable().setDisableNereidsRules("INFER_PREDICATES,PRUNE_EMPTY_PARTITION");
+        CascadesContext queryContext = createCascadesContext(
+                "select lineitem.l_shipdate, orders.o_orderdate from lineitem "
+                        + "inner join orders on lineitem.l_orderkey = 
orders.o_orderkey "
+                        + "where orders.o_orderdate = '2023-10-17'",
+                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_shipdate, orders.o_orderdate from lineitem "
+                        + "full outer join orders on lineitem.l_orderkey = 
orders.o_orderkey",
+                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 = 
TEST_RULE.predicatesCompensateForTest(
+                queryStructInfo, viewStructInfo, viewToQuery, 
comparisonResult, queryContext);
+        Assertions.assertFalse(compensatePredicates.isInvalid());
+        Assertions.assertTrue(compensatePredicates.toList().stream()
+                .anyMatch(expression -> isNotNullOnSlot(expression, 
"l_shipdate")));
+    }
+
+    @Test
+    void testNullRejectCompensationForInnerJoinFullJoinRewriteOnRightSide() {
+        
connectContext.getSessionVariable().setDisableNereidsRules("INFER_PREDICATES,PRUNE_EMPTY_PARTITION");
+        CascadesContext queryContext = createCascadesContext(
+                "select lineitem.l_shipdate, orders.o_orderdate from lineitem "
+                        + "inner join orders on lineitem.l_orderkey = 
orders.o_orderkey "
+                        + "where lineitem.l_shipdate = '2023-10-17'",
+                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_shipdate, orders.o_orderdate from lineitem "
+                        + "full outer join orders on lineitem.l_orderkey = 
orders.o_orderkey",
+                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 = 
TEST_RULE.predicatesCompensateForTest(
+                queryStructInfo, viewStructInfo, viewToQuery, 
comparisonResult, queryContext);
+        Assertions.assertFalse(compensatePredicates.isInvalid());
+        Assertions.assertTrue(compensatePredicates.toList().stream()
+                .anyMatch(expression -> isNotNullOnSlot(expression, 
"o_orderdate")));
+    }
+
+    private static boolean isNotNullOnSlot(Expression expression, String 
slotName) {
+        if (!(expression instanceof Not) || ((Not) 
expression).isGeneratedIsNotNull()
+                || !(((Not) expression).child() instanceof IsNull)) {
+            return false;
+        }
+        Expression slot = ((IsNull) ((Not) expression).child()).child();
+        return slot instanceof SlotReference && 
slotName.equals(((SlotReference) slot).getName());
+    }
+
+    private static class TestMaterializedViewRule extends 
AbstractMaterializedViewRule {
+        @Override
+        public List<Rule> buildRules() {
+            return ImmutableList.of();
+        }
+
+        private SplitPredicate predicatesCompensateForTest(StructInfo 
queryStructInfo,
+                StructInfo viewStructInfo, SlotMapping viewToQuerySlotMapping,
+                ComparisonResult comparisonResult, CascadesContext 
cascadesContext) {
+            return predicatesCompensate(queryStructInfo, viewStructInfo, 
viewToQuerySlotMapping,
+                    comparisonResult, cascadesContext);
+        }
+    }
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_1.groovy 
b/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_1.groovy
index c5f205638e6..73cdc618d86 100644
--- a/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_1.groovy
+++ b/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_1.groovy
@@ -378,7 +378,7 @@ suite("partition_mv_rewrite_dimension_1") {
         waitingMTMVTaskFinished(job_name)
         for (int j = 0; j < join_type_stmt_list.size(); j++) {
             logger.info("j:" + j)
-            if (i == j) {
+            if (i == j || (j == 1 && i in [0, 2, 3])) {
                 mv_rewrite_success(join_type_stmt_list[j], join_type_mv)
                 compare_res(join_type_stmt_list[j] + " order by 1,2,3,4")
             } else {
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_2_left_join.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_2_left_join.groovy
index 70a45b3fd97..8a6c0a70292 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_2_left_join.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_2_left_join.groovy
@@ -210,7 +210,7 @@ suite("partition_mv_rewrite_dimension_2_1") {
         if (i == 0) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [ 0, 2, 4, 5, 10, 11]) {
+                if (j in [ 0, 2, 4, 5, 8, 10, 11]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
@@ -230,7 +230,7 @@ suite("partition_mv_rewrite_dimension_2_1") {
         } else if (i == 2) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [0, 2, 4, 5, 10, 11]) {
+                if (j in [0, 2, 4, 5, 8, 10, 11]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
@@ -281,7 +281,7 @@ suite("partition_mv_rewrite_dimension_2_1") {
         } else if (i == 7) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [4, 5, 7, 9, 10, 11]) {
+                if (j in [3, 4, 5, 7, 9, 10, 11]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
@@ -301,7 +301,7 @@ suite("partition_mv_rewrite_dimension_2_1") {
         } else if (i == 9) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [4, 5, 7, 9, 10, 11]) {
+                if (j in [3, 4, 5, 7, 9, 10, 11]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_2_right_join.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_2_right_join.groovy
index f3fd0795b4b..b9c27e366fa 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_2_right_join.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_2_right_join.groovy
@@ -211,7 +211,7 @@ suite("partition_mv_rewrite_dimension_2_right_join") {
         if (i == 0) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [ 0, 2, 4, 5, 10, 11]) {
+                if (j in [ 0, 2, 4, 5, 8, 10, 11]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
@@ -231,7 +231,7 @@ suite("partition_mv_rewrite_dimension_2_right_join") {
         } else if (i == 2) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [0, 2, 4, 5, 10, 11]) {
+                if (j in [0, 2, 4, 5, 8, 10, 11]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
@@ -282,7 +282,7 @@ suite("partition_mv_rewrite_dimension_2_right_join") {
         } else if (i == 7) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [4, 5, 7, 9, 10, 11]) {
+                if (j in [3, 4, 5, 7, 9, 10, 11]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
@@ -302,7 +302,7 @@ suite("partition_mv_rewrite_dimension_2_right_join") {
         } else if (i == 9) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [4, 5, 7, 9, 10, 11]) {
+                if (j in [3, 4, 5, 7, 9, 10, 11]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_self_conn.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_self_conn.groovy
index e722b6eeb60..7a4995ad80c 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_self_conn.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/dimension/dimension_self_conn.groovy
@@ -293,7 +293,10 @@ suite("partition_mv_rewrite_dimension_self_conn") {
         } else {
             for (int j = 0; j < join_type_stmt_list.size(); j++) {
                 logger.info("j:" + j)
-                if (i == j) {
+                // INNER query can use an OUTER JOIN MV only when the MV 
output has a safe slot
+                // to filter null-generated rows. Here only RIGHT JOIN MV 
exposes a non-nullable
+                // left-side slot for that compensation.
+                if (i == j || (j == 1 && i == 2)) {
                     mv_rewrite_success(join_type_stmt_list[j], 
join_type_self_conn_mv)
                     compare_res(join_type_stmt_list[j] + " order by 1,2,3")
                 } else {
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/dimension_predicate/left_join_filter.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/dimension_predicate/left_join_filter.groovy
index 37c9d3bfde4..74972d1fedc 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/dimension_predicate/left_join_filter.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/dimension_predicate/left_join_filter.groovy
@@ -216,7 +216,7 @@ suite("left_join_filter") {
         } else if (i == 2) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [0, 2, 4, 9]) {
+                if (j in [0, 2, 4, 7, 9]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
@@ -276,7 +276,7 @@ suite("left_join_filter") {
         } else if (i == 8) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [4, 6, 8, 9]) {
+                if (j in [3, 4, 6, 8, 9]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/dimension_predicate/right_join_filter.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/dimension_predicate/right_join_filter.groovy
index c29effd36cb..20d4c125a93 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/dimension_predicate/right_join_filter.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/dimension_predicate/right_join_filter.groovy
@@ -226,7 +226,7 @@ suite("right_join_filter") {
         } else if (i == 3) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [1, 3, 4, 9]) {
+                if (j in [1, 3, 4, 8, 9]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
@@ -266,7 +266,7 @@ suite("right_join_filter") {
         } else if (i == 7) {
             for (int j = 0; j < mv_list_1.size(); j++) {
                 logger.info("j:" + j)
-                if (j in [4, 5, 7, 9]) {
+                if (j in [2, 4, 5, 7, 9]) {
                     mv_rewrite_success(mv_list_1[j], mv_name)
                     compare_res(mv_list_1[j] + " order by 1,2,3,4,5")
                 } else {
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/join/left_outer/outer_join_two_hop_null_reject.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/join/left_outer/outer_join_two_hop_null_reject.groovy
new file mode 100644
index 00000000000..d481394777c
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/mv/join/left_outer/outer_join_two_hop_null_reject.groovy
@@ -0,0 +1,138 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("outer_join_two_hop_null_reject") {
+    String db = context.config.getDbNameByFile(context.file)
+    sql "use ${db}"
+    sql "set runtime_filter_mode=OFF"
+    sql "set enable_nereids_planner=true"
+    sql "set enable_fallback_to_original_planner=false"
+    sql "set enable_materialized_view_rewrite=true"
+    sql "set enable_nereids_timeout=false"
+
+    sql """drop table if exists fact_orders_2hop"""
+    sql """drop table if exists dim_stores_2hop"""
+    sql """drop table if exists dim_regions_2hop"""
+
+    sql """
+        CREATE TABLE IF NOT EXISTS fact_orders_2hop (
+          order_date DATE NOT NULL,
+          store_id INT NOT NULL,
+          amount DECIMALV3(10, 2) NOT NULL
+        )
+        DUPLICATE KEY(order_date, store_id)
+        DISTRIBUTED BY HASH(store_id) BUCKETS 1
+        PROPERTIES (
+          "replication_num" = "1"
+        )
+    """
+
+    sql """
+        CREATE TABLE IF NOT EXISTS dim_stores_2hop (
+          id INT NOT NULL,
+          store_name VARCHAR(32) NOT NULL
+        )
+        DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES (
+          "replication_num" = "1"
+        )
+    """
+
+    sql """
+        CREATE TABLE IF NOT EXISTS dim_regions_2hop (
+          store_id INT NOT NULL,
+          region_name VARCHAR(32) NOT NULL
+        )
+        DUPLICATE KEY(store_id)
+        DISTRIBUTED BY HASH(store_id) BUCKETS 1
+        PROPERTIES (
+          "replication_num" = "1"
+        )
+    """
+
+    sql """
+        insert into fact_orders_2hop values
+        ('2024-01-01', 1, 100.00),
+        ('2024-01-01', 2, 200.00),
+        ('2024-01-02', 3, 300.00),
+        ('2024-01-02', 4, 400.00)
+    """
+
+    sql """
+        insert into dim_stores_2hop values
+        (1, 'Store A'),
+        (2, 'Store B'),
+        (3, 'Store C')
+    """
+
+    sql """
+        insert into dim_regions_2hop values
+        (1, 'West'),
+        (2, 'East'),
+        (3, 'West')
+    """
+
+    sql """analyze table fact_orders_2hop with sync"""
+    sql """analyze table dim_stores_2hop with sync"""
+    sql """analyze table dim_regions_2hop with sync"""
+
+    def compare_res = { def stmt ->
+        sql "set enable_materialized_view_rewrite=false"
+        def origin_res = sql stmt
+        logger.info("origin_res: " + origin_res)
+        sql "set enable_materialized_view_rewrite=true"
+        def mv_origin_res = sql stmt
+        logger.info("mv_origin_res: " + mv_origin_res)
+        assertTrue((mv_origin_res == [] && origin_res == []) || 
(mv_origin_res.size() == origin_res.size()))
+        for (int row = 0; row < mv_origin_res.size(); row++) {
+            assertTrue(mv_origin_res[row].size() == origin_res[row].size())
+            for (int col = 0; col < mv_origin_res[row].size(); col++) {
+                assertTrue(mv_origin_res[row][col] == origin_res[row][col])
+            }
+        }
+    }
+
+    def mvName = "mv_orders_2hop_null_reject"
+    def mvSql = """
+        select o.order_date, o.store_id, d.store_name, r.region_name,
+               sum(o.amount) as total_amount
+        from fact_orders_2hop o
+        left join dim_stores_2hop d
+          on o.store_id = d.id
+        left join dim_regions_2hop r
+          on d.id = r.store_id
+        group by o.order_date, o.store_id, d.store_name, r.region_name
+    """
+    def querySql = """
+        select o.order_date, sum(o.amount)
+        from fact_orders_2hop o
+        left join dim_stores_2hop d
+          on o.store_id = d.id
+        left join dim_regions_2hop r
+          on d.id = r.store_id
+        where r.region_name = 'West'
+        group by o.order_date
+        order by 1
+    """
+
+    create_async_mv(db, mvName, mvSql)
+    mv_rewrite_success_without_check_chosen(querySql, mvName)
+    compare_res(querySql)
+
+    sql """drop materialized view if exists ${mvName}"""
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/join_elim_p_f_key/join_elim_line_pattern.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/join_elim_p_f_key/join_elim_line_pattern.groovy
index 9e1618d18dd..b5dda41c914 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/join_elim_p_f_key/join_elim_line_pattern.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/join_elim_p_f_key/join_elim_line_pattern.groovy
@@ -316,7 +316,7 @@ suite("join_elim_line_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [3, 5, 7]) {
+        if (j in [2, 3, 4, 5, 6, 7]) {
             mv_rewrite_success_without_check_chosen(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 5)
         } else {
@@ -484,7 +484,7 @@ suite("join_elim_line_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [3, 5, 7]) {
+        if (j in [2, 3, 4, 5, 6, 7]) {
             mv_rewrite_success_without_check_chosen(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 5)
         } else {
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/join_elim_p_f_key/join_elim_star_pattern.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/join_elim_p_f_key/join_elim_star_pattern.groovy
index d8850796a3e..c3fe74ce768 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/join_elim_p_f_key/join_elim_star_pattern.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/join_elim_p_f_key/join_elim_star_pattern.groovy
@@ -295,7 +295,7 @@ suite("join_elim_star_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13]) {
+        if (j in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) {
             mv_rewrite_success(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 4)
         } else {
@@ -326,7 +326,7 @@ suite("join_elim_star_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [9, 11, 13]) {
+        if (j in [8, 9, 10, 11, 12, 13]) {
             mv_rewrite_success(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 4)
         } else {
@@ -358,7 +358,7 @@ suite("join_elim_star_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [3, 5, 7]) {
+        if (j in [2, 3, 4, 5, 6, 7]) {
             mv_rewrite_success(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 4)
         } else {
@@ -393,7 +393,7 @@ suite("join_elim_star_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13]) {
+        if (j in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) {
             mv_rewrite_success(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 4)
         } else {
@@ -428,7 +428,7 @@ suite("join_elim_star_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [1, 2, 3, 4, 5, 6, 7, 9, 11, 13]) {
+        if (j in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) {
             mv_rewrite_success(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 4)
         } else {
@@ -462,7 +462,7 @@ suite("join_elim_star_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [1, 2, 3, 4, 5, 6, 7, 9, 11, 13]) {
+        if (j in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) {
             mv_rewrite_success(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 4)
         } else {
@@ -492,7 +492,7 @@ suite("join_elim_star_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [9, 11, 13]) {
+        if (j in [8, 9, 10, 11, 12, 13]) {
             mv_rewrite_success(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 4)
         } else {
@@ -523,7 +523,7 @@ suite("join_elim_star_pattern") {
     create_async_mv(db, mv_name, mv_stmt_2)
     for (int j = 1; j < query_list.size() + 1; j++) {
         logger.info("left mv current query index: " + j)
-        if (j in [3, 5, 7]) {
+        if (j in [2, 3, 4, 5, 6, 7]) {
             mv_rewrite_success(query_list[j - 1], mv_name)
             compare_res(query_list[j - 1], 4)
         } else {
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/inner_join_infer_and_derive.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/inner_join_infer_and_derive.groovy
index 15709ed1f6f..c5768f1b95d 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/inner_join_infer_and_derive.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/inner_join_infer_and_derive.groovy
@@ -221,32 +221,20 @@ suite("inner_join_infer_and_derive") {
         if (mtmv_it == 0) {
             for (int i = 0; i < query_list.size(); i++) {
                 logger.info("i: " + i)
-                if (i in [0, 2, 5, 6, 8, 11]) {
-                    mv_rewrite_fail(query_list[i], mv_name_1)
-                } else {
-                    mv_rewrite_success(query_list[i], mv_name_1)
-                    compare_res(query_list[i] + order_stmt)
-                }
+                mv_rewrite_success(query_list[i], mv_name_1)
+                compare_res(query_list[i] + order_stmt)
             }
         } else if (mtmv_it == 1) {
             for (int i = 0; i < query_list.size(); i++) {
                 logger.info("i: " + i)
-                if (i in [1, 3, 4, 7, 9, 10]) {
-                    mv_rewrite_fail(query_list[i], mv_name_1)
-                } else {
-                    mv_rewrite_success(query_list[i], mv_name_1)
-                    compare_res(query_list[i] + order_stmt)
-                }
+                mv_rewrite_success(query_list[i], mv_name_1)
+                compare_res(query_list[i] + order_stmt)
             }
         } else if (mtmv_it == 2) {
             for (int i = 0; i < query_list.size(); i++) {
                 logger.info("i: " + i)
-                if (i in [12, 13]) {
-                    mv_rewrite_success(query_list[i], mv_name_1)
-                    compare_res(query_list[i] + order_stmt)
-                } else {
-                    mv_rewrite_fail(query_list[i], mv_name_1)
-                }
+                mv_rewrite_success(query_list[i], mv_name_1)
+                compare_res(query_list[i] + order_stmt)
             }
         }
         sql """DROP MATERIALIZED VIEW IF EXISTS ${mv_name_1};"""
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/inner_join_null_reject_compensation.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/inner_join_null_reject_compensation.groovy
new file mode 100644
index 00000000000..a678dd461c8
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/inner_join_null_reject_compensation.groovy
@@ -0,0 +1,217 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("inner_join_null_reject_compensation") {
+    String db = context.config.getDbNameByFile(context.file)
+    sql "use ${db}"
+    sql "set runtime_filter_mode=OFF"
+    sql "set enable_nereids_planner=true"
+    sql "set enable_fallback_to_original_planner=false"
+    sql "set enable_materialized_view_rewrite=true"
+    sql "set pre_materialized_view_rewrite_strategy=FORCE_IN_RBO"
+    sql "set enable_nereids_timeout=false"
+
+    sql """drop materialized view if exists 
mv_inner_join_null_reject_compensation"""
+    sql """drop materialized view if exists mv_repro_left_join"""
+    sql """drop materialized view if exists 
mv_repro_left_join_missing_right_output"""
+    sql """drop materialized view if exists 
mv_repro_left_join_nullable_right_output"""
+    sql """drop table if exists mv_repro_a"""
+    sql """drop table if exists mv_repro_b"""
+    sql """drop table if exists orders_inner_join_null_reject"""
+    sql """drop table if exists lineitem_inner_join_null_reject"""
+
+    sql """
+        create table lineitem_inner_join_null_reject (
+            l_orderkey int not null,
+            l_shipdate date not null,
+            l_suppkey int not null
+        )
+        duplicate key(l_orderkey)
+        distributed by hash(l_orderkey) buckets 1
+        properties (
+            "replication_num" = "1"
+        )
+    """
+
+    sql """
+        create table orders_inner_join_null_reject (
+            o_orderkey int not null,
+            o_orderdate date not null
+        )
+        duplicate key(o_orderkey)
+        distributed by hash(o_orderkey) buckets 1
+        properties (
+            "replication_num" = "1"
+        )
+    """
+
+    sql """
+        create table mv_repro_a (
+            id int null,
+            k int null
+        )
+        duplicate key(id)
+        distributed by hash(id) buckets 1
+        properties (
+            "replication_num" = "1"
+        )
+    """
+
+    sql """
+        create table mv_repro_b (
+            k int null,
+            v int null
+        )
+        duplicate key(k)
+        distributed by hash(k) buckets 1
+        properties (
+            "replication_num" = "1"
+        )
+    """
+
+    sql """
+        insert into lineitem_inner_join_null_reject values
+            (1, '2023-10-17', 10),
+            (999, '2023-10-17', 20)
+    """
+
+    sql """
+        insert into orders_inner_join_null_reject values
+            (1, '2023-10-17'),
+            (888, '2023-10-17')
+    """
+
+    sql """
+        insert into mv_repro_a values
+            (1, 10),
+            (2, 20)
+    """
+
+    sql """
+        insert into mv_repro_b values
+            (10, 100)
+    """
+
+    sql """analyze table lineitem_inner_join_null_reject with sync"""
+    sql """analyze table orders_inner_join_null_reject with sync"""
+    sql """analyze table mv_repro_a with sync"""
+    sql """analyze table mv_repro_b with sync"""
+
+    def withUseMvHint = { def stmt, def mvName ->
+        stmt.replaceFirst("(?i)\\bselect\\b", "select /*+ use_mv(${mvName}) 
*/")
+    }
+
+    def compare_res_with_forced_mv = { def stmt, def mvName ->
+        def stmtWithUseMvHint = withUseMvHint(stmt, mvName)
+        sql "set enable_materialized_view_rewrite=false"
+        def origin_res = sql stmt
+        logger.info("origin_res: " + origin_res)
+        sql "set enable_materialized_view_rewrite=true"
+        mv_rewrite_success(stmtWithUseMvHint, mvName)
+        def mv_origin_res = sql stmtWithUseMvHint
+        logger.info("mv_origin_res: " + mv_origin_res)
+        assertTrue((mv_origin_res == [] && origin_res == []) || 
(mv_origin_res.size() == origin_res.size()))
+        for (int row = 0; row < mv_origin_res.size(); row++) {
+            assertTrue(mv_origin_res[row].size() == origin_res[row].size())
+            for (int col = 0; col < mv_origin_res[row].size(); col++) {
+                assertTrue(mv_origin_res[row][col] == origin_res[row][col])
+            }
+        }
+    }
+
+    def mvName = "mv_inner_join_null_reject_compensation"
+    def mvSql = """
+        select l.l_shipdate, l.l_suppkey, o.o_orderdate
+        from lineitem_inner_join_null_reject l
+        full outer join orders_inner_join_null_reject o
+        on l.l_orderkey = o.o_orderkey
+    """
+
+    create_async_mv(db, mvName, mvSql)
+
+    def queryNeedLeftSideCompensation = """
+        select l.l_shipdate, l.l_suppkey, o.o_orderdate
+        from lineitem_inner_join_null_reject l
+        inner join orders_inner_join_null_reject o
+        on l.l_orderkey = o.o_orderkey
+        where o.o_orderdate = '2023-10-17'
+        order by 1, 2, 3
+    """
+
+    def queryNeedRightSideCompensation = """
+        select l.l_shipdate, l.l_suppkey, o.o_orderdate
+        from lineitem_inner_join_null_reject l
+        inner join orders_inner_join_null_reject o
+        on l.l_orderkey = o.o_orderkey
+        where l.l_shipdate = '2023-10-17'
+        order by 1, 2, 3
+    """
+
+    compare_res_with_forced_mv(queryNeedLeftSideCompensation, mvName)
+    compare_res_with_forced_mv(queryNeedRightSideCompensation, mvName)
+
+    def leftJoinMvName = "mv_repro_left_join"
+    def leftJoinMvSql = """
+        select
+            a.id as a_id,
+            a.k as a_k,
+            b.k as b_k,
+            b.v as b_v
+        from mv_repro_a a
+        left join mv_repro_b b
+        on a.k = b.k
+    """
+
+    create_async_mv(db, leftJoinMvName, leftJoinMvSql)
+
+    def innerJoinQueryOnLeftJoinMv = """
+        select a.id
+        from mv_repro_a a
+        inner join mv_repro_b b
+        on a.k = b.k
+        order by 1
+    """
+
+    compare_res_with_forced_mv(innerJoinQueryOnLeftJoinMv, leftJoinMvName)
+
+    def leftJoinMvWithoutRightOutputName = 
"mv_repro_left_join_missing_right_output"
+    def leftJoinMvWithoutRightOutputSql = """
+        select
+            a.id as a_id,
+            a.k as a_k
+        from mv_repro_a a
+        left join mv_repro_b b
+        on a.k = b.k
+    """
+
+    create_async_mv(db, leftJoinMvWithoutRightOutputName, 
leftJoinMvWithoutRightOutputSql)
+    mv_rewrite_fail(innerJoinQueryOnLeftJoinMv, 
leftJoinMvWithoutRightOutputName)
+
+    def leftJoinMvWithNullableRightOutputName = 
"mv_repro_left_join_nullable_right_output"
+    def leftJoinMvWithNullableRightOutputSql = """
+        select
+            a.id as a_id,
+            a.k as a_k,
+            b.v as b_v
+        from mv_repro_a a
+        left join mv_repro_b b
+        on a.k = b.k
+    """
+
+    create_async_mv(db, leftJoinMvWithNullableRightOutputName, 
leftJoinMvWithNullableRightOutputSql)
+    mv_rewrite_fail(innerJoinQueryOnLeftJoinMv, 
leftJoinMvWithNullableRightOutputName)
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/left_join_infer_and_derive.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/left_join_infer_and_derive.groovy
index 7e51847c3b1..000e1caef1d 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/left_join_infer_and_derive.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/left_join_infer_and_derive.groovy
@@ -211,7 +211,11 @@ suite("left_join_infer_and_derive") {
         if (mtmv_it == 0) {
             for (int i = 0; i < query_list.size(); i++) {
                 logger.info("i: " + i)
-                if (i in [1, 3, 4, 6, 8, 11]) {
+                // For query_stmt_4, the LEFT JOIN is converted to INNER JOIN 
before MV rewrite,
+                // but the join condition can be simplified away, leaving the 
INNER JoinEdge with
+                // no expressions. Without JoinEdge expressions, null-reject 
slots cannot be
+                // inferred, so this case is expected to fail.
+                if (i in [1, 4, 6]) {
                     mv_rewrite_fail(query_list[i], mv_name_1)
                 } else {
                     mv_rewrite_success(query_list[i], mv_name_1)
diff --git 
a/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/right_join_infer_and_derive.groovy
 
b/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/right_join_infer_and_derive.groovy
index 1761efa5ef4..e43cf21e0cc 100644
--- 
a/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/right_join_infer_and_derive.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/mv/join_infer_derive/right_join_infer_and_derive.groovy
@@ -211,7 +211,11 @@ suite("right_join_infer_and_derive") {
         if (mtmv_it == 0) {
             for (int i = 0; i < query_list.size(); i++) {
                 logger.info("i: " + i)
-                if (i in [0, 2, 5, 7, 9, 10]) {
+                // For query_stmt_10, the RIGHT JOIN is converted to INNER 
JOIN before MV rewrite,
+                // but the join condition can be simplified away, leaving the 
INNER JoinEdge with
+                // no expressions. Without JoinEdge expressions, null-reject 
slots cannot be
+                // inferred, so this case is expected to fail.
+                if (i in [0, 7, 10]) {
                     mv_rewrite_fail(query_list[i], mv_name_1)
                 } else {
                     mv_rewrite_success(query_list[i], mv_name_1)


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

Reply via email to