From 48d3fe1ae1a6858a97cad789424bf24c86925ce1 Mon Sep 17 00:00:00 2001 From: HandSonic <8078023+handsonic@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:54:41 +0800 Subject: [PATCH 1/6] fix(mysql): escape SQL identifiers and literals in metadata/DDL paths (#1914) --- .../chat2db/plugin/mysql/MysqlDBManager.java | 14 +- .../chat2db/plugin/mysql/MysqlMetaData.java | 24 +- .../plugin/mysql/MysqlRoutineManager.java | 6 +- .../chat2db/plugin/mysql/MysqlSqlEscapes.java | 186 ++++++++++++++ .../plugin/mysql/builder/MysqlSqlBuilder.java | 50 ++-- .../mysql/constant/MysqlSqlConstants.java | 2 +- .../mysql/enums/type/MysqlColumnTypeEnum.java | 40 +-- .../mysql/enums/type/MysqlIndexTypeEnum.java | 12 +- .../value/template/MysqlDmlValueTemplate.java | 6 +- .../plugin/mysql/MysqlSqlEscapesTest.java | 229 ++++++++++++++++++ 10 files changed, 488 insertions(+), 81 deletions(-) create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java index b18dea8d04..93fe573754 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java @@ -66,12 +66,12 @@ private void exportFunctions(Connection connection, String databaseName, AsyncCo } private void exportFunction(Connection connection, String functionName, AsyncContext asyncContext) throws SQLException { - String sql = String.format(SQL_SHOW_CREATE_FUNCTION_TEMPLATE, functionName); + String sql = String.format(SQL_SHOW_CREATE_FUNCTION_TEMPLATE, format(functionName)); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { asyncContext.write(String.format(FUNCTION_TITLE, functionName)); StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append(SQLConstants.DROP_FUNCTION_IF_EXISTS_SQL_PREFIX).append(functionName).append(SQLConstants.SEMICOLON).append(SQLConstants.LINE_SEPARATOR); + sqlBuilder.append(SQLConstants.DROP_FUNCTION_IF_EXISTS_SQL_PREFIX).append(format(functionName)).append(SQLConstants.SEMICOLON).append(SQLConstants.LINE_SEPARATOR); sqlBuilder.append(DELIMITER_BLOCK_START).append(SQLConstants.LINE_SEPARATOR).append(resultSet.getString(CREATE_FUNCTION_COLUMN)) .append(ROUTINE_DELIMITER) @@ -95,7 +95,7 @@ private void exportTables(Connection connection, String databaseName, String sch public void exportTable(Connection connection, String databaseName, String schemaName, String tableName, AsyncContext asyncContext) throws SQLException { - String sql = String.format(SQL_SHOW_CREATE_TABLE_TEMPLATE, tableName); + String sql = String.format(SQL_SHOW_CREATE_TABLE_TEMPLATE, format(tableName)); asyncContext.info(DateUtil.formatDateTime(new Date()) + EXPORTING_TABLE_MESSAGE + tableName); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { @@ -130,7 +130,7 @@ private void exportViews(Connection connection, String databaseName, AsyncContex } private void exportView(Connection connection, String viewName, AsyncContext asyncContext) throws SQLException { - String sql = String.format(SQL_SHOW_CREATE_VIEW_TEMPLATE, viewName); + String sql = String.format(SQL_SHOW_CREATE_VIEW_TEMPLATE, format(viewName)); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { asyncContext.write(String.format(VIEW_TITLE, viewName)); @@ -152,7 +152,7 @@ private void exportProcedures(Connection connection, AsyncContext asyncContext) } private void exportProcedure(Connection connection, String procedureName, AsyncContext asyncContext) throws SQLException { - String sql = String.format(SQL_SHOW_CREATE_PROCEDURE_TEMPLATE, procedureName); + String sql = String.format(SQL_SHOW_CREATE_PROCEDURE_TEMPLATE, format(procedureName)); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { asyncContext.write(String.format(PROCEDURE_TITLE, procedureName)); @@ -177,7 +177,7 @@ private void exportTriggers(Connection connection, AsyncContext asyncContext) th } private void exportTrigger(Connection connection, String triggerName, AsyncContext asyncContext) throws SQLException { - String sql = String.format(SQL_SHOW_CREATE_TRIGGER_TEMPLATE, triggerName); + String sql = String.format(SQL_SHOW_CREATE_TRIGGER_TEMPLATE, format(triggerName)); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { asyncContext.write(String.format(TRIGGER_TITLE, triggerName)); @@ -241,7 +241,7 @@ public String dropTable(Connection connection, String databaseName, String schem } public static String format(String tableName) { - return SQLConstants.BACK_QUOTE + tableName + SQLConstants.BACK_QUOTE; + return MysqlSqlEscapes.quoteIdentifier(tableName); } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java index bf1cbceab6..e510179aa7 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java @@ -63,9 +63,9 @@ public List databases(Connection connection) { @Override public List tables(Connection connection, @NotEmpty String databaseName, String schemaName, String tableName) { - String sql = String.format(TABLES_SQL, databaseName); + String sql = String.format(TABLES_SQL, MysqlSqlEscapes.escapeSqlLiteral(databaseName)); if (StringUtils.isNotBlank(tableName)) { - sql += SQL_TABLE_NAME_EQUALS_FILTER + tableName + SQL_SINGLE_QUOTE; + sql += SQL_TABLE_NAME_EQUALS_FILTER + MysqlSqlEscapes.escapeSqlLiteral(tableName) + SQL_SINGLE_QUOTE; } Map collationMap = getCollationMap(connection); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { @@ -131,14 +131,14 @@ public String tableDDL(Connection connection, @NotEmpty String databaseName, Str } public static String format(String tableName) { - return SQL_METADATA_QUOTE + tableName + SQL_METADATA_QUOTE; + return MysqlSqlEscapes.quoteIdentifier(tableName); } @Override public Function function(Connection connection, @NotEmpty String databaseName, String schemaName, String functionName) { - String functionInfoSql = String.format(ROUTINES_SQL, FUNCTION, databaseName, functionName); + String functionInfoSql = String.format(ROUTINES_SQL, FUNCTION, MysqlSqlEscapes.escapeSqlLiteral(databaseName), MysqlSqlEscapes.escapeSqlLiteral(functionName)); Function function = DefaultSQLExecutor.getInstance().execute(connection, functionInfoSql, resultSet -> { Function f = new Function(); f.setDatabaseName(databaseName); @@ -162,7 +162,7 @@ public Function function(Connection connection, @NotEmpty String databaseName, S @Override public List triggers(Connection connection, String databaseName, String schemaName) { List triggers = new ArrayList<>(); - String sql = String.format(TRIGGER_SQL_LIST, databaseName); + String sql = String.format(TRIGGER_SQL_LIST, MysqlSqlEscapes.escapeSqlLiteral(databaseName)); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { while (resultSet.next()) { Trigger trigger = new Trigger(); @@ -208,7 +208,7 @@ public List procedures(Connection connection, String databaseName, St @Override public Procedure procedure(Connection connection, @NotEmpty String databaseName, String schemaName, String procedureName) { - String routinesSql = String.format(ROUTINES_SQL, PROCEDURE, databaseName, procedureName); + String routinesSql = String.format(ROUTINES_SQL, PROCEDURE, MysqlSqlEscapes.escapeSqlLiteral(databaseName), MysqlSqlEscapes.escapeSqlLiteral(procedureName)); String showCreateProcedureSql = SQL_SHOW_CREATE_PROCEDURE + mysqlQualifiedName(databaseName, procedureName); Procedure procedure = DefaultSQLExecutor.getInstance().execute(connection, routinesSql, resultSet -> { Procedure p = new Procedure(); @@ -230,7 +230,7 @@ public Procedure procedure(Connection connection, @NotEmpty String databaseName, } @Override public List columns(Connection connection, String databaseName, String schemaName, String tableName) { - String sql = String.format(SELECT_TABLE_COLUMNS, databaseName, tableName); + String sql = String.format(SELECT_TABLE_COLUMNS, MysqlSqlEscapes.escapeSqlLiteral(databaseName), MysqlSqlEscapes.escapeSqlLiteral(tableName)); List tableColumns = new ArrayList<>(); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { while (resultSet.next()) { @@ -285,7 +285,7 @@ private void setColumnSize(TableColumn column, String columnType) { @Override public Table view(Connection connection, String databaseName, String schemaName, String viewName) { - String quoteViewName = MYSQL_IDENTIFIER_PROCESSOR.quoteIdentifier(viewName); + String quoteViewName = MysqlSqlEscapes.quoteIdentifier(viewName); String sql = String.format(VIEW_DDL_SQL, quoteViewName); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { Table table = new Table(); @@ -303,9 +303,9 @@ public Table view(Connection connection, String databaseName, String schemaName, @Override public List indexes(Connection connection, String databaseName, String schemaName, String tableName) { StringBuilder queryBuf = new StringBuilder(SQL_SHOW_INDEX_FROM); - queryBuf.append(SQL_METADATA_QUOTE).append(tableName).append(SQL_METADATA_QUOTE); + queryBuf.append(MysqlSqlEscapes.quoteIdentifier(tableName)); queryBuf.append(SQL_FROM); - queryBuf.append(SQL_METADATA_QUOTE).append(databaseName).append(SQL_METADATA_QUOTE); + queryBuf.append(MysqlSqlEscapes.quoteIdentifier(databaseName)); return DefaultSQLExecutor.getInstance().execute(connection, queryBuf.toString(), resultSet -> { LinkedHashMap map = new LinkedHashMap(); while (resultSet.next()) { @@ -461,7 +461,7 @@ private List queryTableMetaOptions(String sql, IResultSetFunction @Override public String getMetaDataName(String... names) { return Arrays.stream(names).filter(StringUtils::isNotBlank) - .map(name -> SQL_METADATA_QUOTE + name + SQL_METADATA_QUOTE) + .map(MysqlSqlEscapes::quoteIdentifier) .collect(Collectors.joining(SQL_DOT)); } @@ -496,7 +496,7 @@ public ModifyViewConfiguration viewMeta(String databaseName, String schemaName) StringBuilder sqlBuilder = new StringBuilder(100); sqlBuilder.append(SQL_CREATE).append(SQL_VIEW_KEYWORD); if (StringUtils.isNotBlank(databaseName)) { - sqlBuilder.append(SQL_METADATA_QUOTE).append(databaseName).append(SQL_METADATA_QUOTE).append(SQL_DOT); + sqlBuilder.append(MysqlSqlEscapes.quoteIdentifier(databaseName)).append(SQL_DOT); } sqlBuilder.append(SQL_METADATA_QUOTE).append(SQL_UNDEFINED).append(SQL_METADATA_QUOTE); sqlBuilder.append(SQL_AS).append(sql).append(SQL_SEMICOLON); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java index 091c0d9d34..fd47e71266 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java @@ -362,11 +362,7 @@ private String mysqlQualifiedName(String databaseName, String routineName) { } private String quoteMysqlIdentifier(String name) { - String trimmed = StringUtils.trimToEmpty(name); - if (trimmed.matches("^`(?:``|[^`])+`$")) { - return trimmed; - } - return "`" + trimmed.replace("`", "``") + "`"; + return MysqlSqlEscapes.quoteIdentifier(name); } private String routineInvocationName(String name) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java new file mode 100644 index 0000000000..0cde1988aa --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java @@ -0,0 +1,186 @@ +package ai.chat2db.plugin.mysql; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Canonical escaping/quoting helpers for values interpolated into MySQL SQL text (#1914). + * Literal escaping mirrors MysqlAccountSqlBuilder.stringLiteral (backslash first, then single quotes). + */ +public final class MysqlSqlEscapes { + + private static final Pattern MYSQL_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); + private static final Pattern NUMERIC_DEFAULT_PATTERN = Pattern.compile( + "^([+-]?(\\d+(\\.\\d+)?|\\.\\d+)([eE][+-]?\\d+)?|0[xX][0-9a-fA-F]+|[xX]'[0-9a-fA-F]*'|[bB]'[01]*'|(?i:TRUE|FALSE))$"); + private static final Pattern BIT_LITERAL_PATTERN = Pattern.compile("^[01]+$"); + private static final String DEFINER_QUOTED_PART = "'([^'\\\\]|\\\\[\\s\\S])*'|`[^`]+`"; + private static final Pattern DEFINER_PATTERN = Pattern.compile( + "^([A-Za-z0-9_$]+|" + DEFINER_QUOTED_PART + ")@([A-Za-z0-9_.%:$-]+|" + DEFINER_QUOTED_PART + ")$"); + + private MysqlSqlEscapes() { + } + + /** + * Escape a value interpolated into a single-quoted SQL string literal (surrounding quotes NOT added). + * MySQL treats backslash as an escape character, so backslashes are doubled before single quotes. + */ + public static String escapeSqlLiteral(String value) { + if (value == null) { + return null; + } + return value.replace("\\", "\\\\").replace("'", "''"); + } + + /** + * Quote an identifier with backticks: strips one surrounding backtick pair, then doubles every + * embedded backtick. + */ + public static String quoteIdentifier(String name) { + if (StringUtils.isBlank(name)) { + return name; + } + String identifier = name; + if (identifier.length() >= 2 && identifier.startsWith("`") && identifier.endsWith("`")) { + identifier = identifier.substring(1, identifier.length() - 1); + } + return "`" + identifier.replace("`", "``") + "`"; + } + + /** + * Validate a strict MySQL name token (ENGINE / CHARACTER SET / COLLATE style positions where + * escaping is impossible by design). + */ + public static String requireMysqlName(String value, String what) { + if (value == null || !MYSQL_NAME_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("Invalid MySQL " + what + ": " + value); + } + return value; + } + + /** + * Validate a raw DEFAULT literal for numeric-ish columns (positions where quoting would change + * semantics). Accepts decimal/scientific numbers, hex and bit literals, TRUE/FALSE. + */ + public static String requireNumericDefault(String value) { + if (value == null || !NUMERIC_DEFAULT_PATTERN.matcher(value.trim()).matches()) { + throw new IllegalArgumentException("Invalid MySQL default value: " + value); + } + return value; + } + + /** + * Validate content of a b'...' bit literal. + */ + public static String requireBitLiteral(String value) { + if (value == null || !BIT_LITERAL_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("Invalid MySQL bit literal: " + value); + } + return value; + } + + /** + * Validate a DEFINER value (user@host, parts optionally single-quoted or backtick-quoted). + */ + public static String requireDefiner(String value) { + if (value == null || !DEFINER_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("Invalid MySQL definer: " + value); + } + return value; + } + + /** + * Validate an index sort direction: only ASC/DESC are legal, returned in canonical uppercase. + */ + public static String requireAscOrDesc(String value) { + String trimmed = StringUtils.trimToEmpty(value); + if ("ASC".equalsIgnoreCase(trimmed)) { + return "ASC"; + } + if ("DESC".equalsIgnoreCase(trimmed)) { + return "DESC"; + } + throw new IllegalArgumentException("Invalid MySQL index sort direction: " + value); + } + + /** + * Validate an option that must be one of the given enum constants (e.g. view algorithm / + * sql security / check option). Returns the canonical enum name. + */ + public static > String requireEnumConstant(String value, E[] constants, String what) { + for (E constant : constants) { + if (constant.name().equalsIgnoreCase(StringUtils.trimToEmpty(value))) { + return constant.name(); + } + } + throw new IllegalArgumentException("Invalid MySQL " + what + ": " + value); + } + + /** + * Re-escape a comma-separated ENUM/SET value list: each item is stripped of its surrounding + * single quotes (if any), escaped as a SQL string literal and re-quoted. A surrounding pair of + * parentheses is preserved. + */ + public static String quoteEnumValues(String raw) { + if (raw == null) { + return null; + } + String trimmed = raw.trim(); + if (trimmed.isEmpty()) { + return trimmed; + } + boolean parenthesized = trimmed.length() >= 2 && trimmed.startsWith("(") && trimmed.endsWith(")"); + String inner = parenthesized ? trimmed.substring(1, trimmed.length() - 1) : trimmed; + List items = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inQuote = false; + for (int i = 0; i < inner.length(); i++) { + char c = inner.charAt(i); + if (inQuote) { + current.append(c); + if (c == '\\' && i + 1 < inner.length()) { + current.append(inner.charAt(++i)); + } else if (c == '\'') { + if (i + 1 < inner.length() && inner.charAt(i + 1) == '\'') { + current.append('\''); + i++; + } else { + inQuote = false; + } + } + } else if (c == '\'') { + inQuote = true; + current.append(c); + } else if (c == ',') { + items.add(current.toString()); + current.setLength(0); + } else { + current.append(c); + } + } + items.add(current.toString()); + StringBuilder result = new StringBuilder(); + if (parenthesized) { + result.append('('); + } + for (int i = 0; i < items.size(); i++) { + if (i > 0) { + result.append(','); + } + result.append('\'').append(escapeSqlLiteral(unquoteSingle(items.get(i).trim()))).append('\''); + } + if (parenthesized) { + result.append(')'); + } + return result.toString(); + } + + private static String unquoteSingle(String item) { + if (item.length() >= 2 && item.startsWith("'") && item.endsWith("'")) { + return item.substring(1, item.length() - 1); + } + return item; + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java index 4925b607cd..9995b511f0 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java @@ -1,5 +1,9 @@ package ai.chat2db.plugin.mysql.builder; +import ai.chat2db.plugin.mysql.MysqlSqlEscapes; +import ai.chat2db.plugin.mysql.enums.MysqlViewAlgorithmOptionEnum; +import ai.chat2db.plugin.mysql.enums.MysqlViewCheckOptionEnum; +import ai.chat2db.plugin.mysql.enums.MysqlViewSqlSecurityOptionEnum; import ai.chat2db.plugin.mysql.enums.type.MysqlColumnTypeEnum; import ai.chat2db.plugin.mysql.enums.type.MysqlIndexTypeEnum; import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum; @@ -105,15 +109,15 @@ public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfi if (StringUtils.isNotBlank(table.getEngine())) { - script.append(SQLConstants.ENGINE_SQL).append(table.getEngine()); + script.append(SQLConstants.ENGINE_SQL).append(MysqlSqlEscapes.requireMysqlName(table.getEngine(), "engine")); } if (StringUtils.isNotBlank(table.getCharset())) { - script.append(SQLConstants.DEFAULT_CHARACTER_SET_SQL).append(table.getCharset()); + script.append(SQLConstants.DEFAULT_CHARACTER_SET_SQL).append(MysqlSqlEscapes.requireMysqlName(table.getCharset(), "charset")); } if (StringUtils.isNotBlank(table.getCollate())) { - script.append(SQLConstants.COLLATE_SQL).append(table.getCollate()); + script.append(SQLConstants.COLLATE_SQL).append(MysqlSqlEscapes.requireMysqlName(table.getCollate(), "collation")); } if (table.getIncrementValue() != null) { @@ -121,7 +125,7 @@ public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfi } if (StringUtils.isNotBlank(table.getComment())) { - script.append(SQL_COMMENT_WITH_SINGLE_QUOTE).append(table.getComment()).append(SQLConstants.SINGLE_QUOTE); + script.append(SQL_COMMENT_WITH_SINGLE_QUOTE).append(MysqlSqlEscapes.escapeSqlLiteral(table.getComment())).append(SQLConstants.SINGLE_QUOTE); } if (StringUtils.isNotBlank(table.getPartition())) { @@ -152,17 +156,17 @@ public String buildAlterTable(Table oldTable, Table newTable) { script.append(SQLConstants.TAB).append(SQL_RENAME).append(quoteMysqlIdentifier(newTable.getName())).append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!StringUtils.equalsIgnoreCase(oldTable.getComment(), newTable.getComment())) { - script.append(SQLConstants.TAB).append(SQL_COMMENT).append(SQLConstants.SINGLE_QUOTE).append(newTable.getComment()).append(SQLConstants.SINGLE_QUOTE) + script.append(SQLConstants.TAB).append(SQL_COMMENT).append(SQLConstants.SINGLE_QUOTE).append(MysqlSqlEscapes.escapeSqlLiteral(newTable.getComment())).append(SQLConstants.SINGLE_QUOTE) .append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!StringUtils.equalsIgnoreCase(oldTable.getEngine(), newTable.getEngine()) && StringUtils.isNotBlank(newTable.getEngine())) { - script.append(SQLConstants.TAB).append(SQL_ENGINE_ASSIGNMENT).append(newTable.getEngine()).append(SQLConstants.COMMA_LINE_SEPARATOR); + script.append(SQLConstants.TAB).append(SQL_ENGINE_ASSIGNMENT).append(MysqlSqlEscapes.requireMysqlName(newTable.getEngine(), "engine")).append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!StringUtils.equalsIgnoreCase(oldTable.getCharset(), newTable.getCharset()) && StringUtils.isNotBlank(newTable.getCharset())) { - script.append(SQLConstants.TAB).append(SQL_DEFAULT_CHARACTER_SET_ASSIGNMENT).append(newTable.getCharset()).append(SQLConstants.COMMA_LINE_SEPARATOR); + script.append(SQLConstants.TAB).append(SQL_DEFAULT_CHARACTER_SET_ASSIGNMENT).append(MysqlSqlEscapes.requireMysqlName(newTable.getCharset(), "charset")).append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!StringUtils.equalsIgnoreCase(oldTable.getCollate(), newTable.getCollate()) && StringUtils.isNotBlank(newTable.getCollate())) { - script.append(SQLConstants.TAB).append(SQL_COLLATE_ASSIGNMENT).append(newTable.getCollate()).append(SQLConstants.COMMA_LINE_SEPARATOR); + script.append(SQLConstants.TAB).append(SQL_COLLATE_ASSIGNMENT).append(MysqlSqlEscapes.requireMysqlName(newTable.getCollate(), "collation")).append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!Objects.equals(oldTable.getIncrementValue(), newTable.getIncrementValue())) { script.append(SQLConstants.TAB).append(SQL_AUTO_INCREMENT_ASSIGNMENT).append(newTable.getIncrementValue()).append(SQLConstants.COMMA_LINE_SEPARATOR); @@ -256,10 +260,10 @@ public String buildCreateDatabase(Database database) { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append(SQLConstants.CREATE_DATABASE_SQL_PREFIX).append(quoteMysqlIdentifier(database.getName())); if (StringUtils.isNotBlank(database.getCharset())) { - sqlBuilder.append(SQLConstants.DEFAULT_CHARACTER_SET_SQL).append(database.getCharset()); + sqlBuilder.append(SQLConstants.DEFAULT_CHARACTER_SET_SQL).append(MysqlSqlEscapes.requireMysqlName(database.getCharset(), "charset")); } if (StringUtils.isNotBlank(database.getCollation())) { - sqlBuilder.append(SQLConstants.COLLATE_SQL).append(database.getCollation()); + sqlBuilder.append(SQLConstants.COLLATE_SQL).append(MysqlSqlEscapes.requireMysqlName(database.getCollation(), "collation")); } return sqlBuilder.toString(); } @@ -362,7 +366,7 @@ private String[] buildSql(String[] originalArray, String[] targetArray, StringBu MysqlColumnTypeEnum typeEnum = MysqlColumnTypeEnum.getByType(column.getColumnType()); sql.append(typeEnum.buildColumn(column)); sql.append(SQL_AFTER); - sql.append(oldTable.getColumnList().get(max).getName()); + sql.append(quoteMysqlIdentifier(oldTable.getColumnList().get(max).getName())); sql.append(SQLConstants.SEMICOLON_LINE_SEPARATOR); n++; if (Arrays.equals(newArray, targetArray)) { @@ -394,9 +398,9 @@ private String[] buildSql(String[] originalArray, String[] targetArray, StringBu AtomicInteger continuousDataCount = new AtomicInteger(0); String[] newArray = moveElement(originalArray, i, a, targetArray, continuousDataCount); if (i < a) { - sql.append(originalArray[a + continuousDataCount.get()]); + sql.append(quoteMysqlIdentifier(originalArray[a + continuousDataCount.get()])); } else { - sql.append(originalArray[a - 1]); + sql.append(quoteMysqlIdentifier(originalArray[a - 1])); } sql.append(SQLConstants.SEMICOLON_LINE_SEPARATOR); @@ -487,15 +491,15 @@ public String buildCreateView(ModifyView modifyView) { } String algorithm = modifyView.getAlgorithm(); if (StringUtils.isNotBlank(algorithm)) { - createViewSqlBuilder.append(SQL_ALGORITHM).append(algorithm).append(SQLConstants.SPACE); + createViewSqlBuilder.append(SQL_ALGORITHM).append(MysqlSqlEscapes.requireEnumConstant(algorithm, MysqlViewAlgorithmOptionEnum.values(), "algorithm")).append(SQLConstants.SPACE); } String definer = modifyView.getDefiner(); if (StringUtils.isNotBlank(definer)) { - createViewSqlBuilder.append(SQL_DEFINER).append(definer).append(SQLConstants.SPACE); + createViewSqlBuilder.append(SQL_DEFINER).append(MysqlSqlEscapes.requireDefiner(definer)).append(SQLConstants.SPACE); } String security = modifyView.getSecurity(); if (StringUtils.isNotBlank(security)) { - createViewSqlBuilder.append(SQL_SECURITY).append(security).append(SQLConstants.SPACE); + createViewSqlBuilder.append(SQL_SECURITY).append(MysqlSqlEscapes.requireEnumConstant(security, MysqlViewSqlSecurityOptionEnum.values(), "security")).append(SQLConstants.SPACE); } createViewSqlBuilder.append(SQLConstants.VIEW_KEYWORD); String databaseName = modifyView.getDatabaseName(); @@ -519,24 +523,14 @@ public String buildCreateView(ModifyView modifyView) { } String checkOption = modifyView.getCheckOption(); if (StringUtils.isNotBlank(checkOption)) { - createViewSqlBuilder.append(SQLConstants.LINE_SEPARATOR_SQL_WITH).append(checkOption).append(SQLConstants.CHECK_OPTION_SQL); + createViewSqlBuilder.append(SQLConstants.LINE_SEPARATOR_SQL_WITH).append(MysqlSqlEscapes.requireEnumConstant(checkOption, MysqlViewCheckOptionEnum.values(), "check option")).append(SQLConstants.CHECK_OPTION_SQL); } return createViewSqlBuilder + SQLConstants.SEMICOLON; } private static String quoteMysqlIdentifier(String name) { - if (StringUtils.isBlank(name)) { - return name; - } - String identifier = name; - if (identifier.length() >= 2 && identifier.startsWith(SQLConstants.BACK_QUOTE) - && identifier.endsWith(SQLConstants.BACK_QUOTE)) { - identifier = identifier.substring(1, identifier.length() - 1); - } - return SQLConstants.BACK_QUOTE - + identifier.replace(SQLConstants.BACK_QUOTE, SQLConstants.BACK_QUOTE + SQLConstants.BACK_QUOTE) - + SQLConstants.BACK_QUOTE; + return MysqlSqlEscapes.quoteIdentifier(name); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java index a515371835..402252db82 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java @@ -32,7 +32,7 @@ public final class MysqlSqlConstants { public static final String SQL_DROP_PROCEDURE_TEMPLATE = "DROP PROCEDURE %s"; public static final String SQL_DROP_TABLE_TEMPLATE = "DROP TABLE %s"; public static final String SQL_DROP_USER = "DROP USER "; - public static final String SQL_DROP_VIEW_TEMPLATE = "DROP VIEW %s%s"; + public static final String SQL_DROP_VIEW_TEMPLATE = "DROP VIEW %s.%s"; public static final String SQL_ENGINE_ASSIGNMENT = "ENGINE="; public static final String SQL_FIRST_TERMINATOR = " FIRST;\n"; public static final String SQL_FROM = " FROM "; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java index fa9f3efb9e..1c14ee233f 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java @@ -1,5 +1,6 @@ package ai.chat2db.plugin.mysql.enums.type; +import ai.chat2db.plugin.mysql.MysqlSqlEscapes; import ai.chat2db.spi.IColumnBuilder; import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum; import ai.chat2db.community.domain.api.model.metadata.ColumnType; @@ -15,7 +16,6 @@ import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_COMMENT_KEYWORD; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_COMMENT_SPACE_SINGLE_QUOTE; -import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_COLUMN_BACK_QUOTE; import static ai.chat2db.plugin.mysql.constant.MysqlColumnTypeEnumConstants.*; @Getter @@ -150,7 +150,7 @@ public String buildCreateColumnSql(TableColumn column) { } StringBuilder script = new StringBuilder(); - script.append("`").append(column.getName()).append("`").append(" "); + script.append(MysqlSqlEscapes.quoteIdentifier(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); @@ -181,7 +181,7 @@ public String buildAICreateColumnSql(TableColumn column) { } StringBuilder script = new StringBuilder(); - script.append("`").append(column.getName()).append("`").append(" "); + script.append(MysqlSqlEscapes.quoteIdentifier(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); @@ -209,28 +209,28 @@ private String buildCharset(TableColumn column, MysqlColumnTypeEnum type) { if (!type.getColumnType().isSupportCharset() || StringUtils.isEmpty(column.getCharSetName())) { return ""; } - return StringUtils.join("CHARACTER SET ", column.getCharSetName()); + return StringUtils.join("CHARACTER SET ", MysqlSqlEscapes.requireMysqlName(column.getCharSetName(), "charset")); } private String buildCollation(TableColumn column, MysqlColumnTypeEnum type) { if (!type.getColumnType().isSupportCollation() || StringUtils.isEmpty(column.getCollationName())) { return ""; } - return StringUtils.join("COLLATE ", column.getCollationName()); + return StringUtils.join("COLLATE ", MysqlSqlEscapes.requireMysqlName(column.getCollationName(), "collation")); } @Override public String buildModifyColumn(TableColumn tableColumn) { if (EditStatusEnum.DELETE.name().equals(tableColumn.getEditStatus())) { - return StringUtils.join(SQL_DROP_COLUMN_BACK_QUOTE, tableColumn.getName() + "`"); + return StringUtils.join("DROP COLUMN ", MysqlSqlEscapes.quoteIdentifier(tableColumn.getName())); } if (EditStatusEnum.ADD.name().equals(tableColumn.getEditStatus())) { return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn)); } if (EditStatusEnum.MODIFY.name().equals(tableColumn.getEditStatus())) { if (!StringUtils.equalsIgnoreCase(tableColumn.getOldName(), tableColumn.getName())) { - return StringUtils.join("CHANGE COLUMN `", tableColumn.getOldName(), "` ", buildCreateColumnSql(tableColumn)); + return StringUtils.join("CHANGE COLUMN ", MysqlSqlEscapes.quoteIdentifier(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); } else { return StringUtils.join("MODIFY COLUMN ", buildCreateColumnSql(tableColumn)); } @@ -240,14 +240,14 @@ public String buildModifyColumn(TableColumn tableColumn) { public String buildModifyColumn(TableColumn tableColumn, boolean isMove, String columnName) { if (EditStatusEnum.DELETE.name().equals(tableColumn.getEditStatus())) { - return StringUtils.join(SQL_DROP_COLUMN_BACK_QUOTE, tableColumn.getName() + "`"); + return StringUtils.join("DROP COLUMN ", MysqlSqlEscapes.quoteIdentifier(tableColumn.getName())); } if (EditStatusEnum.ADD.name().equals(tableColumn.getEditStatus())) { if (isMove) { if (columnName.equals("-1")) { return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn), " FIRST"); } else { - return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn), " AFTER ", "`"+columnName+"`"); + return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn), " AFTER ", MysqlSqlEscapes.quoteIdentifier(columnName)); } } return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn)); @@ -255,7 +255,7 @@ public String buildModifyColumn(TableColumn tableColumn, boolean isMove, String if (EditStatusEnum.MODIFY.name().equals(tableColumn.getEditStatus())) { String sql; if (!StringUtils.equalsIgnoreCase(tableColumn.getOldName(), tableColumn.getName())) { - sql = StringUtils.join("CHANGE COLUMN `", tableColumn.getOldName(), "` ", buildCreateColumnSql(tableColumn)); + sql = StringUtils.join("CHANGE COLUMN ", MysqlSqlEscapes.quoteIdentifier(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); } else { sql = StringUtils.join("MODIFY COLUMN ", buildCreateColumnSql(tableColumn)); } @@ -275,7 +275,7 @@ private String appendColumnPosition(String sql, boolean isMove, String columnNam if (columnName.equals("-1")) { return StringUtils.join(sql, " FIRST"); } - return StringUtils.join(sql, " AFTER ", "`" + columnName + "`"); + return StringUtils.join(sql, " AFTER ", MysqlSqlEscapes.quoteIdentifier(columnName)); } private String buildAutoIncrement(TableColumn column, MysqlColumnTypeEnum type) { @@ -292,14 +292,14 @@ private String buildComment(TableColumn column, MysqlColumnTypeEnum type) { if (!type.columnType.isSupportComments() || StringUtils.isEmpty(column.getComment())) { return ""; } - return StringUtils.join(SQL_COMMENT_SPACE_SINGLE_QUOTE, column.getComment(), "'"); + return StringUtils.join(SQL_COMMENT_SPACE_SINGLE_QUOTE, MysqlSqlEscapes.escapeSqlLiteral(column.getComment()), "'"); } private String buildExt(TableColumn column, MysqlColumnTypeEnum type) { if (!type.columnType.isSupportExtent() || StringUtils.isEmpty(column.getExtent())) { return ""; } - return column.getExtent(); + return MysqlSqlEscapes.quoteEnumValues(column.getExtent()); } private String buildDefaultValue(TableColumn column, MysqlColumnTypeEnum type) { @@ -316,11 +316,11 @@ private String buildDefaultValue(TableColumn column, MysqlColumnTypeEnum type) { } if (Arrays.asList(CHAR, VARCHAR, BINARY, VARBINARY, SET, ENUM).contains(type)) { - return StringUtils.join("DEFAULT '", column.getDefaultValue(), "'"); + return StringUtils.join("DEFAULT '", MysqlSqlEscapes.escapeSqlLiteral(column.getDefaultValue()), "'"); } if (Arrays.asList(DATE, TIME, YEAR).contains(type)) { - return StringUtils.join("DEFAULT '", column.getDefaultValue(), "'"); + return StringUtils.join("DEFAULT '", MysqlSqlEscapes.escapeSqlLiteral(column.getDefaultValue()), "'"); } if (Arrays.asList(DATETIME, TIMESTAMP).contains(type)) { @@ -328,10 +328,10 @@ private String buildDefaultValue(TableColumn column, MysqlColumnTypeEnum type) { return StringUtils.join("DEFAULT ", column.getDefaultValue()); } - return StringUtils.join("DEFAULT '", column.getDefaultValue(), "'"); + return StringUtils.join("DEFAULT '", MysqlSqlEscapes.escapeSqlLiteral(column.getDefaultValue()), "'"); } - return StringUtils.join("DEFAULT ", column.getDefaultValue()); + return StringUtils.join("DEFAULT ", MysqlSqlEscapes.requireNumericDefault(column.getDefaultValue())); } private String OnUpdateCurrentTimestamp(TableColumn column, MysqlColumnTypeEnum type) { @@ -404,7 +404,7 @@ private String buildDataType(TableColumn column, MysqlColumnTypeEnum type) { if (Arrays.asList(SET, ENUM).contains(type)) { if (!StringUtils.isEmpty(column.getValue())) { - return StringUtils.join(columnType, "(", column.getValue(), ")"); + return StringUtils.join(columnType, "(", MysqlSqlEscapes.quoteEnumValues(column.getValue()), ")"); } } @@ -418,10 +418,10 @@ public String buildColumn(TableColumn column) { } StringBuilder script = new StringBuilder(); - script.append("`").append(column.getName()).append("`").append(" "); + script.append(MysqlSqlEscapes.quoteIdentifier(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); if (StringUtils.isNoneBlank(column.getComment())) { - script.append(SQL_COMMENT_KEYWORD).append(" ").append("'").append(column.getComment()).append("'").append(" "); + script.append(SQL_COMMENT_KEYWORD).append(" ").append("'").append(MysqlSqlEscapes.escapeSqlLiteral(column.getComment())).append("'").append(" "); } return script.toString(); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java index 70049a7555..e47fe50652 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java @@ -1,5 +1,6 @@ package ai.chat2db.plugin.mysql.enums.type; +import ai.chat2db.plugin.mysql.MysqlSqlEscapes; import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum; import ai.chat2db.community.domain.api.model.metadata.IndexType; import ai.chat2db.community.domain.api.model.metadata.TableIndex; @@ -12,7 +13,6 @@ import java.util.Locale; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_COMMENT_SPACE_SINGLE_QUOTE; -import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_INDEX_BACK_QUOTE; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_PRIMARY_KEY; @Getter @@ -96,7 +96,7 @@ private String buildIndexComment(TableIndex tableIndex) { if(StringUtils.isBlank(tableIndex.getComment())){ return ""; }else { - return StringUtils.join(SQL_COMMENT_SPACE_SINGLE_QUOTE,tableIndex.getComment(),"'"); + return StringUtils.join(SQL_COMMENT_SPACE_SINGLE_QUOTE, MysqlSqlEscapes.escapeSqlLiteral(tableIndex.getComment()),"'"); } } @@ -106,9 +106,9 @@ private String buildIndexColumn(TableIndex tableIndex) { script.append("("); for (TableIndexColumn column : tableIndex.getColumnList()) { if(StringUtils.isNotBlank(column.getColumnName())) { - script.append("`").append(column.getColumnName()).append("`"); + script.append(MysqlSqlEscapes.quoteIdentifier(column.getColumnName())); if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) { - script.append(" ").append(column.getAscOrDesc()); + script.append(" ").append(MysqlSqlEscapes.requireAscOrDesc(column.getAscOrDesc())); } script.append(","); } @@ -122,7 +122,7 @@ private String buildIndexName(TableIndex tableIndex) { if(this.equals(PRIMARY_KEY)){ return ""; }else { - return "`"+tableIndex.getName()+"`"; + return MysqlSqlEscapes.quoteIdentifier(tableIndex.getName()); } } @@ -143,7 +143,7 @@ private String buildDropIndex(TableIndex tableIndex) { if (MysqlIndexTypeEnum.PRIMARY_KEY.getName().equals(tableIndex.getType())) { return StringUtils.join(SQL_DROP_PRIMARY_KEY); } - return StringUtils.join(SQL_DROP_INDEX_BACK_QUOTE, tableIndex.getOldName(),"`"); + return StringUtils.join("DROP INDEX ", MysqlSqlEscapes.quoteIdentifier(tableIndex.getOldName())); } public static List getIndexTypes() { return Arrays.asList(MysqlIndexTypeEnum.values()).stream().map(MysqlIndexTypeEnum::getIndexType).collect(java.util.stream.Collectors.toList()); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java index 654a86e48b..f1a27503ee 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java @@ -1,5 +1,7 @@ package ai.chat2db.plugin.mysql.value.template; +import ai.chat2db.plugin.mysql.MysqlSqlEscapes; + import static ai.chat2db.plugin.mysql.constant.MysqlDmlValueTemplateConstants.*; @@ -11,11 +13,11 @@ public class MysqlDmlValueTemplate { public static String wrapGeometry(String value) { - return String.format(GEOMETRY_TEMPLATE, value); + return String.format(GEOMETRY_TEMPLATE, MysqlSqlEscapes.escapeSqlLiteral(value)); } public static String wrapBit(String value) { - return String.format(BIT_TEMPLATE, value); + return String.format(BIT_TEMPLATE, MysqlSqlEscapes.requireBitLiteral(value)); } public static String wrapHex(String value) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java new file mode 100644 index 0000000000..4abc8a2705 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java @@ -0,0 +1,229 @@ +package ai.chat2db.plugin.mysql; + +import ai.chat2db.community.domain.api.config.TableBuilderConfig; +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.community.domain.api.model.view.ModifyView; +import ai.chat2db.plugin.mysql.builder.MysqlSqlBuilder; +import ai.chat2db.plugin.mysql.enums.MysqlViewAlgorithmOptionEnum; +import ai.chat2db.plugin.mysql.enums.type.MysqlColumnTypeEnum; +import ai.chat2db.plugin.mysql.enums.type.MysqlIndexTypeEnum; +import ai.chat2db.plugin.mysql.value.template.MysqlDmlValueTemplate; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_VIEW_TEMPLATE; +import static org.junit.jupiter.api.Assertions.assertEquals; +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 MysqlSqlEscapesTest { + + @Test + void escapeSqlLiteralDoublesBackslashBeforeSingleQuote() { + assertEquals("a''b\\\\c", MysqlSqlEscapes.escapeSqlLiteral("a'b\\c")); + assertEquals("''", MysqlSqlEscapes.escapeSqlLiteral("'")); + assertEquals("plain", MysqlSqlEscapes.escapeSqlLiteral("plain")); + assertNull(MysqlSqlEscapes.escapeSqlLiteral(null)); + } + + @Test + void quoteIdentifierDoublesEmbeddedBackticks() { + assertEquals("`plain`", MysqlSqlEscapes.quoteIdentifier("plain")); + assertEquals("`weird``name`", MysqlSqlEscapes.quoteIdentifier("weird`name")); + assertEquals("`a``; DROP TABLE b; --`", MysqlSqlEscapes.quoteIdentifier("a`; DROP TABLE b; --")); + // one surrounding backtick pair is stripped before doubling + assertEquals("`a``b`", MysqlSqlEscapes.quoteIdentifier("`a`b`")); + assertEquals("`quoted`", MysqlSqlEscapes.quoteIdentifier("`quoted`")); + } + + @Test + void requireMysqlNameRejectsInjection() { + assertEquals("utf8mb4_0900_ai_ci", MysqlSqlEscapes.requireMysqlName("utf8mb4_0900_ai_ci", "collation")); + assertThrows(IllegalArgumentException.class, + () -> MysqlSqlEscapes.requireMysqlName("InnoDB, COMMENT='x'", "engine")); + assertThrows(IllegalArgumentException.class, + () -> MysqlSqlEscapes.requireMysqlName("utf8mb4;DROP TABLE t", "charset")); + } + + @Test + void requireNumericDefaultRejectsNonLiteral() { + assertEquals("42", MysqlSqlEscapes.requireNumericDefault("42")); + assertEquals("-1.5", MysqlSqlEscapes.requireNumericDefault("-1.5")); + assertEquals("1e3", MysqlSqlEscapes.requireNumericDefault("1e3")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireNumericDefault("0);DROP TABLE t")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireNumericDefault("(uuid())")); + } + + @Test + void requireBitLiteralRejectsNonBits() { + assertEquals("0101", MysqlSqlEscapes.requireBitLiteral("0101")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireBitLiteral("2")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireBitLiteral("1' OR '1'='1")); + } + + @Test + void requireDefinerAcceptsOnlyAccountSyntax() { + assertEquals("root@localhost", MysqlSqlEscapes.requireDefiner("root@localhost")); + assertEquals("'root'@'%'", MysqlSqlEscapes.requireDefiner("'root'@'%'")); + assertEquals("`root`@`localhost`", MysqlSqlEscapes.requireDefiner("`root`@`localhost`")); + assertThrows(IllegalArgumentException.class, + () -> MysqlSqlEscapes.requireDefiner("root@localhost SQL SECURITY INVOKER")); + } + + @Test + void requireEnumConstantRejectsUnknownOption() { + assertEquals("MERGE", + MysqlSqlEscapes.requireEnumConstant("merge", MysqlViewAlgorithmOptionEnum.values(), "algorithm")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireEnumConstant( + "MERGE SQL SECURITY INVOKER", MysqlViewAlgorithmOptionEnum.values(), "algorithm")); + } + + @Test + void quoteEnumValuesKeepsBenignListAndNeutralizesMaliciousList() { + assertEquals("'draft','published'", MysqlSqlEscapes.quoteEnumValues("'draft','published'")); + assertEquals("('draft','published')", MysqlSqlEscapes.quoteEnumValues("('draft','published')")); + // unbalanced quote: whole input is re-escaped into a single inert string literal + assertEquals("'''),DROP TABLE t;-- x'", MysqlSqlEscapes.quoteEnumValues("'),DROP TABLE t;-- x")); + // balanced items stay split; hostile content stays inside a re-escaped literal + assertEquals("'a','b''); DROP TABLE t;-- '", MysqlSqlEscapes.quoteEnumValues("'a','b'); DROP TABLE t;-- '")); + } + + @Test + void createColumnSqlQuotesColumnNameAndEscapesComment() { + TableColumn column = TableColumn.builder() + .name("a`b") + .columnType("VARCHAR") + .columnSize(255) + .comment("it's") + .build(); + + String sql = MysqlColumnTypeEnum.VARCHAR.buildCreateColumnSql(column); + + assertTrue(sql.contains("`a``b`"), sql); + assertTrue(sql.contains("COMMENT 'it''s'"), sql); + } + + @Test + void createEnumColumnSqlReEscapesValueList() { + TableColumn column = TableColumn.builder() + .name("e") + .columnType("ENUM") + .value("'),DROP TABLE t;-- x") + .build(); + + String sql = MysqlColumnTypeEnum.ENUM.buildCreateColumnSql(column); + + assertTrue(sql.contains("ENUM('''),DROP TABLE t;-- x')"), sql); + } + + @Test + void createColumnSqlRejectsRawDefaultInjection() { + TableColumn column = TableColumn.builder() + .name("n") + .columnType("INT") + .defaultValue("0);DROP TABLE t") + .build(); + + assertThrows(IllegalArgumentException.class, () -> MysqlColumnTypeEnum.INT.buildCreateColumnSql(column)); + } + + @Test + void indexScriptQuotesIndexNameAndEscapesComment() { + TableIndex tableIndex = TableIndex.builder() + .name("i`x") + .type("Normal") + .method("BTREE") + .comment("c'd") + .columnList(List.of(TableIndexColumn.builder().columnName("col`1").build())) + .build(); + + String sql = MysqlIndexTypeEnum.NORMAL.buildIndexScript(tableIndex); + + assertTrue(sql.contains("`i``x`"), sql); + assertTrue(sql.contains("(`col``1`)"), sql); + assertTrue(sql.contains("COMMENT 'c''d'"), sql); + } + + @Test + void indexScriptCanonicalizesAscOrDescAndRejectsInjection() { + TableIndex benign = TableIndex.builder() + .name("i") + .type("Normal") + .method("BTREE") + .columnList(List.of(TableIndexColumn.builder().columnName("c").ascOrDesc("desc").build())) + .build(); + assertTrue(MysqlIndexTypeEnum.NORMAL.buildIndexScript(benign).contains("(`c` DESC)"), + MysqlIndexTypeEnum.NORMAL.buildIndexScript(benign)); + + TableIndex malicious = TableIndex.builder() + .name("i") + .type("Normal") + .method("BTREE") + .columnList(List.of(TableIndexColumn.builder().columnName("c") + .ascOrDesc("DESC, DROP TABLE t;--").build())) + .build(); + assertThrows(IllegalArgumentException.class, () -> MysqlIndexTypeEnum.NORMAL.buildIndexScript(malicious)); + } + + @Test + void createTableEscapesCommentAndRejectsEngineInjection() { + MysqlSqlBuilder builder = new MysqlSqlBuilder(); + Table table = Table.builder() + .name("t") + .columnList(List.of()) + .indexList(List.of()) + .comment("x'; DROP TABLE u;--") + .build(); + + String sql = builder.ddl().table().buildCreateTable(table, TableBuilderConfig.defaultConfig()); + + assertTrue(sql.contains("COMMENT='x''; DROP TABLE u;--'"), sql); + + Table evilEngine = Table.builder() + .name("t") + .columnList(List.of()) + .indexList(List.of()) + .engine("InnoDB COMMENT='x'") + .build(); + assertThrows(IllegalArgumentException.class, + () -> builder.ddl().table().buildCreateTable(evilEngine, TableBuilderConfig.defaultConfig())); + } + + @Test + void createViewRejectsCheckOptionAndDefinerInjection() { + MysqlSqlBuilder builder = new MysqlSqlBuilder(); + ModifyView modifyView = new ModifyView(); + modifyView.setViewName("v"); + modifyView.setViewBody("select 1"); + modifyView.setCheckOption("CASCADED; DROP TABLE t"); + assertThrows(IllegalArgumentException.class, () -> builder.buildCreateView(modifyView)); + + ModifyView definerView = new ModifyView(); + definerView.setViewName("v"); + definerView.setViewBody("select 1"); + definerView.setDefiner("root@localhost SQL SECURITY INVOKER"); + assertThrows(IllegalArgumentException.class, () -> builder.buildCreateView(definerView)); + } + + @Test + void dropTableAndDropViewQuoteIdentifiers() { + MysqlDBManager manager = new MysqlDBManager(); + assertEquals("DROP TABLE `a``;DROP TABLE b;--`", + manager.dropTable(null, null, null, "a`;DROP TABLE b;--")); + assertEquals("DROP VIEW `db`.`v`", + String.format(SQL_DROP_VIEW_TEMPLATE, MysqlDBManager.format("db"), MysqlDBManager.format("v"))); + } + + @Test + void dmlValueTemplatesEscapeOrValidate() { + assertEquals("ST_GeomFromText('POINT(1 1)')", MysqlDmlValueTemplate.wrapGeometry("POINT(1 1)")); + assertEquals("ST_GeomFromText('x''y')", MysqlDmlValueTemplate.wrapGeometry("x'y")); + assertEquals("b'0101'", MysqlDmlValueTemplate.wrapBit("0101")); + assertThrows(IllegalArgumentException.class, () -> MysqlDmlValueTemplate.wrapBit("1' OR '1'='1")); + } +} From c422c8f681ee7c226d37be4cc9107d4da26100f7 Mon Sep 17 00:00:00 2001 From: HandSonic <8078023+handsonic@users.noreply.github.com> Date: Sun, 26 Jul 2026 18:28:34 +0800 Subject: [PATCH 2/6] fix(mysql): validate hex literals and double backticks in identifier processor (#1914) - MysqlDmlValueTemplate.wrapHex: require hex digits for the 0x template, matching wrapBit's requireBitLiteral - MysqlBinaryProcessor/MysqlVarBinaryProcessor: only pass values through raw when they are well-formed 0x hex literals (isHexLiteral), closing the 0x-prefixed injection passthrough in generated DML - MysqlIdentifierProcessor.quoteIdentifier: double embedded backticks via MysqlSqlEscapes.quoteIdentifierRaw instead of wrapping verbatim - add regression tests for all three paths --- .../chat2db/plugin/mysql/MysqlSqlEscapes.java | 32 +++++++++++++++++++ .../identifier/MysqlIdentifierProcessor.java | 5 +-- .../mysql/value/sub/MysqlBinaryProcessor.java | 3 +- .../value/sub/MysqlVarBinaryProcessor.java | 3 +- .../value/template/MysqlDmlValueTemplate.java | 2 +- .../plugin/mysql/MysqlSqlEscapesTest.java | 17 ++++++++++ 6 files changed, 57 insertions(+), 5 deletions(-) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java index 0cde1988aa..41c6bc5dc6 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java @@ -16,6 +16,8 @@ public final class MysqlSqlEscapes { private static final Pattern NUMERIC_DEFAULT_PATTERN = Pattern.compile( "^([+-]?(\\d+(\\.\\d+)?|\\.\\d+)([eE][+-]?\\d+)?|0[xX][0-9a-fA-F]+|[xX]'[0-9a-fA-F]*'|[bB]'[01]*'|(?i:TRUE|FALSE))$"); private static final Pattern BIT_LITERAL_PATTERN = Pattern.compile("^[01]+$"); + private static final Pattern HEX_DIGITS_PATTERN = Pattern.compile("^[0-9a-fA-F]+$"); + private static final Pattern HEX_LITERAL_PATTERN = Pattern.compile("^0[xX][0-9a-fA-F]+$"); private static final String DEFINER_QUOTED_PART = "'([^'\\\\]|\\\\[\\s\\S])*'|`[^`]+`"; private static final Pattern DEFINER_PATTERN = Pattern.compile( "^([A-Za-z0-9_$]+|" + DEFINER_QUOTED_PART + ")@([A-Za-z0-9_.%:$-]+|" + DEFINER_QUOTED_PART + ")$"); @@ -81,6 +83,36 @@ public static String requireBitLiteral(String value) { return value; } + /** + * Validate the digits of a 0x... hex literal (the template adds the 0x prefix). + */ + public static String requireHexDigits(String value) { + if (value == null || !HEX_DIGITS_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("Invalid MySQL hex digits: " + value); + } + return value; + } + + /** + * True only when the value is a well-formed 0x... hex literal. Values that merely + * start with 0x but contain non-hex characters must not pass through into SQL raw. + */ + public static boolean isHexLiteral(String value) { + return value != null && HEX_LITERAL_PATTERN.matcher(value).matches(); + } + + /** + * Quote an identifier with backticks without stripping a surrounding pair: every + * embedded backtick is doubled. For call sites where the name may itself start or + * end with a backtick character. + */ + public static String quoteIdentifierRaw(String name) { + if (StringUtils.isBlank(name)) { + return name; + } + return "`" + name.replace("`", "``") + "`"; + } + /** * Validate a DEFINER value (user@host, parts optionally single-quoted or backtick-quoted). */ diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java index 7e87ef42c8..b58001151d 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java @@ -1,5 +1,6 @@ package ai.chat2db.plugin.mysql.identifier; +import ai.chat2db.plugin.mysql.MysqlSqlEscapes; import ai.chat2db.spi.DefaultSQLIdentifierProcessor; import org.apache.commons.lang3.StringUtils; @@ -294,7 +295,7 @@ public String quoteIdentifier(String identifier, Integer majorVersion, Integer m if (isValidIdentifier(identifier) && !isReservedKeyword(identifier.toUpperCase(), majorVersion, minorVersion)) { return identifier; } - return "`" + identifier + "`"; + return MysqlSqlEscapes.quoteIdentifierRaw(identifier); } @@ -303,7 +304,7 @@ public String quoteIdentifier(String identifier) { if (isValidIdentifier(identifier) && !isReservedKeyword(identifier.toUpperCase(), null, null)) { return identifier; } - return "`" + identifier + "`"; + return MysqlSqlEscapes.quoteIdentifierRaw(identifier); } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlBinaryProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlBinaryProcessor.java index 9c487cd4f4..e68211259b 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlBinaryProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlBinaryProcessor.java @@ -1,5 +1,6 @@ package ai.chat2db.plugin.mysql.value.sub; +import ai.chat2db.plugin.mysql.MysqlSqlEscapes; import ai.chat2db.plugin.mysql.value.template.MysqlDmlValueTemplate; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; @@ -11,7 +12,7 @@ public class MysqlBinaryProcessor extends DefaultValueProcessor { @Override public String convertSQLValueByType(SQLDataValue dataValue) { String value = dataValue.getValue(); - if (value.startsWith("0x")) { + if (MysqlSqlEscapes.isHexLiteral(value)) { return value; } return MysqlDmlValueTemplate.wrapHex(dataValue.getBlobHexString()); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlVarBinaryProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlVarBinaryProcessor.java index 407a740af3..db9c2b0cfe 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlVarBinaryProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlVarBinaryProcessor.java @@ -1,5 +1,6 @@ package ai.chat2db.plugin.mysql.value.sub; +import ai.chat2db.plugin.mysql.MysqlSqlEscapes; import ai.chat2db.plugin.mysql.value.template.MysqlDmlValueTemplate; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; @@ -13,7 +14,7 @@ public class MysqlVarBinaryProcessor extends DefaultValueProcessor { @Override public String convertSQLValueByType(SQLDataValue dataValue) { String value = dataValue.getValue(); - if (value.startsWith("0x")) { + if (MysqlSqlEscapes.isHexLiteral(value)) { return value; } return MysqlDmlValueTemplate.wrapHex(dataValue.getBlobHexString()); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java index f1a27503ee..6fe672d7ad 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java @@ -21,6 +21,6 @@ public static String wrapBit(String value) { } public static String wrapHex(String value) { - return String.format(HEX_TEMPLATE, value); + return String.format(HEX_TEMPLATE, MysqlSqlEscapes.requireHexDigits(value)); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java index 4abc8a2705..5c7c3ee364 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java @@ -225,5 +225,22 @@ void dmlValueTemplatesEscapeOrValidate() { assertEquals("ST_GeomFromText('x''y')", MysqlDmlValueTemplate.wrapGeometry("x'y")); assertEquals("b'0101'", MysqlDmlValueTemplate.wrapBit("0101")); assertThrows(IllegalArgumentException.class, () -> MysqlDmlValueTemplate.wrapBit("1' OR '1'='1")); + assertEquals("0x4d7953514c", MysqlDmlValueTemplate.wrapHex("4d7953514c")); + assertThrows(IllegalArgumentException.class, () -> MysqlDmlValueTemplate.wrapHex("41, name=(SELECT user())-- ")); + } + + @Test + void hexLiteralPassthroughRequiresWellFormedHex() { + assertTrue(MysqlSqlEscapes.isHexLiteral("0x4D7953514C")); + assertTrue(!MysqlSqlEscapes.isHexLiteral("0x41, name=(SELECT user())-- ")); + assertTrue(!MysqlSqlEscapes.isHexLiteral("0x")); + } + + @Test + void identifierProcessorDoublesEmbeddedBackticks() { + ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor processor = + new ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor(); + assertEquals("`a``b`", processor.quoteIdentifier("a`b")); + assertEquals("plain_name", processor.quoteIdentifier("plain_name")); } } From 818477d617c8ff8e8851a298191cd5acd13dbedd Mon Sep 17 00:00:00 2001 From: HandSonic <8078023+handsonic@users.noreply.github.com> Date: Sun, 26 Jul 2026 18:40:56 +0800 Subject: [PATCH 3/6] fix(mysql): escape truncate/copy table identifiers and add regression tests (#1914) --- .../chat2db/plugin/mysql/MysqlDBManager.java | 18 ++++++++++++++++++ .../mysql/constant/MysqlSqlConstants.java | 3 +++ .../plugin/mysql/MysqlSqlEscapesTest.java | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java index 93fe573754..192acddee1 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java @@ -19,10 +19,13 @@ import java.util.Date; import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; +import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_COPY_TABLE_DATA_TEMPLATE; +import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_COPY_TABLE_STRUCTURE_TEMPLATE; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_PROCEDURE_TEMPLATE; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_TABLE_TEMPLATE; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_VIEW_TEMPLATE; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_SELECT_DATABASE_TEMPLATE; +import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_TRUNCATE_TABLE_TEMPLATE; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_SET_FOREIGN_KEY_CHECKS_DISABLED; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_SET_FOREIGN_KEY_CHECKS_ENABLED; import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_SHOW_CREATE_FUNCTION_TEMPLATE; @@ -240,6 +243,21 @@ public String dropTable(Connection connection, String databaseName, String schem return String.format(SQL_DROP_TABLE_TEMPLATE, format(tableName)); } + @Override + public String truncateTable(Connection connection, String databaseName, String schemaName, String tableName) { + return String.format(SQL_TRUNCATE_TABLE_TEMPLATE, format(tableName)); + } + + @Override + public void copyTable(Connection connection, String databaseName, String schemaName, String tableName, String newTableName, boolean copyData) { + DefaultSQLExecutor.getInstance().execute(connection, buildCopyTableSql(tableName, newTableName, copyData), resultSet -> null); + } + + static String buildCopyTableSql(String tableName, String newTableName, boolean copyData) { + String template = copyData ? SQL_COPY_TABLE_DATA_TEMPLATE : SQL_COPY_TABLE_STRUCTURE_TEMPLATE; + return String.format(template, format(newTableName), format(tableName)); + } + public static String format(String tableName) { return MysqlSqlEscapes.quoteIdentifier(tableName); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java index 402252db82..e4340aff84 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java @@ -20,6 +20,8 @@ public final class MysqlSqlConstants { public static final String SQL_COMMENT_KEYWORD = "COMMENT"; public static final String SQL_COMMENT_SPACE_SINGLE_QUOTE = "COMMENT '"; public static final String SQL_COMMENT_WITH_SINGLE_QUOTE = " COMMENT='"; + public static final String SQL_COPY_TABLE_DATA_TEMPLATE = "CREATE TABLE %s AS SELECT * FROM %s"; + public static final String SQL_COPY_TABLE_STRUCTURE_TEMPLATE = "CREATE TABLE %s AS SELECT * FROM %s WHERE 1=0"; public static final String SQL_CREATE = "create "; public static final String SQL_CREATE_USER = "CREATE USER "; public static final String SQL_DEFAULT_CHARACTER_SET_ASSIGNMENT = "DEFAULT CHARACTER SET="; @@ -60,6 +62,7 @@ public final class MysqlSqlConstants { public static final String SQL_SHOW_INDEX_FROM = "SHOW INDEX FROM "; public static final String SQL_SHOW_PROCEDURE_STATUS = "SHOW PROCEDURE STATUS WHERE Db = DATABASE()"; public static final String SQL_SHOW_TRIGGERS = "SHOW TRIGGERS"; + public static final String SQL_TRUNCATE_TABLE_TEMPLATE = "TRUNCATE TABLE %s"; public static final String SQL_UNDEFINED = "undefined"; public static final String SQL_WITH_GRANT_OPTION = " WITH GRANT OPTION"; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java index 5c7c3ee364..920e7abf28 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java @@ -243,4 +243,23 @@ void identifierProcessorDoublesEmbeddedBackticks() { assertEquals("`a``b`", processor.quoteIdentifier("a`b")); assertEquals("plain_name", processor.quoteIdentifier("plain_name")); } + + @Test + void truncateTableEscapesBacktickIdentifier() { + MysqlDBManager manager = new MysqlDBManager(); + assertEquals("TRUNCATE TABLE `a``b`", + manager.truncateTable(null, null, null, "a`b")); + assertEquals("TRUNCATE TABLE `a``; DROP TABLE b; --`", + manager.truncateTable(null, null, null, "a`; DROP TABLE b; --")); + } + + @Test + void copyTableSqlEscapesBothIdentifiers() { + assertEquals("CREATE TABLE `n``t` AS SELECT * FROM `o``t`", + MysqlDBManager.buildCopyTableSql("o`t", "n`t", true)); + assertEquals("CREATE TABLE `n``t` AS SELECT * FROM `o``t` WHERE 1=0", + MysqlDBManager.buildCopyTableSql("o`t", "n`t", false)); + assertEquals("CREATE TABLE `c` AS SELECT * FROM `a``; DROP TABLE b; --`", + MysqlDBManager.buildCopyTableSql("a`; DROP TABLE b; --", "c", true)); + } } From 80873cb7b11a457ff9e4e2218fc02fd407941a1d Mon Sep 17 00:00:00 2001 From: HandSonic <8078023+handsonic@users.noreply.github.com> Date: Mon, 27 Jul 2026 02:07:23 +0800 Subject: [PATCH 4/6] refactor(mysql): move escaping into MysqlIdentifierProcessor per maintainer review (#1914) - MysqlIdentifierProcessor (SPI ISQLIdentifierProcessor): INSTANCE, always-quote backtick quoteIdentifier with one-pair strip + doubling, escapeString with backslash-first-then-quote doubling, static escapeIdentifier for quoted templates - MysqlMetaData call sites use getSQLIdentifierProcessor(); builders/managers/enums/ value processors use MysqlIdentifierProcessor.INSTANCE - non-escapable validation moved to MysqlSqlGuards (engine/charset/collation names, numeric defaults, bit/hex literals, definers, asc/desc, enum constants, enum/set lists) - MysqlSqlEscapes removed; tests migrated to MysqlIdentifierProcessorTest (630 green) --- .../chat2db/plugin/mysql/MysqlDBManager.java | 3 +- .../chat2db/plugin/mysql/MysqlMetaData.java | 26 +++--- .../plugin/mysql/MysqlRoutineManager.java | 3 +- ...sqlSqlEscapes.java => MysqlSqlGuards.java} | 51 ++--------- .../plugin/mysql/builder/MysqlSqlBuilder.java | 33 +++---- .../mysql/enums/type/MysqlColumnTypeEnum.java | 41 ++++----- .../mysql/enums/type/MysqlIndexTypeEnum.java | 13 +-- .../identifier/MysqlIdentifierProcessor.java | 54 ++++++++++-- .../mysql/value/sub/MysqlBinaryProcessor.java | 4 +- .../value/sub/MysqlVarBinaryProcessor.java | 4 +- .../value/template/MysqlDmlValueTemplate.java | 11 ++- ...java => MysqlIdentifierProcessorTest.java} | 86 ++++++++++--------- 12 files changed, 174 insertions(+), 155 deletions(-) rename chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/{MysqlSqlEscapes.java => MysqlSqlGuards.java} (79%) rename chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/{MysqlSqlEscapesTest.java => MysqlIdentifierProcessorTest.java} (71%) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java index 192acddee1..85c9620d7b 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java @@ -2,6 +2,7 @@ import ai.chat2db.community.tools.exception.BusinessException; import ai.chat2db.plugin.mysql.builder.MysqlSqlBuilder; +import ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor; import ai.chat2db.spi.IDbManager; import ai.chat2db.spi.DefaultDBManager; import ai.chat2db.community.domain.api.model.async.AsyncContext; @@ -259,7 +260,7 @@ static String buildCopyTableSql(String tableName, String newTableName, boolean c } public static String format(String tableName) { - return MysqlSqlEscapes.quoteIdentifier(tableName); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableName); } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java index e510179aa7..04e61b98f7 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java @@ -52,7 +52,7 @@ @Slf4j public class MysqlMetaData extends DefaultMetaService implements IDbMetaData { - public static final ISQLIdentifierProcessor MYSQL_IDENTIFIER_PROCESSOR = new MysqlIdentifierProcessor(); + public static final ISQLIdentifierProcessor MYSQL_IDENTIFIER_PROCESSOR = MysqlIdentifierProcessor.INSTANCE; @Override public List databases(Connection connection) { @@ -63,9 +63,9 @@ public List databases(Connection connection) { @Override public List
tables(Connection connection, @NotEmpty String databaseName, String schemaName, String tableName) { - String sql = String.format(TABLES_SQL, MysqlSqlEscapes.escapeSqlLiteral(databaseName)); + String sql = String.format(TABLES_SQL, getSQLIdentifierProcessor().escapeString(databaseName)); if (StringUtils.isNotBlank(tableName)) { - sql += SQL_TABLE_NAME_EQUALS_FILTER + MysqlSqlEscapes.escapeSqlLiteral(tableName) + SQL_SINGLE_QUOTE; + sql += SQL_TABLE_NAME_EQUALS_FILTER + getSQLIdentifierProcessor().escapeString(tableName) + SQL_SINGLE_QUOTE; } Map collationMap = getCollationMap(connection); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { @@ -131,14 +131,14 @@ public String tableDDL(Connection connection, @NotEmpty String databaseName, Str } public static String format(String tableName) { - return MysqlSqlEscapes.quoteIdentifier(tableName); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableName); } @Override public Function function(Connection connection, @NotEmpty String databaseName, String schemaName, String functionName) { - String functionInfoSql = String.format(ROUTINES_SQL, FUNCTION, MysqlSqlEscapes.escapeSqlLiteral(databaseName), MysqlSqlEscapes.escapeSqlLiteral(functionName)); + String functionInfoSql = String.format(ROUTINES_SQL, FUNCTION, getSQLIdentifierProcessor().escapeString(databaseName), getSQLIdentifierProcessor().escapeString(functionName)); Function function = DefaultSQLExecutor.getInstance().execute(connection, functionInfoSql, resultSet -> { Function f = new Function(); f.setDatabaseName(databaseName); @@ -162,7 +162,7 @@ public Function function(Connection connection, @NotEmpty String databaseName, S @Override public List triggers(Connection connection, String databaseName, String schemaName) { List triggers = new ArrayList<>(); - String sql = String.format(TRIGGER_SQL_LIST, MysqlSqlEscapes.escapeSqlLiteral(databaseName)); + String sql = String.format(TRIGGER_SQL_LIST, getSQLIdentifierProcessor().escapeString(databaseName)); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { while (resultSet.next()) { Trigger trigger = new Trigger(); @@ -208,7 +208,7 @@ public List procedures(Connection connection, String databaseName, St @Override public Procedure procedure(Connection connection, @NotEmpty String databaseName, String schemaName, String procedureName) { - String routinesSql = String.format(ROUTINES_SQL, PROCEDURE, MysqlSqlEscapes.escapeSqlLiteral(databaseName), MysqlSqlEscapes.escapeSqlLiteral(procedureName)); + String routinesSql = String.format(ROUTINES_SQL, PROCEDURE, getSQLIdentifierProcessor().escapeString(databaseName), getSQLIdentifierProcessor().escapeString(procedureName)); String showCreateProcedureSql = SQL_SHOW_CREATE_PROCEDURE + mysqlQualifiedName(databaseName, procedureName); Procedure procedure = DefaultSQLExecutor.getInstance().execute(connection, routinesSql, resultSet -> { Procedure p = new Procedure(); @@ -230,7 +230,7 @@ public Procedure procedure(Connection connection, @NotEmpty String databaseName, } @Override public List columns(Connection connection, String databaseName, String schemaName, String tableName) { - String sql = String.format(SELECT_TABLE_COLUMNS, MysqlSqlEscapes.escapeSqlLiteral(databaseName), MysqlSqlEscapes.escapeSqlLiteral(tableName)); + String sql = String.format(SELECT_TABLE_COLUMNS, getSQLIdentifierProcessor().escapeString(databaseName), getSQLIdentifierProcessor().escapeString(tableName)); List tableColumns = new ArrayList<>(); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { while (resultSet.next()) { @@ -285,7 +285,7 @@ private void setColumnSize(TableColumn column, String columnType) { @Override public Table view(Connection connection, String databaseName, String schemaName, String viewName) { - String quoteViewName = MysqlSqlEscapes.quoteIdentifier(viewName); + String quoteViewName = getSQLIdentifierProcessor().quoteIdentifier(viewName); String sql = String.format(VIEW_DDL_SQL, quoteViewName); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { Table table = new Table(); @@ -303,9 +303,9 @@ public Table view(Connection connection, String databaseName, String schemaName, @Override public List indexes(Connection connection, String databaseName, String schemaName, String tableName) { StringBuilder queryBuf = new StringBuilder(SQL_SHOW_INDEX_FROM); - queryBuf.append(MysqlSqlEscapes.quoteIdentifier(tableName)); + queryBuf.append(getSQLIdentifierProcessor().quoteIdentifier(tableName)); queryBuf.append(SQL_FROM); - queryBuf.append(MysqlSqlEscapes.quoteIdentifier(databaseName)); + queryBuf.append(getSQLIdentifierProcessor().quoteIdentifier(databaseName)); return DefaultSQLExecutor.getInstance().execute(connection, queryBuf.toString(), resultSet -> { LinkedHashMap map = new LinkedHashMap(); while (resultSet.next()) { @@ -461,7 +461,7 @@ private List queryTableMetaOptions(String sql, IResultSetFunction @Override public String getMetaDataName(String... names) { return Arrays.stream(names).filter(StringUtils::isNotBlank) - .map(MysqlSqlEscapes::quoteIdentifier) + .map(getSQLIdentifierProcessor()::quoteIdentifier) .collect(Collectors.joining(SQL_DOT)); } @@ -496,7 +496,7 @@ public ModifyViewConfiguration viewMeta(String databaseName, String schemaName) StringBuilder sqlBuilder = new StringBuilder(100); sqlBuilder.append(SQL_CREATE).append(SQL_VIEW_KEYWORD); if (StringUtils.isNotBlank(databaseName)) { - sqlBuilder.append(MysqlSqlEscapes.quoteIdentifier(databaseName)).append(SQL_DOT); + sqlBuilder.append(getSQLIdentifierProcessor().quoteIdentifier(databaseName)).append(SQL_DOT); } sqlBuilder.append(SQL_METADATA_QUOTE).append(SQL_UNDEFINED).append(SQL_METADATA_QUOTE); sqlBuilder.append(SQL_AS).append(sql).append(SQL_SEMICOLON); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java index fd47e71266..bda4c93780 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java @@ -1,6 +1,7 @@ package ai.chat2db.plugin.mysql; import ai.chat2db.plugin.mysql.converter.MysqlRoutineConverter; +import ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor; import ai.chat2db.plugin.mysql.model.RoutineParameter; import ai.chat2db.community.tools.exception.BusinessException; import ai.chat2db.community.tools.util.I18nUtils; @@ -362,7 +363,7 @@ private String mysqlQualifiedName(String databaseName, String routineName) { } private String quoteMysqlIdentifier(String name) { - return MysqlSqlEscapes.quoteIdentifier(name); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(name); } private String routineInvocationName(String name) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlGuards.java similarity index 79% rename from chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java rename to chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlGuards.java index 41c6bc5dc6..ce2f47c7d6 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlEscapes.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlGuards.java @@ -1,5 +1,6 @@ package ai.chat2db.plugin.mysql; +import ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; @@ -7,10 +8,12 @@ import java.util.regex.Pattern; /** - * Canonical escaping/quoting helpers for values interpolated into MySQL SQL text (#1914). - * Literal escaping mirrors MysqlAccountSqlBuilder.stringLiteral (backslash first, then single quotes). + * Validation helpers for non-escapable SQL positions in MySQL DDL/DML generation + * (engine/charset/collation names, raw numeric defaults, bit/hex literals, definers, + * index sort directions and fixed option sets). Escaping itself lives in + * {@link MysqlIdentifierProcessor}. */ -public final class MysqlSqlEscapes { +public final class MysqlSqlGuards { private static final Pattern MYSQL_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); private static final Pattern NUMERIC_DEFAULT_PATTERN = Pattern.compile( @@ -22,33 +25,7 @@ public final class MysqlSqlEscapes { private static final Pattern DEFINER_PATTERN = Pattern.compile( "^([A-Za-z0-9_$]+|" + DEFINER_QUOTED_PART + ")@([A-Za-z0-9_.%:$-]+|" + DEFINER_QUOTED_PART + ")$"); - private MysqlSqlEscapes() { - } - - /** - * Escape a value interpolated into a single-quoted SQL string literal (surrounding quotes NOT added). - * MySQL treats backslash as an escape character, so backslashes are doubled before single quotes. - */ - public static String escapeSqlLiteral(String value) { - if (value == null) { - return null; - } - return value.replace("\\", "\\\\").replace("'", "''"); - } - - /** - * Quote an identifier with backticks: strips one surrounding backtick pair, then doubles every - * embedded backtick. - */ - public static String quoteIdentifier(String name) { - if (StringUtils.isBlank(name)) { - return name; - } - String identifier = name; - if (identifier.length() >= 2 && identifier.startsWith("`") && identifier.endsWith("`")) { - identifier = identifier.substring(1, identifier.length() - 1); - } - return "`" + identifier.replace("`", "``") + "`"; + private MysqlSqlGuards() { } /** @@ -101,18 +78,6 @@ public static boolean isHexLiteral(String value) { return value != null && HEX_LITERAL_PATTERN.matcher(value).matches(); } - /** - * Quote an identifier with backticks without stripping a surrounding pair: every - * embedded backtick is doubled. For call sites where the name may itself start or - * end with a backtick character. - */ - public static String quoteIdentifierRaw(String name) { - if (StringUtils.isBlank(name)) { - return name; - } - return "`" + name.replace("`", "``") + "`"; - } - /** * Validate a DEFINER value (user@host, parts optionally single-quoted or backtick-quoted). */ @@ -201,7 +166,7 @@ public static String quoteEnumValues(String raw) { if (i > 0) { result.append(','); } - result.append('\'').append(escapeSqlLiteral(unquoteSingle(items.get(i).trim()))).append('\''); + result.append('\'').append(MysqlIdentifierProcessor.INSTANCE.escapeString(unquoteSingle(items.get(i).trim()))).append('\''); } if (parenthesized) { result.append(')'); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java index 9995b511f0..f8305e66ee 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java @@ -1,6 +1,7 @@ package ai.chat2db.plugin.mysql.builder; -import ai.chat2db.plugin.mysql.MysqlSqlEscapes; +import ai.chat2db.plugin.mysql.MysqlSqlGuards; +import ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor; import ai.chat2db.plugin.mysql.enums.MysqlViewAlgorithmOptionEnum; import ai.chat2db.plugin.mysql.enums.MysqlViewCheckOptionEnum; import ai.chat2db.plugin.mysql.enums.MysqlViewSqlSecurityOptionEnum; @@ -109,15 +110,15 @@ public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfi if (StringUtils.isNotBlank(table.getEngine())) { - script.append(SQLConstants.ENGINE_SQL).append(MysqlSqlEscapes.requireMysqlName(table.getEngine(), "engine")); + script.append(SQLConstants.ENGINE_SQL).append(MysqlSqlGuards.requireMysqlName(table.getEngine(), "engine")); } if (StringUtils.isNotBlank(table.getCharset())) { - script.append(SQLConstants.DEFAULT_CHARACTER_SET_SQL).append(MysqlSqlEscapes.requireMysqlName(table.getCharset(), "charset")); + script.append(SQLConstants.DEFAULT_CHARACTER_SET_SQL).append(MysqlSqlGuards.requireMysqlName(table.getCharset(), "charset")); } if (StringUtils.isNotBlank(table.getCollate())) { - script.append(SQLConstants.COLLATE_SQL).append(MysqlSqlEscapes.requireMysqlName(table.getCollate(), "collation")); + script.append(SQLConstants.COLLATE_SQL).append(MysqlSqlGuards.requireMysqlName(table.getCollate(), "collation")); } if (table.getIncrementValue() != null) { @@ -125,7 +126,7 @@ public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfi } if (StringUtils.isNotBlank(table.getComment())) { - script.append(SQL_COMMENT_WITH_SINGLE_QUOTE).append(MysqlSqlEscapes.escapeSqlLiteral(table.getComment())).append(SQLConstants.SINGLE_QUOTE); + script.append(SQL_COMMENT_WITH_SINGLE_QUOTE).append(MysqlIdentifierProcessor.INSTANCE.escapeString(table.getComment())).append(SQLConstants.SINGLE_QUOTE); } if (StringUtils.isNotBlank(table.getPartition())) { @@ -156,17 +157,17 @@ public String buildAlterTable(Table oldTable, Table newTable) { script.append(SQLConstants.TAB).append(SQL_RENAME).append(quoteMysqlIdentifier(newTable.getName())).append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!StringUtils.equalsIgnoreCase(oldTable.getComment(), newTable.getComment())) { - script.append(SQLConstants.TAB).append(SQL_COMMENT).append(SQLConstants.SINGLE_QUOTE).append(MysqlSqlEscapes.escapeSqlLiteral(newTable.getComment())).append(SQLConstants.SINGLE_QUOTE) + script.append(SQLConstants.TAB).append(SQL_COMMENT).append(SQLConstants.SINGLE_QUOTE).append(MysqlIdentifierProcessor.INSTANCE.escapeString(newTable.getComment())).append(SQLConstants.SINGLE_QUOTE) .append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!StringUtils.equalsIgnoreCase(oldTable.getEngine(), newTable.getEngine()) && StringUtils.isNotBlank(newTable.getEngine())) { - script.append(SQLConstants.TAB).append(SQL_ENGINE_ASSIGNMENT).append(MysqlSqlEscapes.requireMysqlName(newTable.getEngine(), "engine")).append(SQLConstants.COMMA_LINE_SEPARATOR); + script.append(SQLConstants.TAB).append(SQL_ENGINE_ASSIGNMENT).append(MysqlSqlGuards.requireMysqlName(newTable.getEngine(), "engine")).append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!StringUtils.equalsIgnoreCase(oldTable.getCharset(), newTable.getCharset()) && StringUtils.isNotBlank(newTable.getCharset())) { - script.append(SQLConstants.TAB).append(SQL_DEFAULT_CHARACTER_SET_ASSIGNMENT).append(MysqlSqlEscapes.requireMysqlName(newTable.getCharset(), "charset")).append(SQLConstants.COMMA_LINE_SEPARATOR); + script.append(SQLConstants.TAB).append(SQL_DEFAULT_CHARACTER_SET_ASSIGNMENT).append(MysqlSqlGuards.requireMysqlName(newTable.getCharset(), "charset")).append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!StringUtils.equalsIgnoreCase(oldTable.getCollate(), newTable.getCollate()) && StringUtils.isNotBlank(newTable.getCollate())) { - script.append(SQLConstants.TAB).append(SQL_COLLATE_ASSIGNMENT).append(MysqlSqlEscapes.requireMysqlName(newTable.getCollate(), "collation")).append(SQLConstants.COMMA_LINE_SEPARATOR); + script.append(SQLConstants.TAB).append(SQL_COLLATE_ASSIGNMENT).append(MysqlSqlGuards.requireMysqlName(newTable.getCollate(), "collation")).append(SQLConstants.COMMA_LINE_SEPARATOR); } if (!Objects.equals(oldTable.getIncrementValue(), newTable.getIncrementValue())) { script.append(SQLConstants.TAB).append(SQL_AUTO_INCREMENT_ASSIGNMENT).append(newTable.getIncrementValue()).append(SQLConstants.COMMA_LINE_SEPARATOR); @@ -260,10 +261,10 @@ public String buildCreateDatabase(Database database) { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append(SQLConstants.CREATE_DATABASE_SQL_PREFIX).append(quoteMysqlIdentifier(database.getName())); if (StringUtils.isNotBlank(database.getCharset())) { - sqlBuilder.append(SQLConstants.DEFAULT_CHARACTER_SET_SQL).append(MysqlSqlEscapes.requireMysqlName(database.getCharset(), "charset")); + sqlBuilder.append(SQLConstants.DEFAULT_CHARACTER_SET_SQL).append(MysqlSqlGuards.requireMysqlName(database.getCharset(), "charset")); } if (StringUtils.isNotBlank(database.getCollation())) { - sqlBuilder.append(SQLConstants.COLLATE_SQL).append(MysqlSqlEscapes.requireMysqlName(database.getCollation(), "collation")); + sqlBuilder.append(SQLConstants.COLLATE_SQL).append(MysqlSqlGuards.requireMysqlName(database.getCollation(), "collation")); } return sqlBuilder.toString(); } @@ -491,15 +492,15 @@ public String buildCreateView(ModifyView modifyView) { } String algorithm = modifyView.getAlgorithm(); if (StringUtils.isNotBlank(algorithm)) { - createViewSqlBuilder.append(SQL_ALGORITHM).append(MysqlSqlEscapes.requireEnumConstant(algorithm, MysqlViewAlgorithmOptionEnum.values(), "algorithm")).append(SQLConstants.SPACE); + createViewSqlBuilder.append(SQL_ALGORITHM).append(MysqlSqlGuards.requireEnumConstant(algorithm, MysqlViewAlgorithmOptionEnum.values(), "algorithm")).append(SQLConstants.SPACE); } String definer = modifyView.getDefiner(); if (StringUtils.isNotBlank(definer)) { - createViewSqlBuilder.append(SQL_DEFINER).append(MysqlSqlEscapes.requireDefiner(definer)).append(SQLConstants.SPACE); + createViewSqlBuilder.append(SQL_DEFINER).append(MysqlSqlGuards.requireDefiner(definer)).append(SQLConstants.SPACE); } String security = modifyView.getSecurity(); if (StringUtils.isNotBlank(security)) { - createViewSqlBuilder.append(SQL_SECURITY).append(MysqlSqlEscapes.requireEnumConstant(security, MysqlViewSqlSecurityOptionEnum.values(), "security")).append(SQLConstants.SPACE); + createViewSqlBuilder.append(SQL_SECURITY).append(MysqlSqlGuards.requireEnumConstant(security, MysqlViewSqlSecurityOptionEnum.values(), "security")).append(SQLConstants.SPACE); } createViewSqlBuilder.append(SQLConstants.VIEW_KEYWORD); String databaseName = modifyView.getDatabaseName(); @@ -523,14 +524,14 @@ public String buildCreateView(ModifyView modifyView) { } String checkOption = modifyView.getCheckOption(); if (StringUtils.isNotBlank(checkOption)) { - createViewSqlBuilder.append(SQLConstants.LINE_SEPARATOR_SQL_WITH).append(MysqlSqlEscapes.requireEnumConstant(checkOption, MysqlViewCheckOptionEnum.values(), "check option")).append(SQLConstants.CHECK_OPTION_SQL); + createViewSqlBuilder.append(SQLConstants.LINE_SEPARATOR_SQL_WITH).append(MysqlSqlGuards.requireEnumConstant(checkOption, MysqlViewCheckOptionEnum.values(), "check option")).append(SQLConstants.CHECK_OPTION_SQL); } return createViewSqlBuilder + SQLConstants.SEMICOLON; } private static String quoteMysqlIdentifier(String name) { - return MysqlSqlEscapes.quoteIdentifier(name); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(name); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java index 1c14ee233f..9671d2f6ed 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java @@ -1,6 +1,7 @@ package ai.chat2db.plugin.mysql.enums.type; -import ai.chat2db.plugin.mysql.MysqlSqlEscapes; +import ai.chat2db.plugin.mysql.MysqlSqlGuards; +import ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor; import ai.chat2db.spi.IColumnBuilder; import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum; import ai.chat2db.community.domain.api.model.metadata.ColumnType; @@ -150,7 +151,7 @@ public String buildCreateColumnSql(TableColumn column) { } StringBuilder script = new StringBuilder(); - script.append(MysqlSqlEscapes.quoteIdentifier(column.getName())).append(" "); + script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); @@ -181,7 +182,7 @@ public String buildAICreateColumnSql(TableColumn column) { } StringBuilder script = new StringBuilder(); - script.append(MysqlSqlEscapes.quoteIdentifier(column.getName())).append(" "); + script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); @@ -209,28 +210,28 @@ private String buildCharset(TableColumn column, MysqlColumnTypeEnum type) { if (!type.getColumnType().isSupportCharset() || StringUtils.isEmpty(column.getCharSetName())) { return ""; } - return StringUtils.join("CHARACTER SET ", MysqlSqlEscapes.requireMysqlName(column.getCharSetName(), "charset")); + return StringUtils.join("CHARACTER SET ", MysqlSqlGuards.requireMysqlName(column.getCharSetName(), "charset")); } private String buildCollation(TableColumn column, MysqlColumnTypeEnum type) { if (!type.getColumnType().isSupportCollation() || StringUtils.isEmpty(column.getCollationName())) { return ""; } - return StringUtils.join("COLLATE ", MysqlSqlEscapes.requireMysqlName(column.getCollationName(), "collation")); + return StringUtils.join("COLLATE ", MysqlSqlGuards.requireMysqlName(column.getCollationName(), "collation")); } @Override public String buildModifyColumn(TableColumn tableColumn) { if (EditStatusEnum.DELETE.name().equals(tableColumn.getEditStatus())) { - return StringUtils.join("DROP COLUMN ", MysqlSqlEscapes.quoteIdentifier(tableColumn.getName())); + return StringUtils.join("DROP COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableColumn.getName())); } if (EditStatusEnum.ADD.name().equals(tableColumn.getEditStatus())) { return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn)); } if (EditStatusEnum.MODIFY.name().equals(tableColumn.getEditStatus())) { if (!StringUtils.equalsIgnoreCase(tableColumn.getOldName(), tableColumn.getName())) { - return StringUtils.join("CHANGE COLUMN ", MysqlSqlEscapes.quoteIdentifier(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); + return StringUtils.join("CHANGE COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); } else { return StringUtils.join("MODIFY COLUMN ", buildCreateColumnSql(tableColumn)); } @@ -240,14 +241,14 @@ public String buildModifyColumn(TableColumn tableColumn) { public String buildModifyColumn(TableColumn tableColumn, boolean isMove, String columnName) { if (EditStatusEnum.DELETE.name().equals(tableColumn.getEditStatus())) { - return StringUtils.join("DROP COLUMN ", MysqlSqlEscapes.quoteIdentifier(tableColumn.getName())); + return StringUtils.join("DROP COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableColumn.getName())); } if (EditStatusEnum.ADD.name().equals(tableColumn.getEditStatus())) { if (isMove) { if (columnName.equals("-1")) { return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn), " FIRST"); } else { - return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn), " AFTER ", MysqlSqlEscapes.quoteIdentifier(columnName)); + return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn), " AFTER ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(columnName)); } } return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn)); @@ -255,7 +256,7 @@ public String buildModifyColumn(TableColumn tableColumn, boolean isMove, String if (EditStatusEnum.MODIFY.name().equals(tableColumn.getEditStatus())) { String sql; if (!StringUtils.equalsIgnoreCase(tableColumn.getOldName(), tableColumn.getName())) { - sql = StringUtils.join("CHANGE COLUMN ", MysqlSqlEscapes.quoteIdentifier(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); + sql = StringUtils.join("CHANGE COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); } else { sql = StringUtils.join("MODIFY COLUMN ", buildCreateColumnSql(tableColumn)); } @@ -275,7 +276,7 @@ private String appendColumnPosition(String sql, boolean isMove, String columnNam if (columnName.equals("-1")) { return StringUtils.join(sql, " FIRST"); } - return StringUtils.join(sql, " AFTER ", MysqlSqlEscapes.quoteIdentifier(columnName)); + return StringUtils.join(sql, " AFTER ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(columnName)); } private String buildAutoIncrement(TableColumn column, MysqlColumnTypeEnum type) { @@ -292,14 +293,14 @@ private String buildComment(TableColumn column, MysqlColumnTypeEnum type) { if (!type.columnType.isSupportComments() || StringUtils.isEmpty(column.getComment())) { return ""; } - return StringUtils.join(SQL_COMMENT_SPACE_SINGLE_QUOTE, MysqlSqlEscapes.escapeSqlLiteral(column.getComment()), "'"); + return StringUtils.join(SQL_COMMENT_SPACE_SINGLE_QUOTE, MysqlIdentifierProcessor.INSTANCE.escapeString(column.getComment()), "'"); } private String buildExt(TableColumn column, MysqlColumnTypeEnum type) { if (!type.columnType.isSupportExtent() || StringUtils.isEmpty(column.getExtent())) { return ""; } - return MysqlSqlEscapes.quoteEnumValues(column.getExtent()); + return MysqlSqlGuards.quoteEnumValues(column.getExtent()); } private String buildDefaultValue(TableColumn column, MysqlColumnTypeEnum type) { @@ -316,11 +317,11 @@ private String buildDefaultValue(TableColumn column, MysqlColumnTypeEnum type) { } if (Arrays.asList(CHAR, VARCHAR, BINARY, VARBINARY, SET, ENUM).contains(type)) { - return StringUtils.join("DEFAULT '", MysqlSqlEscapes.escapeSqlLiteral(column.getDefaultValue()), "'"); + return StringUtils.join("DEFAULT '", MysqlIdentifierProcessor.INSTANCE.escapeString(column.getDefaultValue()), "'"); } if (Arrays.asList(DATE, TIME, YEAR).contains(type)) { - return StringUtils.join("DEFAULT '", MysqlSqlEscapes.escapeSqlLiteral(column.getDefaultValue()), "'"); + return StringUtils.join("DEFAULT '", MysqlIdentifierProcessor.INSTANCE.escapeString(column.getDefaultValue()), "'"); } if (Arrays.asList(DATETIME, TIMESTAMP).contains(type)) { @@ -328,10 +329,10 @@ private String buildDefaultValue(TableColumn column, MysqlColumnTypeEnum type) { return StringUtils.join("DEFAULT ", column.getDefaultValue()); } - return StringUtils.join("DEFAULT '", MysqlSqlEscapes.escapeSqlLiteral(column.getDefaultValue()), "'"); + return StringUtils.join("DEFAULT '", MysqlIdentifierProcessor.INSTANCE.escapeString(column.getDefaultValue()), "'"); } - return StringUtils.join("DEFAULT ", MysqlSqlEscapes.requireNumericDefault(column.getDefaultValue())); + return StringUtils.join("DEFAULT ", MysqlSqlGuards.requireNumericDefault(column.getDefaultValue())); } private String OnUpdateCurrentTimestamp(TableColumn column, MysqlColumnTypeEnum type) { @@ -404,7 +405,7 @@ private String buildDataType(TableColumn column, MysqlColumnTypeEnum type) { if (Arrays.asList(SET, ENUM).contains(type)) { if (!StringUtils.isEmpty(column.getValue())) { - return StringUtils.join(columnType, "(", MysqlSqlEscapes.quoteEnumValues(column.getValue()), ")"); + return StringUtils.join(columnType, "(", MysqlSqlGuards.quoteEnumValues(column.getValue()), ")"); } } @@ -418,10 +419,10 @@ public String buildColumn(TableColumn column) { } StringBuilder script = new StringBuilder(); - script.append(MysqlSqlEscapes.quoteIdentifier(column.getName())).append(" "); + script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); if (StringUtils.isNoneBlank(column.getComment())) { - script.append(SQL_COMMENT_KEYWORD).append(" ").append("'").append(MysqlSqlEscapes.escapeSqlLiteral(column.getComment())).append("'").append(" "); + script.append(SQL_COMMENT_KEYWORD).append(" ").append("'").append(MysqlIdentifierProcessor.INSTANCE.escapeString(column.getComment())).append("'").append(" "); } return script.toString(); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java index e47fe50652..5466227f00 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java @@ -1,6 +1,7 @@ package ai.chat2db.plugin.mysql.enums.type; -import ai.chat2db.plugin.mysql.MysqlSqlEscapes; +import ai.chat2db.plugin.mysql.MysqlSqlGuards; +import ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor; import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum; import ai.chat2db.community.domain.api.model.metadata.IndexType; import ai.chat2db.community.domain.api.model.metadata.TableIndex; @@ -96,7 +97,7 @@ private String buildIndexComment(TableIndex tableIndex) { if(StringUtils.isBlank(tableIndex.getComment())){ return ""; }else { - return StringUtils.join(SQL_COMMENT_SPACE_SINGLE_QUOTE, MysqlSqlEscapes.escapeSqlLiteral(tableIndex.getComment()),"'"); + return StringUtils.join(SQL_COMMENT_SPACE_SINGLE_QUOTE, MysqlIdentifierProcessor.INSTANCE.escapeString(tableIndex.getComment()),"'"); } } @@ -106,9 +107,9 @@ private String buildIndexColumn(TableIndex tableIndex) { script.append("("); for (TableIndexColumn column : tableIndex.getColumnList()) { if(StringUtils.isNotBlank(column.getColumnName())) { - script.append(MysqlSqlEscapes.quoteIdentifier(column.getColumnName())); + script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(column.getColumnName())); if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) { - script.append(" ").append(MysqlSqlEscapes.requireAscOrDesc(column.getAscOrDesc())); + script.append(" ").append(MysqlSqlGuards.requireAscOrDesc(column.getAscOrDesc())); } script.append(","); } @@ -122,7 +123,7 @@ private String buildIndexName(TableIndex tableIndex) { if(this.equals(PRIMARY_KEY)){ return ""; }else { - return MysqlSqlEscapes.quoteIdentifier(tableIndex.getName()); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getName()); } } @@ -143,7 +144,7 @@ private String buildDropIndex(TableIndex tableIndex) { if (MysqlIndexTypeEnum.PRIMARY_KEY.getName().equals(tableIndex.getType())) { return StringUtils.join(SQL_DROP_PRIMARY_KEY); } - return StringUtils.join("DROP INDEX ", MysqlSqlEscapes.quoteIdentifier(tableIndex.getOldName())); + return StringUtils.join("DROP INDEX ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getOldName())); } public static List getIndexTypes() { return Arrays.asList(MysqlIndexTypeEnum.values()).stream().map(MysqlIndexTypeEnum::getIndexType).collect(java.util.stream.Collectors.toList()); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java index b58001151d..552f210e4a 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java @@ -1,6 +1,5 @@ package ai.chat2db.plugin.mysql.identifier; -import ai.chat2db.plugin.mysql.MysqlSqlEscapes; import ai.chat2db.spi.DefaultSQLIdentifierProcessor; import org.apache.commons.lang3.StringUtils; @@ -12,6 +11,11 @@ import static ai.chat2db.plugin.mysql.constant.MysqlIdentifierProcessorConstants.*; public class MysqlIdentifierProcessor extends DefaultSQLIdentifierProcessor { + /** + * Shared stateless instance for call sites without MetaData access. + */ + public static final MysqlIdentifierProcessor INSTANCE = new MysqlIdentifierProcessor(); + private static final Set MYSQL_RESERVED_KEYWORDS = new HashSet<>(); static { @@ -290,21 +294,59 @@ public boolean isReservedKeyword(String identifier, Integer majorVersion, Intege return MYSQL_RESERVED_KEYWORDS.contains(identifier); } + /** + * Always quotes with backticks, stripping one surrounding backtick pair and + * doubling every embedded backtick. + */ @Override public String quoteIdentifier(String identifier, Integer majorVersion, Integer minorVersion) { - if (isValidIdentifier(identifier) && !isReservedKeyword(identifier.toUpperCase(), majorVersion, minorVersion)) { + return quoteIdentifier(identifier); + } + + + @Override + public String quoteIdentifier(String identifier) { + if (StringUtils.isBlank(identifier)) { return identifier; } - return MysqlSqlEscapes.quoteIdentifierRaw(identifier); + return "`" + escapeIdentifierContent(identifier) + "`"; } + @Override + public String quoteIdentifierIgnoreCase(String identifier) { + return quoteIdentifier(identifier); + } + /** + * Escapes a value interpolated into a single-quoted SQL string literal (surrounding + * quotes NOT added). MySQL treats backslash as an escape character, so backslashes + * are doubled before single quotes (mirrors MysqlAccountSqlBuilder.stringLiteral). + */ @Override - public String quoteIdentifier(String identifier) { - if (isValidIdentifier(identifier) && !isReservedKeyword(identifier.toUpperCase(), null, null)) { + public String escapeString(String str) { + if (str == null) { + return null; + } + return str.replace("\\", "\\\\").replace("'", "''"); + } + + private static String escapeIdentifierContent(String identifier) { + if (StringUtils.isBlank(identifier)) { return identifier; } - return MysqlSqlEscapes.quoteIdentifierRaw(identifier); + String stripped = identifier; + if (stripped.length() >= 2 && stripped.startsWith("`") && stripped.endsWith("`")) { + stripped = stripped.substring(1, stripped.length() - 1); + } + return StringUtils.replace(stripped, "`", "``"); + } + + /** + * Escapes identifier content for a position already surrounded by backticks: + * strips one surrounding backtick pair, then doubles every embedded backtick. + */ + public static String escapeIdentifier(String identifier) { + return escapeIdentifierContent(identifier); } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlBinaryProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlBinaryProcessor.java index e68211259b..8c7e9f7130 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlBinaryProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlBinaryProcessor.java @@ -1,6 +1,6 @@ package ai.chat2db.plugin.mysql.value.sub; -import ai.chat2db.plugin.mysql.MysqlSqlEscapes; +import ai.chat2db.plugin.mysql.MysqlSqlGuards; import ai.chat2db.plugin.mysql.value.template.MysqlDmlValueTemplate; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; @@ -12,7 +12,7 @@ public class MysqlBinaryProcessor extends DefaultValueProcessor { @Override public String convertSQLValueByType(SQLDataValue dataValue) { String value = dataValue.getValue(); - if (MysqlSqlEscapes.isHexLiteral(value)) { + if (MysqlSqlGuards.isHexLiteral(value)) { return value; } return MysqlDmlValueTemplate.wrapHex(dataValue.getBlobHexString()); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlVarBinaryProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlVarBinaryProcessor.java index db9c2b0cfe..57b6489034 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlVarBinaryProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/sub/MysqlVarBinaryProcessor.java @@ -1,6 +1,6 @@ package ai.chat2db.plugin.mysql.value.sub; -import ai.chat2db.plugin.mysql.MysqlSqlEscapes; +import ai.chat2db.plugin.mysql.MysqlSqlGuards; import ai.chat2db.plugin.mysql.value.template.MysqlDmlValueTemplate; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; @@ -14,7 +14,7 @@ public class MysqlVarBinaryProcessor extends DefaultValueProcessor { @Override public String convertSQLValueByType(SQLDataValue dataValue) { String value = dataValue.getValue(); - if (MysqlSqlEscapes.isHexLiteral(value)) { + if (MysqlSqlGuards.isHexLiteral(value)) { return value; } return MysqlDmlValueTemplate.wrapHex(dataValue.getBlobHexString()); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java index 6fe672d7ad..88dfe9068a 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/value/template/MysqlDmlValueTemplate.java @@ -1,26 +1,25 @@ package ai.chat2db.plugin.mysql.value.template; -import ai.chat2db.plugin.mysql.MysqlSqlEscapes; +import ai.chat2db.plugin.mysql.MysqlSqlGuards; +import ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor; import static ai.chat2db.plugin.mysql.constant.MysqlDmlValueTemplateConstants.*; - public class MysqlDmlValueTemplate { - public static String wrapGeometry(String value) { - return String.format(GEOMETRY_TEMPLATE, MysqlSqlEscapes.escapeSqlLiteral(value)); + return String.format(GEOMETRY_TEMPLATE, MysqlIdentifierProcessor.INSTANCE.escapeString(value)); } public static String wrapBit(String value) { - return String.format(BIT_TEMPLATE, MysqlSqlEscapes.requireBitLiteral(value)); + return String.format(BIT_TEMPLATE, MysqlSqlGuards.requireBitLiteral(value)); } public static String wrapHex(String value) { - return String.format(HEX_TEMPLATE, MysqlSqlEscapes.requireHexDigits(value)); + return String.format(HEX_TEMPLATE, MysqlSqlGuards.requireHexDigits(value)); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java similarity index 71% rename from chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java rename to chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java index 920e7abf28..92b08ec205 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlSqlEscapesTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java @@ -10,6 +10,7 @@ import ai.chat2db.plugin.mysql.enums.MysqlViewAlgorithmOptionEnum; import ai.chat2db.plugin.mysql.enums.type.MysqlColumnTypeEnum; import ai.chat2db.plugin.mysql.enums.type.MysqlIndexTypeEnum; +import ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor; import ai.chat2db.plugin.mysql.value.template.MysqlDmlValueTemplate; import org.junit.jupiter.api.Test; @@ -21,76 +22,84 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -class MysqlSqlEscapesTest { +class MysqlIdentifierProcessorTest { @Test - void escapeSqlLiteralDoublesBackslashBeforeSingleQuote() { - assertEquals("a''b\\\\c", MysqlSqlEscapes.escapeSqlLiteral("a'b\\c")); - assertEquals("''", MysqlSqlEscapes.escapeSqlLiteral("'")); - assertEquals("plain", MysqlSqlEscapes.escapeSqlLiteral("plain")); - assertNull(MysqlSqlEscapes.escapeSqlLiteral(null)); + void escapeStringDoublesBackslashBeforeSingleQuote() { + assertEquals("a''b\\\\c", MysqlIdentifierProcessor.INSTANCE.escapeString("a'b\\c")); + assertEquals("''", MysqlIdentifierProcessor.INSTANCE.escapeString("'")); + assertEquals("plain", MysqlIdentifierProcessor.INSTANCE.escapeString("plain")); + assertNull(MysqlIdentifierProcessor.INSTANCE.escapeString(null)); } @Test void quoteIdentifierDoublesEmbeddedBackticks() { - assertEquals("`plain`", MysqlSqlEscapes.quoteIdentifier("plain")); - assertEquals("`weird``name`", MysqlSqlEscapes.quoteIdentifier("weird`name")); - assertEquals("`a``; DROP TABLE b; --`", MysqlSqlEscapes.quoteIdentifier("a`; DROP TABLE b; --")); + assertEquals("`plain`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("plain")); + assertEquals("`weird``name`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("weird`name")); + assertEquals("`a``; DROP TABLE b; --`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("a`; DROP TABLE b; --")); // one surrounding backtick pair is stripped before doubling - assertEquals("`a``b`", MysqlSqlEscapes.quoteIdentifier("`a`b`")); - assertEquals("`quoted`", MysqlSqlEscapes.quoteIdentifier("`quoted`")); + assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("`a`b`")); + assertEquals("`quoted`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("`quoted`")); + } + + @Test + void escapeIdentifierEscapesContentForQuotedTemplates() { + assertEquals("WE``IRD", MysqlIdentifierProcessor.escapeIdentifier("WE`IRD")); + assertEquals("ALREADY", MysqlIdentifierProcessor.escapeIdentifier("`ALREADY`")); + assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("a`b")); + assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("a`b", null, null)); } @Test void requireMysqlNameRejectsInjection() { - assertEquals("utf8mb4_0900_ai_ci", MysqlSqlEscapes.requireMysqlName("utf8mb4_0900_ai_ci", "collation")); + assertEquals("utf8mb4_0900_ai_ci", MysqlSqlGuards.requireMysqlName("utf8mb4_0900_ai_ci", "collation")); assertThrows(IllegalArgumentException.class, - () -> MysqlSqlEscapes.requireMysqlName("InnoDB, COMMENT='x'", "engine")); + () -> MysqlSqlGuards.requireMysqlName("InnoDB, COMMENT='x'", "engine")); assertThrows(IllegalArgumentException.class, - () -> MysqlSqlEscapes.requireMysqlName("utf8mb4;DROP TABLE t", "charset")); + () -> MysqlSqlGuards.requireMysqlName("utf8mb4;DROP TABLE t", "charset")); } @Test void requireNumericDefaultRejectsNonLiteral() { - assertEquals("42", MysqlSqlEscapes.requireNumericDefault("42")); - assertEquals("-1.5", MysqlSqlEscapes.requireNumericDefault("-1.5")); - assertEquals("1e3", MysqlSqlEscapes.requireNumericDefault("1e3")); - assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireNumericDefault("0);DROP TABLE t")); - assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireNumericDefault("(uuid())")); + assertEquals("42", MysqlSqlGuards.requireNumericDefault("42")); + assertEquals("-1.5", MysqlSqlGuards.requireNumericDefault("-1.5")); + assertEquals("1e3", MysqlSqlGuards.requireNumericDefault("1e3")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlGuards.requireNumericDefault("0);DROP TABLE t")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlGuards.requireNumericDefault("(uuid())")); } @Test void requireBitLiteralRejectsNonBits() { - assertEquals("0101", MysqlSqlEscapes.requireBitLiteral("0101")); - assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireBitLiteral("2")); - assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireBitLiteral("1' OR '1'='1")); + assertEquals("0101", MysqlSqlGuards.requireBitLiteral("0101")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlGuards.requireBitLiteral("2")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlGuards.requireBitLiteral("1' OR '1'='1")); } @Test void requireDefinerAcceptsOnlyAccountSyntax() { - assertEquals("root@localhost", MysqlSqlEscapes.requireDefiner("root@localhost")); - assertEquals("'root'@'%'", MysqlSqlEscapes.requireDefiner("'root'@'%'")); - assertEquals("`root`@`localhost`", MysqlSqlEscapes.requireDefiner("`root`@`localhost`")); + assertEquals("root@localhost", MysqlSqlGuards.requireDefiner("root@localhost")); + assertEquals("'root'@'%'", MysqlSqlGuards.requireDefiner("'root'@'%'")); + assertEquals("`root`@`localhost`", MysqlSqlGuards.requireDefiner("`root`@`localhost`")); assertThrows(IllegalArgumentException.class, - () -> MysqlSqlEscapes.requireDefiner("root@localhost SQL SECURITY INVOKER")); + () -> MysqlSqlGuards.requireDefiner("root@localhost SQL SECURITY INVOKER")); } @Test void requireEnumConstantRejectsUnknownOption() { assertEquals("MERGE", - MysqlSqlEscapes.requireEnumConstant("merge", MysqlViewAlgorithmOptionEnum.values(), "algorithm")); - assertThrows(IllegalArgumentException.class, () -> MysqlSqlEscapes.requireEnumConstant( + MysqlSqlGuards.requireEnumConstant("merge", MysqlViewAlgorithmOptionEnum.values(), "algorithm")); + assertThrows(IllegalArgumentException.class, () -> MysqlSqlGuards.requireEnumConstant( "MERGE SQL SECURITY INVOKER", MysqlViewAlgorithmOptionEnum.values(), "algorithm")); } @Test void quoteEnumValuesKeepsBenignListAndNeutralizesMaliciousList() { - assertEquals("'draft','published'", MysqlSqlEscapes.quoteEnumValues("'draft','published'")); - assertEquals("('draft','published')", MysqlSqlEscapes.quoteEnumValues("('draft','published')")); + assertEquals("'draft','published'", MysqlSqlGuards.quoteEnumValues("'draft','published'")); + assertEquals("('draft','published')", MysqlSqlGuards.quoteEnumValues("('draft','published')")); // unbalanced quote: whole input is re-escaped into a single inert string literal - assertEquals("'''),DROP TABLE t;-- x'", MysqlSqlEscapes.quoteEnumValues("'),DROP TABLE t;-- x")); + assertEquals("'''),DROP TABLE t;-- x'", MysqlSqlGuards.quoteEnumValues("'),DROP TABLE t;-- x")); // balanced items stay split; hostile content stays inside a re-escaped literal - assertEquals("'a','b''); DROP TABLE t;-- '", MysqlSqlEscapes.quoteEnumValues("'a','b'); DROP TABLE t;-- '")); + assertEquals("'a','b''); DROP TABLE t;-- '", MysqlSqlGuards.quoteEnumValues("'a','b'); DROP TABLE t;-- '")); } @Test @@ -231,17 +240,16 @@ void dmlValueTemplatesEscapeOrValidate() { @Test void hexLiteralPassthroughRequiresWellFormedHex() { - assertTrue(MysqlSqlEscapes.isHexLiteral("0x4D7953514C")); - assertTrue(!MysqlSqlEscapes.isHexLiteral("0x41, name=(SELECT user())-- ")); - assertTrue(!MysqlSqlEscapes.isHexLiteral("0x")); + assertTrue(MysqlSqlGuards.isHexLiteral("0x4D7953514C")); + assertTrue(!MysqlSqlGuards.isHexLiteral("0x41, name=(SELECT user())-- ")); + assertTrue(!MysqlSqlGuards.isHexLiteral("0x")); } @Test - void identifierProcessorDoublesEmbeddedBackticks() { - ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor processor = - new ai.chat2db.plugin.mysql.identifier.MysqlIdentifierProcessor(); + void identifierProcessorAlwaysQuotesAndDoublesEmbeddedBackticks() { + MysqlIdentifierProcessor processor = MysqlIdentifierProcessor.INSTANCE; assertEquals("`a``b`", processor.quoteIdentifier("a`b")); - assertEquals("plain_name", processor.quoteIdentifier("plain_name")); + assertEquals("`plain_name`", processor.quoteIdentifier("plain_name")); } @Test From 980d116c8c147589e9688f90c553c11bed7796de Mon Sep 17 00:00:00 2001 From: HandSonic <8078023+handsonic@users.noreply.github.com> Date: Mon, 27 Jul 2026 03:54:19 +0800 Subject: [PATCH 5/6] fix(mysql): keep SPI quoteIdentifier conditional, reserve always-quote for DDL paths (#1914) --- .../chat2db/plugin/mysql/MysqlDBManager.java | 2 +- .../chat2db/plugin/mysql/MysqlMetaData.java | 12 +++--- .../plugin/mysql/MysqlRoutineManager.java | 2 +- .../plugin/mysql/builder/MysqlSqlBuilder.java | 2 +- .../mysql/enums/type/MysqlColumnTypeEnum.java | 18 ++++----- .../mysql/enums/type/MysqlIndexTypeEnum.java | 6 +-- .../identifier/MysqlIdentifierProcessor.java | 26 +++++++++++-- .../mysql/MysqlIdentifierProcessorTest.java | 39 ++++++++++++++++--- 8 files changed, 76 insertions(+), 31 deletions(-) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java index 85c9620d7b..438477cb98 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java @@ -260,7 +260,7 @@ static String buildCopyTableSql(String tableName, String newTableName, boolean c } public static String format(String tableName) { - return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableName); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName); } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java index 04e61b98f7..144b9d60b9 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlMetaData.java @@ -131,7 +131,7 @@ public String tableDDL(Connection connection, @NotEmpty String databaseName, Str } public static String format(String tableName) { - return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableName); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName); } @Override @@ -285,7 +285,7 @@ private void setColumnSize(TableColumn column, String columnType) { @Override public Table view(Connection connection, String databaseName, String schemaName, String viewName) { - String quoteViewName = getSQLIdentifierProcessor().quoteIdentifier(viewName); + String quoteViewName = MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(viewName); String sql = String.format(VIEW_DDL_SQL, quoteViewName); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { Table table = new Table(); @@ -303,9 +303,9 @@ public Table view(Connection connection, String databaseName, String schemaName, @Override public List indexes(Connection connection, String databaseName, String schemaName, String tableName) { StringBuilder queryBuf = new StringBuilder(SQL_SHOW_INDEX_FROM); - queryBuf.append(getSQLIdentifierProcessor().quoteIdentifier(tableName)); + queryBuf.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName)); queryBuf.append(SQL_FROM); - queryBuf.append(getSQLIdentifierProcessor().quoteIdentifier(databaseName)); + queryBuf.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(databaseName)); return DefaultSQLExecutor.getInstance().execute(connection, queryBuf.toString(), resultSet -> { LinkedHashMap map = new LinkedHashMap(); while (resultSet.next()) { @@ -461,7 +461,7 @@ private List queryTableMetaOptions(String sql, IResultSetFunction @Override public String getMetaDataName(String... names) { return Arrays.stream(names).filter(StringUtils::isNotBlank) - .map(getSQLIdentifierProcessor()::quoteIdentifier) + .map(MysqlIdentifierProcessor.INSTANCE::quoteIdentifierAlways) .collect(Collectors.joining(SQL_DOT)); } @@ -496,7 +496,7 @@ public ModifyViewConfiguration viewMeta(String databaseName, String schemaName) StringBuilder sqlBuilder = new StringBuilder(100); sqlBuilder.append(SQL_CREATE).append(SQL_VIEW_KEYWORD); if (StringUtils.isNotBlank(databaseName)) { - sqlBuilder.append(getSQLIdentifierProcessor().quoteIdentifier(databaseName)).append(SQL_DOT); + sqlBuilder.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(databaseName)).append(SQL_DOT); } sqlBuilder.append(SQL_METADATA_QUOTE).append(SQL_UNDEFINED).append(SQL_METADATA_QUOTE); sqlBuilder.append(SQL_AS).append(sql).append(SQL_SEMICOLON); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java index bda4c93780..97be1b5d2c 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlRoutineManager.java @@ -363,7 +363,7 @@ private String mysqlQualifiedName(String databaseName, String routineName) { } private String quoteMysqlIdentifier(String name) { - return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(name); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(name); } private String routineInvocationName(String name) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java index f8305e66ee..c1a3757208 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/builder/MysqlSqlBuilder.java @@ -531,7 +531,7 @@ public String buildCreateView(ModifyView modifyView) { } private static String quoteMysqlIdentifier(String name) { - return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(name); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(name); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java index 9671d2f6ed..95cdc8a03c 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java @@ -151,7 +151,7 @@ public String buildCreateColumnSql(TableColumn column) { } StringBuilder script = new StringBuilder(); - script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" "); + script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); @@ -182,7 +182,7 @@ public String buildAICreateColumnSql(TableColumn column) { } StringBuilder script = new StringBuilder(); - script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" "); + script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); @@ -224,14 +224,14 @@ private String buildCollation(TableColumn column, MysqlColumnTypeEnum type) { public String buildModifyColumn(TableColumn tableColumn) { if (EditStatusEnum.DELETE.name().equals(tableColumn.getEditStatus())) { - return StringUtils.join("DROP COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableColumn.getName())); + return StringUtils.join("DROP COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getName())); } if (EditStatusEnum.ADD.name().equals(tableColumn.getEditStatus())) { return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn)); } if (EditStatusEnum.MODIFY.name().equals(tableColumn.getEditStatus())) { if (!StringUtils.equalsIgnoreCase(tableColumn.getOldName(), tableColumn.getName())) { - return StringUtils.join("CHANGE COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); + return StringUtils.join("CHANGE COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); } else { return StringUtils.join("MODIFY COLUMN ", buildCreateColumnSql(tableColumn)); } @@ -241,14 +241,14 @@ public String buildModifyColumn(TableColumn tableColumn) { public String buildModifyColumn(TableColumn tableColumn, boolean isMove, String columnName) { if (EditStatusEnum.DELETE.name().equals(tableColumn.getEditStatus())) { - return StringUtils.join("DROP COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableColumn.getName())); + return StringUtils.join("DROP COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getName())); } if (EditStatusEnum.ADD.name().equals(tableColumn.getEditStatus())) { if (isMove) { if (columnName.equals("-1")) { return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn), " FIRST"); } else { - return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn), " AFTER ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(columnName)); + return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn), " AFTER ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(columnName)); } } return StringUtils.join("ADD COLUMN ", buildCreateColumnSql(tableColumn)); @@ -256,7 +256,7 @@ public String buildModifyColumn(TableColumn tableColumn, boolean isMove, String if (EditStatusEnum.MODIFY.name().equals(tableColumn.getEditStatus())) { String sql; if (!StringUtils.equalsIgnoreCase(tableColumn.getOldName(), tableColumn.getName())) { - sql = StringUtils.join("CHANGE COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); + sql = StringUtils.join("CHANGE COLUMN ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getOldName()), " ", buildCreateColumnSql(tableColumn)); } else { sql = StringUtils.join("MODIFY COLUMN ", buildCreateColumnSql(tableColumn)); } @@ -276,7 +276,7 @@ private String appendColumnPosition(String sql, boolean isMove, String columnNam if (columnName.equals("-1")) { return StringUtils.join(sql, " FIRST"); } - return StringUtils.join(sql, " AFTER ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(columnName)); + return StringUtils.join(sql, " AFTER ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(columnName)); } private String buildAutoIncrement(TableColumn column, MysqlColumnTypeEnum type) { @@ -419,7 +419,7 @@ public String buildColumn(TableColumn column) { } StringBuilder script = new StringBuilder(); - script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" "); + script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); if (StringUtils.isNoneBlank(column.getComment())) { script.append(SQL_COMMENT_KEYWORD).append(" ").append("'").append(MysqlIdentifierProcessor.INSTANCE.escapeString(column.getComment())).append("'").append(" "); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java index 5466227f00..e1d541923c 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlIndexTypeEnum.java @@ -107,7 +107,7 @@ private String buildIndexColumn(TableIndex tableIndex) { script.append("("); for (TableIndexColumn column : tableIndex.getColumnList()) { if(StringUtils.isNotBlank(column.getColumnName())) { - script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(column.getColumnName())); + script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getColumnName())); if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) { script.append(" ").append(MysqlSqlGuards.requireAscOrDesc(column.getAscOrDesc())); } @@ -123,7 +123,7 @@ private String buildIndexName(TableIndex tableIndex) { if(this.equals(PRIMARY_KEY)){ return ""; }else { - return MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getName()); + return MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getName()); } } @@ -144,7 +144,7 @@ private String buildDropIndex(TableIndex tableIndex) { if (MysqlIndexTypeEnum.PRIMARY_KEY.getName().equals(tableIndex.getType())) { return StringUtils.join(SQL_DROP_PRIMARY_KEY); } - return StringUtils.join("DROP INDEX ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getOldName())); + return StringUtils.join("DROP INDEX ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getOldName())); } public static List getIndexTypes() { return Arrays.asList(MysqlIndexTypeEnum.values()).stream().map(MysqlIndexTypeEnum::getIndexType).collect(java.util.stream.Collectors.toList()); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java index 552f210e4a..0c71603d06 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java @@ -295,8 +295,9 @@ public boolean isReservedKeyword(String identifier, Integer majorVersion, Intege } /** - * Always quotes with backticks, stripping one surrounding backtick pair and - * doubling every embedded backtick. + * SPI-facing conditional quote: identifiers that are already valid plain identifiers + * and not reserved keywords pass through unquoted; anything else is wrapped in + * backticks with one surrounding pair stripped and embedded backticks doubled. */ @Override public String quoteIdentifier(String identifier, Integer majorVersion, Integer minorVersion) { @@ -306,15 +307,32 @@ public String quoteIdentifier(String identifier, Integer majorVersion, Integer m @Override public String quoteIdentifier(String identifier) { - if (StringUtils.isBlank(identifier)) { + if (identifier == null) { + return null; + } + if (isValidIdentifier(identifier) && !isReservedKeyword(identifier.toUpperCase(), null, null)) { return identifier; } + return quoteIdentifierAlways(identifier); + } + + /** + * Unconditional backtick quote for DDL-generation call sites: strips one surrounding + * backtick pair, then doubles every embedded backtick. + */ + public String quoteIdentifierAlways(String identifier) { + if (identifier == null) { + return null; + } return "`" + escapeIdentifierContent(identifier) + "`"; } + /** + * Always-quote variant that preserves the original identifier case. + */ @Override public String quoteIdentifierIgnoreCase(String identifier) { - return quoteIdentifier(identifier); + return quoteIdentifierAlways(identifier); } /** diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java index 92b08ec205..d23ff0eec9 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java @@ -33,21 +33,45 @@ void escapeStringDoublesBackslashBeforeSingleQuote() { } @Test - void quoteIdentifierDoublesEmbeddedBackticks() { - assertEquals("`plain`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("plain")); + void quoteIdentifierIsConditionalForSpiConsumers() { + // null/blank pass through untouched + assertNull(MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(null)); + assertEquals("", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("")); + assertEquals(" ", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier(" ")); + // valid plain identifiers that are not reserved keywords stay unquoted + assertEquals("plain", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("plain")); + assertEquals("plain_name", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("plain_name")); + // reserved keywords and non-plain identifiers are quoted with doubling + assertEquals("`select`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("select")); assertEquals("`weird``name`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("weird`name")); assertEquals("`a``; DROP TABLE b; --`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("a`; DROP TABLE b; --")); // one surrounding backtick pair is stripped before doubling assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("`a`b`")); assertEquals("`quoted`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("`quoted`")); + // versioned overload delegates to the same conditional behavior + assertEquals("users", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("users", null, null)); + assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("a`b", null, null)); + } + + @Test + void quoteIdentifierAlwaysWrapsUnconditionally() { + assertNull(MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(null)); + assertEquals("``", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("")); + assertEquals("`plain`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("plain")); + assertEquals("`plain_name`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("plain_name")); + assertEquals("`weird``name`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("weird`name")); + assertEquals("`a``; DROP TABLE b; --`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("a`; DROP TABLE b; --")); + assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("`a`b`")); + assertEquals("`quoted`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("`quoted`")); } @Test void escapeIdentifierEscapesContentForQuotedTemplates() { assertEquals("WE``IRD", MysqlIdentifierProcessor.escapeIdentifier("WE`IRD")); assertEquals("ALREADY", MysqlIdentifierProcessor.escapeIdentifier("`ALREADY`")); + // quoteIdentifierIgnoreCase is the always-quote, case-preserving variant assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("a`b")); - assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("a`b", null, null)); + assertEquals("`plain`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("plain")); } @Test @@ -246,10 +270,13 @@ void hexLiteralPassthroughRequiresWellFormedHex() { } @Test - void identifierProcessorAlwaysQuotesAndDoublesEmbeddedBackticks() { + void identifierProcessorDdlPathsKeepAlwaysQuoteSemantics() { MysqlIdentifierProcessor processor = MysqlIdentifierProcessor.INSTANCE; - assertEquals("`a``b`", processor.quoteIdentifier("a`b")); - assertEquals("`plain_name`", processor.quoteIdentifier("plain_name")); + // SPI-facing conditional quote leaves plain identifiers bare + assertEquals("plain_name", processor.quoteIdentifier("plain_name")); + // DDL-generation always quote keeps producing quoted output + assertEquals("`a``b`", processor.quoteIdentifierAlways("a`b")); + assertEquals("`plain_name`", processor.quoteIdentifierAlways("plain_name")); } @Test From 107a909663c35b525e92ef60d2dfa17c19f5cd38 Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 29 Jul 2026 11:34:39 +0800 Subject: [PATCH 6/6] fix(mysql): finalize SQL escaping boundaries --- .../chat2db/plugin/mysql/MysqlDBManager.java | 24 ++- .../chat2db/plugin/mysql/MysqlSqlGuards.java | 151 +++++++++++++----- .../mysql/constant/MysqlSqlConstants.java | 2 +- .../mysql/enums/type/MysqlColumnTypeEnum.java | 15 ++ .../identifier/MysqlIdentifierProcessor.java | 56 ++----- .../mysql/MysqlIdentifierProcessorTest.java | 67 +++++--- 6 files changed, 208 insertions(+), 107 deletions(-) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java index 438477cb98..a2522874e9 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlDBManager.java @@ -73,7 +73,7 @@ private void exportFunction(Connection connection, String functionName, AsyncCon String sql = String.format(SQL_SHOW_CREATE_FUNCTION_TEMPLATE, format(functionName)); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { - asyncContext.write(String.format(FUNCTION_TITLE, functionName)); + asyncContext.write(String.format(FUNCTION_TITLE, formatExportTitleValue(functionName))); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append(SQLConstants.DROP_FUNCTION_IF_EXISTS_SQL_PREFIX).append(format(functionName)).append(SQLConstants.SEMICOLON).append(SQLConstants.LINE_SEPARATOR); @@ -104,7 +104,7 @@ public void exportTable(Connection connection, String databaseName, String schem try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { StringBuilder sqlBuilder = new StringBuilder(); - asyncContext.write(String.format(TABLE_TITLE, tableName)); + asyncContext.write(String.format(TABLE_TITLE, formatExportTitleValue(tableName))); sqlBuilder.append(SQLConstants.DROP_TABLE_IF_EXISTS_SQL_PREFIX).append(format(tableName)).append(SQLConstants.SEMICOLON).append(SQLConstants.LINE_SEPARATOR) .append(resultSet.getString(CREATE_TABLE_COLUMN)).append(SQLConstants.SEMICOLON).append(SQLConstants.LINE_SEPARATOR); asyncContext.write(sqlBuilder.toString()); @@ -137,7 +137,7 @@ private void exportView(Connection connection, String viewName, AsyncContext asy String sql = String.format(SQL_SHOW_CREATE_VIEW_TEMPLATE, format(viewName)); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { - asyncContext.write(String.format(VIEW_TITLE, viewName)); + asyncContext.write(String.format(VIEW_TITLE, formatExportTitleValue(viewName))); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append(SQLConstants.DROP_VIEW_IF_EXISTS_SQL_PREFIX).append(format(viewName)).append(SQLConstants.SEMICOLON).append(SQLConstants.LINE_SEPARATOR) .append(resultSet.getString(CREATE_VIEW_COLUMN)).append(SQLConstants.SEMICOLON).append(SQLConstants.DOUBLE_LINE_SEPARATOR); @@ -159,7 +159,7 @@ private void exportProcedure(Connection connection, String procedureName, AsyncC String sql = String.format(SQL_SHOW_CREATE_PROCEDURE_TEMPLATE, format(procedureName)); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { - asyncContext.write(String.format(PROCEDURE_TITLE, procedureName)); + asyncContext.write(String.format(PROCEDURE_TITLE, formatExportTitleValue(procedureName))); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append(SQLConstants.DROP_PROCEDURE_IF_EXISTS_SQL_PREFIX).append(format(procedureName)).append(SQLConstants.SEMICOLON).append(SQLConstants.LINE_SEPARATOR) .append(DELIMITER_BLOCK_START).append(SQLConstants.LINE_SEPARATOR).append(resultSet.getString(CREATE_PROCEDURE_COLUMN)) @@ -184,7 +184,7 @@ private void exportTrigger(Connection connection, String triggerName, AsyncConte String sql = String.format(SQL_SHOW_CREATE_TRIGGER_TEMPLATE, format(triggerName)); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { - asyncContext.write(String.format(TRIGGER_TITLE, triggerName)); + asyncContext.write(String.format(TRIGGER_TITLE, formatExportTitleValue(triggerName))); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append(SQLConstants.DROP_TRIGGER_IF_EXISTS_SQL_PREFIX).append(format(triggerName)).append(SQLConstants.SEMICOLON).append(SQLConstants.LINE_SEPARATOR) .append(DELIMITER_BLOCK_START).append(SQLConstants.LINE_SEPARATOR).append(resultSet.getString(ORIGINAL_STATEMENT_COLUMN)) @@ -259,13 +259,23 @@ static String buildCopyTableSql(String tableName, String newTableName, boolean c return String.format(template, format(newTableName), format(tableName)); } + static String formatExportTitleValue(String value) { + return value == null ? null : value.replace('\r', ' ').replace('\n', ' '); + } + public static String format(String tableName) { return MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName); } @Override public void dropView(Connection connection, String databaseName, String schemaName, String viewName) { - String sql = String.format(SQL_DROP_VIEW_TEMPLATE, format(databaseName), format(viewName)); - DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> null); + DefaultSQLExecutor.getInstance().execute(connection, buildDropViewSql(databaseName, viewName), resultSet -> null); + } + + static String buildDropViewSql(String databaseName, String viewName) { + String qualifiedName = StringUtils.isEmpty(databaseName) + ? format(viewName) + : format(databaseName) + SQLConstants.DOT + format(viewName); + return String.format(SQL_DROP_VIEW_TEMPLATE, qualifiedName); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlGuards.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlGuards.java index ce2f47c7d6..248b63a4e9 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlGuards.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/MysqlSqlGuards.java @@ -21,9 +21,14 @@ public final class MysqlSqlGuards { private static final Pattern BIT_LITERAL_PATTERN = Pattern.compile("^[01]+$"); private static final Pattern HEX_DIGITS_PATTERN = Pattern.compile("^[0-9a-fA-F]+$"); private static final Pattern HEX_LITERAL_PATTERN = Pattern.compile("^0[xX][0-9a-fA-F]+$"); - private static final String DEFINER_QUOTED_PART = "'([^'\\\\]|\\\\[\\s\\S])*'|`[^`]+`"; + private static final String DEFINER_SINGLE_QUOTED_PART = "'(?:''|[^'\\\\])+'"; + private static final String DEFINER_BACKTICK_QUOTED_PART = "`(?:``|[^`\\\\])+`"; + private static final String DEFINER_QUOTED_PART = "(?:" + DEFINER_SINGLE_QUOTED_PART + "|" + + DEFINER_BACKTICK_QUOTED_PART + ")"; private static final Pattern DEFINER_PATTERN = Pattern.compile( "^([A-Za-z0-9_$]+|" + DEFINER_QUOTED_PART + ")@([A-Za-z0-9_.%:$-]+|" + DEFINER_QUOTED_PART + ")$"); + private static final Pattern COLUMN_TYPE_PATTERN = Pattern.compile( + "^[A-Za-z][A-Za-z0-9_]*(?:\\s*\\(\\s*\\d+(?:\\s*,\\s*\\d+)?\\s*\\))?(?:\\s+[A-Za-z][A-Za-z0-9_]*)*$"); private MysqlSqlGuards() { } @@ -88,6 +93,18 @@ public static String requireDefiner(String value) { return value; } + /** + * Validate a fallback column type expression while preserving common custom type names, + * optional numeric precision/scale and keyword modifiers. + */ + public static String requireColumnType(String value) { + String trimmed = StringUtils.trimToEmpty(value); + if (!COLUMN_TYPE_PATTERN.matcher(trimmed).matches()) { + throw new IllegalArgumentException("Invalid MySQL column type: " + value); + } + return trimmed; + } + /** * Validate an index sort direction: only ASC/DESC are legal, returned in canonical uppercase. */ @@ -116,9 +133,9 @@ public static > String requireEnumConstant(String value, E[] c } /** - * Re-escape a comma-separated ENUM/SET value list: each item is stripped of its surrounding - * single quotes (if any), escaped as a SQL string literal and re-quoted. A surrounding pair of - * parentheses is preserved. + * Parse and re-escape a comma-separated ENUM/SET value list. Quoted values are decoded before + * they are escaped again, so metadata such as {@code 'can''t'} is not double-escaped. A + * surrounding pair of parentheses is preserved. */ public static String quoteEnumValues(String raw) { if (raw == null) { @@ -128,36 +145,14 @@ public static String quoteEnumValues(String raw) { if (trimmed.isEmpty()) { return trimmed; } - boolean parenthesized = trimmed.length() >= 2 && trimmed.startsWith("(") && trimmed.endsWith(")"); - String inner = parenthesized ? trimmed.substring(1, trimmed.length() - 1) : trimmed; - List items = new ArrayList<>(); - StringBuilder current = new StringBuilder(); - boolean inQuote = false; - for (int i = 0; i < inner.length(); i++) { - char c = inner.charAt(i); - if (inQuote) { - current.append(c); - if (c == '\\' && i + 1 < inner.length()) { - current.append(inner.charAt(++i)); - } else if (c == '\'') { - if (i + 1 < inner.length() && inner.charAt(i + 1) == '\'') { - current.append('\''); - i++; - } else { - inQuote = false; - } - } - } else if (c == '\'') { - inQuote = true; - current.append(c); - } else if (c == ',') { - items.add(current.toString()); - current.setLength(0); - } else { - current.append(c); - } + boolean startsWithParenthesis = trimmed.startsWith("("); + boolean endsWithParenthesis = trimmed.endsWith(")"); + if (startsWithParenthesis != endsWithParenthesis) { + throw invalidEnumValues(raw); } - items.add(current.toString()); + boolean parenthesized = startsWithParenthesis; + String inner = parenthesized ? trimmed.substring(1, trimmed.length() - 1) : trimmed; + List items = parseEnumValues(inner, raw); StringBuilder result = new StringBuilder(); if (parenthesized) { result.append('('); @@ -166,7 +161,7 @@ public static String quoteEnumValues(String raw) { if (i > 0) { result.append(','); } - result.append('\'').append(MysqlIdentifierProcessor.INSTANCE.escapeString(unquoteSingle(items.get(i).trim()))).append('\''); + result.append('\'').append(MysqlIdentifierProcessor.INSTANCE.escapeString(items.get(i))).append('\''); } if (parenthesized) { result.append(')'); @@ -174,10 +169,90 @@ public static String quoteEnumValues(String raw) { return result.toString(); } - private static String unquoteSingle(String item) { - if (item.length() >= 2 && item.startsWith("'") && item.endsWith("'")) { - return item.substring(1, item.length() - 1); + private static List parseEnumValues(String inner, String raw) { + List values = new ArrayList<>(); + int index = 0; + while (index < inner.length()) { + while (index < inner.length() && Character.isWhitespace(inner.charAt(index))) { + index++; + } + if (index >= inner.length()) { + throw invalidEnumValues(raw); + } + + String value; + if (inner.charAt(index) == '\'') { + StringBuilder decoded = new StringBuilder(); + index = parseQuotedEnumValue(inner, index + 1, decoded, raw); + value = decoded.toString(); + while (index < inner.length() && Character.isWhitespace(inner.charAt(index))) { + index++; + } + if (index < inner.length() && inner.charAt(index) != ',') { + throw invalidEnumValues(raw); + } + } else { + int comma = inner.indexOf(',', index); + int end = comma < 0 ? inner.length() : comma; + value = inner.substring(index, end).trim(); + if (value.isEmpty()) { + throw invalidEnumValues(raw); + } + index = end; + } + values.add(value); + + if (index >= inner.length()) { + break; + } + index++; + if (index >= inner.length()) { + throw invalidEnumValues(raw); + } + } + if (values.isEmpty()) { + throw invalidEnumValues(raw); } - return item; + return values; + } + + private static int parseQuotedEnumValue(String inner, int index, StringBuilder decoded, String raw) { + while (index < inner.length()) { + char current = inner.charAt(index++); + if (current == '\'') { + if (index < inner.length() && inner.charAt(index) == '\'') { + decoded.append('\''); + index++; + continue; + } + return index; + } + if (current == '\\') { + if (index >= inner.length()) { + throw invalidEnumValues(raw); + } + appendMysqlEscapedCharacter(decoded, inner.charAt(index++)); + continue; + } + decoded.append(current); + } + throw invalidEnumValues(raw); + } + + private static void appendMysqlEscapedCharacter(StringBuilder decoded, char escaped) { + switch (escaped) { + case '0' -> decoded.append('\0'); + case 'b' -> decoded.append('\b'); + case 'n' -> decoded.append('\n'); + case 'r' -> decoded.append('\r'); + case 't' -> decoded.append('\t'); + case 'Z' -> decoded.append((char) 26); + case '%', '_' -> decoded.append('\\').append(escaped); + default -> decoded.append(escaped); + } + } + + private static IllegalArgumentException invalidEnumValues(String raw) { + return new IllegalArgumentException("Invalid MySQL ENUM/SET values: " + raw); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java index e4340aff84..b71ef69e2b 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/constant/MysqlSqlConstants.java @@ -34,7 +34,7 @@ public final class MysqlSqlConstants { public static final String SQL_DROP_PROCEDURE_TEMPLATE = "DROP PROCEDURE %s"; public static final String SQL_DROP_TABLE_TEMPLATE = "DROP TABLE %s"; public static final String SQL_DROP_USER = "DROP USER "; - public static final String SQL_DROP_VIEW_TEMPLATE = "DROP VIEW %s.%s"; + public static final String SQL_DROP_VIEW_TEMPLATE = "DROP VIEW %s"; public static final String SQL_ENGINE_ASSIGNMENT = "ENGINE="; public static final String SQL_FIRST_TERMINATOR = " FIRST;\n"; public static final String SQL_FROM = " FROM "; diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java index 95cdc8a03c..840130fe38 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/enums/type/MysqlColumnTypeEnum.java @@ -142,6 +142,21 @@ public static MysqlColumnTypeEnum getByType(String dataType) { } } + @Override + public String buildDefaultColumn(TableColumn column, boolean comment) { + StringBuilder script = new StringBuilder(); + script.append(MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())) + .append(" ") + .append(MysqlSqlGuards.requireColumnType(column.getColumnType())); + if (comment && StringUtils.isNotBlank(column.getComment())) { + script.append(" ") + .append(SQL_COMMENT_SPACE_SINGLE_QUOTE) + .append(MysqlIdentifierProcessor.INSTANCE.escapeString(column.getComment())) + .append("'"); + } + return script.toString(); + } + @Override public String buildCreateColumnSql(TableColumn column) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java index 0c71603d06..9b4c15de85 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/main/java/ai/chat2db/plugin/mysql/identifier/MysqlIdentifierProcessor.java @@ -296,8 +296,7 @@ public boolean isReservedKeyword(String identifier, Integer majorVersion, Intege /** * SPI-facing conditional quote: identifiers that are already valid plain identifiers - * and not reserved keywords pass through unquoted; anything else is wrapped in - * backticks with one surrounding pair stripped and embedded backticks doubled. + * and not reserved keywords pass through unquoted; anything else is safely quoted. */ @Override public String quoteIdentifier(String identifier, Integer majorVersion, Integer minorVersion) { @@ -316,25 +315,6 @@ public String quoteIdentifier(String identifier) { return quoteIdentifierAlways(identifier); } - /** - * Unconditional backtick quote for DDL-generation call sites: strips one surrounding - * backtick pair, then doubles every embedded backtick. - */ - public String quoteIdentifierAlways(String identifier) { - if (identifier == null) { - return null; - } - return "`" + escapeIdentifierContent(identifier) + "`"; - } - - /** - * Always-quote variant that preserves the original identifier case. - */ - @Override - public String quoteIdentifierIgnoreCase(String identifier) { - return quoteIdentifierAlways(identifier); - } - /** * Escapes a value interpolated into a single-quoted SQL string literal (surrounding * quotes NOT added). MySQL treats backslash as an escape character, so backslashes @@ -345,26 +325,22 @@ public String escapeString(String str) { if (str == null) { return null; } - return str.replace("\\", "\\\\").replace("'", "''"); - } - - private static String escapeIdentifierContent(String identifier) { - if (StringUtils.isBlank(identifier)) { - return identifier; + StringBuilder escaped = new StringBuilder(str.length()); + for (int i = 0; i < str.length(); i++) { + char current = str.charAt(i); + switch (current) { + case '\0' -> escaped.append("\\0"); + case '\b' -> escaped.append("\\b"); + case '\n' -> escaped.append("\\n"); + case '\r' -> escaped.append("\\r"); + case '\t' -> escaped.append("\\t"); + case 26 -> escaped.append("\\Z"); + case '\\' -> escaped.append("\\\\"); + case '\'' -> escaped.append("''"); + default -> escaped.append(current); + } } - String stripped = identifier; - if (stripped.length() >= 2 && stripped.startsWith("`") && stripped.endsWith("`")) { - stripped = stripped.substring(1, stripped.length() - 1); - } - return StringUtils.replace(stripped, "`", "``"); - } - - /** - * Escapes identifier content for a position already surrounded by backticks: - * strips one surrounding backtick pair, then doubles every embedded backtick. - */ - public static String escapeIdentifier(String identifier) { - return escapeIdentifierContent(identifier); + return escaped.toString(); } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java index d23ff0eec9..eb88077860 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mysql/src/test/java/ai/chat2db/plugin/mysql/MysqlIdentifierProcessorTest.java @@ -16,7 +16,6 @@ import java.util.List; -import static ai.chat2db.plugin.mysql.constant.MysqlSqlConstants.SQL_DROP_VIEW_TEMPLATE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -45,9 +44,9 @@ void quoteIdentifierIsConditionalForSpiConsumers() { assertEquals("`select`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("select")); assertEquals("`weird``name`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("weird`name")); assertEquals("`a``; DROP TABLE b; --`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("a`; DROP TABLE b; --")); - // one surrounding backtick pair is stripped before doubling - assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("`a`b`")); - assertEquals("`quoted`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("`quoted`")); + // quoteIdentifier treats its argument as raw identifier content + assertEquals("```a``b```", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("`a`b`")); + assertEquals("```quoted```", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("`quoted`")); // versioned overload delegates to the same conditional behavior assertEquals("users", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("users", null, null)); assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifier("a`b", null, null)); @@ -61,17 +60,16 @@ void quoteIdentifierAlwaysWrapsUnconditionally() { assertEquals("`plain_name`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("plain_name")); assertEquals("`weird``name`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("weird`name")); assertEquals("`a``; DROP TABLE b; --`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("a`; DROP TABLE b; --")); - assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("`a`b`")); - assertEquals("`quoted`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("`quoted`")); + assertEquals("```a``b```", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("`a`b`")); + assertEquals("```quoted```", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("`quoted`")); + assertEquals("`quoted`", MysqlIdentifierProcessor.INSTANCE.removeIdentifierQuote( + MysqlIdentifierProcessor.INSTANCE.quoteIdentifierAlways("`quoted`"))); } @Test - void escapeIdentifierEscapesContentForQuotedTemplates() { - assertEquals("WE``IRD", MysqlIdentifierProcessor.escapeIdentifier("WE`IRD")); - assertEquals("ALREADY", MysqlIdentifierProcessor.escapeIdentifier("`ALREADY`")); - // quoteIdentifierIgnoreCase is the always-quote, case-preserving variant + void quoteIdentifierIgnoreCaseKeepsConditionalSpiSemantics() { assertEquals("`a``b`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("a`b")); - assertEquals("`plain`", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("plain")); + assertEquals("plain", MysqlIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("plain")); } @Test @@ -104,6 +102,9 @@ void requireDefinerAcceptsOnlyAccountSyntax() { assertEquals("root@localhost", MysqlSqlGuards.requireDefiner("root@localhost")); assertEquals("'root'@'%'", MysqlSqlGuards.requireDefiner("'root'@'%'")); assertEquals("`root`@`localhost`", MysqlSqlGuards.requireDefiner("`root`@`localhost`")); + assertEquals("'ro''ot'@'%'", MysqlSqlGuards.requireDefiner("'ro''ot'@'%'")); + assertThrows(IllegalArgumentException.class, + () -> MysqlSqlGuards.requireDefiner("'root\\\\name'@'localhost'")); assertThrows(IllegalArgumentException.class, () -> MysqlSqlGuards.requireDefiner("root@localhost SQL SECURITY INVOKER")); } @@ -117,13 +118,16 @@ void requireEnumConstantRejectsUnknownOption() { } @Test - void quoteEnumValuesKeepsBenignListAndNeutralizesMaliciousList() { + void quoteEnumValuesDecodesBeforeReEscapingAndRejectsMalformedLists() { assertEquals("'draft','published'", MysqlSqlGuards.quoteEnumValues("'draft','published'")); assertEquals("('draft','published')", MysqlSqlGuards.quoteEnumValues("('draft','published')")); - // unbalanced quote: whole input is re-escaped into a single inert string literal - assertEquals("'''),DROP TABLE t;-- x'", MysqlSqlGuards.quoteEnumValues("'),DROP TABLE t;-- x")); - // balanced items stay split; hostile content stays inside a re-escaped literal - assertEquals("'a','b''); DROP TABLE t;-- '", MysqlSqlGuards.quoteEnumValues("'a','b'); DROP TABLE t;-- '")); + assertEquals("'can''t'", MysqlSqlGuards.quoteEnumValues("'can''t'")); + assertEquals("'can''t'", MysqlSqlGuards.quoteEnumValues("'can\\'t'")); + assertEquals("'a\\\\b'", MysqlSqlGuards.quoteEnumValues("'a\\\\b'")); + assertThrows(IllegalArgumentException.class, + () -> MysqlSqlGuards.quoteEnumValues("'),DROP TABLE t;-- x")); + assertThrows(IllegalArgumentException.class, + () -> MysqlSqlGuards.quoteEnumValues("'a','b'); DROP TABLE t;-- '")); } @Test @@ -142,16 +146,30 @@ void createColumnSqlQuotesColumnNameAndEscapesComment() { } @Test - void createEnumColumnSqlReEscapesValueList() { + void createEnumColumnSqlRejectsMalformedValueList() { TableColumn column = TableColumn.builder() .name("e") .columnType("ENUM") .value("'),DROP TABLE t;-- x") .build(); - String sql = MysqlColumnTypeEnum.ENUM.buildCreateColumnSql(column); + assertThrows(IllegalArgumentException.class, + () -> MysqlColumnTypeEnum.ENUM.buildCreateColumnSql(column)); + } - assertTrue(sql.contains("ENUM('''),DROP TABLE t;-- x')"), sql); + @Test + void fallbackColumnQuotesNameEscapesCommentAndValidatesType() { + TableColumn column = TableColumn.builder() + .name("a`b") + .columnType("CUSTOM_TYPE(10) UNSIGNED") + .comment("it's") + .build(); + assertEquals("`a``b` CUSTOM_TYPE(10) UNSIGNED COMMENT 'it''s'", + MysqlColumnTypeEnum.VARCHAR.buildDefaultColumn(column, true)); + + column.setColumnType("CUSTOM_TYPE); DROP TABLE t;--"); + assertThrows(IllegalArgumentException.class, + () -> MysqlColumnTypeEnum.VARCHAR.buildDefaultColumn(column, true)); } @Test @@ -248,8 +266,8 @@ void dropTableAndDropViewQuoteIdentifiers() { MysqlDBManager manager = new MysqlDBManager(); assertEquals("DROP TABLE `a``;DROP TABLE b;--`", manager.dropTable(null, null, null, "a`;DROP TABLE b;--")); - assertEquals("DROP VIEW `db`.`v`", - String.format(SQL_DROP_VIEW_TEMPLATE, MysqlDBManager.format("db"), MysqlDBManager.format("v"))); + assertEquals("DROP VIEW `db`.`v`", MysqlDBManager.buildDropViewSql("db", "v")); + assertEquals("DROP VIEW `v`", MysqlDBManager.buildDropViewSql(null, "v")); } @Test @@ -297,4 +315,11 @@ void copyTableSqlEscapesBothIdentifiers() { assertEquals("CREATE TABLE `c` AS SELECT * FROM `a``; DROP TABLE b; --`", MysqlDBManager.buildCopyTableSql("a`; DROP TABLE b; --", "c", true)); } + + @Test + void exportTitleValueCannotTerminateCommentLine() { + assertEquals("orders DROP TABLE users;-- ", + MysqlDBManager.formatExportTitleValue("orders\nDROP TABLE users;--\r")); + assertNull(MysqlDBManager.formatExportTitleValue(null)); + } }