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} 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/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 new file mode 100644 index 0000000..c21f6e2 --- /dev/null +++ b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListSnapshots.java @@ -0,0 +1,59 @@ +/* + * 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.util.ArrayList; +import java.util.Comparator; +import java.util.List; +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 = + DescribeMetadata.extractSnapshots(table.snapshots(), currentSnapshotId); + + rows.sort(Comparator.comparingLong(DescribeMetadata.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) {} +}