perf(export): stream large SQL INSERT exports in background
This commit is contained in:
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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user