This is an automated email from the ASF dual-hosted git repository.

zeroshade 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 74a3ac683 perf(name_mapping): reduce allocations for wide mapping 
updates (#2267)
74a3ac683 is described below

commit 74a3ac68364bda275c613518e1ee4989e7f7566e
Author: Minh Vu <[email protected]>
AuthorDate: Fri Oct 23 18:22:29 2026 +0200

    perf(name_mapping): reduce allocations for wide mapping updates (#2267)
    
    * perf(name_mapping): update wide mappings in one pass
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * test(name_mapping): preserve nil result for removed aliases
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * style(name_mapping): satisfy lint
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * ci: retrigger flaky macOS race test
    
    Signed-off-by: Minh Vu <[email protected]>
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
---
 name_mapping.go               | 127 ++++++++++++++++++++++++------------------
 name_mapping_bench_test.go    |   2 +-
 name_mapping_one_pass_test.go |  89 +++++++++++++++++++++++++++++
 3 files changed, 164 insertions(+), 54 deletions(-)

diff --git a/name_mapping.go b/name_mapping.go
index 243a102a0..d2beb6e0f 100644
--- a/name_mapping.go
+++ b/name_mapping.go
@@ -141,13 +141,16 @@ func visitMappedFields[S, T any](fields []MappedField, 
visitor NameMappingVisito
 //     After update: {FieldID: 1, Names: ["foo", "foo_update"]}
 //
 // This preserves compatibility with existing data files that reference the 
old field names.
-func UpdateNameMapping(nameMapping NameMapping, updates map[int]NestedField, 
adds map[int][]NestedField) (NameMapping, error) {
-       result, err := VisitNameMapping(nameMapping, 
&updateNameMappingVisitor{updates: updates, adds: adds})
-       if err != nil {
-               return nil, err
+func UpdateNameMapping(nameMapping NameMapping, updates map[int]NestedField, 
adds map[int][]NestedField) (result NameMapping, err error) {
+       if nameMapping == nil {
+               return nil, fmt.Errorf("%w: cannot visit nil NameMapping", 
ErrInvalidArgument)
        }
 
-       return NameMapping(result), nil
+       defer internal.RecoverError(&err)
+
+       updater := updateNameMappingVisitor{updates: updates, adds: adds}
+
+       return 
NameMapping(updater.addNewFields(updater.updatedFields(nameMapping), -1)), nil
 }
 
 type updateNameMappingVisitor struct {
@@ -155,50 +158,65 @@ type updateNameMappingVisitor struct {
        adds    map[int][]NestedField
 }
 
-func (u *updateNameMappingVisitor) Mapping(nm NameMapping, fieldResults 
[]MappedField) []MappedField {
-       return u.addNewFields(fieldResults, -1)
-}
+// updatedFields builds independent copies while handling renames at each 
sibling
+// level. It avoids allocating an intermediate visitor-result slice per level.
+func (u *updateNameMappingVisitor) updatedFields(fields []MappedField) 
[]MappedField {
+       if len(fields) == 0 {
+               return nil
+       }
 
-func (u *updateNameMappingVisitor) Fields(st []MappedField, fieldResults 
[]MappedField) []MappedField {
-       reassignments := make(map[string]int)
-       for _, field := range fieldResults {
-               if field.FieldID != nil {
+       // A rename may take an alias from a sibling, so find the new owners 
before
+       // building the output. Most levels have no updates and need no map.
+       var reassignments map[string]int
+       if len(u.updates) != 0 {
+               for _, field := range fields {
+                       if field.FieldID == nil {
+                               continue
+                       }
                        if update, exists := u.updates[*field.FieldID]; exists {
+                               if reassignments == nil {
+                                       reassignments = make(map[string]int)
+                               }
                                reassignments[update.Name] = update.ID
                        }
                }
        }
 
-       var result []MappedField
-       for _, field := range fieldResults {
-               updatedField := u.removeReassignedNames(field, reassignments)
-               if updatedField != nil {
-                       result = append(result, *updatedField)
+       result := make([]MappedField, 0, len(fields))
+       // Keep the copied IDs together rather than allocating one int per 
field.
+       // Every returned FieldID points into this independent backing slice.
+       var copiedIDs []int
+       for _, field := range fields {
+               children := u.updatedFields(field.Fields)
+               updated := MappedField{
+                       Names:  slices.Clone(field.Names),
+                       Fields: children,
                }
-       }
+               if field.FieldID != nil {
+                       if copiedIDs == nil {
+                               copiedIDs = make([]int, len(fields))
+                       }
+                       id := *field.FieldID
+                       copiedIDs[len(result)] = id
+                       updated.FieldID = &copiedIDs[len(result)]
 
-       return result
-}
+                       if rename, exists := u.updates[id]; exists && 
!slices.Contains(updated.Names, rename.Name) {
+                               updated.Names = append(updated.Names, 
rename.Name)
+                       }
+                       updated.Fields = u.addNewFields(children, id)
+               }
 
-func (u *updateNameMappingVisitor) Field(field MappedField, fieldResult 
[]MappedField) MappedField {
-       if field.FieldID == nil {
-               return MappedField{
-                       Names:  slices.Clone(field.Names),
-                       Fields: fieldResult,
+               if retained := u.removeReassignedNames(updated, reassignments); 
retained != nil {
+                       result = append(result, *retained)
                }
        }
 
-       fieldID := *field.FieldID
-       fieldNames := slices.Clone(field.Names)
-       if update, exists := u.updates[*field.FieldID]; exists && 
!slices.Contains(fieldNames, update.Name) {
-               fieldNames = append(fieldNames, update.Name)
+       // The original visitor returned nil when every alias was discarded.
+       if len(result) == 0 {
+               return nil
        }
 
-       return MappedField{
-               FieldID: &fieldID,
-               Names:   fieldNames,
-               Fields:  u.addNewFields(fieldResult, *field.FieldID),
-       }
+       return result
 }
 
 func (u *updateNameMappingVisitor) removeReassignedNames(field MappedField, 
assignments map[string]int) *MappedField {
@@ -214,7 +232,6 @@ func (u *updateNameMappingVisitor) 
removeReassignedNames(field MappedField, assi
        if len(remainingNames) == 0 {
                return nil
        }
-
        field.Names = remainingNames
 
        return &field
@@ -226,32 +243,36 @@ func (u *updateNameMappingVisitor) 
addNewFields(mappedFields []MappedField, pare
                return mappedFields
        }
 
-       newFields := make([]MappedField, 0, len(fieldsToAdd))
-       for _, add := range fieldsToAdd {
-               fields := visitField(add, createMapping{})
-               if len(fields) == 0 {
-                       fields = nil
-               }
-
-               newFields = append(newFields, MappedField{
-                       FieldID: &add.ID,
-                       Names:   []string{add.Name},
-                       Fields:  fields,
-               })
-       }
-       reassignments := make(map[string]int)
+       reassignments := make(map[string]int, len(fieldsToAdd))
        for _, add := range fieldsToAdd {
                reassignments[add.Name] = add.ID
        }
 
-       fields := make([]MappedField, 0)
+       // mappedFields was freshly allocated by updatedFields, so compacting it
+       // here cannot change the caller's original mapping.
+       fields := mappedFields[:0]
+       if fields == nil {
+               fields = make([]MappedField, 0, len(fieldsToAdd))
+       }
        for _, field := range mappedFields {
-               if updatedField := u.removeReassignedNames(field, 
reassignments); updatedField != nil {
-                       fields = append(fields, *updatedField)
+               if retained := u.removeReassignedNames(field, reassignments); 
retained != nil {
+                       fields = append(fields, *retained)
                }
        }
 
-       return append(fields, newFields...)
+       for _, add := range fieldsToAdd {
+               children := visitField(add, createMapping{})
+               if len(children) == 0 {
+                       children = nil
+               }
+               fields = append(fields, MappedField{
+                       FieldID: &add.ID,
+                       Names:   []string{add.Name},
+                       Fields:  children,
+               })
+       }
+
+       return fields
 }
 
 type NameMappingAccessor struct{}
diff --git a/name_mapping_bench_test.go b/name_mapping_bench_test.go
index 6cd10d650..ca13e5c44 100644
--- a/name_mapping_bench_test.go
+++ b/name_mapping_bench_test.go
@@ -68,7 +68,7 @@ func benchmarkNameMappingFields(count int) 
iceberg.NameMapping {
 }
 
 func BenchmarkUpdateNameMapping(b *testing.B) {
-       for _, size := range []int{2, 8, 256, 1024} {
+       for _, size := range []int{2, 8, 256, 1024, 4096} {
                for _, operation := range []string{"no-updates", "rename", 
"rename-reused", "add-reused"} {
                        b.Run(fmt.Sprintf("%s/%d", operation, size), func(b 
*testing.B) {
                                mapping := benchmarkNameMappingFields(size)
diff --git a/name_mapping_one_pass_test.go b/name_mapping_one_pass_test.go
new file mode 100644
index 000000000..27ad9c76a
--- /dev/null
+++ b/name_mapping_one_pass_test.go
@@ -0,0 +1,89 @@
+// 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"
+       "github.com/stretchr/testify/require"
+)
+
+func TestUpdateNameMappingWideFieldIDsIndependent(t *testing.T) {
+       const fieldCount = 256
+       mapping := make(iceberg.NameMapping, fieldCount)
+       for i := range mapping {
+               mapping[i] = iceberg.MappedField{
+                       FieldID: new(i + 1),
+                       Names:   []string{fmt.Sprintf("field_%d", i)},
+               }
+       }
+
+       updated, err := iceberg.UpdateNameMapping(mapping, nil, nil)
+       require.NoError(t, err)
+       require.Equal(t, mapping, updated)
+
+       *updated[0].FieldID = 9000
+       *updated[1].FieldID = 9001
+       updated[2].Names[0] = "changed"
+
+       require.Equal(t, 1, *mapping[0].FieldID)
+       require.Equal(t, 2, *mapping[1].FieldID)
+       require.Equal(t, "field_2", mapping[2].Names[0])
+       require.Equal(t, 3, *updated[2].FieldID)
+       require.Equal(t, 4, *updated[3].FieldID)
+}
+
+func TestUpdateNameMappingEmptyAndAnonymous(t *testing.T) {
+       _, err := iceberg.UpdateNameMapping(nil, nil, nil)
+       require.ErrorIs(t, err, iceberg.ErrInvalidArgument)
+
+       empty, err := iceberg.UpdateNameMapping(
+               iceberg.NameMapping{}, nil, map[int][]iceberg.NestedField{-1: 
{}},
+       )
+       require.NoError(t, err)
+       require.NotNil(t, empty)
+       require.Empty(t, empty)
+
+       mapping := iceberg.NameMapping{
+               {Names: []string{"alias"}, Fields: []iceberg.MappedField{
+                       {FieldID: new(7), Names: []string{"old", "current"}},
+               }},
+               {Names: []string{}},
+               {FieldID: new(8), Names: []string{"other"}},
+       }
+       updated, err := iceberg.UpdateNameMapping(
+               mapping,
+               map[int]iceberg.NestedField{7: {ID: 7, Name: "renamed"}},
+               nil,
+       )
+       require.NoError(t, err)
+       require.Len(t, updated, 2)
+       require.Nil(t, updated[0].FieldID)
+       require.Equal(t, []string{"old", "current", "renamed"}, 
updated[0].Fields[0].Names)
+       require.Equal(t, 8, *updated[1].FieldID)
+       require.Len(t, mapping, 3)
+}
+
+func TestUpdateNameMappingPrunesNamelessMappingToNil(t *testing.T) {
+       mapping := iceberg.NameMapping{{Names: []string{}}}
+       updated, err := iceberg.UpdateNameMapping(mapping, nil, nil)
+       require.NoError(t, err)
+       require.Nil(t, updated)
+}

Reply via email to