import assert from "node:assert/strict"; import { computed } from "vue"; import { createPinia, setActivePinia } from "pinia"; import { test, vi } from "vitest"; import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts"; import { useQueryStore } from "../../apps/desktop/src/stores/queryStore.ts"; import type { ColumnInfo, ConnectionConfig } from "../../apps/desktop/src/types/database.ts"; vi.mock("vue-i18n", async () => { const actual = await vi.importActual("vue-i18n"); return { ...actual, useI18n: () => ({ t: (key: string) => key }), }; }); vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }), })); function installMemoryStorage() { const values = new Map(); 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"); }; } function conn(id: string): ConnectionConfig { return { id, name: id, db_type: "mysql", host: "localhost", port: 3306, username: "root", password: "", }; } async function waitFor(predicate: () => boolean, timeoutMs = 1000) { const started = Date.now(); while (!predicate()) { if (Date.now() - started > timeoutMs) throw new Error("timed out waiting for condition"); await new Promise((resolve) => setTimeout(resolve, 5)); } } test("data reload executes before slow metadata refresh completes", async () => { const restoreStorage = installMemoryStorage(); const originalFetch = globalThis.fetch; const { useDataGridActions } = await import("../../apps/desktop/src/composables/useDataGridActions.ts"); let executeBody: any; let resolveColumns!: (columns: ColumnInfo[]) => void; globalThis.fetch = (async (input, init) => { const url = new URL(String(input), "http://localhost"); if (url.pathname === "/api/connection/check-health") { return Response.json(null); } if (url.pathname === "/api/schema/columns") { return Response.json(await new Promise((resolve) => (resolveColumns = resolve))); } if (url.pathname === "/api/schema/indexes") { return Response.json([]); } if (url.pathname === "/api/query/build-table-select-sql") { return Response.json("SELECT * FROM `users` LIMIT 50 OFFSET 0"); } if (url.pathname === "/api/query/execute-multi") { executeBody = JSON.parse(String(init?.body ?? "{}")); return Response.json([{ columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]); } return new Response(`unexpected ${url.pathname}`, { status: 500 }); }) as typeof fetch; try { setActivePinia(createPinia()); const connectionStore = useConnectionStore(); const queryStore = useQueryStore(); connectionStore.addEphemeralConnection(conn("mysql-1")); const tabId = queryStore.createTab("mysql-1", "app", "users", "data"); queryStore.setTableMeta(tabId, { tableName: "users", tableType: "TABLE", columns: [], primaryKeys: [] }); const tab = queryStore.tabs.find((item) => item.id === tabId); assert.ok(tab); const actions = useDataGridActions(computed(() => tab)); const reload = actions.onReloadData(undefined, undefined, undefined, undefined, 50, 0); await waitFor(() => !!executeBody, 5_000); assert.equal(executeBody.clientSessionId, tabId); assert.deepEqual(tab.result?.rows, [[1]]); await waitFor(() => typeof resolveColumns === "function"); resolveColumns([ { name: "id", data_type: "int", is_nullable: false, column_default: null, is_primary_key: true, extra: null, }, ]); await reload; await waitFor(() => tab.tableMeta?.columns.length === 1); assert.equal(tab.tableMeta?.primaryKeys[0], "id"); } finally { globalThis.fetch = originalFetch; restoreStorage(); } }); test("data reload preserves current page offset instead of resetting to page 1", async () => { // Verifies the fix: onReloadData called with offset=400 (page 5, pageSize=100) // should pass offset=400 to build-table-select-sql and store it in resultPageOffset, // rather than defaulting to offset=0. const restoreStorage = installMemoryStorage(); const originalFetch = globalThis.fetch; const { useDataGridActions } = await import("../../apps/desktop/src/composables/useDataGridActions.ts"); let buildSqlOptions: any; let executePagination: any; globalThis.fetch = (async (input, init) => { const url = new URL(String(input), "http://localhost"); if (url.pathname === "/api/connection/check-health") return Response.json(null); if (url.pathname === "/api/schema/columns") return Response.json([]); if (url.pathname === "/api/schema/indexes") return Response.json([]); if (url.pathname === "/api/query/build-table-select-sql") { buildSqlOptions = JSON.parse(String(init?.body ?? "{}"))?.options; return Response.json(`SELECT * FROM \`orders\` LIMIT 100 OFFSET ${buildSqlOptions?.offset ?? 0}`); } if (url.pathname === "/api/query/execute-multi") { const body = JSON.parse(String(init?.body ?? "{}")); executePagination = body?.page_offset; return Response.json([{ columns: ["id"], rows: [[42]], affected_rows: 0, execution_time_ms: 1 }]); } return new Response(`unexpected ${url.pathname}`, { status: 500 }); }) as typeof fetch; try { setActivePinia(createPinia()); const connectionStore = useConnectionStore(); const queryStore = useQueryStore(); connectionStore.addEphemeralConnection(conn("mysql-1")); const tabId = queryStore.createTab("mysql-1", "app", "orders", "data"); queryStore.setTableMeta(tabId, { tableName: "orders", tableType: "TABLE", columns: [], primaryKeys: [] }); const tab = queryStore.tabs.find((item) => item.id === tabId); assert.ok(tab); const actions = useDataGridActions(computed(() => tab)); // Simulate refresh from page 5 with pageSize=100 → offset=400 await actions.onReloadData(undefined, undefined, undefined, undefined, 100, 400); assert.equal(buildSqlOptions?.offset, 400, "build-table-select-sql must receive offset=400 to stay on page 5"); assert.equal(buildSqlOptions?.limit, 100, "limit must be 100"); assert.equal(tab.resultPageOffset, 400, "resultPageOffset must be 400 after reload to keep DataGrid on page 5"); } finally { globalThis.fetch = originalFetch; restoreStorage(); } }); test("query toolbar refresh reruns the complete multi-result SQL and keeps the active result", async () => { const restoreStorage = installMemoryStorage(); const { useDataGridActions } = await import("../../apps/desktop/src/composables/useDataGridActions.ts"); try { setActivePinia(createPinia()); const queryStore = useQueryStore(); const tabId = queryStore.createTab("mysql-1", "app", "query", "query"); const tab = queryStore.tabs.find((item) => item.id === tabId); assert.ok(tab); const batchSql = "select * from users; select * from orders"; const refreshedResults = [ { columns: ["id"], rows: [[11]], affected_rows: 0, execution_time_ms: 1, sourceStatement: "select * from users" }, { columns: ["id"], rows: [[22]], affected_rows: 0, execution_time_ms: 1, sourceStatement: "select * from orders" }, ]; tab.sql = batchSql; tab.lastExecutedSql = batchSql; tab.resultBaseSql = batchSql; tab.results = refreshedResults.map((result) => ({ ...result, rows: result.rows.map((row) => [...row]) })); tab.activeResultIndex = 1; tab.result = tab.results[1]; tab.resultSortColumn = "id"; tab.resultSortColumnIndex = 0; tab.resultSortDirection = "desc"; tab.resultSortMode = "database"; tab.resultSortedSql = "select * from orders order by id desc"; const executeTabSql = vi.spyOn(queryStore, "executeTabSql").mockImplementation(async (_tabId, _sql, options) => { assert.equal(tab.resultSortColumn, undefined); assert.equal(tab.resultSortColumnIndex, undefined); assert.equal(tab.resultSortDirection, undefined); assert.equal(tab.resultSortMode, undefined); assert.equal(tab.resultSortedSql, undefined); tab.results = refreshedResults; if (!options?.preserveActiveResultIndex) tab.activeResultIndex = 0; tab.result = refreshedResults[tab.activeResultIndex ?? 0]; }); const actions = useDataGridActions(computed(() => tab)); await actions.onReloadData("select * from orders", undefined, undefined, undefined, 100, 0, "refresh"); await actions.onReloadData("select * from orders", undefined, undefined, undefined, 100, 0, "refresh"); assert.equal(executeTabSql.mock.calls.length, 2); for (const call of executeTabSql.mock.calls) { assert.equal(call[1], batchSql); assert.deepEqual(call[2], { resultBaseSql: batchSql, resultSortedSql: undefined, preserveResultDuringExecution: true, preserveActiveResultIndex: true, }); } assert.equal(tab.results?.length, 2); assert.equal(tab.activeResultIndex, 1); assert.deepEqual(tab.result, refreshedResults[1]); } finally { restoreStorage(); } });