fix(memory): release closed query and connection resources

This commit is contained in:
zipg authored and GitHub committed 2026-08-25 15:47:23 +08:00
1 parent 88e659833d
commit 81c3cc15d2
12 files changed
+477 -32

No files matched your search

@@ -277,6 +277,7 @@ watch(
);
watch([deferredSearchQuery, regexMode], ([newQuery, isRegexMode], [oldQuery, wasRegexMode]) => {
if (!isRegexMode || !newQuery) regexTableSearchScopes.value = [];
// The regex source is a client-side projection; the remote tree-loading
// search state must never carry it, or explicit node expansion would leak
// the expression as a remote searchFilter.
@@ -1,11 +1,13 @@
import { beforeEach, describe, expect, it } from "vitest";
import { loadDataGridStructuredFilterState, saveDataGridStructuredFilterState } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
import { clearDataGridStructuredFilterStatesForTab, loadDataGridStructuredFilterState, saveDataGridStructuredFilterState } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
describe("data grid structured filter persistence", () => {
const cacheKey = "issue-436-filter-view";
const scopeKey = "mysql\0demo\0users";
beforeEach(() => {
clearDataGridStructuredFilterStatesForTab("issue-436-filter-view");
clearDataGridStructuredFilterStatesForTab("lru-tab");
saveDataGridStructuredFilterState(cacheKey, {
scopeKey,
manualWhereInput: "tenant_id = 7",
@@ -36,4 +38,44 @@ describe("data grid structured filter persistence", () => {
serverColumnFilters: {},
});
});
it("clears every result-grid state owned by a closed tab", () => {
saveDataGridStructuredFilterState("tab-1-run-1-0", {
scopeKey,
manualWhereInput: "id > 1",
rules: [],
appliedWhereInput: "id > 1",
serverColumnFilters: {},
});
saveDataGridStructuredFilterState("tab-10-run-1-0", {
scopeKey,
manualWhereInput: "id > 10",
rules: [],
appliedWhereInput: "id > 10",
serverColumnFilters: {},
});
clearDataGridStructuredFilterStatesForTab("tab-1");
expect(loadDataGridStructuredFilterState("tab-1-run-1-0", scopeKey)).toBeUndefined();
expect(loadDataGridStructuredFilterState("tab-10-run-1-0", scopeKey)?.manualWhereInput).toBe("id > 10");
clearDataGridStructuredFilterStatesForTab("tab-10");
});
it("bounds cached result-grid states and refreshes recency on read", () => {
const state = {
scopeKey,
manualWhereInput: "",
rules: [],
appliedWhereInput: "",
serverColumnFilters: {},
};
for (let index = 0; index < 128; index += 1) saveDataGridStructuredFilterState(`lru-tab-${index}`, state);
expect(loadDataGridStructuredFilterState("lru-tab-0", scopeKey)).toBeDefined();
saveDataGridStructuredFilterState("lru-tab-128", state);
expect(loadDataGridStructuredFilterState("lru-tab-0", scopeKey)).toBeDefined();
expect(loadDataGridStructuredFilterState("lru-tab-1", scopeKey)).toBeUndefined();
});
});
@@ -14,6 +14,7 @@ export type DataGridStructuredFilterCacheState = {
serverColumnFilters: Record<number, DataGridCachedServerColumnFilter>;
};
const STRUCTURED_FILTER_STATE_CACHE_MAX_ENTRIES = 128;
const structuredFilterStateCache = new Map<string, DataGridStructuredFilterCacheState>();
export function cloneDataGridStructuredFilterRules(rules: readonly DataGridStructuredFilterRule[]): DataGridStructuredFilterRule[] {
@@ -23,6 +24,8 @@ export function cloneDataGridStructuredFilterRules(rules: readonly DataGridStruc
export function loadDataGridStructuredFilterState(cacheKey: string, scopeKey: string): DataGridStructuredFilterCacheState | undefined {
const cached = structuredFilterStateCache.get(cacheKey);
if (!cached || cached.scopeKey !== scopeKey) return undefined;
structuredFilterStateCache.delete(cacheKey);
structuredFilterStateCache.set(cacheKey, cached);
return {
...cached,
rules: cloneDataGridStructuredFilterRules(cached.rules),
@@ -31,9 +34,22 @@ export function loadDataGridStructuredFilterState(cacheKey: string, scopeKey: st
}
export function saveDataGridStructuredFilterState(cacheKey: string, state: DataGridStructuredFilterCacheState) {
structuredFilterStateCache.delete(cacheKey);
structuredFilterStateCache.set(cacheKey, {
...state,
rules: cloneDataGridStructuredFilterRules(state.rules),
serverColumnFilters: structuredClone(state.serverColumnFilters),
});
while (structuredFilterStateCache.size > STRUCTURED_FILTER_STATE_CACHE_MAX_ENTRIES) {
const oldest = structuredFilterStateCache.keys().next().value;
if (oldest === undefined) break;
structuredFilterStateCache.delete(oldest);
}
}
export function clearDataGridStructuredFilterStatesForTab(tabId: string) {
structuredFilterStateCache.delete(tabId);
for (const cacheKey of structuredFilterStateCache.keys()) {
if (cacheKey.startsWith(`${tabId}-`)) structuredFilterStateCache.delete(cacheKey);
}
}
@@ -94,6 +94,17 @@ describe("objectBrowserRowsCache", () => {
expect(getCachedObjectBrowserRows(isolatedScope)).toEqual([row]);
});
it("rejects old write tokens after their bounded generation state is evicted", () => {
const staleScope = { connectionId: "stale", database: "db", schema: "public" };
const staleToken = createObjectBrowserRowsCacheWriteToken(staleScope);
for (let index = 0; index < 128; index += 1) {
createObjectBrowserRowsCacheWriteToken({ connectionId: `c${index}`, database: "db", schema: "public" });
}
expect(cacheObjectBrowserRows(staleToken, [row])).toBeUndefined();
expect(getCachedObjectBrowserRows(staleScope)).toBeUndefined();
});
it("preserves the original object list freshness when statistics update cached rows", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-28T00:00:00Z"));
+9 -1
View File
@@ -5070,12 +5070,20 @@ function computeBoost(candidate: string, prefix: string): number {
}
// --- History-based ranking ---
const COMPLETION_STATS_MAX_ENTRIES = 512;
const completionStats = new Map<string, number>();
/** Record a user selection to boost future rankings. */
export function recordCompletionSelection(label: string, type: string): void {
const key = `${type}:${label}`;
completionStats.set(key, (completionStats.get(key) || 0) + 1);
const count = completionStats.get(key) || 0;
completionStats.delete(key);
completionStats.set(key, count + 1);
while (completionStats.size > COMPLETION_STATS_MAX_ENTRIES) {
const oldest = completionStats.keys().next().value;
if (oldest === undefined) break;
completionStats.delete(oldest);
}
}
function getHistoryBoost(label: string, type: string): number {
@@ -4,6 +4,7 @@ import type { ObjectBrowserRow } from "@/lib/table/objectBrowserRows";
const OBJECT_BROWSER_ROWS_CACHE_TTL_MS = 30_000;
const OBJECT_BROWSER_ROWS_CACHE_MAX_ENTRIES = 24;
const OBJECT_BROWSER_ROWS_GENERATION_MAX_ENTRIES = 128;
export interface ObjectBrowserRowsCacheScope {
connectionId: string;
@@ -28,6 +29,7 @@ const objectBrowserRowsCache = new MetadataResultCache<ObjectBrowserRow[]>({
now: () => Date.now(),
});
const objectBrowserRowsCacheGenerations = new Map<string, ObjectBrowserRowsCacheGeneration>();
let nextObjectBrowserRowsCacheGeneration = 0;
function objectBrowserRowsScope(scope: ObjectBrowserRowsCacheScope): MetadataScopeInput {
return {
@@ -48,7 +50,15 @@ function objectBrowserRowsCacheGeneration(scope: ObjectBrowserRowsCacheScope): O
const key = metadataScopeKey(cacheScope);
let state = objectBrowserRowsCacheGenerations.get(key);
if (!state) {
state = { generation: 0, scope: metadataScopeParts(cacheScope) };
state = { generation: ++nextObjectBrowserRowsCacheGeneration, scope: metadataScopeParts(cacheScope) };
objectBrowserRowsCacheGenerations.set(key, state);
while (objectBrowserRowsCacheGenerations.size > OBJECT_BROWSER_ROWS_GENERATION_MAX_ENTRIES) {
const oldest = objectBrowserRowsCacheGenerations.keys().next().value;
if (oldest === undefined) break;
objectBrowserRowsCacheGenerations.delete(oldest);
}
} else {
objectBrowserRowsCacheGenerations.delete(key);
objectBrowserRowsCacheGenerations.set(key, state);
}
return state;
@@ -83,13 +93,13 @@ export function cacheObjectBrowserRows(token: ObjectBrowserRowsCacheWriteToken,
export function invalidateObjectBrowserRowsCache(match: MetadataCacheInvalidation): number {
const projected = objectBrowserRowsCacheInvalidation(match);
const matches = metadataCacheInvalidationMatcher(projected);
for (const state of objectBrowserRowsCacheGenerations.values()) {
if (matches(state.scope)) state.generation++;
for (const [key, state] of objectBrowserRowsCacheGenerations) {
if (matches(state.scope)) objectBrowserRowsCacheGenerations.delete(key);
}
return objectBrowserRowsCache.invalidate(projected);
}
export function clearObjectBrowserRowsCache(): void {
objectBrowserRowsCache.clear();
for (const state of objectBrowserRowsCacheGenerations.values()) state.generation++;
objectBrowserRowsCacheGenerations.clear();
}
@@ -107,4 +107,83 @@ describe("connectionStore sidebar table storage", () => {
expect(currentExistingTable?.sizeBytes).toBe(8192);
expect(newTable?.sizeBytes).toBe(16384);
});
it("does not cache a table-size request that finishes after the connection is lost", async () => {
const staleStatistics = deferred<Array<{ name: string; schema: string; total_bytes: number }>>();
const listObjectStatistics = vi
.fn()
.mockReturnValueOnce(staleStatistics.promise)
.mockResolvedValueOnce([{ name: "USERS", schema: "APP", total_bytes: 8192 }]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
listInstalledAgents: vi.fn().mockResolvedValue([]),
listObjectStatistics,
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useConnectionStore();
useSettingsStore().editorSettings.sidebarObjectInfoMode = "size";
const connection = { id: "oracle-1", name: "Oracle", db_type: "oracle", host: "127.0.0.1", port: 1521, username: "APP", password: "", database: "ORCL" } as ConnectionConfig;
const tableNode = (): TreeNode => ({ id: "oracle-1:ORCL:APP:USERS", label: "USERS", type: "table", connectionId: connection.id, database: connection.database, schema: "APP" });
const setLiveTree = () => {
store.connectedIds.add(connection.id);
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [tableNode()] }];
};
store.connections = [connection];
setLiveTree();
const staleLoad = store.loadSidebarTableStorage({ connectionId: connection.id, database: connection.database, schema: "APP" });
store.markConnectionLost(connection.id, new Error("connection closed"));
staleStatistics.resolve([{ name: "USERS", schema: "APP", total_bytes: 4096 }]);
await staleLoad;
setLiveTree();
await store.loadSidebarTableStorage({ connectionId: connection.id, database: connection.database, schema: "APP" });
expect(listObjectStatistics).toHaveBeenCalledTimes(2);
expect(store.treeNodes[0]?.children?.[0]?.sizeBytes).toBe(8192);
});
it("does not cache a database-size request that finishes after the connection is lost", async () => {
const staleStorage = deferred<Array<{ name: string; size_bytes: number }>>();
const listDatabaseStorage = vi
.fn()
.mockReturnValueOnce(staleStorage.promise)
.mockResolvedValueOnce([{ name: "app", size_bytes: 16384 }]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
listDatabaseStorage,
listInstalledAgents: vi.fn().mockResolvedValue([]),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useConnectionStore();
useSettingsStore().editorSettings.sidebarObjectInfoMode = "size";
const connection = { id: "pg-1", name: "PostgreSQL", db_type: "postgres", host: "127.0.0.1", port: 5432, username: "postgres", password: "" } as ConnectionConfig;
const setLiveTree = () => {
store.connectedIds.add(connection.id);
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [{ id: "pg-1:app", label: "app", type: "database", connectionId: connection.id, database: "app" }] }];
};
store.connections = [connection];
setLiveTree();
const staleLoad = store.loadSidebarDatabaseStorage(connection.id);
store.markConnectionLost(connection.id, new Error("connection closed"));
staleStorage.resolve([{ name: "app", size_bytes: 4096 }]);
await staleLoad;
setLiveTree();
await store.loadSidebarDatabaseStorage(connection.id);
expect(listDatabaseStorage).toHaveBeenCalledTimes(2);
expect(store.treeNodes[0]?.children?.[0]?.sizeBytes).toBe(16384);
});
});
+72 -18
View File
@@ -180,6 +180,9 @@ const DEFAULT_KEEPALIVE_INTERVAL_SECS = 30;
const METADATA_LIST_PAGE_CACHE_TTL_MS = 30_000;
const METADATA_LIST_PAGE_CACHE_MAX_ENTRIES = 160;
const SIDEBAR_DATABASE_STORAGE_CACHE_TTL_MS = 30_000;
const SIDEBAR_DATABASE_STORAGE_CACHE_MAX_ENTRIES = 32;
const SIDEBAR_TABLE_STORAGE_CACHE_MAX_ENTRIES = 64;
const SIDEBAR_TABLE_SEARCH_INDEX_CACHE_MAX_ENTRIES = 32;
export const COMPLETION_METADATA_CONCURRENCY = 2;
const MONGO_LEGACY_DRIVER_PROFILE = "mongodb-legacy";
const MONGO_LEGACY_DRIVER_LABEL = "MongoDB (Legacy)";
@@ -418,9 +421,15 @@ export const useConnectionStore = defineStore("connection", () => {
}
});
const treeNodes = ref<TreeNode[]>([]);
const sidebarDatabaseStorageCache = new Map<string, { expiresAt: number; value: DatabaseStorageInfo[] }>();
const sidebarDatabaseStorageCache = new MetadataResultCache<DatabaseStorageInfo[]>({
ttlMs: SIDEBAR_DATABASE_STORAGE_CACHE_TTL_MS,
maxEntries: SIDEBAR_DATABASE_STORAGE_CACHE_MAX_ENTRIES,
});
const sidebarDatabaseStorageInFlight = new Map<string, Promise<DatabaseStorageInfo[]>>();
const sidebarTableStorageCache = new Map<string, { expiresAt: number; value: ObjectStatistics[] }>();
const sidebarTableStorageCache = new MetadataResultCache<ObjectStatistics[]>({
ttlMs: SIDEBAR_DATABASE_STORAGE_CACHE_TTL_MS,
maxEntries: SIDEBAR_TABLE_STORAGE_CACHE_MAX_ENTRIES,
});
const sidebarTableStorageInFlight = new Map<string, Promise<ObjectStatistics[]>>();
const pinnedTreeNodeOrder = ref<string[]>([]);
const pinnedTreeNodeIds = ref<Set<string>>(new Set());
@@ -1078,6 +1087,7 @@ export const useConnectionStore = defineStore("connection", () => {
function markConnectionLost(connectionId: string, error: unknown) {
connectedIds.value.delete(connectionId);
clearSidebarStorageCaches(connectionId);
clearPrimaryVisibleObjectNames(connectionId);
clearConnectionIdentifierQuote(connectionId);
clearConnectionNodeLoading(connectionId);
@@ -2383,6 +2393,24 @@ export const useConnectionStore = defineStore("connection", () => {
return `${treeCacheKey}:table-search-index-v1`;
}
function getSidebarTableSearchIndexMemoryCache(cacheKey: string): TableInfo[] | null | undefined {
if (!sidebarTableSearchIndexCache.has(cacheKey)) return undefined;
const cached = sidebarTableSearchIndexCache.get(cacheKey) ?? null;
sidebarTableSearchIndexCache.delete(cacheKey);
sidebarTableSearchIndexCache.set(cacheKey, cached);
return cached;
}
function setSidebarTableSearchIndexMemoryCache(cacheKey: string, entries: TableInfo[] | null) {
sidebarTableSearchIndexCache.delete(cacheKey);
sidebarTableSearchIndexCache.set(cacheKey, entries);
while (sidebarTableSearchIndexCache.size > SIDEBAR_TABLE_SEARCH_INDEX_CACHE_MAX_ENTRIES) {
const oldest = sidebarTableSearchIndexCache.keys().next().value;
if (oldest === undefined) break;
sidebarTableSearchIndexCache.delete(oldest);
}
}
const sidebarTableSearchIndexManifestCacheKey = "dbx:sidebar-table-search-index-manifest-v1";
function sidebarTableSearchIndexManifestEntry(parent: TreeNode, cacheKey: string): TableSearchIndexManifestEntry | null {
@@ -2619,7 +2647,8 @@ export const useConnectionStore = defineStore("connection", () => {
}
async function readSidebarTableSearchIndexCache(cacheKey: string, connectionId: string): Promise<TableInfo[] | null> {
if (sidebarTableSearchIndexCache.has(cacheKey)) return sidebarTableSearchIndexCache.get(cacheKey) ?? null;
const cached = getSidebarTableSearchIndexMemoryCache(cacheKey);
if (cached !== undefined) return cached;
const pending = sidebarTableSearchIndexInFlight.get(cacheKey);
if (pending) return pending;
const connectionGeneration = sidebarTableSearchIndexConnectionGeneration(connectionId);
@@ -2629,7 +2658,7 @@ export const useConnectionStore = defineStore("connection", () => {
const index = decoded?.tableSearchIndex;
const entries = index ? index.entries.map((entry) => ({ name: entry.name, table_type: entry.tableType, ...(entry.comment !== undefined ? { comment: entry.comment } : {}) })) : null;
if (!sidebarTableSearchIndexGenerationIsCurrent(connectionId, cacheKey, connectionGeneration, scopeGeneration)) return null;
sidebarTableSearchIndexCache.set(cacheKey, entries);
setSidebarTableSearchIndexMemoryCache(cacheKey, entries);
return entries;
})();
sidebarTableSearchIndexInFlight.set(cacheKey, read);
@@ -2705,7 +2734,7 @@ export const useConnectionStore = defineStore("connection", () => {
persisted = true;
});
if (!persisted || !sidebarTableSearchIndexGenerationIsCurrent(connectionId, cacheKey, connectionGeneration, scopeGeneration)) return [];
sidebarTableSearchIndexCache.set(cacheKey, deduped);
setSidebarTableSearchIndexMemoryCache(cacheKey, deduped);
await registerSidebarTableSearchIndexScope(parent, cacheKey);
return deduped;
}
@@ -3158,6 +3187,7 @@ export const useConnectionStore = defineStore("connection", () => {
clearConnectionError(id);
connectionErrorRevisions.delete(id);
connectedIds.value.delete(id);
clearSidebarStorageCaches(id);
clearPrimaryVisibleObjectNames(id);
clearConnectionIdentifierQuote(id);
clearConnectionHealthCheck(id);
@@ -3198,6 +3228,7 @@ export const useConnectionStore = defineStore("connection", () => {
if (!runtimeConfigChanged) return;
clearPrimaryVisibleObjectNames(config.id);
connectedIds.value.delete(config.id);
clearSidebarStorageCaches(config.id);
clearConnectionIdentifierQuote(config.id);
clearConnectionHealthCheck(config.id);
invalidateCompletionCache(config.id);
@@ -3692,6 +3723,7 @@ export const useConnectionStore = defineStore("connection", () => {
if (!cancelled) return false;
clearConnectionError(connectionId);
connectedIds.value.delete(connectionId);
clearSidebarStorageCaches(connectionId);
clearPrimaryVisibleObjectNames(connectionId);
clearConnectionIdentifierQuote(connectionId);
clearConnectionHealthCheck(connectionId);
@@ -3711,6 +3743,7 @@ export const useConnectionStore = defineStore("connection", () => {
cancelLocalConnectionAttempt(connectionId);
connectedIds.value.delete(connectionId);
clearSidebarStorageCaches(connectionId);
clearPrimaryVisibleObjectNames(connectionId);
clearConnectionIdentifierQuote(connectionId);
forgetSuccessfulLocalConnectionAttempt(connectionId);
@@ -3797,6 +3830,7 @@ export const useConnectionStore = defineStore("connection", () => {
clearLoadedChildrenCache(node.id);
}
invalidateCompletionCache(connectionId, database);
clearSidebarStorageCaches(connectionId, database);
invalidateObjectBrowserRowsCache({ connectionId, database });
// 数据库级生命周期边界:与连接级断开一致,数据标签页的元数据 freshness
// 戳必须作废,重开/刷新时重新拉取结构(issue #6623)。bump 数据库级代次
@@ -3920,6 +3954,29 @@ export const useConnectionStore = defineStore("connection", () => {
return `${connectionId}\0${[...databases].sort().join("\0")}`;
}
function sidebarDatabaseStorageCacheScope(connectionId: string, databases: readonly string[]): MetadataScopeInput {
return { kind: "sidebar-database-storage", connectionId, extra: { databases: [...databases].sort() } };
}
function sidebarTableStorageCacheScope(scope: SidebarTableStorageScope): MetadataScopeInput {
return { kind: "sidebar-table-storage", connectionId: scope.connectionId, database: scope.database, schema: scope.schema };
}
function clearSidebarStorageCaches(connectionId: string, database?: string) {
if (database === undefined) sidebarDatabaseStorageCache.invalidate({ connectionId });
sidebarTableStorageCache.invalidate(database === undefined ? { connectionId } : { connectionId, database });
const connectionPrefix = `${connectionId}\0`;
if (database === undefined) {
for (const key of sidebarDatabaseStorageInFlight.keys()) {
if (key.startsWith(connectionPrefix)) sidebarDatabaseStorageInFlight.delete(key);
}
}
const tablePrefix = database === undefined ? connectionPrefix : `${connectionId}\0${database}\0`;
for (const key of sidebarTableStorageInFlight.keys()) {
if (key.startsWith(tablePrefix)) sidebarTableStorageInFlight.delete(key);
}
}
async function loadSidebarDatabaseStorage(connectionId: string, options?: { force?: boolean }): Promise<void> {
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "size" || !connectedIds.value.has(connectionId)) return;
if (!supportsSidebarDatabaseStorage(getConfig(connectionId))) return;
@@ -3928,8 +3985,9 @@ export const useConnectionStore = defineStore("connection", () => {
if (!databases.length) return;
const requestKey = sidebarDatabaseStorageRequestKey(connectionId, databases);
const cached = sidebarDatabaseStorageCache.get(requestKey);
if (!options?.force && cached && cached.expiresAt > Date.now()) {
const cacheScope = sidebarDatabaseStorageCacheScope(connectionId, databases);
const cached = sidebarDatabaseStorageCache.get(cacheScope);
if (!options?.force && cached) {
applySidebarDatabaseStorage(connectionNode?.children, cached.value);
return;
}
@@ -3941,10 +3999,8 @@ export const useConnectionStore = defineStore("connection", () => {
}
try {
const storage = await request;
sidebarDatabaseStorageCache.set(requestKey, {
expiresAt: Date.now() + SIDEBAR_DATABASE_STORAGE_CACHE_TTL_MS,
value: storage,
});
if (sidebarDatabaseStorageInFlight.get(requestKey) !== request || !connectedIds.value.has(connectionId)) return;
sidebarDatabaseStorageCache.set(cacheScope, storage);
const currentNode = findConnectionNode(connectionId);
const currentNames = sidebarDatabaseNames(currentNode?.children);
if (sidebarDatabaseStorageRequestKey(connectionId, currentNames) === requestKey) {
@@ -3967,8 +4023,9 @@ export const useConnectionStore = defineStore("connection", () => {
if (settingsStore.editorSettings.sidebarObjectInfoMode !== "size" || !connectedIds.value.has(scope.connectionId)) return;
if (!supportsSidebarTableStorage(getConfig(scope.connectionId))) return;
const requestKey = sidebarTableStorageRequestKey(scope);
const cached = sidebarTableStorageCache.get(requestKey);
if (!options?.force && cached && cached.expiresAt > Date.now()) {
const cacheScope = sidebarTableStorageCacheScope(scope);
const cached = sidebarTableStorageCache.get(cacheScope);
if (!options?.force && cached) {
applySidebarTableStorage(treeNodes.value, scope, cached.value);
return;
}
@@ -3980,11 +4037,8 @@ export const useConnectionStore = defineStore("connection", () => {
}
try {
const statistics = await request;
if (sidebarTableStorageInFlight.get(requestKey) !== request) return;
sidebarTableStorageCache.set(requestKey, {
expiresAt: Date.now() + SIDEBAR_DATABASE_STORAGE_CACHE_TTL_MS,
value: statistics,
});
if (sidebarTableStorageInFlight.get(requestKey) !== request || !connectedIds.value.has(scope.connectionId)) return;
sidebarTableStorageCache.set(cacheScope, statistics);
applySidebarTableStorage(treeNodes.value, scope, statistics);
} catch (error) {
console.debug("[DBX][sidebar-table-storage:unavailable]", { ...scope, error });
+57 -2
View File
@@ -57,6 +57,7 @@ import { elasticsearchRestRequestRanges, executableStatementRanges, splitSqlStat
import { replaceSqlServerLeadingUseQuery, sqlServerLeadingUseScript, sqlServerUseDatabaseFromStatement } from "@/lib/sql/sqlCompletionLookupTarget";
import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen";
import { clearDataGridPendingSnapshot, clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor";
import { clearDataGridStructuredFilterStatesForTab } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
import { buildTabResultSnapshot, deleteTabResultSnapshot, pruneTabResultSnapshots, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
import { estimateQueryResultsBytes, selectInactiveResultEvictions } from "@/lib/tabs/queryResultSize";
import { queryResultBaseSql, queryResultExecutionSql, resultGridInstanceKey } from "@/lib/tabs/tabPresentation";
@@ -259,6 +260,27 @@ function markQueryResultRunsRowsRaw(resultRuns: NonNullable<QueryTab["resultRuns
return resultRuns;
}
/** Drop heavyweight fields even when a deactivated DataGrid still retains the result object. */
function releaseResultObjectPayload(result: QueryResult): void {
result.columns = [];
result.rows = [];
result.column_types = undefined;
result.column_sortables = undefined;
result.spatial_columns = undefined;
result.spatial_values = undefined;
result.hidden_column_indexes = undefined;
result.local_column_filters = undefined;
result.local_hidden_column_keys = undefined;
result.mongo_documents = undefined;
result.mongo_copy_documents = undefined;
result.large_value_cells = undefined;
result.elasticsearch_raw_body = undefined;
result.messages = undefined;
result.error = undefined;
result.sourceLabel = undefined;
result.sourceStatement = undefined;
}
function preservedResultIndex(results: QueryResult[], currentIndex: number | undefined, preserve: boolean | undefined): number | undefined {
if (preserve !== true || typeof currentIndex !== "number" || currentIndex < 0 || currentIndex >= results.length) return undefined;
return currentIndex;
@@ -1029,7 +1051,26 @@ export const useQueryStore = defineStore("query", () => {
}
}
function releaseTabResultObjectPayloads(tab: QueryTab) {
const results = new Set<QueryResult>();
if (tab.result) results.add(tab.result);
for (const result of tab.results ?? []) results.add(result);
for (const run of tab.resultRuns ?? []) {
if (run.result) results.add(run.result);
for (const result of run.results ?? []) results.add(result);
}
for (const result of results) releaseResultObjectPayload(result);
}
function clearResultRuns(tab: QueryTab) {
for (const run of tab.resultRuns ?? []) clearResultRunPayload(run);
tab.resultRuns = undefined;
tab.activeResultRunId = undefined;
}
function clearResultRunPayload(run: NonNullable<QueryTab["resultRuns"]>[number], options: { evicted?: boolean } = {}) {
if (run.result) releaseResultObjectPayload(run.result);
for (const result of run.results ?? []) releaseResultObjectPayload(result);
run.result = undefined;
run.results = undefined;
run.resultLocalSortOriginalRows = undefined;
@@ -1224,6 +1265,7 @@ export const useQueryStore = defineStore("query", () => {
const removedRun = tab.resultRuns[runIndex];
if (removedRun?.resultSessionId) void closeResultRunSession(tab, removedRun);
if (removedRun?.resultCacheKey) void deleteTabResultSnapshot(removedRun.resultCacheKey);
if (removedRun) clearResultRunPayload(removedRun);
const wasActive = tab.activeResultRunId === runId;
const remainingRuns = tab.resultRuns.filter((run) => run.id !== runId);
tab.resultRuns = remainingRuns;
@@ -1250,6 +1292,7 @@ export const useQueryStore = defineStore("query", () => {
if (!tab.result && !tab.results?.length && !tab.resultEvicted) return false;
const closeSession = closeResultSession(tab);
releaseTabResultObjectPayloads(tab);
clearResultPayload(tab);
await closeSession;
return true;
@@ -1274,8 +1317,8 @@ export const useQueryStore = defineStore("query", () => {
closeOperations.push(closeResultRunSession(tab, run));
}
tab.resultRuns = undefined;
tab.activeResultRunId = undefined;
releaseTabResultObjectPayloads(tab);
clearResultRuns(tab);
clearResultPayload(tab);
await Promise.all(closeOperations);
return true;
@@ -2627,6 +2670,7 @@ export const useQueryStore = defineStore("query", () => {
persistSavedSqlEditorPosition(tabs.value[idx]);
if (tab.mode === "sqlserver-trace") void disposeSqlServerActivityTrace(tab.id);
clearDataGridPendingSnapshotsForTab(id);
clearDataGridStructuredFilterStatesForTab(id);
if (tabs.value[idx].txnSessionId) void rollbackTransaction(id);
if (tabs.value[idx].isExecuting) void cancelTabExecution(id);
if (tabs.value[idx].isExplaining) void cancelTabExplain(id);
@@ -2634,6 +2678,8 @@ export const useQueryStore = defineStore("query", () => {
void closeClientConnectionSession(tabs.value[idx]);
clearResultRunSnapshots(tabs.value[idx]);
void deleteTabResultSnapshot(tabResultCacheKey(id));
releaseTabResultObjectPayloads(tabs.value[idx]);
clearResultRuns(tabs.value[idx]);
clearResultPayload(tabs.value[idx]);
tabs.value.splice(idx, 1);
if (tab.externalSqlPath) refreshExternalSqlFileTitles();
@@ -2922,6 +2968,7 @@ export const useQueryStore = defineStore("query", () => {
.forEach((tab) => {
if (tab.mode === "sqlserver-trace") void disposeSqlServerActivityTrace(tab.id);
clearDataGridPendingSnapshotsForTab(tab.id);
clearDataGridStructuredFilterStatesForTab(tab.id);
if (tab.txnSessionId) void rollbackTransaction(tab.id);
if (tab.isExecuting) void cancelTabExecution(tab.id);
if (tab.isExplaining) void cancelTabExplain(tab.id);
@@ -2929,6 +2976,8 @@ export const useQueryStore = defineStore("query", () => {
void closeClientConnectionSession(tab);
clearResultRunSnapshots(tab);
void deleteTabResultSnapshot(tabResultCacheKey(tab.id));
releaseTabResultObjectPayloads(tab);
clearResultRuns(tab);
clearResultPayload(tab);
});
@@ -3104,10 +3153,16 @@ export const useQueryStore = defineStore("query", () => {
.filter((tab) => predicate(tab))
.forEach((tab) => {
rollbackTabTransaction(tab, { resetAutoCommit: true });
clearDataGridPendingSnapshotsForTab(tab.id);
clearDataGridStructuredFilterStatesForTab(tab.id);
if (tab.isExecuting) void cancelTabExecution(tab.id);
if (tab.isExplaining) void cancelTabExplain(tab.id);
void closeResultSession(tab);
void closeClientConnectionSession(tab);
clearResultRunSnapshots(tab);
void deleteTabResultSnapshot(tabResultCacheKey(tab.id));
releaseTabResultObjectPayloads(tab);
clearResultRuns(tab);
clearResultPayload(tab);
});
}