From 5efbe360d9d050ecd932adce0cd575002358201d Mon Sep 17 00:00:00 2001 From: zipg Date: Mon, 7 Sep 2026 23:20:46 +0800 Subject: [PATCH] feat(postgres): export all schemas in one SQL file --- .../export/DatabaseExportDialog.vue | 18 +- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/ko.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/tr.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + apps/desktop/src/lib/export/databaseExport.ts | 11 + crates/dbx-core/src/database_export.rs | 249 +++++++++++++++--- .../tests/live_postgres_all_schema_export.rs | 186 +++++++++++++ .../issue-8336-postgresql-all-schemas.png | Bin 0 -> 64196 bytes packages/app-tests/databaseExportPlan.test.ts | 17 ++ 15 files changed, 452 insertions(+), 38 deletions(-) create mode 100644 crates/dbx-core/tests/live_postgres_all_schema_export.rs create mode 100644 docs/screenshots/issue-8336-postgresql-all-schemas.png diff --git a/apps/desktop/src/components/export/DatabaseExportDialog.vue b/apps/desktop/src/components/export/DatabaseExportDialog.vue index c9bcd863f4..a3e413f4ce 100644 --- a/apps/desktop/src/components/export/DatabaseExportDialog.vue +++ b/apps/desktop/src/components/export/DatabaseExportDialog.vue @@ -59,6 +59,7 @@ const filteredTables = computed(() => { return tables.value.filter((name) => name.toLowerCase().includes(q)); }); const tableError = ref(null); +const POSTGRES_ALL_SCHEMAS = "__DBX_ALL_SCHEMAS__"; // Options const includeStructure = ref(true); @@ -70,6 +71,7 @@ const omitAutoIncrement = ref(false); // `AUTO_INCREMENT` stripping is a MySQL-only DDL transform (backend gates on // db_type == mysql, which also covers MariaDB / TiDB / OceanBase-MySQL-mode). const isMysqlFamily = computed(() => store.getConfig(connectionId.value)?.db_type === "mysql"); +const isPostgresAllSchemas = computed(() => store.getConfig(connectionId.value)?.db_type === "postgres" && schema.value === POSTGRES_ALL_SCHEMAS); // Export state const isExporting = ref(false); @@ -115,6 +117,7 @@ const canExport = computed(() => { const hasContent = includeStructure.value || includeData.value || includeObjects.value; if (!connectionId.value || !hasContent || isExporting.value) return false; if (exportAllDatabases.value) return selectedDatabases.value.length > 0 && !loadingMeta.value; + if (isPostgresAllSchemas.value) return database.value && schemas.value.length > 0 && !loadingMeta.value; return ( database.value && schema.value && @@ -230,6 +233,13 @@ async function loadSchemas(preferredSchema = "") { } async function loadTables(preferredTable = "", preferredTables: string[] = []) { + if (schema.value === POSTGRES_ALL_SCHEMAS) { + tables.value = []; + selectedTables.value = []; + loadingTables.value = false; + tableError.value = null; + return; + } if (!connectionId.value || !database.value || !schema.value) return; loadingTables.value = true; tableError.value = null; @@ -371,9 +381,9 @@ async function startExport() { exportId: exportId.value, connectionId: connectionId.value, database: database.value, - schema: schema.value, + schema: isPostgresAllSchemas.value ? "" : schema.value, filePath, - selectedTables: includeStructure.value || includeData.value ? buildSelectedTablesPayload(tables.value, selectedTables.value) : undefined, + selectedTables: !isPostgresAllSchemas.value && (includeStructure.value || includeData.value) ? buildSelectedTablesPayload(tables.value, selectedTables.value) : undefined, includeStructure: includeStructure.value, includeData: includeData.value, includeObjects: includeObjects.value, @@ -737,6 +747,7 @@ watch(schema, (value) => { const preferredTables = pendingPrefillTables.value; pendingPrefillTable.value = ""; pendingPrefillTables.value = []; + if (value === POSTGRES_ALL_SCHEMAS) return; if (value) loadTables(preferredTable, preferredTables).catch((e) => toast(String(e), 5000)); }); @@ -846,12 +857,13 @@ watch( + {{ t("databaseExport.allSchemas") }} {{ s }} -
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index e4cbac10a4..6205b2d6e1 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -7708,6 +7708,7 @@ export default { noTables: "No tables found", export: "Export", exportAllDatabases: "Export All Databases", + allSchemas: "All schemas", exporting: "Exporting...", preparing: "Preparing export (reading tables and metadata)...", preparingObject: "Preparing: {object}", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 015f4b8ce4..87b8dbd9ee 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -7290,6 +7290,7 @@ export default withEnglishFallback({ 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})", + allSchemas: "Todos los esquemas", exportError: "Error al exportar: {error}", exportCancelled: "Exportación cancelada", filterTables: "Buscar tablas...", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 15437d36c5..d6cc5a5f24 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -7292,6 +7292,7 @@ export default withEnglishFallback({ 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})", + allSchemas: "Tutti gli schemi", exportError: "Esportazione non riuscita: {error}", exportCancelled: "Esportazione annullata", runInBackground: "Esegui in background", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index c82a64b03c..fd765ff3e3 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -7344,6 +7344,7 @@ export default withEnglishFallback({ exportSuccessWithErrors: "エクスポートは完了しましたが、{count} 個のオブジェクトが失敗しました:{firstError}… 詳細はエクスポートファイル内の -- ERROR コメントを確認してください。", exportAllSuccessWithErrors: "{count} 個のデータベースをエクスポートしましたが、{errorCount} 個のオブジェクトが失敗しました:{firstError}… 詳細はファイル内の -- ERROR コメントを確認してください。", allDatabasesTask: "すべてのデータベース ({count})", + allSchemas: "すべてのスキーマ", exportError: "エクスポートに失敗しました: {error}", exportCancelled: "エクスポートがキャンセルされました", }, diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 8258704f72..1b4101e86c 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -7000,6 +7000,7 @@ export default withEnglishFallback({ exportSuccessWithErrors: "내보내기가 완료되었지만 {count}개 객체가 실패했습니다: {firstError}… 자세한 내용은 내보낸 파일의 -- ERROR 주석을 참조하세요.", exportAllSuccessWithErrors: "데이터베이스 {count}개를 내보냈지만 {errorCount}개 객체가 실패했습니다: {firstError}… 자세한 내용은 파일의 -- ERROR 주석을 참조하세요.", allDatabasesTask: "모든 데이터베이스 ({count})", + allSchemas: "모든 스키마", exportError: "내보내기 실패: {error}", exportCancelled: "내보내기 취소됨", runInBackground: "백그라운드에서 실행", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index a0f366649d..d03e83658a 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -7294,6 +7294,7 @@ export default withEnglishFallback({ 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})", + allSchemas: "Todos os esquemas", exportError: "Falha na exportação: {error}", exportCancelled: "Exportação cancelada", }, diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 6da8987739..bc76c5a2dd 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -7616,6 +7616,7 @@ export default withEnglishFallback({ 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})", + allSchemas: "Tüm şemalar", exportError: "Dışa aktarma başarısız: {error}", exportCancelled: "Dışa aktarma iptal edildi", runInBackground: "Arka planda çalıştır", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index be6eb88a97..cdd7ff9628 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -7689,6 +7689,7 @@ export default withEnglishFallback({ noTables: "未找到表", export: "导出", exportAllDatabases: "导出全部数据库", + allSchemas: "全部 Schema", exporting: "正在导出...", preparing: "正在准备导出(读取表与元数据)...", preparingObject: "正在准备: {object}", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 1d8f480f27..235a99a757 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -6607,6 +6607,7 @@ export default withEnglishFallback({ exportSuccessWithErrors: "匯出完成,但有 {count} 個物件失敗:{firstError}……詳情見匯出檔案中的 -- ERROR 註解。", exportAllSuccessWithErrors: "已匯出 {count} 個資料庫,但有 {errorCount} 個物件失敗:{firstError}……詳情見匯出檔案中的 -- ERROR 註解。", allDatabasesTask: "全部資料庫({count})", + allSchemas: "全部 schema", exportError: "匯出失敗: {error}", exportCancelled: "匯出已取消", runInBackground: "背景執行", diff --git a/apps/desktop/src/lib/export/databaseExport.ts b/apps/desktop/src/lib/export/databaseExport.ts index 2b6477d733..c503b8810b 100644 --- a/apps/desktop/src/lib/export/databaseExport.ts +++ b/apps/desktop/src/lib/export/databaseExport.ts @@ -166,6 +166,17 @@ export function buildAllDatabaseExportPlan(options: AllDatabaseExportPlanInput): displayName: schema, })); } + // PostgreSQL supports multiple schemas in one database. Keep the database + // as the export unit so the backend can produce one restore script for all + // schemas instead of one file per schema. + if (options.dbType === "postgres" && options.schemaAware) { + return options.databases.map((database) => ({ + database, + schema: "", + fileStem: database, + displayName: database, + })); + } return options.databases.flatMap((database) => { const schemas = options.schemaAware ? filterExportableSchemas(options.schemasByDatabase?.[database] ?? [], options.dbType).filter((schema) => schema.trim()) : [database]; const exportSchemas = schemas.length > 0 ? schemas : [database]; diff --git a/crates/dbx-core/src/database_export.rs b/crates/dbx-core/src/database_export.rs index 095d10583c..116a945f42 100644 --- a/crates/dbx-core/src/database_export.rs +++ b/crates/dbx-core/src/database_export.rs @@ -2586,10 +2586,21 @@ pub async fn export_database_sql_core( request: &DatabaseExportRequest, on_progress: impl Fn(ExportProgress) + Sync, ) -> Result<(), String> { + let db_type = state + .configs + .read() + .await + .get(&request.connection_id) + .map(|config| config.db_type) + .ok_or_else(|| format!("Connection config not found: {}", request.connection_id))?; // Keep the large export state machine on the heap. Besides making the // caller future small, this prevents the metadata-prefetch locals from // exhausting the bounded stack used by test and runtime worker threads. - let result = Box::pin(export_database_sql_core_inner(state, request, &on_progress)).await; + let result = if matches!(db_type, DatabaseType::Postgres) && request.schema.trim().is_empty() { + Box::pin(export_postgres_all_schemas_sql_core(state, request, &on_progress)).await + } else { + Box::pin(export_database_sql_core_inner(state, request, &on_progress)).await + }; let metadata_session_id = database_export_client_session_id(&request.export_id); if let Err(error) = state.close_metadata_session_pool(&request.connection_id, Some(&request.database), &metadata_session_id).await @@ -2611,6 +2622,185 @@ pub async fn export_database_sql_core( } } +/// Destination directory that must exist (and stay on the same filesystem) +/// before the export file may be written there, or `None` when the file path +/// has no parent (e.g. a bare file name or the filesystem root). Shared by +/// [`create_database_export_writer`] and the all-schemas export, which +/// validates the destination up front instead of only after exporting every +/// schema to temporary files. +fn export_destination_parent_dir(file_path: &str) -> Option<&std::path::Path> { + let parent = std::path::Path::new(file_path).parent()?; + (!parent.as_os_str().is_empty()).then_some(parent) +} + +async fn create_database_export_writer( + state: &Arc, + request: &DatabaseExportRequest, +) -> Result { + let mut expected_destination_identity = None; + if let Some(parent) = export_destination_parent_dir(&request.file_path) { + expected_destination_identity = ensure_export_destination_dir(state, parent).await?; + } + let file = std::fs::File::create(&request.file_path).map_err(|e| format!("Failed to write file: {e}"))?; + // The directory check above and this `File::create` are separate + // operations: the mount can disappear and be replaced by something else + // at the same path in between. Re-check the identity of the handle we + // actually opened, not just the path, and refuse to keep a backup that + // landed on the wrong filesystem. See #6327. + let opened_destination_identity = export_destination_identity_for_file(&file); + if export_destination_identity_mismatch( + expected_destination_identity.as_ref(), + opened_destination_identity.as_ref(), + ) { + drop(file); + let _ = std::fs::remove_file(&request.file_path); + return Err(format!( + "Backup destination for {} changed while opening the output file -- the directory now \ + resolves to a different filesystem than the one just verified. If a removable or network \ + drive was disconnected and reconnected, retry the backup.", + request.file_path + )); + } + Ok(match request.output_compression { + DatabaseExportOutputCompression::None => DatabaseExportWriter::Plain(BufWriter::new(file)), + DatabaseExportOutputCompression::Gzip => { + DatabaseExportWriter::Gzip(Box::new(GzEncoder::new(BufWriter::new(file), Compression::default()))) + } + }) +} + +fn postgres_export_schema_names(schemas: Vec) -> Vec { + let mut seen = HashSet::new(); + schemas + .into_iter() + .map(|schema| schema.trim().to_string()) + .filter(|schema| !schema.is_empty() && schema != "information_schema" && !schema.starts_with("pg_")) + .filter(|schema| seen.insert(schema.clone())) + .collect() +} + +fn postgres_create_schema_sql(schema: &str) -> String { + format!("CREATE SCHEMA IF NOT EXISTS {};", quote_identifier(schema, &DatabaseType::Postgres)) +} + +async fn export_postgres_all_schemas_sql_core( + state: &Arc, + request: &DatabaseExportRequest, + on_progress: impl Fn(ExportProgress) + Sync, +) -> Result<(), String> { + let schemas = postgres_export_schema_names( + crate::schema::list_schemas_core(state, &request.connection_id, &request.database).await?, + ); + if schemas.is_empty() { + return Err(format!("No exportable schemas found in database '{}'.", request.database)); + } + + // Validate the destination before exporting every schema to temporary + // files: the writer below only runs after the loop, which for large + // databases can be hours away, and a missing or unwritable destination + // must fail fast instead. This is an early fail, not a replacement -- the + // writer still performs its own checks when it opens the output file. + if let Some(parent) = export_destination_parent_dir(&request.file_path) { + ensure_export_destination_dir(state, parent).await?; + } + + let temp_dir = + tempfile::tempdir().map_err(|error| format!("Failed to create temporary export directory: {error}"))?; + let result = async { + let mut schema_outputs = Vec::with_capacity(schemas.len()); + let mut rows_exported = 0_u64; + let mut error_count = 0_u64; + let mut error_summary = None; + + for (schema_index, schema_name) in schemas.iter().enumerate() { + let mut schema_request = request.clone(); + schema_request.schema = schema_name.clone(); + schema_request.file_path = temp_dir.path().join(format!("schema-{schema_index}.sql")).display().to_string(); + schema_request.output_compression = DatabaseExportOutputCompression::None; + + let terminal = Arc::new(std::sync::Mutex::new(None::)); + let terminal_for_callback = terminal.clone(); + let schema_name_for_callback = schema_name.clone(); + let completed_rows = rows_exported; + let child_progress = |mut progress: ExportProgress| { + if matches!(progress.status, ExportStatus::Done | ExportStatus::Cancelled) { + *terminal_for_callback.lock().expect("export terminal mutex poisoned") = Some(progress); + return; + } + progress.export_id = request.export_id.clone(); + progress.current_object = if progress.current_object.is_empty() { + schema_name_for_callback.clone() + } else { + format!("{schema_name_for_callback}: {}", progress.current_object) + }; + progress.rows_exported = completed_rows.saturating_add(progress.rows_exported); + on_progress(progress); + }; + + Box::pin(export_database_sql_core_inner(state, &schema_request, child_progress)).await?; + let terminal = terminal.lock().expect("export terminal mutex poisoned").clone(); + if terminal.as_ref().is_some_and(|progress| matches!(progress.status, ExportStatus::Cancelled)) { + return Err(EXPORT_CANCELLED_ERROR.to_string()); + } + if let Some(progress) = terminal { + rows_exported = rows_exported.saturating_add(progress.rows_exported); + error_count = error_count.saturating_add(progress.error_count); + // Aggregate lenient failure summaries across schemas; keeping + // only the first schema's summary would understate the errors + // behind an error_count that accumulates over all schemas. + if let Some(summary) = progress.error_summary { + error_summary = Some(match error_summary.take() { + Some(existing) => format!("{existing}; {summary}"), + None => summary, + }); + } + } + schema_outputs.push((schema_name.clone(), schema_request.file_path)); + } + + let mut file = create_database_export_writer(state, request).await?; + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); + writeln!(file, "-- Database export: {}", request.database).map_err(|e| format!("Failed to write file: {e}"))?; + writeln!(file, "-- Date: {timestamp}").map_err(|e| format!("Failed to write file: {e}"))?; + writeln!(file, "-- Generated by DBX").map_err(|e| format!("Failed to write file: {e}"))?; + writeln!(file, "-- PostgreSQL schemas: {}", schemas.join(", ")) + .map_err(|e| format!("Failed to write file: {e}"))?; + writeln!(file).map_err(|e| format!("Failed to write file: {e}"))?; + for schema_name in &schemas { + writeln!(file, "{}", postgres_create_schema_sql(schema_name)) + .map_err(|e| format!("Failed to write file: {e}"))?; + } + writeln!(file).map_err(|e| format!("Failed to write file: {e}"))?; + + for (schema_name, path) in schema_outputs { + writeln!(file, "-- Schema export: {schema_name}").map_err(|e| format!("Failed to write file: {e}"))?; + let mut source = std::io::BufReader::new( + std::fs::File::open(path).map_err(|e| format!("Failed to read temporary schema export: {e}"))?, + ); + std::io::copy(&mut source, &mut file).map_err(|e| format!("Failed to combine schema export: {e}"))?; + writeln!(file).map_err(|e| format!("Failed to write file: {e}"))?; + } + file.finish()?; + on_progress(ExportProgress { + export_id: request.export_id.clone(), + current_object: request.database.clone(), + object_index: schemas.len(), + total_objects: schemas.len(), + rows_exported, + total_rows: None, + status: ExportStatus::Done, + error: None, + preparing: false, + error_count, + error_summary, + }); + Ok(()) + } + .await; + + result +} + async fn export_database_sql_core_inner( state: &Arc, request: &DatabaseExportRequest, @@ -2658,38 +2848,7 @@ async fn export_database_sql_core_inner( )) .await?; // 4. Create file - let mut expected_destination_identity = None; - if let Some(parent) = std::path::Path::new(&request.file_path).parent() { - if !parent.as_os_str().is_empty() { - expected_destination_identity = ensure_export_destination_dir(state, parent).await?; - } - } - let file = std::fs::File::create(&request.file_path).map_err(|e| format!("Failed to write file: {e}"))?; - // The directory check above and this `File::create` are separate - // operations: the mount can disappear and be replaced by something else - // at the same path in between. Re-check the identity of the handle we - // actually opened, not just the path, and refuse to keep a backup that - // landed on the wrong filesystem. See #6327. - let opened_destination_identity = export_destination_identity_for_file(&file); - if export_destination_identity_mismatch( - expected_destination_identity.as_ref(), - opened_destination_identity.as_ref(), - ) { - drop(file); - let _ = std::fs::remove_file(&request.file_path); - return Err(format!( - "Backup destination for {} changed while opening the output file -- the directory now \ - resolves to a different filesystem than the one just verified. If a removable or network \ - drive was disconnected and reconnected, retry the backup.", - request.file_path - )); - } - let mut file = match request.output_compression { - DatabaseExportOutputCompression::None => DatabaseExportWriter::Plain(BufWriter::new(file)), - DatabaseExportOutputCompression::Gzip => { - DatabaseExportWriter::Gzip(Box::new(GzEncoder::new(BufWriter::new(file), Compression::default()))) - } - }; + let mut file = create_database_export_writer(state, request).await?; let create_database_preamble = if request.include_create_database && matches!(db_type, DatabaseType::Mysql) { Some(mysql_database_export_preamble_for_request(state, request, &client_session_id).await) @@ -3648,8 +3807,8 @@ mod tests { use super::{ await_export_operation, await_export_stream_operation, clear_export_cancelled, concurrent_metadata_prefetch_allowed, database_export_metadata_prefetch_concurrency, - emit_database_export_cancelled, set_export_cancelled, snapshot_batch_cancelled, ExportStatus, - EXPORT_CANCELLED_ERROR, + emit_database_export_cancelled, postgres_create_schema_sql, postgres_export_schema_names, set_export_cancelled, + snapshot_batch_cancelled, ExportStatus, EXPORT_CANCELLED_ERROR, }; use super::{ build_database_export_object_source_sql, build_database_sql_export, build_export_insert_statements, @@ -3948,6 +4107,26 @@ mod tests { assert!(!ddl.contains("VERSION")); } + #[test] + fn postgres_all_schema_export_filters_system_schemas_and_deduplicates() { + assert_eq!( + postgres_export_schema_names(vec![ + " public ".to_string(), + "pg_catalog".to_string(), + "information_schema".to_string(), + "private".to_string(), + "private".to_string(), + "".to_string(), + ]), + vec!["public".to_string(), "private".to_string()] + ); + } + + #[test] + fn postgres_all_schema_export_creates_quoted_schema_if_missing() { + assert_eq!(postgres_create_schema_sql("tenant\"data"), "CREATE SCHEMA IF NOT EXISTS \"tenant\"\"data\";"); + } + #[test] fn postgres_extension_member_filter_keeps_user_overload_with_same_name() { let mut members = PostgresExtensionMembers::default(); diff --git a/crates/dbx-core/tests/live_postgres_all_schema_export.rs b/crates/dbx-core/tests/live_postgres_all_schema_export.rs new file mode 100644 index 0000000000..a15f45d1c2 --- /dev/null +++ b/crates/dbx-core/tests/live_postgres_all_schema_export.rs @@ -0,0 +1,186 @@ +use dbx_core::connection::AppState; +use dbx_core::database_export::{export_database_sql_core, DatabaseExportRequest, ExportStatus}; +use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; +use dbx_core::query::execute_sql_statement; +use dbx_core::sql::SqlFileRequest; +use dbx_core::sql_file_import::execute_sql_file_path; +use dbx_core::storage::Storage; +use std::sync::{Arc, Mutex}; +use tokio_util::sync::CancellationToken; + +fn live_postgres_config(id: &str, database: &str) -> ConnectionConfig { + let host = std::env::var("DBX_LIVE_POSTGRES_EXPORT_HOST").expect("DBX_LIVE_POSTGRES_EXPORT_HOST"); + let port = + std::env::var("DBX_LIVE_POSTGRES_EXPORT_PORT").ok().and_then(|value| value.parse::().ok()).unwrap_or(5432); + let username = std::env::var("DBX_LIVE_POSTGRES_EXPORT_USER").expect("DBX_LIVE_POSTGRES_EXPORT_USER"); + let password = std::env::var("DBX_LIVE_POSTGRES_EXPORT_PASSWORD").expect("DBX_LIVE_POSTGRES_EXPORT_PASSWORD"); + + serde_json::from_value(serde_json::json!({ + "id": id, + "name": id, + "db_type": DatabaseType::Postgres, + "host": host, + "port": port, + "username": username, + "password": password, + "database": database, + "ssl": false, + "connect_timeout_secs": 10, + "query_timeout_secs": 30, + "idle_timeout_secs": 60, + "keepalive_interval_secs": 0 + })) + .expect("live PostgreSQL export config should deserialize") +} + +#[tokio::test] +#[ignore = "requires a live PostgreSQL account with CREATE DATABASE privilege"] +async fn live_postgres_all_schema_export_restores_one_sql_file() { + let suffix = uuid::Uuid::new_v4().simple().to_string(); + let source_database = format!("dbx_issue8336_src_{}", &suffix[..12]); + let target_database = format!("dbx_issue8336_dst_{}", &suffix[..12]); + let expected_export_database = source_database.clone(); + let admin_connection_id = format!("postgres-all-schema-admin-{suffix}"); + let source_connection_id = format!("postgres-all-schema-source-{suffix}"); + let target_connection_id = format!("postgres-all-schema-target-{suffix}"); + let dir = tempfile::tempdir().expect("create export temp directory"); + let storage = Storage::open(&dir.path().join("storage.db")).await.expect("open temp storage"); + let state = Arc::new(AppState::new(storage)); + state + .configs + .write() + .await + .insert(admin_connection_id.clone(), live_postgres_config(&admin_connection_id, "postgres")); + for database in [&source_database, &target_database] { + Box::pin(execute_sql_statement( + &state, + &admin_connection_id, + "postgres", + &format!("CREATE DATABASE \"{database}\""), + None, + None, + )) + .await + .expect("create disposable PostgreSQL database"); + } + state + .configs + .write() + .await + .insert(source_connection_id.clone(), live_postgres_config(&source_connection_id, &source_database)); + state + .configs + .write() + .await + .insert(target_connection_id.clone(), live_postgres_config(&target_connection_id, &target_database)); + + for statement in [ + "CREATE SCHEMA inventory", + "CREATE SCHEMA reporting", + "CREATE TABLE public.accounts (id integer PRIMARY KEY, name text NOT NULL)", + "CREATE TABLE inventory.products (id integer PRIMARY KEY, sku text NOT NULL)", + "CREATE TABLE reporting.daily_totals (day date PRIMARY KEY, amount numeric(12,2) NOT NULL)", + "CREATE VIEW reporting.positive_totals AS SELECT day, amount FROM reporting.daily_totals WHERE amount > 0", + "INSERT INTO public.accounts VALUES (1, 'Alice'), (2, 'Bob')", + "INSERT INTO inventory.products VALUES (10, 'SKU-10')", + "INSERT INTO reporting.daily_totals VALUES ('2026-09-07', 42.50)", + ] { + Box::pin(execute_sql_statement(&state, &source_connection_id, &source_database, statement, None, None)) + .await + .expect("create source fixture"); + } + + let export_path = dir.path().join("postgres-all-schemas.sql"); + let progress = Arc::new(Mutex::new(Vec::new())); + let progress_sink = progress.clone(); + export_database_sql_core( + &state, + &DatabaseExportRequest { + export_id: format!("postgres-all-schema-export-{suffix}"), + connection_id: source_connection_id, + database: source_database.clone(), + schema: String::new(), + file_path: export_path.display().to_string(), + selected_tables: Vec::new(), + excluded_tables: Vec::new(), + include_structure: true, + include_data: true, + include_objects: true, + include_create_database: false, + drop_table_if_exists: false, + omit_auto_increment: false, + fail_on_error: true, + output_compression: Default::default(), + snapshot_session_id: None, + batch_size: 1000, + }, + move |event| progress_sink.lock().expect("progress mutex poisoned").push(event), + ) + .await + .expect("export every PostgreSQL schema"); + + let exported = std::fs::read_to_string(&export_path).expect("read combined export"); + for expected in [ + "CREATE SCHEMA IF NOT EXISTS \"public\";", + "CREATE SCHEMA IF NOT EXISTS \"inventory\";", + "CREATE SCHEMA IF NOT EXISTS \"reporting\";", + "CREATE TABLE \"public\".\"accounts\"", + "CREATE TABLE \"inventory\".\"products\"", + "CREATE TABLE \"reporting\".\"daily_totals\"", + "positive_totals", + ] { + assert!(exported.contains(expected), "missing {expected} in combined export:\n{exported}"); + } + assert_eq!(exported.matches("-- Schema export:").count(), 3, "unexpected schema sections:\n{exported}"); + assert!( + progress.lock().expect("progress mutex poisoned").iter().any(|event| { + matches!(event.status, ExportStatus::Done) && event.current_object == expected_export_database + }), + "combined export must emit a terminal Done event" + ); + + execute_sql_file_path( + &state, + &SqlFileRequest { + execution_id: format!("postgres-all-schema-import-{suffix}"), + connection_id: target_connection_id.clone(), + database: target_database.clone(), + file_path: export_path.display().to_string(), + continue_on_error: false, + }, + &export_path, + CancellationToken::new(), + std::time::Instant::now(), + |_| {}, + ) + .await + .expect("restore combined PostgreSQL export"); + + let restored = Box::pin(execute_sql_statement( + &state, + &target_connection_id, + &target_database, + "SELECT + (SELECT count(*) FROM public.accounts), + (SELECT count(*) FROM inventory.products), + (SELECT amount::text FROM reporting.positive_totals WHERE day = DATE '2026-09-07')", + None, + None, + )) + .await + .expect("query restored schemas"); + assert_eq!(restored.rows, vec![vec![serde_json::json!(2), serde_json::json!(1), serde_json::json!("42.50")]]); + + for database in [&source_database, &target_database] { + Box::pin(execute_sql_statement( + &state, + &admin_connection_id, + "postgres", + &format!("DROP DATABASE \"{database}\" WITH (FORCE)"), + None, + None, + )) + .await + .expect("drop disposable PostgreSQL database"); + } +} diff --git a/docs/screenshots/issue-8336-postgresql-all-schemas.png b/docs/screenshots/issue-8336-postgresql-all-schemas.png new file mode 100644 index 0000000000000000000000000000000000000000..f512c95230870436d241ef10d33f91888e87d562 GIT binary patch literal 64196 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

?c0o9YXB%1S)PlSLTPd)vMc$i6;0q# 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 Wf_a0`?^OH_%`QVzgF6)4^ZyTE|KKhF literal 0 HcmV?d00001 diff --git a/packages/app-tests/databaseExportPlan.test.ts b/packages/app-tests/databaseExportPlan.test.ts index 288a9108a6..cf5ec6b33e 100644 --- a/packages/app-tests/databaseExportPlan.test.ts +++ b/packages/app-tests/databaseExportPlan.test.ts @@ -53,6 +53,23 @@ test("all-database export includes every schema for schema-aware databases", () ]); }); +test("all-database PostgreSQL export keeps all schemas in one database file", () => { + const plan = buildAllDatabaseExportPlan({ + databases: ["app", "analytics"], + schemaAware: true, + schemasByDatabase: { + app: ["public", "private"], + analytics: ["reporting"], + }, + dbType: "postgres", + }); + + assert.deepEqual(plan, [ + { database: "app", schema: "", fileStem: "app", displayName: "app" }, + { database: "analytics", schema: "", fileStem: "analytics", displayName: "analytics" }, + ]); +}); + test("all-database export uses the database as schema for non-schema-aware databases", () => { const plan = buildAllDatabaseExportPlan({ databases: ["app", "analytics"],