Skip to content
Merged
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 @@ -362,10 +362,7 @@ public SqlContextParser contextParser(DbSqlContextParserRequest sqlContextParser
}
for (Token token : tokensOnDefault) {
String text = token.getText();
if (text.startsWith(".")) {
text = text.substring(1);
}
text = sqlIdentifierProcessor.removeIdentifierQuote(text);
text = normalizeIdentifierToken(sqlIdentifierProcessor, text);
if (tableMap.containsKey(text)) {
SimpleTableColumnMapping simpleTableColumnMapping = new SimpleTableColumnMapping();
Table table = tableMap.get(text);
Expand Down Expand Up @@ -504,6 +501,14 @@ public SqlContextParser contextParser(DbSqlContextParserRequest sqlContextParser
}
}

static String normalizeIdentifierToken(ISQLIdentifierProcessor processor, String tokenText) {
if (tokenText == null) {
return null;
}
String identifier = tokenText.startsWith(".") ? tokenText.substring(1) : tokenText;
return processor.removeIdentifierQuote(identifier);
}

@Override
public List<SqlHover> sqlHover(DbSqlHoverRequest sqlHoverParam) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,9 @@ && matchesPattern(beforeSql + " " + afterSql, SELECT_TIP_COLUMN_FROM_PATTERN)) {
}
}
tableName = split[split.length - 1];
databaseName = sqlIdentifierProcessor.removeIdentifierQuote(databaseName);
schemaName = sqlIdentifierProcessor.removeIdentifierQuote(schemaName);
tableName = sqlIdentifierProcessor.removeIdentifierQuote(tableName);
databaseName = normalizeIdentifierPart(sqlIdentifierProcessor, databaseName);
schemaName = normalizeIdentifierPart(sqlIdentifierProcessor, schemaName);
tableName = normalizeIdentifierPart(sqlIdentifierProcessor, tableName);
tableAlias = sqlIdentifierProcessor.quoteIdentifier(tableAliasEntry.getValue());
if (supportDatabase && StringUtils.isBlank(databaseName)) {
continue;
Expand Down Expand Up @@ -551,17 +551,17 @@ private List<SqlCompletionCandidate> buildColumnCandidates(Map<String, String> t
databaseName = splitName[0];
}
}
databaseName = processor.removeIdentifierQuote(databaseName);
databaseName = normalizeIdentifierPart(processor, databaseName);
if (supportDatabase && StringUtils.isBlank(databaseName)) {
continue;
}
schemaName = processor.removeIdentifierQuote(schemaName);
schemaName = normalizeIdentifierPart(processor, schemaName);
if (supportSchema && StringUtils.isBlank(schemaName)) {
continue;
}
String lastIdentifier = splitName[length - 1];
boolean quoted = processor.isQuoteIdentifier(lastIdentifier);
table = processor.removeIdentifierQuote(lastIdentifier);
table = normalizeIdentifierPart(processor, lastIdentifier);
if (!quoted) {
table = processor.convertIdentifierCase(table);
}
Expand Down Expand Up @@ -645,7 +645,7 @@ private List<SqlCompletionCandidate> buildIdentifierResult(CompletionInfo info,
}
String paramDatabaseName = param.getDatabaseName();
String paramSchemaName = param.getSchemaName();
String lastName = processor.removeIdentifierQuote(names[names.length - 1]);
String lastName = normalizeIdentifierPart(processor, names[names.length - 1]);
List<Database> databases = metaData.databases(connection);
if (CollectionUtils.isNotEmpty(databases)) {
for (Database database : databases) {
Expand Down Expand Up @@ -678,7 +678,7 @@ private List<SqlCompletionCandidate> buildIdentifierResult(CompletionInfo info,
}

String databaseName = names.length - 2 < 0 ? paramDatabaseName : names[names.length - 2];
databaseName = processor.removeIdentifierQuote(databaseName);
databaseName = normalizeIdentifierPart(processor, databaseName);
List<Schema> schemas = metaData.schemas(connection, databaseName);
if (CollectionUtils.isNotEmpty(schemas)) {
for (Schema schema : schemas) {
Expand Down Expand Up @@ -713,8 +713,8 @@ private List<SqlCompletionCandidate> buildIdentifierResult(CompletionInfo info,
if (supportSchema && StringUtils.isBlank(schemaName)) {
return List.of();
}
databaseName = processor.removeIdentifierQuote(databaseName);
schemaName = processor.removeIdentifierQuote(schemaName);
databaseName = normalizeIdentifierPart(processor, databaseName);
schemaName = normalizeIdentifierPart(processor, schemaName);
String tableKey = getTableKey(dataSourceId, databaseName, schemaName);
List<Table> tables = MemoryCacheManage.get(tableKey);
if (CollectionUtils.isEmpty(tables)) {
Expand Down Expand Up @@ -791,6 +791,10 @@ private static SqlCompletionCandidate candidate(SqlCompletionCandidateTypeEnum t
return SqlCompletionCandidate.of(type, label);
}

static String normalizeIdentifierPart(ISQLIdentifierProcessor processor, String identifier) {
return identifier == null ? null : processor.removeIdentifierQuote(identifier);
}

private int resolveCursor(DbSqlCompletionGetRequest param) {
if (param.getCursor() != null) {
return Math.max(0, Math.min(param.getCursor(), Objects.toString(param.getSql(), "").length()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ class SqlCompletionMetadataProviderAdapterTest {

private final SqlCompletionConverter converter = new SqlCompletionConverterImpl();

@Test
void legacyIdentifierProcessorUsesCompatibleAlwaysQuoteDefault() {
BacktickIdentifierProcessor processor = new BacktickIdentifierProcessor();

Assertions.assertEquals("`a``b`", processor.quoteIdentifierAlways("a`b"));
Assertions.assertEquals("a`b",
processor.removeIdentifierQuote(processor.quoteIdentifierAlways("a`b")));
}

@Test
void listTablesUsesConverterAndPrefixFilter() {
FakeMetaData metaData = new FakeMetaData();
Expand Down Expand Up @@ -245,12 +254,16 @@ public String quoteIdentifier(String identifier, Integer majorVersion, Integer m

@Override
public String quoteIdentifier(String identifier) {
return identifier == null ? null : "`" + identifier + "`";
return identifier == null ? null : "`" + identifier.replace("`", "``") + "`";
}

@Override
public String removeIdentifierQuote(String identifier) {
return identifier == null ? null : identifier.replace("`", "");
if (identifier == null || identifier.length() < 2
|| !identifier.startsWith("`") || !identifier.endsWith("`")) {
return identifier;
}
return identifier.substring(1, identifier.length() - 1).replace("``", "`");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ai.chat2db.community.domain.core.impl.db;

import ai.chat2db.spi.DefaultSQLIdentifierProcessor;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

class GenericSqlCompletionEngineTest {

private final DefaultSQLIdentifierProcessor processor = new DefaultSQLIdentifierProcessor();

@Test
void normalizeIdentifierPartUnquotesOnlyOuterDelimiters() {
assertEquals("A\"B", GenericSqlCompletionEngine.normalizeIdentifierPart(processor, "\"A\"\"B\""));
assertEquals("A\"B", GenericSqlCompletionEngine.normalizeIdentifierPart(processor, "A\"B"));
assertNull(GenericSqlCompletionEngine.normalizeIdentifierPart(processor, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ai.chat2db.community.domain.api.model.parser.statement.insert.InsertValueMapping;
import ai.chat2db.community.domain.api.enums.parser.InsertValueMappingStatusEnum;
import ai.chat2db.community.domain.api.model.db.SimpleInsertValueMapping;
import ai.chat2db.spi.DefaultSQLIdentifierProcessor;
import org.antlr.v4.runtime.CommonToken;
import org.antlr.v4.runtime.Token;
import org.junit.jupiter.api.Assertions;
Expand All @@ -13,6 +14,18 @@

class SqlParserServiceImplTest {

@Test
void normalizeIdentifierTokenPreservesEmbeddedQuotes() {
DefaultSQLIdentifierProcessor processor = new DefaultSQLIdentifierProcessor();

Assertions.assertEquals("A\"B",
DbSqlParserServiceImpl.normalizeIdentifierToken(processor, "\"A\"\"B\""));
Assertions.assertEquals("A\"B",
DbSqlParserServiceImpl.normalizeIdentifierToken(processor, "A\"B"));
Assertions.assertEquals("Order",
DbSqlParserServiceImpl.normalizeIdentifierToken(processor, ".\"Order\""));
}

@Test
@SuppressWarnings("unchecked")
void getSimpleInsertValueMappingsReturnsRowTokenRange() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,12 +306,26 @@ public String quoteIdentifier(String identifier) {
return "`" + identifier + "`";
}

@Override
public String quoteIdentifierAlways(String identifier) {
if (identifier == null) {
return null;
}
return "`" + identifier.replace("`", "``") + "`";
}

@Override
public String removeIdentifierQuote(String identifier) {
if (StringUtils.isBlank(identifier)) {
return identifier;
}
return removePattern(identifier, MYSQL_PATTERN);
if (identifier.startsWith("`") && identifier.endsWith("`") && identifier.length() >= 2) {
return identifier.substring(1, identifier.length() - 1).replace("``", "`");
}
if (identifier.startsWith("\"") && identifier.endsWith("\"") && identifier.length() >= 2) {
return identifier.substring(1, identifier.length() - 1).replace("\"\"", "\"");
}
return identifier;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package ai.chat2db.plugin.mysql.identifier;

import ai.chat2db.spi.DefaultSQLIdentifierProcessor;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

/**
* Contract tests for {@link MysqlIdentifierProcessor}.
* Covers the backtick-based {@code quoteIdentifierAlways} /
* {@code removeIdentifierQuote} round-trip and embedded-delimiter escaping.
*/
class MysqlIdentifierProcessorTest {

private final MysqlIdentifierProcessor processor = new MysqlIdentifierProcessor();

@Test
void quoteIdentifierAlways_wrapsInBacktick() {
assertEquals("`mycol`", processor.quoteIdentifierAlways("mycol"));
}

@Test
void quoteIdentifierAlways_escapesEmbeddedBacktick() {
assertEquals("`a``b`", processor.quoteIdentifierAlways("a`b"));
}

@Test
void quoteIdentifierAlways_preservesMixedCase() {
assertEquals("`MixedCase`", processor.quoteIdentifierAlways("MixedCase"));
}

@Test
void quoteIdentifierAlways_handlesNull() {
assertNull(processor.quoteIdentifierAlways(null));
}

@Test
void removeIdentifierQuote_stripsBacktick() {
assertEquals("mycol", processor.removeIdentifierQuote("`mycol`"));
}

@Test
void removeIdentifierQuote_unescapesBacktick() {
assertEquals("a`b", processor.removeIdentifierQuote("`a``b`"));
}

@Test
void removeIdentifierQuote_stripsDoubleQuote() {
assertEquals("mycol", processor.removeIdentifierQuote("\"mycol\""));
}

@Test
void roundTrip_plainName() {
String raw = "mycol";
assertEquals(raw, processor.removeIdentifierQuote(processor.quoteIdentifierAlways(raw)));
}

@Test
void roundTrip_embeddedBacktick() {
String raw = "a`b";
assertEquals(raw, processor.removeIdentifierQuote(processor.quoteIdentifierAlways(raw)));
}

@Test
void roundTrip_mixedCase() {
String raw = "MixedCase";
assertEquals(raw, processor.removeIdentifierQuote(processor.quoteIdentifierAlways(raw)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,26 @@ public String quoteIdentifier(String identifier) {
return "[" + identifier + "]";
}

@Override
public String quoteIdentifierAlways(String identifier) {
if (identifier == null) {
return null;
}
return "[" + identifier.replace("]", "]]") + "]";
}

@Override
public String removeIdentifierQuote(String identifier) {
if (StringUtils.isBlank(identifier)) {
return identifier;
}
return removePattern(identifier, SQL_SERVER_PATTERN);
if (identifier.startsWith("[") && identifier.endsWith("]") && identifier.length() >= 2) {
return identifier.substring(1, identifier.length() - 1).replace("]]", "]");
}
if (identifier.startsWith("\"") && identifier.endsWith("\"") && identifier.length() >= 2) {
return identifier.substring(1, identifier.length() - 1).replace("\"\"", "\"");
}
return identifier;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package ai.chat2db.plugin.sqlserver.identifier;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

/**
* Contract tests for {@link SqlServerIdentifierProcessor}.
* Covers the bracket-based {@code quoteIdentifierAlways} /
* {@code removeIdentifierQuote} round-trip and embedded-delimiter escaping.
*/
class SqlServerIdentifierProcessorTest {

private final SqlServerIdentifierProcessor processor = new SqlServerIdentifierProcessor();

@Test
void quoteIdentifierAlways_wrapsInBrackets() {
assertEquals("[mycol]", processor.quoteIdentifierAlways("mycol"));
}

@Test
void quoteIdentifierAlways_preservesMixedCase() {
assertEquals("[MixedCase]", processor.quoteIdentifierAlways("MixedCase"));
}

@Test
void quoteIdentifierAlways_escapesEmbeddedCloseBracket() {
assertEquals("[a]]b]", processor.quoteIdentifierAlways("a]b"));
}

@Test
void quoteIdentifierAlways_handlesNull() {
assertNull(processor.quoteIdentifierAlways(null));
}

@Test
void removeIdentifierQuote_stripsBrackets() {
assertEquals("mycol", processor.removeIdentifierQuote("[mycol]"));
}

@Test
void removeIdentifierQuote_unescapesCloseBracket() {
assertEquals("a]b", processor.removeIdentifierQuote("[a]]b]"));
}

@Test
void removeIdentifierQuote_stripsDoubleQuote() {
assertEquals("mycol", processor.removeIdentifierQuote("\"mycol\""));
}

@Test
void roundTrip_plainName() {
String raw = "mycol";
assertEquals(raw, processor.removeIdentifierQuote(processor.quoteIdentifierAlways(raw)));
}

@Test
void roundTrip_embeddedCloseBracket() {
String raw = "a]b";
assertEquals(raw, processor.removeIdentifierQuote(processor.quoteIdentifierAlways(raw)));
}

@Test
void roundTrip_mixedCase() {
String raw = "MixedCase";
assertEquals(raw, processor.removeIdentifierQuote(processor.quoteIdentifierAlways(raw)));
}
}
Loading
Loading