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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 0474b87e5b [core] Protect rollback-to-as-latest snapshots from expire 
(#8450)
0474b87e5b is described below

commit 0474b87e5b427e032b1be5a3f92ff3e62802aae6
Author: Jingsong Lee <[email protected]>
AuthorDate: Fri Jul 3 22:33:17 2026 +0800

    [core] Protect rollback-to-as-latest snapshots from expire (#8450)
    
    Fix rollback-to-as-latest so files restored from an older snapshot are
    protected from later expiration. When users roll back by `snapshot_id`,
    the procedure now creates a dedicated non-expiring
    `rollback-to-as-latest-<snapshotId>-<uuid>` tag and passes that `Tag`
    into `TableCommitImpl`; when users roll back by `tag`, the provided tag
    is used as-is.
---
 .../apache/paimon/table/sink/TableCommitImpl.java  |  5 +-
 .../operation/ChainTablePartitionExpireTest.java   |  9 ++-
 .../paimon/operation/FileStoreCommitTest.java      | 45 +++++++++++++
 .../procedure/RollbackToAsLatestProcedure.java     | 78 +++++++++++++++++++---
 .../RollbackToAsLatestProcedureITCase.java         | 53 +++++++++++++++
 5 files changed, 177 insertions(+), 13 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java 
b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
index e0ee31b21e..f5b8c7892b 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableCommitImpl.java
@@ -32,6 +32,7 @@ import org.apache.paimon.operation.FileStoreCommit;
 import org.apache.paimon.operation.PartitionExpire;
 import org.apache.paimon.operation.metrics.CommitMetrics;
 import org.apache.paimon.stats.Statistics;
+import org.apache.paimon.tag.Tag;
 import org.apache.paimon.tag.TagAutoCreation;
 import org.apache.paimon.tag.TagAutoManager;
 import org.apache.paimon.tag.TagTimeExpire;
@@ -219,9 +220,9 @@ public class TableCommitImpl implements InnerTableCommit {
         commit.compactManifest();
     }
 
-    public boolean rollbackToAsLatest(Snapshot targetSnapshot) {
+    public boolean rollbackToAsLatest(Tag targetTag) {
         checkCommitted();
-        boolean success = commit.rollbackToAsLatest(targetSnapshot);
+        boolean success = 
commit.rollbackToAsLatest(targetTag.trimToSnapshot());
         if (success) {
             // Skip automatic expiration for the rollback path. 
rollback_to_as_latest promises not
             // to
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/ChainTablePartitionExpireTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/ChainTablePartitionExpireTest.java
index 0094addcf7..c8440a3506 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/operation/ChainTablePartitionExpireTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/ChainTablePartitionExpireTest.java
@@ -616,8 +616,15 @@ public class ChainTablePartitionExpireTest {
         // regular overwrite) instead of silently breaking the chain.
         FileStoreTable snapshotBranch = 
loadTable(tablePath).switchToBranch("snapshot");
         Snapshot target = snapshotBranch.snapshotManager().snapshot(1);
+        String protectionTag = "rollback-to-as-latest-" + target.id() + "-" + 
UUID.randomUUID();
+        snapshotBranch
+                .tagManager()
+                .createTag(target, protectionTag, null, 
Collections.emptyList(), false);
         try (TableCommitImpl commit = snapshotBranch.newCommit(commitUser)) {
-            assertThatThrownBy(() -> commit.rollbackToAsLatest(target))
+            assertThatThrownBy(
+                            () ->
+                                    commit.rollbackToAsLatest(
+                                            
snapshotBranch.tagManager().getOrThrow(protectionTag)))
                     .hasMessageContaining("Snapshot partition cannot be 
dropped");
         }
         // The dangerous rollback was aborted, so the latest snapshot is 
unchanged.
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
index e2210edea2..10bce28e94 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/FileStoreCommitTest.java
@@ -56,10 +56,12 @@ import org.apache.paimon.schema.TableSchema;
 import org.apache.paimon.stats.ColStats;
 import org.apache.paimon.stats.Statistics;
 import org.apache.paimon.stats.StatsFileHandler;
+import org.apache.paimon.table.ExpireSnapshotsImpl;
 import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.table.FileStoreTableFactory;
 import org.apache.paimon.table.sink.CommitMessage;
 import org.apache.paimon.table.sink.CommitMessageImpl;
+import org.apache.paimon.table.sink.TableCommitImpl;
 import org.apache.paimon.table.source.IncrementalSplit;
 import org.apache.paimon.table.source.Split;
 import org.apache.paimon.types.DataField;
@@ -81,6 +83,7 @@ import org.junit.jupiter.params.provider.ValueSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -1056,6 +1059,48 @@ public class FileStoreCommitTest {
         assertThat(splits).isEmpty();
     }
 
+    @Test
+    public void testExpireSnapshotsKeepsFilesRestoredByRollbackToAsLatest() 
throws Exception {
+        TestFileStore store = createStore(false, 1, 
CoreOptions.ChangelogProducer.NONE);
+        KeyValue original = gen.nextInsert("20201110", 10, 1L, new int[] {1, 
1}, "original");
+        KeyValue replacement = gen.nextInsert("20201111", 11, 2L, new int[] 
{2, 2}, "replacement");
+        KeyValue retainedBeforeRollback =
+                gen.nextInsert("20201112", 12, 3L, new int[] {3, 3}, 
"retained");
+
+        Snapshot target =
+                store.commitData(Collections.singletonList(original), 
gen::getPartition, kv -> 0)
+                        .get(0);
+        String root = TraceableFileIO.SCHEME + "://" + tempDir.toString();
+        FileStoreTable table = FileStoreTableFactory.create(store.fileIO(), 
new Path(root));
+        table.tagManager()
+                .createTag(
+                        target, "expiring-target", Duration.ZERO, 
Collections.emptyList(), false);
+        String protectionTag = "rollback-to-as-latest-" + target.id() + "-" + 
UUID.randomUUID();
+        table.tagManager().createTag(target, protectionTag, null, 
Collections.emptyList(), false);
+        store.overwriteData(
+                Collections.singletonList(replacement),
+                gen::getPartition,
+                kv -> 0,
+                Collections.emptyMap());
+        store.commitData(
+                Collections.singletonList(retainedBeforeRollback), 
gen::getPartition, kv -> 0);
+
+        try (TableCommitImpl commit = 
table.newCommit("rollback-to-as-latest-test")) {
+            
assertThat(commit.rollbackToAsLatest(table.tagManager().getOrThrow(protectionTag)))
+                    .isTrue();
+        }
+        Snapshot rolledBack = store.snapshotManager().latestSnapshot();
+        assertThat(table.tagManager().tags().get(target))
+                .contains("expiring-target")
+                .contains(protectionTag);
+
+        ((ExpireSnapshotsImpl) store.newExpire(1, 1, 
Long.MAX_VALUE)).expireUntil(1, 3);
+
+        List<KeyValue> actual = store.readKvsFromSnapshot(rolledBack.id());
+        assertThat(store.toKvMap(actual))
+                .isEqualTo(store.toKvMap(Collections.singletonList(original)));
+    }
+
     @Test
     public void testManifestCompact() throws Exception {
         TestFileStore store = createStore(false);
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RollbackToAsLatestProcedure.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RollbackToAsLatestProcedure.java
index af1acf618b..46df2916c2 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RollbackToAsLatestProcedure.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RollbackToAsLatestProcedure.java
@@ -25,9 +25,11 @@ import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.table.Table;
 import org.apache.paimon.table.sink.TableCommitImpl;
+import org.apache.paimon.tag.Tag;
 import org.apache.paimon.utils.Preconditions;
 import org.apache.paimon.utils.SnapshotManager;
 import org.apache.paimon.utils.StringUtils;
+import org.apache.paimon.utils.TagManager;
 
 import org.apache.flink.table.annotation.ArgumentHint;
 import org.apache.flink.table.annotation.DataTypeHint;
@@ -35,6 +37,7 @@ import org.apache.flink.table.annotation.ProcedureHint;
 import org.apache.flink.table.procedure.ProcedureContext;
 import org.apache.flink.types.Row;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.SortedMap;
@@ -54,6 +57,7 @@ import java.util.UUID;
 public class RollbackToAsLatestProcedure extends ProcedureBase {
 
     public static final String IDENTIFIER = "rollback_to_as_latest";
+    private static final String ROLLBACK_TO_AS_LATEST_TAG_PREFIX = 
"rollback-to-as-latest-";
 
     @ProcedureHint(
             argument = {
@@ -80,20 +84,39 @@ public class RollbackToAsLatestProcedure extends 
ProcedureBase {
         Preconditions.checkArgument(
                 hasTag != hasSnapshot, "Must specify exactly one of tag and 
snapshot_id.");
 
+        TagManager tagManager = store.newTagManager();
+        Tag targetTag;
         Snapshot targetSnapshot;
         if (hasTag) {
-            targetSnapshot = 
store.newTagManager().getOrThrow(tagName).trimToSnapshot();
+            targetTag = tagManager.getOrThrow(tagName);
+            targetSnapshot = targetTag.trimToSnapshot();
         } else {
-            targetSnapshot = findSnapshot(store, snapshotId);
+            targetTag = null;
+            targetSnapshot = findSnapshot(store, tagManager, snapshotId);
         }
 
-        try (TableCommitImpl commit =
-                fileStoreTable.newCommit("rollback-to-as-latest-" + 
UUID.randomUUID().toString())) {
-            Preconditions.checkState(
-                    commit.rollbackToAsLatest(targetSnapshot),
-                    "Failed to roll back to snapshot %s as latest.",
-                    targetSnapshot.id());
+        String createdRollbackTag = null;
+        String commitUser = "rollback-to-as-latest-" + 
UUID.randomUUID().toString();
+        try {
+            if (!hasTag) {
+                createdRollbackTag = createRollbackToAsLatestTag(tagManager, 
targetSnapshot);
+                targetTag = tagManager.getOrThrow(createdRollbackTag);
+            }
+
+            try (TableCommitImpl commit = 
fileStoreTable.newCommit(commitUser)) {
+                boolean success = commit.rollbackToAsLatest(targetTag);
+                Preconditions.checkState(
+                        success,
+                        "Failed to roll back to snapshot %s as latest.",
+                        targetSnapshot.id());
+            }
         } catch (Exception e) {
+            try {
+                deleteCreatedRollbackTagIfNotCommitted(
+                        store, tagManager, createdRollbackTag, 
latestSnapshot.id() + 1, commitUser);
+            } catch (Exception cleanupException) {
+                e.addSuppressed(cleanupException);
+            }
             throw new RuntimeException(
                     String.format(
                             "Failed to roll back to snapshot %s as latest.", 
targetSnapshot.id()),
@@ -108,13 +131,48 @@ public class RollbackToAsLatestProcedure extends 
ProcedureBase {
         };
     }
 
-    private Snapshot findSnapshot(FileStore<?> store, long snapshotId) {
+    private String createRollbackToAsLatestTag(TagManager tagManager, Snapshot 
targetSnapshot) {
+        String tagName =
+                ROLLBACK_TO_AS_LATEST_TAG_PREFIX
+                        + targetSnapshot.id()
+                        + "-"
+                        + UUID.randomUUID().toString();
+        tagManager.createTag(targetSnapshot, tagName, null, 
Collections.emptyList(), false);
+        return tagName;
+    }
+
+    private void deleteCreatedRollbackTagIfNotCommitted(
+            FileStore<?> store,
+            TagManager tagManager,
+            String createdRollbackTag,
+            long rollbackSnapshotId,
+            String commitUser) {
+        if (createdRollbackTag == null
+                || rollbackSnapshotCommitted(
+                        store.snapshotManager(), rollbackSnapshotId, 
commitUser)) {
+            return;
+        }
+
+        tagManager.deleteTag(
+                createdRollbackTag,
+                store.newTagDeletion(),
+                store.snapshotManager(),
+                Collections.emptyList());
+    }
+
+    private boolean rollbackSnapshotCommitted(
+            SnapshotManager snapshotManager, long rollbackSnapshotId, String 
commitUser) {
+        return snapshotManager.snapshotExists(rollbackSnapshotId)
+                && 
commitUser.equals(snapshotManager.snapshot(rollbackSnapshotId).commitUser());
+    }
+
+    private Snapshot findSnapshot(FileStore<?> store, TagManager tagManager, 
long snapshotId) {
         SnapshotManager snapshotManager = store.snapshotManager();
         if (snapshotManager.snapshotExists(snapshotId)) {
             return snapshotManager.snapshot(snapshotId);
         }
 
-        SortedMap<Snapshot, List<String>> tags = store.newTagManager().tags();
+        SortedMap<Snapshot, List<String>> tags = tagManager.tags();
         for (Map.Entry<Snapshot, List<String>> entry : tags.entrySet()) {
             if (entry.getKey().id() == snapshotId) {
                 return entry.getKey();
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/RollbackToAsLatestProcedureITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/RollbackToAsLatestProcedureITCase.java
index 383e9334e0..8e4e8a8103 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/RollbackToAsLatestProcedureITCase.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/RollbackToAsLatestProcedureITCase.java
@@ -29,12 +29,18 @@ import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.table.sink.BatchTableCommit;
 import org.apache.paimon.table.sink.BatchTableWrite;
 import org.apache.paimon.table.sink.BatchWriteBuilder;
+import org.apache.paimon.tag.Tag;
+import org.apache.paimon.utils.Pair;
 import org.apache.paimon.utils.SnapshotManager;
+import org.apache.paimon.utils.TagManager;
 
 import org.apache.flink.types.Row;
 import org.junit.jupiter.api.Test;
 
+import java.time.Duration;
+import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -101,6 +107,47 @@ public class RollbackToAsLatestProcedureITCase extends 
CatalogITCaseBase {
         assertTrue(snapshotManager.snapshotExists(2));
         assertTrue(snapshotManager.snapshotExists(3));
         assertThat(sql("SELECT * FROM T")).containsExactly(Row.of(1, "a"));
+        assertThat(tagsOfSnapshot(table.tagManager(), 1))
+                .extracting(Pair::getRight)
+                .containsExactly("tag-1");
+    }
+
+    @Test
+    public void testRollbackCreatesNonExpiringProtectionTag() throws Exception 
{
+        sql("CREATE TABLE T (id INT, name STRING)");
+
+        FileStoreTable table = paimonTable("T");
+        SnapshotManager snapshotManager = table.snapshotManager();
+
+        commitRow(table, 1, "a");
+        commitRow(table, 2, "b");
+        commitRow(table, 3, "c");
+        assertEquals(3, snapshotManager.latestSnapshotId());
+
+        Snapshot target = snapshotManager.snapshot(1);
+        TagManager tagManager = table.tagManager();
+        tagManager.createTag(
+                target, "expiring-target", Duration.ZERO, 
Collections.emptyList(), false);
+
+        assertThat(sql("CALL sys.rollback_to_as_latest(`table` => 'default.T', 
snapshot_id => 1)"))
+                .containsExactly(Row.of(3L, 1L, 4L));
+
+        List<Pair<Tag, String>> targetTags = tagsOfSnapshot(tagManager, 
target.id());
+        assertThat(targetTags)
+                .anyMatch(
+                        tag ->
+                                tag.getRight().equals("expiring-target")
+                                        && Duration.ZERO.equals(
+                                                
tag.getLeft().getTagTimeRetained()));
+        assertThat(targetTags)
+                .anyMatch(
+                        tag ->
+                                tag.getRight()
+                                                .startsWith(
+                                                        
"rollback-to-as-latest-"
+                                                                + target.id()
+                                                                + "-")
+                                        && tag.getLeft().getTagTimeRetained() 
== null);
     }
 
     @Test
@@ -199,6 +246,12 @@ public class RollbackToAsLatestProcedureITCase extends 
CatalogITCaseBase {
         
assertThat(snapshot.deltaRecordCount()).isEqualTo(expectedDeltaRecordCount);
     }
 
+    private List<Pair<Tag, String>> tagsOfSnapshot(TagManager tagManager, long 
snapshotId) {
+        return tagManager.tagObjects().stream()
+                .filter(tag -> tag.getLeft().id() == snapshotId)
+                .collect(Collectors.toList());
+    }
+
     private void commitRow(FileStoreTable table, int id, String name) throws 
Exception {
         BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder();
         try (BatchTableWrite write = writeBuilder.newWrite();

Reply via email to