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 557a041f1 perf(table): use range masks for contiguous positional 
deletes (#2227)
557a041f1 is described below

commit 557a041f18c566c68128d17a9b36c7b8617c00a7
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 20 22:16:35 2026 +0200

    perf(table): use range masks for contiguous positional deletes (#2227)
    
    * perf(table): use range masks for contiguous positional deletes
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * style(table): format range-mask test fields
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * perf(table): iterate surviving bits for dense positional deletes
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * test(table): cover dense positional deletes and bitmap boundaries
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * style(table): fix lint findings
    
    * style(table): gofmt positional delete range test
    
    * test(table): format positional delete range test
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
---
 table/arrow_scanner.go                      |  47 +++++++
 table/arrow_scanner_posdelete_range_test.go | 204 ++++++++++++++++++++++++++++
 2 files changed, 251 insertions(+)

diff --git a/table/arrow_scanner.go b/table/arrow_scanner.go
index f9ca7d6b2..92364b795 100644
--- a/table/arrow_scanner.go
+++ b/table/arrow_scanner.go
@@ -24,6 +24,8 @@ import (
        "io"
        "iter"
        "maps"
+       "math"
+       "math/bits"
        "slices"
        "strconv"
        "strings"
@@ -1348,6 +1350,51 @@ func combinePositionalDeleteBitmap(
        cursor *rowPositionCursor,
        nrows int64,
 ) arrow.Array {
+       // Unpruned batches occupy one contiguous file range. Query the bitmap 
once
+       // rather than doing a roaring lookup for every row, including clean 
batches.
+       // Advancing the cursor matters even when no rows are deleted.
+       if !cursor.src.pruned() && cursor.consumed >= 0 && nrows >= 0 &&
+               nrows <= math.MaxInt64-cursor.consumed {
+               start := cursor.consumed
+               cursor.consumed += nrows
+               mask := deletes.KeepMaskForRange(uint64(start), 
uint64(cursor.consumed))
+               if mask == nil {
+                       return nil
+               }
+
+               kept := 0
+               for _, value := range mask {
+                       kept += bits.OnesCount8(value)
+               }
+
+               bldr := array.NewInt64Builder(mem)
+               defer bldr.Release()
+               if kept > 0 {
+                       bldr.Resize(kept)
+               }
+               if kept <= int(nrows)/2 {
+                       // For dense deletes, visit surviving bits instead of 
every row.
+                       // KeepMaskForRange clears padding bits in the final 
byte.
+                       for byteIndex, keep := range mask {
+                               for keep != 0 {
+                                       bit := bits.TrailingZeros8(keep)
+                                       bldr.UnsafeAppend(int64(byteIndex*8 + 
bit))
+                                       keep &= keep - 1
+                               }
+                       }
+               } else {
+                       // Dense keep masks are faster with a straight row scan.
+                       for i := range nrows {
+                               if mask[i>>3]&(1<<uint(i&7)) != 0 {
+                                       bldr.UnsafeAppend(i)
+                               }
+                       }
+               }
+
+               return bldr.NewArray()
+       }
+
+       // Keep the row-by-row cursor for scans with skipped row groups.
        var bldr *array.Int64Builder
 
        for i := range nrows {
diff --git a/table/arrow_scanner_posdelete_range_test.go 
b/table/arrow_scanner_posdelete_range_test.go
new file mode 100644
index 000000000..a9716585c
--- /dev/null
+++ b/table/arrow_scanner_posdelete_range_test.go
@@ -0,0 +1,204 @@
+// 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 (
+       "math/rand"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/iceberg-go/table/dv"
+       tblutils "github.com/apache/iceberg-go/table/internal"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+// The previous per-row implementation is preserved here to measure the same
+// scan path and compare its output and cursor advancement against the range 
path.
+func combinePositionalDeleteBitmapBefore(
+       mem memory.Allocator,
+       deletes *dv.RoaringPositionBitmap,
+       cursor *rowPositionCursor,
+       nrows int64,
+) arrow.Array {
+       var bldr *array.Int64Builder
+
+       for i := range nrows {
+               if deletes.Contains(uint64(cursor.next())) {
+                       if bldr == nil {
+                               bldr = array.NewInt64Builder(mem)
+                               if i > 0 {
+                                       bldr.Resize(int(nrows - 1))
+                               }
+                               for j := range i {
+                                       bldr.UnsafeAppend(j)
+                               }
+                       }
+
+                       continue
+               }
+
+               if bldr != nil {
+                       if bldr.Cap() == 0 {
+                               bldr.Resize(int(nrows - i))
+                       }
+                       bldr.UnsafeAppend(i)
+               }
+       }
+       if bldr == nil {
+               return nil
+       }
+       defer bldr.Release()
+
+       return bldr.NewArray()
+}
+
+func TestCombinePositionalDeleteBitmapRangeParity(t *testing.T) {
+       const boundary uint64 = 1 << 32
+       cases := []struct {
+               name       string
+               start      int64
+               batches    []int64
+               positions  []uint64
+               rangeStart uint64
+               rangeEnd   uint64
+               spans      []tblutils.RowGroupSpan
+       }{
+               {name: "empty", batches: []int64{0, 0}},
+               {name: "clean across batches", batches: []int64{4, 64, 11}, 
positions: []uint64{1000}},
+               {name: "sparse across batches", batches: []int64{8, 65, 16}, 
positions: []uint64{1, 33, 88}},
+               {name: "all deleted", batches: []int64{3, 5}, rangeEnd: 8},
+               {name: "single surviving row", batches: []int64{9}, rangeStart: 
1, rangeEnd: 9},
+               {name: "non-byte-aligned dense deletes", batches: []int64{7, 
3}, rangeStart: 0, rangeEnd: 9},
+               {
+                       name:       "dense deletes across a 32-bit boundary",
+                       start:      int64(boundary - 4),
+                       batches:    []int64{8},
+                       rangeStart: boundary - 3,
+                       rangeEnd:   boundary + 4,
+               },
+               {name: "partially deleted", batches: []int64{65, 64}, 
rangeStart: 32, rangeEnd: 98},
+               {name: "crosses 32-bit buckets", start: int64(boundary - 4), 
batches: []int64{3, 8}, positions: []uint64{boundary - 3, boundary - 1, 
boundary, boundary + 5}},
+               {name: "high positions", start: 1 << 40, batches: []int64{3, 
8}, positions: []uint64{1<<40 + 1, 1<<40 + 10}},
+               {
+                       name:    "pruned row groups",
+                       batches: []int64{4, 4},
+                       spans: []tblutils.RowGroupSpan{
+                               {FirstRowPos: 10, NumRows: 4},
+                               {FirstRowPos: int64(boundary), NumRows: 4},
+                       },
+                       positions: []uint64{11, boundary + 1, boundary + 3},
+               },
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       bitmap := dv.NewRoaringPositionBitmap()
+                       for _, pos := range tc.positions {
+                               bitmap.Set(pos)
+                       }
+                       if tc.rangeEnd > tc.rangeStart {
+                               bitmap.SetRange(tc.rangeStart, tc.rangeEnd)
+                       }
+                       checkPosDeleteRangeParity(t, bitmap, tc.start, 
tc.spans, tc.batches)
+               })
+       }
+}
+
+func TestCombinePositionalDeleteBitmapRangeRandomized(t *testing.T) {
+       rng := rand.New(rand.NewSource(1020))
+       for i := range 200 {
+               start := []int64{0, 73, 1<<32 - 32, 1 << 40}[i%4]
+               nrows := int64(rng.Intn(256))
+               bitmap := dv.NewRoaringPositionBitmap()
+               for range rng.Intn(128) {
+                       bitmap.Set(uint64(start) + 
uint64(rng.Intn(int(nrows)+3)))
+               }
+               checkPosDeleteRangeParity(t, bitmap, start, nil, []int64{nrows 
/ 2, nrows - nrows/2})
+       }
+}
+
+func checkPosDeleteRangeParity(t *testing.T, bitmap *dv.RoaringPositionBitmap,
+       start int64, spans []tblutils.RowGroupSpan, batches []int64,
+) {
+       t.Helper()
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       source := &rowPositionSource{spans: spans}
+       gotCursor, wantCursor := source.cursor(), source.cursor()
+       gotCursor.consumed, wantCursor.consumed = start, start
+
+       for _, nrows := range batches {
+               got := combinePositionalDeleteBitmap(mem, bitmap, gotCursor, 
nrows)
+               want := combinePositionalDeleteBitmapBefore(mem, bitmap, 
wantCursor, nrows)
+               if want == nil {
+                       require.Nil(t, got)
+               } else {
+                       require.NotNil(t, got)
+                       assert.Equal(t, want.(*array.Int64).Int64Values(), 
got.(*array.Int64).Int64Values())
+                       got.Release()
+                       want.Release()
+               }
+               assert.Equal(t, wantCursor.consumed, gotCursor.consumed)
+               assert.Equal(t, wantCursor.spanIdx, gotCursor.spanIdx)
+       }
+}
+
+var positionalDeleteRangeBenchmarkSink int64
+
+func BenchmarkCombinePositionalDeleteBitmapRange(b *testing.B) {
+       const nrows int64 = 65_536
+       for _, tc := range []struct {
+               name  string
+               setup func(*dv.RoaringPositionBitmap)
+       }{
+               {name: "clean", setup: func(bm *dv.RoaringPositionBitmap) { 
bm.Set(uint64(nrows + 100)) }},
+               {name: "sparse", setup: func(bm *dv.RoaringPositionBitmap) {
+                       bm.Set(1)
+                       bm.Set(uint64(nrows - 1))
+               }},
+               {name: "half", setup: func(bm *dv.RoaringPositionBitmap) { 
bm.SetRange(0, uint64(nrows/2)) }},
+               {name: "all", setup: func(bm *dv.RoaringPositionBitmap) { 
bm.SetRange(0, uint64(nrows)) }},
+       } {
+               b.Run(tc.name, func(b *testing.B) {
+                       bitmap := dv.NewRoaringPositionBitmap()
+                       tc.setup(bitmap)
+                       for _, bench := range []struct {
+                               name string
+                               fn   func(memory.Allocator, 
*dv.RoaringPositionBitmap, *rowPositionCursor, int64) arrow.Array
+                       }{
+                               {name: "before", fn: 
combinePositionalDeleteBitmapBefore},
+                               {name: "after", fn: 
combinePositionalDeleteBitmap},
+                       } {
+                               b.Run(bench.name, func(b *testing.B) {
+                                       b.ReportAllocs()
+                                       b.ResetTimer()
+                                       for b.Loop() {
+                                               cursor := 
(&rowPositionSource{}).cursor()
+                                               indices := 
bench.fn(memory.DefaultAllocator, bitmap, cursor, nrows)
+                                               if indices != nil {
+                                                       
positionalDeleteRangeBenchmarkSink = int64(indices.Len())
+                                                       indices.Release()
+                                               }
+                                       }
+                               })
+                       }
+               })
+       }
+}

Reply via email to