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

seawinde 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 2d3a5b1584f [fix](fe) Keep table version monotonic after truncate 
(#66255)
2d3a5b1584f is described below

commit 2d3a5b1584fc967e13b5c24bd826b6ef3b0d1fc8
Author: seawinde <[email protected]>
AuthorDate: Mon Aug 10 09:53:45 2026 +0800

    [fix](fe) Keep table version monotonic after truncate (#66255)
    
    
    
    MTMV snapshots for non-PCT base tables compare `(tableId,
    tableVersion)`.
    `TRUNCATE` keeps the table ID but reset the non-Cloud table version to
    1,
    so later writes could reuse a previously recorded snapshot and cause
    AUTO
    refresh to miss changed data.
    
    **Root cause:** `InternalCatalog.truncateTableInternal()` replaced the
    old
    partitions and then called `OlapTable.resetVisibleVersion()`. A sequence
    such
    as version 3 -> TRUNCATE to 1 -> two writes to version 3 creates an ABA
    collision for table-level snapshot consumers.
    
    **Change Summary:**
    
    | File | Change Description |
    |------|--------------------|
    | `InternalCatalog.java` | Increment the non-Cloud table version once
    after whole-table or partition TRUNCATE; Cloud remains Meta
    Service-managed |
    | `OlapTable.java`, `TableAttributes.java` | Remove the TRUNCATE-only
    version reset helpers |
    | `TruncateTableCommandTest.java` | Verify whole-table and partition
    TRUNCATE each advance the table version once |
    | `test_truncate_table_mtmv.groovy` | Reproduce the non-PCT table
    snapshot ABA while preserving the existing expected output |
    | `truncate_version_reset.groovy` | Keep the SimpleAggCache regression
    description consistent with monotonic versions |
    
    **Design Rationale:** The table version represents table-level data
    changes,
    while PCT refresh separately compares partition ID and partition
    version.
    Reusing the existing table version avoids new persisted flags or
    MTMV-specific
    invalidation state. Live execution and journal replay use the same
    locked path.
    Cloud is skipped locally because `commit_partition` already advances and
    returns the Meta Service table version.
---
 .../apache/doris/datasource/InternalCatalog.java   | 22 +++++++----
 .../apache/doris/persist/TruncateTableInfo.java    | 16 +++++++-
 .../plans/commands/TruncateTableCommandTest.java   |  5 +++
 .../doris/persist/TruncateTableInfoTest.java       | 46 ++++++++++++++++++++++
 .../data/mtmv_p0/test_truncate_table_mtmv.out      |  4 ++
 .../suites/mtmv_p0/test_truncate_table_mtmv.groovy | 30 +++++++++++++-
 .../truncate_version_reset.groovy                  | 32 ++-------------
 7 files changed, 117 insertions(+), 38 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
index 793122ddb0b..aa68e03d49b 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/InternalCatalog.java
@@ -3801,8 +3801,10 @@ public class InternalCatalog implements 
CatalogIf<Database> {
 
             //replace
             Map<Long, RecyclePartitionParam> recyclePartitionParamMap  =  new 
HashMap<>();
+            long version = Config.isNotCloudMode() ? 
olapTable.getNextVersion() : 0L;
+            long versionTimeMs = Config.isNotCloudMode() ? 
System.currentTimeMillis() : 0L;
             oldPartitions = truncateTableInternal(olapTable, newPartitions,
-                    truncateEntireTable, recyclePartitionParamMap, forceDrop);
+                    truncateEntireTable, recyclePartitionParamMap, forceDrop, 
version, versionTimeMs);
             if (truncateEntireTable) {
                 
Env.getCurrentEnv().getAnalysisManager().removeTableStats(olapTable.getId());
             } else {
@@ -3814,7 +3816,7 @@ public class InternalCatalog implements 
CatalogIf<Database> {
             TruncateTableInfo info =
                     new TruncateTableInfo(db.getId(), db.getFullName(), 
olapTable.getId(), olapTable.getName(),
                     newPartitions, truncateEntireTable,
-                            rawTruncateSql, oldPartitions, forceDrop, 
updateRecords);
+                            rawTruncateSql, oldPartitions, forceDrop, 
updateRecords, version, versionTimeMs);
             Env.getCurrentEnv().getEditLog().logTruncateTable(info);
         } catch (DdlException e) {
             failedCleanCallback.run();
@@ -3830,7 +3832,8 @@ public class InternalCatalog implements 
CatalogIf<Database> {
     }
 
     private List<Partition> truncateTableInternal(OlapTable olapTable, 
List<Partition> newPartitions,
-            boolean isEntireTable, Map<Long, RecyclePartitionParam> 
recyclePartitionParamMap, boolean isforceDrop) {
+            boolean isEntireTable, Map<Long, RecyclePartitionParam> 
recyclePartitionParamMap, boolean isforceDrop,
+            long version, long versionTimeMs) {
         // use new partitions to replace the old ones.
         List<Partition> oldPartitions = Lists.newArrayList();
         for (Partition newPartition : newPartitions) {
@@ -3860,9 +3863,13 @@ public class InternalCatalog implements 
CatalogIf<Database> {
             
olapTable.dropPartitionForTruncate(olapTable.getDatabase().getId(), 
isforceDrop, pair.getValue());
         }
 
-        // Reset table-level visibleVersion to TABLE_INIT_VERSION so it stays 
consistent
-        // with the newly created partitions (which also start at 
PARTITION_INIT_VERSION).
-        olapTable.resetVisibleVersion();
+        if (Config.isNotCloudMode() && version > 0) {
+            // Persisted values make the version transition deterministic 
during journal replay.
+            olapTable.updateVisibleVersionAndTime(version, versionTimeMs);
+        } else {
+            // Preserve legacy replay and Cloud's local cache invalidation 
behavior.
+            olapTable.resetVisibleVersion();
+        }
 
         return oldPartitions;
     }
@@ -3876,7 +3883,8 @@ public class InternalCatalog implements 
CatalogIf<Database> {
         try {
             Map<Long, RecyclePartitionParam> recyclePartitionParamMap =  new 
HashMap<>();
             truncateTableInternal(olapTable, info.getPartitions(), 
info.isEntireTable(),
-                                    recyclePartitionParamMap, isForceDrop);
+                                    recyclePartitionParamMap, isForceDrop,
+                                    info.getVersion(), 
info.getVersionTimeMs());
 
             // add tablet to inverted index
             TabletInvertedIndex invertedIndex = Env.getCurrentInvertedIndex();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/persist/TruncateTableInfo.java 
b/fe/fe-core/src/main/java/org/apache/doris/persist/TruncateTableInfo.java
index b846d1acbdc..7edffceaf0b 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/persist/TruncateTableInfo.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/persist/TruncateTableInfo.java
@@ -60,6 +60,10 @@ public class TruncateTableInfo implements Writable {
     private Map<Long, Long> updateRecords;
     @SerializedName(value = "ut")
     private long updateTime;
+    @SerializedName(value = "version")
+    private long version;
+    @SerializedName(value = "versionTime")
+    private long versionTimeMs;
 
     public TruncateTableInfo() {
 
@@ -68,7 +72,7 @@ public class TruncateTableInfo implements Writable {
     // for internal table
     public TruncateTableInfo(long dbId, String db, long tblId, String table, 
List<Partition> partitions,
             boolean isEntireTable, String rawSql, List<Partition> 
oldPartitions, boolean force,
-            Map<Long, Long> updateRecords) {
+            Map<Long, Long> updateRecords, long version, long versionTimeMs) {
         this.dbId = dbId;
         this.db = db;
         this.tblId = tblId;
@@ -81,6 +85,8 @@ public class TruncateTableInfo implements Writable {
         }
         this.force = force;
         this.updateRecords = updateRecords;
+        this.version = version;
+        this.versionTimeMs = versionTimeMs;
     }
 
     // for external table
@@ -144,6 +150,14 @@ public class TruncateTableInfo implements Writable {
         return updateTime;
     }
 
+    public long getVersion() {
+        return version;
+    }
+
+    public long getVersionTimeMs() {
+        return versionTimeMs;
+    }
+
     public static TruncateTableInfo read(DataInput in) throws IOException {
         String json = Text.readString(in);
         return GsonUtils.GSON.fromJson(json, TruncateTableInfo.class);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/TruncateTableCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/TruncateTableCommandTest.java
index 3bc1e8a43df..53bd64e64d4 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/TruncateTableCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/TruncateTableCommandTest.java
@@ -166,11 +166,15 @@ public class TruncateTableCommandTest extends 
TestWithFeService {
         checkShowTabletResultNum("internal.testcommand.tblcommand", 
"p20210903", 4);
         checkShowTabletResultNum("internal.testcommand.tblcommand", 
"p20210904", 5);
 
+        OlapTable table = 
Env.getCurrentInternalCatalog().getDbNullable("testcommand")
+                .getOlapTableOrDdlException("tblcommand");
+        long visibleVersion = table.getVisibleVersion();
         String truncateStr = "truncate table internal.testcommand.tblcommand;";
         NereidsParser nereidsParser = new NereidsParser();
         LogicalPlan plan = nereidsParser.parseSingle(truncateStr);
         Assertions.assertTrue(plan instanceof TruncateTableCommand);
         Env.getCurrentEnv().truncateTable((TruncateTableCommand) plan);
+        Assertions.assertEquals(visibleVersion + 1, table.getVisibleVersion());
 
         checkShowTabletResultNum("internal.testcommand.tblcommand", 
"p20210901", 2);
         checkShowTabletResultNum("internal.testcommand.tblcommand", 
"p20210902", 3);
@@ -181,6 +185,7 @@ public class TruncateTableCommandTest extends 
TestWithFeService {
         plan = nereidsParser.parseSingle(truncateStr);
         Assertions.assertTrue(plan instanceof TruncateTableCommand);
         Env.getCurrentEnv().truncateTable((TruncateTableCommand) plan);
+        Assertions.assertEquals(visibleVersion + 2, table.getVisibleVersion());
 
         checkShowTabletResultNum("internal.testcommand.tblcommand", 
"p20210901", 2);
         checkShowTabletResultNum("internal.testcommand.tblcommand", 
"p20210902", 3);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/persist/TruncateTableInfoTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/persist/TruncateTableInfoTest.java
new file mode 100644
index 00000000000..7a5ec9a4db9
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/persist/TruncateTableInfoTest.java
@@ -0,0 +1,46 @@
+// 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.persist;
+
+import org.apache.doris.persist.gson.GsonUtils;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TruncateTableInfoTest {
+    @Test
+    void testVersionSerialization() {
+        TruncateTableInfo info = new TruncateTableInfo(1L, "db", 2L, "tbl", 
Lists.newArrayList(),
+                true, "TRUNCATE TABLE tbl", Lists.newArrayList(), true, 
Maps.newHashMap(), 10L, 20L);
+
+        TruncateTableInfo deserialized = 
GsonUtils.GSON.fromJson(info.toJson(), TruncateTableInfo.class);
+
+        Assertions.assertEquals(10L, deserialized.getVersion());
+        Assertions.assertEquals(20L, deserialized.getVersionTimeMs());
+    }
+
+    @Test
+    void testLegacyVersionDefaults() {
+        TruncateTableInfo deserialized = GsonUtils.GSON.fromJson("{}", 
TruncateTableInfo.class);
+
+        Assertions.assertEquals(0L, deserialized.getVersion());
+        Assertions.assertEquals(0L, deserialized.getVersionTimeMs());
+    }
+}
diff --git a/regression-test/data/mtmv_p0/test_truncate_table_mtmv.out 
b/regression-test/data/mtmv_p0/test_truncate_table_mtmv.out
index 1cbcc0cd370..baf4feba49a 100644
--- a/regression-test/data/mtmv_p0/test_truncate_table_mtmv.out
+++ b/regression-test/data/mtmv_p0/test_truncate_table_mtmv.out
@@ -8,5 +8,9 @@
 2      2
 3      3
 
+-- !non_pct_truncate --
+2      2
+3      3
+
 -- !truncate_table --
 
diff --git a/regression-test/suites/mtmv_p0/test_truncate_table_mtmv.groovy 
b/regression-test/suites/mtmv_p0/test_truncate_table_mtmv.groovy
index 3e390d0c905..ed7f9b642aa 100644
--- a/regression-test/suites/mtmv_p0/test_truncate_table_mtmv.groovy
+++ b/regression-test/suites/mtmv_p0/test_truncate_table_mtmv.groovy
@@ -23,6 +23,7 @@ suite("test_truncate_table_mtmv","mtmv") {
     String mvName = "${suiteName}_mv"
     sql """drop table if exists `${tableName}`"""
     sql """drop materialized view if exists ${mvName};"""
+    sql """drop table if exists test_truncate_table_mtmv_dim"""
 
     sql """
         CREATE TABLE ${tableName}
@@ -42,6 +43,17 @@ suite("test_truncate_table_mtmv","mtmv") {
             "replication_num" = "1"
         );
         """
+    sql """
+        CREATE TABLE test_truncate_table_mtmv_dim (
+            k2 TINYINT NOT NULL
+        )
+        UNIQUE KEY(k2)
+        DISTRIBUTED BY HASH(k2) BUCKETS 1
+        PROPERTIES (
+            'replication_num' = '1',
+            'enable_unique_key_merge_on_write' = 'true'
+        )
+        """
     sql """
         CREATE MATERIALIZED VIEW ${mvName}
         BUILD DEFERRED REFRESH AUTO ON MANUAL
@@ -51,12 +63,15 @@ suite("test_truncate_table_mtmv","mtmv") {
         'replication_num' = '1'
         )
         AS
-        SELECT * from ${tableName};
+        SELECT fact.k2, fact.k3
+        FROM ${tableName} fact
+        INNER JOIN test_truncate_table_mtmv_dim dim ON fact.k2 = dim.k2;
         """
 
     sql """
         insert into ${tableName} values(1,1),(2,2),(3,3);
         """
+    sql """insert into test_truncate_table_mtmv_dim values(1),(2),(3)"""
      sql """
         REFRESH MATERIALIZED VIEW ${mvName} AUTO
         """
@@ -73,6 +88,19 @@ suite("test_truncate_table_mtmv","mtmv") {
     waitingMTMVTaskFinishedByMvName(mvName)
     order_qt_truncate_partition "SELECT * FROM ${mvName}"
 
+    // Save a non-PCT table snapshot at version 3 while the MV does not 
contain k2=2.
+    sql """delete from test_truncate_table_mtmv_dim where k2=2"""
+    sql """REFRESH MATERIALIZED VIEW ${mvName} COMPLETE"""
+    waitingMTMVTaskFinishedByMvName(mvName)
+
+    // Without monotonic table versions, reset to 1 plus two inserts collides 
with version 3.
+    sql """truncate table test_truncate_table_mtmv_dim"""
+    sql """insert into test_truncate_table_mtmv_dim values(1),(2)"""
+    sql """insert into test_truncate_table_mtmv_dim values(3)"""
+    sql """REFRESH MATERIALIZED VIEW ${mvName} AUTO"""
+    waitingMTMVTaskFinishedByMvName(mvName)
+    order_qt_non_pct_truncate "SELECT * FROM ${mvName}"
+
     // truncate table
     sql """
         truncate table ${tableName};
diff --git 
a/regression-test/suites/nereids_rules_p0/rewrite_simple_agg_to_constant/truncate_version_reset.groovy
 
b/regression-test/suites/nereids_rules_p0/rewrite_simple_agg_to_constant/truncate_version_reset.groovy
index caef303ff5b..0d86ca64cd0 100644
--- 
a/regression-test/suites/nereids_rules_p0/rewrite_simple_agg_to_constant/truncate_version_reset.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/rewrite_simple_agg_to_constant/truncate_version_reset.groovy
@@ -16,25 +16,8 @@
 // under the License.
 
 /**
- * Regression test for: TRUNCATE TABLE must reset 
TableAttributes.visibleVersion.
- *
- * Bug: before the fix, truncateTableInternal() replaced partition data but did
- * not call olapTable.resetVisibleVersion().  As a result:
- *   - Partition.visibleVersion was reset to PARTITION_INIT_VERSION (1).
- *   - TableAttributes.visibleVersion kept its old, higher value.
- *   - TableAttributes.visibleVersionTime was never updated.
- *
- * Consequence for RewriteSimpleAggToConstantRule / SimpleAggCacheMgr:
- *   The cache entry was keyed by versionTime.  Because versionTime did not
- *   change at truncate time, the *caller* saw the same versionTime as the
- *   stale cached entry → cache HIT → the rule returned the pre-truncate
- *   count/min/max instead of the correct post-truncate values.
- *
- * The fix adds olapTable.resetVisibleVersion() inside truncateTableInternal(),
- * which bumps both visibleVersion (back to TABLE_INIT_VERSION = 1) and
- * visibleVersionTime (to System.currentTimeMillis()).  The new versionTime
- * differs from the cached entry's versionTime → cache MISS → the rule
- * correctly falls back to BE execution and returns the right result.
+ * TRUNCATE changes visible rows, so it must advance the table version 
metadata.
+ * SimpleAggCacheMgr uses that metadata to invalidate cached aggregate values.
  */
 suite("truncate_version_reset") {
     sql "SET enable_nereids_planner=true"
@@ -86,23 +69,14 @@ suite("truncate_version_reset") {
     // Confirm the cached count is correct before truncate.
     order_qt_count_before_truncate "SELECT count(*) FROM tbl;"
 
-    // -----------------------------------------------------------------------
-    // TRUNCATE the table.
-    // After the fix, resetVisibleVersion() is called inside
-    // truncateTableInternal(), which updates visibleVersionTime.
-    // The cache entry's versionTime no longer matches → cache is invalidated.
-    // -----------------------------------------------------------------------
+    // Truncate must invalidate the cached aggregate value.
     sql "TRUNCATE TABLE tbl;"
 
     // count(*) must return 0.
     // Without the fix, the stale cache entry (count = 5) would be returned.
     order_qt_count_after_truncate "SELECT count(*) FROM tbl;"
 
-    // -----------------------------------------------------------------------
     // Insert new rows after truncate, then verify count(*) reflects them.
-    // This also validates that the version counter is correctly reset so
-    // subsequent transactions start from the right next-version.
-    // -----------------------------------------------------------------------
     sql "INSERT INTO tbl VALUES (10, 100), (20, 200);"
 
     // After insert the count must be 2.


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

Reply via email to