mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 08:13:34 +00:00
fix(export): surface lenient per-object failures in export completion
Closes #8184
This commit is contained in:
@@ -152,6 +152,17 @@ function joinExportPath(directory: string, fileName: string): string {
|
||||
return `${directory.replace(/[\\/]+$/, "")}${separator}${fileName}`;
|
||||
}
|
||||
|
||||
// Lenient exports write per-object failures into the SQL file as `-- ERROR`
|
||||
// comments and still finish; completion must warn instead of reporting plain
|
||||
// success (#8184).
|
||||
function toastDatabaseExportCompletion(errorCount: number, errorSummary: string | null) {
|
||||
if (errorCount > 0) {
|
||||
toast(t("databaseExport.exportSuccessWithErrors", { count: errorCount, firstError: errorSummary ?? "" }), 8000);
|
||||
return;
|
||||
}
|
||||
toast(t("databaseExport.exportSuccess"), 3000);
|
||||
}
|
||||
|
||||
async function loadDatabases(connId: string) {
|
||||
if (!connId) return;
|
||||
loadingMeta.value = true;
|
||||
@@ -370,7 +381,7 @@ async function startExport() {
|
||||
finishExportTiming();
|
||||
exportDone.value = true;
|
||||
isExporting.value = false;
|
||||
toast(t("databaseExport.exportSuccess"), 3000);
|
||||
toastDatabaseExportCompletion(progress.errorCount ?? 0, progress.errorSummary ?? null);
|
||||
} else if (progress.status === "Error") {
|
||||
finishExportTiming();
|
||||
exportError.value = progress.error;
|
||||
@@ -432,6 +443,8 @@ async function startAllDatabasesExport() {
|
||||
exportCancelled.value = false;
|
||||
batchDatabaseIndex.value = 0;
|
||||
batchRowsExported.value = 0;
|
||||
let batchLenientErrorCount = 0;
|
||||
let batchFirstErrorSummary: string | null = null;
|
||||
|
||||
const dbs = [...selectedDatabases.value];
|
||||
const connectionType = store.getConfig(connectionId.value)?.db_type;
|
||||
@@ -475,7 +488,7 @@ async function startAllDatabasesExport() {
|
||||
const filePath = isTauriRuntime() ? joinExportPath(directoryPath, `${sanitizeFileName(item.fileStem)}.sql`) : `__web_export_${currentExportId}.sql`;
|
||||
let currentDatabaseRowsExported = 0;
|
||||
|
||||
await runWithDatabaseBackupSnapshot(
|
||||
const terminal = await runWithDatabaseBackupSnapshot(
|
||||
{
|
||||
connectionId: connectionId.value,
|
||||
database: item.database,
|
||||
@@ -530,6 +543,8 @@ async function startAllDatabasesExport() {
|
||||
(terminal) => terminal.status === "Done",
|
||||
);
|
||||
|
||||
batchLenientErrorCount += terminal.errorCount ?? 0;
|
||||
batchFirstErrorSummary ??= terminal.errorSummary ?? null;
|
||||
if (exportError.value || exportCancelled.value) break;
|
||||
activeDatabaseExportId.value = "";
|
||||
}
|
||||
@@ -547,10 +562,16 @@ async function startAllDatabasesExport() {
|
||||
totalRows: null,
|
||||
status: "Done",
|
||||
error: null,
|
||||
errorCount: batchLenientErrorCount,
|
||||
errorSummary: batchFirstErrorSummary,
|
||||
};
|
||||
exportProgress.value = finalProgress;
|
||||
updateDatabaseExportTask(batchId, finalProgress);
|
||||
toast(t("databaseExport.exportAllSuccess", { count: dbs.length }), 3000);
|
||||
if (batchLenientErrorCount > 0) {
|
||||
toast(t("databaseExport.exportAllSuccessWithErrors", { count: dbs.length, errorCount: batchLenientErrorCount, firstError: batchFirstErrorSummary ?? "" }), 8000);
|
||||
} else {
|
||||
toast(t("databaseExport.exportAllSuccess", { count: dbs.length }), 3000);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
exportError.value = e?.message || String(e);
|
||||
|
||||
@@ -7560,6 +7560,8 @@ export default {
|
||||
allRowsExported: "Total rows exported: {count}",
|
||||
exportSuccess: "Database exported successfully",
|
||||
exportAllSuccess: "{count} databases exported successfully",
|
||||
exportSuccessWithErrors: "Export finished with {count} failed object(s): {firstError}… See the -- ERROR comments inside the exported file for details.",
|
||||
exportAllSuccessWithErrors: "{count} databases exported, but {errorCount} object(s) failed: {firstError}… See the -- ERROR comments inside the exported files.",
|
||||
allDatabasesTask: "All databases ({count})",
|
||||
exportError: "Export failed: {error}",
|
||||
exportCancelled: "Export cancelled",
|
||||
|
||||
@@ -7214,6 +7214,8 @@ export default withEnglishFallback({
|
||||
allRowsExported: "Total de filas exportadas: {count}",
|
||||
exportSuccess: "Base de datos exportada exitosamente",
|
||||
exportAllSuccess: "{count} bases de datos exportadas exitosamente",
|
||||
exportSuccessWithErrors: "La exportación terminó con {count} objeto(s) fallidos: {firstError}… Consulta los comentarios -- ERROR en el archivo exportado.",
|
||||
exportAllSuccessWithErrors: "Se exportaron {count} bases de datos, pero {errorCount} objeto(s) fallaron: {firstError}… Consulta los comentarios -- ERROR en los archivos.",
|
||||
allDatabasesTask: "Todas las bases de datos ({count})",
|
||||
exportError: "Error al exportar: {error}",
|
||||
exportCancelled: "Exportación cancelada",
|
||||
|
||||
@@ -7216,6 +7216,8 @@ export default withEnglishFallback({
|
||||
allRowsExported: "Totale righe esportate: {count}",
|
||||
exportSuccess: "Database esportato con successo",
|
||||
exportAllSuccess: "{count} database esportati con successo",
|
||||
exportSuccessWithErrors: "Esportazione completata con {count} oggetti non riusciti: {firstError}… Dettagli nei commenti -- ERROR nel file esportato.",
|
||||
exportAllSuccessWithErrors: "Esportati {count} database, ma {errorCount} oggetti non riusciti: {firstError}… Dettagli nei commenti -- ERROR nei file.",
|
||||
allDatabasesTask: "Tutti i database ({count})",
|
||||
exportError: "Esportazione non riuscita: {error}",
|
||||
exportCancelled: "Esportazione annullata",
|
||||
|
||||
@@ -7268,6 +7268,8 @@ export default withEnglishFallback({
|
||||
allRowsExported: "累計エクスポート行数: {count}",
|
||||
exportSuccess: "データベースを正常にエクスポートしました",
|
||||
exportAllSuccess: "{count}個のデータベースを正常にエクスポートしました",
|
||||
exportSuccessWithErrors: "エクスポートは完了しましたが、{count} 個のオブジェクトが失敗しました:{firstError}… 詳細はエクスポートファイル内の -- ERROR コメントを確認してください。",
|
||||
exportAllSuccessWithErrors: "{count} 個のデータベースをエクスポートしましたが、{errorCount} 個のオブジェクトが失敗しました:{firstError}… 詳細はファイル内の -- ERROR コメントを確認してください。",
|
||||
allDatabasesTask: "すべてのデータベース ({count})",
|
||||
exportError: "エクスポートに失敗しました: {error}",
|
||||
exportCancelled: "エクスポートがキャンセルされました",
|
||||
|
||||
@@ -6924,6 +6924,8 @@ export default withEnglishFallback({
|
||||
allRowsExported: "총 내보낸 행: {count}",
|
||||
exportSuccess: "데이터베이스를 성공적으로 내보냈습니다",
|
||||
exportAllSuccess: "데이터베이스 {count}개를 성공적으로 내보냈습니다",
|
||||
exportSuccessWithErrors: "내보내기가 완료되었지만 {count}개 객체가 실패했습니다: {firstError}… 자세한 내용은 내보낸 파일의 -- ERROR 주석을 참조하세요.",
|
||||
exportAllSuccessWithErrors: "데이터베이스 {count}개를 내보냈지만 {errorCount}개 객체가 실패했습니다: {firstError}… 자세한 내용은 파일의 -- ERROR 주석을 참조하세요.",
|
||||
allDatabasesTask: "모든 데이터베이스 ({count})",
|
||||
exportError: "내보내기 실패: {error}",
|
||||
exportCancelled: "내보내기 취소됨",
|
||||
|
||||
@@ -7218,6 +7218,8 @@ export default withEnglishFallback({
|
||||
allRowsExported: "Total de linhas exportadas: {count}",
|
||||
exportSuccess: "Banco de dados exportado com sucesso",
|
||||
exportAllSuccess: "{count} bancos de dados exportados com sucesso",
|
||||
exportSuccessWithErrors: "A exportação terminou com {count} objeto(s) com falha: {firstError}… Veja os comentários -- ERROR no arquivo exportado.",
|
||||
exportAllSuccessWithErrors: "{count} bancos de dados exportados, mas {errorCount} objeto(s) falharam: {firstError}… Veja os comentários -- ERROR nos arquivos.",
|
||||
allDatabasesTask: "Todos os bancos de dados ({count})",
|
||||
exportError: "Falha na exportação: {error}",
|
||||
exportCancelled: "Exportação cancelada",
|
||||
|
||||
@@ -7550,6 +7550,8 @@ export default withEnglishFallback({
|
||||
allRowsExported: "Dışa aktarılan toplam satır: {count}",
|
||||
exportSuccess: "Veritabanı başarıyla dışa aktarıldı",
|
||||
exportAllSuccess: "{count} veritabanı başarıyla dışa aktarıldı",
|
||||
exportSuccessWithErrors: "Dışa aktarma tamamlandı ancak {count} nesne başarısız oldu: {firstError}… Ayrıntılar için dışa aktarılan dosyadaki -- ERROR yorumlarına bakın.",
|
||||
exportAllSuccessWithErrors: "{count} veritabanı dışa aktarıldı ancak {errorCount} nesne başarısız oldu: {firstError}… Ayrıntılar için dosyalardaki -- ERROR yorumlarına bakın.",
|
||||
allDatabasesTask: "Tüm veritabanları ({count})",
|
||||
exportError: "Dışa aktarma başarısız: {error}",
|
||||
exportCancelled: "Dışa aktarma iptal edildi",
|
||||
|
||||
@@ -7541,6 +7541,8 @@ export default withEnglishFallback({
|
||||
allRowsExported: "累计已导出 {count} 行",
|
||||
exportSuccess: "数据库导出成功",
|
||||
exportAllSuccess: "已成功导出 {count} 个数据库",
|
||||
exportSuccessWithErrors: "导出完成,但有 {count} 个对象失败:{firstError}……详情见导出文件中的 -- ERROR 注释。",
|
||||
exportAllSuccessWithErrors: "已导出 {count} 个数据库,但有 {errorCount} 个对象失败:{firstError}……详情见导出文件中的 -- ERROR 注释。",
|
||||
allDatabasesTask: "全部数据库({count})",
|
||||
exportError: "导出失败: {error}",
|
||||
exportCancelled: "导出已取消",
|
||||
|
||||
@@ -6531,6 +6531,8 @@ export default withEnglishFallback({
|
||||
allRowsExported: "累計已匯出 {count} 列",
|
||||
exportSuccess: "資料庫匯出成功",
|
||||
exportAllSuccess: "已成功匯出 {count} 個資料庫",
|
||||
exportSuccessWithErrors: "匯出完成,但有 {count} 個物件失敗:{firstError}……詳情見匯出檔案中的 -- ERROR 註解。",
|
||||
exportAllSuccessWithErrors: "已匯出 {count} 個資料庫,但有 {errorCount} 個物件失敗:{firstError}……詳情見匯出檔案中的 -- ERROR 註解。",
|
||||
allDatabasesTask: "全部資料庫({count})",
|
||||
exportError: "匯出失敗: {error}",
|
||||
exportCancelled: "匯出已取消",
|
||||
|
||||
@@ -4825,6 +4825,10 @@ export interface ExportProgress {
|
||||
error: string | null;
|
||||
/** True while listing schema / prefetching metadata before objects are written. */
|
||||
preparing?: boolean;
|
||||
/** Per-object failures written into the file as `-- ERROR` comments (lenient mode). */
|
||||
errorCount?: number;
|
||||
/** First lenient failure, for completion warnings without opening the file. */
|
||||
errorSummary?: string | null;
|
||||
}
|
||||
|
||||
// --- Table Export ---
|
||||
|
||||
@@ -268,6 +268,32 @@ pub struct ExportProgress {
|
||||
/// True while listing schema / prefetching table metadata — before objects are written.
|
||||
#[serde(default)]
|
||||
pub preparing: bool,
|
||||
/// Per-object failures written into the file as `-- ERROR` comments in
|
||||
/// lenient mode. Strict mode fails the whole export instead, so this stays
|
||||
/// zero. Without it a partially failed export reports plain success and
|
||||
/// the errors are only discoverable by opening the file (#8184).
|
||||
#[serde(default)]
|
||||
pub error_count: u64,
|
||||
/// First lenient failure, so completion surfaces can show what went wrong
|
||||
/// without opening the exported file.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error_summary: Option<String>,
|
||||
}
|
||||
|
||||
/// Collects lenient per-object export failures for the terminal progress.
|
||||
#[derive(Default)]
|
||||
struct LenientExportErrors {
|
||||
count: usize,
|
||||
first: Option<String>,
|
||||
}
|
||||
|
||||
impl LenientExportErrors {
|
||||
fn record(&mut self, message: String) {
|
||||
if self.first.is_none() {
|
||||
self.first = Some(message.clone());
|
||||
}
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -1972,10 +1998,16 @@ fn database_export_metadata_prefetch_concurrency(db_type: DatabaseType) -> usize
|
||||
}
|
||||
}
|
||||
|
||||
fn record_export_error<W: Write>(file: &mut W, fail_on_error: bool, message: String) -> Result<(), String> {
|
||||
fn record_export_error<W: Write>(
|
||||
file: &mut W,
|
||||
fail_on_error: bool,
|
||||
message: String,
|
||||
lenient_errors: &mut LenientExportErrors,
|
||||
) -> Result<(), String> {
|
||||
if fail_on_error {
|
||||
Err(message)
|
||||
} else {
|
||||
lenient_errors.record(message.clone());
|
||||
writeln!(file, "-- ERROR {message}").map_err(|error| format!("Failed to write file: {error}"))
|
||||
}
|
||||
}
|
||||
@@ -2139,6 +2171,8 @@ fn emit_database_export_running(
|
||||
status: ExportStatus::Running,
|
||||
error: None,
|
||||
preparing,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2160,6 +2194,8 @@ fn emit_database_export_cancelled(
|
||||
status: ExportStatus::Cancelled,
|
||||
error: None,
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2534,6 +2570,7 @@ async fn export_database_sql_core_inner(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut lenient_errors = LenientExportErrors::default();
|
||||
|
||||
// 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);
|
||||
@@ -2632,7 +2669,12 @@ async fn export_database_sql_core_inner(
|
||||
match list_postgres_extension_members(state, &pool_key, &request.schema).await {
|
||||
Ok(members) => members,
|
||||
Err(e) => {
|
||||
record_export_error(&mut file, request.fail_on_error, format!("reading extension members: {e}"))?;
|
||||
record_export_error(
|
||||
&mut file,
|
||||
request.fail_on_error,
|
||||
format!("reading extension members: {e}"),
|
||||
&mut lenient_errors,
|
||||
)?;
|
||||
PostgresExtensionMembers::default()
|
||||
}
|
||||
}
|
||||
@@ -2656,7 +2698,12 @@ async fn export_database_sql_core_inner(
|
||||
})
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
record_export_error(&mut file, request.fail_on_error, format!("exporting extensions: {e}"))?;
|
||||
record_export_error(
|
||||
&mut file,
|
||||
request.fail_on_error,
|
||||
format!("exporting extensions: {e}"),
|
||||
&mut lenient_errors,
|
||||
)?;
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
@@ -2698,7 +2745,12 @@ async fn export_database_sql_core_inner(
|
||||
Ok(sequences) => sequences,
|
||||
Err(e) if e == EXPORT_CANCELLED_ERROR => return Err(EXPORT_CANCELLED_ERROR.to_string()),
|
||||
Err(e) => {
|
||||
record_export_error(&mut file, request.fail_on_error, format!("exporting sequences: {e}"))?;
|
||||
record_export_error(
|
||||
&mut file,
|
||||
request.fail_on_error,
|
||||
format!("exporting sequences: {e}"),
|
||||
&mut lenient_errors,
|
||||
)?;
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
@@ -2972,6 +3024,8 @@ async fn export_database_sql_core_inner(
|
||||
status: ExportStatus::Running,
|
||||
error: None,
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
});
|
||||
|
||||
// Export structure
|
||||
@@ -2994,6 +3048,8 @@ async fn export_database_sql_core_inner(
|
||||
status: ExportStatus::Running,
|
||||
error: None,
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
});
|
||||
|
||||
writeln!(file, "{};\n", generate_postgres_sequence_create_ddl(sequence, &request.schema))
|
||||
@@ -3040,6 +3096,7 @@ async fn export_database_sql_core_inner(
|
||||
&mut file,
|
||||
request.fail_on_error,
|
||||
format!("exporting table structure {table_name}: {e}"),
|
||||
&mut lenient_errors,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -3079,6 +3136,7 @@ async fn export_database_sql_core_inner(
|
||||
&mut file,
|
||||
request.fail_on_error,
|
||||
format!("exporting columns for table {table_name}: {e}"),
|
||||
&mut lenient_errors,
|
||||
)?;
|
||||
object_index += 1;
|
||||
continue;
|
||||
@@ -3128,6 +3186,8 @@ async fn export_database_sql_core_inner(
|
||||
status: ExportStatus::Running,
|
||||
error: None,
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
});
|
||||
Ok(())
|
||||
},
|
||||
@@ -3195,6 +3255,7 @@ async fn export_database_sql_core_inner(
|
||||
&mut file,
|
||||
request.fail_on_error,
|
||||
format!("exporting data for table {table_name}: {error}"),
|
||||
&mut lenient_errors,
|
||||
)?;
|
||||
break;
|
||||
}
|
||||
@@ -3232,6 +3293,8 @@ async fn export_database_sql_core_inner(
|
||||
status: ExportStatus::Running,
|
||||
error: None,
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
});
|
||||
if row_count < batch_size {
|
||||
break;
|
||||
@@ -3276,6 +3339,8 @@ async fn export_database_sql_core_inner(
|
||||
status: ExportStatus::Running,
|
||||
error: None,
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
});
|
||||
|
||||
match crate::schema::get_object_source_core(
|
||||
@@ -3303,7 +3368,12 @@ async fn export_database_sql_core_inner(
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
record_export_error(&mut file, request.fail_on_error, format!("exporting view {view_name}: {e}"))?;
|
||||
record_export_error(
|
||||
&mut file,
|
||||
request.fail_on_error,
|
||||
format!("exporting view {view_name}: {e}"),
|
||||
&mut lenient_errors,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3328,6 +3398,8 @@ async fn export_database_sql_core_inner(
|
||||
status: ExportStatus::Running,
|
||||
error: None,
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
});
|
||||
|
||||
match crate::schema::get_object_source_core(
|
||||
@@ -3359,6 +3431,7 @@ async fn export_database_sql_core_inner(
|
||||
&mut file,
|
||||
request.fail_on_error,
|
||||
format!("exporting procedure {proc_name}: {e}"),
|
||||
&mut lenient_errors,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -3384,6 +3457,8 @@ async fn export_database_sql_core_inner(
|
||||
status: ExportStatus::Running,
|
||||
error: None,
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
});
|
||||
|
||||
match crate::schema::get_object_source_core(
|
||||
@@ -3415,6 +3490,7 @@ async fn export_database_sql_core_inner(
|
||||
&mut file,
|
||||
request.fail_on_error,
|
||||
format!("exporting function {func_name}: {e}"),
|
||||
&mut lenient_errors,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -3450,6 +3526,8 @@ async fn export_database_sql_core_inner(
|
||||
status: ExportStatus::Done,
|
||||
error: None,
|
||||
preparing: false,
|
||||
error_count: lenient_errors.count as u64,
|
||||
error_summary: lenient_errors.first.clone(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -3518,6 +3596,7 @@ mod tests {
|
||||
DdlNormalizeOptions, ExportedTableSql, PostgresExportExtension, PostgresExportSequence,
|
||||
PostgresExtensionMembers, DATABASE_EXPORT_INSERT_BATCH_SIZE, DATABASE_EXPORT_ROW_LIMIT,
|
||||
};
|
||||
use super::{ExportProgress, LenientExportErrors};
|
||||
use crate::connection::AppState;
|
||||
use crate::models::connection::DatabaseType;
|
||||
use crate::storage::Storage;
|
||||
@@ -5407,14 +5486,73 @@ mod tests {
|
||||
let path = std::env::temp_dir().join(format!("dbx-strict-export-{}.sql", uuid::Uuid::new_v4()));
|
||||
let mut file = std::fs::File::create(&path).unwrap();
|
||||
|
||||
let result = record_export_error(&mut file, true, "exporting table users: permission denied".to_string());
|
||||
let mut lenient_errors = LenientExportErrors::default();
|
||||
let result = record_export_error(
|
||||
&mut file,
|
||||
true,
|
||||
"exporting table users: permission denied".to_string(),
|
||||
&mut lenient_errors,
|
||||
);
|
||||
drop(file);
|
||||
|
||||
assert_eq!(result.unwrap_err(), "exporting table users: permission denied");
|
||||
assert_eq!(lenient_errors.count, 0);
|
||||
assert!(std::fs::read_to_string(&path).unwrap().is_empty());
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lenient_export_errors_track_count_and_first_failure_for_terminal_progress() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-lenient-export-{}.sql", uuid::Uuid::new_v4()));
|
||||
let mut file = std::fs::File::create(&path).unwrap();
|
||||
|
||||
let mut lenient_errors = LenientExportErrors::default();
|
||||
record_export_error(
|
||||
&mut file,
|
||||
false,
|
||||
"exporting table orders: RPC call timed out".to_string(),
|
||||
&mut lenient_errors,
|
||||
)
|
||||
.unwrap();
|
||||
record_export_error(
|
||||
&mut file,
|
||||
false,
|
||||
"exporting view active_users: RPC call timed out".to_string(),
|
||||
&mut lenient_errors,
|
||||
)
|
||||
.unwrap();
|
||||
drop(file);
|
||||
|
||||
// The terminal Done progress must carry enough information to warn
|
||||
// instead of reporting plain success (#8184).
|
||||
assert_eq!(lenient_errors.count, 2);
|
||||
assert_eq!(lenient_errors.first.as_deref(), Some("exporting table orders: RPC call timed out"));
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(contents.contains("-- ERROR exporting table orders: RPC call timed out"));
|
||||
assert!(contents.contains("-- ERROR exporting view active_users: RPC call timed out"));
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_progress_deserializes_events_without_lenient_error_fields() {
|
||||
// Progress events persisted by older versions lack errorCount /
|
||||
// errorSummary; they must keep deserializing with zeroed defaults.
|
||||
let legacy = serde_json::from_value::<ExportProgress>(serde_json::json!({
|
||||
"exportId": "export-1",
|
||||
"currentObject": "users",
|
||||
"objectIndex": 3,
|
||||
"totalObjects": 10,
|
||||
"rowsExported": 120,
|
||||
"totalRows": null,
|
||||
"status": "Done",
|
||||
"error": null
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(legacy.error_count, 0);
|
||||
assert_eq!(legacy.error_summary, None);
|
||||
}
|
||||
|
||||
async fn test_app_state(scratch_dir: &std::path::Path) -> AppState {
|
||||
let storage = Storage::open(&scratch_dir.join("storage.db")).await.unwrap();
|
||||
AppState::new(storage)
|
||||
|
||||
@@ -105,6 +105,8 @@ pub async fn start_database_export(
|
||||
status: ExportStatus::Error,
|
||||
error: Some(e.clone()),
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
};
|
||||
if let Ok(json) = serde_json::to_string(&progress) {
|
||||
let _ = tx.send(json);
|
||||
|
||||
@@ -59,6 +59,8 @@ pub async fn export_database_sql(
|
||||
status: ExportStatus::Error,
|
||||
error: Some(e),
|
||||
preparing: false,
|
||||
error_count: 0,
|
||||
error_summary: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user