From d8f2bce320733776dc1515f5587e0f4eb5387ece Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sun, 31 May 2026 13:45:53 +0800 Subject: [PATCH] fix(query): release stale result payloads --- apps/desktop/src/stores/queryStore.ts | 28 ++++++--- packages/app-tests/queryStore.test.ts | 91 +++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index b21b5ebbc8..456d953395 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -266,20 +266,28 @@ export const useQueryStore = defineStore("query", () => { } function closeOtherTabs(id: string) { - tabs.value.filter((tab) => tab.id !== id && tab.isExecuting).forEach((tab) => void cancelTabExecution(tab.id)); - tabs.value.filter((tab) => tab.id !== id && tab.isExplaining).forEach((tab) => void cancelTabExplain(tab.id)); - tabs.value.filter((tab) => tab.id !== id).forEach((tab) => void closeResultSession(tab)); - tabs.value.filter((tab) => tab.id !== id).forEach((tab) => void closeClientConnectionSession(tab)); + tabs.value + .filter((tab) => tab.id !== id) + .forEach((tab) => { + if (tab.isExecuting) void cancelTabExecution(tab.id); + if (tab.isExplaining) void cancelTabExplain(tab.id); + void closeResultSession(tab); + void closeClientConnectionSession(tab); + clearResultPayload(tab); + }); const next = closeOtherTabsState(tabs.value, activeTabId.value, id); tabs.value = next.tabs; activeTabId.value = next.activeTabId; } function closeAllTabs() { - tabs.value.filter((tab) => tab.isExecuting).forEach((tab) => void cancelTabExecution(tab.id)); - tabs.value.filter((tab) => tab.isExplaining).forEach((tab) => void cancelTabExplain(tab.id)); - tabs.value.forEach((tab) => void closeResultSession(tab)); - tabs.value.forEach((tab) => void closeClientConnectionSession(tab)); + tabs.value.forEach((tab) => { + if (tab.isExecuting) void cancelTabExecution(tab.id); + if (tab.isExplaining) void cancelTabExplain(tab.id); + void closeResultSession(tab); + void closeClientConnectionSession(tab); + clearResultPayload(tab); + }); const next = closeAllTabsState(tabs.value, activeTabId.value); tabs.value = next.tabs; activeTabId.value = next.activeTabId; @@ -545,6 +553,8 @@ export const useQueryStore = defineStore("query", () => { tab.executionId = executionId; tab.lastExecutedSql = sql; tab.resultTotalRowCount = undefined; + const previousResultSessionClose = closeResultSession(tab, options?.pagination?.sessionId); + clearResultPayload(tab); console.info("[DBX][executeTabSql:start]", { traceId, tabId: id, @@ -573,7 +583,7 @@ export const useQueryStore = defineStore("query", () => { ? conn.query_timeout_secs : 30; const settingsStore = useSettingsStore(); - await closeResultSession(tab, options?.pagination?.sessionId); + await previousResultSessionClose; if (tab.mode === "query") { const pagination = options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 }; const plan = await api.prepareQueryPaginationExecutionPlan({ diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 5294f88d5a..eac1b1c919 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -136,6 +136,97 @@ test("evicting cached tab results releases multi-result payloads and sessions", } }); +test("closing tabs clears removed result payloads before dropping tab references", async () => { + const restoreStorage = installMemoryStorage(); + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => { + return new Response(JSON.stringify(true), { status: 200, headers: { "Content-Type": "application/json" } }); + }) as typeof fetch; + try { + setActivePinia(createPinia()); + const store = useQueryStore(); + const keepId = store.createTab("conn-1", "db", "keep"); + const closeId = store.createTab("conn-1", "db", "close"); + const closingTab = store.tabs.find((item) => item.id === closeId); + + assert.ok(closingTab); + closingTab.result = { + columns: ["payload"], + rows: [[new Array(10_000).fill("x").join("")]], + affected_rows: 0, + execution_time_ms: 1, + session_id: "session-close", + }; + closingTab.results = [closingTab.result]; + closingTab.activeResultIndex = 0; + closingTab.resultSessionId = "session-close"; + + store.closeOtherTabs(keepId); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.equal(closingTab.result, undefined); + assert.equal(closingTab.results, undefined); + assert.equal(closingTab.activeResultIndex, undefined); + assert.equal(closingTab.resultSessionId, undefined); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + +test("starting a new query clears the previous result payload immediately", async () => { + const restoreStorage = installMemoryStorage(); + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + const store = useQueryStore(); + const originalFetch = globalThis.fetch; + + connectionStore.addEphemeralConnection(conn("conn-1")); + const tabId = store.createTab("conn-1", "db", "Query"); + const tab = store.tabs.find((item) => item.id === tabId); + assert.ok(tab); + tab.result = { + columns: ["old"], + rows: [[new Array(10_000).fill("old").join("")]], + affected_rows: 0, + execution_time_ms: 1, + }; + + globalThis.fetch = (async (input) => { + const url = String(input); + if (url === "/api/query/prepare-pagination-plan") { + return new Response(JSON.stringify({ sqlToExecute: "select 1", useAgentResultSession: false }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + if (url === "/api/query/execute-multi") { + return new Response( + JSON.stringify([{ columns: ["new"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + if (url === "/api/query/analyze-editability") { + return new Response(JSON.stringify({ editable: false, reason: "complex-source" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response("unexpected request", { status: 500 }); + }) as typeof fetch; + + try { + const execution = store.executeTabSql(tabId, "select 1"); + assert.equal(tab.result, undefined); + assert.equal(tab.results, undefined); + await execution; + assert.deepEqual(tab.result?.columns, ["new"]); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + test("tab reuse is scoped by mode and schema instead of title alone", () => { const restoreStorage = installMemoryStorage(); try {