perf(export): stream large SQL INSERT exports in background

This commit is contained in:
DengQingNian authored and GitHub committed 2026-07-27 22:47:00 +08:00
1 parent a77e089848
commit c8b4224587
14 files changed
+1047 -38

No files matched your search

@@ -306,7 +306,7 @@ interface DataGridProps {
exportSql?: string;
onExecuteSql?: (sql: string) => Promise<void>;
fullExportResult?: (onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => Promise<QueryResult | undefined>;
queryResultExportRequest?: (options: { exportId: string; filePath: string; format: "csv" | "xlsx" | "txt"; includeSqlSheet?: boolean }) => Promise<api.QueryResultExportRequest | undefined>;
queryResultExportRequest?: (options: { exportId: string; filePath: string; format: "csv" | "xlsx" | "txt" | "sql"; includeSqlSheet?: boolean; exportTableName?: string; exportColumnTypes?: Array<string | null | undefined> }) => Promise<api.QueryResultExportRequest | undefined>;
allExportResults?: Array<{ sheetName: string; result: QueryResult; sql?: string }>;
exportFileBaseName?: string;
customSaveHandler?: import("@/composables/useDataGridEditor").CustomSaveHandler;
@@ -1760,8 +1760,8 @@ const tableColumnTypesByName = computed(() => {
}
return map;
});
const visibleColumnTypes = computed(() =>
visibleColumnIndexes.value.map((index) =>
const allColumnTypes = computed(() =>
props.result.columns.map((_, index) =>
resolveResultColumnType({
resultColumnType: props.result.column_types?.[index],
resultColumnName: props.result.columns[index]?.toLocaleLowerCase(),
@@ -1770,6 +1770,7 @@ const visibleColumnTypes = computed(() =>
}),
),
);
const visibleColumnTypes = computed(() => visibleColumnIndexes.value.map((index) => allColumnTypes.value[index]));
const visibleColumnCount = computed(() => visibleColumnIndexes.value.length);
const numericColumnRightAlign = computed(() => (settingsStore.editorSettings.numericColumnRightAlign ?? true) && !showTranspose.value);
@@ -5182,6 +5183,7 @@ const {
sourceColumns: visibleSourceColumns,
mongoDocuments: computed(() => props.result.mongo_copy_documents ?? props.result.mongo_documents),
columnTypes: visibleColumnTypes,
allColumnTypes,
whereInput: computed(() => currentWhereInput()),
orderBy: computed(() => currentOrderBy()),
exportBatchSize: computed(() => settingsStore.editorSettings.exportBatchSize),
@@ -1353,7 +1353,9 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
:total-row-count-loading="activeTab.resultTotalRowCountLoading"
:on-execute-sql="async (sql: string) => emit('executeSql', sql)"
:full-export-result="(onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => queryStore.fetchTabResultForExport(activeTab.id, onProgress)"
:query-result-export-request="(options: { exportId: string; filePath: string; format: 'csv' | 'xlsx' | 'txt'; includeSqlSheet?: boolean }) => queryStore.buildQueryResultExportRequest(activeTab.id, options)"
:query-result-export-request="
(options: { exportId: string; filePath: string; format: 'csv' | 'xlsx' | 'txt' | 'sql'; includeSqlSheet?: boolean; exportTableName?: string; exportColumnTypes?: Array<string | null | undefined> }) => queryStore.buildQueryResultExportRequest(activeTab.id, options)
"
:all-export-results="allResultExportSheets"
:export-file-base-name="activeTab.title"
@update:order-by-input="(v: string) => (activeTab.orderByInput = v)"
@@ -6,6 +6,7 @@ import * as api from "@/lib/backend/api";
import { type CellSelectionMatrix, type CellSelectionRange, type SelectionData } from "@/lib/dataGrid/gridSelection";
import type { DataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor";
import { useToast } from "@/composables/useToast";
import { useExportTracker } from "@/composables/useExportTracker";
import { displayCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
import { tryStartExclusiveActivation, type ActionActivationGuard } from "@/lib/connection/actionActivation";
import { copyToClipboard } from "@/lib/common/clipboard";
@@ -78,6 +79,7 @@ export interface UseDataGridExportOptions {
sourceColumns: ComputedRef<Array<string | undefined> | undefined>;
mongoDocuments?: ComputedRef<unknown[] | undefined>;
columnTypes: ComputedRef<Array<string | undefined> | undefined>;
allColumnTypes?: ComputedRef<Array<string | undefined> | undefined>;
whereInput: ComputedRef<string | undefined>;
orderBy: ComputedRef<string | undefined>;
exportBatchSize: ComputedRef<number>;
@@ -92,7 +94,7 @@ export interface UseDataGridExportOptions {
selectedRowIds: Ref<Set<number>> | ComputedRef<Set<number>>;
hasRowSelection: ComputedRef<boolean>;
fullExportResult?: (onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => Promise<QueryResult | undefined>;
queryResultExportRequest?: (options: { exportId: string; filePath: string; format: "csv" | "xlsx" | "txt"; includeSqlSheet?: boolean }) => Promise<QueryResultExportRequest | undefined>;
queryResultExportRequest?: (options: { exportId: string; filePath: string; format: "csv" | "xlsx" | "txt" | "sql"; includeSqlSheet?: boolean; exportTableName?: string; exportColumnTypes?: Array<string | null | undefined> }) => 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
@@ -138,6 +140,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
const { t } = useI18n();
const { toast } = useToast();
const exportGuard: ActionActivationGuard = {};
const { addTask, updateTableExportTask, registerTaskCancelHandler, unregisterTaskCancelHandler, removeTask } = useExportTracker();
const {
columns,
@@ -159,6 +162,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
whereInput,
orderBy,
columnTypes,
allColumnTypes: allColumnTypesOption,
exportBatchSize,
hasCellSelection,
hasColumnSelection: hasColumnSelectionOption,
@@ -185,6 +189,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
const allColumns = allColumnsOption ?? columns;
const allDisplayItems = allDisplayItemsOption ?? displayItems;
const allSourceColumns = allSourceColumnsOption ?? sourceColumns;
const allColumnTypes = computed(() => allColumnTypesOption?.value ?? columnTypes.value);
const visibleColumnIndexes = visibleColumnIndexesOption ?? computed(() => columns.value.map((_, index) => index));
const hasColumnSelection = hasColumnSelectionOption ?? computed(() => false);
@@ -1018,11 +1023,83 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
return true;
}
async function exportQueryResultSqlViaBackend(rowIds?: number[]): Promise<boolean> {
// Guard: only for query-result context without complete local result, desktop only
if (rowIds !== undefined || context.value !== "results" || !queryResultExportRequest) return false;
if (hasCompleteLocalResult?.value) return false;
if (!isTauriRuntime()) return false; // Web → local export fallback
// 1. Save dialog FIRST (immediate user feedback)
let outputPath = exportFileName("query-result", "sql");
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: outputPath,
filters: [{ name: "SQL", extensions: ["sql"] }],
});
if (!path) return true;
outputPath = path as string;
// 2. Register background task FIRST so its exportId drives the request.
// addTask generates its own ID — capture it so progress callbacks match.
const task = addTask(tableMeta.value?.tableName || "Query Result", "sql", outputPath);
const taskExportId = task.exportId;
let request: api.QueryResultExportRequest;
try {
const built = await queryResultExportRequest({
exportId: taskExportId,
filePath: outputPath,
format: "sql",
exportTableName: tableMeta.value?.tableName,
exportColumnTypes: allColumnTypes.value?.map((t) => t ?? null) as Array<string | null | undefined> | undefined,
});
if (!built) {
// builder declined — clean up the task we just registered
removeTask(taskExportId);
return false;
}
request = built;
} catch {
removeTask(taskExportId);
return false;
}
registerTaskCancelHandler(taskExportId, () => api.cancelQueryResultExport(taskExportId, request.executionId));
try {
await api.startQueryResultExport(request, (progress) => {
updateTableExportTask(taskExportId, progress);
if (progress.status === "Done") {
toast(t("grid.exported"));
}
});
} catch (e) {
// 4. Startup rejection → mark task Error (fixes stuck-Running bug)
updateTableExportTask(taskExportId, {
exportId: taskExportId,
tableName: tableMeta.value?.tableName || "Query Result",
rowsExported: 0,
totalRows: null,
status: "Error" as const,
errorMessage: (e as Error)?.message || String(e),
});
throw e;
} finally {
unregisterTaskCancelHandler(taskExportId);
}
return true;
}
async function exportSql(rowIds?: number[]) {
await runExclusiveExport(async () => {
try {
// Step 1: table-data context — existing backend table export
if (await exportFullTableDataViaBackend("sql", rowIds)) return;
// Step 2: query-result context — NEW backend streaming with background task
if (await exportQueryResultSqlViaBackend(rowIds)) return;
// Step 3: fallback — local export (Web and edge-case scenarios)
const result = await resultToExport(rowIds, undefined, true, false);
const exportData = sqlInsertExportData(result);
const content = await formatSqlInsert({
+3 -1
View File
@@ -2747,7 +2747,7 @@ export interface QueryResultExportRequest {
databaseType: DatabaseType;
useAgentCursor: boolean;
filePath: string;
format: "csv" | "xlsx" | "txt";
format: "csv" | "xlsx" | "txt" | "sql";
includeSqlSheet?: boolean;
pageSize: number;
rowLimit?: number | null;
@@ -2757,6 +2757,8 @@ export interface QueryResultExportRequest {
clientSessionId?: string;
executionId?: string;
dateTimeFormat?: string;
exportTableName?: string;
exportColumnTypes?: Array<string | null | undefined>;
numericColumnRightAlign?: boolean;
}
+5 -1
View File
@@ -84,8 +84,10 @@ function cloneTabDraft<T>(value: T): T {
interface BuildQueryResultExportRequestOptions {
exportId: string;
filePath: string;
format: "csv" | "xlsx" | "txt";
format: "csv" | "xlsx" | "txt" | "sql";
includeSqlSheet?: boolean;
exportTableName?: string;
exportColumnTypes?: Array<string | null | undefined>;
}
type DroppedTableObjectType = "TABLE" | "VIEW" | "MATERIALIZED_VIEW";
@@ -4705,6 +4707,8 @@ export const useQueryStore = defineStore("query", () => {
keysetOptimizationEnabled: settings.queryExportKeysetOptimizationEnabled,
clientSessionId,
executionId: uuid(),
exportTableName: options.exportTableName,
exportColumnTypes: options.exportColumnTypes,
numericColumnRightAlign: settings.numericColumnRightAlign,
};
}