- {{
- t("databaseExport.currentTable", {
- table: exportProgress.currentObject,
- current: exportProgress.objectIndex,
- total: exportProgress.totalObjects,
- })
- }}
+
+
+
+ {{ progressStatusText }}
-
+
{{ exportAllDatabases ? t("databaseExport.allRowsExported", { count: exportProgress.rowsExported.toLocaleString() }) : t("databaseExport.rowsExported", { current: exportProgress.objectIndex, total: exportProgress.totalObjects, count: exportProgress.rowsExported.toLocaleString() }) }}
@@ -793,3 +845,22 @@ watch(
+
+
diff --git a/apps/desktop/src/composables/useExportTracker.ts b/apps/desktop/src/composables/useExportTracker.ts
index 4fa09698f..fb4ba0f6c 100644
--- a/apps/desktop/src/composables/useExportTracker.ts
+++ b/apps/desktop/src/composables/useExportTracker.ts
@@ -381,7 +381,10 @@ export function useExportTracker() {
function updateDatabaseExportTask(exportId: string, progress: api.ExportProgress) {
const task = taskMap.get(exportId);
if (!task) return;
- task.tableName = progress.currentObject || task.tableName;
+ // Keep the database label during metadata prefetch; only follow object names while writing.
+ if (!progress.preparing && progress.currentObject) {
+ task.tableName = progress.currentObject;
+ }
task.rowsExported = progress.rowsExported;
task.totalRows = progress.totalRows;
task.status = normalizeExportStatus(progress.status);
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index f90c72802..ca969391c 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -4986,6 +4986,8 @@ export default {
export: "Export",
exportAllDatabases: "Export All Databases",
exporting: "Exporting...",
+ preparing: "Preparing export (reading tables and metadata)...",
+ preparingObject: "Preparing: {object}",
selectExportDirectory: "Select export directory",
currentDatabase: "Database {current}/{total}",
currentTable: "Current: {table} ({current}/{total})",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index d4b63ab3e..2a1077a13 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -4753,6 +4753,8 @@ export default withEnglishFallback({
export: "Exportar",
exportAllDatabases: "Exportar todas las bases de datos",
exporting: "Exportando...",
+ preparing: "Preparando exportación (leyendo tablas y metadatos)...",
+ preparingObject: "Preparando: {object}",
selectExportDirectory: "Seleccionar directorio de exportación",
currentDatabase: "Base de datos {current}/{total}",
currentTable: "Actual: {table} ({current}/{total})",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 896e59a45..574045ee9 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -4752,6 +4752,8 @@ export default withEnglishFallback({
export: "Esporta",
exportAllDatabases: "Esporta tutti i database",
exporting: "Esportazione in corso...",
+ preparing: "Preparazione esportazione (lettura tabelle e metadati)...",
+ preparingObject: "Preparazione: {object}",
selectExportDirectory: "Seleziona directory di esportazione",
currentDatabase: "Database {current}/{total}",
currentTable: "Corrente: {table} ({current}/{total})",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index 06000ed92..a730ed8e5 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -4753,6 +4753,8 @@ export default withEnglishFallback({
export: "エクスポート",
exportAllDatabases: "すべてのデータベースをエクスポート",
exporting: "エクスポート中...",
+ preparing: "エクスポートを準備中(テーブルとメタデータを読み取り中)...",
+ preparingObject: "準備中: {object}",
selectExportDirectory: "エクスポート先ディレクトリを選択",
currentDatabase: "データベース {current}/{total}",
currentTable: "現在: {table} ({current}/{total})",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 1e8981088..9a27824bc 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -4755,6 +4755,8 @@ export default withEnglishFallback({
export: "Exportar",
exportAllDatabases: "Exportar todos os bancos de dados",
exporting: "Exportando...",
+ preparing: "Preparando exportação (lendo tabelas e metadados)...",
+ preparingObject: "Preparando: {object}",
selectExportDirectory: "Selecionar diretório de exportação",
currentDatabase: "Banco de dados {current}/{total}",
currentTable: "Atual: {table} ({current}/{total})",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 0e60625be..1ef31f1dd 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -4985,6 +4985,8 @@ export default withEnglishFallback({
export: "导出",
exportAllDatabases: "导出全部数据库",
exporting: "正在导出...",
+ preparing: "正在准备导出(读取表与元数据)...",
+ preparingObject: "正在准备: {object}",
selectExportDirectory: "选择导出目录",
currentDatabase: "数据库 {current}/{total}",
currentTable: "当前: {table} ({current}/{total})",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 9b8883eb5..523676181 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -4422,6 +4422,8 @@ export default withEnglishFallback({
export: "匯出",
exportAllDatabases: "匯出全部資料庫",
exporting: "正在匯出……",
+ preparing: "正在準備匯出(讀取資料表與中繼資料)...",
+ preparingObject: "正在準備: {object}",
selectExportDirectory: "選擇匯出目錄",
currentDatabase: "資料庫 {current}/{total}",
currentTable: "目前: {table} ({current}/{total})",
diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts
index 4da46fa26..f0d25ed3d 100644
--- a/apps/desktop/src/lib/backend/tauri.ts
+++ b/apps/desktop/src/lib/backend/tauri.ts
@@ -2851,6 +2851,8 @@ export interface ExportProgress {
totalRows: number | null;
status: "Running" | "Done" | "Error" | "Cancelled";
error: string | null;
+ /** True while listing schema / prefetching metadata before objects are written. */
+ preparing?: boolean;
}
// --- Table Export ---
diff --git a/crates/dbx-core/src/database_export.rs b/crates/dbx-core/src/database_export.rs
index 4858e5cb3..dadd48fee 100644
--- a/crates/dbx-core/src/database_export.rs
+++ b/crates/dbx-core/src/database_export.rs
@@ -165,6 +165,9 @@ pub struct ExportProgress {
pub total_rows: Option
,
pub status: ExportStatus,
pub error: Option,
+ /// True while listing schema / prefetching table metadata — before objects are written.
+ #[serde(default)]
+ pub preparing: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -1294,11 +1297,36 @@ fn write_database_export_rows(
}
}
+fn emit_database_export_running(
+ on_progress: &(impl Fn(ExportProgress) + Sync),
+ export_id: &str,
+ current_object: impl Into,
+ object_index: usize,
+ total_objects: usize,
+ rows_exported: u64,
+ preparing: bool,
+) {
+ on_progress(ExportProgress {
+ export_id: export_id.to_string(),
+ current_object: current_object.into(),
+ object_index,
+ total_objects,
+ rows_exported,
+ total_rows: None,
+ status: ExportStatus::Running,
+ error: None,
+ preparing,
+ });
+}
+
pub async fn export_database_sql_core(
state: &crate::connection::AppState,
request: &DatabaseExportRequest,
on_progress: impl Fn(ExportProgress) + Sync,
) -> Result<(), String> {
+ // Emit immediately so the UI is never blank while we list schema metadata.
+ emit_database_export_running(&on_progress, &request.export_id, "", 0, 0, 0, true);
+
// 1. Get database type
let db_type = state
.configs
@@ -1493,53 +1521,15 @@ pub async fn export_database_sql_core(
let mut object_index: usize = 0;
let mut total_rows_exported = 0_u64;
+ // total_objects is known later for the write phase; preparing updates stay
+ // presence-only so the UI does not show a counter that later resets.
+ emit_database_export_running(&on_progress, &request.export_id, "", 0, 0, 0, true);
- // Export tables
let batch_size = if request.batch_size == 0 { 1000 } else { request.batch_size };
- for extension in &postgres_extensions {
- if is_export_cancelled(&request.export_id).await {
- return Err("Export cancelled".to_string());
- }
- on_progress(ExportProgress {
- export_id: request.export_id.clone(),
- current_object: extension.name.clone(),
- object_index,
- total_objects,
- rows_exported: total_rows_exported,
- total_rows: None,
- status: ExportStatus::Running,
- error: None,
- });
- writeln!(file, "{}\n", generate_postgres_extension_ddl(extension))
- .map_err(|e| format!("Failed to write file: {e}"))?;
- object_index += 1;
- }
-
- for sequence in postgres_sequences.iter().filter(|sequence| sequence.owner_table.is_none()) {
- if is_export_cancelled(&request.export_id).await {
- return Err("Export cancelled".to_string());
- }
-
- on_progress(ExportProgress {
- export_id: request.export_id.clone(),
- current_object: sequence.name.clone(),
- object_index,
- total_objects,
- rows_exported: total_rows_exported,
- total_rows: None,
- status: ExportStatus::Running,
- error: None,
- });
-
- writeln!(file, "{};\n", generate_postgres_sequence_create_ddl(sequence, &request.schema))
- .map_err(|e| format!("Failed to write file: {e}"))?;
- object_index += 1;
- }
-
// 预取各表的 DDL 与列元数据:逐表串行往返在多表数据库上是整库导出耗时的
// 主要来源(每表 1-2 次网络往返 × 表数)。有界并发预取后,下方写出循环仍按
- // 原顺序消费,文件内容与逐表查询完全一致。
+ // 原顺序消费,文件内容与逐表查询完全一致。放在写出循环之前,避免准备态与导出态来回跳。
struct PrefetchedTableMetadata {
ddl: Option>,
columns: Option, String>>,
@@ -1611,6 +1601,18 @@ pub async fn export_database_sql_core(
.buffer_unordered(EXPORT_METADATA_PREFETCH_CONCURRENCY);
while let Some((index, metadata)) = prefetch_stream.next().await {
prefetched_table_metadata[index] = Some(metadata);
+ if let Some(table_info) = tables.get(index) {
+ // Presence-only updates: no prepare counter that later resets to 0/N.
+ emit_database_export_running(
+ &on_progress,
+ &request.export_id,
+ table_info.name.clone(),
+ 0,
+ 0,
+ total_rows_exported,
+ true,
+ );
+ }
// 取消后不再调度新的预取任务(已在途的任务随 stream 释放而中止),
// 写出循环入口的取消检查负责最终收尾
if is_export_cancelled_now(&request.export_id) {
@@ -1619,6 +1621,44 @@ pub async fn export_database_sql_core(
}
}
+ for extension in &postgres_extensions {
+ if is_export_cancelled(&request.export_id).await {
+ return Err("Export cancelled".to_string());
+ }
+ emit_database_export_running(
+ &on_progress,
+ &request.export_id,
+ extension.name.clone(),
+ object_index,
+ total_objects,
+ total_rows_exported,
+ false,
+ );
+ writeln!(file, "{}\n", generate_postgres_extension_ddl(extension))
+ .map_err(|e| format!("Failed to write file: {e}"))?;
+ object_index += 1;
+ }
+
+ for sequence in postgres_sequences.iter().filter(|sequence| sequence.owner_table.is_none()) {
+ if is_export_cancelled(&request.export_id).await {
+ return Err("Export cancelled".to_string());
+ }
+
+ emit_database_export_running(
+ &on_progress,
+ &request.export_id,
+ sequence.name.clone(),
+ object_index,
+ total_objects,
+ total_rows_exported,
+ false,
+ );
+
+ writeln!(file, "{};\n", generate_postgres_sequence_create_ddl(sequence, &request.schema))
+ .map_err(|e| format!("Failed to write file: {e}"))?;
+ object_index += 1;
+ }
+
for (table_index, table_info) in tables.iter().enumerate().filter(|_| exports_database_tables(request)) {
// Check cancellation
if is_export_cancelled(&request.export_id).await {
@@ -1631,6 +1671,7 @@ pub async fn export_database_sql_core(
total_rows: None,
status: ExportStatus::Cancelled,
error: None,
+ preparing: false,
});
return Ok(());
}
@@ -1647,6 +1688,7 @@ pub async fn export_database_sql_core(
total_rows: None,
status: ExportStatus::Running,
error: None,
+ preparing: false,
});
// Export structure
@@ -1668,6 +1710,7 @@ pub async fn export_database_sql_core(
total_rows: None,
status: ExportStatus::Running,
error: None,
+ preparing: false,
});
writeln!(file, "{};\n", generate_postgres_sequence_create_ddl(sequence, &request.schema))
@@ -1779,6 +1822,7 @@ pub async fn export_database_sql_core(
total_rows: None,
status: ExportStatus::Running,
error: None,
+ preparing: false,
});
Ok(())
},
@@ -1816,6 +1860,7 @@ pub async fn export_database_sql_core(
total_rows,
status: ExportStatus::Cancelled,
error: None,
+ preparing: false,
});
return Ok(());
}
@@ -1864,6 +1909,7 @@ pub async fn export_database_sql_core(
total_rows,
status: ExportStatus::Running,
error: None,
+ preparing: false,
});
if row_count < batch_size {
break;
@@ -1907,6 +1953,7 @@ pub async fn export_database_sql_core(
total_rows: None,
status: ExportStatus::Running,
error: None,
+ preparing: false,
});
match crate::schema::get_object_source_core(
@@ -1958,6 +2005,7 @@ pub async fn export_database_sql_core(
total_rows: None,
status: ExportStatus::Running,
error: None,
+ preparing: false,
});
match crate::schema::get_object_source_core(
@@ -2013,6 +2061,7 @@ pub async fn export_database_sql_core(
total_rows: None,
status: ExportStatus::Running,
error: None,
+ preparing: false,
});
match crate::schema::get_object_source_core(
@@ -2067,6 +2116,7 @@ pub async fn export_database_sql_core(
total_rows: None,
status: ExportStatus::Done,
error: None,
+ preparing: false,
});
Ok(())
diff --git a/crates/dbx-web/src/routes/database_export.rs b/crates/dbx-web/src/routes/database_export.rs
index c33cbc252..523f6c50f 100644
--- a/crates/dbx-web/src/routes/database_export.rs
+++ b/crates/dbx-web/src/routes/database_export.rs
@@ -94,6 +94,7 @@ pub async fn start_database_export(
total_rows: None,
status: ExportStatus::Error,
error: Some(e.clone()),
+ preparing: false,
};
if let Ok(json) = serde_json::to_string(&progress) {
let _ = tx.send(json);
diff --git a/src-tauri/src/commands/database_export.rs b/src-tauri/src/commands/database_export.rs
index 048c4ddbc..8149e3079 100644
--- a/src-tauri/src/commands/database_export.rs
+++ b/src-tauri/src/commands/database_export.rs
@@ -49,6 +49,7 @@ pub async fn export_database_sql(
total_rows: None,
status: ExportStatus::Error,
error: Some(e),
+ preparing: false,
},
);
}