diff --git a/src/lib/sqlCompletion.ts b/src/lib/sqlCompletion.ts index 444ea9c139..3f99e36da4 100644 --- a/src/lib/sqlCompletion.ts +++ b/src/lib/sqlCompletion.ts @@ -18,7 +18,8 @@ const SQL_KEYWORDS = [ "DESCRIBE", "SHOW", "SUMMARIZE", "PRAGMA", ]; -const TABLE_TRIGGER_KEYWORDS = new Set(["from", "join", "update", "into"]); +const TABLE_TRIGGER_KEYWORDS = new Set(["from", "join", "update", "into", "table", "describe", "explain"]); +const JOIN_MODIFIERS = new Set(["left", "right", "inner", "outer", "cross", "full", "natural"]); export interface SqlCompletionTable { name: string; @@ -83,7 +84,7 @@ export function buildSqlCompletionItemsFromContext( items.push(...buildTableItems(context.prefix, input.tables)); } - if (!context.qualifier && !context.suggestTables) { + if (!context.qualifier) { items.push(...buildKeywordItems(context.prefix)); } @@ -103,10 +104,14 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet const lastWord = /([A-Za-z_][\w$]*)$/.exec(beforeToken)?.[1]?.toLowerCase() ?? ""; const referencedTables = extractReferencedTables(sql); + const afterTableTrigger = TABLE_TRIGGER_KEYWORDS.has(lastWord) + || (JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) + || isInTableListContext(beforeToken); + return { prefix, qualifier, - suggestTables: TABLE_TRIGGER_KEYWORDS.has(lastWord), + suggestTables: afterTableTrigger || (!qualifier && prefix.length > 0), suggestColumns: !!qualifier || referencedTables.length > 0, referencedTables, }; @@ -150,6 +155,16 @@ function buildTableItems(prefix: string, tables: SqlCompletionTable[]): SqlCompl })); } +function isFollowedByJoin(beforeToken: string): boolean { + const words = beforeToken.trimEnd().split(/\s+/); + const second = words[words.length - 2]?.toLowerCase(); + return second === "join" || JOIN_MODIFIERS.has(second ?? ""); +} + +function isInTableListContext(beforeToken: string): boolean { + return /,\s*$/.test(beforeToken) && /\b(?:from|join|update|into)\b/i.test(beforeToken); +} + function buildColumnItems( context: SqlCompletionContext, columnsByTable: Map, diff --git a/tests/sqlCompletion.test.ts b/tests/sqlCompletion.test.ts index 139d6aa1de..f752fb8e9c 100644 --- a/tests/sqlCompletion.test.ts +++ b/tests/sqlCompletion.test.ts @@ -74,3 +74,46 @@ test("suggests columns from referenced tables in select list", () => { assert.equal(items[0]?.label, "name"); assert.equal(items[0]?.type, "column"); }); + +test("suggests tables after LEFT JOIN", () => { + const sql = "select * from users left join us"; + const items = buildSqlCompletionItems(sql, sql.length, { + tables, + columnsByTable, + }); + + assert.ok(items.some((item) => item.label === "users" && item.type === "table")); + assert.ok(items.some((item) => item.label === "user_profiles" && item.type === "table")); +}); + +test("suggests tables after comma in FROM clause", () => { + const sql = "select * from users, or"; + const items = buildSqlCompletionItems(sql, sql.length, { + tables, + columnsByTable, + }); + + assert.ok(items.some((item) => item.label === "orders" && item.type === "table")); +}); + +test("suggests tables as fallback when typing an identifier", () => { + const sql = "us"; + const items = buildSqlCompletionItems(sql, sql.length, { + tables, + columnsByTable, + }); + + assert.ok(items.some((item) => item.label === "users" && item.type === "table")); + assert.ok(items.some((item) => item.type === "keyword")); +}); + +test("always includes keywords alongside table suggestions", () => { + const sql = "select * from us"; + const items = buildSqlCompletionItems(sql, sql.length, { + tables, + columnsByTable, + }); + + assert.ok(items.some((item) => item.type === "table")); + assert.ok(items.some((item) => item.type === "keyword" && item.label === "USING")); +});