This is an automated email from the ASF dual-hosted git repository.

shaofengshi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 054c6f7df [#5383] Add create table command to the Gravitino CLI (#5717)
054c6f7df is described below

commit 054c6f7dfa834fd61eb96f37036f82865f6c4e74
Author: Justin Mclean <jus...@classsoftware.com>
AuthorDate: Wed Dec 4 15:05:42 2024 +1100

    [#5383] Add create table command to the Gravitino CLI (#5717)
    
    ### What changes were proposed in this pull request?
    
     Add the ability to create a table via the Gravitino CLI.
    
    ### Why are the changes needed?
    
    so we can create tables via the CLI.
    
    Fix: #5383
    
    ### Does this PR introduce _any_ user-facing change?
    
    No, but it expands on CLI commands.
    
    ### How was this patch tested?
    
    Tested locally.
---
 clients/cli/build.gradle.kts                       |   1 +
 .../org/apache/gravitino/cli/DefaultConverter.java |   5 +
 .../org/apache/gravitino/cli/ErrorMessages.java    |   2 +-
 .../apache/gravitino/cli/GravitinoCommandLine.java |   4 +-
 .../org/apache/gravitino/cli/GravitinoOptions.java |   4 +-
 .../org/apache/gravitino/cli/ReadTableCSV.java     | 152 ++++++++++++++++++++
 .../apache/gravitino/cli/TestableCommandLine.java  |  13 ++
 .../apache/gravitino/cli/commands/CreateTable.java | 118 ++++++++++++++++
 .../apache/gravitino/cli/TestDefaultConverter.java |  24 ++++
 .../org/apache/gravitino/cli/TestReadTableCSV.java | 153 +++++++++++++++++++++
 .../apache/gravitino/cli/TestTableCommands.java    |  31 +++++
 docs/cli.md                                        |  16 +++
 gradle/libs.versions.toml                          |   2 +
 13 files changed, 522 insertions(+), 3 deletions(-)

diff --git a/clients/cli/build.gradle.kts b/clients/cli/build.gradle.kts
index 71608ee91..9358808f9 100644
--- a/clients/cli/build.gradle.kts
+++ b/clients/cli/build.gradle.kts
@@ -24,6 +24,7 @@ plugins {
 
 dependencies {
   implementation(libs.commons.cli.new)
+  implementation(libs.commons.csv)
   implementation(libs.guava)
   implementation(libs.slf4j.api)
   implementation(libs.slf4j.simple)
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/DefaultConverter.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/DefaultConverter.java
index 29b0b3475..c13b77265 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/DefaultConverter.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/DefaultConverter.java
@@ -35,6 +35,11 @@ public class DefaultConverter {
    * @return An instance of the appropriate default value.
    */
   public static Expression convert(String defaultValue, String dataType) {
+
+    if (dataType == null || dataType.isEmpty()) {
+      return Column.DEFAULT_VALUE_NOT_SET;
+    }
+
     Type convertedDatatype = ParseType.toType(dataType);
 
     if (defaultValue == null || defaultValue.isEmpty()) {
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
index 351a40fc4..774f4d790 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
@@ -50,7 +50,7 @@ public class ErrorMessages {
   public static final String TAG_EMPTY = "Error: Must configure --tag option.";
   public static final String UNKNOWN_ROLE = "Unknown role.";
   public static final String ROLE_EXISTS = "Role already exists.";
-
+  public static final String TABLE_EXISTS = "Table already exists.";
   public static final String INVALID_SET_COMMAND =
       "Unsupported combination of options either use --name, --user, --group 
or --property and --value.";
   public static final String INVALID_REMOVE_COMMAND =
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java
index ade9947de..683efe9f3 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java
@@ -315,7 +315,9 @@ public class GravitinoCommandLine extends 
TestableCommandLine {
         newTableDetails(url, ignore, metalake, catalog, schema, 
table).handle();
       }
     } else if (CommandActions.CREATE.equals(command)) {
-      // TODO
+      String columnFile = line.getOptionValue(GravitinoOptions.COLUMNFILE);
+      String comment = line.getOptionValue(GravitinoOptions.COMMENT);
+      newCreateTable(url, ignore, metalake, catalog, schema, table, 
columnFile, comment).handle();
     } else if (CommandActions.DELETE.equals(command)) {
       boolean force = line.hasOption(GravitinoOptions.FORCE);
       newDeleteTable(url, ignore, force, metalake, catalog, schema, 
table).handle();
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java
index 4663650ea..fe09380d4 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java
@@ -50,6 +50,7 @@ public class GravitinoOptions {
   public static final String ROLE = "role";
   public static final String AUDIT = "audit";
   public static final String FORCE = "force";
+  public static final String COLUMNFILE = "columnfile";
   public static final String INDEX = "index";
   public static final String DISTRIBUTION = "distribution";
   public static final String PARTITION = "partition";
@@ -96,6 +97,7 @@ public class GravitinoOptions {
     options.addOption(createArgOption(DEFAULT, "default column value"));
     options.addOption(createSimpleOption("o", OWNER, "display entity owner"));
     options.addOption(createArgOption("r", ROLE, "role name"));
+    options.addOption(createArgOption(COLUMNFILE, "CSV file describing 
columns"));
 
     // Properties and tags can have multiple values
     options.addOption(createArgsOption("p", PROPERTIES, "property name/value 
pairs"));
@@ -104,7 +106,7 @@ public class GravitinoOptions {
     // Force delete entities and rename metalake operations
     options.addOption(createSimpleOption("f", FORCE, "force operation"));
 
-    options.addOption(createArgOption(null, OUTPUT, "output format 
(plain/table)"));
+    options.addOption(createArgOption(OUTPUT, "output format (plain/table)"));
 
     return options;
   }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/ReadTableCSV.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/ReadTableCSV.java
new file mode 100644
index 000000000..3a9ca3a6d
--- /dev/null
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/ReadTableCSV.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.cli;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+import org.apache.gravitino.rel.Column;
+
+public class ReadTableCSV {
+
+  private enum ExpectedColumns {
+    NAME("Name"),
+    DATATYPE("Datatype"),
+    COMMENT("Comment"),
+    NULLABLE("Nullable"),
+    AUTOINCREMENT("AutoIncrement"),
+    DEFAULTVALUE("DefaultValue"),
+    DEFAULTTYPE("DefaultType");
+
+    private final String name;
+
+    ExpectedColumns(String name) {
+      this.name = name;
+    }
+
+    public String getName() {
+      return name;
+    }
+  }
+
+  public Column[] columns(Map<String, List<String>> tableData) {
+    List<String> names = tableData.get(ExpectedColumns.NAME.getName());
+    List<String> datatypes = tableData.get(ExpectedColumns.DATATYPE.getName());
+    List<String> comments = tableData.get(ExpectedColumns.COMMENT.getName());
+    List<String> nullables = tableData.get(ExpectedColumns.NULLABLE.getName());
+    List<String> autoIncs = 
tableData.get(ExpectedColumns.AUTOINCREMENT.getName());
+    List<String> defaultTypes = 
tableData.get(ExpectedColumns.DEFAULTTYPE.getName());
+    List<String> defaulValues = 
tableData.get(ExpectedColumns.DEFAULTVALUE.getName());
+    int size = names.size();
+    Column[] columns = new Column[size];
+
+    for (int i = 0; i < size; i++) {
+      String columnName = names.get(i);
+      String datatype = datatypes.get(i);
+      String comment = comments.get(i);
+      boolean nullable = nullables.get(i).equals("true");
+      boolean auto = autoIncs.get(i).equals("true");
+      String defaultValue = defaulValues.get(i);
+      String defaultType = defaultTypes.get(i);
+
+      if (defaultType == null || defaultType.isEmpty()) {
+        defaultType = datatype;
+      }
+
+      Column column =
+          Column.of(
+              columnName,
+              ParseType.toType(datatype),
+              comment,
+              nullable,
+              auto,
+              DefaultConverter.convert(defaultValue, defaultType));
+      columns[i] = column;
+    }
+
+    return columns;
+  }
+
+  public Map<String, List<String>> parse(String csvFile) {
+
+    // Initialize a Map to store each column's values in a list
+    HashMap<String, List<String>> tableData = new HashMap<>();
+    for (ExpectedColumns column : ExpectedColumns.values()) {
+      tableData.put(column.getName(), new ArrayList<>());
+    }
+
+    try (BufferedReader reader =
+        Files.newBufferedReader(Paths.get(csvFile), StandardCharsets.UTF_8)) {
+      CSVParser csvParser =
+          new CSVParser(
+              reader,
+              CSVFormat.Builder.create()
+                  .setHeader(
+                      Arrays.stream(ExpectedColumns.values())
+                          .map(ExpectedColumns::getName)
+                          .toArray(String[]::new))
+                  .setIgnoreHeaderCase(true)
+                  .setSkipHeaderRecord(true)
+                  .setTrim(true)
+                  .setIgnoreEmptyLines(true)
+                  .build());
+      for (CSVRecord cvsRecord : csvParser) {
+        String defaultValue = null;
+        String value = null;
+
+        for (ExpectedColumns column : ExpectedColumns.values()) {
+          switch (column) {
+            case NULLABLE:
+              defaultValue = "true";
+              break;
+            case AUTOINCREMENT:
+              defaultValue = "false";
+              break;
+            default:
+              defaultValue = null;
+              break;
+          }
+
+          try {
+            value = cvsRecord.get(column.getName());
+          } catch (IllegalArgumentException exp) {
+            value = defaultValue; // missing value
+          }
+
+          tableData.get(column.getName()).add(value);
+        }
+      }
+    } catch (IOException exp) {
+      System.err.println(exp.getMessage());
+    }
+
+    return tableData;
+  }
+}
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
index 7c5d62ccf..ac285804e 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
@@ -33,6 +33,7 @@ import org.apache.gravitino.cli.commands.CreateGroup;
 import org.apache.gravitino.cli.commands.CreateMetalake;
 import org.apache.gravitino.cli.commands.CreateRole;
 import org.apache.gravitino.cli.commands.CreateSchema;
+import org.apache.gravitino.cli.commands.CreateTable;
 import org.apache.gravitino.cli.commands.CreateTag;
 import org.apache.gravitino.cli.commands.CreateTopic;
 import org.apache.gravitino.cli.commands.CreateUser;
@@ -815,4 +816,16 @@ public class TestableCommandLine {
     return new UpdateColumnDefault(
         url, ignore, metalake, catalog, schema, table, column, defaultValue, 
dataType);
   }
+
+  protected CreateTable newCreateTable(
+      String url,
+      boolean ignore,
+      String metalake,
+      String catalog,
+      String schema,
+      String table,
+      String columnFile,
+      String comment) {
+    return new CreateTable(url, ignore, metalake, catalog, schema, table, 
columnFile, comment);
+  }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateTable.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateTable.java
new file mode 100644
index 000000000..91e7b6e30
--- /dev/null
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateTable.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.cli.commands;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.cli.ErrorMessages;
+import org.apache.gravitino.cli.ReadTableCSV;
+import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.exceptions.NoSuchCatalogException;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.TableAlreadyExistsException;
+import org.apache.gravitino.rel.Column;
+
+public class CreateTable extends Command {
+  protected final String metalake;
+  protected final String catalog;
+  protected final String schema;
+  protected final String table;
+  protected final String columnFile;
+  protected final String comment;
+
+  /**
+   * Create a new table.
+   *
+   * @param url The URL of the Gravitino server.
+   * @param ignoreVersions If true don't check the client/server versions 
match.
+   * @param metalake The name of the metalake.
+   * @param catalog The name of the catalog.
+   * @param schema The name of the schema.
+   * @param table The name of the table.
+   * @param columnFile The file name containing the CSV column info.
+   * @param comment The table's comment.
+   */
+  public CreateTable(
+      String url,
+      boolean ignoreVersions,
+      String metalake,
+      String catalog,
+      String schema,
+      String table,
+      String columnFile,
+      String comment) {
+    super(url, ignoreVersions);
+    this.metalake = metalake;
+    this.catalog = catalog;
+    this.schema = schema;
+    this.table = table;
+    this.columnFile = columnFile;
+    this.comment = comment;
+  }
+
+  /** Create a new table. */
+  @Override
+  public void handle() {
+    NameIdentifier tableName;
+    GravitinoClient client;
+    ReadTableCSV readTableCSV = new ReadTableCSV();
+    Map<String, List<String>> tableData;
+    Column[] columns;
+
+    try {
+      tableName = NameIdentifier.of(schema, table);
+      client = buildClient(metalake);
+    } catch (NoSuchMetalakeException err) {
+      System.err.println(ErrorMessages.UNKNOWN_METALAKE);
+      return;
+    } catch (Exception exp) {
+      System.err.println("Error initializing client or table name: " + 
exp.getMessage());
+      return;
+    }
+
+    try {
+      tableData = readTableCSV.parse(columnFile);
+      columns = readTableCSV.columns(tableData);
+    } catch (Exception exp) {
+      System.err.println("Error reading or parsing column file: " + 
exp.getMessage());
+      return;
+    }
+
+    try {
+      client.loadCatalog(catalog).asTableCatalog().createTable(tableName, 
columns, comment, null);
+    } catch (NoSuchCatalogException err) {
+      System.err.println(ErrorMessages.UNKNOWN_CATALOG);
+      return;
+    } catch (NoSuchSchemaException err) {
+      System.err.println(ErrorMessages.UNKNOWN_SCHEMA);
+      return;
+    } catch (TableAlreadyExistsException err) {
+      System.err.println(ErrorMessages.TABLE_EXISTS);
+      return;
+    } catch (Exception exp) {
+      System.err.println(exp.getMessage());
+      return;
+    }
+
+    System.out.println(table + " created");
+  }
+}
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestDefaultConverter.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestDefaultConverter.java
index 7aa20bd37..8dc892b6f 100644
--- 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestDefaultConverter.java
+++ 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestDefaultConverter.java
@@ -28,6 +28,30 @@ import org.junit.jupiter.api.Test;
 
 class TestDefaultConverter {
 
+  @Test
+  void testConvertNulls() {
+    String defaultValue = null;
+    String dataType = null;
+    Expression result = DefaultConverter.convert(defaultValue, dataType);
+
+    assertEquals(
+        Column.DEFAULT_VALUE_NOT_SET,
+        result,
+        "Expected DEFAULT_VALUE_NOT_SET for null defaultValue.");
+  }
+
+  @Test
+  void testConvertEmpty() {
+    String defaultValue = "";
+    String dataType = "";
+    Expression result = DefaultConverter.convert(defaultValue, dataType);
+
+    assertEquals(
+        Column.DEFAULT_VALUE_NOT_SET,
+        result,
+        "Expected DEFAULT_VALUE_NOT_SET for null defaultValue.");
+  }
+
   @Test
   void testConvertNullDefaultValue() {
     String defaultValue = null;
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestReadTableCSV.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestReadTableCSV.java
new file mode 100644
index 000000000..d0e8001aa
--- /dev/null
+++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestReadTableCSV.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
+import org.apache.gravitino.cli.DefaultConverter;
+import org.apache.gravitino.cli.ParseType;
+import org.apache.gravitino.cli.ReadTableCSV;
+import org.apache.gravitino.rel.Column;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestReadTableCSV {
+
+  private ReadTableCSV readTableCSV;
+  private Path tempFile;
+
+  @BeforeEach
+  public void setUp() throws IOException {
+    readTableCSV = new ReadTableCSV();
+    tempFile = Files.createTempFile("test-data", ".csv");
+
+    // Write the header the CSV file
+    try (BufferedWriter writer = Files.newBufferedWriter(tempFile, 
StandardCharsets.UTF_8)) {
+      CSVPrinter csvPrinter =
+          new CSVPrinter(
+              writer,
+              CSVFormat.Builder.create()
+                  .setHeader(
+                      "Name",
+                      "Datatype",
+                      "Comment",
+                      "Nullable",
+                      "AutoIncrement",
+                      "DefaultValue",
+                      "DefaultType")
+                  .build());
+
+      // Print records to the CSV file
+      csvPrinter.printRecord("name", "String", "Sample comment");
+      csvPrinter.printRecord("ID", "Integer", "Another comment", "false", 
"true", "0");
+      csvPrinter.printRecord(
+          "location", "String", "More comments", "false", "false", "Sydney", 
"String");
+    }
+  }
+
+  @Test
+  public void testParse() {
+    Map<String, List<String>> tableData = 
readTableCSV.parse(tempFile.toString());
+
+    // Validate the data in the tableData map
+    assertEquals(
+        Arrays.asList("name", "ID", "location"), tableData.get("Name"), "Name 
column should match");
+    assertEquals(
+        Arrays.asList("String", "Integer", "String"),
+        tableData.get("Datatype"),
+        "Datatype column should match");
+    assertEquals(
+        Arrays.asList("Sample comment", "Another comment", "More comments"),
+        tableData.get("Comment"),
+        "Comment column should match");
+    assertEquals(
+        Arrays.asList("true", "false", "false"),
+        tableData.get("Nullable"),
+        "Nullable column should match");
+    assertEquals(
+        Arrays.asList("false", "true", "false"),
+        tableData.get("AutoIncrement"),
+        "AutoIncrement column should match");
+    assertEquals(
+        Arrays.asList(null, "0", "Sydney"),
+        tableData.get("DefaultValue"),
+        "DefaultValue column should match");
+    assertEquals(
+        Arrays.asList(null, null, "String"),
+        tableData.get("DefaultType"),
+        "DefaultType column should match");
+  }
+
+  @Test
+  void testColumns() {
+    ReadTableCSV readTableCSV = new ReadTableCSV();
+    Map<String, List<String>> tableData = 
readTableCSV.parse(tempFile.toString());
+    Column[] columns = readTableCSV.columns(tableData);
+    Column expectedColumn1 =
+        Column.of("name", ParseType.toType("STRING"), "Sample comment", true, 
false, null);
+    Column expectedColumn2 =
+        Column.of(
+            "ID",
+            ParseType.toType("INTEGER"),
+            "Another comment",
+            false,
+            true,
+            DefaultConverter.convert("0", "INTEGER"));
+    Column expectedColumn3 =
+        Column.of(
+            "location",
+            ParseType.toType("STRING"),
+            "More comments",
+            false,
+            false,
+            DefaultConverter.convert("Sydney", "STRING"));
+
+    assertEquals(3, columns.length, "Number of columns should match");
+
+    assertEquals(expectedColumn1.name(), columns[0].name());
+    assertEquals(expectedColumn1.dataType(), columns[0].dataType());
+    assertEquals(expectedColumn1.comment(), columns[0].comment());
+    assertEquals(expectedColumn1.nullable(), columns[0].nullable());
+    assertEquals(expectedColumn1.autoIncrement(), columns[0].autoIncrement());
+    assertEquals(expectedColumn1.defaultValue(), columns[0].defaultValue());
+
+    assertEquals(expectedColumn2.name(), columns[1].name());
+    assertEquals(expectedColumn2.dataType(), columns[1].dataType());
+    assertEquals(expectedColumn2.comment(), columns[1].comment());
+    assertEquals(expectedColumn2.nullable(), columns[1].nullable());
+    assertEquals(expectedColumn2.autoIncrement(), columns[1].autoIncrement());
+    assertEquals(expectedColumn2.defaultValue(), columns[1].defaultValue());
+
+    assertEquals(expectedColumn3.name(), columns[2].name());
+    assertEquals(expectedColumn3.dataType(), columns[2].dataType());
+    assertEquals(expectedColumn3.comment(), columns[2].comment());
+    assertEquals(expectedColumn3.nullable(), columns[2].nullable());
+    assertEquals(expectedColumn3.autoIncrement(), columns[2].autoIncrement());
+    assertEquals(expectedColumn3.defaultValue(), columns[2].defaultValue());
+  }
+}
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestTableCommands.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestTableCommands.java
index e800bc60e..07cbdbdcc 100644
--- a/clients/cli/src/test/java/org/apache/gravitino/cli/TestTableCommands.java
+++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestTableCommands.java
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.when;
 
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Options;
+import org.apache.gravitino.cli.commands.CreateTable;
 import org.apache.gravitino.cli.commands.DeleteTable;
 import org.apache.gravitino.cli.commands.ListIndexes;
 import org.apache.gravitino.cli.commands.ListTableProperties;
@@ -379,4 +380,34 @@ class TestTableCommands {
     commandLine.handleCommandLine();
     verify(mockUpdate).handle();
   }
+
+  @Test
+  void testCreateTable() {
+    CreateTable mockCreate = mock(CreateTable.class);
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema.users");
+    
when(mockCommandLine.hasOption(GravitinoOptions.COLUMNFILE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.COLUMNFILE)).thenReturn("users.csv");
+    when(mockCommandLine.hasOption(GravitinoOptions.COMMENT)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.COMMENT)).thenReturn("comment");
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.TABLE, 
CommandActions.CREATE));
+    doReturn(mockCreate)
+        .when(commandLine)
+        .newCreateTable(
+            GravitinoCommandLine.DEFAULT_URL,
+            false,
+            "metalake_demo",
+            "catalog",
+            "schema",
+            "users",
+            "users.csv",
+            "comment");
+    commandLine.handleCommandLine();
+    verify(mockCreate).handle();
+  }
 }
diff --git a/docs/cli.md b/docs/cli.md
index 75eb219ce..c0e868869 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -33,6 +33,7 @@ The general structure for running commands with the Gravitino 
CLI is `gcli entit
  -a,--audit              display audit information
     --auto <arg>         column value auto-increments (true/false)
  -c,--comment <arg>      entity comment
+    --columnfile <arg>   CSV file describing columns
  -d,--distribution       display distribution information
     --datatype <arg>     column data type
     --default <arg>      default column value
@@ -367,6 +368,16 @@ Setting and removing schema properties is not currently 
supported by the Java AP
 
 ### Table commands
 
+When creating a table the columns are specified in CSV file specifying the 
name of the column, the datatype, a comment, true or false if the column is 
nullable, true or false if the column is auto incremented, a default value and 
a default type. Not all of the columns need to be specifed just the name and 
datatype columns. If not specified comment default to null, nullability to true 
and auto increment to false. If only the default value is specified it defaults 
to the same data type as  [...]
+
+Example CSV file
+```text
+Name,Datatype,Comment,Nullable,AutoIncrement,DefaultValue,DefaultType
+name,String,person's name
+ID,Integer,unique id,false,true
+location,String,city they work in,false,false,Sydney,String
+```
+
 #### Show all tables
 
 ```bash
@@ -431,6 +442,11 @@ gcli table set --name catalog_postgres.hr.salaries 
--property test --value value
 gcli table remove --name catalog_postgres.hr.salaries --property test
 ```
 
+#### Create a table
+
+```bash
+gcli table create --name catalog_postgres.hr.salaries --comment "comment" 
--columnfile ~/table.csv
+```
 
 ### User commands
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4b7441ea2..f1f29cf18 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -40,6 +40,7 @@ hadoop-minikdc = "3.3.0"
 htrace-core4 = "4.1.0-incubating"
 httpclient5 = "5.2.1"
 mockserver = "5.15.0"
+commons-csv = "1.12.0"
 commons-lang3 = "3.14.0"
 commons-lang = "2.6"
 commons-logging = "1.2"
@@ -178,6 +179,7 @@ airlift-resolver = { group = "io.airlift.resolver", name = 
"resolver", version.r
 httpclient5 = { group = "org.apache.httpcomponents.client5", name = 
"httpclient5", version.ref = "httpclient5" }
 mockserver-netty = { group = "org.mock-server", name = "mockserver-netty", 
version.ref = "mockserver" }
 mockserver-client-java = { group = "org.mock-server", name = 
"mockserver-client-java", version.ref = "mockserver" }
+commons-csv = { group = "org.apache.commons", name = "commons-csv", 
version.ref = "commons-csv" }
 commons-lang = { group = "commons-lang", name = "commons-lang", version.ref = 
"commons-lang" }
 commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", 
version.ref = "commons-lang3" }
 commons-logging = { group = "commons-logging", name = "commons-logging", 
version.ref = "commons-logging" }

Reply via email to