diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleDBManager.java index 73fddf7578..912f29b234 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleDBManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleDBManager.java @@ -1,5 +1,6 @@ package ai.chat2db.plugin.oracle; +import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor; import ai.chat2db.spi.IDbManager; import ai.chat2db.spi.DefaultDBManager; import ai.chat2db.community.domain.api.model.account.*; @@ -20,7 +21,6 @@ import ai.chat2db.spi.model.request.TriggerMetadataRequest; import ai.chat2db.spi.model.request.ViewMetadataRequest; import ai.chat2db.spi.DefaultSQLExecutor; -import ai.chat2db.spi.util.SqlUtils; import cn.hutool.core.date.DateUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -70,7 +70,7 @@ private void exportTables(Connection connection, String databaseName, String sch public void exportTable(Connection connection, String databaseName, String schemaName, String tableName, AsyncContext asyncContext) throws SQLException { String tableDDL = Chat2DBContext.getDbMetaData().tableDDL(connection, new TableMetadataRequest(databaseName, schemaName, tableName)); - String sqlBuilder = "DROP TABLE " + SqlUtils.quoteObjectName(tableName) + ";\n" + tableDDL + "\n"; + String sqlBuilder = "DROP TABLE " + qualifiedName(schemaName, tableName, false) + ";\n" + tableDDL + "\n"; asyncContext.write(sqlBuilder); if (asyncContext.isContainsData()) { exportTableData(connection, databaseName, schemaName, tableName, asyncContext); @@ -112,7 +112,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(SQL_SELECT_TRIGGER_NAME_ALL_TRIGGERS, schemaName); + String sql = String.format(SQL_SELECT_TRIGGER_NAME_ALL_TRIGGERS, OracleIdentifierProcessor.INSTANCE.escapeString(schemaName)); try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { while (resultSet.next()) { String triggerName = resultSet.getString("TRIGGER_NAME"); @@ -152,7 +152,9 @@ public void connectDatabase(Connection connection, String database) { } String schemaName = connectInfo.getSchemaName(); try { - DefaultSQLExecutor.getInstance().execute(connection, SQL_ALTER_SESSION_SET_CURRENT_SCHEMA + schemaName + "\""); + DefaultSQLExecutor.getInstance().execute(connection, + SQL_ALTER_SESSION_SET_CURRENT_SCHEMA + + OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName)); } catch (SQLException e) { log.error("connectDatabase error", e); } @@ -160,19 +162,25 @@ public void connectDatabase(Connection connection, String database) { @Override public void copyTable(Connection connection, String databaseName, String schemaName, String tableName, String newTableName, boolean copyData) throws SQLException { - String sql = ""; + String source = qualifiedName(schemaName, tableName, true); + String target = qualifiedName(schemaName, newTableName, true); + String sql; if (copyData) { - sql = "CREATE TABLE " + SqlUtils.quoteObjectName(newTableName) + " AS SELECT * FROM " + SqlUtils.quoteObjectName(tableName); + sql = "CREATE TABLE " + target + " AS SELECT * FROM " + source; } else { - sql = "CREATE TABLE " + SqlUtils.quoteObjectName(newTableName) + " AS SELECT * FROM " + SqlUtils.quoteObjectName(tableName) + " WHERE 1=0"; + 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) { - String sql = "DROP TABLE " + SqlUtils.quoteObjectName(tableName); - return sql; + return "DROP TABLE " + qualifiedName(schemaName, tableName, false); + } + + @Override + public String truncateTable(Connection connection, String databaseName, String schemaName, String tableName) { + return "TRUNCATE TABLE " + qualifiedName(schemaName, tableName, true); } @Override @@ -182,7 +190,23 @@ public void exportTableData(Connection connection, String databaseName, String s @Override public void dropView(Connection connection, String databaseName, String schemaName, String viewName) { - String sql = "DROP VIEW " + SqlUtils.quoteObjectName(schemaName) + "." + SqlUtils.quoteObjectName(viewName); + String sql = "DROP VIEW " + qualifiedName(schemaName, viewName, false); DefaultSQLExecutor.getInstance().execute(connection, sql, (resultSet) -> null); } + + private static String qualifiedName(String schemaName, String objectName, boolean normalizeQuotedObject) { + String normalizedObject = normalizeQuotedObject ? normalizeQuotedIdentifier(objectName) : objectName; + String quotedObject = OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(normalizedObject); + if (StringUtils.isBlank(schemaName)) { + return quotedObject; + } + return OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName) + "." + quotedObject; + } + + private static String normalizeQuotedIdentifier(String identifier) { + if (OracleIdentifierProcessor.INSTANCE.isQuoteIdentifier(identifier)) { + return OracleIdentifierProcessor.INSTANCE.removeIdentifierQuote(identifier); + } + return identifier; + } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleMetaData.java index b044489077..cf976939cc 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleMetaData.java @@ -7,7 +7,6 @@ import ai.chat2db.plugin.oracle.enums.type.OracleDefaultValueEnum; import ai.chat2db.plugin.oracle.enums.type.OracleIndexTypeEnum; import ai.chat2db.plugin.oracle.value.OracleValueProcessor; -import ai.chat2db.community.tools.util.EasyStringUtils; import ai.chat2db.community.tools.util.I18nUtils; import ai.chat2db.spi.IDbMetaData; import ai.chat2db.spi.ISQLIdentifierProcessor; @@ -45,11 +44,11 @@ public class OracleMetaData extends DefaultMetaService implements IDbMetaData { - public static final ISQLIdentifierProcessor ORACLE_SQL_IDENTIFIER_PROCESSOR = new OracleIdentifierProcessor(); + public static final ISQLIdentifierProcessor ORACLE_SQL_IDENTIFIER_PROCESSOR = OracleIdentifierProcessor.INSTANCE; @Override public List procedures(Connection connection, String databaseName, String schemaName) { - String sql = String.format(PROCEDURE_LIST_DDL, schemaName); + String sql = String.format(PROCEDURE_LIST_DDL, escapeSqlLiteral(schemaName)); ArrayList procedures = new ArrayList<>(); DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { while (resultSet.next()) { @@ -69,11 +68,11 @@ public List schemas(Connection connection, String databaseName) { @Override public String tableDDL(Connection connection, String databaseName, String schemaName, String tableName) { - String sql = String.format(TABLE_DDL_SQL, tableName, schemaName); - String tableCommentSql = String.format(TABLE_COMMENT_SQL, schemaName, tableName); - String tableColumnCommentSql = String.format(TABLE_COLUMN_COMMENT_SQL, schemaName, tableName); - String tableIndexSql = String.format(TABLE_INDEX_DDL_SQL, schemaName, tableName); - String PUIndexSql = String.format(PU_INDEX_NAME_SQL, schemaName, tableName); + String sql = String.format(TABLE_DDL_SQL, escapeSqlLiteral(tableName), escapeSqlLiteral(schemaName)); + String tableCommentSql = String.format(TABLE_COMMENT_SQL, escapeSqlLiteral(schemaName), escapeSqlLiteral(tableName)); + String tableColumnCommentSql = String.format(TABLE_COLUMN_COMMENT_SQL, escapeSqlLiteral(schemaName), escapeSqlLiteral(tableName)); + String tableIndexSql = String.format(TABLE_INDEX_DDL_SQL, escapeSqlLiteral(schemaName), escapeSqlLiteral(tableName)); + String PUIndexSql = String.format(PU_INDEX_NAME_SQL, escapeSqlLiteral(schemaName), escapeSqlLiteral(tableName)); StringBuilder ddlBuilder = new StringBuilder(); DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { try { @@ -88,8 +87,8 @@ public String tableDDL(Connection connection, String databaseName, String schema if (resultSet.next()) { String tableComment = resultSet.getString("comments"); if (StringUtils.isNotBlank(tableComment)) { - ddlBuilder.append("\nCOMMENT ON TABLE ").append(SqlUtils.quoteObjectName(tableName)).append(" IS ") - .append(EasyStringUtils.escapeAndQuoteString(tableComment)).append(";"); + ddlBuilder.append("\nCOMMENT ON TABLE ").append(qualifiedName(schemaName, tableName)).append(" IS ") + .append(quoteStringLiteral(tableComment)).append(";"); } } }); @@ -99,9 +98,9 @@ public String tableDDL(Connection connection, String databaseName, String schema String columnComment = resultSet.getString("comments"); if (StringUtils.isNotBlank(columnComment)) { ddlBuilder.append("\nCOMMENT ON COLUMN ") - .append(SqlUtils.quoteObjectName(tableName)).append(".") - .append(SqlUtils.quoteObjectName(columnName)).append(" IS ") - .append(EasyStringUtils.escapeAndQuoteString(columnComment)).append(";"); + .append(qualifiedName(schemaName, tableName)).append(".") + .append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(columnName)).append(" IS ") + .append(quoteStringLiteral(columnComment)).append(";"); } } }); @@ -159,7 +158,19 @@ static String buildTablesSql(String schemaName, String tableName) { } private static String escapeSqlLiteral(String value) { - return StringUtils.replace(value, "'", "''"); + return OracleIdentifierProcessor.INSTANCE.escapeString(value); + } + + private static String quoteStringLiteral(String value) { + return OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(value); + } + + private static String qualifiedName(String schemaName, String objectName) { + String quotedObject = OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(objectName); + if (StringUtils.isBlank(schemaName)) { + return quotedObject; + } + return OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName) + "." + quotedObject; } @@ -206,7 +217,7 @@ public List columns(Connection connection, String databaseName, Str private Map getTableColumns(Connection connection, String databaseName, String schemaName, String tableName) { Map tableColumns = new HashMap<>(); - String sql = String.format(SELECT_TAB_COLS, schemaName, tableName); + String sql = String.format(SELECT_TAB_COLS, escapeSqlLiteral(schemaName), escapeSqlLiteral(tableName)); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { while (resultSet.next()) { TableColumn tableColumn = new TableColumn(); @@ -261,7 +272,7 @@ private Map getTableColumns(Connection connection, String d @Override public Function function(Connection connection, @NotEmpty String databaseName, String schemaName, String functionName) { - String sql = String.format(ROUTINES_SQL, "FUNCTION", schemaName, functionName); + String sql = String.format(ROUTINES_SQL, "FUNCTION", escapeSqlLiteral(schemaName), escapeSqlLiteral(functionName)); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { Function function = new Function(); function.setDatabaseName(databaseName); @@ -291,7 +302,7 @@ public Function function(Connection connection, @NotEmpty String databaseName, S @Override public List indexes(Connection connection, String databaseName, String schemaName, String tableName) { - String pkSql = String.format(SELECT_PK_SQL, schemaName, tableName); + String pkSql = String.format(SELECT_PK_SQL, escapeSqlLiteral(schemaName), escapeSqlLiteral(tableName)); Set pkSet = new HashSet<>(); DefaultSQLExecutor.getInstance().execute(connection, pkSql, resultSet -> { while (resultSet.next()) { @@ -301,7 +312,7 @@ public List indexes(Connection connection, String databaseName, Stri } ); - String sql = String.format(SELECT_TABLE_INDEX, schemaName, tableName); + String sql = String.format(SELECT_TABLE_INDEX, escapeSqlLiteral(schemaName), escapeSqlLiteral(tableName)); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { LinkedHashMap map = new LinkedHashMap(); while (resultSet.next()) { @@ -360,7 +371,7 @@ private TableIndexColumn getTableIndexColumn(ResultSet resultSet) throws SQLExce @Override public List triggers(Connection connection, String databaseName, String schemaName) { List triggers = new ArrayList<>(); - return DefaultSQLExecutor.getInstance().execute(connection, String.format(TRIGGER_SQL_LIST, schemaName), + return DefaultSQLExecutor.getInstance().execute(connection, String.format(TRIGGER_SQL_LIST, escapeSqlLiteral(schemaName)), resultSet -> { while (resultSet.next()) { String triggerName = resultSet.getString("TRIGGER_NAME"); @@ -378,7 +389,7 @@ public List triggers(Connection connection, String databaseName, String @Override public Trigger trigger(Connection connection, @NotEmpty String databaseName, String schemaName, String triggerName) { - String sql = String.format(TRIGGER_DDL_SQL, triggerName, schemaName); + String sql = String.format(TRIGGER_DDL_SQL, escapeSqlLiteral(triggerName), escapeSqlLiteral(schemaName)); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { Trigger trigger = new Trigger(); trigger.setDatabaseName(databaseName); @@ -394,7 +405,7 @@ public Trigger trigger(Connection connection, @NotEmpty String databaseName, Str @Override public Procedure procedure(Connection connection, @NotEmpty String databaseName, String schemaName, String procedureName) { - String sql = String.format(ROUTINES_SQL, "PROCEDURE", schemaName, procedureName); + String sql = String.format(ROUTINES_SQL, "PROCEDURE", escapeSqlLiteral(schemaName), escapeSqlLiteral(procedureName)); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { Procedure procedure = new Procedure(); procedure.setDatabaseName(databaseName); @@ -428,7 +439,7 @@ static void appendRoutineSourceText(StringBuilder bodyBuilder, String sourceText @Override public Table view(Connection connection, String databaseName, String schemaName, String viewName) { - String sql = String.format(VIEW_DDL_SQL, schemaName, viewName); + String sql = String.format(VIEW_DDL_SQL, escapeSqlLiteral(schemaName), escapeSqlLiteral(viewName)); return DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { Table table = new Table(); table.setDatabaseName(databaseName); @@ -459,7 +470,13 @@ public TableMeta getTableMeta(String databaseName, String schemaName, String tab @Override public String getMetaDataName(String... names) { - return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).map(name -> "\"" + name + "\"").collect(Collectors.joining(".")); + List identifiers = Arrays.stream(names) + .filter(StringUtils::isNotBlank) + .toList(); + int first = Math.max(0, identifiers.size() - 2); + return identifiers.subList(first, identifiers.size()).stream() + .map(OracleIdentifierProcessor.INSTANCE::quoteIdentifierAlways) + .collect(Collectors.joining(".")); } @@ -525,9 +542,9 @@ public ModifyViewConfiguration viewMeta(String databaseName, String schemaName) StringBuilder sqlBuilder = new StringBuilder(100); sqlBuilder.append(SQL_CREATE).append("view "); if (StringUtils.isNotBlank(schemaName)) { - sqlBuilder.append("\"").append(schemaName).append("\"").append("."); + sqlBuilder.append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName)).append("."); } - sqlBuilder.append("\"").append("undefined").append("\""); + sqlBuilder.append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways("undefined")); sqlBuilder.append(" AS \n").append(sql).append(";"); configuration.setPreviewSql(sqlBuilder.toString()); configuration.setSql(sql); diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleSqlGuards.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleSqlGuards.java new file mode 100644 index 0000000000..ff418a898b --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/OracleSqlGuards.java @@ -0,0 +1,249 @@ +package ai.chat2db.plugin.oracle; + +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 Oracle SQL fragments that cannot be escaped + * because they are emitted as syntax rather than as identifiers or literals. + */ +public final class OracleSqlGuards { + + private static final Set COLUMN_CLAUSE_KEYWORDS = Set.of( + "COLLATE", "CONSTRAINT", "CHECK", "DEFAULT", "DISABLE", "ENABLE", "GENERATED", + "IDENTITY", "INVISIBLE", "PRIMARY", "REFERENCES", "UNIQUE", "VISIBLE"); + + private OracleSqlGuards() { + } + + /** + * Validates one Oracle DEFAULT expression without re-encoding literals + * returned by the data dictionary. Functions, casts, sequence expressions, + * datetime/interval literals, quoted identifiers, and Oracle q-quotes are + * accepted when their delimiters are balanced. + */ + public static String requireDefaultValue(String value) { + if (StringUtils.isBlank(value)) { + throw invalid("DEFAULT expression", value); + } + scanExpression(value.trim(), false, "DEFAULT expression"); + return value; + } + + /** + * Validates one complete Oracle column type expression, including + * parameterized built-in types 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; + } + + /** + * Backward-compatible name retained for the existing Oracle call sites. + */ + public static String requireSafeTypeName(String typeName) { + return requireColumnTypeExpression(typeName); + } + + public static String requireUnit(String unit) { + String trimmed = StringUtils.trimToEmpty(unit); + if (!"CHAR".equalsIgnoreCase(trimmed) && !"BYTE".equalsIgnoreCase(trimmed)) { + throw new IllegalArgumentException("Unsupported Oracle 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 Oracle index sort direction: " + value); + } + + /** + * Returns the raw hex digits represented by {@code value}. Non-hex input is + * replaced with the caller-provided base16 encoding, which is verified too. + */ + public static String normalizeHexLiteral(String value, String fallbackHex) { + if (value == null) { + return null; + } + String candidate = value.startsWith("0x") ? value.substring(2) : value; + if (isHex(candidate)) { + return candidate; + } + if (fallbackHex != null && isHex(fallbackHex)) { + return fallbackHex; + } + throw new IllegalArgumentException("Invalid Oracle RAW/BLOB hex value"); + } + + public static String requireKeyword(String description, String value, String... allowedValues) { + for (String allowedValue : allowedValues) { + if (allowedValue.equalsIgnoreCase(StringUtils.trimToEmpty(value))) { + return allowedValue; + } + } + throw new IllegalArgumentException("Unsupported Oracle " + description + ": " + 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.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); + } + i = scanQuoted(expression, i, c, description); + continue; + } + if (c == ';' || Character.isISOControl(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 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 isHex(String value) { + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if ((c < '0' || c > '9') && (c < 'A' || c > 'F') && (c < 'a' || c > 'f')) { + return false; + } + } + return true; + } + + 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 Oracle " + description + ": " + value); + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/builder/OracleSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/builder/OracleSqlBuilder.java index 7002777246..9fab73aa3c 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/builder/OracleSqlBuilder.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/builder/OracleSqlBuilder.java @@ -2,27 +2,50 @@ import ai.chat2db.spi.constant.SQLConstants; +import ai.chat2db.plugin.oracle.OracleSqlGuards; +import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor; import ai.chat2db.plugin.oracle.enums.type.OracleColumnTypeEnum; import ai.chat2db.plugin.oracle.enums.type.OracleIndexTypeEnum; -import ai.chat2db.plugin.oracle.util.OracleUtil; 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.view.ModifyView; 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.config.TableBuilderConfig; -import ai.chat2db.spi.util.SqlUtils; 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.stream.Collectors; import static ai.chat2db.plugin.oracle.constant.OracleSqlBuilderConstants.*; +import static ai.chat2db.spi.constant.DefaultSqlBuilderConstants.SQL_AND; +import static ai.chat2db.spi.constant.DefaultSqlBuilderConstants.SQL_SET_2; +import static ai.chat2db.spi.constant.DefaultSqlBuilderConstants.SQL_UPDATE; +import static ai.chat2db.spi.constant.DefaultSqlBuilderConstants.SQL_WHERE_2; public class OracleSqlBuilder extends DefaultSqlBuilder { + @Override + public String quoteIdentifier(String identifier) { + return OracleIdentifierProcessor.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(OracleIdentifierProcessor.INSTANCE::quoteIdentifierAlways) + .collect(Collectors.joining(SQLConstants.DOT)); + } @@ -49,7 +72,10 @@ protected String appendSingleRowLimit(String operationType, String tableName, St return sql; } String body = sql.substring(0, sql.length() - whereClause.length()); - return body + SQL_WHERE_ROWID_IN_OPEN_PAREN_SELECT_ROWID_FROM + tableName + whereClause + VALUE_AND_ROWNUM_EQUAL_1_CLOSE_PAREN; + String quotedTableName = OracleIdentifierProcessor.INSTANCE.isQuoteIdentifier(tableName) + ? tableName : quoteIdentifier(tableName); + return body + SQL_WHERE_ROWID_IN_OPEN_PAREN_SELECT_ROWID_FROM + + quotedTableName + whereClause + VALUE_AND_ROWNUM_EQUAL_1_CLOSE_PAREN; } @Override @@ -57,9 +83,7 @@ public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfi StringBuilder script = new StringBuilder(); script.append(SQL_CREATE_TABLE) - .append(OracleUtil.quoteIdentifier(table.getSchemaName())) - .append(SQLConstants.DOT) - .append(OracleUtil.quoteIdentifierIgnoreCase(table.getName())) + .append(quoteQualifiedIdentifier(table.getDatabaseName(), table.getSchemaName(), table.getName())) .append(SQLConstants.OPEN_PARENTHESIS).append(SQLConstants.LINE_SEPARATOR); for (TableColumn column : table.getColumnList()) { @@ -67,9 +91,7 @@ public String buildCreateTable(Table table, TableBuilderConfig tableBuilderConfi continue; } OracleColumnTypeEnum typeEnum = OracleColumnTypeEnum.getByType(column.getColumnType()); - if (typeEnum == null) { - continue; - } + typeEnum = typeEnum == null ? OracleColumnTypeEnum.VARCHAR2 : typeEnum; script.append(SQLConstants.TAB).append(typeEnum.buildCreateColumnSql(column)).append(SQLConstants.COMMA_LINE_SEPARATOR); } @@ -107,9 +129,7 @@ public String buildAITableSchema(Table table) { StringBuilder script = new StringBuilder(); script.append(SQL_CREATE_TABLE) - .append(OracleUtil.quoteIdentifier(table.getSchemaName())) - .append(SQLConstants.DOT) - .append(OracleUtil.quoteIdentifierIgnoreCase(table.getName())) + .append(quoteQualifiedIdentifier(table.getDatabaseName(), table.getSchemaName(), table.getName())) .append(SQLConstants.OPEN_PARENTHESIS).append(SQLConstants.LINE_SEPARATOR); for (TableColumn column : table.getColumnList()) { @@ -117,9 +137,7 @@ public String buildAITableSchema(Table table) { continue; } OracleColumnTypeEnum typeEnum = OracleColumnTypeEnum.getByType(column.getColumnType()); - if (typeEnum == null) { - continue; - } + typeEnum = typeEnum == null ? OracleColumnTypeEnum.VARCHAR2 : typeEnum; script.append(SQLConstants.TAB).append(typeEnum.buildAICreateColumnSql(column)).append(SQLConstants.COMMA_LINE_SEPARATOR); } @@ -151,24 +169,27 @@ public String buildAITableSchema(Table table) { private String buildTableComment(Table table) { - StringBuilder script = new StringBuilder(); - script.append(SQL_COMMENT_TABLE_2).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_2 + + 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); @@ -177,9 +198,7 @@ public String buildAlterTable(Table oldTable, Table newTable) { String editStatus = tableColumn.getEditStatus(); if (StringUtils.isNotBlank(editStatus)) { OracleColumnTypeEnum typeEnum = OracleColumnTypeEnum.getByType(tableColumn.getColumnType()); - if (typeEnum == null) { - continue; - } + typeEnum = typeEnum == null ? OracleColumnTypeEnum.VARCHAR2 : typeEnum; script.append(SQLConstants.TAB).append(typeEnum.buildModifyColumn(tableColumn)).append(SQLConstants.SEMICOLON_LINE_SEPARATOR); if (StringUtils.isNotBlank(tableColumn.getComment()) &&!EditStatusEnum.DELETE.name().equals(editStatus)) { @@ -231,10 +250,7 @@ public String buildPageLimit(PageLimitRequest request) { @Override protected void buildTableName(String databaseName, String schemaName, String tableName, StringBuilder script) { - if (StringUtils.isNotBlank(databaseName)) { - script.append(SqlUtils.quoteObjectName(databaseName)).append('.'); - } - script.append(SqlUtils.quoteObjectName(tableName)); + script.append(quoteQualifiedIdentifier(databaseName, schemaName, tableName)); } @@ -242,11 +258,30 @@ protected void buildTableName(String databaseName, String schemaName, String tab protected void buildColumns(List columnList, StringBuilder script) { if (CollectionUtils.isNotEmpty(columnList)) { script.append(SQLConstants.SPACE_OPEN_PARENTHESIS) - .append(columnList.stream().map(SqlUtils::quoteObjectName).collect(Collectors.joining(SQLConstants.COMMA))) + .append(columnList.stream() + .map(OracleIdentifierProcessor.INSTANCE::quoteIdentifierAlways) + .collect(Collectors.joining(SQLConstants.COMMA))) .append(SQLConstants.CLOSE_PARENTHESIS_SPACE); } } + @Override + public String buildUpdate(UpdateSqlRequest request) { + StringBuilder script = new StringBuilder(SQL_UPDATE); + buildTableName(request.getDatabaseName(), request.getSchemaName(), request.getTableName(), script); + script.append(SQL_SET_2); + 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(SQL_WHERE_2); + script.append(request.getPrimaryKeyMap().entrySet().stream() + .map(entry -> quoteIdentifier(entry.getKey()) + SQLConstants.EQUAL_SQL + entry.getValue()) + .collect(Collectors.joining(SQL_AND))); + } + return script.toString(); + } + @Override public String buildCreateView(ModifyView modifyView) { StringBuilder createViewSqlBuilder = new StringBuilder(100); @@ -259,7 +294,8 @@ public String buildCreateView(ModifyView modifyView) { } String editClause = modifyView.getEditClause(); if (StringUtils.isNotBlank(editClause)) { - createViewSqlBuilder.append(editClause); + createViewSqlBuilder.append(OracleSqlGuards.requireKeyword("view edition clause", editClause, + "EDITIONING", "EDITIONABLE", "EDITIONABLE EDITIONING", "NONEDITIONABLE")); } createViewSqlBuilder.append(SQLConstants.VIEW_KEYWORD); String schemaName = modifyView.getSchemaName(); @@ -278,15 +314,20 @@ public String buildCreateView(ModifyView modifyView) { } String shareClause = modifyView.getShareClause(); if (StringUtils.isNotBlank(shareClause)) { - createViewSqlBuilder.append(SQL_SHARING_EQUAL).append(shareClause).append(SQLConstants.SPACE); + createViewSqlBuilder.append(SQL_SHARING_EQUAL) + .append(OracleSqlGuards.requireKeyword("view sharing clause", shareClause, + "METADATA", "EXTENDED DATA", "DATA", "NONE")) + .append(SQLConstants.SPACE); } String collationClause = modifyView.getCollationClause(); if (StringUtils.isNotBlank(collationClause)) { - createViewSqlBuilder.append(SQL_DEFAULT_COLLATE).append(collationClause).append(SQLConstants.SPACE); + createViewSqlBuilder.append(SQL_DEFAULT_COLLATE) + .append(quoteOracleIdentifier(collationClause)).append(SQLConstants.SPACE); } String security = modifyView.getSecurity(); if (StringUtils.isNotBlank(security)) { - createViewSqlBuilder.append(security).append(SQLConstants.SPACE); + createViewSqlBuilder.append(OracleSqlGuards.requireKeyword("view security clause", security, + "CURRENT_USER", "DEFINER")).append(SQLConstants.SPACE); } createViewSqlBuilder.append(SQLConstants.LINE_SEPARATOR_SQL_AS); String viewBody = modifyView.getViewBody(); @@ -299,16 +340,22 @@ public String buildCreateView(ModifyView modifyView) { } String subqueryRestrictionClause = modifyView.getSubqueryRestrictionClause(); if (StringUtils.isNotBlank(subqueryRestrictionClause)) { - createViewSqlBuilder.append(subqueryRestrictionClause).append(SQLConstants.SPACE); + createViewSqlBuilder.append(OracleSqlGuards.requireKeyword("view restriction clause", + subqueryRestrictionClause, "READ ONLY", "CHECK OPTION", SQL_CHECK_OPTION_CONSTRAINT)) + .append(SQLConstants.SPACE); } String subqueryConstraintName = modifyView.getSubqueryConstraintName(); - if (StringUtils.equalsAnyIgnoreCase(SQL_CHECK_OPTION_CONSTRAINT, subqueryRestrictionClause) + if (StringUtils.equalsAnyIgnoreCase(subqueryRestrictionClause, "CHECK OPTION", SQL_CHECK_OPTION_CONSTRAINT) && StringUtils.isNotBlank(subqueryConstraintName)) { - createViewSqlBuilder.append(subqueryConstraintName).append(SQLConstants.SPACE); + if (!StringUtils.equalsIgnoreCase(SQL_CHECK_OPTION_CONSTRAINT, subqueryRestrictionClause)) { + createViewSqlBuilder.append("CONSTRAINT "); + } + createViewSqlBuilder.append(quoteOracleIdentifier(subqueryConstraintName)).append(SQLConstants.SPACE); } String containerClause = modifyView.getContainerClause(); if (StringUtils.isNotBlank(containerClause)) { - createViewSqlBuilder.append(containerClause); + createViewSqlBuilder.append(OracleSqlGuards.requireKeyword("view container clause", containerClause, + "CONTAINER MAP", "CONTAINERS DEFAULT")); } createViewSqlBuilder.append(SQLConstants.SEMICOLON); @@ -328,15 +375,11 @@ public String buildCreateView(ModifyView modifyView) { } private static String quoteOracleIdentifier(String identifier) { - return SQLConstants.DOUBLE_QUOTE - + identifier.replace(SQLConstants.DOUBLE_QUOTE, SQLConstants.DOUBLE_QUOTE + SQLConstants.DOUBLE_QUOTE) - + SQLConstants.DOUBLE_QUOTE; + return OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(identifier); } private static String quoteStringLiteral(String value) { - return SQLConstants.SINGLE_QUOTE - + value.replace(SQLConstants.SINGLE_QUOTE, SQLConstants.SINGLE_QUOTE + SQLConstants.SINGLE_QUOTE) - + SQLConstants.SINGLE_QUOTE; + return OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(value); } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/constant/OracleDBManagerConstants.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/constant/OracleDBManagerConstants.java index 371b852f4f..9a46b406ce 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/constant/OracleDBManagerConstants.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/constant/OracleDBManagerConstants.java @@ -38,7 +38,7 @@ public final class OracleDBManagerConstants { - public static final String SQL_ALTER_SESSION_SET_CURRENT_SCHEMA = "ALTER SESSION SET CURRENT_SCHEMA = \""; + public static final String SQL_ALTER_SESSION_SET_CURRENT_SCHEMA = "ALTER SESSION SET CURRENT_SCHEMA = "; public static final String SQL_SELECT_TRIGGER_NAME_ALL_TRIGGERS = "SELECT TRIGGER_NAME FROM all_triggers where OWNER='%s'"; private OracleDBManagerConstants() { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/enums/type/OracleColumnTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/enums/type/OracleColumnTypeEnum.java index 344284cad6..76f238f0ef 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/enums/type/OracleColumnTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/enums/type/OracleColumnTypeEnum.java @@ -1,17 +1,19 @@ package ai.chat2db.plugin.oracle.enums.type; -import ai.chat2db.plugin.oracle.util.OracleUtil; +import ai.chat2db.plugin.oracle.OracleSqlGuards; +import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor; 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 java.util.Objects; import static ai.chat2db.plugin.oracle.constant.OracleColumnTypeEnumConstants.*; public enum OracleColumnTypeEnum implements IColumnBuilder { @@ -116,7 +118,12 @@ public enum OracleColumnTypeEnum implements IColumnBuilder { private ColumnType columnType; public static OracleColumnTypeEnum getByType(String dataType) { - return COLUMN_TYPE_MAP.get(SqlUtils.removeDigits(dataType.toUpperCase())); + if (StringUtils.isBlank(dataType)) { + return null; + } + String normalized = dataType.trim().toUpperCase(Locale.ROOT) + .replaceAll("\\s+", " "); + return COLUMN_TYPE_MAP.get(normalized); } private static Map COLUMN_TYPE_MAP = Maps.newHashMap(); @@ -138,13 +145,13 @@ public ColumnType getColumnType() { @Override public String buildCreateColumnSql(TableColumn column) { - OracleColumnTypeEnum type = COLUMN_TYPE_MAP.get(column.getColumnType().toUpperCase()); + OracleColumnTypeEnum type = getByType(column.getColumnType()); if (type == null) { - return OracleUtil.quoteIdentifierIgnoreCase(column.getName()) + " " + column.getColumnType(); + return buildUnknownColumnSql(column); } StringBuilder script = new StringBuilder(); - script.append(OracleUtil.quoteIdentifierIgnoreCase(column.getName())).append(" "); + script.append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); @@ -157,13 +164,13 @@ public String buildCreateColumnSql(TableColumn column) { @Override public String buildAICreateColumnSql(TableColumn column) { - OracleColumnTypeEnum type = COLUMN_TYPE_MAP.get(column.getColumnType().toUpperCase()); + OracleColumnTypeEnum type = getByType(column.getColumnType()); if (type == null) { - return OracleUtil.quoteIdentifierIgnoreCase(column.getName()) + " " + column.getColumnType(); + return buildUnknownColumnSql(column); } StringBuilder script = new StringBuilder(); - script.append(OracleUtil.quoteIdentifierIgnoreCase(column.getName())).append(" "); + script.append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); @@ -199,7 +206,7 @@ private String buildDefaultValue(TableColumn column, OracleColumnTypeEnum type) return StringUtils.join("DEFAULT NULL"); } - return StringUtils.join("DEFAULT ", column.getDefaultValue()); + return StringUtils.join("DEFAULT ", OracleSqlGuards.requireDefaultValue(column.getDefaultValue())); } private String buildDataType(TableColumn column, OracleColumnTypeEnum type) { @@ -213,7 +220,7 @@ private String buildDataType(TableColumn column, OracleColumnTypeEnum 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(OracleSqlGuards.requireUnit(column.getUnit())).append(")"); } return script.toString(); } @@ -265,25 +272,28 @@ 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(OracleIdentifierProcessor.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(); - 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(buildModifyColumnSql(tableColumn, tableColumn.getOldColumn())).append(") \n"); - if (!StringUtils.equalsIgnoreCase(tableColumn.getOldName(), tableColumn.getName())) { + if (!StringUtils.equals(tableColumn.getOldName(), tableColumn.getName())) { script.append(";"); - 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(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getOldName())) + .append(" TO ") + .append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableColumn.getName())); } return script.toString(); @@ -293,25 +303,54 @@ public String buildModifyColumn(TableColumn tableColumn) { } public String buildModifyColumnSql(TableColumn column, TableColumn oldColumn) { - OracleColumnTypeEnum type = COLUMN_TYPE_MAP.get(column.getColumnType().toUpperCase()); + OracleColumnTypeEnum type = getByType(column.getColumnType()); if (type == null) { - return ""; + return buildUnknownColumnSql(column); } StringBuilder script = new StringBuilder(); - script.append("\"").append(column.getName()).append("\"").append(" "); + script.append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())).append(" "); script.append(buildDataType(column, type)).append(" "); script.append(buildDefaultValue(column, type)).append(" "); - if (oldColumn.getNullable() != column.getNullable()) { + if (oldColumn != null && !Objects.equals(oldColumn.getNullable(), column.getNullable())) { script.append(buildNullable(column, type)).append(" "); } return script.toString(); } + private static String buildUnknownColumnSql(TableColumn column) { + StringBuilder script = new StringBuilder(); + script.append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getName())) + .append(" ") + .append(OracleSqlGuards.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(OracleSqlGuards.requireDefaultValue(defaultValue)); + } + } + if (column.getNullable() != null) { + script.append(column.getNullable() == 1 ? " NULL" : " NOT NULL"); + } + return script.toString(); + } + + private static String qualifiedTableName(TableColumn column) { + String tableName = OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getTableName()); + if (StringUtils.isBlank(column.getSchemaName())) { + return tableName; + } + return OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getSchemaName()) + "." + tableName; + } + public static List getTypes() { return Arrays.stream(OracleColumnTypeEnum.values()).map(columnTypeEnum -> columnTypeEnum.getColumnType() diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/enums/type/OracleIndexTypeEnum.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/enums/type/OracleIndexTypeEnum.java index 69b8bac31e..f91ad77521 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/enums/type/OracleIndexTypeEnum.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/enums/type/OracleIndexTypeEnum.java @@ -1,5 +1,7 @@ package ai.chat2db.plugin.oracle.enums.type; +import ai.chat2db.plugin.oracle.OracleSqlGuards; +import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor; 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; @@ -71,14 +73,19 @@ public static OracleIndexTypeEnum 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 CONSTRAINT ").append(tableIndex.getTableName() + "_pk").append(" PRIMARY KEY ").append(buildIndexColumn(tableIndex)); + script.append(SQL_ALTER_TABLE).append(qualifiedName(tableIndex.getSchemaName(), tableIndex.getTableName())) + .append(" ADD CONSTRAINT ") + .append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableIndex.getTableName() + "_pk")) + .append(" 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(); } @@ -89,9 +96,9 @@ private String buildIndexColumn(TableIndex tableIndex) { script.append("("); for (TableIndexColumn column : tableIndex.getColumnList()) { if (StringUtils.isNotBlank(column.getColumnName())) { - script.append("\"").append(column.getColumnName()).append("\""); + script.append(OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(column.getColumnName())); if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) { - script.append(" ").append(column.getAscOrDesc()); + script.append(" ").append(OracleSqlGuards.requireAscOrDesc(column.getAscOrDesc())); } script.append(","); } @@ -102,7 +109,7 @@ private String buildIndexColumn(TableIndex tableIndex) { } private String buildIndexName(TableIndex tableIndex) { - return "\"" + tableIndex.getSchemaName() + "\"." + "\"" + tableIndex.getName() + "\""; + return qualifiedName(tableIndex.getSchemaName(), tableIndex.getName()); } public String buildModifyIndex(TableIndex tableIndex) { @@ -120,7 +127,7 @@ public String buildModifyIndex(TableIndex tableIndex) { private String buildDropIndex(TableIndex tableIndex) { if (OracleIndexTypeEnum.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(); @@ -130,6 +137,14 @@ private String buildDropIndex(TableIndex tableIndex) { return script.toString(); } + private static String qualifiedName(String schemaName, String objectName) { + String quotedObject = OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(objectName); + if (StringUtils.isBlank(schemaName)) { + return quotedObject; + } + return OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(schemaName) + "." + quotedObject; + } + public static List getIndexTypes() { return Arrays.asList(OracleIndexTypeEnum.values()).stream().map(OracleIndexTypeEnum::getIndexType).collect(java.util.stream.Collectors.toList()); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/identifier/OracleIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/identifier/OracleIdentifierProcessor.java index 7fd6cc8570..324fe61231 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/identifier/OracleIdentifierProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/identifier/OracleIdentifierProcessor.java @@ -4,11 +4,18 @@ import org.apache.commons.lang3.StringUtils; import java.util.HashSet; +import java.util.Locale; import java.util.Set; - +/** + * Oracle dialect identifier processor: double-quoted identifiers with embedded-quote + * doubling, and single-quote doubling for string literals. Shared stateless + * instance available via {@link #INSTANCE} for call sites without MetaData access. + */ public class OracleIdentifierProcessor extends DefaultSQLIdentifierProcessor { + public static final OracleIdentifierProcessor INSTANCE = new OracleIdentifierProcessor(); + private static final Set ORACLE_RESERVED_KEYWORDS = new HashSet<>(); static { @@ -126,41 +133,48 @@ public class OracleIdentifierProcessor extends DefaultSQLIdentifierProcessor { @Override public boolean isReservedKeyword(String identifier, Integer majorVersion, Integer minorVersion) { - return ORACLE_RESERVED_KEYWORDS.contains(identifier); + return identifier != null && ORACLE_RESERVED_KEYWORDS.contains(identifier.toUpperCase(Locale.ROOT)); } @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); } @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); } @Override @@ -168,8 +182,66 @@ public String convertIdentifierCase(String identifier) { if (StringUtils.isBlank(identifier)) { return identifier; }else { - return identifier.toUpperCase(); + return identifier.toUpperCase(Locale.ROOT); + } + } + + /** + * Escapes a value interpolated into a single-quoted SQL string literal by + * doubling every single quote (surrounding quotes NOT added). + */ + @Override + public String escapeString(String str) { + return str == null ? null : StringUtils.replace(str, "'", "''"); + } + + /** + * Escapes and surrounds one Oracle string literal. Oracle does not treat a + * backslash as an escape character, so only single quotes are doubled. + */ + public String quoteStringLiteral(String str) { + return str == null ? null : "'" + escapeString(str) + "'"; + } + + private static String escapeIdentifierContent(String identifier) { + return identifier == null ? null : StringUtils.replace(identifier, "\"", "\"\""); + } + + /** + * Escapes identifier content for a position already surrounded by double + * quotes by doubling every embedded double quote. + */ + public static String escapeIdentifier(String identifier) { + return escapeIdentifierContent(identifier); + } + + /** + * Escapes an identifier and wraps it in double quotes unconditionally. Unlike the + * SPI {@link #quoteIdentifier(String)} (which only quotes when the dialect requires + * it), generated DDL quotes every identifier; this keeps that position byte-identical. + */ + @Override + public String quoteIdentifierAlways(String identifier) { + if (identifier == null) { + return null; + } + return "\"" + escapeIdentifierContent(identifier) + "\""; + } + + 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-oracle/src/main/java/ai/chat2db/plugin/oracle/value/OracleValueProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/OracleValueProcessor.java index 0e014e1e4d..847cc58d81 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/OracleValueProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/OracleValueProcessor.java @@ -1,8 +1,8 @@ package ai.chat2db.plugin.oracle.value; import ai.chat2db.plugin.oracle.enums.type.OracleColumnTypeEnum; +import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor; import ai.chat2db.plugin.oracle.value.factory.OracleValueProcessorFactory; -import ai.chat2db.community.tools.util.EasyStringUtils; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; import ai.chat2db.community.domain.api.model.value.SQLDataValue; @@ -48,7 +48,7 @@ public String getJdbcSqlValueString(JDBCDataValue dataValue) { } if (value instanceof String stringValue) { if (StringUtils.isBlank(stringValue)) { - return EasyStringUtils.quoteString(stringValue); + return OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(stringValue); } } return convertJDBCValueStrByType(dataValue); @@ -63,9 +63,9 @@ public String convertSQLValueByType(SQLDataValue dataValue) { } } catch (Exception e) { log.warn("convertSQLValueByType error", e); - return super.convertSQLValueByType(dataValue); + return OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(dataValue.getValue()); } - return super.convertSQLValueByType(dataValue); + return OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(dataValue.getValue()); } @@ -95,9 +95,14 @@ public String convertJDBCValueStrByType(JDBCDataValue dataValue) { } } catch (Exception e) { log.warn("convertJDBCValueStrByType error", e); - return super.convertJDBCValueStrByType(dataValue); + return quoteJdbcString(dataValue); } - return super.convertJDBCValueStrByType(dataValue); + return quoteJdbcString(dataValue); + } + + private static String quoteJdbcString(JDBCDataValue dataValue) { + String value = dataValue.getString(); + return value == null ? "NULL" : OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(value); } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleBlobProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleBlobProcessor.java index 9f3a02a993..c4c7bc9c4a 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleBlobProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleBlobProcessor.java @@ -1,6 +1,7 @@ package ai.chat2db.plugin.oracle.value.sub; import ai.chat2db.community.tools.util.EasyStringUtils; +import ai.chat2db.plugin.oracle.OracleSqlGuards; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; import ai.chat2db.community.domain.api.model.value.SQLDataValue; @@ -12,21 +13,8 @@ public class OracleBlobProcessor extends DefaultValueProcessor { @Override public String convertSQLValueByType(SQLDataValue dataValue) { - String value = dataValue.getValue(); - if (value.startsWith("0x")) { - return EasyStringUtils.quoteString(value.substring(2)); - } else { - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - boolean isDigit = (c >= '0' && c <= '9'); - boolean isUpperCaseHex = (c >= 'A' && c <= 'F'); - boolean isLowerCaseHex = (c >= 'a' && c <= 'f'); - if (!isDigit && !isUpperCaseHex && !isLowerCaseHex) { - return EasyStringUtils.quoteString(dataValue.getBlobHexString()); - } - } - return EasyStringUtils.quoteString(value); - } + return EasyStringUtils.quoteString(OracleSqlGuards.normalizeHexLiteral( + dataValue.getValue(), dataValue.getBlobHexString())); } @Override diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleClobProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleClobProcessor.java index 83901eeef0..ca0cc13089 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleClobProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleClobProcessor.java @@ -1,6 +1,6 @@ package ai.chat2db.plugin.oracle.value.sub; -import ai.chat2db.community.tools.util.EasyStringUtils; +import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; import ai.chat2db.community.domain.api.model.value.SQLDataValue; @@ -10,7 +10,7 @@ public class OracleClobProcessor extends DefaultValueProcessor { @Override public String convertSQLValueByType(SQLDataValue dataValue) { - return EasyStringUtils.escapeAndQuoteString(dataValue.getValue()); + return OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(dataValue.getValue()); } @@ -22,6 +22,6 @@ public String convertJDBCValueByType(JDBCDataValue dataValue) { @Override public String convertJDBCValueStrByType(JDBCDataValue dataValue) { - return EasyStringUtils.escapeAndQuoteString(dataValue.getClobString()); + return OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(dataValue.getClobString()); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleLongProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleLongProcessor.java index 0a0e20b7b0..8e0b28925b 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleLongProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleLongProcessor.java @@ -1,6 +1,6 @@ package ai.chat2db.plugin.oracle.value.sub; -import ai.chat2db.community.tools.util.EasyStringUtils; +import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; import ai.chat2db.community.domain.api.model.value.SQLDataValue; @@ -10,7 +10,7 @@ public class OracleLongProcessor extends DefaultValueProcessor { @Override public String convertSQLValueByType(SQLDataValue dataValue) { - return EasyStringUtils.escapeAndQuoteString(dataValue.getValue()); + return OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(dataValue.getValue()); } @@ -22,6 +22,6 @@ public String convertJDBCValueByType(JDBCDataValue dataValue) { @Override public String convertJDBCValueStrByType(JDBCDataValue dataValue) { - return EasyStringUtils.escapeAndQuoteString(dataValue.getCharsetString()); + return OracleIdentifierProcessor.INSTANCE.quoteStringLiteral(dataValue.getCharsetString()); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleLongRawProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleLongRawProcessor.java index da85b40e7a..24c4b7411f 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleLongRawProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleLongRawProcessor.java @@ -1,6 +1,7 @@ package ai.chat2db.plugin.oracle.value.sub; import ai.chat2db.community.tools.util.EasyStringUtils; +import ai.chat2db.plugin.oracle.OracleSqlGuards; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; import ai.chat2db.community.domain.api.model.value.SQLDataValue; @@ -12,21 +13,8 @@ public class OracleLongRawProcessor extends DefaultValueProcessor { @Override public String convertSQLValueByType(SQLDataValue dataValue) { - String value = dataValue.getValue(); - if (value.startsWith("0x")) { - return EasyStringUtils.quoteString(value.substring(2)); - } else { - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - boolean isDigit = (c >= '0' && c <= '9'); - boolean isUpperCaseHex = (c >= 'A' && c <= 'F'); - boolean isLowerCaseHex = (c >= 'a' && c <= 'f'); - if (!isDigit && !isUpperCaseHex && !isLowerCaseHex) { - return EasyStringUtils.quoteString(dataValue.getBlobHexString()); - } - } - return EasyStringUtils.quoteString(value); - } + return EasyStringUtils.quoteString(OracleSqlGuards.normalizeHexLiteral( + dataValue.getValue(), dataValue.getBlobHexString())); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleRawValueProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleRawValueProcessor.java index 430b166d57..da793c949a 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleRawValueProcessor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/sub/OracleRawValueProcessor.java @@ -1,6 +1,7 @@ package ai.chat2db.plugin.oracle.value.sub; import ai.chat2db.community.tools.util.EasyStringUtils; +import ai.chat2db.plugin.oracle.OracleSqlGuards; import ai.chat2db.spi.DefaultValueProcessor; import ai.chat2db.spi.model.value.JDBCDataValue; import ai.chat2db.community.domain.api.model.value.SQLDataValue; @@ -11,21 +12,8 @@ public class OracleRawValueProcessor extends DefaultValueProcessor { @Override public String convertSQLValueByType(SQLDataValue dataValue) { - String value = dataValue.getValue(); - if (value.startsWith("0x")) { - return EasyStringUtils.quoteString(value.substring(2)); - } else { - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - boolean isDigit = (c >= '0' && c <= '9'); - boolean isUpperCaseHex = (c >= 'A' && c <= 'F'); - boolean isLowerCaseHex = (c >= 'a' && c <= 'f'); - if (!isDigit && !isUpperCaseHex && !isLowerCaseHex) { - return EasyStringUtils.quoteString(dataValue.getBlobHexString()); - } - } - return EasyStringUtils.quoteString(value); - } + return EasyStringUtils.quoteString(OracleSqlGuards.normalizeHexLiteral( + dataValue.getValue(), dataValue.getBlobHexString())); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/template/OracleDmlValueTemplate.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/template/OracleDmlValueTemplate.java index 6b63ebe938..dcdc2e7cb1 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/template/OracleDmlValueTemplate.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/main/java/ai/chat2db/plugin/oracle/value/template/OracleDmlValueTemplate.java @@ -1,45 +1,45 @@ package ai.chat2db.plugin.oracle.value.template; -import static ai.chat2db.plugin.oracle.constant.OracleDmlValueTemplateConstants.*; +import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor; +import static ai.chat2db.plugin.oracle.constant.OracleDmlValueTemplateConstants.*; -public class OracleDmlValueTemplate { - +public class OracleDmlValueTemplate { public static String wrapDate(String date) { - return String.format(DATE_TEMPLATE, date); + return String.format(DATE_TEMPLATE, OracleIdentifierProcessor.INSTANCE.escapeString(date)); } public static String wrapTimestamp(String timestamp, int scale) { - return String.format(TIMESTAMP_TEMPLATE, timestamp, scale); + return String.format(TIMESTAMP_TEMPLATE, OracleIdentifierProcessor.INSTANCE.escapeString(timestamp), scale); } public static String wrapTimestampTz(String timestamp, int scale) { - return String.format(TIMESTAMP_TZ_TEMPLATE, timestamp, scale); + return String.format(TIMESTAMP_TZ_TEMPLATE, OracleIdentifierProcessor.INSTANCE.escapeString(timestamp), scale); } public static String wrapTimestampTzWithOutNanos(String timestamp) { - return String.format(TIMESTAMP_TZ_WITHOUT_NANOS_TEMPLATE, timestamp); + return String.format(TIMESTAMP_TZ_WITHOUT_NANOS_TEMPLATE, OracleIdentifierProcessor.INSTANCE.escapeString(timestamp)); } public static String wrapIntervalYearToMonth(String year, int precision) { - return String.format(INTERVAL_YEAR_TO_MONTH_TEMPLATE, year, precision); + return String.format(INTERVAL_YEAR_TO_MONTH_TEMPLATE, OracleIdentifierProcessor.INSTANCE.escapeString(year), precision); } public static String wrapIntervalDayToSecond(String day, int precision, int scale) { - return String.format(INTERVAL_DAY_TO_SECOND_TEMPLATE, day, precision, scale); + return String.format(INTERVAL_DAY_TO_SECOND_TEMPLATE, OracleIdentifierProcessor.INSTANCE.escapeString(day), precision, scale); } public static String wrapXml(String xml) { - return String.format(XML_TEMPLATE, xml); + return String.format(XML_TEMPLATE, OracleIdentifierProcessor.INSTANCE.escapeString(xml)); } } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/test/java/ai/chat2db/plugin/oracle/OracleIdentifierProcessorTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/test/java/ai/chat2db/plugin/oracle/OracleIdentifierProcessorTest.java new file mode 100644 index 0000000000..6b50dcdcdd --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oracle/src/test/java/ai/chat2db/plugin/oracle/OracleIdentifierProcessorTest.java @@ -0,0 +1,414 @@ +package ai.chat2db.plugin.oracle; + +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.metadata.DataType; +import ai.chat2db.community.domain.api.model.value.SQLDataValue; +import ai.chat2db.community.domain.api.model.view.ModifyView; +import ai.chat2db.plugin.oracle.builder.OracleSqlBuilder; +import ai.chat2db.plugin.oracle.enums.type.OracleColumnTypeEnum; +import ai.chat2db.plugin.oracle.enums.type.OracleIndexTypeEnum; +import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor; +import ai.chat2db.plugin.oracle.value.sub.OracleRawValueProcessor; +import ai.chat2db.plugin.oracle.value.template.OracleDmlValueTemplate; +import ai.chat2db.plugin.oracle.value.OracleValueProcessor; +import ai.chat2db.spi.model.request.DropTableRequest; +import ai.chat2db.spi.model.request.TruncateTableRequest; +import ai.chat2db.spi.model.request.UpdateSqlRequest; +import com.google.common.io.BaseEncoding; +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 OracleIdentifierProcessorTest { + + @Test + void quoteIdentifierPassesThroughNullAndBlank() { + assertNull(OracleIdentifierProcessor.INSTANCE.quoteIdentifier(null)); + assertNull(OracleIdentifierProcessor.INSTANCE.quoteIdentifier(null, 12, 2)); + assertNull(OracleIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase(null)); + assertEquals("", OracleIdentifierProcessor.INSTANCE.quoteIdentifier("")); + assertEquals(" ", OracleIdentifierProcessor.INSTANCE.quoteIdentifier(" ")); + assertEquals(" ", OracleIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase(" ")); + } + + @Test + void quoteIdentifierIsConditionalForSpiConsumers() { + OracleIdentifierProcessor processor = OracleIdentifierProcessor.INSTANCE; + // plain uppercase identifier: returned unquoted + assertEquals("EMPLOYEES", processor.quoteIdentifier("EMPLOYEES")); + // lowercase: quoted to preserve case + assertEquals("\"employees\"", processor.quoteIdentifier("employees")); + // reserved keyword: quoted + assertEquals("\"SELECT\"", processor.quoteIdentifier("SELECT")); + assertEquals("\"TABLE\"", processor.quoteIdentifier("TABLE", 12, 2)); + // invalid characters: quoted with embedded quotes doubled + assertEquals("\"A\"\"B\"", processor.quoteIdentifier("A\"B")); + assertEquals("\"WE IRD\"", processor.quoteIdentifier("WE IRD")); + // already-quoted input is unwrapped once, not double-wrapped + assertEquals("\"ALREADY\"", processor.quoteIdentifier("\"ALREADY\"")); + // versioned overload delegates to the single-arg conditional variant + assertEquals(processor.quoteIdentifier("employees"), processor.quoteIdentifier("employees", 19, 0)); + } + + @Test + void quoteIdentifierIgnoreCasePreservesCaseAndIsConditional() { + OracleIdentifierProcessor processor = OracleIdentifierProcessor.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 quoteIdentifierAlwaysWrapsUnconditionallyForDdlPaths() { + OracleIdentifierProcessor processor = OracleIdentifierProcessor.INSTANCE; + assertNull(processor.quoteIdentifierAlways(null)); + assertEquals("\"EMPLOYEES\"", processor.quoteIdentifierAlways("EMPLOYEES")); + assertEquals("\"employees\"", processor.quoteIdentifierAlways("employees")); + assertEquals("\"A\"\"B\"", processor.quoteIdentifierAlways("A\"B")); + assertEquals("\"\"\"ALREADY\"\"\"", processor.quoteIdentifierAlways("\"ALREADY\"")); + + String[] rawIdentifiers = {"", "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 escapeSqlLiteralDoublesSingleQuotes() { + assertEquals("O''Brien", OracleIdentifierProcessor.INSTANCE.escapeString("O'Brien")); + assertNull(OracleIdentifierProcessor.INSTANCE.escapeString(null)); + assertEquals("plain", OracleIdentifierProcessor.INSTANCE.escapeString("plain")); + assertEquals("'C:\\tmp\\O''Brien'", + OracleIdentifierProcessor.INSTANCE.quoteStringLiteral("C:\\tmp\\O'Brien")); + } + + @Test + void valueProcessorsDoNotRewriteOracleBackslashes() { + SQLDataValue value = new SQLDataValue(); + value.setValue("C:\\tmp\\O'Brien"); + DataType type = new DataType(); + type.setDataTypeName("VARCHAR2"); + value.setDataType(type); + + assertEquals("'C:\\tmp\\O''Brien'", new OracleValueProcessor().convertSQLValueByType(value)); + + type.setDataTypeName("CLOB"); + assertEquals("'C:\\tmp\\O''Brien'", new OracleValueProcessor().convertSQLValueByType(value)); + } + + @Test + void escapeIdentifierDoublesEveryDoubleQuote() { + assertEquals("WE\"\"IRD", OracleIdentifierProcessor.escapeIdentifier("WE\"IRD")); + assertEquals("\"\"ALREADY\"\"", OracleIdentifierProcessor.escapeIdentifier("\"ALREADY\"")); + assertEquals("\"A\"\"B\"", OracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways("A\"B")); + assertNull(OracleIdentifierProcessor.escapeIdentifier(null)); + } + + @Test + void reservedWordsAndCaseConversionAreLocaleIndependent() { + Locale original = Locale.getDefault(); + try { + Locale.setDefault(Locale.forLanguageTag("tr-TR")); + assertTrue(OracleIdentifierProcessor.INSTANCE.isReservedKeyword("insert", null, null)); + assertEquals("ID", OracleIdentifierProcessor.INSTANCE.convertIdentifierCase("id")); + assertEquals("\"insert\"", OracleIdentifierProcessor.INSTANCE.quoteIdentifier("insert")); + } finally { + Locale.setDefault(original); + } + } + + @Test + void getMetaDataNameNeutralizesEmbeddedQuotes() { + OracleMetaData metaData = new OracleMetaData(); + String result = metaData.getMetaDataName("PUBLIC", "A\".\"B"); + assertEquals("\"PUBLIC\".\"A\"\".\"\"B\"", result); + assertFalse(result.contains("A\".\"B\"."), "injection payload must not break out of the quoted identifier"); + } + + @Test + void dropTableQuotesAndEscapesTableName() { + String sql = new OracleDBManager().dropTable(null, null, null, "T\"; DROP TABLE U; --"); + assertEquals("DROP TABLE \"T\"\"; DROP TABLE U; --\"", sql); + } + + @Test + void dbManagerQualifiesRawAndServicePrequotedNames() throws Exception { + OracleDBManager manager = new OracleDBManager(); + assertEquals("DROP TABLE \"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\"")); + } + + @Test + void inheritedBuilderPathsUseOracleSchemaQualification() { + OracleSqlBuilder builder = new OracleSqlBuilder(); + 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("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 metadataQualifiedNamesAreLimitedToSchemaAndObject() { + assertEquals("\"SALES\".\"ORDERS\"", + new OracleMetaData().getMetaDataName("ignored_database", "SALES", "ORDERS")); + } + + @Test + void buildCreateViewEscapesNamesAndComment() { + ModifyView view = new ModifyView(); + view.setSchemaName("SA\"LES"); + view.setViewName("V\"; DROP TABLE U; --"); + view.setViewBody("SELECT ID FROM EMPLOYEE"); + view.setComment("x'); DROP TABLE USERS; --"); + + String sql = new OracleSqlBuilder().buildCreateView(view); + + assertTrue(sql.startsWith("CREATE VIEW \"SA\"\"LES\".\"V\"\"; DROP TABLE U; --\"")); + assertTrue(sql.endsWith("COMMENT ON TABLE \"SA\"\"LES\".\"V\"\"; DROP TABLE U; --\" is 'x''); DROP TABLE USERS; --';")); + } + + @Test + void buildCreateViewRejectsUnrecognizedRawClauses() { + ModifyView view = new ModifyView(); + view.setSchemaName("SALES"); + view.setViewName("V"); + view.setViewBody("SELECT 1 FROM DUAL"); + view.setEditClause("EDITIONING; DROP TABLE U; --"); + + assertThrows(IllegalArgumentException.class, () -> new OracleSqlBuilder().buildCreateView(view)); + + view.setEditClause(null); + view.setSubqueryRestrictionClause("CHECK OPTION"); + view.setSubqueryConstraintName("C\"; DROP TABLE U; --"); + String sql = new OracleSqlBuilder().buildCreateView(view); + assertTrue(sql.contains("CHECK OPTION CONSTRAINT \"C\"\"; DROP TABLE U; --\""), sql); + } + + @Test + void buildCreateTableEscapesNamesAndComments() { + Table table = new Table(); + table.setSchemaName("S\"CHEMA"); + table.setName("T\"ABLE"); + table.setComment("x'); DROP TABLE USERS; --"); + TableColumn column = new TableColumn(); + column.setName("C\"OL"); + column.setSchemaName("S\"CHEMA"); + column.setTableName("T\"ABLE"); + column.setColumnType("VARCHAR2"); + column.setColumnSize(20); + column.setComment("c'); DROP TABLE U; --"); + table.setColumnList(List.of(column)); + table.setIndexList(List.of()); + + String sql = new OracleSqlBuilder().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 buildModifyColumnDeleteEscapesQualifiedNames() { + TableColumn column = new TableColumn(); + column.setEditStatus("DELETE"); + column.setSchemaName("S\"; DROP TABLE U; --"); + column.setTableName("T"); + column.setName("C"); + + String sql = OracleColumnTypeEnum.VARCHAR2.buildModifyColumn(column); + + assertEquals("ALTER TABLE \"S\"\"; DROP TABLE U; --\".\"T\" DROP COLUMN \"C\"", sql); + } + + @Test + void buildModifyColumnPreservesUnknownTypesAndCaseOnlyRenames() { + TableColumn oldColumn = new TableColumn(); + oldColumn.setNullable(1); + + TableColumn column = new TableColumn(); + column.setEditStatus("MODIFY"); + column.setSchemaName("S"); + column.setTableName("T"); + column.setOldName("MixedCase"); + column.setName("mixedcase"); + column.setColumnType("\"APP\".\"Order Type\""); + column.setNullable(1); + column.setOldColumn(oldColumn); + + String sql = OracleColumnTypeEnum.VARCHAR2.buildModifyColumn(column); + + assertTrue(sql.contains("MODIFY (\"mixedcase\" \"APP\".\"Order Type\" NULL)"), sql); + assertTrue(sql.contains("RENAME COLUMN \"MixedCase\" TO \"mixedcase\""), sql); + } + + @Test + void buildIndexScriptEscapesNamesAndValidatesDirection() { + TableIndex index = new TableIndex(); + index.setType(OracleIndexTypeEnum.NORMAL.getName()); + index.setSchemaName("S\"; X"); + index.setTableName("T"); + index.setName("I\"X"); + TableIndexColumn indexColumn = new TableIndexColumn(); + indexColumn.setColumnName("C\"D"); + indexColumn.setAscOrDesc("desc"); + index.setColumnList(List.of(indexColumn)); + + String sql = OracleIndexTypeEnum.NORMAL.buildIndexScript(index); + + assertEquals("CREATE INDEX \"S\"\"; X\".\"I\"\"X\" ON \"S\"\"; X\".\"T\" (\"C\"\"D\" DESC)", sql); + } + + @Test + void buildIndexColumnRejectsHostileSortDirection() { + TableIndex index = new TableIndex(); + index.setType(OracleIndexTypeEnum.NORMAL.getName()); + index.setSchemaName("S"); + index.setTableName("T"); + index.setName("I"); + TableIndexColumn indexColumn = new TableIndexColumn(); + indexColumn.setColumnName("C"); + indexColumn.setAscOrDesc("DESC; DROP TABLE U; --"); + index.setColumnList(List.of(indexColumn)); + + assertThrows(IllegalArgumentException.class, () -> OracleIndexTypeEnum.NORMAL.buildIndexScript(index)); + } + + @Test + void buildCreateColumnSqlAcceptsLegitimateDefaults() { + String[] valid = {"SYSDATE", "CURRENT_TIMESTAMP", "USER", "SEQ.NEXTVAL", "-1", "1.5", + "'Y'", "'0'", "'O''Brien'", "'1970-01-01'", "''", "SYS_GUID()", + "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) { + TableColumn column = new TableColumn(); + column.setName("c1"); + column.setColumnType("VARCHAR2"); + column.setDefaultValue(defaultValue); + assertTrue(OracleColumnTypeEnum.VARCHAR2.buildCreateColumnSql(column).contains("DEFAULT " + defaultValue), + "default should be accepted: " + defaultValue); + } + } + + @Test + void buildCreateColumnSqlRejectsDefaultValuesThatReshapeDdl() { + String[] payloads = { + "0) --", + "0 --", + "1, x INT", + "0); DROP TABLE x--", + "'abc", + "'a'--", + "'a'; DROP TABLE x--", + "0 NOT NULL", + "0 CHECK (1=1)", + "0 CONSTRAINT injected UNIQUE" + }; + for (String payload : payloads) { + TableColumn column = new TableColumn(); + column.setName("c1"); + column.setColumnType("VARCHAR2"); + column.setDefaultValue(payload); + assertThrows(IllegalArgumentException.class, + () -> OracleColumnTypeEnum.VARCHAR2.buildCreateColumnSql(column), + "default should be rejected: " + payload); + } + } + + @Test + void buildCreateColumnSqlAcceptsUnknownButSafeTypeNames() { + String[] valid = {"MYCUSTOMTYPE", "VARCHAR2(20)", "NUMBER(10,2)", "TIMESTAMP(6) WITH TIME ZONE", + "INTERVAL DAY(2) TO SECOND(6)", "VARCHAR2(20 CHAR)", "\"APP\".\"Order Type\"", + "REF \"APP\".\"Object Type\""}; + for (String typeName : valid) { + TableColumn column = new TableColumn(); + column.setName("c1"); + column.setColumnType(typeName); + assertEquals("\"c1\" " + typeName, OracleColumnTypeEnum.VARCHAR2.buildCreateColumnSql(column), + "type should be accepted: " + typeName); + } + } + + @Test + void buildCreateColumnSqlRejectsHostileTypeNames() { + String[] payloads = {"INTEGER); DROP TABLE U; --", "INT, x INT", "INT'--", "INT\"--", "0) --", + "INTEGER NOT NULL", "VARCHAR2(20) DEFAULT 0", "INTEGER CHECK(1=1)"}; + for (String payload : payloads) { + TableColumn column = new TableColumn(); + column.setName("c1"); + column.setColumnType(payload); + assertThrows(IllegalArgumentException.class, + () -> OracleColumnTypeEnum.VARCHAR2.buildCreateColumnSql(column), + "type should be rejected: " + payload); + } + } + + @Test + void requireUnitAcceptsCharAndByteOnly() { + assertEquals("CHAR", OracleSqlGuards.requireUnit("CHAR")); + assertEquals("byte", OracleSqlGuards.requireUnit("byte")); + assertThrows(IllegalArgumentException.class, () -> OracleSqlGuards.requireUnit("BYTE); DROP TABLE U; --")); + } + + @Test + void requireAscOrDescCanonicalizesAndRejects() { + assertEquals("ASC", OracleSqlGuards.requireAscOrDesc("asc")); + assertEquals("DESC", OracleSqlGuards.requireAscOrDesc(" DESC ")); + assertThrows(IllegalArgumentException.class, () -> OracleSqlGuards.requireAscOrDesc("DESC; DROP TABLE U; --")); + } + + @Test + void wrapDateEscapesSingleQuotesInLiteralPosition() { + String sql = OracleDmlValueTemplate.wrapDate("2020-01-01' OR '1'='1"); + assertEquals("TO_DATE('2020-01-01'' OR ''1''=''1', 'SYYYY-MM-DD HH24:MI:SS')", sql); + assertFalse(sql.contains("'2020-01-01' OR")); + } + + @Test + void rawProcessorNeutralizesNonHexAfterZeroXPrefix() { + SQLDataValue dataValue = new SQLDataValue(); + dataValue.setValue("0x'; DROP TABLE U; --"); + + String sql = new OracleRawValueProcessor().convertSQLValueByType(dataValue); + + String expectedHex = BaseEncoding.base16().encode("0x'; DROP TABLE U; --".getBytes()); + assertEquals("'" + expectedHex + "'", sql); + } + + @Test + void rawProcessorKeepsValidHexLiteral() { + SQLDataValue dataValue = new SQLDataValue(); + dataValue.setValue("0x1A2b"); + + String sql = new OracleRawValueProcessor().convertSQLValueByType(dataValue); + + assertEquals("'1A2b'", sql); + } +}