From 7a18701cfb146a45091cfb4299fa50b08da073d2 Mon Sep 17 00:00:00 2001
From: HandSonic <8078023+handsonic@users.noreply.github.com>
Date: Sun, 26 Jul 2026 21:27:39 +0800
Subject: [PATCH 1/6] fix(dm): escape SQL identifiers and literals in
metadata/DDL paths (#1914)
---
.../chat2db-community-dm/pom.xml | 5 +
.../ai/chat2db/plugin/dm/DMDBManager.java | 24 +--
.../java/ai/chat2db/plugin/dm/DMMetaData.java | 24 +--
.../ai/chat2db/plugin/dm/DMSqlEscapes.java | 69 ++++++++
.../plugin/dm/builder/DMSqlBuilder.java | 17 +-
.../dm/enums/type/DMColumnTypeEnum.java | 19 +--
.../plugin/dm/enums/type/DMIndexTypeEnum.java | 17 +-
.../chat2db/plugin/dm/DMSqlEscapesTest.java | 148 ++++++++++++++++++
8 files changed, 276 insertions(+), 47 deletions(-)
create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlEscapes.java
create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlEscapesTest.java
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/pom.xml b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/pom.xml
index 3b0e93f665..4a90701922 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/pom.xml
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/pom.xml
@@ -19,6 +19,11 @@
ai.chat2db
chat2db-community-oracle
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
chat2db-community-dm
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
index 04ba0859ba..ca4620ec58 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
@@ -40,7 +40,7 @@ public class DMDBManager extends DefaultDBManager implements IDbManager {
private String format(String tableName) {
- return "\"" + tableName + "\"";
+ return DMSqlEscapes.quoteIdentifier(tableName);
}
@@ -66,7 +66,7 @@ public void exportDatabase(Connection connection, String databaseName, String sc
}
private void exportTables(Connection connection, String databaseName, String schemaName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(SQL_SELECT_TABLE_NAME_ALL_TABLES, schemaName);
+ String sql = String.format(SQL_SELECT_TABLE_NAME_ALL_TABLES, DMSqlEscapes.escapeSqlLiteral(schemaName));
try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
String tableName = resultSet.getString("TABLE_NAME");
@@ -77,7 +77,7 @@ private void exportTables(Connection connection, String databaseName, String sch
@Override
public void exportTable(Connection connection, String databaseName, String schemaName, String tableName, AsyncContext asyncContext) throws SQLException {
- String tableDDLSql = String.format(tableDDL, tableName, schemaName);
+ String tableDDLSql = String.format(tableDDL, DMSqlEscapes.escapeSqlLiteral(tableName), DMSqlEscapes.escapeSqlLiteral(schemaName));
StringBuilder ddlBuilder = new StringBuilder();
DefaultSQLExecutor.getInstance().execute(connection, tableDDLSql, resultSet -> {
if (resultSet.next()) {
@@ -91,7 +91,7 @@ public void exportTable(Connection connection, String databaseName, String schem
String tableComment = tables.get(0).getComment();
if (StringUtils.isNotBlank(tableComment)) {
ddlBuilder.append(SQL_COMMENT_TABLE).append(format(schemaName)).append(".").append(format(tableName))
- .append(" IS '").append(tableComment.replace("'", "''")).append("'").append(";").append("\n");
+ .append(" IS '").append(DMSqlEscapes.escapeSqlLiteral(tableComment)).append("'").append(";").append("\n");
}
}
List columns = metaData.columns(connection,
@@ -103,7 +103,7 @@ public void exportTable(Connection connection, String databaseName, String schem
if (StringUtils.isNotBlank(comment)) {
ddlBuilder.append(SQL_COMMENT_COLUMN).append(format(schemaName)).append(".").append(format(tableName))
.append(".").append(format(columnName)).append(" IS ")
- .append("'").append(comment.replace("'", "''"))
+ .append("'").append(DMSqlEscapes.escapeSqlLiteral(comment))
.append("';").append("\n");
}
}
@@ -135,7 +135,7 @@ public void exportTable(Connection connection, String databaseName, String schem
&& (CollectionUtils.isNotEmpty(uniqueConstraintIndexName) && !uniqueConstraintIndexName.contains(indexName))) {
String sql = "select DBMS_METADATA.GET_DDL('INDEX','%s') as INDEX_DDL";
try {
- DefaultSQLExecutor.getInstance().execute(connection, String.format(sql, indexName), resultSet -> {
+ DefaultSQLExecutor.getInstance().execute(connection, String.format(sql, DMSqlEscapes.escapeSqlLiteral(indexName)), resultSet -> {
if (resultSet.next()) {
ddlBuilder.append(resultSet.getString("INDEX_DDL")).append("\n");
}
@@ -167,7 +167,7 @@ private void exportViews(Connection connection, String schemaName, AsyncContext
}
private void exportView(Connection connection, String viewName, String schemaName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(SQL_SELECT_DBMS_METADATA_GET_DDL, viewName, schemaName);
+ String sql = String.format(SQL_SELECT_DBMS_METADATA_GET_DDL, DMSqlEscapes.escapeSqlLiteral(viewName), DMSqlEscapes.escapeSqlLiteral(schemaName));
try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
StringBuilder sqlBuilder = new StringBuilder();
@@ -187,7 +187,7 @@ private void exportProcedures(Connection connection, String schemaName, AsyncCon
}
private void exportProcedure(Connection connection, String schemaName, String procedureName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(ROUTINES_SQL, "PROC", schemaName, procedureName);
+ String sql = String.format(ROUTINES_SQL, "PROC", DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(procedureName));
try (PreparedStatement statement = connection.prepareStatement(sql); ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
StringBuilder sqlBuilder = new StringBuilder();
@@ -198,7 +198,7 @@ private void exportProcedure(Connection connection, String schemaName, String pr
}
private void exportTriggers(Connection connection, String schemaName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(TRIGGER_SQL_LIST, schemaName);
+ String sql = String.format(TRIGGER_SQL_LIST, DMSqlEscapes.escapeSqlLiteral(schemaName));
try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
String triggerName = resultSet.getString("TRIGGER_NAME");
@@ -208,7 +208,7 @@ private void exportTriggers(Connection connection, String schemaName, AsyncConte
}
private void exportTrigger(Connection connection, String schemaName, String triggerName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(TRIGGER_SQL, schemaName, triggerName);
+ String sql = String.format(TRIGGER_SQL, DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(triggerName));
try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
StringBuilder sqlBuilder = new StringBuilder();
@@ -226,7 +226,7 @@ public void connectDatabase(Connection connection, String database) {
}
String schemaName = connectInfo.getSchemaName();
try {
- DefaultSQLExecutor.getInstance().execute(connection, String.format(SQL_SET_SCHEMA, schemaName));
+ DefaultSQLExecutor.getInstance().execute(connection, String.format(SQL_SET_SCHEMA, DMSqlEscapes.escapeIdentifier(schemaName)));
} catch (SQLException e) {
log.error("connectDatabase error", e);
}
@@ -234,6 +234,6 @@ public void connectDatabase(Connection connection, String database) {
@Override
public String dropTable(Connection connection, String databaseName, String schemaName, String tableName) {
- return String.format(SQL_DROP_TABLE_EXISTS, tableName);
+ return String.format(SQL_DROP_TABLE_EXISTS, DMSqlEscapes.quoteIdentifier(tableName));
}
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
index e041d6a4eb..61e99441b1 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
@@ -50,13 +50,13 @@ public List schemas(Connection connection, String databaseName) {
}
private String format(String tableName) {
- return "\"" + tableName + "\"";
+ return DMSqlEscapes.quoteIdentifier(tableName);
}
protected static String tableDDL = "SELECT dbms_metadata.get_ddl('TABLE', '%s','%s') as ddl FROM dual ;";
public String tableDDL(Connection connection, String databaseName, String schemaName, String tableName) {
- String tableDDLSql = String.format(tableDDL, tableName, schemaName);
+ String tableDDLSql = String.format(tableDDL, DMSqlEscapes.escapeSqlLiteral(tableName), DMSqlEscapes.escapeSqlLiteral(schemaName));
StringBuilder ddlBuilder = new StringBuilder();
DefaultSQLExecutor.getInstance().execute(connection, tableDDLSql, resultSet -> {
if (resultSet.next()) {
@@ -69,7 +69,7 @@ public String tableDDL(Connection connection, String databaseName, String schema
String tableComment = tables.get(0).getComment();
if (StringUtils.isNotBlank(tableComment)) {
ddlBuilder.append(SQL_COMMENT_TABLE).append(format(schemaName)).append(".").append(format(tableName))
- .append(" IS '").append(tableComment.replace("'", "''")).append("'").append(";").append("\n");
+ .append(" IS '").append(DMSqlEscapes.escapeSqlLiteral(tableComment)).append("'").append(";").append("\n");
}
}
List columns = this.columns(connection, databaseName, schemaName, tableName);
@@ -80,7 +80,7 @@ public String tableDDL(Connection connection, String databaseName, String schema
if (StringUtils.isNotBlank(comment)) {
ddlBuilder.append(SQL_COMMENT_COLUMN).append(format(schemaName)).append(".").append(format(tableName))
.append(".").append(format(columnName)).append(" IS ")
- .append("'").append(comment.replace("'", "''"))
+ .append("'").append(DMSqlEscapes.escapeSqlLiteral(comment))
.append("';").append("\n");
}
}
@@ -115,7 +115,7 @@ public String tableDDL(Connection connection, String databaseName, String schema
if (StringUtils.isNotBlank(indexName) && !isPrimaryKey && !isUniqueConstraint) {
String sql = "select DBMS_METADATA.GET_DDL('INDEX','%s') as INDEX_DDL";
try {
- DefaultSQLExecutor.getInstance().execute(connection, String.format(sql, indexName), resultSet -> {
+ DefaultSQLExecutor.getInstance().execute(connection, String.format(sql, DMSqlEscapes.escapeSqlLiteral(indexName)), resultSet -> {
if (resultSet.next()) {
ddlBuilder.append(resultSet.getString("INDEX_DDL")).append("\n");
}
@@ -151,7 +151,7 @@ public List columns(Connection connection, String databaseName, Str
public Function function(Connection connection, @NotEmpty String databaseName, String schemaName,
String functionName) {
- String sql = String.format(ROUTINES_SQL, "PROC", schemaName, functionName);
+ String sql = String.format(ROUTINES_SQL, "PROC", DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(functionName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
StringBuilder sb = new StringBuilder();
while (resultSet.next()) {
@@ -171,7 +171,7 @@ public Function function(Connection connection, @NotEmpty String databaseName, S
@Override
public Procedure procedure(Connection connection, @NotEmpty String databaseName, String schemaName,
String procedureName) {
- String sql = String.format(ROUTINES_SQL, "PROC", schemaName, procedureName);
+ String sql = String.format(ROUTINES_SQL, "PROC", DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(procedureName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
StringBuilder sb = new StringBuilder();
while (resultSet.next()) {
@@ -193,7 +193,7 @@ public Procedure procedure(Connection connection, @NotEmpty String databaseName,
@Override
public List triggers(Connection connection, String databaseName, String schemaName) {
List triggers = new ArrayList<>();
- String sql = String.format(TRIGGER_SQL_LIST, schemaName);
+ String sql = String.format(TRIGGER_SQL_LIST, DMSqlEscapes.escapeSqlLiteral(schemaName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
while (resultSet.next()) {
Trigger trigger = new Trigger();
@@ -210,7 +210,7 @@ public List triggers(Connection connection, String databaseName, String
public Trigger trigger(Connection connection, @NotEmpty String databaseName, String schemaName,
String triggerName) {
- String sql = String.format(TRIGGER_SQL, schemaName, triggerName);
+ String sql = String.format(TRIGGER_SQL, DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(triggerName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
Trigger trigger = new Trigger();
trigger.setDatabaseName(databaseName);
@@ -227,7 +227,7 @@ public Trigger trigger(Connection connection, @NotEmpty String databaseName, Str
@Override
public Table view(Connection connection, String databaseName, String schemaName, String viewName) {
- String sql = String.format(VIEW_SQL, schemaName, viewName);
+ String sql = String.format(VIEW_SQL, DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(viewName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
Table table = new Table();
table.setDatabaseName(databaseName);
@@ -244,7 +244,7 @@ public Table view(Connection connection, String databaseName, String schemaName,
@Override
public List indexes(Connection connection, String databaseName, String schemaName, String tableName) {
- String sql = String.format(INDEX_SQL, schemaName, tableName);
+ String sql = String.format(INDEX_SQL, DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(tableName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
LinkedHashMap map = new LinkedHashMap();
while (resultSet.next()) {
@@ -324,7 +324,7 @@ public ISQLIdentifierProcessor getSQLIdentifierProcessor() {
@Override
public String getMetaDataName(String... names) {
- return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(name -> "\"" + name + "\"").collect(Collectors.joining("."));
+ return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(name -> DMSqlEscapes.quoteIdentifier(name)).collect(Collectors.joining("."));
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlEscapes.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlEscapes.java
new file mode 100644
index 0000000000..c6fa97467f
--- /dev/null
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlEscapes.java
@@ -0,0 +1,69 @@
+package ai.chat2db.plugin.dm;
+
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Escaping helpers for DM SQL text: single-quoted string literals and
+ * double-quoted identifiers.
+ */
+public final class DMSqlEscapes {
+
+ /**
+ * Legitimate column DEFAULT expressions: quoted string literals (with ''
+ * escapes), numeric literals, or keyword/function forms such as
+ * CURRENT_TIMESTAMP, SYSDATE, USER, SEQ.NEXTVAL. Anything else is rejected
+ * because DEFAULT values are emitted verbatim.
+ */
+ private static final Pattern DEFAULT_EXPRESSION = Pattern.compile(
+ "'([^']|'')*'|[+-]?(\\d+(\\.\\d+)?|\\.\\d+)|[A-Za-z_][A-Za-z0-9_]*([.][A-Za-z_][A-Za-z0-9_]*)*(\\s*\\([^;)]*\\))?");
+
+ private DMSqlEscapes() {
+ }
+
+ /**
+ * Escapes a value interpolated into a single-quoted SQL string literal by
+ * doubling every single quote.
+ */
+ public static String escapeSqlLiteral(String value) {
+ return StringUtils.replace(value, "'", "''");
+ }
+
+ /**
+ * Escapes identifier content for a position already surrounded by double
+ * quotes: strips one surrounding quote pair, then doubles every embedded
+ * double quote.
+ */
+ public static String escapeIdentifier(String identifier) {
+ if (identifier == null) {
+ return null;
+ }
+ String unquoted = identifier;
+ if (unquoted.length() >= 2 && unquoted.startsWith("\"") && unquoted.endsWith("\"")) {
+ unquoted = unquoted.substring(1, unquoted.length() - 1);
+ }
+ return unquoted.replace("\"", "\"\"");
+ }
+
+ /**
+ * Quotes an identifier with double quotes, doubling every embedded double
+ * quote.
+ */
+ public static String quoteIdentifier(String identifier) {
+ return "\"" + escapeIdentifier(identifier) + "\"";
+ }
+
+ /**
+ * Validates a column DEFAULT expression that is emitted verbatim into DDL.
+ * Accepts quoted string literals (escaped via doubling), numeric literals,
+ * and keyword/function forms; rejects everything else.
+ */
+ public static String requireDefaultExpression(String defaultValue) {
+ String trimmed = defaultValue.trim();
+ if (!DEFAULT_EXPRESSION.matcher(trimmed).matches()) {
+ throw new IllegalArgumentException("Invalid DM default expression: " + defaultValue);
+ }
+ return trimmed;
+ }
+}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
index 4920f62517..94a03130cd 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
@@ -2,6 +2,7 @@
import ai.chat2db.spi.constant.SQLConstants;
+import ai.chat2db.plugin.dm.DMSqlEscapes;
import ai.chat2db.plugin.dm.enums.type.DMColumnTypeEnum;
import ai.chat2db.plugin.dm.enums.type.DMIndexTypeEnum;
import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum;
@@ -42,7 +43,7 @@ public class DMSqlBuilder extends DefaultSqlBuilder {
public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfig) {
StringBuilder script = new StringBuilder();
- script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(table.getSchemaName()).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(table.getName()).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
+ script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
for (TableColumn column : table.getColumnList()) {
if (StringUtils.isBlank(column.getName()) || StringUtils.isBlank(column.getColumnType())) {
@@ -98,7 +99,7 @@ public String buildAITableSchema(Table table) {
}
StringBuilder script = new StringBuilder();
- script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(table.getSchemaName()).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(table.getName()).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
+ script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
for (TableColumn column : table.getColumnList()) {
if (StringUtils.isBlank(column.getName()) || StringUtils.isBlank(column.getColumnType())) {
@@ -136,13 +137,13 @@ public String buildAITableSchema(Table table) {
private String buildTableComment(Table table) {
StringBuilder script = new StringBuilder();
- script.append(SQL_COMMENT_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(table.getSchemaName()).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(table.getName()).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(table.getComment()).append(SQLConstants.SINGLE_QUOTE);
+ script.append(SQL_COMMENT_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(DMSqlEscapes.escapeSqlLiteral(table.getComment())).append(SQLConstants.SINGLE_QUOTE);
return script.toString();
}
private String buildComment(TableColumn column) {
StringBuilder script = new StringBuilder();
- script.append(SQL_COMMENT_COLUMN).append(SQLConstants.DOUBLE_QUOTE).append(column.getSchemaName()).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(column.getTableName()).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(column.getName()).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(column.getComment()).append(SQLConstants.SINGLE_QUOTE);
+ script.append(SQL_COMMENT_COLUMN).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(column.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(column.getTableName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(column.getName())).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(DMSqlEscapes.escapeSqlLiteral(column.getComment())).append(SQLConstants.SINGLE_QUOTE);
return script.toString();
}
@@ -151,8 +152,8 @@ public String buildAlterTable(Table oldTable, Table newTable) {
StringBuilder script = new StringBuilder();
if (!StringUtils.equalsIgnoreCase(oldTable.getName(), newTable.getName())) {
- script.append(SQL_ALTER_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(oldTable.getSchemaName()).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(oldTable.getName()).append(SQLConstants.DOUBLE_QUOTE);
- script.append(SQLConstants.SPACE).append(SQL_RENAME).append(SQLConstants.DOUBLE_QUOTE).append(newTable.getName()).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
+ script.append(SQL_ALTER_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(oldTable.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(oldTable.getName())).append(SQLConstants.DOUBLE_QUOTE);
+ script.append(SQLConstants.SPACE).append(SQL_RENAME).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(newTable.getName())).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
}
if (!StringUtils.equalsIgnoreCase(oldTable.getComment(), newTable.getComment())) {
script.append(SQLConstants.EMPTY).append(buildTableComment(newTable)).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
@@ -211,9 +212,9 @@ public String buildPageLimit(PageLimitRequest request) {
@Override
public String buildCreateSchema(Schema schema) {
StringBuilder sqlBuilder = new StringBuilder();
- sqlBuilder.append(SQL_CREATE_SCHEMA+schema.getName()+SQLConstants.DOUBLE_QUOTE);
+ sqlBuilder.append(SQL_CREATE_SCHEMA+DMSqlEscapes.escapeIdentifier(schema.getName())+SQLConstants.DOUBLE_QUOTE);
if(StringUtils.isNotBlank(schema.getOwner())){
- sqlBuilder.append(SQLConstants.SCHEMA_AUTHORIZATION_SQL).append(schema.getOwner());
+ sqlBuilder.append(SQLConstants.SCHEMA_AUTHORIZATION_SQL).append(DMSqlEscapes.quoteIdentifier(schema.getOwner()));
}
return sqlBuilder.toString();
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
index 55e02416a6..0f9cd053c3 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
@@ -1,5 +1,6 @@
package ai.chat2db.plugin.dm.enums.type;
+import ai.chat2db.plugin.dm.DMSqlEscapes;
import ai.chat2db.spi.IColumnBuilder;
import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum;
import ai.chat2db.community.domain.api.model.metadata.ColumnType;
@@ -158,7 +159,7 @@ public String buildCreateColumnSql(TableColumn column) {
}
StringBuilder script = new StringBuilder();
- script.append("\"").append(column.getName()).append("\"").append(" ");
+ script.append(DMSqlEscapes.quoteIdentifier(column.getName())).append(" ");
script.append(buildDataType(column, type)).append(" ");
@@ -179,7 +180,7 @@ public String buildAICreateColumnSql(TableColumn column) {
}
StringBuilder script = new StringBuilder();
- script.append("\"").append(column.getName()).append("\"").append(" ");
+ script.append(DMSqlEscapes.quoteIdentifier(column.getName())).append(" ");
script.append(buildDataType(column, type)).append(" ");
@@ -232,7 +233,7 @@ private String buildDefaultValue(TableColumn column, DMColumnTypeEnum type) {
return StringUtils.join("DEFAULT NULL");
}
- return StringUtils.join("DEFAULT ", column.getDefaultValue());
+ return StringUtils.join("DEFAULT ", DMSqlEscapes.requireDefaultExpression(column.getDefaultValue()));
}
private String buildDataType(TableColumn column, DMColumnTypeEnum type) {
@@ -297,25 +298,25 @@ public String buildModifyColumn(TableColumn tableColumn) {
if (EditStatusEnum.DELETE.name().equals(tableColumn.getEditStatus())) {
StringBuilder script = new StringBuilder();
- script.append(SQL_ALTER_TABLE).append("\"").append(tableColumn.getSchemaName()).append("\".\"").append(tableColumn.getTableName()).append("\"");
- script.append(" ").append(SQL_DROP_COLUMN).append("\"").append(tableColumn.getName()).append("\"");
+ script.append(SQL_ALTER_TABLE).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getTableName())).append("\"");
+ script.append(" ").append(SQL_DROP_COLUMN).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getName())).append("\"");
return script.toString();
}
if (EditStatusEnum.ADD.name().equals(tableColumn.getEditStatus())) {
StringBuilder script = new StringBuilder();
- script.append(SQL_ALTER_TABLE).append("\"").append(tableColumn.getSchemaName()).append("\".\"").append(tableColumn.getTableName()).append("\"");
+ script.append(SQL_ALTER_TABLE).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getTableName())).append("\"");
script.append(" ").append("ADD (").append(buildCreateColumnSql(tableColumn)).append(")");
return script.toString();
}
if (EditStatusEnum.MODIFY.name().equals(tableColumn.getEditStatus())) {
StringBuilder script = new StringBuilder();
if (!StringUtils.equals(tableColumn.getOldName(), tableColumn.getName())) {
- script.append(SQL_ALTER_TABLE).append("\"").append(tableColumn.getSchemaName()).append("\".\"").append(tableColumn.getTableName()).append("\"");
- script.append(" ").append(SQL_RENAME_COLUMN).append("\"").append(tableColumn.getOldName()).append("\"").append(" TO ").append("\"").append(tableColumn.getName()).append("\"");
+ script.append(SQL_ALTER_TABLE).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getTableName())).append("\"");
+ script.append(" ").append(SQL_RENAME_COLUMN).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getOldName())).append("\"").append(" TO ").append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getName())).append("\"");
script.append(";\n");
}
- script.append(SQL_ALTER_TABLE).append("\"").append(tableColumn.getSchemaName()).append("\".\"").append(tableColumn.getTableName()).append("\"");
+ script.append(SQL_ALTER_TABLE).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getTableName())).append("\"");
script.append(" ").append("MODIFY (").append(buildCreateColumnSql(tableColumn)).append(") \n");
return script.toString();
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
index 873a931460..6a1fbfe25d 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
@@ -1,5 +1,6 @@
package ai.chat2db.plugin.dm.enums.type;
+import ai.chat2db.plugin.dm.DMSqlEscapes;
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;
@@ -72,14 +73,14 @@ public static DMIndexTypeEnum getByType(String type) {
public String buildIndexScript(TableIndex tableIndex) {
StringBuilder script = new StringBuilder();
if (PRIMARY_KEY.equals(this)) {
- script.append(SQL_ALTER_TABLE_2).append(tableIndex.getSchemaName()).append("\".\"").append(tableIndex.getTableName()).append("\" ADD PRIMARY KEY ").append(buildIndexColumn(tableIndex));
+ script.append(SQL_ALTER_TABLE_2).append(DMSqlEscapes.escapeIdentifier(tableIndex.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableIndex.getTableName())).append("\" ADD PRIMARY KEY ").append(buildIndexColumn(tableIndex));
} else {
if (UNIQUE.equals(this)) {
script.append(SQL_CREATE_UNIQUE_INDEX);
} else {
script.append(SQL_CREATE_INDEX);
}
- script.append(buildIndexName(tableIndex)).append(SQL_ON).append(tableIndex.getSchemaName()).append("\".\"").append(tableIndex.getTableName()).append("\" ").append(buildIndexColumn(tableIndex));
+ script.append(buildIndexName(tableIndex)).append(SQL_ON).append(DMSqlEscapes.escapeIdentifier(tableIndex.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableIndex.getTableName())).append("\" ").append(buildIndexColumn(tableIndex));
}
return script.toString();
}
@@ -90,9 +91,13 @@ 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(DMSqlEscapes.quoteIdentifier(column.getColumnName()));
if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) {
- script.append(" ").append(column.getAscOrDesc());
+ String ascOrDesc = column.getAscOrDesc();
+ if (!"ASC".equalsIgnoreCase(ascOrDesc) && !"DESC".equalsIgnoreCase(ascOrDesc)) {
+ throw new IllegalArgumentException("Invalid index column sort order: " + ascOrDesc);
+ }
+ script.append(" ").append(ascOrDesc);
}
script.append(",");
}
@@ -103,7 +108,7 @@ private String buildIndexColumn(TableIndex tableIndex) {
}
private String buildIndexName(TableIndex tableIndex) {
- return "\"" + tableIndex.getSchemaName() + "\"." + "\"" + tableIndex.getName() + "\"";
+ return DMSqlEscapes.quoteIdentifier(tableIndex.getSchemaName()) + "." + DMSqlEscapes.quoteIdentifier(tableIndex.getName());
}
public String buildModifyIndex(TableIndex tableIndex) {
@@ -121,7 +126,7 @@ public String buildModifyIndex(TableIndex tableIndex) {
private String buildDropIndex(TableIndex tableIndex) {
if (DMIndexTypeEnum.PRIMARY_KEY.getName().equals(tableIndex.getType())) {
- String tableName = "\"" + tableIndex.getSchemaName() + "\"." + "\"" + tableIndex.getTableName() + "\"";
+ String tableName = DMSqlEscapes.quoteIdentifier(tableIndex.getSchemaName()) + "." + DMSqlEscapes.quoteIdentifier(tableIndex.getTableName());
return StringUtils.join(SQL_ALTER_TABLE,tableName,SQL_DROP_PRIMARY_KEY);
}
StringBuilder script = new StringBuilder();
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlEscapesTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlEscapesTest.java
new file mode 100644
index 0000000000..42f4dd4c15
--- /dev/null
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlEscapesTest.java
@@ -0,0 +1,148 @@
+package ai.chat2db.plugin.dm;
+
+import ai.chat2db.community.domain.api.model.metadata.Schema;
+import ai.chat2db.community.domain.api.model.metadata.Table;
+import ai.chat2db.community.domain.api.model.metadata.TableColumn;
+import ai.chat2db.community.domain.api.model.metadata.TableIndex;
+import ai.chat2db.community.domain.api.model.metadata.TableIndexColumn;
+import ai.chat2db.plugin.dm.builder.DMSqlBuilder;
+import ai.chat2db.plugin.dm.enums.type.DMColumnTypeEnum;
+import ai.chat2db.plugin.dm.enums.type.DMIndexTypeEnum;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DMSqlEscapesTest {
+
+ @Test
+ void escapeSqlLiteralDoublesSingleQuotes() {
+ assertEquals("O''Brien", DMSqlEscapes.escapeSqlLiteral("O'Brien"));
+ assertEquals("x'' OR ''1''=''1", DMSqlEscapes.escapeSqlLiteral("x' OR '1'='1"));
+ assertEquals("plain", DMSqlEscapes.escapeSqlLiteral("plain"));
+ }
+
+ @Test
+ void quoteIdentifierDoublesEmbeddedDoubleQuotes() {
+ assertEquals("\"plain\"", DMSqlEscapes.quoteIdentifier("plain"));
+ assertEquals("\"we\"\"ird\"", DMSqlEscapes.quoteIdentifier("we\"ird"));
+ }
+
+ @Test
+ void escapeIdentifierStripsOneSurroundingQuotePairBeforeDoubling() {
+ assertEquals("already", DMSqlEscapes.escapeIdentifier("\"already\""));
+ assertEquals("a\"\"b", DMSqlEscapes.escapeIdentifier("a\"b"));
+ assertEquals("plain", DMSqlEscapes.escapeIdentifier("plain"));
+ }
+
+ @Test
+ void createTableSqlNeutralizesMaliciousSchemaAndComment() {
+ Table table = new Table();
+ table.setSchemaName("s\"; DROP TABLE t; --");
+ table.setName("tab");
+ table.setComment("x'; DROP TABLE t; --");
+ TableColumn column = new TableColumn();
+ column.setName("c1");
+ column.setColumnType("INT");
+ table.setColumnList(List.of(column));
+ table.setIndexList(List.of());
+
+ String sql = new DMSqlBuilder().buildCreateTable(table, null);
+
+ assertTrue(sql.contains("\"s\"\"; DROP TABLE t; --\".\"tab\""));
+ assertFalse(sql.contains("\"s\"; DROP TABLE t; --\""));
+ assertTrue(sql.contains("IS 'x''; DROP TABLE t; --'"));
+ assertFalse(sql.contains("IS 'x'; DROP TABLE t; --'"));
+ }
+
+ @Test
+ void dropTableQuotesAndEscapesTableName() {
+ String sql = new DMDBManager().dropTable(null, null, null, "a\"; DROP TABLE b; --");
+
+ assertEquals("DROP TABLE IF EXISTS \"a\"\"; DROP TABLE b; --\"", sql);
+ }
+
+ @Test
+ void metaDataNameEscapesEachIdentifierPart() {
+ String name = new DMMetaData().getMetaDataName("sch\"ema", "ta\"ble");
+
+ assertEquals("\"sch\"\"ema\".\"ta\"\"ble\"", name);
+ }
+
+ @Test
+ void createSchemaQuotesOwnerAsIdentifier() {
+ Schema schema = new Schema();
+ schema.setName("app");
+ schema.setOwner("owner; DROP USER x; --");
+
+ String sql = new DMSqlBuilder().buildCreateSchema(schema);
+
+ assertEquals("CREATE SCHEMA \"app\" AUTHORIZATION \"owner; DROP USER x; --\"", sql);
+ }
+
+ @Test
+ void indexSortOrderAcceptsAscDescAndRejectsInjection() {
+ TableIndex index = new TableIndex();
+ index.setSchemaName("s");
+ index.setTableName("t");
+ index.setName("i");
+ TableIndexColumn column = new TableIndexColumn();
+ column.setColumnName("c");
+ column.setAscOrDesc("desc");
+ index.setColumnList(List.of(column));
+
+ assertTrue(DMIndexTypeEnum.NORMAL.buildIndexScript(index).contains("\"c\" desc"));
+
+ column.setAscOrDesc("DESC; DROP TABLE x; --");
+ assertThrows(IllegalArgumentException.class, () -> DMIndexTypeEnum.NORMAL.buildIndexScript(index));
+ }
+
+ @Test
+ void quotedStringDefaultsAndUnitPassThroughUnchanged() {
+ TableColumn column = new TableColumn();
+ column.setName("c1");
+ column.setColumnType("VARCHAR");
+ column.setColumnSize(10);
+ column.setUnit("BYTE");
+ column.setDefaultValue("'O''Brien'");
+
+ String sql = DMColumnTypeEnum.VARCHAR.buildCreateColumnSql(column);
+
+ assertTrue(sql.contains("VARCHAR(10 BYTE)"));
+ assertTrue(sql.contains("DEFAULT 'O''Brien'"));
+
+ TableColumn emptyStringDefault = new TableColumn();
+ emptyStringDefault.setName("c2");
+ emptyStringDefault.setColumnType("VARCHAR");
+ emptyStringDefault.setDefaultValue("EMPTY_STRING");
+
+ assertTrue(DMColumnTypeEnum.VARCHAR.buildCreateColumnSql(emptyStringDefault).contains("DEFAULT ''"));
+ }
+
+ @Test
+ void defaultExpressionAcceptsLegitimateAndRejectsInjection() {
+ org.junit.jupiter.api.Assertions.assertEquals("'abc'", DMSqlEscapes.requireDefaultExpression("'abc'"));
+ org.junit.jupiter.api.Assertions.assertEquals("'O''Brien'", DMSqlEscapes.requireDefaultExpression("'O''Brien'"));
+ org.junit.jupiter.api.Assertions.assertEquals("-1.5", DMSqlEscapes.requireDefaultExpression("-1.5"));
+ org.junit.jupiter.api.Assertions.assertEquals("CURRENT_TIMESTAMP", DMSqlEscapes.requireDefaultExpression("CURRENT_TIMESTAMP"));
+ org.junit.jupiter.api.Assertions.assertEquals("SEQ.NEXTVAL", DMSqlEscapes.requireDefaultExpression("SEQ.NEXTVAL"));
+ org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
+ () -> DMSqlEscapes.requireDefaultExpression("1; DROP TABLE t"));
+ org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
+ () -> DMSqlEscapes.requireDefaultExpression("x' OR '1'='1"));
+ }
+
+ @Test
+ void createColumnRejectsMaliciousDefault() {
+ TableColumn column = new TableColumn();
+ column.setName("c");
+ column.setColumnType("INT");
+ column.setDefaultValue("1; DROP TABLE t;--");
+ org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
+ () -> DMColumnTypeEnum.INT.buildCreateColumnSql(column));
+ }
+}
From 4d5a318a2840a2767716e3c1c7285a22c2c2086f Mon Sep 17 00:00:00 2001
From: HandSonic <8078023+handsonic@users.noreply.github.com>
Date: Mon, 27 Jul 2026 02:00:07 +0800
Subject: [PATCH 2/6] refactor(dm): move escaping into DMIdentifierProcessor
per maintainer review (#1914)
- strengthen DMIdentifierProcessor (SPI ISQLIdentifierProcessor): always-quote
quoteIdentifier with embedded-quote doubling, escapeString with single-quote
doubling, static escapeIdentifier for pre-quoted templates, INSTANCE
- DMMetaData.getSQLIdentifierProcessor() returns the shared INSTANCE; metadata
call sites use it (quoteIdentifier / escapeString / method refs)
- builders/DBManager/enums use DMIdentifierProcessor.INSTANCE / static escapeIdentifier
- non-escapable DEFAULT-expression validation moved to public DMSqlGuards
- DMSqlEscapes removed; tests migrated to DMIdentifierProcessorTest (11 green)
---
.../ai/chat2db/plugin/dm/DMDBManager.java | 25 +++----
.../java/ai/chat2db/plugin/dm/DMMetaData.java | 30 ++++----
.../ai/chat2db/plugin/dm/DMSqlEscapes.java | 69 -------------------
.../ai/chat2db/plugin/dm/DMSqlGuards.java | 36 ++++++++++
.../plugin/dm/builder/DMSqlBuilder.java | 18 ++---
.../dm/enums/type/DMColumnTypeEnum.java | 21 +++---
.../plugin/dm/enums/type/DMIndexTypeEnum.java | 12 ++--
.../dm/identifier/DMIdentifierProcessor.java | 64 ++++++++++-------
...st.java => DMIdentifierProcessorTest.java} | 33 ++++-----
9 files changed, 146 insertions(+), 162 deletions(-)
delete mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlEscapes.java
create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
rename chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/{DMSqlEscapesTest.java => DMIdentifierProcessorTest.java} (80%)
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
index ca4620ec58..737840def5 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
@@ -1,6 +1,7 @@
package ai.chat2db.plugin.dm;
import ai.chat2db.plugin.dm.enums.type.DMIndexTypeEnum;
+import ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor;
import ai.chat2db.spi.IDbManager;
import ai.chat2db.spi.IDbMetaData;
import ai.chat2db.spi.DefaultDBManager;
@@ -40,7 +41,7 @@ public class DMDBManager extends DefaultDBManager implements IDbManager {
private String format(String tableName) {
- return DMSqlEscapes.quoteIdentifier(tableName);
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableName);
}
@@ -66,7 +67,7 @@ public void exportDatabase(Connection connection, String databaseName, String sc
}
private void exportTables(Connection connection, String databaseName, String schemaName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(SQL_SELECT_TABLE_NAME_ALL_TABLES, DMSqlEscapes.escapeSqlLiteral(schemaName));
+ String sql = String.format(SQL_SELECT_TABLE_NAME_ALL_TABLES, DMIdentifierProcessor.INSTANCE.escapeString(schemaName));
try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
String tableName = resultSet.getString("TABLE_NAME");
@@ -77,7 +78,7 @@ private void exportTables(Connection connection, String databaseName, String sch
@Override
public void exportTable(Connection connection, String databaseName, String schemaName, String tableName, AsyncContext asyncContext) throws SQLException {
- String tableDDLSql = String.format(tableDDL, DMSqlEscapes.escapeSqlLiteral(tableName), DMSqlEscapes.escapeSqlLiteral(schemaName));
+ String tableDDLSql = String.format(tableDDL, DMIdentifierProcessor.INSTANCE.escapeString(tableName), DMIdentifierProcessor.INSTANCE.escapeString(schemaName));
StringBuilder ddlBuilder = new StringBuilder();
DefaultSQLExecutor.getInstance().execute(connection, tableDDLSql, resultSet -> {
if (resultSet.next()) {
@@ -91,7 +92,7 @@ public void exportTable(Connection connection, String databaseName, String schem
String tableComment = tables.get(0).getComment();
if (StringUtils.isNotBlank(tableComment)) {
ddlBuilder.append(SQL_COMMENT_TABLE).append(format(schemaName)).append(".").append(format(tableName))
- .append(" IS '").append(DMSqlEscapes.escapeSqlLiteral(tableComment)).append("'").append(";").append("\n");
+ .append(" IS '").append(DMIdentifierProcessor.INSTANCE.escapeString(tableComment)).append("'").append(";").append("\n");
}
}
List columns = metaData.columns(connection,
@@ -103,7 +104,7 @@ public void exportTable(Connection connection, String databaseName, String schem
if (StringUtils.isNotBlank(comment)) {
ddlBuilder.append(SQL_COMMENT_COLUMN).append(format(schemaName)).append(".").append(format(tableName))
.append(".").append(format(columnName)).append(" IS ")
- .append("'").append(DMSqlEscapes.escapeSqlLiteral(comment))
+ .append("'").append(DMIdentifierProcessor.INSTANCE.escapeString(comment))
.append("';").append("\n");
}
}
@@ -135,7 +136,7 @@ public void exportTable(Connection connection, String databaseName, String schem
&& (CollectionUtils.isNotEmpty(uniqueConstraintIndexName) && !uniqueConstraintIndexName.contains(indexName))) {
String sql = "select DBMS_METADATA.GET_DDL('INDEX','%s') as INDEX_DDL";
try {
- DefaultSQLExecutor.getInstance().execute(connection, String.format(sql, DMSqlEscapes.escapeSqlLiteral(indexName)), resultSet -> {
+ DefaultSQLExecutor.getInstance().execute(connection, String.format(sql, DMIdentifierProcessor.INSTANCE.escapeString(indexName)), resultSet -> {
if (resultSet.next()) {
ddlBuilder.append(resultSet.getString("INDEX_DDL")).append("\n");
}
@@ -167,7 +168,7 @@ private void exportViews(Connection connection, String schemaName, AsyncContext
}
private void exportView(Connection connection, String viewName, String schemaName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(SQL_SELECT_DBMS_METADATA_GET_DDL, DMSqlEscapes.escapeSqlLiteral(viewName), DMSqlEscapes.escapeSqlLiteral(schemaName));
+ String sql = String.format(SQL_SELECT_DBMS_METADATA_GET_DDL, DMIdentifierProcessor.INSTANCE.escapeString(viewName), DMIdentifierProcessor.INSTANCE.escapeString(schemaName));
try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
StringBuilder sqlBuilder = new StringBuilder();
@@ -187,7 +188,7 @@ private void exportProcedures(Connection connection, String schemaName, AsyncCon
}
private void exportProcedure(Connection connection, String schemaName, String procedureName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(ROUTINES_SQL, "PROC", DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(procedureName));
+ String sql = String.format(ROUTINES_SQL, "PROC", DMIdentifierProcessor.INSTANCE.escapeString(schemaName), DMIdentifierProcessor.INSTANCE.escapeString(procedureName));
try (PreparedStatement statement = connection.prepareStatement(sql); ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
StringBuilder sqlBuilder = new StringBuilder();
@@ -198,7 +199,7 @@ private void exportProcedure(Connection connection, String schemaName, String pr
}
private void exportTriggers(Connection connection, String schemaName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(TRIGGER_SQL_LIST, DMSqlEscapes.escapeSqlLiteral(schemaName));
+ String sql = String.format(TRIGGER_SQL_LIST, DMIdentifierProcessor.INSTANCE.escapeString(schemaName));
try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
String triggerName = resultSet.getString("TRIGGER_NAME");
@@ -208,7 +209,7 @@ private void exportTriggers(Connection connection, String schemaName, AsyncConte
}
private void exportTrigger(Connection connection, String schemaName, String triggerName, AsyncContext asyncContext) throws SQLException {
- String sql = String.format(TRIGGER_SQL, DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(triggerName));
+ String sql = String.format(TRIGGER_SQL, DMIdentifierProcessor.INSTANCE.escapeString(schemaName), DMIdentifierProcessor.INSTANCE.escapeString(triggerName));
try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
StringBuilder sqlBuilder = new StringBuilder();
@@ -226,7 +227,7 @@ public void connectDatabase(Connection connection, String database) {
}
String schemaName = connectInfo.getSchemaName();
try {
- DefaultSQLExecutor.getInstance().execute(connection, String.format(SQL_SET_SCHEMA, DMSqlEscapes.escapeIdentifier(schemaName)));
+ DefaultSQLExecutor.getInstance().execute(connection, String.format(SQL_SET_SCHEMA, DMIdentifierProcessor.escapeIdentifier(schemaName)));
} catch (SQLException e) {
log.error("connectDatabase error", e);
}
@@ -234,6 +235,6 @@ public void connectDatabase(Connection connection, String database) {
@Override
public String dropTable(Connection connection, String databaseName, String schemaName, String tableName) {
- return String.format(SQL_DROP_TABLE_EXISTS, DMSqlEscapes.quoteIdentifier(tableName));
+ return String.format(SQL_DROP_TABLE_EXISTS, DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableName));
}
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
index 61e99441b1..0e5455be28 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
@@ -39,10 +39,6 @@
@Slf4j
public class DMMetaData extends DefaultMetaService implements IDbMetaData {
-
-
- private static final ISQLIdentifierProcessor DM_IDENTIFIER_PROCESSOR = new DMIdentifierProcessor();
-
@Override
public List schemas(Connection connection, String databaseName) {
List schemas = DefaultSQLExecutor.getInstance().schemas(connection, databaseName, null);
@@ -50,13 +46,13 @@ public List schemas(Connection connection, String databaseName) {
}
private String format(String tableName) {
- return DMSqlEscapes.quoteIdentifier(tableName);
+ return getSQLIdentifierProcessor().quoteIdentifier(tableName);
}
protected static String tableDDL = "SELECT dbms_metadata.get_ddl('TABLE', '%s','%s') as ddl FROM dual ;";
public String tableDDL(Connection connection, String databaseName, String schemaName, String tableName) {
- String tableDDLSql = String.format(tableDDL, DMSqlEscapes.escapeSqlLiteral(tableName), DMSqlEscapes.escapeSqlLiteral(schemaName));
+ String tableDDLSql = String.format(tableDDL, getSQLIdentifierProcessor().escapeString(tableName), getSQLIdentifierProcessor().escapeString(schemaName));
StringBuilder ddlBuilder = new StringBuilder();
DefaultSQLExecutor.getInstance().execute(connection, tableDDLSql, resultSet -> {
if (resultSet.next()) {
@@ -69,7 +65,7 @@ public String tableDDL(Connection connection, String databaseName, String schema
String tableComment = tables.get(0).getComment();
if (StringUtils.isNotBlank(tableComment)) {
ddlBuilder.append(SQL_COMMENT_TABLE).append(format(schemaName)).append(".").append(format(tableName))
- .append(" IS '").append(DMSqlEscapes.escapeSqlLiteral(tableComment)).append("'").append(";").append("\n");
+ .append(" IS '").append(getSQLIdentifierProcessor().escapeString(tableComment)).append("'").append(";").append("\n");
}
}
List columns = this.columns(connection, databaseName, schemaName, tableName);
@@ -80,7 +76,7 @@ public String tableDDL(Connection connection, String databaseName, String schema
if (StringUtils.isNotBlank(comment)) {
ddlBuilder.append(SQL_COMMENT_COLUMN).append(format(schemaName)).append(".").append(format(tableName))
.append(".").append(format(columnName)).append(" IS ")
- .append("'").append(DMSqlEscapes.escapeSqlLiteral(comment))
+ .append("'").append(getSQLIdentifierProcessor().escapeString(comment))
.append("';").append("\n");
}
}
@@ -115,7 +111,7 @@ public String tableDDL(Connection connection, String databaseName, String schema
if (StringUtils.isNotBlank(indexName) && !isPrimaryKey && !isUniqueConstraint) {
String sql = "select DBMS_METADATA.GET_DDL('INDEX','%s') as INDEX_DDL";
try {
- DefaultSQLExecutor.getInstance().execute(connection, String.format(sql, DMSqlEscapes.escapeSqlLiteral(indexName)), resultSet -> {
+ DefaultSQLExecutor.getInstance().execute(connection, String.format(sql, getSQLIdentifierProcessor().escapeString(indexName)), resultSet -> {
if (resultSet.next()) {
ddlBuilder.append(resultSet.getString("INDEX_DDL")).append("\n");
}
@@ -151,7 +147,7 @@ public List columns(Connection connection, String databaseName, Str
public Function function(Connection connection, @NotEmpty String databaseName, String schemaName,
String functionName) {
- String sql = String.format(ROUTINES_SQL, "PROC", DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(functionName));
+ String sql = String.format(ROUTINES_SQL, "PROC", getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(functionName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
StringBuilder sb = new StringBuilder();
while (resultSet.next()) {
@@ -171,7 +167,7 @@ public Function function(Connection connection, @NotEmpty String databaseName, S
@Override
public Procedure procedure(Connection connection, @NotEmpty String databaseName, String schemaName,
String procedureName) {
- String sql = String.format(ROUTINES_SQL, "PROC", DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(procedureName));
+ String sql = String.format(ROUTINES_SQL, "PROC", getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(procedureName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
StringBuilder sb = new StringBuilder();
while (resultSet.next()) {
@@ -193,7 +189,7 @@ public Procedure procedure(Connection connection, @NotEmpty String databaseName,
@Override
public List triggers(Connection connection, String databaseName, String schemaName) {
List triggers = new ArrayList<>();
- String sql = String.format(TRIGGER_SQL_LIST, DMSqlEscapes.escapeSqlLiteral(schemaName));
+ String sql = String.format(TRIGGER_SQL_LIST, getSQLIdentifierProcessor().escapeString(schemaName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
while (resultSet.next()) {
Trigger trigger = new Trigger();
@@ -210,7 +206,7 @@ public List triggers(Connection connection, String databaseName, String
public Trigger trigger(Connection connection, @NotEmpty String databaseName, String schemaName,
String triggerName) {
- String sql = String.format(TRIGGER_SQL, DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(triggerName));
+ String sql = String.format(TRIGGER_SQL, getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(triggerName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
Trigger trigger = new Trigger();
trigger.setDatabaseName(databaseName);
@@ -227,7 +223,7 @@ public Trigger trigger(Connection connection, @NotEmpty String databaseName, Str
@Override
public Table view(Connection connection, String databaseName, String schemaName, String viewName) {
- String sql = String.format(VIEW_SQL, DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(viewName));
+ String sql = String.format(VIEW_SQL, getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(viewName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
Table table = new Table();
table.setDatabaseName(databaseName);
@@ -244,7 +240,7 @@ public Table view(Connection connection, String databaseName, String schemaName,
@Override
public List indexes(Connection connection, String databaseName, String schemaName, String tableName) {
- String sql = String.format(INDEX_SQL, DMSqlEscapes.escapeSqlLiteral(schemaName), DMSqlEscapes.escapeSqlLiteral(tableName));
+ String sql = String.format(INDEX_SQL, getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(tableName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
LinkedHashMap map = new LinkedHashMap();
while (resultSet.next()) {
@@ -319,12 +315,12 @@ public IValueProcessor getValueProcessor() {
@Override
public ISQLIdentifierProcessor getSQLIdentifierProcessor() {
- return DM_IDENTIFIER_PROCESSOR;
+ return DMIdentifierProcessor.INSTANCE;
}
@Override
public String getMetaDataName(String... names) {
- return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(name -> DMSqlEscapes.quoteIdentifier(name)).collect(Collectors.joining("."));
+ return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(getSQLIdentifierProcessor()::quoteIdentifier).collect(Collectors.joining("."));
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlEscapes.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlEscapes.java
deleted file mode 100644
index c6fa97467f..0000000000
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlEscapes.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package ai.chat2db.plugin.dm;
-
-import java.util.regex.Pattern;
-
-import org.apache.commons.lang3.StringUtils;
-
-/**
- * Escaping helpers for DM SQL text: single-quoted string literals and
- * double-quoted identifiers.
- */
-public final class DMSqlEscapes {
-
- /**
- * Legitimate column DEFAULT expressions: quoted string literals (with ''
- * escapes), numeric literals, or keyword/function forms such as
- * CURRENT_TIMESTAMP, SYSDATE, USER, SEQ.NEXTVAL. Anything else is rejected
- * because DEFAULT values are emitted verbatim.
- */
- private static final Pattern DEFAULT_EXPRESSION = Pattern.compile(
- "'([^']|'')*'|[+-]?(\\d+(\\.\\d+)?|\\.\\d+)|[A-Za-z_][A-Za-z0-9_]*([.][A-Za-z_][A-Za-z0-9_]*)*(\\s*\\([^;)]*\\))?");
-
- private DMSqlEscapes() {
- }
-
- /**
- * Escapes a value interpolated into a single-quoted SQL string literal by
- * doubling every single quote.
- */
- public static String escapeSqlLiteral(String value) {
- return StringUtils.replace(value, "'", "''");
- }
-
- /**
- * Escapes identifier content for a position already surrounded by double
- * quotes: strips one surrounding quote pair, then doubles every embedded
- * double quote.
- */
- public static String escapeIdentifier(String identifier) {
- if (identifier == null) {
- return null;
- }
- String unquoted = identifier;
- if (unquoted.length() >= 2 && unquoted.startsWith("\"") && unquoted.endsWith("\"")) {
- unquoted = unquoted.substring(1, unquoted.length() - 1);
- }
- return unquoted.replace("\"", "\"\"");
- }
-
- /**
- * Quotes an identifier with double quotes, doubling every embedded double
- * quote.
- */
- public static String quoteIdentifier(String identifier) {
- return "\"" + escapeIdentifier(identifier) + "\"";
- }
-
- /**
- * Validates a column DEFAULT expression that is emitted verbatim into DDL.
- * Accepts quoted string literals (escaped via doubling), numeric literals,
- * and keyword/function forms; rejects everything else.
- */
- public static String requireDefaultExpression(String defaultValue) {
- String trimmed = defaultValue.trim();
- if (!DEFAULT_EXPRESSION.matcher(trimmed).matches()) {
- throw new IllegalArgumentException("Invalid DM default expression: " + defaultValue);
- }
- return trimmed;
- }
-}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
new file mode 100644
index 0000000000..af9fcaaad7
--- /dev/null
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
@@ -0,0 +1,36 @@
+package ai.chat2db.plugin.dm;
+
+import java.util.regex.Pattern;
+
+/**
+ * Validation helpers for non-escapable SQL positions in DM DDL generation
+ * (column DEFAULT expressions emitted verbatim). Escaping itself lives in
+ * {@link ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor}.
+ */
+public final class DMSqlGuards {
+
+ /**
+ * Legitimate column DEFAULT expressions: quoted string literals (with ''
+ * escapes), numeric literals, or keyword/function forms such as
+ * CURRENT_TIMESTAMP, SYSDATE, USER, SEQ.NEXTVAL. Anything else is rejected
+ * because DEFAULT values are emitted verbatim.
+ */
+ private static final Pattern DEFAULT_EXPRESSION = Pattern.compile(
+ "'([^']|'')*'|[+-]?(\\d+(\\.\\d+)?|\\.\\d+)|[A-Za-z_][A-Za-z0-9_]*([.][A-Za-z_][A-Za-z0-9_]*)*(\\s*\\([^;)]*\\))?");
+
+ private DMSqlGuards() {
+ }
+
+ /**
+ * Validates a column DEFAULT expression that is emitted verbatim into DDL.
+ * Accepts quoted string literals (escaped via doubling), numeric literals,
+ * and keyword/function forms; rejects everything else.
+ */
+ public static String requireDefaultExpression(String defaultValue) {
+ String trimmed = defaultValue.trim();
+ if (!DEFAULT_EXPRESSION.matcher(trimmed).matches()) {
+ throw new IllegalArgumentException("Invalid DM default expression: " + defaultValue);
+ }
+ return trimmed;
+ }
+}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
index 94a03130cd..ddd18e11c3 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
@@ -2,7 +2,7 @@
import ai.chat2db.spi.constant.SQLConstants;
-import ai.chat2db.plugin.dm.DMSqlEscapes;
+import ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor;
import ai.chat2db.plugin.dm.enums.type.DMColumnTypeEnum;
import ai.chat2db.plugin.dm.enums.type.DMIndexTypeEnum;
import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum;
@@ -43,7 +43,7 @@ public class DMSqlBuilder extends DefaultSqlBuilder {
public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfig) {
StringBuilder script = new StringBuilder();
- script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
+ script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
for (TableColumn column : table.getColumnList()) {
if (StringUtils.isBlank(column.getName()) || StringUtils.isBlank(column.getColumnType())) {
@@ -99,7 +99,7 @@ public String buildAITableSchema(Table table) {
}
StringBuilder script = new StringBuilder();
- script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
+ script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
for (TableColumn column : table.getColumnList()) {
if (StringUtils.isBlank(column.getName()) || StringUtils.isBlank(column.getColumnType())) {
@@ -137,13 +137,13 @@ public String buildAITableSchema(Table table) {
private String buildTableComment(Table table) {
StringBuilder script = new StringBuilder();
- script.append(SQL_COMMENT_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(DMSqlEscapes.escapeSqlLiteral(table.getComment())).append(SQLConstants.SINGLE_QUOTE);
+ script.append(SQL_COMMENT_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(DMIdentifierProcessor.INSTANCE.escapeString(table.getComment())).append(SQLConstants.SINGLE_QUOTE);
return script.toString();
}
private String buildComment(TableColumn column) {
StringBuilder script = new StringBuilder();
- script.append(SQL_COMMENT_COLUMN).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(column.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(column.getTableName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(column.getName())).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(DMSqlEscapes.escapeSqlLiteral(column.getComment())).append(SQLConstants.SINGLE_QUOTE);
+ script.append(SQL_COMMENT_COLUMN).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(column.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(column.getTableName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(column.getName())).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(DMIdentifierProcessor.INSTANCE.escapeString(column.getComment())).append(SQLConstants.SINGLE_QUOTE);
return script.toString();
}
@@ -152,8 +152,8 @@ public String buildAlterTable(Table oldTable, Table newTable) {
StringBuilder script = new StringBuilder();
if (!StringUtils.equalsIgnoreCase(oldTable.getName(), newTable.getName())) {
- script.append(SQL_ALTER_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(oldTable.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(oldTable.getName())).append(SQLConstants.DOUBLE_QUOTE);
- script.append(SQLConstants.SPACE).append(SQL_RENAME).append(SQLConstants.DOUBLE_QUOTE).append(DMSqlEscapes.escapeIdentifier(newTable.getName())).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
+ script.append(SQL_ALTER_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(oldTable.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(oldTable.getName())).append(SQLConstants.DOUBLE_QUOTE);
+ script.append(SQLConstants.SPACE).append(SQL_RENAME).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(newTable.getName())).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
}
if (!StringUtils.equalsIgnoreCase(oldTable.getComment(), newTable.getComment())) {
script.append(SQLConstants.EMPTY).append(buildTableComment(newTable)).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
@@ -212,9 +212,9 @@ public String buildPageLimit(PageLimitRequest request) {
@Override
public String buildCreateSchema(Schema schema) {
StringBuilder sqlBuilder = new StringBuilder();
- sqlBuilder.append(SQL_CREATE_SCHEMA+DMSqlEscapes.escapeIdentifier(schema.getName())+SQLConstants.DOUBLE_QUOTE);
+ sqlBuilder.append(SQL_CREATE_SCHEMA+DMIdentifierProcessor.escapeIdentifier(schema.getName())+SQLConstants.DOUBLE_QUOTE);
if(StringUtils.isNotBlank(schema.getOwner())){
- sqlBuilder.append(SQLConstants.SCHEMA_AUTHORIZATION_SQL).append(DMSqlEscapes.quoteIdentifier(schema.getOwner()));
+ sqlBuilder.append(SQLConstants.SCHEMA_AUTHORIZATION_SQL).append(DMIdentifierProcessor.INSTANCE.quoteIdentifier(schema.getOwner()));
}
return sqlBuilder.toString();
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
index 0f9cd053c3..906c390793 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
@@ -1,6 +1,7 @@
package ai.chat2db.plugin.dm.enums.type;
-import ai.chat2db.plugin.dm.DMSqlEscapes;
+import ai.chat2db.plugin.dm.DMSqlGuards;
+import ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor;
import ai.chat2db.spi.IColumnBuilder;
import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum;
import ai.chat2db.community.domain.api.model.metadata.ColumnType;
@@ -159,7 +160,7 @@ public String buildCreateColumnSql(TableColumn column) {
}
StringBuilder script = new StringBuilder();
- script.append(DMSqlEscapes.quoteIdentifier(column.getName())).append(" ");
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" ");
script.append(buildDataType(column, type)).append(" ");
@@ -180,7 +181,7 @@ public String buildAICreateColumnSql(TableColumn column) {
}
StringBuilder script = new StringBuilder();
- script.append(DMSqlEscapes.quoteIdentifier(column.getName())).append(" ");
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" ");
script.append(buildDataType(column, type)).append(" ");
@@ -233,7 +234,7 @@ private String buildDefaultValue(TableColumn column, DMColumnTypeEnum type) {
return StringUtils.join("DEFAULT NULL");
}
- return StringUtils.join("DEFAULT ", DMSqlEscapes.requireDefaultExpression(column.getDefaultValue()));
+ return StringUtils.join("DEFAULT ", DMSqlGuards.requireDefaultExpression(column.getDefaultValue()));
}
private String buildDataType(TableColumn column, DMColumnTypeEnum type) {
@@ -298,25 +299,25 @@ public String buildModifyColumn(TableColumn tableColumn) {
if (EditStatusEnum.DELETE.name().equals(tableColumn.getEditStatus())) {
StringBuilder script = new StringBuilder();
- script.append(SQL_ALTER_TABLE).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getTableName())).append("\"");
- script.append(" ").append(SQL_DROP_COLUMN).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getName())).append("\"");
+ script.append(SQL_ALTER_TABLE).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getTableName())).append("\"");
+ script.append(" ").append(SQL_DROP_COLUMN).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getName())).append("\"");
return script.toString();
}
if (EditStatusEnum.ADD.name().equals(tableColumn.getEditStatus())) {
StringBuilder script = new StringBuilder();
- script.append(SQL_ALTER_TABLE).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getTableName())).append("\"");
+ script.append(SQL_ALTER_TABLE).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getTableName())).append("\"");
script.append(" ").append("ADD (").append(buildCreateColumnSql(tableColumn)).append(")");
return script.toString();
}
if (EditStatusEnum.MODIFY.name().equals(tableColumn.getEditStatus())) {
StringBuilder script = new StringBuilder();
if (!StringUtils.equals(tableColumn.getOldName(), tableColumn.getName())) {
- script.append(SQL_ALTER_TABLE).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getTableName())).append("\"");
- script.append(" ").append(SQL_RENAME_COLUMN).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getOldName())).append("\"").append(" TO ").append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getName())).append("\"");
+ script.append(SQL_ALTER_TABLE).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getTableName())).append("\"");
+ script.append(" ").append(SQL_RENAME_COLUMN).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getOldName())).append("\"").append(" TO ").append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getName())).append("\"");
script.append(";\n");
}
- script.append(SQL_ALTER_TABLE).append("\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableColumn.getTableName())).append("\"");
+ script.append(SQL_ALTER_TABLE).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getTableName())).append("\"");
script.append(" ").append("MODIFY (").append(buildCreateColumnSql(tableColumn)).append(") \n");
return script.toString();
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
index 6a1fbfe25d..a747b900b2 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
@@ -1,6 +1,6 @@
package ai.chat2db.plugin.dm.enums.type;
-import ai.chat2db.plugin.dm.DMSqlEscapes;
+import ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor;
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;
@@ -73,14 +73,14 @@ public static DMIndexTypeEnum getByType(String type) {
public String buildIndexScript(TableIndex tableIndex) {
StringBuilder script = new StringBuilder();
if (PRIMARY_KEY.equals(this)) {
- script.append(SQL_ALTER_TABLE_2).append(DMSqlEscapes.escapeIdentifier(tableIndex.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableIndex.getTableName())).append("\" ADD PRIMARY KEY ").append(buildIndexColumn(tableIndex));
+ script.append(SQL_ALTER_TABLE_2).append(DMIdentifierProcessor.escapeIdentifier(tableIndex.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableIndex.getTableName())).append("\" ADD PRIMARY KEY ").append(buildIndexColumn(tableIndex));
} else {
if (UNIQUE.equals(this)) {
script.append(SQL_CREATE_UNIQUE_INDEX);
} else {
script.append(SQL_CREATE_INDEX);
}
- script.append(buildIndexName(tableIndex)).append(SQL_ON).append(DMSqlEscapes.escapeIdentifier(tableIndex.getSchemaName())).append("\".\"").append(DMSqlEscapes.escapeIdentifier(tableIndex.getTableName())).append("\" ").append(buildIndexColumn(tableIndex));
+ script.append(buildIndexName(tableIndex)).append(SQL_ON).append(DMIdentifierProcessor.escapeIdentifier(tableIndex.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableIndex.getTableName())).append("\" ").append(buildIndexColumn(tableIndex));
}
return script.toString();
}
@@ -91,7 +91,7 @@ private String buildIndexColumn(TableIndex tableIndex) {
script.append("(");
for (TableIndexColumn column : tableIndex.getColumnList()) {
if (StringUtils.isNotBlank(column.getColumnName())) {
- script.append(DMSqlEscapes.quoteIdentifier(column.getColumnName()));
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifier(column.getColumnName()));
if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) {
String ascOrDesc = column.getAscOrDesc();
if (!"ASC".equalsIgnoreCase(ascOrDesc) && !"DESC".equalsIgnoreCase(ascOrDesc)) {
@@ -108,7 +108,7 @@ private String buildIndexColumn(TableIndex tableIndex) {
}
private String buildIndexName(TableIndex tableIndex) {
- return DMSqlEscapes.quoteIdentifier(tableIndex.getSchemaName()) + "." + DMSqlEscapes.quoteIdentifier(tableIndex.getName());
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getSchemaName()) + "." + DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getName());
}
public String buildModifyIndex(TableIndex tableIndex) {
@@ -126,7 +126,7 @@ public String buildModifyIndex(TableIndex tableIndex) {
private String buildDropIndex(TableIndex tableIndex) {
if (DMIndexTypeEnum.PRIMARY_KEY.getName().equals(tableIndex.getType())) {
- String tableName = DMSqlEscapes.quoteIdentifier(tableIndex.getSchemaName()) + "." + DMSqlEscapes.quoteIdentifier(tableIndex.getTableName());
+ String tableName = DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getSchemaName()) + "." + DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getTableName());
return StringUtils.join(SQL_ALTER_TABLE,tableName,SQL_DROP_PRIMARY_KEY);
}
StringBuilder script = new StringBuilder();
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java
index e84c08e74f..fb16939afe 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java
@@ -8,6 +8,11 @@
public class DMIdentifierProcessor extends DefaultSQLIdentifierProcessor {
+ /**
+ * Shared stateless instance for call sites without MetaData access.
+ */
+ public static final DMIdentifierProcessor INSTANCE = new DMIdentifierProcessor();
+
public static final Set DM_RESERVED_KEYWORDS = new HashSet<>();
static {
@@ -252,39 +257,32 @@ public boolean isReservedKeyword(String identifier, Integer majorVersion, Intege
return DM_RESERVED_KEYWORDS.contains(identifier);
}
+ /**
+ * Always quotes with double quotes, stripping one surrounding quote pair and
+ * doubling every embedded double quote.
+ */
@Override
public String quoteIdentifier(String identifier, Integer majorVersion, Integer minorVersion) {
- if (isValidIdentifier(identifier)) {
- if (containsLowerCase(identifier) || isReservedKeyword(identifier.toUpperCase(), majorVersion, minorVersion)) {
- return StringUtils.wrap(identifier, '"');
- }
- return identifier;
- }
- return StringUtils.wrap(identifier, '"');
-
+ return quoteIdentifier(identifier);
}
@Override
public String quoteIdentifier(String identifier) {
- if (isValidIdentifier(identifier)) {
- if (containsLowerCase(identifier) || isReservedKeyword(identifier.toUpperCase(), null, null)) {
- return StringUtils.wrap(identifier, '"');
- }
- return identifier;
- }
- return StringUtils.wrap(identifier, '"');
-
+ return "\"" + escapeIdentifierContent(identifier) + "\"";
}
@Override
public String quoteIdentifierIgnoreCase(String identifier) {
- if (isValidIdentifier(identifier)) {
- if (isReservedKeyword(identifier.toUpperCase(), null, null)) {
- return StringUtils.wrap(identifier, '"');
- }
- return identifier;
- }
- return StringUtils.wrap(identifier, '"');
+ return quoteIdentifier(identifier);
+ }
+
+ /**
+ * Escapes a value interpolated into a single-quoted SQL string literal by
+ * doubling every single quote. Returns {@code null} for {@code null}.
+ */
+ @Override
+ public String escapeString(String str) {
+ return StringUtils.replace(str, "'", "''");
}
@Override
@@ -295,4 +293,24 @@ public String convertIdentifierCase(String identifier) {
return identifier.toUpperCase();
}
}
+
+ private static String escapeIdentifierContent(String identifier) {
+ if (identifier == null) {
+ return null;
+ }
+ String unquoted = identifier;
+ if (unquoted.length() >= 2 && unquoted.startsWith("\"") && unquoted.endsWith("\"")) {
+ unquoted = unquoted.substring(1, unquoted.length() - 1);
+ }
+ return unquoted.replace("\"", "\"\"");
+ }
+
+ /**
+ * Escapes identifier content for a position already surrounded by double
+ * quotes: strips one surrounding quote pair, then doubles every embedded
+ * double quote. Returns {@code null} for {@code null}.
+ */
+ public static String escapeIdentifier(String identifier) {
+ return escapeIdentifierContent(identifier);
+ }
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlEscapesTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
similarity index 80%
rename from chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlEscapesTest.java
rename to chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
index 42f4dd4c15..66b8e8ae82 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMSqlEscapesTest.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
@@ -8,6 +8,7 @@
import ai.chat2db.plugin.dm.builder.DMSqlBuilder;
import ai.chat2db.plugin.dm.enums.type.DMColumnTypeEnum;
import ai.chat2db.plugin.dm.enums.type.DMIndexTypeEnum;
+import ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -17,26 +18,26 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
-class DMSqlEscapesTest {
+class DMIdentifierProcessorTest {
@Test
void escapeSqlLiteralDoublesSingleQuotes() {
- assertEquals("O''Brien", DMSqlEscapes.escapeSqlLiteral("O'Brien"));
- assertEquals("x'' OR ''1''=''1", DMSqlEscapes.escapeSqlLiteral("x' OR '1'='1"));
- assertEquals("plain", DMSqlEscapes.escapeSqlLiteral("plain"));
+ assertEquals("O''Brien", DMIdentifierProcessor.INSTANCE.escapeString("O'Brien"));
+ assertEquals("x'' OR ''1''=''1", DMIdentifierProcessor.INSTANCE.escapeString("x' OR '1'='1"));
+ assertEquals("plain", DMIdentifierProcessor.INSTANCE.escapeString("plain"));
}
@Test
void quoteIdentifierDoublesEmbeddedDoubleQuotes() {
- assertEquals("\"plain\"", DMSqlEscapes.quoteIdentifier("plain"));
- assertEquals("\"we\"\"ird\"", DMSqlEscapes.quoteIdentifier("we\"ird"));
+ assertEquals("\"plain\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("plain"));
+ assertEquals("\"we\"\"ird\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("we\"ird"));
}
@Test
void escapeIdentifierStripsOneSurroundingQuotePairBeforeDoubling() {
- assertEquals("already", DMSqlEscapes.escapeIdentifier("\"already\""));
- assertEquals("a\"\"b", DMSqlEscapes.escapeIdentifier("a\"b"));
- assertEquals("plain", DMSqlEscapes.escapeIdentifier("plain"));
+ assertEquals("already", DMIdentifierProcessor.escapeIdentifier("\"already\""));
+ assertEquals("a\"\"b", DMIdentifierProcessor.escapeIdentifier("a\"b"));
+ assertEquals("plain", DMIdentifierProcessor.escapeIdentifier("plain"));
}
@Test
@@ -125,15 +126,15 @@ void quotedStringDefaultsAndUnitPassThroughUnchanged() {
@Test
void defaultExpressionAcceptsLegitimateAndRejectsInjection() {
- org.junit.jupiter.api.Assertions.assertEquals("'abc'", DMSqlEscapes.requireDefaultExpression("'abc'"));
- org.junit.jupiter.api.Assertions.assertEquals("'O''Brien'", DMSqlEscapes.requireDefaultExpression("'O''Brien'"));
- org.junit.jupiter.api.Assertions.assertEquals("-1.5", DMSqlEscapes.requireDefaultExpression("-1.5"));
- org.junit.jupiter.api.Assertions.assertEquals("CURRENT_TIMESTAMP", DMSqlEscapes.requireDefaultExpression("CURRENT_TIMESTAMP"));
- org.junit.jupiter.api.Assertions.assertEquals("SEQ.NEXTVAL", DMSqlEscapes.requireDefaultExpression("SEQ.NEXTVAL"));
+ org.junit.jupiter.api.Assertions.assertEquals("'abc'", DMSqlGuards.requireDefaultExpression("'abc'"));
+ org.junit.jupiter.api.Assertions.assertEquals("'O''Brien'", DMSqlGuards.requireDefaultExpression("'O''Brien'"));
+ org.junit.jupiter.api.Assertions.assertEquals("-1.5", DMSqlGuards.requireDefaultExpression("-1.5"));
+ org.junit.jupiter.api.Assertions.assertEquals("CURRENT_TIMESTAMP", DMSqlGuards.requireDefaultExpression("CURRENT_TIMESTAMP"));
+ org.junit.jupiter.api.Assertions.assertEquals("SEQ.NEXTVAL", DMSqlGuards.requireDefaultExpression("SEQ.NEXTVAL"));
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
- () -> DMSqlEscapes.requireDefaultExpression("1; DROP TABLE t"));
+ () -> DMSqlGuards.requireDefaultExpression("1; DROP TABLE t"));
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
- () -> DMSqlEscapes.requireDefaultExpression("x' OR '1'='1"));
+ () -> DMSqlGuards.requireDefaultExpression("x' OR '1'='1"));
}
@Test
From 91e86713c943fbd832cd25aca0c1da9c2795dd41 Mon Sep 17 00:00:00 2001
From: HandSonic <8078023+handsonic@users.noreply.github.com>
Date: Mon, 27 Jul 2026 03:50:49 +0800
Subject: [PATCH 3/6] fix(dm): keep SPI quoteIdentifier conditional, reserve
always-quote for DDL paths (#1914)
---
.../ai/chat2db/plugin/dm/DMDBManager.java | 4 +--
.../java/ai/chat2db/plugin/dm/DMMetaData.java | 4 +--
.../ai/chat2db/plugin/dm/DMSqlGuards.java | 8 +++--
.../plugin/dm/builder/DMSqlBuilder.java | 2 +-
.../dm/enums/type/DMColumnTypeEnum.java | 4 +--
.../plugin/dm/enums/type/DMIndexTypeEnum.java | 6 ++--
.../dm/identifier/DMIdentifierProcessor.java | 29 +++++++++++++++++--
.../plugin/dm/DMIdentifierProcessorTest.java | 27 +++++++++++++++--
8 files changed, 67 insertions(+), 17 deletions(-)
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
index 737840def5..1b96c0cd4e 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
@@ -41,7 +41,7 @@ public class DMDBManager extends DefaultDBManager implements IDbManager {
private String format(String tableName) {
- return DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableName);
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName);
}
@@ -235,6 +235,6 @@ public void connectDatabase(Connection connection, String database) {
@Override
public String dropTable(Connection connection, String databaseName, String schemaName, String tableName) {
- return String.format(SQL_DROP_TABLE_EXISTS, DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableName));
+ return String.format(SQL_DROP_TABLE_EXISTS, DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName));
}
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
index 0e5455be28..06fcbc30e7 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
@@ -46,7 +46,7 @@ public List schemas(Connection connection, String databaseName) {
}
private String format(String tableName) {
- return getSQLIdentifierProcessor().quoteIdentifier(tableName);
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName);
}
protected static String tableDDL = "SELECT dbms_metadata.get_ddl('TABLE', '%s','%s') as ddl FROM dual ;";
@@ -320,7 +320,7 @@ public ISQLIdentifierProcessor getSQLIdentifierProcessor() {
@Override
public String getMetaDataName(String... names) {
- return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(getSQLIdentifierProcessor()::quoteIdentifier).collect(Collectors.joining("."));
+ return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(DMIdentifierProcessor.INSTANCE::quoteIdentifierAlways).collect(Collectors.joining("."));
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
index af9fcaaad7..ee6fbfdccb 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
@@ -12,11 +12,13 @@ public final class DMSqlGuards {
/**
* Legitimate column DEFAULT expressions: quoted string literals (with ''
* escapes), numeric literals, or keyword/function forms such as
- * CURRENT_TIMESTAMP, SYSDATE, USER, SEQ.NEXTVAL. Anything else is rejected
- * because DEFAULT values are emitted verbatim.
+ * CURRENT_TIMESTAMP, SYSDATE, USER, SEQ.NEXTVAL. Function-call arguments
+ * tolerate quoted string literals and one level of nested parentheses
+ * (e.g. NVL(SUM(x),0)); semicolons are never allowed. Anything else is
+ * rejected because DEFAULT values are emitted verbatim.
*/
private static final Pattern DEFAULT_EXPRESSION = Pattern.compile(
- "'([^']|'')*'|[+-]?(\\d+(\\.\\d+)?|\\.\\d+)|[A-Za-z_][A-Za-z0-9_]*([.][A-Za-z_][A-Za-z0-9_]*)*(\\s*\\([^;)]*\\))?");
+ "'([^']|'')*'|[+-]?(\\d+(\\.\\d+)?|\\.\\d+)|[A-Za-z_][A-Za-z0-9_]*([.][A-Za-z_][A-Za-z0-9_]*)*(\\s*\\((?:'(?:[^']|'')*'|\\((?:'(?:[^']|'')*'|[^()';])*\\)|[^()';])*\\))?");
private DMSqlGuards() {
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
index ddd18e11c3..fae4505b86 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
@@ -214,7 +214,7 @@ public String buildCreateSchema(Schema schema) {
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append(SQL_CREATE_SCHEMA+DMIdentifierProcessor.escapeIdentifier(schema.getName())+SQLConstants.DOUBLE_QUOTE);
if(StringUtils.isNotBlank(schema.getOwner())){
- sqlBuilder.append(SQLConstants.SCHEMA_AUTHORIZATION_SQL).append(DMIdentifierProcessor.INSTANCE.quoteIdentifier(schema.getOwner()));
+ sqlBuilder.append(SQLConstants.SCHEMA_AUTHORIZATION_SQL).append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schema.getOwner()));
}
return sqlBuilder.toString();
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
index 906c390793..161c373209 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
@@ -160,7 +160,7 @@ public String buildCreateColumnSql(TableColumn column) {
}
StringBuilder script = new StringBuilder();
- script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" ");
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" ");
script.append(buildDataType(column, type)).append(" ");
@@ -181,7 +181,7 @@ public String buildAICreateColumnSql(TableColumn column) {
}
StringBuilder script = new StringBuilder();
- script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifier(column.getName())).append(" ");
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" ");
script.append(buildDataType(column, type)).append(" ");
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
index a747b900b2..19ee1214ba 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
@@ -91,7 +91,7 @@ private String buildIndexColumn(TableIndex tableIndex) {
script.append("(");
for (TableIndexColumn column : tableIndex.getColumnList()) {
if (StringUtils.isNotBlank(column.getColumnName())) {
- script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifier(column.getColumnName()));
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getColumnName()));
if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) {
String ascOrDesc = column.getAscOrDesc();
if (!"ASC".equalsIgnoreCase(ascOrDesc) && !"DESC".equalsIgnoreCase(ascOrDesc)) {
@@ -108,7 +108,7 @@ private String buildIndexColumn(TableIndex tableIndex) {
}
private String buildIndexName(TableIndex tableIndex) {
- return DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getSchemaName()) + "." + DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getName());
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getSchemaName()) + "." + DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getName());
}
public String buildModifyIndex(TableIndex tableIndex) {
@@ -126,7 +126,7 @@ public String buildModifyIndex(TableIndex tableIndex) {
private String buildDropIndex(TableIndex tableIndex) {
if (DMIndexTypeEnum.PRIMARY_KEY.getName().equals(tableIndex.getType())) {
- String tableName = DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getSchemaName()) + "." + DMIdentifierProcessor.INSTANCE.quoteIdentifier(tableIndex.getTableName());
+ String tableName = DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getSchemaName()) + "." + DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getTableName());
return StringUtils.join(SQL_ALTER_TABLE,tableName,SQL_DROP_PRIMARY_KEY);
}
StringBuilder script = new StringBuilder();
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java
index fb16939afe..d787c5cc18 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java
@@ -258,7 +258,10 @@ public boolean isReservedKeyword(String identifier, Integer majorVersion, Intege
}
/**
- * Always quotes with double quotes, stripping one surrounding quote pair and
+ * SPI-facing conditional quoting: {@code null} and blank identifiers are
+ * returned unchanged; identifiers that are already valid for the dialect
+ * and are not reserved keywords are returned unquoted; anything else is
+ * wrapped with double quotes, stripping one surrounding quote pair and
* doubling every embedded double quote.
*/
@Override
@@ -268,12 +271,34 @@ public String quoteIdentifier(String identifier, Integer majorVersion, Integer m
@Override
public String quoteIdentifier(String identifier) {
+ if (StringUtils.isBlank(identifier)) {
+ return identifier;
+ }
+ if (isValidIdentifier(identifier)
+ && !isReservedKeyword(identifier.toUpperCase(java.util.Locale.ROOT), null, null)) {
+ return identifier;
+ }
+ return quoteIdentifierAlways(identifier);
+ }
+
+ /**
+ * Unconditional quoting for DDL-generation call sites: wraps with double
+ * quotes, stripping one surrounding quote pair and doubling every embedded
+ * double quote. Returns {@code null} for {@code null}.
+ */
+ public String quoteIdentifierAlways(String identifier) {
+ if (identifier == null) {
+ return null;
+ }
return "\"" + escapeIdentifierContent(identifier) + "\"";
}
+ /**
+ * Always-quote SPI 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-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
index 66b8e8ae82..fe315939ab 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
@@ -15,6 +15,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -28,11 +29,31 @@ void escapeSqlLiteralDoublesSingleQuotes() {
}
@Test
- void quoteIdentifierDoublesEmbeddedDoubleQuotes() {
- assertEquals("\"plain\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("plain"));
+ void quoteIdentifierIsConditionalForSpiConsumers() {
+ assertNull(DMIdentifierProcessor.INSTANCE.quoteIdentifier(null));
+ assertEquals("", DMIdentifierProcessor.INSTANCE.quoteIdentifier(""));
+ assertEquals("plain", DMIdentifierProcessor.INSTANCE.quoteIdentifier("plain"));
+ assertEquals("plain", DMIdentifierProcessor.INSTANCE.quoteIdentifier("plain", null, null));
+ assertEquals("\"SELECT\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("SELECT"));
+ assertEquals("\"select\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("select"));
+ assertEquals("\"weird name\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("weird name"));
assertEquals("\"we\"\"ird\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("we\"ird"));
}
+ @Test
+ void quoteIdentifierAlwaysQuotesUnconditionally() {
+ assertNull(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(null));
+ assertEquals("\"plain\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways("plain"));
+ assertEquals("\"SELECT\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways("SELECT"));
+ assertEquals("\"we\"\"ird\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways("we\"ird"));
+ }
+
+ @Test
+ void quoteIdentifierIgnoreCaseIsTheAlwaysQuoteVariant() {
+ assertEquals("\"plain\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("plain"));
+ assertEquals("\"MixedCase\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("MixedCase"));
+ }
+
@Test
void escapeIdentifierStripsOneSurroundingQuotePairBeforeDoubling() {
assertEquals("already", DMIdentifierProcessor.escapeIdentifier("\"already\""));
@@ -131,6 +152,8 @@ void defaultExpressionAcceptsLegitimateAndRejectsInjection() {
org.junit.jupiter.api.Assertions.assertEquals("-1.5", DMSqlGuards.requireDefaultExpression("-1.5"));
org.junit.jupiter.api.Assertions.assertEquals("CURRENT_TIMESTAMP", DMSqlGuards.requireDefaultExpression("CURRENT_TIMESTAMP"));
org.junit.jupiter.api.Assertions.assertEquals("SEQ.NEXTVAL", DMSqlGuards.requireDefaultExpression("SEQ.NEXTVAL"));
+ org.junit.jupiter.api.Assertions.assertEquals("NVL(SUM(x),0)", DMSqlGuards.requireDefaultExpression("NVL(SUM(x),0)"));
+ org.junit.jupiter.api.Assertions.assertEquals("NVL(SUM('a;b'),0)", DMSqlGuards.requireDefaultExpression("NVL(SUM('a;b'),0)"));
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
() -> DMSqlGuards.requireDefaultExpression("1; DROP TABLE t"));
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
From 24228a48a68c38d25949918bbef477a110b9cbb7 Mon Sep 17 00:00:00 2001
From: HandSonic <8078023+handsonic@users.noreply.github.com>
Date: Mon, 27 Jul 2026 09:24:47 +0800
Subject: [PATCH 4/6] fix(dm): unroll quoted-literal regex to avoid CodeQL
polynomial-regex alert (#1914)
---
.../src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
index ee6fbfdccb..c5da0fb052 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
@@ -18,7 +18,7 @@ public final class DMSqlGuards {
* rejected because DEFAULT values are emitted verbatim.
*/
private static final Pattern DEFAULT_EXPRESSION = Pattern.compile(
- "'([^']|'')*'|[+-]?(\\d+(\\.\\d+)?|\\.\\d+)|[A-Za-z_][A-Za-z0-9_]*([.][A-Za-z_][A-Za-z0-9_]*)*(\\s*\\((?:'(?:[^']|'')*'|\\((?:'(?:[^']|'')*'|[^()';])*\\)|[^()';])*\\))?");
+ "'[^']*(?:''[^']*)*'|[+-]?(\\d+(\\.\\d+)?|\\.\\d+)|[A-Za-z_][A-Za-z0-9_]*([.][A-Za-z_][A-Za-z0-9_]*)*(\\s*\\((?:'[^']*(?:''[^']*)*'|\\((?:'[^']*(?:''[^']*)*'|[^()';])*\\)|[^()';])*\\))?");
private DMSqlGuards() {
}
From 88984b40a21a4db13733e08de5488851cfe19130 Mon Sep 17 00:00:00 2001
From: HandSonic <8078023+handsonic@users.noreply.github.com>
Date: Mon, 27 Jul 2026 10:13:26 +0800
Subject: [PATCH 5/6] fix(dm): replace DEFAULT validator regex with linear
scanners (CodeQL ReDoS) (#1914)
Balanced-paren + quoted-literal aware scanning keeps NVL(SUM(x),0) and
quoted defaults accepted; semicolons and unbalanced input rejected.
---
.../ai/chat2db/plugin/dm/DMSqlGuards.java | 149 ++++++++++++++++--
1 file changed, 133 insertions(+), 16 deletions(-)
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
index c5da0fb052..89c98baf86 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
@@ -1,38 +1,155 @@
package ai.chat2db.plugin.dm;
-import java.util.regex.Pattern;
-
/**
* Validation helpers for non-escapable SQL positions in DM DDL generation
* (column DEFAULT expressions emitted verbatim). Escaping itself lives in
* {@link ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor}.
+ * All shapes are recognized with linear-time scanners (no regex), so the
+ * checks cannot be driven into regex backtracking.
*/
public final class DMSqlGuards {
- /**
- * Legitimate column DEFAULT expressions: quoted string literals (with ''
- * escapes), numeric literals, or keyword/function forms such as
- * CURRENT_TIMESTAMP, SYSDATE, USER, SEQ.NEXTVAL. Function-call arguments
- * tolerate quoted string literals and one level of nested parentheses
- * (e.g. NVL(SUM(x),0)); semicolons are never allowed. Anything else is
- * rejected because DEFAULT values are emitted verbatim.
- */
- private static final Pattern DEFAULT_EXPRESSION = Pattern.compile(
- "'[^']*(?:''[^']*)*'|[+-]?(\\d+(\\.\\d+)?|\\.\\d+)|[A-Za-z_][A-Za-z0-9_]*([.][A-Za-z_][A-Za-z0-9_]*)*(\\s*\\((?:'[^']*(?:''[^']*)*'|\\((?:'[^']*(?:''[^']*)*'|[^()';])*\\)|[^()';])*\\))?");
-
private DMSqlGuards() {
}
/**
* Validates a column DEFAULT expression that is emitted verbatim into DDL.
- * Accepts quoted string literals (escaped via doubling), numeric literals,
- * and keyword/function forms; rejects everything else.
+ * Legitimate forms: quoted string literals (with '' escapes), numeric literals,
+ * or keyword/function forms such as CURRENT_TIMESTAMP, SYSDATE, USER, SEQ.NEXTVAL.
+ * Function-call arguments tolerate quoted string literals and nested balanced
+ * parentheses (e.g. NVL(SUM(x),0)); semicolons are never allowed. Anything else
+ * is rejected because DEFAULT values are emitted verbatim.
*/
public static String requireDefaultExpression(String defaultValue) {
String trimmed = defaultValue.trim();
- if (!DEFAULT_EXPRESSION.matcher(trimmed).matches()) {
+ if (!isQuotedStringLiteral(trimmed) && !isNumericLiteral(trimmed) && !isIdentChainOrCall(trimmed)) {
throw new IllegalArgumentException("Invalid DM default expression: " + defaultValue);
}
return trimmed;
}
+
+ /**
+ * True when {@code s} is exactly one single-quoted string literal with '' escapes.
+ * Linear scan, no backtracking.
+ */
+ static boolean isQuotedStringLiteral(String s) {
+ return s.length() >= 2 && s.charAt(0) == '\'' && quotedLiteralEnd(s, 0) == s.length();
+ }
+
+ /**
+ * Returns the index just past the single-quoted literal that starts at
+ * {@code start} (where {@code s.charAt(start) == '\''}), or -1 when the literal
+ * is unterminated. Doubled quotes are consumed as escapes.
+ */
+ private static int quotedLiteralEnd(String s, int start) {
+ int i = start + 1;
+ int n = s.length();
+ while (i < n) {
+ if (s.charAt(i) == '\'') {
+ if (i + 1 < n && s.charAt(i + 1) == '\'') {
+ i += 2;
+ continue;
+ }
+ return i + 1;
+ }
+ i++;
+ }
+ return -1;
+ }
+
+ /**
+ * Numeric literal: optional sign, digits with optional fractional part, or a
+ * leading-dot fraction (e.g. {@code -1}, {@code 1.5}, {@code .5}).
+ */
+ private static boolean isNumericLiteral(String s) {
+ int n = s.length();
+ int i = (s.startsWith("+") || s.startsWith("-")) ? 1 : 0;
+ boolean intDigits = false;
+ while (i < n && Character.isDigit(s.charAt(i))) {
+ i++;
+ intDigits = true;
+ }
+ boolean dotSeen = false;
+ boolean fracDigits = false;
+ if (i < n && s.charAt(i) == '.') {
+ dotSeen = true;
+ i++;
+ while (i < n && Character.isDigit(s.charAt(i))) {
+ i++;
+ fracDigits = true;
+ }
+ }
+ if (i != n) {
+ return false;
+ }
+ return dotSeen ? (intDigits || fracDigits) && fracDigits : intDigits;
+ }
+
+ /**
+ * True for an identifier chain {@code ident(.ident)*} (e.g. SYSDATE, SEQ.NEXTVAL)
+ * optionally followed by a parenthesized argument list. Arguments may contain
+ * single-quoted string literals and nested balanced parentheses; semicolons and
+ * stray quotes/parens are rejected. Linear scan.
+ */
+ static boolean isIdentChainOrCall(String s) {
+ int n = s.length();
+ if (n == 0 || !isIdentStart(s.charAt(0))) {
+ return false;
+ }
+ int i = 1;
+ while (i < n) {
+ char c = s.charAt(i);
+ if (isIdentPart(c)) {
+ i++;
+ } else if (c == '.' && i + 1 < n && isIdentStart(s.charAt(i + 1))) {
+ i++;
+ } else {
+ break;
+ }
+ }
+ while (i < n && Character.isWhitespace(s.charAt(i))) {
+ i++;
+ }
+ if (i == n) {
+ return true;
+ }
+ if (s.charAt(i) != '(') {
+ return false;
+ }
+ int depth = 0;
+ while (i < n) {
+ char c = s.charAt(i);
+ if (c == '\'') {
+ int end = quotedLiteralEnd(s, i);
+ if (end < 0) {
+ return false;
+ }
+ i = end;
+ continue;
+ }
+ if (c == '(') {
+ depth++;
+ } else if (c == ')') {
+ depth--;
+ if (depth == 0) {
+ return i == n - 1;
+ }
+ if (depth < 0) {
+ return false;
+ }
+ } else if (c == ';') {
+ return false;
+ }
+ i++;
+ }
+ return false;
+ }
+
+ private static boolean isIdentStart(char c) {
+ return Character.isLetter(c) || c == '_';
+ }
+
+ private static boolean isIdentPart(char c) {
+ return Character.isLetterOrDigit(c) || c == '_';
+ }
}
From b490b5bef61d4e92326badc668e27460eeb17112 Mon Sep 17 00:00:00 2001
From: zgq
Date: Wed, 29 Jul 2026 18:24:33 +0800
Subject: [PATCH 6/6] fix(dm): complete identifier-safe SQL generation
---
.../ai/chat2db/plugin/dm/DMDBManager.java | 40 +-
.../java/ai/chat2db/plugin/dm/DMMetaData.java | 7 +-
.../ai/chat2db/plugin/dm/DMSqlGuards.java | 320 +++++++++------
.../plugin/dm/builder/DMSqlBuilder.java | 135 +++++--
.../dm/constant/DMDBManagerConstants.java | 2 +-
.../dm/constant/DMSqlBuilderConstants.java | 2 +-
.../dm/enums/type/DMColumnTypeEnum.java | 64 ++-
.../plugin/dm/enums/type/DMIndexTypeEnum.java | 32 +-
.../dm/identifier/DMIdentifierProcessor.java | 80 ++--
.../plugin/dm/value/sub/DMBitProcessor.java | 3 +-
.../plugin/dm/DMIdentifierProcessorTest.java | 365 ++++++++++++++----
11 files changed, 770 insertions(+), 280 deletions(-)
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
index 1b96c0cd4e..de26cd35f4 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMDBManager.java
@@ -227,14 +227,50 @@ public void connectDatabase(Connection connection, String database) {
}
String schemaName = connectInfo.getSchemaName();
try {
- DefaultSQLExecutor.getInstance().execute(connection, String.format(SQL_SET_SCHEMA, DMIdentifierProcessor.escapeIdentifier(schemaName)));
+ DefaultSQLExecutor.getInstance().execute(connection,
+ String.format(SQL_SET_SCHEMA, DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName)));
} catch (SQLException e) {
log.error("connectDatabase error", e);
}
}
+ @Override
+ public void copyTable(Connection connection, String databaseName, String schemaName, String tableName,
+ String newTableName, boolean copyData) throws SQLException {
+ String source = qualifiedName(schemaName, tableName, true);
+ String target = qualifiedName(schemaName, newTableName, true);
+ String sql;
+ if (copyData) {
+ sql = "CREATE TABLE " + target + " AS SELECT * FROM " + source;
+ } else {
+ sql = "CREATE TABLE " + target + " AS SELECT * FROM " + source + " WHERE 1=0";
+ }
+ DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> null);
+ }
+
@Override
public String dropTable(Connection connection, String databaseName, String schemaName, String tableName) {
- return String.format(SQL_DROP_TABLE_EXISTS, DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName));
+ return String.format(SQL_DROP_TABLE_EXISTS, qualifiedName(schemaName, tableName, false));
+ }
+
+ @Override
+ public String truncateTable(Connection connection, String databaseName, String schemaName, String tableName) {
+ return "TRUNCATE TABLE " + qualifiedName(schemaName, tableName, true);
+ }
+
+ private static String qualifiedName(String schemaName, String objectName, boolean normalizeQuotedObject) {
+ String normalizedObject = normalizeQuotedObject ? normalizeQuotedIdentifier(objectName) : objectName;
+ String quotedObject = DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(normalizedObject);
+ if (StringUtils.isBlank(schemaName)) {
+ return quotedObject;
+ }
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName) + "." + quotedObject;
+ }
+
+ private static String normalizeQuotedIdentifier(String identifier) {
+ if (DMIdentifierProcessor.INSTANCE.isQuoteIdentifier(identifier)) {
+ return DMIdentifierProcessor.INSTANCE.removeIdentifierQuote(identifier);
+ }
+ return identifier;
}
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
index 06fcbc30e7..43e6422da4 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMMetaData.java
@@ -134,7 +134,8 @@ public List columns(Connection connection, String databaseName, Str
List columns = super.columns(connection, databaseName, schemaName, tableName);
for (TableColumn column : columns) {
String columnType = column.getColumnType();
- if (StringUtils.equals(columnType.toUpperCase(), DMColumnTypeEnum.TIMESTAMP.name())) {
+ if (columnType != null
+ && StringUtils.equals(columnType.toUpperCase(Locale.ROOT), DMColumnTypeEnum.TIMESTAMP.name())) {
column.setColumnSize(column.getDecimalDigits());
}
}
@@ -320,6 +321,10 @@ public ISQLIdentifierProcessor getSQLIdentifierProcessor() {
@Override
public String getMetaDataName(String... names) {
+ if (names.length == 3) {
+ String qualifier = StringUtils.isNotBlank(names[1]) ? names[1] : names[0];
+ return getMetaDataName(qualifier, names[2]);
+ }
return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(DMIdentifierProcessor.INSTANCE::quoteIdentifierAlways).collect(Collectors.joining("."));
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
index 89c98baf86..357c873b6c 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
@@ -1,155 +1,251 @@
package ai.chat2db.plugin.dm;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
/**
- * Validation helpers for non-escapable SQL positions in DM DDL generation
- * (column DEFAULT expressions emitted verbatim). Escaping itself lives in
- * {@link ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor}.
- * All shapes are recognized with linear-time scanners (no regex), so the
- * checks cannot be driven into regex backtracking.
+ * Structural validation for DM SQL fragments that are emitted as syntax
+ * rather than as identifiers or string literals.
*/
public final class DMSqlGuards {
+ private static final Set COLUMN_CLAUSE_KEYWORDS = Set.of(
+ "COLLATE", "CONSTRAINT", "CHECK", "DEFAULT", "DISABLE", "ENABLE", "GENERATED",
+ "IDENTITY", "INVISIBLE", "PRIMARY", "REFERENCES", "UNIQUE", "VISIBLE");
+
private DMSqlGuards() {
}
/**
- * Validates a column DEFAULT expression that is emitted verbatim into DDL.
- * Legitimate forms: quoted string literals (with '' escapes), numeric literals,
- * or keyword/function forms such as CURRENT_TIMESTAMP, SYSDATE, USER, SEQ.NEXTVAL.
- * Function-call arguments tolerate quoted string literals and nested balanced
- * parentheses (e.g. NVL(SUM(x),0)); semicolons are never allowed. Anything else
- * is rejected because DEFAULT values are emitted verbatim.
+ * Validates one DM DEFAULT expression without re-encoding serialized
+ * literals returned by metadata.
*/
- public static String requireDefaultExpression(String defaultValue) {
- String trimmed = defaultValue.trim();
- if (!isQuotedStringLiteral(trimmed) && !isNumericLiteral(trimmed) && !isIdentChainOrCall(trimmed)) {
- throw new IllegalArgumentException("Invalid DM default expression: " + defaultValue);
+ public static String requireDefaultExpression(String value) {
+ if (StringUtils.isBlank(value)) {
+ throw invalid("DEFAULT expression", value);
}
- return trimmed;
+ scanExpression(value.trim(), false, "DEFAULT expression");
+ return value;
}
/**
- * True when {@code s} is exactly one single-quoted string literal with '' escapes.
- * Linear scan, no backtracking.
+ * Validates one complete DM column type expression, including
+ * parameterized built-in and schema-qualified user-defined types.
*/
- static boolean isQuotedStringLiteral(String s) {
- return s.length() >= 2 && s.charAt(0) == '\'' && quotedLiteralEnd(s, 0) == s.length();
- }
-
- /**
- * Returns the index just past the single-quoted literal that starts at
- * {@code start} (where {@code s.charAt(start) == '\''}), or -1 when the literal
- * is unterminated. Doubled quotes are consumed as escapes.
- */
- private static int quotedLiteralEnd(String s, int start) {
- int i = start + 1;
- int n = s.length();
- while (i < n) {
- if (s.charAt(i) == '\'') {
- if (i + 1 < n && s.charAt(i + 1) == '\'') {
- i += 2;
- continue;
- }
- return i + 1;
- }
- i++;
+ public static String requireColumnTypeExpression(String typeName) {
+ if (StringUtils.isBlank(typeName)) {
+ throw invalid("column type", typeName);
}
- return -1;
+ scanExpression(typeName.trim(), true, "column type");
+ return typeName;
}
- /**
- * Numeric literal: optional sign, digits with optional fractional part, or a
- * leading-dot fraction (e.g. {@code -1}, {@code 1.5}, {@code .5}).
- */
- private static boolean isNumericLiteral(String s) {
- int n = s.length();
- int i = (s.startsWith("+") || s.startsWith("-")) ? 1 : 0;
- boolean intDigits = false;
- while (i < n && Character.isDigit(s.charAt(i))) {
- i++;
- intDigits = true;
- }
- boolean dotSeen = false;
- boolean fracDigits = false;
- if (i < n && s.charAt(i) == '.') {
- dotSeen = true;
- i++;
- while (i < n && Character.isDigit(s.charAt(i))) {
- i++;
- fracDigits = true;
- }
- }
- if (i != n) {
- return false;
+ public static String requireUnit(String unit) {
+ String trimmed = StringUtils.trimToEmpty(unit);
+ if (!"CHAR".equalsIgnoreCase(trimmed) && !"BYTE".equalsIgnoreCase(trimmed)) {
+ throw new IllegalArgumentException("Unsupported DM VARCHAR unit: " + unit);
}
- return dotSeen ? (intDigits || fracDigits) && fracDigits : intDigits;
+ return trimmed;
}
- /**
- * True for an identifier chain {@code ident(.ident)*} (e.g. SYSDATE, SEQ.NEXTVAL)
- * optionally followed by a parenthesized argument list. Arguments may contain
- * single-quoted string literals and nested balanced parentheses; semicolons and
- * stray quotes/parens are rejected. Linear scan.
- */
- static boolean isIdentChainOrCall(String s) {
- int n = s.length();
- if (n == 0 || !isIdentStart(s.charAt(0))) {
- return false;
+ public static String requireAscOrDesc(String value) {
+ String trimmed = StringUtils.trimToEmpty(value);
+ if ("ASC".equalsIgnoreCase(trimmed)) {
+ return "ASC";
}
- int i = 1;
- while (i < n) {
- char c = s.charAt(i);
- if (isIdentPart(c)) {
- i++;
- } else if (c == '.' && i + 1 < n && isIdentStart(s.charAt(i + 1))) {
- i++;
- } else {
- break;
- }
+ if ("DESC".equalsIgnoreCase(trimmed)) {
+ return "DESC";
}
- while (i < n && Character.isWhitespace(s.charAt(i))) {
- i++;
+ throw new IllegalArgumentException("Invalid DM index sort direction: " + value);
+ }
+
+ public static String requireBitLiteral(String value) {
+ if (StringUtils.isBlank(value)) {
+ return "NULL";
}
- if (i == n) {
- return true;
+ String trimmed = StringUtils.trimToEmpty(value);
+ if ("0".equals(trimmed) || "false".equalsIgnoreCase(trimmed)) {
+ return "0";
}
- if (s.charAt(i) != '(') {
- return false;
+ if ("1".equals(trimmed) || "true".equalsIgnoreCase(trimmed)) {
+ return "1";
}
- int depth = 0;
- while (i < n) {
- char c = s.charAt(i);
- if (c == '\'') {
- int end = quotedLiteralEnd(s, i);
- if (end < 0) {
- return false;
+ throw new IllegalArgumentException("Invalid DM BIT literal: " + value);
+ }
+
+ private static void scanExpression(String expression, boolean typeExpression, String description) {
+ Deque delimiters = new ArrayDeque<>();
+ List topLevelWords = new ArrayList<>();
+ boolean sawToken = false;
+
+ for (int i = 0; i < expression.length(); i++) {
+ char c = expression.charAt(i);
+ if (Character.isISOControl(c)) {
+ throw invalid(description, expression);
+ }
+ if (Character.isWhitespace(c)) {
+ continue;
+ }
+ sawToken = true;
+
+ if (isAlternativeQuoteStart(expression, i)) {
+ if (typeExpression) {
+ throw invalid(description, expression);
+ }
+ i = scanAlternativeQuote(expression, i, description);
+ continue;
+ }
+ if (c == '\'' || c == '"') {
+ if (typeExpression && c == '\'') {
+ throw invalid(description, expression);
+ }
+ int end = scanQuoted(expression, i, c, description);
+ if (c == '\'' && hasInvalidAttachedLiteralPrefix(expression, i, end)) {
+ throw invalid(description, expression);
}
i = end;
continue;
}
+ if (c == ';'
+ || startsWith(expression, i, "--")
+ || startsWith(expression, i, "/*")
+ || startsWith(expression, i, "*/")) {
+ throw invalid(description, expression);
+ }
if (c == '(') {
- depth++;
- } else if (c == ')') {
- depth--;
- if (depth == 0) {
- return i == n - 1;
+ delimiters.push(c);
+ continue;
+ }
+ if (c == ')') {
+ if (delimiters.isEmpty()) {
+ throw invalid(description, expression);
+ }
+ delimiters.pop();
+ continue;
+ }
+ if (c == '[' || c == ']' || c == '{' || c == '}') {
+ throw invalid(description, expression);
+ }
+ if (c == ',' && delimiters.isEmpty()) {
+ throw invalid(description, expression);
+ }
+ if (typeExpression && !isTypeCharacter(c)) {
+ throw invalid(description, expression);
+ }
+ if (Character.isLetter(c) || c == '_') {
+ int wordEnd = i + 1;
+ while (wordEnd < expression.length() && isWordCharacter(expression.charAt(wordEnd))) {
+ wordEnd++;
}
- if (depth < 0) {
- return false;
+ if (delimiters.isEmpty()) {
+ topLevelWords.add(expression.substring(i, wordEnd).toUpperCase(Locale.ROOT));
+ }
+ i = wordEnd - 1;
+ }
+ }
+
+ if (!sawToken || !delimiters.isEmpty()) {
+ throw invalid(description, expression);
+ }
+ rejectColumnClauseTokens(topLevelWords, description, expression);
+ }
+
+ private static int scanQuoted(String expression, int start, char quote, String description) {
+ for (int i = start + 1; i < expression.length(); i++) {
+ if (expression.charAt(i) == quote) {
+ if (i + 1 < expression.length() && expression.charAt(i + 1) == quote) {
+ i++;
+ continue;
}
- } else if (c == ';') {
- return false;
+ return i;
+ }
+ }
+ throw invalid(description, expression);
+ }
+
+ private static boolean hasInvalidAttachedLiteralPrefix(String expression, int quoteStart, int quoteEnd) {
+ if (quoteStart == 0 || Character.isWhitespace(expression.charAt(quoteStart - 1))) {
+ return false;
+ }
+ if (!isWordCharacter(expression.charAt(quoteStart - 1))) {
+ return false;
+ }
+ int prefixStart = quoteStart - 1;
+ while (prefixStart > 0 && isWordCharacter(expression.charAt(prefixStart - 1))) {
+ prefixStart--;
+ }
+ String prefix = expression.substring(prefixStart, quoteStart);
+ if ("N".equalsIgnoreCase(prefix)) {
+ return false;
+ }
+ if (!"X".equalsIgnoreCase(prefix)) {
+ return true;
+ }
+ for (int i = quoteStart + 1; i < quoteEnd; i++) {
+ char c = expression.charAt(i);
+ if ((c < '0' || c > '9') && (c < 'A' || c > 'F') && (c < 'a' || c > 'f')) {
+ return true;
}
- i++;
}
return false;
}
- private static boolean isIdentStart(char c) {
- return Character.isLetter(c) || c == '_';
+ private static boolean isAlternativeQuoteStart(String expression, int offset) {
+ return offset + 2 < expression.length()
+ && (expression.charAt(offset) == 'q' || expression.charAt(offset) == 'Q')
+ && expression.charAt(offset + 1) == '\'';
+ }
+
+ private static int scanAlternativeQuote(String expression, int start, String description) {
+ char open = expression.charAt(start + 2);
+ char close = switch (open) {
+ case '[' -> ']';
+ case '{' -> '}';
+ case '(' -> ')';
+ case '<' -> '>';
+ default -> open;
+ };
+ for (int i = start + 3; i + 1 < expression.length(); i++) {
+ if (expression.charAt(i) == close && expression.charAt(i + 1) == '\'') {
+ return i + 1;
+ }
+ }
+ throw invalid(description, expression);
+ }
+
+ private static void rejectColumnClauseTokens(List words, String description, String expression) {
+ for (String word : words) {
+ if (COLUMN_CLAUSE_KEYWORDS.contains(word)) {
+ throw invalid(description, expression);
+ }
+ }
+ for (int i = 0; i + 1 < words.size(); i++) {
+ if ("NOT".equals(words.get(i)) && "NULL".equals(words.get(i + 1))) {
+ throw invalid(description, expression);
+ }
+ }
+ }
+
+ private static boolean isTypeCharacter(char c) {
+ return Character.isLetterOrDigit(c) || c == '_' || c == '$' || c == '#'
+ || c == '.' || c == '%' || c == '*' || c == '+' || c == '-' || c == ',';
+ }
+
+ private static boolean isWordCharacter(char c) {
+ return Character.isLetterOrDigit(c) || c == '_' || c == '$' || c == '#';
+ }
+
+ private static boolean startsWith(String value, int offset, String candidate) {
+ return offset + candidate.length() <= value.length() && value.startsWith(candidate, offset);
}
- private static boolean isIdentPart(char c) {
- return Character.isLetterOrDigit(c) || c == '_';
+ private static IllegalArgumentException invalid(String description, String value) {
+ return new IllegalArgumentException("Invalid DM " + description + ": " + value);
}
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
index fae4505b86..eae8c148fd 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/builder/DMSqlBuilder.java
@@ -5,9 +5,11 @@
import ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor;
import ai.chat2db.plugin.dm.enums.type.DMColumnTypeEnum;
import ai.chat2db.plugin.dm.enums.type.DMIndexTypeEnum;
+import ai.chat2db.community.domain.api.enums.plugin.DmlTypeEnum;
import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum;
import ai.chat2db.spi.DefaultSqlBuilder;
import ai.chat2db.spi.model.request.PageLimitRequest;
+import ai.chat2db.spi.model.request.UpdateSqlRequest;
import ai.chat2db.community.domain.api.model.account.*;
import ai.chat2db.community.domain.api.model.async.*;
import ai.chat2db.community.domain.api.config.*;
@@ -20,39 +22,77 @@
import ai.chat2db.community.domain.api.model.view.*;
import ai.chat2db.community.domain.api.config.TableBuilderConfig;
import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
+import java.util.stream.Collectors;
import static ai.chat2db.plugin.dm.constant.DMSqlBuilderConstants.*;
public class DMSqlBuilder extends DefaultSqlBuilder {
+ @Override
+ public String quoteIdentifier(String identifier) {
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(identifier);
+ }
+ @Override
+ public String quoteQualifiedIdentifier(String... identifiers) {
+ if (identifiers.length == 3) {
+ String qualifier = StringUtils.isNotBlank(identifiers[1]) ? identifiers[1] : identifiers[0];
+ return quoteQualifiedIdentifier(qualifier, identifiers[2]);
+ }
+ return Arrays.stream(identifiers)
+ .filter(StringUtils::isNotBlank)
+ .map(DMIdentifierProcessor.INSTANCE::quoteIdentifierAlways)
+ .collect(Collectors.joining(SQLConstants.DOT));
+ }
-
-
-
-
-
-
-
+ @Override
+ public String buildTemplate(Table table, String type) {
+ if (table == null || CollectionUtils.isEmpty(table.getColumnList()) || StringUtils.isBlank(type)) {
+ return SQLConstants.EMPTY;
+ }
+ String tableName = quoteQualifiedIdentifier(table.getSchemaName(), table.getName());
+ List columnNames = table.getColumnList().stream()
+ .map(column -> quoteIdentifier(column.getName()))
+ .toList();
+ if (DmlTypeEnum.INSERT.name().equalsIgnoreCase(type)) {
+ return "INSERT INTO " + tableName + " (" + String.join(SQLConstants.COMMA, columnNames)
+ + ") VALUES (" + columnNames.stream().map(name -> SQLConstants.SPACE)
+ .collect(Collectors.joining(SQLConstants.COMMA)) + ")";
+ }
+ if (DmlTypeEnum.UPDATE.name().equalsIgnoreCase(type)) {
+ return "UPDATE " + tableName + " SET " + columnNames.stream()
+ .map(name -> name + SQLConstants.EQUAL_SQL + SQLConstants.SPACE)
+ .collect(Collectors.joining(SQLConstants.COMMA)) + " WHERE ";
+ }
+ if (DmlTypeEnum.DELETE.name().equalsIgnoreCase(type)) {
+ return "DELETE FROM " + tableName + " WHERE ";
+ }
+ if (DmlTypeEnum.SELECT.name().equalsIgnoreCase(type)) {
+ return "SELECT " + String.join(SQLConstants.COMMA, columnNames) + " FROM " + tableName;
+ }
+ return SQLConstants.EMPTY;
+ }
@Override
public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfig) {
StringBuilder script = new StringBuilder();
- script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
+ script.append(SQL_CREATE_TABLE)
+ .append(quoteQualifiedIdentifier(table.getDatabaseName(), table.getSchemaName(), table.getName()))
+ .append(SQLConstants.SPACE_OPEN_PARENTHESIS).append(SQLConstants.LINE_SEPARATOR);
for (TableColumn column : table.getColumnList()) {
if (StringUtils.isBlank(column.getName()) || StringUtils.isBlank(column.getColumnType())) {
continue;
}
DMColumnTypeEnum typeEnum = DMColumnTypeEnum.getByType(column.getColumnType());
- if(typeEnum == null){
- continue;
- }
+ typeEnum = typeEnum == null ? DMColumnTypeEnum.VARCHAR : typeEnum;
script.append(SQLConstants.TAB).append(typeEnum.buildCreateColumnSql(column)).append(SQLConstants.COMMA_LINE_SEPARATOR);
}
@@ -99,16 +139,16 @@ public String buildAITableSchema(Table table) {
}
StringBuilder script = new StringBuilder();
- script.append(SQL_CREATE_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_OPEN_PAREN).append(SQLConstants.LINE_SEPARATOR);
+ script.append(SQL_CREATE_TABLE)
+ .append(quoteQualifiedIdentifier(table.getDatabaseName(), table.getSchemaName(), table.getName()))
+ .append(SQLConstants.SPACE_OPEN_PARENTHESIS).append(SQLConstants.LINE_SEPARATOR);
for (TableColumn column : table.getColumnList()) {
if (StringUtils.isBlank(column.getName()) || StringUtils.isBlank(column.getColumnType())) {
continue;
}
DMColumnTypeEnum typeEnum = DMColumnTypeEnum.getByType(column.getColumnType());
- if(typeEnum == null){
- continue;
- }
+ typeEnum = typeEnum == null ? DMColumnTypeEnum.VARCHAR : typeEnum;
script.append(SQLConstants.TAB).append(typeEnum.buildAICreateColumnSql(column)).append(SQLConstants.COMMA_LINE_SEPARATOR);
}
@@ -136,24 +176,27 @@ public String buildAITableSchema(Table table) {
}
private String buildTableComment(Table table) {
- StringBuilder script = new StringBuilder();
- script.append(SQL_COMMENT_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(table.getName())).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(DMIdentifierProcessor.INSTANCE.escapeString(table.getComment())).append(SQLConstants.SINGLE_QUOTE);
- return script.toString();
+ return SQL_COMMENT_TABLE
+ + quoteQualifiedIdentifier(table.getDatabaseName(), table.getSchemaName(), table.getName())
+ + " IS " + quoteStringLiteral(table.getComment());
}
private String buildComment(TableColumn column) {
- StringBuilder script = new StringBuilder();
- script.append(SQL_COMMENT_COLUMN).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(column.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(column.getTableName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(column.getName())).append(VALUE_DOUBLE_QUOTE_IS_SINGLE_QUOTE).append(DMIdentifierProcessor.INSTANCE.escapeString(column.getComment())).append(SQLConstants.SINGLE_QUOTE);
- return script.toString();
+ return SQL_COMMENT_COLUMN
+ + quoteQualifiedIdentifier(column.getSchemaName(), column.getTableName())
+ + SQLConstants.DOT + quoteIdentifier(column.getName())
+ + " IS " + quoteStringLiteral(column.getComment());
}
@Override
public String buildAlterTable(Table oldTable, Table newTable) {
StringBuilder script = new StringBuilder();
- if (!StringUtils.equalsIgnoreCase(oldTable.getName(), newTable.getName())) {
- script.append(SQL_ALTER_TABLE).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(oldTable.getSchemaName())).append(SQLConstants.DOUBLE_QUOTE_DOT_DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(oldTable.getName())).append(SQLConstants.DOUBLE_QUOTE);
- script.append(SQLConstants.SPACE).append(SQL_RENAME).append(SQLConstants.DOUBLE_QUOTE).append(DMIdentifierProcessor.escapeIdentifier(newTable.getName())).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
+ if (!StringUtils.equals(oldTable.getName(), newTable.getName())) {
+ script.append(SQL_ALTER_TABLE)
+ .append(quoteQualifiedIdentifier(oldTable.getDatabaseName(), oldTable.getSchemaName(), oldTable.getName()));
+ script.append(SQLConstants.SPACE).append(SQL_RENAME).append(quoteIdentifier(newTable.getName()))
+ .append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
}
if (!StringUtils.equalsIgnoreCase(oldTable.getComment(), newTable.getComment())) {
script.append(SQLConstants.EMPTY).append(buildTableComment(newTable)).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
@@ -162,9 +205,7 @@ public String buildAlterTable(Table oldTable, Table newTable) {
String editStatus = tableColumn.getEditStatus();
if (StringUtils.isNotBlank(editStatus)) {
DMColumnTypeEnum typeEnum = DMColumnTypeEnum.getByType(tableColumn.getColumnType());
- if(typeEnum == null){
- continue;
- }
+ typeEnum = typeEnum == null ? DMColumnTypeEnum.VARCHAR : typeEnum;
script.append(SQLConstants.TAB).append(typeEnum.buildModifyColumn(tableColumn)).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
if (StringUtils.isNotBlank(tableColumn.getComment())&&!Objects.equals(EditStatusEnum.DELETE.toString(),editStatus)) {
script.append(SQLConstants.LINE_SEPARATOR).append(buildComment(tableColumn)).append(SQLConstants.SEMICOLON_LINE_SEPARATOR);
@@ -209,14 +250,50 @@ public String buildPageLimit(PageLimitRequest request) {
return sqlStr.toString();
}
+ @Override
+ protected void buildTableName(String databaseName, String schemaName, String tableName, StringBuilder script) {
+ script.append(quoteQualifiedIdentifier(databaseName, schemaName, tableName));
+ }
+
+ @Override
+ protected void buildColumns(List columnList, StringBuilder script) {
+ if (CollectionUtils.isNotEmpty(columnList)) {
+ script.append(SQLConstants.SPACE_OPEN_PARENTHESIS)
+ .append(columnList.stream().map(this::quoteIdentifier)
+ .collect(Collectors.joining(SQLConstants.COMMA)))
+ .append(SQLConstants.CLOSE_PARENTHESIS);
+ }
+ }
+
+ @Override
+ public String buildUpdate(UpdateSqlRequest request) {
+ StringBuilder script = new StringBuilder("UPDATE ");
+ buildTableName(request.getDatabaseName(), request.getSchemaName(), request.getTableName(), script);
+ script.append(" SET ");
+ script.append(request.getRow().entrySet().stream()
+ .map(entry -> quoteIdentifier(entry.getKey()) + SQLConstants.EQUAL_SQL + entry.getValue())
+ .collect(Collectors.joining(SQLConstants.COMMA)));
+ if (MapUtils.isNotEmpty(request.getPrimaryKeyMap())) {
+ script.append(" WHERE ");
+ script.append(request.getPrimaryKeyMap().entrySet().stream()
+ .map(entry -> quoteIdentifier(entry.getKey()) + SQLConstants.EQUAL_SQL + entry.getValue())
+ .collect(Collectors.joining(SQLConstants.SQL_AND)));
+ }
+ return script.toString();
+ }
+
@Override
public String buildCreateSchema(Schema schema) {
StringBuilder sqlBuilder = new StringBuilder();
- sqlBuilder.append(SQL_CREATE_SCHEMA+DMIdentifierProcessor.escapeIdentifier(schema.getName())+SQLConstants.DOUBLE_QUOTE);
+ sqlBuilder.append(SQL_CREATE_SCHEMA).append(quoteIdentifier(schema.getName()));
if(StringUtils.isNotBlank(schema.getOwner())){
- sqlBuilder.append(SQLConstants.SCHEMA_AUTHORIZATION_SQL).append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schema.getOwner()));
+ sqlBuilder.append(SQLConstants.SCHEMA_AUTHORIZATION_SQL).append(quoteIdentifier(schema.getOwner()));
}
return sqlBuilder.toString();
}
+
+ private static String quoteStringLiteral(String value) {
+ return DMIdentifierProcessor.INSTANCE.quoteStringLiteral(value);
+ }
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/constant/DMDBManagerConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/constant/DMDBManagerConstants.java
index 14bc656884..14848f7e43 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/constant/DMDBManagerConstants.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/constant/DMDBManagerConstants.java
@@ -35,7 +35,7 @@ public final class DMDBManagerConstants {
public static final String SQL_COMMENT_COLUMN = "COMMENT ON COLUMN ";
public static final String SQL_COMMENT_TABLE = "COMMENT ON TABLE ";
public static final String SQL_DROP_TABLE_EXISTS = "DROP TABLE IF EXISTS %s";
- public static final String SQL_SET_SCHEMA = "SET SCHEMA \"%s\"";
+ public static final String SQL_SET_SCHEMA = "SET SCHEMA %s";
public static final String SQL_SELECT_DBMS_METADATA_GET_DDL = "SELECT DBMS_METADATA.GET_DDL('VIEW','%s','%s') as ddl FROM DUAL;";
public static final String SQL_SELECT_TABLE_NAME_ALL_TABLES = "SELECT TABLE_NAME FROM ALL_TABLES where OWNER='%s' ";
public static final String ROUTINES_SQL = "SELECT OWNER, NAME, TEXT FROM ALL_SOURCE WHERE TYPE = '%s' AND OWNER = '%s' AND NAME = '%s' ORDER BY LINE";
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/constant/DMSqlBuilderConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/constant/DMSqlBuilderConstants.java
index 893445b707..1f09273913 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/constant/DMSqlBuilderConstants.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/constant/DMSqlBuilderConstants.java
@@ -32,7 +32,7 @@ public final class DMSqlBuilderConstants {
public static final String SQL_ALTER_TABLE = "ALTER TABLE ";
public static final String SQL_COMMENT_COLUMN = "COMMENT ON COLUMN ";
public static final String SQL_COMMENT_TABLE = "COMMENT ON TABLE ";
- public static final String SQL_CREATE_SCHEMA = "CREATE SCHEMA \"";
+ public static final String SQL_CREATE_SCHEMA = "CREATE SCHEMA ";
public static final String SQL_CREATE_TABLE = "CREATE TABLE ";
public static final String SQL_LIMIT = " LIMIT ";
public static final String SQL_OFFSET = " OFFSET ";
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
index 161c373209..eab5732bb4 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMColumnTypeEnum.java
@@ -6,12 +6,12 @@
import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum;
import ai.chat2db.community.domain.api.model.metadata.ColumnType;
import ai.chat2db.community.domain.api.model.metadata.TableColumn;
-import ai.chat2db.spi.util.SqlUtils;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import static ai.chat2db.plugin.dm.constant.DMColumnTypeEnumConstants.*;
@@ -131,8 +131,10 @@ public enum DMColumnTypeEnum implements IColumnBuilder {
private ColumnType columnType;
public static DMColumnTypeEnum getByType(String dataType) {
- String type = SqlUtils.removeDigits(dataType.toUpperCase());
- return COLUMN_TYPE_MAP.get(type);
+ if (StringUtils.isBlank(dataType)) {
+ return null;
+ }
+ return COLUMN_TYPE_MAP.get(StringUtils.normalizeSpace(dataType).toUpperCase(Locale.ROOT));
}
private static Map COLUMN_TYPE_MAP = Maps.newHashMap();
@@ -154,9 +156,9 @@ public ColumnType getColumnType() {
@Override
public String buildCreateColumnSql(TableColumn column) {
- DMColumnTypeEnum type = COLUMN_TYPE_MAP.get(column.getColumnType().toUpperCase());
+ DMColumnTypeEnum type = getByType(column.getColumnType());
if (type == null) {
- return buildDefaultColumn(column, false);
+ return buildUnknownColumnSql(column);
}
StringBuilder script = new StringBuilder();
@@ -175,9 +177,9 @@ public String buildCreateColumnSql(TableColumn column) {
@Override
public String buildAICreateColumnSql(TableColumn column) {
- DMColumnTypeEnum type = COLUMN_TYPE_MAP.get(column.getColumnType().toUpperCase());
+ DMColumnTypeEnum type = getByType(column.getColumnType());
if (type == null) {
- return buildDefaultColumn(column, false);
+ return buildUnknownColumnSql(column);
}
StringBuilder script = new StringBuilder();
@@ -245,7 +247,8 @@ private String buildDataType(TableColumn column, DMColumnTypeEnum type) {
if (column.getColumnSize() != null && StringUtils.isEmpty(column.getUnit())) {
script.append("(").append(column.getColumnSize()).append(")");
} else if (column.getColumnSize() != null && !StringUtils.isEmpty(column.getUnit())) {
- script.append("(").append(column.getColumnSize()).append(" ").append(column.getUnit()).append(")");
+ script.append("(").append(column.getColumnSize()).append(" ")
+ .append(DMSqlGuards.requireUnit(column.getUnit())).append(")");
}
return script.toString();
}
@@ -299,25 +302,29 @@ public String buildModifyColumn(TableColumn tableColumn) {
if (EditStatusEnum.DELETE.name().equals(tableColumn.getEditStatus())) {
StringBuilder script = new StringBuilder();
- script.append(SQL_ALTER_TABLE).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getTableName())).append("\"");
- script.append(" ").append(SQL_DROP_COLUMN).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getName())).append("\"");
+ script.append(SQL_ALTER_TABLE).append(qualifiedTableName(tableColumn));
+ script.append(" ").append(SQL_DROP_COLUMN)
+ .append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getName()));
return script.toString();
}
if (EditStatusEnum.ADD.name().equals(tableColumn.getEditStatus())) {
StringBuilder script = new StringBuilder();
- script.append(SQL_ALTER_TABLE).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getTableName())).append("\"");
+ script.append(SQL_ALTER_TABLE).append(qualifiedTableName(tableColumn));
script.append(" ").append("ADD (").append(buildCreateColumnSql(tableColumn)).append(")");
return script.toString();
}
if (EditStatusEnum.MODIFY.name().equals(tableColumn.getEditStatus())) {
StringBuilder script = new StringBuilder();
if (!StringUtils.equals(tableColumn.getOldName(), tableColumn.getName())) {
- script.append(SQL_ALTER_TABLE).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getTableName())).append("\"");
- script.append(" ").append(SQL_RENAME_COLUMN).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getOldName())).append("\"").append(" TO ").append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getName())).append("\"");
+ script.append(SQL_ALTER_TABLE).append(qualifiedTableName(tableColumn));
+ script.append(" ").append(SQL_RENAME_COLUMN)
+ .append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getOldName()))
+ .append(" TO ")
+ .append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getName()));
script.append(";\n");
}
- script.append(SQL_ALTER_TABLE).append("\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableColumn.getTableName())).append("\"");
+ script.append(SQL_ALTER_TABLE).append(qualifiedTableName(tableColumn));
script.append(" ").append("MODIFY (").append(buildCreateColumnSql(tableColumn)).append(") \n");
return script.toString();
@@ -326,6 +333,35 @@ public String buildModifyColumn(TableColumn tableColumn) {
return "";
}
+ private static String buildUnknownColumnSql(TableColumn column) {
+ StringBuilder script = new StringBuilder();
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName()))
+ .append(" ")
+ .append(DMSqlGuards.requireColumnTypeExpression(column.getColumnType()));
+ if (StringUtils.isNotEmpty(column.getDefaultValue())) {
+ String defaultValue = column.getDefaultValue();
+ if ("EMPTY_STRING".equalsIgnoreCase(defaultValue.trim())) {
+ script.append(" DEFAULT ''");
+ } else if ("NULL".equalsIgnoreCase(defaultValue.trim())) {
+ script.append(" DEFAULT NULL");
+ } else {
+ script.append(" DEFAULT ").append(DMSqlGuards.requireDefaultExpression(defaultValue));
+ }
+ }
+ if (column.getNullable() != null) {
+ script.append(column.getNullable() == 1 ? " NULL" : " NOT NULL");
+ }
+ return script.toString();
+ }
+
+ private static String qualifiedTableName(TableColumn column) {
+ String tableName = DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getTableName());
+ if (StringUtils.isBlank(column.getSchemaName())) {
+ return tableName;
+ }
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getSchemaName()) + "." + tableName;
+ }
+
public static List getTypes() {
return Arrays.stream(DMColumnTypeEnum.values()).map(columnTypeEnum ->
columnTypeEnum.getColumnType()
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
index 19ee1214ba..e291be7396 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/enums/type/DMIndexTypeEnum.java
@@ -1,5 +1,6 @@
package ai.chat2db.plugin.dm.enums.type;
+import ai.chat2db.plugin.dm.DMSqlGuards;
import ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor;
import ai.chat2db.community.domain.api.enums.plugin.EditStatusEnum;
import ai.chat2db.community.domain.api.model.metadata.IndexType;
@@ -73,14 +74,18 @@ public static DMIndexTypeEnum getByType(String type) {
public String buildIndexScript(TableIndex tableIndex) {
StringBuilder script = new StringBuilder();
if (PRIMARY_KEY.equals(this)) {
- script.append(SQL_ALTER_TABLE_2).append(DMIdentifierProcessor.escapeIdentifier(tableIndex.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableIndex.getTableName())).append("\" ADD PRIMARY KEY ").append(buildIndexColumn(tableIndex));
+ script.append(SQL_ALTER_TABLE)
+ .append(qualifiedName(tableIndex.getSchemaName(), tableIndex.getTableName()))
+ .append(" ADD PRIMARY KEY ").append(buildIndexColumn(tableIndex));
} else {
if (UNIQUE.equals(this)) {
script.append(SQL_CREATE_UNIQUE_INDEX);
} else {
script.append(SQL_CREATE_INDEX);
}
- script.append(buildIndexName(tableIndex)).append(SQL_ON).append(DMIdentifierProcessor.escapeIdentifier(tableIndex.getSchemaName())).append("\".\"").append(DMIdentifierProcessor.escapeIdentifier(tableIndex.getTableName())).append("\" ").append(buildIndexColumn(tableIndex));
+ script.append(buildIndexName(tableIndex)).append(" ON ")
+ .append(qualifiedName(tableIndex.getSchemaName(), tableIndex.getTableName()))
+ .append(" ").append(buildIndexColumn(tableIndex));
}
return script.toString();
}
@@ -89,26 +94,27 @@ public String buildIndexScript(TableIndex tableIndex) {
private String buildIndexColumn(TableIndex tableIndex) {
StringBuilder script = new StringBuilder();
script.append("(");
+ boolean hasColumn = false;
for (TableIndexColumn column : tableIndex.getColumnList()) {
if (StringUtils.isNotBlank(column.getColumnName())) {
+ hasColumn = true;
script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getColumnName()));
if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) {
- String ascOrDesc = column.getAscOrDesc();
- if (!"ASC".equalsIgnoreCase(ascOrDesc) && !"DESC".equalsIgnoreCase(ascOrDesc)) {
- throw new IllegalArgumentException("Invalid index column sort order: " + ascOrDesc);
- }
- script.append(" ").append(ascOrDesc);
+ script.append(" ").append(DMSqlGuards.requireAscOrDesc(column.getAscOrDesc()));
}
script.append(",");
}
}
+ if (!hasColumn) {
+ throw new IllegalArgumentException("DM index must contain at least one named column");
+ }
script.deleteCharAt(script.length() - 1);
script.append(")");
return script.toString();
}
private String buildIndexName(TableIndex tableIndex) {
- return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getSchemaName()) + "." + DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getName());
+ return qualifiedName(tableIndex.getSchemaName(), tableIndex.getName());
}
public String buildModifyIndex(TableIndex tableIndex) {
@@ -126,7 +132,7 @@ public String buildModifyIndex(TableIndex tableIndex) {
private String buildDropIndex(TableIndex tableIndex) {
if (DMIndexTypeEnum.PRIMARY_KEY.getName().equals(tableIndex.getType())) {
- String tableName = DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getSchemaName()) + "." + DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getTableName());
+ String tableName = qualifiedName(tableIndex.getSchemaName(), tableIndex.getTableName());
return StringUtils.join(SQL_ALTER_TABLE,tableName,SQL_DROP_PRIMARY_KEY);
}
StringBuilder script = new StringBuilder();
@@ -136,6 +142,14 @@ private String buildDropIndex(TableIndex tableIndex) {
return script.toString();
}
+ private static String qualifiedName(String schemaName, String objectName) {
+ String quotedObject = DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(objectName);
+ if (StringUtils.isBlank(schemaName)) {
+ return quotedObject;
+ }
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName) + "." + quotedObject;
+ }
+
public static List getIndexTypes() {
return Arrays.asList(DMIndexTypeEnum.values()).stream().map(DMIndexTypeEnum::getIndexType).collect(java.util.stream.Collectors.toList());
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java
index d787c5cc18..765933ede9 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/identifier/DMIdentifierProcessor.java
@@ -4,6 +4,7 @@
import org.apache.commons.lang3.StringUtils;
import java.util.HashSet;
+import java.util.Locale;
import java.util.Set;
public class DMIdentifierProcessor extends DefaultSQLIdentifierProcessor {
@@ -254,7 +255,7 @@ public class DMIdentifierProcessor extends DefaultSQLIdentifierProcessor {
@Override
public boolean isReservedKeyword(String identifier, Integer majorVersion, Integer minorVersion) {
- return DM_RESERVED_KEYWORDS.contains(identifier);
+ return identifier != null && DM_RESERVED_KEYWORDS.contains(identifier.toUpperCase(Locale.ROOT));
}
/**
@@ -271,11 +272,18 @@ public String quoteIdentifier(String identifier, Integer majorVersion, Integer m
@Override
public String quoteIdentifier(String identifier) {
+ if (identifier == null) {
+ return null;
+ }
if (StringUtils.isBlank(identifier)) {
return identifier;
}
+ if (isValidQuotedIdentifier(identifier)) {
+ return identifier;
+ }
if (isValidIdentifier(identifier)
- && !isReservedKeyword(identifier.toUpperCase(java.util.Locale.ROOT), null, null)) {
+ && !containsLowerCase(identifier)
+ && !isReservedKeyword(identifier, null, null)) {
return identifier;
}
return quoteIdentifierAlways(identifier);
@@ -283,21 +291,23 @@ public String quoteIdentifier(String identifier) {
/**
* Unconditional quoting for DDL-generation call sites: wraps with double
- * quotes, stripping one surrounding quote pair and doubling every embedded
- * double quote. Returns {@code null} for {@code null}.
+ * quotes and doubling every embedded double quote, including quotes at the
+ * raw name boundaries. Returns {@code null} for {@code null}.
*/
- public String quoteIdentifierAlways(String identifier) {
+ @Override
+ public String quoteIdentifierIgnoreCase(String identifier) {
if (identifier == null) {
return null;
}
- return "\"" + escapeIdentifierContent(identifier) + "\"";
- }
-
- /**
- * Always-quote SPI variant that preserves the original identifier case.
- */
- @Override
- public String quoteIdentifierIgnoreCase(String identifier) {
+ if (StringUtils.isBlank(identifier)) {
+ return identifier;
+ }
+ if (isValidQuotedIdentifier(identifier)) {
+ return identifier;
+ }
+ if (isValidIdentifier(identifier) && !isReservedKeyword(identifier, null, null)) {
+ return identifier;
+ }
return quoteIdentifierAlways(identifier);
}
@@ -314,28 +324,48 @@ public String escapeString(String str) {
public String convertIdentifierCase(String identifier) {
if (StringUtils.isBlank(identifier)) {
return identifier;
- }else {
- return identifier.toUpperCase();
+ } else {
+ return identifier.toUpperCase(Locale.ROOT);
}
}
private static String escapeIdentifierContent(String identifier) {
- if (identifier == null) {
- return null;
- }
- String unquoted = identifier;
- if (unquoted.length() >= 2 && unquoted.startsWith("\"") && unquoted.endsWith("\"")) {
- unquoted = unquoted.substring(1, unquoted.length() - 1);
- }
- return unquoted.replace("\"", "\"\"");
+ return identifier == null ? null : StringUtils.replace(identifier, "\"", "\"\"");
}
/**
* Escapes identifier content for a position already surrounded by double
- * quotes: strips one surrounding quote pair, then doubles every embedded
- * double quote. Returns {@code null} for {@code null}.
+ * quotes. Returns {@code null} for {@code null}.
*/
public static String escapeIdentifier(String identifier) {
return escapeIdentifierContent(identifier);
}
+
+ @Override
+ public String quoteIdentifierAlways(String identifier) {
+ if (identifier == null) {
+ return null;
+ }
+ return "\"" + escapeIdentifierContent(identifier) + "\"";
+ }
+
+ public String quoteStringLiteral(String value) {
+ return value == null ? null : "'" + escapeString(value) + "'";
+ }
+
+ private static boolean isValidQuotedIdentifier(String identifier) {
+ if (identifier.length() < 2 || identifier.charAt(0) != '"'
+ || identifier.charAt(identifier.length() - 1) != '"') {
+ return false;
+ }
+ for (int i = 1; i < identifier.length() - 1; i++) {
+ if (identifier.charAt(i) == '"') {
+ if (i + 1 >= identifier.length() - 1 || identifier.charAt(i + 1) != '"') {
+ return false;
+ }
+ i++;
+ }
+ }
+ return true;
+ }
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/value/sub/DMBitProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/value/sub/DMBitProcessor.java
index 649082411d..c4cc4c54af 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/value/sub/DMBitProcessor.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/value/sub/DMBitProcessor.java
@@ -1,5 +1,6 @@
package ai.chat2db.plugin.dm.value.sub;
+import ai.chat2db.plugin.dm.DMSqlGuards;
import ai.chat2db.spi.DefaultValueProcessor;
import ai.chat2db.spi.model.value.JDBCDataValue;
import ai.chat2db.community.domain.api.model.value.SQLDataValue;
@@ -11,7 +12,7 @@ public class DMBitProcessor extends DefaultValueProcessor {
@Override
public String convertSQLValueByType(SQLDataValue dataValue) {
- return dataValue.getValue();
+ return DMSqlGuards.requireBitLiteral(dataValue.getValue());
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
index fe315939ab..d5a8833709 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
@@ -1,17 +1,28 @@
package ai.chat2db.plugin.dm;
+import ai.chat2db.community.domain.api.model.metadata.DataType;
+import ai.chat2db.community.domain.api.enums.plugin.DmlTypeEnum;
import ai.chat2db.community.domain.api.model.metadata.Schema;
import ai.chat2db.community.domain.api.model.metadata.Table;
import ai.chat2db.community.domain.api.model.metadata.TableColumn;
import ai.chat2db.community.domain.api.model.metadata.TableIndex;
import ai.chat2db.community.domain.api.model.metadata.TableIndexColumn;
+import ai.chat2db.community.domain.api.model.value.SQLDataValue;
import ai.chat2db.plugin.dm.builder.DMSqlBuilder;
import ai.chat2db.plugin.dm.enums.type.DMColumnTypeEnum;
import ai.chat2db.plugin.dm.enums.type.DMIndexTypeEnum;
import ai.chat2db.plugin.dm.identifier.DMIdentifierProcessor;
+import ai.chat2db.plugin.dm.value.DMValueProcessor;
+import ai.chat2db.plugin.dm.value.sub.DMBitProcessor;
+import ai.chat2db.spi.model.request.DropTableRequest;
+import ai.chat2db.spi.model.request.SingleInsertSqlRequest;
+import ai.chat2db.spi.model.request.TruncateTableRequest;
+import ai.chat2db.spi.model.request.UpdateSqlRequest;
import org.junit.jupiter.api.Test;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -22,151 +33,335 @@
class DMIdentifierProcessorTest {
@Test
- void escapeSqlLiteralDoublesSingleQuotes() {
- assertEquals("O''Brien", DMIdentifierProcessor.INSTANCE.escapeString("O'Brien"));
- assertEquals("x'' OR ''1''=''1", DMIdentifierProcessor.INSTANCE.escapeString("x' OR '1'='1"));
- assertEquals("plain", DMIdentifierProcessor.INSTANCE.escapeString("plain"));
+ void quoteIdentifierPassesThroughNullAndBlank() {
+ assertNull(DMIdentifierProcessor.INSTANCE.quoteIdentifier(null));
+ assertNull(DMIdentifierProcessor.INSTANCE.quoteIdentifier(null, null, null));
+ assertNull(DMIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase(null));
+ assertEquals("", DMIdentifierProcessor.INSTANCE.quoteIdentifier(""));
+ assertEquals(" ", DMIdentifierProcessor.INSTANCE.quoteIdentifier(" "));
+ assertEquals(" ", DMIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase(" "));
}
@Test
- void quoteIdentifierIsConditionalForSpiConsumers() {
- assertNull(DMIdentifierProcessor.INSTANCE.quoteIdentifier(null));
- assertEquals("", DMIdentifierProcessor.INSTANCE.quoteIdentifier(""));
- assertEquals("plain", DMIdentifierProcessor.INSTANCE.quoteIdentifier("plain"));
- assertEquals("plain", DMIdentifierProcessor.INSTANCE.quoteIdentifier("plain", null, null));
- assertEquals("\"SELECT\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("SELECT"));
- assertEquals("\"select\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("select"));
- assertEquals("\"weird name\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("weird name"));
- assertEquals("\"we\"\"ird\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("we\"ird"));
+ void quoteIdentifierPreservesDmCaseSemantics() {
+ DMIdentifierProcessor processor = DMIdentifierProcessor.INSTANCE;
+ assertEquals("EMPLOYEES", processor.quoteIdentifier("EMPLOYEES"));
+ assertEquals("\"employees\"", processor.quoteIdentifier("employees"));
+ assertEquals("\"MixedCase\"", processor.quoteIdentifier("MixedCase"));
+ assertEquals("\"SELECT\"", processor.quoteIdentifier("SELECT"));
+ assertEquals("\"select\"", processor.quoteIdentifier("select", null, null));
+ assertEquals("\"A\"\"B\"", processor.quoteIdentifier("A\"B"));
+ assertEquals("\"ALREADY\"", processor.quoteIdentifier("\"ALREADY\""));
}
@Test
- void quoteIdentifierAlwaysQuotesUnconditionally() {
- assertNull(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(null));
- assertEquals("\"plain\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways("plain"));
- assertEquals("\"SELECT\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways("SELECT"));
- assertEquals("\"we\"\"ird\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways("we\"ird"));
+ void quoteIdentifierIgnoreCaseIsConditional() {
+ DMIdentifierProcessor processor = DMIdentifierProcessor.INSTANCE;
+ assertEquals("employees", processor.quoteIdentifierIgnoreCase("employees"));
+ assertEquals("EMPLOYEES", processor.quoteIdentifierIgnoreCase("EMPLOYEES"));
+ assertEquals("\"select\"", processor.quoteIdentifierIgnoreCase("select"));
+ assertEquals("\"A\"\"B\"", processor.quoteIdentifierIgnoreCase("A\"B"));
}
@Test
- void quoteIdentifierIgnoreCaseIsTheAlwaysQuoteVariant() {
- assertEquals("\"plain\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("plain"));
- assertEquals("\"MixedCase\"", DMIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("MixedCase"));
+ void quoteIdentifierAlwaysRoundTripsEveryRawName() {
+ DMIdentifierProcessor processor = DMIdentifierProcessor.INSTANCE;
+ assertNull(processor.quoteIdentifierAlways(null));
+ String[] rawIdentifiers = {"", "plain", "SELECT", "MixedCase", "A\"B", "\"ALREADY\"",
+ "\"A", "A\"", "\"\"", "A\"\"B"};
+ for (String raw : rawIdentifiers) {
+ assertEquals(raw, processor.removeIdentifierQuote(processor.quoteIdentifierAlways(raw)),
+ "always-quote round trip must preserve the raw identifier");
+ }
}
@Test
- void escapeIdentifierStripsOneSurroundingQuotePairBeforeDoubling() {
- assertEquals("already", DMIdentifierProcessor.escapeIdentifier("\"already\""));
+ void stringAndIdentifierContentEscapersEncodeEveryDelimiter() {
+ assertEquals("O''Brien", DMIdentifierProcessor.INSTANCE.escapeString("O'Brien"));
+ assertEquals("'C:\\tmp\\O''Brien'", DMIdentifierProcessor.INSTANCE.quoteStringLiteral("C:\\tmp\\O'Brien"));
+ assertNull(DMIdentifierProcessor.INSTANCE.escapeString(null));
assertEquals("a\"\"b", DMIdentifierProcessor.escapeIdentifier("a\"b"));
- assertEquals("plain", DMIdentifierProcessor.escapeIdentifier("plain"));
+ assertEquals("\"\"already\"\"", DMIdentifierProcessor.escapeIdentifier("\"already\""));
}
@Test
- void createTableSqlNeutralizesMaliciousSchemaAndComment() {
- Table table = new Table();
- table.setSchemaName("s\"; DROP TABLE t; --");
- table.setName("tab");
- table.setComment("x'; DROP TABLE t; --");
- TableColumn column = new TableColumn();
- column.setName("c1");
- column.setColumnType("INT");
- table.setColumnList(List.of(column));
- table.setIndexList(List.of());
+ void reservedWordsAndCaseConversionAreLocaleIndependent() {
+ Locale original = Locale.getDefault();
+ try {
+ Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+ assertTrue(DMIdentifierProcessor.INSTANCE.isReservedKeyword("insert", null, null));
+ assertEquals("ID", DMIdentifierProcessor.INSTANCE.convertIdentifierCase("id"));
+ assertEquals("\"insert\"", DMIdentifierProcessor.INSTANCE.quoteIdentifier("insert"));
+ } finally {
+ Locale.setDefault(original);
+ }
+ }
+
+ @Test
+ void createTableEscapesNamesAndComments() {
+ Table table = table("S\"CHEMA", "T\"ABLE", "VARCHAR");
+ table.setComment("x'); DROP TABLE USERS; --");
+ TableColumn column = table.getColumnList().get(0);
+ column.setName("C\"OL");
+ column.setComment("c'); DROP TABLE U; --");
+
+ String sql = new DMSqlBuilder().buildCreateTable(table, null);
+
+ assertTrue(sql.startsWith("CREATE TABLE \"S\"\"CHEMA\".\"T\"\"ABLE\" ("), sql);
+ assertTrue(sql.contains("COMMENT ON COLUMN \"S\"\"CHEMA\".\"T\"\"ABLE\".\"C\"\"OL\" IS 'c''); DROP TABLE U; --'"), sql);
+ assertTrue(sql.contains("COMMENT ON TABLE \"S\"\"CHEMA\".\"T\"\"ABLE\" IS 'x''); DROP TABLE USERS; --'"), sql);
+ }
+
+ @Test
+ void createTableOmitsBlankQualifierInsteadOfRenderingNullSchema() {
+ Table table = table(null, "ORDERS", "INT");
String sql = new DMSqlBuilder().buildCreateTable(table, null);
- assertTrue(sql.contains("\"s\"\"; DROP TABLE t; --\".\"tab\""));
- assertFalse(sql.contains("\"s\"; DROP TABLE t; --\""));
- assertTrue(sql.contains("IS 'x''; DROP TABLE t; --'"));
- assertFalse(sql.contains("IS 'x'; DROP TABLE t; --'"));
+ assertTrue(sql.startsWith("CREATE TABLE \"ORDERS\" ("), sql);
+ assertFalse(sql.contains("\"null\""), sql);
}
@Test
- void dropTableQuotesAndEscapesTableName() {
- String sql = new DMDBManager().dropTable(null, null, null, "a\"; DROP TABLE b; --");
+ void managerUsesSchemaQualifiedNames() throws Exception {
+ DMDBManager manager = new DMDBManager();
+ assertEquals("DROP TABLE IF EXISTS \"SA\"\"LES\".\"ORDERS\"",
+ manager.dropTable(null, "ignored_database", "SA\"LES", "ORDERS"));
+ assertEquals("TRUNCATE TABLE \"SA\"\"LES\".\"OR\"\"DERS\"",
+ manager.truncateTable(null, "ignored_database", "SA\"LES", "\"OR\"\"DERS\""));
+ assertEquals("DROP TABLE IF EXISTS \"T\"\"; DROP TABLE U; --\"",
+ manager.dropTable(null, null, null, "T\"; DROP TABLE U; --"));
+ }
- assertEquals("DROP TABLE IF EXISTS \"a\"\"; DROP TABLE b; --\"", sql);
+ @Test
+ void inheritedBuilderPathsUseDmQualification() {
+ DMSqlBuilder builder = new DMSqlBuilder();
+ assertEquals("SELECT * FROM \"SA\"\"LES\".\"ORDERS\"",
+ builder.buildSelectTable("ignored_database", "SA\"LES", "ORDERS"));
+ assertEquals("SELECT COUNT(1) FROM \"SA\"\"LES\".\"ORDERS\"",
+ builder.buildSelectCount("ignored_database", "SA\"LES", "ORDERS"));
+ assertEquals("DROP TABLE \"SA\"\"LES\".\"ORDERS\"",
+ builder.buildDropTable(new DropTableRequest("ignored_database", "SA\"LES", "ORDERS")));
+ assertEquals("TRUNCATE TABLE \"SA\"\"LES\".\"ORDERS\"",
+ builder.buildTruncateTable(new TruncateTableRequest("ignored_database", "SA\"LES", "ORDERS")));
+ assertEquals("INSERT INTO \"SA\"\"LES\".\"ORDERS\" (\"C\"\"OL\") VALUES (1)",
+ builder.buildInsert(SingleInsertSqlRequest.builder()
+ .databaseName("ignored_database")
+ .schemaName("SA\"LES")
+ .tableName("ORDERS")
+ .columnList(List.of("C\"OL"))
+ .valueList(List.of("1"))
+ .build()));
+ assertEquals("UPDATE \"SA\"\"LES\".\"ORDERS\" SET \"C\"\"OL\" = 1 WHERE \"I\"\"D\" = 2",
+ builder.buildUpdate(UpdateSqlRequest.builder()
+ .databaseName("ignored_database")
+ .schemaName("SA\"LES")
+ .tableName("ORDERS")
+ .row(Map.of("C\"OL", "1"))
+ .primaryKeyMap(Map.of("I\"D", "2"))
+ .build()));
}
@Test
- void metaDataNameEscapesEachIdentifierPart() {
- String name = new DMMetaData().getMetaDataName("sch\"ema", "ta\"ble");
+ void dmlTemplatesAlwaysQuoteSchemaTableAndColumns() {
+ Table table = table("SA\"LES", "OR\"DERS", "INT");
+ table.getColumnList().get(0).setName("C\"OL");
+ DMSqlBuilder builder = new DMSqlBuilder();
+
+ assertEquals("INSERT INTO \"SA\"\"LES\".\"OR\"\"DERS\" (\"C\"\"OL\") VALUES ( )",
+ builder.buildTemplate(table, DmlTypeEnum.INSERT.name()));
+ assertEquals("UPDATE \"SA\"\"LES\".\"OR\"\"DERS\" SET \"C\"\"OL\" = WHERE ",
+ builder.buildTemplate(table, DmlTypeEnum.UPDATE.name()));
+ assertEquals("DELETE FROM \"SA\"\"LES\".\"OR\"\"DERS\" WHERE ",
+ builder.buildTemplate(table, DmlTypeEnum.DELETE.name()));
+ assertEquals("SELECT \"C\"\"OL\" FROM \"SA\"\"LES\".\"OR\"\"DERS\"",
+ builder.buildTemplate(table, DmlTypeEnum.SELECT.name()));
+ }
- assertEquals("\"sch\"\"ema\".\"ta\"\"ble\"", name);
+ @Test
+ void metadataQualifiedNamesAreLimitedToSchemaAndObject() {
+ DMMetaData metaData = new DMMetaData();
+ assertEquals("\"SALES\".\"ORDERS\"",
+ metaData.getMetaDataName("ignored_database", "SALES", "ORDERS"));
+ assertEquals("\"SA\"\"LES\".\"OR\"\"DERS\"",
+ metaData.getMetaDataName("SA\"LES", "OR\"DERS"));
}
@Test
- void createSchemaQuotesOwnerAsIdentifier() {
+ void createSchemaQuotesNameAndOwner() {
Schema schema = new Schema();
schema.setName("app");
schema.setOwner("owner; DROP USER x; --");
- String sql = new DMSqlBuilder().buildCreateSchema(schema);
-
- assertEquals("CREATE SCHEMA \"app\" AUTHORIZATION \"owner; DROP USER x; --\"", sql);
+ assertEquals("CREATE SCHEMA \"app\" AUTHORIZATION \"owner; DROP USER x; --\"",
+ new DMSqlBuilder().buildCreateSchema(schema));
}
@Test
- void indexSortOrderAcceptsAscDescAndRejectsInjection() {
+ void indexScriptEscapesNamesAndCanonicalizesDirection() {
TableIndex index = new TableIndex();
- index.setSchemaName("s");
- index.setTableName("t");
- index.setName("i");
+ index.setType(DMIndexTypeEnum.NORMAL.getName());
+ index.setSchemaName("S\"; X");
+ index.setTableName("T");
+ index.setName("I\"X");
TableIndexColumn column = new TableIndexColumn();
- column.setColumnName("c");
- column.setAscOrDesc("desc");
+ column.setColumnName("C\"D");
+ column.setAscOrDesc(" desc ");
index.setColumnList(List.of(column));
- assertTrue(DMIndexTypeEnum.NORMAL.buildIndexScript(index).contains("\"c\" desc"));
+ assertEquals("CREATE INDEX \"S\"\"; X\".\"I\"\"X\" ON \"S\"\"; X\".\"T\" (\"C\"\"D\" DESC)",
+ DMIndexTypeEnum.NORMAL.buildIndexScript(index));
column.setAscOrDesc("DESC; DROP TABLE x; --");
assertThrows(IllegalArgumentException.class, () -> DMIndexTypeEnum.NORMAL.buildIndexScript(index));
}
@Test
- void quotedStringDefaultsAndUnitPassThroughUnchanged() {
- TableColumn column = new TableColumn();
- column.setName("c1");
- column.setColumnType("VARCHAR");
+ void knownColumnTypeAcceptsOnlySupportedUnit() {
+ TableColumn column = column("c1", "VARCHAR");
column.setColumnSize(10);
- column.setUnit("BYTE");
+ column.setUnit("byte");
column.setDefaultValue("'O''Brien'");
String sql = DMColumnTypeEnum.VARCHAR.buildCreateColumnSql(column);
- assertTrue(sql.contains("VARCHAR(10 BYTE)"));
- assertTrue(sql.contains("DEFAULT 'O''Brien'"));
+ assertTrue(sql.contains("VARCHAR(10 byte)"), sql);
+ assertTrue(sql.contains("DEFAULT 'O''Brien'"), sql);
+
+ column.setUnit("BYTE); DROP TABLE U; --");
+ assertThrows(IllegalArgumentException.class,
+ () -> DMColumnTypeEnum.VARCHAR.buildCreateColumnSql(column));
+ }
+
+ @Test
+ void defaultExpressionAcceptsLegitimateDmForms() {
+ String[] valid = {"SYSDATE", "CURRENT_TIMESTAMP", "USER", "SEQ.NEXTVAL", "-1", "1.5",
+ "'Y'", "'O''Brien'", "N'abc'", "X'1A'", "SYS_GUID()",
+ "NVL(SUM(x),0)", "TO_DATE('1970-01-01', 'YYYY-MM-DD')",
+ "CAST('1' AS NUMBER(10,2))", "\"My Seq\".NEXTVAL",
+ "TIMESTAMP '2020-01-01 00:00:00'", "INTERVAL '1' DAY",
+ "q'[O'Brien]'", "'a'||'b'", "now()"};
+ for (String defaultValue : valid) {
+ assertEquals(defaultValue, DMSqlGuards.requireDefaultExpression(defaultValue), defaultValue);
+ }
+ }
+
+ @Test
+ void defaultExpressionRejectsFragmentsThatReshapeDdl() {
+ String[] payloads = {"0) --", "0 --", "1, x INT", "0); DROP TABLE x--", "'abc", "0\n+1",
+ "'a'--", "'a'; DROP TABLE x--", "0 NOT NULL", "0 CHECK (1=1)",
+ "0 CONSTRAINT injected UNIQUE", "x' OR '1'='1", "NVL(1,/*comment*/0)"};
+ for (String payload : payloads) {
+ assertThrows(IllegalArgumentException.class,
+ () -> DMSqlGuards.requireDefaultExpression(payload), payload);
+ }
+ }
+
+ @Test
+ void unknownColumnTypesArePreservedOnlyWhenStructurallySafe() {
+ String[] valid = {"MYCUSTOMTYPE", "VARCHAR(20)", "NUMBER(10,2)",
+ "TIMESTAMP(6) WITH TIME ZONE", "INTERVAL DAY(2) TO SECOND(6)",
+ "VARCHAR(20 CHAR)", "\"APP\".\"Order Type\"", "REF \"APP\".\"Object Type\""};
+ for (String typeName : valid) {
+ TableColumn column = column("c1", typeName);
+ assertEquals("\"c1\" " + typeName,
+ DMColumnTypeEnum.VARCHAR.buildCreateColumnSql(column), typeName);
+ }
+
+ String[] payloads = {"INTEGER); DROP TABLE U; --", "INT, x INT", "INT'--", "INT\"--",
+ "0) --", "INTEGER NOT NULL", "VARCHAR(20) DEFAULT 0", "INTEGER CHECK(1=1)"};
+ for (String typeName : payloads) {
+ TableColumn column = column("c1", typeName);
+ assertThrows(IllegalArgumentException.class,
+ () -> DMColumnTypeEnum.VARCHAR.buildCreateColumnSql(column), typeName);
+ }
+ }
+
+ @Test
+ void createTableKeepsSafeUnknownTypeInsteadOfDroppingColumn() {
+ Table table = table("S", "T", "\"APP\".\"Order Type\"");
+
+ String sql = new DMSqlBuilder().buildCreateTable(table, null);
+
+ assertTrue(sql.contains("\"C\" \"APP\".\"Order Type\""), sql);
+ }
+
+ @Test
+ void caseOnlyColumnRenameIsNotSkipped() {
+ TableColumn column = column("mixedcase", "VARCHAR");
+ column.setEditStatus("MODIFY");
+ column.setSchemaName("S");
+ column.setTableName("T");
+ column.setOldName("MixedCase");
+
+ String sql = DMColumnTypeEnum.VARCHAR.buildModifyColumn(column);
+
+ assertTrue(sql.contains("RENAME COLUMN \"MixedCase\" TO \"mixedcase\""), sql);
+ }
+
+ @Test
+ void caseOnlyTableRenameIsNotSkipped() {
+ Table oldTable = table("S", "MixedCase", "VARCHAR");
+ Table newTable = table("S", "mixedcase", "VARCHAR");
+
+ String sql = new DMSqlBuilder().buildAlterTable(oldTable, newTable);
- TableColumn emptyStringDefault = new TableColumn();
- emptyStringDefault.setName("c2");
- emptyStringDefault.setColumnType("VARCHAR");
- emptyStringDefault.setDefaultValue("EMPTY_STRING");
+ assertTrue(sql.startsWith("ALTER TABLE \"S\".\"MixedCase\" RENAME TO \"mixedcase\""), sql);
+ }
+
+ @Test
+ void indexRequiresAtLeastOneNamedColumn() {
+ TableIndex index = new TableIndex();
+ index.setSchemaName("S");
+ index.setTableName("T");
+ index.setName("IDX");
+ index.setColumnList(List.of(new TableIndexColumn()));
- assertTrue(DMColumnTypeEnum.VARCHAR.buildCreateColumnSql(emptyStringDefault).contains("DEFAULT ''"));
+ assertThrows(IllegalArgumentException.class,
+ () -> DMIndexTypeEnum.NORMAL.buildIndexScript(index));
}
@Test
- void defaultExpressionAcceptsLegitimateAndRejectsInjection() {
- org.junit.jupiter.api.Assertions.assertEquals("'abc'", DMSqlGuards.requireDefaultExpression("'abc'"));
- org.junit.jupiter.api.Assertions.assertEquals("'O''Brien'", DMSqlGuards.requireDefaultExpression("'O''Brien'"));
- org.junit.jupiter.api.Assertions.assertEquals("-1.5", DMSqlGuards.requireDefaultExpression("-1.5"));
- org.junit.jupiter.api.Assertions.assertEquals("CURRENT_TIMESTAMP", DMSqlGuards.requireDefaultExpression("CURRENT_TIMESTAMP"));
- org.junit.jupiter.api.Assertions.assertEquals("SEQ.NEXTVAL", DMSqlGuards.requireDefaultExpression("SEQ.NEXTVAL"));
- org.junit.jupiter.api.Assertions.assertEquals("NVL(SUM(x),0)", DMSqlGuards.requireDefaultExpression("NVL(SUM(x),0)"));
- org.junit.jupiter.api.Assertions.assertEquals("NVL(SUM('a;b'),0)", DMSqlGuards.requireDefaultExpression("NVL(SUM('a;b'),0)"));
- org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
- () -> DMSqlGuards.requireDefaultExpression("1; DROP TABLE t"));
- org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
- () -> DMSqlGuards.requireDefaultExpression("x' OR '1'='1"));
+ void bitValuesAreCanonicalizedInsteadOfEmittedAsRawSql() {
+ DMBitProcessor processor = new DMBitProcessor();
+ SQLDataValue value = new SQLDataValue();
+ value.setValue(" true ");
+ assertEquals("1", processor.convertSQLValueByType(value));
+ value.setValue("false");
+ assertEquals("0", processor.convertSQLValueByType(value));
+ value.setValue(" ");
+ assertEquals("NULL", processor.convertSQLValueByType(value));
+ value.setValue("1); DROP TABLE U; --");
+ assertThrows(IllegalArgumentException.class, () -> processor.convertSQLValueByType(value));
}
@Test
- void createColumnRejectsMaliciousDefault() {
+ void dmlValueFallbackEscapesStringLiteralContent() {
+ SQLDataValue value = new SQLDataValue();
+ value.setValue("O'Brien");
+ DataType type = new DataType();
+ type.setDataTypeName("VARCHAR");
+ value.setDataType(type);
+
+ assertEquals("'O''Brien'", new DMValueProcessor().convertSQLValueByType(value));
+ }
+
+ private static Table table(String schemaName, String tableName, String columnType) {
+ Table table = new Table();
+ table.setSchemaName(schemaName);
+ table.setName(tableName);
+ TableColumn column = column("C", columnType);
+ column.setSchemaName(schemaName);
+ column.setTableName(tableName);
+ table.setColumnList(List.of(column));
+ table.setIndexList(List.of());
+ return table;
+ }
+
+ private static TableColumn column(String name, String columnType) {
TableColumn column = new TableColumn();
- column.setName("c");
- column.setColumnType("INT");
- column.setDefaultValue("1; DROP TABLE t;--");
- org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
- () -> DMColumnTypeEnum.INT.buildCreateColumnSql(column));
+ column.setName(name);
+ column.setColumnType(columnType);
+ return column;
}
}