swarm / companies/starter/work/project/src/migration/dry_run.rs

raw file 1945 bytes
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