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 9adfd2d8a1f [fix](mv) Compensate complete invalid roll-up partition 
buckets (#67882)
9adfd2d8a1f is described below

commit 9adfd2d8a1fda119e9f309ce27d2f8612563015d
Author: morrySnow <[email protected]>
AuthorDate: Mon Sep 14 19:03:05 2026 +0800

    [fix](mv) Compensate complete invalid roll-up partition buckets (#67882)
    
    ## Problem
    
    During partition-union rewrite, a roll-up materialized-view partition
    can cover multiple base-table partitions. When that MV partition is
    invalid and removed as a whole, compensating only the base partitions
    that overlap the raw query partition set can leave the replacement scope
    smaller than the removed MV bucket and produce incorrect aggregate
    results.
    
    ## Root cause
    
    The compensation calculation used the intersection between an invalid MV
    partition's complete base-partition mapping and the raw query partitions
    as both the relevance test and the compensation set. This lost the
    atomic relationship between one MV partition and every base partition
    mapped to it.
    
    ## Reproduction
    
    Use one valid MV partition mapped to `p3`, one invalid roll-up partition
    mapped to `{p1, p2}`, and another invalid partition mapped to `{p4,
    p5}`. For a raw query partition set `{p1, p3}`, the old calculation
    compensated only `p1`, even though the complete intersecting roll-up
    bucket was removed. The disjoint `{p4, p5}` bucket must still remain
    outside compensation.
    
    ## Fix
    
    Use the intersection only to decide whether an invalid MV partition is
    relevant to the query. Once relevant, add its complete base-partition
    mapping to compensation. Invalid MV partitions with no intersection
    remain excluded, preserving the existing query-range guard. The existing
    multi-PCT-table merge behavior is unchanged.
    
    ## Tests
    
    - `./run-fe-ut.sh --run
    org.apache.doris.nereids.rules.exploration.mv.PartitionCompensatorTest`
    (13 tests passed)
    - `DISABLE_BUILD_UI=ON ./build.sh --fe` (passed; the host's Node.js 16
    cannot build the current UI, so the supported no-UI FE build path was
    used)
    - `./run-regression-test.sh --run -s mtmv_range_date_part_up_rewrite
    ...` (1 suite passed)
---
 .../rules/exploration/mv/PartitionCompensator.java |  7 +-
 .../exploration/mv/PartitionCompensatorTest.java   | 78 ++++++++++++++++++++++
 2 files changed, 83 insertions(+), 2 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java
index a8613cf3dc6..13bd6973555 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensator.java
@@ -200,8 +200,11 @@ public class PartitionCompensator {
                 // Base table partition maybe deleted, need not union
                 continue;
             }
-            Sets.intersection(baseTablePartitions, 
queryUsedBaseTablePartitionNameSet)
-                    .copyInto(baseTableNeedUnionPartitionNameSet);
+            if (!Sets.intersection(baseTablePartitions, 
queryUsedBaseTablePartitionNameSet).isEmpty()) {
+                // An MV partition is the atomic unit removed from the 
rewritten plan. If any base
+                // partition in its roll-up bucket is used by the query, 
compensate the whole bucket.
+                baseTableNeedUnionPartitionNameSet.addAll(baseTablePartitions);
+            }
         }
         // If related base table creates partitions or mv is created with ttl, 
need base table union
         Sets.difference(queryUsedBaseTablePartitionNameSet, 
mvValidBaseTablePartitionNameSet)
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorTest.java
index 0ad6d8b49ed..e3ab9688e5f 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/PartitionCompensatorTest.java
@@ -487,6 +487,84 @@ public class PartitionCompensatorTest extends 
TestWithFeService {
                 .forEach(v -> Assertions.assertEquals(expectedUnion, v));
     }
 
+    @SuppressWarnings("unchecked")
+    @Test
+    public void 
testCalcInvalidPartitionsCompensatesWholeIntersectingRollupBucket()
+            throws Exception {
+        DatabaseIf<?> baseDb = mockDatabase("cat", 1L, "db", 2L);
+        MTMVRelatedTableIf relatedTable = mockRelatedTableIf(
+                "base_t", 10L, ImmutableList.of("cat", "db", "base_t"), 
baseDb);
+        BaseColInfo colInfo = new BaseColInfo("dt", new 
BaseTableInfo(relatedTable));
+
+        DatabaseIf<?> mvDb = mockDatabase("internal", 3L, "mv_db", 4L);
+        MTMV mtmv = Mockito.mock(MTMV.class);
+        Mockito.when(mtmv.getName()).thenReturn("mv1");
+        Mockito.when(mtmv.getId()).thenReturn(100L);
+        Mockito.when(mtmv.getDatabase()).thenReturn(mvDb);
+        Mockito.when(mtmv.selectNonEmptyPartitionIds(ArgumentMatchers.any(), 
ArgumentMatchers.any()))
+                .thenReturn(ImmutableList.of(1L));
+
+        long validMvPartitionId = 101L;
+        long partiallyStaleMvPartitionId = 102L;
+        long disjointMvPartitionId = 103L;
+        Partition validMvPartition = mockPartition(validMvPartitionId, 
"mv_valid");
+        Partition partiallyStaleMvPartition = 
mockPartition(partiallyStaleMvPartitionId, "mv_partially_stale");
+        Partition disjointMvPartition = mockPartition(disjointMvPartitionId, 
"mv_disjoint");
+        
Mockito.when(mtmv.getPartition(validMvPartitionId)).thenReturn(validMvPartition);
+        
Mockito.when(mtmv.getPartition(partiallyStaleMvPartitionId)).thenReturn(partiallyStaleMvPartition);
+        
Mockito.when(mtmv.getPartition(disjointMvPartitionId)).thenReturn(disjointMvPartition);
+
+        PartitionInfo mvPartitionInfo = Mockito.mock(PartitionInfo.class);
+        Mockito.when(mtmv.getPartitionInfo()).thenReturn(mvPartitionInfo);
+        
Mockito.when(mvPartitionInfo.getType()).thenReturn(PartitionType.RANGE);
+        MTMVPartitionInfo mvPctInfo = Mockito.mock(MTMVPartitionInfo.class);
+        Mockito.when(mtmv.getMvPartitionInfo()).thenReturn(mvPctInfo);
+        
Mockito.when(mvPctInfo.getPctTables()).thenReturn(ImmutableSet.of(relatedTable));
+        
Mockito.when(mvPctInfo.getPctInfos()).thenReturn(ImmutableList.of(colInfo));
+
+        Map<String, Set<String>> relatedPartitionMapping = new HashMap<>();
+        relatedPartitionMapping.put("mv_valid", ImmutableSet.of("p3"));
+        relatedPartitionMapping.put("mv_partially_stale", 
ImmutableSet.of("p1", "p2"));
+        relatedPartitionMapping.put("mv_disjoint", ImmutableSet.of("p4", 
"p5"));
+        Map<MTMVRelatedTableIf, Map<String, Set<String>>> partitionMappings = 
new HashMap<>();
+        partitionMappings.put(relatedTable, relatedPartitionMapping);
+
+        AsyncMaterializationContext matCtx = 
Mockito.mock(AsyncMaterializationContext.class);
+        Mockito.when(matCtx.getMtmv()).thenReturn(mtmv);
+        
Mockito.when(matCtx.calculatePartitionMappings(ArgumentMatchers.any())).thenReturn(partitionMappings);
+
+        Map<BaseTableInfo, Collection<Partition>> canRewriteMap = new 
HashMap<>();
+        canRewriteMap.put(new BaseTableInfo(mtmv), 
ImmutableList.of(validMvPartition));
+        StatementContext stmtCtx = Mockito.mock(StatementContext.class);
+        
Mockito.when(stmtCtx.getMvCanRewritePartitionsMap()).thenReturn(canRewriteMap);
+        CascadesContext cascadesCtx = Mockito.mock(CascadesContext.class);
+        Mockito.when(cascadesCtx.getStatementContext()).thenReturn(stmtCtx);
+
+        LogicalOlapScan selectedMvScan = Mockito.mock(LogicalOlapScan.class);
+        Mockito.when(selectedMvScan.getTable()).thenReturn(mtmv);
+        Mockito.when(selectedMvScan.getSelectedPartitionIds())
+                .thenReturn(ImmutableList.of(
+                        validMvPartitionId, partiallyStaleMvPartitionId, 
disjointMvPartitionId));
+        Plan rewrittenPlan = Mockito.mock(Plan.class);
+        Mockito.when(rewrittenPlan.collectToList(ArgumentMatchers.any()))
+                .thenReturn(ImmutableList.of(selectedMvScan));
+
+        // The query touches p1 in the stale roll-up bucket and p3 in a valid 
bucket. Once the stale
+        // MV partition is removed, its complete {p1, p2} mapping must be 
compensated atomically.
+        // The completely disjoint {p4, p5} bucket is removed from the MV scan 
but is not compensated.
+        Map<List<String>, Set<String>> queryUsedPartitions = new HashMap<>();
+        queryUsedPartitions.put(relatedTable.getFullQualifiers(), 
ImmutableSet.of("p1", "p3"));
+
+        Pair<Map<BaseTableInfo, Set<String>>, Map<BaseColInfo, Set<String>>> 
result =
+                PartitionCompensator.calcInvalidPartitions(
+                        queryUsedPartitions, rewrittenPlan, matCtx, 
cascadesCtx);
+
+        Assertions.assertNotNull(result);
+        Assertions.assertEquals(ImmutableSet.of("mv_partially_stale", 
"mv_disjoint"),
+                result.key().get(new BaseTableInfo(mtmv)));
+        Assertions.assertEquals(ImmutableSet.of("p1", "p2"), 
result.value().get(colInfo));
+    }
+
     @SuppressWarnings("unchecked")
     @Test
     public void 
testCalcInvalidPartitionsDoesNotCompensateBasePartitionsUnusedByQuery()


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

Reply via email to