This is an automated email from the ASF dual-hosted git repository.
laskoviymishka 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 d785b279 fix(table): guard DV filtering against stale row counts
(#1624)
d785b279 is described below
commit d785b279495fb19c54fc370ad660633b13f5bb6b
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 5 20:10:59 2026 +0200
fix(table): guard DV filtering against stale row counts (#1624)
* fix(table): guard DV filtering against stale row counts
* refactor(table): share DV mask bounds logic
* perf(table): pass through rows beyond DV bounds
* chore(ci): retrigger integration checks
* test(table): cover stale DV row count boundaries
---
table/arrow_scanner.go | 52 +++++++++++++++++++++++++-------------
table/dv_scanner_read_test.go | 59 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 94 insertions(+), 17 deletions(-)
diff --git a/table/arrow_scanner.go b/table/arrow_scanner.go
index 027018f6..49819ea3 100644
--- a/table/arrow_scanner.go
+++ b/table/arrow_scanner.go
@@ -530,6 +530,10 @@ func processPositionalDeletes(ctx context.Context, deletes
set[int64], cursor *r
}
}
+func deletionVectorKeepsRow(keepBits []byte, rowCount, pos int64) bool {
+ return pos >= rowCount || keepBits[pos>>3]&(1<<(uint(pos)&7)) != 0
+}
+
// filterByDeletionVector returns a pipeline step that drops rows present in
// the bitmap by precomputing a bit-packed keep-mask covering the whole file
// once and slicing the relevant range per batch into
compute.FilterRecordBatch.
@@ -560,16 +564,37 @@ func filterByDeletionVector(ctx context.Context, bitmap
*dv.RoaringPositionBitma
currentIdx := nextIdx
nextIdx += nrows
- // Wrap (and slice) the shared keep-mask buffer for
this batch.
- // array.NewSlice on a Boolean array tracks the
bit-level offset,
- // so we don't need byte-aligned slicing — currentIdx
can land
- // anywhere within a byte.
- full := array.NewBoolean(int(rowCount), buf, nil, 0)
- defer full.Release()
- sliced := array.NewSlice(full, currentIdx,
nextIdx).(*array.Boolean)
- defer sliced.Release()
+ if nextIdx <= rowCount {
+ // Wrap (and slice) the shared keep-mask buffer
for this batch.
+ // array.NewSlice on a Boolean array tracks the
bit-level offset,
+ // so we don't need byte-aligned slicing —
currentIdx can land
+ // anywhere within a byte.
+ full := array.NewBoolean(int(rowCount), buf,
nil, 0)
+ defer full.Release()
+ sliced := array.NewSlice(full, currentIdx,
nextIdx).(*array.Boolean)
+ defer sliced.Release()
+
+ return compute.FilterRecordBatch(ctx, r,
sliced, compute.DefaultFilterOptions())
+ }
+ if currentIdx >= rowCount {
+ r.Retain()
+
+ return r, nil
+ }
+
+ // A stale manifest count can be smaller than the rows
emitted by
+ // the file. Preserve rows beyond the mask rather than
slicing past
+ // its bounds and panicking.
+ bldr := array.NewBooleanBuilder(mem)
+ defer bldr.Release()
+ bldr.Reserve(int(nrows))
+ for pos := currentIdx; pos < nextIdx; pos++ {
+ bldr.Append(deletionVectorKeepsRow(keepBits,
rowCount, pos))
+ }
+ mask := bldr.NewBooleanArray()
+ defer mask.Release()
- return compute.FilterRecordBatch(ctx, r, sliced,
compute.DefaultFilterOptions())
+ return compute.FilterRecordBatch(ctx, r, mask,
compute.DefaultFilterOptions())
}
bldr := array.NewBooleanBuilder(mem)
@@ -580,14 +605,7 @@ func filterByDeletionVector(ctx context.Context, bitmap
*dv.RoaringPositionBitma
// keepBits is sized for rowCount; a pos past it means
row-group
// metadata and the manifest's File.Count() disagree.
Keep the row
// rather than indexing out of bounds.
- if pos >= rowCount {
- bldr.Append(true)
-
- continue
- }
- // Test keep bit at absolute position pos: byte pos/8,
bit pos%8,
- // LSB-first (the layout the fast path's
array.NewBoolean reads).
- bldr.Append(keepBits[pos>>3]&(1<<(uint(pos)&7)) != 0)
+ bldr.Append(deletionVectorKeepsRow(keepBits, rowCount,
pos))
}
mask := bldr.NewBooleanArray()
defer mask.Release()
diff --git a/table/dv_scanner_read_test.go b/table/dv_scanner_read_test.go
index 54d3d4cf..b2453832 100644
--- a/table/dv_scanner_read_test.go
+++ b/table/dv_scanner_read_test.go
@@ -347,3 +347,62 @@ func TestFilterByDeletionVectorOutOfBoundsPosition(t
*testing.T) {
defer out.Release()
assert.Equal(t, []int64{0, 1, 2},
out.Column(0).(*array.Int64).Int64Values())
}
+
+func TestFilterByDeletionVectorStaleRowCount(t *testing.T) {
+ ctx := context.Background()
+ mem := memory.NewGoAllocator()
+
+ mkBatch := func(values ...int64) arrow.RecordBatch {
+ bldr := array.NewInt64Builder(mem)
+ defer bldr.Release()
+ bldr.AppendValues(values, nil)
+ col := bldr.NewArray()
+ defer col.Release()
+ schema := arrow.NewSchema([]arrow.Field{{Name: "pos", Type:
arrow.PrimitiveTypes.Int64}}, nil)
+
+ return array.NewRecordBatch(schema, []arrow.Array{col},
int64(len(values)))
+ }
+
+ bitmap := dv.NewRoaringPositionBitmap()
+ bitmap.Set(1)
+ bitmap.Set(3)
+ filter := filterByDeletionVector(ctx, bitmap, 4,
(&rowPositionSource{}).cursor())
+
+ withinCount, err := filter(mkBatch(0, 1))
+ require.NoError(t, err)
+ defer withinCount.Release()
+ assert.Equal(t, []int64{0},
withinCount.Column(0).(*array.Int64).Int64Values())
+
+ boundaryCount, err := filter(mkBatch(2, 3))
+ require.NoError(t, err)
+ defer boundaryCount.Release()
+ assert.Equal(t, []int64{2},
boundaryCount.Column(0).(*array.Int64).Int64Values())
+
+ straddlingFilter := filterByDeletionVector(ctx, bitmap, 4,
(&rowPositionSource{}).cursor())
+ straddlingPrefix, err := straddlingFilter(mkBatch(0, 1))
+ require.NoError(t, err)
+ defer straddlingPrefix.Release()
+ assert.Equal(t, []int64{0},
straddlingPrefix.Column(0).(*array.Int64).Int64Values())
+
+ straddlingCount, err := straddlingFilter(mkBatch(2, 3, 4, 5))
+ require.NoError(t, err)
+ defer straddlingCount.Release()
+ assert.Equal(t, []int64{2, 4, 5},
straddlingCount.Column(0).(*array.Int64).Int64Values())
+
+ boundaryFilter := filterByDeletionVector(ctx, bitmap, 4,
(&rowPositionSource{}).cursor())
+ boundaryBatch, err := boundaryFilter(mkBatch(0, 1, 2, 3))
+ require.NoError(t, err)
+ defer boundaryBatch.Release()
+ assert.Equal(t, []int64{0, 2},
boundaryBatch.Column(0).(*array.Int64).Int64Values())
+
+ afterBoundary, err := boundaryFilter(mkBatch(4, 5))
+ require.NoError(t, err)
+ defer afterBoundary.Release()
+ assert.Equal(t, []int64{4, 5},
afterBoundary.Column(0).(*array.Int64).Int64Values())
+
+ zeroRowCountFilter := filterByDeletionVector(ctx, bitmap, 0,
(&rowPositionSource{}).cursor())
+ zeroRowCount, err := zeroRowCountFilter(mkBatch(6, 7))
+ require.NoError(t, err)
+ defer zeroRowCount.Release()
+ assert.Equal(t, []int64{6, 7},
zeroRowCount.Column(0).(*array.Int64).Int64Values())
+}