diff --git a/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/RESTCatalogTestBase.java b/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/RESTCatalogTestBase.java index 66b7a8ef..bfdc07cd 100644 --- a/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/RESTCatalogTestBase.java +++ b/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/RESTCatalogTestBase.java @@ -159,7 +159,20 @@ protected File createTempCliConfig() throws Exception { File tempConfigFile = File.createTempFile("ice-rest-cli-", ".yaml"); tempConfigFile.deleteOnExit(); - String configContent = "uri: http://localhost:8080\n"; + // Match server warehouse + MinIO S3 settings so CLI commands that read s3:// paths directly + // (e.g. describe-metadata on metadata.json) use S3FileIO against MinIO, not default AWS. + String minioEndpoint = getMinioEndpoint(); + String configContent = + "uri: http://localhost:8080\n" + + "warehouse: s3://test-bucket/warehouse\n" + + "s3:\n" + + " endpoint: " + + minioEndpoint + + "\n" + + " pathStyleAccess: true\n" + + " accessKeyID: minioadmin\n" + + " secretAccessKey: minioadmin\n" + + " region: us-east-1\n"; Files.write(tempConfigFile.toPath(), configContent.getBytes()); return tempConfigFile; 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 31805c08..e4bf2f31 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 @@ -104,6 +104,9 @@ if command -v aws &>/dev/null && [ -n "{{MINIO_ENDPOINT}}" ]; then S3_PATH="s3://${S3_BUCKET}/${NAMESPACE_NAME}/iris_no_copy/$(basename ${INPUT_PATH})" export AWS_ACCESS_KEY_ID=minioadmin export AWS_SECRET_ACCESS_KEY=minioadmin + # Iceberg S3FileIO / AWS SDK v2 need a region even for MinIO endpoints. + export AWS_REGION=us-east-1 + export AWS_DEFAULT_REGION=us-east-1 if aws s3 cp --endpoint-url "{{MINIO_ENDPOINT}}" "${INPUT_PATH}" "${S3_PATH}" 2>/dev/null; then if {{ICE_CLI}} --config {{CLI_CONFIG}} create-table ${TABLE_NO_COPY} --schema-from-parquet="file://${INPUT_PATH}" 2>/dev/null && \ {{ICE_CLI}} --config {{CLI_CONFIG}} insert ${TABLE_NO_COPY} --no-copy "${S3_PATH}" 2>/dev/null; then @@ -123,6 +126,35 @@ fi {{ICE_CLI}} --config {{CLI_CONFIG}} files ${TABLE_SORTED} >> /tmp/basic_ops_files.txt echo "OK Listed files in tables" +# Describe-metadata --manifests on the iris table. +# We need the current metadata.json key on S3; locate it via aws ls (best-effort). +if command -v aws &>/dev/null && [ -n "{{MINIO_ENDPOINT}}" ]; then + export AWS_ACCESS_KEY_ID=minioadmin + export AWS_SECRET_ACCESS_KEY=minioadmin + export AWS_REGION=us-east-1 + export AWS_DEFAULT_REGION=us-east-1 + META_PREFIX="s3://${S3_BUCKET}/warehouse/${NAMESPACE_NAME}/iris/metadata/" + META_KEY=$(aws --endpoint-url "{{MINIO_ENDPOINT}}" s3 ls "${META_PREFIX}" 2>/dev/null \ + | awk '/metadata\.json$/{print $NF}' | sort | tail -1) + if [ -n "${META_KEY}" ]; then + {{ICE_CLI}} --config {{CLI_CONFIG}} describe-metadata --manifests \ + "${META_PREFIX}${META_KEY}" \ + > /tmp/basic_ops_describe_metadata_manifests.txt + for needle in "manifests:" ".avro" "partitionSpecId:" "dataFiles:" "PARQUET"; do + if ! grep -q "${needle}" /tmp/basic_ops_describe_metadata_manifests.txt; then + echo "FAIL: describe-metadata --manifests output missing '${needle}'" + cat /tmp/basic_ops_describe_metadata_manifests.txt + exit 1 + fi + done + echo "OK describe-metadata --manifests verified for ${TABLE_IRIS}" + else + echo "SKIP describe-metadata --manifests: no metadata.json found via aws ls" + fi +else + echo "SKIP describe-metadata --manifests: aws CLI or MINIO_ENDPOINT not available" +fi + # List tables in the namespace via list-tables command {{ICE_CLI}} --config {{CLI_CONFIG}} list-tables ${NAMESPACE_NAME} > /tmp/basic_ops_list_tables.txt for t in ${TABLE_IRIS} ${TABLE_PARTITIONED} ${TABLE_SORTED}; do @@ -134,6 +166,7 @@ for t in ${TABLE_IRIS} ${TABLE_PARTITIONED} ${TABLE_SORTED}; do done echo "OK list-tables listed tables in ${NAMESPACE_NAME}" + # Cleanup tables then namespace {{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE_IRIS} {{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE_PARTITIONED} diff --git a/ice-rest-catalog/src/test/resources/scenarios/basic-operations/verify.sh.tmpl b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/verify.sh.tmpl index 941c14f0..a3793a5b 100644 --- a/ice-rest-catalog/src/test/resources/scenarios/basic-operations/verify.sh.tmpl +++ b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/verify.sh.tmpl @@ -32,6 +32,21 @@ for f in /tmp/basic_ops_scan_iris.txt /tmp/basic_ops_scan_partitioned.txt /tmp/b fi done +# Verify describe-metadata --manifests output if it was produced (aws available in run.sh) +DM="/tmp/basic_ops_describe_metadata_manifests.txt" +if [ -f "$DM" ]; then + if [ ! -s "$DM" ]; then + echo "FAIL $DM exists but is empty" + exit 1 + fi + for needle in "manifests:" ".avro" "partitionSpecId:" "dataFiles:" "PARQUET"; do + if ! grep -q "${needle}" "$DM"; then + echo "FAIL $DM missing '${needle}'" + exit 1 + fi + done +fi + # Verify files output contains expected structure (Snapshots/Snapshot/Manifest/Datafile and S3 paths) F="/tmp/basic_ops_files.txt" if [ ! -f "$F" ] || [ ! -s "$F" ]; then @@ -81,6 +96,7 @@ for t in test_ns.iris test_ns.taxis_p_by_day test_ns.taxis_s_by_day; do done # Cleanup temp files +rm -f /tmp/basic_ops_list_namespace.txt /tmp/basic_ops_describe.txt /tmp/basic_ops_scan_iris.txt /tmp/basic_ops_scan_partitioned.txt /tmp/basic_ops_scan_sorted.txt /tmp/basic_ops_files.txt /tmp/basic_ops_describe_metadata_manifests.txt rm -f /tmp/basic_ops_list_namespace.txt /tmp/basic_ops_describe.txt /tmp/basic_ops_scan_iris.txt /tmp/basic_ops_scan_partitioned.txt /tmp/basic_ops_scan_sorted.txt /tmp/basic_ops_files.txt /tmp/basic_ops_list_tables.txt echo "OK Verification passed" 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 a81962d8..60e9a22c 100644 --- a/ice/src/main/java/com/altinity/ice/cli/Main.java +++ b/ice/src/main/java/com/altinity/ice/cli/Main.java @@ -18,6 +18,7 @@ import com.altinity.ice.cli.internal.cmd.DeleteNamespace; import com.altinity.ice.cli.internal.cmd.DeleteTable; import com.altinity.ice.cli.internal.cmd.Describe; +import com.altinity.ice.cli.internal.cmd.DescribeMetadata; import com.altinity.ice.cli.internal.cmd.DescribeParquet; import com.altinity.ice.cli.internal.cmd.Files; import com.altinity.ice.cli.internal.cmd.Insert; @@ -237,6 +238,70 @@ void describeParquet( } } + @CommandLine.Command(name = "describe-metadata", description = "Describe Iceberg metadata file.") + void describeMetadata( + @CommandLine.Parameters( + arity = "1", + paramLabel = "", + description = "Path to metadata.json file") + String target, + @CommandLine.Option( + names = {"-a", "--all"}, + description = "Show everything") + boolean showAll, + @CommandLine.Option( + names = {"-s", "--summary"}, + description = "Show table UUID, format version, location, current snapshot, etc.") + boolean showSummary, + @CommandLine.Option( + names = {"-S", "--schema"}, + description = "Show full schema with field IDs and types") + boolean showSchema, + @CommandLine.Option( + names = {"--snapshots"}, + description = "List all snapshots") + boolean showSnapshots, + @CommandLine.Option( + names = {"--history"}, + description = "Show snapshot log and metadata log") + boolean showHistory, + @CommandLine.Option( + names = {"--manifests"}, + description = "Drill into manifest list for current snapshot") + boolean showManifests, + @CommandLine.Option( + names = {"--json"}, + description = "Output JSON instead of YAML") + boolean json) + throws IOException { + Config config = Config.load(configFile()); + var icebergConfig = config.toIcebergConfig(); + + var options = new ArrayList(); + if (showAll || showSummary) { + options.add(DescribeMetadata.Option.SUMMARY); + } + if (showAll || showSchema) { + options.add(DescribeMetadata.Option.SCHEMA); + } + if (showAll || showSnapshots) { + options.add(DescribeMetadata.Option.SNAPSHOTS); + } + if (showAll || showHistory) { + options.add(DescribeMetadata.Option.HISTORY); + } + if (showAll || showManifests) { + options.add(DescribeMetadata.Option.MANIFESTS); + } + + if (options.isEmpty()) { + options.add(DescribeMetadata.Option.SUMMARY); + } + + DescribeMetadata.run( + icebergConfig, target, json, options.toArray(new DescribeMetadata.Option[0])); + } + public record IceSortOrder( @JsonProperty("column") String column, @JsonProperty("desc") boolean desc, 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 new file mode 100644 index 00000000..6ffff298 --- /dev/null +++ b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/DescribeMetadata.java @@ -0,0 +1,303 @@ +/* + * 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.altinity.ice.internal.iceberg.io.SchemeFileIO; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.iceberg.*; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.io.FileIO; +import org.apache.iceberg.types.Conversions; +import org.apache.iceberg.types.Types; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class DescribeMetadata { + + private static final Logger logger = LoggerFactory.getLogger(DescribeMetadata.class); + + private DescribeMetadata() {} + + public enum Option { + ALL, + SUMMARY, + SCHEMA, + SNAPSHOTS, + HISTORY, + MANIFESTS + } + + public static void run( + Map icebergConfig, String filePath, boolean json, Option... options) + throws IOException { + try (SchemeFileIO io = new SchemeFileIO()) { + io.initialize(icebergConfig); + TableMetadata metadata = TableMetadataParser.read(io, filePath); + MetadataInfo info = extractMetadataInfo(metadata, io, options); + + ObjectMapper mapper = json ? new ObjectMapper() : new ObjectMapper(new YAMLFactory()); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + String output = mapper.writeValueAsString(info); + System.out.println(output); + } + } + + private static MetadataInfo extractMetadataInfo( + TableMetadata metadata, FileIO io, Option... options) { + var optionsSet = Set.of(options); + boolean includeAll = optionsSet.contains(Option.ALL); + + Summary summary = null; + if (includeAll || optionsSet.contains(Option.SUMMARY)) { + summary = extractSummary(metadata); + } + + SchemaInfo schema = null; + if (includeAll || optionsSet.contains(Option.SCHEMA)) { + schema = extractSchema(metadata); + } + + List snapshots = null; + if (includeAll || optionsSet.contains(Option.SNAPSHOTS)) { + snapshots = extractSnapshots(metadata); + } + + HistoryInfo history = null; + if (includeAll || optionsSet.contains(Option.HISTORY)) { + history = extractHistory(metadata); + } + + List manifests = null; + if (includeAll || optionsSet.contains(Option.MANIFESTS)) { + manifests = extractManifests(metadata, metadata.schema(), io); + } + + return new MetadataInfo(summary, schema, snapshots, history, manifests); + } + + private static Summary extractSummary(TableMetadata metadata) { + Snapshot currentSnapshot = metadata.currentSnapshot(); + Long currentSnapshotId = currentSnapshot != null ? currentSnapshot.snapshotId() : null; + String partitionSpec = + metadata.spec().isUnpartitioned() ? "unpartitioned" : metadata.spec().toString(); + String sortOrder = + metadata.sortOrder().isUnsorted() ? "unsorted" : metadata.sortOrder().toString(); + Map properties = metadata.properties().isEmpty() ? null : metadata.properties(); + + return new Summary( + metadata.uuid(), + metadata.formatVersion(), + metadata.location(), + metadata.lastUpdatedMillis(), + Instant.ofEpochMilli(metadata.lastUpdatedMillis()).toString(), + currentSnapshotId, + metadata.snapshots().size(), + partitionSpec, + sortOrder, + properties); + } + + private static SchemaInfo extractSchema(TableMetadata metadata) { + Schema schema = metadata.schema(); + List fields = new ArrayList<>(); + for (Types.NestedField field : schema.columns()) { + fields.add( + new FieldInfo( + field.fieldId(), field.name(), field.type().toString(), field.isRequired())); + } + return new SchemaInfo(schema.schemaId(), fields); + } + + private static List extractSnapshots(TableMetadata metadata) { + List snapshots = new ArrayList<>(); + for (Snapshot snapshot : metadata.snapshots()) { + snapshots.add( + new SnapshotInfo( + snapshot.snapshotId(), + snapshot.parentId(), + snapshot.sequenceNumber(), + snapshot.timestampMillis(), + Instant.ofEpochMilli(snapshot.timestampMillis()).toString(), + snapshot.operation(), + snapshot.summary(), + snapshot.manifestListLocation())); + } + return snapshots; + } + + private static HistoryInfo extractHistory(TableMetadata metadata) { + List snapshotLog = new ArrayList<>(); + for (var entry : metadata.snapshotLog()) { + snapshotLog.add( + new SnapshotLogEntry( + entry.snapshotId(), + entry.timestampMillis(), + Instant.ofEpochMilli(entry.timestampMillis()).toString())); + } + + List metadataLog = new ArrayList<>(); + for (TableMetadata.MetadataLogEntry entry : metadata.previousFiles()) { + metadataLog.add( + new MetadataLogEntry( + entry.file(), + entry.timestampMillis(), + Instant.ofEpochMilli(entry.timestampMillis()).toString())); + } + + return new HistoryInfo( + snapshotLog.isEmpty() ? null : snapshotLog, metadataLog.isEmpty() ? null : metadataLog); + } + + private static List extractManifests( + TableMetadata metadata, org.apache.iceberg.Schema schema, FileIO io) { + Snapshot currentSnapshot = metadata.currentSnapshot(); + if (currentSnapshot == null) { + return List.of(); + } + + List manifestFiles; + try { + manifestFiles = currentSnapshot.allManifests(io); + } catch (Exception e) { + logger.warn("Failed to read manifest list: {}", e.getMessage()); + return List.of(); + } + + List manifests = new ArrayList<>(); + for (ManifestFile manifest : manifestFiles) { + List dataFiles = new ArrayList<>(); + try (CloseableIterable files = ManifestFiles.read(manifest, io)) { + for (DataFile file : files) { + dataFiles.add( + new DataFileInfo( + file.location(), + file.format().name(), + file.recordCount(), + file.fileSizeInBytes(), + file.partition().toString(), + file.columnSizes(), + file.valueCounts(), + file.nullValueCounts(), + convertBounds(schema, file.lowerBounds()), + convertBounds(schema, file.upperBounds()))); + } + } catch (Exception e) { + logger.warn("Failed to read manifest {}: {}", manifest.path(), e.getMessage()); + } + + manifests.add( + new ManifestInfo( + manifest.path(), + manifest.addedFilesCount(), + manifest.existingFilesCount(), + manifest.deletedFilesCount(), + manifest.partitionSpecId(), + dataFiles.isEmpty() ? null : dataFiles)); + } + + return manifests; + } + + private static Map convertBounds( + Schema schema, Map bounds) { + if (bounds == null) { + return null; + } + Map result = new HashMap<>(); + for (var entry : bounds.entrySet()) { + Types.NestedField field = schema.findField(entry.getKey()); + if (field != null) { + result.put( + entry.getKey(), Conversions.fromByteBuffer(field.type(), entry.getValue()).toString()); + } + } + return result.isEmpty() ? null : result; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record MetadataInfo( + Summary summary, + SchemaInfo schema, + List snapshots, + HistoryInfo history, + List manifests) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record Summary( + String uuid, + int formatVersion, + String location, + long lastUpdatedMillis, + String lastUpdated, + Long currentSnapshotId, + int numSnapshots, + String partitionSpec, + String sortOrder, + Map properties) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SchemaInfo(int schemaId, List fields) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record FieldInfo(int fieldId, String name, String type, boolean required) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SnapshotInfo( + long snapshotId, + Long parentId, + long sequenceNumber, + long timestampMillis, + String timestamp, + String operation, + Map summary, + String manifestListLocation) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record HistoryInfo( + List snapshotLog, List metadataLog) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SnapshotLogEntry(long snapshotId, long timestampMillis, String timestamp) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record MetadataLogEntry(String file, long timestampMillis, String timestamp) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ManifestInfo( + String path, + Integer addedFilesCount, + Integer existingFilesCount, + Integer deletedFilesCount, + int partitionSpecId, + List dataFiles) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DataFileInfo( + String path, + String format, + long recordCount, + long fileSizeInBytes, + String partition, + Map columnSizes, + Map valueCounts, + Map nullValueCounts, + Map lowerBounds, + Map upperBounds) {} +} diff --git a/ice/src/test/java/com/altinity/ice/cli/internal/cmd/DescribeMetadataTest.java b/ice/src/test/java/com/altinity/ice/cli/internal/cmd/DescribeMetadataTest.java new file mode 100644 index 00000000..3fbebd69 --- /dev/null +++ b/ice/src/test/java/com/altinity/ice/cli/internal/cmd/DescribeMetadataTest.java @@ -0,0 +1,174 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Map; +import java.util.Objects; +import org.apache.iceberg.CatalogProperties; +import org.testng.annotations.Test; + +public class DescribeMetadataTest { + + private static String testMetadataPath() { + String path = + Objects.requireNonNull( + DescribeMetadataTest.class + .getClassLoader() + .getResource("com/altinity/ice/cli/internal/cmd/test-v1.metadata.json")) + .getPath(); + return "file://" + path; + } + + private static Map testConfig() { + String filePath = testMetadataPath(); + String dir = filePath.substring(0, filePath.lastIndexOf('/')); + return Map.of(CatalogProperties.WAREHOUSE_LOCATION, dir); + } + + @Test + public void testDescribeMetadataSummary() throws IOException { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent)); + + try { + DescribeMetadata.run( + testConfig(), testMetadataPath(), false, DescribeMetadata.Option.SUMMARY); + + String output = outContent.toString(); + + assertThat(output).contains("uuid:"); + assertThat(output).contains("fb3de834-aa02-4154-b3e2-a1f528e8e7c4"); + assertThat(output).contains("formatVersion: 2"); + assertThat(output).contains("location:"); + assertThat(output).contains("currentSnapshotId:"); + assertThat(output).contains("numSnapshots: 1"); + assertThat(output).contains("partitionSpec:"); + assertThat(output).contains("sortOrder:"); + } finally { + System.setOut(originalOut); + } + } + + @Test + public void testDescribeMetadataSchema() throws IOException { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent)); + + try { + DescribeMetadata.run(testConfig(), testMetadataPath(), false, DescribeMetadata.Option.SCHEMA); + + String output = outContent.toString(); + + assertThat(output).contains("schema:"); + assertThat(output).contains("schemaId: 0"); + assertThat(output).contains("fields:"); + assertThat(output).contains("name: \"id\""); + assertThat(output).contains("name: \"name\""); + assertThat(output).contains("name: \"ts\""); + assertThat(output).contains("type: \"long\""); + assertThat(output).contains("type: \"string\""); + } finally { + System.setOut(originalOut); + } + } + + @Test + public void testDescribeMetadataSnapshots() throws IOException { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent)); + + try { + DescribeMetadata.run( + testConfig(), testMetadataPath(), false, DescribeMetadata.Option.SNAPSHOTS); + + String output = outContent.toString(); + + assertThat(output).contains("snapshots:"); + assertThat(output).contains("snapshotId:"); + assertThat(output).contains("timestamp:"); + assertThat(output).contains("operation: \"append\""); + assertThat(output).contains("manifestListLocation:"); + } finally { + System.setOut(originalOut); + } + } + + @Test + public void testDescribeMetadataHistory() throws IOException { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent)); + + try { + DescribeMetadata.run( + testConfig(), testMetadataPath(), false, DescribeMetadata.Option.HISTORY); + + String output = outContent.toString(); + + assertThat(output).contains("history:"); + assertThat(output).contains("snapshotLog:"); + assertThat(output).contains("snapshotId:"); + assertThat(output).contains("timestamp:"); + } finally { + System.setOut(originalOut); + } + } + + @Test + public void testDescribeMetadataJson() throws IOException { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent)); + + try { + DescribeMetadata.run(testConfig(), testMetadataPath(), true, DescribeMetadata.Option.SUMMARY); + + String output = outContent.toString(); + + assertThat(output).contains("{"); + assertThat(output).contains("}"); + assertThat(output).contains("\"summary\""); + assertThat(output).contains("\"uuid\""); + assertThat(output).contains("\"fb3de834-aa02-4154-b3e2-a1f528e8e7c4\""); + } finally { + System.setOut(originalOut); + } + } + + @Test + public void testDescribeMetadataDefaultIsSummary() throws IOException { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + System.setOut(new PrintStream(outContent)); + + try { + DescribeMetadata.run( + testConfig(), testMetadataPath(), false, DescribeMetadata.Option.SUMMARY); + + String output = outContent.toString(); + + assertThat(output).contains("summary:"); + assertThat(output).doesNotContain("schema:"); + assertThat(output).doesNotContain("snapshots:"); + assertThat(output).doesNotContain("history:"); + assertThat(output).doesNotContain("manifests:"); + } finally { + System.setOut(originalOut); + } + } +} diff --git a/ice/src/test/resources/com/altinity/ice/cli/internal/cmd/test-v1.metadata.json b/ice/src/test/resources/com/altinity/ice/cli/internal/cmd/test-v1.metadata.json new file mode 100644 index 00000000..29618b88 --- /dev/null +++ b/ice/src/test/resources/com/altinity/ice/cli/internal/cmd/test-v1.metadata.json @@ -0,0 +1,66 @@ +{ + "format-version": 2, + "table-uuid": "fb3de834-aa02-4154-b3e2-a1f528e8e7c4", + "location": "warehouse/test_ns/test_table", + "last-sequence-number": 1, + "last-updated-ms": 1700000000000, + "last-column-id": 3, + "current-schema-id": 0, + "schemas": [ + { + "type": "struct", + "schema-id": 0, + "fields": [ + {"id": 1, "name": "id", "required": true, "type": "long"}, + {"id": 2, "name": "name", "required": false, "type": "string"}, + {"id": 3, "name": "ts", "required": false, "type": "timestamptz"} + ], + "identifier-field-ids": [1] + } + ], + "default-spec-id": 0, + "partition-specs": [ + {"spec-id": 0, "fields": []} + ], + "last-partition-id": 999, + "default-sort-order-id": 0, + "sort-orders": [ + {"order-id": 0, "fields": []} + ], + "properties": { + "owner": "test-user", + "created-at": "2023-11-14T22:13:20" + }, + "current-snapshot-id": 1234567890123456789, + "refs": { + "main": { + "snapshot-id": 1234567890123456789, + "type": "branch" + } + }, + "snapshots": [ + { + "snapshot-id": 1234567890123456789, + "sequence-number": 1, + "timestamp-ms": 1700000000000, + "summary": { + "operation": "append", + "added-data-files": "1", + "added-records": "100", + "total-data-files": "1", + "total-records": "100" + }, + "manifest-list": "warehouse/test_ns/test_table/metadata/snap-1234567890123456789-0-abc.avro", + "schema-id": 0 + } + ], + "snapshot-log": [ + { + "snapshot-id": 1234567890123456789, + "timestamp-ms": 1700000000000 + } + ], + "metadata-log": [], + "statistics": [], + "partition-statistics": [] +}