You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by at...@apache.org on 2026/08/10 09:27:33 UTC

(iceberg-go) branch main updated: fix(table): read legacy property update field names (#1725)

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 517c5dea5 fix(table): read legacy property update field names (#1725)
517c5dea5 is described below

commit 517c5dea5ddb5f4a707024004298f36ebfaa44c3
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Mon Aug 10 11:27:27 2026 +0200

    fix(table): read legacy property update field names (#1725)
    
    * fix(table): accept legacy property update fields
    
    * test(table): cover legacy property update edge cases
---
 table/updates.go      | 31 ++++++++++++++++++++++
 table/updates_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 102 insertions(+)

diff --git a/table/updates.go b/table/updates.go
index d27f8b31b..d59c66a47 100644
--- a/table/updates.go
+++ b/table/updates.go
@@ -100,6 +100,32 @@ var requiredUpdateFields = map[string][]string{
 	UpdateRemoveEncryptionKey:       {"key-id"},
 }
 
+func normalizeLegacyPropertyFields(action string, raw json.RawMessage) (json.RawMessage, error) {
+	var modern, legacy string
+	switch action {
+	case UpdateSetProperties:
+		modern, legacy = "updates", "updated"
+	case UpdateRemoveProperties:
+		modern, legacy = "removals", "removed"
+	default:
+		return raw, nil
+	}
+
+	var object map[string]json.RawMessage
+	if err := json.Unmarshal(raw, &object); err != nil {
+		return nil, err
+	}
+
+	if _, ok := object[modern]; !ok {
+		if value, ok := object[legacy]; ok {
+			object[modern] = value
+		}
+	}
+	delete(object, legacy)
+
+	return json.Marshal(object)
+}
+
 func validateRequiredUpdateFields(action string, raw json.RawMessage) error {
 	fields := requiredUpdateFields[action]
 	if len(fields) == 0 {
@@ -195,6 +221,11 @@ func (u *Updates) UnmarshalJSON(data []byte) error {
 		default:
 			return fmt.Errorf("%w: unknown update action: %s", iceberg.ErrInvalidArgument, base.ActionName)
 		}
+		normalized, err := normalizeLegacyPropertyFields(base.ActionName, raw)
+		if err != nil {
+			return err
+		}
+		raw = normalized
 		if err := validateRequiredUpdateFields(base.ActionName, raw); err != nil {
 			return err
 		}
diff --git a/table/updates_test.go b/table/updates_test.go
index f8b27f164..abf81fe63 100644
--- a/table/updates_test.go
+++ b/table/updates_test.go
@@ -549,6 +549,77 @@ func TestUnmarshalAddSpecWithoutIDUsesInitialSpecID(t *testing.T) {
 	assert.Equal(t, iceberg.InitialPartitionSpecID, update.Spec.ID())
 }
 
+func TestUnmarshalUpdatesAcceptsLegacyPropertyFields(t *testing.T) {
+	data := []byte(`[
+		{"action":"set-properties","updated":{"key":"legacy"}},
+		{"action":"remove-properties","removed":["key"]},
+		{"action":"set-properties","updated":{}},
+		{"action":"remove-properties","removed":[]},
+		{"action":"set-properties","updated":{"key":"legacy"},"updates":{"key":"modern"}},
+		{"action":"remove-properties","removed":["legacy"],"removals":["modern"]}
+	]`)
+
+	var updates Updates
+	require.NoError(t, json.Unmarshal(data, &updates))
+	assert.Equal(t, Updates{
+		NewSetPropertiesUpdate(iceberg.Properties{"key": "legacy"}),
+		NewRemovePropertiesUpdate([]string{"key"}),
+		NewSetPropertiesUpdate(iceberg.Properties{}),
+		NewRemovePropertiesUpdate([]string{}),
+		NewSetPropertiesUpdate(iceberg.Properties{"key": "modern"}),
+		NewRemovePropertiesUpdate([]string{"modern"}),
+	}, updates)
+
+	encoded, err := json.Marshal(updates)
+	require.NoError(t, err)
+	assert.JSONEq(t, `[
+		{"action":"set-properties","updates":{"key":"legacy"}},
+		{"action":"remove-properties","removals":["key"]},
+		{"action":"set-properties","updates":{}},
+		{"action":"remove-properties","removals":[]},
+		{"action":"set-properties","updates":{"key":"modern"}},
+		{"action":"remove-properties","removals":["modern"]}
+	]`, string(encoded))
+}
+
+func TestUnmarshalUpdatesRejectsNullLegacyPropertyFields(t *testing.T) {
+	tests := []struct {
+		name  string
+		data  string
+		field string
+	}{
+		{
+			name:  "set-properties legacy field",
+			data:  `{"action":"set-properties","updated":null}`,
+			field: "updates",
+		},
+		{
+			name:  "remove-properties legacy field",
+			data:  `{"action":"remove-properties","removed":null}`,
+			field: "removals",
+		},
+		{
+			name:  "set-properties canonical field takes precedence",
+			data:  `{"action":"set-properties","updated":{"key":"legacy"},"updates":null}`,
+			field: "updates",
+		},
+		{
+			name:  "remove-properties canonical field takes precedence",
+			data:  `{"action":"remove-properties","removed":["legacy"],"removals":null}`,
+			field: "removals",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var updates Updates
+			err := json.Unmarshal([]byte("["+tt.data+"]"), &updates)
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), tt.field)
+		})
+	}
+}
+
 func TestUnmarshalUpdatesReplacesExistingSlice(t *testing.T) {
 	var updates Updates
 	require.NoError(t, json.Unmarshal([]byte(`[