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 23eb1c983 perf(table): skip stats for inapplicable equality deletes 
(#2230)
23eb1c983 is described below

commit 23eb1c983e838480ff58f86ccaf3f204bdeeb58d
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 20 19:25:51 2026 +0200

    perf(table): skip stats for inapplicable equality deletes (#2230)
    
    * perf(table): skip stats for inapplicable equality deletes
    
    * style(table): fix lint findings
---
 table/equality_delete_index.go               |   7 +
 table/equality_delete_sequence_guard_test.go | 190 +++++++++++++++++++++++++++
 2 files changed, 197 insertions(+)

diff --git a/table/equality_delete_index.go b/table/equality_delete_index.go
index 1a3f0287e..030fa6cc7 100644
--- a/table/equality_delete_index.go
+++ b/table/equality_delete_index.go
@@ -537,6 +537,13 @@ func (idx *equalityDeleteIndex) forDataFile(dataEntry 
iceberg.ManifestEntry) ([]
        }
 
        dataSeqNum := dataEntry.SequenceNum()
+       // Equality deletes are sorted by sequence within each list. When even
+       // the newest delete is not newer than the data file, no delete applies.
+       // Avoid initializing or copying column statistics in that common case.
+       if (len(idx.global) == 0 || 
idx.global[len(idx.global)-1].entry.SequenceNum() <= dataSeqNum) &&
+               (len(partitionEntries) == 0 || 
partitionEntries[len(partitionEntries)-1].entry.SequenceNum() <= dataSeqNum) {
+               return nil, nil
+       }
        var dataStats *equalityDeleteDataFileStats
        if idx.schema != nil {
                stats := equalityDeleteDataFileStatsFor(dataEntry.DataFile())
diff --git a/table/equality_delete_sequence_guard_test.go 
b/table/equality_delete_sequence_guard_test.go
new file mode 100644
index 000000000..2fd7e8fee
--- /dev/null
+++ b/table/equality_delete_sequence_guard_test.go
@@ -0,0 +1,190 @@
+// 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 (
+       "fmt"
+       "testing"
+
+       "github.com/apache/iceberg-go"
+       "github.com/stretchr/testify/require"
+)
+
+// This wrapper does not implement DataFileStatsRef. Any statistics access
+// falls back to the getters below, so a skipped lookup must not call them.
+type equalityDeleteStatsForbiddenFile struct {
+       iceberg.DataFile
+}
+
+func (equalityDeleteStatsForbiddenFile) ValueCounts() map[int]int64 {
+       panic("unused value counts")
+}
+
+func (equalityDeleteStatsForbiddenFile) NullValueCounts() map[int]int64 {
+       panic("unused null counts")
+}
+
+func (equalityDeleteStatsForbiddenFile) NaNValueCounts() map[int]int64 {
+       panic("unused NaN counts")
+}
+
+func (equalityDeleteStatsForbiddenFile) LowerBoundValues() map[int][]byte {
+       panic("unused lower bounds")
+}
+
+func (equalityDeleteStatsForbiddenFile) UpperBoundValues() map[int][]byte {
+       panic("unused upper bounds")
+}
+
+func TestEqualityDeleteIndexSkipsUnusedDataFileStatistics(t *testing.T) {
+       schema := equalityDeleteMetricsTestSchema(iceberg.PrimitiveTypes.Int32, 
true)
+       partA := map[int]any{1000: int32(1)}
+       partB := map[int]any{1000: int32(2)}
+       newDelete := func(path string, specID int32, partition map[int]any, seq 
int64) iceberg.ManifestEntry {
+               return newEqualityDeleteMetricsTestEntry(path, specID, 
partition,
+                       iceberg.EntryContentEqDeletes, seq, []int{1}, nil, nil, 
nil, nil, nil)
+       }
+       idx, err := buildEqualityDeleteIndex([]iceberg.ManifestEntry{
+               newDelete("delete-a.parquet", 1, partA, 2),
+               newDelete("delete-b.parquet", 1, partB, 10),
+       }, equalityDeleteIndexTestSpecs(), schema)
+       require.NoError(t, err)
+
+       for _, tc := range []struct {
+               name      string
+               partition map[int]any
+               seq       int64
+               skipStats bool
+               want      []string
+       }{
+               {name: "same sequence", partition: partA, seq: 2, skipStats: 
true},
+               {name: "newer than own partition", partition: partA, seq: 5, 
skipStats: true},
+               {name: "newer than all deletes", partition: partA, seq: 11, 
skipStats: true},
+               {name: "other partition", partition: map[int]any{1000: 
int32(3)}, seq: 1, skipStats: true},
+               {name: "older data", partition: partA, seq: 1, want: 
[]string{"delete-a.parquet"}},
+               {name: "later delete in own partition", partition: partB, seq: 
5, want: []string{"delete-b.parquet"}},
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       entry := 
newEqualityDeleteMetricsTestEntry("data.parquet", 1, tc.partition,
+                               iceberg.EntryContentData, tc.seq, nil, nil, 
nil, nil, nil, nil)
+                       if tc.skipStats {
+                               seq := entry.SequenceNum()
+                               entry = 
iceberg.NewManifestEntry(iceberg.EntryStatusADDED, nil, &seq, nil,
+                                       
equalityDeleteStatsForbiddenFile{entry.DataFile()})
+                       }
+
+                       matched, err := idx.forDataFile(entry)
+                       require.NoError(t, err)
+                       if tc.want == nil {
+                               require.Empty(t, matched)
+                       } else {
+                               require.Equal(t, tc.want, 
equalityDeleteMetricPaths(matched))
+                       }
+               })
+       }
+
+       t.Run("global delete still applies", func(t *testing.T) {
+               globalIdx, err := 
buildEqualityDeleteIndex([]iceberg.ManifestEntry{
+                       newDelete("delete-a.parquet", 1, partA, 2),
+                       newDelete("global.parquet", 0, nil, 8),
+               }, equalityDeleteIndexTestSpecs(), schema)
+               require.NoError(t, err)
+               data := newEqualityDeleteMetricsTestEntry("data.parquet", 1, 
partA,
+                       iceberg.EntryContentData, 5, nil, nil, nil, nil, nil, 
nil)
+               matched, err := globalIdx.forDataFile(data)
+               require.NoError(t, err)
+               require.Equal(t, []string{"global.parquet"}, 
equalityDeleteMetricPaths(matched))
+       })
+
+       t.Run("global delete older than data skips stats", func(t *testing.T) {
+               globalIdx, err := 
buildEqualityDeleteIndex([]iceberg.ManifestEntry{
+                       newDelete("delete-a.parquet", 1, partA, 2),
+                       newDelete("global.parquet", 0, nil, 3),
+               }, equalityDeleteIndexTestSpecs(), schema)
+               require.NoError(t, err)
+
+               data := newEqualityDeleteMetricsTestEntry("data.parquet", 1, 
partA,
+                       iceberg.EntryContentData, 5, nil, nil, nil, nil, nil, 
nil)
+               seq := data.SequenceNum()
+               entry := iceberg.NewManifestEntry(iceberg.EntryStatusADDED, 
nil, &seq, nil,
+                       equalityDeleteStatsForbiddenFile{data.DataFile()})
+               matched, err := globalIdx.forDataFile(entry)
+               require.NoError(t, err)
+               require.Empty(t, matched)
+       })
+
+       t.Run("invalid partition is still rejected", func(t *testing.T) {
+               data := newEqualityDeleteMetricsTestEntry("data.parquet", 1,
+                       map[int]any{1000: make(chan struct{})}, 
iceberg.EntryContentData,
+                       11, nil, nil, nil, nil, nil, nil)
+               _, err := idx.forDataFile(data)
+               require.Error(t, err)
+       })
+}
+
+func BenchmarkEqualityDeleteIndexSequenceSkip(b *testing.B) {
+       const fileCount = 1024
+       partA := map[int]any{1000: int32(1)}
+       partB := map[int]any{1000: int32(2)}
+       specs := equalityDeleteIndexTestSpecs()
+       schema := equalityDeleteMetricsTestSchema(iceberg.PrimitiveTypes.Int32, 
true)
+       deletes := []iceberg.ManifestEntry{
+               newEqualityDeleteMetricsTestEntry("delete-a.parquet", 1, partA,
+                       iceberg.EntryContentEqDeletes, 2, []int{1}, nil, nil, 
nil, nil, nil),
+               newEqualityDeleteMetricsTestEntry("delete-b.parquet", 1, partB,
+                       iceberg.EntryContentEqDeletes, 10, []int{1}, nil, nil, 
nil, nil, nil),
+       }
+       idx, err := buildEqualityDeleteIndex(deletes, specs, schema)
+       if err != nil {
+               b.Fatal(err)
+       }
+
+       for _, tc := range []struct {
+               name string
+               seq  int64
+       }{
+               {name: "no applicable deletes", seq: 5},
+               {name: "same delete sequence", seq: 2},
+               {name: "applicable delete", seq: 1},
+       } {
+               b.Run(tc.name, func(b *testing.B) {
+                       data := make([]iceberg.ManifestEntry, fileCount)
+                       for i := range data {
+                               data[i] = newEqualityDeleteMetricsTestEntry(
+                                       fmt.Sprintf("data-%d.parquet", i), 1, 
partA,
+                                       iceberg.EntryContentData, tc.seq, nil,
+                                       map[int]int64{1: 1}, map[int]int64{1: 
0},
+                                       nil, nil, nil)
+                       }
+                       b.ReportAllocs()
+                       b.ReportMetric(fileCount, "data_files")
+                       b.ResetTimer()
+                       for range b.N {
+                               matched := 0
+                               for _, entry := range data {
+                                       files, err := idx.forDataFile(entry)
+                                       if err != nil {
+                                               b.Fatal(err)
+                                       }
+                                       matched += len(files)
+                               }
+                               equalityDeleteBenchmarkSink = matched
+                       }
+               })
+       }
+}

Reply via email to