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 7fab843eb perf(table): precompute positional-delete path metrics 
(#2272)
7fab843eb is described below

commit 7fab843eb41390f66433c63f8410d05fc486b96b
Author: Minh Vu <[email protected]>
AuthorDate: Fri Oct 23 18:35:24 2026 +0200

    perf(table): precompute positional-delete path metrics (#2272)
    
    * perf(table): precompute positional-delete path metrics
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * test(table): use integer range in path benchmark
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * ci: retrigger flaky macOS race test
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * test(table): ensure cached positional-delete bounds own their bytes
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
---
 table/positional_delete_index.go      | 112 ++++++++++++++++++++++------------
 table/positional_delete_index_test.go |  57 +++++++++++++++++
 2 files changed, 131 insertions(+), 38 deletions(-)

diff --git a/table/positional_delete_index.go b/table/positional_delete_index.go
index b103291e4..fa358a692 100644
--- a/table/positional_delete_index.go
+++ b/table/positional_delete_index.go
@@ -18,7 +18,6 @@
 package table
 
 import (
-       "bytes"
        "cmp"
        "fmt"
        "slices"
@@ -32,8 +31,61 @@ import (
 // data-file lookup only visits deletes that can apply to that file.
 type positionalDeleteIndex struct {
        byPath             map[string][]iceberg.ManifestEntry
-       byPartition        map[string][]iceberg.ManifestEntry
-       byIntegerPartition 
map[positionalIntegerPartitionKey][]iceberg.ManifestEntry
+       byPartition        map[string][]positionalPartitionDeleteEntry
+       byIntegerPartition 
map[positionalIntegerPartitionKey][]positionalPartitionDeleteEntry
+}
+
+// positionalPartitionDeleteEntry caches the only path statistics needed for
+// matching a partition-scoped delete against many data files. Own the bound
+// strings rather than retaining borrowed DataFile metric byte slices.
+type positionalPartitionDeleteEntry struct {
+       entry       iceberg.ManifestEntry
+       pathMetrics positionalDeletePathMetrics
+}
+
+type positionalDeletePathMetrics struct {
+       lower    string
+       upper    string
+       hasUpper bool
+       skip     bool
+}
+
+func newPositionalPartitionDeleteEntry(entry iceberg.ManifestEntry) 
positionalPartitionDeleteEntry {
+       return positionalPartitionDeleteEntry{
+               entry:       entry,
+               pathMetrics: positionalDeletePathMetricsFor(entry.DataFile()),
+       }
+}
+
+func positionalDeletePathMetricsFor(deleteFile iceberg.DataFile) 
positionalDeletePathMetrics {
+       if deleteFile.Count() == 0 {
+               return positionalDeletePathMetrics{skip: true}
+       }
+
+       valueCounts, nullCounts, nanCounts, lowerBounds, upperBounds := 
dataFileStats(deleteFile)
+       if valueCount, ok := valueCounts[filePathFieldID]; ok {
+               if nullCount, ok := nullCounts[filePathFieldID]; ok && 
nullCount == valueCount {
+                       return positionalDeletePathMetrics{skip: true}
+               }
+               if nanCount, ok := nanCounts[filePathFieldID]; ok && nanCount 
== valueCount {
+                       return positionalDeletePathMetrics{skip: true}
+               }
+       }
+
+       var metrics positionalDeletePathMetrics
+       if lower := lowerBounds[filePathFieldID]; lower != nil {
+               metrics.lower = string(lower)
+       }
+       if upper := upperBounds[filePathFieldID]; upper != nil {
+               metrics.upper = string(upper)
+               metrics.hasUpper = true
+       }
+
+       return metrics
+}
+
+func (m positionalDeletePathMetrics) mayMatch(path string) bool {
+       return !m.skip && m.lower <= path && (!m.hasUpper || path <= m.upper)
 }
 
 // Most integer-backed partition transforms (bucket, day, hour, identity)
@@ -91,12 +143,13 @@ func buildPositionalDeleteIndex(entries 
[]iceberg.ManifestEntry) (*positionalDel
                        continue
                }
 
+               indexed := newPositionalPartitionDeleteEntry(entry)
                partition := dataFilePartition(deleteFile)
                if key, ok := singleIntegerPartitionKey(deleteFile.SpecID(), 
partition); ok {
                        if idx.byIntegerPartition == nil {
-                               idx.byIntegerPartition = 
make(map[positionalIntegerPartitionKey][]iceberg.ManifestEntry)
+                               idx.byIntegerPartition = 
make(map[positionalIntegerPartitionKey][]positionalPartitionDeleteEntry)
                        }
-                       idx.byIntegerPartition[key] = 
append(idx.byIntegerPartition[key], entry)
+                       idx.byIntegerPartition[key] = 
append(idx.byIntegerPartition[key], indexed)
 
                        continue
                }
@@ -106,9 +159,9 @@ func buildPositionalDeleteIndex(entries 
[]iceberg.ManifestEntry) (*positionalDel
                        return nil, fmt.Errorf("indexing positional delete file 
%s: %w", deleteFile.FilePath(), err)
                }
                if idx.byPartition == nil {
-                       idx.byPartition = 
make(map[string][]iceberg.ManifestEntry)
+                       idx.byPartition = 
make(map[string][]positionalPartitionDeleteEntry)
                }
-               idx.byPartition[partitionKey] = 
append(idx.byPartition[partitionKey], entry)
+               idx.byPartition[partitionKey] = 
append(idx.byPartition[partitionKey], indexed)
        }
 
        sortBySequence := func(entries []iceberg.ManifestEntry) {
@@ -119,11 +172,16 @@ func buildPositionalDeleteIndex(entries 
[]iceberg.ManifestEntry) (*positionalDel
        for _, pathEntries := range idx.byPath {
                sortBySequence(pathEntries)
        }
+       sortPartitionBySequence := func(entries 
[]positionalPartitionDeleteEntry) {
+               slices.SortStableFunc(entries, func(a, b 
positionalPartitionDeleteEntry) int {
+                       return cmp.Compare(a.entry.SequenceNum(), 
b.entry.SequenceNum())
+               })
+       }
        for _, partitionEntries := range idx.byPartition {
-               sortBySequence(partitionEntries)
+               sortPartitionBySequence(partitionEntries)
        }
        for _, partitionEntries := range idx.byIntegerPartition {
-               sortBySequence(partitionEntries)
+               sortPartitionBySequence(partitionEntries)
        }
 
        return idx, nil
@@ -138,7 +196,7 @@ func (idx *positionalDeleteIndex) forDataFile(dataEntry 
iceberg.ManifestEntry) (
        }
 
        dataFile := dataEntry.DataFile()
-       var partitionEntries []iceberg.ManifestEntry
+       var partitionEntries []positionalPartitionDeleteEntry
        if len(idx.byPartition) > 0 || len(idx.byIntegerPartition) > 0 {
                partition := dataFilePartition(dataFile)
                if key, ok := singleIntegerPartitionKey(dataFile.SpecID(), 
partition); ok {
@@ -166,21 +224,20 @@ func (idx *positionalDeleteIndex) forDataFile(dataEntry 
iceberg.ManifestEntry) (
 
 func appendPartitionDeletesFromSequence(
        out []iceberg.DataFile,
-       entries []iceberg.ManifestEntry,
+       entries []positionalPartitionDeleteEntry,
        dataSeqNum int64,
        dataFilePath string,
 ) ([]iceberg.DataFile, error) {
        start := sort.Search(len(entries), func(i int) bool {
-               return entries[i].SequenceNum() >= dataSeqNum
+               return entries[i].entry.SequenceNum() >= dataSeqNum
        })
        if start == len(entries) {
                return out, nil
        }
 
-       for _, entry := range entries[start:] {
-               deleteFile := entry.DataFile()
-               if filePathMayMatch(deleteFile, dataFilePath) {
-                       out = append(out, deleteFile)
+       for _, indexed := range entries[start:] {
+               if indexed.pathMetrics.mayMatch(dataFilePath) {
+                       out = append(out, indexed.entry.DataFile())
                }
        }
 
@@ -192,28 +249,7 @@ func appendPartitionDeletesFromSequence(
 // by the positional-delete index and keeps missing or malformed metadata
 // conservative.
 func filePathMayMatch(deleteFile iceberg.DataFile, dataFilePath string) bool {
-       if deleteFile.Count() == 0 {
-               return false
-       }
-
-       valueCounts, nullCounts, nanCounts, lowerBounds, upperBounds := 
dataFileStats(deleteFile)
-       if valueCount, ok := valueCounts[filePathFieldID]; ok {
-               if nullCount, ok := nullCounts[filePathFieldID]; ok && 
nullCount == valueCount {
-                       return false
-               }
-               if nanCount, ok := nanCounts[filePathFieldID]; ok && nanCount 
== valueCount {
-                       return false
-               }
-       }
-
-       if lower := lowerBounds[filePathFieldID]; lower != nil && 
bytes.Compare(lower, []byte(dataFilePath)) > 0 {
-               return false
-       }
-       if upper := upperBounds[filePathFieldID]; upper != nil && 
bytes.Compare(upper, []byte(dataFilePath)) < 0 {
-               return false
-       }
-
-       return true
+       return positionalDeletePathMetricsFor(deleteFile).mayMatch(dataFilePath)
 }
 
 func appendPositionalDeletesFromSequence(
diff --git a/table/positional_delete_index_test.go 
b/table/positional_delete_index_test.go
index d71ae9792..88478bce2 100644
--- a/table/positional_delete_index_test.go
+++ b/table/positional_delete_index_test.go
@@ -349,6 +349,21 @@ func 
TestPositionalDeleteIndexPrunesPartitionEntriesUsingPathMetrics(t *testing.
                positionalDeletePaths(matched))
 }
 
+func TestPositionalDeleteCachedBoundsOwnTheirBytes(t *testing.T) {
+       entry := newPositionalDeleteIndexTestEntryWithBounds(
+               "delete.parquet", 1, nil, 5, nil, "alpha", "omega",
+       )
+       file := entry.DataFile().(*positionalDeleteIndexTestFile)
+       metrics := positionalDeletePathMetricsFor(file)
+       assert.True(t, metrics.mayMatch("middle"))
+
+       // Metrics may borrow these byte slices. Cached bounds must not borrow
+       // them, because a caller can reuse the underlying metadata buffers.
+       file.lowerBounds[filePathFieldID][0] = 'z'
+       file.upperBounds[filePathFieldID][0] = 'a'
+       assert.True(t, metrics.mayMatch("middle"))
+}
+
 func TestFilePathMayMatch(t *testing.T) {
        const dataFilePath = "data-m.parquet"
 
@@ -478,6 +493,7 @@ func TestFilePathMayMatch(t *testing.T) {
                                upperBounds: upperBounds,
                        }
                        assert.Equal(t, tt.want, filePathMayMatch(file, 
dataFilePath))
+                       assert.Equal(t, tt.want, 
positionalDeletePathMetricsFor(file).mayMatch(dataFilePath))
                })
        }
 }
@@ -530,6 +546,47 @@ func 
TestPositionalDeleteIndexRejectsUnsupportedPartitionValue(t *testing.T) {
        assert.ErrorContains(t, err, "partition field 1000")
 }
 
+// BenchmarkPositionalDeleteIndexPartitionPathBounds exercises repeated 
data-file
+// lookups against a large partition bucket. The deleted file's bounds usually
+// disqualify nearly every candidate, but the index must remain conservative.
+func BenchmarkPositionalDeleteIndexPartitionPathBounds(b *testing.B) {
+       const numDeletes = 2048
+       const numDataFiles = 512
+
+       partition := map[int]any{1000: int32(7)}
+       deleteEntries := make([]iceberg.ManifestEntry, numDeletes)
+       for i := range deleteEntries {
+               deleteEntries[i] = newPositionalDeleteIndexTestEntryWithBounds(
+                       fmt.Sprintf("delete-%04d.parquet", i), 1, partition, 5, 
nil,
+                       fmt.Sprintf("s3://bucket/data-%06d.parquet", i*8),
+                       fmt.Sprintf("s3://bucket/data-%06d.parquet", i*8+23))
+               file := 
deleteEntries[i].DataFile().(*positionalDeleteIndexTestFile)
+               file.valueCounts = map[int]int64{filePathFieldID: 1}
+               file.nullCounts = map[int]int64{filePathFieldID: 0}
+       }
+       idx, err := buildPositionalDeleteIndex(deleteEntries)
+       if err != nil {
+               b.Fatal(err)
+       }
+
+       dataEntries := make([]iceberg.ManifestEntry, numDataFiles)
+       for i := range dataEntries {
+               dataEntries[i] = newPositionalDeleteIndexDataEntry(
+                       fmt.Sprintf("s3://bucket/data-%06d.parquet", i*32), 1, 
partition, 5)
+       }
+       b.ReportAllocs()
+       b.ResetTimer()
+       for i := range b.N {
+               matched, err := idx.forDataFile(dataEntries[i%numDataFiles])
+               if err != nil {
+                       b.Fatal(err)
+               }
+               if len(matched) == 0 {
+                       b.Fatal("expected matching deletes")
+               }
+       }
+}
+
 func positionalDeletePaths(files []iceberg.DataFile) []string {
        paths := make([]string, len(files))
        for i, file := range files {

Reply via email to