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


##########
table/update_schema.go:
##########
@@ -388,6 +388,11 @@ func (u *UpdateSchema) deleteColumn(path []string) error {
                        return fmt.Errorf("field that has updates cannot be 
deleted: %s", fullName)
                }
        }
+       for _, move := range u.moves[parentID] {

Review Comment:
   the two guards close both orderings in #1903, but they're patching entry 
points — the real silent drop is in `moveFields`, where the `!found { continue 
}` branch (~line 1370) removes the field from the working slice and then bails 
with no error. this PR makes that branch unreachable for the tested orderings, 
but it's still a landmine for any future path.
   
   and there's already one it doesn't cover: `MoveBefore(["age"], ["name"])` 
then `DeleteColumn(["age"])`. this guard only inspects `move.RelativeTo`, not 
`move.FieldID`, so neither guard fires and age's staged move is silently 
swallowed at apply (output happens to be correct, but there's no diagnostic).
   
   I'd rather fix `moveFields` to not drop on `!found` — restore the field and 
continue, or return an error — so the root is closed in one place instead of 
enumerating call-site orderings. wdyt?



##########
table/update_schema_test.go:
##########
@@ -887,6 +887,47 @@ func TestMoveColumn(t *testing.T) {
        })
 }
 
+func TestMoveRelativeToDeletedColumnRejected(t *testing.T) {

Review Comment:
   all four cases expect an error, so this proves the guards fire but not that 
they fire only when they should. I'd add a positive case — 
`DeleteColumn(["name"])` then `MoveBefore(["age"], ["id"])` should still 
succeed and land in the expected order — so an over-eager guard can't slip 
through unnoticed.



##########
table/update_schema_test.go:
##########
@@ -887,6 +887,47 @@ func TestMoveColumn(t *testing.T) {
        })
 }
 
+func TestMoveRelativeToDeletedColumnRejected(t *testing.T) {
+       tests := []struct {
+               name  string
+               build func(*UpdateSchema) *UpdateSchema
+       }{
+               {
+                       name: "delete then move before",
+                       build: func(update *UpdateSchema) *UpdateSchema {
+                               return 
update.DeleteColumn([]string{"name"}).MoveBefore([]string{"age"}, 
[]string{"name"})
+                       },
+               },
+               {
+                       name: "move before then delete",
+                       build: func(update *UpdateSchema) *UpdateSchema {
+                               return update.MoveBefore([]string{"age"}, 
[]string{"name"}).DeleteColumn([]string{"name"})
+                       },
+               },
+               {
+                       name: "delete then move after",
+                       build: func(update *UpdateSchema) *UpdateSchema {
+                               return 
update.DeleteColumn([]string{"name"}).MoveAfter([]string{"age"}, 
[]string{"name"})
+                       },
+               },
+               {
+                       name: "move after then delete",
+                       build: func(update *UpdateSchema) *UpdateSchema {
+                               return update.MoveAfter([]string{"age"}, 
[]string{"name"}).DeleteColumn([]string{"name"})
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       tbl := New([]string{"id"}, testMetadata, "", nil, nil)
+                       _, err := 
tt.build(NewUpdateSchema(tbl.NewTransaction(), true, true)).Apply()
+                       require.ErrorContains(t, err, "move target")

Review Comment:
   these assertions can't tell the two guards apart — both error messages 
contain "move target", so cases 1/3 exercise the `moveColumn` guard and 2/4 the 
`deleteColumn` guard, but all four pass on the same substring. if one guard 
broke and the other picked up the slack, this would stay green.
   
   I'd split it by the distinguishing text: assert `"cannot be deleted"` for 
the move-then-delete cases and `"has been deleted"` for the delete-then-move 
cases.



##########
table/update_schema_test.go:
##########
@@ -887,6 +887,47 @@ func TestMoveColumn(t *testing.T) {
        })
 }
 
+func TestMoveRelativeToDeletedColumnRejected(t *testing.T) {
+       tests := []struct {
+               name  string
+               build func(*UpdateSchema) *UpdateSchema
+       }{
+               {
+                       name: "delete then move before",
+                       build: func(update *UpdateSchema) *UpdateSchema {
+                               return 
update.DeleteColumn([]string{"name"}).MoveBefore([]string{"age"}, 
[]string{"name"})

Review Comment:
   these are all flat top-level fields, so the `deleteColumn` guard only ever 
scans `u.moves[-1]` (parentID resolves to the root). a nested target like 
`address.city` keys its move under `address`'s ID — a different bucket that's 
currently untested. one nested case would pin that path down.



-- 
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