fix(oracle): refresh table hover metadata from local index

Closes #8056
This commit is contained in:
t8y2 committed 2026-09-04 00:44:21 +08:00
1 parent fe955da729
commit 37dc74d9de
4 files changed
+109 -10

No files matched your search

@@ -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);
@@ -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");
@@ -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<string, string>();
@@ -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: [
+7 -1
View File
@@ -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);