Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
<groupId>ai.chat2db</groupId>
<artifactId>chat2db-community-oracle</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +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.ISQLIdentifierProcessor;
import ai.chat2db.spi.util.SqlUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
Expand All @@ -18,19 +20,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 {
Expand Down Expand Up @@ -85,7 +87,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");
Expand All @@ -98,5 +100,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));
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package ai.chat2db.plugin.oceanbase.oracle.identifier;

import ai.chat2db.plugin.oracle.identifier.OracleIdentifierProcessor;
import org.apache.commons.lang3.StringUtils;

/**
* OceanBase (Oracle mode) dialect identifier processor.
* <p>
* {@link #quoteIdentifier(String)} is SPI-facing and conditional: plain,
* non-reserved identifiers pass through unquoted so completion/matching
* consumers keep working; anything else is wrapped with double quotes.
* {@link #quoteIdentifierAlways(String)} is the unconditional variant reserved
* for DDL-generation call sites that must emit quoted output.
* String literals are escaped by single-quote doubling via {@link #escapeString(String)}.
* Shared stateless instance available via {@link #INSTANCE} for call sites without
* MetaData access.
*/
public class OceanbaseOracleIdentifierProcessor extends OracleIdentifierProcessor {

public static final OceanbaseOracleIdentifierProcessor INSTANCE = new OceanbaseOracleIdentifierProcessor();

/**
* Conditional quoting for SPI consumers: {@code null} passes through, blank
* is returned unchanged, a valid plain identifier that is not a reserved
* keyword is returned unquoted; otherwise the identifier is wrapped with
* double quotes (one surrounding quote pair stripped, embedded double
* quotes doubled).
*/
@Override
public String quoteIdentifier(String identifier) {
if (identifier == null) {
return null;
}
if (StringUtils.isBlank(identifier)) {
return identifier;
}
if (isValidIdentifier(identifier) && !isReservedKeyword(identifier.toUpperCase(), null, null)) {
return identifier;
}
return quoteIdentifierAlways(identifier);
}

@Override
public String quoteIdentifier(String identifier, Integer majorVersion, Integer minorVersion) {
return quoteIdentifier(identifier);
}

/**
* The always-quote SPI variant: preserves case and unconditionally wraps.
*/
@Override
public String quoteIdentifierIgnoreCase(String identifier) {
return quoteIdentifierAlways(identifier);
}

/**
* Unconditional quoting for DDL-generation call sites: {@code null}/blank pass
* through unchanged, otherwise wraps with double quotes, stripping one surrounding
* quote pair and doubling every embedded double quote.
*/
public String quoteIdentifierAlways(String identifier) {
if (org.apache.commons.lang3.StringUtils.isBlank(identifier)) {
return identifier;
}
return "\"" + escapeIdentifierContent(identifier) + "\"";
}

/**
* Escapes a value interpolated into a single-quoted SQL string literal by
* doubling every single quote.
*/
@Override
public String escapeString(String str) {
return str == null ? "" : StringUtils.replace(str, "'", "''");
}

private static String escapeIdentifierContent(String identifier) {
if (identifier == null) {
return "";
}
String stripped = identifier;
if (stripped.length() >= 2 && stripped.startsWith("\"") && stripped.endsWith("\"")) {
stripped = stripped.substring(1, stripped.length() - 1);
}
return StringUtils.replace(stripped, "\"", "\"\"");
}

/**
* Escapes identifier content for a position already surrounded by double
* quotes: strips one surrounding quote pair, then doubles every embedded
* double quote.
*/
public static String escapeIdentifier(String identifier) {
return escapeIdentifierContent(identifier);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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;

class OceanbaseOracleIdentifierProcessorTest {

@Test
void escapeSqlLiteralDoublesSingleQuotes() {
assertEquals("O''Brien", OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString("O'Brien"));
assertEquals("plain", OceanbaseOracleIdentifierProcessor.INSTANCE.escapeString("plain"));
assertEquals("", 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"));
assertEquals("\"we\"\"ird\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierAlways("\"we\"ird\""));
}

@Test
void quoteIdentifierIgnoreCaseIsTheAlwaysQuoteVariant() {
assertNull(OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase(null));
assertEquals("\"MY_TABLE\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("MY_TABLE"));
assertEquals("\"my_table\"", OceanbaseOracleIdentifierProcessor.INSTANCE.quoteIdentifierIgnoreCase("my_table"));
}

@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'"));
}
}
Loading