From 37dc74d9deca47e2631bea66ef5180af188d3ceb Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 4 Sep 2026 00:40:51 +0800 Subject: [PATCH] fix(oracle): refresh table hover metadata from local index Closes #8056 --- .../src/components/editor/QueryEditor.vue | 15 ++-- .../editor/queryEditorHoverDdlCopy.spec.ts | 15 ++++ .../connectionStore.completion.spec.ts | 81 ++++++++++++++++++- apps/desktop/src/stores/connectionStore.ts | 8 +- 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index ed96bbb673..46a6f24512 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -2896,15 +2896,14 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number) try { let hoverTables = cachedTables.filter((table) => hoverTableMatchesScope(table, hoverScope)); - if (hoverTables.length === 0) { - const loadedTables = usesLocalOnlyCompletionMetadata() - ? connectionStore.lookupLocalCompletionTables(props.connectionId, hoverScope.database, tableLookupName, MAX_COMPLETION_TABLES, hoverScope.schema, hoverScope.catalog) - : await connectionStore.listCompletionTables(props.connectionId, hoverScope.database, tableLookupName, MAX_COMPLETION_TABLES, hoverScope.schema, false, hoverScope.schema, hoverScope.catalog); - hoverTables = scopeHoverTables(loadedTables, hoverScope); - cachedTables = mergeCompletionTables(cachedTables, hoverTables); - } - let table = matchTable(qualifiedTableLookup, hoverTables) ?? matchTable(tableLookupName, hoverTables) ?? matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables); + if (!table) { + const localTables = connectionStore.lookupLocalCompletionTables(props.connectionId, hoverScope.database, tableLookupName, MAX_COMPLETION_TABLES, hoverScope.schema, hoverScope.catalog); + const localHoverTables = scopeHoverTables(localTables, hoverScope); + hoverTables = mergeCompletionTables(localHoverTables, hoverTables); + cachedTables = mergeCompletionTables(localHoverTables, cachedTables); + table = matchTable(qualifiedTableLookup, hoverTables) ?? matchTable(tableLookupName, hoverTables) ?? matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables); + } if (!table && !usesLocalOnlyCompletionMetadata()) { const loadedTables = await connectionStore.listCompletionTables(props.connectionId, hoverScope.database, tableLookupName, MAX_COMPLETION_TABLES, hoverScope.schema, false, hoverScope.schema, hoverScope.catalog); const remoteHoverTables = scopeHoverTables(loadedTables, hoverScope); diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorHoverDdlCopy.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorHoverDdlCopy.spec.ts index 5e3e038fac..cf2490dd4a 100644 --- a/apps/desktop/src/lib/__tests__/editor/queryEditorHoverDdlCopy.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorHoverDdlCopy.spec.ts @@ -4,6 +4,21 @@ import { describe, expect, it } from "vitest"; const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8"); describe("QueryEditor hover DDL copy", () => { + it("reconciles the local sidebar index before remote hover fallback", () => { + const hoverStart = queryEditorSource.indexOf("async function resolveSqlHoverTooltip"); + const hoverEnd = queryEditorSource.indexOf("\n}\n\nfunction sqlErrorDecorationRange", hoverStart); + const hoverSource = queryEditorSource.slice(hoverStart, hoverEnd); + const localLookup = hoverSource.indexOf("connectionStore.lookupLocalCompletionTables"); + const remoteLookup = hoverSource.indexOf("connectionStore.listCompletionTables"); + + expect(hoverStart).toBeGreaterThanOrEqual(0); + expect(hoverEnd).toBeGreaterThan(hoverStart); + expect(localLookup).toBeGreaterThanOrEqual(0); + expect(remoteLookup).toBeGreaterThan(localLookup); + expect(hoverSource).toContain("hoverTables = mergeCompletionTables(localHoverTables, hoverTables);"); + expect(hoverSource).toContain("cachedTables = mergeCompletionTables(localHoverTables, cachedTables);"); + }); + it("gates only the table DDL branch on the preference, keeping column hover", () => { expect(queryEditorSource).not.toContain("if (!settingsStore.editorSettings.showTableDdlHoverPreview) return null;"); expect(queryEditorSource).toContain("if (table && settingsStore.editorSettings.showTableDdlHoverPreview && !semanticQualifierIsRowSource"); diff --git a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts index 4d93200934..d8c3aebf42 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts @@ -1,7 +1,7 @@ import { createPinia, setActivePinia } from "pinia"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getSqlCompletionContext } from "@/lib/sql/sqlCompletion"; -import type { ConnectionConfig } from "@/types/database"; +import type { ConnectionConfig, TreeNode } from "@/types/database"; function installLocalStorage() { const data = new Map(); @@ -471,6 +471,85 @@ describe("connectionStore completion assistant", () => { expect(store.lookupLocalCompletionTables("oracle-1", "ORCL", "", 20, "scott")).toEqual(tables); }); + it("reconciles a non-empty filtered cache with a table added to the sidebar tree", async () => { + const completionAssistantSearch = vi.fn().mockResolvedValue({ + candidates: [{ name: "REPORT_0001", kind: "table", schema: "APP" }], + incomplete: false, + fallback_used: false, + }); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + completionAssistantSearch, + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [oracleConnection()]; + store.connectedIds.add("oracle-1"); + + const first = await store.listCompletionTables("oracle-1", "ORCL", "REPORT", 200, "APP"); + const assistantCallsBeforeSidebarLoad = completionAssistantSearch.mock.calls.length; + store.treeNodes = [ + { + id: "oracle-1:ORCL:APP:tables", + label: "Tables", + type: "group-tables", + connectionId: "oracle-1", + database: "ORCL", + schema: "APP", + children: [ + { id: "report-0001", label: "REPORT_0001", type: "table", connectionId: "oracle-1", database: "ORCL", schema: "APP" }, + { id: "report-1001", label: "REPORT_1001", type: "table", connectionId: "oracle-1", database: "ORCL", schema: "APP" }, + { id: "other-schema", label: "REPORT_1001", type: "table", connectionId: "oracle-1", database: "ORCL", schema: "OTHER" }, + ], + }, + ] as TreeNode[]; + + const merged = await store.listCompletionTables("oracle-1", "ORCL", "REPORT", 200, "APP"); + + expect(first).toEqual([expect.objectContaining({ name: "REPORT_0001", schema: "APP" })]); + expect(merged.map((table) => table.name)).toEqual(["REPORT_0001", "REPORT_1001"]); + expect(merged.every((table) => table.schema === "APP")).toBe(true); + expect(completionAssistantSearch).toHaveBeenCalledTimes(assistantCallsBeforeSidebarLoad); + }); + + it("reconciles an empty filtered cache after the sidebar loads a late table", async () => { + const completionAssistantSearch = vi.fn().mockResolvedValue({ candidates: [], incomplete: false, fallback_used: false }); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + completionAssistantSearch, + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [oracleConnection()]; + store.connectedIds.add("oracle-1"); + + const first = await store.listCompletionTables("oracle-1", "ORCL", "REPORT_1001", 200, "APP"); + const assistantCallsBeforeSidebarLoad = completionAssistantSearch.mock.calls.length; + store.treeNodes = [ + { + id: "oracle-1:ORCL:APP:tables", + label: "Tables", + type: "group-tables", + connectionId: "oracle-1", + database: "ORCL", + schema: "APP", + children: [{ id: "report-1001", label: "REPORT_1001", type: "table", connectionId: "oracle-1", database: "ORCL", schema: "APP" }], + }, + ] as TreeNode[]; + + const merged = await store.listCompletionTables("oracle-1", "ORCL", "REPORT_1001", 200, "APP"); + + expect(first).toEqual([]); + expect(merged).toEqual([expect.objectContaining({ name: "REPORT_1001", schema: "APP", type: "table" })]); + expect(completionAssistantSearch).toHaveBeenCalledTimes(assistantCallsBeforeSidebarLoad); + }); + it("maps global Oracle tables with safe qualification and schema priority", async () => { const completionAssistantSearch = vi.fn().mockResolvedValue({ candidates: [ diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index f9afe0da52..cde3d0ec6e 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -7711,7 +7711,13 @@ export const useConnectionStore = defineStore("connection", () => { // case-insensitive because tableMatchScore normalizes internally. const relaxedFilter = relaxedCompletionTableFilter(trimmedFilter); const cacheKey = `${connectionId}:${database}:${catalog ?? ""}:${trimmedFilter}:${limit ?? ""}:${schema ?? ""}:${globalSearch ? "global" : "scoped"}:${currentSchema ?? ""}`; - if (completionTablesCache.value[cacheKey]) { + const cachedTables = completionTablesCache.value[cacheKey]; + if (cachedTables) { + const localTables = lookupLocalCompletionTables(connectionId, database, trimmedFilter, limit, schema, catalog); + if (localTables.length === 0) return cachedTables; + const mergedTables = dedupeCompletionTables([...localTables, ...cachedTables]); + completionTablesCache.value[cacheKey] = limit ? mergedTables.slice(0, limit) : mergedTables; + indexCompletionTables(connectionId, database, schema, completionTablesCache.value[cacheKey], catalog); return completionTablesCache.value[cacheKey]; } const requestRevision = completionCacheRevision(connectionId, database);