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 b1d1f7f83 perf(table): read only deletion vectors in merge-on-read 
deletes (#2196)
b1d1f7f83 is described below

commit b1d1f7f8398eff3d936737e32874edf1bd0a878f
Author: Badal Prasad Singh <[email protected]>
AuthorDate: Mon Oct 19 22:10:16 2026 +0530

    perf(table): read only deletion vectors in merge-on-read deletes (#2196)
    
    minor
    
    Signed-off-by: badalprasadsingh <[email protected]>
---
 table/transaction.go | 88 +++++++++++++++++++++++++++++++---------------------
 1 file changed, 53 insertions(+), 35 deletions(-)

diff --git a/table/transaction.go b/table/transaction.go
index a90c9f83b..3e9bac41f 100644
--- a/table/transaction.go
+++ b/table/transaction.go
@@ -2517,7 +2517,7 @@ func (t *Transaction) performCopyOnWriteDeletion(ctx 
context.Context, operation
        }
 
        for _, df := range filesToDelete {
-               deletes.removeDataFile(updater, df)
+               deletes.dvs.removeDataFile(updater, df)
        }
 
        if len(entriesToRewrite) > 0 {
@@ -2582,28 +2582,19 @@ func (t *Transaction) performMergeOnReadDeletion(ctx 
context.Context, snapshotPr
                return nil, err
        }
 
-       // SetProperties above may have staged a name mapping, which replaces 
t.meta, so re-read it.
-       meta, err = t.txnMeta()
-       if err != nil {
-               return nil, err
-       }
-
        // On v3 every removed file takes its deletion vector with it, and a 
partially deleted file's vector is
-       // merged into its replacement. One read serves both. DVs only exist 
from v3, so v2 skips it.
-       var deletes planningDeletes
+       // merged into its replacement. Both need only the deletion vectors, 
read once.
+       // DVs only exist from v3, so v2 skips the read.
+       var dvs liveDeletionVectors
        if meta.formatVersion >= 3 && (len(filesToDelete) > 0 || 
len(withPartialDeletions) > 0) {
-               builtMeta, err := meta.Build()
-               if err != nil {
-                       return nil, fmt.Errorf("failed to build metadata: %w", 
err)
-               }
-               deletes, err = t.readPlanningDeletes(fs, builtMeta)
+               dvs, err = t.readLiveDeletionVectors(fs)
                if err != nil {
                        return nil, err
                }
        }
 
        for _, df := range filesToDelete {
-               deletes.removeDataFile(updater, df)
+               dvs.removeDataFile(updater, df)
        }
 
        if len(withPartialDeletions) > 0 {
@@ -2611,7 +2602,7 @@ func (t *Transaction) performMergeOnReadDeletion(ctx 
context.Context, snapshotPr
                for _, entry := range withPartialDeletions {
                        partialFiles = append(partialFiles, entry.DataFile())
                }
-               referenced, err := t.writePositionDeletesForFiles(ctx, fs, 
updater, deletes.dvs, partialFiles, filter, caseSensitive, concurrency, 
commitUUID)
+               referenced, err := t.writePositionDeletesForFiles(ctx, fs, 
updater, dvs, partialFiles, filter, caseSensitive, concurrency, commitUUID)
                if err != nil {
                        return nil, err
                }
@@ -2903,24 +2894,35 @@ func (t *Transaction) 
classifyFilesForFilteredDeletions(ctx context.Context, fs
        return filesToDelete, filesWithPartialDeletes, nil
 }
 
+// maps a data file path to the live deletion vector that references it.
+type liveDeletionVectors map[string]iceberg.ManifestEntry
+
+// removeDataFile removes df and, as the spec requires, the deletion vector 
that references it.
+//
+// Removing a deletion vector makes the commit non-replayable (see 
snapshotProducer.commitManifests).
+// Position and equality delete files are left alone, since they can still 
apply to other files.
+func (dvs liveDeletionVectors) removeDataFile(updater *snapshotProducer, df 
iceberg.DataFile) {
+       updater.deleteDataFile(df)
+       if dv, ok := dvs[df.FilePath()]; ok {
+               updater.removeDeletionVector(dv.DataFile())
+       }
+}
+
 // planningDeletes indexes the live delete files of the planning snapshot the 
same way scan planning indexes them.
 type planningDeletes struct {
        positional *positionalDeleteIndex
-       dvs        map[string]iceberg.ManifestEntry
+       dvs        liveDeletionVectors
        equality   *equalityDeleteIndex
 }
 
-// readPlanningDeletes reads and indexes every live delete file of the 
planning snapshot.
-//
-// Delete manifests are not pruned by the row filter: with filter id=2 the 
rewrite keeps id=1,
-// so a delete on id=1 must still be applied.
+// classifyLiveDeletes reads every live delete file of the planning snapshot 
and sorts it by kind.
 //
 // Known limitation (#2160): every live delete manifest of the snapshot is 
read on each call,
 // with no partition pruning, so a narrow delete or rewrite on a delete-heavy 
table pays for all of them.
-func (t *Transaction) readPlanningDeletes(fs io.IO, builtMeta Metadata) 
(planningDeletes, error) {
+func (t *Transaction) classifyLiveDeletes(fs io.IO) 
(classifiedManifestEntries, error) {
        meta, err := t.txnMeta()
        if err != nil {
-               return planningDeletes{}, err
+               return classifiedManifestEntries{}, err
        }
 
        var liveDeletes []iceberg.ManifestEntry
@@ -2929,13 +2931,27 @@ func (t *Transaction) readPlanningDeletes(fs io.IO, 
builtMeta Metadata) (plannin
                // into a new DV would bring back deleted rows. See #1372.
                for entry, err := range s.entries(fs, 
iceberg.ManifestContentDeletes, true) {
                        if err != nil {
-                               return planningDeletes{}, fmt.Errorf("failed to 
read delete manifests: %w", err)
+                               return classifiedManifestEntries{}, 
fmt.Errorf("failed to read delete manifests: %w", err)
                        }
                        liveDeletes = append(liveDeletes, entry)
                }
        }
 
-       classified, err := classifyManifestEntries(liveDeletes)
+       return classifyManifestEntries(liveDeletes)
+}
+
+// readPlanningDeletes reads and indexes every live delete file of the 
planning snapshot. Copy-on-write
+// uses it because a rewrite must apply every kind of delete to the rows it 
keeps.
+//
+// Delete manifests are not pruned by the row filter: with filter id=2 the 
rewrite keeps id=1,
+// so a delete on id=1 must still be applied.
+func (t *Transaction) readPlanningDeletes(fs io.IO, builtMeta Metadata) 
(planningDeletes, error) {
+       meta, err := t.txnMeta()
+       if err != nil {
+               return planningDeletes{}, err
+       }
+
+       classified, err := t.classifyLiveDeletes(fs)
        if err != nil {
                return planningDeletes{}, err
        }
@@ -2955,14 +2971,16 @@ func (t *Transaction) readPlanningDeletes(fs io.IO, 
builtMeta Metadata) (plannin
        return planningDeletes{positional: positional, dvs: dvs, equality: 
equality}, nil
 }
 
-// removeDataFile removes df and, as the spec requires, the deletion vector 
that references it.
-// Removing a deletion vector makes the commit non-replayable (see 
snapshotProducer.commitManifests).
-// Position and equality delete files are left alone, since they can still 
apply to other files.
-func (d planningDeletes) removeDataFile(updater *snapshotProducer, df 
iceberg.DataFile) {
-       updater.deleteDataFile(df)
-       if dv, ok := d.dvs[df.FilePath()]; ok {
-               updater.removeDeletionVector(dv.DataFile())
+// readLiveDeletionVectors returns the live deletion vectors of the planning 
snapshot. Merge-on-read uses it
+// because it only drops or merges deletion vectors, so it skips building the 
position and equality delete
+// indexes and neither pays for them nor fails on them.
+func (t *Transaction) readLiveDeletionVectors(fs io.IO) (liveDeletionVectors, 
error) {
+       classified, err := t.classifyLiveDeletes(fs)
+       if err != nil {
+               return nil, err
        }
+
+       return buildDVIndex(classified.dvEntries)
 }
 
 // rewriteFilesWithFilter rewrites data files by preserving only rows that do 
NOT match the filter.
@@ -3011,7 +3029,7 @@ func (t *Transaction) rewriteFilesWithFilter(ctx 
context.Context, fs io.IO, upda
                        return fmt.Errorf("failed to rewrite file %s: %w", 
task.File.FilePath(), err)
                }
 
-               deletes.removeDataFile(updater, task.File)
+               deletes.dvs.removeDataFile(updater, task.File)
                for _, rewrittenFile := range rewrittenFiles {
                        updater.appendDataFile(rewrittenFile)
                }
@@ -3274,7 +3292,7 @@ func (t *Transaction) rewriteSingleFile(ctx 
context.Context, args rewriteSingleF
 // data-file paths those deletes reference. A position-delete that does not
 // record its referenced data file is omitted from the returned paths (it
 // cannot be existence-checked), matching RowDelta.validate and Java.
-func (t *Transaction) writePositionDeletesForFiles(ctx context.Context, fs 
io.IO, updater *snapshotProducer, dvs map[string]iceberg.ManifestEntry, files 
[]iceberg.DataFile, filter iceberg.BooleanExpression, caseSensitive bool, 
concurrency int, commitUUID uuid.UUID) ([]string, error) {
+func (t *Transaction) writePositionDeletesForFiles(ctx context.Context, fs 
io.IO, updater *snapshotProducer, dvs liveDeletionVectors, files 
[]iceberg.DataFile, filter iceberg.BooleanExpression, caseSensitive bool, 
concurrency int, commitUUID uuid.UUID) ([]string, error) {
        meta, err := t.txnMeta()
        if err != nil {
                return nil, err
@@ -3300,7 +3318,7 @@ func (t *Transaction) writePositionDeletesForFiles(ctx 
context.Context, fs io.IO
        // into the new DV (the incremental deletes) and the superseded DVs are
        // removed. Without this a second delete on the same file would write a
        // second live DV, corrupting the table (buildDVIndex rejects it).
-       // dvs holds the live DVs keyed by data file path, and is empty on v2.
+       // dvs is empty on v2.
        existingDVs := make(map[string]iceberg.DataFile)
        existingDVBitmaps := make(map[string]*dv.RoaringPositionBitmap)
        for _, df := range files {

Reply via email to