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 c23b885d3 fix(table): reject duplicate snapshot IDs (#1592)
c23b885d3 is described below
commit c23b885d3cdb174868af47bfe11bc578bae65af4
Author: Minh Vu <[email protected]>
AuthorDate: Thu Jul 30 09:49:54 2026 +0200
fix(table): reject duplicate snapshot IDs (#1592)
## What changed
Validate snapshot ID uniqueness before constructing or checking snapshot
references. Add coverage for identical duplicate snapshots and duplicate
IDs carrying different manifest lists.
## Why
Snapshot lookup returns the first matching entry. Accepting duplicate
IDs made current-snapshot and reference resolution depend on JSON array
order.
## Testing
- `go test ./table -run
'Test(RejectDuplicateSnapshotIDs|MetadataV[123]Parsing)$' -count=1`
Signed-off-by: Minh Vu <[email protected]>
---
table/metadata.go | 16 ++++++++++++++++
table/metadata_internal_test.go | 38 ++++++++++++++++++++++++++++++++++++++
2 files changed, 54 insertions(+)
diff --git a/table/metadata.go b/table/metadata.go
index fd9e8896a..4aa87c94b 100644
--- a/table/metadata.go
+++ b/table/metadata.go
@@ -2264,6 +2264,10 @@ func (c *commonMetadata) validate() error {
return err
}
+ if err := c.checkSnapshots(); err != nil {
+ return err
+ }
+
c.constructRefs()
if err := c.checkMainRefMatchesCurrentSnapshot(); err != nil {
@@ -2277,6 +2281,18 @@ func (c *commonMetadata) validate() error {
return nil
}
+func (c *commonMetadata) checkSnapshots() error {
+ seen := make(map[int64]struct{}, len(c.SnapshotList))
+ for _, snapshot := range c.SnapshotList {
+ if _, ok := seen[snapshot.SnapshotID]; ok {
+ return fmt.Errorf("%w: duplicate snapshot ID %d",
ErrInvalidMetadata, snapshot.SnapshotID)
+ }
+ seen[snapshot.SnapshotID] = struct{}{}
+ }
+
+ return nil
+}
+
func (c *commonMetadata) NameMapping() iceberg.NameMapping {
if nameMappingJson, ok := c.Props[DefaultNameMappingKey]; ok {
nm := iceberg.NameMapping{}
diff --git a/table/metadata_internal_test.go b/table/metadata_internal_test.go
index 7aaabe31f..bfe821968 100644
--- a/table/metadata_internal_test.go
+++ b/table/metadata_internal_test.go
@@ -564,6 +564,44 @@ func TestRejectInvalidSchemaEntries(t *testing.T) {
})
}
+func TestRejectDuplicateSnapshotIDs(t *testing.T) {
+ var metadata map[string]any
+ decoder := json.NewDecoder(strings.NewReader(ExampleTableMetadataV2))
+ decoder.UseNumber()
+ require.NoError(t, decoder.Decode(&metadata))
+ snapshots := metadata["snapshots"].([]any)
+ current := snapshots[len(snapshots)-1].(map[string]any)
+
+ tests := []struct {
+ name string
+ duplicate map[string]any
+ }{
+ {name: "identical snapshot", duplicate: maps.Clone(current)},
+ {
+ name: "different manifest list",
+ duplicate: func() map[string]any {
+ duplicate := maps.Clone(current)
+ duplicate["manifest-list"] =
"s3://bucket/metadata/different.avro"
+
+ return duplicate
+ }(),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ invalid := maps.Clone(metadata)
+ invalid["snapshots"] = append(slices.Clone(snapshots),
tt.duplicate)
+ data, err := json.Marshal(invalid)
+ require.NoError(t, err)
+
+ _, err = ParseMetadataBytes(data)
+ require.ErrorIs(t, err, ErrInvalidMetadata)
+ assert.ErrorContains(t, err, "duplicate snapshot ID
3055729675574597004")
+ })
+ }
+}
+
func TestSortOrderNotFound(t *testing.T) {
metadataSortOrderNotFound := `{
"format-version": 2,