diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 1abead9d55..b3da3757b3 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -1356,7 +1356,8 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E scheduleCompletionMetadataRefresh(completionContext); if (!explicit) return localResult; } - if (!explicit) { + const shouldResolveAsyncColumnCompletion = completionContext.suggestColumns && completionContext.referencedTables.length > 0 && completionContext.prefix.length > 0; + if (!explicit && !shouldResolveAsyncColumnCompletion) { scheduleCompletionMetadataRefresh(completionContext); return null; } diff --git a/apps/desktop/src/lib/__tests__/sqlCompletion.context.spec.ts b/apps/desktop/src/lib/__tests__/sqlCompletion.context.spec.ts index f67a121687..6c7a57a7ad 100644 --- a/apps/desktop/src/lib/__tests__/sqlCompletion.context.spec.ts +++ b/apps/desktop/src/lib/__tests__/sqlCompletion.context.spec.ts @@ -134,6 +134,17 @@ describe("sqlCompletion scoped context classification", () => { expect(context.suggestColumns).toBe(true); }); + it("classifies unqualified WHERE field input as column context", () => { + const sql = "SELECT * FROM A1User WHERE userc"; + const context = getSqlCompletionContext(sql, sql.length); + + expect(context.contextKind).toBe("column"); + expect(context.prefix).toBe("userc"); + expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "A1User" })])); + expect(context.suggestColumns).toBe(true); + expect(context.suggestRoutines).toBe(false); + }); + it("classifies CALL routine contexts", () => { const sql = "CALL usp_"; const context = getSqlCompletionContext(sql, sql.length); diff --git a/apps/desktop/src/lib/sqlCompletion.ts b/apps/desktop/src/lib/sqlCompletion.ts index e0fedeea3e..b2873b9d89 100644 --- a/apps/desktop/src/lib/sqlCompletion.ts +++ b/apps/desktop/src/lib/sqlCompletion.ts @@ -1174,7 +1174,8 @@ class SqlCompletionProvider { return dedupeAndSort(buildMongoCompletionItemsFromContext({ mode: "root", prefix: context.prefix, from: 0 }).map(mongoCompletionItemToSqlCompletionItem)); } - if (!context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions && !context.exclusiveRoutineSuggestions) { + const preferReferencedColumns = hasMatchingReferencedColumnPrefix(context, this.input.columnsByTable); + if (!preferReferencedColumns && !context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions && !context.exclusiveRoutineSuggestions) { const snippets = this.databaseType === "manticoresearch" ? [...(this.input.snippets ?? DEFAULT_SQL_SNIPPETS), ...MANTICORESEARCH_SQL_SNIPPETS] : (this.input.snippets ?? DEFAULT_SQL_SNIPPETS); this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase)); this.items.push(...buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType)); @@ -2786,6 +2787,8 @@ function buildColumnItems(context: SqlCompletionContext, columnsByTable: Map referencedTableMatchesColumnQualifier(table, q, qLower, qualifiedTarget)); relevantCols = allColumns.filter((column) => relatedTables.some((table) => columnMatchesReferencedTable(column, table)) || (!!qualifiedTarget && columnMatchesQualifiedTable(column, qualifiedTarget))); + } else if (context.referencedTables.length > 0) { + relevantCols = allColumns.filter((column) => context.referencedTables.some((table) => columnMatchesReferencedTable(column, table))); } // Count name frequencies to detect duplicates across tables @@ -2833,6 +2836,21 @@ function buildColumnItems(context: SqlCompletionContext, columnsByTable: Map): boolean { + if (!context.suggestColumns || !context.prefix || context.referencedTables.length === 0) return false; + + for (const [key, cols] of columnsByTable.entries()) { + for (const column of cols) { + if (!matchesPrefix(column.name, context.prefix)) continue; + if (context.referencedTables.some((table) => columnMatchesReferencedTable({ ...column, key }, table))) { + return true; + } + } + } + + return false; +} + function qualifiedTableTargetFromContext(context: SqlCompletionContext): { schema: string; table: string } | null { const parts = context.qualifierParts ?? context.qualifier?.split(".").filter(Boolean) ?? []; if (parts.length < 2) return null; diff --git a/packages/app-tests/sqlCompletion.test.ts b/packages/app-tests/sqlCompletion.test.ts index 24aa6b68f4..2d3b8b16ab 100644 --- a/packages/app-tests/sqlCompletion.test.ts +++ b/packages/app-tests/sqlCompletion.test.ts @@ -836,6 +836,65 @@ test("suggests stored procedures after CALL", () => { ); }); +test("prioritizes referenced table columns in WHERE field input", () => { + const sql = "select * from A1User WHERE userc"; + const items = buildSqlCompletionItems(sql, sql.length, { + tables: [{ name: "A1User", schema: "dbo", type: "table" }], + objects: [ + { name: "P1UserCodeGenerate", schema: "dbo", type: "procedure" }, + { name: "F22UserAccUnit", schema: "dbo", type: "function" }, + ], + columnsByTable: new Map([ + [ + "dbo.A1User", + [ + { name: "UserCode", table: "A1User", schema: "dbo", dataType: "varchar" }, + { name: "UserName", table: "A1User", schema: "dbo", dataType: "varchar" }, + ], + ], + [ + "dbo.OtherUserTable", + [ + { name: "UserCheck", table: "OtherUserTable", schema: "dbo", dataType: "varchar" }, + ], + ], + ]), + databaseType: "sqlserver", + }); + + assert.deepEqual( + items.map((item) => [item.label, item.type]), + [["UserCode", "column"]], + ); +}); + +test("keeps snippets below matching WHERE field columns", () => { + const sql = "select * from demo_2000_tables.t_0001 WHERE i"; + const items = buildSqlCompletionItems(sql, sql.length, { + tables: [{ name: "t_0001", schema: "demo_2000_tables", type: "table" }], + columnsByTable: new Map([ + [ + "demo_2000_tables.t_0001", + [ + { name: "id", table: "t_0001", schema: "demo_2000_tables", dataType: "int", comment: "注释test" }, + { name: "image_url", table: "t_0001", schema: "demo_2000_tables", dataType: "varchar(512)", comment: "xixixi" }, + { name: "image_mime", table: "t_0001", schema: "demo_2000_tables", dataType: "varchar(64)", comment: "hahaha" }, + ], + ], + ]), + }); + + assert.deepEqual( + items.slice(0, 3).map((item) => [item.label, item.type]), + [ + ["id", "column"], + ["image_url", "column"], + ["image_mime", "column"], + ], + ); + assert.equal(items.some((item) => item.type === "snippet" && item.label === "insert into"), false); +}); + test("suggests user functions and triggers with fuzzy matching", () => { const sql = "select fun"; const items = buildSqlCompletionItems(sql, sql.length, {