From af9c2b4d848806df11d5dc2519d71bf9f0525f2e Mon Sep 17 00:00:00 2001 From: Lord of Abyss <103809695+Abyss-lord@users.noreply.github.com> Date: Wed, 26 Feb 2025 10:12:31 +0800 Subject: [PATCH] [#6496] feat(CLI): Support plain format output for Schema and Table command (#6497) ### What changes were proposed in this pull request? Support plain format output for Schema and Table command ### Why are the changes needed? Fix: #6496 ### Does this PR introduce _any_ user-facing change? No ### How was this patch tested? local test ```bash gcli schema list -m demo_metalake --name Hive_catalog -i default,Default Hive database gcli table list -m demo_metalake --name Hive_catalog.default -i employee_partitions source_table test_dt_partition test_key_en sales order_tb1 tmp float_test test_tbl gcli table details -m demo_metalake --name Hive_catalog.default.test1 -i test1,N/A gcli schema details -m demo_metalake --name Hive_catalog.default -i default,Default Hive database ``` --- .../gravitino/cli/outputs/OutputFormat.java | 2 +- .../gravitino/cli/outputs/PlainFormat.java | 113 ++++++++++++++++-- .../gravitino/cli/output/TestPlainFormat.java | 113 ++++++++++++++++-- 3 files changed, 209 insertions(+), 19 deletions(-) diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java index fe0d7b7c9e0..378fdf746bb 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/OutputFormat.java @@ -26,7 +26,7 @@ */ public interface OutputFormat { /** Joiner for creating comma-separated output strings, ignoring null values */ - Joiner COMMA_JOINER = Joiner.on(", ").skipNulls(); + Joiner COMMA_JOINER = Joiner.on(",").skipNulls(); /** Joiner for creating line-separated output strings, ignoring null values */ Joiner NEWLINE_JOINER = Joiner.on(System.lineSeparator()).skipNulls(); diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java index f61578ffc6f..ed35c9fe46f 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/outputs/PlainFormat.java @@ -21,9 +21,12 @@ import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import org.apache.gravitino.Audit; import org.apache.gravitino.Catalog; import org.apache.gravitino.Metalake; +import org.apache.gravitino.Schema; import org.apache.gravitino.cli.CommandContext; +import org.apache.gravitino.rel.Table; /** Plain format to print a pretty string to standard out. */ public abstract class PlainFormat extends BaseOutputFormat { @@ -45,6 +48,16 @@ public static void output(Object entity, CommandContext context) { new CatalogPlainFormat(context).output((Catalog) entity); } else if (entity instanceof Catalog[]) { new CatalogListPlainFormat(context).output((Catalog[]) entity); + } else if (entity instanceof Schema) { + new SchemaPlainFormat(context).output((Schema) entity); + } else if (entity instanceof Schema[]) { + new SchemaListPlainFormat(context).output((Schema[]) entity); + } else if (entity instanceof Table) { + new TablePlainFormat(context).output((Table) entity); + } else if (entity instanceof Table[]) { + new TableListPlainFormat(context).output((Table[]) entity); + } else if (entity instanceof Audit) { + new AuditPlainFormat(context).output((Audit) entity); } else { throw new IllegalArgumentException("Unsupported object type"); } @@ -124,14 +137,98 @@ public CatalogListPlainFormat(CommandContext context) { /** {@inheritDoc} */ @Override public String getOutput(Catalog[] catalogs) { - if (catalogs == null || catalogs.length == 0) { - output("No catalogs exist.", System.out); - return null; - } else { - List catalogNames = - Arrays.stream(catalogs).map(Catalog::name).collect(Collectors.toList()); - return NEWLINE_JOINER.join(catalogNames); - } + + List catalogNames = + Arrays.stream(catalogs).map(Catalog::name).collect(Collectors.toList()); + return NEWLINE_JOINER.join(catalogNames); + } + } + + /** + * Formats a single {@link Schema} instance as a comma-separated string. Output format: name, + * comment + */ + static final class SchemaPlainFormat extends PlainFormat { + public SchemaPlainFormat(CommandContext context) { + super(context); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Schema schema) { + return COMMA_JOINER.join(schema.name(), schema.comment()); + } + } + + /** + * Formats an array of Schemas, outputting one name per line. Returns null if the array is empty + * or null. + */ + static final class SchemaListPlainFormat extends PlainFormat { + public SchemaListPlainFormat(CommandContext context) { + super(context); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Schema[] schemas) { + List schemaNames = + Arrays.stream(schemas).map(Schema::name).collect(Collectors.toList()); + return NEWLINE_JOINER.join(schemaNames); + } + } + + /** + * Formats a single Table instance with detailed column information. Output format: table_name, + * table_comment + */ + static final class TablePlainFormat extends PlainFormat { + public TablePlainFormat(CommandContext context) { + super(context); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Table table) { + String comment = table.comment() == null ? "N/A" : table.comment(); + return COMMA_JOINER.join(new String[] {table.name(), comment}); + } + } + + /** + * Formats an array of Tables, outputting one name per line. Returns null if the array is empty or + * null. + */ + static final class TableListPlainFormat extends PlainFormat { + public TableListPlainFormat(CommandContext context) { + super(context); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Table[] tables) { + List tableNames = Arrays.stream(tables).map(Table::name).collect(Collectors.toList()); + return NEWLINE_JOINER.join(tableNames); + } + } + + /** + * Formats an instance of {@link Audit} , outputting the audit information. Output format: + * creator, create_time, modified, modified_time + */ + static final class AuditPlainFormat extends PlainFormat { + public AuditPlainFormat(CommandContext context) { + super(context); + } + + /** {@inheritDoc} */ + @Override + public String getOutput(Audit audit) { + return COMMA_JOINER.join( + audit.creator(), + audit.createTime() == null ? "N/A" : audit.createTime(), + audit.lastModifier() == null ? "N/A" : audit.lastModifier(), + audit.lastModifiedTime() == null ? "N/A" : audit.lastModifiedTime()); } } } diff --git a/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestPlainFormat.java b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestPlainFormat.java index 300a12e9d57..33836588f83 100644 --- a/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestPlainFormat.java +++ b/clients/cli/src/test/java/org/apache/gravitino/cli/output/TestPlainFormat.java @@ -33,6 +33,9 @@ import org.apache.gravitino.Schema; import org.apache.gravitino.cli.CommandContext; import org.apache.gravitino.cli.outputs.PlainFormat; +import org.apache.gravitino.rel.Table; +import org.apache.gravitino.rel.types.Type; +import org.apache.gravitino.rel.types.Types; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -64,7 +67,7 @@ void testMetalakeDetailsWithPlainFormat() { PlainFormat.output(mockMetalake, mockContext); String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); - Assertions.assertEquals("demo_metalake, This is a demo metalake", output); + Assertions.assertEquals("demo_metalake,This is a demo metalake", output); } @Test @@ -85,8 +88,7 @@ void testCatalogDetailsWithPlainFormat() { PlainFormat.output(mockCatalog, mockContext); String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); - Assertions.assertEquals( - "demo_catalog, RELATIONAL, demo_provider, This is a demo catalog", output); + Assertions.assertEquals("demo_catalog,RELATIONAL,demo_provider,This is a demo catalog", output); } @Test @@ -102,6 +104,75 @@ void testListCatalogWithPlainFormat() { Assertions.assertEquals("catalog1\n" + "catalog2", output); } + @Test + void testSchemaDetailsWithPlainFormat() { + CommandContext mockContext = getMockContext(); + Schema mockSchema = getMockSchema(); + PlainFormat.output(mockSchema, mockContext); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals("demo_schema,This is a demo schema", output); + } + + @Test + void testListSchemaWithPlainFormat() { + CommandContext mockContext = getMockContext(); + Schema mockSchema1 = getMockSchema("schema1", "This is a schema"); + Schema mockSchema2 = getMockSchema("schema2", "This is another schema"); + + PlainFormat.output(new Schema[] {mockSchema1, mockSchema2}, mockContext); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals("schema1\n" + "schema2", output); + } + + @Test + void testTableDetailsWithPlainFormat() { + CommandContext mockContext = getMockContext(); + Table mockTable = getMockTable(); + PlainFormat.output(mockTable, mockContext); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals("demo_table,This is a demo table", output); + } + + @Test + void testAuditWithTableFormat() { + CommandContext mockContext = getMockContext(); + Audit mockAudit = mock(Audit.class); + when(mockAudit.creator()).thenReturn("demo_user"); + when(mockAudit.createTime()).thenReturn(Instant.ofEpochMilli(1611111111111L)); + when(mockAudit.lastModifier()).thenReturn("demo_user"); + when(mockAudit.lastModifiedTime()).thenReturn(Instant.ofEpochMilli(1611111111111L)); + + PlainFormat.output(mockAudit, mockContext); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals( + "demo_user,2021-01-20T02:51:51.111Z,demo_user,2021-01-20T02:51:51.111Z", output); + } + + @Test + void testAuditWithTableFormatWithNullValues() { + CommandContext mockContext = getMockContext(); + Audit mockAudit = mock(Audit.class); + when(mockAudit.creator()).thenReturn("demo_user"); + when(mockAudit.createTime()).thenReturn(null); + when(mockAudit.lastModifier()).thenReturn(null); + when(mockAudit.lastModifiedTime()).thenReturn(null); + + PlainFormat.output(mockAudit, mockContext); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals("demo_user,N/A,N/A,N/A", output); + } + + @Test + void testListTableWithPlainFormat() { + CommandContext mockContext = getMockContext(); + Table mockTable1 = getMockTable("table1", "This is a table"); + Table mockTable2 = getMockTable("table2", "This is another table"); + + PlainFormat.output(new Table[] {mockTable1, mockTable2}, mockContext); + String output = new String(outContent.toByteArray(), StandardCharsets.UTF_8).trim(); + Assertions.assertEquals("table1\n" + "table2", output); + } + @Test void testOutputWithUnsupportType() { CommandContext mockContext = getMockContext(); @@ -159,13 +230,35 @@ private Schema getMockSchema(String name, String comment) { return mockSchema; } - private Audit getMockAudit() { - Audit mockAudit = mock(Audit.class); - when(mockAudit.creator()).thenReturn("demo_user"); - when(mockAudit.createTime()).thenReturn(Instant.ofEpochMilli(1611111111111L)); - when(mockAudit.lastModifier()).thenReturn("demo_user"); - when(mockAudit.lastModifiedTime()).thenReturn(Instant.ofEpochMilli(1611111111111L)); + private Table getMockTable() { + return getMockTable("demo_table", "This is a demo table"); + } + + private Table getMockTable(String name, String comment) { + Table mockTable = mock(Table.class); + org.apache.gravitino.rel.Column mockColumnInt = + getMockColumn("id", Types.IntegerType.get(), "This is a int column", false, true); + org.apache.gravitino.rel.Column mockColumnString = + getMockColumn("name", Types.StringType.get(), "This is a string column", true, false); + + when(mockTable.name()).thenReturn(name); + when(mockTable.comment()).thenReturn(comment); + when(mockTable.columns()) + .thenReturn(new org.apache.gravitino.rel.Column[] {mockColumnInt, mockColumnString}); + + return mockTable; + } + + private org.apache.gravitino.rel.Column getMockColumn( + String name, Type dataType, String comment, boolean nullable, boolean autoIncrement) { + + org.apache.gravitino.rel.Column mockColumn = mock(org.apache.gravitino.rel.Column.class); + when(mockColumn.name()).thenReturn(name); + when(mockColumn.dataType()).thenReturn(dataType); + when(mockColumn.comment()).thenReturn(comment); + when(mockColumn.nullable()).thenReturn(nullable); + when(mockColumn.autoIncrement()).thenReturn(autoIncrement); - return mockAudit; + return mockColumn; } }