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..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
@@ -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 "\"" + tableName + "\"";
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(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, 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, tableName, 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(tableComment.replace("'", "''")).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(comment.replace("'", "''"))
+ .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, 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, viewName, 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", schemaName, 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, 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, schemaName, 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,14 +227,50 @@ 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, 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, 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 e041d6a4eb..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
@@ -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 "\"" + tableName + "\"";
+ return DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(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, 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(tableComment.replace("'", "''")).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(comment.replace("'", "''"))
+ .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, indexName), resultSet -> {
+ DefaultSQLExecutor.getInstance().execute(connection, String.format(sql, getSQLIdentifierProcessor().escapeString(indexName)), resultSet -> {
if (resultSet.next()) {
ddlBuilder.append(resultSet.getString("INDEX_DDL")).append("\n");
}
@@ -138,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());
}
}
@@ -151,7 +148,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", getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(functionName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
StringBuilder sb = new StringBuilder();
while (resultSet.next()) {
@@ -171,7 +168,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", getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(procedureName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
StringBuilder sb = new StringBuilder();
while (resultSet.next()) {
@@ -193,7 +190,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, getSQLIdentifierProcessor().escapeString(schemaName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
while (resultSet.next()) {
Trigger trigger = new Trigger();
@@ -210,7 +207,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, getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(triggerName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
Trigger trigger = new Trigger();
trigger.setDatabaseName(databaseName);
@@ -227,7 +224,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, getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(viewName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
Table table = new Table();
table.setDatabaseName(databaseName);
@@ -244,7 +241,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, getSQLIdentifierProcessor().escapeString(schemaName), getSQLIdentifierProcessor().escapeString(tableName));
return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
LinkedHashMap map = new LinkedHashMap();
while (resultSet.next()) {
@@ -319,12 +316,16 @@ 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 -> "\"" + name + "\"").collect(Collectors.joining("."));
+ 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
new file mode 100644
index 0000000000..357c873b6c
--- /dev/null
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/main/java/ai/chat2db/plugin/dm/DMSqlGuards.java
@@ -0,0 +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;
+
+/**
+ * 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 one DM DEFAULT expression without re-encoding serialized
+ * literals returned by metadata.
+ */
+ public static String requireDefaultExpression(String value) {
+ if (StringUtils.isBlank(value)) {
+ throw invalid("DEFAULT expression", value);
+ }
+ scanExpression(value.trim(), false, "DEFAULT expression");
+ return value;
+ }
+
+ /**
+ * Validates one complete DM column type expression, including
+ * parameterized built-in and schema-qualified user-defined types.
+ */
+ public static String requireColumnTypeExpression(String typeName) {
+ if (StringUtils.isBlank(typeName)) {
+ throw invalid("column type", typeName);
+ }
+ scanExpression(typeName.trim(), true, "column type");
+ return typeName;
+ }
+
+ 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 trimmed;
+ }
+
+ public static String requireAscOrDesc(String value) {
+ String trimmed = StringUtils.trimToEmpty(value);
+ if ("ASC".equalsIgnoreCase(trimmed)) {
+ return "ASC";
+ }
+ if ("DESC".equalsIgnoreCase(trimmed)) {
+ return "DESC";
+ }
+ throw new IllegalArgumentException("Invalid DM index sort direction: " + value);
+ }
+
+ public static String requireBitLiteral(String value) {
+ if (StringUtils.isBlank(value)) {
+ return "NULL";
+ }
+ String trimmed = StringUtils.trimToEmpty(value);
+ if ("0".equals(trimmed) || "false".equalsIgnoreCase(trimmed)) {
+ return "0";
+ }
+ if ("1".equals(trimmed) || "true".equalsIgnoreCase(trimmed)) {
+ return "1";
+ }
+ 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 == '(') {
+ 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 (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;
+ }
+ 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;
+ }
+ }
+ return false;
+ }
+
+ 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 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 4920f62517..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
@@ -2,11 +2,14 @@
import ai.chat2db.spi.constant.SQLConstants;
+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.*;
@@ -19,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(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(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);
}
@@ -98,16 +139,16 @@ 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(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);
}
@@ -135,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(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);
- 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(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);
- 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(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);
+ 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);
@@ -161,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);
@@ -208,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+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(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 55e02416a6..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
@@ -1,15 +1,17 @@
package ai.chat2db.plugin.dm.enums.type;
+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;
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.*;
@@ -129,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();
@@ -152,13 +156,13 @@ 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();
- script.append("\"").append(column.getName()).append("\"").append(" ");
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" ");
script.append(buildDataType(column, type)).append(" ");
@@ -173,13 +177,13 @@ 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();
- script.append("\"").append(column.getName()).append("\"").append(" ");
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" ");
script.append(buildDataType(column, type)).append(" ");
@@ -232,7 +236,7 @@ private String buildDefaultValue(TableColumn column, DMColumnTypeEnum type) {
return StringUtils.join("DEFAULT NULL");
}
- return StringUtils.join("DEFAULT ", column.getDefaultValue());
+ return StringUtils.join("DEFAULT ", DMSqlGuards.requireDefaultExpression(column.getDefaultValue()));
}
private String buildDataType(TableColumn column, DMColumnTypeEnum type) {
@@ -243,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();
}
@@ -297,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(tableColumn.getSchemaName()).append("\".\"").append(tableColumn.getTableName()).append("\"");
- script.append(" ").append(SQL_DROP_COLUMN).append("\"").append(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(tableColumn.getSchemaName()).append("\".\"").append(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(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(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(tableColumn.getSchemaName()).append("\".\"").append(tableColumn.getTableName()).append("\"");
+ script.append(SQL_ALTER_TABLE).append(qualifiedTableName(tableColumn));
script.append(" ").append("MODIFY (").append(buildCreateColumnSql(tableColumn)).append(") \n");
return script.toString();
@@ -324,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 873a931460..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,7 @@
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;
import ai.chat2db.community.domain.api.model.metadata.TableIndex;
@@ -72,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(tableIndex.getSchemaName()).append("\".\"").append(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(tableIndex.getSchemaName()).append("\".\"").append(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();
}
@@ -88,22 +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())) {
- script.append("\"").append(column.getColumnName()).append("\"");
+ hasColumn = true;
+ script.append(DMIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getColumnName()));
if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) {
- script.append(" ").append(column.getAscOrDesc());
+ 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 "\"" + tableIndex.getSchemaName() + "\"." + "\"" + tableIndex.getName() + "\"";
+ return qualifiedName(tableIndex.getSchemaName(), tableIndex.getName());
}
public String buildModifyIndex(TableIndex tableIndex) {
@@ -121,7 +132,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 = qualifiedName(tableIndex.getSchemaName(), tableIndex.getTableName());
return StringUtils.join(SQL_ALTER_TABLE,tableName,SQL_DROP_PRIMARY_KEY);
}
StringBuilder script = new StringBuilder();
@@ -131,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 e84c08e74f..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,10 +4,16 @@
import org.apache.commons.lang3.StringUtils;
import java.util.HashSet;
+import java.util.Locale;
import java.util.Set;
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 {
@@ -249,50 +255,117 @@ 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));
}
+ /**
+ * 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
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, '"');
- }
+ if (identifier == null) {
+ return null;
+ }
+ if (StringUtils.isBlank(identifier)) {
return identifier;
}
- return StringUtils.wrap(identifier, '"');
-
+ if (isValidQuotedIdentifier(identifier)) {
+ return identifier;
+ }
+ if (isValidIdentifier(identifier)
+ && !containsLowerCase(identifier)
+ && !isReservedKeyword(identifier, null, null)) {
+ return identifier;
+ }
+ return quoteIdentifierAlways(identifier);
}
+ /**
+ * Unconditional quoting for DDL-generation call sites: wraps with double
+ * quotes and doubling every embedded double quote, including quotes at the
+ * raw name boundaries. Returns {@code null} for {@code null}.
+ */
@Override
public String quoteIdentifierIgnoreCase(String identifier) {
- if (isValidIdentifier(identifier)) {
- if (isReservedKeyword(identifier.toUpperCase(), null, null)) {
- return StringUtils.wrap(identifier, '"');
- }
+ if (identifier == null) {
+ return null;
+ }
+ if (StringUtils.isBlank(identifier)) {
+ return identifier;
+ }
+ if (isValidQuotedIdentifier(identifier)) {
+ return identifier;
+ }
+ if (isValidIdentifier(identifier) && !isReservedKeyword(identifier, null, null)) {
return identifier;
}
- return StringUtils.wrap(identifier, '"');
+ return quoteIdentifierAlways(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
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) {
+ return identifier == null ? null : StringUtils.replace(identifier, "\"", "\"\"");
+ }
+
+ /**
+ * Escapes identifier content for a position already surrounded by double
+ * 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
new file mode 100644
index 0000000000..d5a8833709
--- /dev/null
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-dm/src/test/java/ai/chat2db/plugin/dm/DMIdentifierProcessorTest.java
@@ -0,0 +1,367 @@
+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;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DMIdentifierProcessorTest {
+
+ @Test
+ 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 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 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 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 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("\"\"already\"\"", DMIdentifierProcessor.escapeIdentifier("\"already\""));
+ }
+
+ @Test
+ 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.startsWith("CREATE TABLE \"ORDERS\" ("), sql);
+ assertFalse(sql.contains("\"null\""), sql);
+ }
+
+ @Test
+ 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; --"));
+ }
+
+ @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 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()));
+ }
+
+ @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 createSchemaQuotesNameAndOwner() {
+ Schema schema = new Schema();
+ schema.setName("app");
+ schema.setOwner("owner; DROP USER x; --");
+
+ assertEquals("CREATE SCHEMA \"app\" AUTHORIZATION \"owner; DROP USER x; --\"",
+ new DMSqlBuilder().buildCreateSchema(schema));
+ }
+
+ @Test
+ void indexScriptEscapesNamesAndCanonicalizesDirection() {
+ TableIndex index = new TableIndex();
+ index.setType(DMIndexTypeEnum.NORMAL.getName());
+ index.setSchemaName("S\"; X");
+ index.setTableName("T");
+ index.setName("I\"X");
+ TableIndexColumn column = new TableIndexColumn();
+ column.setColumnName("C\"D");
+ column.setAscOrDesc(" desc ");
+ index.setColumnList(List.of(column));
+
+ 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 knownColumnTypeAcceptsOnlySupportedUnit() {
+ TableColumn column = column("c1", "VARCHAR");
+ column.setColumnSize(10);
+ column.setUnit("byte");
+ column.setDefaultValue("'O''Brien'");
+
+ String sql = DMColumnTypeEnum.VARCHAR.buildCreateColumnSql(column);
+
+ 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);
+
+ 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()));
+
+ assertThrows(IllegalArgumentException.class,
+ () -> DMIndexTypeEnum.NORMAL.buildIndexScript(index));
+ }
+
+ @Test
+ 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 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(name);
+ column.setColumnType(columnType);
+ return column;
+ }
+}