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<String, TableReport>,
pub checksums: HashMap<String, ChecksumPair>,
pub errors: Vec<String>,
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<RowDiff>,
}
#[derive(Debug, Serialize, Clone)]
pub struct RowDiff {
pub table: String,
pub pk: String,
pub change_type: String, // "insert", "update", "delete"
pub source_values: HashMap<String, String>,
pub staging_values: HashMap<String, String>,
}
#[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<DryRunReport, String> {
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<String, TableReport> = HashMap::new();
let mut checksums: HashMap<String, ChecksumPair> = HashMap::new();
let