feat(sqlCompletion): 优化表别名建议功能,避免使用 SQL 关键字 (#2805)

This commit is contained in:
二丫讲梵 2026-07-08 00:01:24 +08:00 committed by GitHub
parent 8364f468a0
commit 7eff6e544b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 2 deletions

View File

@ -1,3 +1,4 @@
import { Cassandra, MariaSQL, MSSQL, MySQL, PLSQL, PostgreSQL, SQLite, StandardSQL } from "@codemirror/lang-sql";
import type { DatabaseType, SqlSnippet } from "@/types/database";
import { buildMongoCompletionItemsFromContext, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
@ -1064,6 +1065,15 @@ const SQL_ALIAS_RESERVED_WORDS = new Set([
"with",
]);
const SQL_ALIAS_KEYWORD_WORDS = new Set(sqlAliasKeywordWords(SQL_KEYWORDS.join(" "), StandardSQL.spec.keywords, MySQL.spec.keywords, MariaSQL.spec.keywords, PostgreSQL.spec.keywords, MSSQL.spec.keywords, SQLite.spec.keywords, PLSQL.spec.keywords, Cassandra.spec.keywords));
function sqlAliasKeywordWords(...sources: Array<string | undefined>): string[] {
return sources
.flatMap((source) => (source ?? "").split(/\s+/))
.filter((keyword) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(keyword))
.map((keyword) => keyword.toLowerCase());
}
export interface SqlCompletionTable {
name: string;
schema?: string;
@ -3045,7 +3055,7 @@ function generateTableCompletionAlias(tableName: string, existing = new Set<stri
const candidates = buildAliasCandidates(tableName);
for (const candidate of candidates.filter(Boolean)) {
if (SQL_ALIAS_RESERVED_WORDS.has(candidate.toLowerCase())) continue;
if (isUnsafeSqlAlias(candidate.toLowerCase())) continue;
if (!existing.has(candidate.toLowerCase())) return candidate;
for (let index = 2; index < 100; index++) {
const numbered = `${candidate}${index}`;
@ -3081,7 +3091,11 @@ function buildAliasCandidates(tableName: string): string[] {
function aliasConflicts(candidate: string, existing: Set<string>): boolean {
const lower = candidate.toLowerCase();
return existing.has(lower) || SQL_ALIAS_RESERVED_WORDS.has(lower);
return existing.has(lower) || isUnsafeSqlAlias(lower);
}
function isUnsafeSqlAlias(candidate: string): boolean {
return SQL_ALIAS_RESERVED_WORDS.has(candidate) || SQL_ALIAS_KEYWORD_WORDS.has(candidate);
}
function isFollowedByJoin(beforeToken: string): boolean {

View File

@ -1832,6 +1832,50 @@ test("automatic table aliases avoid reserved words", () => {
assert.equal(tableItem!.apply, "orders AS ord");
});
test("table alias suggestions avoid SQL keywords", () => {
const items = buildSqlCompletionItems("select * from item_file ", "select * from item_file ".length, {
tables: [{ name: "item_file", schema: "public", type: "table" }],
columnsByTable,
});
const aliasItem = items.find((item) => item.type === "snippet" && item.detail === "alias for item_file");
assert.ok(aliasItem);
assert.notEqual(aliasItem!.apply, "AS if ");
assert.equal(aliasItem!.apply, "AS it ");
});
test("automatic table aliases avoid SQL keywords", () => {
const cases: Array<[string, string]> = [
["account_store", "account_store AS ac"],
["account_type", "account_type AS ac"],
["data_order", "data_order AS da"],
["invoice_note", "invoice_note AS inv"],
["item_file", "item_file AS it"],
["item_status", "item_status AS it"],
["new_order", "new_order AS ne"],
["no_config", "no_config AS nc"],
["order_node", "order_node AS ord"],
["order_flow", "order_flow AS ord"],
["order_region", "order_region AS ord"],
["row_value", "row_value AS rv"],
["use_case", "use_case AS uc"],
["user_role", "user_role AS ur"],
];
for (const [tableName, expectedApply] of cases) {
const sql = `select * from ${tableName.slice(0, 3)}`;
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [{ name: tableName, schema: "public", type: "table" }],
columnsByTable,
autoAliasTables: true,
});
const tableItem = items.find((item) => item.type === "table" && item.label === tableName);
assert.ok(tableItem, `should suggest ${tableName}`);
assert.equal(tableItem!.apply, expectedApply);
}
});
test("table alias suggestions avoid existing aliases", () => {
const items = buildSqlCompletionItems("select * from customer_orders co join customer_orders ", "select * from customer_orders co join customer_orders ".length, {
tables: [...tables, { name: "customer_orders", schema: "public", type: "table" }],