fix(export): keep MySQL database export snapshots alive

This commit is contained in:
zipg authored and GitHub committed 2026-08-06 16:32:39 +08:00
1 parent 71a3b383d3
commit 9d348f8ef7
17 files changed
+290 -35

No files matched your search

@@ -4,6 +4,7 @@ import { isTerminalTransferProgress } from "@/lib/backend/transferProgress";
export type BackgroundTaskKind = "table-export" | "database-export" | "sql-file" | "data-transfer";
export type BackgroundTaskStatus = "Running" | "Writing" | "Done" | "Error" | "Cancelled";
export type DatabaseExportSource = "manual" | "scheduled";
export interface DataTransferFailure {
table: string;
@@ -21,6 +22,9 @@ export interface ExportTask {
totalRows: number | null;
status: BackgroundTaskStatus;
errorMessage: string | null;
databaseExportSource?: DatabaseExportSource;
currentObject?: string;
preparing?: boolean;
objectIndex?: number;
totalObjects?: number;
overallPercent?: number;
@@ -254,7 +258,7 @@ export function useExportTracker() {
return task;
}
function addDatabaseExportTask(exportId: string, label: string, filePath: string): ExportTask {
function addDatabaseExportTask(exportId: string, label: string, filePath: string, databaseExportSource: DatabaseExportSource = "manual"): ExportTask {
const task = reactive<ExportTask>({
exportId,
kind: "database-export",
@@ -265,6 +269,9 @@ export function useExportTracker() {
totalRows: null,
status: "Running",
errorMessage: null,
databaseExportSource,
currentObject: "",
preparing: true,
objectIndex: 0,
totalObjects: 0,
startedAt: Date.now(),
@@ -392,10 +399,8 @@ export function useExportTracker() {
function updateDatabaseExportTask(exportId: string, progress: api.ExportProgress & { overallPercent?: number }) {
const task = taskMap.get(exportId);
if (!task) return;
// Keep the database label during metadata prefetch; only follow object names while writing.
if (!progress.preparing && progress.currentObject) {
task.tableName = progress.currentObject;
}
task.currentObject = progress.currentObject;
task.preparing = !!progress.preparing;
task.rowsExported = progress.rowsExported;
task.totalRows = progress.totalRows;
task.status = normalizeExportStatus(progress.status);
@@ -182,7 +182,7 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
activeScheduleIds.add(schedule.id);
activeRunIds.add(runId);
cancellationRequested.delete(runId);
addDatabaseExportTask(runId, schedule.name, schedule.destinationDirectory);
addDatabaseExportTask(runId, schedule.name, schedule.destinationDirectory, "scheduled");
registerTaskCancelHandler(runId, () => cancelRun(runId));
let finalStatus: Exclude<DatabaseBackupRunStatus, "running"> = "success";
@@ -219,27 +219,29 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
finalStatus = "cancelled";
break;
}
const schemasByDatabase = connection.db_type === "postgres" ? { [database]: await api.listSchemas(schedule.connectionId, database) } : undefined;
const databasePlan = buildAllDatabaseExportPlan({
databases: [database],
schemaAware: connection.db_type === "postgres",
schemasByDatabase,
});
if (databasePlan.length === 0) throw new Error(`Database ${database} did not resolve to any schemas.`);
const scopedDatabasePlan: Array<(typeof databasePlan)[number] & { selectedTables?: string[]; excludedTables?: string[] }> = [];
for (const item of databasePlan) {
if (schedule.tableFilterMode === "all") {
scopedDatabasePlan.push(item);
continue;
}
const availableTables = (await api.listTables(schedule.connectionId, item.database, item.schema)).map((table) => table.name);
const scope = resolveScheduledDatabaseBackupTableScope(schedule.tableFilterMode, schedule.tablePatterns, availableTables, item.database, item.schema, tableNamesCaseSensitive);
includedTableCount += scope.includedTables.length;
if (scope.includedTables.length === 0) continue;
scopedDatabasePlan.push({ ...item, selectedTables: scope.selectedTables, excludedTables: scope.excludedTables });
}
const snapshot = await api.beginDatabaseBackupSnapshot(schedule.connectionId, database);
let snapshotCompleted = false;
try {
const databasePlan = buildAllDatabaseExportPlan({
databases: [database],
schemaAware: connection.db_type === "postgres",
schemasByDatabase: { [database]: snapshot.schemas },
});
if (databasePlan.length === 0) throw new Error(`Database ${database} did not resolve to any schemas.`);
const scopedDatabasePlan: Array<(typeof databasePlan)[number] & { selectedTables?: string[]; excludedTables?: string[] }> = [];
for (const item of databasePlan) {
if (schedule.tableFilterMode === "all") {
scopedDatabasePlan.push(item);
continue;
}
const availableTables = (await api.listTables(schedule.connectionId, item.database, item.schema)).map((table) => table.name);
const scope = resolveScheduledDatabaseBackupTableScope(schedule.tableFilterMode, schedule.tablePatterns, availableTables, item.database, item.schema, tableNamesCaseSensitive);
includedTableCount += scope.includedTables.length;
if (scope.includedTables.length === 0) continue;
scopedDatabasePlan.push({ ...item, selectedTables: scope.selectedTables, excludedTables: scope.excludedTables });
}
for (const [planIndex, item] of scopedDatabasePlan.entries()) {
if (cancellationRequested.has(runId)) {
finalStatus = "cancelled";