diff --git a/core/src/main/java/org/apache/iceberg/TableMetadata.java b/core/src/main/java/org/apache/iceberg/TableMetadata.java index 5b56ca7e882c..b811d8c42f15 100644 --- a/core/src/main/java/org/apache/iceberg/TableMetadata.java +++ b/core/src/main/java/org/apache/iceberg/TableMetadata.java @@ -1327,6 +1327,10 @@ public Builder setRef(String name, SnapshotRef ref) { Snapshot snapshot = snapshotsById.get(snapshotId); ValidationException.check( snapshot != null, "Cannot set %s to unknown snapshot: %s", name, snapshotId); + ValidationException.check( + !SnapshotRef.MAIN_BRANCH.equals(name) || ref.isBranch(), + "Cannot set %s to a tag, it must be a branch", + SnapshotRef.MAIN_BRANCH); if (SnapshotRef.MAIN_BRANCH.equals(name)) { this.currentSnapshotId = ref.snapshotId(); diff --git a/core/src/test/java/org/apache/iceberg/TestSnapshotManager.java b/core/src/test/java/org/apache/iceberg/TestSnapshotManager.java index 9f612383d5ba..f9266c7a81bc 100644 --- a/core/src/test/java/org/apache/iceberg/TestSnapshotManager.java +++ b/core/src/test/java/org/apache/iceberg/TestSnapshotManager.java @@ -323,6 +323,25 @@ public void testCreateTagFailsWhenRefAlreadyExists() { .hasMessage("Ref tag2 already exists"); } + @TestTemplate + public void testCreateTagNamedMainFails() { + // stage a snapshot so that the table has a snapshot but no main branch ref yet + table.newAppend().appendFile(FILE_A).stageOnly().commit(); + Snapshot stagedSnapshot = Iterables.getOnlyElement(table.snapshots()); + + assertThatThrownBy( + () -> + table + .manageSnapshots() + .createTag(SnapshotRef.MAIN_BRANCH, stagedSnapshot.snapshotId()) + .commit()) + .isInstanceOf(ValidationException.class) + .hasMessage("Cannot set main to a tag, it must be a branch"); + + // the failed commit must not have created the main ref + assertThat(table.ops().refresh().ref(SnapshotRef.MAIN_BRANCH)).isNull(); + } + @TestTemplate public void testRemoveBranch() { table.newAppend().appendFile(FILE_A).commit(); diff --git a/core/src/test/java/org/apache/iceberg/TestTableMetadata.java b/core/src/test/java/org/apache/iceberg/TestTableMetadata.java index 8e3f0e638f28..ce176dd2bee9 100644 --- a/core/src/test/java/org/apache/iceberg/TestTableMetadata.java +++ b/core/src/test/java/org/apache/iceberg/TestTableMetadata.java @@ -2148,4 +2148,42 @@ public void testAddSnapshotWithStaleFirstRowIdIsRetryable() { .isInstanceOf(RetryableValidationException.class) .hasMessageContaining("Cannot add a snapshot, first-row-id is behind table next-row-id"); } + + @Test + public void testSetRefRejectsTagForMainBranch() { + TableMetadata base = + TableMetadata.newTableMetadata( + TEST_SCHEMA, PartitionSpec.unpartitioned(), "location", ImmutableMap.of()); + + Snapshot snapshot = + new BaseSnapshot( + 1, + 1L, + null, + System.currentTimeMillis(), + null, + null, + null, + "file:/s1.avro", + null, + null, + null); + TableMetadata withSnapshot = TableMetadata.buildFrom(base).addSnapshot(snapshot).build(); + + assertThatThrownBy( + () -> + TableMetadata.buildFrom(withSnapshot) + .setRef( + SnapshotRef.MAIN_BRANCH, + SnapshotRef.tagBuilder(snapshot.snapshotId()).build())) + .isInstanceOf(ValidationException.class) + .hasMessage("Cannot set main to a tag, it must be a branch"); + + // any other ref name can still be a tag + TableMetadata withTag = + TableMetadata.buildFrom(withSnapshot) + .setRef("tag1", SnapshotRef.tagBuilder(snapshot.snapshotId()).build()) + .build(); + assertThat(withTag.ref("tag1").isTag()).isTrue(); + } }