From ce096ca21f189275662ee0eb8a77f261493c792f Mon Sep 17 00:00:00 2001 From: zipg Date: Mon, 6 Jul 2026 18:28:23 +0800 Subject: [PATCH] fix(completion): scoped table lookup, preserve casing, lower Oracle table-function boost * fix: improve schema-qualified SQL completion * fix(completion): preserve casing in completion table cache key and relaxed filter --------- Co-authored-by: zipg <4047349+zipg@users.noreply.github.com> Co-authored-by: t8y2 <1156263951@qq.com> --- .../src/components/editor/QueryEditor.vue | 4 +-- .../sql/sqlCompletion.context.spec.ts | 11 ++++++++ apps/desktop/src/lib/sql/sqlCompletion.ts | 2 +- .../connectionStore.completion.spec.ts | 26 +++++++++++++++++++ apps/desktop/src/stores/connectionStore.ts | 17 +++++++----- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 1f2e8cd081..985bb9926e 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -2024,8 +2024,8 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext } const qualifierDatabase = completionContext.qualifier ? findExactName(databaseNames, completionContext.qualifier) : undefined; const tableLookupDatabase = qualifierDatabase ?? props.database!; - const tableLookupSchema = qualifierDatabase ? undefined : props.schema; - const tableLookupFilter = completionContext.qualifier && !qualifierDatabase ? completionContext.qualifier : completionContext.prefix; + const tableLookupSchema = qualifierDatabase ? undefined : completionContext.qualifier && completionContext.suggestTables ? completionContext.qualifier : props.schema; + const tableLookupFilter = completionContext.qualifier && completionContext.suggestTables ? completionContext.prefix : completionContext.qualifier && !qualifierDatabase ? completionContext.qualifier : completionContext.prefix; let tables = shouldLoadTables ? localOnlyMetadata ? connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookupDatabase, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema) 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 151b621216..b0fa4f26cf 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts @@ -271,4 +271,15 @@ describe("sqlCompletion scoped metadata ranking", () => { expect(items.length).toBeLessThanOrEqual(200); expect(items[0]?.label).toBe("TempTable_000"); }); + + it("ranks real Oracle tables before built-in table functions in FROM contexts", () => { + const sql = "SELECT * FROM "; + const items = buildSqlCompletionItems(sql, sql.length, { + databaseType: "oracle", + tables: [{ name: "ORDERS_10K", schema: "DBX_TEST", type: "table" }], + columnsByTable: new Map(), + }); + + expect(items.findIndex((item) => item.label === "ORDERS_10K")).toBeLessThan(items.findIndex((item) => item.label === "TABLE")); + }); }); diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index ed27b88d1f..e6720ad7ba 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -2729,7 +2729,7 @@ function buildOracleTableFunctionItems(prefix: string): SqlCompletionItem[] { .map((item) => ({ ...item, type: "function" as const, - boost: computeBoost(item.label, prefix) + 2200, + boost: computeBoost(item.label, prefix) + 600, })); } diff --git a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts index ba25cdc579..7555c2fd44 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts @@ -121,6 +121,32 @@ describe("connectionStore completion assistant", () => { expect(dwsTables).toEqual([]); }); + it("preserves table filter casing for assistant searches", async () => { + const completionAssistantSearch = vi.fn().mockResolvedValue({ + candidates: [{ name: "TEST_USERS", kind: "table", schema: "SYSDBA" }], + incomplete: false, + fallback_used: false, + }); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + completionAssistantSearch, + listSchemas: vi.fn().mockResolvedValue(["SYSDBA"]), + listTables: vi.fn().mockResolvedValue([]), + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [postgresConnection()]; + store.connectedIds.add("pg-1"); + + const tables = await store.listCompletionTables("pg-1", "app", "TEST_", 20, "SYSDBA"); + + expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ mask: "TEST_", schema: "SYSDBA", parent_schema: "SYSDBA" })); + expect(tables).toEqual([{ name: "TEST_USERS", schema: "SYSDBA", type: "table" }]); + }); + it("limits concurrent completion column metadata requests per connection database", async () => { const gates = [deferred(), deferred(), deferred(), deferred()]; let activeColumns = 0; diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 4345ebbb6b..953bec8645 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -3916,9 +3916,14 @@ export const useConnectionStore = defineStore("connection", () => { } async function listCompletionTables(connectionId: string, database: string, filter = "", limit?: number, schema?: string): Promise { - const normalizedFilter = filter.trim().toLowerCase(); - const relaxedFilter = relaxedCompletionTableFilter(normalizedFilter); - const cacheKey = `${connectionId}:${database}:${normalizedFilter}:${limit ?? ""}:${schema ?? ""}`; + const trimmedFilter = filter.trim(); + const normalizedFilter = trimmedFilter.toLowerCase(); + // Remote queries (Dameng/Oracle) are case-sensitive, so the cache key must + // preserve original casing — otherwise "TEST" and "test" collide and the + // second lookup returns the first's stale results. Local lookups below stay + // case-insensitive because tableMatchScore normalizes internally. + const relaxedFilter = relaxedCompletionTableFilter(trimmedFilter); + const cacheKey = `${connectionId}:${database}:${trimmedFilter}:${limit ?? ""}:${schema ?? ""}`; if (completionTablesCache.value[cacheKey]) { return completionTablesCache.value[cacheKey]; } @@ -3932,10 +3937,10 @@ export const useConnectionStore = defineStore("connection", () => { if (normalizedFilter || limit) { let results: SqlCompletionTable[] = []; try { - results = await listCompletionAssistantTables(connectionId, database, normalizedFilter, limit, schema); + results = await listCompletionAssistantTables(connectionId, database, trimmedFilter, limit, schema); } catch { if (schema) { - const tables = await api.listTables(connectionId, database, schema, normalizedFilter, limit); + const tables = await api.listTables(connectionId, database, schema, trimmedFilter, limit); results = tables.map((table) => ({ name: table.name, schema, @@ -3983,7 +3988,7 @@ export const useConnectionStore = defineStore("connection", () => { return completionTablesCache.value[cacheKey]; } - let tables = await api.listTables(connectionId, database, database, normalizedFilter, limit); + let tables = await api.listTables(connectionId, database, database, trimmedFilter, limit); if (tables.length === 0 && relaxedFilter) { tables = await api.listTables(connectionId, database, database, relaxedFilter, expandedCompletionLimit(limit)); }