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 2e89f3a81cb [fix](mv) Prevent snapshot reads from using current MVs 
(#67700)
2e89f3a81cb is described below

commit 2e89f3a81cbf0effe06491a383109ab36ec72923
Author: morrySnow <[email protected]>
AuthorDate: Wed Sep 9 16:19:12 2026 +0800

    [fix](mv) Prevent snapshot reads from using current MVs (#67700)
    
    ## Problem
    
    An external-table query that explicitly reads an older snapshot can
    still enter materialized view rewrite. The candidate materialized view
    represents the table state at its refresh snapshot, so using a current
    materialized view for a historical query can silently return data from
    the wrong point in time.
    
    ## Root cause
    
    `LogicalFileScan` stores standard `FOR TIME AS OF` and `FOR VERSION AS
    OF` clauses in `tableSnapshot`. The materialized-view eligibility
    checker rejected scans with table samples or scan parameters, but did
    not inspect `tableSnapshot`. As a result, the historical scan was
    treated like an ordinary latest-snapshot scan.
    
    ## How to reproduce
    
    1. Create an Iceberg table and insert an initial row, then record that
    snapshot ID.
    2. Insert newer data and refresh a materialized view over the table at
    the current snapshot.
    3. Enable materialized-view rewrite and query the Iceberg table with
    `FOR VERSION AS OF <old_snapshot_id>` (the same issue applies to `FOR
    TIME AS OF`).
    4. Before this change, the historical query can be considered eligible
    for rewrite by the current-snapshot materialized view, producing current
    rather than historical results.
    
    The same condition can be reproduced directly in the optimizer by
    building a `LogicalFileScan` with a non-empty `tableSnapshot`: the
    table-query-operator checker previously returned false.
    
    ## Fix
    
    Treat a non-empty `LogicalFileScan.tableSnapshot` as a table-level query
    operator, alongside table samples and scan parameters. This
    conservatively prevents materialized-view rewrite until the optimizer
    can prove that the query snapshot and materialized-view refresh snapshot
    are semantically equivalent.
    
    Add a unit test that constructs a file scan with a version snapshot and
    verifies that the checker rejects it from ordinary rewrite eligibility.
---
 .../nereids/rules/exploration/mv/MaterializedViewUtils.java  |  6 ++++--
 .../rules/exploration/mv/MaterializedViewUtilsTest.java      | 12 ++++++++++++
 2 files changed, 16 insertions(+), 2 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtils.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtils.java
index 4b6221cd5a9..9ccf8de2114 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtils.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtils.java
@@ -630,9 +630,11 @@ public class MaterializedViewUtils {
         public Boolean visitLogicalRelation(LogicalRelation relation, Void 
context) {
             if (relation instanceof LogicalFileScan) {
                 LogicalFileScan fileScan = (LogicalFileScan) relation;
-                // Relation scan parameters can select data different from the 
MV refresh input.
+                // Relation scan operators can select data different from the 
MV refresh input.
                 // Treat them as query operators until rewrite can prove 
equivalent semantics.
-                if (fileScan.getTableSample().isPresent() || 
fileScan.getScanParams().isPresent()) {
+                if (fileScan.getTableSample().isPresent()
+                        || fileScan.getScanParams().isPresent()
+                        || fileScan.getTableSnapshot().isPresent()) {
                     return true;
                 }
             }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtilsTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtilsTest.java
index db357bed441..7f235171043 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtilsTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/exploration/mv/MaterializedViewUtilsTest.java
@@ -18,6 +18,7 @@
 package org.apache.doris.nereids.rules.exploration.mv;
 
 import org.apache.doris.analysis.TableScanParams;
+import org.apache.doris.analysis.TableSnapshot;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.catalog.TableIf;
 import org.apache.doris.mtmv.BaseTableInfo;
@@ -1016,6 +1017,17 @@ public class MaterializedViewUtilsTest extends 
TestWithFeService {
                 .visitLogicalRelation(fileScan, null));
     }
 
+    @Test
+    public void containTableQueryOperatorWithTableSnapshotTest() {
+        LogicalFileScan fileScan = Mockito.mock(LogicalFileScan.class);
+        Mockito.when(fileScan.getTableSample()).thenReturn(Optional.empty());
+        Mockito.when(fileScan.getScanParams()).thenReturn(Optional.empty());
+        
Mockito.when(fileScan.getTableSnapshot()).thenReturn(Optional.of(TableSnapshot.versionOf("1")));
+
+        
Assertions.assertTrue(MaterializedViewUtils.TableQueryOperatorChecker.INSTANCE
+                .visitLogicalRelation(fileScan, null));
+    }
+
     @Test
     public void getRelatedTableInfoWhenMultiPartitionExprs() {
         PlanChecker.from(connectContext)


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

Reply via email to