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 27ec170ef7 [iceberg] Skip an already-deleted manifest list when 
expiring old metadata (#9343)
27ec170ef7 is described below

commit 27ec170ef7aab1cb87d882af1aab112d29d3e139
Author: Lucas Gameiro <[email protected]>
AuthorDate: Sun Aug 23 17:21:28 2026 +0200

    [iceberg] Skip an already-deleted manifest list when expiring old metadata 
(#9343)
---
 .../paimon/iceberg/IcebergCommitCallback.java      |  7 ++++
 .../paimon/iceberg/IcebergCompatibilityTest.java   | 48 ++++++++++++++++++++++
 2 files changed, 55 insertions(+)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/iceberg/IcebergCommitCallback.java
 
b/paimon-core/src/main/java/org/apache/paimon/iceberg/IcebergCommitCallback.java
index f8a41f56f2..3f4532639f 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/iceberg/IcebergCommitCallback.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/iceberg/IcebergCommitCallback.java
@@ -1608,6 +1608,13 @@ public class IcebergCommitCallback implements 
CommitCallback, TagCallback {
                 }
                 expiredManifestLists.add(listName);
 
+                // A retained metadata JSON can reference a list an earlier 
rebuild already
+                // deleted. Reading it must not fail: we only open it to 
delete what it
+                // points at, and that earlier pass already did so.
+                if (!table.fileIO().exists(listPath)) {
+                    continue;
+                }
+
                 for (IcebergManifestFileMeta meta : 
manifestList.read(listName)) {
                     String metaName = new Path(meta.manifestPath()).getName();
                     if (expiredManifestFileMetas.contains(metaName)) {
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
index 6d1d9fc886..ca90e7a608 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
@@ -463,6 +463,54 @@ public class IcebergCompatibilityTest {
         commit.close();
     }
 
+    @Test
+    public void testExpireAllBeforeSkipsAlreadyDeletedManifestList() throws 
Exception {
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {DataTypes.INT(), DataTypes.INT()}, new 
String[] {"k", "v"});
+        FileStoreTable table =
+                createPaimonTable(rowType, Collections.emptyList(), 
Collections.emptyList(), -1);
+
+        String commitUser = UUID.randomUUID().toString();
+        TableWriteImpl<?> write = table.newWrite(commitUser);
+        TableCommitImpl commit = table.newCommit(commitUser);
+
+        write.write(GenericRow.of(1, 10));
+        commit.commit(1, write.prepareCommit(false, 1));
+        write.write(GenericRow.of(2, 20));
+        commit.commit(2, write.prepareCommit(false, 2));
+
+        IcebergPathFactory pathFactory =
+                new IcebergPathFactory(new Path(table.location(), "metadata"));
+        long latest = table.latestSnapshot().get().id();
+
+        // v(latest - 1) is retained by previous-versions-max = 1, but an 
earlier from-scratch
+        // rebuild deletes the manifest lists of every version below the one 
it rebuilds at.
+        // Reproduce that state: the metadata JSON survives, the list it 
points at does not.
+        Path retainedMetadataPath = pathFactory.toMetadataPath(latest - 1);
+        IcebergMetadata retained = IcebergMetadata.fromPath(table.fileIO(), 
retainedMetadataPath);
+        Path danglingListPath = new 
Path(retained.currentSnapshot().manifestList());
+        table.fileIO().deleteQuietly(danglingListPath);
+        assertThat(table.fileIO().exists(retainedMetadataPath)).isTrue();
+        assertThat(table.fileIO().exists(danglingListPath)).isFalse();
+
+        // Dropping the base metadata sends the next commit down the 
from-scratch path, which
+        // calls expireAllBefore and so walks the retained JSON above.
+        table.fileIO().deleteQuietly(pathFactory.toMetadataPath(latest));
+
+        write.write(GenericRow.of(3, 30));
+        commit.commit(3, write.prepareCommit(false, 3));
+
+        // The rebuild completed and published a usable Iceberg head.
+        Path rebuiltMetadataPath = 
pathFactory.toMetadataPath(table.latestSnapshot().get().id());
+        assertThat(table.fileIO().exists(rebuiltMetadataPath)).isTrue();
+        assertThat(getIcebergResult())
+                .containsExactlyInAnyOrder("Record(1, 10)", "Record(2, 20)", 
"Record(3, 30)");
+
+        write.close();
+        commit.close();
+    }
+
     @Test
     public void testCommitAfterRollbackDoesNotDuplicateSchemas() throws 
Exception {
         RowType rowType =

Reply via email to