fix(sql): keep column completion after subqueries

This commit is contained in:
t8y2 2026-07-08 00:53:05 +08:00
parent b3f09219ab
commit 611f19fd8b
2 changed files with 79 additions and 2 deletions

View File

@ -161,6 +161,64 @@ describe("sqlCompletion scoped context classification", () => {
expect(context.suggestColumns).toBe(true);
});
it("keeps alias-qualified column context after select-list subqueries", () => {
const sql = `
SELECT
p.id,
p.create_user_name 'creator',
(SELECT t.\`code\` FROM sys_user t WHERE t.user_id = p.apply_user_id) 'creator_code',
p.
FROM sys_process p
LIMIT 10
`;
const cursor = sql.indexOf("p.\n FROM");
const context = getSqlCompletionContext(sql, cursor + 2);
expect(context.contextKind).toBe("alias_column");
expect(context.qualifier).toBe("p");
expect(context.suggestTables).toBe(false);
expect(context.exclusiveTableSuggestions).toBe(false);
expect(context.suggestColumns).toBe(true);
});
it("suggests alias columns after select-list subqueries instead of tables", () => {
const sql = `
SELECT
p.id,
p.create_user_name 'creator',
(SELECT t.\`code\` FROM sys_user t WHERE t.user_id = p.apply_user_id) 'creator_code',
p.
FROM sys_process p
LIMIT 10
`;
const cursor = sql.indexOf("p.\n FROM") + 2;
const items = buildSqlCompletionItems(sql, cursor, {
dialect: "mysql",
tables: [
{ name: "act_evt_log", type: "table" },
{ name: "sys_process", type: "table" },
{ name: "sys_user", type: "table" },
],
columnsByTable: new Map([
[
"sys_process",
[
{ name: "id", table: "sys_process" },
{ name: "create_user_name", table: "sys_process" },
{ name: "apply_user_id", table: "sys_process" },
],
],
["sys_user", [{ name: "code", table: "sys_user" }]],
]),
});
const columnLabels = items.filter((item) => item.type === "column").map((item) => item.label);
expect(columnLabels).toEqual(expect.arrayContaining(["id", "create_user_name", "apply_user_id"]));
expect(items[0]?.type).toBe("column");
expect(items.some((item) => item.type === "table")).toBe(false);
expect(items.some((item) => item.type === "keyword")).toBe(false);
});
it("classifies unqualified WHERE field input as column context", () => {
const sql = "SELECT * FROM A1User WHERE userc";
const context = getSqlCompletionContext(sql, sql.length);

View File

@ -1670,7 +1670,7 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
const suggestRoutines = inCallRoutineContext || oracleTableFunctionContext || inPotentialPackageMemberContext || (!preferColumnsOverGlobalRoutines && !exclusiveTableSuggestions && !exclusiveColumnSuggestions && !insertInfo && prefix.length >= 2);
const statementKind = detectStatementKind(beforeCursor || fullStatement);
const preferredKeywords = preferredKeywordsForCompletion(beforeCursor, beforeToken, selectListColumnContext, exclusiveTableSuggestions, updateInfo, deleteInfo);
const preferredKeywords = qualifier ? [] : preferredKeywordsForCompletion(beforeCursor, beforeToken, selectListColumnContext, exclusiveTableSuggestions, updateInfo, deleteInfo);
const contextKind = detectCompletionContextKind({
qualifier,
exclusiveTableSuggestions,
@ -3106,7 +3106,26 @@ function isFollowedByJoin(beforeToken: string): boolean {
function isInTableListContext(beforeToken: string): boolean {
if (isInOrderOrGroupByContext(beforeToken)) return false;
return /,\s*$/.test(beforeToken) && /\b(?:from|join|update|into)\b/i.test(beforeToken);
const cleaned = stripSqlLiterals(beforeToken).trimEnd();
if (!/,\s*$/.test(cleaned)) return false;
// Only commas in the active top-level table segment should continue table completion.
const lastTableIntro = Math.max(lastTopLevelKeywordIndex(cleaned, "from"), lastTopLevelKeywordIndex(cleaned, "join"), lastTopLevelKeywordIndex(cleaned, "update"), lastTopLevelKeywordIndex(cleaned, "into"));
if (lastTableIntro < 0) return false;
const lastBoundary = Math.max(
lastTopLevelKeywordIndex(cleaned, "where"),
lastTopLevelKeywordIndex(cleaned, "set"),
lastTopLevelKeywordIndex(cleaned, "group"),
lastTopLevelKeywordIndex(cleaned, "order"),
lastTopLevelKeywordIndex(cleaned, "having"),
lastTopLevelKeywordIndex(cleaned, "limit"),
lastTopLevelKeywordIndex(cleaned, "offset"),
lastTopLevelKeywordIndex(cleaned, "union"),
lastTopLevelKeywordIndex(cleaned, "intersect"),
lastTopLevelKeywordIndex(cleaned, "except"),
);
return lastBoundary < lastTableIntro;
}
function collectCompletionColumns(columnsByTable: Map<string, SqlCompletionColumn[]>): Array<SqlCompletionColumn & { key: string }> {