From 10001a1826d001d29fd266faa067acc8377ab91d Mon Sep 17 00:00:00 2001 From: kanthi subramanian Date: Fri, 8 May 2026 14:50:56 -0500 Subject: [PATCH 1/4] Added CLI command to list snapshots --- ice/README.md | 6 ++ .../main/java/com/altinity/ice/cli/Main.java | 25 ++++++ .../ice/cli/internal/cmd/ListSnapshots.java | 86 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java diff --git a/ice/README.md b/ice/README.md index 6968712..d33f837 100644 --- a/ice/README.md +++ b/ice/README.md @@ -165,6 +165,12 @@ ice files flowers.iris # list partitions ice list-partitions nyc.taxis_p_by_day +# list current and previous snapshots +ice list-snapshots flowers.iris + +# only the latest 5 +ice list-snapshots flowers.iris --limit 5 + # describe a parquet file directly ice describe-parquet file://iris.parquet ``` diff --git a/ice/src/main/java/com/altinity/ice/cli/Main.java b/ice/src/main/java/com/altinity/ice/cli/Main.java index 7d8753f..af2181c 100644 --- a/ice/src/main/java/com/altinity/ice/cli/Main.java +++ b/ice/src/main/java/com/altinity/ice/cli/Main.java @@ -25,6 +25,7 @@ import com.altinity.ice.cli.internal.cmd.InsertWatch; import com.altinity.ice.cli.internal.cmd.ListNamespaces; import com.altinity.ice.cli.internal.cmd.ListPartitions; +import com.altinity.ice.cli.internal.cmd.ListSnapshots; import com.altinity.ice.cli.internal.cmd.ListTables; import com.altinity.ice.cli.internal.cmd.Scan; import com.altinity.ice.cli.internal.config.Config; @@ -733,6 +734,30 @@ void listPartitions( } } + @CommandLine.Command( + name = "list-snapshots", + description = "List current and previous snapshots of a table.") + void listSnapshots( + @CommandLine.Parameters( + arity = "1", + paramLabel = "", + description = "Table name (e.g. ns1.table1)") + String name, + @CommandLine.Option( + names = {"--limit"}, + description = "Show only the most recent N snapshots (0 = all)", + defaultValue = "0") + int limit, + @CommandLine.Option( + names = {"--json"}, + description = "Output JSON instead of YAML") + boolean json) + throws IOException { + try (RESTCatalog catalog = loadCatalog()) { + ListSnapshots.run(catalog, TableIdentifier.parse(name), json, limit); + } + } + @CommandLine.Command(name = "delete-table", description = "Delete table.") void deleteTable( @CommandLine.Parameters( diff --git a/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java new file mode 100644 index 0000000..ed2017c --- /dev/null +++ b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 Altinity Inc and/or its affiliates. All rights reserved. + * + * Licensed 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 + */ +package com.altinity.ice.cli.internal.cmd; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.rest.RESTCatalog; + +public final class ListSnapshots { + + private ListSnapshots() {} + + public static void run(RESTCatalog catalog, TableIdentifier tableId, boolean json, int limit) + throws IOException { + Table table = catalog.loadTable(tableId); + Long currentSnapshotId = + table.currentSnapshot() != null ? table.currentSnapshot().snapshotId() : null; + + List rows = new ArrayList<>(); + for (Snapshot snapshot : table.snapshots()) { + Long parentId = snapshot.parentId(); + rows.add( + new SnapshotInfo( + snapshot.snapshotId(), + parentId, + snapshot.sequenceNumber(), + snapshot.timestampMillis(), + Instant.ofEpochMilli(snapshot.timestampMillis()).toString(), + snapshot.operation(), + currentSnapshotId != null && snapshot.snapshotId() == currentSnapshotId, + snapshot.summary(), + snapshot.manifestListLocation())); + } + + rows.sort(Comparator.comparingLong(SnapshotInfo::timestampMillis)); + + if (limit > 0 && rows.size() > limit) { + rows = new ArrayList<>(rows.subList(rows.size() - limit, rows.size())); + } + + var result = new Result(tableId.toString(), currentSnapshotId, rows); + output(result, json); + } + + private static void output(Result result, boolean json) throws IOException { + ObjectMapper mapper = + json + ? new ObjectMapper() + : new ObjectMapper(new YAMLFactory().enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + System.out.println(mapper.writeValueAsString(result)); + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + record Result(String table, Long currentSnapshotId, List snapshots) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + record SnapshotInfo( + long snapshotId, + Long parentId, + long sequenceNumber, + long timestampMillis, + String timestamp, + String operation, + boolean current, + Map summary, + String manifestListLocation) {} +} From 088022ce5461aa8916978dfe850e750296b5db03 Mon Sep 17 00:00:00 2001 From: kanthi subramanian Date: Mon, 11 May 2026 11:46:55 -0500 Subject: [PATCH 2/4] Move the list snapshots logic to a common function. --- .../cli/internal/cmd/DescribeMetadata.java | 17 ++++++--- .../ice/cli/internal/cmd/ListSnapshots.java | 37 +++---------------- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/ice/src/main/java/com/altinity/ice/cli/internal/cmd/DescribeMetadata.java b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/DescribeMetadata.java index 6ffff29..9bf0757 100644 --- a/ice/src/main/java/com/altinity/ice/cli/internal/cmd/DescribeMetadata.java +++ b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/DescribeMetadata.java @@ -76,7 +76,9 @@ private static MetadataInfo extractMetadataInfo( List snapshots = null; if (includeAll || optionsSet.contains(Option.SNAPSHOTS)) { - snapshots = extractSnapshots(metadata); + Snapshot cur = metadata.currentSnapshot(); + Long currentSnapshotId = cur != null ? cur.snapshotId() : null; + snapshots = extractSnapshots(metadata.snapshots(), currentSnapshotId); } HistoryInfo history = null; @@ -125,10 +127,11 @@ private static SchemaInfo extractSchema(TableMetadata metadata) { return new SchemaInfo(schema.schemaId(), fields); } - private static List extractSnapshots(TableMetadata metadata) { - List snapshots = new ArrayList<>(); - for (Snapshot snapshot : metadata.snapshots()) { - snapshots.add( + public static List extractSnapshots( + Iterable snapshots, Long currentSnapshotId) { + List result = new ArrayList<>(); + for (Snapshot snapshot : snapshots) { + result.add( new SnapshotInfo( snapshot.snapshotId(), snapshot.parentId(), @@ -136,10 +139,11 @@ private static List extractSnapshots(TableMetadata metadata) { snapshot.timestampMillis(), Instant.ofEpochMilli(snapshot.timestampMillis()).toString(), snapshot.operation(), + currentSnapshotId != null && snapshot.snapshotId() == currentSnapshotId, snapshot.summary(), snapshot.manifestListLocation())); } - return snapshots; + return result; } private static HistoryInfo extractHistory(TableMetadata metadata) { @@ -266,6 +270,7 @@ public record SnapshotInfo( long timestampMillis, String timestamp, String operation, + boolean current, Map summary, String manifestListLocation) {} diff --git a/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java index ed2017c..c21f6e2 100644 --- a/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java +++ b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java @@ -14,12 +14,9 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; import java.io.IOException; -import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Map; -import org.apache.iceberg.Snapshot; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.rest.RESTCatalog; @@ -34,23 +31,10 @@ public static void run(RESTCatalog catalog, TableIdentifier tableId, boolean jso Long currentSnapshotId = table.currentSnapshot() != null ? table.currentSnapshot().snapshotId() : null; - List rows = new ArrayList<>(); - for (Snapshot snapshot : table.snapshots()) { - Long parentId = snapshot.parentId(); - rows.add( - new SnapshotInfo( - snapshot.snapshotId(), - parentId, - snapshot.sequenceNumber(), - snapshot.timestampMillis(), - Instant.ofEpochMilli(snapshot.timestampMillis()).toString(), - snapshot.operation(), - currentSnapshotId != null && snapshot.snapshotId() == currentSnapshotId, - snapshot.summary(), - snapshot.manifestListLocation())); - } + List rows = + DescribeMetadata.extractSnapshots(table.snapshots(), currentSnapshotId); - rows.sort(Comparator.comparingLong(SnapshotInfo::timestampMillis)); + rows.sort(Comparator.comparingLong(DescribeMetadata.SnapshotInfo::timestampMillis)); if (limit > 0 && rows.size() > limit) { rows = new ArrayList<>(rows.subList(rows.size() - limit, rows.size())); @@ -70,17 +54,6 @@ private static void output(Result result, boolean json) throws IOException { } @JsonInclude(JsonInclude.Include.NON_NULL) - record Result(String table, Long currentSnapshotId, List snapshots) {} - - @JsonInclude(JsonInclude.Include.NON_NULL) - record SnapshotInfo( - long snapshotId, - Long parentId, - long sequenceNumber, - long timestampMillis, - String timestamp, - String operation, - boolean current, - Map summary, - String manifestListLocation) {} + record Result( + String table, Long currentSnapshotId, List snapshots) {} } From 00d5b7adafbd3f7cfec16dc1c635302ade712992 Mon Sep 17 00:00:00 2001 From: kanthi subramanian Date: Tue, 12 May 2026 11:57:41 -0500 Subject: [PATCH 3/4] Add list snapshots to basic operations scenarios --- .../scenarios/basic-operations/run.sh.tmpl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ice-rest-catalog/src/test/resources/scenarios/basic-operations/run.sh.tmpl b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/run.sh.tmpl index de9184a..a5c4f49 100644 --- a/ice-rest-catalog/src/test/resources/scenarios/basic-operations/run.sh.tmpl +++ b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/run.sh.tmpl @@ -208,6 +208,23 @@ for t in ${TABLE_IRIS} ${TABLE_PARTITIONED} ${TABLE_SORTED}; do done echo "OK list-tables listed tables in ${NAMESPACE_NAME}" +# List snapshots for the iris table +{{ICE_CLI}} --config {{CLI_CONFIG}} list-snapshots ${TABLE_IRIS} > /tmp/basic_ops_list_snapshots.txt +if ! grep -q "snapshotId" /tmp/basic_ops_list_snapshots.txt; then + echo "FAIL: list-snapshots output missing 'snapshotId'" + cat /tmp/basic_ops_list_snapshots.txt + exit 1 +fi +echo "OK list-snapshots verified for ${TABLE_IRIS}" + +# List snapshots with --limit 1 +{{ICE_CLI}} --config {{CLI_CONFIG}} list-snapshots ${TABLE_IRIS} --limit 1 > /tmp/basic_ops_list_snapshots_limit.txt +if ! grep -q "snapshotId" /tmp/basic_ops_list_snapshots_limit.txt; then + echo "FAIL: list-snapshots --limit 1 output missing 'snapshotId'" + cat /tmp/basic_ops_list_snapshots_limit.txt + exit 1 +fi +echo "OK list-snapshots --limit 1 verified for ${TABLE_IRIS}" # Cleanup tables then namespace {{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE_IRIS} From 51e35edc97f8069e42959b024fbf451c5bba1391 Mon Sep 17 00:00:00 2001 From: Andrew Xie Date: Fri, 15 May 2026 12:32:12 -0400 Subject: [PATCH 4/4] Use TreePrinter for list-snapshots --- .../ice/cli/internal/cmd/ListSnapshots.java | 82 +++++++++++++++++-- .../ice/cli/internal/util/TreePrinter.java | 29 ++++++- 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java index c21f6e2..aa9a27b 100644 --- a/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java +++ b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java @@ -9,6 +9,7 @@ */ package com.altinity.ice.cli.internal.cmd; +import com.altinity.ice.cli.internal.util.TreePrinter; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -16,7 +17,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.rest.RESTCatalog; @@ -40,17 +45,76 @@ public static void run(RESTCatalog catalog, TableIdentifier tableId, boolean jso rows = new ArrayList<>(rows.subList(rows.size() - limit, rows.size())); } - var result = new Result(tableId.toString(), currentSnapshotId, rows); - output(result, json); + if (json) { + var result = new Result(tableId.toString(), currentSnapshotId, rows); + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + System.out.println(mapper.writeValueAsString(result)); + return; + } + + printTree(tableId.toString(), currentSnapshotId, rows); } - private static void output(Result result, boolean json) throws IOException { - ObjectMapper mapper = - json - ? new ObjectMapper() - : new ObjectMapper(new YAMLFactory().enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - System.out.println(mapper.writeValueAsString(result)); + private static void printTree( + String tableName, Long currentSnapshotId, List rows) + throws IOException { + StringBuilder rootLabel = new StringBuilder(); + rootLabel.append("table: ").append(tableName); + if (currentSnapshotId != null) { + rootLabel.append("\ncurrentSnapshotId: ").append(currentSnapshotId); + } + + if (rows.isEmpty()) { + TreePrinter.print(new TreePrinter.Node(rootLabel.toString(), List.of())); + System.out.println("(no snapshots)"); + return; + } + + ObjectMapper yamlMapper = + new ObjectMapper( + new YAMLFactory() + .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)); + yamlMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + Set presentIds = new HashSet<>(rows.size()); + for (DescribeMetadata.SnapshotInfo info : rows) { + presentIds.add(info.snapshotId()); + } + + Map> childrenByParent = new HashMap<>(); + List roots = new ArrayList<>(); + for (DescribeMetadata.SnapshotInfo info : rows) { + Long parentId = info.parentId(); + if (parentId == null || !presentIds.contains(parentId)) { + roots.add(info); + } else { + childrenByParent.computeIfAbsent(parentId, k -> new ArrayList<>()).add(info); + } + } + + List rootChildren = new ArrayList<>(roots.size()); + for (DescribeMetadata.SnapshotInfo root : roots) { + rootChildren.add(buildNode(root, childrenByParent, yamlMapper)); + } + + TreePrinter.print(new TreePrinter.Node(rootLabel.toString(), rootChildren)); + } + + private static TreePrinter.Node buildNode( + DescribeMetadata.SnapshotInfo info, + Map> childrenByParent, + ObjectMapper yamlMapper) + throws IOException { + List children = + childrenByParent.getOrDefault(info.snapshotId(), List.of()); + List childNodes = new ArrayList<>(children.size()); + + for (DescribeMetadata.SnapshotInfo child : children) { + childNodes.add(buildNode(child, childrenByParent, yamlMapper)); + } + return new TreePrinter.Node(yamlMapper.writeValueAsString(info), childNodes); } @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/ice/src/main/java/com/altinity/ice/cli/internal/util/TreePrinter.java b/ice/src/main/java/com/altinity/ice/cli/internal/util/TreePrinter.java index 82e87a7..2d1558a 100644 --- a/ice/src/main/java/com/altinity/ice/cli/internal/util/TreePrinter.java +++ b/ice/src/main/java/com/altinity/ice/cli/internal/util/TreePrinter.java @@ -10,20 +10,35 @@ package com.altinity.ice.cli.internal.util; import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public final class TreePrinter { private TreePrinter() {} - public record Node(String label, List children) { + public record Node(List label, List children) { public Node(String label) { - this(label, List.of()); + this(splitLines(label), List.of()); + } + + public Node(String label, List children) { + this(splitLines(label), children); } public Node { + label = List.copyOf(label); children = List.copyOf(children); } + + private static List splitLines(String label) { + List lines = new ArrayList<>(Arrays.asList(label.split("\n", -1))); + while (lines.size() > 1 && lines.getLast().isEmpty()) { + lines.removeLast(); + } + return lines; + } } public static void print(Node root) { @@ -31,7 +46,9 @@ public static void print(Node root) { } public static void print(Node root, PrintStream out) { - out.println(root.label()); + for (String line : root.label()) { + out.println(line); + } printChildren(root.children(), "", out); } @@ -41,7 +58,11 @@ private static void printChildren(List children, String descendantIndent, boolean isLast = (i == children.size() - 1); String connector = isLast ? "└── " : "├── "; String childDescendantIndent = descendantIndent + (isLast ? " " : "│ "); - out.println(descendantIndent + connector + child.label()); + List lines = child.label(); + out.println(descendantIndent + connector + lines.getFirst()); + for (int j = 1; j < lines.size(); j++) { + out.println(childDescendantIndent + lines.get(j)); + } printChildren(child.children(), childDescendantIndent, out); } }