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 8f8b994aa fix(table): snapshot summary counts and sizes for deletion 
vectors (#2352)
8f8b994aa is described below

commit 8f8b994aadeca0299aec7ad8343b0812987660c8
Author: Akriti <[email protected]>
AuthorDate: Wed Nov 4 21:36:45 2026 +0530

    fix(table): snapshot summary counts and sizes for deletion vectors (#2352)
    
    * dev
    
    * test(table): address review feedback on DV summary tests
    
    * fix: calculate dv sizes
    
    * docs: add compatibility note for DV snapshot summaries
    
    * docs: refine DV compatibility note
---
 table/commit_metrics.go          |  8 ++++---
 table/commit_metrics_test.go     | 10 +++++---
 table/dv_rewrite_test.go         |  3 ++-
 table/row_delta_test.go          |  4 ++++
 table/snapshots.go               | 32 ++++++++++++++++++++++----
 table/snapshots_internal_test.go | 49 ++++++++++++++++++++++++++++++++++++++++
 website/src/releases.md          |  1 +
 7 files changed, 96 insertions(+), 11 deletions(-)

diff --git a/table/commit_metrics.go b/table/commit_metrics.go
index 2db0b74c8..0cd1abd6a 100644
--- a/table/commit_metrics.go
+++ b/table/commit_metrics.go
@@ -81,9 +81,8 @@ func summaryCounter(props iceberg.Properties, key string, 
unit metrics.Unit) *me
 // snapshotSummary): attempts and total-duration come from the commit itself,
 // and every other metric is read from the snapshot summary under its spec key
 // and emitted under Java's commit-report field name so dashboards line up
-// across implementations. Metrics whose summary key iceberg-go does not yet
-// populate (DVs) are absent and therefore omitted — exactly as Java's
-// counterFrom omits summary keys it does not find.
+// across implementations. A metric whose summary key is absent is omitted,
+// exactly as Java's counterFrom omits summary keys it does not find.
 func buildCommitReport(tableName string, snap *Snapshot, attempts int64, dur 
time.Duration) metrics.CommitReport {
        var (
                snapshotID int64
@@ -127,6 +126,9 @@ func buildCommitReport(tableName string, snap *Snapshot, 
attempts int64, dur tim
                        AddedPositionalDeleteFiles:   
count(addedPosDeleteFilesKey),
                        RemovedPositionalDeleteFiles: 
count(removedPosDeleteFilesKey),
 
+                       AddedDVs:   count(addedDVsKey),
+                       RemovedDVs: count(removedDVsKey),
+
                        AddedRecords:   count(addedRecordsKey),
                        RemovedRecords: count(deletedRecordsKey),
                        TotalRecords:   count(totalRecordsKey),
diff --git a/table/commit_metrics_test.go b/table/commit_metrics_test.go
index d298a2fdb..c283774c3 100644
--- a/table/commit_metrics_test.go
+++ b/table/commit_metrics_test.go
@@ -59,6 +59,8 @@ func TestBuildCommitReport(t *testing.T) {
                                totalFileSizeKey:       "5000000",
                                addedPosDeletesKey:     "2",
                                addedPosDeleteFilesKey: "1",
+                               addedDVsKey:            "7",
+                               removedDVsKey:          "8",
                                addedEqDeletesKey:      "3",
                                addedEqDeleteFilesKey:  "1",
                                manifestsCreatedKey:    "6",
@@ -91,6 +93,10 @@ func TestBuildCommitReport(t *testing.T) {
        assert.Equal(t, int64(12345), m.AddedRecords.Value)
        require.NotNil(t, m.TotalRecords)
        assert.Equal(t, int64(20000), m.TotalRecords.Value)
+       require.NotNil(t, m.AddedDVs)
+       assert.Equal(t, int64(7), m.AddedDVs.Value)
+       require.NotNil(t, m.RemovedDVs)
+       assert.Equal(t, int64(8), m.RemovedDVs.Value)
 
        // Name-translated mappings (iceberg-go summary key -> Java report 
name).
        require.NotNil(t, m.RemovedDataFiles, "deleted-data-files -> 
removed-data-files")
@@ -123,10 +129,8 @@ func TestBuildCommitReport(t *testing.T) {
        require.NotNil(t, m.ManifestEntriesProcessed, "entries-processed -> 
manifest-entries-processed")
        assert.Equal(t, int64(9), m.ManifestEntriesProcessed.Value)
 
-       // Keys absent from the summary are omitted, not zeroed; metrics with no
-       // iceberg-go summary key (DVs) stay unset.
+       // Keys absent from the summary are omitted, not zeroed.
        assert.Nil(t, m.RemovedDeleteFiles)
-       assert.Nil(t, m.AddedDVs)
 }
 
 func TestBuildCommitReportIncludesEnvironmentContext(t *testing.T) {
diff --git a/table/dv_rewrite_test.go b/table/dv_rewrite_test.go
index dbae9dc8c..ed6cb806e 100644
--- a/table/dv_rewrite_test.go
+++ b/table/dv_rewrite_test.go
@@ -586,7 +586,8 @@ func TestRemovingDataFileRemovesItsUnnamedDV(t *testing.T) {
                                snap := tbl.CurrentSnapshot()
                                live, removed := snapshotDeleteEntryFiles(t, 
snap, iceio.LocalFS{})
                                assert.Contains(t, pathRefPairs(removed), 
[2]string{dvB.FilePath(), dataB.FilePath()})
-                               assert.Equal(t, "1", 
snap.Summary.Properties["removed-position-delete-files"])
+                               assert.Equal(t, "1", 
snap.Summary.Properties["removed-dvs"])
+                               assert.NotContains(t, snap.Summary.Properties, 
"removed-position-delete-files")
                                assert.Equal(t, strconv.Itoa(len(live)), 
snap.Summary.Properties["total-delete-files"])
                        })
                }
diff --git a/table/row_delta_test.go b/table/row_delta_test.go
index e17c877df..f0e23a033 100644
--- a/table/row_delta_test.go
+++ b/table/row_delta_test.go
@@ -890,6 +890,10 @@ func TestRowDeltaDVSupersessionSingleSnapshot(t 
*testing.T) {
        assert.Equal(t, table.OpDelete, snap.Summary.Operation)
        assert.Equal(t, "1", snap.Summary.Properties["added-delete-files"])
        assert.Equal(t, "1", snap.Summary.Properties["removed-delete-files"])
+       assert.Equal(t, "1", snap.Summary.Properties["added-dvs"])
+       assert.Equal(t, "1", snap.Summary.Properties["removed-dvs"])
+       assert.NotContains(t, snap.Summary.Properties, 
"added-position-delete-files")
+       assert.NotContains(t, snap.Summary.Properties, 
"removed-position-delete-files")
 
        fs := iceio.LocalFS{}
        liveDVs, removedDVs := snapshotDVEntries(t, snap, fs)
diff --git a/table/snapshots.go b/table/snapshots.go
index 7e49aaad3..eb10667cf 100644
--- a/table/snapshots.go
+++ b/table/snapshots.go
@@ -70,6 +70,7 @@ const (
        addedPosDeleteFilesKey    = "added-position-delete-files"
        addedRecordsKey           = "added-records"
        addedEqDeleteFilesKey     = "added-equality-delete-files"
+       addedDVsKey               = "added-dvs"
        deletedDataFilesKey       = "deleted-data-files"
        deletedRecordsKey         = "deleted-records"
        removedDeleteFilesKey     = "removed-delete-files"
@@ -78,6 +79,7 @@ const (
        removedFileSizeKey        = "removed-files-size"
        removedPosDeletesKey      = "removed-position-deletes"
        removedPosDeleteFilesKey  = "removed-position-delete-files"
+       removedDVsKey             = "removed-dvs"
        totalEqDeletesKey         = "total-equality-deletes"
        totalPosDeletesKey        = "total-position-deletes"
        totalDataFilesKey         = "total-data-files"
@@ -97,6 +99,8 @@ type updateMetrics struct {
        removedEqDeleteFiles  int64
        addedPosDeleteFiles   int64
        removedPosDeleteFiles int64
+       addedDVs              int64
+       removedDVs            int64
        addedDeleteFiles      int64
        removedDeleteFiles    int64
        addedRecords          int64
@@ -107,15 +111,29 @@ type updateMetrics struct {
        removedEqDeletes      int64
 }
 
+// contentSizeInBytes returns the bytes a file contributes to the table. A DV
+// shares its Puffin file with other DVs, so it counts only its own blob.
+func contentSizeInBytes(df iceberg.DataFile) int64 {
+       if IsDeletionVector(df) && df.ContentSizeInBytes() != nil {
+               return *df.ContentSizeInBytes()
+       }
+
+       return df.FileSizeBytes()
+}
+
 func (m *updateMetrics) addDataFile(df iceberg.DataFile) error {
-       m.addedFileSize += df.FileSizeBytes()
+       m.addedFileSize += contentSizeInBytes(df)
        switch df.ContentType() {
        case iceberg.EntryContentData:
                m.addedDataFiles++
                m.addedRecords += df.Count()
        case iceberg.EntryContentPosDeletes:
                m.addedDeleteFiles++
-               m.addedPosDeleteFiles++
+               if IsDeletionVector(df) {
+                       m.addedDVs++
+               } else {
+                       m.addedPosDeleteFiles++
+               }
                m.addedPosDeletes += df.Count()
        case iceberg.EntryContentEqDeletes:
                m.addedDeleteFiles++
@@ -129,14 +147,18 @@ func (m *updateMetrics) addDataFile(df iceberg.DataFile) 
error {
 }
 
 func (m *updateMetrics) removeFile(df iceberg.DataFile) error {
-       m.removedFileSize += df.FileSizeBytes()
+       m.removedFileSize += contentSizeInBytes(df)
        switch df.ContentType() {
        case iceberg.EntryContentData:
                m.removedDataFiles++
                m.deletedRecords += df.Count()
        case iceberg.EntryContentPosDeletes:
                m.removedDeleteFiles++
-               m.removedPosDeleteFiles++
+               if IsDeletionVector(df) {
+                       m.removedDVs++
+               } else {
+                       m.removedPosDeleteFiles++
+               }
                m.removedPosDeletes += df.Count()
        case iceberg.EntryContentEqDeletes:
                m.removedDeleteFiles++
@@ -165,6 +187,8 @@ func (m *updateMetrics) toProps() iceberg.Properties {
        setWhenPositive(props, removedEqDeleteFilesKey, m.removedEqDeleteFiles)
        setWhenPositive(props, addedPosDeleteFilesKey, m.addedPosDeleteFiles)
        setWhenPositive(props, removedPosDeleteFilesKey, 
m.removedPosDeleteFiles)
+       setWhenPositive(props, addedDVsKey, m.addedDVs)
+       setWhenPositive(props, removedDVsKey, m.removedDVs)
        setWhenPositive(props, addedDeleteFilesKey, m.addedDeleteFiles)
        setWhenPositive(props, removedDeleteFilesKey, m.removedDeleteFiles)
        setWhenPositive(props, addedRecordsKey, m.addedRecords)
diff --git a/table/snapshots_internal_test.go b/table/snapshots_internal_test.go
index b35d5f70a..67cf50b1a 100644
--- a/table/snapshots_internal_test.go
+++ b/table/snapshots_internal_test.go
@@ -294,3 +294,52 @@ func TestUpdateSnapshotSummariesPreservesLargeTotals(t 
*testing.T) {
        assert.Equal(t, "4000000000", result.Properties[totalRecordsKey])
        assert.Equal(t, "9000000000", result.Properties[totalFileSizeKey])
 }
+
+func TestSnapshotSummaryCollectorDeletionVectors(t *testing.T) {
+       var ssc SnapshotSummaryCollector
+       spec := *iceberg.UnpartitionedSpec
+
+       // Two DVs that share one Puffin file, as DVWriter writes them.
+       newDV := func(ref string, count, offset, size int64) iceberg.DataFile {
+               return must(iceberg.NewDataFileBuilder(spec, 
iceberg.EntryContentPosDeletes,
+                       "/path/to/batch.puffin", iceberg.PuffinFile, nil, nil, 
nil, count, 1000)).
+                       ReferencedDataFile(ref).
+                       ContentOffset(offset).
+                       ContentSizeInBytes(size).
+                       Build()
+       }
+       dvA := newDV("/path/to/data-a.parquet", 2, 4, 300)
+       dvB := newDV("/path/to/data-b.parquet", 4, 304, 400)
+
+       oldDV := must(iceberg.NewDataFileBuilder(spec, 
iceberg.EntryContentPosDeletes,
+               "/path/to/old.puffin", iceberg.PuffinFile, nil, nil, nil, 1, 
600)).
+               ReferencedDataFile("/path/to/data-a.parquet").
+               ContentOffset(4).
+               ContentSizeInBytes(250).
+               Build()
+
+       posDelete := must(iceberg.NewDataFileBuilder(spec, 
iceberg.EntryContentPosDeletes,
+               "/path/to/pos-delete.parquet", iceberg.ParquetFile, nil, nil, 
nil, 3, 800)).Build()
+
+       require.NoError(t, ssc.addFile(dvA, tableSchemaSimple, spec))
+       require.NoError(t, ssc.addFile(dvB, tableSchemaSimple, spec))
+       require.NoError(t, ssc.addFile(posDelete, tableSchemaSimple, spec))
+       require.NoError(t, ssc.removeFile(oldDV, tableSchemaSimple, spec))
+
+       props := ssc.build()
+
+       // DVs are counted on their own...
+       assert.Equal(t, "2", props["added-dvs"])
+       assert.Equal(t, "1", props["removed-dvs"])
+       // ...and are not counted as position delete files.
+       assert.Equal(t, "1", props["added-position-delete-files"])
+       assert.NotContains(t, props, "removed-position-delete-files")
+       // DVs still count as delete files and as position deletes.
+       assert.Equal(t, "3", props["added-delete-files"])
+       assert.Equal(t, "1", props["removed-delete-files"])
+       assert.Equal(t, "9", props["added-position-deletes"]) // 2 + 4 + 3
+       assert.Equal(t, "1", props["removed-position-deletes"])
+       // A DV counts only its own blob, not the whole shared Puffin file.
+       assert.Equal(t, "1500", props["added-files-size"]) // 300 + 400 + 800
+       assert.Equal(t, "250", props["removed-files-size"])
+}
diff --git a/website/src/releases.md b/website/src/releases.md
index 0dc5a7c51..75490333d 100644
--- a/website/src/releases.md
+++ b/website/src/releases.md
@@ -92,6 +92,7 @@ Per-release notes (highlights, breaking changes, 
contributors) are published on
 - **Data-file replacement methods renamed.** Use `ReplaceDataFilesByPath` for 
string paths. Use `ReplaceDataFiles` for prebuilt `DataFile` values instead of 
`ReplaceDataFilesWithDataFiles`.
 
 - `maintenance.PurgeFiles` (previously `Table.PurgeFiles`) now calls 
`IO.Remove` concurrently, with up to 32 calls in flight, when the filesystem 
does not implement `BulkRemovableIO`. Custom IO implementations, including 
those registered with `io.Register`, must make `Remove` safe for concurrent use 
and synchronize any shared mutable state. The bulk deletion path is unchanged.
+- **Snapshot summaries report deletion vectors separately.** On v3 tables, DVs 
are counted under `added-dvs` and `removed-dvs` instead of 
`added-position-delete-files` and `removed-position-delete-files`. 
`added-files-size` and `removed-files-size` use each DV's 
`content_size_in_bytes` instead of its whole Puffin file, so they go down for 
commits with DVs. `total-files-size` is updated from these values. Partition 
summaries, commit reports and OTel metrics change the same way. Existing s [...]
 
 - **Partition and sort source accessors renamed.** Replace 
`PartitionField.SourceID()` and `SortField.SourceID()` with `FirstSourceID()`. 
Their behavior and serialized fields are unchanged.
 

Reply via email to