fix(export): preserve local query result constraints

This commit is contained in:
zipg authored and GitHub committed 2026-07-21 21:31:17 +08:00
1 parent b54110036b
commit 81e23ab870
5 files changed
+192 -7

No files matched your search

+14 -5
View File
@@ -129,7 +129,7 @@ import { applyColumnFormatter, buildColumnFormatterKey, getSupportedTimeZoneOpti
import { temporalCellEditorConfig, type TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor";
import { isCancelSearchShortcut, isCopyCurrentRowShortcut, isDeleteCurrentRowShortcut, isFocusSearchShortcut, isModRShortcut, isSaveShortcut, isToggleTransposeShortcut } from "@/lib/editor/keyboardShortcuts";
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGrid/dataGridScrollGutter";
import { canGoNextDataGridPage } from "@/lib/dataGrid/dataGridPagination";
import { canGoNextDataGridPage, hasCompleteLocalDataGridResult } from "@/lib/dataGrid/dataGridPagination";
import { dataGridCountQueryOptions } from "@/lib/dataGrid/dataGridQueryOptions";
import { dataGridBottomScrollTop, dataGridScrollPosition, isDataGridAtScrollBottom, isDataGridNearScrollBottom, shouldCheckInfiniteScrollAfterScroll, type DataGridScrollPosition } from "@/lib/dataGrid/dataGridInfiniteScroll";
import { CANVAS_DATA_GRID_ROW_HEIGHT, dataGridSearchMatchKey, drawCanvasDataGrid } from "@/lib/dataGrid/canvasDataGridRenderer";
@@ -2359,10 +2359,19 @@ 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);
// Skip re-executing a query when the first page already contains every row.
// This also covers paginated queries whose first page is shorter than the limit.
const hasCompleteLocalResult = computed(() =>
hasCompleteLocalDataGridResult({
isResultsContext: isResultsContext.value,
rowCount: props.result.rows.length,
pageLimit: props.pageLimit,
pageOffset: props.pageOffset,
totalRowCount: serverKnownTotalRowCount.value,
truncated: props.result.truncated,
hasMore: props.result.has_more,
}),
);
const canGoNextPage = computed(() => {
return canGoNextDataGridPage({
hasMore: props.result.has_more,
@@ -234,6 +234,22 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
return pattern ? { ...result, rows: formatTemporalRowsForExport(result.rows, result.columnTypes, pattern) } : result;
}
function normalizeCompleteLocalResult(result: QueryResult): { columns: string[]; columnTypes: string[]; rows: CellValue[][] } {
const hiddenColumnIndexes = new Set(result.hidden_column_indexes ?? []);
const exportedColumnIndexes = result.columns.map((_, index) => index).filter((index) => !hiddenColumnIndexes.has(index));
const hasHiddenColumns = exportedColumnIndexes.length !== result.columns.length;
const editorSettings = useSettingsStore().editorSettings;
const rows = editorSettings.exportRowLimitEnabled ? result.rows.slice(0, editorSettings.exportRowLimit) : result.rows;
// Internal key columns are query-only metadata. Keep every user column,
// including columns hidden manually in the grid, while preserving alignment.
return {
columns: hasHiddenColumns ? exportedColumnIndexes.map((index) => result.columns[index]!) : result.columns,
columnTypes: hasHiddenColumns ? exportedColumnIndexes.map((index) => result.column_types?.[index] ?? "") : (result.column_types ?? []),
rows: hasHiddenColumns ? rows.map((row) => exportedColumnIndexes.map((index) => row[index])) : rows,
};
}
async function resultToExport(rowIds?: number[], onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void, useFullExport = true, formatDateTime = true): Promise<{ columns: string[]; columnTypes: string[]; rows: CellValue[][] }> {
if (useFullExport && rowIds === undefined && fullExportResult && !hasCompleteLocalResult?.value) {
const result = await fullExportResult(onProgress);
@@ -245,7 +261,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
// 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 applyGlobalDateTimeExportFormat({ columns: completeLocalResult.value.columns, columnTypes: completeLocalResult.value.column_types ?? [], rows: completeLocalResult.value.rows }, formatDateTime);
return applyGlobalDateTimeExportFormat(normalizeCompleteLocalResult(completeLocalResult.value), formatDateTime);
}
return applyGlobalDateTimeExportFormat(
{
@@ -11,6 +11,28 @@ export interface CanGoNextDataGridPageOptions {
allRowsLoaded?: boolean;
}
export interface CompleteLocalDataGridResultOptions {
isResultsContext: boolean;
rowCount: number;
pageLimit?: number;
pageOffset?: number;
totalRowCount?: number;
truncated?: boolean;
hasMore?: boolean;
}
export function hasCompleteLocalDataGridResult(options: CompleteLocalDataGridResultOptions): boolean {
if (!options.isResultsContext || options.truncated === true || options.hasMore === true) return false;
if (options.pageLimit === undefined) return true;
if ((options.pageOffset ?? 0) !== 0) return false;
const pageLimit = Math.max(1, options.pageLimit);
if (options.rowCount < pageLimit) return true;
const totalRowCount = options.totalRowCount;
return typeof totalRowCount === "number" && Number.isFinite(totalRowCount) && totalRowCount >= 0 && options.rowCount >= totalRowCount;
}
export function canGoNextDataGridPage(options: CanGoNextDataGridPageOptions): boolean {
if (options.hasMore === true) return true;