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/08 07:57:48 UTC

(iceberg-go) branch main updated: fix(view): replace metadata state during JSON decoding (#1646)

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 bb959163d fix(view): replace metadata state during JSON decoding (#1646)
bb959163d is described below

commit bb959163dde5ec2a8455f795b7bbfdfb868868bc
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Sat Aug 8 09:57:42 2026 +0200

    fix(view): replace metadata state during JSON decoding (#1646)
    
    * fix(view): replace metadata state during JSON decoding
    
    * fix(view): reinitialize metadata caches after decoding
    
    * test(view): cover metadata decode reuse
---
 view/metadata.go      | 19 ++++++++---
 view/metadata_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 102 insertions(+), 5 deletions(-)

diff --git a/view/metadata.go b/view/metadata.go
index d78698858..fd280ec88 100644
--- a/view/metadata.go
+++ b/view/metadata.go
@@ -654,18 +654,27 @@ func (m *metadata) init() {
 
 func (m *metadata) UnmarshalJSON(b []byte) error {
 	type Alias metadata
-	aux := (*Alias)(m)
-
-	aux.FormatVersionValue = -1
-	aux.CurrentVersionIDValue = -1
+	next := metadata{
+		FormatVersionValue:    -1,
+		CurrentVersionIDValue: -1,
+	}
+	aux := (*Alias)(&next)
 
 	if err := json.Unmarshal(b, aux); err != nil {
 		return err
 	}
 
+	next.init()
+
+	if err := next.validate(); err != nil {
+		return err
+	}
+
+	*m = next
+	// init() rebinds the lazy-index closures to m after the value copy.
 	m.init()
 
-	return m.validate()
+	return nil
 }
 
 // NewMetadata creates a new view metadata object using the provided version, schema, location, and props,
diff --git a/view/metadata_test.go b/view/metadata_test.go
index 7a622bc07..7b46a8669 100644
--- a/view/metadata_test.go
+++ b/view/metadata_test.go
@@ -22,6 +22,7 @@ import (
 	"errors"
 	"fmt"
 	"slices"
+	"strings"
 	"testing"
 
 	"github.com/apache/iceberg-go"
@@ -143,6 +144,93 @@ func TestUnmarshalViewMetadata(t *testing.T) {
 	assert.Equal(t, iceberg.Properties{"prop": "value"}, md.Properties())
 }
 
+func TestMetadataUnmarshalReplacesReceiverState(t *testing.T) {
+	var metadata metadata
+	require.NoError(t, json.Unmarshal([]byte(exampleViewJSON), &metadata))
+
+	var reduced map[string]json.RawMessage
+	require.NoError(t, json.Unmarshal([]byte(exampleViewJSON), &reduced))
+	delete(reduced, "properties")
+	delete(reduced, "version-log")
+	reducedData, err := json.Marshal(reduced)
+	require.NoError(t, err)
+
+	require.NoError(t, json.Unmarshal(reducedData, &metadata))
+	assert.Empty(t, metadata.Props)
+	assert.Empty(t, metadata.VersionLogList)
+
+	beforeFailure, err := json.Marshal(&metadata)
+	require.NoError(t, err)
+	invalid := strings.Replace(exampleViewJSON, `"current-version-id": 1`, `"current-version-id": 99`, 1)
+	require.Error(t, json.Unmarshal([]byte(invalid), &metadata))
+	afterFailure, err := json.Marshal(&metadata)
+	require.NoError(t, err)
+	assert.Equal(t, beforeFailure, afterFailure)
+}
+
+func TestMetadataUnmarshalDoesNotMutateInputAndRoundTripsAfterReuse(t *testing.T) {
+	input := []byte(exampleViewJSON)
+	originalInput := append([]byte(nil), input...)
+
+	var md metadata
+	require.NoError(t, json.Unmarshal(input, &md))
+	assert.Equal(t, originalInput, input)
+
+	var replacement map[string]any
+	require.NoError(t, json.Unmarshal([]byte(exampleViewJSON), &replacement))
+	replacement["current-version-id"] = int64(2)
+	replacement["versions"].([]any)[0].(map[string]any)["version-id"] = int64(2)
+	replacement["versions"].([]any)[0].(map[string]any)["schema-id"] = 2
+	replacement["schemas"].([]any)[0].(map[string]any)["schema-id"] = 2
+	replacementData, err := json.Marshal(replacement)
+	require.NoError(t, err)
+	require.NoError(t, json.Unmarshal(replacementData, &md))
+
+	encoded, err := json.Marshal(&md)
+	require.NoError(t, err)
+	var roundTripped metadata
+	require.NoError(t, json.Unmarshal(encoded, &roundTripped))
+	reencoded, err := json.Marshal(&roundTripped)
+	require.NoError(t, err)
+	assert.JSONEq(t, string(encoded), string(reencoded))
+}
+
+func TestMetadataUnmarshalReplacesLookupCaches(t *testing.T) {
+	var metadata metadata
+	require.NoError(t, json.Unmarshal([]byte(exampleViewJSON), &metadata))
+
+	// Prime every lookup cache before replacing the metadata.
+	assert.Equal(t, int64(1), metadata.CurrentVersion().VersionID)
+	assert.Equal(t, 0, metadata.CurrentSchema().ID)
+	assert.Contains(t, metadata.SchemasByID(), 0)
+
+	var replacement map[string]any
+	require.NoError(t, json.Unmarshal([]byte(exampleViewJSON), &replacement))
+	replacement["current-version-id"] = int64(2)
+	replacement["versions"].([]any)[0].(map[string]any)["version-id"] = int64(2)
+	replacement["versions"].([]any)[0].(map[string]any)["schema-id"] = 2
+	replacement["schemas"].([]any)[0].(map[string]any)["schema-id"] = 2
+	replacement["version-log"].([]any)[0].(map[string]any)["version-id"] = int64(2)
+	replacementData, err := json.Marshal(replacement)
+	require.NoError(t, err)
+
+	require.NoError(t, json.Unmarshal(replacementData, &metadata))
+	assert.Equal(t, int64(2), metadata.CurrentVersion().VersionID)
+	assert.Equal(t, 2, metadata.CurrentSchema().ID)
+	assert.NotContains(t, metadata.SchemasByID(), 0)
+	assert.Contains(t, metadata.SchemasByID(), 2)
+
+	_, oldVersionExists := metadata.lazyVersionsByID()[1]
+	_, newVersionExists := metadata.lazyVersionsByID()[2]
+	assert.False(t, oldVersionExists)
+	assert.True(t, newVersionExists)
+
+	invalid := strings.Replace(string(replacementData), `"current-version-id":2`, `"current-version-id":99`, 1)
+	require.Error(t, json.Unmarshal([]byte(invalid), &metadata))
+	assert.Equal(t, int64(2), metadata.CurrentVersion().VersionID)
+	assert.Equal(t, 2, metadata.CurrentSchema().ID)
+}
+
 func TestValidMetadataDeserialization(t *testing.T) {
 	validJSON := `{
 		"view-uuid": "fa6506c3-7681-40c8-86dc-e36561f83385",