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)
}
}