fix(grid): reuse local result rows for csv/xlsx export instead of re-running query

* fix(grid): reuse local result rows for csv/xlsx export instead of re-running query

Exporting the full result set ("导出当前结果集全部数据 CSV") previously
re-executed the source SQL on the database via the backend streaming path
(exportQueryResultViaBackend -> start_query_result_export), and the frontend
fallback (fullExportResult/fetchTabResultForExport) did the same. For a slow
query this means running it a second time just to export rows already on
screen, which can hang or crash (e.g. a 2-minute paimon query returning 30
rows).

When the in-memory result already holds the complete set (no server-side
pagination, not truncated, no further pages), skip both re-executing paths and
write the displayed rows directly. Large/truncated/paginated results keep the
original streaming behavior unchanged.

* fix(grid): export raw in-memory result for complete local result set

---------

Co-authored-by: t8y2 <1156263951@qq.com>
This commit is contained in:
jischeng 2026-07-06 20:06:57 +08:00 committed by GitHub
parent 15114f58dd
commit 5db0fc4fb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 39 additions and 3 deletions

View File

@ -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),

View File

@ -52,6 +52,23 @@ export interface UseDataGridExportOptions {
hasRowSelection: ComputedRef<boolean>;
fullExportResult?: (onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => Promise<QueryResult | undefined>;
queryResultExportRequest?: (options: { exportId: string; filePath: string; format: "csv" | "xlsx" }) => Promise<QueryResultExportRequest | undefined>;
/**
* 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<boolean>;
/**
* 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<QueryResult | undefined>;
allExportResults?: ComputedRef<Array<{ sheetName: string; result: QueryResult }> | undefined>;
currentResultLabel?: ComputedRef<string | undefined>;
exportFileBaseName?: ComputedRef<string | undefined>;
@ -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";