diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/pom.xml b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/pom.xml
index f0dbc859ce..af0c477c44 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/pom.xml
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/pom.xml
@@ -19,6 +19,11 @@
ai.chat2db
chat2db-community-oracle
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/main/java/ai/chat2db/plugin/oceanbase/oracle/OceanbaseOracleMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/main/java/ai/chat2db/plugin/oceanbase/oracle/OceanbaseOracleMetaData.java
index 243647521b..353d82759e 100644
--- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/main/java/ai/chat2db/plugin/oceanbase/oracle/OceanbaseOracleMetaData.java
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/main/java/ai/chat2db/plugin/oceanbase/oracle/OceanbaseOracleMetaData.java
@@ -1,10 +1,11 @@
package ai.chat2db.plugin.oceanbase.oracle;
+import ai.chat2db.plugin.oceanbase.oracle.identifier.OceanbaseOracleIdentifierProcessor;
import ai.chat2db.plugin.oracle.OracleMetaData;
import ai.chat2db.community.tools.util.EasyStringUtils;
import ai.chat2db.spi.IDbMetaData;
import ai.chat2db.spi.DefaultSQLExecutor;
-import ai.chat2db.spi.util.SqlUtils;
+import ai.chat2db.spi.ISQLIdentifierProcessor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@@ -18,19 +19,19 @@
@Slf4j
public class OceanbaseOracleMetaData extends OracleMetaData implements IDbMetaData {
-
-
-
-
+ @Override
+ public ISQLIdentifierProcessor getSQLIdentifierProcessor() {
+ return OceanbaseOracleIdentifierProcessor.INSTANCE;
+ }
@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 PUIndexSql = String.format(PU_INDEX_NAME_SQL, schemaName, tableName);
- String tableIndexNameSql = String.format(TABLE_INDEX_NAME_SQL, schemaName, tableName);
+ String sql = buildTableDdlSql(tableName, schemaName);
+ String tableCommentSql = buildTableCommentSql(schemaName, tableName);
+ String tableColumnCommentSql = buildTableColumnCommentSql(schemaName, tableName);
+ String PUIndexSql = buildPuIndexNameSql(schemaName, tableName);
+ String tableIndexNameSql = buildTableIndexNameSql(schemaName, tableName);
StringBuilder ddlBuilder = new StringBuilder();
DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> {
try {
@@ -45,7 +46,9 @@ 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 ")
+ ddlBuilder.append("\nCOMMENT ON TABLE ")
+ .append(OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName))
+ .append(" IS ")
.append(EasyStringUtils.escapeAndQuoteString(tableComment)).append(";");
}
}
@@ -56,8 +59,8 @@ 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(OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(tableName)).append(".")
+ .append(OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(columnName)).append(" IS ")
.append(EasyStringUtils.escapeAndQuoteString(columnComment)).append(";");
}
}
@@ -85,7 +88,7 @@ public String tableDDL(Connection connection, String databaseName, String schema
return indexNames;
});
for (String index : indexes) {
- String tableIndexSql = String.format(TABLE_INDEX_DDL_SQL, index, schemaName);
+ String tableIndexSql = buildTableIndexDdlSql(index, schemaName);
DefaultSQLExecutor.getInstance().execute(connection, tableIndexSql, resultSet -> {
while (resultSet.next()) {
String ddl = resultSet.getString("ddl");
@@ -98,5 +101,35 @@ public String tableDDL(Connection connection, String databaseName, String schema
return ddlBuilder.toString();
}
+ static String buildTableDdlSql(String tableName, String schemaName) {
+ return String.format(TABLE_DDL_SQL, OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(tableName),
+ OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(schemaName));
+ }
+
+ static String buildTableCommentSql(String schemaName, String tableName) {
+ return String.format(TABLE_COMMENT_SQL, OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(schemaName),
+ OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(tableName));
+ }
+
+ static String buildTableColumnCommentSql(String schemaName, String tableName) {
+ return String.format(TABLE_COLUMN_COMMENT_SQL, OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(schemaName),
+ OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(tableName));
+ }
+
+ static String buildPuIndexNameSql(String schemaName, String tableName) {
+ return String.format(PU_INDEX_NAME_SQL, OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(schemaName),
+ OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(tableName));
+ }
+
+ static String buildTableIndexNameSql(String schemaName, String tableName) {
+ return String.format(TABLE_INDEX_NAME_SQL, OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(schemaName),
+ OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(tableName));
+ }
+
+ static String buildTableIndexDdlSql(String indexName, String schemaName) {
+ return String.format(TABLE_INDEX_DDL_SQL, OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(indexName),
+ OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(schemaName));
+ }
+
}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/main/java/ai/chat2db/plugin/oceanbase/oracle/identifier/OceanbaseOracleIdentifierProcessor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/main/java/ai/chat2db/plugin/oceanbase/oracle/identifier/OceanbaseOracleIdentifierProcessor.java
new file mode 100644
index 0000000000..9b9e5eaa98
--- /dev/null
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/main/java/ai/chat2db/plugin/oceanbase/oracle/identifier/OceanbaseOracleIdentifierProcessor.java
@@ -0,0 +1,12 @@
+package ai.chat2db.plugin.oceanbase.oracle.identifier;
+
+import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor;
+
+/**
+ * OceanBase Oracle mode uses Oracle's identifier, case-folding, and literal
+ * escaping rules.
+ */
+public class OceanbaseOracleIdentifierProcessor extends OracleIdentifierProcessor {
+
+ public static final OceanbaseOracleIdentifierProcessor INSTANCE = new OceanbaseOracleIdentifierProcessor();
+}
diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/test/java/ai/chat2db/plugin/oceanbase/oracle/OceanbaseOracleIdentifierProcessorTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/test/java/ai/chat2db/plugin/oceanbase/oracle/OceanbaseOracleIdentifierProcessorTest.java
new file mode 100644
index 0000000000..852c09dfa4
--- /dev/null
+++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-oceanbase-oracle/src/test/java/ai/chat2db/plugin/oceanbase/oracle/OceanbaseOracleIdentifierProcessorTest.java
@@ -0,0 +1,126 @@
+package ai.chat2db.plugin.oceanbase.oracle;
+
+import ai.chat2db.plugin.oceanbase.oracle.identifier.OceanbaseOracleIdentifierProcessor;
+import org.junit.jupiter.api.Test;
+
+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.assertSame;
+
+class OceanbaseOracleIdentifierProcessorTest {
+
+ @Test
+ void escapeSqlLiteralDoublesSingleQuotes() {
+ assertEquals("O''Brien", OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString("O'Brien"));
+ assertEquals("plain", OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString("plain"));
+ assertNull(OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString(null));
+ }
+
+ @Test
+ void quoteIdentifierPassesThroughNullAndBlank() {
+ assertNull(OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier(null));
+ assertEquals("", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier(""));
+ assertEquals(" ", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier(" "));
+ }
+
+ @Test
+ void quoteIdentifierLeavesPlainIdentifiersUnquoted() {
+ assertEquals("MY_TABLE", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("MY_TABLE"));
+ assertEquals("\"my_table\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("my_table"));
+ assertEquals("COL_1", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("COL_1"));
+ }
+
+ @Test
+ void quoteIdentifierQuotesReservedKeywordsAndSpecialChars() {
+ assertEquals("\"TABLE\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("TABLE"));
+ assertEquals("\"select\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("select"));
+ assertEquals("\"has space\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("has space"));
+ assertEquals("\"we\"\"ird\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("we\"ird"));
+ assertEquals("\"we\"\"ird\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("\"we\"\"ird\""));
+ }
+
+ @Test
+ void versionedQuoteIdentifierDelegatesToConditional() {
+ assertEquals("MY_TABLE", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("MY_TABLE", 4, 2));
+ assertEquals("\"TABLE\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier("TABLE", null, null));
+ assertNull(OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifier(null, null, null));
+ }
+
+ @Test
+ void quoteIdentifierAlwaysQuotesUnconditionally() {
+ assertNull(OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(null));
+ assertEquals("\"MY_TABLE\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways("MY_TABLE"));
+ assertEquals("\"we\"\"ird\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways("we\"ird"));
+
+ String[] rawIdentifiers = {"", "A\"B", "\"ALREADY\"", "\"A", "A\"", "\"\"", "A\"\"B"};
+ for (String raw : rawIdentifiers) {
+ assertEquals(raw,
+ OceanbaseOracleIdentifierProcessor.INSTANCE.removeIdentifierQuote(
+ OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways(raw)));
+ }
+ }
+
+ @Test
+ void quoteIdentifierIgnoreCasePreservesCaseConditionally() {
+ assertNull(OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase(null));
+ assertEquals("MY_TABLE", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("MY_TABLE"));
+ assertEquals("my_table", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("my_table"));
+ assertEquals("\"select\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("select"));
+ }
+
+ @Test
+ void metadataExposesTheOceanbaseOracleProcessor() {
+ assertSame(OceanbaseOracleIdentifierProcessor.INSTANCE,
+ new OceanbaseOracleMetaData().getSQLIdentifierProcessor());
+ }
+
+ @Test
+ void buildTableDdlSqlNeutralizesMaliciousNames() {
+ String sql = OceanbaseOracleMetaData.buildTableDdlSql("T' OR '1'='1", "S' OR '1'='1");
+
+ assertEquals("select dbms_metadata.get_ddl('TABLE','T'' OR ''1''=''1','S'' OR ''1''=''1') as sql from dual",
+ sql);
+ assertFalse(sql.contains("'T' OR '1'='1'"));
+ }
+
+ @Test
+ void buildTableCommentSqlNeutralizesMaliciousNames() {
+ String sql = OceanbaseOracleMetaData.buildTableCommentSql("SCOTT' OR '1'='1", "O'Brien");
+
+ assertEquals("select owner, table_name, comments from ALL_TAB_COMMENTS where OWNER = 'SCOTT'' OR ''1''=''1'"
+ + " and TABLE_NAME = 'O''Brien'", sql);
+ }
+
+ @Test
+ void buildTableIndexDdlSqlNeutralizesMaliciousIndexName() {
+ String sql = OceanbaseOracleMetaData.buildTableIndexDdlSql("IDX', 'S', 'X", "SCOTT");
+
+ assertEquals(String.format(
+ ai.chat2db.plugin.oceanbase.constant.OceanbaseOracleMetaDataConstants.TABLE_INDEX_DDL_SQL,
+ "IDX'', ''S'', ''X", "SCOTT"),
+ sql);
+ assertFalse(sql.contains("'IDX', 'S', 'X'"));
+ }
+
+ @Test
+ void allMetadataQueriesEscapeLiteralArguments() {
+ assertEquals("SELECT owner, table_name, column_name, comments\n"
+ + "FROM all_col_comments\n"
+ + "WHERE owner = 'S''1' and table_name = 'T''1' and comments is not null",
+ OceanbaseOracleMetaData.buildTableColumnCommentSql("S'1", "T'1"));
+ assertEquals("SELECT DISTINCT AC.INDEX_NAME\n"
+ + "FROM ALL_CONSTRAINTS AC\n"
+ + "WHERE AC.OWNER = 'S''1' AND AC.TABLE_NAME = 'T''1'\n"
+ + " AND AC.CONSTRAINT_TYPE IN ('P','U')",
+ OceanbaseOracleMetaData.buildPuIndexNameSql("S'1", "T'1"));
+ assertEquals("select\n"
+ + " INDEX_NAME\n"
+ + "from\n"
+ + " SYS.ALL_INDEXES\n"
+ + "where\n"
+ + " OWNER = 'S''1'\n"
+ + " and TABLE_NAME = 'T''1'\n",
+ OceanbaseOracleMetaData.buildTableIndexNameSql("S'1", "T'1"));
+ }
+}