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

lzljs3620320 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 e84b7b49cd [rest] Add fromSnapshot to rollback (#6905)
e84b7b49cd is described below

commit e84b7b49cd18032849fdf79d906f36a988f9c847
Author: Jingsong Lee <[email protected]>
AuthorDate: Fri Dec 26 16:18:27 2025 +0800

    [rest] Add fromSnapshot to rollback (#6905)
---
 docs/static/rest-catalog-open-api.yaml             |  4 ++
 .../main/java/org/apache/paimon/rest/RESTApi.java  | 18 +++++++-
 .../paimon/rest/requests/RollbackTableRequest.java | 20 ++++++++-
 .../org/apache/paimon/catalog/AbstractCatalog.java |  2 +-
 .../java/org/apache/paimon/catalog/Catalog.java    | 19 ++++++++-
 .../org/apache/paimon/catalog/DelegateCatalog.java |  4 +-
 .../java/org/apache/paimon/rest/RESTCatalog.java   |  4 +-
 .../org/apache/paimon/rest/MockRESTMessage.java    |  4 +-
 .../org/apache/paimon/rest/RESTCatalogServer.java  | 48 ++++++++++++++--------
 .../org/apache/paimon/rest/RESTCatalogTest.java    | 13 +++++-
 10 files changed, 106 insertions(+), 30 deletions(-)

diff --git a/docs/static/rest-catalog-open-api.yaml 
b/docs/static/rest-catalog-open-api.yaml
index 1cbb02040f..7641ab5cb9 100644
--- a/docs/static/rest-catalog-open-api.yaml
+++ b/docs/static/rest-catalog-open-api.yaml
@@ -2763,6 +2763,10 @@ components:
       properties:
         instant:
           $ref: '#/components/schemas/Instant'
+        fromSnapshot:
+          type: integer
+          format: int64
+          nullable: true
     Instant:
       anyOf:
         - $ref: '#/components/schemas/SnapshotInstant'
diff --git a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java 
b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java
index 648c395513..57d2e2989c 100644
--- a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java
+++ b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java
@@ -595,7 +595,23 @@ public class RESTApi {
      *     this table
      */
     public void rollbackTo(Identifier identifier, Instant instant) {
-        RollbackTableRequest request = new RollbackTableRequest(instant);
+        rollbackTo(identifier, instant, null);
+    }
+
+    /**
+     * Rollback instant for table.
+     *
+     * @param identifier database name and table name.
+     * @param instant instant to rollback
+     * @param fromSnapshot snapshot from, success only occurs when the latest 
snapshot is this
+     *     snapshot.
+     * @throws NoSuchResourceException Exception thrown on HTTP 404 means the 
table or the snapshot
+     *     or the tag not exists
+     * @throws ForbiddenException Exception thrown on HTTP 403 means don't 
have the permission for
+     *     this table
+     */
+    public void rollbackTo(Identifier identifier, Instant instant, @Nullable 
Long fromSnapshot) {
+        RollbackTableRequest request = new RollbackTableRequest(instant, 
fromSnapshot);
         client.post(
                 resourcePaths.rollbackTable(
                         identifier.getDatabaseName(), 
identifier.getObjectName()),
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/RollbackTableRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/RollbackTableRequest.java
index 1995ee5df9..9c8ca25c66 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/RollbackTableRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/RollbackTableRequest.java
@@ -24,24 +24,42 @@ import org.apache.paimon.table.Instant;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonInclude;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
 
+import javax.annotation.Nullable;
+
 /** Request for rollback table. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class RollbackTableRequest implements RESTRequest {
 
     private static final String FIELD_INSTANT = "instant";
+    private static final String FIELD_FROM_SNAPSHOT = "fromSnapshot";
 
     @JsonProperty(FIELD_INSTANT)
     private final Instant instant;
 
+    @JsonProperty(FIELD_FROM_SNAPSHOT)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    @Nullable
+    private final Long fromSnapshot;
+
     @JsonCreator
-    public RollbackTableRequest(@JsonProperty(FIELD_INSTANT) Instant instant) {
+    public RollbackTableRequest(
+            @JsonProperty(FIELD_INSTANT) Instant instant,
+            @JsonProperty(FIELD_FROM_SNAPSHOT) @Nullable Long fromSnapshot) {
         this.instant = instant;
+        this.fromSnapshot = fromSnapshot;
     }
 
     @JsonGetter(FIELD_INSTANT)
     public Instant getInstant() {
         return instant;
     }
+
+    @JsonGetter(FIELD_FROM_SNAPSHOT)
+    @Nullable
+    public Long getFromSnapshot() {
+        return fromSnapshot;
+    }
 }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
index 97e8436a75..1fd023aa9f 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
@@ -558,7 +558,7 @@ public abstract class AbstractCatalog implements Catalog {
     }
 
     @Override
-    public void rollbackTo(Identifier identifier, Instant instant)
+    public void rollbackTo(Identifier identifier, Instant instant, @Nullable 
Long fromSnapshot)
             throws Catalog.TableNotExistException {
         throw new UnsupportedOperationException();
     }
diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java 
b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
index 7d98180c3c..dd9521b12e 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
@@ -717,7 +717,24 @@ public interface Catalog extends AutoCloseable {
      * @throws UnsupportedOperationException if the catalog does not {@link
      *     #supportsVersionManagement()}
      */
-    void rollbackTo(Identifier identifier, Instant instant) throws 
Catalog.TableNotExistException;
+    default void rollbackTo(Identifier identifier, Instant instant)
+            throws Catalog.TableNotExistException {
+        rollbackTo(identifier, instant, null);
+    }
+
+    /**
+     * rollback table by the given {@link Identifier} and instant.
+     *
+     * @param identifier path of the table
+     * @param instant like snapshotId or tagName
+     * @param fromSnapshot snapshot from, success only occurs when the latest 
snapshot is this
+     *     snapshot.
+     * @throws Catalog.TableNotExistException if the table does not exist
+     * @throws UnsupportedOperationException if the catalog does not {@link
+     *     #supportsVersionManagement()}
+     */
+    void rollbackTo(Identifier identifier, Instant instant, @Nullable Long 
fromSnapshot)
+            throws Catalog.TableNotExistException;
 
     /**
      * Create a new branch for this table. By default, an empty branch will be 
created using the
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
index 9fc057aa90..eff0c0b229 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
@@ -207,9 +207,9 @@ public abstract class DelegateCatalog implements Catalog {
     }
 
     @Override
-    public void rollbackTo(Identifier identifier, Instant instant)
+    public void rollbackTo(Identifier identifier, Instant instant, @Nullable 
Long fromSnapshot)
             throws Catalog.TableNotExistException {
-        wrapped.rollbackTo(identifier, instant);
+        wrapped.rollbackTo(identifier, instant, fromSnapshot);
     }
 
     @Override
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
index cff58bf713..eeaede934d 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
@@ -373,10 +373,10 @@ public class RESTCatalog implements Catalog {
     }
 
     @Override
-    public void rollbackTo(Identifier identifier, Instant instant)
+    public void rollbackTo(Identifier identifier, Instant instant, @Nullable 
Long fromSnapshot)
             throws Catalog.TableNotExistException {
         try {
-            api.rollbackTo(identifier, instant);
+            api.rollbackTo(identifier, instant, fromSnapshot);
         } catch (NoSuchResourceException e) {
             if (StringUtils.equals(e.resourceType(), 
ErrorResponse.RESOURCE_TYPE_SNAPSHOT)) {
                 throw new IllegalArgumentException(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
index fe15ba5370..f487815e4d 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
@@ -267,11 +267,11 @@ public class MockRESTMessage {
     }
 
     public static RollbackTableRequest rollbackTableRequestBySnapshot(long 
snapshotId) {
-        return new RollbackTableRequest(Instant.snapshot(snapshotId));
+        return new RollbackTableRequest(Instant.snapshot(snapshotId), null);
     }
 
     public static RollbackTableRequest rollbackTableRequestByTag(String 
tagName) {
-        return new RollbackTableRequest(Instant.tag(tagName));
+        return new RollbackTableRequest(Instant.tag(tagName), null);
     }
 
     public static AlterViewRequest alterViewRequest() {
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
index f4cad6e47d..65b3a65ff8 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
@@ -118,6 +118,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testcontainers.shaded.com.google.common.collect.ImmutableMap;
 
+import javax.annotation.Nullable;
+
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -478,7 +480,8 @@ public class RESTCatalogServer {
                                 long snapshotId =
                                         ((Instant.SnapshotInstant) 
requestBody.getInstant())
                                                 .getSnapshotId();
-                                return rollbackTableByIdHandle(identifier, 
snapshotId);
+                                return rollbackTableByIdHandle(
+                                        identifier, snapshotId, 
requestBody.getFromSnapshot());
                             } else if (requestBody.getInstant() instanceof 
Instant.TagInstant) {
                                 String tagName =
                                         ((Instant.TagInstant) 
requestBody.getInstant())
@@ -844,26 +847,35 @@ public class RESTCatalogServer {
                 requestBody.getStatistics());
     }
 
-    private MockResponse rollbackTableByIdHandle(Identifier identifier, long 
snapshotId)
-            throws Exception {
+    private MockResponse rollbackTableByIdHandle(
+            Identifier identifier, long snapshotId, @Nullable Long 
fromSnapshot) throws Exception {
         FileStoreTable table = getFileTable(identifier);
         String identifierWithSnapshotId = 
geTableFullNameWithSnapshotId(identifier, snapshotId);
-        if 
(tableWithSnapshotId2SnapshotStore.containsKey(identifierWithSnapshotId)) {
-            table =
-                    table.copy(
-                            Collections.singletonMap(
-                                    SNAPSHOT_CLEAN_EMPTY_DIRECTORIES.key(), 
"true"));
-            long latestSnapshotId = table.snapshotManager().latestSnapshotId();
-            table.rollbackTo(snapshotId);
-            cleanSnapshot(identifier, snapshotId, latestSnapshotId);
-            tableLatestSnapshotStore.put(
-                    identifier.getFullName(),
-                    
tableWithSnapshotId2SnapshotStore.get(identifierWithSnapshotId));
-            return new MockResponse().setResponseCode(200);
+        TableSnapshot toSnapshot = 
tableWithSnapshotId2SnapshotStore.get(identifierWithSnapshotId);
+        if (toSnapshot == null) {
+            return mockResponse(
+                    new ErrorResponse(
+                            ErrorResponse.RESOURCE_TYPE_SNAPSHOT, "" + 
snapshotId, "", 404),
+                    404);
         }
-        return mockResponse(
-                new ErrorResponse(ErrorResponse.RESOURCE_TYPE_SNAPSHOT, "" + 
snapshotId, "", 404),
-                404);
+        long latestSnapshotId = table.snapshotManager().latestSnapshotId();
+        if (fromSnapshot != null && fromSnapshot != latestSnapshotId) {
+            return mockResponse(
+                    new ErrorResponse(
+                            null,
+                            null,
+                            String.format(
+                                    "Latest snapshot %s is not %s", 
latestSnapshotId, fromSnapshot),
+                            500),
+                    500);
+        }
+        table =
+                table.copy(
+                        
Collections.singletonMap(SNAPSHOT_CLEAN_EMPTY_DIRECTORIES.key(), "true"));
+        table.rollbackTo(snapshotId);
+        cleanSnapshot(identifier, snapshotId, latestSnapshotId);
+        tableLatestSnapshotStore.put(identifier.getFullName(), toSnapshot);
+        return new MockResponse().setResponseCode(200);
     }
 
     private MockResponse rollbackTableByTagNameHandle(Identifier identifier, 
String tagName)
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
index 4694c4f9c8..b0910e3e65 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
@@ -50,6 +50,7 @@ import org.apache.paimon.schema.Schema;
 import org.apache.paimon.schema.SchemaChange;
 import org.apache.paimon.schema.SchemaManager;
 import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.Instant;
 import org.apache.paimon.table.Table;
 import org.apache.paimon.table.TableSnapshot;
 import org.apache.paimon.table.object.ObjectTable;
@@ -1751,24 +1752,32 @@ public abstract class RESTCatalogTest extends 
CatalogTestBase {
             GenericRow record = GenericRow.of(i);
             write.write(record);
             commit.commit(i, write.prepareCommit(false, i));
-            table.createTag("tag-" + i);
+            table.createTag("tag-" + (i + 1));
         }
         write.close();
         commit.close();
+
+        // rollback to snapshot 4
         long rollbackToSnapshotId = 4;
         table.rollbackTo(rollbackToSnapshotId);
         assertThat(table.snapshotManager().snapshot(rollbackToSnapshotId))
                 
.isEqualTo(restCatalog.loadSnapshot(identifier).get().snapshot());
         assertThat(table.tagManager().tagExists("tag-" + (rollbackToSnapshotId 
+ 2))).isFalse();
         assertThat(table.snapshotManager().snapshotExists(rollbackToSnapshotId 
+ 1)).isFalse();
-
         assertThrows(
                 IllegalArgumentException.class, () -> 
table.rollbackTo(rollbackToSnapshotId + 1));
 
+        // rollback to snapshot 3
         String rollbackToTagName = "tag-" + (rollbackToSnapshotId - 1);
         table.rollbackTo(rollbackToTagName);
         Snapshot tagSnapshot = 
table.tagManager().getOrThrow(rollbackToTagName).trimToSnapshot();
         
assertThat(tagSnapshot).isEqualTo(restCatalog.loadSnapshot(identifier).get().snapshot());
+
+        // rollback to snapshot 2 from snapshot
+        assertThatThrownBy(() -> catalog.rollbackTo(identifier, 
Instant.snapshot(2L), 4L))
+                .hasMessageContaining("Latest snapshot 3 is not 4");
+        catalog.rollbackTo(identifier, Instant.snapshot(2L), 3L);
+        assertThat(table.latestSnapshot().get().id()).isEqualTo(2);
     }
 
     @Test

Reply via email to