This is an automated email from the ASF dual-hosted git repository.
nicholasjiang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-paimon-webui.git
The following commit(s) were added to refs/heads/main by this push:
new aa93eb6 [Improvement] Refactor api module (#45)
aa93eb6 is described below
commit aa93eb671baff1ec37d1d2471b2c45f1c9848b21
Author: s7monk <[email protected]>
AuthorDate: Tue Oct 10 21:05:00 2023 +0800
[Improvement] Refactor api module (#45)
---
.../paimon/web/api/catalog/PaimonService.java | 300 +++++++++++++
.../web/api/catalog/PaimonServiceFactory.java | 55 +++
.../ColumnException.java} | 42 +-
.../web/api/exception/DatabaseException.java | 50 +++
.../TableException.java} | 42 +-
.../paimon/web/api/table/ColumnMetadata.java | 12 +
.../apache/paimon/web/api/table/TableChange.java | 480 +++++++++++++++++++++
.../paimon/web/api/catalog/PaimonServiceTest.java | 309 +++++++++++++
8 files changed, 1236 insertions(+), 54 deletions(-)
diff --git
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/catalog/PaimonService.java
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/catalog/PaimonService.java
new file mode 100644
index 0000000..034505d
--- /dev/null
+++
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/catalog/PaimonService.java
@@ -0,0 +1,300 @@
+/*
+ * 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.paimon.web.api.catalog;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.schema.SchemaChange;
+import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.web.api.exception.ColumnException;
+import org.apache.paimon.web.api.exception.DatabaseException;
+import org.apache.paimon.web.api.exception.TableException;
+import org.apache.paimon.web.api.table.ColumnMetadata;
+import org.apache.paimon.web.api.table.TableChange;
+import org.apache.paimon.web.api.table.TableMetadata;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Paimon service. */
+public class PaimonService {
+ private final Catalog catalog;
+
+ private final String name;
+
+ public PaimonService(Catalog catalog, String name) {
+ this.catalog = catalog;
+ this.name = name;
+ }
+
+ public Catalog catalog() {
+ return catalog;
+ }
+
+ public String catalogName() {
+ return name;
+ }
+
+ public List<String> listDatabases() {
+ return catalog.listDatabases();
+ }
+
+ public boolean databaseExists(String databaseName) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+ return catalog.databaseExists(databaseName);
+ }
+
+ public void createDatabase(String databaseName) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+
+ try {
+ catalog.createDatabase(databaseName, false);
+ } catch (Catalog.DatabaseAlreadyExistException e) {
+ throw new DatabaseException.DatabaseAlreadyExistsException(
+ String.format(
+ "The database '%s' already exists in the
catalog.", databaseName));
+ }
+ }
+
+ public void dropDatabase(String databaseName) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+ try {
+ catalog.dropDatabase(databaseName, false, true);
+ } catch (Catalog.DatabaseNotExistException e) {
+ throw new DatabaseException.DatabaseNotExistException(
+ String.format(
+ "The database '%s' does not exist in the
catalog.", databaseName));
+ } catch (Catalog.DatabaseNotEmptyException e) {
+ throw new DatabaseException.DatabaseNotEmptyException(
+ String.format("The database '%s' is not empty.",
databaseName));
+ }
+ }
+
+ public List<String> listTables(String databaseName) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+ try {
+ return catalog.listTables(databaseName);
+ } catch (Catalog.DatabaseNotExistException e) {
+ throw new DatabaseException.DatabaseNotExistException(
+ String.format(
+ "The database '%s' does not exist in the
catalog.", databaseName));
+ }
+ }
+
+ public boolean tableExists(String databaseName, String tableName) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+ Preconditions.checkNotNull(tableName, "Table name cannot be null.");
+
+ Identifier identifier = Identifier.create(databaseName, tableName);
+ return catalog.tableExists(identifier);
+ }
+
+ public void createTable(String databaseName, String tableName,
TableMetadata tableMetadata) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+ Preconditions.checkNotNull(tableName, "Table name cannot be null.");
+
+ Schema.Builder schemaBuilder =
+ Schema.newBuilder()
+ .partitionKeys(
+ tableMetadata.partitionKeys() == null
+ ? ImmutableList.of()
+ :
ImmutableList.copyOf(tableMetadata.partitionKeys()))
+ .primaryKey(
+ tableMetadata.primaryKeys() == null
+ ? ImmutableList.of()
+ :
ImmutableList.copyOf(tableMetadata.primaryKeys()))
+ .comment(tableMetadata.comment() == null ? "" :
tableMetadata.comment())
+ .options(tableMetadata.options());
+
+ for (ColumnMetadata column : tableMetadata.columns()) {
+ schemaBuilder.column(column.name(), column.type(),
column.description());
+ }
+
+ Schema schema = schemaBuilder.build();
+
+ Identifier identifier = Identifier.create(databaseName, tableName);
+
+ try {
+ catalog.createTable(identifier, schema, false);
+ } catch (Catalog.TableAlreadyExistException e) {
+ throw new TableException.TableAlreadyExistException(
+ String.format("The table '%s' already exists in the
database.", tableName));
+ } catch (Catalog.DatabaseNotExistException e) {
+ throw new DatabaseException.DatabaseNotExistException(
+ String.format(
+ "The database '%s' does not exist in the
catalog.", databaseName));
+ }
+ }
+
+ public Table getTable(String databaseName, String tableName) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+ Preconditions.checkNotNull(tableName, "Table name cannot be null.");
+
+ Identifier identifier = Identifier.create(databaseName, tableName);
+ try {
+ return catalog.getTable(identifier);
+ } catch (Catalog.TableNotExistException e) {
+ throw new TableException.TableNotExistException(
+ String.format("The table '%s' does not exist in the
database.", tableName));
+ }
+ }
+
+ public void dropTable(String databaseName, String tableName) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+ Preconditions.checkNotNull(tableName, "Table name cannot be null.");
+
+ Identifier identifier = Identifier.create(databaseName, tableName);
+ try {
+ catalog.dropTable(identifier, false);
+ } catch (Catalog.TableNotExistException e) {
+ throw new TableException.TableNotExistException(
+ String.format("The table '%s' does not exist in the
database.", tableName));
+ }
+ }
+
+ public void renameTable(String databaseName, String fromTable, String
toTable) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+ Preconditions.checkNotNull(fromTable, "From table name cannot be
null.");
+ Preconditions.checkNotNull(toTable, "To table name cannot be null.");
+
+ Identifier fromTableIdentifier = Identifier.create(databaseName,
fromTable);
+ Identifier toTableIdentifier = Identifier.create(databaseName,
toTable);
+ try {
+ catalog.renameTable(fromTableIdentifier, toTableIdentifier, false);
+ } catch (Catalog.TableNotExistException e) {
+ throw new TableException.TableNotExistException(
+ String.format("The table '%s' does not exist in the
database.", fromTable));
+ } catch (Catalog.TableAlreadyExistException e) {
+ throw new TableException.TableAlreadyExistException(
+ String.format("The table '%s' already exists in the
database.", toTable));
+ }
+ }
+
+ public void alterTable(String databaseName, String tableName,
List<TableChange> tableChanges) {
+ Preconditions.checkNotNull(databaseName, "Database name cannot be
null.");
+ Preconditions.checkNotNull(tableName, "Table name cannot be null.");
+
+ if (!tableExists(databaseName, tableName)) {
+ return;
+ }
+
+ Identifier identifier = Identifier.create(databaseName, tableName);
+
+ List<SchemaChange> changes = new ArrayList<>();
+ if (null != tableChanges) {
+ List<SchemaChange> schemaChanges =
+ tableChanges.stream()
+ .flatMap(tableChange ->
toSchemaChange(tableChange).stream())
+ .collect(Collectors.toList());
+ changes.addAll(schemaChanges);
+ }
+
+ try {
+ catalog.alterTable(identifier, changes, false);
+ } catch (Catalog.ColumnAlreadyExistException e) {
+ throw new
ColumnException.ColumnAlreadyExistException(e.getMessage());
+ } catch (Catalog.TableNotExistException e) {
+ throw new TableException.TableNotExistException(
+ String.format("The table '%s' does not exist in the
database.", tableName));
+ } catch (Catalog.ColumnNotExistException e) {
+ throw new ColumnException.ColumnNotExistException(e.getMessage());
+ }
+ }
+
+ private List<SchemaChange> toSchemaChange(TableChange change) {
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+ if (change instanceof TableChange.AddColumn) {
+ TableChange.AddColumn add = (TableChange.AddColumn) change;
+ String comment = add.getColumn().description();
+ SchemaChange.Move move = getMove(add.getPosition(),
add.getColumn().name());
+ schemaChanges.add(
+ SchemaChange.addColumn(
+ add.getColumn().name(), add.getColumn().type(),
comment, move));
+ return schemaChanges;
+ } else if (change instanceof TableChange.DropColumn) {
+ TableChange.DropColumn drop = (TableChange.DropColumn) change;
+ schemaChanges.add(SchemaChange.dropColumn(drop.getColumnName()));
+ return schemaChanges;
+ } else if (change instanceof TableChange.ModifyColumnName) {
+ TableChange.ModifyColumnName modify =
(TableChange.ModifyColumnName) change;
+ schemaChanges.add(
+ SchemaChange.renameColumn(
+ modify.getOldColumnName(),
modify.getNewColumnName()));
+ return schemaChanges;
+ } else if (change instanceof TableChange.ModifyColumnType) {
+ TableChange.ModifyColumnType modify =
(TableChange.ModifyColumnType) change;
+ DataType newColumnType = modify.getNewType();
+ DataType oldColumnType = modify.getOldColumn().type();
+ if (newColumnType.isNullable() != oldColumnType.isNullable()) {
+ schemaChanges.add(
+ SchemaChange.updateColumnNullability(
+ modify.getNewColumn().name(),
newColumnType.isNullable()));
+ }
+ schemaChanges.add(
+
SchemaChange.updateColumnType(modify.getOldColumn().name(), newColumnType));
+ return schemaChanges;
+ } else if (change instanceof TableChange.ModifyColumnPosition) {
+ TableChange.ModifyColumnPosition modify =
(TableChange.ModifyColumnPosition) change;
+ SchemaChange.Move move = getMove(modify.getNewPosition(),
modify.getNewColumn().name());
+ schemaChanges.add(SchemaChange.updateColumnPosition(move));
+ return schemaChanges;
+ } else if (change instanceof TableChange.ModifyColumnComment) {
+ TableChange.ModifyColumnComment modify =
(TableChange.ModifyColumnComment) change;
+ schemaChanges.add(
+ SchemaChange.updateColumnComment(
+ modify.getNewColumn().name(),
modify.getNewComment()));
+ return schemaChanges;
+ } else if (change instanceof TableChange.SetOption) {
+ TableChange.SetOption setOption = (TableChange.SetOption) change;
+ String key = setOption.getKey();
+ String value = setOption.getValue();
+
+ SchemaManager.checkAlterTablePath(key);
+
+ schemaChanges.add(SchemaChange.setOption(key, value));
+ return schemaChanges;
+ } else if (change instanceof TableChange.RemoveOption) {
+ TableChange.RemoveOption removeOption = (TableChange.RemoveOption)
change;
+
schemaChanges.add(SchemaChange.removeOption(removeOption.getKey()));
+ return schemaChanges;
+ } else {
+ throw new UnsupportedOperationException(
+ "Change is not supported: " + change.getClass());
+ }
+ }
+
+ private SchemaChange.Move getMove(TableChange.ColumnPosition
columnPosition, String fieldName) {
+ SchemaChange.Move move = null;
+ if (columnPosition instanceof TableChange.First) {
+ move = SchemaChange.Move.first(fieldName);
+ } else if (columnPosition instanceof TableChange.After) {
+ move =
+ SchemaChange.Move.after(
+ fieldName, ((TableChange.After)
columnPosition).column());
+ }
+ return move;
+ }
+}
diff --git
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/catalog/PaimonServiceFactory.java
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/catalog/PaimonServiceFactory.java
new file mode 100644
index 0000000..6edaaae
--- /dev/null
+++
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/catalog/PaimonServiceFactory.java
@@ -0,0 +1,55 @@
+/*
+ * 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.paimon.web.api.catalog;
+
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.CatalogFactory;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.web.api.common.CatalogProperties;
+import org.apache.paimon.web.api.common.MetastoreType;
+
+import org.apache.commons.lang3.StringUtils;
+
+/** Paimon service factory. */
+public class PaimonServiceFactory {
+
+ public static PaimonService createFileSystemCatalogService(String name,
String warehouse) {
+ Options options = new Options();
+ options.set(CatalogProperties.WAREHOUSE, warehouse + "/" + name);
+
+ CatalogContext context = CatalogContext.create(options);
+
+ return new PaimonService(CatalogFactory.createCatalog(context), name);
+ }
+
+ public static PaimonService createHiveCatalogService(
+ String name, String warehouse, String uri, String hiveConfDir) {
+ Options options = new Options();
+ options.set(CatalogProperties.WAREHOUSE, warehouse + "/" + name);
+
+ options.set(CatalogProperties.METASTORE,
MetastoreType.HIVE.toString());
+ options.set(CatalogProperties.URI, uri);
+ if (StringUtils.isNotBlank(hiveConfDir)) {
+ options.set(CatalogProperties.HIVE_CONF_DIR, hiveConfDir);
+ }
+ CatalogContext context = CatalogContext.create(options);
+
+ return new PaimonService(CatalogFactory.createCatalog(context), name);
+ }
+}
diff --git
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/exception/ColumnException.java
similarity index 53%
copy from
paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
copy to
paimon-web-api/src/main/java/org/apache/paimon/web/api/exception/ColumnException.java
index 163f50d..43c7b22 100644
---
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
+++
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/exception/ColumnException.java
@@ -16,40 +16,28 @@
* limitations under the License.
*/
-package org.apache.paimon.web.api.table;
+package org.apache.paimon.web.api.exception;
-import org.apache.paimon.types.DataType;
+/** Column exception. */
+public class ColumnException extends RuntimeException {
-import javax.annotation.Nullable;
-
-/** table metadata. */
-public class ColumnMetadata {
-
- private final String name;
-
- private final DataType type;
-
- private final @Nullable String description;
-
- public ColumnMetadata(String name, DataType type) {
- this(name, type, null);
+ public ColumnException(String message) {
+ super(message);
}
- public ColumnMetadata(String name, DataType type, String description) {
- this.name = name;
- this.type = type;
- this.description = description;
- }
+ /** The exception that the column has already existed. */
+ public static class ColumnAlreadyExistException extends ColumnException {
- public String name() {
- return this.name;
+ public ColumnAlreadyExistException(String message) {
+ super(message);
+ }
}
- public DataType type() {
- return this.type;
- }
+ /** The exception that the column does not exist. */
+ public static class ColumnNotExistException extends ColumnException {
- public String description() {
- return this.description;
+ public ColumnNotExistException(String message) {
+ super(message);
+ }
}
}
diff --git
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/exception/DatabaseException.java
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/exception/DatabaseException.java
new file mode 100644
index 0000000..3c3468e
--- /dev/null
+++
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/exception/DatabaseException.java
@@ -0,0 +1,50 @@
+/*
+ * 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.paimon.web.api.exception;
+
+/** Database exception. */
+public class DatabaseException extends RuntimeException {
+ public DatabaseException(String message) {
+ super(message);
+ }
+
+ /** The exception that the database already exists. */
+ public static class DatabaseAlreadyExistsException extends
DatabaseException {
+
+ public DatabaseAlreadyExistsException(String message) {
+ super(message);
+ }
+ }
+
+ /** The exception that the database is not empty. */
+ public static class DatabaseNotEmptyException extends DatabaseException {
+
+ public DatabaseNotEmptyException(String message) {
+ super(message);
+ }
+ }
+
+ /** The exception that the database does not exist. */
+ public static class DatabaseNotExistException extends DatabaseException {
+
+ public DatabaseNotExistException(String message) {
+ super(message);
+ }
+ }
+}
diff --git
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/exception/TableException.java
similarity index 53%
copy from
paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
copy to
paimon-web-api/src/main/java/org/apache/paimon/web/api/exception/TableException.java
index 163f50d..a7b7546 100644
---
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
+++
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/exception/TableException.java
@@ -16,40 +16,28 @@
* limitations under the License.
*/
-package org.apache.paimon.web.api.table;
+package org.apache.paimon.web.api.exception;
-import org.apache.paimon.types.DataType;
+/** Table exception. */
+public class TableException extends RuntimeException {
-import javax.annotation.Nullable;
-
-/** table metadata. */
-public class ColumnMetadata {
-
- private final String name;
-
- private final DataType type;
-
- private final @Nullable String description;
-
- public ColumnMetadata(String name, DataType type) {
- this(name, type, null);
+ public TableException(String message) {
+ super(message);
}
- public ColumnMetadata(String name, DataType type, String description) {
- this.name = name;
- this.type = type;
- this.description = description;
- }
+ /** The exception that the table already exists. */
+ public static class TableAlreadyExistException extends TableException {
- public String name() {
- return this.name;
+ public TableAlreadyExistException(String message) {
+ super(message);
+ }
}
- public DataType type() {
- return this.type;
- }
+ /** The exception that the table does not exist. */
+ public static class TableNotExistException extends TableException {
- public String description() {
- return this.description;
+ public TableNotExistException(String message) {
+ super(message);
+ }
}
}
diff --git
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
index 163f50d..4d09c6e 100644
---
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
+++
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/ColumnMetadata.java
@@ -31,6 +31,10 @@ public class ColumnMetadata {
private final @Nullable String description;
+ public ColumnMetadata(String name) {
+ this(name, null, null);
+ }
+
public ColumnMetadata(String name, DataType type) {
this(name, type, null);
}
@@ -52,4 +56,12 @@ public class ColumnMetadata {
public String description() {
return this.description;
}
+
+ public ColumnMetadata copy(DataType newDataType) {
+ return new ColumnMetadata(this.name, newDataType, this.description);
+ }
+
+ public ColumnMetadata setComment(String comment) {
+ return comment == null ? this : new ColumnMetadata(this.name,
this.type, comment);
+ }
}
diff --git
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableChange.java
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableChange.java
new file mode 100644
index 0000000..ec0f392
--- /dev/null
+++
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableChange.java
@@ -0,0 +1,480 @@
+/*
+ * 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.paimon.web.api.table;
+
+import org.apache.paimon.types.DataType;
+
+import javax.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * This change represents the modification of the table including adding,
modifying and dropping
+ * column etc.
+ */
+public interface TableChange {
+ static AddColumn add(ColumnMetadata column) {
+ return new AddColumn(column, null);
+ }
+
+ static AddColumn add(ColumnMetadata column, @Nullable ColumnPosition
position) {
+ return new AddColumn(column, position);
+ }
+
+ static ModifyColumn modify(
+ ColumnMetadata oldColumn,
+ ColumnMetadata newColumn,
+ @Nullable ColumnPosition columnPosition) {
+ return new ModifyColumn(oldColumn, newColumn, columnPosition);
+ }
+
+ static ModifyColumnType modifyColumnType(ColumnMetadata oldColumn,
DataType newType) {
+ return new ModifyColumnType(oldColumn, newType);
+ }
+
+ static ModifyColumnName modifyColumnName(ColumnMetadata oldColumn, String
newName) {
+ return new ModifyColumnName(oldColumn, newName);
+ }
+
+ static ModifyColumnComment modifyColumnComment(ColumnMetadata oldColumn,
String newComment) {
+ return new ModifyColumnComment(oldColumn, newComment);
+ }
+
+ static ModifyColumnPosition modifyColumnPosition(
+ ColumnMetadata oldColumn, ColumnPosition columnPosition) {
+ return new ModifyColumnPosition(oldColumn, columnPosition);
+ }
+
+ static DropColumn dropColumn(String columnName) {
+ return new DropColumn(columnName);
+ }
+
+ static SetOption set(String key, String value) {
+ return new SetOption(key, value);
+ }
+
+ static RemoveOption remove(String key) {
+ return new RemoveOption(key);
+ }
+
+ /** A table change to add a column. */
+ class AddColumn implements TableChange {
+
+ private final ColumnMetadata column;
+ private final ColumnPosition position;
+
+ private AddColumn(ColumnMetadata column, ColumnPosition position) {
+ this.column = column;
+ this.position = position;
+ }
+
+ public ColumnMetadata getColumn() {
+ return column;
+ }
+
+ @Nullable
+ public ColumnPosition getPosition() {
+ return position;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof AddColumn)) {
+ return false;
+ }
+ AddColumn addColumn = (AddColumn) o;
+ return Objects.equals(column, addColumn.column)
+ && Objects.equals(position, addColumn.position);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(column, position);
+ }
+
+ @Override
+ public String toString() {
+ return "AddColumn{" + "column=" + column + ", position=" +
position + '}';
+ }
+ }
+
+ /** A base schema change to modify a column. */
+ class ModifyColumn implements TableChange {
+
+ protected final ColumnMetadata oldColumn;
+ protected final ColumnMetadata newColumn;
+
+ protected final @Nullable ColumnPosition newPosition;
+
+ public ModifyColumn(
+ ColumnMetadata oldColumn,
+ ColumnMetadata newColumn,
+ @Nullable ColumnPosition newPosition) {
+ this.oldColumn = oldColumn;
+ this.newColumn = newColumn;
+ this.newPosition = newPosition;
+ }
+
+ public ColumnMetadata getOldColumn() {
+ return oldColumn;
+ }
+
+ public ColumnMetadata getNewColumn() {
+ return newColumn;
+ }
+
+ public @Nullable ColumnPosition getNewPosition() {
+ return newPosition;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ModifyColumn)) {
+ return false;
+ }
+ ModifyColumn that = (ModifyColumn) o;
+ return Objects.equals(oldColumn, that.oldColumn)
+ && Objects.equals(newColumn, that.newColumn)
+ && Objects.equals(newPosition, that.newPosition);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(oldColumn, newColumn, newPosition);
+ }
+
+ @Override
+ public String toString() {
+ return "ModifyColumn{"
+ + "oldColumn="
+ + oldColumn
+ + ", newColumn="
+ + newColumn
+ + ", newPosition="
+ + newPosition
+ + '}';
+ }
+ }
+
+ /** A table change that modify the column data type. */
+ class ModifyColumnType extends ModifyColumn {
+
+ private ModifyColumnType(ColumnMetadata oldColumn, DataType newType) {
+ super(oldColumn, oldColumn.copy(newType), null);
+ }
+
+ /** Get the column type for the new column. */
+ public DataType getNewType() {
+ return newColumn.type();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof ModifyColumnType) && super.equals(o);
+ }
+
+ @Override
+ public String toString() {
+ return "ModifyColumnType{" + "Column=" + oldColumn + ", newType="
+ getNewType() + '}';
+ }
+ }
+
+ /** A table change to modify the column name. */
+ class ModifyColumnName extends ModifyColumn {
+
+ private ModifyColumnName(ColumnMetadata oldColumn, String newName) {
+ super(oldColumn, createNewColumn(oldColumn, newName), null);
+ }
+
+ private static ColumnMetadata createNewColumn(ColumnMetadata
oldColumn, String newName) {
+ return new ColumnMetadata(newName, oldColumn.type(),
oldColumn.description());
+ }
+
+ public String getOldColumnName() {
+ return oldColumn.name();
+ }
+
+ /** Returns the new column name after renaming the column name. */
+ public String getNewColumnName() {
+ return newColumn.name();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof ModifyColumnName) && super.equals(o);
+ }
+
+ @Override
+ public String toString() {
+ return "ModifyColumnName{"
+ + "Column="
+ + oldColumn
+ + ", newName="
+ + getNewColumnName()
+ + '}';
+ }
+ }
+
+ /** A table change to modify the column comment. */
+ class ModifyColumnComment extends ModifyColumn {
+
+ private final String newComment;
+
+ private ModifyColumnComment(ColumnMetadata oldColumn, String
newComment) {
+ super(oldColumn, oldColumn.setComment(newComment), null);
+ this.newComment = newComment;
+ }
+
+ /** Get the new comment for the column. */
+ public String getNewComment() {
+ return newComment;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof ModifyColumnComment) && super.equals(o);
+ }
+
+ @Override
+ public String toString() {
+ return "ModifyColumnComment{"
+ + "Column="
+ + oldColumn
+ + ", newComment="
+ + newComment
+ + '}';
+ }
+ }
+
+ /** A table change to modify the column position. */
+ class ModifyColumnPosition extends ModifyColumn {
+
+ public ModifyColumnPosition(ColumnMetadata oldColumn, ColumnPosition
newPosition) {
+ super(oldColumn, oldColumn, newPosition);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof ModifyColumnPosition) && super.equals(o);
+ }
+
+ @Override
+ public String toString() {
+ return "ModifyColumnPosition{"
+ + "Column="
+ + oldColumn
+ + ", newPosition="
+ + newPosition
+ + '}';
+ }
+ }
+
+ /** A table change to drop the column. */
+ class DropColumn implements TableChange {
+
+ private final String columnName;
+
+ private DropColumn(String columnName) {
+ this.columnName = columnName;
+ }
+
+ /** Returns the column name. */
+ public String getColumnName() {
+ return columnName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DropColumn)) {
+ return false;
+ }
+ DropColumn that = (DropColumn) o;
+ return Objects.equals(columnName, that.columnName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(columnName);
+ }
+
+ @Override
+ public String toString() {
+ return "DropColumn{" + "columnName=" + columnName + '}';
+ }
+ }
+
+ /** A table change to set the table option. */
+ class SetOption implements TableChange {
+
+ private final String key;
+ private final String value;
+
+ private SetOption(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ /** Returns the Option key to set. */
+ public String getKey() {
+ return key;
+ }
+
+ /** Returns the Option value to set. */
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SetOption)) {
+ return false;
+ }
+ SetOption setOption = (SetOption) o;
+ return Objects.equals(key, setOption.key) && Objects.equals(value,
setOption.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, value);
+ }
+
+ @Override
+ public String toString() {
+ return "SetOption{" + "key=" + key + ", value=" + value + '}';
+ }
+ }
+
+ /** A table change to remove the table option. */
+ class RemoveOption implements TableChange {
+
+ private final String key;
+
+ public RemoveOption(String key) {
+ this.key = key;
+ }
+
+ /** Returns the Option key to reset. */
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof RemoveOption)) {
+ return false;
+ }
+ RemoveOption that = (RemoveOption) o;
+ return Objects.equals(key, that.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key);
+ }
+
+ @Override
+ public String toString() {
+ return "RemoveOption{" + "key=" + key + '}';
+ }
+ }
+
+ /** The position of the modified or added column. */
+ interface ColumnPosition {
+
+ /** Get the position to place the column at the first. */
+ static ColumnPosition first() {
+ return First.INSTANCE;
+ }
+
+ /** Get the position to place the column after the specified column. */
+ static ColumnPosition after(String column) {
+ return new After(column);
+ }
+ }
+
+ /** Column position FIRST means the specified column should be the first
column. */
+ final class First implements ColumnPosition {
+ private static final First INSTANCE = new First();
+
+ private First() {}
+
+ @Override
+ public String toString() {
+ return "FIRST";
+ }
+ }
+
+ /** Column position AFTER means the specified column should be put after
the given `column`. */
+ final class After implements ColumnPosition {
+ private final String column;
+
+ private After(String column) {
+ this.column = column;
+ }
+
+ public String column() {
+ return column;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof After)) {
+ return false;
+ }
+ After after = (After) o;
+ return Objects.equals(column, after.column);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(column);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("AFTER %s", escapeIdentifier(column));
+ }
+
+ private static String escapeBackticks(String s) {
+ return s.replace("`", "``");
+ }
+
+ private static String escapeIdentifier(String s) {
+ return "`" + escapeBackticks(s) + "`";
+ }
+ }
+}
diff --git
a/paimon-web-api/src/test/java/org/apache/paimon/web/api/catalog/PaimonServiceTest.java
b/paimon-web-api/src/test/java/org/apache/paimon/web/api/catalog/PaimonServiceTest.java
new file mode 100644
index 0000000..37af5de
--- /dev/null
+++
b/paimon-web-api/src/test/java/org/apache/paimon/web/api/catalog/PaimonServiceTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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.paimon.web.api.catalog;
+
+import org.apache.paimon.table.Table;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.web.api.exception.DatabaseException;
+import org.apache.paimon.web.api.exception.TableException;
+import org.apache.paimon.web.api.table.ColumnMetadata;
+import org.apache.paimon.web.api.table.TableChange;
+import org.apache.paimon.web.api.table.TableMetadata;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/** The test class of catalog creator in {@link PaimonService}. */
+public class PaimonServiceTest {
+
+ private String warehouse;
+
+ private PaimonService service;
+
+ @TempDir private Path tempFile;
+
+ private final String db = "test_default_db";
+
+ @BeforeEach
+ public void before() {
+ warehouse = tempFile.toUri().toString();
+ service =
PaimonServiceFactory.createFileSystemCatalogService("paimon", warehouse);
+ service.createDatabase(db);
+ }
+
+ @AfterEach
+ public void after() {
+ if (service.databaseExists(db)) {
+ service.dropDatabase(db);
+ }
+ }
+
+ @Test
+ public void testDatabaseExists() {
+ boolean exists = service.databaseExists(db);
+ assertThat(exists).isTrue();
+ }
+
+ @Test
+ public void testListDatabase() {
+ service.createDatabase("db1");
+ List<String> databases = service.listDatabases();
+ assertThat(databases).contains("test_default_db", "db1");
+ dropDatabase("db1");
+ }
+
+ @Test
+ public void testCreateDatabase() {
+ service.createDatabase("test_db");
+ boolean exists = service.databaseExists("test_db");
+ assertThat(exists).isTrue();
+
+
assertThatExceptionOfType(DatabaseException.DatabaseAlreadyExistsException.class)
+ .isThrownBy(() -> service.createDatabase("test_db"))
+ .withMessage("The database 'test_db' already exists in the
catalog.");
+
+ dropDatabase("test_db");
+ }
+
+ @Test
+ public void testDropDatabase() {
+ service.createDatabase("db2");
+ boolean exists = service.databaseExists("db2");
+ assertThat(exists).isTrue();
+ service.dropDatabase("db2");
+ boolean notExist = service.databaseExists("db2");
+ assertThat(notExist).isFalse();
+ }
+
+ @Test
+ public void testTableExists() {
+ createTable(db, "tb1");
+ boolean exists = service.tableExists(db, "tb1");
+ boolean notExists = service.tableExists(db, "tb_not");
+ assertThat(exists).isTrue();
+ assertThat(notExists).isFalse();
+ }
+
+ @Test
+ public void testListTables() {
+ createTable(db, "tb1");
+ createTable(db, "tb2");
+ List<String> tables = service.listTables(db);
+ assertThat(tables).contains("tb1", "tb2");
+ }
+
+ @Test
+ public void testCreateTable() {
+ createTable(db, "tb1");
+ boolean exists = service.tableExists(db, "tb1");
+ assertThat(exists).isTrue();
+
+
assertThatExceptionOfType(TableException.TableAlreadyExistException.class)
+ .isThrownBy(() -> createTable(db, "tb1"))
+ .withMessage("The table 'tb1' already exists in the
database.");
+
+
assertThatExceptionOfType(DatabaseException.DatabaseNotExistException.class)
+ .isThrownBy(() -> createTable("db1", "tb1"))
+ .withMessage("The database 'db1' does not exist in the
catalog.");
+ }
+
+ @Test
+ public void testGetTable() {
+ createTable(db, "tb1");
+ Table tb1 = service.getTable(db, "tb1");
+ assertThat(tb1).isInstanceOf(Table.class);
+ assertThat(tb1.name()).isEqualTo("tb1");
+
+ assertThatExceptionOfType(TableException.TableNotExistException.class)
+ .isThrownBy(() -> service.getTable(db, "tb2"))
+ .withMessage("The table 'tb2' does not exist in the
database.");
+ }
+
+ @Test
+ public void testRenameTable() {
+ createTable(db, "tb1");
+ createTable(db, "tb3");
+ createTable(db, "tb4");
+ createTable(db, "tb6");
+ assertThat(service.tableExists(db, "tb1")).isTrue();
+ service.renameTable(db, "tb1", "tb2");
+ assertThat(service.tableExists(db, "tb1")).isFalse();
+ assertThat(service.tableExists(db, "tb2")).isTrue();
+
+
assertThatExceptionOfType(TableException.TableAlreadyExistException.class)
+ .isThrownBy(() -> service.renameTable(db, "tb3", "tb4"))
+ .withMessage("The table 'tb4' already exists in the
database.");
+
+ assertThatExceptionOfType(TableException.TableNotExistException.class)
+ .isThrownBy(() -> service.renameTable(db, "tb5", "tb7"))
+ .withMessage("The table 'tb5' does not exist in the
database.");
+ }
+
+ @Test
+ public void testDropTable() {
+ createTable(db, "tb1");
+ assertThat(service.tableExists(db, "tb1")).isTrue();
+ service.dropTable(db, "tb1");
+ assertThat(service.tableExists(db, "tb1")).isFalse();
+
+ assertThatExceptionOfType(TableException.TableNotExistException.class)
+ .isThrownBy(() -> service.dropTable(db, "tb5"))
+ .withMessage("The table 'tb5' does not exist in the
database.");
+ }
+
+ @Test
+ public void testAddColumn() {
+ createTable(db, "tb1");
+ ColumnMetadata age = new ColumnMetadata("age", DataTypes.INT());
+ TableChange.ColumnPosition columnPosition =
TableChange.ColumnPosition.after("id");
+ TableChange.AddColumn add = TableChange.add(age, columnPosition);
+ List<TableChange> tableChanges = new ArrayList<>();
+ tableChanges.add(add);
+ service.alterTable(db, "tb1", tableChanges);
+ Table tb1 = service.getTable(db, "tb1");
+ List<String> fieldNames = tb1.rowType().getFieldNames();
+ assertThat(fieldNames).contains("id", "age", "name");
+ }
+
+ @Test
+ public void testModifyColumnType() {
+ createTable(db, "tb1");
+ ColumnMetadata id = new ColumnMetadata("id", DataTypes.INT());
+ TableChange.ModifyColumnType modifyColumnType =
+ TableChange.modifyColumnType(id, DataTypes.BIGINT());
+ List<TableChange> tableChanges = new ArrayList<>();
+ tableChanges.add(modifyColumnType);
+ service.alterTable(db, "tb1", tableChanges);
+ Table tb1 = service.getTable(db, "tb1");
+ List<DataType> fieldTypes = tb1.rowType().getFieldTypes();
+ assertThat(fieldTypes).contains(DataTypes.BIGINT(),
DataTypes.STRING());
+ }
+
+ @Test
+ public void testModifyColumnName() {
+ createTable(db, "tb1");
+ ColumnMetadata id = new ColumnMetadata("id", DataTypes.INT());
+ TableChange.ModifyColumnName modifyColumnName =
TableChange.modifyColumnName(id, "age");
+ List<TableChange> tableChanges = new ArrayList<>();
+ tableChanges.add(modifyColumnName);
+ service.alterTable(db, "tb1", tableChanges);
+ Table tb1 = service.getTable(db, "tb1");
+ List<String> fieldNames = tb1.rowType().getFieldNames();
+ assertThat(fieldNames).contains("age", "name");
+ }
+
+ @Test
+ public void testModifyColumnComment() {
+ createTable(db, "tb1");
+ ColumnMetadata id = new ColumnMetadata("id", DataTypes.INT());
+ TableChange.ModifyColumnComment modifyColumnComment =
+ TableChange.modifyColumnComment(id, "id");
+ List<TableChange> tableChanges = new ArrayList<>();
+ tableChanges.add(modifyColumnComment);
+ service.alterTable(db, "tb1", tableChanges);
+ Table tb1 = service.getTable(db, "tb1");
+ String description = tb1.rowType().getFields().get(0).description();
+ assertThat(description).isEqualTo("id");
+ }
+
+ @Test
+ public void testModifyColumnPosition() {
+ createTable(db, "tb1");
+ ColumnMetadata name = new ColumnMetadata("name", DataTypes.STRING());
+ TableChange.ColumnPosition columnPosition =
TableChange.ColumnPosition.first();
+ TableChange.ModifyColumnPosition modifyColumnPosition =
+ TableChange.modifyColumnPosition(name, columnPosition);
+ List<TableChange> tableChanges = new ArrayList<>();
+ tableChanges.add(modifyColumnPosition);
+ service.alterTable(db, "tb1", tableChanges);
+ Table tb1 = service.getTable(db, "tb1");
+ String columnName = tb1.rowType().getFields().get(0).name();
+ assertThat(columnName).isEqualTo("name");
+ }
+
+ @Test
+ public void testDropColumn() {
+ createTable(db, "tb1");
+ TableChange.DropColumn dropColumn = TableChange.dropColumn("id");
+ List<TableChange> tableChanges = new ArrayList<>();
+ tableChanges.add(dropColumn);
+ service.alterTable(db, "tb1", tableChanges);
+ Table tb1 = service.getTable(db, "tb1");
+ List<String> fieldNames = tb1.rowType().getFieldNames();
+ assertThat(fieldNames).contains("name");
+ assertThat(fieldNames).doesNotContain("id");
+ }
+
+ @Test
+ public void testSetOption() {
+ createTable(db, "tb1");
+ TableChange.SetOption setOption = TableChange.set("bucket", "2");
+ List<TableChange> tableChanges = new ArrayList<>();
+ tableChanges.add(setOption);
+ service.alterTable(db, "tb1", tableChanges);
+ Table tb1 = service.getTable(db, "tb1");
+ String bucket = tb1.options().get("bucket");
+ assertThat(bucket).isEqualTo("2");
+ }
+
+ @Test
+ public void testRemoveOption() {
+ createTable(db, "tb1");
+ TableChange.SetOption setOption = TableChange.set("bucket", "2");
+ List<TableChange> tableChanges = new ArrayList<>();
+ tableChanges.add(setOption);
+ service.alterTable(db, "tb1", tableChanges);
+ Table tb1 = service.getTable(db, "tb1");
+ String bucket = tb1.options().get("bucket");
+ assertThat(bucket).isEqualTo("2");
+
+ TableChange.RemoveOption resetOption = TableChange.remove("bucket");
+ List<TableChange> changes = new ArrayList<>();
+ changes.add(resetOption);
+ service.alterTable(db, "tb1", changes);
+ Table tb = service.getTable(db, "tb1");
+ assertThat(tb.options().get("bucket")).isEqualTo(null);
+ }
+
+ private void createTable(String databaseName, String tableName) {
+ List<ColumnMetadata> columns = new ArrayList<>();
+ ColumnMetadata id = new ColumnMetadata("id", DataTypes.INT());
+ ColumnMetadata name = new ColumnMetadata("name", DataTypes.STRING());
+ columns.add(id);
+ columns.add(name);
+ TableMetadata tableMetadata =
TableMetadata.builder().columns(columns).build();
+ service.createTable(databaseName, tableName, tableMetadata);
+ }
+
+ private void dropDatabase(String databaseName) {
+ if (service.databaseExists(databaseName)) {
+ service.dropDatabase(databaseName);
+ }
+ }
+}