diff --git a/.gitignore b/.gitignore index 52ecf19..4ae9503 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea/ target /demo/data /demo/clickhouse-udfs.xml diff --git a/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/run.sh.tmpl b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/run.sh.tmpl index c6bdf2a..0e6b07f 100644 --- a/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/run.sh.tmpl +++ b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/run.sh.tmpl @@ -15,8 +15,21 @@ INPUT_PATH="${SCENARIO_DIR}/${INPUT_FILE}" {{ICE_CLI}} --config {{CLI_CONFIG}} insert --create-table ${TABLE_NAME} ${INPUT_PATH} --partition="${PARTITION_SPEC}" echo "OK Inserted data with partitioning into table ${TABLE_NAME}" -# Describe the table to verify partitioning (if describe command exists) -# {{ICE_CLI}} --config {{CLI_CONFIG}} describe-table ${TABLE_NAME} +# List partitions and validate output +LIST_PARTITIONS_OUT=$(mktemp) +trap "rm -f '${LIST_PARTITIONS_OUT}'" EXIT +{{ICE_CLI}} --config {{CLI_CONFIG}} list-partitions ${TABLE_NAME} > "${LIST_PARTITIONS_OUT}" +if ! grep -q "partitions:" "${LIST_PARTITIONS_OUT}"; then + echo "FAIL: list-partitions output missing 'partitions:' section" + cat "${LIST_PARTITIONS_OUT}" + exit 1 +fi +if ! grep -qE -- "- *[^=]+=" "${LIST_PARTITIONS_OUT}"; then + echo "FAIL: list-partitions output has no partition entries (expected at least one key=value)" + cat "${LIST_PARTITIONS_OUT}" + exit 1 +fi +echo "OK Listed and validated partitions for ${TABLE_NAME}" # Cleanup {{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE_NAME} 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 a10e1da..b89050c 100644 --- a/ice/src/main/java/com/altinity/ice/cli/Main.java +++ b/ice/src/main/java/com/altinity/ice/cli/Main.java @@ -21,6 +21,7 @@ import com.altinity.ice.cli.internal.cmd.DescribeParquet; import com.altinity.ice.cli.internal.cmd.Insert; import com.altinity.ice.cli.internal.cmd.InsertWatch; +import com.altinity.ice.cli.internal.cmd.ListPartitions; import com.altinity.ice.cli.internal.cmd.Scan; import com.altinity.ice.cli.internal.config.Config; import com.altinity.ice.cli.internal.iceberg.rest.RESTCatalogFactory; @@ -594,6 +595,23 @@ void scanTable( } } + @CommandLine.Command(name = "list-partitions", description = "List partitions in a table.") + void listPartitions( + @CommandLine.Parameters( + arity = "1", + paramLabel = "", + description = "Table name (e.g. ns1.table1)") + String name, + @CommandLine.Option( + names = {"--json"}, + description = "Output JSON instead of YAML") + boolean json) + throws IOException { + try (RESTCatalog catalog = loadCatalog()) { + ListPartitions.run(catalog, TableIdentifier.parse(name), json); + } + } + @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/ListPartitions.java b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListPartitions.java new file mode 100644 index 0000000..e19dc5f --- /dev/null +++ b/ice/src/main/java/com/altinity/ice/cli/internal/cmd/ListPartitions.java @@ -0,0 +1,74 @@ +/* + * 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.List; +import java.util.TreeSet; +import org.apache.iceberg.FileScanTask; +import org.apache.iceberg.PartitionField; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.rest.RESTCatalog; + +public final class ListPartitions { + + private ListPartitions() {} + + public static void run(RESTCatalog catalog, TableIdentifier tableId, boolean json) + throws IOException { + org.apache.iceberg.Table table = catalog.loadTable(tableId); + PartitionSpec spec = table.spec(); + + if (!spec.isPartitioned()) { + var result = new Result(tableId.toString(), List.of(), List.of()); + output(result, json); + return; + } + + List partitionSpec = new ArrayList<>(); + for (PartitionField field : spec.fields()) { + String sourceColumn = table.schema().findField(field.sourceId()).name(); + partitionSpec.add( + new PartitionFieldInfo(sourceColumn, field.name(), field.transform().toString())); + } + + TreeSet partitionPaths = new TreeSet<>(); + try (CloseableIterable tasks = table.newScan().planFiles()) { + for (FileScanTask task : tasks) { + String path = spec.partitionToPath(task.file().partition()); + partitionPaths.add(path); + } + } + + var result = new Result(tableId.toString(), partitionSpec, new ArrayList<>(partitionPaths)); + 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, List partitionSpec, List partitions) {} + + record PartitionFieldInfo(String sourceColumn, String name, String transform) {} +}