From 4a22889b17b908b463f006430e37c0fdbb6d79fe Mon Sep 17 00:00:00 2001 From: zipg Date: Fri, 21 Aug 2026 12:22:14 +0800 Subject: [PATCH] fix(editor): prevent stale completion metadata after refresh --- .../src/components/editor/QueryEditor.vue | 10 ++ .../connectionStore.completion.spec.ts | 138 ++++++++++++++++++ .../__tests__/connectionStore.refresh.spec.ts | 37 ++++- apps/desktop/src/stores/connectionStore.ts | 127 +++++++++++----- 4 files changed, 275 insertions(+), 37 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 30acdb36a8..648d11d18a 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -5522,6 +5522,16 @@ watch( }, ); +watch( + () => connectionStore.completionCacheRevision(props.connectionId, props.database), + () => { + completionEpoch++; + refreshCompletionCache(); + setSemanticDiagnostics([]); + scheduleSemanticDiagnostics(); + }, +); + watch([() => props.clientSessionId, () => props.completionContextVersion], () => { completionEpoch++; refreshCompletionCache(); diff --git a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts index 56adf118d8..8127a3ac57 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts @@ -682,6 +682,144 @@ describe("connectionStore completion assistant", () => { expect(getColumns.mock.calls.map((call) => call[3])).toEqual(["users", "orders", "users"]); }); + it("does not let an invalidated column request overwrite fresh metadata", async () => { + const staleColumns = deferred(); + const freshColumns = deferred(); + const getColumns = vi.fn().mockReturnValueOnce(staleColumns.promise).mockReturnValueOnce(freshColumns.promise); + const column = (name: string) => ({ + name, + data_type: "integer", + is_nullable: false, + column_default: null, + is_primary_key: false, + extra: null, + comment: null, + }); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + completionAssistantSearch: vi.fn().mockRejectedValue(new Error("assistant unavailable")), + getColumns, + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [sqlServerConnection()]; + store.connectedIds.add("sqlserver-1"); + + const staleRequest = store.listCompletionColumns("sqlserver-1", "app", "users", "dbo"); + await vi.waitFor(() => expect(getColumns).toHaveBeenCalledTimes(1)); + store.invalidateCompletionTableCache("sqlserver-1", "app", "users", "dbo"); + const freshRequest = store.listCompletionColumns("sqlserver-1", "app", "users", "dbo"); + freshColumns.resolve([column("fresh_column")]); + await expect(freshRequest).resolves.toEqual([expect.objectContaining({ name: "fresh_column" })]); + + staleColumns.resolve([column("stale_column")]); + await expect(staleRequest).resolves.toEqual([expect.objectContaining({ name: "fresh_column" })]); + await expect(store.listCompletionColumns("sqlserver-1", "app", "users", "dbo")).resolves.toEqual([expect.objectContaining({ name: "fresh_column" })]); + expect(getColumns).toHaveBeenCalledTimes(2); + }); + + it("does not reuse or index a stale assistant column response after invalidation", async () => { + const staleResponse = deferred(); + const freshResponse = deferred(); + const completionAssistantSearch = vi.fn().mockReturnValueOnce(staleResponse.promise).mockReturnValueOnce(freshResponse.promise); + const response = (name: string) => ({ + candidates: [{ name, kind: "column", parent_schema: "dbo", parent_name: "users", data_type: "integer" }], + 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 = [sqlServerConnection()]; + store.connectedIds.add("sqlserver-1"); + + const staleRequest = store.listCompletionColumns("sqlserver-1", "app", "users", "dbo"); + await vi.waitFor(() => expect(completionAssistantSearch).toHaveBeenCalledTimes(1)); + store.invalidateCompletionTableCache("sqlserver-1", "app", "users", "dbo"); + const freshRequest = store.listCompletionColumns("sqlserver-1", "app", "users", "dbo"); + await vi.waitFor(() => expect(completionAssistantSearch).toHaveBeenCalledTimes(2)); + + freshResponse.resolve(response("fresh_column")); + await expect(freshRequest).resolves.toEqual([expect.objectContaining({ name: "fresh_column" })]); + + staleResponse.resolve(response("stale_column")); + await expect(staleRequest).resolves.toEqual([expect.objectContaining({ name: "fresh_column" })]); + expect(store.lookupLocalCompletionColumns("sqlserver-1", "app", "users", "dbo")).toEqual([expect.objectContaining({ name: "fresh_column" })]); + }); + + it("does not let an invalidated table request overwrite fresh metadata", async () => { + const staleTables = deferred(); + const freshTables = deferred(); + const listTables = vi.fn().mockReturnValueOnce(staleTables.promise).mockReturnValueOnce(freshTables.promise); + const table = (name: string) => ({ name, table_type: "BASE TABLE", comment: null }); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + listTables, + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [postgresConnection()]; + store.connectedIds.add("pg-1"); + + const staleRequest = store.listCompletionTables("pg-1", "app", "", undefined, "public"); + await vi.waitFor(() => expect(listTables).toHaveBeenCalledTimes(1)); + store.invalidateCompletionCache("pg-1", "app"); + const freshRequest = store.listCompletionTables("pg-1", "app", "", undefined, "public"); + await vi.waitFor(() => expect(listTables).toHaveBeenCalledTimes(2)); + + freshTables.resolve([table("fresh_table")]); + await expect(freshRequest).resolves.toEqual([expect.objectContaining({ name: "fresh_table" })]); + + staleTables.resolve([table("stale_table")]); + await expect(staleRequest).resolves.toEqual([expect.objectContaining({ name: "fresh_table" })]); + await expect(store.listCompletionTables("pg-1", "app", "", undefined, "public")).resolves.toEqual([expect.objectContaining({ name: "fresh_table" })]); + expect(listTables).toHaveBeenCalledTimes(2); + }); + + it("does not let an invalidated foreign key request overwrite fresh metadata", async () => { + const staleForeignKeys = deferred(); + const freshForeignKeys = deferred(); + const listForeignKeys = vi.fn().mockReturnValueOnce(staleForeignKeys.promise).mockReturnValueOnce(freshForeignKeys.promise); + const foreignKey = (name: string) => ({ name, column: "customer_id", ref_schema: "public", ref_table: "customers", ref_column: "id" }); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + listForeignKeys, + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [postgresConnection()]; + store.connectedIds.add("pg-1"); + + const staleRequest = store.listCompletionForeignKeys("pg-1", "app", "orders", "public"); + await vi.waitFor(() => expect(listForeignKeys).toHaveBeenCalledTimes(1)); + store.invalidateCompletionTableCache("pg-1", "app", "orders", "public"); + const freshRequest = store.listCompletionForeignKeys("pg-1", "app", "orders", "public"); + await vi.waitFor(() => expect(listForeignKeys).toHaveBeenCalledTimes(2)); + + freshForeignKeys.resolve([foreignKey("fresh_fk")]); + await expect(freshRequest).resolves.toEqual([expect.objectContaining({ name: "fresh_fk" })]); + + staleForeignKeys.resolve([foreignKey("stale_fk")]); + await expect(staleRequest).resolves.toEqual([expect.objectContaining({ name: "fresh_fk" })]); + await expect(store.listCompletionForeignKeys("pg-1", "app", "orders", "public")).resolves.toEqual([expect.objectContaining({ name: "fresh_fk" })]); + expect(listForeignKeys).toHaveBeenCalledTimes(2); + }); + it("keeps the same table cached in other catalogs", async () => { const getColumns = vi.fn(async (_connectionId: string, _database: string, _schema: string, table: string, catalog?: string) => [ { diff --git a/apps/desktop/src/stores/__tests__/connectionStore.refresh.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.refresh.spec.ts index 0518ecee97..f9e9cae3d4 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.refresh.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.refresh.spec.ts @@ -51,11 +51,27 @@ function objectGroup(type: "group-views" | "group-procedures", key: string, chil }; } -function installApiMocks(options?: { listTables?: ReturnType; listObjects?: ReturnType }) { +function column(name: string) { + return { + name, + data_type: "VARCHAR", + is_nullable: true, + column_default: null, + is_primary_key: false, + extra: null, + comment: null, + numeric_precision: null, + numeric_scale: null, + character_maximum_length: 255, + }; +} + +function installApiMocks(options?: { getColumns?: ReturnType; listTables?: ReturnType; listObjects?: ReturnType }) { vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); vi.doMock("@/lib/backend/api", () => ({ checkConnectionHealth: vi.fn().mockResolvedValue(undefined), deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + getColumns: options?.getColumns ?? vi.fn().mockResolvedValue([]), listInstalledAgents: vi.fn().mockResolvedValue([]), listObjects: options?.listObjects ?? vi.fn().mockResolvedValue([]), listTables: options?.listTables ?? vi.fn().mockResolvedValue([]), @@ -131,4 +147,23 @@ describe("connectionStore tree refresh state", () => { expect(store.isTreeNodeChildrenLoaded(viewsGroupId)).toBe(false); expect(store.isTreeNodeChildrenLoaded(proceduresGroupId)).toBe(false); }); + + it("reloads completion columns after refreshing their schema tree", async () => { + const getColumns = vi + .fn() + .mockResolvedValueOnce([column("id")]) + .mockResolvedValueOnce([column("id"), column("new_column")]); + const listTables = vi.fn().mockResolvedValue([{ name: "users", table_type: "BASE TABLE", comment: null }]); + installApiMocks({ getColumns, listTables }); + const root = schemaNode([]); + const store = await createStore(root); + + await expect(store.listCompletionColumns("pg-1", "app", "users", "public")).resolves.toEqual([expect.objectContaining({ name: "id" })]); + + await store.refreshTreeNode(root); + + await expect(store.listCompletionColumns("pg-1", "app", "users", "public")).resolves.toEqual([expect.objectContaining({ name: "id" }), expect.objectContaining({ name: "new_column" })]); + expect(getColumns).toHaveBeenCalledTimes(2); + expect(store.completionCacheRevision("pg-1", "app")).toBeGreaterThan(0); + }); }); diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index c00b25b77c..f640cf6e41 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -461,6 +461,7 @@ export const useConnectionStore = defineStore("connection", () => { const completionColumnIndex = new Map(); const completionForeignKeyIndex = new Map(); const completionInFlight = new Map>(); + const completionCacheRevisions = ref>({}); const completionMetadataLimiter = new MetadataTaskLimiter(COMPLETION_METADATA_CONCURRENCY, (event) => { console.debug("[DBX][completion-metadata:limit]", event); }); @@ -2841,7 +2842,20 @@ export const useConnectionStore = defineStore("connection", () => { return pastedCount; } + function completionCacheRevision(connectionId?: string, database?: string): number { + if (!connectionId) return 0; + const connectionRevision = completionCacheRevisions.value[connectionId] ?? 0; + if (database == null) return connectionRevision; + return connectionRevision + (completionCacheRevisions.value[`${connectionId}:${database}`] ?? 0); + } + + function bumpCompletionCacheRevision(connectionId: string, database?: string) { + const key = database == null ? connectionId : `${connectionId}:${database}`; + completionCacheRevisions.value[key] = (completionCacheRevisions.value[key] ?? 0) + 1; + } + function invalidateCompletionCache(connectionId: string, database?: string) { + bumpCompletionCacheRevision(connectionId, database); invalidateMetadataCaches({ connectionId, database }); if (database == null) delete completionDatabasesCache.value[connectionId]; const cachePrefix = database == null ? `${connectionId}:` : `${connectionId}:${database}:`; @@ -6208,6 +6222,7 @@ export const useConnectionStore = defineStore("connection", () => { } async function refreshTreeNode(node: TreeNode) { + invalidateCompletionCachesForNode(node); invalidateMetadataCachesForNode(node); if (objectTypesForGroupNode(node.type)) { clearLoadedChildrenCache(node.id, { deletePersisted: false }); @@ -6378,6 +6393,7 @@ export const useConnectionStore = defineStore("connection", () => { } function invalidateCompletionTableCache(connectionId: string, database: string, tableName: string, schema?: string, catalog?: string): number { + bumpCompletionCacheRevision(connectionId, database); const matches = (key: string) => completionTableCacheKeyMatches(key, connectionId, database, tableName, schema, catalog); let removed = 0; for (const cache of [completionColumnsCache.value, completionForeignKeysCache.value]) { @@ -6397,6 +6413,19 @@ export const useConnectionStore = defineStore("connection", () => { return removed; } + function invalidateCompletionCachesForNode(node: TreeNode) { + if (!node.connectionId) return; + if (typeof node.database !== "string") { + invalidateCompletionCache(node.connectionId); + return; + } + if (node.tableName) { + invalidateCompletionTableCache(node.connectionId, node.database, node.tableName, node.schema, node.catalog); + return; + } + invalidateCompletionCache(node.connectionId, node.database); + } + function touchCompletionIndex(index: Map, key: string, value: T, max = COMPLETION_CACHE_MAX) { index.set(key, { ...value, touched: Date.now() }); if (index.size <= max) return; @@ -6437,8 +6466,8 @@ export const useConnectionStore = defineStore("connection", () => { }); } - async function completionAssistantSearch(request: CompletionAssistantRequest) { - return withCompletionInFlight(`assistant:${completionAssistantRequestKey(request)}`, async () => { + async function completionAssistantSearch(request: CompletionAssistantRequest, requestRevision = completionCacheRevision(request.connection_id, request.database)) { + return withCompletionInFlight(`${request.connection_id}:${request.database}:assistant:${requestRevision}:${completionAssistantRequestKey(request)}`, async () => { await ensureConnected(request.connection_id); return api.completionAssistantSearch(request); }); @@ -6668,23 +6697,26 @@ export const useConnectionStore = defineStore("connection", () => { })); } - async function listCompletionAssistantTables(connectionId: string, database: string, filter: string, limit?: number, schema?: string, globalSearch = false, currentSchema?: string): Promise { + async function listCompletionAssistantTables(connectionId: string, database: string, filter: string, limit?: number, schema?: string, globalSearch = false, currentSchema?: string, requestRevision = completionCacheRevision(connectionId, database)): Promise { const oracleAssistant = getConfig(connectionId)?.db_type === "oracle"; const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, globalSearch ? currentSchema : (schema ?? currentSchema)) : schema?.trim() || undefined; const objectKinds: CompletionAssistantObjectKind[] = ["table", "view"]; - const response = await completionAssistantSearch({ - connection_id: connectionId, - database, - schema: preferredSchema ?? null, - object_kinds: objectKinds, - mask: filter.trim(), - max_results: limit ?? 200, - global_search: globalSearch, - parent_schema: globalSearch ? null : (schema ?? null), - match_mode: "prefix", - }); + const response = await completionAssistantSearch( + { + connection_id: connectionId, + database, + schema: preferredSchema ?? null, + object_kinds: objectKinds, + mask: filter.trim(), + max_results: limit ?? 200, + global_search: globalSearch, + parent_schema: globalSearch ? null : (schema ?? null), + match_mode: "prefix", + }, + requestRevision, + ); const tables = completionAssistantTables(response.candidates, preferredSchema, oracleAssistant); - indexCompletionTables(connectionId, database, schema, tables); + if (requestRevision === completionCacheRevision(connectionId, database)) indexCompletionTables(connectionId, database, schema, tables); return tables; } @@ -6726,20 +6758,23 @@ export const useConnectionStore = defineStore("connection", () => { return objects; } - async function listCompletionAssistantColumns(connectionId: string, database: string, table: string, schema?: string, context?: { tableQuoted?: boolean; schemaQuoted?: boolean }): Promise { - const response = await completionAssistantSearch({ - connection_id: connectionId, - database, - schema: schema ?? null, - object_kinds: ["column"], - mask: "", - max_results: 500, - parent_schema: schema ?? null, - parent_name: table, - match_mode: "prefix", - }); + async function listCompletionAssistantColumns(connectionId: string, database: string, table: string, schema?: string, context?: { tableQuoted?: boolean; schemaQuoted?: boolean }, requestRevision = completionCacheRevision(connectionId, database)): Promise { + const response = await completionAssistantSearch( + { + connection_id: connectionId, + database, + schema: schema ?? null, + object_kinds: ["column"], + mask: "", + max_results: 500, + parent_schema: schema ?? null, + parent_name: table, + match_mode: "prefix", + }, + requestRevision, + ); const columns = completionAssistantColumns(response.candidates, table, schema, context); - if (columns.length > 0) indexCompletionColumns(connectionId, database, table, schema, columns); + if (columns.length > 0 && requestRevision === completionCacheRevision(connectionId, database)) indexCompletionColumns(connectionId, database, table, schema, columns); return columns; } @@ -7108,6 +7143,7 @@ export const useConnectionStore = defineStore("connection", () => { if (completionTablesCache.value[cacheKey]) { return completionTablesCache.value[cacheKey]; } + const requestRevision = completionCacheRevision(connectionId, database); return withCompletionInFlight( `${cacheKey}:tables`, @@ -7118,7 +7154,7 @@ export const useConnectionStore = defineStore("connection", () => { if (normalizedFilter || limit) { let results: SqlCompletionTable[] = []; try { - results = await listCompletionAssistantTables(connectionId, database, trimmedFilter, limit, schema, globalSearch, currentSchema); + results = await listCompletionAssistantTables(connectionId, database, trimmedFilter, limit, schema, globalSearch, currentSchema, requestRevision); } catch { if (schema) { const tables = await listCompletionTableMetadata(connectionId, database, schema, trimmedFilter, limit, catalog); @@ -7136,7 +7172,7 @@ export const useConnectionStore = defineStore("connection", () => { if (results.length === 0 && relaxedFilter) { if (globalSearch) { try { - results = await listCompletionAssistantTables(connectionId, database, relaxedFilter, expandedCompletionLimit(limit), schema, true, currentSchema); + results = await listCompletionAssistantTables(connectionId, database, relaxedFilter, expandedCompletionLimit(limit), schema, true, currentSchema, requestRevision); } catch { results = []; } @@ -7158,15 +7194,17 @@ export const useConnectionStore = defineStore("connection", () => { } } const limitedTables = limit ? dedupeCompletionTables(results).slice(0, limit) : results; + if (requestRevision !== completionCacheRevision(connectionId, database)) return listCompletionTables(connectionId, database, filter, limit, schema, globalSearch, currentSchema, catalog, options); completionTablesCache.value[cacheKey] = limitedTables; indexCompletionTables(connectionId, database, undefined, limitedTables, catalog); evictOldestCacheEntries(completionTablesCache.value, COMPLETION_CACHE_MAX); return completionTablesCache.value[cacheKey]; } + let scopedTables: SqlCompletionTable[]; if (schema) { const tables = await listCompletionTableMetadata(connectionId, database, schema, undefined, undefined, catalog); - completionTablesCache.value[cacheKey] = tables.map((table) => ({ + scopedTables = tables.map((table) => ({ name: table.name, catalog, schema, @@ -7174,8 +7212,10 @@ export const useConnectionStore = defineStore("connection", () => { ...completionStableTableType(table.table_type), })); } else { - completionTablesCache.value[cacheKey] = lookupLocalCompletionTables(connectionId, database, normalizedFilter, limit, undefined, catalog); + scopedTables = lookupLocalCompletionTables(connectionId, database, normalizedFilter, limit, undefined, catalog); } + if (requestRevision !== completionCacheRevision(connectionId, database)) return listCompletionTables(connectionId, database, filter, limit, schema, globalSearch, currentSchema, catalog, options); + completionTablesCache.value[cacheKey] = scopedTables; indexCompletionTables(connectionId, database, undefined, completionTablesCache.value[cacheKey], catalog); evictOldestCacheEntries(completionTablesCache.value, COMPLETION_CACHE_MAX); return completionTablesCache.value[cacheKey]; @@ -7186,6 +7226,7 @@ export const useConnectionStore = defineStore("connection", () => { if (tables.length === 0 && relaxedFilter) { tables = await listCompletionTableMetadata(connectionId, database, querySchema, relaxedFilter, expandedCompletionLimit(limit), catalog); } + if (requestRevision !== completionCacheRevision(connectionId, database)) return listCompletionTables(connectionId, database, filter, limit, schema, globalSearch, currentSchema, catalog, options); completionTablesCache.value[cacheKey] = tables.map((table) => ({ name: table.name, catalog, @@ -7363,15 +7404,16 @@ export const useConnectionStore = defineStore("connection", () => { const sessionCacheScope = usesOracleCurrentSchema && context?.clientSessionId ? `:${context.clientSessionId}:${context.version ?? 0}` : ""; const cacheKey = `${connectionId}:${database}:${catalog ?? ""}:${completionSchema || ""}:${completionTable}${sessionCacheScope}`; if (!completionColumnsCache.value[cacheKey]) { + const requestRevision = completionCacheRevision(connectionId, database); await withCompletionInFlight( `${cacheKey}:columns`, async () => { await ensureConnected(connectionId); if (!usesOracleCurrentSchema && !catalog) { try { - const assistantColumns = await listCompletionAssistantColumns(connectionId, database, completionTable, completionSchema, context); + const assistantColumns = await listCompletionAssistantColumns(connectionId, database, completionTable, completionSchema, context, requestRevision); if (assistantColumns.length > 0) { - completionColumnsCache.value[cacheKey] = assistantColumns.map((column) => ({ + const columns = assistantColumns.map((column) => ({ name: column.name, data_type: column.dataType ?? "", is_nullable: column.isNullable ?? true, @@ -7383,6 +7425,8 @@ export const useConnectionStore = defineStore("connection", () => { numeric_scale: null, character_maximum_length: null, })); + if (requestRevision !== completionCacheRevision(connectionId, database)) return; + completionColumnsCache.value[cacheKey] = columns; evictOldestCacheEntries(completionColumnsCache.value, COMPLETION_CACHE_MAX); return; } @@ -7391,13 +7435,19 @@ export const useConnectionStore = defineStore("connection", () => { } } const querySchema = usesOracleCurrentSchema ? "" : metadataQuerySchema(connectionId, database, completionSchema); - completionColumnsCache.value[cacheKey] = await api.getColumns(connectionId, database, querySchema, completionTable, catalog, usesOracleCurrentSchema ? context?.clientSessionId : undefined); + const columns = await api.getColumns(connectionId, database, querySchema, completionTable, catalog, usesOracleCurrentSchema ? context?.clientSessionId : undefined); + if (requestRevision !== completionCacheRevision(connectionId, database)) return; + completionColumnsCache.value[cacheKey] = columns; evictOldestCacheEntries(completionColumnsCache.value, COMPLETION_CACHE_MAX); }, { scope: completionLimiterScope(connectionId, database), kind: "columns" }, ); } + if (!completionColumnsCache.value[cacheKey]) { + return listCompletionColumns(connectionId, database, table, schema, context, catalog); + } + const columns = completionColumnsCache.value[cacheKey].map((column) => ({ name: column.name, table: completionTable, @@ -7419,17 +7469,21 @@ export const useConnectionStore = defineStore("connection", () => { const cacheKey = `${connectionId}:${database}:${schema || ""}:${table}`; if (!completionForeignKeysCache.value[cacheKey]) { + const requestRevision = completionCacheRevision(connectionId, database); await withCompletionInFlight( `${cacheKey}:fkeys`, async () => { await ensureConnected(connectionId); const querySchema = metadataQuerySchema(connectionId, database, schema); - completionForeignKeysCache.value[cacheKey] = await api.listForeignKeys(connectionId, database, querySchema, table); + const foreignKeys = await api.listForeignKeys(connectionId, database, querySchema, table); + if (requestRevision !== completionCacheRevision(connectionId, database)) return; + completionForeignKeysCache.value[cacheKey] = foreignKeys; evictOldestCacheEntries(completionForeignKeysCache.value, COMPLETION_CACHE_MAX); }, { scope: completionLimiterScope(connectionId, database), kind: "foreignKeys" }, ); } + if (!completionForeignKeysCache.value[cacheKey]) return listCompletionForeignKeys(connectionId, database, table, schema); const foreignKeys = sqlCompletionForeignKeys(completionForeignKeysCache.value[cacheKey]); indexCompletionForeignKeys(connectionId, database, table, schema, foreignKeys); @@ -8365,6 +8419,7 @@ export const useConnectionStore = defineStore("connection", () => { listMongoCompletionFields, invalidateCompletionCache, invalidateCompletionTableCache, + completionCacheRevision, invalidateMetadataCache, exportConnectionsToFile, readImportFile,