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 89c5e6415 perf(table): append inspect partition values directly (#2116)
89c5e6415 is described below
commit 89c5e6415f704590c6cc566e822419bb14ef0768
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 6 10:26:30 2026 +0200
perf(table): append inspect partition values directly (#2116)
* perf(table): append inspect partition values directly
* test(table): cover mixed and special partition values
---
table/inspect_files.go | 99 +++++++++++--
table/inspect_files_bench_test.go | 52 +++++++
table/inspect_partition_append_test.go | 259 +++++++++++++++++++++++++++++++++
3 files changed, 395 insertions(+), 15 deletions(-)
diff --git a/table/inspect_files.go b/table/inspect_files.go
index f10575216..4705f785f 100644
--- a/table/inspect_files.go
+++ b/table/inspect_files.go
@@ -555,11 +555,12 @@ type inspectPartitionBuilder struct {
}
type inspectPartitionFieldBuilder struct {
- id int
- name string
- typ iceberg.Type
- arrowType arrow.DataType
- builder array.Builder
+ id int
+ name string
+ typ iceberg.Type
+ arrowType arrow.DataType
+ builder array.Builder
+ appendValue func(any) error
}
func newInspectContentFileBuilder(bldr *array.RecordBuilder, partitionType
*iceberg.StructType) (inspectContentFileBuilder, error) {
@@ -737,13 +738,15 @@ func newInspectPartitionBuilder(
fields := make([]inspectPartitionFieldBuilder, 0,
len(partitionType.FieldList))
for _, field := range partitionType.FieldList {
fieldBuilder := lookup[field.ID]
- fields = append(fields, inspectPartitionFieldBuilder{
+ partitionField := inspectPartitionFieldBuilder{
id: field.ID,
name: field.Name,
typ: field.Type,
arrowType: fieldBuilder.Type(),
builder: fieldBuilder,
- })
+ }
+ partitionField.appendValue =
newInspectPartitionFieldAppender(partitionField)
+ fields = append(fields, partitionField)
}
return &inspectPartitionBuilder{builder: builder, fields: fields}, nil
@@ -752,22 +755,88 @@ func newInspectPartitionBuilder(
func (b *inspectPartitionBuilder) append(values map[int]any) error {
b.builder.Append(true)
for _, field := range b.fields {
- value := values[field.id]
- if value == nil {
- field.builder.AppendNull()
-
- continue
+ if err := field.appendValue(values[field.id]); err != nil {
+ return err
}
+ }
+
+ return nil
+}
+
+func newInspectPartitionFieldAppender(field inspectPartitionFieldBuilder)
func(any) error {
+ appendScalar := func(value any) error {
sc, err := inspectValueScalar(value, field.typ, field.arrowType)
if err != nil {
return fmt.Errorf("partition field %q: %w", field.name,
err)
}
- if err := scalar.Append(field.builder, sc); err != nil {
- return err
+
+ return scalar.Append(field.builder, sc)
+ }
+ appendNull := func() {
+ field.builder.AppendNull()
+ }
+
+ switch field.typ.(type) {
+ case iceberg.BooleanType:
+ if builder, ok := field.builder.(*array.BooleanBuilder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.Int32Type:
+ if builder, ok := field.builder.(*array.Int32Builder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.Int64Type:
+ if builder, ok := field.builder.(*array.Int64Builder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.Float32Type:
+ if builder, ok := field.builder.(*array.Float32Builder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.Float64Type:
+ if builder, ok := field.builder.(*array.Float64Builder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.StringType:
+ if builder, ok := field.builder.(*array.StringBuilder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.BinaryType:
+ if builder, ok := field.builder.(*array.BinaryBuilder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
}
}
- return nil
+ return func(value any) error {
+ if value == nil {
+ appendNull()
+
+ return nil
+ }
+
+ return appendScalar(value)
+ }
+}
+
+func newInspectTypedPartitionAppender[T any](
+ appendValue func(T),
+ appendNull func(),
+ appendScalar func(any) error,
+) func(any) error {
+ return func(value any) error {
+ if value == nil {
+ appendNull()
+
+ return nil
+ }
+ typed, ok := value.(T)
+ if !ok {
+ return appendScalar(value)
+ }
+ appendValue(typed)
+
+ return nil
+ }
}
func inspectValueScalar(value any, typ iceberg.Type, arrowType arrow.DataType)
(scalar.Scalar, error) {
diff --git a/table/inspect_files_bench_test.go
b/table/inspect_files_bench_test.go
index ac5604a44..7e980aaf3 100644
--- a/table/inspect_files_bench_test.go
+++ b/table/inspect_files_bench_test.go
@@ -97,3 +97,55 @@ func benchmarkInspectContentFiles(
return partitionType, files
}
+
+func BenchmarkInspectContentFileAppenderMixedPartitions(b *testing.B) {
+ types := []iceberg.Type{iceberg.PrimitiveTypes.Bool,
iceberg.PrimitiveTypes.Int32, iceberg.PrimitiveTypes.Int64,
iceberg.PrimitiveTypes.Float32, iceberg.PrimitiveTypes.Float64,
iceberg.PrimitiveTypes.String, iceberg.PrimitiveTypes.Binary,
iceberg.PrimitiveTypes.Date}
+ raw := []any{true, int32(34), int64(9007199254740993), float32(1.25),
float64(2.5), "partition", []byte("binary"), iceberg.Date(12)}
+ fields := make([]iceberg.NestedField, len(types))
+ partitions := make([]iceberg.PartitionField, len(types))
+ for i, typ := range types {
+ fields[i] = iceberg.NestedField{ID: i + 1, Name:
fmt.Sprintf("field_%d", i), Type: typ}
+ partitions[i] = iceberg.PartitionField{SourceIDs: []int{i + 1},
FieldID: 1000 + i, Name: fields[i].Name, Transform: iceberg.IdentityTransform{}}
+ }
+ spec := iceberg.NewPartitionSpec(partitions...)
+ partitionType := spec.PartitionType(iceberg.NewSchema(0, fields...))
+ for _, nullEvery := range []int{0, 2} {
+ b.Run(fmt.Sprintf("fields=8/files=4096/null_every=%d",
nullEvery), func(b *testing.B) {
+ files := make([]iceberg.DataFile, 4096)
+ for i := range files {
+ values := make(map[int]any, len(types))
+ for j, value := range raw {
+ if nullEvery > 0 && (i+j)%nullEvery ==
0 {
+ value = nil
+ }
+ values[1000+j] = value
+ }
+ file, err := iceberg.NewDataFileBuilder(spec,
iceberg.EntryContentData, fmt.Sprintf("file-%d.parquet", i),
iceberg.ParquetFile, values, map[int]string{1007: "date"}, nil, 1, 1)
+ if err != nil {
+ b.Fatal(err)
+ }
+ files[i] = file.Build()
+ }
+ arrowSchema, err :=
SchemaToArrowSchema(DataFilesSchema(partitionType), nil, true, false)
+ if err != nil {
+ b.Fatal(err)
+ }
+ builder :=
array.NewRecordBuilder(memory.DefaultAllocator, arrowSchema)
+ defer builder.Release()
+ appendFile :=
newInspectContentFileAppender(partitionType)
+ b.ReportAllocs()
+ for b.Loop() {
+ for _, file := range files {
+ if err := appendFile(builder, file);
err != nil {
+ b.Fatal(err)
+ }
+ }
+ record := builder.NewRecordBatch()
+ if record.NumRows() != int64(len(files)) {
+ b.Fatalf("got %d rows, want %d",
record.NumRows(), len(files))
+ }
+ record.Release()
+ }
+ })
+ }
+}
diff --git a/table/inspect_partition_append_test.go
b/table/inspect_partition_append_test.go
new file mode 100644
index 000000000..1706f3431
--- /dev/null
+++ b/table/inspect_partition_append_test.go
@@ -0,0 +1,259 @@
+// 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"
+ "testing"
+
+ "github.com/apache/arrow-go/v18/arrow"
+ "github.com/apache/arrow-go/v18/arrow/array"
+ "github.com/apache/arrow-go/v18/arrow/decimal128"
+ "github.com/apache/arrow-go/v18/arrow/memory"
+ "github.com/apache/iceberg-go"
+ "github.com/stretchr/testify/require"
+)
+
+func TestInspectPartitionBuilderAppendsTypedAndFallbackValues(t *testing.T) {
+ tests := []struct {
+ name string
+ typ iceberg.Type
+ value any
+ check func(*testing.T, arrow.Array)
+ }{
+ {
+ name: "boolean",
+ typ: iceberg.PrimitiveTypes.Bool,
+ value: true,
+ check: func(t *testing.T, value arrow.Array) {
+ require.True(t, value.(*array.Boolean).Value(0))
+ },
+ },
+ {
+ name: "int32",
+ typ: iceberg.PrimitiveTypes.Int32,
+ value: int32(32),
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, int32(32),
value.(*array.Int32).Value(0))
+ },
+ },
+ {
+ name: "int64",
+ typ: iceberg.PrimitiveTypes.Int64,
+ value: int64(64),
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, int64(64),
value.(*array.Int64).Value(0))
+ },
+ },
+ {
+ name: "float32",
+ typ: iceberg.PrimitiveTypes.Float32,
+ value: float32(1.25),
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, float32(1.25),
value.(*array.Float32).Value(0))
+ },
+ },
+ {
+ name: "float64",
+ typ: iceberg.PrimitiveTypes.Float64,
+ value: float64(2.5),
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, float64(2.5),
value.(*array.Float64).Value(0))
+ },
+ },
+ {
+ name: "string",
+ typ: iceberg.PrimitiveTypes.String,
+ value: "partition",
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, "partition",
value.(*array.String).Value(0))
+ },
+ },
+ {
+ name: "binary",
+ typ: iceberg.PrimitiveTypes.Binary,
+ value: []byte{0, 1, 255},
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, []byte{0, 1, 255},
value.(*array.Binary).Value(0))
+ },
+ },
+ {
+ name: "date scalar fallback",
+ typ: iceberg.PrimitiveTypes.Date,
+ value: iceberg.Date(12),
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, arrow.Date32(12),
value.(*array.Date32).Value(0))
+ },
+ },
+ {
+ name: "time scalar fallback",
+ typ: iceberg.PrimitiveTypes.Time,
+ value: iceberg.Time(1234),
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, arrow.Time64(1234),
value.(*array.Time64).Value(0))
+ },
+ },
+ {
+ name: "timestamp scalar fallback",
+ typ: iceberg.PrimitiveTypes.Timestamp,
+ value: iceberg.Timestamp(1234),
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, arrow.Timestamp(1234),
value.(*array.Timestamp).Value(0))
+ },
+ },
+ {
+ name: "nanosecond timestamp scalar fallback",
+ typ: iceberg.PrimitiveTypes.TimestampNs,
+ value: iceberg.TimestampNano(1234),
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, arrow.Timestamp(1234),
value.(*array.Timestamp).Value(0))
+ },
+ },
+ {
+ name: "decimal scalar fallback",
+ typ: iceberg.DecimalTypeOf(10, 2),
+ value: iceberg.DecimalLiteral{Val:
decimal128.FromI64(123), Scale: 2},
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, decimal128.FromI64(123),
value.(*array.Decimal128).Value(0))
+ },
+ },
+ {
+ name: "fixed scalar fallback",
+ typ: iceberg.FixedTypeOf(3),
+ value: []byte("abc"),
+ check: func(t *testing.T, value arrow.Array) {
+ require.Equal(t, []byte("abc"),
value.(*array.FixedSizeBinary).Value(0))
+ },
+ },
+ }
+
+ fields := make([]iceberg.NestedField, len(tests))
+ values := make(map[int]any, len(tests))
+ for i, tt := range tests {
+ fields[i] = iceberg.NestedField{ID: i + 2, Name: tt.name, Type:
tt.typ}
+ values[i+2] = tt.value
+ }
+ builder, partitionBuilder := newInspectPartitionBuilderForTest(t,
fields)
+ require.NoError(t, partitionBuilder.append(values))
+
+ partition := builder.NewArray().(*array.Struct)
+ defer partition.Release()
+ for i, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.check(t, partition.Field(i))
+ })
+ }
+}
+
+func TestInspectPartitionBuilderAppendsNullValues(t *testing.T) {
+ builder, partitionBuilder := newInspectPartitionBuilderForTest(t,
[]iceberg.NestedField{
+ {ID: 2, Name: "value", Type: iceberg.PrimitiveTypes.Int32},
+ })
+ require.NoError(t, partitionBuilder.append(nil))
+
+ partition := builder.NewArray().(*array.Struct)
+ defer partition.Release()
+ require.True(t, partition.Field(0).IsNull(0))
+}
+
+func TestInspectPartitionBuilderScalarFallbackPreservesValidation(t
*testing.T) {
+ tests := []struct {
+ name string
+ typ iceberg.Type
+ value any
+ wantErr string
+ }{
+ {name: "compatible integer string", typ:
iceberg.PrimitiveTypes.Int32, value: "42"},
+ {name: "invalid integer string", typ:
iceberg.PrimitiveTypes.Int32, value: "not-an-int", wantErr: "partition field
\"value\": strconv.ParseInt"},
+ {name: "wrong date value", typ: iceberg.PrimitiveTypes.Date,
value: int64(1), wantErr: "partition field \"value\": unsupported date"},
+ {name: "wrong fixed width", typ: iceberg.FixedTypeOf(2), value:
[]byte("x"), wantErr: "invalid scalar value of len"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ builder, partitionBuilder :=
newInspectPartitionBuilderForTest(t, []iceberg.NestedField{
+ {ID: 2, Name: "value", Type: tt.typ},
+ })
+ err := partitionBuilder.append(map[int]any{2: tt.value})
+ if tt.wantErr != "" {
+ require.ErrorContains(t, err, tt.wantErr)
+
+ return
+ }
+ require.NoError(t, err)
+ partition := builder.NewArray().(*array.Struct)
+ defer partition.Release()
+ require.Equal(t, int32(42),
partition.Field(0).(*array.Int32).Value(0))
+ })
+ }
+}
+
+func newInspectPartitionBuilderForTest(
+ t *testing.T,
+ fields []iceberg.NestedField,
+) (*array.StructBuilder, *inspectPartitionBuilder) {
+ t.Helper()
+ partitionType := &iceberg.StructType{FieldList: fields}
+ schema, err := SchemaToArrowSchema(iceberg.NewSchema(0,
iceberg.NestedField{
+ ID: 1, Name: "partition", Type: partitionType, Required: true,
+ }), nil, true, false)
+ require.NoError(t, err)
+ structType, ok := schema.Field(0).Type.(*arrow.StructType)
+ require.True(t, ok)
+ builder := array.NewStructBuilder(memory.DefaultAllocator, structType)
+ t.Cleanup(builder.Release)
+ partitionBuilder, err := newInspectPartitionBuilder(builder,
partitionType)
+ require.NoError(t, err)
+
+ return builder, partitionBuilder
+}
+
+func TestInspectPartitionBuilderPreservesEmptyAndSpecialValues(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ typ iceberg.Type
+ value any
+ check func(*testing.T, arrow.Array)
+ }{
+ {"empty string", iceberg.PrimitiveTypes.String, "", func(t
*testing.T, a arrow.Array) { require.Empty(t, a.(*array.String).Value(0)) }},
+ {"empty binary", iceberg.PrimitiveTypes.Binary, []byte{},
func(t *testing.T, a arrow.Array) { require.Empty(t,
a.(*array.Binary).Value(0)) }},
+ {"float32 negative zero", iceberg.PrimitiveTypes.Float32,
float32(math.Copysign(0, -1)), func(t *testing.T, a arrow.Array) {
require.True(t, math.Signbit(float64(a.(*array.Float32).Value(0)))) }},
+ {"float64 NaN", iceberg.PrimitiveTypes.Float64, math.NaN(),
func(t *testing.T, a arrow.Array) { require.True(t,
math.IsNaN(a.(*array.Float64).Value(0))) }},
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ builder, partitionBuilder :=
newInspectPartitionBuilderForTest(t, []iceberg.NestedField{{ID: 2, Name:
"value", Type: tt.typ}})
+ require.NoError(t,
partitionBuilder.append(map[int]any{2: tt.value}))
+ require.NoError(t, partitionBuilder.append(nil))
+ partition := builder.NewArray().(*array.Struct)
+ defer partition.Release()
+ require.True(t, partition.Field(0).IsValid(0))
+ require.True(t, partition.Field(0).IsNull(1))
+ tt.check(t, partition.Field(0))
+ })
+ }
+}
+
+func TestInspectPartitionBuilderCopiesBinaryValues(t *testing.T) {
+ builder, partitionBuilder := newInspectPartitionBuilderForTest(t,
[]iceberg.NestedField{{ID: 2, Name: "value", Type:
iceberg.PrimitiveTypes.Binary}})
+ value := []byte("abc")
+ require.NoError(t, partitionBuilder.append(map[int]any{2: value}))
+ value[0] = 'x'
+ partition := builder.NewArray().(*array.Struct)
+ defer partition.Release()
+ require.Equal(t, []byte("abc"),
partition.Field(0).(*array.Binary).Value(0))
+}