diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/pom.xml b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/pom.xml index c91a2bf741..f8564399e5 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/pom.xml +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/pom.xml @@ -19,6 +19,11 @@ ai.chat2db chat2db-community-postgresql + + org.junit.jupiter + junit-jupiter + test + chat2db-community-kingbase diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseDBManager.java index bb38195730..8684c50444 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseDBManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseDBManager.java @@ -1,6 +1,7 @@ package ai.chat2db.plugin.kingbase; import ai.chat2db.spi.IDbManager; +import ai.chat2db.plugin.kingbase.identifier.KingBaseSQLIdentifierProcessor; import ai.chat2db.spi.DefaultDBManager; import ai.chat2db.community.domain.api.model.async.AsyncContext; import ai.chat2db.spi.model.datasource.ConnectInfo; @@ -29,7 +30,8 @@ public Connection getConnection(ConnectInfo connectInfo) { connectInfo.setSchemaName(null); Connection connection = super.getConnection(connectInfo); if (StringUtils.isNotBlank(schemaName)) { - String sql = String.format(SQL_SET_SEARCH_PATH_USER_PUBLIC, schemaName); + String sql = String.format(SQL_SET_SEARCH_PATH_USER_PUBLIC, + KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName)); try { DefaultSQLExecutor.getInstance().execute(connection, sql); } catch (SQLException e) { @@ -56,21 +58,31 @@ public String replaceDatabaseInJdbcUrl(String url, String newDatabase) { @Override public String dropTable(Connection connection, String databaseName, String schemaName, String tableName) { - String sql = "drop table if exists " +tableName; - return sql; + return "DROP TABLE IF EXISTS " + qualifiedTableName(schemaName, tableName); } @Override public void copyTable(Connection connection, String databaseName, String schemaName, String tableName, String newTableName,boolean copyData) throws SQLException { - String sql = ""; - if(copyData){ - sql = "CREATE TABLE " + newTableName + " AS TABLE " + tableName + " WITH DATA"; - }else { - sql = "CREATE TABLE " + newTableName + " AS TABLE " + tableName + " WITH NO DATA"; - } + String sql = buildCopyTableSql(schemaName, tableName, newTableName, copyData); DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> null); } + static String buildCopyTableSql(String schemaName, String tableName, String newTableName, + boolean copyData) { + return "CREATE TABLE " + qualifiedTableName(schemaName, newTableName) + + " AS TABLE " + qualifiedTableName(schemaName, tableName) + + (copyData ? " WITH DATA" : " WITH NO DATA"); + } + + private static String qualifiedTableName(String schemaName, String tableName) { + String quotedTable = KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName); + if (StringUtils.isBlank(schemaName)) { + return quotedTable; + } + return KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName) + + "." + quotedTable; + } + @Override public void exportTableData(Connection connection, String databaseName, String schemaName, String tableName, AsyncContext asyncContext) { exportTableData(connection, databaseName, schemaName, tableName, asyncContext, 10000); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseMetaData.java index e5ff36b3e9..afd8af1055 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseMetaData.java @@ -9,7 +9,6 @@ import ai.chat2db.spi.ISQLIdentifierProcessor; import ai.chat2db.spi.ISqlBuilder; import ai.chat2db.spi.DefaultMetaService; -import ai.chat2db.spi.DefaultSQLIdentifierProcessor; import ai.chat2db.community.domain.api.model.account.*; import ai.chat2db.community.domain.api.model.async.*; import ai.chat2db.community.domain.api.config.*; @@ -22,7 +21,6 @@ import ai.chat2db.community.domain.api.model.view.*; import ai.chat2db.spi.sql.Chat2DBContext; import ai.chat2db.spi.DefaultSQLExecutor; -import ai.chat2db.spi.util.SqlUtils; import jakarta.validation.constraints.NotEmpty; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -48,8 +46,6 @@ public class KingBaseMetaData extends DefaultMetaService implements IDbMetaData - public static final DefaultSQLIdentifierProcessor KINGBASE_SQL_IDENTIFIER_PROCESSOR = new KingBaseSQLIdentifierProcessor(); - @Override public List databases(Connection connection) { String sql = "SELECT datname FROM sys_database"; @@ -83,7 +79,7 @@ private String format(String objectName) { if (StringUtils.isBlank(objectName)) { return objectName; } else { - return SqlUtils.quoteObjectName(objectName); + return KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(objectName); } } @@ -112,7 +108,7 @@ public String tableDDL(Connection connection, String databaseName, String schema StringBuilder ddlBuilder = new StringBuilder(200); - String formatTableName = format(tableName); + String formatTableName = getMetaDataName(schemaName, tableName); ddlBuilder.append(SQL_CREATE_TABLE).append(formatTableName); String options = DefaultSQLExecutor.getInstance().preExecute(connection, TABLE_OPTION_SQL, new String[]{schemaName, tableName}, resultSet -> { if (resultSet.next()) { @@ -139,9 +135,9 @@ public String tableDDL(Connection connection, String databaseName, String schema constraintsBuilder.append(",\n"); } constraintsBuilder.append("\t").append(" constraint ") - .append(constraintName) + .append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(constraintName)) .append(" ") - .append(constraintDefinition.toLowerCase()); + .append(constraintDefinition); } } if (!constraintsBuilder.isEmpty()) { @@ -158,7 +154,9 @@ public String tableDDL(Connection connection, String databaseName, String schema String partitionDefinition = resultSet.getString("PARTITION_DEFINITION"); boolean isParentTable = resultSet.getBoolean("is_parent_table"); if (StringUtils.isNotBlank(parentTableName) && StringUtils.isNotBlank(partitionDefinition)) { - ddlBuilder.append("\n").append(" partition of ").append(SqlUtils.quoteObjectName(parentTableName)).append("\n"); + ddlBuilder.append("\n").append(" partition of ") + .append(getMetaDataName(resultSet.getString("parent_schema"), parentTableName)) + .append("\n"); if (!constraintsBuilder.isEmpty()) { ddlBuilder.append("(\n") .append(constraintsBuilder) @@ -180,9 +178,9 @@ public String tableDDL(Connection connection, String databaseName, String schema String table_name = resultSet.getString("TABLE_NAME"); if (StringUtils.isNotBlank(owner) && StringUtils.isNotBlank(table_name)) { tableOwnerBuilder.append(SQL_ALTER_TABLE) - .append(format(table_name)) + .append(getMetaDataName(schemaName, table_name)) .append(" owner to ") - .append(owner) + .append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(owner)) .append(";").append("\n"); } } @@ -197,11 +195,11 @@ public String tableDDL(Connection connection, String databaseName, String schema String privilegeType = resultSet.getString("PRIVILEGE_TYPE"); if (StringUtils.isNotBlank(privilegeType)) { tablePrivilegeBuilder.append(SQL_GRANT) - .append(privilegeType.toLowerCase()) + .append(KingBaseSqlGuards.requirePrivilege(privilegeType)) .append(SQL_ON) .append(formatTableName) .append(" to ") - .append(grantee) + .append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(grantee)) .append(";").append("\n"); } } @@ -413,7 +411,7 @@ else if ("USER-DEFINED".equals(dataType)) { boolean isPartitioned = false; if (resultSet.next()) { ddlBuilder.append(" partition by ") - .append(resultSet.getString("partition_key").toLowerCase()) + .append(resultSet.getString("partition_key")) .append(";"); isPartitioned = true; ddlBuilder.append("\n"); @@ -428,18 +426,22 @@ else if ("USER-DEFINED".equals(dataType)) { String parentTableName = resultSet.getString("PARENT_TABLE"); String partitionDefinition = resultSet.getString("PARTITION_DEFINITION"); if (StringUtils.isNotBlank(parentTableName) && StringUtils.isNotBlank(partitionDefinition)) { - ddlBuilder.append("\n").append(SQL_CREATE_TABLE).append(format(subName)).append("\n") - .append("partition of ").append(parentTableName).append("\n") - .append(partitionDefinition.toLowerCase()).append(";\n"); + // These three names are quote_ident() output from LIST_PARTITIONED_SUB_TABLE_SQL. + ddlBuilder.append("\n").append(SQL_CREATE_TABLE) + .append(resultSet.getString("schema_name")).append(".").append(subName).append("\n") + .append("partition of ").append(format(schemaName)).append(".") + .append(parentTableName).append("\n") + .append(partitionDefinition).append(";\n"); } } }); } else if (childTableInfo.size() >= 2) { + String parentSchemaName = childTableInfo.get(0); String parentTableName = childTableInfo.get(1); ddlBuilder.append(" ").append(" inherits ") .append("(") - .append(format(parentTableName)) + .append(getMetaDataName(parentSchemaName, parentTableName)) .append(")").append("\n"); if (StringUtils.isNotBlank(options)) { ddlBuilder.append(" ").append(options).append("\n"); @@ -472,7 +474,7 @@ else if ("USER-DEFINED".equals(dataType)) { String comment = table.getComment(); if (StringUtils.isNotBlank(comment)) { ddlBuilder.append("\n").append(SQL_COMMENT_TABLE).append(formatTableName).append(" is ") - .append("'").append(comment).append("'") + .append("'").append(getSQLIdentifierProcessor().escapeString(comment)).append("'") .append(";\n"); } } @@ -481,7 +483,7 @@ else if ("USER-DEFINED".equals(dataType)) { String name = column.getName(); String comment = column.getComment(); if (StringUtils.isNotBlank(comment)) { - comment = KINGBASE_SQL_IDENTIFIER_PROCESSOR.escapeString(comment); + comment = getSQLIdentifierProcessor().escapeString(comment); ddlBuilder.append("\n").append(SQL_COMMENT_COLUMN) .append(formatTableName).append(".").append(format(name)) .append(" is ") @@ -498,7 +500,8 @@ else if ("USER-DEFINED".equals(dataType)) { String index_name = resultSet.getString("index_name"); String index_comment = resultSet.getString("index_comment"); - ddlBuilder.append(SQL_COMMENT_INDEX).append(index_name) + ddlBuilder.append(SQL_COMMENT_INDEX).append(resultSet.getString("schema_name")) + .append(".").append(index_name) .append(" is ").append(index_comment).append(";\n"); } @@ -625,7 +628,7 @@ public TableMeta getTableMeta(String databaseName, String schemaName, String tab @Override public String getMetaDataName(String... names) { - return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(name -> "\"" + name + "\"").collect(Collectors.joining(".")); + return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(KingBaseSQLIdentifierProcessor.INSTANCE::quoteIdentifierAlways).collect(Collectors.joining(".")); } @Override @@ -640,6 +643,6 @@ public List getSystemSchemas() { @Override public ISQLIdentifierProcessor getSQLIdentifierProcessor() { - return KINGBASE_SQL_IDENTIFIER_PROCESSOR; + return KingBaseSQLIdentifierProcessor.INSTANCE; } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseSqlGuards.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseSqlGuards.java new file mode 100644 index 0000000000..0b77f2b99f --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/KingBaseSqlGuards.java @@ -0,0 +1,35 @@ +package ai.chat2db.plugin.kingbase; + +import ai.chat2db.plugin.postgresql.PostgreSqlGuards; + +/** + * Validation helpers for non-escapable SQL expression positions in KingBase DDL + * generation. KingBase shares PostgreSQL's expression grammar for these paths, + * so the mature PostgreSQL scanners remain the source of truth. Escaping lives in + * {@link ai.chat2db.plugin.kingbase.identifier.KingBaseSQLIdentifierProcessor}. + */ +public final class KingBaseSqlGuards { + + private KingBaseSqlGuards() { + } + + public static String requireDefaultExpression(String value) { + return PostgreSqlGuards.requireDefaultExpression(value); + } + + public static String requireColumnTypeExpression(String value) { + return PostgreSqlGuards.requireColumnTypeExpression(value); + } + + public static boolean isTemporalExpression(String value) { + return PostgreSqlGuards.isTemporalExpression(value); + } + + public static boolean isFunctionOrCastExpression(String value) { + return PostgreSqlGuards.isFunctionOrCastExpression(value); + } + + public static String requirePrivilege(String privilege) { + return PostgreSqlGuards.requirePrivilege(privilege); + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/builder/KingBaseSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/builder/KingBaseSqlBuilder.java index 9f6d1f14bc..5f0de38ec5 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/builder/KingBaseSqlBuilder.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/builder/KingBaseSqlBuilder.java @@ -2,10 +2,12 @@ import ai.chat2db.spi.constant.SQLConstants; +import ai.chat2db.community.domain.api.enums.plugin.DmlTypeEnum; import ai.chat2db.plugin.kingbase.enums.type.KingBaseColumnTypeEnum; import ai.chat2db.plugin.kingbase.enums.type.KingBaseIndexTypeEnum; import ai.chat2db.spi.DefaultSqlBuilder; import ai.chat2db.spi.model.request.PageLimitRequest; +import ai.chat2db.spi.model.request.UpdateSqlRequest; import ai.chat2db.community.domain.api.model.account.*; import ai.chat2db.community.domain.api.model.async.*; import ai.chat2db.community.domain.api.config.*; @@ -18,17 +20,86 @@ import ai.chat2db.community.domain.api.model.view.*; import ai.chat2db.community.domain.api.config.TableBuilderConfig; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static ai.chat2db.plugin.kingbase.constant.KingBaseSqlBuilderConstants.*; +import ai.chat2db.plugin.kingbase.KingBaseSqlGuards; +import ai.chat2db.plugin.kingbase.identifier.KingBaseSQLIdentifierProcessor; public class KingBaseSqlBuilder extends DefaultSqlBuilder { + @Override + public String quoteIdentifier(String identifier) { + return KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(identifier); + } + + @Override + public String quoteQualifiedIdentifier(String... identifiers) { + if (identifiers.length == 3) { + return quoteQualifiedIdentifier(identifiers[1], identifiers[2]); + } + return Arrays.stream(identifiers) + .filter(StringUtils::isNotBlank) + .map(KingBaseSQLIdentifierProcessor.INSTANCE::quoteIdentifierAlways) + .collect(Collectors.joining(SQLConstants.DOT)); + } + + @Override + public String quoteAlias(String alias) { + return quoteIdentifier(alias); + } + + @Override + public String buildUpdate(UpdateSqlRequest request) { + StringBuilder script = new StringBuilder(SQLConstants.UPDATE_KEYWORD + SQLConstants.SPACE); + buildTableName(request.getDatabaseName(), request.getSchemaName(), request.getTableName(), script); + script.append(" SET "); + script.append(request.getRow().entrySet().stream() + .map(entry -> quoteIdentifier(entry.getKey()) + SQLConstants.EQUAL_SQL + entry.getValue()) + .collect(Collectors.joining(SQLConstants.COMMA))); + if (MapUtils.isNotEmpty(request.getPrimaryKeyMap())) { + script.append(" WHERE "); + script.append(request.getPrimaryKeyMap().entrySet().stream() + .map(entry -> quoteIdentifier(entry.getKey()) + SQLConstants.EQUAL_SQL + entry.getValue()) + .collect(Collectors.joining(SQLConstants.SQL_AND))); + } + return script.toString(); + } + + @Override + public String buildTemplate(Table table, String type) { + if (table == null || CollectionUtils.isEmpty(table.getColumnList()) || StringUtils.isBlank(type)) { + return SQLConstants.EMPTY; + } + String tableName = quoteQualifiedIdentifier(table.getSchemaName(), table.getName()); + List columnNames = table.getColumnList().stream() + .map(column -> quoteIdentifier(column.getName())) + .toList(); + if (DmlTypeEnum.INSERT.name().equalsIgnoreCase(type)) { + return "INSERT INTO " + tableName + " (" + String.join(SQLConstants.COMMA, columnNames) + + ") VALUES (" + columnNames.stream().map(name -> SQLConstants.SPACE) + .collect(Collectors.joining(SQLConstants.COMMA)) + ")"; + } + if (DmlTypeEnum.UPDATE.name().equalsIgnoreCase(type)) { + return "UPDATE " + tableName + " SET " + columnNames.stream() + .map(name -> name + SQLConstants.EQUAL_SQL + SQLConstants.SPACE) + .collect(Collectors.joining(SQLConstants.COMMA)) + " WHERE "; + } + if (DmlTypeEnum.DELETE.name().equalsIgnoreCase(type)) { + return "DELETE FROM " + tableName + " WHERE "; + } + if (DmlTypeEnum.SELECT.name().equalsIgnoreCase(type)) { + return "SELECT " + String.join(SQLConstants.COMMA, columnNames) + " FROM " + tableName; + } + return SQLConstants.EMPTY; + } @@ -50,18 +121,22 @@ public class KingBaseSqlBuilder extends DefaultSqlBuilder { @Override public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfig) { + boolean needFullTableName = tableBuilderConfig != null + && BooleanUtils.isTrue(tableBuilderConfig.getNeedFullTableName()); StringBuilder script = new StringBuilder(); script.append(SQL_CREATE_TABLE); - script.append(SQLConstants.DOUBLE_QUOTE).append(table.getName()).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.SPACE_OPEN_PARENTHESIS).append(SQLConstants.SPACE).append(SQLConstants.LINE_SEPARATOR); + script.append(needFullTableName + ? quoteQualifiedIdentifier(table.getSchemaName(), table.getName()) + : quoteIdentifier(table.getName())) + .append(SQLConstants.SPACE_OPEN_PARENTHESIS).append(SQLConstants.SPACE) + .append(SQLConstants.LINE_SEPARATOR); for (TableColumn column : table.getColumnList()) { if (StringUtils.isBlank(column.getName()) || StringUtils.isBlank(column.getColumnType())) { continue; } - KingBaseColumnTypeEnum typeEnum = KingBaseColumnTypeEnum.getByType(column.getColumnType()); - if(typeEnum ==null){ - continue; - } - script.append(SQLConstants.TAB).append(typeEnum.buildCreateColumnSql(column)).append(SQLConstants.COMMA_LINE_SEPARATOR); + script.append(SQLConstants.TAB) + .append(KingBaseColumnTypeEnum.buildCreateColumnSqlSafely(column)) + .append(SQLConstants.COMMA_LINE_SEPARATOR); } Map> tableIndexMap = table.getIndexList().stream() .collect(Collectors.partitioningBy(v -> KingBaseIndexTypeEnum.NORMAL.getName().equals(v.getType()))); @@ -83,7 +158,7 @@ public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfi script = new StringBuilder(script.substring(0, script.length() - 2)); script.append(SQLConstants.LINE_SEPARATOR_CLOSE_PARENTHESIS); if(StringUtils.isNotBlank(table.getTablespace())){ - script.append(SQL_TABLESPACE_DOUBLE_QUOTE).append(table.getTablespace()).append(SQLConstants.DOUBLE_QUOTE_SEMICOLON); + script.append(" TABLESPACE ").append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(table.getTablespace())).append(SQLConstants.SEMICOLON); }else { script.append(SQL_TABLESPACE_DOUBLE_QUOTE_SYS_DEFAULT_DOUBLE_QUOTE_SEMICOLON); } @@ -101,8 +176,11 @@ public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfi } if (StringUtils.isNotBlank(table.getComment())) { script.append(SQLConstants.LINE_SEPARATOR); - script.append(SQL_COMMENT_TABLE).append(SQLConstants.SPACE).append(SQLConstants.DOUBLE_QUOTE).append(table.getName()).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE) - .append(table.getComment()).append(SQLConstants.SINGLE_QUOTE_SEMICOLON_LINE_SEPARATOR); + script.append(SQL_COMMENT_TABLE).append(SQLConstants.SPACE) + .append(quoteQualifiedIdentifier( + needFullTableName ? table.getSchemaName() : null, table.getName())) + .append(SQLConstants.SQL_IS_SINGLE_QUOTE) + .append(KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(table.getComment())).append(SQLConstants.SINGLE_QUOTE_SEMICOLON_LINE_SEPARATOR); } List tableColumnList = table.getColumnList().stream().filter(v -> StringUtils.isNotBlank(v.getComment())).toList(); for (TableColumn tableColumn : tableColumnList) { @@ -128,32 +206,35 @@ public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfi @Override public String buildAlterTable(Table oldTable, Table newTable) { StringBuilder script = new StringBuilder(); - if (!StringUtils.equalsIgnoreCase(oldTable.getName(), newTable.getName())) { - script.append(SQL_ALTER_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(oldTable.getName()).append(SQLConstants.DOUBLE_QUOTE); - script.append(SQLConstants.TAB).append(SQL_RENAME).append(SQLConstants.DOUBLE_QUOTE).append(newTable.getName()).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.SEMICOLON_LINE_SEPARATOR); + String oldQualifiedName = quoteQualifiedIdentifier(oldTable.getSchemaName(), oldTable.getName()); + String newQualifiedName = quoteQualifiedIdentifier(newTable.getSchemaName(), newTable.getName()); + if (!StringUtils.equals(oldTable.getName(), newTable.getName())) { + script.append(SQL_ALTER_TABLE).append(oldQualifiedName); + script.append(SQLConstants.TAB).append(SQL_RENAME).append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(newTable.getName())).append(SQLConstants.SEMICOLON_LINE_SEPARATOR); } - newTable.setColumnList(newTable.getColumnList().stream().filter(v -> StringUtils.isNotBlank(v.getEditStatus())).toList()); newTable.setIndexList(newTable.getIndexList().stream().filter(v -> StringUtils.isNotBlank(v.getEditStatus())).toList()); List columnNameList = newTable.getColumnList().stream().filter(v -> v.getOldName() != null && !StringUtils.equals(v.getOldName(), v.getName())).toList(); for (TableColumn tableColumn : columnNameList) { - script.append(SQL_ALTER_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(newTable.getName()).append(VALUE_DOUBLE_QUOTE).append(SQL_RENAME_COLUMN) - .append(tableColumn.getOldName()).append(VALUE_DOUBLE_QUOTE_TO_DOUBLE_QUOTE).append(tableColumn.getName()).append(SQLConstants.DOUBLE_QUOTE_SEMICOLON_LINE_SEPARATOR); + script.append(SQL_ALTER_TABLE).append(newQualifiedName).append(SQLConstants.SPACE).append("RENAME COLUMN ") + .append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getOldName())).append(" TO ").append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getName())).append(SQLConstants.SEMICOLON_LINE_SEPARATOR); } Map> tableIndexMap = newTable.getIndexList().stream() .collect(Collectors.partitioningBy(v -> KingBaseIndexTypeEnum.NORMAL.getName().equals(v.getType()))); StringBuilder scriptModify = new StringBuilder(); Boolean modify = false; - scriptModify.append(SQL_ALTER_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(newTable.getName()).append(VALUE_DOUBLE_QUOTE_2); + scriptModify.append(SQL_ALTER_TABLE).append(newQualifiedName).append(" \n"); for (TableColumn tableColumn : newTable.getColumnList()) { - KingBaseColumnTypeEnum typeEnum = KingBaseColumnTypeEnum.getByType(tableColumn.getColumnType()); - if(typeEnum == null){ + if (StringUtils.isBlank(tableColumn.getEditStatus())) { continue; } - scriptModify.append(SQLConstants.TAB).append(typeEnum.buildModifyColumn(tableColumn)).append(SQLConstants.COMMA_LINE_SEPARATOR); - modify = true; + String modifyColumn = KingBaseColumnTypeEnum.buildModifyColumnSafely(tableColumn); + if (StringUtils.isNotBlank(modifyColumn)) { + scriptModify.append(SQLConstants.TAB).append(modifyColumn).append(SQLConstants.COMMA_LINE_SEPARATOR); + modify = true; + } } for (TableIndex tableIndex : tableIndexMap.get(Boolean.FALSE)) { @@ -183,8 +264,8 @@ public String buildAlterTable(Table oldTable, Table newTable) { } if (!StringUtils.equals(oldTable.getComment(), newTable.getComment())) { script.append(SQLConstants.LINE_SEPARATOR); - script.append(SQL_COMMENT_TABLE).append(SQLConstants.SPACE).append(SQLConstants.DOUBLE_QUOTE).append(newTable.getName()).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE) - .append(newTable.getComment()).append(SQLConstants.SINGLE_QUOTE_SEMICOLON_LINE_SEPARATOR); + script.append(SQL_COMMENT_TABLE).append(SQLConstants.SPACE).append(newQualifiedName).append(SQLConstants.SQL_IS_SINGLE_QUOTE) + .append(KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(newTable.getComment())).append(SQLConstants.SINGLE_QUOTE_SEMICOLON_LINE_SEPARATOR); } for (TableColumn tableColumn : newTable.getColumnList()) { KingBaseColumnTypeEnum typeEnum = KingBaseColumnTypeEnum.getByType(tableColumn.getColumnType()); @@ -229,19 +310,21 @@ public String buildPageLimit(PageLimitRequest request) { @Override public String buildCreateDatabase(Database database) { StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append(SQL_CREATE_DATABASE+database.getName()); + sqlBuilder.append(SQL_CREATE_DATABASE).append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(database.getName())); String owner = database.getOwner(); if (StringUtils.isBlank(owner)) { owner = SYSTEM_KEYWORD; } - sqlBuilder.append(SQL_WITH_OWNER_EQUAL_DOUBLE_QUOTE).append(owner).append(SQLConstants.DOUBLE_QUOTE); + sqlBuilder.append(" WITH OWNER = ").append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(owner)); if (StringUtils.isNotBlank(database.getCharset())) { - sqlBuilder.append(SQL_ENCODING).append(database.getCharset()).append(SQLConstants.EMPTY); + sqlBuilder.append(SQL_ENCODING).append(SQLConstants.SINGLE_QUOTE) + .append(KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(database.getCharset())) + .append(SQLConstants.SINGLE_QUOTE); } sqlBuilder.append(SQLConstants.SEMICOLON_LINE_SEPARATOR); if (StringUtils.isNotBlank(database.getComment())) { - sqlBuilder.append(SQL_COMMENT_DATABASE).append(database.getName()).append(SQLConstants.SQL_IS_SINGLE_QUOTE).append(database.getComment()).append(SQLConstants.SINGLE_QUOTE_SEMICOLON); + sqlBuilder.append(SQL_COMMENT_DATABASE).append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(database.getName())).append(SQLConstants.SQL_IS_SINGLE_QUOTE).append(KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(database.getComment())).append(SQLConstants.SINGLE_QUOTE_SEMICOLON); } return sqlBuilder.toString(); } @@ -250,13 +333,29 @@ public String buildCreateDatabase(Database database) { @Override public String buildCreateSchema(Schema schema){ StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append(SQL_CREATE_SCHEMA+schema.getName()+SQLConstants.EMPTY); + sqlBuilder.append(SQL_CREATE_SCHEMA).append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schema.getName())); String owner = schema.getOwner(); if(StringUtils.isBlank(schema.getOwner())){ owner = SYSTEM_KEYWORD; } - sqlBuilder.append(SQL_AUTHORIZATION_DOUBLE_QUOTE).append(owner).append(SQLConstants.DOUBLE_QUOTE); + sqlBuilder.append(" AUTHORIZATION ").append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(owner)); return sqlBuilder.toString(); } + @Override + protected void buildTableName(String databaseName, String schemaName, String tableName, + StringBuilder script) { + script.append(quoteQualifiedIdentifier(databaseName, schemaName, tableName)); + } + + @Override + protected void buildColumns(List columnList, StringBuilder script) { + if (CollectionUtils.isNotEmpty(columnList)) { + script.append(SQLConstants.SPACE_OPEN_PARENTHESIS) + .append(columnList.stream().map(this::quoteIdentifier) + .collect(Collectors.joining(SQLConstants.COMMA))) + .append(SQLConstants.CLOSE_PARENTHESIS_SPACE); + } + } + } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseColumnTypeEnumConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseColumnTypeEnumConstants.java index ee0ff102a8..fb0cb6daf4 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseColumnTypeEnumConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseColumnTypeEnumConstants.java @@ -15,9 +15,9 @@ public final class KingBaseColumnTypeEnumConstants { - public static final String SQL_ALTER_COLUMN = "ALTER COLUMN \""; + public static final String SQL_ALTER_COLUMN = "ALTER COLUMN "; public static final String SQL_COMMENT_COLUMN = "COMMENT ON COLUMN"; - public static final String SQL_DROP_COLUMN = "DROP COLUMN `"; + public static final String SQL_DROP_COLUMN = "DROP COLUMN "; private KingBaseColumnTypeEnumConstants() { } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseDBManagerConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseDBManagerConstants.java index 046e0188dd..f664f833cc 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseDBManagerConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseDBManagerConstants.java @@ -14,7 +14,7 @@ public final class KingBaseDBManagerConstants { - public static final String SQL_SET_SEARCH_PATH_USER_PUBLIC = "SET search_path TO \"%s\",\"$user\",\"public\""; + public static final String SQL_SET_SEARCH_PATH_USER_PUBLIC = "SET search_path TO %s,\"$user\",\"public\""; private KingBaseDBManagerConstants() { } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseIndexTypeEnumConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseIndexTypeEnumConstants.java index 9edf204b0f..1480df8d88 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseIndexTypeEnumConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/constant/KingBaseIndexTypeEnumConstants.java @@ -17,8 +17,8 @@ public final class KingBaseIndexTypeEnumConstants { public static final String SQL_COMMENT_CONSTRAINT = "COMMENT ON CONSTRAINT"; public static final String SQL_COMMENT_INDEX = "COMMENT ON INDEX"; public static final String SQL_CREATE = "CREATE"; - public static final String SQL_DROP_CONSTRAINT = "DROP CONSTRAINT \""; - public static final String SQL_DROP_INDEX = "DROP INDEX \""; + public static final String SQL_DROP_CONSTRAINT = "DROP CONSTRAINT "; + public static final String SQL_DROP_INDEX = "DROP INDEX "; public static final String SQL_ON = "ON "; private KingBaseIndexTypeEnumConstants() { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/enums/type/KingBaseColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/enums/type/KingBaseColumnTypeEnum.java index 8c6b747aeb..da6978ce91 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/enums/type/KingBaseColumnTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/enums/type/KingBaseColumnTypeEnum.java @@ -4,12 +4,14 @@ import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum; import ai.chat2db.community.domain.api.model.metadata.ColumnType; import ai.chat2db.community.domain.api.model.metadata.TableColumn; -import ai.chat2db.spi.util.SqlUtils; +import ai.chat2db.plugin.kingbase.KingBaseSqlGuards; +import ai.chat2db.plugin.kingbase.identifier.KingBaseSQLIdentifierProcessor; import com.google.common.collect.Maps; import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; import static ai.chat2db.plugin.kingbase.constant.KingBaseColumnTypeEnumConstants.*; @@ -77,11 +79,11 @@ public enum KingBaseColumnTypeEnum implements IColumnBuilder { - private static Map COLUMN_TYPE_MAP = Maps.newHashMap(); + private static final Map COLUMN_TYPE_MAP = Maps.newHashMap(); static { for (KingBaseColumnTypeEnum value : KingBaseColumnTypeEnum.values()) { - COLUMN_TYPE_MAP.put(value.getColumnType().getTypeName(), value); + COLUMN_TYPE_MAP.put(value.getColumnType().getTypeName().toUpperCase(Locale.ROOT), value); } } @@ -93,7 +95,20 @@ public enum KingBaseColumnTypeEnum implements IColumnBuilder { } public static KingBaseColumnTypeEnum getByType(String dataType) { - return COLUMN_TYPE_MAP.get(SqlUtils.removeDigits(dataType.toUpperCase())); + if (StringUtils.isBlank(dataType)) { + return null; + } + String typeExpression = KingBaseSqlGuards.requireColumnTypeExpression(dataType); + String baseType = typeExpression; + int argumentsStart = baseType.indexOf('('); + if (argumentsStart >= 0) { + baseType = baseType.substring(0, argumentsStart); + } + while (baseType.stripTrailing().endsWith("[]")) { + baseType = baseType.stripTrailing(); + baseType = baseType.substring(0, baseType.length() - 2); + } + return COLUMN_TYPE_MAP.get(baseType.trim().toUpperCase(Locale.ROOT)); } public static List getTypes() { @@ -106,15 +121,40 @@ public ColumnType getColumnType() { return columnType; } + public static String buildCreateColumnSqlSafely(TableColumn column) { + KingBaseColumnTypeEnum type = getByType(column.getColumnType()); + return type == null ? buildSafeFallbackColumn(column) : type.buildCreateColumnSql(column); + } + + public static String buildModifyColumnSafely(TableColumn column) { + KingBaseColumnTypeEnum type = getByType(column.getColumnType()); + if (type != null) { + return type.buildModifyColumn(column); + } + if (EditStatusEnum.DELETE.name().equals(column.getEditStatus())) { + return SQL_DROP_COLUMN + KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName()); + } + if (EditStatusEnum.ADD.name().equals(column.getEditStatus())) { + return "ADD COLUMN " + buildSafeFallbackColumn(column); + } + if (EditStatusEnum.MODIFY.name().equals(column.getEditStatus())) { + String columnName = KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName()); + String dataType = KingBaseSqlGuards.requireColumnTypeExpression(column.getColumnType()); + return SQL_ALTER_COLUMN + columnName + " TYPE " + dataType + + " USING " + columnName + "::" + dataType; + } + return ""; + } + @Override public String buildCreateColumnSql(TableColumn column) { - KingBaseColumnTypeEnum type = COLUMN_TYPE_MAP.get(column.getColumnType().toUpperCase()); + KingBaseColumnTypeEnum type = getByType(column.getColumnType()); if (type == null) { - return buildDefaultColumn(column, false); + return buildSafeFallbackColumn(column); } StringBuilder script = new StringBuilder(); - script.append("\"").append(column.getName()).append("\"").append(" "); + script.append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); @@ -132,30 +172,30 @@ private String buildCollation(TableColumn column, KingBaseColumnTypeEnum type) { if (!type.getColumnType().isSupportCollation() || StringUtils.isEmpty(column.getCollationName())) { return ""; } - return StringUtils.join("\"", column.getCollationName(), "\""); + return KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getCollationName()); } @Override public String buildModifyColumn(TableColumn column) { if (EditStatusEnum.DELETE.name().equals(column.getEditStatus())) { - return StringUtils.join(SQL_DROP_COLUMN, column.getName() + "`"); + return StringUtils.join(SQL_DROP_COLUMN, KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())); } if (EditStatusEnum.ADD.name().equals(column.getEditStatus())) { return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(column)); } if (EditStatusEnum.MODIFY.name().equals(column.getEditStatus())) { StringBuilder script = new StringBuilder(); - script.append(SQL_ALTER_COLUMN).append(column.getName()).append("\" TYPE ").append(buildDataType(column, this)).append(",\n"); + script.append(SQL_ALTER_COLUMN).append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" TYPE ").append(buildDataType(column, this)).append(",\n"); if (column.getNullable() != null && 1 == column.getNullable()) { - script.append("\t").append(SQL_ALTER_COLUMN).append(column.getName()).append("\" DROP NOT NULL ,\n"); + script.append("\t").append(SQL_ALTER_COLUMN).append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" DROP NOT NULL ,\n"); } else { - script.append("\t").append(SQL_ALTER_COLUMN).append(column.getName()).append("\" SET NOT NULL ,\n"); + script.append("\t").append(SQL_ALTER_COLUMN).append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" SET NOT NULL ,\n"); } String defaultValue = buildDefaultValue(column, this); if (StringUtils.isNotBlank(defaultValue)) { - script.append(SQL_ALTER_COLUMN).append(column.getName()).append("\" SET ").append(defaultValue).append(",\n"); + script.append(SQL_ALTER_COLUMN).append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" SET ").append(defaultValue).append(",\n"); } script = new StringBuilder(script.substring(0, script.length() - 2)); return script.toString(); @@ -168,8 +208,16 @@ public String buildComment(TableColumn column, KingBaseColumnTypeEnum type) { || EditStatusEnum.DELETE.name().equals(column.getEditStatus())) { return ""; } - return StringUtils.join(SQL_COMMENT_COLUMN, " \"", column.getTableName(), - "\".\"", column.getName(), "\" IS '", column.getComment(), "';"); + return StringUtils.join(SQL_COMMENT_COLUMN, " ", qualifiedName(column.getSchemaName(), column.getTableName()), + ".", KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName()), " IS '", KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(column.getComment()), "';"); + } + + private static String qualifiedName(String schemaName, String objectName) { + String quotedName = KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(objectName); + if (StringUtils.isBlank(schemaName)) { + return quotedName; + } + return KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName) + "." + quotedName; } private String buildDefaultValue(TableColumn column, KingBaseColumnTypeEnum type) { @@ -186,17 +234,41 @@ private String buildDefaultValue(TableColumn column, KingBaseColumnTypeEnum type } if (Arrays.asList(CHAR, VARCHAR).contains(type)) { - return StringUtils.join("DEFAULT '", column.getDefaultValue(), "'"); + if (KingBaseSqlGuards.isFunctionOrCastExpression(column.getDefaultValue())) { + return StringUtils.join("DEFAULT ", + KingBaseSqlGuards.requireDefaultExpression(column.getDefaultValue())); + } + return StringUtils.join("DEFAULT '", KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(column.getDefaultValue()), "'"); } if (Arrays.asList(TIMESTAMP, TIME, TIMETZ, TIMESTAMPTZ, DATE).contains(type)) { - if ("CURRENT_TIMESTAMP".equalsIgnoreCase(column.getDefaultValue().trim())) { - return StringUtils.join("DEFAULT ", column.getDefaultValue()); + if (KingBaseSqlGuards.isTemporalExpression(column.getDefaultValue())) { + return StringUtils.join("DEFAULT ", + KingBaseSqlGuards.requireDefaultExpression(column.getDefaultValue())); } - return StringUtils.join("DEFAULT '", column.getDefaultValue(), "'"); + return StringUtils.join("DEFAULT '", KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(column.getDefaultValue()), "'"); } - return StringUtils.join("DEFAULT ", column.getDefaultValue()); + return StringUtils.join("DEFAULT ", KingBaseSqlGuards.requireDefaultExpression(column.getDefaultValue())); + } + + private static String buildSafeFallbackColumn(TableColumn column) { + StringBuilder script = new StringBuilder(); + script.append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())) + .append(" ") + .append(KingBaseSqlGuards.requireColumnTypeExpression(column.getColumnType())); + if (column.getNullable() != null) { + script.append(column.getNullable() == 1 ? " NULL" : " NOT NULL"); + } + if (StringUtils.isNotEmpty(column.getDefaultValue())) { + if ("EMPTY_STRING".equalsIgnoreCase(column.getDefaultValue().trim())) { + script.append(" DEFAULT ''"); + } else { + script.append(" DEFAULT ") + .append(KingBaseSqlGuards.requireDefaultExpression(column.getDefaultValue())); + } + } + return script.toString(); } private String buildNullable(TableColumn column, KingBaseColumnTypeEnum type) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/enums/type/KingBaseIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/enums/type/KingBaseIndexTypeEnum.java index df1080bc8a..41b7bf17ee 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/enums/type/KingBaseIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/enums/type/KingBaseIndexTypeEnum.java @@ -4,6 +4,7 @@ import ai.chat2db.community.domain.api.model.metadata.IndexType; import ai.chat2db.community.domain.api.model.metadata.TableIndex; import ai.chat2db.community.domain.api.model.metadata.TableIndexColumn; +import ai.chat2db.plugin.kingbase.identifier.KingBaseSQLIdentifierProcessor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; @@ -74,7 +75,7 @@ public String buildIndexScript(TableIndex tableIndex) { script.append(buildIndexUnique(tableIndex)).append(" "); script.append(buildIndexConcurrently(tableIndex)).append(" "); script.append(buildIndexName(tableIndex)).append(" "); - script.append(SQL_ON).append("\"").append(tableIndex.getTableName()).append("\"").append(" "); + script.append(SQL_ON).append(qualifiedName(tableIndex.getSchemaName(), tableIndex.getTableName())).append(" "); script.append(buildIndexMethod(tableIndex)).append(" "); script.append(buildIndexColumn(tableIndex)); } else { @@ -92,16 +93,16 @@ private String buildForeignColum(TableIndex tableIndex) { StringBuilder script = new StringBuilder(); script.append(" REFERENCES "); if (StringUtils.isNotBlank(tableIndex.getForeignSchemaName())) { - script.append(tableIndex.getForeignSchemaName()).append("."); + script.append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getForeignSchemaName())).append("."); } if (StringUtils.isNotBlank(tableIndex.getForeignTableName())) { - script.append(tableIndex.getForeignTableName()).append(" "); + script.append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getForeignTableName())).append(" "); } if (CollectionUtils.isNotEmpty(tableIndex.getForeignColumnNamelist())) { script.append("("); for (String column : tableIndex.getForeignColumnNamelist()) { if (StringUtils.isNotBlank(column)) { - script.append("\"").append(column).append("\"").append(","); + script.append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column)).append(","); } } script.deleteCharAt(script.length() - 1); @@ -114,7 +115,7 @@ private String buildForeignColum(TableIndex tableIndex) { private String buildIndexMethod(TableIndex tableIndex) { if (StringUtils.isNotBlank(tableIndex.getMethod())) { - return "USING " + tableIndex.getMethod(); + return "USING " + KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getMethod()); } else { return ""; } @@ -141,10 +142,12 @@ public String buildIndexComment(TableIndex tableIndex) { return ""; } else if (NORMAL.equals(this)) { return StringUtils.join(SQL_COMMENT_INDEX, " ", - "\"", tableIndex.getName(), "\" IS '", tableIndex.getComment(), "';"); + qualifiedName(tableIndex.getSchemaName(), tableIndex.getName()), " IS '", KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(tableIndex.getComment()), "';"); } else { - return StringUtils.join(SQL_COMMENT_CONSTRAINT, " \"", tableIndex.getName(), "\" ON \"", tableIndex.getSchemaName(), - "\".\"", tableIndex.getTableName(), "\" IS '", tableIndex.getComment(), "';"); + return StringUtils.join(SQL_COMMENT_CONSTRAINT, " ", + KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getName()), " ON ", + qualifiedName(tableIndex.getSchemaName(), tableIndex.getTableName()), " IS '", + KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(tableIndex.getComment()), "';"); } } @@ -153,7 +156,7 @@ private String buildIndexColumn(TableIndex tableIndex) { script.append("("); for (TableIndexColumn column : tableIndex.getColumnList()) { if (StringUtils.isNotBlank(column.getColumnName())) { - script.append("\"").append(column.getColumnName()).append("\"").append(","); + script.append(KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getColumnName())).append(","); } } script.deleteCharAt(script.length() - 1); @@ -162,7 +165,7 @@ private String buildIndexColumn(TableIndex tableIndex) { } private String buildIndexName(TableIndex tableIndex) { - return "\"" + tableIndex.getName() + "\""; + return KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getName()); } public String buildModifyIndex(TableIndex tableIndex) { @@ -181,8 +184,17 @@ public String buildModifyIndex(TableIndex tableIndex) { private String buildDropIndex(TableIndex tableIndex) { if (NORMAL.equals(this)) { - return StringUtils.join(SQL_DROP_INDEX, tableIndex.getOldName(), "\""); + return StringUtils.join(SQL_DROP_INDEX, + qualifiedName(tableIndex.getSchemaName(), tableIndex.getOldName())); } - return StringUtils.join(SQL_DROP_CONSTRAINT, tableIndex.getOldName(), "\""); + return StringUtils.join(SQL_DROP_CONSTRAINT, KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getOldName())); + } + + private static String qualifiedName(String schemaName, String objectName) { + String quotedName = KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(objectName); + if (StringUtils.isBlank(schemaName)) { + return quotedName; + } + return KingBaseSQLIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName) + "." + quotedName; } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/identifier/KingBaseSQLIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/identifier/KingBaseSQLIdentifierProcessor.java index 05e9487581..664b1f4e01 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/identifier/KingBaseSQLIdentifierProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/main/java/ai/chat2db/plugin/kingbase/identifier/KingBaseSQLIdentifierProcessor.java @@ -1,20 +1,12 @@ package ai.chat2db.plugin.kingbase.identifier; -import ai.chat2db.spi.DefaultSQLIdentifierProcessor; -import org.apache.commons.lang3.StringUtils; +import ai.chat2db.plugin.postgresql.identifier.PostgreSQLIdentifierProcessor; -public class KingBaseSQLIdentifierProcessor extends DefaultSQLIdentifierProcessor { - - - @Override - public String quoteIdentifier(String identifier) { - if (isValidIdentifier(identifier)) { - if (containsUpperCase(identifier) || isReservedKeyword(identifier.toUpperCase(), null, null)) { - return StringUtils.wrap(identifier, '"'); - } - return identifier; - } - return StringUtils.wrap(identifier, '"'); - } +/** + * KingBase uses PostgreSQL-compatible identifier folding, delimiters, reserved + * words, and string literal escaping. + */ +public class KingBaseSQLIdentifierProcessor extends PostgreSQLIdentifierProcessor { + public static final KingBaseSQLIdentifierProcessor INSTANCE = new KingBaseSQLIdentifierProcessor(); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/test/java/ai/chat2db/plugin/kingbase/KingBaseSQLIdentifierProcessorTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/test/java/ai/chat2db/plugin/kingbase/KingBaseSQLIdentifierProcessorTest.java new file mode 100644 index 0000000000..e4f54ad94d --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-kingbase/src/test/java/ai/chat2db/plugin/kingbase/KingBaseSQLIdentifierProcessorTest.java @@ -0,0 +1,340 @@ +package ai.chat2db.plugin.kingbase; + +import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum; +import ai.chat2db.community.domain.api.config.TableBuilderConfig; +import ai.chat2db.community.domain.api.model.metadata.Database; +import ai.chat2db.community.domain.api.model.metadata.Schema; +import ai.chat2db.community.domain.api.model.metadata.Table; +import ai.chat2db.community.domain.api.model.metadata.TableColumn; +import ai.chat2db.community.domain.api.model.metadata.TableIndex; +import ai.chat2db.community.domain.api.model.metadata.TableIndexColumn; +import ai.chat2db.plugin.kingbase.builder.KingBaseSqlBuilder; +import ai.chat2db.plugin.kingbase.enums.type.KingBaseColumnTypeEnum; +import ai.chat2db.plugin.kingbase.enums.type.KingBaseIndexTypeEnum; +import ai.chat2db.plugin.kingbase.identifier.KingBaseSQLIdentifierProcessor; +import ai.chat2db.spi.model.request.DropTableRequest; +import ai.chat2db.spi.model.request.UpdateSqlRequest; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class KingBaseSQLIdentifierProcessorTest { + + @Test + void escapeStringDoublesSingleQuotes() { + assertNull(KingBaseSQLIdentifierProcessor.INSTANCE.escapeString(null)); + assertEquals("plain", KingBaseSQLIdentifierProcessor.INSTANCE.escapeString("plain")); + assertEquals("O''Brien", KingBaseSQLIdentifierProcessor.INSTANCE.escapeString("O'Brien")); + assertEquals("a''; DROP TABLE t; --", KingBaseSQLIdentifierProcessor.INSTANCE.escapeString("a'; DROP TABLE t; --")); + } + + @Test + void escapeIdentifierTreatsBoundaryQuotesAsRawContent() { + assertNull(KingBaseSQLIdentifierProcessor.escapeIdentifier(null)); + assertEquals("plain", KingBaseSQLIdentifierProcessor.escapeIdentifier("plain")); + assertEquals("we\"\"name", KingBaseSQLIdentifierProcessor.escapeIdentifier("we\"name")); + assertEquals("\"\"quoted\"\"", KingBaseSQLIdentifierProcessor.escapeIdentifier("\"quoted\"")); + } + + @Test + void quoteIdentifierConditionallyQuotes() { + KingBaseSQLIdentifierProcessor processor = KingBaseSQLIdentifierProcessor.INSTANCE; + assertNull(processor.quoteIdentifier(null)); + assertEquals("", processor.quoteIdentifier("")); + assertEquals("plain", processor.quoteIdentifier("plain")); + assertEquals("\"UPPER\"", processor.quoteIdentifier("UPPER")); + assertEquals("\"select\"", processor.quoteIdentifier("select")); + assertEquals("plain", processor.quoteIdentifier("plain", null, null)); + assertEquals("\"we\"\"name\"", processor.quoteIdentifier("we\"name")); + assertEquals("\"evil\"\"; DROP TABLE t; --\"", + processor.quoteIdentifier("evil\"; DROP TABLE t; --")); + } + + @Test + void quoteIdentifierIgnoreCaseRemainsConditionalAndPreservesCase() { + KingBaseSQLIdentifierProcessor processor = KingBaseSQLIdentifierProcessor.INSTANCE; + assertNull(processor.quoteIdentifierIgnoreCase(null)); + assertEquals("plain", processor.quoteIdentifierIgnoreCase("plain")); + assertEquals("\"MixedCase\"", processor.quoteIdentifierIgnoreCase("MixedCase")); + assertEquals("\"we\"\"name\"", processor.quoteIdentifierIgnoreCase("we\"name")); + } + + @Test + void quoteIdentifierAlwaysWrapsAndDoublesEmbeddedQuotes() { + KingBaseSQLIdentifierProcessor processor = KingBaseSQLIdentifierProcessor.INSTANCE; + assertNull(processor.quoteIdentifierAlways(null)); + assertEquals("\"\"", processor.quoteIdentifierAlways("")); + assertEquals("\"plain\"", processor.quoteIdentifierAlways("plain")); + assertEquals("\"UPPER\"", processor.quoteIdentifierAlways("UPPER")); + assertEquals("\"we\"\"name\"", processor.quoteIdentifierAlways("we\"name")); + assertEquals("\"\"\"quoted\"\"\"", processor.quoteIdentifierAlways("\"quoted\"")); + assertEquals("\"evil\"\"; DROP TABLE t; --\"", + processor.quoteIdentifierAlways("evil\"; DROP TABLE t; --")); + } + + @Test + void alwaysQuoteAndRemoveQuoteRoundTripExactRawIdentifiers() { + KingBaseSQLIdentifierProcessor processor = KingBaseSQLIdentifierProcessor.INSTANCE; + for (String raw : List.of("plain", "a\"b", "\"leading", "trailing\"", "\"both\"", "")) { + assertEquals(raw, processor.removeIdentifierQuote(processor.quoteIdentifierAlways(raw)), raw); + } + } + + @Test + void createTableNeutralizesMaliciousNamesAndComments() { + Table table = new Table(); + table.setName("evil\"; DROP TABLE t; --"); + table.setComment("x'; DROP TABLE t; --"); + + TableColumn column = new TableColumn(); + column.setName("c1"); + column.setColumnType("VARCHAR"); + table.setColumnList(List.of(column)); + + TableIndex index = new TableIndex(); + index.setName("idx\"evil"); + index.setType("Normal"); + index.setTableName("evil\"; DROP TABLE t; --"); + TableIndexColumn indexColumn = new TableIndexColumn(); + indexColumn.setColumnName("c1"); + index.setColumnList(List.of(indexColumn)); + table.setIndexList(List.of(index)); + + String script = new KingBaseSqlBuilder().buildCreateTable(table, null); + assertTrue(script.contains("CREATE TABLE \"evil\"\"; DROP TABLE t; --\""), script); + assertTrue(script.contains("IS 'x''; DROP TABLE t; --'"), script); + assertTrue(script.contains("\"idx\"\"evil\""), script); + assertTrue(script.contains("ON \"evil\"\"; DROP TABLE t; --\""), script); + } + + @Test + void alterTableRenameNeutralizesMaliciousNames() { + Table oldTable = new Table(); + oldTable.setName("old_t"); + oldTable.setColumnList(List.of()); + oldTable.setIndexList(List.of()); + Table newTable = new Table(); + newTable.setName("new\"; DROP TABLE t; --"); + newTable.setColumnList(List.of()); + newTable.setIndexList(List.of()); + + String script = new KingBaseSqlBuilder().buildAlterTable(oldTable, newTable); + assertTrue(script.contains("ALTER TABLE \"old_t\""), script); + assertTrue(script.contains("RENAME TO \"new\"\"; DROP TABLE t; --\""), script); + } + + @Test + void createDatabaseEscapesAndValidates() { + Database database = new Database(); + database.setName("db\"; DROP TABLE t; --"); + database.setCharset("UTF8"); + database.setComment("c'; DROP TABLE t; --"); + + String script = new KingBaseSqlBuilder().buildCreateDatabase(database); + assertTrue(script.contains("CREATE DATABASE \"db\"\"; DROP TABLE t; --\""), script); + assertTrue(script.contains("IS 'c''; DROP TABLE t; --'"), script); + + Database hostileCharset = new Database(); + hostileCharset.setName("db2"); + hostileCharset.setCharset("UTF8'; DROP TABLE t; --"); + String hostileCharsetSql = new KingBaseSqlBuilder().buildCreateDatabase(hostileCharset); + assertTrue(hostileCharsetSql.contains("ENCODING 'UTF8''; DROP TABLE t; --'"), hostileCharsetSql); + + Database quotedCharset = new Database(); + quotedCharset.setName("db3"); + quotedCharset.setCharset("UTF8"); + String ok = new KingBaseSqlBuilder().buildCreateDatabase(quotedCharset); + assertTrue(ok.contains("ENCODING 'UTF8'"), ok); + } + + @Test + void createSchemaNeutralizesMaliciousNames() { + Schema schema = new Schema(); + schema.setName("sch\"; x; --"); + String script = new KingBaseSqlBuilder().buildCreateSchema(schema); + assertTrue(script.contains("CREATE SCHEMA \"sch\"\"; x; --\""), script); + assertTrue(script.contains("AUTHORIZATION \"SYSTEM\""), script); + } + + @Test + void columnTypeEnumEscapesNamesCommentsAndDefaults() { + TableColumn column = new TableColumn(); + column.setName("c\"; x--"); + column.setColumnType("VARCHAR"); + column.setDefaultValue("O'Brien"); + String createColumn = KingBaseColumnTypeEnum.VARCHAR.buildCreateColumnSql(column); + assertTrue(createColumn.startsWith("\"c\"\"; x--\" VARCHAR"), createColumn); + assertTrue(createColumn.contains("DEFAULT 'O''Brien'"), createColumn); + + TableColumn commentColumn = new TableColumn(); + commentColumn.setName("c1"); + commentColumn.setTableName("t1"); + commentColumn.setComment("y'; DROP TABLE t; --"); + String comment = KingBaseColumnTypeEnum.VARCHAR.buildComment(commentColumn, KingBaseColumnTypeEnum.VARCHAR); + assertEquals("COMMENT ON COLUMN \"t1\".\"c1\" IS 'y''; DROP TABLE t; --';", comment); + + TableColumn badDefault = new TableColumn(); + badDefault.setName("n"); + badDefault.setColumnType("INTEGER"); + badDefault.setDefaultValue("1; DROP TABLE t--"); + assertThrows(IllegalArgumentException.class, + () -> KingBaseColumnTypeEnum.INTEGER.buildCreateColumnSql(badDefault)); + + TableColumn okDefault = new TableColumn(); + okDefault.setName("n"); + okDefault.setColumnType("INTEGER"); + okDefault.setDefaultValue("-1"); + String ok = KingBaseColumnTypeEnum.INTEGER.buildCreateColumnSql(okDefault); + assertTrue(ok.contains("DEFAULT -1"), ok); + + TableColumn quotedDefault = new TableColumn(); + quotedDefault.setName("n"); + quotedDefault.setColumnType("TEXT"); + quotedDefault.setDefaultValue("'quoted string'"); + String okQuoted = KingBaseColumnTypeEnum.TEXT.buildCreateColumnSql(quotedDefault); + assertTrue(okQuoted.contains("DEFAULT 'quoted string'"), okQuoted); + } + + @Test + void indexTypeEnumEscapesNamesAndComments() { + TableIndex index = new TableIndex(); + index.setName("i\"; x--"); + index.setComment("y'; z--"); + String comment = KingBaseIndexTypeEnum.NORMAL.buildIndexComment(index); + assertEquals("COMMENT ON INDEX \"i\"\"; x--\" IS 'y''; z--';", comment); + + TableIndex fk = new TableIndex(); + fk.setName("fk1"); + fk.setForeignSchemaName("s\"; x--"); + fk.setForeignTableName("ft"); + fk.setForeignColumnNamelist(List.of("c1")); + TableIndexColumn fkColumn = new TableIndexColumn(); + fkColumn.setColumnName("c1"); + fk.setColumnList(List.of(fkColumn)); + String fkScript = KingBaseIndexTypeEnum.FOREIGN.buildIndexScript(fk); + assertTrue(fkScript.contains("REFERENCES \"s\"\"; x--\".\"ft\" (\"c1\")"), fkScript); + + TableIndex drop = new TableIndex(); + drop.setOldName("o\"; x--"); + drop.setEditStatus(EditStatusEnum.DELETE.name()); + String dropScript = KingBaseIndexTypeEnum.NORMAL.buildModifyIndex(drop); + assertEquals("DROP INDEX \"o\"\"; x--\"", dropScript); + + TableIndex method = new TableIndex(); + method.setName("idx"); + method.setTableName("orders"); + method.setMethod("btree); DROP TABLE t;--"); + method.setColumnList(List.of(fkColumn)); + String methodScript = KingBaseIndexTypeEnum.NORMAL.buildIndexScript(method); + assertTrue(methodScript.contains("USING \"btree); DROP TABLE t;--\""), methodScript); + } + + @Test + void dbManagerQuotesObjectNames() { + String sql = new KingBaseDBManager().dropTable(null, null, "s\"x", "t\"; x--"); + assertEquals("DROP TABLE IF EXISTS \"s\"\"x\".\"t\"\"; x--\"", sql); + assertEquals("CREATE TABLE \"sales\".\"orders_copy\" AS TABLE \"sales\".\"orders\" WITH DATA", + KingBaseDBManager.buildCopyTableSql("sales", "orders", "orders_copy", true)); + } + + @Test + void metaDataNameDoublesEmbeddedQuotes() { + String name = new KingBaseMetaData().getMetaDataName("s", "we\"ird"); + assertEquals("\"s\".\"we\"\"ird\"", name); + } + + @Test + void spiProcessorIsConditionalForCompletionConsumers() { + KingBaseSQLIdentifierProcessor processor = KingBaseSQLIdentifierProcessor.INSTANCE; + assertEquals("plain", processor.quoteIdentifier("plain")); + assertEquals("\"UPPER\"", processor.quoteIdentifier("UPPER")); + assertEquals("\"select\"", processor.quoteIdentifier("select")); + assertEquals("\"we\"\"name\"", processor.quoteIdentifier("we\"name")); + assertEquals("plain", processor.removeIdentifierQuote("\"plain\"")); + assertFalse(processor.isQuoteIdentifier("plain")); + assertTrue(processor.isQuoteIdentifier("\"plain\"")); + } + + @Test + void defaultAndTypeGuardsPreserveLegalSyntaxAndRejectDdlReshape() { + for (String value : List.of("now()", "nextval('audit.event_id_seq'::regclass)", + "timezone('UTC'::text, now())", "ARRAY[]::integer[]", + "$tag$comma, -- and ; stay literal$tag$")) { + assertEquals(value, KingBaseSqlGuards.requireDefaultExpression(value)); + } + assertEquals("numeric(10,2)", KingBaseSqlGuards.requireColumnTypeExpression("numeric(10,2)")); + assertEquals("\"Tenant\".\"InvoiceType\"[]", + KingBaseSqlGuards.requireColumnTypeExpression("\"Tenant\".\"InvoiceType\"[]")); + for (String value : List.of("1, injected integer", "0 NOT NULL", "0 CHECK (false)", + "now()); DROP TABLE x", "'a'; DROP TABLE x--")) { + assertThrows(IllegalArgumentException.class, + () -> KingBaseSqlGuards.requireDefaultExpression(value), value); + } + for (String value : List.of("text, injected integer", "text DEFAULT 0", "text); DROP TABLE t;--")) { + assertThrows(IllegalArgumentException.class, + () -> KingBaseSqlGuards.requireColumnTypeExpression(value), value); + } + } + + @Test + void fallbackColumnTypesAreValidatedInsteadOfDropped() { + TableColumn column = new TableColumn(); + column.setName("state\"value"); + column.setColumnType("public.invoice_state"); + column.setNullable(0); + column.setDefaultValue("'OPEN'::public.invoice_state"); + assertEquals("\"state\"\"value\" public.invoice_state NOT NULL DEFAULT 'OPEN'::public.invoice_state", + KingBaseColumnTypeEnum.buildCreateColumnSqlSafely(column)); + + column.setColumnType("text DEFAULT 0"); + assertThrows(IllegalArgumentException.class, + () -> KingBaseColumnTypeEnum.buildCreateColumnSqlSafely(column)); + } + + @Test + void inheritedBuilderPathsQuoteKingBaseIdentifiersAndIgnoreDatabaseQualifier() { + KingBaseSqlBuilder builder = new KingBaseSqlBuilder(); + assertEquals("SELECT COUNT(1) FROM \"sales\"\"x\".\"orders\"\"x\"", + builder.buildSelectCount("ignored_database", "sales\"x", "orders\"x")); + assertEquals("DROP TABLE \"sales\"\"x\".\"orders\"\"x\"", + builder.buildDropTable(new DropTableRequest("ignored_database", "sales\"x", "orders\"x"))); + + UpdateSqlRequest update = UpdateSqlRequest.builder() + .databaseName("ignored_database") + .schemaName("sales\"schema") + .tableName("orders\"table") + .row(Map.of("total\"value", "42")) + .primaryKeyMap(Map.of("order\"id", "7")) + .build(); + assertEquals("UPDATE \"sales\"\"schema\".\"orders\"\"table\" SET \"total\"\"value\" = 42" + + " WHERE \"order\"\"id\" = 7", + builder.buildUpdate(update)); + } + + @Test + void createTableAndCaseOnlyRenameUseQualifiedNames() { + Table table = new Table(); + table.setSchemaName("sales\"schema"); + table.setName("orders\"table"); + table.setColumnList(List.of(TableColumn.builder().name("id\"value").columnType("INTEGER").build())); + table.setIndexList(List.of()); + TableBuilderConfig config = TableBuilderConfig.defaultConfig(); + config.setNeedFullTableName(true); + String createSql = new KingBaseSqlBuilder().buildCreateTable(table, config); + assertTrue(createSql.startsWith("CREATE TABLE \"sales\"\"schema\".\"orders\"\"table\""), createSql); + + Table renamed = Table.builder().schemaName("sales").name("Orders") + .columnList(List.of()).indexList(List.of()).build(); + Table original = Table.builder().schemaName("sales").name("orders") + .columnList(List.of()).indexList(List.of()).build(); + assertEquals("ALTER TABLE \"sales\".\"orders\"\tRENAME TO \"Orders\";\n", + new KingBaseSqlBuilder().buildAlterTable(original, renamed)); + } +}