use serde::Serialize; use sqlx::{PgPool, Row}; use std::collections::HashMap; use std::time::Instant; /// Result of a single dry-run execution. #[derive(Debug, Serialize)] pub struct DryRunReport { pub started_at: String, pub duration_ms: u64, pub source_tables: HashMap, pub checksums: HashMap, pub errors: Vec, pub total_rows_migrated: u64, pub total_rows_skipped: u64, pub dry_run: bool, } #[derive(Debug, Serialize, Clone)] pub struct TableReport { pub table: String, pub rows_read: u64, pub rows_would_migrate: u64, pub rows_skipped: u64, pub sample_diffs: Vec, } #[derive(Debug, Serialize, Clone)] pub struct RowDiff { pub table: String, pub pk: String, pub change_type: String, // "insert", "update", "delete" pub source_values: HashMap, pub staging_values: HashMap, } #[derive(Debug, Serialize, Clone)] pub struct ChecksumPair { pub table: String, pub source_md5: String, pub staging_md5: String, pub match: bool, } /// Executes the full migration plan in dry-run mode. /// Opens a read-only connection to the staging copy, simulates all /// row-level changes, and produces a DryRunReport. No writes are issued. pub async fn execute_dry_run( staging_pool: &PgPool, source_pool: &PgPool, tables: &[String], ) -> Result { let start = Instant::now(); // Verify both pools are reachable before doing any work. source_pool .execute("SELECT 1") .await .map_err(|e| format!("source pool unreachable: {e}"))?; staging_pool .execute("SELECT 1") .await .map_err(|e| format!("staging pool unreachable: {e}"))?; let mut table_reports: HashMap = HashMap::new(); let mut checksums: HashMap = HashMap::new(); let