Improve memory retention in query results (#395)

This commit is contained in:
SuLea-IT 2026-05-22 10:42:58 +08:00 committed by GitHub
parent e1fc3a4fc1
commit b23988161a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 114 additions and 21 deletions

View File

@ -103,7 +103,7 @@ const showSettingsDialog = ref(false);
const showDriverStore = ref(false);
const agentDriverUpdateCount = ref(0);
const showHistory = ref(false);
const showAiPanel = ref(localStorage.getItem("dbx-ai-panel-open") !== "false");
const showAiPanel = ref(localStorage.getItem("dbx-ai-panel-open") === "true");
const aiPanelReady = ref(false);
const { sidebarWidth, aiPanelWidth, historyWidth, startSidebarResize, startAiPanelResize, startHistoryResize } =
usePanelResize();

View File

@ -62,6 +62,22 @@ export const useQueryStore = defineStore("query", () => {
}
}
function clearResultPayload(tab: QueryTab, options: { evicted?: boolean } = {}) {
tab.result = undefined;
tab.results = undefined;
tab.activeResultIndex = undefined;
tab.resultSessionId = undefined;
tab.queryAnalysis = undefined;
tab.querySourceColumns = undefined;
tab.queryEditabilityReason = undefined;
tab.resultEvicted = options.evicted ? true : undefined;
}
async function evictCachedResult(tab: QueryTab) {
await closeResultSession(tab);
clearResultPayload(tab, { evicted: true });
}
const _persistSnapshot = computed(() =>
tabs.value.map((t) => ({
id: t.id,
@ -164,8 +180,7 @@ export const useQueryStore = defineStore("query", () => {
if (tabs.value[idx].isExecuting) void cancelTabExecution(id);
if (tabs.value[idx].isExplaining) void cancelTabExplain(id);
void closeResultSession(tabs.value[idx]);
tabs.value[idx].result = undefined;
tabs.value[idx].results = undefined;
clearResultPayload(tabs.value[idx]);
tabs.value.splice(idx, 1);
if (activeTabId.value === id) {
activeTabId.value = tabs.value[Math.min(idx, tabs.value.length - 1)]?.id ?? null;
@ -248,13 +263,11 @@ export const useQueryStore = defineStore("query", () => {
tab.database = database;
tab.schema = undefined;
tab.objectBrowser = undefined;
tab.result = undefined;
void closeResultSession(tab);
clearResultPayload(tab);
tab.lastExecutedSql = undefined;
tab.resultBaseSql = undefined;
tab.resultSortedSql = undefined;
tab.queryAnalysis = undefined;
tab.querySourceColumns = undefined;
tab.queryEditabilityReason = undefined;
clearExplain(tab);
tab.tableMeta = undefined;
}
@ -272,13 +285,11 @@ export const useQueryStore = defineStore("query", () => {
tab.connectionId = connectionId;
tab.database = database;
tab.schema = undefined;
tab.result = undefined;
void closeResultSession(tab);
clearResultPayload(tab);
tab.lastExecutedSql = undefined;
tab.resultBaseSql = undefined;
tab.resultSortedSql = undefined;
tab.queryAnalysis = undefined;
tab.querySourceColumns = undefined;
tab.queryEditabilityReason = undefined;
clearExplain(tab);
tab.tableMeta = undefined;
}
@ -325,6 +336,9 @@ export const useQueryStore = defineStore("query", () => {
const tab = tabs.value.find((t) => t.id === id);
if (!tab) return;
tab.result = toErrorResult(e);
tab.results = undefined;
tab.activeResultIndex = undefined;
tab.resultSessionId = undefined;
tab.isExecuting = false;
tab.isCancelling = false;
tab.executionId = undefined;
@ -608,7 +622,7 @@ export const useQueryStore = defineStore("query", () => {
});
}
}
trimResultCache();
await trimResultCache();
}
async function explainTabSql(id: string, sql: string, databaseType?: DatabaseType) {
@ -707,17 +721,11 @@ export const useQueryStore = defineStore("query", () => {
tab.queryEditabilityReason = undefined;
}
function trimResultCache() {
const inactive = tabs.value.filter((t) => t.id !== activeTabId.value && t.result);
async function trimResultCache() {
const inactive = tabs.value.filter((t) => t.id !== activeTabId.value && (t.result || t.results));
if (inactive.length > MAX_CACHED_RESULTS) {
const toEvict = inactive.slice(0, inactive.length - MAX_CACHED_RESULTS);
toEvict.forEach((t) => {
t.result = undefined;
t.resultEvicted = true;
t.queryAnalysis = undefined;
t.querySourceColumns = undefined;
t.queryEditabilityReason = undefined;
});
await Promise.all(toEvict.map((t) => evictCachedResult(t)));
}
}

View File

@ -2,6 +2,25 @@ import { strict as assert } from "node:assert";
import test from "node:test";
import { createPinia, setActivePinia } from "pinia";
import { useQueryStore } from "../../apps/desktop/src/stores/queryStore.ts";
import type { QueryResult } from "../../apps/desktop/src/types/database.ts";
function installMemoryStorage() {
const values = new Map<string, string>();
const original = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => values.set(key, value),
removeItem: (key: string) => values.delete(key),
clear: () => values.clear(),
},
});
return () => {
if (original) Object.defineProperty(globalThis, "localStorage", original);
else Reflect.deleteProperty(globalThis, "localStorage");
};
}
test("setErrorResult stops loading and shows the error result", () => {
setActivePinia(createPinia());
@ -18,3 +37,65 @@ test("setErrorResult stops loading and shows the error result", () => {
assert.deepEqual(tab?.result?.columns, ["Error"]);
assert.deepEqual(tab?.result?.rows, [["Error: metadata failed"]]);
});
test("evicting cached tab results releases multi-result payloads and sessions", async () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());
const store = useQueryStore();
const originalFetch = globalThis.fetch;
let executeCount = 0;
const closedSessions: string[] = [];
globalThis.fetch = (async (input, init) => {
const url = String(input);
if (url === "/api/query/execute-multi") {
executeCount++;
const results: QueryResult[] = [
{
columns: ["id"],
rows: [[executeCount]],
affected_rows: 0,
execution_time_ms: 1,
session_id: `session-${executeCount}`,
},
{
columns: ["detail"],
rows: [[`payload-${executeCount}`]],
affected_rows: 0,
execution_time_ms: 1,
},
];
return new Response(JSON.stringify(results), { status: 200, headers: { "Content-Type": "application/json" } });
}
if (url === "/api/query/close-session") {
const body = JSON.parse(String(init?.body ?? "{}"));
closedSessions.push(body.sessionId);
return new Response(JSON.stringify(true), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response("unexpected request", { status: 500 });
}) as typeof fetch;
try {
const tabIds: string[] = [];
for (let i = 0; i < 7; i++) {
const tabId = store.createTab("conn-1", "db", `Query ${i + 1}`);
tabIds.push(tabId);
await store.executeTabSql(tabId, `select ${i + 1}; select ${i + 1} as detail`);
}
const evicted = store.tabs.find((tab) => tab.id === tabIds[0]);
assert.equal(executeCount, 7);
assert.equal(evicted?.result, undefined);
assert.equal(evicted?.results, undefined);
assert.equal(evicted?.activeResultIndex, undefined);
assert.equal(evicted?.resultSessionId, undefined);
assert.equal(evicted?.resultEvicted, true);
assert.deepEqual(closedSessions, ["session-1"]);
} finally {
globalThis.fetch = originalFetch;
restoreStorage();
}
});

View File

@ -16,6 +16,10 @@ test("app defers cold-start side panels and modal pages behind async components"
}
});
test("AI assistant panel stays closed on first launch to preserve startup memory", () => {
assert.match(appSource, /const showAiPanel = ref\(localStorage\.getItem\("dbx-ai-panel-open"\) === "true"\)/);
});
test("app dialogs keep non-primary dialogs out of the startup chunk", () => {
for (const component of ["ConnectionDialog", "EditorSettingsDialog", "DangerConfirmDialog"]) {
assert.doesNotMatch(appDialogsSource, new RegExp(`import ${component} from`));