use std::fmt::Write as _;
use std::process::ExitCode;
use std::time::Duration;
use super::{Migrator, Registered, RollbackReport, RunReport, State, Status};
use crate::{Result, database, env};
pub async fn run(migrations: Vec<Registered>) -> ExitCode {
let command = std::env::args().nth(1);
match execute(command.as_deref(), migrations).await {
Ok(code) => code,
Err(err) => {
eprintln!("error: {err}");
ExitCode::FAILURE
}
}
}
async fn execute(command: Option<&str>, migrations: Vec<Registered>) -> Result<ExitCode> {
if !matches!(command, None | Some("run" | "status" | "rollback")) {
eprintln!("usage: migrate [run|status|rollback]");
return Ok(ExitCode::from(2));
}
env::load_dotenv()?;
let path = database::path_from_env();
let mut migrator = Migrator::new(database::connect(&path).await?, migrations)?;
println!("Database: {path}\n");
let (output, ok) = match command {
Some("status") => (format_status(&migrator.status().await?), true),
Some("rollback") => {
let report = migrator.rollback().await?;
(format_rollback(&report), report.failed.is_none())
}
_ => {
let report = migrator.run().await?;
(format_run(&report), report.failed.is_none())
}
};
print!("{output}");
Ok(if ok {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
pub fn format_run(report: &RunReport) -> String {
let mut out = String::new();
for outcome in &report.applied {
let _ = writeln!(
out,
" DONE {} {}",
outcome.name,
millis(outcome.duration)
);
}
if let Some(failure) = &report.failed {
let _ = writeln!(
out,
" FAILED {}\n {}",
failure.name, failure.error
);
}
let skipped = report.skipped.len();
if report.applied.is_empty() && report.failed.is_none() {
let _ = writeln!(out, "Nothing to migrate ({skipped} already applied).");
return out;
}
let _ = writeln!(
out,
"\nBatch {}: {} applied, {skipped} skipped (already applied).",
report.batch,
report.applied.len()
);
if let Some(failure) = &report.failed {
let _ = writeln!(out, "{}", failure_note(&failure.name, failure.reverted));
}
out
}
pub fn format_rollback(report: &RollbackReport) -> String {
let mut out = String::new();
let Some(batch) = report.batch else {
out.push_str("Nothing to roll back.\n");
return out;
};
let _ = writeln!(out, "Rolling back batch {batch}:");
for outcome in &report.rolled_back {
let _ = writeln!(
out,
" REVERTED {} {}",
outcome.name,
millis(outcome.duration)
);
}
if let Some(failure) = &report.failed {
let _ = writeln!(
out,
" FAILED {}\n {}",
failure.name, failure.error
);
let _ = writeln!(
out,
"\n{} Migrations reverted above stay reverted.",
failure_note(&failure.name, failure.reverted)
);
}
out
}
pub fn format_status(statuses: &[Status]) -> String {
if statuses.is_empty() {
return "No migrations found.\n".to_owned();
}
let mut out = format!(
"{:<8} {:<6} {:<20} Migration\n",
"Status", "Batch", "Executed at"
);
let mut any_missing = false;
for status in statuses {
let (label, batch, at) = match &status.state {
State::Applied { batch, executed_at } => {
("Ran", batch.to_string(), executed_at.as_str())
}
State::Pending => ("Pending", String::new(), ""),
State::Missing { batch, executed_at } => {
any_missing = true;
("Missing", batch.to_string(), executed_at.as_str())
}
};
let _ = writeln!(out, "{label:<8} {batch:<6} {at:<20} {}", status.name);
}
if any_missing {
out.push_str(
"\nMissing: recorded as applied, but not registered in database/migrations/mod.rs.\n",
);
}
out
}
fn failure_note(name: &str, reverted: bool) -> String {
if reverted {
format!("`{name}` was rolled back and not recorded; fix it and run the command again.")
} else {
format!(
"`{name}` is non-transactional: statements before the failure may have been applied. \
Inspect the database before retrying."
)
}
}
fn millis(duration: Duration) -> String {
format!("({} ms)", duration.as_millis())
}