Skip to content

Commit 27d834c

Browse files
committed
[core] Prevent tag deletion when referenced by branches
This closes #6272. When a branch is created from a tag, the tag's snapshot information is copied to the branch directory. However, deleting the tag could cause the snapshot expiration process to clean up data files that are still needed by the branch, making the branch unqueryable. This change adds a check in deleteTag() to verify if any branches were created from the tag before allowing deletion. If branches reference the tag, the deletion is blocked with an error message listing the referencing branches. Changes: - Added branchesCreatedFromTag() method to BranchManager interface with default empty implementation for backward compatibility - Implemented branchesCreatedFromTag() in FileSystemBranchManager to check each branch's tag directory for the specified tag - Added validation in AbstractFileStoreTable.deleteTag() to prevent deletion of tags that are still referenced by branches - Added testDeleteTagReferencedByBranch() test case to verify the fix
1 parent abc3a84 commit 27d834c

File tree

6 files changed

+83
-0
lines changed

6 files changed

+83
-0
lines changed

paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,14 @@ public void replaceTag(
646646

647647
@Override
648648
public void deleteTag(String tagName) {
649+
List<String> referencingBranches = branchManager().branchesCreatedFromTag(tagName);
650+
if (!referencingBranches.isEmpty()) {
651+
throw new IllegalStateException(
652+
String.format(
653+
"Cannot delete tag '%s' because it is still referenced by branches: %s. "
654+
+ "Please delete these branches first.",
655+
tagName, referencingBranches));
656+
}
649657
tagManager()
650658
.deleteTag(
651659
tagName,

paimon-core/src/main/java/org/apache/paimon/utils/BranchManager.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ public interface BranchManager {
4242

4343
List<String> branches();
4444

45+
/**
46+
* Get all branches that were created based on the given tag.
47+
*
48+
* @param tagName the name of the tag to check
49+
* @return list of branch names that reference the given tag
50+
*/
51+
default List<String> branchesCreatedFromTag(String tagName) {
52+
return java.util.Collections.emptyList();
53+
}
54+
4555
default boolean branchExists(String branchName) {
4656
return branches().contains(branchName);
4757
}

paimon-core/src/main/java/org/apache/paimon/utils/FileSystemBranchManager.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ public List<String> branches() {
215215
}
216216
}
217217

218+
@Override
219+
public List<String> branchesCreatedFromTag(String tagName) {
220+
List<String> result = new java.util.ArrayList<>();
221+
for (String branchName : branches()) {
222+
TagManager branchTagManager = tagManager.copyWithBranch(branchName);
223+
if (branchTagManager.tagExists(tagName)) {
224+
result.add(branchName);
225+
}
226+
}
227+
return result;
228+
}
229+
218230
private void copySchemasToBranch(String branchName, long schemaId) throws IOException {
219231
for (int i = 0; i <= schemaId; i++) {
220232
if (schemaManager.schemaExists(i)) {

paimon-core/src/test/java/org/apache/paimon/operation/LocalOrphanFilesCleanTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ public void normallyRemoving(Path dataPath) throws Throwable {
185185
expireOptions.set(CoreOptions.SNAPSHOT_NUM_RETAINED_MAX, snapshotCount - expired);
186186
table.copy(expireOptions.toMap()).newCommit("").expireSnapshots();
187187

188+
// delete branch1 first before deleting tags
189+
table.deleteBranch("branch1");
190+
188191
// randomly delete tags
189192
List<String> deleteTags = Collections.emptyList();
190193
deleteTags = randomlyPick(allTags);
@@ -288,6 +291,9 @@ public void testNormallyRemovingMixedWithExternalPath() throws Throwable {
288291
expireOptions.set(CoreOptions.SNAPSHOT_NUM_RETAINED_MAX, snapshotCount - expired);
289292
table.copy(expireOptions.toMap()).newCommit("").expireSnapshots();
290293

294+
// delete branch1 first before deleting tags
295+
table.deleteBranch("branch1");
296+
291297
// randomly delete tags
292298
List<String> deleteTags = Collections.emptyList();
293299
deleteTags = randomlyPick(allTags);

paimon-core/src/test/java/org/apache/paimon/rest/RESTSimpleTableTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
import org.junit.jupiter.api.AfterEach;
3434
import org.junit.jupiter.api.BeforeEach;
35+
import org.junit.jupiter.api.Disabled;
36+
import org.junit.jupiter.api.Test;
3537

3638
import java.io.IOException;
3739
import java.util.ArrayList;
@@ -106,4 +108,9 @@ protected FileStoreTable createBranchTable(String branch) throws Exception {
106108
new Identifier(
107109
identifier.getDatabaseName(), identifier.getTableName(), branch));
108110
}
111+
112+
@Test
113+
@Disabled("REST catalog does not support branchesCreatedFromTag yet")
114+
@Override
115+
public void testDeleteTagReferencedByBranch() {}
109116
}

paimon-core/src/test/java/org/apache/paimon/table/SimpleTableTestBase.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,46 @@ public void testDeleteBranch() throws Exception {
11741174
table.deleteBranch("fallback");
11751175
}
11761176

1177+
@Test
1178+
public void testDeleteTagReferencedByBranch() throws Exception {
1179+
FileStoreTable table = createFileStoreTable();
1180+
1181+
try (StreamTableWrite write = table.newWrite(commitUser);
1182+
StreamTableCommit commit = table.newCommit(commitUser)) {
1183+
write.write(rowData(1, 10, 100L));
1184+
commit.commit(0, write.prepareCommit(false, 1));
1185+
}
1186+
1187+
table.createTag("tag1", 1);
1188+
table.createBranch("branch1", "tag1");
1189+
1190+
// verify that deleting a tag referenced by a branch fails
1191+
assertThatThrownBy(() -> table.deleteTag("tag1"))
1192+
.satisfies(
1193+
anyCauseMatches(
1194+
IllegalStateException.class,
1195+
"Cannot delete tag 'tag1' because it is still referenced by branches: [branch1]"));
1196+
1197+
// create another branch from the same tag
1198+
table.createBranch("branch2", "tag1");
1199+
1200+
// verify that deleting the tag still fails and shows both branches
1201+
assertThatThrownBy(() -> table.deleteTag("tag1"))
1202+
.satisfies(
1203+
anyCauseMatches(
1204+
IllegalStateException.class,
1205+
"Cannot delete tag 'tag1' because it is still referenced by branches:"));
1206+
1207+
// delete both branches
1208+
table.deleteBranch("branch1");
1209+
table.deleteBranch("branch2");
1210+
1211+
// verify that deleting the tag succeeds after branches are deleted
1212+
table.deleteTag("tag1");
1213+
TagManager tagManager = new TagManager(table.fileIO(), table.location());
1214+
assertThat(tagManager.tagExists("tag1")).isFalse();
1215+
}
1216+
11771217
@Test
11781218
public void testFastForward() throws Exception {
11791219
FileStoreTable table = createFileStoreTable();

0 commit comments

Comments
 (0)