This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git
The following commit(s) were added to refs/heads/main by this push:
new 5cee5eb30 feat(metrics): report skipped-data-files and
skipped-delete-files in ScanReport (#2430)
5cee5eb30 is described below
commit 5cee5eb305c75bb312408863b04e787aed6d80c8
Author: Revanth <[email protected]>
AuthorDate: Wed Nov 4 11:34:14 2026 -0600
feat(metrics): report skipped-data-files and skipped-delete-files in
ScanReport (#2430)
---
table/incremental_append_scan.go | 1 +
table/incremental_changelog_scan.go | 1 +
table/scan_metrics.go | 44 +++++-
table/scan_metrics_test.go | 7 +-
table/scan_skipped_files_test.go | 245 ++++++++++++++++++++++++++++++
table/scanner.go | 35 ++++-
table/scanner_manifest_projection_test.go | 2 +
7 files changed, 323 insertions(+), 12 deletions(-)
diff --git a/table/incremental_append_scan.go b/table/incremental_append_scan.go
index 277caec92..dfce4336c 100644
--- a/table/incremental_append_scan.go
+++ b/table/incremental_append_scan.go
@@ -205,6 +205,7 @@ func (s *IncrementalAppendScan) PlanFiles(ctx
context.Context) ([]FileScanTask,
if err != nil {
return nil, err
}
+ acc.skippedDataFiles += entries.metrics.skippedDataFiles
tasks := make([]FileScanTask, 0, len(entries.dataEntries))
for _, entry := range entries.dataEntries {
diff --git a/table/incremental_changelog_scan.go
b/table/incremental_changelog_scan.go
index d1918b60f..1711eafc6 100644
--- a/table/incremental_changelog_scan.go
+++ b/table/incremental_changelog_scan.go
@@ -270,6 +270,7 @@ func (s *IncrementalChangelogScan) PlanFiles(ctx
context.Context) ([]ChangelogSc
if err != nil {
return nil, err
}
+ acc.skippedDataFiles += entries.metrics.skippedDataFiles
dataEntries = entries.dataEntries
}
diff --git a/table/scan_metrics.go b/table/scan_metrics.go
index a541d548d..194df7745 100644
--- a/table/scan_metrics.go
+++ b/table/scan_metrics.go
@@ -35,10 +35,13 @@ import (
// fields remain plain integers without atomics.
//
// The scanned/skipped manifest counts measure partition-spec pruning (the
-// filter applied while listing manifests). The per-entry skipped-data-files /
-// skipped-delete-files counts (which would be incremented inside the
concurrent
-// manifest-entry loop, and would use atomics) and indexed-delete-files are
left
-// for a follow-up and omitted rather than reported as zero.
+// filter applied while listing manifests). The skipped data/delete file counts
+// measure the next step: entries the scan reads from scanned manifests (live
+// entries for table scans) that the partition or inclusive metrics evaluator
+// rejected. Each manifest worker counts its own rejections and the planner
+// merges them after the workers finish. Entries in pruned manifests count only
+// toward the skipped manifest counts. indexed-delete-files is left for a
+// follow-up and omitted rather than reported as zero.
type scanMetricsAccumulator struct {
totalDataManifests int64
totalDeleteManifests int64
@@ -46,6 +49,8 @@ type scanMetricsAccumulator struct {
scannedDeleteManifests int64
skippedDataManifests int64
skippedDeleteManifests int64
+ skippedDataFiles int64
+ skippedDeleteFiles int64
resultDataFiles int64
resultDeleteFiles int64
@@ -56,6 +61,31 @@ type scanMetricsAccumulator struct {
dvs int64
}
+// countRejections wraps a data-file filter so every file it rejects increments
+// *skipped. Errors are passed through and not counted. Build the wrapper
inside
+// the manifest worker that owns skipped, never in the evaluator caches that
+// workers share.
+func countRejections(filter func(iceberg.DataFile) (bool, error), skipped
*int64) func(iceberg.DataFile) (bool, error) {
+ return func(df iceberg.DataFile) (bool, error) {
+ use, err := filter(df)
+ if err == nil && !use {
+ *skipped++
+ }
+
+ return use, err
+ }
+}
+
+// addSkippedFiles merges one manifest's rejected-entry count into the counter
+// for that manifest's content type, as Java does in ManifestReader.entries.
+func (acc *scanMetricsAccumulator) addSkippedFiles(manifest
iceberg.ManifestFile, skipped int64) {
+ if manifest.ManifestContent() == iceberg.ManifestContentDeletes {
+ acc.skippedDeleteFiles += skipped
+ } else {
+ acc.skippedDataFiles += skipped
+ }
+}
+
func counterCount(v int64) *metrics.CounterResult {
return metrics.NewCounterResult(metrics.UnitCount, v)
}
@@ -107,8 +137,8 @@ func (acc *scanMetricsAccumulator) addResultDeleteMetrics(t
FileScanTask) {
// describes exactly what was used rather than re-resolving it here. projected
may
// be nil when the projection could not be resolved, in which case the report
// simply carries no projected fields. Only the metrics that are actually
measured
-// are populated; unmeasured ones (e.g. skipped-data-files,
indexed-delete-files)
-// are left unset and omitted.
+// are populated; unmeasured ones (e.g. indexed-delete-files) are left unset
and
+// omitted.
func (scan *Scan) buildScanReport(acc *scanMetricsAccumulator, schema,
projected *iceberg.Schema, planning time.Duration) metrics.ScanReport {
ids, names := projectedFields(projected)
envCtx := iceberg.EnvironmentContext()
@@ -154,6 +184,8 @@ func (scan *Scan) buildScanReport(acc
*scanMetricsAccumulator, schema, projected
ScannedDeleteManifests:
counterCount(acc.scannedDeleteManifests),
SkippedDataManifests:
counterCount(acc.skippedDataManifests),
SkippedDeleteManifests:
counterCount(acc.skippedDeleteManifests),
+ SkippedDataFiles:
counterCount(acc.skippedDataFiles),
+ SkippedDeleteFiles:
counterCount(acc.skippedDeleteFiles),
TotalFileSizeInBytes:
counterBytes(acc.totalFileSize),
TotalDeleteFileSizeInBytes:
counterBytes(acc.totalDeleteFileSize),
EqualityDeleteFiles:
counterCount(acc.equalityDeleteFiles),
diff --git a/table/scan_metrics_test.go b/table/scan_metrics_test.go
index ec6abc204..29e1ec2ff 100644
--- a/table/scan_metrics_test.go
+++ b/table/scan_metrics_test.go
@@ -64,6 +64,8 @@ func TestBuildScanReport(t *testing.T) {
dvs: 1,
resultDeleteFiles: 4,
totalDeleteFileSize: 512,
+ skippedDataFiles: 7,
+ skippedDeleteFiles: 3,
}
sr := scan.buildScanReport(acc, meta.CurrentSchema(),
meta.CurrentSchema(), 5*time.Millisecond)
@@ -91,6 +93,10 @@ func TestBuildScanReport(t *testing.T) {
assert.Equal(t, int64(1), m.PositionalDeleteFiles.Value)
assert.Equal(t, int64(2), m.EqualityDeleteFiles.Value)
assert.Equal(t, int64(1), m.DVs.Value)
+ require.NotNil(t, m.SkippedDataFiles)
+ assert.Equal(t, int64(7), m.SkippedDataFiles.Value)
+ require.NotNil(t, m.SkippedDeleteFiles)
+ assert.Equal(t, int64(3), m.SkippedDeleteFiles.Value)
require.NotNil(t, m.TotalFileSizeInBytes)
assert.Equal(t, metrics.UnitBytes, m.TotalFileSizeInBytes.Unit)
@@ -98,7 +104,6 @@ func TestBuildScanReport(t *testing.T) {
assert.Equal(t, int64(512), m.TotalDeleteFileSizeInBytes.Value)
// Unmeasured metrics are left unset (omitted on the wire).
- assert.Nil(t, m.SkippedDataFiles)
assert.Nil(t, m.IndexedDeleteFiles)
}
diff --git a/table/scan_skipped_files_test.go b/table/scan_skipped_files_test.go
new file mode 100644
index 000000000..7cb77977f
--- /dev/null
+++ b/table/scan_skipped_files_test.go
@@ -0,0 +1,245 @@
+// 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 table
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/apache/iceberg-go"
+ iceio "github.com/apache/iceberg-go/io"
+ "github.com/apache/iceberg-go/metrics"
+ "github.com/stretchr/testify/require"
+)
+
+// skippedFilesTestTable builds a single append snapshot over a table
+// partitioned by identity(category):
+//
+// - data manifest A: categories 0, 1 and 2, four files each, with id bounds
+// [0,9], [10,19], [20,29] and [30,39], plus one DELETED entry in category
0;
+// - data manifest B: two files in category 5, so a category filter that
+// excludes 5 prunes the whole manifest;
+// - a delete manifest with one position delete file per category 0, 1 and 2.
+//
+// A filter of category == 1 AND id >= 25 then rejects 8 live entries in A by
+// partition, 2 more by column bounds, and keeps 2.
+func skippedFilesTestTable(t *testing.T, rep metrics.Reporter) *Table {
+ t.Helper()
+ ctx := context.Background()
+
+ const (
+ location = "mem://default/skipped-files"
+ snapshotID = int64(1)
+ )
+ schema := iceberg.NewSchema(0,
+ iceberg.NestedField{ID: 1, Name: "id", Type:
iceberg.PrimitiveTypes.Int32, Required: true},
+ iceberg.NestedField{ID: 2, Name: "category", Type:
iceberg.PrimitiveTypes.Int32, Required: true},
+ )
+ spec := iceberg.NewPartitionSpec(iceberg.PartitionField{
+ SourceIDs: []int{2}, FieldID: 1000, Name: "category",
Transform: iceberg.IdentityTransform{},
+ })
+
+ fsys, err := iceio.LoadFS(ctx, nil, location)
+ require.NoError(t, err)
+ fs := fsys.(iceio.WriteFileIO)
+
+ bound := func(v int32) []byte {
+ b, err := iceberg.Int32Literal(v).MarshalBinary()
+ require.NoError(t, err)
+
+ return b
+ }
+ dataFile := func(category, lower int32) iceberg.DataFile {
+ builder, err := iceberg.NewDataFileBuilder(spec,
iceberg.EntryContentData,
+ fmt.Sprintf("%s/data/category=%d/id-%d.parquet",
location, category, lower),
+ iceberg.ParquetFile, map[int]any{1000: category}, nil,
nil, 10, 100)
+ require.NoError(t, err)
+
+ return builder.
+ LowerBoundValues(map[int][]byte{1: bound(lower)}).
+ UpperBoundValues(map[int][]byte{1: bound(lower + 9)}).
+ Build()
+ }
+ added := func(df iceberg.DataFile) iceberg.ManifestEntry {
+ return iceberg.NewManifestEntry(iceberg.EntryStatusADDED,
new(snapshotID), nil, nil, df)
+ }
+ writeManifest := func(path string, content iceberg.ManifestContent,
entries []iceberg.ManifestEntry) iceberg.ManifestFile {
+ var buf bytes.Buffer
+ writer, err := iceberg.NewManifestWriter(2, &buf, spec, schema,
snapshotID,
+ iceberg.WithManifestWriterContent(content))
+ require.NoError(t, err)
+ for _, entry := range entries {
+ if entry.Status() == iceberg.EntryStatusDELETED {
+ require.NoError(t, writer.Delete(entry))
+ } else {
+ require.NoError(t, writer.Add(entry))
+ }
+ }
+ require.NoError(t, writer.Close())
+ manifest, err := writer.ToManifestFile(path, int64(buf.Len()),
+ iceberg.WithManifestFileContent(content))
+ require.NoError(t, err)
+ require.NoError(t, fs.WriteFile(ctx, path, buf.Bytes()))
+
+ return manifest
+ }
+
+ var entriesA []iceberg.ManifestEntry
+ for category := range int32(3) {
+ for lower := int32(0); lower < 40; lower += 10 {
+ entriesA = append(entriesA, added(dataFile(category,
lower)))
+ }
+ }
+ // Rejected by the partition filter, but deleted entries are not live,
so
+ // they must not count as skipped.
+ entriesA = append(entriesA, iceberg.NewManifestEntry(
+ iceberg.EntryStatusDELETED, new(snapshotID), new(int64(1)),
new(int64(1)), dataFile(0, 100)))
+ manifestA := writeManifest(location+"/metadata/data-a.avro",
iceberg.ManifestContentData, entriesA)
+ manifestB := writeManifest(location+"/metadata/data-b.avro",
iceberg.ManifestContentData,
+ []iceberg.ManifestEntry{added(dataFile(5, 0)),
added(dataFile(5, 10))})
+
+ var deleteEntries []iceberg.ManifestEntry
+ for category := range int32(3) {
+ builder, err := iceberg.NewDataFileBuilder(spec,
iceberg.EntryContentPosDeletes,
+ fmt.Sprintf("%s/data/category=%d/pos-delete.parquet",
location, category),
+ iceberg.ParquetFile, map[int]any{1000: category}, nil,
nil, 1, 10)
+ require.NoError(t, err)
+ deleteEntries = append(deleteEntries, added(builder.Build()))
+ }
+ deleteManifest := writeManifest(location+"/metadata/deletes.avro",
iceberg.ManifestContentDeletes, deleteEntries)
+
+ listPath := location + "/metadata/snap-1.avro"
+ var listBuf bytes.Buffer
+ seqNum := int64(1)
+ require.NoError(t, iceberg.WriteManifestList(2, &listBuf, snapshotID,
nil, &seqNum, 0,
+ []iceberg.ManifestFile{manifestA, manifestB, deleteManifest}))
+ require.NoError(t, fs.WriteFile(ctx, listPath, listBuf.Bytes()))
+
+ meta, err := NewMetadata(schema, &spec, UnsortedSortOrder, location,
nil)
+ require.NoError(t, err)
+ txn := New(Identifier{"db", "tbl"}, meta, "metadata.json", nil,
nil).NewTransaction()
+ txn.meta.snapshotList = []Snapshot{{
+ SnapshotID: snapshotID,
+ TimestampMs: 1000,
+ ManifestList: listPath,
+ SequenceNumber: seqNum,
+ SchemaID: &schema.ID,
+ Summary: &Summary{Operation: OpAppend},
+ }}
+ txn.meta.snapshotLog = []SnapshotLogEntry{{SnapshotID: snapshotID,
TimestampMs: 1000}}
+ txn.meta.currentSnapshotID = new(snapshotID)
+ built, err := txn.meta.Build()
+ require.NoError(t, err)
+
+ return New(Identifier{"db", "tbl"}, built, "metadata.json",
+ func(context.Context) (iceio.IO, error) { return fs, nil }, nil,
+ WithMetricsReporter(rep))
+}
+
+var skippedFilesTestFilter = iceberg.NewAnd(
+ iceberg.EqualTo(iceberg.Reference("category"), int32(1)),
+ iceberg.GreaterThanEqual(iceberg.Reference("id"), int32(25)),
+)
+
+func singleScanReport(t *testing.T, rep *metrics.InMemoryReporter)
metrics.ScanReport {
+ t.Helper()
+ reports := rep.Reports()
+ require.Len(t, reports, 1)
+ report, ok := reports[0].(metrics.ScanReport)
+ require.True(t, ok)
+
+ return report
+}
+
+func TestPlanFilesReportsSkippedDataAndDeleteFiles(t *testing.T) {
+ rep := &metrics.InMemoryReporter{}
+ tbl := skippedFilesTestTable(t, rep)
+
+ tasks, err :=
tbl.Scan(WithRowFilter(skippedFilesTestFilter)).PlanFiles(context.Background())
+ require.NoError(t, err)
+ require.Len(t, tasks, 2)
+
+ m := singleScanReport(t, rep).Metrics
+ // Manifest B is pruned as a whole, so its two files count only toward
+ // skipped-data-manifests.
+ require.Equal(t, int64(1), m.ScannedDataManifests.Value)
+ require.Equal(t, int64(1), m.SkippedDataManifests.Value)
+ // 8 live entries rejected by partition plus 2 by id bounds. The DELETED
+ // entry is not counted.
+ require.NotNil(t, m.SkippedDataFiles)
+ require.Equal(t, metrics.UnitCount, m.SkippedDataFiles.Unit)
+ require.Equal(t, int64(10), m.SkippedDataFiles.Value)
+ require.Equal(t, int64(2), m.ResultDataFiles.Value)
+ // The position deletes in categories 0 and 2 are rejected by partition.
+ require.NotNil(t, m.SkippedDeleteFiles)
+ require.Equal(t, int64(2), m.SkippedDeleteFiles.Value)
+}
+
+func TestPlanFilesReportsZeroSkippedFilesWithoutFilter(t *testing.T) {
+ rep := &metrics.InMemoryReporter{}
+ tbl := skippedFilesTestTable(t, rep)
+
+ tasks, err := tbl.Scan().PlanFiles(context.Background())
+ require.NoError(t, err)
+ require.Len(t, tasks, 14)
+
+ // Java always reports every counter, so zero is emitted rather than
omitted.
+ m := singleScanReport(t, rep).Metrics
+ require.NotNil(t, m.SkippedDataFiles)
+ require.Equal(t, int64(0), m.SkippedDataFiles.Value)
+ require.NotNil(t, m.SkippedDeleteFiles)
+ require.Equal(t, int64(0), m.SkippedDeleteFiles.Value)
+}
+
+func TestIncrementalAppendScanReportsSkippedDataFiles(t *testing.T) {
+ rep := &metrics.InMemoryReporter{}
+ tbl := skippedFilesTestTable(t, rep)
+
+ tasks, err :=
tbl.NewIncrementalAppendScan(WithRowFilter(skippedFilesTestFilter)).
+ ToSnapshot(1).PlanFiles(context.Background())
+ require.NoError(t, err)
+ require.Len(t, tasks, 2)
+
+ m := singleScanReport(t, rep).Metrics
+ require.Equal(t, int64(10), m.SkippedDataFiles.Value)
+ require.Equal(t, int64(0), m.SkippedDeleteFiles.Value)
+}
+
+func TestIncrementalChangelogScanReportsSkippedDataFiles(t *testing.T) {
+ rep := &metrics.InMemoryReporter{}
+ tbl := incrementalChangelogTestTable(t)
+
+ // Manifest 2 holds the DELETED entry for partition 1 and the ADDED
entry for
+ // partition 2, so it survives manifest pruning. A changelog scan reads
+ // deleted entries as changes, so the partition evaluator rejecting that
+ // entry counts as a skipped file. The other manifests hold only other
+ // partitions and are pruned whole.
+ tasks, err := tbl.NewIncrementalChangelogScan(
+ WithRowFilter(iceberg.EqualTo(iceberg.Reference("id"),
int32(2))),
+ WithReporter(rep),
+ ).PlanFiles(context.Background())
+ require.NoError(t, err)
+ require.Len(t, tasks, 1)
+
+ m := singleScanReport(t, rep).Metrics
+ require.Equal(t, int64(1), m.ScannedDataManifests.Value)
+ require.Equal(t, int64(1), m.SkippedDataFiles.Value)
+ require.Equal(t, int64(0), m.SkippedDeleteFiles.Value)
+}
diff --git a/table/scanner.go b/table/scanner.go
index c8ad9effd..5d9ff9ed5 100644
--- a/table/scanner.go
+++ b/table/scanner.go
@@ -157,6 +157,10 @@ type manifestEntries struct {
equalityDeleteEntries []iceberg.ManifestEntry
dvEntries []iceberg.ManifestEntry
mu sync.Mutex
+
+ // metrics carries only the skipped data and delete file counts from
+ // collection, for callers that merge them into a scan report.
+ metrics scanMetricsAccumulator
}
type classifiedManifestEntries struct {
@@ -1282,6 +1286,7 @@ func (scan *Scan)
collectManifestEntriesWithSchemaOptionsAndMinSequenceNum(
manifestIO := newManifestIOBatch(scan.ioF, concurrencyLimit)
manifestResults := make([]classifiedManifestEntries, len(manifestList))
+ skippedFiles := make([]int64, len(manifestList))
g, gctx := errgroup.WithContext(ctx)
g.SetLimit(concurrencyLimit)
@@ -1310,11 +1315,14 @@ func (scan *Scan)
collectManifestEntriesWithSchemaOptionsAndMinSequenceNum(
// Keep pruning stats until equality and
positional deletes are indexed.
projection =
&iceberg.ManifestEntryProjection{IncludePruningStats: true}
}
+ var skipped int64
manifestEntries, err := openManifestWithReadOptions(
- fs, mf, partEval, metricsEval, projection,
false, discardDeleted, discardExisting)
+ fs, mf, countRejections(partEval, &skipped),
countRejections(metricsEval, &skipped),
+ projection, false, discardDeleted,
discardExisting)
if err != nil {
return err
}
+ skippedFiles[manifestIndex] = skipped
classified, err :=
classifyManifestEntries(manifestEntries)
if err != nil {
return err
@@ -1329,7 +1337,12 @@ func (scan *Scan)
collectManifestEntriesWithSchemaOptionsAndMinSequenceNum(
return nil, err
}
- return flattenClassifiedManifestEntries(manifestResults), nil
+ entries := flattenClassifiedManifestEntries(manifestResults)
+ for i, mf := range manifestList {
+ entries.metrics.addSkippedFiles(mf, skippedFiles[i])
+ }
+
+ return entries, nil
}
func (scan *Scan) dataManifestProjection(retainDataFileStats bool)
(iceberg.ManifestEntryProjection, bool) {
@@ -1383,13 +1396,16 @@ func (scan *Scan) planDataManifestTasks(
eqDeleteIndex *equalityDeleteIndex,
) ([]FileScanTask, error) {
return scan.planDataManifestTasksWithOptions(
- ctx, manifestList, schema, minSeqNum, posDeleteIndex, dvIndex,
eqDeleteIndex, false, false)
+ ctx, manifestList, schema, &scanMetricsAccumulator{},
minSeqNum, posDeleteIndex, dvIndex, eqDeleteIndex, false, false)
}
+// planDataManifestTasksWithOptions merges the data entries its manifest
+// workers reject into acc.skippedDataFiles once all workers finish.
func (scan *Scan) planDataManifestTasksWithOptions(
ctx context.Context,
manifestList []iceberg.ManifestFile,
schema *iceberg.Schema,
+ acc *scanMetricsAccumulator,
minSeqNum int64,
posDeleteIndex *positionalDeleteIndex,
dvIndex map[string]iceberg.ManifestEntry,
@@ -1408,6 +1424,7 @@ func (scan *Scan) planDataManifestTasksWithOptions(
}
taskBatches := make([][]FileScanTask, len(manifestList))
+ skippedFiles := make([]int64, len(manifestList))
taskCapacities := make([]int, len(manifestList))
taskOffsets := make([]int, len(manifestList))
directBuffer := scan.rowFilter == nil ||
scan.rowFilter.Equals(iceberg.AlwaysTrue{})
@@ -1480,7 +1497,10 @@ func (scan *Scan) planDataManifestTasksWithOptions(
stripTaskColumnStats = p.IncludePruningStats &&
!dropColumnStats
projection = &p
}
- err = streamManifest(fs, manifest, partEval,
metricsEval, projection, dropColumnStats, func(entry iceberg.ManifestEntry)
error {
+ var skipped int64
+ countedPartEval := countRejections(partEval, &skipped)
+ countedMetricsEval := countRejections(metricsEval,
&skipped)
+ err = streamManifest(fs, manifest, countedPartEval,
countedMetricsEval, projection, dropColumnStats, func(entry
iceberg.ManifestEntry) error {
dataFile := entry.DataFile()
if dataFile.ContentType() !=
iceberg.EntryContentData {
return fmt.Errorf("%w: data manifest
contains %s file %q",
@@ -1505,6 +1525,7 @@ func (scan *Scan) planDataManifestTasksWithOptions(
}
taskBatches[index] = tasks
+ skippedFiles[index] = skipped
return nil
})
@@ -1513,6 +1534,9 @@ func (scan *Scan) planDataManifestTasksWithOptions(
if err := g.Wait(); err != nil {
return nil, err
}
+ for index, manifest := range manifestList {
+ acc.addSkippedFiles(manifest, skippedFiles[index])
+ }
var results []FileScanTask
if directBuffer {
@@ -1761,6 +1785,7 @@ func (scan *Scan) planFilesLocal(
if err != nil {
return nil, err
}
+ acc.skippedDeleteFiles +=
deleteEntries.metrics.skippedDeleteFiles
}
// Equality-delete pruning is the only data-manifest consumer of column
// metrics after the delete entries have been indexed. Positional
deletes and
@@ -1785,7 +1810,7 @@ func (scan *Scan) planFilesLocal(
// Step 4: Stream data entries into per-manifest task batches, then
flatten
// them in manifest order.
plannedTasks, err := scan.planDataManifestTasksWithOptions(
- ctx, dataManifests, schema, minSeqNum, posDeleteIndex, dvIndex,
eqDeleteIndex,
+ ctx, dataManifests, schema, acc, minSeqNum, posDeleteIndex,
dvIndex, eqDeleteIndex,
projectScanColumns, retainDataFileStats)
if err != nil {
return nil, err
diff --git a/table/scanner_manifest_projection_test.go
b/table/scanner_manifest_projection_test.go
index dc90b1ae1..1a65a6202 100644
--- a/table/scanner_manifest_projection_test.go
+++ b/table/scanner_manifest_projection_test.go
@@ -268,6 +268,7 @@ func
TestPlanDataManifestTasksWithProjectionRetainsEqualityDeletePruning(t *test
t.Context(),
[]iceberg.ManifestFile{manifest},
schema,
+ &scanMetricsAccumulator{},
0,
posDeleteIndex,
dvIndex,
@@ -314,6 +315,7 @@ func
TestPlanDataManifestTasksWithProjectionRetainsEqualityDeletePruning(t *test
t.Context(),
[]iceberg.ManifestFile{manifest},
schema,
+ &scanMetricsAccumulator{},
0,
posDeleteIndex,
dvIndex,