From cb754cd0cc8bd33ab8ff15ccb1393d004f479cad Mon Sep 17 00:00:00 2001 From: jearhe <0verme7753@gmail.com> Date: Tue, 1 Sep 2026 16:17:54 +0800 Subject: [PATCH] fix(grid): preserve local result filters --- apps/desktop/src/lib/tabs/tabResultCache.ts | 9 +++ .../__tests__/queryStore.switchTab.spec.ts | 32 +++++++++ packages/app-tests/queryStore.test.ts | 2 + packages/app-tests/tabResultCache.test.ts | 70 ++++++++++++++++++- 4 files changed, 112 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/tabs/tabResultCache.ts b/apps/desktop/src/lib/tabs/tabResultCache.ts index 3af0b1cf22..c104e71ea3 100644 --- a/apps/desktop/src/lib/tabs/tabResultCache.ts +++ b/apps/desktop/src/lib/tabs/tabResultCache.ts @@ -55,6 +55,7 @@ interface ColumnarQueryResult { execution_error?: true; statement_index?: number; column_types?: string[]; + local_column_filters?: QueryResult["local_column_filters"]; columnValues: CellValue[][]; rowCount: number; mongo_documents?: unknown[]; @@ -327,6 +328,10 @@ function clonePlain(value: T): T { } } +function cloneLocalColumnFilters(filters: QueryResult["local_column_filters"]): QueryResult["local_column_filters"] { + return filters ? Object.fromEntries(Object.entries(filters).map(([columnIndex, values]) => [columnIndex, [...values]])) : undefined; +} + function stripSessionIds(result: QueryResult | undefined): QueryResult | undefined { if (!result) return undefined; return { @@ -334,6 +339,7 @@ function stripSessionIds(result: QueryResult | undefined): QueryResult | undefin execution_error: result.execution_error, statement_index: result.statement_index, column_types: result.column_types ? [...result.column_types] : undefined, + local_column_filters: cloneLocalColumnFilters(result.local_column_filters), spatial_columns: result.spatial_columns?.map((entry) => ({ column_index: entry.column_index, srid: entry.srid })), spatial_values: result.spatial_values?.map((row) => [...row]), large_value_cells: result.large_value_cells?.map((cell) => ({ ...cell })), @@ -377,6 +383,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult execution_error: result.execution_error, statement_index: result.statement_index, column_types: result.column_types ? [...result.column_types] : undefined, + local_column_filters: cloneLocalColumnFilters(result.local_column_filters), spatial_columns: result.spatial_columns?.map((entry) => ({ column_index: entry.column_index, srid: entry.srid })), spatial_values: result.spatial_values?.map((row) => [...row]), large_value_cells: result.large_value_cells?.map((cell) => ({ ...cell })), @@ -403,6 +410,7 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul execution_error: result.execution_error, statement_index: result.statement_index, column_types: result.column_types ? [...result.column_types] : undefined, + local_column_filters: cloneLocalColumnFilters(result.local_column_filters), spatial_columns: result.spatial_columns?.map((entry) => ({ column_index: entry.column_index, srid: entry.srid })), spatial_values: result.spatial_values?.map((row) => [...row]), large_value_cells: result.large_value_cells?.map((cell) => ({ ...cell })), @@ -712,6 +720,7 @@ export function decodeTabResultSnapshot(bytes: Uint8Array | ArrayBuffer): TabRes return undefined; } if (!isRecord(decoded.payload)) return undefined; + // SAFETY: The validated envelope is produced by encodeTabResultSnapshot, so its record payload has the snapshot shape expected here. return payloadToSnapshot(decoded.payload as unknown as TabResultSnapshotPayload); } diff --git a/apps/desktop/src/stores/__tests__/queryStore.switchTab.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.switchTab.spec.ts index 08390a49e5..1d8ce3d4b2 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.switchTab.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.switchTab.spec.ts @@ -179,6 +179,38 @@ describe("queryStore switchTab", () => { expect(tab.result.local_column_filters).toBeUndefined(); }); + it("keeps local filters isolated across result-run switches and clearing", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const queryStore = useQueryStore(); + const tabId = queryStore.createTab("pg-1", "app", "Query", "query"); + const tab = queryStore.tabs.find((item) => item.id === tabId)!; + const result = (filters?: Record) => ({ + columns: ["id", "status"], + rows: [[1, "active"]], + affected_rows: 0, + execution_time_ms: 1, + local_column_filters: filters, + }); + tab.resultRuns = [ + { id: "run-a", title: "Run A", sequence: 1, sql: "select 1", createdAt: 1, result: result() }, + { id: "run-b", title: "Run B", sequence: 2, sql: "select 2", createdAt: 2, result: result({ "1": ["str:pending"] }) }, + ]; + + expect(await queryStore.setActiveResultRun(tabId, "run-a")).toBe(true); + queryStore.updateDataGridLocalColumnFilters(tabId, { "1": ["str:active"] }); + expect(tab.resultRuns?.[0]?.result?.local_column_filters).toEqual({ "1": ["str:active"] }); + + expect(await queryStore.setActiveResultRun(tabId, "run-b")).toBe(true); + expect(tab.result?.local_column_filters).toEqual({ "1": ["str:pending"] }); + expect(await queryStore.setActiveResultRun(tabId, "run-a")).toBe(true); + expect(tab.result?.local_column_filters).toEqual({ "1": ["str:active"] }); + + queryStore.updateDataGridLocalColumnFilters(tabId, {}); + expect(await queryStore.setActiveResultRun(tabId, "run-b")).toBe(true); + expect(await queryStore.setActiveResultRun(tabId, "run-a")).toBe(true); + expect(tab.result?.local_column_filters).toBeUndefined(); + }); + it("stores hidden data-grid column keys on the tab result", async () => { const { useQueryStore } = await import("@/stores/queryStore"); const queryStore = useQueryStore(); diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 75524840b2..ecdf9ab301 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -4605,6 +4605,7 @@ test("starting a new query clears the previous result payload immediately", asyn rows: [[new Array(10_000).fill("old").join("")]], affected_rows: 0, execution_time_ms: 1, + local_column_filters: { "0": ["str:old"] }, }; globalThis.fetch = withConnectionHealthMock(async (input) => { @@ -4636,6 +4637,7 @@ test("starting a new query clears the previous result payload immediately", asyn assert.equal(tab.results, undefined); await execution; assert.deepEqual(tab.result?.columns, ["new"]); + assert.equal(tab.result?.local_column_filters, undefined); } finally { globalThis.fetch = originalFetch; restoreStorage(); diff --git a/packages/app-tests/tabResultCache.test.ts b/packages/app-tests/tabResultCache.test.ts index b734933ac7..f83f53510d 100644 --- a/packages/app-tests/tabResultCache.test.ts +++ b/packages/app-tests/tabResultCache.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; import { buildTabResultSnapshot, decodeTabResultSnapshot, encodeTabResultSnapshot } from "../../apps/desktop/src/lib/tabs/tabResultCache.ts"; -import type { QueryTab } from "../../apps/desktop/src/types/database.ts"; +import type { QueryResult, QueryTab } from "../../apps/desktop/src/types/database.ts"; function queryTab(overrides: Partial = {}): QueryTab { return { @@ -102,6 +102,74 @@ test("result snapshots strip session handles from result runs", () => { assert.deepEqual(snapshot?.resultRuns?.[0]?.resultLocalSortOriginalMongoCopyDocuments, [{ _id: { $oid: "507f1f77bcf86cd799439012" } }]); }); +test("result snapshots preserve local column filters for all result windows", () => { + const result = (filters?: Record): QueryResult => ({ + columns: ["id", "status"], + rows: [[1, "active"]], + affected_rows: 0, + execution_time_ms: 1, + local_column_filters: filters, + }); + const tab = queryTab({ + result: result({ "1": ["str:root"] }), + results: [result({ "1": ["str:first"] }), result({ "1": ["str:second"] })], + activeResultIndex: 0, + resultRuns: [ + { + id: "run-a", + title: "Run A", + sequence: 1, + sql: "select 1", + createdAt: 1, + result: result({ "1": ["str:run-a"] }), + results: [result({ "1": ["str:run-a-first"] }), result()], + activeResultIndex: 0, + }, + { + id: "run-b", + title: "Run B", + sequence: 2, + sql: "select 2", + createdAt: 2, + result: result({ "1": ["str:run-b"] }), + results: [result({ "1": ["str:run-b-first"] })], + activeResultIndex: 0, + }, + ], + activeResultRunId: "run-a", + }); + + const snapshot = buildTabResultSnapshot(tab); + assert.deepEqual(snapshot?.result?.local_column_filters, { "1": ["str:root"] }); + assert.deepEqual( + snapshot?.results?.map((item) => item.local_column_filters), + [{ "1": ["str:first"] }, { "1": ["str:second"] }], + ); + assert.deepEqual( + snapshot?.resultRuns?.map((run) => run.result?.local_column_filters), + [{ "1": ["str:run-a"] }, { "1": ["str:run-b"] }], + ); + assert.deepEqual( + snapshot?.resultRuns?.[0]?.results?.map((item) => item.local_column_filters), + [{ "1": ["str:run-a-first"] }, undefined], + ); + + const restored = decodeTabResultSnapshot(encodeTabResultSnapshot(snapshot!)); + assert.deepEqual(restored?.result?.local_column_filters, { "1": ["str:root"] }); + assert.deepEqual( + restored?.results?.map((item) => item.local_column_filters), + [{ "1": ["str:first"] }, { "1": ["str:second"] }], + ); + assert.deepEqual( + restored?.resultRuns?.map((run) => run.result?.local_column_filters), + [{ "1": ["str:run-a"] }, { "1": ["str:run-b"] }], + ); + assert.deepEqual( + restored?.resultRuns?.[0]?.results?.map((item) => item.local_column_filters), + [{ "1": ["str:run-a-first"] }, undefined], + ); +}); + test("result snapshots encode as binary columnar payloads and decode back to rows", () => { const snapshot = buildTabResultSnapshot( queryTab({