fix(editor): skip duplicate table aliases
This commit is contained in:
parent
0650428d1c
commit
02e1b943e8
|
|
@ -1158,6 +1158,7 @@ export interface SqlCompletionContext {
|
|||
deleteTarget?: { table: string; schema?: string };
|
||||
oracleTableFunctionContext?: boolean;
|
||||
autoAliasTableCompletions: boolean;
|
||||
tableAliasAfterCursor?: boolean;
|
||||
contextKind: SqlCompletionContextKind;
|
||||
}
|
||||
|
||||
|
|
@ -1286,7 +1287,7 @@ class SqlCompletionProvider {
|
|||
}
|
||||
|
||||
const emptyTableNameCompletion = !context.prefix && (context.suggestTables || context.exclusiveTableSuggestions);
|
||||
if (!pendingJoinKeyword && !emptyTableNameCompletion && context.referencedTables.length > 0 && !context.suggestColumns && !context.insertTable) {
|
||||
if (!pendingJoinKeyword && !emptyTableNameCompletion && !context.tableAliasAfterCursor && context.referencedTables.length > 0 && !context.suggestColumns && !context.insertTable) {
|
||||
this.items.push(...buildAliasItems(context, this.databaseType));
|
||||
}
|
||||
|
||||
|
|
@ -1617,6 +1618,50 @@ function isCallRoutineContext(beforeToken: string): boolean {
|
|||
return /\bcall\s+(?:[A-Za-z_][\w$]*\.)?$/i.test(beforeToken) || /\bcall\s+(?:[A-Za-z_][\w$]*\.)?[A-Za-z_][\w$]*$/i.test(beforeToken);
|
||||
}
|
||||
|
||||
const SQL_IDENTIFIER_CONTINUE_CHAR = /[$_\u200c\u200d\p{ID_Continue}]/u;
|
||||
|
||||
function hasTableAliasAfterCursor(sql: string, cursor: number): boolean {
|
||||
if (hasAliasMarkerAt(sql, cursor, false)) return true;
|
||||
let pos = cursor;
|
||||
while (pos < sql.length) {
|
||||
const codePoint = sql.codePointAt(pos);
|
||||
if (codePoint === undefined) break;
|
||||
const char = String.fromCodePoint(codePoint);
|
||||
if (char !== "." && !SQL_IDENTIFIER_CONTINUE_CHAR.test(char)) break;
|
||||
// Advance by the full code point so supplementary Unicode identifiers
|
||||
// do not leave the scan between UTF-16 surrogate halves.
|
||||
pos += char.length;
|
||||
}
|
||||
if (sql[pos] === '"' || sql[pos] === "`" || sql[pos] === "]") pos++;
|
||||
return hasAliasMarkerAt(sql, pos, true);
|
||||
}
|
||||
|
||||
function hasAliasMarkerAt(sql: string, pos: number, allowImplicitAlias: boolean): boolean {
|
||||
const following = sql.slice(skipSqlWhitespaceAndComments(sql, pos));
|
||||
if (/^as\b/i.test(following)) return true;
|
||||
if (/^(?:"[^"]+"|`[^`]+`|\[[^\]]+\])/.test(following)) return true;
|
||||
if (!allowImplicitAlias) return false;
|
||||
const implicitAlias = /^([A-Za-z_][\w$]*)/.exec(following)?.[1]?.toLowerCase();
|
||||
return !!implicitAlias && !isUnsafeSqlAlias(implicitAlias);
|
||||
}
|
||||
|
||||
function skipSqlWhitespaceAndComments(sql: string, pos: number): number {
|
||||
for (;;) {
|
||||
while (pos < sql.length && /\s/.test(sql[pos])) pos++;
|
||||
if (sql.startsWith("--", pos)) {
|
||||
const newline = sql.indexOf("\n", pos + 2);
|
||||
if (newline === -1) return sql.length;
|
||||
pos = newline + 1;
|
||||
} else if (sql.startsWith("/*", pos)) {
|
||||
const end = sql.indexOf("*/", pos + 2);
|
||||
if (end === -1) return sql.length;
|
||||
pos = end + 2;
|
||||
} else {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getSqlCompletionContext(sql: string, cursor: number): SqlCompletionContext {
|
||||
// Extract the full statement at cursor position for referenced tables
|
||||
const fullStatement = extractStatementAt(sql, cursor);
|
||||
|
|
@ -1664,7 +1709,8 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
|
|||
|
||||
const afterTableTrigger = TABLE_TRIGGER_KEYWORDS.has(lastWord) || (JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) || isInTableListContext(beforeToken);
|
||||
const exclusiveTableSuggestions = EXCLUSIVE_TABLE_TRIGGER_KEYWORDS.has(lastWord) || (JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) || isInTableListContext(beforeToken);
|
||||
const autoAliasTableCompletions = lastWord === "from" || lastWord === "join" || (JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) || isInTableListContext(beforeToken);
|
||||
const tableAliasAfterCursor = hasTableAliasAfterCursor(sql, cursor);
|
||||
const autoAliasTableCompletions = (lastWord === "from" || lastWord === "join" || (JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) || isInTableListContext(beforeToken)) && !tableAliasAfterCursor;
|
||||
const exclusiveColumnSuggestions = !!qualifier && !exclusiveTableSuggestions && !insertInfo;
|
||||
const activePrefixIsCte = cteDefs.some((cte) => normalizeIdentifierPart(cte.name) === normalizeIdentifierPart(prefix));
|
||||
if (exclusiveTableSuggestions && prefix && !activePrefixIsCte && referencedTables.length > 1) {
|
||||
|
|
@ -1728,6 +1774,7 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
|
|||
deleteTarget: deleteInfo?.target,
|
||||
oracleTableFunctionContext,
|
||||
autoAliasTableCompletions,
|
||||
tableAliasAfterCursor,
|
||||
contextKind,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2009,6 +2009,59 @@ test("automatic table aliases avoid reserved words", () => {
|
|||
assert.equal(tableItem!.apply, "orders AS ord");
|
||||
});
|
||||
|
||||
test("automatic table aliases respect text after the cursor", () => {
|
||||
const cases: Array<[string, number, string]> = [
|
||||
["select * from ord AS o", "select * from ord".length, "orders"],
|
||||
["select * from ord o", "select * from ord".length, "orders"],
|
||||
["select * from ord where id = 1", "select * from ord".length, "orders AS ord"],
|
||||
["select * from ord", "select * from ord".length, "orders AS ord"],
|
||||
["select * from ord, users", "select * from ord".length, "orders AS ord"],
|
||||
["select * from orders AS o", "select * from or".length, "orders"],
|
||||
["select * from ord单 AS o", "select * from ord".length, "orders"],
|
||||
["select * from orde\u0301 AS o", "select * from ord".length, "orders"],
|
||||
["select * from ord𐐀 AS o", "select * from ord".length, "orders"],
|
||||
['select * from ord AS "o"', "select * from ord".length, "orders"],
|
||||
["select * from ord `o`", "select * from ord".length, "orders"],
|
||||
["select * from ord AS ", "select * from ord".length, "orders"],
|
||||
["select * from ord /* comment */ AS o", "select * from ord".length, "orders"],
|
||||
["select * from ord -- comment\n o", "select * from ord".length, "orders"],
|
||||
["select * from ord\n o", "select * from ord".length, "orders"],
|
||||
["select * from ord /* ; */ AS o", "select * from ord".length, "orders"],
|
||||
["select * from ord /* comment */ where id = 1", "select * from ord".length, "orders AS ord"],
|
||||
];
|
||||
|
||||
for (const [sql, cursor, expectedApply] of cases) {
|
||||
const items = buildSqlCompletionItems(sql, cursor, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
autoAliasTables: true,
|
||||
});
|
||||
|
||||
const tableItem = items.find((item) => item.type === "table" && item.label === "orders");
|
||||
assert.ok(tableItem, `should suggest orders for ${sql}`);
|
||||
assert.equal(tableItem!.apply, expectedApply, sql);
|
||||
}
|
||||
});
|
||||
|
||||
test("table alias suggestions respect text after the cursor", () => {
|
||||
const aliasedCases: Array<[string, number]> = [
|
||||
["select * from orders AS o", "select * from orders ".length],
|
||||
['select * from orders AS "o"', "select * from orders ".length],
|
||||
["select * from orders AS ", "select * from orders ".length],
|
||||
["select * from orders /* c */ AS o", "select * from orders ".length],
|
||||
];
|
||||
|
||||
for (const [sql, cursor] of aliasedCases) {
|
||||
const items = buildSqlCompletionItems(sql, cursor, { tables, columnsByTable });
|
||||
const aliasItem = items.find((item) => item.type === "snippet" && item.detail === "alias for orders");
|
||||
assert.equal(aliasItem, undefined, sql);
|
||||
}
|
||||
|
||||
const items = buildSqlCompletionItems("select * from orders ", "select * from orders ".length, { tables, columnsByTable });
|
||||
const aliasItem = items.find((item) => item.type === "snippet" && item.detail === "alias for orders");
|
||||
assert.ok(aliasItem, "alias snippet should remain available when no alias follows");
|
||||
});
|
||||
|
||||
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" }],
|
||||
|
|
|
|||
Loading…
Reference in New Issue