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 f503b457b fix(name_mapping): isolate updated mappings (#1547)
f503b457b is described below

commit f503b457b9a274c5faa81af1ec9ccf7457da7d0f
Author: Minh Vu <[email protected]>
AuthorDate: Fri Jul 31 15:24:54 2026 +0200

    fix(name_mapping): isolate updated mappings (#1547)
    
    ## What changed
    
    Clone mapped field IDs and name slices while building the result of
    UpdateNameMapping. Fields without IDs now retain the independently
    visited child results as well.
    
    ## Why
    
    The update visitor reused FieldID pointers from the input mapping.
    Mutating the returned mapping could therefore change the original.
    Appending a renamed field to a name slice with spare capacity could also
    write into the input backing array during the update itself.
    
    The regression test covers both alias paths across parent and nested
    fields.
    
    ## Testing
    
    - go test .
    - go vet .
    - go test ./...
---
 name_mapping.go      | 13 +++++---
 name_mapping_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 94 insertions(+), 4 deletions(-)

diff --git a/name_mapping.go b/name_mapping.go
index fa4ec12d7..d9f63e2d7 100644
--- a/name_mapping.go
+++ b/name_mapping.go
@@ -182,16 +182,20 @@ func (u *updateNameMappingVisitor) Fields(st 
[]MappedField, fieldResults []Mappe
 
 func (u *updateNameMappingVisitor) Field(field MappedField, fieldResult 
[]MappedField) MappedField {
        if field.FieldID == nil {
-               return field
+               return MappedField{
+                       Names:  slices.Clone(field.Names),
+                       Fields: fieldResult,
+               }
        }
 
-       fieldNames := field.Names
+       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)
        }
 
        return MappedField{
-               FieldID: field.FieldID,
+               FieldID: &fieldID,
                Names:   fieldNames,
                Fields:  u.addNewFields(fieldResult, *field.FieldID),
        }
@@ -200,7 +204,8 @@ func (u *updateNameMappingVisitor) Field(field MappedField, 
fieldResult []Mapped
 func (u *updateNameMappingVisitor) removeReassignedNames(field MappedField, 
assignments map[string]int) *MappedField {
        removedNames := make(map[string]struct{})
        for _, name := range field.Names {
-               if assignedID, exists := assignments[name]; exists && 
assignedID != *field.FieldID {
+               assignedID, exists := assignments[name]
+               if exists && (field.FieldID == nil || assignedID != 
*field.FieldID) {
                        removedNames[name] = struct{}{}
                }
        }
diff --git a/name_mapping_test.go b/name_mapping_test.go
index 973225218..656bb4fa6 100644
--- a/name_mapping_test.go
+++ b/name_mapping_test.go
@@ -184,6 +184,91 @@ func TestUpdateNameMapping(t *testing.T) {
                assert.Equal(t, originalMapping, result)
        })
 
+       t.Run("result does not alias the original", func(t *testing.T) {
+               fieldID, childID, anonymousChildID := 1, 2, 3
+               parentNames := make([]string, 2)
+               parentNames[0] = "parent"
+               original := iceberg.NameMapping{
+                       {
+                               FieldID: &fieldID,
+                               Names:   parentNames[:1],
+                               Fields: []iceberg.MappedField{{
+                                       FieldID: &childID,
+                                       Names:   []string{"child"},
+                               }},
+                       },
+                       {
+                               Names: []string{"anonymous"},
+                               Fields: []iceberg.MappedField{{
+                                       FieldID: &anonymousChildID,
+                                       Names:   []string{"anonymous-child"},
+                               }},
+                       },
+               }
+
+               result, err := iceberg.UpdateNameMapping(
+                       original,
+                       map[int]iceberg.NestedField{1: {ID: 1, Name: 
"renamed-parent"}},
+                       map[int][]iceberg.NestedField{},
+               )
+               require.NoError(t, err)
+               assert.Empty(t, parentNames[1])
+
+               *result[0].FieldID = 10
+               result[0].Names[0] = "changed-parent"
+               *result[0].Fields[0].FieldID = 20
+               result[0].Fields[0].Names[0] = "changed-child"
+               result[1].Names[0] = "changed-anonymous"
+               *result[1].Fields[0].FieldID = 30
+               result[1].Fields[0].Names[0] = "changed-anonymous-child"
+
+               assert.Equal(t, 1, *original[0].FieldID)
+               assert.Equal(t, []string{"parent"}, original[0].Names)
+               assert.Equal(t, 2, *original[0].Fields[0].FieldID)
+               assert.Equal(t, []string{"child"}, original[0].Fields[0].Names)
+               assert.Equal(t, []string{"anonymous"}, original[1].Names)
+               assert.Equal(t, 3, *original[1].Fields[0].FieldID)
+               assert.Equal(t, []string{"anonymous-child"}, 
original[1].Fields[0].Names)
+       })
+
+       t.Run("update nested field under anonymous parent", func(t *testing.T) {
+               childID := 1
+               original := iceberg.NameMapping{{
+                       Names: []string{"anonymous"},
+                       Fields: []iceberg.MappedField{{
+                               FieldID: &childID,
+                               Names:   []string{"child"},
+                       }},
+               }}
+
+               result, err := iceberg.UpdateNameMapping(
+                       original,
+                       map[int]iceberg.NestedField{1: {ID: 1, Name: 
"renamed-child"}},
+                       map[int][]iceberg.NestedField{},
+               )
+               require.NoError(t, err)
+               assert.Equal(t, []string{"child", "renamed-child"}, 
result[0].Fields[0].Names)
+       })
+
+       t.Run("remove reassigned name from anonymous field", func(t *testing.T) 
{
+               fieldID := 1
+               original := iceberg.NameMapping{
+                       {Names: []string{"renamed"}},
+                       {FieldID: &fieldID, Names: []string{"original"}},
+               }
+
+               result, err := iceberg.UpdateNameMapping(
+                       original,
+                       map[int]iceberg.NestedField{1: {ID: 1, Name: 
"renamed"}},
+                       map[int][]iceberg.NestedField{},
+               )
+               require.NoError(t, err)
+               assert.Equal(t, iceberg.NameMapping{{
+                       FieldID: makeID(1),
+                       Names:   []string{"original", "renamed"},
+               }}, result)
+       })
+
        t.Run("update mapping with updates and adds", func(t *testing.T) {
                updates := map[int]iceberg.NestedField{
                        1: {ID: 1, Name: "foo_update", Type: 
&iceberg.StringType{}},

Reply via email to