diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index bfed63d882..8ee06a8305 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -205,7 +205,7 @@ import { replaceColumnValueFilterCondition, } from "@/lib/dataGrid/dataGridColumnFilter"; import { normalizeResultPageSize, resultPageSizeMenuOptions } from "@/lib/dataGrid/paginationPageSize"; -import { continuousQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; +import { continuousQueryResultMaxRows, effectiveQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; import { allNullColumnIndexes } from "@/lib/dataGrid/dataGridColumnVisibility"; import { buildDataGridColumnLookupItems, dataGridColumnCommentFor, filterDataGridColumnLookupItems } from "@/lib/dataGrid/dataGridColumnLookup"; import { uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder"; @@ -633,6 +633,8 @@ const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridR const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode); const compactDataGridToolbar = computed(() => isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)); const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll); +const queryResultMaxRows = computed(() => effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows)); +const paginationMaxRows = computed(() => (isResultsContext.value ? queryResultMaxRows.value : undefined)); const infiniteScrollMaxRows = computed(() => continuousQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows)); const flatteningMultiLineEnabled = computed(() => settingsStore.editorSettings.flatteningMultiLineText); const expandedCellEditor = ref<{ rowId: number; col: number } | null>(null); @@ -3058,6 +3060,7 @@ const paginationTotalRowCount = computed(() => paginationTotalRowCount: props.paginationTotalRowCount, serverKnownTotalRowCount: serverKnownTotalRowCount.value, totalRowCountIsExact: totalRowCountIsExact.value, + maxRows: paginationMaxRows.value, }), ); // Only a server-confirmed total drives pagination — an inferred total means @@ -3335,8 +3338,9 @@ function applyCustomPageSize() { } function jumpToCountedLastPage(total: number) { - if (total <= 0) return; - const lastPageNum = Math.max(1, Math.ceil(total / pageSize.value)); + const paginationTotal = resolveDataGridPaginationTotal({ paginationTotalRowCount: total, totalRowCountIsExact: true, maxRows: paginationMaxRows.value }); + if (paginationTotal === undefined || paginationTotal <= 0) return; + const lastPageNum = Math.max(1, Math.ceil(paginationTotal / pageSize.value)); if (lastPageNum <= currentPage.value) return; // Do not bump currentPage before the new page loads — otherwise stale rows // briefly render with last-page indexes (e.g. 12001-13000) and flash a fake full page. diff --git a/apps/desktop/src/lib/dataGrid/dataGridPagination.ts b/apps/desktop/src/lib/dataGrid/dataGridPagination.ts index b89b8383d3..0abb9160fd 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridPagination.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridPagination.ts @@ -42,9 +42,10 @@ export function dataGridTotalRowCountLabelKey(totalRowCountIsExact: boolean, ine return inexactMode === "estimated" ? "grid.totalRowCountEstimated" : "grid.totalRowCountAtLeast"; } -export function resolveDataGridPaginationTotal(options: { paginationTotalRowCount?: number; serverKnownTotalRowCount?: number; totalRowCountIsExact: boolean }): number | undefined { - if (options.paginationTotalRowCount !== undefined) return options.paginationTotalRowCount; - return options.totalRowCountIsExact ? options.serverKnownTotalRowCount : undefined; +export function resolveDataGridPaginationTotal(options: { paginationTotalRowCount?: number; serverKnownTotalRowCount?: number; totalRowCountIsExact: boolean; maxRows?: number }): number | undefined { + const total = options.paginationTotalRowCount ?? (options.totalRowCountIsExact ? options.serverKnownTotalRowCount : undefined); + if (total === undefined || options.maxRows === undefined) return total; + return Math.min(total, options.maxRows); } export function hasCompleteLocalDataGridResult(options: CompleteLocalDataGridResultOptions): boolean { diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 813aeb9d96..23ade64681 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -4115,7 +4115,6 @@ export const useQueryStore = defineStore("query", () => { traceId: string; elapsed: () => string; timeoutSecs: number; - maxRows?: number; }) { const resultRowCount = options.result.rows.length; if (resultRowCount <= 0) { @@ -4158,11 +4157,12 @@ export const useQueryStore = defineStore("query", () => { setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined); return; } - const cappedTotal = capQueryResultTotal(total, options.maxRows); - setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, cappedTotal); + // COUNT describes all matching rows; the configured result limit only + // constrains how many of them pagination may load and retain. + setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, total); queryExecutionLog("info", "count:done", { traceId: options.traceId, - total: cappedTotal, + total, elapsed: options.elapsed(), }); } catch (error) { @@ -5151,7 +5151,6 @@ export const useQueryStore = defineStore("query", () => { traceId, elapsed, timeoutSecs: queryTimeoutSecs, - maxRows: queryResultMaxRows, }); } queryExecutionLog("info", "result:assigned", { diff --git a/packages/app-tests/dataGridPagination.test.ts b/packages/app-tests/dataGridPagination.test.ts index 5b3b1c85ae..b08209be6c 100644 --- a/packages/app-tests/dataGridPagination.test.ts +++ b/packages/app-tests/dataGridPagination.test.ts @@ -28,6 +28,40 @@ test("estimated display totals do not become pagination bounds", () => { ); }); +test("exact display totals keep pagination inside the configured result cap", () => { + assert.equal( + resolveDataGridPaginationTotal({ + serverKnownTotalRowCount: 175_390, + totalRowCountIsExact: true, + maxRows: 100_000, + }), + 100_000, + ); + assert.equal( + resolveDataGridPaginationTotal({ + serverKnownTotalRowCount: 99_999, + totalRowCountIsExact: true, + maxRows: 100_000, + }), + 99_999, + ); + assert.equal( + resolveDataGridPaginationTotal({ + serverKnownTotalRowCount: 100_000, + totalRowCountIsExact: true, + maxRows: 100_000, + }), + 100_000, + ); + assert.equal( + resolveDataGridPaginationTotal({ + serverKnownTotalRowCount: 175_390, + totalRowCountIsExact: true, + }), + 175_390, + ); +}); + test("first query page is complete when its known total is already loaded", () => { assert.equal( hasCompleteLocalDataGridResult({ @@ -208,11 +242,19 @@ test("last page always re-counts when a count path is available", () => { test("jumping to last page does not rewrite indexes before the new page loads", () => { const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8"); const jumpFn = source.match(/function jumpToCountedLastPage\(total: number\) \{[\s\S]*?\n\}/)?.[0] ?? ""; + assert.match(jumpFn, /resolveDataGridPaginationTotal/); + assert.match(jumpFn, /maxRows: paginationMaxRows\.value/); assert.match(jumpFn, /emit\("paginate"/); assert.doesNotMatch(jumpFn, /currentPage\.value\s*=/); assert.match(source, /function rowNumberPageOffset/); }); +test("query result caps do not limit table-data pagination", () => { + const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8"); + assert.match(source, /const paginationMaxRows = computed\(\(\) => \(isResultsContext\.value \? queryResultMaxRows\.value : undefined\)\)/); + assert.equal(source.match(/maxRows: paginationMaxRows\.value/g)?.length, 2); +}); + test("row number gutter width tracks the largest visible row index", () => { const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8"); assert.match(source, /dataGridRowNumberColumnWidth/); diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index dd8d88b371..edfe6d5a55 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -7169,7 +7169,7 @@ for (const resultState of [ { label: "truncated", result: { truncated: true, has_more: false } }, { label: "ambiguous exhaustion", result: {} }, ]) { - test(`oracle agent ${resultState.label} short page uses COUNT and caps the configured result total`, async () => { + test(`oracle agent ${resultState.label} short page preserves an exact COUNT beyond the result cap`, async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia()); const connectionStore = useConnectionStore(); @@ -7214,7 +7214,7 @@ for (const resultState of [ } if (url === "/api/query/execute") { countRequests += 1; - return new Response(JSON.stringify({ columns: ["COUNT(*)"], rows: [[3_357_833]], affected_rows: 0, execution_time_ms: 1 }), { status: 200, headers: { "Content-Type": "application/json" } }); + return new Response(JSON.stringify({ columns: ["COUNT(*)"], rows: [[175_390]], 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" }), { @@ -7227,11 +7227,11 @@ for (const resultState of [ try { await store.executeTabSql(tabId, "SELECT ID FROM LARGE_TABLE"); - await waitFor(() => tab.resultTotalRowCount === 100_000); + await waitFor(() => tab.resultTotalRowCount === 175_390); assert.equal(countRequests, 1); assert.equal(tab.result?.rows.length, 10_000); - assert.equal(tab.resultTotalRowCount, 100_000); + assert.equal(tab.resultTotalRowCount, 175_390); } finally { globalThis.fetch = originalFetch; restoreStorage();