laskoviymishka commented on code in PR #1641:
URL: https://github.com/apache/iceberg-go/pull/1641#discussion_r3740296400


##########
table/update_spec_test.go:
##########
@@ -568,3 +568,126 @@ func TestUpdateSpecCommit(t *testing.T) {
                assert.Nil(t, err)
        })
 }
+
+func TestUpdateSpecReuseHistoricalFieldID(t *testing.T) {
+       t.Run("re-add deleted field without a name reuses historical field ID", 
func(t *testing.T) {
+               txn := testPartitionedTable.NewTransaction()
+               specUpdate := table.NewUpdateSpec(txn, false)
+
+               _, _, err := specUpdate.
+                       RemoveField("id_identity").
+                       AddField("id", iceberg.IdentityTransform{}, "").
+                       BuildUpdates()
+               require.NoError(t, err)
+
+               newSpec, err := specUpdate.Apply()
+               require.NoError(t, err)
+
+               // The re-added field on source 1 must recycle the original 
field ID
+               // (PartitionDataIDStart == 1000) and the historical name, 
rather than
+               // allocating a brand-new field ID.
+               reAdded := newSpec.FieldsBySourceID(1)
+               require.Len(t, reAdded, 1)
+               assert.Equal(t, iceberg.PartitionDataIDStart, 
reAdded[0].FieldID)
+               assert.Equal(t, "id_identity", reAdded[0].Name)
+               assert.Equal(t, iceberg.IdentityTransform{}, 
reAdded[0].Transform)
+
+               // The untouched field must remain unchanged.
+               untouched := newSpec.FieldsBySourceID(5)
+               require.Len(t, untouched, 1)
+               assert.Equal(t, iceberg.PartitionDataIDStart+1, 
untouched[0].FieldID)
+               assert.Equal(t, "street_void", untouched[0].Name)
+       })
+
+       t.Run("re-add deleted field with matching name reuses historical field 
ID", func(t *testing.T) {
+               txn := testPartitionedTable.NewTransaction()
+               specUpdate := table.NewUpdateSpec(txn, false)
+
+               _, _, err := specUpdate.
+                       RemoveField("id_identity").
+                       AddField("id", iceberg.IdentityTransform{}, 
"id_identity").
+                       BuildUpdates()
+               require.NoError(t, err)
+
+               newSpec, err := specUpdate.Apply()
+               require.NoError(t, err)
+
+               reAdded := newSpec.FieldsBySourceID(1)
+               require.Len(t, reAdded, 1)
+               assert.Equal(t, iceberg.PartitionDataIDStart, 
reAdded[0].FieldID)
+               assert.Equal(t, "id_identity", reAdded[0].Name)
+       })
+
+       t.Run("re-add deleted field with a different name allocates a new field 
ID", func(t *testing.T) {
+               txn := testPartitionedTable.NewTransaction()
+               specUpdate := table.NewUpdateSpec(txn, false)
+
+               _, _, err := specUpdate.
+                       RemoveField("id_identity").
+                       AddField("id", iceberg.IdentityTransform{}, 
"id_renamed").
+                       BuildUpdates()
+               require.NoError(t, err)
+
+               newSpec, err := specUpdate.Apply()
+               require.NoError(t, err)
+
+               // A different explicit name must NOT reuse the historical 
field ID; a
+               // fresh field ID is allocated after the last assigned one 
(1001 -> 1002).
+               reAdded := newSpec.FieldsBySourceID(1)
+               require.Len(t, reAdded, 1)
+               assert.Equal(t, iceberg.PartitionDataIDStart+2, 
reAdded[0].FieldID)

Review Comment:
   This assertion codifies the exact divergence zeroshade flagged as the 
expected result. Per Java, a same-update remove then re-add with a different 
name is undo-delete plus rename, so this should be `PartitionDataIDStart` 
(1000) with name `"id_renamed"`, not 1002.
   
   As written the suite is green against the buggy path instead of against 
Java, so even a correct fix would fail right here. I'd flip it to 1000 once the 
impl preserves the ID.



##########
table/update_spec.go:
##########
@@ -369,11 +370,14 @@ func (us *UpdateSpec) partitionField(key transformKey, 
name string) (iceberg.Par
                }
                for _, field := range historicalFields {
                        if field.SourceID() == sourceId && 
field.Transform.String() == transformName {
-                               if len(name) > 0 && field.Name == name {
+                               // Reuse the historical field's ID when no 
explicit name is
+                               // requested (match on source + transform 
alone) or when the
+                               // requested name matches.
+                               if len(name) == 0 || field.Name == name {

Review Comment:
   On the no-name branch we return the first (source, transform) match while 
iterating specs in ascending spec-ID order, and hand back that historical 
`field.Name`. If the same source+transform carried different names across specs 
(renamed before removal), this returns the oldest name, which can be stale for 
the current schema.
   
   I'd document which spec is meant to win here and add a multi-spec test, 
which also covers zeroshade's precedence note. wdyt?



##########
table/update_spec_test.go:
##########
@@ -568,3 +568,126 @@ func TestUpdateSpecCommit(t *testing.T) {
                assert.Nil(t, err)
        })
 }
+
+func TestUpdateSpecReuseHistoricalFieldID(t *testing.T) {
+       t.Run("re-add deleted field without a name reuses historical field ID", 
func(t *testing.T) {
+               txn := testPartitionedTable.NewTransaction()
+               specUpdate := table.NewUpdateSpec(txn, false)
+
+               _, _, err := specUpdate.
+                       RemoveField("id_identity").
+                       AddField("id", iceberg.IdentityTransform{}, "").
+                       BuildUpdates()
+               require.NoError(t, err)
+
+               newSpec, err := specUpdate.Apply()
+               require.NoError(t, err)
+
+               // The re-added field on source 1 must recycle the original 
field ID
+               // (PartitionDataIDStart == 1000) and the historical name, 
rather than
+               // allocating a brand-new field ID.
+               reAdded := newSpec.FieldsBySourceID(1)
+               require.Len(t, reAdded, 1)
+               assert.Equal(t, iceberg.PartitionDataIDStart, 
reAdded[0].FieldID)
+               assert.Equal(t, "id_identity", reAdded[0].Name)
+               assert.Equal(t, iceberg.IdentityTransform{}, 
reAdded[0].Transform)

Review Comment:
   While we're here, I'd add a `LastAssignedFieldID` assertion on this reuse 
path. The early return skips the counter, so a bug that reuses the ID but still 
bumps `last-partition-id` would pass every assertion we have today. Something 
like `assert.Equal(t, iceberg.PartitionDataIDStart+1, 
newSpec.LastAssignedFieldID())` pins zeroshade's last-partition-id ask to a 
concrete check.



##########
table/update_spec_test.go:
##########
@@ -568,3 +568,126 @@ func TestUpdateSpecCommit(t *testing.T) {
                assert.Nil(t, err)
        })
 }
+
+func TestUpdateSpecReuseHistoricalFieldID(t *testing.T) {
+       t.Run("re-add deleted field without a name reuses historical field ID", 
func(t *testing.T) {
+               txn := testPartitionedTable.NewTransaction()
+               specUpdate := table.NewUpdateSpec(txn, false)
+
+               _, _, err := specUpdate.
+                       RemoveField("id_identity").
+                       AddField("id", iceberg.IdentityTransform{}, "").
+                       BuildUpdates()
+               require.NoError(t, err)
+
+               newSpec, err := specUpdate.Apply()
+               require.NoError(t, err)
+
+               // The re-added field on source 1 must recycle the original 
field ID
+               // (PartitionDataIDStart == 1000) and the historical name, 
rather than
+               // allocating a brand-new field ID.
+               reAdded := newSpec.FieldsBySourceID(1)
+               require.Len(t, reAdded, 1)
+               assert.Equal(t, iceberg.PartitionDataIDStart, 
reAdded[0].FieldID)
+               assert.Equal(t, "id_identity", reAdded[0].Name)
+               assert.Equal(t, iceberg.IdentityTransform{}, 
reAdded[0].Transform)
+
+               // The untouched field must remain unchanged.
+               untouched := newSpec.FieldsBySourceID(5)
+               require.Len(t, untouched, 1)
+               assert.Equal(t, iceberg.PartitionDataIDStart+1, 
untouched[0].FieldID)
+               assert.Equal(t, "street_void", untouched[0].Name)
+       })
+
+       t.Run("re-add deleted field with matching name reuses historical field 
ID", func(t *testing.T) {
+               txn := testPartitionedTable.NewTransaction()
+               specUpdate := table.NewUpdateSpec(txn, false)
+
+               _, _, err := specUpdate.
+                       RemoveField("id_identity").
+                       AddField("id", iceberg.IdentityTransform{}, 
"id_identity").
+                       BuildUpdates()
+               require.NoError(t, err)
+
+               newSpec, err := specUpdate.Apply()
+               require.NoError(t, err)
+
+               reAdded := newSpec.FieldsBySourceID(1)
+               require.Len(t, reAdded, 1)
+               assert.Equal(t, iceberg.PartitionDataIDStart, 
reAdded[0].FieldID)
+               assert.Equal(t, "id_identity", reAdded[0].Name)

Review Comment:
   Small consistency thing: subtests 1 and 4 assert the reused `Transform` but 
this one doesn't. Worth adding `assert.Equal(t, iceberg.IdentityTransform{}, 
reAdded[0].Transform)` so the matching-name path proves it recycles the 
transform too.



##########
table/update_spec.go:
##########
@@ -369,11 +370,14 @@ func (us *UpdateSpec) partitionField(key transformKey, 
name string) (iceberg.Par
                }
                for _, field := range historicalFields {
                        if field.SourceID() == sourceId && 
field.Transform.String() == transformName {

Review Comment:
   The comparison logic here is sound for canonical transform strings. The one 
case I wanted to flag: this is a textual match on `Transform.String()`, so a 
non-canonical string from externally-written metadata (something like 
`"bucket[016]"` vs `"bucket[16]"`) would miss a semantically identical 
historical field and allocate a fresh ID.
   
   If `ParseTransform` + `String()` round-trips canonically for every transform 
we can just note that assumption in a comment; otherwise a structural compare 
would be safer. wdyt?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to