diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 1abaf5f5c..18caa18ee 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -3278,6 +3278,10 @@ const hasKnownTotalRowCount = computed(() => typeof serverKnownTotalRowCount.val // rowCount IS the total. Without this hint, the "page is full → assume more" // fallback in canGoNextDataGridPage lets the user keep clicking next forever. const allRowsLoaded = computed(() => isResultsContext.value && props.pageLimit === undefined); +// True when the in-memory result already holds the complete result set (results +// context, no server-side pagination, not truncated, no further pages). Used to +// skip re-executing the query on export and instead write the local rows. +const hasCompleteLocalResult = computed(() => !!props.result && allRowsLoaded.value && props.result.truncated !== true && props.result.has_more !== true); const canGoNextPage = computed(() => { return canGoNextDataGridPage({ hasMore: props.result.has_more, @@ -5827,6 +5831,8 @@ const { hasRowSelection, fullExportResult: props.fullExportResult, queryResultExportRequest: props.queryResultExportRequest, + hasCompleteLocalResult, + completeLocalResult: computed(() => (hasCompleteLocalResult.value ? props.result : undefined)), allExportResults: computed(() => props.allExportResults), currentResultLabel: computed(() => props.result.sourceLabel), exportFileBaseName: computed(() => props.exportFileBaseName), diff --git a/apps/desktop/src/composables/useDataGridExport.ts b/apps/desktop/src/composables/useDataGridExport.ts index a354c9fe8..b707c84b1 100644 --- a/apps/desktop/src/composables/useDataGridExport.ts +++ b/apps/desktop/src/composables/useDataGridExport.ts @@ -52,6 +52,23 @@ export interface UseDataGridExportOptions { hasRowSelection: ComputedRef; fullExportResult?: (onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => Promise; queryResultExportRequest?: (options: { exportId: string; filePath: string; format: "csv" | "xlsx" }) => Promise; + /** + * True when the in-memory result already holds the complete result set — + * i.e. the query ran without server-side pagination, was not truncated, and + * has no further pages. When true, full-result exports skip the re-executing + * backend/frontend streaming paths and write the local rows directly, so a + * slow query is never re-run just to export rows that are already on screen. + */ + hasCompleteLocalResult?: ComputedRef; + /** + * The raw in-memory QueryResult to use for "export all" when + * hasCompleteLocalResult is true. Exports the original query result (all + * rows, all columns, committed values) so the output matches the original + * re-run-SQL semantics — displayItems only covers visible columns and + * reflects client-side filters/search and unsaved edits, which would + * silently change what "export all data" produces. + */ + completeLocalResult?: ComputedRef; allExportResults?: ComputedRef | undefined>; currentResultLabel?: ComputedRef; exportFileBaseName?: ComputedRef; @@ -122,6 +139,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) { hasRowSelection, fullExportResult, queryResultExportRequest, + hasCompleteLocalResult, + completeLocalResult, allExportResults, currentResultLabel, exportFileBaseName, @@ -146,10 +165,18 @@ export function useDataGridExport(options: UseDataGridExportOptions) { } async function resultToExport(rowIds?: number[], onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void, useFullExport = true): Promise<{ columns: string[]; rows: CellValue[][] }> { - if (useFullExport && rowIds === undefined && fullExportResult) { + if (useFullExport && rowIds === undefined && fullExportResult && !hasCompleteLocalResult?.value) { const result = await fullExportResult(onProgress); if (result) return { columns: result.columns, rows: result.rows }; } + // The full result is already in memory — export the raw QueryResult (all + // rows, all columns, committed values) so "export all data" matches the + // original re-run-SQL semantics. displayItems only covers visible columns + // and reflects client-side filters/search and unsaved edits, which would + // silently change what the export contains. + if (useFullExport && rowIds === undefined && hasCompleteLocalResult?.value && completeLocalResult?.value) { + return { columns: completeLocalResult.value.columns, rows: completeLocalResult.value.rows }; + } return { columns: columns.value, rows: rowsToExport(rowIds).map((item) => item.data), @@ -512,7 +539,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { if (await exportQueryResultViaBackend("csv", rowIds)) return; if (await exportFullTableDataViaBackend("csv", rowIds)) return; - const needsFullExport = rowIds === undefined && !!fullExportResult; + const needsFullExport = rowIds === undefined && !!fullExportResult && !hasCompleteLocalResult?.value; if (needsFullExport && exportProgressDialog && exportProgressState) { exportProgressState.value = { title: t("exportProgress.title"), @@ -716,7 +743,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { if (!path) return; outputPath = path as string; } - const needsFullExport = rowIds === undefined && !!fullExportResult; + const needsFullExport = rowIds === undefined && !!fullExportResult && !hasCompleteLocalResult?.value; if (needsFullExport && exportProgressDialog && exportProgressState) { exportProgressState.value = { title: t("exportProgress.title"), @@ -908,6 +935,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) { if (rowIds !== undefined || context.value !== "results" || !queryResultExportRequest) { return false; } + // The full result is already in memory — don't re-execute the query on the + // backend just to stream the same rows back to a file. + if (hasCompleteLocalResult?.value) return false; const extension = format; const filterName = format === "csv" ? "CSV" : "Excel";