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 d65c69add perf(partitions): avoid temporary fields in PartitionType 
(#2121)
d65c69add is described below

commit d65c69add21ef84e40b64b3accf229c5c8135430
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 6 18:33:12 2026 +0200

    perf(partitions): avoid temporary fields in PartitionType (#2121)
---
 partitions.go                 | 24 +++++++------
 partitions_test.go            | 84 +++++++++++++++++++++++++++++++++++++++++++
 partitions_type_bench_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+), 10 deletions(-)

diff --git a/partitions.go b/partitions.go
index 58014bd41..efd11e621 100644
--- a/partitions.go
+++ b/partitions.go
@@ -867,8 +867,8 @@ func (ps *PartitionSpec) resolvedPartitionFields(schema 
*Schema) []resolvedParti
        fields := make([]resolvedPartitionField, 0, len(ps.fields))
        for _, field := range ps.fields {
                sourceType := Type(UnknownType{})
-               if typ, ok := schema.FindTypeByID(field.SourceID()); ok {
-                       sourceType = typ
+               if sourceField, ok := schema.FindFieldByIDRef(field.SourceID(), 
internal.SchemaRef{}); ok {
+                       sourceType = sourceField.Type
                }
 
                fields = append(fields, resolvedPartitionField{
@@ -896,15 +896,19 @@ func (ps *PartitionSpec) resolvedPartitionFields(schema 
*Schema) []resolvedParti
 // retains the field's position and lets transforms with fixed result types,
 // such as bucket, continue to resolve their result type.
 func (ps *PartitionSpec) PartitionType(schema *Schema) *StructType {
-       resolvedFields := ps.resolvedPartitionFields(schema)
-       nestedFields := make([]NestedField, 0, len(resolvedFields))
-       for _, field := range resolvedFields {
-               nestedFields = append(nestedFields, NestedField{
-                       ID:       field.field.FieldID,
-                       Name:     field.field.Name,
-                       Type:     field.resultType,
+       nestedFields := make([]NestedField, len(ps.fields))
+       for i, field := range ps.fields {
+               sourceType := Type(UnknownType{})
+               if typ, ok := schema.FindTypeByID(field.SourceID()); ok {
+                       sourceType = typ
+               }
+
+               nestedFields[i] = NestedField{
+                       ID:       field.FieldID,
+                       Name:     field.Name,
+                       Type:     field.Transform.ResultType(sourceType),
                        Required: false,
-               })
+               }
        }
 
        return &StructType{FieldList: nestedFields}
diff --git a/partitions_test.go b/partitions_test.go
index a1b5a4b74..542b3e87d 100644
--- a/partitions_test.go
+++ b/partitions_test.go
@@ -523,6 +523,90 @@ func TestPartitionType(t *testing.T) {
                "expected: %s, got: %s", expectedWithDroppedSources, actual)
 }
 
+func TestPartitionTypePreservesFieldOrderAndResultTypes(t *testing.T) {
+       schema := iceberg.NewSchema(0,
+               iceberg.NestedField{ID: 1, Name: "record", Type: 
&iceberg.StructType{FieldList: []iceberg.NestedField{
+                       {ID: 2, Name: "nested", Type: iceberg.Int64Type{}, 
Required: true},
+               }}},
+               iceberg.NestedField{ID: 3, Name: "ts", Type: 
iceberg.TimestampTzNsType{}, Required: true},
+               iceberg.NestedField{ID: 4, Name: "amount", Type: 
iceberg.DecimalTypeOf(12, 2)},
+       )
+       unknown, err := iceberg.ParseTransform("custom_transform[42]")
+       require.NoError(t, err)
+
+       fields := []iceberg.PartitionField{
+               {SourceIDs: []int{2}, FieldID: 1007, Name: "nested", Transform: 
iceberg.IdentityTransform{}},
+               {SourceIDs: []int{3}, FieldID: 1001, Name: "ts", Transform: 
iceberg.IdentityTransform{}},
+               {SourceIDs: []int{4}, FieldID: 1006, Name: "amount", Transform: 
iceberg.TruncateTransform{Width: 10}},
+               {SourceIDs: []int{99}, FieldID: 1002, Name: "missing_bucket", 
Transform: iceberg.BucketTransform{NumBuckets: 16}},
+               {SourceIDs: []int{99}, FieldID: 1005, Name: "missing_day", 
Transform: iceberg.DayTransform{}},
+               {SourceIDs: []int{99}, FieldID: 1003, Name: "missing_identity", 
Transform: iceberg.IdentityTransform{}},
+               {SourceIDs: []int{0}, FieldID: 1004, Name: "void", Transform: 
iceberg.VoidTransform{}},
+               {SourceIDs: []int{99}, FieldID: 1000, Name: "custom", 
Transform: unknown},
+       }
+       spec := iceberg.NewPartitionSpec(fields...)
+
+       want := []iceberg.NestedField{
+               {ID: 1007, Name: "nested", Type: iceberg.Int64Type{}},
+               {ID: 1001, Name: "ts", Type: iceberg.TimestampTzNsType{}},
+               {ID: 1006, Name: "amount", Type: iceberg.DecimalTypeOf(12, 2)},
+               {ID: 1002, Name: "missing_bucket", Type: iceberg.Int32Type{}},
+               {ID: 1005, Name: "missing_day", Type: iceberg.DateType{}},
+               {ID: 1003, Name: "missing_identity", Type: 
iceberg.UnknownType{}},
+               {ID: 1004, Name: "void", Type: iceberg.UnknownType{}},
+               {ID: 1000, Name: "custom", Type: iceberg.StringType{}},
+       }
+       require.Equal(t, want, spec.PartitionType(schema).FieldList)
+
+       for i, field := range fields {
+               require.True(t, field.Equals(spec.Field(i)))
+       }
+}
+
+func TestPartitionTypeResolvesEachSchema(t *testing.T) {
+       spec := iceberg.NewPartitionSpec(iceberg.PartitionField{
+               SourceIDs: []int{1}, FieldID: 1000, Name: "value", Transform: 
iceberg.IdentityTransform{},
+       })
+       schemas := []*iceberg.Schema{
+               iceberg.NewSchema(0, iceberg.NestedField{ID: 1, Name: "value", 
Type: iceberg.StringType{}}),
+               iceberg.NewSchema(0, iceberg.NestedField{ID: 1, Name: "value", 
Type: iceberg.Int64Type{}}),
+               iceberg.NewSchema(0),
+       }
+       types := []iceberg.Type{iceberg.StringType{}, iceberg.Int64Type{}, 
iceberg.UnknownType{}}
+       for range 2 {
+               for i, schema := range schemas {
+                       require.Equal(t, []iceberg.NestedField{
+                               {ID: 1000, Name: "value", Type: types[i]},
+                       }, spec.PartitionType(schema).FieldList)
+               }
+       }
+}
+
+func TestPartitionTypeReturnsIndependentFields(t *testing.T) {
+       schema := iceberg.NewSchema(0, iceberg.NestedField{ID: 1, Name: 
"value", Type: iceberg.StringType{}})
+       spec := iceberg.NewPartitionSpec(iceberg.PartitionField{
+               SourceIDs: []int{1}, FieldID: 1000, Name: "value", Transform: 
iceberg.IdentityTransform{},
+       })
+       first := spec.PartitionType(schema)
+       second := spec.PartitionType(schema)
+       first.FieldList[0] = iceberg.NestedField{ID: 2000, Name: "changed", 
Type: iceberg.Int64Type{}, Required: true}
+
+       want := []iceberg.NestedField{{ID: 1000, Name: "value", Type: 
iceberg.StringType{}}}
+       require.Equal(t, want, second.FieldList)
+       require.Equal(t, want, spec.PartitionType(schema).FieldList)
+       require.Equal(t, "value", spec.Field(0).Name)
+       require.Equal(t, 1000, spec.Field(0).FieldID)
+}
+
+func TestPartitionTypeUnpartitioned(t *testing.T) {
+       for _, spec := range []iceberg.PartitionSpec{{}, 
iceberg.NewPartitionSpec()} {
+               partitionType := spec.PartitionType(nil)
+               require.NotNil(t, partitionType)
+               require.NotNil(t, partitionType.FieldList)
+               require.Empty(t, partitionType.FieldList)
+       }
+}
+
 type partitionRecord []any
 
 func (p partitionRecord) Size() int            { return len(p) }
diff --git a/partitions_type_bench_test.go b/partitions_type_bench_test.go
new file mode 100644
index 000000000..dcceb26b8
--- /dev/null
+++ b/partitions_type_bench_test.go
@@ -0,0 +1,82 @@
+// 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 iceberg_test
+
+import (
+       "fmt"
+       "testing"
+
+       "github.com/apache/iceberg-go"
+)
+
+var partitionTypeBenchmarkResult *iceberg.StructType
+
+func BenchmarkPartitionTypeFieldCount(b *testing.B) {
+       for _, numFields := range []int{0, 1, 3, 20, 100} {
+               b.Run(fmt.Sprintf("fields=%d", numFields), func(b *testing.B) {
+                       fields := make([]iceberg.NestedField, numFields)
+                       partitionFields := make([]iceberg.PartitionField, 
numFields)
+                       for i := range fields {
+                               name := fmt.Sprintf("field_%d", i)
+                               fields[i] = iceberg.NestedField{ID: i + 1, 
Name: name, Type: iceberg.StringType{}}
+                               partitionFields[i] = iceberg.PartitionField{
+                                       SourceIDs: []int{i + 1}, FieldID: 1000 
+ i, Name: name,
+                                       Transform: iceberg.IdentityTransform{},
+                               }
+                       }
+                       schema := iceberg.NewSchema(0, fields...)
+                       spec := iceberg.NewPartitionSpec(partitionFields...)
+                       partitionTypeBenchmarkResult = 
spec.PartitionType(schema)
+
+                       b.ReportAllocs()
+                       b.ResetTimer()
+                       for range b.N {
+                               partitionTypeBenchmarkResult = 
spec.PartitionType(schema)
+                       }
+               })
+       }
+}
+
+func BenchmarkPartitionTypeMultipleSchemasWarmIndexes(b *testing.B) {
+       spec := iceberg.NewPartitionSpec(
+               iceberg.PartitionField{
+                       SourceIDs: []int{1}, FieldID: 1000, Name: "name",
+                       Transform: iceberg.IdentityTransform{},
+               },
+               iceberg.PartitionField{
+                       SourceIDs: []int{2}, FieldID: 1001, Name: "count",
+                       Transform: iceberg.IdentityTransform{},
+               },
+       )
+       schemas := make([]*iceberg.Schema, 10)
+       for i := range schemas {
+               schemas[i] = iceberg.NewSchema(i,
+                       iceberg.NestedField{ID: 1, Name: "name", Type: 
iceberg.StringType{}},
+                       iceberg.NestedField{ID: 2, Name: "count", Type: 
iceberg.Int64Type{}},
+               )
+               // Warm the schema's field-ID index before timing, matching 
normal
+               // repeated planning against already-used metadata.
+               partitionTypeBenchmarkResult = spec.PartitionType(schemas[i])
+       }
+
+       b.ReportAllocs()
+       b.ResetTimer()
+       for i := 0; b.Loop(); i++ {
+               partitionTypeBenchmarkResult = 
spec.PartitionType(schemas[i%len(schemas)])
+       }
+}

Reply via email to