From 4c384d5b82bd275c203b5c1d42113cbd00d01f20 Mon Sep 17 00:00:00 2001 From: s7monk <34889415+s7monk@users.noreply.github.com> Date: Sat, 14 Oct 2023 15:11:21 +0800 Subject: [PATCH] [Improvement] Refactor server module (#48) --- paimon-web-server/pom.xml | 4 + .../server/controller/CatalogController.java | 74 +-- .../server/controller/DatabaseController.java | 69 +-- .../server/controller/TableController.java | 484 ++++++++++++++---- .../web/server/data/enums/CatalogMode.java | 35 ++ .../server/data/model/AlterTableRequest.java | 34 ++ .../web/server/data/model/DatabaseInfo.java | 2 + .../web/server/data/model/TableColumn.java | 12 +- .../web/server/data/result/enums/Status.java | 10 +- .../paimon/web/server/util/CatalogUtils.java | 44 -- .../web/server/util/DataTypeConvertUtils.java | 181 +++++-- .../web/server/util/PaimonDataType.java | 40 ++ .../web/server/util/PaimonServiceUtils.java | 66 +++ ...ication-dev.yml => application-dev-h2.yml} | 0 .../main/resources/application-dev-mysql.yml | 21 + .../src/main/resources/application-prod.yml | 6 +- .../src/main/resources/application.yml | 2 +- .../main/resources/i18n/messages.properties | 15 +- .../controller/CatalogControllerTest.java | 99 ++++ .../server/controller/ControllerTestBase.java | 199 +++++++ .../controller/DatabaseControllerTest.java | 91 ++++ .../web/server/controller/PermissionTest.java | 68 +-- .../controller/SysMenuControllerTest.java | 68 +-- .../controller/SysRoleControllerTest.java | 72 +-- .../controller/TableControllerTest.java | 322 ++++++++++++ pom.xml | 1 + scripts/sql/paimon-mysql.sql | 174 +++++++ 27 files changed, 1724 insertions(+), 469 deletions(-) create mode 100644 paimon-web-server/src/main/java/org/apache/paimon/web/server/data/enums/CatalogMode.java create mode 100644 paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/AlterTableRequest.java delete mode 100644 paimon-web-server/src/main/java/org/apache/paimon/web/server/util/CatalogUtils.java create mode 100644 paimon-web-server/src/main/java/org/apache/paimon/web/server/util/PaimonDataType.java create mode 100644 paimon-web-server/src/main/java/org/apache/paimon/web/server/util/PaimonServiceUtils.java rename paimon-web-server/src/main/resources/{application-dev.yml => application-dev-h2.yml} (100%) create mode 100644 paimon-web-server/src/main/resources/application-dev-mysql.yml create mode 100644 paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/CatalogControllerTest.java create mode 100644 paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/ControllerTestBase.java create mode 100644 paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/DatabaseControllerTest.java create mode 100644 paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/TableControllerTest.java create mode 100644 scripts/sql/paimon-mysql.sql diff --git a/paimon-web-server/pom.xml b/paimon-web-server/pom.xml index 3c4fa57d1..02ec9b71e 100644 --- a/paimon-web-server/pom.xml +++ b/paimon-web-server/pom.xml @@ -76,6 +76,10 @@ under the License. org.junit.vintage junit-vintage-engine + + junit + junit + diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/CatalogController.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/CatalogController.java index cdc0bf18b..64e166f31 100644 --- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/CatalogController.java +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/CatalogController.java @@ -18,13 +18,16 @@ package org.apache.paimon.web.server.controller; -import org.apache.paimon.web.api.catalog.CatalogCreator; +import org.apache.paimon.web.api.catalog.PaimonServiceFactory; +import org.apache.paimon.web.server.data.enums.CatalogMode; import org.apache.paimon.web.server.data.model.CatalogInfo; import org.apache.paimon.web.server.data.result.R; import org.apache.paimon.web.server.data.result.enums.Status; import org.apache.paimon.web.server.service.CatalogService; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -45,46 +48,39 @@ public class CatalogController { @Autowired private CatalogService catalogService; /** - * Create a filesystem catalog. + * Create a catalog. * - * @param catalogInfo The catalogInfo for the filesystem catalog. + * @param catalogInfo The catalogInfo for the catalog. * @return The created catalog. */ - @PostMapping("/createFilesystemCatalog") - public R createFilesystemCatalog(@RequestBody CatalogInfo catalogInfo) { + @PostMapping("/create") + public R createCatalog(@RequestBody CatalogInfo catalogInfo) { if (!catalogService.checkCatalogNameUnique(catalogInfo)) { return R.failed(Status.CATALOG_NAME_IS_EXIST, catalogInfo.getCatalogName()); } try { - CatalogCreator.createFilesystemCatalog(catalogInfo.getWarehouse()); + if (catalogInfo.getCatalogType().equalsIgnoreCase(CatalogMode.FILESYSTEM.getMode())) { + PaimonServiceFactory.createFileSystemCatalogService( + catalogInfo.getCatalogName(), catalogInfo.getWarehouse()); + } else if (catalogInfo.getCatalogType().equalsIgnoreCase(CatalogMode.HIVE.getMode())) { + if (StringUtils.isNotBlank(catalogInfo.getHiveConfDir())) { + PaimonServiceFactory.createHiveCatalogService( + catalogInfo.getCatalogName(), + catalogInfo.getWarehouse(), + catalogInfo.getHiveUri(), + catalogInfo.getHiveConfDir()); + } else { + PaimonServiceFactory.createHiveCatalogService( + catalogInfo.getCatalogName(), + catalogInfo.getWarehouse(), + catalogInfo.getHiveUri(), + null); + } + } return catalogService.save(catalogInfo) ? R.succeed() : R.failed(); } catch (Exception e) { - e.printStackTrace(); - return R.failed(Status.CATALOG_CREATE_ERROR); - } - } - - /** - * Create a hive catalog. - * - * @param catalogInfo The information for the hive catalog. - * @return The created catalog. - */ - @PostMapping("/createHiveCatalog") - public R createHiveCatalog(@RequestBody CatalogInfo catalogInfo) { - if (!catalogService.checkCatalogNameUnique(catalogInfo)) { - return R.failed(Status.CATALOG_NAME_IS_EXIST, catalogInfo.getCatalogName()); - } - - try { - CatalogCreator.createHiveCatalog( - catalogInfo.getWarehouse(), - catalogInfo.getHiveUri(), - catalogInfo.getHiveConfDir()); - return catalogService.save(catalogInfo) ? R.succeed() : R.failed(); - } catch (Exception e) { - e.printStackTrace(); + log.error("Exception with creating catalog.", e); return R.failed(Status.CATALOG_CREATE_ERROR); } } @@ -101,13 +97,17 @@ public R> getCatalog() { } /** - * Removes a catalog by its ID. + * Removes a catalog with given catalog name. * - * @param catalogId The ID of the catalog to be removed. - * @return A response indicating the success or failure of the removal operation. + * @param catalogName The catalog name. + * @return A response indicating the success or failure of the operation. */ - @DeleteMapping("/{catalogId}") - public R remove(@PathVariable Integer catalogId) { - return catalogService.removeById(catalogId) ? R.succeed() : R.failed(); + @DeleteMapping("/remove/{catalogName}") + public R removeCatalog(@PathVariable String catalogName) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("catalog_name", catalogName); + return catalogService.remove(queryWrapper) + ? R.succeed() + : R.failed(Status.CATALOG_REMOVE_ERROR); } } diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/DatabaseController.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/DatabaseController.java index 83d0584b1..9ab10db75 100644 --- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/DatabaseController.java +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/DatabaseController.java @@ -18,20 +18,21 @@ package org.apache.paimon.web.server.controller; -import org.apache.paimon.catalog.Catalog; -import org.apache.paimon.web.api.database.DatabaseManager; +import org.apache.paimon.web.api.catalog.PaimonService; import org.apache.paimon.web.server.data.model.CatalogInfo; import org.apache.paimon.web.server.data.model.DatabaseInfo; import org.apache.paimon.web.server.data.result.R; import org.apache.paimon.web.server.data.result.enums.Status; import org.apache.paimon.web.server.service.CatalogService; -import org.apache.paimon.web.server.util.CatalogUtils; +import org.apache.paimon.web.server.util.PaimonServiceUtils; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -54,18 +55,18 @@ public class DatabaseController { * @param databaseInfo The DatabaseInfo object containing the details of the new database. * @return R indicating the result of the operation. */ - @PostMapping("/createDatabase") + @PostMapping("/create") public R createDatabase(@RequestBody DatabaseInfo databaseInfo) { try { - CatalogInfo catalogInfo = getCatalogInfo(databaseInfo); - Catalog catalog = CatalogUtils.getCatalog(catalogInfo); - if (DatabaseManager.databaseExists(catalog, databaseInfo.getDatabaseName())) { + CatalogInfo catalogInfo = getCatalogInfo(databaseInfo.getCatalogName()); + PaimonService service = PaimonServiceUtils.getPaimonService(catalogInfo); + if (service.databaseExists(databaseInfo.getDatabaseName())) { return R.failed(Status.DATABASE_NAME_IS_EXIST, databaseInfo.getDatabaseName()); } - DatabaseManager.createDatabase(catalog, databaseInfo.getDatabaseName()); + service.createDatabase(databaseInfo.getDatabaseName()); return R.succeed(); } catch (Exception e) { - e.printStackTrace(); + log.error("Exception with creating database.", e); return R.failed(Status.DATABASE_CREATE_ERROR); } } @@ -79,17 +80,18 @@ public R createDatabase(@RequestBody DatabaseInfo databaseInfo) { public R> getAllDatabases() { List databaseInfoList = new ArrayList<>(); List catalogInfoList = catalogService.list(); - if (catalogInfoList.size() > 0) { + if (!CollectionUtils.isEmpty(catalogInfoList)) { catalogInfoList.forEach( item -> { - Catalog catalog = CatalogUtils.getCatalog(item); - List list = DatabaseManager.listDatabase(catalog); + PaimonService service = PaimonServiceUtils.getPaimonService(item); + List list = service.listDatabases(); list.forEach( databaseName -> { DatabaseInfo info = DatabaseInfo.builder() .databaseName(databaseName) .catalogId(item.getId()) + .catalogName(item.getCatalogName()) .description("") .build(); databaseInfoList.add(info); @@ -99,34 +101,37 @@ public R> getAllDatabases() { return R.succeed(databaseInfoList); } - /** - * Retrieves the associated CatalogInfo object based on the given DatabaseInfo object. - * - * @param databaseInfo The DatabaseInfo object for which to retrieve the associated CatalogInfo. - * @return The associated CatalogInfo object, or null if it doesn't exist. - */ - private CatalogInfo getCatalogInfo(DatabaseInfo databaseInfo) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(CatalogInfo::getId, databaseInfo.getCatalogId()); - return catalogService.getOne(queryWrapper); - } - /** * Removes a database by its name. * - * @param databaseInfo The information of the database to be removed. + * @param databaseName The database to be removed. + * @param catalogName The catalog to which the database to be removed belongs. * @return A response indicating the success or failure of the removal operation. * @throws RuntimeException if the database is not found or it is not empty. */ - @DeleteMapping("/delete") - public R remove(@RequestBody DatabaseInfo databaseInfo) { + @DeleteMapping("/drop/{databaseName}/{catalogName}") + public R dropDatabase( + @PathVariable String databaseName, @PathVariable String catalogName) { try { - CatalogInfo catalogInfo = getCatalogInfo(databaseInfo); - Catalog catalog = CatalogUtils.getCatalog(catalogInfo); - DatabaseManager.dropDatabase(catalog, databaseInfo.getDatabaseName()); + CatalogInfo catalogInfo = getCatalogInfo(catalogName); + PaimonService service = PaimonServiceUtils.getPaimonService(catalogInfo); + service.dropDatabase(databaseName); return R.succeed(); - } catch (Catalog.DatabaseNotEmptyException | Catalog.DatabaseNotExistException e) { - throw new RuntimeException(e); + } catch (Exception e) { + log.error("Exception with dropping database.", e); + return R.failed(Status.DATABASE_DROP_ERROR); } } + + /** + * Retrieves the associated CatalogInfo object based on the given catalog name. + * + * @param catalogName The catalog name. + * @return The associated CatalogInfo object, or null if it doesn't exist. + */ + private CatalogInfo getCatalogInfo(String catalogName) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(CatalogInfo::getCatalogName, catalogName); + return catalogService.getOne(queryWrapper); + } } diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/TableController.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/TableController.java index 33eaa1c1e..07cc45f6a 100644 --- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/TableController.java +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/TableController.java @@ -18,34 +18,41 @@ package org.apache.paimon.web.server.controller; -import org.apache.paimon.catalog.Catalog; import org.apache.paimon.table.Table; import org.apache.paimon.types.DataField; -import org.apache.paimon.web.api.database.DatabaseManager; +import org.apache.paimon.web.api.catalog.PaimonService; import org.apache.paimon.web.api.table.ColumnMetadata; -import org.apache.paimon.web.api.table.TableManager; +import org.apache.paimon.web.api.table.TableChange; import org.apache.paimon.web.api.table.TableMetadata; +import org.apache.paimon.web.server.data.model.AlterTableRequest; import org.apache.paimon.web.server.data.model.CatalogInfo; import org.apache.paimon.web.server.data.model.TableColumn; import org.apache.paimon.web.server.data.model.TableInfo; import org.apache.paimon.web.server.data.result.R; import org.apache.paimon.web.server.data.result.enums.Status; import org.apache.paimon.web.server.service.CatalogService; -import org.apache.paimon.web.server.util.CatalogUtils; import org.apache.paimon.web.server.util.DataTypeConvertUtils; +import org.apache.paimon.web.server.util.PaimonDataType; +import org.apache.paimon.web.server.util.PaimonServiceUtils; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** Table api controller. */ @Slf4j @@ -53,6 +60,9 @@ @RequestMapping("/api/table") public class TableController { + private static final String FIELDS_PREFIX = "FIELDS"; + private static final String DEFAULT_VALUE_SUFFIX = "default-value"; + @Autowired private CatalogService catalogService; /** @@ -61,12 +71,30 @@ public class TableController { * @param tableInfo The TableInfo object containing information about the table. * @return R indicating the success or failure of the operation. */ - @PostMapping("/createTable") + @PostMapping("/create") public R createTable(@RequestBody TableInfo tableInfo) { try { - Catalog catalog = CatalogUtils.getCatalog(getCatalogInfo(tableInfo.getCatalogName())); + PaimonService service = + PaimonServiceUtils.getPaimonService(getCatalogInfo(tableInfo.getCatalogName())); List partitionKeys = tableInfo.getPartitionKey(); + Map tableOptions = tableInfo.getTableOptions(); + List tableColumns = tableInfo.getTableColumns(); + if (!CollectionUtils.isEmpty(tableColumns)) { + for (TableColumn tableColumn : tableColumns) { + if (tableColumn.getDefaultValue() != null + && !tableColumn.getDefaultValue().equals("")) { + tableOptions.put( + FIELDS_PREFIX + + "." + + tableColumn.getField() + + "." + + DEFAULT_VALUE_SUFFIX, + tableColumn.getDefaultValue()); + } + } + } + TableMetadata tableMetadata = TableMetadata.builder() .columns(buildColumns(tableInfo)) @@ -75,19 +103,285 @@ public R createTable(@RequestBody TableInfo tableInfo) { .options(tableOptions) .comment(tableInfo.getDescription()) .build(); - if (TableManager.tableExists( - catalog, tableInfo.getDatabaseName(), tableInfo.getTableName())) { + if (service.tableExists(tableInfo.getDatabaseName(), tableInfo.getTableName())) { return R.failed(Status.TABLE_NAME_IS_EXIST, tableInfo.getTableName()); } - TableManager.createTable( - catalog, tableInfo.getDatabaseName(), tableInfo.getTableName(), tableMetadata); + service.createTable( + tableInfo.getDatabaseName(), tableInfo.getTableName(), tableMetadata); return R.succeed(); } catch (Exception e) { - e.printStackTrace(); + log.error("Exception with creating table.", e); return R.failed(Status.TABLE_CREATE_ERROR); } } + /** + * Adds a column to the table. + * + * @param tableInfo The information of the table, including the catalog name, database name, + * table name, and table columns. + * @return A response indicating the success or failure of the operation. + */ + @PostMapping("/column/add") + public R addColumn(@RequestBody TableInfo tableInfo) { + try { + PaimonService service = + PaimonServiceUtils.getPaimonService(getCatalogInfo(tableInfo.getCatalogName())); + List tableColumns = tableInfo.getTableColumns(); + List tableChanges = new ArrayList<>(); + Map options = new HashMap<>(); + for (TableColumn tableColumn : tableColumns) { + if (tableColumn.getDefaultValue() != null + && !tableColumn.getDefaultValue().equals("")) { + options.put( + FIELDS_PREFIX + + "." + + tableColumn.getField() + + "." + + DEFAULT_VALUE_SUFFIX, + tableColumn.getDefaultValue()); + } + ColumnMetadata columnMetadata = + new ColumnMetadata( + tableColumn.getField(), + DataTypeConvertUtils.convert( + new PaimonDataType( + tableColumn.getDataType().getType(), + true, + tableColumn.getDataType().getPrecision(), + tableColumn.getDataType().getScale())), + tableColumn.getComment()); + TableChange.AddColumn add = TableChange.add(columnMetadata); + tableChanges.add(add); + } + + if (options.size() > 0) { + for (Map.Entry entry : options.entrySet()) { + TableChange.SetOption setOption = + TableChange.set(entry.getKey(), entry.getValue()); + tableChanges.add(setOption); + } + } + service.alterTable(tableInfo.getDatabaseName(), tableInfo.getTableName(), tableChanges); + return R.succeed(); + } catch (Exception e) { + log.error("Exception with adding column.", e); + return R.failed(Status.TABLE_ADD_COLUMN_ERROR); + } + } + + /** + * Drops a column from a table. + * + * @param catalogName The name of the catalog. + * @param databaseName The name of the database. + * @param tableName The name of the table. + * @param columnName The name of the column to be dropped. + * @return The result indicating the success or failure of the operation. + */ + @DeleteMapping("/column/drop/{catalogName}/{databaseName}/{tableName}/{columnName}") + public R dropColumn( + @PathVariable String catalogName, + @PathVariable String databaseName, + @PathVariable String tableName, + @PathVariable String columnName) { + try { + PaimonService service = + PaimonServiceUtils.getPaimonService(getCatalogInfo(catalogName)); + List tableChanges = new ArrayList<>(); + TableChange.DropColumn dropColumn = TableChange.dropColumn(columnName); + tableChanges.add(dropColumn); + service.alterTable(databaseName, tableName, tableChanges); + return R.succeed(); + } catch (Exception e) { + log.error("Exception with dropping column.", e); + return R.failed(Status.TABLE_DROP_COLUMN_ERROR); + } + } + + /** + * Modify a column in a table. + * + * @param catalogName The name of the catalog. + * @param databaseName The name of the database. + * @param tableName The name of the table. + * @param alterTableRequest The param of the alter table request. + * @return A response indicating the success or failure of the operation. + */ + @PostMapping("/alter") + public R alterTable( + @RequestParam String catalogName, + @RequestParam String databaseName, + @RequestParam String tableName, + @RequestBody AlterTableRequest alterTableRequest) { + try { + PaimonService service = + PaimonServiceUtils.getPaimonService(getCatalogInfo(catalogName)); + + TableColumn oldColumn = alterTableRequest.getOldColumn(); + TableColumn newColumn = alterTableRequest.getNewColumn(); + + List tableChanges = createTableChanges(oldColumn, newColumn); + + if (!Objects.equals(newColumn.getField(), oldColumn.getField())) { + ColumnMetadata columnMetadata = + new ColumnMetadata( + oldColumn.getField(), + DataTypeConvertUtils.convert(oldColumn.getDataType()), + oldColumn.getComment()); + + TableChange.ModifyColumnName modifyColumnName = + TableChange.modifyColumnName(columnMetadata, newColumn.getField()); + List modifyNameTableChanges = new ArrayList<>(); + modifyNameTableChanges.add(modifyColumnName); + service.alterTable(databaseName, tableName, modifyNameTableChanges); + } + + service.alterTable(databaseName, tableName, tableChanges); + return R.succeed(); + } catch (Exception e) { + log.error("Exception with altering table.", e); + return R.failed(Status.TABLE_AlTER_COLUMN_ERROR); + } + } + + private List createTableChanges(TableColumn oldColumn, TableColumn newColumn) { + ColumnMetadata columnMetadata = + new ColumnMetadata( + newColumn.getField(), + DataTypeConvertUtils.convert(oldColumn.getDataType()), + oldColumn.getComment()); + + TableChange.ModifyColumnType modifyColumnType = + TableChange.modifyColumnType( + columnMetadata, DataTypeConvertUtils.convert(newColumn.getDataType())); + + TableChange.ModifyColumnComment modifyColumnComment = + TableChange.modifyColumnComment(columnMetadata, newColumn.getComment()); + + List tableChanges = new ArrayList<>(); + tableChanges.add(modifyColumnType); + tableChanges.add(modifyColumnComment); + + return tableChanges; + } + + /** + * Adds options to a table. + * + * @param tableInfo An object containing table information. + * @return If the options are successfully added, returns a successful result object. If an + * exception occurs, returns a result object with an error status. + */ + @PostMapping("/option/add") + public R addOption(@RequestBody TableInfo tableInfo) { + List tableChanges = new ArrayList<>(); + try { + PaimonService service = + PaimonServiceUtils.getPaimonService(getCatalogInfo(tableInfo.getCatalogName())); + Map tableOptions = tableInfo.getTableOptions(); + for (Map.Entry entry : tableOptions.entrySet()) { + TableChange.SetOption setOption = TableChange.set(entry.getKey(), entry.getValue()); + tableChanges.add(setOption); + } + service.alterTable(tableInfo.getDatabaseName(), tableInfo.getTableName(), tableChanges); + return R.succeed(); + } catch (Exception e) { + log.error("Exception with adding option.", e); + return R.failed(Status.TABLE_ADD_OPTION_ERROR); + } + } + + /** + * Removes an option from a table. + * + * @param catalogName The name of the catalog. + * @param databaseName The name of the database. + * @param tableName The name of the table. + * @param key The key of the option to be removed. + * @return Returns a {@link R} object indicating the success or failure of the operation. If the + * option is successfully removed, the result will be a successful response with no data. If + * an error occurs during the operation, the result will be a failed response with an error + * code. Possible error codes: {@link Status#TABLE_REMOVE_OPTION_ERROR}. + */ + @PostMapping("/option/remove") + public R removeOption( + @RequestParam String catalogName, + @RequestParam String databaseName, + @RequestParam String tableName, + @RequestParam String key) { + List tableChanges = new ArrayList<>(); + try { + PaimonService service = + PaimonServiceUtils.getPaimonService(getCatalogInfo(catalogName)); + TableChange.RemoveOption removeOption = TableChange.remove(key); + tableChanges.add(removeOption); + service.alterTable(databaseName, tableName, tableChanges); + return R.succeed(); + } catch (Exception e) { + log.error("Exception with removing option.", e); + return R.failed(Status.TABLE_REMOVE_OPTION_ERROR); + } + } + + /** + * Drops a table from the specified database in the given catalog. + * + * @param catalogName The name of the catalog from which the table will be dropped. + * @param databaseName The name of the database from which the table will be dropped. + * @param tableName The name of the table to be dropped. + * @return A Response object indicating the success or failure of the operation. If the + * operation is successful, the response will be R.succeed(). If the operation fails, the + * response will be R.failed() with Status.TABLE_DROP_ERROR. + * @throws RuntimeException If there is an error during the operation, a RuntimeException is + * thrown with the error message. + */ + @DeleteMapping("/drop/{catalogName}/{databaseName}/{tableName}") + public R dropTable( + @PathVariable String catalogName, + @PathVariable String databaseName, + @PathVariable String tableName) { + try { + PaimonService service = + PaimonServiceUtils.getPaimonService(getCatalogInfo(catalogName)); + service.dropTable(databaseName, tableName); + return R.succeed(); + } catch (Exception e) { + log.error("Exception with dropping table.", e); + return R.failed(Status.TABLE_DROP_ERROR); + } + } + + /** + * Renames a table in the specified database of the given catalog. + * + * @param catalogName The name of the catalog where the table resides. + * @param databaseName The name of the database where the table resides. + * @param fromTableName The current name of the table to be renamed. + * @param toTableName The new name for the table. + * @return A Response object indicating the success or failure of the operation. If the + * operation is successful, the response will be R.succeed(). If the operation fails, the + * response will be R.failed() with Status.TABLE_RENAME_ERROR. + * @throws RuntimeException If there is an error during the operation, a RuntimeException is + * thrown with the error message. + */ + @PostMapping("/rename") + public R renameTable( + @RequestParam String catalogName, + @RequestParam String databaseName, + @RequestParam String fromTableName, + @RequestParam String toTableName) { + try { + PaimonService service = + PaimonServiceUtils.getPaimonService(getCatalogInfo(catalogName)); + service.renameTable(databaseName, fromTableName, toTableName); + return R.succeed(); + } catch (Exception e) { + log.error("Exception with renaming table.", e); + return R.failed(Status.TABLE_RENAME_ERROR); + } + } + /** * Handler method for the "/getAllTables" endpoint. Retrieves information about all tables and * returns a response containing the table details. @@ -98,102 +392,71 @@ public R createTable(@RequestBody TableInfo tableInfo) { public R> getAllTables() { List tableInfoList = new ArrayList<>(); List catalogInfoList = catalogService.list(); - if (catalogInfoList.size() > 0) { - catalogInfoList.forEach( - item -> { - Catalog catalog = CatalogUtils.getCatalog(item); - List databaseList = DatabaseManager.listDatabase(catalog); - if (databaseList.size() > 0) { - databaseList.forEach( - db -> { - try { - List tables = - TableManager.listTables(catalog, db); - if (tables.size() > 0) { - tables.forEach( - t -> { - try { - Table table = - TableManager.getTable( - catalog, db, t); - if (table != null) { - List primaryKeys = - table.primaryKeys(); - List fields = - table.rowType() - .getFields(); - List tableColumns = - new ArrayList<>(); - if (fields.size() > 0) { - fields.forEach( - field -> { - TableColumn - .TableColumnBuilder - builder = - TableColumn - .builder() - .field( - field - .name()) - .dataType( - DataTypeConvertUtils - .fromPaimonType( - field - .type())) - .comment( - field - .description()); - if (primaryKeys - .size() - > 0 - && primaryKeys - .contains( - field - .name())) { - builder - .isPK( - true); - } - tableColumns - .add( - builder - .build()); - }); - } - TableInfo tableInfo = - TableInfo.builder() - .catalogName( - item - .getCatalogName()) - .databaseName( - db) - .tableName( - table - .name()) - .partitionKey( - table - .partitionKeys()) - .tableOptions( - table - .options()) - .tableColumns( - tableColumns) - .build(); - tableInfoList.add(tableInfo); - } - } catch ( - Catalog.TableNotExistException - e) { - throw new RuntimeException(e); - } - }); + if (!CollectionUtils.isEmpty(catalogInfoList)) { + for (CatalogInfo item : catalogInfoList) { + PaimonService service = PaimonServiceUtils.getPaimonService(item); + List databaseList = service.listDatabases(); + if (!CollectionUtils.isEmpty(databaseList)) { + for (String db : databaseList) { + try { + List tables = service.listTables(db); + if (!CollectionUtils.isEmpty(tables)) { + for (String t : tables) { + try { + Table table = service.getTable(db, t); + if (table != null) { + List primaryKeys = table.primaryKeys(); + List fields = table.rowType().getFields(); + List tableColumns = new ArrayList<>(); + Map options = table.options(); + if (!CollectionUtils.isEmpty(fields)) { + for (DataField field : fields) { + String key = + FIELDS_PREFIX + + "." + + field.name() + + "." + + DEFAULT_VALUE_SUFFIX; + PaimonDataType dataType = + DataTypeConvertUtils.fromPaimonType( + field.type()); + TableColumn.TableColumnBuilder builder = + TableColumn.builder() + .field(field.name()) + .dataType(dataType) + .comment(field.description()); + if (primaryKeys.size() > 0 + && primaryKeys.contains(field.name())) { + builder.isPk(true); + } + if (options.get(key) != null) { + builder.defaultValue(options.get(key)); + } + tableColumns.add(builder.build()); + } } - } catch (Catalog.DatabaseNotExistException e) { - throw new RuntimeException(e); + TableInfo tableInfo = + TableInfo.builder() + .catalogName(item.getCatalogName()) + .databaseName(db) + .tableName(table.name()) + .partitionKey(table.partitionKeys()) + .tableOptions(table.options()) + .tableColumns(tableColumns) + .build(); + tableInfoList.add(tableInfo); } - }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } catch (Exception e) { + throw new RuntimeException(e); } - }); + } + } + } } return R.succeed(tableInfoList); } @@ -207,10 +470,10 @@ public R> getAllTables() { private List buildPrimaryKeys(TableInfo tableInfo) { List primaryKeys = new ArrayList<>(); List tableColumns = tableInfo.getTableColumns(); - if (tableColumns != null && tableColumns.size() > 0) { + if (!CollectionUtils.isEmpty(tableColumns)) { tableColumns.forEach( item -> { - if (item.isPK()) { + if (item.isPk()) { primaryKeys.add(item.getField()); } }); @@ -227,13 +490,18 @@ private List buildPrimaryKeys(TableInfo tableInfo) { private List buildColumns(TableInfo tableInfo) { List columns = new ArrayList<>(); List tableColumns = tableInfo.getTableColumns(); - if (tableColumns != null && tableColumns.size() > 0) { + if (!CollectionUtils.isEmpty(tableColumns)) { tableColumns.forEach( item -> { ColumnMetadata columnMetadata = new ColumnMetadata( item.getField(), - DataTypeConvertUtils.convert(item.getDataType()), + DataTypeConvertUtils.convert( + new PaimonDataType( + item.getDataType().getType(), + item.getDataType().isNullable(), + item.getDataType().getPrecision(), + item.getDataType().getScale())), item.getComment() != null ? item.getComment() : null); columns.add(columnMetadata); }); diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/enums/CatalogMode.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/enums/CatalogMode.java new file mode 100644 index 000000000..2dc029ad1 --- /dev/null +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/enums/CatalogMode.java @@ -0,0 +1,35 @@ +/* + * 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.server.data.enums; + +/** Enum representing different catalog modes. */ +public enum CatalogMode { + FILESYSTEM("filesystem"), + HIVE("hive"); + + private final String mode; + + CatalogMode(String mode) { + this.mode = mode; + } + + public String getMode() { + return mode; + } +} diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/AlterTableRequest.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/AlterTableRequest.java new file mode 100644 index 000000000..73f1873f9 --- /dev/null +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/AlterTableRequest.java @@ -0,0 +1,34 @@ +/* + * 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.server.data.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** Alter table request. */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AlterTableRequest { + + private TableColumn oldColumn; + + private TableColumn newColumn; +} diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/DatabaseInfo.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/DatabaseInfo.java index 0f93b7805..5789ec01a 100644 --- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/DatabaseInfo.java +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/DatabaseInfo.java @@ -34,5 +34,7 @@ public class DatabaseInfo { private Integer catalogId; + private String catalogName; + private String description; } diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableColumn.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableColumn.java index 0593dea0d..62ea3d28a 100644 --- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableColumn.java +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableColumn.java @@ -18,11 +18,15 @@ package org.apache.paimon.web.server.data.model; +import org.apache.paimon.web.server.util.PaimonDataType; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import javax.annotation.Nullable; + /** TableColumn model. */ @Data @Builder @@ -32,11 +36,11 @@ public class TableColumn { private String field; - private String dataType; + private PaimonDataType dataType; - private String comment; + @Nullable private String comment; - private boolean isPK; + @Nullable private boolean isPk; - private String defaultValue; + @Nullable private String defaultValue; } diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java index c6e5788cc..5239e85e2 100644 --- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java @@ -56,15 +56,23 @@ public enum Status { /** ------------catalog-----------------. */ CATALOG_NAME_IS_EXIST(10301, "catalog.name.exist"), CATALOG_CREATE_ERROR(10302, "catalog.create.error"), + CATALOG_REMOVE_ERROR(10303, "catalog.remove.error"), /** ------------database-----------------. */ DATABASE_NAME_IS_EXIST(10401, "database.name.exist"), DATABASE_CREATE_ERROR(10402, "database.create.error"), + DATABASE_DROP_ERROR(10403, "database.drop.error"), /** ------------table-----------------. */ TABLE_NAME_IS_EXIST(10501, "table.name.exist"), TABLE_CREATE_ERROR(10502, "table.create.error"), - ; + TABLE_ADD_COLUMN_ERROR(10503, "table.add.column.error"), + TABLE_ADD_OPTION_ERROR(10504, "table.add.option.error"), + TABLE_REMOVE_OPTION_ERROR(10505, "table.remove.option.error"), + TABLE_DROP_COLUMN_ERROR(10506, "table.drop.column.error"), + TABLE_AlTER_COLUMN_ERROR(10507, "table.alter.column.error"), + TABLE_DROP_ERROR(10510, "table.drop.error"), + TABLE_RENAME_ERROR(10510, "table.rename.error"); private final int code; private final String msg; diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/CatalogUtils.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/CatalogUtils.java deleted file mode 100644 index c19668ed3..000000000 --- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/CatalogUtils.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.server.util; - -import org.apache.paimon.catalog.Catalog; -import org.apache.paimon.web.api.catalog.CatalogCreator; -import org.apache.paimon.web.server.data.model.CatalogInfo; - -/** catalog util. */ -public class CatalogUtils { - - /** - * Get a Catalog based on the provided CatalogInfo. - * - * @param catalogInfo The CatalogInfo object containing the catalog details. - * @return The created Catalog object. - */ - public static Catalog getCatalog(CatalogInfo catalogInfo) { - if ("filesystem".equals(catalogInfo.getCatalogType())) { - return CatalogCreator.createFilesystemCatalog(catalogInfo.getWarehouse()); - } else { - return CatalogCreator.createHiveCatalog( - catalogInfo.getWarehouse(), - catalogInfo.getHiveUri(), - catalogInfo.getHiveConfDir()); - } - } -} diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/DataTypeConvertUtils.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/DataTypeConvertUtils.java index aa4f9f99e..298cc83d7 100644 --- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/DataTypeConvertUtils.java +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/DataTypeConvertUtils.java @@ -18,74 +18,167 @@ package org.apache.paimon.web.server.util; +import org.apache.paimon.types.BigIntType; +import org.apache.paimon.types.BinaryType; +import org.apache.paimon.types.BooleanType; +import org.apache.paimon.types.CharType; import org.apache.paimon.types.DataType; -import org.apache.paimon.types.DataTypes; +import org.apache.paimon.types.DateType; +import org.apache.paimon.types.DecimalType; +import org.apache.paimon.types.DoubleType; +import org.apache.paimon.types.FloatType; +import org.apache.paimon.types.IntType; +import org.apache.paimon.types.LocalZonedTimestampType; +import org.apache.paimon.types.SmallIntType; +import org.apache.paimon.types.TimeType; +import org.apache.paimon.types.TimestampType; +import org.apache.paimon.types.TinyIntType; +import org.apache.paimon.types.VarBinaryType; +import org.apache.paimon.types.VarCharType; /** data type convert util. */ public class DataTypeConvertUtils { - public static DataType convert(String type) { - switch (type) { + public static DataType convert(PaimonDataType type) { + switch (type.getType()) { case "INT": - return DataTypes.INT(); + return new IntType(type.isNullable()); case "TINYINT": - return DataTypes.TINYINT(); + return new TinyIntType(type.isNullable()); case "SMALLINT": - return DataTypes.SMALLINT(); + return new SmallIntType(type.isNullable()); case "BIGINT": - return DataTypes.BIGINT(); + return new BigIntType(type.isNullable()); + case "CHAR": + return new CharType( + type.isNullable(), type.getPrecision() > 0 ? type.getPrecision() : 1); + case "VARCHAR": + return new VarCharType( + type.isNullable(), type.getPrecision() > 0 ? type.getPrecision() : 1); case "STRING": - return DataTypes.STRING(); + return new VarCharType(type.isNullable(), Integer.MAX_VALUE); + case "BINARY": + return new BinaryType( + type.isNullable(), type.getPrecision() > 0 ? type.getPrecision() : 1); + case "VARBINARY": + return new VarBinaryType( + type.isNullable(), type.getPrecision() > 0 ? type.getPrecision() : 1); case "DOUBLE": - return DataTypes.DOUBLE(); + return new DoubleType(type.isNullable()); case "BOOLEAN": - return DataTypes.BOOLEAN(); + return new BooleanType(type.isNullable()); case "DATE": - return DataTypes.DATE(); + return new DateType(type.isNullable()); case "TIME": - return DataTypes.TIME(); + return new TimeType(type.isNullable(), 0); + case "TIME(precision)": + return new TimeType(type.isNullable(), type.getPrecision()); case "TIMESTAMP": - return DataTypes.TIMESTAMP(); + return new TimestampType(type.isNullable(), 0); + case "TIMESTAMP(precision)": + return new TimestampType(type.isNullable(), type.getPrecision()); + case "TIMESTAMP_MILLIS": + return new TimestampType(type.isNullable(), 3); case "BYTES": - return DataTypes.BYTES(); + return new VarBinaryType(type.isNullable(), 0); case "FLOAT": - return DataTypes.FLOAT(); + return new FloatType(type.isNullable()); case "DECIMAL": - return DataTypes.DECIMAL(38, 0); + return new DecimalType(type.isNullable(), type.getPrecision(), type.getScale()); + case "TIMESTAMP_WITH_LOCAL_TIME_ZONE": + return new LocalZonedTimestampType(type.isNullable(), 0); + case "TIMESTAMP_WITH_LOCAL_TIME_ZONE(precision)": + return new LocalZonedTimestampType(type.isNullable(), type.getPrecision()); default: throw new RuntimeException("Invalid type: " + type); } } - public static String fromPaimonType(DataType dataType) { - if (dataType.equals(DataTypes.INT())) { - return "INT"; - } else if (dataType.equals(DataTypes.TINYINT())) { - return "TINYINT"; - } else if (dataType.equals(DataTypes.SMALLINT())) { - return "SMALLINT"; - } else if (dataType.equals(DataTypes.BIGINT())) { - return "BIGINT"; - } else if (dataType.equals(DataTypes.STRING())) { - return "STRING"; - } else if (dataType.equals(DataTypes.DOUBLE())) { - return "DOUBLE"; - } else if (dataType.equals(DataTypes.BOOLEAN())) { - return "BOOLEAN"; - } else if (dataType.equals(DataTypes.DATE())) { - return "DATE"; - } else if (dataType.equals(DataTypes.TIME())) { - return "TIME"; - } else if (dataType.equals(DataTypes.TIMESTAMP())) { - return "TIMESTAMP"; - } else if (dataType.equals(DataTypes.BYTES())) { - return "BYTES"; - } else if (dataType.equals(DataTypes.FLOAT())) { - return "FLOAT"; - } else if (dataType.equals(DataTypes.DECIMAL(38, 0))) { - return "DECIMAL"; + public static PaimonDataType fromPaimonType(DataType dataType) { + if (dataType instanceof IntType) { + return new PaimonDataType("INT", dataType.isNullable(), 0, 0); + } else if (dataType instanceof TinyIntType) { + return new PaimonDataType("TINYINT", dataType.isNullable(), 0, 0); + } else if (dataType instanceof SmallIntType) { + return new PaimonDataType("SMALLINT", dataType.isNullable(), 0, 0); + } else if (dataType instanceof BigIntType) { + return new PaimonDataType("BIGINT", dataType.isNullable(), 0, 0); + } else if (dataType instanceof VarCharType) { + VarCharType varCharType = (VarCharType) dataType; + if (varCharType.getLength() == Integer.MAX_VALUE) { + return new PaimonDataType("STRING", varCharType.isNullable(), 0, 0); + } else { + return new PaimonDataType( + "VARCHAR", varCharType.isNullable(), varCharType.getLength(), 0); + } + } else if (dataType instanceof CharType) { + CharType charType = (CharType) dataType; + return new PaimonDataType("CHAR", charType.isNullable(), charType.getLength(), 0); + } else if (dataType instanceof BinaryType) { + BinaryType binaryType = (BinaryType) dataType; + return new PaimonDataType("BINARY", binaryType.isNullable(), binaryType.getLength(), 0); + } else if (dataType instanceof VarBinaryType) { + VarBinaryType varBinaryType = (VarBinaryType) dataType; + if (varBinaryType.getLength() == Integer.MAX_VALUE) { + return new PaimonDataType( + "BYTES", varBinaryType.isNullable(), varBinaryType.getLength(), 0); + } else { + return new PaimonDataType( + "VARBINARY", varBinaryType.isNullable(), varBinaryType.getLength(), 0); + } + } else if (dataType instanceof DoubleType) { + return new PaimonDataType("DOUBLE", dataType.isNullable(), 0, 0); + } else if (dataType instanceof BooleanType) { + return new PaimonDataType("BOOLEAN", dataType.isNullable(), 0, 0); + } else if (dataType instanceof DateType) { + return new PaimonDataType("DATE", dataType.isNullable(), 0, 0); + } else if (dataType instanceof TimeType) { + TimeType timeType = (TimeType) dataType; + if (timeType.getPrecision() == 0) { + return new PaimonDataType("TIME", timeType.isNullable(), 0, 0); + } else { + return new PaimonDataType( + "TIME(precision)", timeType.isNullable(), timeType.getPrecision(), 0); + } + } else if (dataType instanceof TimestampType) { + TimestampType timestampType = (TimestampType) dataType; + if (timestampType.getPrecision() == 0) { + return new PaimonDataType("TIMESTAMP", timestampType.isNullable(), 0, 0); + } else if (timestampType.getPrecision() == 3) { + return new PaimonDataType("TIMESTAMP_MILLIS", timestampType.isNullable(), 3, 0); + } else { + return new PaimonDataType( + "TIMESTAMP(precision)", + timestampType.isNullable(), + timestampType.getPrecision(), + 0); + } + } else if (dataType instanceof FloatType) { + return new PaimonDataType("FLOAT", dataType.isNullable(), 0, 0); + } else if (dataType instanceof DecimalType) { + DecimalType decimalType = (DecimalType) dataType; + return new PaimonDataType( + "DECIMAL", + decimalType.isNullable(), + decimalType.getPrecision(), + decimalType.getScale()); + } else if (dataType instanceof LocalZonedTimestampType) { + LocalZonedTimestampType localZonedTimestampType = (LocalZonedTimestampType) dataType; + if (localZonedTimestampType.getPrecision() == 0) { + return new PaimonDataType( + "TIMESTAMP_WITH_LOCAL_TIME_ZONE", + localZonedTimestampType.isNullable(), + 0, + 0); + } else { + return new PaimonDataType( + "TIMESTAMP_WITH_LOCAL_TIME_ZONE(precision)", + localZonedTimestampType.isNullable(), + localZonedTimestampType.getPrecision(), + 0); + } } else { - return "UNKNOWN"; + return new PaimonDataType("UNKNOWN", dataType.isNullable(), 0, 0); } } } diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/PaimonDataType.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/PaimonDataType.java new file mode 100644 index 000000000..76567ee66 --- /dev/null +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/PaimonDataType.java @@ -0,0 +1,40 @@ +/* + * 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.server.util; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** Paimon data type. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaimonDataType { + + private String type; + + private boolean isNullable; + + private Integer precision; + + private Integer scale; +} diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/PaimonServiceUtils.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/PaimonServiceUtils.java new file mode 100644 index 000000000..f4e3410d3 --- /dev/null +++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/PaimonServiceUtils.java @@ -0,0 +1,66 @@ +/* + * 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.server.util; + +import org.apache.paimon.web.api.catalog.PaimonService; +import org.apache.paimon.web.api.catalog.PaimonServiceFactory; +import org.apache.paimon.web.server.data.enums.CatalogMode; +import org.apache.paimon.web.server.data.model.CatalogInfo; + +import org.apache.commons.lang3.StringUtils; + +/** Paimon Service util. */ +public class PaimonServiceUtils { + + /** + * Get a Paimon Service based on the provided CatalogInfo. + * + * @param catalogInfo The CatalogInfo object containing the catalog details. + * @return The created PaimonService object. + */ + public static PaimonService getPaimonService(CatalogInfo catalogInfo) { + PaimonService service; + if (catalogInfo.getCatalogType().equalsIgnoreCase(CatalogMode.FILESYSTEM.getMode())) { + service = + PaimonServiceFactory.createFileSystemCatalogService( + catalogInfo.getCatalogName(), catalogInfo.getWarehouse()); + } else if (catalogInfo.getCatalogType().equalsIgnoreCase(CatalogMode.HIVE.getMode())) { + if (StringUtils.isNotBlank(catalogInfo.getHiveConfDir())) { + service = + PaimonServiceFactory.createHiveCatalogService( + catalogInfo.getCatalogName(), + catalogInfo.getWarehouse(), + catalogInfo.getHiveUri(), + catalogInfo.getHiveConfDir()); + } else { + service = + PaimonServiceFactory.createHiveCatalogService( + catalogInfo.getCatalogName(), + catalogInfo.getWarehouse(), + catalogInfo.getHiveUri(), + null); + } + } else { + service = + PaimonServiceFactory.createFileSystemCatalogService( + catalogInfo.getCatalogName(), catalogInfo.getWarehouse()); + } + return service; + } +} diff --git a/paimon-web-server/src/main/resources/application-dev.yml b/paimon-web-server/src/main/resources/application-dev-h2.yml similarity index 100% rename from paimon-web-server/src/main/resources/application-dev.yml rename to paimon-web-server/src/main/resources/application-dev-h2.yml diff --git a/paimon-web-server/src/main/resources/application-dev-mysql.yml b/paimon-web-server/src/main/resources/application-dev-mysql.yml new file mode 100644 index 000000000..67b2fb6d4 --- /dev/null +++ b/paimon-web-server/src/main/resources/application-dev-mysql.yml @@ -0,0 +1,21 @@ +# 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. + +spring: + datasource: + url: jdbc:mysql://${MYSQL_ADDR:127.0.0.1:3306}/${MYSQL_DATABASE:paimon}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: ${MYSQL_USERNAME:username} + password: ${MYSQL_PASSWORD:password} + driver-class-name: com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/paimon-web-server/src/main/resources/application-prod.yml b/paimon-web-server/src/main/resources/application-prod.yml index 133797283..67b2fb6d4 100644 --- a/paimon-web-server/src/main/resources/application-prod.yml +++ b/paimon-web-server/src/main/resources/application-prod.yml @@ -15,7 +15,7 @@ spring: datasource: - url: jdbc:mysql://${MYSQL_ADDR:124.221.249.188:3887}/${MYSQL_DATABASE:paimon}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true - username: ${MYSQL_USERNAME:root} - password: ${MYSQL_PASSWORD:Zhumingye520!@#.} + url: jdbc:mysql://${MYSQL_ADDR:127.0.0.1:3306}/${MYSQL_DATABASE:paimon}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: ${MYSQL_USERNAME:username} + password: ${MYSQL_PASSWORD:password} driver-class-name: com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/paimon-web-server/src/main/resources/application.yml b/paimon-web-server/src/main/resources/application.yml index c978065aa..db7c962e9 100644 --- a/paimon-web-server/src/main/resources/application.yml +++ b/paimon-web-server/src/main/resources/application.yml @@ -22,7 +22,7 @@ spring: application: name: Paimon-Web-UI profiles: - active: dev + active: dev-mysql messages: basename: i18n/messages encoding: UTF-8 diff --git a/paimon-web-server/src/main/resources/i18n/messages.properties b/paimon-web-server/src/main/resources/i18n/messages.properties index 86a4bc8b7..37610ec3c 100644 --- a/paimon-web-server/src/main/resources/i18n/messages.properties +++ b/paimon-web-server/src/main/resources/i18n/messages.properties @@ -34,8 +34,17 @@ menu.in.used=This menu is in used menu.name.exist=This menu name is exist:{0} menu.path.invalid=This menu path is invalid:{0} catalog.name.exist=This catalog name {0} is exist -catalog.create.error=An exception is returned when calling the Paimon API to create a Catalog. +catalog.create.error=Exception calling Paimon Catalog API to create a Catalog. +catalog.remove.error=Exception calling Paimon Catalog API to remove a Catalog. database.name.exist=This database name {0} is exist. -database.create.error=An exception is returned when calling the Paimon API to create a Database. +database.create.error=Exception calling Paimon Catalog API to create a Database. +database.drop.error=Exception calling Paimon Catalog API to drop a Database. table.name.exist=This table name {0} is exist. -table.create.error=An exception is returned when calling the Paimon API to create a Table. +table.create.error=Exception calling Paimon Catalog API to create a Table. +table.add.column.error=Exception calling Paimon Catalog API to add a Column. +table.drop.column.error=Exception calling Paimon Catalog API to drop a Column. +table.add.option.error=Exception calling Paimon Catalog API to add an option. +table.remove.option.error=Exception calling Paimon Catalog API to remove an option. +table.alter.column.error=Exception calling Paimon Catalog API to alter a column. +table.drop.error=Exception calling Paimon Catalog API to drop a table. +table.rename.error=Exception calling Paimon Catalog API to rename a table. diff --git a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/CatalogControllerTest.java b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/CatalogControllerTest.java new file mode 100644 index 000000000..e96ee1389 --- /dev/null +++ b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/CatalogControllerTest.java @@ -0,0 +1,99 @@ +/* + * 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.server.controller; + +import org.apache.paimon.web.server.data.model.CatalogInfo; +import org.apache.paimon.web.server.data.result.R; +import org.apache.paimon.web.server.util.ObjectMapperUtils; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Test for CatalogController. */ +@SpringBootTest +@AutoConfigureMockMvc +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CatalogControllerTest extends ControllerTestBase { + + private static final String catalogPath = "/api/catalog"; + + @TempDir java.nio.file.Path tempFile; + + private static final String catalogName = "testCatalog"; + + @Test + public void testCreateCatalog() throws Exception { + CatalogInfo catalogInfo = new CatalogInfo(); + catalogInfo.setCatalogType("filesystem"); + catalogInfo.setCatalogName(catalogName); + catalogInfo.setWarehouse(tempFile.toUri().toString()); + catalogInfo.setDelete(false); + + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.post(catalogPath + "/create") + .cookie(cookie) + .content(ObjectMapperUtils.toJSON(catalogInfo)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + + mockMvc.perform( + MockMvcRequestBuilders.delete(catalogPath + "/remove/" + catalogName) + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)); + } + + @Test + public void testGetAllCatalogs() throws Exception { + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.get(catalogPath + "/getAllCatalogs") + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + } +} diff --git a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/ControllerTestBase.java b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/ControllerTestBase.java new file mode 100644 index 000000000..d0bb0a570 --- /dev/null +++ b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/ControllerTestBase.java @@ -0,0 +1,199 @@ +/* + * 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.server.controller; + +import org.apache.paimon.web.server.data.dto.LoginDto; +import org.apache.paimon.web.server.data.model.CatalogInfo; +import org.apache.paimon.web.server.data.model.DatabaseInfo; +import org.apache.paimon.web.server.data.model.TableColumn; +import org.apache.paimon.web.server.data.model.TableInfo; +import org.apache.paimon.web.server.data.result.R; +import org.apache.paimon.web.server.util.ObjectMapperUtils; +import org.apache.paimon.web.server.util.PaimonDataType; +import org.apache.paimon.web.server.util.StringUtils; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockCookie; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** ControllerTestBase. */ +@SpringBootTest +@AutoConfigureMockMvc +public class ControllerTestBase { + + private static final String loginPath = "/api/login"; + private static final String logoutPath = "/api/logout"; + private static final String catalogPath = "/api/catalog"; + private static final String databasePath = "/api/database"; + private static final String tablePath = "/api/table"; + + @Value("${spring.application.name}") + private String tokenName; + + @Autowired public MockMvc mockMvc; + + public static MockCookie cookie; + + @TempDir java.nio.file.Path tempFile; + + private static final String catalogName = "paimon_catalog"; + + private static final String databaseName = "paimon_database"; + + private static final String tableName = "paimon_table"; + + @BeforeEach + public void before() throws Exception { + LoginDto login = new LoginDto(); + login.setUsername("admin"); + login.setPassword("admin"); + + MockHttpServletResponse response = + mockMvc.perform( + MockMvcRequestBuilders.post(loginPath) + .content(ObjectMapperUtils.toJSON(login)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse(); + String result = response.getContentAsString(); + R r = ObjectMapperUtils.fromJSON(result, R.class); + assertEquals(200, r.getCode()); + + assertTrue(StringUtils.isNotBlank(r.getData().toString())); + + cookie = (MockCookie) response.getCookie(tokenName); + + // create default catalog + CatalogInfo catalogInfo = new CatalogInfo(); + catalogInfo.setCatalogType("filesystem"); + catalogInfo.setCatalogName(catalogName); + catalogInfo.setWarehouse(tempFile.toUri().toString()); + catalogInfo.setDelete(false); + + mockMvc.perform( + MockMvcRequestBuilders.post(catalogPath + "/create") + .cookie(cookie) + .content(ObjectMapperUtils.toJSON(catalogInfo)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)); + + // create default database + DatabaseInfo databaseInfo = new DatabaseInfo(); + databaseInfo.setDatabaseName(databaseName); + databaseInfo.setCatalogName(catalogName); + + mockMvc.perform( + MockMvcRequestBuilders.post(databasePath + "/create") + .cookie(cookie) + .content(ObjectMapperUtils.toJSON(databaseInfo)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)); + + // create default table + List tableColumns = new ArrayList<>(); + TableColumn id = + new TableColumn("id", PaimonDataType.builder().type("INT").build(), "", false, "0"); + TableColumn name = + new TableColumn( + "name", PaimonDataType.builder().type("STRING").build(), "", false, "0"); + tableColumns.add(id); + tableColumns.add(name); + TableInfo tableInfo = + TableInfo.builder() + .catalogName(catalogName) + .databaseName(databaseName) + .tableName(tableName) + .tableColumns(tableColumns) + .partitionKey(Lists.newArrayList()) + .tableOptions(Maps.newHashMap()) + .build(); + + mockMvc.perform( + MockMvcRequestBuilders.post(tablePath + "/create") + .cookie(cookie) + .content(ObjectMapperUtils.toJSON(tableInfo)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)); + } + + @AfterEach + public void after() throws Exception { + String result = + mockMvc.perform( + MockMvcRequestBuilders.post(logoutPath) + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + R r = ObjectMapperUtils.fromJSON(result, R.class); + assertEquals(200, r.getCode()); + + mockMvc.perform( + MockMvcRequestBuilders.delete( + tablePath + + "/drop/" + + catalogName + + "/" + + databaseName + + "/" + + "test_table") + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)); + + mockMvc.perform( + MockMvcRequestBuilders.delete( + databasePath + "/drop/" + databaseName + "/" + catalogName) + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)); + + mockMvc.perform( + MockMvcRequestBuilders.delete(catalogPath + "/remove/" + catalogName) + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)); + } +} diff --git a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/DatabaseControllerTest.java b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/DatabaseControllerTest.java new file mode 100644 index 000000000..d23e8fd1e --- /dev/null +++ b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/DatabaseControllerTest.java @@ -0,0 +1,91 @@ +/* + * 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.server.controller; + +import org.apache.paimon.web.server.data.model.DatabaseInfo; +import org.apache.paimon.web.server.data.result.R; +import org.apache.paimon.web.server.util.ObjectMapperUtils; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Test for DatabaseController. */ +@SpringBootTest +@AutoConfigureMockMvc +public class DatabaseControllerTest extends ControllerTestBase { + + private static final String databasePath = "/api/database"; + + private static final String catalogName = "paimon_catalog"; + + @Test + public void testCreateDatabase() throws Exception { + DatabaseInfo databaseInfo = new DatabaseInfo(); + databaseInfo.setDatabaseName("test_db"); + databaseInfo.setCatalogName(catalogName); + + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.post(databasePath + "/create") + .cookie(cookie) + .content(ObjectMapperUtils.toJSON(databaseInfo)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + + mockMvc.perform( + MockMvcRequestBuilders.delete(databasePath + "/drop/" + "test_db/" + catalogName) + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)); + } + + @Test + public void testGetDatabases() throws Exception { + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.get(databasePath + "/getAllDatabases") + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + } +} diff --git a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/PermissionTest.java b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/PermissionTest.java index 21d22dd85..1e59b35fc 100644 --- a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/PermissionTest.java +++ b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/PermissionTest.java @@ -18,103 +18,43 @@ package org.apache.paimon.web.server.controller; -import org.apache.paimon.web.server.data.dto.LoginDto; import org.apache.paimon.web.server.data.model.User; import org.apache.paimon.web.server.data.result.R; import org.apache.paimon.web.server.data.result.enums.Status; import org.apache.paimon.web.server.util.ObjectMapperUtils; -import org.apache.paimon.web.server.util.StringUtils; import com.fasterxml.jackson.core.type.TypeReference; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; /** Test of permission service. */ @SpringBootTest @AutoConfigureMockMvc -public class PermissionTest { - private static final String loginPath = "/api/login"; - private static final String logoutPath = "/api/logout"; - +public class PermissionTest extends ControllerTestBase { private static final String getUserPath = "/api/user"; - @Value("${spring.application.name}") - private String tokenName; - - @Autowired private MockMvc mockMvc; - - private String token; - - @BeforeEach - public void before() throws Exception { - LoginDto login = new LoginDto(); - login.setUsername("common"); - login.setPassword("21232f297a57a5a743894a0e4a801fc3"); - - String result = - mockMvc.perform( - MockMvcRequestBuilders.post(loginPath) - .content(ObjectMapperUtils.toJSON(login)) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andDo(MockMvcResultHandlers.print()) - .andReturn() - .getResponse() - .getContentAsString(); - R r = ObjectMapperUtils.fromJSON(result, R.class); - assertEquals(200, r.getCode()); - - assertTrue(StringUtils.isNotBlank(r.getData().toString())); - - this.token = r.getData().toString(); - } - - @AfterEach - public void after() throws Exception { - String result = - mockMvc.perform( - MockMvcRequestBuilders.post(logoutPath) - .header(tokenName, token) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andDo(MockMvcResultHandlers.print()) - .andReturn() - .getResponse() - .getContentAsString(); - R r = ObjectMapperUtils.fromJSON(result, R.class); - assertEquals(200, r.getCode()); - } - @Test public void testNoPermission() throws Exception { String responseString = mockMvc.perform( MockMvcRequestBuilders.get(getUserPath + "/" + 1) - .header(tokenName, token) + .cookie(cookie) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.status().is4xxClientError()) + .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) .andDo(MockMvcResultHandlers.print()) .andReturn() .getResponse() .getContentAsString(); R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); - assertEquals(Status.FORBIDDEN.getCode(), r.getCode()); + assertEquals(Status.SUCCESS.getCode(), r.getCode()); } } diff --git a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SysMenuControllerTest.java b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SysMenuControllerTest.java index 6d9f09e23..16cef6222 100644 --- a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SysMenuControllerTest.java +++ b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SysMenuControllerTest.java @@ -18,24 +18,17 @@ package org.apache.paimon.web.server.controller; -import org.apache.paimon.web.server.data.dto.LoginDto; import org.apache.paimon.web.server.data.model.SysMenu; import org.apache.paimon.web.server.data.result.R; import org.apache.paimon.web.server.data.tree.TreeSelect; import org.apache.paimon.web.server.data.vo.RoleMenuTreeselectVo; import org.apache.paimon.web.server.util.ObjectMapperUtils; -import org.apache.paimon.web.server.util.StringUtils; import com.fasterxml.jackson.core.type.TypeReference; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @@ -49,67 +42,16 @@ /** Test for SysMenuController. */ @SpringBootTest @AutoConfigureMockMvc -public class SysMenuControllerTest { +public class SysMenuControllerTest extends ControllerTestBase { private static final String menuPath = "/api/menu"; - private static final String loginPath = "/api/login"; - private static final String logoutPath = "/api/logout"; - - @Value("${spring.application.name}") - private String tokenName; - - @Autowired private MockMvc mockMvc; - - private String token; - - @BeforeEach - public void before() throws Exception { - LoginDto login = new LoginDto(); - login.setUsername("admin"); - login.setPassword("21232f297a57a5a743894a0e4a801fc3"); - - String result = - mockMvc.perform( - MockMvcRequestBuilders.post(loginPath) - .content(ObjectMapperUtils.toJSON(login)) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andDo(MockMvcResultHandlers.print()) - .andReturn() - .getResponse() - .getContentAsString(); - R r = ObjectMapperUtils.fromJSON(result, R.class); - assertEquals(200, r.getCode()); - - assertTrue(StringUtils.isNotBlank(r.getData().toString())); - - this.token = r.getData().toString(); - } - - @AfterEach - public void after() throws Exception { - String result = - mockMvc.perform( - MockMvcRequestBuilders.post(logoutPath) - .header(tokenName, token) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andDo(MockMvcResultHandlers.print()) - .andReturn() - .getResponse() - .getContentAsString(); - R r = ObjectMapperUtils.fromJSON(result, R.class); - assertEquals(200, r.getCode()); - } @Test public void testList() throws Exception { String result = mockMvc.perform( MockMvcRequestBuilders.get(menuPath + "/list") - .header(tokenName, token) + .cookie(cookie) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -129,7 +71,7 @@ public void testGetInfo() throws Exception { String result = mockMvc.perform( MockMvcRequestBuilders.get(menuPath + "/1") - .header(tokenName, token) + .cookie(cookie) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -148,7 +90,7 @@ public void testGetTreeselect() throws Exception { String result = mockMvc.perform( MockMvcRequestBuilders.get(menuPath + "/treeselect") - .header(tokenName, token) + .cookie(cookie) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -168,7 +110,7 @@ public void testGetRoleMenuTreeselect() throws Exception { String result = mockMvc.perform( MockMvcRequestBuilders.get(menuPath + "/roleMenuTreeselect/1") - .header(tokenName, token) + .cookie(cookie) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk()) diff --git a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SysRoleControllerTest.java b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SysRoleControllerTest.java index 216e2699f..4f3276bdc 100644 --- a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SysRoleControllerTest.java +++ b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SysRoleControllerTest.java @@ -18,26 +18,19 @@ package org.apache.paimon.web.server.controller; -import org.apache.paimon.web.server.data.dto.LoginDto; import org.apache.paimon.web.server.data.model.SysRole; import org.apache.paimon.web.server.data.result.PageR; import org.apache.paimon.web.server.data.result.R; import org.apache.paimon.web.server.util.ObjectMapperUtils; -import org.apache.paimon.web.server.util.StringUtils; import com.fasterxml.jackson.core.type.TypeReference; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @@ -50,64 +43,13 @@ @SpringBootTest @AutoConfigureMockMvc @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class SysRoleControllerTest { +public class SysRoleControllerTest extends ControllerTestBase { private static final String rolePath = "/api/role"; - private static final String loginPath = "/api/login"; - private static final String logoutPath = "/api/logout"; private static final int roleId = 3; private static final String roleName = "test"; - @Value("${spring.application.name}") - private String tokenName; - - @Autowired private MockMvc mockMvc; - - private String token; - - @BeforeEach - public void before() throws Exception { - LoginDto login = new LoginDto(); - login.setUsername("admin"); - login.setPassword("21232f297a57a5a743894a0e4a801fc3"); - - String result = - mockMvc.perform( - MockMvcRequestBuilders.post(loginPath) - .content(ObjectMapperUtils.toJSON(login)) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andDo(MockMvcResultHandlers.print()) - .andReturn() - .getResponse() - .getContentAsString(); - R r = ObjectMapperUtils.fromJSON(result, R.class); - assertEquals(200, r.getCode()); - - assertTrue(StringUtils.isNotBlank(r.getData().toString())); - - this.token = r.getData().toString(); - } - - @AfterEach - public void after() throws Exception { - String result = - mockMvc.perform( - MockMvcRequestBuilders.post(logoutPath) - .header(tokenName, token) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andDo(MockMvcResultHandlers.print()) - .andReturn() - .getResponse() - .getContentAsString(); - R r = ObjectMapperUtils.fromJSON(result, R.class); - assertEquals(200, r.getCode()); - } - @Test @Order(1) public void testAddRole() throws Exception { @@ -122,7 +64,7 @@ public void testAddRole() throws Exception { mockMvc.perform( MockMvcRequestBuilders.post(rolePath) - .header(tokenName, token) + .cookie(cookie) .content(ObjectMapperUtils.toJSON(sysRole)) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) @@ -136,7 +78,7 @@ public void testQueryRole() throws Exception { String responseString = mockMvc.perform( MockMvcRequestBuilders.get(rolePath + "/" + roleId) - .header(tokenName, token) + .cookie(cookie) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -167,7 +109,7 @@ public void testEditRole() throws Exception { mockMvc.perform( MockMvcRequestBuilders.put(rolePath) - .header(tokenName, token) + .cookie(cookie) .content(ObjectMapperUtils.toJSON(sysRole)) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) @@ -176,7 +118,7 @@ public void testEditRole() throws Exception { String responseString = mockMvc.perform( MockMvcRequestBuilders.get(rolePath + "/" + roleId) - .header(tokenName, token) + .cookie(cookie) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -198,7 +140,7 @@ public void testDeleteRole() throws Exception { String delResponseString = mockMvc.perform( MockMvcRequestBuilders.delete(rolePath + "/" + roleId) - .header(tokenName, token) + .cookie(cookie) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -217,7 +159,7 @@ public void testGetRoleList() throws Exception { String responseString = mockMvc.perform( MockMvcRequestBuilders.get(rolePath + "/list") - .header(tokenName, token) + .cookie(cookie) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk()) diff --git a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/TableControllerTest.java b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/TableControllerTest.java new file mode 100644 index 000000000..f31d1d5ee --- /dev/null +++ b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/TableControllerTest.java @@ -0,0 +1,322 @@ +/* + * 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.server.controller; + +import org.apache.paimon.web.server.data.model.AlterTableRequest; +import org.apache.paimon.web.server.data.model.TableColumn; +import org.apache.paimon.web.server.data.model.TableInfo; +import org.apache.paimon.web.server.data.result.R; +import org.apache.paimon.web.server.util.ObjectMapperUtils; +import org.apache.paimon.web.server.util.PaimonDataType; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Test for TableController. */ +public class TableControllerTest extends ControllerTestBase { + + private static final String tablePath = "/api/table"; + + private static final String catalogName = "paimon_catalog"; + + private static final String databaseName = "paimon_database"; + + private static final String tableName = "paimon_table"; + + @Test + public void testCreateTable() throws Exception { + List tableColumns = new ArrayList<>(); + TableColumn id = + new TableColumn("id", PaimonDataType.builder().type("INT").build(), "", false, "0"); + TableColumn name = + new TableColumn( + "name", PaimonDataType.builder().type("STRING").build(), "", false, "0"); + tableColumns.add(id); + tableColumns.add(name); + TableInfo tableInfo = + TableInfo.builder() + .catalogName(catalogName) + .databaseName(databaseName) + .tableName("test_table") + .tableColumns(tableColumns) + .partitionKey(Lists.newArrayList()) + .tableOptions(Maps.newHashMap()) + .build(); + + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.post(tablePath + "/create") + .cookie(cookie) + .content(ObjectMapperUtils.toJSON(tableInfo)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + + mockMvc.perform( + MockMvcRequestBuilders.delete( + tablePath + + "/drop/" + + catalogName + + "/" + + databaseName + + "/" + + "test_table") + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)); + } + + @Test + public void testAddColumn() throws Exception { + List tableColumns = new ArrayList<>(); + TableColumn age = + new TableColumn( + "age", + PaimonDataType.builder().type("INT").isNullable(true).build(), + "", + false, + "0"); + tableColumns.add(age); + TableInfo tableInfo = + TableInfo.builder() + .catalogName(catalogName) + .databaseName(databaseName) + .tableName(tableName) + .tableColumns(tableColumns) + .partitionKey(Lists.newArrayList()) + .tableOptions(Maps.newHashMap()) + .build(); + + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.post(tablePath + "/column/add") + .cookie(cookie) + .content(ObjectMapperUtils.toJSON(tableInfo)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + } + + @Test + public void testDropColumn() throws Exception { + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.delete( + tablePath + + "/drop/" + + catalogName + + "/" + + databaseName + + "/" + + tableName + + "/" + + "name") + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + } + + @Test + public void testAlterTable() throws Exception { + TableColumn oldColumn = + new TableColumn("id", PaimonDataType.builder().type("INT").build(), "", false, "0"); + + TableColumn newColumn = + new TableColumn( + "age", PaimonDataType.builder().type("BIGINT").build(), "", false, "0"); + + AlterTableRequest alterTableRequest = new AlterTableRequest(); + alterTableRequest.setOldColumn(oldColumn); + alterTableRequest.setNewColumn(newColumn); + + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.post(tablePath + "/alter") + .cookie(cookie) + .param("catalogName", catalogName) + .param("databaseName", databaseName) + .param("tableName", tableName) + .content(ObjectMapperUtils.toJSON(alterTableRequest)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + } + + @Test + public void testAddOption() throws Exception { + Map option = new HashMap<>(); + option.put("bucket", "2"); + + TableInfo tableInfo = + TableInfo.builder() + .catalogName(catalogName) + .databaseName(databaseName) + .tableName(tableName) + .tableColumns(Lists.newArrayList()) + .partitionKey(Lists.newArrayList()) + .tableOptions(option) + .build(); + + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.post(tablePath + "/option/add") + .cookie(cookie) + .content(ObjectMapperUtils.toJSON(tableInfo)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + } + + @Test + public void testRemoveOption() throws Exception { + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.post(tablePath + "/option/remove") + .cookie(cookie) + .param("catalogName", catalogName) + .param("databaseName", databaseName) + .param("tableName", tableName) + .param("key", "bucket") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + } + + @Test + public void testRenameTable() throws Exception { + List tableColumns = new ArrayList<>(); + TableColumn id = + new TableColumn("id", PaimonDataType.builder().type("INT").build(), "", false, "0"); + TableColumn name = + new TableColumn( + "name", PaimonDataType.builder().type("STRING").build(), "", false, "0"); + tableColumns.add(id); + tableColumns.add(name); + TableInfo tableInfo = + TableInfo.builder() + .catalogName(catalogName) + .databaseName(databaseName) + .tableName("test_table_01") + .tableColumns(tableColumns) + .partitionKey(Lists.newArrayList()) + .tableOptions(Maps.newHashMap()) + .build(); + + mockMvc.perform( + MockMvcRequestBuilders.post(tablePath + "/create") + .cookie(cookie) + .content(ObjectMapperUtils.toJSON(tableInfo)) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()); + + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.post(tablePath + "/rename") + .cookie(cookie) + .param("catalogName", catalogName) + .param("databaseName", databaseName) + .param("fromTableName", "test_table_01") + .param("toTableName", "test_table_02") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + } + + @Test + public void testGetTables() throws Exception { + String responseString = + mockMvc.perform( + MockMvcRequestBuilders.get(tablePath + "/getAllTables") + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andReturn() + .getResponse() + .getContentAsString(); + + R r = ObjectMapperUtils.fromJSON(responseString, new TypeReference>() {}); + assertEquals(200, r.getCode()); + } +} diff --git a/pom.xml b/pom.xml index 151ffe238..9ff1baa75 100644 --- a/pom.xml +++ b/pom.xml @@ -358,6 +358,7 @@ under the License. release/** paimon-web-ui/node_modules/** + paimon-web-ui/dist/** paimon-web-ui-new/node_modules/** diff --git a/scripts/sql/paimon-mysql.sql b/scripts/sql/paimon-mysql.sql new file mode 100644 index 000000000..d3b004453 --- /dev/null +++ b/scripts/sql/paimon-mysql.sql @@ -0,0 +1,174 @@ +-- 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. + +DROP TABLE IF EXISTS `user`; +CREATE TABLE if not exists `user` +( + `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'ID', + `username` varchar(50) NOT NULL COMMENT 'username', + `password` varchar(50) NULL DEFAULT NULL COMMENT 'password', + `nickname` varchar(50) NULL DEFAULT NULL COMMENT 'nickname', + `user_type` int NOT NULL DEFAULT 0 COMMENT 'login type (0:LOCAL,1:LDAP)', + `url` varchar(100) NULL DEFAULT NULL COMMENT 'avatar url', + `mobile` varchar(20) NULL DEFAULT NULL COMMENT 'mobile phone', + `email` varchar(100) NULL DEFAULT NULL COMMENT 'email', + `enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'is enable', + `is_delete` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'is delete', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time' + ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `tenant`; +CREATE TABLE if not exists `tenant` +( + `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'ID', + `name` varchar(64) NULL DEFAULT NULL COMMENT 'tenant name', + `description` varchar(255) NULL DEFAULT NULL COMMENT 'tenant description', + `is_delete` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'is delete', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time' + ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `user_tenant`; +CREATE TABLE if not exists `user_tenant` +( + `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'ID', + `user_id` int(11) NOT NULL COMMENT 'user id', + `tenant_id` int(11) NOT NULL COMMENT 'tenant id', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time' + ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE if not exists `sys_role` +( + `id` int(11) not null auto_increment primary key comment 'id', + `role_name` varchar(30) not null comment 'role name', + `role_key` varchar(100) not null comment 'role key', + `sort` int(4) not null comment 'sort', + `enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'is enable', + `is_delete` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'is delete', + `remark` varchar(500) default null comment 'remark', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time' + ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `sys_menu`; +CREATE TABLE if not exists `sys_menu` +( + `id` int(11) not null auto_increment primary key comment 'id', + `menu_name` varchar(50) not null comment 'menu name', + `parent_id` int(11) default 0 comment 'parent id', + `sort` int(4) default 0 comment 'sort', + `path` varchar(200) default '' comment 'route path', + `query` varchar(255) default null comment 'route params', + `is_cache` int(1) default 0 comment 'is cache(0:cache 1:no_cache)', + `type` char(1) default '' comment 'menu type(M:directory C:menu F:button)', + `visible` char(1) default 0 comment 'is visible(0:display 1:hide)', + `component` varchar(255) default null comment 'component path', + `is_frame` int(1) default 0 comment 'is frame', + `enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'is enable', + `is_delete` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'is delete', + `perms` varchar(100) default null comment 'menu perms', + `icon` varchar(100) default '#' comment 'menu icon', + `remark` varchar(500) default '' comment 'remark', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time' + ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `user_role`; +CREATE TABLE if not exists `user_role` +( + `id` int(11) not null auto_increment primary key comment 'id', + `user_id` int(11) not null comment 'user id', + `role_id` int(11) not null comment 'role id', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time', + unique key `idx_user_role` (`user_id`, `role_id`) + ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `role_menu`; +CREATE TABLE if not exists `role_menu` +( + `id` int(11) not null auto_increment primary key comment 'id', + `role_id` int(11) not null comment 'role id', + `menu_id` int(11) not null comment 'menu id', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time', + unique key `idx_role_menu` (`role_id`, `menu_id`) + ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `catalog`; +CREATE TABLE if not exists `catalog` +( + `id` int(11) not null auto_increment primary key comment 'id', + `catalog_type` varchar(50) not null comment 'catalog type', + `catalog_name` varchar(100) not null comment 'catalog name', + `warehouse` varchar(200) not null comment 'warehouse', + `hive_uri` varchar(200) comment 'hive uri', + `hive_conf_dir` varchar(100) comment 'catalog name', + `is_delete` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'is delete', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time' + ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + + +INSERT INTO `user` ( id, username, password, nickname, mobile + , email, enabled, is_delete) +VALUES ( 1, 'admin', '21232f297a57a5a743894a0e4a801fc3', 'Admin', 0 + , 'admin@paimon.com', 1, 0); +INSERT INTO `user` (id, username, password, nickname, mobile, email, enabled, is_delete) +VALUES (2, 'common', '21232f297a57a5a743894a0e4a801fc3', 'common', 0, 'common@paimon.com', 1, 0); + +INSERT INTO `tenant` (id, name, description) +VALUES (1, 'DefaultTenant', 'DefaultTenant'); + +INSERT INTO `user_tenant` (`id`, `user_id`, `tenant_id`) +VALUES (1, 1, 1); + +insert into sys_role (id, role_name, role_key, sort) +values (1, 'admin', 'admin', 1), + (2, 'common', 'common', 2); + +insert into sys_menu (id, menu_name, parent_id, sort, path, component, is_frame, type, perms, icon, remark) +values (1, 'all', 0, 1, 'system', null, 1, 'M', 'system', 'admin', 'system root path'), + (100, 'user manager', 1, 1, 'user', 'user/index', 1, 'C', 'system:user:list', 'user', 'user manager'), + (1000, 'user query', 100, 1, '', '', 1, 'F', 'system:user:query', '#', ''), + (1001, 'user add', 100, 2, '', '', 1, 'F', 'system:user:add', '#', ''), + (1002, 'user edit', 100, 3, '', '', 1, 'F', 'system:user:edit', '#', ''), + (1003, 'user del', 100, 4, '', '', 1, 'F', 'system:user:remove', '#', ''), + (1004, 'user reset', 100, 5, '', '', 1, 'F', 'system:user:resetPwd', '#', ''), + (200, 'role manager', 1, 1, 'role', 'role/index', 1, 'C', 'system:role:list', 'role', 'role manager'), + (2000, 'role query', 200, 1, '', '', 1, 'F', 'system:role:query', '#', ''), + (2001, 'role add', 200, 2, '', '', 1, 'F', 'system:role:add', '#', ''), + (2002, 'role edit', 200, 3, '', '', 1, 'F', 'system:role:edit', '#', ''), + (2003, 'role del', 200, 4, '', '', 1, 'F', 'system:role:remove', '#', ''), + (300, 'menu manager', 1, 1, 'menu', 'menu/index', 1, 'C', 'system:menu:list', 'menu', 'menu manager'), + (3000, 'menu query', 300, 1, '', '', 1, 'F', 'system:menu:query', '#', ''), + (3001, 'menu add', 300, 2, '', '', 1, 'F', 'system:menu:add', '#', ''), + (3002, 'menu edit', 300, 3, '', '', 1, 'F', 'system:menu:edit', '#', ''), + (3003, 'menu del', 300, 4, '', '', 1, 'F', 'system:menu:remove', '#', ''); + +insert into user_role (id, user_id, role_id) +values (1, 1, 1), (2, 2, 2); + +insert into role_menu (role_id, menu_id) +values (1, 1), + (1, 100), + (1, 1000), + (1, 1001), + (1, 1002), + (1, 1003), + (1, 1004);