From 41e97baa6e26a8c4068d313780ff023a20ac11c2 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 1 Jun 2026 19:31:49 +0800 Subject: [PATCH] fix(desktop): scope SQL alias completions to columns --- .../src/components/editor/QueryEditor.vue | 12 ++- apps/desktop/src/lib/sqlCompletion.ts | 6 ++ packages/app-tests/sqlCompletion.test.ts | 73 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 80e11fac2d..109980f0be 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -917,7 +917,9 @@ async function performAsyncCompletionWithResult( } } - const shouldLoadTables = completionContext.suggestTables || !!completionContext.qualifier; + const shouldLoadTables = + completionContext.suggestTables || + (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)); let tables = shouldLoadTables ? await connectionStore.listCompletionTables( props.connectionId!, @@ -1075,6 +1077,14 @@ async function performAsyncCompletionWithResult( return buildCompletionResult(items, position, completionContext.prefix.length, fullDoc); } +function isReferencedTableQualifier(completionContext: ReturnType): boolean { + if (!completionContext.qualifier) return false; + const qualifier = completionContext.qualifier.toLowerCase(); + return completionContext.referencedTables.some( + (table) => table.alias?.toLowerCase() === qualifier || table.name.toLowerCase() === qualifier, + ); +} + async function refreshCompletionCache() { cachedTables = []; cachedColumnsByTable.clear(); diff --git a/apps/desktop/src/lib/sqlCompletion.ts b/apps/desktop/src/lib/sqlCompletion.ts index b4626a4c28..d0574c0254 100644 --- a/apps/desktop/src/lib/sqlCompletion.ts +++ b/apps/desktop/src/lib/sqlCompletion.ts @@ -1926,6 +1926,12 @@ function buildJoinConditionItemsForPair( ) { boost = 1800; } + // Pattern 7: Generic FK column → id when table names do not reveal the relationship. + else if (leftName !== "id" && leftName.endsWith("_id") && rightName === "id") { + boost = 1650; + } else if (rightName !== "id" && rightName.endsWith("_id") && leftName === "id") { + boost = 1650; + } if (!boost) continue; const label = `${leftLabel} = ${rightLabel}`; diff --git a/packages/app-tests/sqlCompletion.test.ts b/packages/app-tests/sqlCompletion.test.ts index 10706599d3..52a656d5b3 100644 --- a/packages/app-tests/sqlCompletion.test.ts +++ b/packages/app-tests/sqlCompletion.test.ts @@ -203,6 +203,48 @@ test("keeps explicit alias column suggestions scoped to the alias table", () => ); }); +test("suggests only fields after numbered table aliases in join conditions", () => { + const sql = "select * from public.users t1 join public.orders t2 on t1."; + const items = buildSqlCompletionItems(sql, sql.length, { + tables, + columnsByTable, + }); + + assert.deepEqual( + items.map((item) => [item.label, item.type]), + [ + ["id", "column"], + ["name", "column"], + ["email", "column"], + ], + ); + assert.equal( + items.some((item) => item.type === "table"), + false, + ); +}); + +test("scopes numbered alias field suggestions to the requested joined table", () => { + const sql = "select * from public.users t1 join public.orders t2 on t2."; + const items = buildSqlCompletionItems(sql, sql.length, { + tables, + columnsByTable, + }); + + assert.deepEqual( + items.map((item) => [item.label, item.type]), + [ + ["id", "column"], + ["user_id", "column"], + ["status", "column"], + ], + ); + assert.equal( + items.some((item) => item.type === "table"), + false, + ); +}); + test("suggests columns from referenced tables in select list", () => { const sql = "select na from public.users u join public.orders o on u.id = o.user_id"; const cursor = "select na".length; @@ -996,6 +1038,37 @@ test("suggests join condition for created_by → id pattern", () => { assert.ok(creatorJoin, "should suggest id = created_by join"); }); +test("suggests generic foreign-key to id join when table names differ from column names", () => { + const colsWithGenericFk = new Map([ + [ + "public.first_table", + [ + { name: "id", table: "first_table", schema: "public", dataType: "bigint" }, + { name: "user_id", table: "first_table", schema: "public", dataType: "bigint" }, + ], + ], + [ + "public.second_table", + [ + { name: "id", table: "second_table", schema: "public", dataType: "bigint" }, + { name: "name", table: "second_table", schema: "public", dataType: "varchar" }, + ], + ], + ]); + const sql = "select * from public.first_table t1 join public.second_table t2 on "; + const items = buildSqlCompletionItems(sql, sql.length, { + tables: [ + { name: "first_table", schema: "public", type: "table" }, + { name: "second_table", schema: "public", type: "table" }, + ], + columnsByTable: colsWithGenericFk, + }); + + const genericJoin = items.find((item) => item.label === "t1.user_id = t2.id"); + assert.ok(genericJoin, "should suggest generic *_id = id joins"); + assert.equal(genericJoin.apply, "t1.user_id = t2.id"); +}); + // --- Fuzzy matching --- test("fuzzy matches table names with character gaps", () => {