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 912383979 perf(table): fast-path single int64 equality deletes (#2157)
912383979 is described below
commit 9123839791c3e28277fc87848c4555bfc253db68
Author: Minh Vu <[email protected]>
AuthorDate: Sun Oct 11 21:22:26 2026 +0200
perf(table): fast-path single int64 equality deletes (#2157)
---
table/equality_delete_reader.go | 179 ++++++++++++++++---
table/equality_delete_reader_bench_test.go | 108 ++++++++++++
table/equality_delete_reader_internal_test.go | 244 ++++++++++++++++++++++++++
3 files changed, 509 insertions(+), 22 deletions(-)
diff --git a/table/equality_delete_reader.go b/table/equality_delete_reader.go
index 400965701..e2f75f637 100644
--- a/table/equality_delete_reader.go
+++ b/table/equality_delete_reader.go
@@ -51,12 +51,18 @@ var ErrConflictingEqualityDeleteMetadata =
errors.New("conflicting equality dele
// equalityDeleteSet holds the set of delete keys and the column names
// used to look them up in data records. Each set corresponds to one
// group of equality field IDs — delete files with different field IDs
-// produce separate sets. The set is immutable after construction so it
-// can be shared by tasks with the same delete files.
+// produce separate sets. Key material and metadata are immutable after
+// construction; derived typed indexes are built once and shared safely.
+// Typed indexes retain additional map storage alongside keys for the lifetime
+// of the set. Merged groups can hold their own indexes on top of per-file
ones.
+// Do not copy the set after the first use of an index.
type equalityDeleteSet struct {
keys set[string]
fieldIDs []int
colNames []string
+
+ singleInt64Once sync.Once
+ singleInt64 *singleInt64EqualityDeleteSet
}
type arrowFieldRef struct {
@@ -1288,6 +1294,125 @@ func makeColEncoder(arr arrow.Array) colEncoder {
}
}
+type singleInt64EqualityDeleteSet struct {
+ keys set[uint64]
+ hasNull bool
+}
+
+func (e *equalityDeleteSet) singleInt64Set(fileSchema *iceberg.Schema)
*singleInt64EqualityDeleteSet {
+ if fileSchema == nil || len(e.fieldIDs) != 1 {
+ return nil
+ }
+
+ field, ok := fileSchema.FindFieldByID(e.fieldIDs[0])
+ if !ok {
+ return nil
+ }
+ if _, ok := field.Type.(iceberg.Int64Type); !ok {
+ return nil
+ }
+
+ e.singleInt64Once.Do(func() {
+ result := &singleInt64EqualityDeleteSet{
+ keys: make(set[uint64], len(e.keys)),
+ }
+ for key := range e.keys {
+ if len(key) == 1 && key[0] == 0 {
+ result.hasNull = true
+
+ continue
+ }
+ if len(key) != 9 || key[0] != 1 {
+ // Unexpected key formats must keep using the
generic path.
+ return
+ }
+
+ value := uint64(key[1])<<56 |
+ uint64(key[2])<<48 |
+ uint64(key[3])<<40 |
+ uint64(key[4])<<32 |
+ uint64(key[5])<<24 |
+ uint64(key[6])<<16 |
+ uint64(key[7])<<8 |
+ uint64(key[8])
+ result.keys[value] = struct{}{}
+ }
+
+ e.singleInt64 = result
+ })
+
+ return e.singleInt64
+}
+
+type equalityDeleteMask struct {
+ buffer *memory.Buffer
+ bits []byte
+}
+
+func (m *equalityDeleteMask) ensure(mem memory.Allocator, numRows int) {
+ if m.buffer != nil {
+ return
+ }
+
+ m.buffer = memory.NewResizableBuffer(mem)
+ m.buffer.Resize(int(bitutil.BytesForBits(int64(numRows))))
+ m.bits = m.buffer.Bytes()
+ for i := range m.bits {
+ m.bits[i] = 0xFF
+ }
+}
+
+func (m *equalityDeleteMask) release() {
+ if m.buffer != nil {
+ m.buffer.Release()
+ }
+}
+
+// applySingleInt64 returns false for unexpected Arrow column types, allowing
+// the caller to fall back to the generic field encoder.
+func (m *equalityDeleteMask) applySingleInt64(
+ column arrow.Array,
+ deleteSet *singleInt64EqualityDeleteSet,
+ mem memory.Allocator,
+) bool {
+ values, ok := column.(*array.Int64)
+ if !ok {
+ return false
+ }
+
+ rawValues := values.Int64Values()
+ numRows := values.Len()
+ if values.NullN() == 0 {
+ for row := range numRows {
+ if m.bits != nil && !bitutil.BitIsSet(m.bits, row) {
+ continue
+ }
+ if _, deleted :=
deleteSet.keys[uint64(rawValues[row])]; deleted {
+ m.ensure(mem, numRows)
+ bitutil.ClearBit(m.bits, row)
+ }
+ }
+ } else {
+ for row := range numRows {
+ if m.bits != nil && !bitutil.BitIsSet(m.bits, row) {
+ continue
+ }
+
+ if values.IsNull(row) {
+ if !deleteSet.hasNull {
+ continue
+ }
+ } else if _, deleted :=
deleteSet.keys[uint64(rawValues[row])]; !deleted {
+ continue
+ }
+ m.ensure(mem, numRows)
+ bitutil.ClearBit(m.bits, row)
+ }
+ }
+
+ return true
+}
+
// processEqualityDeletesColumnarForFile resolves field paths once per file and
// typed column encoders once per batch, then iterates rows without per-row
type
// switches. Each delete set is applied independently because sets may have
@@ -1323,18 +1448,38 @@ func processEqualityDeletesColumnarForFile(ctx
context.Context, eqDeleteSets []*
}
}
+ singleInt64Sets := make([]*singleInt64EqualityDeleteSet,
len(eqDeleteSets))
+ for i, eqDel := range eqDeleteSets {
+ // Nested fields need parent validity checks and use the
generic path.
+ // Do not duplicate their encoded keys in an index that cannot
be used.
+ if len(fieldRefs[i]) == 1 && len(fieldRefs[i][0].path) == 1 {
+ singleInt64Sets[i] = eqDel.singleInt64Set(fileSchema)
+ }
+ }
+
return func(r arrow.RecordBatch) (arrow.RecordBatch, error) {
defer r.Release()
mem := compute.GetAllocator(ctx)
numRows := int(r.NumRows())
- var maskBuf *memory.Buffer
- var maskBytes []byte
+ var mask equalityDeleteMask
+ defer mask.release()
var keyBuf bytes.Buffer
for setIdx, eqDel := range eqDeleteSets {
+ if int64Set := singleInt64Sets[setIdx]; int64Set != nil
{
+ column, _, err := arrowArraysAtField(
+ r, fieldRefs[setIdx][0],
eqDel.fieldIDs[0], eqDel.colNames[0], dataFilePath)
+ if err != nil {
+ return nil, err
+ }
+ if mask.applySingleInt64(column, int64Set, mem)
{
+ continue
+ }
+ }
+
encoders := make([]colEncoder, len(eqDel.colNames))
for i, name := range eqDel.colNames {
var err error
@@ -1345,7 +1490,7 @@ func processEqualityDeletesColumnarForFile(ctx
context.Context, eqDeleteSets []*
}
for row := range numRows {
- if maskBytes != nil &&
!bitutil.BitIsSet(maskBytes, row) {
+ if mask.bits != nil &&
!bitutil.BitIsSet(mask.bits, row) {
continue
}
@@ -1359,22 +1504,12 @@ func processEqualityDeletesColumnarForFile(ctx
context.Context, eqDeleteSets []*
continue
}
- if maskBuf == nil {
- maskBuf = memory.NewResizableBuffer(mem)
- defer maskBuf.Release()
-
maskBuf.Resize(int(bitutil.BytesForBits(int64(numRows))))
- maskBytes = maskBuf.Bytes()
-
- for i := range maskBytes {
- maskBytes[i] = 0xFF
- }
- }
-
- bitutil.ClearBit(maskBytes, row)
+ mask.ensure(mem, numRows)
+ bitutil.ClearBit(mask.bits, row)
}
}
- if maskBuf == nil {
+ if mask.buffer == nil {
r.Retain()
return r, nil
@@ -1382,14 +1517,14 @@ func processEqualityDeletesColumnarForFile(ctx
context.Context, eqDeleteSets []*
maskData := array.NewData(
arrow.FixedWidthTypes.Boolean, numRows,
- []*memory.Buffer{nil, maskBuf}, nil, 0, 0)
- mask := array.NewBooleanData(maskData)
+ []*memory.Buffer{nil, mask.buffer}, nil, 0, 0)
+ filterMask := array.NewBooleanData(maskData)
maskData.Release()
- defer mask.Release()
+ defer filterMask.Release()
filtered, err := compute.Filter(ctx,
compute.NewDatumWithoutOwning(r),
- compute.NewDatumWithoutOwning(mask),
+ compute.NewDatumWithoutOwning(filterMask),
*compute.DefaultFilterOptions())
if err != nil {
return nil, err
diff --git a/table/equality_delete_reader_bench_test.go
b/table/equality_delete_reader_bench_test.go
index bcdb536a7..4e55edc5d 100644
--- a/table/equality_delete_reader_bench_test.go
+++ b/table/equality_delete_reader_bench_test.go
@@ -528,6 +528,114 @@ func buildBenchDeleteSetStringNoMatch(numDeletes int)
*equalityDeleteSet {
}
}
+func buildBenchDeleteSetSingleInt64(numDeletes int) *equalityDeleteSet {
+ keys := make(set[string], numDeletes)
+ var buf bytes.Buffer
+
+ for i := range numDeletes {
+ buf.Reset()
+ buf.WriteByte(1)
+ bufPutUint64(&buf, uint64(int64(i)*3))
+ keys[buf.String()] = struct{}{}
+ }
+
+ return &equalityDeleteSet{
+ keys: keys,
+ fieldIDs: []int{1},
+ colNames: []string{"id"},
+ }
+}
+
+func buildBenchDeleteSetSingleInt64NoMatch(numDeletes int) *equalityDeleteSet {
+ keys := make(set[string], numDeletes)
+ var buf bytes.Buffer
+
+ for i := range numDeletes {
+ buf.Reset()
+ buf.WriteByte(1)
+ bufPutUint64(&buf, uint64(-int64(i)-1))
+ keys[buf.String()] = struct{}{}
+ }
+
+ return &equalityDeleteSet{
+ keys: keys,
+ fieldIDs: []int{1},
+ colNames: []string{"id"},
+ }
+}
+
+func buildBenchRecordNullableInt(mem memory.Allocator, numRows int)
arrow.RecordBatch {
+ schema := arrow.NewSchema([]arrow.Field{
+ {Name: "id", Type: arrow.PrimitiveTypes.Int64, Nullable: true},
+ {Name: "category", Type: arrow.PrimitiveTypes.Int64},
+ }, nil)
+ builder := array.NewRecordBuilder(mem, schema)
+ defer builder.Release()
+ ids := builder.Field(0).(*array.Int64Builder)
+ categories := builder.Field(1).(*array.Int64Builder)
+
+ for i := range numRows {
+ // Keep 25% of rows null to exercise the null-aware lookup loop.
+ if i%4 == 0 {
+ ids.AppendNull()
+ } else {
+ ids.Append(int64(i))
+ }
+ categories.Append(int64(i % 100))
+ }
+
+ return builder.NewRecordBatch()
+}
+
+func benchSingleInt64EqualityDeletes(
+ b *testing.B,
+ buildRec func(memory.Allocator, int) arrow.RecordBatch,
+ buildDel func(int) *equalityDeleteSet,
+) {
+ b.Helper()
+
+ for _, benchmark := range []struct {
+ name string
+ fast bool
+ }{
+ {name: "generic"},
+ {name: "fast", fast: true},
+ } {
+ b.Run(benchmark.name, func(b *testing.B) {
+ benchEqDeletesForFile(b, buildRec, func(numDeletes int)
*equalityDeleteSet {
+ deleteSet := buildDel(numDeletes)
+ if !benchmark.fast {
+ // Keep the same schema and keys while
disabling the optional index.
+ deleteSet.singleInt64Once.Do(func() {})
+ }
+
+ return deleteSet
+ }, benchIntFileSchema())
+ })
+ }
+}
+
+func BenchmarkProcessEqualityDeletesSingleInt64(b *testing.B) {
+ benchSingleInt64EqualityDeletes(b, buildBenchRecordInt,
buildBenchDeleteSetSingleInt64)
+}
+
+func BenchmarkProcessEqualityDeletesSingleInt64NoMatch(b *testing.B) {
+ benchSingleInt64EqualityDeletes(b, buildBenchRecordInt,
buildBenchDeleteSetSingleInt64NoMatch)
+}
+
+func BenchmarkProcessEqualityDeletesSingleInt64NullsRetained(b *testing.B) {
+ benchSingleInt64EqualityDeletes(b, buildBenchRecordNullableInt,
buildBenchDeleteSetSingleInt64)
+}
+
+func BenchmarkProcessEqualityDeletesSingleInt64NullsDeleted(b *testing.B) {
+ benchSingleInt64EqualityDeletes(b, buildBenchRecordNullableInt,
func(numDeletes int) *equalityDeleteSet {
+ deleteSet := buildBenchDeleteSetSingleInt64(numDeletes)
+ deleteSet.keys[string([]byte{0})] = struct{}{}
+
+ return deleteSet
+ })
+}
+
func BenchmarkProcessEqualityDeletesInt(b *testing.B) {
benchEqDeletes(b, buildBenchRecordInt, buildBenchDeleteSetInt,
benchIntFileSchema())
}
diff --git a/table/equality_delete_reader_internal_test.go
b/table/equality_delete_reader_internal_test.go
index 0368e7fd8..3870bba85 100644
--- a/table/equality_delete_reader_internal_test.go
+++ b/table/equality_delete_reader_internal_test.go
@@ -22,6 +22,7 @@ import (
"context"
"errors"
"fmt"
+ "math"
"os"
"path/filepath"
"strings"
@@ -864,6 +865,249 @@ func
TestProcessEqualityDeletesReturnsOriginalBatchWhenNoRowsMatch(t *testing.T)
result.Release()
}
+func TestProcessEqualityDeletesSingleInt64MatchesGeneric(t *testing.T) {
+ fileSchema := iceberg.NewSchema(0,
+ iceberg.NestedField{ID: 1, Name: "id", Type:
iceberg.PrimitiveTypes.Int64},
+ )
+ schema := arrow.NewSchema([]arrow.Field{{
+ Name: "id", Type: arrow.PrimitiveTypes.Int64, Nullable: true,
+ }}, nil)
+ values := []int64{math.MinInt64, math.MaxInt64, -1, 0, 1, 7, 0, 42}
+ deleteValues := []int64{math.MinInt64, math.MaxInt64, -1, 0}
+ valid := []bool{true, true, true, false, true, true, false, true}
+
+ for _, tt := range []struct {
+ name string
+ valid []bool
+ deletes []int64
+ hasNull bool
+ wantRows int64
+ wantNulls int
+ }{
+ {name: "integer extremes", deletes: deleteValues, wantRows: 3},
+ {name: "nulls retained", valid: valid, deletes: deleteValues,
wantRows: 5, wantNulls: 2},
+ {name: "nulls deleted", valid: valid, deletes: deleteValues,
hasNull: true, wantRows: 3},
+ {name: "no matches", valid: valid, deletes: []int64{99},
wantRows: 8, wantNulls: 2},
+ {name: "empty set", valid: valid, wantRows: 8, wantNulls: 2},
+ } {
+ for _, offset := range []int{0, 3, 9} {
+ t.Run(fmt.Sprintf("%s/offset=%d", tt.name, offset),
func(t *testing.T) {
+ mem :=
memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+ ctx := compute.WithAllocator(t.Context(), mem)
+
+ builder := array.NewInt64Builder(mem)
+ builder.AppendValues(make([]int64, offset), nil)
+ builder.AppendValues(values, tt.valid)
+ full := builder.NewArray()
+ builder.Release()
+ sliced := array.NewSlice(full, int64(offset),
int64(offset+len(values)))
+ full.Release()
+ record := array.NewRecordBatch(schema,
[]arrow.Array{sliced}, int64(len(values)))
+ sliced.Release()
+ defer record.Release()
+
+ deleteSet :=
int64EqualityDeleteSet(tt.deletes...)
+ if tt.hasNull {
+ deleteSet.keys[string([]byte{0})] =
struct{}{}
+ }
+ genericSet := &equalityDeleteSet{
+ keys: deleteSet.keys, fieldIDs:
deleteSet.fieldIDs, colNames: deleteSet.colNames,
+ }
+ // Disable only the optional index, keeping the
same schema and keys.
+ genericSet.singleInt64Once.Do(func() {})
+ generic, err :=
processEqualityDeletesColumnarForFile(ctx,
+ []*equalityDeleteSet{genericSet},
fileSchema, "data.parquet")
+ require.NoError(t, err)
+ require.Nil(t, genericSet.singleInt64)
+ record.Retain()
+ want, err := generic(record)
+ require.NoError(t, err)
+ defer want.Release()
+
+ fast, err :=
processEqualityDeletesColumnarForFile(ctx,
+ []*equalityDeleteSet{deleteSet},
fileSchema, "data.parquet")
+ require.NoError(t, err)
+ require.NotNil(t, deleteSet.singleInt64, "the
typed path must be exercised")
+ record.Retain()
+ got, err := fast(record)
+ require.NoError(t, err)
+ defer got.Release()
+
+ assert.True(t, array.RecordEqual(want, got),
"generic: %s; fast: %s", want, got)
+ assert.Equal(t, tt.wantRows, got.NumRows())
+ assert.Equal(t, tt.wantNulls,
got.Column(0).NullN())
+ if tt.wantRows == record.NumRows() {
+ assert.Same(t, record, want)
+ assert.Same(t, record, got)
+ }
+ })
+ }
+ }
+}
+
+func TestProcessEqualityDeletesCombinesFastAndGenericSets(t *testing.T) {
+ for _, nullable := range []bool{false, true} {
+ for _, genericFirst := range []bool{false, true} {
+ t.Run(fmt.Sprintf("nullable=%t/generic_first=%t",
nullable, genericFirst), func(t *testing.T) {
+ mem :=
memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+ ctx := compute.WithAllocator(t.Context(), mem)
+
+ schema := arrow.NewSchema([]arrow.Field{
+ {Name: "id", Type:
arrow.PrimitiveTypes.Int64, Nullable: true},
+ {Name: "category", Type:
arrow.BinaryTypes.String},
+ }, nil)
+ builder := array.NewRecordBuilder(mem, schema)
+ ids := builder.Field(0).(*array.Int64Builder)
+ ids.AppendValues([]int64{1, 2, 3, 4, 5, 6}, nil)
+ if nullable {
+ ids.AppendNull()
+ } else {
+ ids.Append(0)
+ }
+ ids.Append(7)
+
builder.Field(1).(*array.StringBuilder).AppendValues(
+ []string{"keep", "drop", "drop",
"keep", "keep", "drop", "keep", "keep"}, nil)
+ record := builder.NewRecordBatch()
+ builder.Release()
+ defer record.Release()
+
+ fastSet := int64EqualityDeleteSet(1, 2, 5, 0)
+ fastSet.keys[string([]byte{0})] = struct{}{}
+ var key bytes.Buffer
+ encodeArrowValue(&key, record.Column(1), 1)
+ genericSet := &equalityDeleteSet{
+ keys: set[string]{key.String(): {}},
fieldIDs: []int{2}, colNames: []string{"category"},
+ }
+ deleteSets := []*equalityDeleteSet{fastSet,
genericSet}
+ if genericFirst {
+ deleteSets[0], deleteSets[1] =
deleteSets[1], deleteSets[0]
+ }
+ fileSchema := iceberg.NewSchema(0,
+ iceberg.NestedField{ID: 1, Name: "id",
Type: iceberg.PrimitiveTypes.Int64},
+ iceberg.NestedField{ID: 2, Name:
"category", Type: iceberg.PrimitiveTypes.String},
+ )
+ process, err :=
processEqualityDeletesColumnarForFile(ctx, deleteSets, fileSchema,
"data.parquet")
+ require.NoError(t, err)
+ require.NotNil(t, fastSet.singleInt64)
+ require.Nil(t, genericSet.singleInt64)
+
+ record.Retain()
+ result, err := process(record)
+ require.NoError(t, err)
+ defer result.Release()
+ require.Equal(t, int64(2), result.NumRows())
+ assert.Equal(t, []int64{4, 7},
result.Column(0).(*array.Int64).Int64Values())
+ })
+ }
+ }
+}
+
+func TestProcessEqualityDeletesSingleInt64FallsBackForUnexpectedKeys(t
*testing.T) {
+ for _, key := range []string{"", "\x01\x00",
"\x02\x00\x00\x00\x00\x00\x00\x00\x00"} {
+ t.Run(fmt.Sprintf("key=%x", key), func(t *testing.T) {
+ mem :=
memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+ ctx := compute.WithAllocator(t.Context(), mem)
+ builder := array.NewInt64Builder(mem)
+ builder.AppendValues([]int64{math.MinInt64, -1, 42},
nil)
+ values := builder.NewArray()
+ builder.Release()
+ schema := arrow.NewSchema([]arrow.Field{{Name: "id",
Type: arrow.PrimitiveTypes.Int64}}, nil)
+ record := array.NewRecordBatch(schema,
[]arrow.Array{values}, 3)
+ values.Release()
+ defer record.Release()
+
+ deleteSet := int64EqualityDeleteSet(math.MinInt64, -1)
+ deleteSet.keys[key] = struct{}{}
+ fileSchema := iceberg.NewSchema(0,
+ iceberg.NestedField{ID: 1, Name: "id", Type:
iceberg.PrimitiveTypes.Int64},
+ )
+ process, err :=
processEqualityDeletesColumnarForFile(ctx,
+ []*equalityDeleteSet{deleteSet}, fileSchema,
"data.parquet")
+ require.NoError(t, err)
+ require.Nil(t, deleteSet.singleInt64, "unexpected key
formats must use the generic path")
+ record.Retain()
+ result, err := process(record)
+ require.NoError(t, err)
+ defer result.Release()
+ assert.Equal(t, []int64{42},
result.Column(0).(*array.Int64).Int64Values())
+ })
+ }
+}
+
+func TestProcessEqualityDeletesReleasesMaskAfterLaterSetError(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+ ctx := compute.WithAllocator(t.Context(), mem)
+ builder := array.NewInt64Builder(mem)
+ builder.AppendValues([]int64{1, 2}, nil)
+ values := builder.NewArray()
+ builder.Release()
+ schema := arrow.NewSchema([]arrow.Field{{Name: "id", Type:
arrow.PrimitiveTypes.Int64}}, nil)
+ record := array.NewRecordBatch(schema, []arrow.Array{values}, 2)
+ values.Release()
+ defer record.Release()
+
+ fileSchema := iceberg.NewSchema(0,
+ iceberg.NestedField{ID: 1, Name: "id", Type:
iceberg.PrimitiveTypes.Int64},
+ iceberg.NestedField{ID: 2, Name: "missing", Type:
iceberg.PrimitiveTypes.String},
+ )
+ process, err := processEqualityDeletesColumnarForFile(ctx,
[]*equalityDeleteSet{
+ int64EqualityDeleteSet(1),
+ {fieldIDs: []int{2}, colNames: []string{"missing"}},
+ }, fileSchema, "data.parquet")
+ require.NoError(t, err)
+
+ record.Retain()
+ result, err := process(record)
+ require.ErrorContains(t, err, "equality field ID 2 (missing) not found
in data.parquet")
+ assert.Nil(t, result)
+}
+
+func TestProcessEqualityDeletesNestedInt64DoesNotBuildUnusedIndex(t
*testing.T) {
+ mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+ ctx := compute.WithAllocator(t.Context(), mem)
+
+ builder := array.NewInt64Builder(mem)
+ builder.AppendValues([]int64{123, 456, 789}, nil)
+ child := builder.NewArray()
+ builder.Release()
+ defer child.Release()
+ validity := memory.NewBufferBytes([]byte{0x06})
+ defer validity.Release()
+ parent, err := array.NewStructArrayWithNulls(
+ []arrow.Array{child}, []string{"id"}, validity, 1, 0)
+ require.NoError(t, err)
+
+ arrowSchema := arrow.NewSchema([]arrow.Field{{
+ Name: "person", Type: parent.DataType(), Nullable: true,
+ }}, nil)
+ record := array.NewRecordBatch(arrowSchema, []arrow.Array{parent}, 3)
+ parent.Release()
+
+ fileSchema := iceberg.NewSchema(0, iceberg.NestedField{
+ ID: 2, Name: "person", Type: &iceberg.StructType{FieldList:
[]iceberg.NestedField{
+ {ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int64,
Required: true},
+ }},
+ })
+ deleteSet := int64EqualityDeleteSet(456)
+ deleteSet.colNames[0] = "person.id"
+ deleteSet.keys[string([]byte{0})] = struct{}{}
+ process, err := processEqualityDeletesColumnarForFile(ctx,
+ []*equalityDeleteSet{deleteSet}, fileSchema, "nested.parquet")
+ require.NoError(t, err)
+ assert.Nil(t, deleteSet.singleInt64, "nested fields must not allocate
an unused typed index")
+
+ result, err := process(record)
+ require.NoError(t, err)
+ defer result.Release()
+ require.Equal(t, int64(1), result.NumRows())
+ assert.Equal(t, int64(789),
result.Column(0).(*array.Struct).Field(0).(*array.Int64).Value(0))
+}
+
func int64EqualityDeleteSet(values ...int64) *equalityDeleteSet {
keys := make(set[string], len(values))
for _, value := range values {