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 311cbf674 perf(table): bound partition metadata record batches (#2364)
311cbf674 is described below

commit 311cbf674923f2032b4711b67f681dc7cbbc12e2
Author: Minh Vu <[email protected]>
AuthorDate: Sat Oct 31 09:48:27 2026 +0100

    perf(table): bound partition metadata record batches (#2364)
---
 table/inspect_partitions.go            |  64 +++++++---
 table/inspect_partitions_batch_test.go | 206 +++++++++++++++++++++++++++++++++
 2 files changed, 251 insertions(+), 19 deletions(-)

diff --git a/table/inspect_partitions.go b/table/inspect_partitions.go
index 4bb492bcf..36c9e2120 100644
--- a/table/inspect_partitions.go
+++ b/table/inspect_partitions.go
@@ -153,27 +153,55 @@ func (i InspectTable) Partitions(ctx context.Context) 
(array.RecordReader, error
                return nil, fmt.Errorf("inspect partitions: %w", err)
        }
 
-       bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
-       defer bldr.Release()
-       for _, aggregate := range aggregates {
-               if err := appendPartitionAggregate(bldr, partitionType, 
aggregate); err != nil {
-                       return nil, fmt.Errorf("inspect partitions: append 
partition: %w", err)
-               }
-       }
+       return i.partitionAggregateReader(ctx, arrowSchema, partitionType, 
aggregates), nil
+}
 
-       rr, err := singleBatchReader(arrowSchema, bldr)
-       if err != nil {
-               return nil, fmt.Errorf("inspect partitions: %w", err)
-       }
+// partitionAggregateReader encodes at most inspectRecordBatchSize partitions 
per
+// Arrow batch. Aggregate discovery is eager, but the Arrow buffers are bounded
+// and released as each record is consumed.
+func (i InspectTable) partitionAggregateReader(
+       ctx context.Context,
+       arrowSchema *arrow.Schema,
+       partitionType *iceberg.StructType,
+       aggregates []*inspectPartitionAggregate,
+) array.RecordReader {
+       return array.ReaderFromIter(arrowSchema, func(yield 
func(arrow.RecordBatch, error) bool) {
+               bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
+               defer bldr.Release()
+
+               for index, aggregate := range aggregates {
+                       if index&1023 == 0 {
+                               if err := ctx.Err(); err != nil {
+                                       _ = yield(nil, err)
+
+                                       return
+                               }
+                       }
+                       if err := appendPartitionAggregate(bldr, partitionType, 
*aggregate); err != nil {
+                               _ = yield(nil, fmt.Errorf("inspect partitions: 
append partition: %w", err))
+
+                               return
+                       }
+                       if (index+1)%inspectRecordBatchSize == 0 {
+                               if !yield(bldr.NewRecordBatch(), nil) {
+                                       return
+                               }
+                       }
+               }
 
-       return rr, nil
+               // Match the prior single-batch API for empty tables, but do 
not emit
+               // an extra empty batch after an exact multiple of the batch 
size.
+               if len(aggregates) == 0 || 
len(aggregates)%inspectRecordBatchSize != 0 {
+                       _ = yield(bldr.NewRecordBatch(), nil)
+               }
+       })
 }
 
 // inspectPartitionManifestConcurrency bounds concurrent manifest reads and
 // per-manifest partition aggregation state.
 const inspectPartitionManifestConcurrency = 8
 
-func (i InspectTable) partitionAggregates(ctx context.Context, partitionType 
*iceberg.StructType) ([]inspectPartitionAggregate, error) {
+func (i InspectTable) partitionAggregates(ctx context.Context, partitionType 
*iceberg.StructType) ([]*inspectPartitionAggregate, error) {
        snapshot := i.tbl.Metadata().CurrentSnapshot()
        if snapshot == nil {
                return nil, nil
@@ -265,13 +293,11 @@ func (i InspectTable) partitionAggregates(ctx 
context.Context, partitionType *ic
                }
        }
 
-       result := make([]inspectPartitionAggregate, 0, len(aggregates))
-       for _, aggregate := range aggregates {
-               result = append(result, *aggregate)
-       }
-       sort.Slice(result, func(left, right int) bool { return 
result[left].orderingKey < result[right].orderingKey })
+       sort.Slice(aggregates, func(left, right int) bool {
+               return aggregates[left].orderingKey < 
aggregates[right].orderingKey
+       })
 
-       return result, nil
+       return aggregates, nil
 }
 
 // foldInspectPartitionManifest performs the original partition accounting 
using
diff --git a/table/inspect_partitions_batch_test.go 
b/table/inspect_partitions_batch_test.go
new file mode 100644
index 000000000..289d010db
--- /dev/null
+++ b/table/inspect_partitions_batch_test.go
@@ -0,0 +1,206 @@
+// 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 (
+       "context"
+       "fmt"
+       "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"
+       "github.com/stretchr/testify/require"
+)
+
+func inspectPartitionBatchFixture(
+       tb testing.TB,
+       count int,
+) (*iceberg.StructType, *arrow.Schema, []*inspectPartitionAggregate) {
+       tb.Helper()
+
+       partitionType := &iceberg.StructType{FieldList: []iceberg.NestedField{
+               {ID: 1000, Name: "part", Type: iceberg.PrimitiveTypes.Int32, 
Required: true},
+       }}
+       arrowSchema, err := 
SchemaToArrowSchema(PartitionsSchema(partitionType), nil, true, false)
+       if err != nil {
+               tb.Fatal(err)
+       }
+
+       aggregates := make([]*inspectPartitionAggregate, count)
+       for i := range aggregates {
+               aggregates[i] = &inspectPartitionAggregate{
+                       partition:       map[int]any{1000: int32(i)},
+                       dataRecordCount: int64(i + 1),
+                       dataFileCount:   1,
+               }
+       }
+
+       return partitionType, arrowSchema, aggregates
+}
+
+func TestInspectPartitionAggregateReaderBoundedBatches(t *testing.T) {
+       for _, count := range []int{
+               0, 1, inspectRecordBatchSize - 1, inspectRecordBatchSize,
+               inspectRecordBatchSize + 1, 2*inspectRecordBatchSize + 1,
+       } {
+               t.Run(fmt.Sprintf("partitions=%d", count), func(t *testing.T) {
+                       partitionType, arrowSchema, aggregates := 
inspectPartitionBatchFixture(t, count)
+                       mem := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                       t.Cleanup(func() { mem.AssertSize(t, 0) })
+
+                       reader := (InspectTable{alloc: 
mem}).partitionAggregateReader(
+                               t.Context(), arrowSchema, partitionType, 
aggregates)
+                       defer reader.Release()
+
+                       seen := 0
+                       var batchSizes []int
+                       for reader.Next() {
+                               batch := reader.RecordBatch()
+                               batchSizes = append(batchSizes, 
int(batch.NumRows()))
+                               require.LessOrEqual(t, batch.NumRows(), 
int64(inspectRecordBatchSize))
+
+                               partition := 
batch.Column(0).(*array.Struct).Field(0).(*array.Int32)
+                               recordCount := batch.Column(2).(*array.Int64)
+                               for offset := range int(batch.NumRows()) {
+                                       require.Equal(t, int32(seen), 
partition.Value(offset))
+                                       require.Equal(t, int64(seen+1), 
recordCount.Value(offset))
+                                       seen++
+                               }
+                       }
+                       require.NoError(t, reader.Err())
+                       require.Equal(t, count, seen)
+                       if count == 0 {
+                               require.Equal(t, []int{0}, batchSizes)
+                       } else {
+                               want := make([]int, 0, 
(count+inspectRecordBatchSize-1)/inspectRecordBatchSize)
+                               for remaining := count; remaining > 0; 
remaining -= inspectRecordBatchSize {
+                                       want = append(want, min(remaining, 
inspectRecordBatchSize))
+                               }
+                               require.Equal(t, want, batchSizes)
+                       }
+               })
+       }
+}
+
+// Exercise the public metadata-table reader across a real manifest, including
+// the boundary between two Arrow batches.
+func TestInspectPartitionsAcrossRecordBatchBoundary(t *testing.T) {
+       const count = inspectRecordBatchSize + 1
+
+       spec := partitionedSpec()
+       const snapshotID = int64(1)
+       sequenceNumber := int64(1)
+       entries := make([]iceberg.ManifestEntry, 0, count)
+       for index := range count {
+               dataFile := newTestDataFileWithCount(
+                       t, spec, 
fmt.Sprintf("mem://default/table-location/data/partition-%d.parquet", index),
+                       map[int]any{1000: int32(index)}, 1)
+               entries = append(entries, iceberg.NewManifestEntry(
+                       iceberg.EntryStatusADDED, new(snapshotID), 
&sequenceNumber, &sequenceNumber, dataFile))
+       }
+
+       tbl := inspectFilesTableWithManifests(t, spec, inspectManifestSpec{
+               content: iceberg.ManifestContentData,
+               entries: entries,
+       })
+       reader, err := tbl.Inspect().Partitions(t.Context())
+       require.NoError(t, err)
+       defer reader.Release()
+
+       seen := make(map[int32]bool, count)
+       var previousKey string
+       var batchSizes []int
+       for reader.Next() {
+               batch := reader.RecordBatch()
+               batchSizes = append(batchSizes, int(batch.NumRows()))
+               partitions := 
batch.Column(0).(*array.Struct).Field(0).(*array.Int32)
+               records := batch.Column(2).(*array.Int64)
+               for i := range int(batch.NumRows()) {
+                       value := partitions.Value(i)
+                       require.False(t, seen[value], "duplicate partition %d", 
value)
+                       seen[value] = true
+                       require.EqualValues(t, 1, records.Value(i))
+                       key := inspectPartitionKey(map[int]any{1000: value})
+                       if len(seen) > 1 && key <= previousKey {
+                               t.Fatalf("out-of-order partition key %q after 
%q", key, previousKey)
+                       }
+                       previousKey = key
+               }
+       }
+       require.NoError(t, reader.Err())
+       require.Len(t, seen, count)
+       require.Equal(t, []int{inspectRecordBatchSize, 1}, batchSizes)
+}
+
+func TestInspectPartitionAggregateReaderCancellation(t *testing.T) {
+       partitionType, arrowSchema, aggregates := inspectPartitionBatchFixture(
+               t, 2*inspectRecordBatchSize)
+       ctx, cancel := context.WithCancel(t.Context())
+       defer cancel()
+
+       reader := (InspectTable{alloc: 
memory.DefaultAllocator}).partitionAggregateReader(
+               ctx, arrowSchema, partitionType, aggregates)
+       defer reader.Release()
+
+       require.True(t, reader.Next())
+       require.EqualValues(t, inspectRecordBatchSize, 
reader.RecordBatch().NumRows())
+       cancel()
+       require.False(t, reader.Next())
+       require.ErrorIs(t, reader.Err(), context.Canceled)
+}
+
+var inspectPartitionBatchBenchmarkSink int64
+
+// BenchmarkInspectPartitionBatchEncoding measures the bounded partition
+// metadata encoder at different partition counts.
+func BenchmarkInspectPartitionBatchEncoding(b *testing.B) {
+       for _, count := range []int{4096, 16384, 65536} {
+               partitionType, arrowSchema, aggregates := 
inspectPartitionBatchFixture(b, count)
+               b.Run(fmt.Sprintf("partitions=%d/bounded", count), func(b 
*testing.B) {
+                       inspector := InspectTable{alloc: 
memory.DefaultAllocator}
+                       b.ReportAllocs()
+                       b.ReportMetric(float64(count), "partitions/op")
+                       b.ResetTimer()
+                       var maxBatchRows int64
+                       for b.Loop() {
+                               reader := inspector.partitionAggregateReader(
+                                       context.Background(), arrowSchema, 
partitionType, aggregates)
+                               var seen int64
+                               for reader.Next() {
+                                       rows := reader.RecordBatch().NumRows()
+                                       seen += rows
+                                       if rows > maxBatchRows {
+                                               maxBatchRows = rows
+                                       }
+                               }
+                               readErr := reader.Err()
+                               reader.Release()
+                               if readErr != nil {
+                                       b.Fatal(readErr)
+                               }
+                               if seen != int64(count) {
+                                       b.Fatalf("read %d partitions, want %d", 
seen, count)
+                               }
+                               inspectPartitionBatchBenchmarkSink = seen
+                       }
+                       b.ReportMetric(float64(maxBatchRows), "max_batch_rows")
+               })
+       }
+}

Reply via email to