slave_framework/migration/
console.rs1use std::fmt::Write as _;
8use std::process::ExitCode;
9use std::time::Duration;
10
11use super::{Migrator, Registered, RollbackReport, RunReport, State, Status};
12use crate::database::Database;
13use crate::{Result, env};
14
15pub async fn run(migrations: Vec<Registered>) -> ExitCode {
19 let command = std::env::args().nth(1);
20 match execute(command.as_deref(), migrations).await {
21 Ok(code) => code,
22 Err(err) => {
23 eprintln!("error: {err}");
24 ExitCode::FAILURE
25 }
26 }
27}
28
29async fn execute(command: Option<&str>, migrations: Vec<Registered>) -> Result<ExitCode> {
30 if !matches!(
31 command,
32 None | Some("run" | "status" | "rollback" | "fresh")
33 ) {
34 eprintln!("usage: migrate [run|status|rollback|fresh]");
35 return Ok(ExitCode::from(2));
36 }
37
38 env::load_dotenv()?;
39 let database = Database::from_env().await?;
40 let conn = database.connect().await?;
41 println!("Database: {}\n", database.url());
42 if command == Some("fresh") {
43 let dropped = super::drop_all_tables(&conn).await?;
44 println!("Dropped {dropped} table(s).\n");
45 }
46 let mut migrator = Migrator::new(conn, migrations)?;
47
48 let (output, ok) = match command {
49 Some("status") => (format_status(&migrator.status().await?), true),
50 Some("rollback") => {
51 let report = migrator.rollback().await?;
52 (format_rollback(&report), report.failed.is_none())
53 }
54 _ => {
55 let report = migrator.run().await?;
56 (format_run(&report), report.failed.is_none())
57 }
58 };
59 print!("{output}");
60 Ok(if ok {
61 ExitCode::SUCCESS
62 } else {
63 ExitCode::FAILURE
64 })
65}
66
67pub fn format_run(report: &RunReport) -> String {
68 let mut out = String::new();
69 for outcome in &report.applied {
70 let _ = writeln!(
71 out,
72 " DONE {} {}",
73 outcome.name,
74 millis(outcome.duration)
75 );
76 }
77 if let Some(failure) = &report.failed {
78 let _ = writeln!(
79 out,
80 " FAILED {}\n {}",
81 failure.name, failure.error
82 );
83 }
84
85 let skipped = report.skipped.len();
86 if report.applied.is_empty() && report.failed.is_none() {
87 let _ = writeln!(out, "Nothing to migrate ({skipped} already applied).");
88 return out;
89 }
90 let _ = writeln!(
91 out,
92 "\nBatch {}: {} applied, {skipped} skipped (already applied).",
93 report.batch,
94 report.applied.len()
95 );
96 if let Some(failure) = &report.failed {
97 let _ = writeln!(out, "{}", failure_note(&failure.name, failure.reverted));
98 }
99 out
100}
101
102pub fn format_rollback(report: &RollbackReport) -> String {
103 let mut out = String::new();
104 let Some(batch) = report.batch else {
105 out.push_str("Nothing to roll back.\n");
106 return out;
107 };
108 let _ = writeln!(out, "Rolling back batch {batch}:");
109 for outcome in &report.rolled_back {
110 let _ = writeln!(
111 out,
112 " REVERTED {} {}",
113 outcome.name,
114 millis(outcome.duration)
115 );
116 }
117 if let Some(failure) = &report.failed {
118 let _ = writeln!(
119 out,
120 " FAILED {}\n {}",
121 failure.name, failure.error
122 );
123 let _ = writeln!(
124 out,
125 "\n{} Migrations reverted above stay reverted.",
126 failure_note(&failure.name, failure.reverted)
127 );
128 }
129 out
130}
131
132pub fn format_status(statuses: &[Status]) -> String {
133 if statuses.is_empty() {
134 return "No migrations found.\n".to_owned();
135 }
136 let mut out = format!(
137 "{:<8} {:<6} {:<20} Migration\n",
138 "Status", "Batch", "Executed at"
139 );
140 let mut any_missing = false;
141 for status in statuses {
142 let (label, batch, at) = match &status.state {
143 State::Applied { batch, executed_at } => {
144 ("Ran", batch.to_string(), executed_at.as_str())
145 }
146 State::Pending => ("Pending", String::new(), ""),
147 State::Missing { batch, executed_at } => {
148 any_missing = true;
149 ("Missing", batch.to_string(), executed_at.as_str())
150 }
151 };
152 let _ = writeln!(out, "{label:<8} {batch:<6} {at:<20} {}", status.name);
153 }
154 if any_missing {
155 out.push_str(
156 "\nMissing: recorded as applied, but not registered in database/migrations/mod.rs.\n",
157 );
158 }
159 out
160}
161
162fn failure_note(name: &str, reverted: bool) -> String {
163 if reverted {
164 format!("`{name}` was rolled back and not recorded; fix it and run the command again.")
165 } else {
166 format!(
167 "`{name}` is non-transactional: statements before the failure may have been applied. \
168 Inspect the database before retrying."
169 )
170 }
171}
172
173fn millis(duration: Duration) -> String {
174 format!("({} ms)", duration.as_millis())
175}