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 e28adfa817 [core][flink][spark] Support repairing the earliest
snapshot hint (#8883)
e28adfa817 is described below
commit e28adfa8178fa8197082dc5a2cb10f2e0a2061bc
Author: jerry <[email protected]>
AuthorDate: Thu Jul 30 19:21:16 2026 +0800
[core][flink][spark] Support repairing the earliest snapshot hint (#8883)
---
.../org/apache/paimon/utils/SnapshotManager.java | 50 +++++++++
.../apache/paimon/utils/SnapshotManagerTest.java | 122 +++++++++++++++++++++
.../ProcedurePositionalArgumentsITCase.java | 13 +++
.../procedure/RepairEarliestSnapshotProcedure.java | 54 +++++++++
.../services/org.apache.paimon.factories.Factory | 1 +
.../RepairEarliestSnapshotProcedureITCase.java | 53 +++++++++
.../org/apache/paimon/spark/SparkProcedures.java | 2 +
.../procedure/RepairEarliestSnapshotProcedure.java | 92 ++++++++++++++++
.../RepairEarliestSnapshotProcedureTest.scala | 42 +++++++
9 files changed, 429 insertions(+)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/utils/SnapshotManager.java
b/paimon-core/src/main/java/org/apache/paimon/utils/SnapshotManager.java
index af21181214..984a98902e 100644
--- a/paimon-core/src/main/java/org/apache/paimon/utils/SnapshotManager.java
+++ b/paimon-core/src/main/java/org/apache/paimon/utils/SnapshotManager.java
@@ -272,6 +272,56 @@ public class SnapshotManager implements Serializable {
}
}
+ /**
+ * Repairs the earliest snapshot hint to the start of a continuous suffix
ending at the latest
+ * snapshot and returns the previous earliest snapshot id.
+ */
+ public long repairEarliestSnapshot(long snapshotId) {
+ long previous =
+ Preconditions.checkNotNull(
+ earliestSnapshotId(),
+ "Cannot repair earliest snapshot for an empty table.");
+ long latest =
+ Preconditions.checkNotNull(
+ latestSnapshotId(), "Cannot repair earliest snapshot
for an empty table.");
+ Preconditions.checkArgument(
+ snapshotId >= previous,
+ "Snapshot %s must not be earlier than current earliest
snapshot %s.",
+ snapshotId,
+ previous);
+ Preconditions.checkArgument(
+ snapshotId <= latest,
+ "Snapshot %s must not be later than latest snapshot %s.",
+ snapshotId,
+ latest);
+ Set<Long> snapshotIds;
+ try {
+ snapshotIds =
+ snapshotIdStream()
+ .filter(id -> id >= snapshotId && id <= latest)
+ .collect(Collectors.toSet());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ for (long id = snapshotId; ; id++) {
+ Preconditions.checkArgument(
+ snapshotIds.contains(id), "Snapshot %s does not exist.",
id);
+ if (id == latest) {
+ break;
+ }
+ }
+ Preconditions.checkArgument(
+ snapshotId == previous || !snapshotExists(snapshotId - 1),
+ "Snapshot %s does not immediately follow a snapshot gap.",
+ snapshotId);
+ try {
+ commitEarliestHint(snapshotId);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return previous;
+ }
+
public @Nullable Long pickOrLatest(Predicate<Snapshot> predicate) {
Long latestId = latestSnapshotId();
Long earliestId = earliestSnapshotId();
diff --git
a/paimon-core/src/test/java/org/apache/paimon/utils/SnapshotManagerTest.java
b/paimon-core/src/test/java/org/apache/paimon/utils/SnapshotManagerTest.java
index 02b21ba906..e44af2cd90 100644
--- a/paimon-core/src/test/java/org/apache/paimon/utils/SnapshotManagerTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/utils/SnapshotManagerTest.java
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
import javax.annotation.Nullable;
@@ -93,6 +94,127 @@ public class SnapshotManagerTest {
assertThat(snapshotManager.earliestSnapshot().id()).isEqualTo(isRaceCondition ?
1 : 0);
}
+ @Test
+ public void testRepairEarliestSnapshot() throws IOException {
+ FileIO fileIO = LocalFileIO.create();
+ SnapshotManager snapshotManager = newSnapshotManager(fileIO, new
Path(tempDir.toString()));
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(1), createSnapshotWithMillis(1,
1000).toJson());
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(5), createSnapshotWithMillis(5,
5000).toJson());
+ snapshotManager.commitEarliestHint(1);
+ snapshotManager.commitLatestHint(5);
+
+ assertThat(snapshotManager.repairEarliestSnapshot(5)).isEqualTo(1);
+ assertThat(snapshotManager.earliestSnapshotId()).isEqualTo(5);
+
+ assertThat(snapshotManager.repairEarliestSnapshot(5)).isEqualTo(5);
+ assertThat(snapshotManager.earliestSnapshotId()).isEqualTo(5);
+ }
+
+ @Test
+ public void testRepairEarliestSnapshotRejectsBackwardTarget() throws
IOException {
+ FileIO fileIO = LocalFileIO.create();
+ SnapshotManager snapshotManager = newSnapshotManager(fileIO, new
Path(tempDir.toString()));
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(1), createSnapshotWithMillis(1,
1000).toJson());
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(5), createSnapshotWithMillis(5,
5000).toJson());
+ snapshotManager.commitEarliestHint(5);
+ snapshotManager.commitLatestHint(5);
+
+ assertThatThrownBy(() -> snapshotManager.repairEarliestSnapshot(1))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("must not be earlier than current
earliest snapshot");
+ }
+
+ @Test
+ public void testRepairEarliestSnapshotRejectsMissingTarget() throws
IOException {
+ FileIO fileIO = LocalFileIO.create();
+ SnapshotManager snapshotManager = newSnapshotManager(fileIO, new
Path(tempDir.toString()));
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(1), createSnapshotWithMillis(1,
1000).toJson());
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(5), createSnapshotWithMillis(5,
5000).toJson());
+ snapshotManager.commitEarliestHint(1);
+ snapshotManager.commitLatestHint(5);
+
+ assertThatThrownBy(() -> snapshotManager.repairEarliestSnapshot(4))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Snapshot 4 does not exist");
+ }
+
+ @Test
+ public void testRepairEarliestSnapshotRejectsNonContinuousSuffix() throws
IOException {
+ FileIO fileIO = LocalFileIO.create();
+ SnapshotManager snapshotManager = newSnapshotManager(fileIO, new
Path(tempDir.toString()));
+ for (long snapshotId : new long[] {1, 3, 5, 6}) {
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(snapshotId),
+ createSnapshotWithMillis(snapshotId, snapshotId *
1000).toJson());
+ }
+ snapshotManager.commitEarliestHint(1);
+ snapshotManager.commitLatestHint(6);
+
+ assertThatThrownBy(() -> snapshotManager.repairEarliestSnapshot(3))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Snapshot 4 does not exist");
+ }
+
+ @Test
+ public void testRepairEarliestSnapshotListsSnapshots() throws IOException {
+ FileIO fileIO = Mockito.spy(LocalFileIO.create());
+ SnapshotManager snapshotManager = newSnapshotManager(fileIO, new
Path(tempDir.toString()));
+ for (long snapshotId : new long[] {1, 3, 4, 5, 6}) {
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(snapshotId),
+ createSnapshotWithMillis(snapshotId, snapshotId *
1000).toJson());
+ }
+ snapshotManager.commitEarliestHint(1);
+ snapshotManager.commitLatestHint(6);
+ Mockito.clearInvocations(fileIO);
+
+ assertThat(snapshotManager.repairEarliestSnapshot(3)).isEqualTo(1);
+ Mockito.verify(fileIO).listStatus(snapshotManager.snapshotDirectory());
+ Mockito.verify(fileIO,
Mockito.never()).exists(snapshotManager.snapshotPath(4));
+ }
+
+ @Test
+ public void testRepairEarliestSnapshotRejectsTargetAfterLatest() throws
IOException {
+ FileIO fileIO = LocalFileIO.create();
+ SnapshotManager snapshotManager = newSnapshotManager(fileIO, new
Path(tempDir.toString()));
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(1), createSnapshotWithMillis(1,
1000).toJson());
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(5), createSnapshotWithMillis(5,
5000).toJson());
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(10), createSnapshotWithMillis(10,
10000).toJson());
+ snapshotManager.commitEarliestHint(1);
+ snapshotManager.commitLatestHint(5);
+
+ assertThatThrownBy(() -> snapshotManager.repairEarliestSnapshot(10))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("must not be later than latest
snapshot");
+ }
+
+ @Test
+ public void testRepairEarliestSnapshotRejectsContinuousTarget() throws
IOException {
+ FileIO fileIO = LocalFileIO.create();
+ SnapshotManager snapshotManager = newSnapshotManager(fileIO, new
Path(tempDir.toString()));
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(1), createSnapshotWithMillis(1,
1000).toJson());
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(4), createSnapshotWithMillis(4,
4000).toJson());
+ fileIO.tryToWriteAtomic(
+ snapshotManager.snapshotPath(5), createSnapshotWithMillis(5,
5000).toJson());
+ snapshotManager.commitEarliestHint(1);
+ snapshotManager.commitLatestHint(5);
+
+ assertThatThrownBy(() -> snapshotManager.repairEarliestSnapshot(5))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("does not immediately follow a snapshot
gap");
+ }
+
@Test
public void testEarliestSnapshotThrowsWhenRetryExhausted() throws
IOException {
FileIO localFileIO = LocalFileIO.create();
diff --git
a/paimon-flink/paimon-flink-1.18/src/test/java/org/apache/paimon/flink/procedure/ProcedurePositionalArgumentsITCase.java
b/paimon-flink/paimon-flink-1.18/src/test/java/org/apache/paimon/flink/procedure/ProcedurePositionalArgumentsITCase.java
index 5dac8cd200..b5350f495b 100644
---
a/paimon-flink/paimon-flink-1.18/src/test/java/org/apache/paimon/flink/procedure/ProcedurePositionalArgumentsITCase.java
+++
b/paimon-flink/paimon-flink-1.18/src/test/java/org/apache/paimon/flink/procedure/ProcedurePositionalArgumentsITCase.java
@@ -482,6 +482,19 @@ public class ProcedurePositionalArgumentsITCase extends
CatalogITCaseBase {
.doesNotThrowAnyException();
}
+ @Test
+ public void testRepairEarliestSnapshot() throws Exception {
+ sql("CREATE TABLE T (k INT)");
+ FileStoreTable table = paimonTable("T");
+
table.fileIO().tryToWriteAtomic(table.snapshotManager().snapshotPath(1), "");
+
table.fileIO().tryToWriteAtomic(table.snapshotManager().snapshotPath(5), "");
+ table.snapshotManager().commitEarliestHint(1);
+ table.snapshotManager().commitLatestHint(5);
+
+ assertThat(sql("CALL sys.repair_earliest_snapshot('default.T', 5)"))
+ .containsExactly(Row.of(1L, 5L));
+ }
+
@Test
public void testRewriteFileIndex() {
sql(
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RepairEarliestSnapshotProcedure.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RepairEarliestSnapshotProcedure.java
new file mode 100644
index 0000000000..5ce16fa5b8
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/RepairEarliestSnapshotProcedure.java
@@ -0,0 +1,54 @@
+/*
+ * 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.table.FileStoreTable;
+import org.apache.paimon.utils.SnapshotManager;
+
+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;
+import org.apache.flink.types.Row;
+
+/** A procedure to repair the earliest snapshot. */
+public class RepairEarliestSnapshotProcedure extends ProcedureBase {
+
+ public static final String IDENTIFIER = "repair_earliest_snapshot";
+
+ @ProcedureHint(
+ argument = {
+ @ArgumentHint(name = "table", type = @DataTypeHint("STRING")),
+ @ArgumentHint(name = "snapshot_id", type =
@DataTypeHint("BIGINT"))
+ })
+ public @DataTypeHint(
+ "ROW<previous_earliest_snapshot_id BIGINT,
current_earliest_snapshot_id BIGINT>") Row[]
+ call(ProcedureContext procedureContext, String tableId, Long
snapshotId)
+ throws Catalog.TableNotExistException {
+ SnapshotManager snapshotManager = ((FileStoreTable)
table(tableId)).snapshotManager();
+ long previous = snapshotManager.repairEarliestSnapshot(snapshotId);
+ return new Row[] {Row.of(previous, snapshotId)};
+ }
+
+ @Override
+ public String identifier() {
+ return IDENTIFIER;
+ }
+}
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 7cf7aefbf3..e0eee1005a 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
@@ -82,6 +82,7 @@ org.apache.paimon.flink.procedure.ExpireSnapshotsProcedure
org.apache.paimon.flink.procedure.ExpireChangelogsProcedure
org.apache.paimon.flink.procedure.ExpirePartitionsProcedure
org.apache.paimon.flink.procedure.PurgeFilesProcedure
+org.apache.paimon.flink.procedure.RepairEarliestSnapshotProcedure
org.apache.paimon.flink.procedure.privilege.InitFileBasedPrivilegeProcedure
org.apache.paimon.flink.procedure.privilege.CreatePrivilegedUserProcedure
org.apache.paimon.flink.procedure.privilege.DropPrivilegedUserProcedure
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/RepairEarliestSnapshotProcedureITCase.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/RepairEarliestSnapshotProcedureITCase.java
new file mode 100644
index 0000000000..ded069c335
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/procedure/RepairEarliestSnapshotProcedureITCase.java
@@ -0,0 +1,53 @@
+/*
+ * 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.flink.CatalogITCaseBase;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.utils.SnapshotManager;
+
+import org.apache.flink.types.Row;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** IT cases for {@link RepairEarliestSnapshotProcedure}. */
+public class RepairEarliestSnapshotProcedureITCase extends CatalogITCaseBase {
+
+ @Test
+ public void testRepairEarliestSnapshot() throws Exception {
+ sql("CREATE TABLE T (k INT)");
+ for (int i = 1; i <= 5; i++) {
+ sql("INSERT INTO T VALUES (" + i + ")");
+ }
+
+ FileStoreTable table = paimonTable("T");
+ SnapshotManager snapshotManager = table.snapshotManager();
+ snapshotManager.deleteSnapshot(2);
+ snapshotManager.deleteSnapshot(3);
+ snapshotManager.deleteSnapshot(4);
+
+ assertThat(
+ sql(
+ "CALL sys.repair_earliest_snapshot("
+ + "`table` => 'default.T', snapshot_id
=> 5)"))
+ .containsExactly(Row.of(1L, 5L));
+ assertThat(snapshotManager.earliestSnapshotId()).isEqualTo(5);
+ }
+}
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 6a2203b9cb..8fb1377323 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
@@ -50,6 +50,7 @@ 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.RepairEarliestSnapshotProcedure;
import org.apache.paimon.spark.procedure.RepairProcedure;
import org.apache.paimon.spark.procedure.ReplaceTagProcedure;
import org.apache.paimon.spark.procedure.RescaleProcedure;
@@ -113,6 +114,7 @@ public class SparkProcedures {
procedureBuilders.put("expire_snapshots",
ExpireSnapshotsProcedure::builder);
procedureBuilders.put("expire_partitions",
ExpirePartitionsProcedure::builder);
procedureBuilders.put("repair", RepairProcedure::builder);
+ procedureBuilders.put("repair_earliest_snapshot",
RepairEarliestSnapshotProcedure::builder);
procedureBuilders.put("fast_forward", FastForwardProcedure::builder);
procedureBuilders.put("merge_branch", MergeBranchProcedure::builder);
procedureBuilders.put("reset_consumer",
ResetConsumerProcedure::builder);
diff --git
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RepairEarliestSnapshotProcedure.java
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RepairEarliestSnapshotProcedure.java
new file mode 100644
index 0000000000..6ce3cb33dc
--- /dev/null
+++
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RepairEarliestSnapshotProcedure.java
@@ -0,0 +1,92 @@
+/*
+ * 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.paimon.table.FileStoreTable;
+import org.apache.paimon.utils.SnapshotManager;
+
+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.Metadata;
+import org.apache.spark.sql.types.StructField;
+import org.apache.spark.sql.types.StructType;
+
+import static org.apache.spark.sql.types.DataTypes.LongType;
+import static org.apache.spark.sql.types.DataTypes.StringType;
+
+/** A procedure to repair the earliest snapshot. */
+public class RepairEarliestSnapshotProcedure extends BaseProcedure {
+
+ private static final ProcedureParameter[] PARAMETERS =
+ new ProcedureParameter[] {
+ ProcedureParameter.required("table", StringType),
+ ProcedureParameter.required("snapshot_id", LongType)
+ };
+
+ private static final StructType OUTPUT_TYPE =
+ new StructType(
+ new StructField[] {
+ new StructField(
+ "previous_earliest_snapshot_id", LongType,
false, Metadata.empty()),
+ new StructField(
+ "current_earliest_snapshot_id", LongType,
false, Metadata.empty())
+ });
+
+ private RepairEarliestSnapshotProcedure(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());
+ long snapshotId = args.getLong(1);
+ return modifyPaimonTable(
+ tableIdent,
+ table -> {
+ SnapshotManager snapshotManager = ((FileStoreTable)
table).snapshotManager();
+ long previous =
snapshotManager.repairEarliestSnapshot(snapshotId);
+ return new InternalRow[] {newInternalRow(previous,
snapshotId)};
+ });
+ }
+
+ public static ProcedureBuilder builder() {
+ return new BaseProcedure.Builder<RepairEarliestSnapshotProcedure>() {
+ @Override
+ public RepairEarliestSnapshotProcedure doBuild() {
+ return new RepairEarliestSnapshotProcedure(tableCatalog());
+ }
+ };
+ }
+
+ @Override
+ public String description() {
+ return "RepairEarliestSnapshotProcedure";
+ }
+}
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/RepairEarliestSnapshotProcedureTest.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/RepairEarliestSnapshotProcedureTest.scala
new file mode 100644
index 0000000000..1895eb5f05
--- /dev/null
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/RepairEarliestSnapshotProcedureTest.scala
@@ -0,0 +1,42 @@
+/*
+ * 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.paimon.spark.PaimonSparkTestBase
+
+import org.apache.spark.sql.Row
+import org.assertj.core.api.Assertions.assertThat
+
+class RepairEarliestSnapshotProcedureTest extends PaimonSparkTestBase {
+
+ test("Paimon procedure: repair earliest snapshot") {
+ spark.sql("CREATE TABLE T (k INT) USING PAIMON")
+
+ val snapshotManager = loadTable("T").snapshotManager()
+ snapshotManager.fileIO.tryToWriteAtomic(snapshotManager.snapshotPath(1),
"")
+ snapshotManager.fileIO.tryToWriteAtomic(snapshotManager.snapshotPath(5),
"")
+ snapshotManager.commitEarliestHint(1)
+ snapshotManager.commitLatestHint(5)
+
+ checkAnswer(
+ spark.sql("CALL paimon.sys.repair_earliest_snapshot(table => 'test.T',
snapshot_id => 5)"),
+ Row(1L, 5L) :: Nil)
+ assertThat(snapshotManager.earliestSnapshotId).isEqualTo(5)
+ }
+}