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{}},