fix(editor): prevent stale completion metadata after refresh

This commit is contained in:
zipg authored and GitHub committed 2026-08-21 12:22:14 +08:00
1 parent 4bf7551b82
commit 4a22889b17
4 files changed
+275 -37

No files matched your search

@@ -5522,6 +5522,16 @@ watch(
},
);
watch(
() => connectionStore.completionCacheRevision(props.connectionId, props.database),
() => {
completionEpoch++;
refreshCompletionCache();
setSemanticDiagnostics([]);
scheduleSemanticDiagnostics();
},
);
watch([() => props.clientSessionId, () => props.completionContextVersion], () => {
completionEpoch++;
refreshCompletionCache();
@@ -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<unknown[]>();
const freshColumns = deferred<unknown[]>();
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<any>();
const freshResponse = deferred<any>();
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<any[]>();
const freshTables = deferred<any[]>();
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<any[]>();
const freshForeignKeys = deferred<any[]>();
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) => [
{
@@ -51,11 +51,27 @@ function objectGroup(type: "group-views" | "group-procedures", key: string, chil
};
}
function installApiMocks(options?: { listTables?: ReturnType<typeof vi.fn>; listObjects?: ReturnType<typeof vi.fn> }) {
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<typeof vi.fn>; listTables?: ReturnType<typeof vi.fn>; listObjects?: ReturnType<typeof vi.fn> }) {
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);
});
});
+91 -36
View File
@@ -461,6 +461,7 @@ export const useConnectionStore = defineStore("connection", () => {
const completionColumnIndex = new Map<string, { touched: number; columns: SqlCompletionColumn[] }>();
const completionForeignKeyIndex = new Map<string, { touched: number; foreignKeys: SqlCompletionForeignKey[] }>();
const completionInFlight = new Map<string, Promise<unknown>>();
const completionCacheRevisions = ref<Record<string, number>>({});
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<T>(index: Map<string, { touched: number } & T>, 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<SqlCompletionTable[]> {
async function listCompletionAssistantTables(connectionId: string, database: string, filter: string, limit?: number, schema?: string, globalSearch = false, currentSchema?: string, requestRevision = completionCacheRevision(connectionId, database)): Promise<SqlCompletionTable[]> {
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<SqlCompletionColumn[]> {
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<SqlCompletionColumn[]> {
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,