diff --git a/.gitignore b/.gitignore index 4ae9503..1c72e43 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ target .devbox /.envrc *.credentials +.DS_Store 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 b4ffde0..b71713b 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 @@ -17,6 +17,15 @@ INPUT_PATH="${SCENARIO_DIR}/${INPUT_FILE}" {{ICE_CLI}} --config {{CLI_CONFIG}} create-namespace ${NAMESPACE_NAME} echo "OK Created namespace: ${NAMESPACE_NAME}" +# List namespaces via list-namespaces command (top-level) +{{ICE_CLI}} --config {{CLI_CONFIG}} list-namespaces > /tmp/basic_ops_list_namespace.txt +if ! grep -q "${NAMESPACE_NAME}" /tmp/basic_ops_list_namespace.txt; then + echo "FAIL: list-namespaces output missing namespace ${NAMESPACE_NAME}" + cat /tmp/basic_ops_list_namespace.txt + exit 1 +fi +echo "OK list-namespaces listed ${NAMESPACE_NAME}" + # List namespaces (ice describe) - no output when no tables yet {{ICE_CLI}} --config {{CLI_CONFIG}} describe echo "OK Listed namespaces" 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 59692b4..6b2555f 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 @@ -3,12 +3,13 @@ set -e # Verification script - checks that the test completed successfully # Exit code 0 = success, non-zero = failure -# Expects run.sh to have written: basic_ops_describe.txt, basic_ops_scan_iris.txt, -# basic_ops_scan_partitioned.txt, basic_ops_scan_sorted.txt, basic_ops_files.txt under /tmp +# Expects run.sh to have written: basic_ops_list_namespace.txt, basic_ops_describe.txt, +# basic_ops_scan_iris.txt, basic_ops_scan_partitioned.txt, basic_ops_scan_sorted.txt, +# basic_ops_files.txt under /tmp echo "Verifying basic operations test..." -for f in /tmp/basic_ops_describe.txt /tmp/basic_ops_scan_iris.txt /tmp/basic_ops_scan_partitioned.txt /tmp/basic_ops_scan_sorted.txt; do +for f in /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; do if [ ! -f "$f" ]; then echo "FAIL Output file not found: $f" exit 1 @@ -65,8 +66,14 @@ if ! grep -qE "s3a://test-bucket/warehouse/test_ns/.*/data/.*\.parquet" "$F"; th echo "FAIL $F does not contain expected datafile path pattern" exit 1 fi +# Verify list-namespaces output contains the test namespace +if ! grep -q "test_ns" /tmp/basic_ops_list_namespace.txt; then + echo "FAIL basic_ops_list_namespace.txt does not contain expected namespace 'test_ns'" + exit 1 +fi + # Cleanup temp files -rm -f /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 +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 echo "OK Verification passed" exit 0 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 efbe172..b8a8b30 100644 --- a/ice/src/main/java/com/altinity/ice/cli/Main.java +++ b/ice/src/main/java/com/altinity/ice/cli/Main.java @@ -22,6 +22,7 @@ import com.altinity.ice.cli.internal.cmd.Files; import com.altinity.ice.cli.internal.cmd.Insert; 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.Scan; import com.altinity.ice.cli.internal.config.Config; @@ -708,6 +709,28 @@ void deleteNamespace( } } + @CommandLine.Command(name = "list-namespaces", description = "List namespaces.") + void listNamespaces( + @CommandLine.Parameters( + arity = "0..1", + paramLabel = "", + description = + "Parent namespace to list children of (e.g. parent_ns). Omit for top-level.") + String parent, + @CommandLine.Option( + names = {"--json"}, + description = "Output JSON instead of YAML") + boolean json) + throws IOException { + try (RESTCatalog catalog = loadCatalog()) { + Namespace namespace = + (parent == null || parent.isEmpty()) + ? Namespace.empty() + : Namespace.of(parent.split("[.]")); + ListNamespaces.run(catalog, namespace, json); + } + } + @CommandLine.Command(name = "delete", description = "Delete data from catalog.") void delete( @CommandLine.Parameters( diff --git a/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListNamespaces.java b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListNamespaces.java new file mode 100644 index 0000000..d3e8ad0 --- /dev/null +++ b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListNamespaces.java @@ -0,0 +1,45 @@ +/* + * 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.List; +import java.util.stream.Collectors; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.rest.RESTCatalog; + +public final class ListNamespaces { + + private ListNamespaces() {} + + public static void run(RESTCatalog catalog, Namespace parent, boolean json) throws IOException { + List namespaces = catalog.listNamespaces(parent); + List names = + namespaces.stream().map(Namespace::toString).sorted().collect(Collectors.toList()); + var result = new Result(names); + 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(List namespaces) {} +}