Skip to content

Commit 84a7efc

Browse files
subkanthixieandrew
andauthored
feat: Add list-snapshots command (#163)
Co-authored-by: Andrew Xie <dev@xie.is>
1 parent ce8c59b commit 84a7efc

6 files changed

Lines changed: 207 additions & 10 deletions

File tree

ice-rest-catalog/src/test/resources/scenarios/basic-operations/run.sh.tmpl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,23 @@ for t in ${TABLE_IRIS} ${TABLE_PARTITIONED} ${TABLE_SORTED}; do
212212
done
213213
echo "OK list-tables listed tables in ${NAMESPACE_NAME}"
214214

215+
# List snapshots for the iris table
216+
{{ICE_CLI}} --config {{CLI_CONFIG}} list-snapshots ${TABLE_IRIS} > /tmp/basic_ops_list_snapshots.txt
217+
if ! grep -q "snapshotId" /tmp/basic_ops_list_snapshots.txt; then
218+
echo "FAIL: list-snapshots output missing 'snapshotId'"
219+
cat /tmp/basic_ops_list_snapshots.txt
220+
exit 1
221+
fi
222+
echo "OK list-snapshots verified for ${TABLE_IRIS}"
223+
224+
# List snapshots with --limit 1
225+
{{ICE_CLI}} --config {{CLI_CONFIG}} list-snapshots ${TABLE_IRIS} --limit 1 > /tmp/basic_ops_list_snapshots_limit.txt
226+
if ! grep -q "snapshotId" /tmp/basic_ops_list_snapshots_limit.txt; then
227+
echo "FAIL: list-snapshots --limit 1 output missing 'snapshotId'"
228+
cat /tmp/basic_ops_list_snapshots_limit.txt
229+
exit 1
230+
fi
231+
echo "OK list-snapshots --limit 1 verified for ${TABLE_IRIS}"
215232

216233
# Cleanup tables then namespace
217234
{{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE_IRIS}

ice/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ ice files flowers.iris
184184
# list partitions
185185
ice list-partitions nyc.taxis_p_by_day
186186

187+
# list current and previous snapshots
188+
ice list-snapshots flowers.iris
189+
190+
# only the latest 5
191+
ice list-snapshots flowers.iris --limit 5
192+
187193
# describe a parquet file directly
188194
ice describe-parquet file://iris.parquet
189195
```

ice/src/main/java/com/altinity/ice/cli/Main.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.altinity.ice.cli.internal.cmd.InsertWatch;
2626
import com.altinity.ice.cli.internal.cmd.ListNamespaces;
2727
import com.altinity.ice.cli.internal.cmd.ListPartitions;
28+
import com.altinity.ice.cli.internal.cmd.ListSnapshots;
2829
import com.altinity.ice.cli.internal.cmd.ListTables;
2930
import com.altinity.ice.cli.internal.cmd.Scan;
3031
import com.altinity.ice.cli.internal.config.Config;
@@ -747,6 +748,30 @@ void listPartitions(
747748
}
748749
}
749750

751+
@CommandLine.Command(
752+
name = "list-snapshots",
753+
description = "List current and previous snapshots of a table.")
754+
void listSnapshots(
755+
@CommandLine.Parameters(
756+
arity = "1",
757+
paramLabel = "<name>",
758+
description = "Table name (e.g. ns1.table1)")
759+
String name,
760+
@CommandLine.Option(
761+
names = {"--limit"},
762+
description = "Show only the most recent N snapshots (0 = all)",
763+
defaultValue = "0")
764+
int limit,
765+
@CommandLine.Option(
766+
names = {"--json"},
767+
description = "Output JSON instead of YAML")
768+
boolean json)
769+
throws IOException {
770+
try (RESTCatalog catalog = loadCatalog()) {
771+
ListSnapshots.run(catalog, TableIdentifier.parse(name), json, limit);
772+
}
773+
}
774+
750775
@CommandLine.Command(name = "delete-table", description = "Delete table.")
751776
void deleteTable(
752777
@CommandLine.Parameters(

ice/src/main/java/com/altinity/ice/cli/internal/cmd/DescribeMetadata.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ private static MetadataInfo extractMetadataInfo(
7676

7777
List<SnapshotInfo> snapshots = null;
7878
if (includeAll || optionsSet.contains(Option.SNAPSHOTS)) {
79-
snapshots = extractSnapshots(metadata);
79+
Snapshot cur = metadata.currentSnapshot();
80+
Long currentSnapshotId = cur != null ? cur.snapshotId() : null;
81+
snapshots = extractSnapshots(metadata.snapshots(), currentSnapshotId);
8082
}
8183

8284
HistoryInfo history = null;
@@ -125,21 +127,23 @@ private static SchemaInfo extractSchema(TableMetadata metadata) {
125127
return new SchemaInfo(schema.schemaId(), fields);
126128
}
127129

128-
private static List<SnapshotInfo> extractSnapshots(TableMetadata metadata) {
129-
List<SnapshotInfo> snapshots = new ArrayList<>();
130-
for (Snapshot snapshot : metadata.snapshots()) {
131-
snapshots.add(
130+
public static List<SnapshotInfo> extractSnapshots(
131+
Iterable<Snapshot> snapshots, Long currentSnapshotId) {
132+
List<SnapshotInfo> result = new ArrayList<>();
133+
for (Snapshot snapshot : snapshots) {
134+
result.add(
132135
new SnapshotInfo(
133136
snapshot.snapshotId(),
134137
snapshot.parentId(),
135138
snapshot.sequenceNumber(),
136139
snapshot.timestampMillis(),
137140
Instant.ofEpochMilli(snapshot.timestampMillis()).toString(),
138141
snapshot.operation(),
142+
currentSnapshotId != null && snapshot.snapshotId() == currentSnapshotId,
139143
snapshot.summary(),
140144
snapshot.manifestListLocation()));
141145
}
142-
return snapshots;
146+
return result;
143147
}
144148

145149
private static HistoryInfo extractHistory(TableMetadata metadata) {
@@ -266,6 +270,7 @@ public record SnapshotInfo(
266270
long timestampMillis,
267271
String timestamp,
268272
String operation,
273+
boolean current,
269274
Map<String, String> summary,
270275
String manifestListLocation) {}
271276

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright (c) 2025 Altinity Inc and/or its affiliates. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*/
10+
package com.altinity.ice.cli.internal.cmd;
11+
12+
import com.altinity.ice.cli.internal.util.TreePrinter;
13+
import com.fasterxml.jackson.annotation.JsonInclude;
14+
import com.fasterxml.jackson.databind.ObjectMapper;
15+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
16+
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
17+
import java.io.IOException;
18+
import java.util.ArrayList;
19+
import java.util.Comparator;
20+
import java.util.HashMap;
21+
import java.util.HashSet;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Set;
25+
import org.apache.iceberg.Table;
26+
import org.apache.iceberg.catalog.TableIdentifier;
27+
import org.apache.iceberg.rest.RESTCatalog;
28+
29+
public final class ListSnapshots {
30+
31+
private ListSnapshots() {}
32+
33+
public static void run(RESTCatalog catalog, TableIdentifier tableId, boolean json, int limit)
34+
throws IOException {
35+
Table table = catalog.loadTable(tableId);
36+
Long currentSnapshotId =
37+
table.currentSnapshot() != null ? table.currentSnapshot().snapshotId() : null;
38+
39+
List<DescribeMetadata.SnapshotInfo> rows =
40+
DescribeMetadata.extractSnapshots(table.snapshots(), currentSnapshotId);
41+
42+
rows.sort(Comparator.comparingLong(DescribeMetadata.SnapshotInfo::timestampMillis));
43+
44+
if (limit > 0 && rows.size() > limit) {
45+
rows = new ArrayList<>(rows.subList(rows.size() - limit, rows.size()));
46+
}
47+
48+
if (json) {
49+
var result = new Result(tableId.toString(), currentSnapshotId, rows);
50+
ObjectMapper mapper = new ObjectMapper();
51+
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
52+
System.out.println(mapper.writeValueAsString(result));
53+
return;
54+
}
55+
56+
printTree(tableId.toString(), currentSnapshotId, rows);
57+
}
58+
59+
private static void printTree(
60+
String tableName, Long currentSnapshotId, List<DescribeMetadata.SnapshotInfo> rows)
61+
throws IOException {
62+
StringBuilder rootLabel = new StringBuilder();
63+
rootLabel.append("table: ").append(tableName);
64+
if (currentSnapshotId != null) {
65+
rootLabel.append("\ncurrentSnapshotId: ").append(currentSnapshotId);
66+
}
67+
68+
if (rows.isEmpty()) {
69+
TreePrinter.print(new TreePrinter.Node(rootLabel.toString(), List.of()));
70+
System.out.println("(no snapshots)");
71+
return;
72+
}
73+
74+
ObjectMapper yamlMapper =
75+
new ObjectMapper(
76+
new YAMLFactory()
77+
.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
78+
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));
79+
yamlMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
80+
81+
Set<Long> presentIds = new HashSet<>(rows.size());
82+
for (DescribeMetadata.SnapshotInfo info : rows) {
83+
presentIds.add(info.snapshotId());
84+
}
85+
86+
Map<Long, List<DescribeMetadata.SnapshotInfo>> childrenByParent = new HashMap<>();
87+
List<DescribeMetadata.SnapshotInfo> roots = new ArrayList<>();
88+
for (DescribeMetadata.SnapshotInfo info : rows) {
89+
Long parentId = info.parentId();
90+
if (parentId == null || !presentIds.contains(parentId)) {
91+
roots.add(info);
92+
} else {
93+
childrenByParent.computeIfAbsent(parentId, k -> new ArrayList<>()).add(info);
94+
}
95+
}
96+
97+
List<TreePrinter.Node> rootChildren = new ArrayList<>(roots.size());
98+
for (DescribeMetadata.SnapshotInfo root : roots) {
99+
rootChildren.add(buildNode(root, childrenByParent, yamlMapper));
100+
}
101+
102+
TreePrinter.print(new TreePrinter.Node(rootLabel.toString(), rootChildren));
103+
}
104+
105+
private static TreePrinter.Node buildNode(
106+
DescribeMetadata.SnapshotInfo info,
107+
Map<Long, List<DescribeMetadata.SnapshotInfo>> childrenByParent,
108+
ObjectMapper yamlMapper)
109+
throws IOException {
110+
List<DescribeMetadata.SnapshotInfo> children =
111+
childrenByParent.getOrDefault(info.snapshotId(), List.of());
112+
List<TreePrinter.Node> childNodes = new ArrayList<>(children.size());
113+
114+
for (DescribeMetadata.SnapshotInfo child : children) {
115+
childNodes.add(buildNode(child, childrenByParent, yamlMapper));
116+
}
117+
return new TreePrinter.Node(yamlMapper.writeValueAsString(info), childNodes);
118+
}
119+
120+
@JsonInclude(JsonInclude.Include.NON_NULL)
121+
record Result(
122+
String table, Long currentSnapshotId, List<DescribeMetadata.SnapshotInfo> snapshots) {}
123+
}

ice/src/main/java/com/altinity/ice/cli/internal/util/TreePrinter.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,45 @@
1010
package com.altinity.ice.cli.internal.util;
1111

1212
import java.io.PrintStream;
13+
import java.util.ArrayList;
14+
import java.util.Arrays;
1315
import java.util.List;
1416

1517
public final class TreePrinter {
1618

1719
private TreePrinter() {}
1820

19-
public record Node(String label, List<Node> children) {
21+
public record Node(List<String> label, List<Node> children) {
2022
public Node(String label) {
21-
this(label, List.of());
23+
this(splitLines(label), List.of());
24+
}
25+
26+
public Node(String label, List<Node> children) {
27+
this(splitLines(label), children);
2228
}
2329

2430
public Node {
31+
label = List.copyOf(label);
2532
children = List.copyOf(children);
2633
}
34+
35+
private static List<String> splitLines(String label) {
36+
List<String> lines = new ArrayList<>(Arrays.asList(label.split("\n", -1)));
37+
while (lines.size() > 1 && lines.getLast().isEmpty()) {
38+
lines.removeLast();
39+
}
40+
return lines;
41+
}
2742
}
2843

2944
public static void print(Node root) {
3045
print(root, System.out);
3146
}
3247

3348
public static void print(Node root, PrintStream out) {
34-
out.println(root.label());
49+
for (String line : root.label()) {
50+
out.println(line);
51+
}
3552
printChildren(root.children(), "", out);
3653
}
3754

@@ -41,7 +58,11 @@ private static void printChildren(List<Node> children, String descendantIndent,
4158
boolean isLast = (i == children.size() - 1);
4259
String connector = isLast ? "└── " : "├── ";
4360
String childDescendantIndent = descendantIndent + (isLast ? " " : "│ ");
44-
out.println(descendantIndent + connector + child.label());
61+
List<String> lines = child.label();
62+
out.println(descendantIndent + connector + lines.getFirst());
63+
for (int j = 1; j < lines.size(); j++) {
64+
out.println(childDescendantIndent + lines.get(j));
65+
}
4566
printChildren(child.children(), childDescendantIndent, out);
4667
}
4768
}

0 commit comments

Comments
 (0)