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 52c1108a feat(cli): add snapshots and refs commands (#1072)
52c1108a is described below

commit 52c1108ad8719c446c9d3c1fbbab1368107051cb
Author: Tanmay Rauth <[email protected]>
AuthorDate: Wed May 13 12:36:42 2026 -0700

    feat(cli): add snapshots and refs commands (#1072)
    
    Add `iceberg snapshots TABLE_ID` to list snapshots with ID, timestamp,
    parent, operation, and file counts. Add `iceberg refs TABLE_ID` to list
    snapshot refs with --type filter.
    
    
    Depends On: #1073
---
 cmd/iceberg/snapshots.go      | 174 ++++++++++++++++++++++++++++--
 cmd/iceberg/snapshots_test.go | 240 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 403 insertions(+), 11 deletions(-)

diff --git a/cmd/iceberg/snapshots.go b/cmd/iceberg/snapshots.go
index d47ae656..aebd68c0 100644
--- a/cmd/iceberg/snapshots.go
+++ b/cmd/iceberg/snapshots.go
@@ -19,24 +19,176 @@ package main
 
 import (
        "context"
-       "errors"
+       "encoding/json"
        "os"
+       "strconv"
+       "time"
 
        "github.com/apache/iceberg-go/catalog"
        "github.com/apache/iceberg-go/table"
+       "github.com/pterm/pterm"
 )
 
-func runSnapshots(_ context.Context, output Output, _ catalog.Catalog, _ 
*SnapshotsCmd) {
-       output.Error(errors.New("snapshots: not yet implemented"))
-       os.Exit(1)
+func runSnapshots(ctx context.Context, output Output, cat catalog.Catalog, cmd 
*SnapshotsCmd) {
+       tbl := loadTable(ctx, output, cat, cmd.TableID)
+       output.Snapshots(tbl)
 }
 
-func runRefs(_ context.Context, output Output, _ catalog.Catalog, _ *RefsCmd) {
-       output.Error(errors.New("refs: not yet implemented"))
-       os.Exit(1)
+func runRefs(ctx context.Context, output Output, cat catalog.Catalog, cmd 
*RefsCmd) {
+       tbl := loadTable(ctx, output, cat, cmd.TableID)
+       output.Refs(tbl, cmd.Type)
 }
 
-func (textOutput) Snapshots(_ *table.Table)      {}
-func (jsonOutput) Snapshots(_ *table.Table)      {}
-func (textOutput) Refs(_ *table.Table, _ string) {}
-func (jsonOutput) Refs(_ *table.Table, _ string) {}
+func buildSnapshotEntries(tbl *table.Table) []SnapshotEntry {
+       snapshots := tbl.Metadata().Snapshots()
+       entries := make([]SnapshotEntry, 0, len(snapshots))
+
+       for i := len(snapshots) - 1; i >= 0; i-- {
+               s := snapshots[i]
+
+               op := ""
+               addedFiles := "-"
+               deletedFiles := "-"
+
+               if s.Summary != nil {
+                       op = string(s.Summary.Operation)
+                       if v, ok := s.Summary.Properties["added-data-files"]; 
ok {
+                               addedFiles = v
+                       }
+                       if v, ok := s.Summary.Properties["deleted-data-files"]; 
ok {
+                               deletedFiles = v
+                       }
+               }
+
+               entries = append(entries, SnapshotEntry{
+                       SnapshotID:       s.SnapshotID,
+                       Timestamp:        
time.UnixMilli(s.TimestampMs).UTC().Format(time.RFC3339),
+                       ParentSnapshotID: s.ParentSnapshotID,
+                       Operation:        op,
+                       AddedDataFiles:   addedFiles,
+                       DeletedDataFiles: deletedFiles,
+               })
+       }
+
+       return entries
+}
+
+func buildRefEntries(tbl *table.Table, filterType string) []RefEntry {
+       var entries []RefEntry
+
+       for name, ref := range tbl.Metadata().Refs() {
+               refType := string(ref.SnapshotRefType)
+               if filterType != "" && refType != filterType {
+                       continue
+               }
+
+               entries = append(entries, RefEntry{
+                       Name:               name,
+                       Type:               refType,
+                       SnapshotID:         ref.SnapshotID,
+                       MaxRefAgeMs:        ref.MaxRefAgeMs,
+                       MaxSnapshotAgeMs:   ref.MaxSnapshotAgeMs,
+                       MinSnapshotsToKeep: ref.MinSnapshotsToKeep,
+               })
+       }
+
+       return entries
+}
+
+func (t textOutput) Snapshots(tbl *table.Table) {
+       entries := buildSnapshotEntries(tbl)
+
+       if len(entries) == 0 {
+               pterm.Println("No snapshots found.")
+
+               return
+       }
+
+       data := pterm.TableData{{"SNAPSHOT ID", "TIMESTAMP", "PARENT", "OP", 
"+FILES", "-FILES"}}
+
+       for _, e := range entries {
+               parent := "-"
+               if e.ParentSnapshotID != nil {
+                       parent = strconv.FormatInt(*e.ParentSnapshotID, 10)
+               }
+
+               data = append(data, []string{
+                       strconv.FormatInt(e.SnapshotID, 10),
+                       e.Timestamp,
+                       parent,
+                       e.Operation,
+                       e.AddedDataFiles,
+                       e.DeletedDataFiles,
+               })
+       }
+
+       pterm.DefaultTable.
+               WithHasHeader(true).
+               WithHeaderRowSeparator("-").
+               WithData(data).Render()
+}
+
+func (j jsonOutput) Snapshots(tbl *table.Table) {
+       entries := buildSnapshotEntries(tbl)
+
+       result := struct {
+               Table     string          `json:"table"`
+               Snapshots []SnapshotEntry `json:"snapshots"`
+       }{
+               Table:     tableIDString(tbl),
+               Snapshots: entries,
+       }
+
+       if err := json.NewEncoder(os.Stdout).Encode(result); err != nil {
+               j.Error(err)
+       }
+}
+
+func (t textOutput) Refs(tbl *table.Table, filterType string) {
+       entries := buildRefEntries(tbl, filterType)
+
+       if len(entries) == 0 {
+               pterm.Println("No refs found.")
+
+               return
+       }
+
+       data := pterm.TableData{{"NAME", "TYPE", "SNAPSHOT ID", "MAX REF AGE", 
"MAX SNAP AGE", "MIN SNAPS"}}
+
+       for _, e := range entries {
+               minSnaps := "-"
+               if e.MinSnapshotsToKeep != nil {
+                       minSnaps = strconv.Itoa(*e.MinSnapshotsToKeep)
+               }
+
+               data = append(data, []string{
+                       e.Name,
+                       e.Type,
+                       strconv.FormatInt(e.SnapshotID, 10),
+                       formatDurationMs(e.MaxRefAgeMs),
+                       formatDurationMs(e.MaxSnapshotAgeMs),
+                       minSnaps,
+               })
+       }
+
+       pterm.DefaultTable.
+               WithHasHeader(true).
+               WithHeaderRowSeparator("-").
+               WithData(data).Render()
+}
+
+func (j jsonOutput) Refs(tbl *table.Table, filterType string) {
+       entries := buildRefEntries(tbl, filterType)
+
+       result := struct {
+               Table string     `json:"table"`
+               Refs  []RefEntry `json:"refs"`
+       }{
+               Table: tableIDString(tbl),
+               Refs:  entries,
+       }
+
+       if err := json.NewEncoder(os.Stdout).Encode(result); err != nil {
+               j.Error(err)
+       }
+}
diff --git a/cmd/iceberg/snapshots_test.go b/cmd/iceberg/snapshots_test.go
new file mode 100644
index 00000000..74376836
--- /dev/null
+++ b/cmd/iceberg/snapshots_test.go
@@ -0,0 +1,240 @@
+// 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 main
+
+import (
+       "bytes"
+       "os"
+       "testing"
+
+       "github.com/apache/iceberg-go/table"
+       "github.com/pterm/pterm"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+const snapshotsTestMetadata = `{
+    "format-version": 2,
+    "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1",
+    "location": "s3://bucket/test/location",
+    "last-sequence-number": 2,
+    "last-updated-ms": 1602638573590,
+    "last-column-id": 3,
+    "current-schema-id": 0,
+    "schemas": [
+        {"type": "struct", "schema-id": 0, "fields": [{"id": 1, "name": "x", 
"required": true, "type": "long"}]}
+    ],
+    "default-spec-id": 0,
+    "partition-specs": [{"spec-id": 0, "fields": []}],
+    "last-partition-id": 0,
+    "default-sort-order-id": 0,
+    "sort-orders": [{"order-id": 0, "fields": []}],
+    "properties": {},
+    "current-snapshot-id": 2000000000000000002,
+    "snapshots": [
+        {
+            "snapshot-id": 2000000000000000001,
+            "timestamp-ms": 1615100955770,
+            "sequence-number": 1,
+            "summary": {"operation": "append", "added-data-files": "3", 
"deleted-data-files": "0"},
+            "manifest-list": "s3://a/b/1.avro",
+            "schema-id": 0
+        },
+        {
+            "snapshot-id": 2000000000000000002,
+            "parent-snapshot-id": 2000000000000000001,
+            "timestamp-ms": 1625100955770,
+            "sequence-number": 2,
+            "summary": {"operation": "overwrite", "added-data-files": "1", 
"deleted-data-files": "2"},
+            "manifest-list": "s3://a/b/2.avro",
+            "schema-id": 0
+        }
+    ],
+    "snapshot-log": [],
+    "metadata-log": [],
+    "refs": {
+        "main": {"snapshot-id": 2000000000000000002, "type": "branch"},
+        "v1.0": {"snapshot-id": 2000000000000000001, "type": "tag", 
"max-ref-age-ms": 86400000}
+    }
+}`
+
+func TestBuildSnapshotEntries(t *testing.T) {
+       meta, err := table.ParseMetadataBytes([]byte(snapshotsTestMetadata))
+       require.NoError(t, err)
+
+       tbl := table.New([]string{"db", "events"}, meta, "", nil, nil)
+       entries := buildSnapshotEntries(tbl)
+
+       require.Len(t, entries, 2)
+
+       assert.Equal(t, int64(2000000000000000002), entries[0].SnapshotID)
+       assert.Equal(t, "overwrite", entries[0].Operation)
+       assert.Equal(t, "1", entries[0].AddedDataFiles)
+       assert.Equal(t, "2", entries[0].DeletedDataFiles)
+       require.NotNil(t, entries[0].ParentSnapshotID)
+       assert.Equal(t, int64(2000000000000000001), 
*entries[0].ParentSnapshotID)
+
+       assert.Equal(t, int64(2000000000000000001), entries[1].SnapshotID)
+       assert.Equal(t, "append", entries[1].Operation)
+       assert.Equal(t, "3", entries[1].AddedDataFiles)
+       assert.Nil(t, entries[1].ParentSnapshotID)
+}
+
+func TestBuildSnapshotEntriesNilSummary(t *testing.T) {
+       const metadata = `{
+        "format-version": 2,
+        "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1",
+        "location": "s3://bucket/test/location",
+        "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": "x", "required": true, "type": "long"}]}],
+        "default-spec-id": 0,
+        "partition-specs": [{"spec-id": 0, "fields": []}],
+        "last-partition-id": 0,
+        "default-sort-order-id": 0,
+        "sort-orders": [{"order-id": 0, "fields": []}],
+        "properties": {},
+        "current-snapshot-id": 1001,
+        "snapshots": [{"snapshot-id": 1001, "timestamp-ms": 1615100955770, 
"sequence-number": 1, "manifest-list": "s3://a/b/1.avro", "schema-id": 0}],
+        "snapshot-log": [],
+        "metadata-log": [],
+        "refs": {"main": {"snapshot-id": 1001, "type": "branch"}}
+    }`
+
+       meta, err := table.ParseMetadataBytes([]byte(metadata))
+       require.NoError(t, err)
+
+       tbl := table.New([]string{"db", "tbl"}, meta, "", nil, nil)
+       entries := buildSnapshotEntries(tbl)
+
+       require.Len(t, entries, 1)
+       assert.Equal(t, "", entries[0].Operation)
+       assert.Equal(t, "-", entries[0].AddedDataFiles)
+       assert.Equal(t, "-", entries[0].DeletedDataFiles)
+}
+
+func TestBuildRefEntries(t *testing.T) {
+       meta, err := table.ParseMetadataBytes([]byte(snapshotsTestMetadata))
+       require.NoError(t, err)
+
+       tbl := table.New([]string{"db", "events"}, meta, "", nil, nil)
+
+       all := buildRefEntries(tbl, "")
+       assert.Len(t, all, 2)
+
+       branches := buildRefEntries(tbl, "branch")
+       assert.Len(t, branches, 1)
+       assert.Equal(t, "main", branches[0].Name)
+
+       tags := buildRefEntries(tbl, "tag")
+       assert.Len(t, tags, 1)
+       assert.Equal(t, "v1.0", tags[0].Name)
+       require.NotNil(t, tags[0].MaxRefAgeMs)
+       assert.Equal(t, int64(86400000), *tags[0].MaxRefAgeMs)
+}
+
+func TestTextOutputSnapshots(t *testing.T) {
+       var buf bytes.Buffer
+       pterm.SetDefaultOutput(&buf)
+       pterm.DisableColor()
+
+       meta, err := table.ParseMetadataBytes([]byte(snapshotsTestMetadata))
+       require.NoError(t, err)
+
+       tbl := table.New([]string{"db", "events"}, meta, "", nil, nil)
+       buf.Reset()
+
+       textOutput{}.Snapshots(tbl)
+
+       output := buf.String()
+       assert.Contains(t, output, "SNAPSHOT ID")
+       assert.Contains(t, output, "TIMESTAMP")
+       assert.Contains(t, output, "PARENT")
+       assert.Contains(t, output, "OP")
+       assert.Contains(t, output, "2000000000000000002")
+       assert.Contains(t, output, "overwrite")
+}
+
+func TestJSONOutputSnapshots(t *testing.T) {
+       oldStdout := os.Stdout
+       r, w, _ := os.Pipe()
+       os.Stdout = w
+       defer func() { os.Stdout = oldStdout }()
+
+       meta, err := table.ParseMetadataBytes([]byte(snapshotsTestMetadata))
+       require.NoError(t, err)
+
+       tbl := table.New([]string{"db", "events"}, meta, "", nil, nil)
+
+       jsonOutput{}.Snapshots(tbl)
+
+       w.Close()
+       var buf bytes.Buffer
+       _, _ = buf.ReadFrom(r)
+
+       output := buf.String()
+       assert.Contains(t, output, `"table":"db.events"`)
+       assert.Contains(t, output, `"snapshot_id":2000000000000000002`)
+       assert.Contains(t, output, `"operation":"overwrite"`)
+}
+
+func TestTextOutputRefs(t *testing.T) {
+       var buf bytes.Buffer
+       pterm.SetDefaultOutput(&buf)
+       pterm.DisableColor()
+
+       meta, err := table.ParseMetadataBytes([]byte(snapshotsTestMetadata))
+       require.NoError(t, err)
+
+       tbl := table.New([]string{"db", "events"}, meta, "", nil, nil)
+       buf.Reset()
+
+       textOutput{}.Refs(tbl, "")
+
+       output := buf.String()
+       assert.Contains(t, output, "NAME")
+       assert.Contains(t, output, "TYPE")
+       assert.Contains(t, output, "SNAPSHOT ID")
+       assert.Contains(t, output, "main")
+       assert.Contains(t, output, "v1.0")
+       assert.Contains(t, output, "24h")
+}
+
+func TestJSONOutputRefs(t *testing.T) {
+       oldStdout := os.Stdout
+       r, w, _ := os.Pipe()
+       os.Stdout = w
+       defer func() { os.Stdout = oldStdout }()
+
+       meta, err := table.ParseMetadataBytes([]byte(snapshotsTestMetadata))
+       require.NoError(t, err)
+
+       tbl := table.New([]string{"db", "events"}, meta, "", nil, nil)
+
+       jsonOutput{}.Refs(tbl, "")
+
+       w.Close()
+       var buf bytes.Buffer
+       _, _ = buf.ReadFrom(r)
+
+       output := buf.String()
+       assert.Contains(t, output, `"table":"db.events"`)
+       assert.Contains(t, output, `"refs":[`)
+}

Reply via email to