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 625d0d4e81 [core] Carry table UUID and writer identity in catalog
rollback (#10520)
625d0d4e81 is described below
commit 625d0d4e815d3897358a35728e8650d636a3e340
Author: Jingsong Lee <[email protected]>
AuthorDate: Tue Oct 13 09:27:35 2026 +0800
[core] Carry table UUID and writer identity in catalog rollback (#10520)
---
docs/static/rest-catalog-open-api.yaml | 11 +++
.../main/java/org/apache/paimon/rest/RESTApi.java | 26 ++---
.../paimon/rest/requests/RollbackTableRequest.java | 31 +++++-
.../java/org/apache/paimon/AbstractFileStore.java | 2 +-
.../org/apache/paimon/catalog/AbstractCatalog.java | 7 +-
.../java/org/apache/paimon/catalog/Catalog.java | 29 +++---
.../org/apache/paimon/catalog/DelegateCatalog.java | 9 +-
.../java/org/apache/paimon/rest/RESTCatalog.java | 9 +-
.../apache/paimon/table/CatalogEnvironment.java | 17 +++-
.../org/apache/paimon/tag/SnapshotLoaderImpl.java | 15 ++-
.../org/apache/paimon/rest/MockRESTMessage.java | 4 +-
.../org/apache/paimon/rest/RESTCatalogTest.java | 5 +-
.../apache/paimon/rest/RollbackProtocolTest.java | 107 +++++++++++++++++++++
.../paimon/table/CatalogEnvironmentTest.java | 91 ++++++++++++++++++
.../table/FallbackReadFileStoreTableTest.java | 52 ++++++++++
.../apache/paimon/tag/SnapshotLoaderImplTest.java | 88 +++++++++++++++++
16 files changed, 450 insertions(+), 53 deletions(-)
diff --git a/docs/static/rest-catalog-open-api.yaml
b/docs/static/rest-catalog-open-api.yaml
index a52be685d0..559a433024 100644
--- a/docs/static/rest-catalog-open-api.yaml
+++ b/docs/static/rest-catalog-open-api.yaml
@@ -3514,12 +3514,23 @@ components:
$ref: '#/components/schemas/PartitionStatistics'
RollbackTableRequest:
type: object
+ description: >-
+ tableId identifies the expected table incarnation independently of
lock ownership.
+ For rollback within a writer's commit lock, also supply commitUser and
fromSnapshot so
+ the server can validate the expected head and owner. Omit commitUser
when no writer
+ lock is used, and omit tableId only when its UUID is unavailable.
properties:
instant:
$ref: '#/components/schemas/Instant'
fromSnapshot:
type: [ integer, "null" ]
format: int64
+ tableId:
+ type: [ string, "null" ]
+ description: Expected table or branch UUID, independently of
commitUser.
+ commitUser:
+ type: [ string, "null" ]
+ description: Exact owner of the writer's lock.
RollbackSchemaRequest:
type: object
required:
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 77a871d400..c454f26341 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
@@ -738,30 +738,24 @@ public class RESTApi {
* Rollback instant for table.
*
* @param identifier database name and table name.
- * @param instant instant to rollback
- * @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) {
- rollbackTo(identifier, instant, null);
- }
-
- /**
- * Rollback instant for table.
- *
- * @param identifier database name and table name.
+ * @param tableUuid expected table incarnation, or null when its UUID is
unavailable
* @param instant instant to rollback
* @param fromSnapshot snapshot from, success only occurs when the latest
snapshot is this
* snapshot.
+ * @param commitUser owner of the writer lock, or null without a writer
lock
* @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);
+ public void rollbackTo(
+ Identifier identifier,
+ @Nullable String tableUuid,
+ Instant instant,
+ @Nullable Long fromSnapshot,
+ @Nullable String commitUser) {
+ RollbackTableRequest request =
+ new RollbackTableRequest(instant, fromSnapshot, tableUuid,
commitUser);
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 9c8ca25c66..77fc97049c 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
@@ -29,6 +29,8 @@ import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonPro
import javax.annotation.Nullable;
+import java.beans.ConstructorProperties;
+
/** Request for rollback table. */
@JsonIgnoreProperties(ignoreUnknown = true)
public class RollbackTableRequest implements RESTRequest {
@@ -44,12 +46,39 @@ public class RollbackTableRequest implements RESTRequest {
@Nullable
private final Long fromSnapshot;
+ @JsonProperty("tableId")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @Nullable
+ private final String tableId;
+
+ @JsonProperty("commitUser")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @Nullable
+ private final String commitUser;
+
@JsonCreator
+ @ConstructorProperties({"instant", "fromSnapshot", "tableId",
"commitUser"})
public RollbackTableRequest(
@JsonProperty(FIELD_INSTANT) Instant instant,
- @JsonProperty(FIELD_FROM_SNAPSHOT) @Nullable Long fromSnapshot) {
+ @JsonProperty(FIELD_FROM_SNAPSHOT) @Nullable Long fromSnapshot,
+ @JsonProperty("tableId") @Nullable String tableId,
+ @JsonProperty("commitUser") @Nullable String commitUser) {
this.instant = instant;
this.fromSnapshot = fromSnapshot;
+ this.tableId = tableId;
+ this.commitUser = commitUser;
+ }
+
+ @JsonGetter("tableId")
+ @Nullable
+ public String getTableId() {
+ return tableId;
+ }
+
+ @JsonGetter("commitUser")
+ @Nullable
+ public String getCommitUser() {
+ return commitUser;
}
@JsonGetter(FIELD_INSTANT)
diff --git a/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
b/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
index fe381cbaae..8dd60a7603 100644
--- a/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
+++ b/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
@@ -307,7 +307,7 @@ abstract class AbstractFileStore<T> implements FileStore<T>
{
snapshotManager,
scanner);
CommitRollback rollback = null;
- TableRollback tableRollback =
catalogEnvironment.catalogTableRollback();
+ TableRollback tableRollback =
catalogEnvironment.catalogTableRollback(null, null);
if (tableRollback != null) {
rollback = new CommitRollback(tableRollback);
}
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 08f28ce9fc..d9503d43d9 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
@@ -712,7 +712,12 @@ public abstract class AbstractCatalog implements Catalog {
}
@Override
- public void rollbackTo(Identifier identifier, Instant instant, @Nullable
Long fromSnapshot)
+ public void rollbackTo(
+ Identifier identifier,
+ @Nullable String tableUuid,
+ Instant instant,
+ @Nullable Long fromSnapshot,
+ @Nullable String commitUser)
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 2f61c526ff..02e318a13e 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
@@ -715,7 +715,7 @@ public interface Catalog extends AutoCloseable {
* <ul>
* <li>{@link #commitSnapshot(Identifier, String, String, Snapshot,
List)}.
* <li>{@link #loadSnapshot(Identifier)}.
- * <li>{@link #rollbackTo(Identifier, Instant)}.
+ * <li>{@link #rollbackTo(Identifier, String, Instant, Long, String)}.
* <li>{@link #createBranch(Identifier, String, String)}.
* <li>{@link #dropBranch(Identifier, String)}.
* <li>{@link #renameBranch(Identifier, String, String)}.
@@ -840,31 +840,24 @@ public interface Catalog extends AutoCloseable {
}
/**
- * rollback table by the given {@link Identifier} and instant.
- *
- * @param identifier path of the table
- * @param instant like snapshotId or tagName
- * @throws Catalog.TableNotExistException if the table does not exist
- * @throws UnsupportedOperationException if the catalog does not {@link
- * #supportsVersionManagement()}
- */
- default void rollbackTo(Identifier identifier, Instant instant)
- throws Catalog.TableNotExistException {
- rollbackTo(identifier, instant, null);
- }
-
- /**
- * rollback table by the given {@link Identifier} and instant.
+ * Rollback table by the given {@link Identifier} and instant.
*
* @param identifier path of the table
+ * @param tableUuid expected table incarnation, or null when its UUID is
unavailable
* @param instant like snapshotId or tagName
* @param fromSnapshot snapshot from, success only occurs when the latest
snapshot is this
- * snapshot.
+ * snapshot
+ * @param commitUser owner of the writer's lock, or null when no writer
lock is used
* @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)
+ void rollbackTo(
+ Identifier identifier,
+ @Nullable String tableUuid,
+ Instant instant,
+ @Nullable Long fromSnapshot,
+ @Nullable String commitUser)
throws Catalog.TableNotExistException;
/**
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 41052b8baf..89159783fb 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
@@ -249,9 +249,14 @@ public abstract class DelegateCatalog implements Catalog {
}
@Override
- public void rollbackTo(Identifier identifier, Instant instant, @Nullable
Long fromSnapshot)
+ public void rollbackTo(
+ Identifier identifier,
+ @Nullable String tableUuid,
+ Instant instant,
+ @Nullable Long fromSnapshot,
+ @Nullable String commitUser)
throws Catalog.TableNotExistException {
- wrapped.rollbackTo(identifier, instant, fromSnapshot);
+ wrapped.rollbackTo(identifier, tableUuid, instant, fromSnapshot,
commitUser);
}
@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 61ad72db0d..56939c3a61 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
@@ -540,10 +540,15 @@ public class RESTCatalog implements Catalog {
}
@Override
- public void rollbackTo(Identifier identifier, Instant instant, @Nullable
Long fromSnapshot)
+ public void rollbackTo(
+ Identifier identifier,
+ @Nullable String tableUuid,
+ Instant instant,
+ @Nullable Long fromSnapshot,
+ @Nullable String commitUser)
throws Catalog.TableNotExistException {
try {
- api.rollbackTo(identifier, instant, fromSnapshot);
+ api.rollbackTo(identifier, tableUuid, instant, fromSnapshot,
commitUser);
} catch (NoSuchResourceException e) {
if (StringUtils.equals(e.resourceType(),
ErrorResponse.RESOURCE_TYPE_SNAPSHOT)) {
throw new IllegalArgumentException(
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java
b/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java
index d35c50706d..5988ee128f 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java
@@ -180,13 +180,22 @@ public class CatalogEnvironment implements Serializable {
lockContext, lockIdentifier, uuid, commitUser,
tableOptions);
}
+ /** Create rollback with the table incarnation and optional writer lock
context. */
@Nullable
- public TableRollback catalogTableRollback() {
+ public TableRollback catalogTableRollback(
+ @Nullable String branch, @Nullable String commitUser) {
if (catalogLoader != null && supportsVersionManagement) {
Catalog catalog = catalogLoader.load();
+ Identifier rollbackIdentifier =
+ branch == null
+ ? identifier
+ : new Identifier(
+ identifier.getDatabaseName(),
+ identifier.getTableName(),
+ branch);
return (instant, fromSnapshot) -> {
try {
- catalog.rollbackTo(identifier, instant, fromSnapshot);
+ catalog.rollbackTo(rollbackIdentifier, uuid, instant,
fromSnapshot, commitUser);
} catch (Catalog.TableNotExistException e) {
throw new RuntimeException(e);
}
@@ -215,7 +224,7 @@ public class CatalogEnvironment implements Serializable {
if (catalogLoader == null) {
return null;
}
- return new SnapshotLoaderImpl(catalogLoader, identifier);
+ return new SnapshotLoaderImpl(catalogLoader, identifier, uuid);
}
@Nullable
@@ -280,7 +289,7 @@ public class CatalogEnvironment implements Serializable {
public CatalogEnvironment copy(Identifier identifier) {
return new CatalogEnvironment(
identifier,
- uuid,
+ identifier.equals(this.identifier) ? uuid : null,
catalogLoader,
lockFactory,
lockContext,
diff --git
a/paimon-core/src/main/java/org/apache/paimon/tag/SnapshotLoaderImpl.java
b/paimon-core/src/main/java/org/apache/paimon/tag/SnapshotLoaderImpl.java
index 53e7f54697..8a47e88b1b 100644
--- a/paimon-core/src/main/java/org/apache/paimon/tag/SnapshotLoaderImpl.java
+++ b/paimon-core/src/main/java/org/apache/paimon/tag/SnapshotLoaderImpl.java
@@ -26,6 +26,8 @@ import org.apache.paimon.table.Instant;
import org.apache.paimon.table.TableSnapshot;
import org.apache.paimon.utils.SnapshotLoader;
+import javax.annotation.Nullable;
+
import java.io.IOException;
import java.util.Optional;
@@ -34,10 +36,13 @@ public class SnapshotLoaderImpl implements SnapshotLoader {
private final CatalogLoader catalogLoader;
private final Identifier identifier;
+ @Nullable private final String uuid;
- public SnapshotLoaderImpl(CatalogLoader catalogLoader, Identifier
identifier) {
+ public SnapshotLoaderImpl(
+ CatalogLoader catalogLoader, Identifier identifier, @Nullable
String uuid) {
this.catalogLoader = catalogLoader;
this.identifier = identifier;
+ this.uuid = uuid;
}
@Override
@@ -54,7 +59,7 @@ public class SnapshotLoaderImpl implements SnapshotLoader {
@Override
public void rollback(Instant instant) throws IOException {
try (Catalog catalog = catalogLoader.load()) {
- catalog.rollbackTo(identifier, instant);
+ catalog.rollbackTo(identifier, uuid, instant, null, null);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
@@ -64,8 +69,10 @@ public class SnapshotLoaderImpl implements SnapshotLoader {
@Override
public SnapshotLoader copyWithBranch(String branch) {
+ Identifier branchIdentifier =
+ new Identifier(identifier.getDatabaseName(),
identifier.getTableName(), branch);
+ // A different branch can have its own UUID. Its identity is unknown
in this view.
return new SnapshotLoaderImpl(
- catalogLoader,
- new Identifier(identifier.getDatabaseName(),
identifier.getTableName(), branch));
+ catalogLoader, branchIdentifier,
branchIdentifier.equals(identifier) ? uuid : null);
}
}
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 4d4f101bd6..dafeae833a 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
@@ -274,11 +274,11 @@ public class MockRESTMessage {
}
public static RollbackTableRequest rollbackTableRequestBySnapshot(long
snapshotId) {
- return new RollbackTableRequest(Instant.snapshot(snapshotId), null);
+ return new RollbackTableRequest(Instant.snapshot(snapshotId), null,
null, null);
}
public static RollbackTableRequest rollbackTableRequestByTag(String
tagName) {
- return new RollbackTableRequest(Instant.tag(tagName), null);
+ return new RollbackTableRequest(Instant.tag(tagName), null, null,
null);
}
public static AlterViewRequest alterViewRequest() {
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 05f62688eb..67106e1616 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
@@ -2377,9 +2377,10 @@ public abstract class RESTCatalogTest extends
CatalogTestBase {
assertThat(tagSnapshot).isEqualTo(restCatalog.loadSnapshot(identifier).get().snapshot());
// rollback to snapshot 2 from snapshot
- assertThatThrownBy(() -> catalog.rollbackTo(identifier,
Instant.snapshot(2L), 4L))
+ assertThatThrownBy(
+ () -> catalog.rollbackTo(identifier, null,
Instant.snapshot(2L), 4L, null))
.hasMessageContaining("Latest snapshot 3 is not 4");
- catalog.rollbackTo(identifier, Instant.snapshot(2L), 3L);
+ catalog.rollbackTo(identifier, null, Instant.snapshot(2L), 3L, null);
assertThat(table.latestSnapshot().get().id()).isEqualTo(2);
}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/rest/RollbackProtocolTest.java
b/paimon-core/src/test/java/org/apache/paimon/rest/RollbackProtocolTest.java
new file mode 100644
index 0000000000..b26aad6899
--- /dev/null
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RollbackProtocolTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.paimon.rest;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.CatalogLoader;
+import org.apache.paimon.catalog.DelegateCatalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.rest.requests.RollbackTableRequest;
+import org.apache.paimon.table.Instant;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.node.ObjectNode;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests rollback routing and optional writer context through a delegated
REST catalog. */
+class RollbackProtocolTest {
+ @ParameterizedTest
+ @CsvSource({
+ "owned,snapshot", "unowned,snapshot", "without-uuid,snapshot",
+ "owned,tag", "unowned,tag", "without-uuid,tag"
+ })
+ void testRollbackCarriesTableUuidIndependentlyOfWriterContext(String
context, String kind)
+ throws Exception {
+ try (MockWebServer server = new MockWebServer()) {
+ server.start();
+ Options options = new Options();
+ options.set(RESTCatalogOptions.URI, server.url("/").toString());
+ options.set(RESTCatalogOptions.TOKEN_PROVIDER, "bear");
+ options.set(RESTCatalogOptions.TOKEN, "token");
+ options.set(RESTCatalogInternalOptions.PREFIX, "catalog");
+ try (Catalog catalog =
+ new DelegateCatalog(new
RESTCatalog(CatalogContext.create(options), false)) {
+ @Override
+ public CatalogLoader catalogLoader() {
+ return wrapped.catalogLoader();
+ }
+ }) {
+ Identifier identifier = new Identifier("db", "table", "dev");
+ Instant instant =
+ "snapshot".equals(kind) ? Instant.snapshot(1L) :
Instant.tag("tag-1");
+ server.enqueue(
+ new MockResponse()
+ .setResponseCode(200)
+ .setHeader("Content-Type", "application/json")
+ .setBody("{}"));
+ String tableUuid = "without-uuid".equals(context) ? null :
"table-id";
+ String commitUser = "owned".equals(context) ? "Writer-1" :
null;
+ catalog.rollbackTo(identifier, tableUuid, instant, 2L,
commitUser);
+ RecordedRequest recorded = server.takeRequest(10,
TimeUnit.SECONDS);
+ assertThat(recorded.getMethod()).isEqualTo("POST");
+ assertThat(recorded.getPath())
+
.isEqualTo("/v1/catalog/databases/db/tables/table%24branch_dev/rollback");
+ String body = recorded.getBody().readUtf8();
+ ObjectNode payload = RESTApi.fromJson(body, ObjectNode.class);
+
assertThat(payload.get("instant").get("type").asText()).isEqualTo(kind);
+ RollbackTableRequest request = RESTApi.fromJson(body,
RollbackTableRequest.class);
+ assertThat(request.getFromSnapshot()).isEqualTo(2L);
+ if ("snapshot".equals(kind)) {
+ assertThat(((Instant.SnapshotInstant)
request.getInstant()).getSnapshotId())
+ .isEqualTo(1L);
+ } else {
+ assertThat(((Instant.TagInstant)
request.getInstant()).getTagName())
+ .isEqualTo("tag-1");
+ }
+ if (tableUuid != null) {
+ assertThat(request.getTableId()).isEqualTo(tableUuid);
+ } else {
+ assertThat(payload.has("tableId")).isFalse();
+ }
+ if (commitUser != null) {
+ assertThat(request.getCommitUser()).isEqualTo(commitUser);
+ } else {
+ assertThat(payload.has("commitUser")).isFalse();
+ }
+ assertThat(payload.size())
+ .isEqualTo(2 + (tableUuid == null ? 0 : 1) +
(commitUser == null ? 0 : 1));
+ }
+ }
+ }
+}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/CatalogEnvironmentTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/CatalogEnvironmentTest.java
index c8c5f4bb63..1b18b7039b 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/CatalogEnvironmentTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/CatalogEnvironmentTest.java
@@ -42,6 +42,7 @@ import org.apache.paimon.utils.SnapshotManager;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.io.IOException;
@@ -64,6 +65,96 @@ class CatalogEnvironmentTest {
private static final String READ_VIA_OPTION = RESTApi.HEADER_PREFIX +
RESTApi.READ_VIA_HEADER;
+ @ParameterizedTest
+ @ValueSource(strings = {"main", "dev"})
+ void testCatalogTableRollbackUsesRuntimeBranchAndOwner(String branch)
throws Exception {
+ Catalog catalog = mock(Catalog.class);
+ CatalogEnvironment environment =
+ new CatalogEnvironment(
+ new Identifier("db", "table", "original"),
+ "table-id",
+ () -> catalog,
+ null,
+ null,
+ null,
+ true,
+ false);
+ Instant instant = Instant.snapshot(1L);
+ environment.catalogTableRollback(branch, "writer").rollbackTo(instant,
2L);
+ verify(catalog)
+ .rollbackTo(
+ new Identifier("db", "table", branch), "table-id",
instant, 2L, "writer");
+ }
+
+ @ParameterizedTest
+ @CsvSource({"main,table-id", "dev,table-id", "main,", "dev,"})
+ void testCatalogTableRollbackWithoutOwnerPreservesTableUuid(String branch,
String tableUuid)
+ throws Exception {
+ Catalog catalog = mock(Catalog.class);
+ CatalogEnvironment environment =
+ new CatalogEnvironment(
+ new Identifier("db", "table", branch),
+ tableUuid,
+ () -> catalog,
+ null,
+ null,
+ null,
+ true,
+ false);
+ Instant instant = Instant.tag("tag-1");
+ environment.catalogTableRollback(null, null).rollbackTo(instant, null);
+ verify(catalog)
+ .rollbackTo(new Identifier("db", "table", branch), tableUuid,
instant, null, null);
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "main,main,table-id,table-id",
+ "dev,dev,branch-id,branch-id",
+ "main,dev,table-id,",
+ "dev,main,branch-id,",
+ "main,main,,",
+ "main,dev,,"
+ })
+ void testCopiedEnvironmentRollbackUsesTargetIdentity(
+ String sourceBranch, String targetBranch, String sourceUuid,
String expectedUuid)
+ throws Exception {
+ Catalog catalog = mock(Catalog.class);
+ Identifier sourceIdentifier =
+ "main".equals(sourceBranch)
+ ? Identifier.create("db", "table")
+ : new Identifier("db", "table", sourceBranch);
+ CatalogEnvironment environment =
+ new CatalogEnvironment(
+ sourceIdentifier, sourceUuid, () -> catalog, null,
null, null, true, false);
+ Identifier targetIdentifier = new Identifier("db", "table",
targetBranch);
+ CatalogEnvironment copied = environment.copy(targetIdentifier);
+
+ Instant snapshot = Instant.snapshot(1L);
+ copied.catalogTableRollback(null, null).rollbackTo(snapshot, 2L);
+ verify(catalog).rollbackTo(targetIdentifier, expectedUuid, snapshot,
2L, null);
+
+ Instant tag = Instant.tag("checkpoint");
+ copied.snapshotLoader().rollback(tag);
+ verify(catalog).rollbackTo(targetIdentifier, expectedUuid, tag, null,
null);
+ assertThat(environment.uuid()).isEqualTo(sourceUuid);
+ }
+
+ @Test
+ void testCatalogTableRollbackWithoutCatalogReturnsNull() {
+ assertThat(CatalogEnvironment.empty().catalogTableRollback(null,
null)).isNull();
+ }
+
+ @Test
+ void testCatalogTableRollbackWithoutVersionManagementReturnsNull() {
+ Catalog catalog = mock(Catalog.class);
+ CatalogEnvironment environment =
+ new CatalogEnvironment(null, null, () -> catalog, null, null,
null, false, false);
+ assertThat(environment.catalogTableRollback(null, null)).isNull();
+ assertThat(environment.catalogTableRollback("dev", "writer")).isNull();
+ verifyNoInteractions(catalog);
+ }
+
@Test
void testDependencyReadContextForRestCatalog() {
Identifier root = Identifier.create("db", "root$branch_dev");
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/FallbackReadFileStoreTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/FallbackReadFileStoreTableTest.java
index 4e139d80c5..78ac14e268 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/FallbackReadFileStoreTableTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/FallbackReadFileStoreTableTest.java
@@ -19,6 +19,7 @@
package org.apache.paimon.table;
import org.apache.paimon.CoreOptions;
+import org.apache.paimon.catalog.Catalog;
import org.apache.paimon.catalog.Identifier;
import org.apache.paimon.catalog.TableQueryAuthResult;
import org.apache.paimon.data.BinaryRow;
@@ -544,6 +545,57 @@ public class FallbackReadFileStoreTableTest {
assertThat(switchedId.getDatabaseName()).isEqualTo("mydb");
assertThat(switchedId.getBranchName()).isEqualTo(branchName);
assertThat(switchedId.getObjectName()).isEqualTo("mytable$branch_bc");
+ assertThat(switched.catalogEnvironment().uuid()).isNull();
+ assertThat(mainTable.catalogEnvironment().uuid()).isEqualTo("uuid-1");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"scan.fallback-branch", "scan.primary-branch"})
+ void testBranchViewsRollbackWithoutSourceUuid(String branchOption) throws
Exception {
+ FileStoreTable localTable = createTable();
+ localTable.createBranch("bc");
+ Catalog catalog = Mockito.mock(Catalog.class);
+ Identifier mainId = Identifier.create("mydb", "mytable");
+ CatalogEnvironment environment =
+ new CatalogEnvironment(
+ mainId, "main-uuid", () -> catalog, null, null, null,
true, false);
+ Options options = new Options();
+ options.set(branchOption, "bc");
+ FallbackReadFileStoreTable table =
+ (FallbackReadFileStoreTable)
+ FileStoreTableFactory.create(
+ fileIO, tablePath, localTable.schema(),
options, environment);
+ Identifier branchId = new Identifier("mydb", "mytable", "bc");
+
+ table.other().rollbackTo(1L);
+ Mockito.verify(catalog)
+ .rollbackTo(
+ Mockito.eq(branchId),
+ Mockito.isNull(),
+ Mockito.argThat(
+ instant ->
+ instant instanceof
Instant.SnapshotInstant
+ && ((Instant.SnapshotInstant)
instant)
+
.getSnapshotId()
+ == 1L),
+ Mockito.isNull(),
+ Mockito.isNull());
+
+ table.switchToBranch("bc").rollbackTo("checkpoint");
+ Mockito.verify(catalog)
+ .rollbackTo(
+ Mockito.eq(branchId),
+ Mockito.isNull(),
+ Mockito.argThat(
+ instant ->
+ instant instanceof Instant.TagInstant
+ && "checkpoint"
+ .equals(
+
((Instant.TagInstant) instant)
+
.getTagName())),
+ Mockito.isNull(),
+ Mockito.isNull());
+ assertThat(table.catalogEnvironment().uuid()).isEqualTo("main-uuid");
}
private void writeDataIntoTable(
diff --git
a/paimon-core/src/test/java/org/apache/paimon/tag/SnapshotLoaderImplTest.java
b/paimon-core/src/test/java/org/apache/paimon/tag/SnapshotLoaderImplTest.java
new file mode 100644
index 0000000000..772564281b
--- /dev/null
+++
b/paimon-core/src/test/java/org/apache/paimon/tag/SnapshotLoaderImplTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.paimon.tag;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.table.CatalogEnvironment;
+import org.apache.paimon.table.Instant;
+import org.apache.paimon.utils.SnapshotLoader;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/** Tests table incarnation propagation through catalog snapshot loaders. */
+class SnapshotLoaderImplTest {
+ @ParameterizedTest
+ @CsvSource({"main,main-uuid", "dev,branch-uuid", "main,", "dev,"})
+ void testRollbackPreservesTableUuid(String branch, String uuid) throws
Exception {
+ Catalog catalog = mock(Catalog.class);
+ Identifier identifier = new Identifier("db", "table", branch);
+ CatalogEnvironment environment = environment(catalog, identifier,
uuid);
+ Instant instant = Instant.tag("checkpoint");
+
+ environment.snapshotLoader().rollback(instant);
+
+ verify(catalog).rollbackTo(identifier, uuid, instant, null, null);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"main", "dev"})
+ void testCopyWithinBranchKeepsTableUuid(String branch) throws Exception {
+ Catalog catalog = mock(Catalog.class);
+ // The main branch also has an implicit null spelling.
+ Identifier identifier =
+ "main".equals(branch)
+ ? Identifier.create("db", "table")
+ : new Identifier("db", "table", branch);
+ SnapshotLoader loader = environment(catalog, identifier,
"table-uuid").snapshotLoader();
+ Instant instant = Instant.snapshot(1L);
+
+ loader.copyWithBranch(branch).rollback(instant);
+
+ verify(catalog)
+ .rollbackTo(
+ new Identifier("db", "table", branch), "table-uuid",
instant, null, null);
+ }
+
+ @ParameterizedTest
+ @CsvSource({"main,dev", "dev,main"})
+ void testCopyToAnotherBranchDoesNotReuseSourceUuid(String source, String
target)
+ throws Exception {
+ Catalog catalog = mock(Catalog.class);
+ SnapshotLoader loader =
+ environment(catalog, new Identifier("db", "table", source),
"source-uuid")
+ .snapshotLoader();
+ Instant instant = Instant.snapshot(1L);
+
+ loader.copyWithBranch(target).rollback(instant);
+
+ verify(catalog)
+ .rollbackTo(new Identifier("db", "table", target), null,
instant, null, null);
+ }
+
+ private CatalogEnvironment environment(Catalog catalog, Identifier
identifier, String uuid) {
+ return new CatalogEnvironment(
+ identifier, uuid, () -> catalog, null, null, null, true,
false);
+ }
+}