diff --git a/apps/desktop/src/stores/__tests__/connectionStore.sidebarTableStorage.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.sidebarTableStorage.spec.ts new file mode 100644 index 0000000000..f91c75e1db --- /dev/null +++ b/apps/desktop/src/stores/__tests__/connectionStore.sidebarTableStorage.spec.ts @@ -0,0 +1,110 @@ +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ConnectionConfig, TreeNode } from "@/types/database"; + +function installLocalStorage() { + const data = new Map(); + vi.stubGlobal("localStorage", { + getItem: vi.fn((key: string) => data.get(key) ?? null), + setItem: vi.fn((key: string, value: string) => data.set(key, value)), + removeItem: vi.fn((key: string) => data.delete(key)), + }); +} + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +describe("connectionStore sidebar table storage", () => { + beforeEach(() => { + vi.resetModules(); + vi.unstubAllGlobals(); + installLocalStorage(); + setActivePinia(createPinia()); + }); + + it("reloads table sizes after refreshing an object list with a newly created table", async () => { + const listTables = vi.fn().mockResolvedValue([ + { name: "EXISTING_TABLE", table_type: "BASE TABLE", comment: null }, + { name: "NEW_TABLE", table_type: "BASE TABLE", comment: null }, + ]); + const initialStatistics = deferred>(); + const refreshedStatistics = deferred>(); + const listObjectStatistics = vi.fn().mockReturnValueOnce(initialStatistics.promise).mockReturnValueOnce(refreshedStatistics.promise); + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + listInstalledAgents: vi.fn().mockResolvedValue([]), + listObjects: vi.fn().mockResolvedValue([]), + listObjectStatistics, + listTables, + loadSchemaCache: vi.fn().mockResolvedValue(null), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveSchemaCache: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const { useSettingsStore } = await import("@/stores/settingsStore"); + const store = useConnectionStore(); + const settingsStore = useSettingsStore(); + settingsStore.editorSettings.sidebarObjectDisplay = "simple"; + settingsStore.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 existingTable: TreeNode = { + id: "oracle-1:ORCL:APP:EXISTING_TABLE", + label: "EXISTING_TABLE", + type: "table", + connectionId: connection.id, + database: connection.database, + schema: "APP", + }; + const schemaNode: TreeNode = { + id: "oracle-1:ORCL:APP", + label: "APP", + type: "schema", + connectionId: connection.id, + database: connection.database, + schema: "APP", + isExpanded: true, + children: [existingTable], + }; + store.connections = [connection]; + store.connectedIds.add(connection.id); + store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [schemaNode] }]; + + const initialLoad = store.loadSidebarTableStorage({ connectionId: connection.id, database: connection.database, schema: "APP" }); + expect(listObjectStatistics).toHaveBeenCalledTimes(1); + + await store.refreshObjectListTreeNode(connection.id, connection.database, "APP"); + expect(listObjectStatistics).toHaveBeenCalledTimes(2); + + const currentExistingTable = schemaNode.children?.find((node) => node.label === "EXISTING_TABLE"); + const newTable = schemaNode.children?.find((node) => node.label === "NEW_TABLE"); + refreshedStatistics.resolve([ + { name: "EXISTING_TABLE", schema: "APP", total_bytes: 8192 }, + { name: "NEW_TABLE", schema: "APP", total_bytes: 16384 }, + ]); + await vi.waitFor(() => expect(newTable?.sizeBytes).toBe(16384)); + + initialStatistics.resolve([{ name: "EXISTING_TABLE", schema: "APP", total_bytes: 4096 }]); + await initialLoad; + expect(currentExistingTable?.sizeBytes).toBe(8192); + expect(newTable?.sizeBytes).toBe(16384); + }); +}); diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 9778f55245..ae323886ba 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -2948,13 +2948,14 @@ export const useConnectionStore = defineStore("connection", () => { return; } - let request = sidebarTableStorageInFlight.get(requestKey); + let request = options?.force ? undefined : sidebarTableStorageInFlight.get(requestKey); if (!request) { request = api.listObjectStatistics(scope.connectionId, scope.database, scope.schema); sidebarTableStorageInFlight.set(requestKey, request); } 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, @@ -5264,9 +5265,10 @@ export const useConnectionStore = defineStore("connection", () => { const node = shouldRefreshSchemaNode ? findNode(treeNodes.value, `${connectionId}:${database}:${schema}`) : null; if (node) { await refreshTreeNode(node); - return; + } else { + await refreshDatabaseTreeNode(connectionId, database, catalog); } - await refreshDatabaseTreeNode(connectionId, database, catalog); + void loadSidebarTableStorage({ connectionId, database, schema: schema || "" }, { force: true }); } function isSchemaAwareDatabase(connectionId: string): boolean {