diff --git a/apps/desktop/src/components/export/DatabaseExportDialog.vue b/apps/desktop/src/components/export/DatabaseExportDialog.vue index ccde7620c..48102cf87 100644 --- a/apps/desktop/src/components/export/DatabaseExportDialog.vue +++ b/apps/desktop/src/components/export/DatabaseExportDialog.vue @@ -12,7 +12,7 @@ import * as api from "@/lib/backend/api"; import type { ExportProgress } from "@/lib/backend/api"; import { isSchemaAware } from "@/lib/database/databaseFeatureSupport"; import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions"; -import { buildAllDatabaseExportPlan, generateDatabaseExportId, runDatabaseExportUntilTerminal, runWithDatabaseBackupSnapshot, type AllDatabaseExportPlanItem } from "@/lib/export/databaseExport"; +import { buildAllDatabaseExportPlan, generateDatabaseExportId, runDatabaseExportUntilTerminal, runWithDatabaseBackupSnapshot, shouldUseDatabaseBackupSnapshot, type AllDatabaseExportPlanItem } from "@/lib/export/databaseExport"; import { buildSelectedTablesPayload } from "@/lib/export/databaseExportSelection"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { useToast } from "@/composables/useToast"; @@ -304,7 +304,7 @@ async function startExport() { { connectionId: connectionId.value, database: database.value, - enabled: includeData.value && (connectionType === "mysql" || connectionType === "postgres"), + enabled: shouldUseDatabaseBackupSnapshot(connectionType, includeData.value, isTauriRuntime()), }, async (snapshotSessionId) => { const request: api.DatabaseExportRequest = { @@ -323,7 +323,7 @@ async function startExport() { snapshotSessionId, batchSize: 1000, }; - await api.exportDatabaseSql(request, (progress) => { + return runDatabaseExportUntilTerminal(request, (progress) => { exportProgress.value = { ...progress }; updateDatabaseExportTask(progress.exportId, progress); if (progress.status === "Done") { @@ -342,7 +342,7 @@ async function startExport() { } }); }, - () => !exportError.value && !exportCancelled.value, + (terminal) => terminal.status === "Done", ); } catch (e: any) { exportError.value = e?.message || String(e); @@ -438,7 +438,7 @@ async function startAllDatabasesExport() { { connectionId: connectionId.value, database: item.database, - enabled: includeData.value && (connectionType === "mysql" || connectionType === "postgres"), + enabled: shouldUseDatabaseBackupSnapshot(connectionType, includeData.value, isTauriRuntime()), }, (snapshotSessionId) => runDatabaseExportUntilTerminal( diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index bda1d8a95..9169201f8 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2423,7 +2423,7 @@ export default withEnglishFallback({ changeOpenMode: "Modificar modo de apertura", closeRightTabs: "Cerrar pestañas a la derecha", compileObject: "Compilar", - compileObjectSuccess: "\"{name}\" compilado con éxito", + compileObjectSuccess: '"{name}" compilado con éxito', }, visibleDatabases: { title: "Bases de datos visibles", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 8c0a67e4f..8055a4acf 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2421,7 +2421,7 @@ export default withEnglishFallback({ changeOpenMode: "Modifica modalità apertura", closeRightTabs: "Chiudi le schede a destra", compileObject: "Compila", - compileObjectSuccess: "\"{name}\" compilato con successo", + compileObjectSuccess: '"{name}" compilato con successo', }, visibleDatabases: { title: "Database Visibili", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 4028a4860..9f8276cfb 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2448,7 +2448,7 @@ export default withEnglishFallback({ changeOpenMode: "開き方を変更", closeRightTabs: "右側のタブを閉じる", compileObject: "コンパイル", - compileObjectSuccess: "\"{name}\" のコンパイルに成功しました", + compileObjectSuccess: '"{name}" のコンパイルに成功しました', }, visibleDatabases: { title: "表示するデータベース", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 4353057c7..4cd57ad14 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2423,7 +2423,7 @@ export default withEnglishFallback({ changeOpenMode: "Alterar modo de abertura", closeRightTabs: "Fechar abas à direita", compileObject: "Compilar", - compileObjectSuccess: "\"{name}\" compilado com sucesso", + compileObjectSuccess: '"{name}" compilado com sucesso', }, visibleDatabases: { title: "Bancos de dados visíveis", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 249d1101a..438e5701b 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2422,7 +2422,7 @@ export default withEnglishFallback({ changeOpenMode: "修改開啟方式", closeRightTabs: "關閉右側標籤頁", compileObject: "編譯", - compileObjectSuccess: "\"{name}\" 編譯成功", + compileObjectSuccess: '"{name}" 編譯成功', }, visibleDatabases: { title: "顯示資料庫", diff --git a/apps/desktop/src/lib/export/databaseExport.ts b/apps/desktop/src/lib/export/databaseExport.ts index 4abf1501d..906fbf68b 100644 --- a/apps/desktop/src/lib/export/databaseExport.ts +++ b/apps/desktop/src/lib/export/databaseExport.ts @@ -93,6 +93,10 @@ export function generateDatabaseExportId(): string { return uuid(); } +export function shouldUseDatabaseBackupSnapshot(databaseType: DatabaseType | undefined, includeData: boolean, desktopRuntime: boolean): boolean { + return desktopRuntime && includeData && (databaseType === "mysql" || databaseType === "postgres"); +} + export async function runDatabaseExportUntilTerminal(request: api.DatabaseExportRequest, onProgress: (progress: api.ExportProgress) => void): Promise { return new Promise((resolve, reject) => { api diff --git a/packages/app-tests/databaseExportSnapshot.test.ts b/packages/app-tests/databaseExportSnapshot.test.ts index 66cbde7e5..dd62c4871 100644 --- a/packages/app-tests/databaseExportSnapshot.test.ts +++ b/packages/app-tests/databaseExportSnapshot.test.ts @@ -1,25 +1,68 @@ import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; import { beforeEach, test, vi } from "vitest"; -const { beginDatabaseBackupSnapshot, rollbackManualTransaction } = vi.hoisted(() => ({ +const { beginDatabaseBackupSnapshot, exportDatabaseSql, rollbackManualTransaction } = vi.hoisted(() => ({ beginDatabaseBackupSnapshot: vi.fn(), + exportDatabaseSql: vi.fn(), rollbackManualTransaction: vi.fn(), })); vi.mock("@/lib/backend/api", () => ({ beginDatabaseBackupSnapshot, + exportDatabaseSql, rollbackManualTransaction, })); -import { runWithDatabaseBackupSnapshot } from "../../apps/desktop/src/lib/export/databaseExport.ts"; +import { runDatabaseExportUntilTerminal, runWithDatabaseBackupSnapshot, shouldUseDatabaseBackupSnapshot } from "../../apps/desktop/src/lib/export/databaseExport.ts"; beforeEach(() => { beginDatabaseBackupSnapshot.mockReset(); + exportDatabaseSql.mockReset(); rollbackManualTransaction.mockReset(); beginDatabaseBackupSnapshot.mockResolvedValue({ sessionId: "snapshot-1" }); rollbackManualTransaction.mockResolvedValue({}); }); +test("database export snapshots are limited to supported desktop data exports", () => { + assert.equal(shouldUseDatabaseBackupSnapshot("mysql", true, true), true); + assert.equal(shouldUseDatabaseBackupSnapshot("postgres", true, true), true); + assert.equal(shouldUseDatabaseBackupSnapshot("oracle", true, true), false); + assert.equal(shouldUseDatabaseBackupSnapshot("sqlserver", true, true), false); + assert.equal(shouldUseDatabaseBackupSnapshot("mysql", false, true), false); + assert.equal(shouldUseDatabaseBackupSnapshot("postgres", true, false), false); +}); + +test("database export waits for terminal progress before releasing its snapshot", async () => { + let emitProgress: ((progress: any) => void) | undefined; + exportDatabaseSql.mockImplementation(async (_request, onProgress) => { + emitProgress = onProgress; + }); + + const operation = runDatabaseExportUntilTerminal({ exportId: "export-1" } as any, () => {}); + let settled = false; + void operation.then(() => { + settled = true; + }); + await Promise.resolve(); + + assert.equal(settled, false); + emitProgress?.({ exportId: "export-1", status: "Running" }); + await Promise.resolve(); + assert.equal(settled, false); + + emitProgress?.({ exportId: "export-1", status: "Done" }); + assert.equal((await operation).status, "Done"); +}); + +test("single database export holds its snapshot until terminal progress", () => { + const source = readFileSync("apps/desktop/src/components/export/DatabaseExportDialog.vue", "utf8"); + const singleExport = source.slice(source.indexOf("async function startExport()"), source.indexOf("async function startAllDatabasesExport()")); + + assert.match(singleExport, /return runDatabaseExportUntilTerminal\(request,/); + assert.doesNotMatch(singleExport, /await api\.exportDatabaseSql\(request,/); +}); + test("database export snapshot is released after success", async () => { const operation = vi.fn(async (sessionId: string | undefined) => `done:${sessionId}`);