Skip to content

Commit

Permalink
[#5383] Add create table command to the Gravitino CLI (#5717)
Browse files Browse the repository at this point in the history
### 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.
  • Loading branch information
justinmclean authored Dec 4, 2024
1 parent 7759d0b commit 054c6f7
Show file tree
Hide file tree
Showing 13 changed files with 522 additions and 3 deletions.
1 change: 1 addition & 0 deletions clients/cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,9 @@ private void handleTableCommand() {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -96,6 +97,7 @@ public Options options() {
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"));
Expand All @@ -104,7 +106,7 @@ public Options options() {
// 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;
}
Expand Down
152 changes: 152 additions & 0 deletions clients/cli/src/main/java/org/apache/gravitino/cli/ReadTableCSV.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
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;
Expand Down Expand Up @@ -815,4 +816,16 @@ protected UpdateColumnDefault newUpdateColumnDefault(
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);
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,30 @@

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;
Expand Down
Loading

0 comments on commit 054c6f7

Please sign in to comment.