```rust
use anyhow::{Result, Context};
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DryRunReport {
pub dry_run: bool,
pub records_to_insert: u64,
pub records_to_update: u64,
pub records_to_delete: u64,
pub validation_errors: Vec<String>,
pub estimated_duration_secs: u64,
pub completed_at: String,
}
pub struct MigrationRunner {
dry_run: bool,
source_conn: String,
target_conn: String,
}
impl MigrationRunner {
pub fn new(source_conn: &str, target_conn: &str, dry_run: bool) -> Self {
Self {
dry_run,
source_conn: source_conn.to_string(),
target_conn: target_conn.to_string(),
}
}
pub fn execute(&self) -> Result<DryRunReport> {
let start = Instant::now();
if self.dry_run {
self.run_dry()
} else {
self.run_production()
}
.map(|mut report| {
let elapsed = start.elapsed();
report.estimated_duration_secs = elapsed.as_secs();
report.completed_at = chrono::Utc::now().to_rfc3339();
report
})
}
fn run_dry(&self) -> Result<DryRunReport> {
// Simulate the migration by querying counts and validating constraints
// without writing to the target database.
let inserts = self.count_records_to_insert()?;
let updates = self.count_records_to_update()?;
let deletes = self.count_records_to_delete()?;
let errors = self.validate_constraints()?;
Ok(DryRunReport {
dry_run: true,
records_to_insert: inserts,
records_to_update: updates,
records_to_delete: deletes,
validation_errors: errors,
estimated_duration_secs: 0,
completed_at: String::new(),
})
}
fn run_production(&self) -> Result<DryRunReport> {
// Actual migration execution (not shown in full for brevity,
// but follows the same count logic before executing writes)
let inserts = self.count_records_to_insert()?