This is an automated email from the ASF dual-hosted git repository.
diqiu50 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 c47e61b3ae [#9756] feat(catalogs): Support alternation operations for
ClickHouse tables (#9826)
c47e61b3ae is described below
commit c47e61b3aed896168d7ac2f028f01001004fae8d
Author: Qi Yu <[email protected]>
AuthorDate: Thu Feb 5 21:52:32 2026 +0800
[#9756] feat(catalogs): Support alternation operations for ClickHouse
tables (#9826)
### What changes were proposed in this pull request?
Support alternation operations for ClickHouse tables.
### Why are the changes needed?
It's a feature.
Fix: #9756
### Does this PR introduce _any_ user-facing change?
N/A
### How was this patch tested?
UTs
---------
Co-authored-by: flaming-archer <[email protected]>
---
.../operations/ClickHouseTableOperations.java | 358 ++++++++++++-
.../operations/TestClickHouseTableOperations.java | 596 +++++++++++++++++++++
.../jdbc/operation/JdbcTableOperations.java | 4 +
3 files changed, 955 insertions(+), 3 deletions(-)
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
index 8356c84291..0fc69d8279 100644
---
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
@@ -22,6 +22,7 @@ import static
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesM
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.ENGINE_PROPERTY_ENTRY;
import static org.apache.gravitino.rel.Column.DEFAULT_VALUE_NOT_SET;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
@@ -29,20 +30,27 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.StringIdentifier;
import
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.TableConstants;
import
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata;
import org.apache.gravitino.catalog.jdbc.JdbcColumn;
+import org.apache.gravitino.catalog.jdbc.JdbcTable;
import org.apache.gravitino.catalog.jdbc.operation.JdbcTableOperations;
+import org.apache.gravitino.catalog.jdbc.utils.JdbcConnectorUtils;
import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.TableChange;
import org.apache.gravitino.rel.expressions.distributions.Distribution;
import org.apache.gravitino.rel.expressions.distributions.Distributions;
@@ -55,7 +63,6 @@ import org.apache.gravitino.rel.indexes.Indexes;
public class ClickHouseTableOperations extends JdbcTableOperations {
- @SuppressWarnings("unused")
private static final String CLICKHOUSE_NOT_SUPPORT_NESTED_COLUMN_MSG =
"Clickhouse does not support nested column names.";
@@ -283,6 +290,23 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
return "YES".equalsIgnoreCase(resultSet.getString("IS_AUTOINCREMENT"));
}
+ @Override
+ public void alterTable(String databaseName, String tableName, TableChange...
changes)
+ throws NoSuchTableException {
+ LOG.info("Attempting to alter table {} from database {}", tableName,
databaseName);
+ try (Connection connection = getConnection(databaseName)) {
+ String sql = generateAlterTableSql(databaseName, tableName, changes);
+ if (StringUtils.isEmpty(sql)) {
+ LOG.info("No changes to alter table {} from database {}", tableName,
databaseName);
+ return;
+ }
+ JdbcConnectorUtils.executeUpdate(connection, sql);
+ LOG.info("Alter table {} from database {}", tableName, databaseName);
+ } catch (final SQLException se) {
+ throw this.exceptionMapper.toGravitinoException(se);
+ }
+ }
+
@Override
protected Map<String, String> getTableProperties(Connection connection,
String tableName)
throws SQLException {
@@ -327,8 +351,336 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
@Override
protected String generateAlterTableSql(
String databaseName, String tableName, TableChange... changes) {
- throw new UnsupportedOperationException(
- "ClickHouseTableOperations.generateAlterTableSql is not implemented
yet.");
+ // Not all operations require the original table information, so lazy
loading is used here
+ JdbcTable lazyLoadTable = null;
+ TableChange.UpdateComment updateComment = null;
+ List<TableChange.SetProperty> setProperties = new ArrayList<>();
+ List<String> alterSql = new ArrayList<>();
+
+ for (TableChange change : changes) {
+ if (change instanceof TableChange.UpdateComment) {
+ updateComment = (TableChange.UpdateComment) change;
+
+ } else if (change instanceof TableChange.SetProperty setProperty) {
+ // The set attribute needs to be added at the end.
+ setProperties.add(setProperty);
+
+ } else if (change instanceof TableChange.RemoveProperty) {
+ // Clickhouse does not support deleting table attributes, it can be
replaced by Set Property
+ throw new UnsupportedOperationException(
+ "Remove property for ClickHouse is not supported yet");
+
+ } else if (change instanceof TableChange.AddColumn addColumn) {
+ lazyLoadTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ alterSql.add(addColumnFieldDefinition(addColumn));
+
+ } else if (change instanceof TableChange.RenameColumn renameColumn) {
+ lazyLoadTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ alterSql.add(renameColumnFieldDefinition(renameColumn));
+
+ } else if (change instanceof TableChange.UpdateColumnDefaultValue
updateColumnDefaultValue) {
+ lazyLoadTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ alterSql.add(
+ updateColumnDefaultValueFieldDefinition(updateColumnDefaultValue,
lazyLoadTable));
+
+ } else if (change instanceof TableChange.UpdateColumnType
updateColumnType) {
+ lazyLoadTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ alterSql.add(updateColumnTypeFieldDefinition(updateColumnType,
lazyLoadTable));
+
+ } else if (change instanceof TableChange.UpdateColumnComment
updateColumnComment) {
+ lazyLoadTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ alterSql.add(updateColumnCommentFieldDefinition(updateColumnComment,
lazyLoadTable));
+
+ } else if (change instanceof TableChange.UpdateColumnPosition
updateColumnPosition) {
+ lazyLoadTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ alterSql.add(updateColumnPositionFieldDefinition(updateColumnPosition,
lazyLoadTable));
+
+ } else if (change instanceof TableChange.DeleteColumn deleteColumn) {
+ lazyLoadTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ String deleteColSql = deleteColumnFieldDefinition(deleteColumn,
lazyLoadTable);
+
+ if (StringUtils.isNotEmpty(deleteColSql)) {
+ alterSql.add(deleteColSql);
+ }
+
+ } else if (change instanceof TableChange.UpdateColumnNullability) {
+ lazyLoadTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ alterSql.add(
+ updateColumnNullabilityDefinition(
+ (TableChange.UpdateColumnNullability) change, lazyLoadTable));
+
+ } else if (change instanceof TableChange.DeleteIndex) {
+ lazyLoadTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ alterSql.add(deleteIndexDefinition(lazyLoadTable,
(TableChange.DeleteIndex) change));
+
+ } else if (change instanceof TableChange.UpdateColumnAutoIncrement) {
+ // Auto increment functionality was added in ClickHouse 25.1. Since
this PR is based on
+ // 23.x, we throw unsupported operation here.
+ throw new UnsupportedOperationException(
+ "ClickHouse auto increment is not supported in this version.");
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported table change type: " + change.getClass().getName());
+ }
+ }
+
+ // Last modified comment
+ if (null != updateComment) {
+ String newComment = updateComment.getNewComment();
+ if (null == StringIdentifier.fromComment(newComment)) {
+ // Detect and add Gravitino id.
+ JdbcTable jdbcTable = getOrCreateTable(databaseName, tableName,
lazyLoadTable);
+ StringIdentifier identifier =
StringIdentifier.fromComment(jdbcTable.comment());
+ if (null != identifier) {
+ newComment = StringIdentifier.addToComment(identifier, newComment);
+ }
+ }
+ String escapedComment = newComment.replace("'", "''");
+ alterSql.add(" MODIFY COMMENT '%s'".formatted(escapedComment));
+ }
+
+ if (!setProperties.isEmpty()) {
+ alterSql.add(generateAlterTableProperties(setProperties));
+ }
+
+ // Remove all empty SQL statements
+ List<String> nonEmptySQLs =
+
alterSql.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toList());
+ if (CollectionUtils.isEmpty(nonEmptySQLs)) {
+ return "";
+ }
+
+ // Return the generated SQL statement
+ String result =
+ "ALTER TABLE %s \n%s;"
+ .formatted(quoteIdentifier(tableName), String.join(",\n",
nonEmptySQLs));
+ LOG.info("Generated alter table:{} sql: {}", databaseName + "." +
tableName, result);
+ return result;
+ }
+
+ @VisibleForTesting
+ private String deleteIndexDefinition(
+ JdbcTable lazyLoadTable, TableChange.DeleteIndex deleteIndex) {
+ boolean indexExists =
+ Arrays.stream(lazyLoadTable.index())
+ .anyMatch(index -> index.name().equals(deleteIndex.getName()));
+
+ // Index does not exist
+ if (!indexExists) {
+ // If ifExists is true, return empty string to skip the operation
+ if (deleteIndex.isIfExists()) {
+ return "";
+ } else {
+ throw new IllegalArgumentException(
+ "Index '%s' does not exist".formatted(deleteIndex.getName()));
+ }
+ }
+
+ return "DROP INDEX %s ".formatted(quoteIdentifier(deleteIndex.getName()));
+ }
+
+ private String updateColumnNullabilityDefinition(
+ TableChange.UpdateColumnNullability change, JdbcTable table) {
+ validateUpdateColumnNullable(change, table);
+
+ String col = change.fieldName()[0];
+ JdbcColumn column = getJdbcColumnFromTable(table, col);
+ JdbcColumn updateColumn =
+ JdbcColumn.builder()
+ .withName(col)
+ .withDefaultValue(column.defaultValue())
+ .withNullable(change.nullable())
+ .withType(column.dataType())
+ .withComment(column.comment())
+ .withAutoIncrement(column.autoIncrement())
+ .build();
+
+ return "%s %s %s"
+ .formatted(
+ MODIFY_COLUMN,
+ quoteIdentifier(col),
+ appendColumnDefinition(updateColumn, new StringBuilder()));
+ }
+
+ private String generateAlterTableProperties(List<TableChange.SetProperty>
setProperties) {
+ if (CollectionUtils.isNotEmpty(setProperties)) {
+ throw new UnsupportedOperationException(
+ "Alter table properties in ClickHouse is not supported");
+ }
+
+ return "";
+ }
+
+ private String updateColumnCommentFieldDefinition(
+ TableChange.UpdateColumnComment updateColumnComment, JdbcTable
jdbcTable) {
+ String newComment = updateColumnComment.getNewComment();
+ if (updateColumnComment.fieldName().length > 1) {
+ throw new
UnsupportedOperationException(CLICKHOUSE_NOT_SUPPORT_NESTED_COLUMN_MSG);
+ }
+
+ String col = updateColumnComment.fieldName()[0];
+ JdbcColumn column = getJdbcColumnFromTable(jdbcTable, col);
+ JdbcColumn updateColumn =
+ JdbcColumn.builder()
+ .withName(col)
+ .withDefaultValue(column.defaultValue())
+ .withNullable(column.nullable())
+ .withType(column.dataType())
+ .withComment(newComment)
+ .withAutoIncrement(column.autoIncrement())
+ .build();
+
+ return "%s %s %s"
+ .formatted(
+ MODIFY_COLUMN,
+ quoteIdentifier(col),
+ appendColumnDefinition(updateColumn, new StringBuilder()));
+ }
+
+ private String addColumnFieldDefinition(TableChange.AddColumn addColumn) {
+ String dataType = typeConverter.fromGravitino(addColumn.getDataType());
+ if (addColumn.fieldName().length > 1) {
+ throw new
UnsupportedOperationException(CLICKHOUSE_NOT_SUPPORT_NESTED_COLUMN_MSG);
+ }
+
+ String col = addColumn.fieldName()[0];
+ StringBuilder columnDefinition = new StringBuilder();
+ // [IF NOT EXISTS] name [type] [default_expr] [codec] [AFTER name_after |
FIRST]
+ if (addColumn.isNullable()) {
+ columnDefinition.append(
+ "ADD COLUMN %s Nullable(%s) ".formatted(quoteIdentifier(col),
dataType));
+ } else {
+ columnDefinition.append("ADD COLUMN %s %s
".formatted(quoteIdentifier(col), dataType));
+ }
+
+ if (addColumn.isAutoIncrement()) {
+ throw new UnsupportedOperationException(
+ "ClickHouse does not support adding auto increment column");
+ }
+
+ // Append default value if available
+ if (!Column.DEFAULT_VALUE_NOT_SET.equals(addColumn.getDefaultValue())) {
+ columnDefinition.append(
+ "DEFAULT %s "
+
.formatted(columnDefaultValueConverter.fromGravitino(addColumn.getDefaultValue())));
+ }
+
+ // Append comment if available after default value
+ if (StringUtils.isNotEmpty(addColumn.getComment())) {
+ String escapedComment = StringUtils.replace(addColumn.getComment(), "'",
"''");
+ columnDefinition.append(" COMMENT '%s' ".formatted(escapedComment));
+ }
+
+ // Append position if available
+ if (addColumn.getPosition() instanceof TableChange.First) {
+ columnDefinition.append(" FIRST ");
+ } else if (addColumn.getPosition() instanceof TableChange.After
afterPosition) {
+ columnDefinition.append(" AFTER %s
".formatted(quoteIdentifier(afterPosition.getColumn())));
+ }
+
+ return columnDefinition.toString();
+ }
+
+ private String renameColumnFieldDefinition(TableChange.RenameColumn
renameColumn) {
+ if (renameColumn.fieldName().length > 1) {
+ throw new
UnsupportedOperationException(CLICKHOUSE_NOT_SUPPORT_NESTED_COLUMN_MSG);
+ }
+
+ String oldColumnName = renameColumn.fieldName()[0];
+ String newColumnName = renameColumn.getNewName();
+
+ return "RENAME COLUMN %s TO %s"
+ .formatted(quoteIdentifier(oldColumnName),
quoteIdentifier(newColumnName));
+ }
+
+ private String updateColumnPositionFieldDefinition(
+ TableChange.UpdateColumnPosition updateColumnPosition, JdbcTable
jdbcTable) {
+ if (updateColumnPosition.fieldName().length > 1) {
+ throw new
UnsupportedOperationException(CLICKHOUSE_NOT_SUPPORT_NESTED_COLUMN_MSG);
+ }
+
+ String col = updateColumnPosition.fieldName()[0];
+ JdbcColumn column = getJdbcColumnFromTable(jdbcTable, col);
+
+ StringBuilder columnDefinition = new StringBuilder();
+ columnDefinition.append(" %s %s ".formatted(MODIFY_COLUMN,
quoteIdentifier(col)));
+ appendColumnDefinition(column, columnDefinition);
+
+ if (updateColumnPosition.getPosition() instanceof TableChange.First) {
+ columnDefinition.append(" FIRST ");
+ } else if (updateColumnPosition.getPosition() instanceof TableChange.After
afterPosition) {
+ columnDefinition.append(
+ " %s %s ".formatted(AFTER,
quoteIdentifier(afterPosition.getColumn())));
+ } else {
+ Arrays.stream(jdbcTable.columns())
+ .reduce((column1, column2) -> column2)
+ .map(Column::name)
+ .ifPresent(s -> columnDefinition.append(" %s %s ".formatted(AFTER,
quoteIdentifier(s))));
+ }
+ return columnDefinition.toString();
+ }
+
+ private String deleteColumnFieldDefinition(
+ TableChange.DeleteColumn deleteColumn, JdbcTable jdbcTable) {
+ if (deleteColumn.fieldName().length > 1) {
+ throw new
UnsupportedOperationException(CLICKHOUSE_NOT_SUPPORT_NESTED_COLUMN_MSG);
+ }
+
+ String col = deleteColumn.fieldName()[0];
+ boolean colExists = columnExists(jdbcTable, col);
+ if (!colExists) {
+ if (BooleanUtils.isTrue(deleteColumn.getIfExists())) {
+ return "";
+ } else {
+ throw new IllegalArgumentException("Delete column '%s' does not
exist.".formatted(col));
+ }
+ }
+
+ return "DROP COLUMN %s".formatted(quoteIdentifier(col));
+ }
+
+ private String updateColumnDefaultValueFieldDefinition(
+ TableChange.UpdateColumnDefaultValue updateColumnDefaultValue, JdbcTable
jdbcTable) {
+ if (updateColumnDefaultValue.fieldName().length > 1) {
+ throw new
UnsupportedOperationException(CLICKHOUSE_NOT_SUPPORT_NESTED_COLUMN_MSG);
+ }
+
+ String col = updateColumnDefaultValue.fieldName()[0];
+ JdbcColumn column = getJdbcColumnFromTable(jdbcTable, col);
+ StringBuilder sqlBuilder = new StringBuilder(MODIFY_COLUMN +
quoteIdentifier(col));
+ JdbcColumn newColumn =
+ JdbcColumn.builder()
+ .withName(col)
+ .withType(column.dataType())
+ .withNullable(column.nullable())
+ .withComment(column.comment())
+ .withDefaultValue(updateColumnDefaultValue.getNewDefaultValue())
+ .build();
+
+ return appendColumnDefinition(newColumn, sqlBuilder).toString();
+ }
+
+ private String updateColumnTypeFieldDefinition(
+ TableChange.UpdateColumnType updateColumnType, JdbcTable jdbcTable) {
+ if (updateColumnType.fieldName().length > 1) {
+ throw new
UnsupportedOperationException(CLICKHOUSE_NOT_SUPPORT_NESTED_COLUMN_MSG);
+ }
+
+ String col = updateColumnType.fieldName()[0];
+ JdbcColumn column = getJdbcColumnFromTable(jdbcTable, col);
+ StringBuilder sqlBuilder =
+ new StringBuilder("%s %s ".formatted(MODIFY_COLUMN,
quoteIdentifier(col)));
+
+ JdbcColumn newColumn =
+ JdbcColumn.builder()
+ .withName(col)
+ .withType(updateColumnType.getNewDataType())
+ .withComment(column.comment())
+ .withDefaultValue(column.defaultValue())
+ .withNullable(column.nullable())
+ .withAutoIncrement(column.autoIncrement())
+ .build();
+ return appendColumnDefinition(newColumn, sqlBuilder).toString();
}
private StringBuilder appendColumnDefinition(JdbcColumn column,
StringBuilder sqlBuilder) {
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperations.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperations.java
index b5bc0af80f..1bc181fabf 100644
---
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperations.java
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperations.java
@@ -30,6 +30,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.catalog.clickhouse.ClickHouseConstants;
import
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.TableConstants;
import
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata;
@@ -39,6 +40,9 @@ import
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseExceptionConv
import
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseTypeConverter;
import org.apache.gravitino.catalog.jdbc.JdbcColumn;
import org.apache.gravitino.catalog.jdbc.JdbcTable;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.TableChange;
import org.apache.gravitino.rel.expressions.NamedReference;
import org.apache.gravitino.rel.expressions.distributions.Distributions;
import org.apache.gravitino.rel.expressions.literals.Literals;
@@ -59,6 +63,377 @@ import org.junit.jupiter.api.Test;
public class TestClickHouseTableOperations extends TestClickHouse {
private static final Type STRING = Types.StringType.get();
private static final Type INT = Types.IntegerType.get();
+ private static final Type LONG = Types.LongType.get();
+
+ @Test
+ public void testCreateAndAlterTable() {
+ String tableName = RandomStringUtils.randomAlphabetic(16) + "_op_table";
+ String tableComment = "test_comment";
+ List<JdbcColumn> columns = new ArrayList<>();
+ columns.add(
+ JdbcColumn.builder()
+ .withName("col_1")
+ .withType(STRING)
+ .withComment("test_comment")
+ .withNullable(true)
+ .build());
+ columns.add(
+ JdbcColumn.builder()
+ .withName("col_2")
+ .withType(INT)
+ .withNullable(false)
+ .withComment("set primary key")
+ .build());
+ columns.add(
+ JdbcColumn.builder()
+ .withName("col_3")
+ .withType(INT)
+ .withNullable(true)
+ .withDefaultValue(Literals.NULL)
+ .build());
+ columns.add(
+ JdbcColumn.builder()
+ .withName("col_4")
+ .withType(STRING)
+ .withDefaultValue(Literals.of("hello world", STRING))
+ .withNullable(false)
+ .build());
+ Map<String, String> properties = new HashMap<>();
+
+ Index[] indexes = new Index[] {};
+ // create table
+ TABLE_OPERATIONS.create(
+ TEST_DB_NAME.toString(),
+ tableName,
+ columns.toArray(new JdbcColumn[0]),
+ tableComment,
+ properties,
+ null,
+ Distributions.NONE,
+ indexes,
+ getSortOrders("col_2"));
+
+ // list table
+ List<String> tables = TABLE_OPERATIONS.listTables(TEST_DB_NAME.toString());
+ Assertions.assertTrue(tables.contains(tableName));
+
+ // load table
+ JdbcTable load = TABLE_OPERATIONS.load(TEST_DB_NAME.toString(), tableName);
+ assertionsTableInfo(
+ tableName, tableComment, columns, properties, indexes,
Transforms.EMPTY_TRANSFORM, load);
+
+ // rename table
+ String newName = "new_table";
+ Assertions.assertDoesNotThrow(
+ () -> TABLE_OPERATIONS.rename(TEST_DB_NAME.toString(), tableName,
newName));
+ Assertions.assertDoesNotThrow(() ->
TABLE_OPERATIONS.load(TEST_DB_NAME.toString(), newName));
+
+ // alter table add column
+ JdbcColumn newColumn =
+ JdbcColumn.builder()
+ .withName("col_5")
+ .withType(STRING)
+ .withComment("new_add")
+ .withNullable(false) //
+ .build();
+ TABLE_OPERATIONS.alterTable(
+ TEST_DB_NAME.toString(),
+ newName,
+ TableChange.addColumn(
+ new String[] {newColumn.name()},
+ newColumn.dataType(),
+ newColumn.comment(),
+ TableChange.ColumnPosition.after("col_1"),
+ newColumn.nullable(),
+ newColumn.autoIncrement(),
+ newColumn.defaultValue()));
+ load = TABLE_OPERATIONS.load(TEST_DB_NAME.toString(), newName);
+ List<JdbcColumn> alterColumns =
+ new ArrayList<JdbcColumn>() {
+ {
+ add(columns.get(0));
+ add(newColumn);
+ add(columns.get(1));
+ add(columns.get(2));
+ add(columns.get(3));
+ }
+ };
+ assertionsTableInfo(
+ newName, tableComment, alterColumns, properties, indexes,
Transforms.EMPTY_TRANSFORM, load);
+
+ // Detect unsupported properties
+ TableChange setProperty = TableChange.setProperty(CLICKHOUSE_ENGINE_KEY,
"ABC");
+ UnsupportedOperationException gravitinoRuntimeException =
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> TABLE_OPERATIONS.alterTable(TEST_DB_NAME.toString(),
newName, setProperty));
+ Assertions.assertTrue(
+ StringUtils.contains(
+ gravitinoRuntimeException.getMessage(),
+ "Alter table properties in ClickHouse is not supported"));
+
+ // delete column
+ TABLE_OPERATIONS.alterTable(
+ TEST_DB_NAME.toString(),
+ newName,
+ TableChange.deleteColumn(new String[] {newColumn.name()}, true));
+ load = TABLE_OPERATIONS.load(TEST_DB_NAME.toString(), newName);
+ assertionsTableInfo(
+ newName, tableComment, columns, properties, indexes,
Transforms.EMPTY_TRANSFORM, load);
+
+ TableChange deleteColumn = TableChange.deleteColumn(new String[]
{newColumn.name()}, false);
+ IllegalArgumentException illegalArgumentException =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> TABLE_OPERATIONS.alterTable(TEST_DB_NAME.toString(),
newName, deleteColumn));
+ Assertions.assertEquals(
+ "Delete column '%s' does not exist.".formatted(newColumn.name()),
+ illegalArgumentException.getMessage());
+ Assertions.assertDoesNotThrow(
+ () ->
+ TABLE_OPERATIONS.alterTable(
+ TEST_DB_NAME.toString(),
+ newName,
+ TableChange.deleteColumn(new String[] {newColumn.name()},
true)));
+
+ TABLE_OPERATIONS.alterTable(
+ TEST_DB_NAME.toString(),
+ newName,
+ TableChange.deleteColumn(new String[] {newColumn.name()}, true));
+ Assertions.assertTrue(
+ TABLE_OPERATIONS.drop(TEST_DB_NAME.toString(), newName), "table should
be dropped");
+
+ GravitinoRuntimeException exception =
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class,
+ () -> TABLE_OPERATIONS.drop(TEST_DB_NAME.toString(), newName));
+ Assertions.assertTrue(StringUtils.contains(exception.getMessage(), "does
not exist"));
+ }
+
+ @Test
+ public void testAlterTable() {
+ String tableName = RandomStringUtils.randomAlphabetic(16) + "_al_table";
+ String tableComment = "test_comment";
+ List<JdbcColumn> columns = new ArrayList<>();
+ JdbcColumn col_1 =
+ JdbcColumn.builder()
+ .withName("col_1")
+ .withType(INT)
+ .withComment("id")
+ .withNullable(false)
+ .withDefaultValue(Literals.integerLiteral(0))
+ .build();
+ columns.add(col_1);
+ JdbcColumn col_2 =
+ JdbcColumn.builder()
+ .withName("col_2")
+ .withType(STRING)
+ .withComment("name")
+ .withDefaultValue(Literals.of("hello world", STRING))
+ .withNullable(false)
+ .build();
+ columns.add(col_2);
+ JdbcColumn col_3 =
+ JdbcColumn.builder()
+ .withName("col_3")
+ .withType(STRING)
+ .withComment("name")
+ .withDefaultValue(Literals.NULL)
+ .build();
+ // `col_1` int NOT NULL COMMENT 'id' ,
+ // `col_2` STRING(255) NOT NULL DEFAULT 'hello world' COMMENT 'name' ,
+ // `col_3` STRING(255) NULL DEFAULT NULL COMMENT 'name' ,
+ columns.add(col_3);
+ Map<String, String> properties = new HashMap<>();
+
+ // create table
+ TABLE_OPERATIONS.create(
+ TEST_DB_NAME.toString(),
+ tableName,
+ columns.toArray(new JdbcColumn[0]),
+ tableComment,
+ properties,
+ null,
+ Distributions.NONE,
+ null,
+ getSortOrders("col_2"));
+ JdbcTable load = TABLE_OPERATIONS.load(TEST_DB_NAME.toString(), tableName);
+ assertionsTableInfo(
+ tableName, tableComment, columns, properties, null,
Transforms.EMPTY_TRANSFORM, load);
+
+ // Update column type
+ TABLE_OPERATIONS.alterTable(
+ TEST_DB_NAME.toString(),
+ tableName,
+ TableChange.updateColumnType(new String[] {col_1.name()}, LONG));
+
+ load = TABLE_OPERATIONS.load(TEST_DB_NAME.toString(), tableName);
+
+ // After modifying the type, some attributes of the corresponding column
are not
+ // supported.
+ columns.clear();
+ col_1 =
+ JdbcColumn.builder()
+ .withName(col_1.name())
+ .withType(LONG)
+ .withComment(col_1.comment())
+ .withNullable(col_1.nullable())
+ .withDefaultValue(Literals.longLiteral(0L))
+ .build();
+ columns.add(col_1);
+ columns.add(col_2);
+ columns.add(col_3);
+ assertionsTableInfo(
+ tableName, tableComment, columns, properties, null,
Transforms.EMPTY_TRANSFORM, load);
+
+ String newComment = "new_comment";
+ // update table comment and column comment
+ // `col_1` int NOT NULL COMMENT 'id' ,
+ // `col_2` STRING(255) NOT NULL DEFAULT 'hello world' COMMENT
'new_comment' ,
+ // `col_3` STRING(255) NULL DEFAULT NULL COMMENT 'name' ,
+ TABLE_OPERATIONS.alterTable(
+ TEST_DB_NAME.toString(),
+ tableName,
+ TableChange.updateColumnType(new String[] {col_1.name()}, INT),
+ TableChange.updateColumnComment(new String[] {col_2.name()},
newComment));
+ load = TABLE_OPERATIONS.load(TEST_DB_NAME.toString(), tableName);
+
+ columns.clear();
+ col_1 =
+ JdbcColumn.builder()
+ .withName(col_1.name())
+ .withType(INT)
+ .withComment(col_1.comment())
+ .withAutoIncrement(col_1.autoIncrement())
+ .withNullable(col_1.nullable())
+ .withDefaultValue(Literals.integerLiteral(0))
+ .build();
+ col_2 =
+ JdbcColumn.builder()
+ .withName(col_2.name())
+ .withType(col_2.dataType())
+ .withComment(newComment)
+ .withAutoIncrement(col_2.autoIncrement())
+ .withNullable(col_2.nullable())
+ .withDefaultValue(col_2.defaultValue())
+ .build();
+ columns.add(col_1);
+ columns.add(col_2);
+ columns.add(col_3);
+ assertionsTableInfo(
+ tableName, tableComment, columns, properties, null,
Transforms.EMPTY_TRANSFORM, load);
+
+ String newColName_1 = "new_col_1";
+ // rename column
+ // update table comment and column comment
+ // `new_col_1` int NOT NULL COMMENT 'id' ,
+ // `new_col_2` STRING(255) NOT NULL DEFAULT 'hello world' COMMENT
'new_comment'
+ // ,
+ // `col_3` STRING(255) NULL DEFAULT NULL COMMENT 'name' ,
+ TABLE_OPERATIONS.alterTable(
+ TEST_DB_NAME.toString(),
+ tableName,
+ TableChange.renameColumn(new String[] {col_1.name()}, newColName_1));
+
+ load = TABLE_OPERATIONS.load(TEST_DB_NAME.toString(), tableName);
+
+ columns.clear();
+ col_1 =
+ JdbcColumn.builder()
+ .withName(newColName_1)
+ .withType(col_1.dataType())
+ .withComment(col_1.comment())
+ .withAutoIncrement(col_1.autoIncrement())
+ .withNullable(col_1.nullable())
+ .withDefaultValue(col_1.defaultValue())
+ .build();
+ columns.add(col_1);
+ columns.add(col_2);
+ columns.add(col_3);
+ assertionsTableInfo(
+ tableName, tableComment, columns, properties, null,
Transforms.EMPTY_TRANSFORM, load);
+ }
+
+ @Test
+ public void testAlterTableUpdateColumnDefaultValue() {
+ String tableName = RandomNameUtils.genRandomName("properties_table_");
+ String tableComment = "test_comment";
+ List<JdbcColumn> columns = new ArrayList<>();
+ columns.add(
+ JdbcColumn.builder()
+ .withName("col_1")
+ .withType(Types.DecimalType.of(10, 2))
+ .withComment("test_decimal")
+ .withNullable(false)
+ .withDefaultValue(Literals.decimalLiteral(Decimal.of("0.00", 10,
2)))
+ .build());
+ columns.add(
+ JdbcColumn.builder()
+ .withName("col_2")
+ .withType(Types.LongType.get())
+ .withNullable(false)
+ .withDefaultValue(Literals.longLiteral(0L))
+ .withComment("long type")
+ .build());
+ columns.add(
+ JdbcColumn.builder()
+ .withName("col_3")
+ .withType(Types.TimestampType.withoutTimeZone(0))
+ .withNullable(false)
+ .withComment("timestamp")
+
.withDefaultValue(Literals.timestampLiteral(LocalDateTime.parse("2013-01-01T00:00:00")))
+ .build());
+ columns.add(
+ JdbcColumn.builder()
+ .withName("col_4")
+ .withType(Types.StringType.get())
+ .withNullable(false)
+ .withComment("STRING")
+ .withDefaultValue(Literals.of("hello", Types.StringType.get()))
+ .build());
+ Map<String, String> properties = new HashMap<>();
+
+ Index[] indexes = new Index[0];
+ // create table
+ TABLE_OPERATIONS.create(
+ TEST_DB_NAME.toString(),
+ tableName,
+ columns.toArray(new JdbcColumn[0]),
+ tableComment,
+ properties,
+ null,
+ Distributions.NONE,
+ indexes,
+ getSortOrders("col_2"));
+
+ JdbcTable loaded = TABLE_OPERATIONS.load(TEST_DB_NAME.toString(),
tableName);
+ assertionsTableInfo(
+ tableName, tableComment, columns, properties, indexes,
Transforms.EMPTY_TRANSFORM, loaded);
+
+ TABLE_OPERATIONS.alterTable(
+ TEST_DB_NAME.toString(),
+ tableName,
+ TableChange.updateColumnDefaultValue(
+ new String[] {columns.get(0).name()},
+ Literals.decimalLiteral(Decimal.of("1.23", 10, 2))),
+ TableChange.updateColumnDefaultValue(
+ new String[] {columns.get(1).name()}, Literals.longLiteral(1L)),
+ TableChange.updateColumnDefaultValue(
+ new String[] {columns.get(2).name()},
+
Literals.timestampLiteral(LocalDateTime.parse("2024-04-01T00:00:00"))),
+ TableChange.updateColumnDefaultValue(
+ new String[] {columns.get(3).name()}, Literals.of("world",
Types.StringType.get())));
+
+ loaded = TABLE_OPERATIONS.load(TEST_DB_NAME.toString(), tableName);
+ Assertions.assertEquals(
+ Literals.decimalLiteral(Decimal.of("1.23", 10, 2)),
loaded.columns()[0].defaultValue());
+ Assertions.assertEquals(Literals.longLiteral(1L),
loaded.columns()[1].defaultValue());
+ Assertions.assertEquals(
+ Literals.timestampLiteral(LocalDateTime.parse("2024-04-01T00:00:00")),
+ loaded.columns()[2].defaultValue());
+ Assertions.assertEquals(
+ Literals.of("world", Types.StringType.get()),
loaded.columns()[3].defaultValue());
+ }
@Test
public void testCreateAndLoadTable() {
@@ -633,4 +1008,225 @@ public class TestClickHouseTableOperations extends
TestClickHouse {
tableName, columns, comment, properties, partitioning, distribution,
indexes, sortOrders);
}
}
+
+ @Test
+ public void testGenerateAlterTableSqlCoverage() {
+ StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+ ops.initialize(
+ null,
+ new ClickHouseExceptionConverter(),
+ new ClickHouseTypeConverter(),
+ new ClickHouseColumnDefaultValueConverter(),
+ new HashMap<>());
+
+ JdbcTable table = buildStubTable();
+ ops.setTable(table);
+
+ TableChange[] changes =
+ new TableChange[] {
+ TableChange.addColumn(
+ new String[] {"c_new"},
+ Types.StringType.get(),
+ "new column",
+ TableChange.ColumnPosition.after("c1"),
+ true,
+ false,
+ Column.DEFAULT_VALUE_NOT_SET),
+ TableChange.updateColumnDefaultValue(
+ new String[] {"c2"}, Literals.of("val", Types.StringType.get())),
+ TableChange.updateColumnType(new String[] {"c1"},
Types.LongType.get()),
+ TableChange.updateColumnComment(new String[] {"c1"}, "c1_comment"),
+ TableChange.updateColumnPosition(new String[] {"c1"},
TableChange.ColumnPosition.first()),
+ TableChange.deleteColumn(new String[] {"c3"}, false),
+ TableChange.updateColumnNullability(new String[] {"c2"}, false),
+ TableChange.deleteIndex("idx1", false),
+ TableChange.renameColumn(new String[] {"c2"}, "c2_new"),
+ TableChange.updateComment("new_table_comment")
+ };
+
+ String sql = ops.buildAlterSql("db", "tbl", changes);
+
+ Assertions.assertTrue(sql.contains("ADD COLUMN `c_new` Nullable(String)"));
+ Assertions.assertTrue(sql.contains("RENAME COLUMN `c2` TO `c2_new`"));
+ Assertions.assertTrue(sql.contains("DEFAULT 'val'"));
+ Assertions.assertTrue(sql.contains("Int64"));
+ Assertions.assertTrue(sql.contains("COMMENT 'c1_comment'"));
+ Assertions.assertTrue(sql.contains("FIRST"));
+ Assertions.assertTrue(sql.contains("DROP COLUMN `c3`"));
+ Assertions.assertTrue(sql.contains("DROP INDEX `idx1`"));
+ Assertions.assertTrue(sql.contains("MODIFY COMMENT 'new_table_comment'"));
+ Assertions.assertTrue(sql.startsWith("ALTER TABLE `tbl`"));
+ }
+
+ @Test
+ public void testAlterTableDeleteColumnIfExistsNoOpReturnsEmpty() {
+ StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+ ops.initialize(
+ null,
+ new ClickHouseExceptionConverter(),
+ new ClickHouseTypeConverter(),
+ new ClickHouseColumnDefaultValueConverter(),
+ new HashMap<>());
+ ops.setTable(buildStubTable());
+
+ String sql =
+ ops.buildAlterSql(
+ "db",
+ "tbl",
+ new TableChange[] {TableChange.deleteColumn(new String[]
{"missing"}, true)});
+ Assertions.assertEquals("", sql);
+ }
+
+ @Test
+ public void testAlterTableDeleteIndexBranches() {
+ StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+ ops.initialize(
+ null,
+ new ClickHouseExceptionConverter(),
+ new ClickHouseTypeConverter(),
+ new ClickHouseColumnDefaultValueConverter(),
+ new HashMap<>());
+ ops.setTable(buildStubTable());
+
+ String sqlSkip =
+ ops.buildAlterSql(
+ "db", "tbl", new TableChange[] {TableChange.deleteIndex("missing",
true)});
+ Assertions.assertEquals("", sqlSkip);
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ops.buildAlterSql(
+ "db", "tbl", new TableChange[]
{TableChange.deleteIndex("missing", false)}));
+ }
+
+ @Test
+ public void testAlterTableNullabilityValidationFails() {
+ StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+ ops.initialize(
+ null,
+ new ClickHouseExceptionConverter(),
+ new ClickHouseTypeConverter(),
+ new ClickHouseColumnDefaultValueConverter(),
+ new HashMap<>());
+ ops.setTable(buildStubTableWithNullDefault());
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ops.buildAlterSql(
+ "db",
+ "tbl",
+ new TableChange[] {
+ TableChange.updateColumnNullability(new String[] {"c3"},
false)
+ }));
+ }
+
+ @Test
+ public void testAlterTableSetPropertyUnsupported() {
+ StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+ ops.initialize(
+ null,
+ new ClickHouseExceptionConverter(),
+ new ClickHouseTypeConverter(),
+ new ClickHouseColumnDefaultValueConverter(),
+ new HashMap<>());
+ ops.setTable(buildStubTable());
+
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ ops.buildAlterSql("db", "tbl", new TableChange[]
{TableChange.setProperty("k", "v")}));
+ }
+
+ @Test
+ public void testAlterTableRemovePropertyUnsupported() {
+ StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+ ops.initialize(
+ null,
+ new ClickHouseExceptionConverter(),
+ new ClickHouseTypeConverter(),
+ new ClickHouseColumnDefaultValueConverter(),
+ new HashMap<>());
+ ops.setTable(buildStubTable());
+
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> ops.buildAlterSql("db", "tbl", new TableChange[]
{TableChange.removeProperty("k")}));
+ }
+
+ private static JdbcTable buildStubTable() {
+ JdbcColumn c1 =
+ JdbcColumn.builder()
+ .withName("c1")
+ .withType(Types.IntegerType.get())
+ .withNullable(false)
+ .withDefaultValue(Literals.integerLiteral(1))
+ .build();
+ JdbcColumn c2 =
+ JdbcColumn.builder()
+ .withName("c2")
+ .withType(Types.StringType.get())
+ .withNullable(true)
+ .withDefaultValue(Literals.of("x", Types.StringType.get()))
+ .build();
+ JdbcColumn c3 =
+ JdbcColumn.builder()
+ .withName("c3")
+ .withType(Types.StringType.get())
+ .withNullable(true)
+ .withDefaultValue(DEFAULT_VALUE_NOT_SET)
+ .build();
+ return JdbcTable.builder()
+ .withName("tbl")
+ .withColumns(new JdbcColumn[] {c1, c2, c3})
+ .withIndexes(
+ new Index[] {
+ Indexes.primary(Indexes.DEFAULT_PRIMARY_KEY_NAME, new String[][]
{{"c1"}}),
+ Indexes.unique("idx1", new String[][] {{"c2"}})
+ })
+ .withComment("table_comment")
+ .withTableOperation(null)
+ .build();
+ }
+
+ private static JdbcTable buildStubTableWithNullDefault() {
+ JdbcColumn c1 =
JdbcColumn.builder().withName("c1").withType(Types.IntegerType.get()).build();
+ JdbcColumn c2 =
JdbcColumn.builder().withName("c2").withType(Types.StringType.get()).build();
+ JdbcColumn c3 =
+ JdbcColumn.builder()
+ .withName("c3")
+ .withType(Types.StringType.get())
+ .withNullable(true)
+ .withDefaultValue(Literals.NULL)
+ .build();
+ return JdbcTable.builder()
+ .withName("tbl")
+ .withColumns(new JdbcColumn[] {c1, c2, c3})
+ .withIndexes(
+ new Index[] {
+ Indexes.primary(Indexes.DEFAULT_PRIMARY_KEY_NAME, new String[][]
{{"c1"}})
+ })
+ .withComment("table_comment")
+ .withTableOperation(null)
+ .build();
+ }
+
+ private static final class StubClickHouseTableOperations extends
ClickHouseTableOperations {
+ private JdbcTable table;
+
+ void setTable(JdbcTable table) {
+ this.table = table;
+ }
+
+ @Override
+ protected JdbcTable getOrCreateTable(
+ String databaseName, String tableName, JdbcTable lazyLoadCreateTable) {
+ return table;
+ }
+
+ String buildAlterSql(String db, String tableName, TableChange[] changes) {
+ return generateAlterTableSql(db, tableName, changes);
+ }
+ }
}
diff --git
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/operation/JdbcTableOperations.java
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/operation/JdbcTableOperations.java
index 1877ecf519..7af4c94b0c 100644
---
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/operation/JdbcTableOperations.java
+++
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/operation/JdbcTableOperations.java
@@ -661,6 +661,10 @@ public abstract class JdbcTableOperations implements
TableOperation {
"Column %s does not exist in table %s", colName,
jdbcTable.name()));
}
+ protected boolean columnExists(JdbcTable table, String columnName) {
+ return Arrays.stream(table.columns()).anyMatch(col ->
col.name().equals(columnName));
+ }
+
protected Connection getConnection(String catalog) throws SQLException {
Connection connection = dataSource.getConnection();
connection.setCatalog(catalog);