diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index afb0660bb..dac69e456 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -2555,9 +2555,9 @@ export const useQueryStore = defineStore("query", () => { executionPromise = api.executeInManualTransaction(tab.txnSessionId, sqlToExecute, tab.database, executionSchema, pageLimit); } else { console.info("[DBX][executeTabSql:execute-multi:start]", { traceId, elapsed: elapsed() }); - // Data tabs should reuse the already-open pool; session pools are reserved - // for query tabs/background tasks that need connection-local state isolation. - const clientSessionId = tab.mode === "query" ? tabClientSessionId(tab) : undefined; + // Query and data tabs use a tab-scoped pool so repeated executions keep + // connection-local state and avoid MySQL pool resets on every refresh. + const clientSessionId = tab.mode === "query" || tab.mode === "data" ? tabClientSessionId(tab) : undefined; const executionOptions = { ...(typeof pageLimit === "number" ? useAgentResultSession diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 9f8aea6ba..2499cc092 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -3515,7 +3515,7 @@ test("query execution is scoped to the tab client session", async () => { } }); -test("data tab execution reuses the shared connection pool", async () => { +test("data tab execution uses a tab-scoped client session", async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia()); const connectionStore = useConnectionStore(); @@ -3541,7 +3541,7 @@ test("data tab execution reuses the shared connection pool", async () => { try { await store.executeTabSql(tabId, "select * from users"); - assert.equal(executeBody.clientSessionId, undefined); + assert.equal(executeBody.clientSessionId, tabId); assert.equal(executeBody.timeoutSecs, 30); } finally { globalThis.fetch = originalFetch; @@ -3549,6 +3549,55 @@ test("data tab execution reuses the shared connection pool", async () => { } }); +test("closing a data tab releases its tab-scoped client session", 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", "users", "data", "public"); + let executeBody: any; + const closedSessions: any[] = []; + + globalThis.fetch = withConnectionHealthMock(async (input, init) => { + const url = String(input); + if (url === "/api/query/execute-multi") { + executeBody = JSON.parse(String(init?.body ?? "{}")); + return new Response(JSON.stringify([{ columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + if (url === "/api/query/close-client-session") { + closedSessions.push(JSON.parse(String(init?.body ?? "{}"))); + return new Response(JSON.stringify(true), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response("unexpected request", { status: 500 }); + }); + + try { + await store.executeTabSql(tabId, "select * from users"); + assert.equal(executeBody.clientSessionId, tabId); + + store.closeTab(tabId, { force: true }); + + // closeClientConnectionSession is fire-and-forget; wait for the request to land. + await waitFor(() => closedSessions.some((body) => body.clientSessionId === tabId)); + assert.ok( + closedSessions.some((body) => body.clientSessionId === tabId && body.connectionId === "conn-1"), + `expected close-client-session for tab session, got ${JSON.stringify(closedSessions)}`, + ); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + test("query execution keeps automatically counting total rows in the background", async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia()); diff --git a/packages/app-tests/useDataGridActions.test.ts b/packages/app-tests/useDataGridActions.test.ts index 514372eb8..0669cd4b8 100644 --- a/packages/app-tests/useDataGridActions.test.ts +++ b/packages/app-tests/useDataGridActions.test.ts @@ -97,8 +97,8 @@ test("data reload executes before slow metadata refresh completes", async () => const actions = useDataGridActions(computed(() => tab)); const reload = actions.onReloadData(undefined, undefined, undefined, undefined, 50, 0); - await waitFor(() => !!executeBody); - assert.equal(executeBody.clientSessionId, undefined); + await waitFor(() => !!executeBody, 5_000); + assert.equal(executeBody.clientSessionId, tabId); assert.deepEqual(tab.result?.rows, [[1]]); await waitFor(() => typeof resolveColumns === "function");