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 35884672bf [core] Support rename branch API and procedures (#7549)
35884672bf is described below
commit 35884672bfa9337e9df153c097662bf8a5307c8c
Author: xuzifu666 <[email protected]>
AuthorDate: Mon Mar 30 17:49:05 2026 +0800
[core] Support rename branch API and procedures (#7549)
Currently user can't change branch name directly, need to create a new
branch and then delete the old one, which is inconvenient. Therefore
provide an API for rename branch. This PR provided:
1. Experimental rename branch API in Table
2. RenameBranchProcedure in Flink/Spark
3. RestCatalog api for rename branch
---
docs/content/flink/procedures.md | 18 ++
docs/content/spark/procedures.md | 12 ++
docs/static/rest-catalog-open-api.yaml | 48 +++++
.../main/java/org/apache/paimon/rest/RESTApi.java | 22 +++
.../java/org/apache/paimon/rest/ResourcePaths.java | 13 ++
.../paimon/rest/requests/RenameBranchRequest.java | 46 +++++
.../org/apache/paimon/catalog/AbstractCatalog.java | 6 +
.../java/org/apache/paimon/catalog/Catalog.java | 15 ++
.../org/apache/paimon/catalog/DelegateCatalog.java | 6 +
.../java/org/apache/paimon/rest/RESTCatalog.java | 14 ++
.../paimon/table/AbstractFileStoreTable.java | 5 +
.../paimon/table/DelegatedFileStoreTable.java | 5 +
.../java/org/apache/paimon/table/FormatTable.java | 5 +
.../org/apache/paimon/table/ReadonlyTable.java | 8 +
.../main/java/org/apache/paimon/table/Table.java | 4 +
.../org/apache/paimon/utils/BranchManager.java | 2 +
.../apache/paimon/utils/CatalogBranchManager.java | 5 +
.../paimon/utils/FileSystemBranchManager.java | 19 ++
.../org/apache/paimon/rest/RESTCatalogServer.java | 46 ++++-
.../org/apache/paimon/rest/RESTCatalogTest.java | 21 ++-
.../paimon/utils/FileSystemBranchManagerTest.java | 196 +++++++++++++++++++++
.../flink/procedure/RenameBranchProcedure.java | 59 +++++++
.../services/org.apache.paimon.factories.Factory | 1 +
.../org/apache/paimon/flink/BranchSqlITCase.java | 54 ++++++
.../org/apache/paimon/spark/SparkProcedures.java | 2 +
.../spark/procedure/RenameBranchProcedure.java | 90 ++++++++++
.../spark/procedure/BranchProcedureTest.scala | 50 ++++++
27 files changed, 762 insertions(+), 10 deletions(-)
diff --git a/docs/content/flink/procedures.md b/docs/content/flink/procedures.md
index 739e3d4754..49e65d705b 100644
--- a/docs/content/flink/procedures.md
+++ b/docs/content/flink/procedures.md
@@ -793,6 +793,24 @@ All available procedures are listed below.
CALL sys.delete_branch(`table` => 'default.T', branch => 'branch1')
</td>
</tr>
+ <tr>
+ <td>rename_branch</td>
+ <td>
+ -- Use named argument<br/>
+ CALL [catalog.]sys.rename_branch(`table` => 'identifier', from_branch
=> 'branchName', to_branch => 'newBranchName')<br/><br/>
+ -- Use indexed argument<br/>
+ CALL [catalog.]sys.rename_branch('identifier', 'branchName',
'newBranchName')
+ </td>
+ <td>
+ To rename a branch. Arguments:
+ <li>table: the target table identifier. Cannot be empty.</li>
+ <li>from_branch: name of the branch to be renamed.</li>
+ <li>to_branch: new name of the branch.</li>
+ </td>
+ <td>
+ CALL sys.rename_branch(`table` => 'default.T', from_branch =>
'branch1', to_branch => 'branch2')
+ </td>
+ </tr>
<tr>
<td>fast_forward</td>
<td>
diff --git a/docs/content/spark/procedures.md b/docs/content/spark/procedures.md
index 29ad120eab..ffc7005f42 100644
--- a/docs/content/spark/procedures.md
+++ b/docs/content/spark/procedures.md
@@ -317,6 +317,18 @@ This section introduce all available spark procedures
about paimon.
CALL sys.delete_branch(table => 'test_db.T', branch => 'test_branch')
</td>
</tr>
+ <tr>
+ <td>rename_branch</td>
+ <td>
+ To rename a branch. Arguments:
+ <li>table: the target table identifier. Cannot be empty.</li>
+ <li>from_branch: name of the branch to be renamed.</li>
+ <li>to_branch: new name of the branch.</li>
+ </td>
+ <td>
+ CALL sys.rename_branch(table => 'test_db.T', from_branch =>
'test_branch', to_branch => 'new_branch')
+ </td>
+ </tr>
<tr>
<td>fast_forward</td>
<td>
diff --git a/docs/static/rest-catalog-open-api.yaml
b/docs/static/rest-catalog-open-api.yaml
index a4bda071f9..33f64524ec 100644
--- a/docs/static/rest-catalog-open-api.yaml
+++ b/docs/static/rest-catalog-open-api.yaml
@@ -1183,6 +1183,49 @@ paths:
$ref: '#/components/responses/BranchNotExistErrorResponse'
"500":
$ref: '#/components/responses/ServerErrorResponse'
+ /v1/{prefix}/databases/{database}/tables/{table}/branches/{branch}/rename:
+ post:
+ tags:
+ - branch
+ summary: rename branch
+ operationId: renameBranch
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: database
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: table
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: branch
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RenameBranchRequest'
+ responses:
+ "200":
+ description: Success, no content
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "404":
+ $ref: '#/components/responses/BranchNotExistErrorResponse'
+ "409":
+ description: Branch already exists
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
/v1/{prefix}/databases/{database}/tables/{table}/branches/{branch}/forward:
post:
tags:
@@ -3251,6 +3294,11 @@ components:
fromTag:
nullable: true
type: string
+ RenameBranchRequest:
+ type: object
+ properties:
+ toBranch:
+ type: string
ForwardBranchRequest:
type: object
properties:
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 a939c5ce16..88761ca5f0 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
@@ -49,6 +49,7 @@ import org.apache.paimon.rest.requests.ForwardBranchRequest;
import org.apache.paimon.rest.requests.ListPartitionsByNamesRequest;
import org.apache.paimon.rest.requests.MarkDonePartitionsRequest;
import org.apache.paimon.rest.requests.RegisterTableRequest;
+import org.apache.paimon.rest.requests.RenameBranchRequest;
import org.apache.paimon.rest.requests.RenameTableRequest;
import org.apache.paimon.rest.requests.ResetConsumerRequest;
import org.apache.paimon.rest.requests.RollbackSchemaRequest;
@@ -965,6 +966,27 @@ public class RESTApi {
restAuthFunction);
}
+ /**
+ * Rename branch for table.
+ *
+ * @param identifier database name and table name.
+ * @param fromBranch source branch name
+ * @param toBranch target branch name
+ * @throws NoSuchResourceException Exception thrown on HTTP 404 means the
branch not exists
+ * @throws AlreadyExistsException Exception thrown on HTTP 409 means the
target branch already
+ * exists
+ * @throws ForbiddenException Exception thrown on HTTP 403 means don't
have the permission for
+ * this table
+ */
+ public void renameBranch(Identifier identifier, String fromBranch, String
toBranch) {
+ RenameBranchRequest request = new RenameBranchRequest(toBranch);
+ client.post(
+ resourcePaths.renameBranch(
+ identifier.getDatabaseName(),
identifier.getObjectName(), fromBranch),
+ request,
+ restAuthFunction);
+ }
+
/**
* Forward branch for table.
*
diff --git a/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
b/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
index 466ac975b4..6d1d16c82f 100644
--- a/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
+++ b/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
@@ -262,6 +262,19 @@ public class ResourcePaths {
"forward");
}
+ public String renameBranch(String databaseName, String tableName, String
branch) {
+ return SLASH.join(
+ V1,
+ prefix,
+ DATABASES,
+ encodeString(databaseName),
+ TABLES,
+ encodeString(tableName),
+ BRANCHES,
+ encodeString(branch),
+ "rename");
+ }
+
public String tags(String databaseName, String objectName) {
return SLASH.join(
V1,
diff --git
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/RenameBranchRequest.java
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/RenameBranchRequest.java
new file mode 100644
index 0000000000..63cf0011fa
--- /dev/null
+++
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/RenameBranchRequest.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.paimon.rest.requests;
+
+import org.apache.paimon.rest.RESTRequest;
+
+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.JsonProperty;
+
+/** Request for renaming branch. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class RenameBranchRequest implements RESTRequest {
+
+ private static final String FIELD_TO_BRANCH = "toBranch";
+
+ @JsonProperty(FIELD_TO_BRANCH)
+ private final String toBranch;
+
+ @JsonCreator
+ public RenameBranchRequest(@JsonProperty(FIELD_TO_BRANCH) String toBranch)
{
+ this.toBranch = toBranch;
+ }
+
+ @JsonGetter(FIELD_TO_BRANCH)
+ public String toBranch() {
+ return toBranch;
+ }
+}
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 709010d2ce..ad51ab1f4d 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
@@ -516,6 +516,12 @@ public abstract class AbstractCatalog implements Catalog {
throw new UnsupportedOperationException();
}
+ @Override
+ public void renameBranch(Identifier identifier, String fromBranch, String
toBranch)
+ throws BranchNotExistException, BranchAlreadyExistException {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public void fastForward(Identifier identifier, String branch) throws
BranchNotExistException {
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 8924dc6ae1..6d337f3716 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
@@ -663,6 +663,7 @@ public interface Catalog extends AutoCloseable {
* <li>{@link #rollbackTo(Identifier, Instant)}.
* <li>{@link #createBranch(Identifier, String, String)}.
* <li>{@link #dropBranch(Identifier, String)}.
+ * <li>{@link #renameBranch(Identifier, String, String)}.
* <li>{@link #listBranches(Identifier)}.
* <li>{@link #getTag(Identifier, String)}.
* <li>{@link #createTag(Identifier, String, Long, String, boolean)}.
@@ -875,6 +876,20 @@ public interface Catalog extends AutoCloseable {
*/
void dropBranch(Identifier identifier, String branch) throws
BranchNotExistException;
+ /**
+ * Rename a branch for this table.
+ *
+ * @param identifier path of the table, cannot be system or branch name.
+ * @param fromBranch the source branch name
+ * @param toBranch the target branch name
+ * @throws BranchNotExistException if the source branch doesn't exist
+ * @throws BranchAlreadyExistException if the target branch already exists
+ * @throws UnsupportedOperationException if the catalog does not {@link
+ * #supportsVersionManagement()}
+ */
+ void renameBranch(Identifier identifier, String fromBranch, String
toBranch)
+ throws BranchNotExistException, BranchAlreadyExistException;
+
/**
* Fast-forward a branch to main branch.
*
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 4a8eeabe50..ec5138a8cb 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
@@ -246,6 +246,12 @@ public abstract class DelegateCatalog implements Catalog {
wrapped.dropBranch(identifier, branch);
}
+ @Override
+ public void renameBranch(Identifier identifier, String fromBranch, String
toBranch)
+ throws BranchNotExistException, BranchAlreadyExistException {
+ wrapped.renameBranch(identifier, fromBranch, toBranch);
+ }
+
@Override
public void fastForward(Identifier identifier, String branch) throws
BranchNotExistException {
wrapped.fastForward(identifier, branch);
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 dbb2a8fd48..364cdb3cd5 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
@@ -733,6 +733,20 @@ public class RESTCatalog implements Catalog {
}
}
+ @Override
+ public void renameBranch(Identifier identifier, String fromBranch, String
toBranch)
+ throws BranchNotExistException, BranchAlreadyExistException {
+ try {
+ api.renameBranch(identifier, fromBranch, toBranch);
+ } catch (NoSuchResourceException e) {
+ throw new BranchNotExistException(identifier, fromBranch, e);
+ } catch (AlreadyExistsException e) {
+ throw new BranchAlreadyExistException(identifier, toBranch, e);
+ } catch (ForbiddenException e) {
+ throw new TableNoPermissionException(identifier, e);
+ }
+ }
+
@Override
public void fastForward(Identifier identifier, String branch) throws
BranchNotExistException {
try {
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java
b/paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java
index 52b9d162a5..3583d17c08 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java
@@ -718,6 +718,11 @@ abstract class AbstractFileStoreTable implements
FileStoreTable {
branchManager().dropBranch(branchName);
}
+ @Override
+ public void renameBranch(String fromBranch, String toBranch) {
+ branchManager().renameBranch(fromBranch, toBranch);
+ }
+
@Override
public void fastForward(String branchName) {
branchManager().fastForward(branchName);
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/DelegatedFileStoreTable.java
b/paimon-core/src/main/java/org/apache/paimon/table/DelegatedFileStoreTable.java
index 9eaa4f8d2e..1f234d81ff 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/DelegatedFileStoreTable.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/DelegatedFileStoreTable.java
@@ -285,6 +285,11 @@ public abstract class DelegatedFileStoreTable implements
FileStoreTable {
wrapped.deleteBranch(branchName);
}
+ @Override
+ public void renameBranch(String fromBranch, String toBranch) {
+ wrapped.renameBranch(fromBranch, toBranch);
+ }
+
@Override
public void fastForward(String branchName) {
wrapped.fastForward(branchName);
diff --git a/paimon-core/src/main/java/org/apache/paimon/table/FormatTable.java
b/paimon-core/src/main/java/org/apache/paimon/table/FormatTable.java
index d7af8510a9..7f13faf753 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/FormatTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/FormatTable.java
@@ -417,6 +417,11 @@ public interface FormatTable extends Table {
throw new UnsupportedOperationException();
}
+ @Override
+ default void renameBranch(String fromBranch, String toBranch) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
default void fastForward(String branchName) {
throw new UnsupportedOperationException();
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/ReadonlyTable.java
b/paimon-core/src/main/java/org/apache/paimon/table/ReadonlyTable.java
index 55f14e7025..b685f86e3e 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/ReadonlyTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/ReadonlyTable.java
@@ -265,6 +265,14 @@ public interface ReadonlyTable extends InnerTable {
this.getClass().getSimpleName()));
}
+ @Override
+ default void renameBranch(String fromBranch, String toBranch) {
+ throw new UnsupportedOperationException(
+ String.format(
+ "Readonly Table %s does not support renameBranch.",
+ this.getClass().getSimpleName()));
+ }
+
@Override
default void fastForward(String branchName) {
throw new UnsupportedOperationException(
diff --git a/paimon-core/src/main/java/org/apache/paimon/table/Table.java
b/paimon-core/src/main/java/org/apache/paimon/table/Table.java
index 13d56d6849..98c00c0101 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/Table.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/Table.java
@@ -202,6 +202,10 @@ public interface Table extends Serializable {
}
}
+ /** Rename a branch. */
+ @Experimental
+ void renameBranch(String fromBranch, String toBranch);
+
/** Merge a branch to main branch. */
@Experimental
void fastForward(String branchName);
diff --git
a/paimon-core/src/main/java/org/apache/paimon/utils/BranchManager.java
b/paimon-core/src/main/java/org/apache/paimon/utils/BranchManager.java
index e055724efe..c830a799bf 100644
--- a/paimon-core/src/main/java/org/apache/paimon/utils/BranchManager.java
+++ b/paimon-core/src/main/java/org/apache/paimon/utils/BranchManager.java
@@ -59,6 +59,8 @@ public interface BranchManager {
void fastForward(String branchName);
+ void renameBranch(String fromBranch, String toBranch);
+
List<String> branches();
default boolean branchExists(String branchName) {
diff --git
a/paimon-core/src/main/java/org/apache/paimon/utils/CatalogBranchManager.java
b/paimon-core/src/main/java/org/apache/paimon/utils/CatalogBranchManager.java
index 8723135bd0..4ba9c39475 100644
---
a/paimon-core/src/main/java/org/apache/paimon/utils/CatalogBranchManager.java
+++
b/paimon-core/src/main/java/org/apache/paimon/utils/CatalogBranchManager.java
@@ -108,6 +108,11 @@ public class CatalogBranchManager implements BranchManager
{
});
}
+ @Override
+ public void renameBranch(String fromBranch, String toBranch) {
+ executePost(catalog -> catalog.renameBranch(identifier, fromBranch,
toBranch));
+ }
+
@Override
public List<String> branches() {
return executeGet(catalog -> catalog.listBranches(identifier));
diff --git
a/paimon-core/src/main/java/org/apache/paimon/utils/FileSystemBranchManager.java
b/paimon-core/src/main/java/org/apache/paimon/utils/FileSystemBranchManager.java
index ef3a04ec99..3257ea1060 100644
---
a/paimon-core/src/main/java/org/apache/paimon/utils/FileSystemBranchManager.java
+++
b/paimon-core/src/main/java/org/apache/paimon/utils/FileSystemBranchManager.java
@@ -220,6 +220,25 @@ public class FileSystemBranchManager implements
BranchManager {
checkArgument(!branchExists(branchName), "Branch name '%s' already
exists.", branchName);
}
+ @Override
+ public void renameBranch(String fromBranch, String toBranch) {
+ checkArgument(!BranchManager.isMainBranch(fromBranch), "Cannot rename
the main branch.");
+ checkArgument(branchExists(fromBranch), "Branch name '%s' doesn't
exist.", fromBranch);
+ checkArgument(!branchExists(toBranch), "Branch name '%s' already
exists.", toBranch);
+ BranchManager.validateBranch(toBranch);
+
+ try {
+ // Use rename for atomic operation and better performance
+ fileIO.rename(branchPath(fromBranch), branchPath(toBranch));
+ } catch (IOException e) {
+ throw new RuntimeException(
+ String.format(
+ "Exception occurs when rename branch from '%s' to
'%s'.",
+ fromBranch, toBranch),
+ e);
+ }
+ }
+
@Override
public List<String> branches() {
try {
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 bd9036a4b0..5961101c3a 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
@@ -61,6 +61,7 @@ import org.apache.paimon.rest.requests.CreateTagRequest;
import org.apache.paimon.rest.requests.CreateViewRequest;
import org.apache.paimon.rest.requests.ListPartitionsByNamesRequest;
import org.apache.paimon.rest.requests.MarkDonePartitionsRequest;
+import org.apache.paimon.rest.requests.RenameBranchRequest;
import org.apache.paimon.rest.requests.RenameTableRequest;
import org.apache.paimon.rest.requests.ResetConsumerRequest;
import org.apache.paimon.rest.requests.RollbackSchemaRequest;
@@ -1837,15 +1838,42 @@ public class RESTCatalogServer {
case "POST":
if (resources.length == 6) {
branch = RESTUtil.decodeString(resources[4]);
- branchManager.fastForward(branch);
- branchIdentifier =
- new Identifier(
- identifier.getDatabaseName(),
- identifier.getTableName(),
- branch);
- tableLatestSnapshotStore.put(
- identifier.getFullName(),
-
tableLatestSnapshotStore.get(branchIdentifier.getFullName()));
+ if ("rename".equals(resources[5])) {
+ // Rename branch: /branches/{branch}/rename
+ RenameBranchRequest requestBody =
+ RESTApi.fromJson(data,
RenameBranchRequest.class);
+ String toBranch = requestBody.toBranch();
+ table.renameBranch(branch, toBranch);
+ // Update store for renamed branch
+ Identifier fromBranchIdentifier =
+ new Identifier(
+ identifier.getDatabaseName(),
+ identifier.getTableName(),
+ branch);
+ Identifier toBranchIdentifier =
+ new Identifier(
+ identifier.getDatabaseName(),
+ identifier.getTableName(),
+ toBranch);
+ tableLatestSnapshotStore.put(
+ toBranchIdentifier.getFullName(),
+ tableLatestSnapshotStore.get(
+
fromBranchIdentifier.getFullName()));
+ tableMetadataStore.put(
+ toBranchIdentifier.getFullName(),
+
tableMetadataStore.get(fromBranchIdentifier.getFullName()));
+ } else if ("forward".equals(resources[5])) {
+ // Fast forward branch
+ branchManager.fastForward(branch);
+ branchIdentifier =
+ new Identifier(
+ identifier.getDatabaseName(),
+ identifier.getTableName(),
+ branch);
+ tableLatestSnapshotStore.put(
+ identifier.getFullName(),
+
tableLatestSnapshotStore.get(branchIdentifier.getFullName()));
+ }
} else {
CreateBranchRequest requestBody =
RESTApi.fromJson(data,
CreateBranchRequest.class);
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 0c20dfdc17..e70a11321a 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
@@ -2132,7 +2132,26 @@ public abstract class RESTCatalogTest extends
CatalogTestBase {
Catalog.BranchAlreadyExistException.class,
() -> restCatalog.createBranch(identifier, "my_branch", null));
assertThat(restCatalog.listBranches(identifier)).containsOnly("my_branch");
- restCatalog.dropBranch(identifier, "my_branch");
+
+ // Test rename branch
+ restCatalog.renameBranch(identifier, "my_branch", "renamed_branch");
+
assertThat(restCatalog.listBranches(identifier)).containsOnly("renamed_branch");
+ assertThat(restCatalog.getTable(new Identifier(databaseName, "table",
"renamed_branch")))
+ .isNotNull();
+
+ // Test rename to existing branch should fail
+ restCatalog.createBranch(identifier, "another_branch", null);
+ assertThrows(
+ Catalog.BranchAlreadyExistException.class,
+ () -> restCatalog.renameBranch(identifier, "renamed_branch",
"another_branch"));
+
+ // Test rename non-existent branch should fail
+ assertThrows(
+ Catalog.BranchNotExistException.class,
+ () -> restCatalog.renameBranch(identifier,
"non_existent_branch", "new_branch"));
+
+ restCatalog.dropBranch(identifier, "renamed_branch");
+ restCatalog.dropBranch(identifier, "another_branch");
assertThrows(
Catalog.BranchNotExistException.class,
diff --git
a/paimon-core/src/test/java/org/apache/paimon/utils/FileSystemBranchManagerTest.java
b/paimon-core/src/test/java/org/apache/paimon/utils/FileSystemBranchManagerTest.java
new file mode 100644
index 0000000000..c74b53d3f3
--- /dev/null
+++
b/paimon-core/src/test/java/org/apache/paimon/utils/FileSystemBranchManagerTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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.utils;
+
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.FileIOFinder;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.types.DataTypes;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Unit tests for {@link FileSystemBranchManager}. */
+class FileSystemBranchManagerTest {
+
+ @TempDir java.nio.file.Path tempDir;
+
+ private Path tablePath;
+ private FileIO fileIO;
+ private SnapshotManager snapshotManager;
+ private TagManager tagManager;
+ private SchemaManager schemaManager;
+ private FileSystemBranchManager branchManager;
+
+ @BeforeEach
+ void before() throws Exception {
+ tablePath = new Path(tempDir.toUri().toString());
+ fileIO = FileIOFinder.find(tablePath);
+
+ // Create schema
+ Schema schema =
+ Schema.newBuilder()
+ .column("product_id", DataTypes.INT())
+ .column("price", DataTypes.INT())
+ .column("sales", DataTypes.INT())
+ .primaryKey("product_id")
+ .build();
+
+ // Create schema manager and initialize schema
+ schemaManager = new SchemaManager(fileIO, tablePath);
+ schemaManager.createTable(schema);
+
+ // Create snapshot, tag, and branch managers
+ snapshotManager = new SnapshotManager(fileIO, tablePath, null, null,
null);
+ tagManager = new TagManager(fileIO, tablePath);
+ branchManager =
+ new FileSystemBranchManager(
+ fileIO, tablePath, snapshotManager, tagManager,
schemaManager);
+ }
+
+ @Test
+ void testRenameBranchBasic() {
+ // Create a branch
+ branchManager.createBranch("old_branch");
+ assertThat(branchManager.branchExists("old_branch")).isTrue();
+
+ // Rename the branch
+ branchManager.renameBranch("old_branch", "new_branch");
+
+ // Verify old branch no longer exists
+ assertThat(branchManager.branchExists("old_branch")).isFalse();
+
+ // Verify new branch exists
+ assertThat(branchManager.branchExists("new_branch")).isTrue();
+
+ // Verify branches list contains new branch
+ List<String> branches = branchManager.branches();
+ assertThat(branches).contains("new_branch");
+ assertThat(branches).doesNotContain("old_branch");
+ }
+
+ @Test
+ void testRenameNonExistentBranch() {
+ // Try to rename non-existent branch should throw exception
+ assertThatThrownBy(() -> branchManager.renameBranch("non_existent",
"new_branch"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("doesn't exist");
+ }
+
+ @Test
+ void testRenameToExistingBranch() {
+ // Create two branches
+ branchManager.createBranch("branch1");
+ branchManager.createBranch("branch2");
+
+ // Try to rename to existing branch should throw exception
+ assertThatThrownBy(() -> branchManager.renameBranch("branch1",
"branch2"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("already exists");
+ }
+
+ @Test
+ void testRenameMainBranchShouldFail() {
+ // Try to rename main branch should throw exception
+ assertThatThrownBy(() -> branchManager.renameBranch("main",
"renamed_main"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("main");
+ }
+
+ @Test
+ void testRenameBranchFromTag() {
+ // Create a tag
+ branchManager.createBranch("old_branch");
+
+ // Rename branch created from tag
+ branchManager.renameBranch("old_branch", "new_branch");
+
+ // Verify new branch exists
+ assertThat(branchManager.branchExists("new_branch")).isTrue();
+ assertThat(branchManager.branchExists("old_branch")).isFalse();
+ }
+
+ @Test
+ void testRenameBranchPreservesData() {
+ // Create a branch
+ branchManager.createBranch("test_branch");
+ assertThat(branchManager.branchExists("test_branch")).isTrue();
+
+ // Rename the branch
+ branchManager.renameBranch("test_branch", "renamed_branch");
+
+ // Verify the renamed branch exists and the original does not
+ assertThat(branchManager.branchExists("test_branch")).isFalse();
+ assertThat(branchManager.branchExists("renamed_branch")).isTrue();
+ }
+
+ @Test
+ void testRenameBranchMultipleTimes() {
+ // Create a branch
+ branchManager.createBranch("branch1");
+
+ // Rename multiple times
+ branchManager.renameBranch("branch1", "branch2");
+ branchManager.renameBranch("branch2", "branch3");
+
+ // Verify final state
+ assertThat(branchManager.branchExists("branch1")).isFalse();
+ assertThat(branchManager.branchExists("branch2")).isFalse();
+ assertThat(branchManager.branchExists("branch3")).isTrue();
+
+ List<String> branches = branchManager.branches();
+ assertThat(branches).contains("branch3");
+ assertThat(branches).doesNotContain("branch1", "branch2");
+ }
+
+ @Test
+ void testRenameBranchValidatesBranchNames() {
+ // Try to rename with invalid target branch name
+ branchManager.createBranch("valid_branch");
+
+ // Test numeric branch name
+ assertThatThrownBy(() -> branchManager.renameBranch("valid_branch",
"123"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("pure numeric");
+
+ // Test blank branch name
+ assertThatThrownBy(() -> branchManager.renameBranch("valid_branch",
""))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("blank");
+ }
+
+ @Test
+ void testRenameBranchToSameName() {
+ // Create a branch
+ branchManager.createBranch("same_name");
+
+ // Try to rename to the same name should throw exception
+ assertThatThrownBy(() -> branchManager.renameBranch("same_name",
"same_name"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("already exists");
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RenameBranchProcedure.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RenameBranchProcedure.java
new file mode 100644
index 0000000000..8b60c7aa50
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RenameBranchProcedure.java
@@ -0,0 +1,59 @@
+/*
+ * 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.flink.procedure;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.table.Table;
+
+import org.apache.flink.table.annotation.ArgumentHint;
+import org.apache.flink.table.annotation.DataTypeHint;
+import org.apache.flink.table.annotation.ProcedureHint;
+import org.apache.flink.table.procedure.ProcedureContext;
+
+/**
+ * Rename branch procedure. Usage:
+ *
+ * <pre><code>
+ * CALL sys.rename_branch('tableId', 'fromBranch', 'toBranch')
+ * </code></pre>
+ */
+public class RenameBranchProcedure extends ProcedureBase {
+
+ public static final String IDENTIFIER = "rename_branch";
+
+ @Override
+ public String identifier() {
+ return IDENTIFIER;
+ }
+
+ @ProcedureHint(
+ argument = {
+ @ArgumentHint(name = "table", type = @DataTypeHint("STRING")),
+ @ArgumentHint(name = "from_branch", type =
@DataTypeHint("STRING")),
+ @ArgumentHint(name = "to_branch", type =
@DataTypeHint("STRING"))
+ })
+ public String[] call(
+ ProcedureContext procedureContext, String tableId, String
fromBranch, String toBranch)
+ throws Catalog.TableNotExistException {
+ Table table = catalog.getTable(Identifier.fromString(tableId));
+ table.renameBranch(fromBranch, toBranch);
+ return new String[] {"Success"};
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
b/paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
index 092b470aa7..f87a250ec2 100644
---
a/paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
+++
b/paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
@@ -62,6 +62,7 @@ org.apache.paimon.flink.procedure.ExpireTagsProcedure
org.apache.paimon.flink.procedure.ReplaceTagProcedure
org.apache.paimon.flink.procedure.CreateBranchProcedure
org.apache.paimon.flink.procedure.DeleteBranchProcedure
+org.apache.paimon.flink.procedure.RenameBranchProcedure
org.apache.paimon.flink.procedure.DropPartitionProcedure
org.apache.paimon.flink.procedure.MergeIntoProcedure
org.apache.paimon.flink.procedure.ResetConsumerProcedure
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BranchSqlITCase.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BranchSqlITCase.java
index bcd3e0d964..1b7d50c852 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BranchSqlITCase.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BranchSqlITCase.java
@@ -869,6 +869,60 @@ public class BranchSqlITCase extends CatalogITCaseBase {
.isEmpty();
}
+ @Test
+ public void testRenameBranch() throws Exception {
+ sql(
+ "CREATE TABLE T ("
+ + " pt INT"
+ + ", k INT"
+ + ", v STRING"
+ + ", PRIMARY KEY (pt, k) NOT ENFORCED"
+ + " ) PARTITIONED BY (pt) WITH ("
+ + " 'bucket' = '2'"
+ + " )");
+
+ // snapshot 1.
+ sql("INSERT INTO T VALUES (1, 10, 'apple'), (1, 20, 'banana')");
+ // snapshot 2.
+ sql("INSERT INTO T VALUES (2, 10, 'cat'), (2, 20, 'dog')");
+
+ // create tag
+ sql("CALL sys.create_tag('default.T', 'tag1', 1)");
+
+ // create branch from tag
+ sql("CALL sys.create_branch('default.T', 'branch1', 'tag1')");
+
+ // verify branch exists
+ FileStoreTable table = paimonTable("T");
+ assertThat(table.branchManager().branchExists("branch1")).isTrue();
+
+ // rename branch
+ sql("CALL sys.rename_branch('default.T', 'branch1', 'branch2')");
+
+ // verify old branch does not exist
+ table = paimonTable("T");
+ assertThat(table.branchManager().branchExists("branch1")).isFalse();
+
+ // verify new branch exists
+ assertThat(table.branchManager().branchExists("branch2")).isTrue();
+
+ // verify data in renamed branch
+ assertThat(collectResult("SELECT * FROM T$branch_branch2"))
+ .containsExactlyInAnyOrder("+I[1, 10, apple]", "+I[1, 20,
banana]");
+
+ // rename non-existent branch should fail
+ assertThatThrownBy(
+ () ->
+ sql(
+ "CALL sys.rename_branch('default.T',
'nonexistent', 'new_branch')"))
+ .hasMessageContaining("Branch");
+
+ // rename to existing branch should fail
+ sql("CALL sys.create_branch('default.T', 'branch3')");
+ assertThatThrownBy(() -> sql("CALL sys.rename_branch('default.T',
'branch2', 'branch3')"))
+ .hasMessageContaining("Branch");
+ }
+
private List<String> collectResult(String sql) throws Exception {
List<String> result = new ArrayList<>();
try (CloseableIterator<Row> it = tEnv.executeSql(sql).collect()) {
diff --git
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
index 74bf04d68c..f27478bbbd 100644
---
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
+++
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
@@ -46,6 +46,7 @@ import org.apache.paimon.spark.procedure.ProcedureBuilder;
import org.apache.paimon.spark.procedure.PurgeFilesProcedure;
import org.apache.paimon.spark.procedure.RemoveOrphanFilesProcedure;
import org.apache.paimon.spark.procedure.RemoveUnexistingFilesProcedure;
+import org.apache.paimon.spark.procedure.RenameBranchProcedure;
import org.apache.paimon.spark.procedure.RenameTagProcedure;
import org.apache.paimon.spark.procedure.RepairProcedure;
import org.apache.paimon.spark.procedure.ReplaceTagProcedure;
@@ -98,6 +99,7 @@ public class SparkProcedures {
procedureBuilders.put("create_global_index",
CreateGlobalIndexProcedure::builder);
procedureBuilders.put("drop_global_index",
DropGlobalIndexProcedure::builder);
procedureBuilders.put("delete_branch", DeleteBranchProcedure::builder);
+ procedureBuilders.put("rename_branch", RenameBranchProcedure::builder);
procedureBuilders.put("compact", CompactProcedure::builder);
procedureBuilders.put("compact_database",
CompactDatabaseProcedure::builder);
procedureBuilders.put("rescale", RescaleProcedure::builder);
diff --git
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RenameBranchProcedure.java
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RenameBranchProcedure.java
new file mode 100644
index 0000000000..f30209875e
--- /dev/null
+++
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RenameBranchProcedure.java
@@ -0,0 +1,90 @@
+/*
+ * 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.spark.procedure;
+
+import org.apache.spark.sql.catalyst.InternalRow;
+import org.apache.spark.sql.connector.catalog.Identifier;
+import org.apache.spark.sql.connector.catalog.TableCatalog;
+import org.apache.spark.sql.types.DataTypes;
+import org.apache.spark.sql.types.Metadata;
+import org.apache.spark.sql.types.StructField;
+import org.apache.spark.sql.types.StructType;
+
+import static org.apache.spark.sql.types.DataTypes.StringType;
+
+/** Spark procedure to rename a branch. */
+public class RenameBranchProcedure extends BaseProcedure {
+
+ private static final ProcedureParameter[] PARAMETERS =
+ new ProcedureParameter[] {
+ ProcedureParameter.required("table", StringType),
+ ProcedureParameter.required("from_branch", StringType),
+ ProcedureParameter.required("to_branch", StringType)
+ };
+
+ private static final StructType OUTPUT_TYPE =
+ new StructType(
+ new StructField[] {
+ new StructField("result", DataTypes.BooleanType, true,
Metadata.empty())
+ });
+
+ protected RenameBranchProcedure(TableCatalog tableCatalog) {
+ super(tableCatalog);
+ }
+
+ @Override
+ public ProcedureParameter[] parameters() {
+ return PARAMETERS;
+ }
+
+ @Override
+ public StructType outputType() {
+ return OUTPUT_TYPE;
+ }
+
+ @Override
+ public InternalRow[] call(InternalRow args) {
+ Identifier tableIdent = toIdentifier(args.getString(0),
PARAMETERS[0].name());
+ String fromBranch = args.getString(1);
+ String toBranch = args.getString(2);
+ InternalRow[] result =
+ modifyPaimonTable(
+ tableIdent,
+ table -> {
+ table.renameBranch(fromBranch, toBranch);
+ InternalRow outputRow = newInternalRow(true);
+ return new InternalRow[] {outputRow};
+ });
+ return result;
+ }
+
+ public static ProcedureBuilder builder() {
+ return new BaseProcedure.Builder<RenameBranchProcedure>() {
+ @Override
+ public RenameBranchProcedure doBuild() {
+ return new RenameBranchProcedure(tableCatalog());
+ }
+ };
+ }
+
+ @Override
+ public String description() {
+ return "RenameBranchProcedure";
+ }
+}
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/BranchProcedureTest.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/BranchProcedureTest.scala
index 67786a47fe..0db7436019 100644
---
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/BranchProcedureTest.scala
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/BranchProcedureTest.scala
@@ -181,4 +181,54 @@ class BranchProcedureTest extends PaimonSparkTestBase with
StreamTest {
Seq(Row("20240725", "apple", 5), Row("20240725", "banana", 7)))
}
}
+
+ test("Paimon Procedure: rename branch") {
+ withTable("T") {
+ sql("CREATE TABLE T (a INT, b STRING) TBLPROPERTIES ('primary-key'='a',
'bucket'='3')")
+
+ sql("INSERT INTO T VALUES (1, 'a'), (2, 'b')")
+
+ // create tag
+ sql("CALL sys.create_tag(table => 'test.T', tag => 'tag1', snapshot =>
1)")
+
+ // create branch from tag
+ sql("CALL sys.create_branch(table => 'test.T', branch => 'branch1', tag
=> 'tag1')")
+
+ // verify branch exists
+ val table = loadTable("T")
+ assert(table.branchManager().branchExists("branch1"))
+
+ // rename branch
+ checkAnswer(
+ sql(
+ "CALL sys.rename_branch(table => 'test.T', from_branch => 'branch1',
to_branch => 'branch2')"),
+ Row(true) :: Nil
+ )
+
+ // verify old branch does not exist
+ assert(!table.branchManager().branchExists("branch1"))
+
+ // verify new branch exists
+ assert(table.branchManager().branchExists("branch2"))
+
+ // verify data in renamed branch
+ checkAnswer(
+ sql("SELECT * FROM `T$branch_branch2` ORDER BY a"),
+ Row(1, "a") :: Row(2, "b") :: Nil
+ )
+
+ // rename non-existent branch should fail
+ intercept[Exception] {
+ sql(
+ "CALL sys.rename_branch(table => 'test.T', from_branch =>
'nonexistent', to_branch => 'new_branch')")
+ }
+
+ // rename to existing branch should fail
+ sql("CALL sys.create_branch(table => 'test.T', branch => 'branch3')")
+ intercept[Exception] {
+ sql(
+ "CALL sys.rename_branch(table => 'test.T', from_branch => 'branch2',
to_branch => 'branch3')")
+ }
+ }
+ }
}