From 611f19fd8b82ba30dd10d7549fe2a71b924fdca0 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 8 Jul 2026 00:53:05 +0800 Subject: [PATCH] fix(sql): keep column completion after subqueries --- .../sql/sqlCompletion.context.spec.ts | 58 +++++++++++++++++++ apps/desktop/src/lib/sql/sqlCompletion.ts | 23 +++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts index ca6e2140f..dd414139c 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts @@ -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); diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index fe956e7ae..8266ad598 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -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): Array {