fix(completion): quote SQL Server identifiers

This commit is contained in:
Eve
2026-09-09 13:58:15 +08:00
committed by GitHub
parent b2784def56
commit cd43ff07f0
2 changed files with 77 additions and 4 deletions
+20 -4
View File
@@ -3498,7 +3498,15 @@ function splitQualifiedNameRawParts(input: string): string[] {
continue;
}
if (ch === "[" && !inDoubleQuote && !inBacktick) inBracket = true;
if (ch === "]" && inBracket) inBracket = false;
if (ch === "]" && inBracket) {
current += ch;
if (input[i + 1] === "]") {
current += input[++i];
} else {
inBracket = false;
}
continue;
}
if (ch === "." && !inDoubleQuote && !inBacktick && !inBracket) {
parts.push(current.trim());
current = "";
@@ -3537,6 +3545,8 @@ export function quoteSqlIdentifier(identifier: string, dialect?: SqlCompletionAp
}
const POSTGRES_IDENTIFIER_KEYWORDS = new Set(SQL_KEYWORDS.map((keyword) => keyword.toLowerCase()));
const SQLSERVER_IDENTIFIER_KEYWORDS = new Set(sqlDialectCompletionWords(MSSQL.spec.keywords).map((keyword) => keyword.toLowerCase()));
const SQLSERVER_REGULAR_IDENTIFIER = /^[\p{L}_][\p{L}\p{Nd}_@$#]*$/u;
// Unlike quoteSqlIdentifier (also used by the data grid condition editor, which
// intentionally leaves MySQL identifiers unquoted and applies backticks itself
@@ -3547,14 +3557,19 @@ function quoteCompletionApplyIdentifier(identifier: string, dialect?: SqlComplet
if (!requiresMysqlIdentifierQuote(identifier, POSTGRES_IDENTIFIER_KEYWORDS)) return identifier;
return `\`${identifier.replaceAll("`", "``")}\``;
}
if (dialect === "sqlserver") {
if (SQLSERVER_REGULAR_IDENTIFIER.test(identifier) && !requiresMysqlIdentifierQuote(identifier.toLowerCase(), SQLSERVER_IDENTIFIER_KEYWORDS)) return identifier;
const escaped = identifier.replaceAll("]", "]]");
return `[${escaped}]`;
}
return quoteSqlIdentifier(identifier, dialect);
}
function quoteCompletionApplyName(applyName: string, dialect?: SqlCompletionApplyDialect): string {
if (dialect !== "mysql" && dialect !== "oracle") return applyName;
if (dialect !== "mysql" && dialect !== "oracle" && dialect !== "sqlserver") return applyName;
const parts = splitQualifiedNameRawParts(applyName);
if (parts.length === 0) return applyName;
return parts.map((part) => (isQuotedIdentifier(part) ? part : quoteCompletionApplyIdentifier(part, dialect))).join(".");
return parts.map((part) => ((dialect === "sqlserver" && !part) || isQuotedIdentifier(part) ? part : quoteCompletionApplyIdentifier(part, dialect))).join(".");
}
function quoteCompletionRoutineIdentifier(identifier: string, dialect?: SqlCompletionApplyDialect): string {
@@ -3640,7 +3655,8 @@ function buildTableItems(
.map((table) => {
const qualifiedByContext = !!qualifierSchema && !!table.schema && normalizeIdentifierPart(qualifierSchema) === normalizeIdentifierPart(table.schema);
const { ambiguousTableName, defaultApplyName } = resolveTableSchemaQualification(table, dialect, databaseType, currentSchema, schemasByTableName);
const suppliedApplyName = table.applyName?.trim();
const rawSuppliedApplyName = table.applyName?.trim();
const suppliedApplyName = dialect === "sqlserver" && rawSuppliedApplyName ? quoteCompletionApplyName(rawSuppliedApplyName, dialect) : rawSuppliedApplyName;
const suppliedApplyNameIsQualified = suppliedApplyName?.includes(".") === true;
const applyName = qualifiedByContext ? quoteCompletionApplyIdentifier(table.name, dialect) : ambiguousTableName && !!table.schema && (!suppliedApplyName || !suppliedApplyNameIsQualified) ? defaultApplyName : (suppliedApplyName ?? defaultApplyName);
const alias = autoAliasTables ? generateTableCompletionAlias(table.name, existingAliases) : "";
+57
View File
@@ -995,6 +995,63 @@ test("replaces typed Unicode prefixes through semantic SQL Server completion", (
}
});
test("brackets only SQL Server completion identifiers that require delimiters", () => {
const sql = "select * from ";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [
{ name: "Orders", schema: "dbo", type: "table" },
{ name: "名称", schema: "dbo", type: "table" },
{ name: "04保险事前", schema: "dbo", type: "table" },
{ name: "含]括号", schema: "dbo", type: "table" },
{ name: "BACKUP", schema: "dbo", type: "table" },
],
columnsByTable: new Map(),
databaseType: "sqlserver",
dialect: "sqlserver",
});
assert.deepEqual(Object.fromEntries(items.filter((item) => item.type === "table").map((item) => [item.label, item.apply])), {
Orders: "Orders",
名称: "名称",
"04保险事前": "[04保险事前]",
"含]括号": "[含]]括号]",
BACKUP: "[BACKUP]",
});
});
test("quotes qualified SQL Server table apply names", () => {
const sql = "select * from ";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [
{ name: "04保险事前", schema: "dbo", type: "table", applyName: "dbo.04保险事前" },
{ name: "04归档", schema: "dbo", type: "table", applyName: "dbo.[04归档]" },
{ name: "04省略模式", schema: "dbo", type: "table", applyName: "datacenter..04省略模式" },
{ name: "含].括号", schema: "dbo", type: "table", applyName: "dbo.[含]].括号]" },
],
columnsByTable: new Map(),
databaseType: "sqlserver",
dialect: "sqlserver",
});
assert.equal(items.find((item) => item.label === "04保险事前")?.apply, "dbo.[04保险事前]");
assert.equal(items.find((item) => item.label === "04归档")?.apply, "dbo.[04归档]");
assert.equal(items.find((item) => item.label === "04省略模式")?.apply, "datacenter..[04省略模式]");
assert.equal(items.find((item) => item.label === "含].括号")?.apply, "dbo.[含]].括号]");
});
test("quotes qualified SQL Server routine apply names", () => {
const sql = "select run";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [{ name: "run_report", schema: "dbo", type: "procedure", applyName: "dbo.04备份" }],
columnsByTable: new Map(),
databaseType: "sqlserver",
dialect: "sqlserver",
});
assert.equal(items.find((item) => item.label === "run_report")?.apply, "dbo.[04备份]()");
});
test("replaces a Unicode prefix inside an open SQL Server bracket identifier", () => {
const sql = "select * from test where [名";
const cursor = sql.length;