This is an automated email from the ASF dual-hosted git repository.

amogh-jahagirdar pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new f5704ee94b Core: v4: Prune leaf manifests with no live files in 
FilePlanner (#18725)
f5704ee94b is described below

commit f5704ee94b62839d79d9caf38045a3275fd3b4f0
Author: Anoop Johnson <[email protected]>
AuthorDate: Mon Nov 2 12:41:33 2026 -0800

    Core: v4: Prune leaf manifests with no live files in FilePlanner (#18725)
    
    Scan planning returns only live files (ADDED, EXISTING, or MODIFIED).
    Skip expanding a leaf manifest whose manifest_info reports none of these
    live file counts: the leaf is never opened and is counted as a skipped
    data manifest.
---
 .../main/java/org/apache/iceberg/FilePlanner.java  | 14 +++-
 .../java/org/apache/iceberg/TestFilePlanner.java   | 78 ++++++++++++++++++----
 2 files changed, 78 insertions(+), 14 deletions(-)

diff --git a/core/src/main/java/org/apache/iceberg/FilePlanner.java 
b/core/src/main/java/org/apache/iceberg/FilePlanner.java
index ca63c2b64f..12d93c8cd2 100644
--- a/core/src/main/java/org/apache/iceberg/FilePlanner.java
+++ b/core/src/main/java/org/apache/iceberg/FilePlanner.java
@@ -140,11 +140,15 @@ class FilePlanner {
 
     List<CloseableIterable<TrackedFile>> leafPlanTasks = Lists.newArrayList();
     for (ManifestFile leaf : leafManifests) {
-      leafPlanTasks.add(reader(leaf));
+      if (hasLiveFiles(leaf)) {
+        leafPlanTasks.add(reader(leaf));
+      } else {
+        scanMetrics.skippedDataManifests().increment();
+      }
     }
 
     CloseableIterable<TrackedFile> leafFiles =
-        executorService != null && leafManifests.size() > 1
+        executorService != null && leafPlanTasks.size() > 1
             ? new ParallelIterable<>(leafPlanTasks, executorService)
             : CloseableIterable.concat(leafPlanTasks);
 
@@ -204,6 +208,12 @@ class FilePlanner {
         ResidualEvaluator.of(spec, filter, caseSensitive));
   }
 
+  private static boolean hasLiveFiles(ManifestFile leaf) {
+    return leaf.hasAddedFiles()
+        || leaf.hasExistingFiles()
+        || (leaf.modifiedFilesCount() != null && leaf.modifiedFilesCount() > 
0);
+  }
+
   /** Per-spec task inputs computed once and shared across all files of a 
spec. */
   private static class TaskContext {
     private final String schemaAsString;
diff --git a/core/src/test/java/org/apache/iceberg/TestFilePlanner.java 
b/core/src/test/java/org/apache/iceberg/TestFilePlanner.java
index 318fe7df01..9a52fffd66 100644
--- a/core/src/test/java/org/apache/iceberg/TestFilePlanner.java
+++ b/core/src/test/java/org/apache/iceberg/TestFilePlanner.java
@@ -517,6 +517,47 @@ class TestFilePlanner {
     }
   }
 
+  @ParameterizedTest
+  @FieldSource("MANIFEST_FORMATS")
+  void leafReportingNoLiveFilesIsPruned(FileFormat format) throws IOException {
+    ManifestFile addedLeaf =
+        writeManifest(format, EMPTY_PARTITION, 
ImmutableList.of(unpartitionedDataFile(FILE_A)));
+    ManifestFile existingLeaf =
+        writeManifest(format, EMPTY_PARTITION, 
ImmutableList.of(unpartitionedDataFile(FILE_B)));
+    ManifestFile modifiedLeaf =
+        writeManifest(format, EMPTY_PARTITION, 
ImmutableList.of(unpartitionedDataFile(FILE_C)));
+    String deadLeafLocation =
+        "s3://bucket/db/table/dead-leaf." + 
format.name().toLowerCase(Locale.ROOT);
+    ManifestFile root =
+        writeManifest(
+            format,
+            EMPTY_PARTITION,
+            ImmutableList.of(
+                manifestRef(FileContent.DATA_MANIFEST, addedLeaf.path(), 
addedOnly(1)),
+                manifestRef(FileContent.DATA_MANIFEST, existingLeaf.path(), 
existingOnly(1)),
+                manifestRef(FileContent.DATA_MANIFEST, modifiedLeaf.path(), 
modifiedOnly(1)),
+                manifestRef(FileContent.DATA_MANIFEST, deadLeafLocation, 
deletedOnly(2))));
+
+    FileIO io = Mockito.spy(fileIO);
+    ScanMetrics metrics = ScanMetrics.of(new DefaultMetricsContext());
+    FilePlanner planner =
+        new FilePlanner(io, root, TABLE_SCHEMA, 
UNPARTITIONED_SPECS).scanMetrics(metrics);
+
+    List<FileScanTask> tasks;
+    try (CloseableIterable<FileScanTask> plan = planner.planFiles()) {
+      tasks = Lists.newArrayList(plan);
+    }
+
+    assertThat(tasks)
+        .extracting(task -> task.file().location())
+        .as("leaves with added, existing, or modified files are expanded; 
deleted-only is pruned")
+        .containsExactlyInAnyOrder(FILE_A, FILE_B, FILE_C);
+    Mockito.verify(io, Mockito.never()).newInputFile(deadLeafLocation);
+    assertThat(metrics.skippedDataManifests().value())
+        .as("the pruned leaf is counted as a skipped data manifest")
+        .isEqualTo(1L);
+  }
+
   @ParameterizedTest
   @FieldSource("MANIFEST_FORMATS")
   void deleteManifestInRootUnsupported(FileFormat format) throws IOException {
@@ -537,7 +578,7 @@ class TestFilePlanner {
   void v3ManifestLeafUnsupported(FileFormat format) throws IOException {
     String leafLocation = "s3://bucket/db/table/v3-leaf." + 
format.name().toLowerCase(Locale.ROOT);
     TrackedFile v3Leaf =
-        manifestRef(FileContent.DATA_MANIFEST, leafLocation, 
dataManifestInfo(1, 0));
+        manifestRef(FileContent.DATA_MANIFEST, leafLocation, manifestInfo(1, 
0, 0, 0, 0));
     ManifestFile root = writeManifest(format, EMPTY_PARTITION, 
ImmutableList.of(v3Leaf));
 
     FilePlanner planner = new FilePlanner(fileIO, root, TABLE_SCHEMA, 
UNPARTITIONED_SPECS);
@@ -636,11 +677,11 @@ class TestFilePlanner {
   }
 
   private static TrackedFile dataManifest(String location, int addedFiles) {
-    return manifestRef(FileContent.DATA_MANIFEST, location, 
dataManifestInfo(addedFiles));
+    return manifestRef(FileContent.DATA_MANIFEST, location, 
addedOnly(addedFiles));
   }
 
   private static TrackedFile dataManifestWithStats(String location, 
ContentStats stats) {
-    return manifestRefWithStats(FileContent.DATA_MANIFEST, location, stats, 
dataManifestInfo(1));
+    return manifestRefWithStats(FileContent.DATA_MANIFEST, location, stats, 
addedOnly(1));
   }
 
   private static Integer lowerBound(DataFile file, int fieldId) {
@@ -676,22 +717,35 @@ class TestFilePlanner {
         0L); // totalBytes
   }
 
-  private static ManifestInfo dataManifestInfo(int addedFiles) {
-    return dataManifestInfo(addedFiles, FORMAT_VERSION_V4);
+  private static ManifestInfo addedOnly(int addedFiles) {
+    return manifestInfo(addedFiles, 0, 0, 0, FORMAT_VERSION_V4);
+  }
+
+  private static ManifestInfo existingOnly(int existingFiles) {
+    return manifestInfo(0, existingFiles, 0, 0, FORMAT_VERSION_V4);
+  }
+
+  private static ManifestInfo modifiedOnly(int modifiedFiles) {
+    return manifestInfo(0, 0, modifiedFiles, 0, FORMAT_VERSION_V4);
+  }
+
+  private static ManifestInfo deletedOnly(int deletedFiles) {
+    return manifestInfo(0, 0, 0, deletedFiles, FORMAT_VERSION_V4);
   }
 
-  private static ManifestInfo dataManifestInfo(int addedFiles, int 
formatVersion) {
+  private static ManifestInfo manifestInfo(
+      int addedFiles, int existingFiles, int modifiedFiles, int deletedFiles, 
int formatVersion) {
     return ManifestInfoStruct.builder()
         .addedFilesCount(addedFiles)
-        .existingFilesCount(0)
-        .deletedFilesCount(0)
+        .existingFilesCount(existingFiles)
+        .deletedFilesCount(deletedFiles)
         .replacedFilesCount(0)
-        .modifiedFilesCount(0)
+        .modifiedFilesCount(modifiedFiles)
         .addedRowsCount(addedFiles * RECORD_COUNT)
-        .existingRowsCount(0)
-        .deletedRowsCount(0)
+        .existingRowsCount(existingFiles * RECORD_COUNT)
+        .deletedRowsCount(deletedFiles * RECORD_COUNT)
         .replacedRowsCount(0)
-        .modifiedRowsCount(0)
+        .modifiedRowsCount(modifiedFiles * RECORD_COUNT)
         .minSequenceNumber(0L)
         .formatVersion(formatVersion)
         .build();

Reply via email to