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

raw file 1939 bytes
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use anyhow::Result;

#[derive(Debug, Serialize, Deserialize)]
pub struct DryRunReport {
    pub migration_name: String,
    pub dry_run: bool,
    pub rows_affected: i64,
    pub schema_changes: Vec<String>,
    pub fk_violations: Vec<String>,
    pub errors: Vec<String>,
}

pub struct DryRunExecutor {
    pool: PgPool,
}

impl DryRunExecutor {
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }

    pub async fn execute(&self, migration_name: &str, sql_statements: Vec<String>) -> Result<DryRunReport> {
        let mut report = DryRunReport {
            migration_name: migration_name.to_string(),
            dry_run: true,
            rows_affected: 0,
            schema_changes: vec![],
            fk_violations: vec![],
            errors: vec![],
        };

        // Begin a transaction that we will ALWAYS roll back
        let mut tx = self.pool.begin().await?;

        for stmt in &sql_statements {
            match tx.execute(stmt).await {
                Ok(result) => {
                    report.rows_affected += result.rows_affected();
                    // Capture schema changes if the statement is DDL
                    if stmt.trim().starts_with("ALTER TABLE") || stmt.trim().starts_with("CREATE TABLE") {
                        report.schema_changes.push(stmt.clone());
                    }
                }
                Err(e) => {
                    report.errors.push(format!("{}: {}", stmt, e.to_string()));
                    // Check for FK violations specifically
                    let msg = e.to_string();
                    if msg.contains("foreign key constraint") {
                        report.fk_violations.push(msg);
                    }
                }
            }
        }

        // ALWAYS roll back — this is the entire point of dry-run
        tx.rollback().await?;

        Ok(report)
    }
}