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, pub fk_violations: Vec, pub errors: Vec, } 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) -> Result { 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) } }