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/07/07 09:53:02 UTC

(iceberg-go) branch main updated: fix(table): clone selected fields in scan options (#1390)

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 058bc238 fix(table): clone selected fields in scan options (#1390)
058bc238 is described below

commit 058bc2388d161ed4aa3c8848989c337c659153ec
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Tue Jul 7 11:52:56 2026 +0200

    fix(table): clone selected fields in scan options (#1390)
    
    ## Summary
    - fix(table): clone selected fields inside WithSelectedFields
    - add regression test that mutating caller slice after option creation
    does not affect scan.selectedFields
    
    ## Testing
    - go test ./table -run TestWithSelectedFields -count=1
    - go test ./table -count=1
---
 cmd/iceberg/output.go      | 16 ++++++--
 cmd/iceberg/output_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++
 table/scan_options_test.go | 37 +++++++++++++++++
 table/table.go             |  2 +-
 4 files changed, 149 insertions(+), 5 deletions(-)

diff --git a/cmd/iceberg/output.go b/cmd/iceberg/output.go
index ef3394dd..8f20fd37 100644
--- a/cmd/iceberg/output.go
+++ b/cmd/iceberg/output.go
@@ -59,6 +59,14 @@ type Output interface {
 
 type textOutput struct{}
 
+func snapshotSchemaText(schemaID *int) string {
+	if schemaID == nil {
+		return "unknown"
+	}
+
+	return strconv.Itoa(*schemaID)
+}
+
 func (textOutput) Identifiers(idlist []table.Identifier) {
 	data := pterm.TableData{[]string{"IDs"}}
 	for _, ids := range idlist {
@@ -90,8 +98,8 @@ func (t textOutput) DescribeTable(tbl *table.Table) {
 		}
 
 		snapshotList = append(snapshotList, pterm.LeveledListItem{
-			Level: 0, Text: fmt.Sprintf("Snapshot %d, schema %d%s",
-				s.SnapshotID, *s.SchemaID, manifest),
+			Level: 0, Text: fmt.Sprintf("Snapshot %d, schema %s%s",
+				s.SnapshotID, snapshotSchemaText(s.SchemaID), manifest),
 		})
 	}
 
@@ -142,8 +150,8 @@ func (t textOutput) Files(tbl *table.Table, history bool) {
 
 		snapshotTree = append(snapshotTree, pterm.LeveledListItem{
 			Level: 0,
-			Text: fmt.Sprintf("Snapshot %d, schema %d%s",
-				snap.SnapshotID, *snap.SchemaID, manifest),
+			Text: fmt.Sprintf("Snapshot %d, schema %s%s",
+				snap.SnapshotID, snapshotSchemaText(snap.SchemaID), manifest),
 		})
 
 		afs, err := tbl.FS(context.TODO())
diff --git a/cmd/iceberg/output_test.go b/cmd/iceberg/output_test.go
index 3cb20196..4d149b90 100644
--- a/cmd/iceberg/output_test.go
+++ b/cmd/iceberg/output_test.go
@@ -19,9 +19,12 @@ package main
 
 import (
 	"bytes"
+	"context"
 	"os"
+	"strings"
 	"testing"
 
+	iceio "github.com/apache/iceberg-go/io"
 	"github.com/apache/iceberg-go/table"
 	"github.com/pterm/pterm"
 	"github.com/stretchr/testify/assert"
@@ -407,3 +410,99 @@ func Test_jsonOutput_DescribeTable(t *testing.T) {
 		})
 	}
 }
+
+func Test_textOutput_DescribeTableSnapshotWithoutSchemaID(t *testing.T) {
+	meta := `{
+    "format-version": 2,
+    "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1",
+    "location": "mem://default/table",
+    "last-sequence-number": 1,
+    "last-updated-ms": 1602638573590,
+    "last-column-id": 1,
+    "current-schema-id": 0,
+    "schemas": [
+        {"type": "struct", "schema-id": 0, "fields": [{"id": 1, "name": "id", "required": true, "type": "int"}]}
+    ],
+    "default-spec-id": 0,
+    "partition-specs": [{"spec-id": 0, "fields": []}],
+    "last-partition-id": 1000,
+    "default-sort-order-id": 0,
+    "sort-orders": [
+        {"order-id": 0, "fields": []}
+    ],
+    "properties": {},
+    "current-snapshot-id": 100,
+    "snapshots": [
+        {
+            "snapshot-id": 100,
+            "timestamp-ms": 1602638573590,
+            "sequence-number": 1,
+            "summary": {"operation": "append"}
+        }
+    ],
+    "snapshot-log": [],
+    "refs": {}
+}`
+
+	var buf bytes.Buffer
+	pterm.SetDefaultOutput(&buf)
+	pterm.DisableColor()
+
+	parsed, err := table.ParseMetadataBytes([]byte(meta))
+	assert.NoError(t, err)
+	tbl := table.New([]string{"t"}, parsed, "", nil, nil)
+
+	textOutput{}.DescribeTable(tbl)
+
+	assert.True(t, strings.Contains(buf.String(), "Snapshot 100, schema unknown"))
+}
+
+func Test_textOutput_FilesSnapshotWithoutSchemaID(t *testing.T) {
+	meta := `{
+    "format-version": 2,
+    "table-uuid": "c8e7f6d4-03fe-4693-9a96-a0705ddf69c2",
+    "location": "mem://default/table",
+    "last-sequence-number": 1,
+    "last-updated-ms": 1602638573590,
+    "last-column-id": 1,
+    "current-schema-id": 0,
+    "schemas": [
+        {"type": "struct", "schema-id": 0, "fields": [{"id": 1, "name": "id", "required": true, "type": "int"}]}
+    ],
+    "default-spec-id": 0,
+    "partition-specs": [{"spec-id": 0, "fields": []}],
+    "last-partition-id": 1000,
+    "default-sort-order-id": 0,
+    "sort-orders": [
+        {"order-id": 0, "fields": []}
+    ],
+    "properties": {},
+    "current-snapshot-id": 100,
+    "snapshots": [
+        {
+            "snapshot-id": 100,
+            "timestamp-ms": 1602638573590,
+            "sequence-number": 1,
+            "summary": {"operation": "append"}
+        }
+    ],
+    "snapshot-log": [],
+    "refs": {}
+}`
+
+	var buf bytes.Buffer
+	pterm.SetDefaultOutput(&buf)
+	pterm.DisableColor()
+
+	memFS, err := iceio.LoadFS(context.Background(), nil, "mem://default/table")
+	assert.NoError(t, err)
+	parsed, err := table.ParseMetadataBytes([]byte(meta))
+	assert.NoError(t, err)
+	tbl := table.New([]string{"db", "events"}, parsed, "", func(_ context.Context) (iceio.IO, error) {
+		return memFS, nil
+	}, nil)
+
+	textOutput{}.Files(tbl, false)
+
+	assert.True(t, strings.Contains(buf.String(), "Snapshot 100, schema unknown"))
+}
diff --git a/table/scan_options_test.go b/table/scan_options_test.go
new file mode 100644
index 00000000..ac42d8a8
--- /dev/null
+++ b/table/scan_options_test.go
@@ -0,0 +1,37 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package table
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestWithSelectedFieldsDoesNotAliasCallerSlice(t *testing.T) {
+	fields := []string{"id", "name"}
+
+	option := WithSelectedFields(fields...)
+	scan := &Scan{}
+	option(scan)
+
+	fields[0] = "updated_id"
+
+	assert.Equal(t, []string{"id", "name"}, scan.selectedFields)
+	assert.NotEqual(t, fields[0], scan.selectedFields[0])
+}
diff --git a/table/table.go b/table/table.go
index ac554bfb..b443fe79 100644
--- a/table/table.go
+++ b/table/table.go
@@ -769,7 +769,7 @@ func WithSelectedFields(fields ...string) ScanOption {
 	}
 
 	return func(scan *Scan) {
-		scan.selectedFields = fields
+		scan.selectedFields = slices.Clone(fields)
 	}
 }