1use std::io::Write;
25
26use clap::{Parser, Subcommand};
27use sqlx::Database;
28
29use crate::error::Error;
30use crate::migrator::{Migrate, Plan};
31
32#[derive(Parser, Debug)]
34pub struct MigrationCommand {
35 #[command(subcommand)]
36 sub_command: SubCommand,
37}
38
39impl MigrationCommand {
40 pub async fn parse_and_run<DB>(
45 connection: &mut <DB as Database>::Connection,
46 migrator: Box<dyn Migrate<DB>>,
47 ) -> Result<(), Error>
48 where
49 DB: Database,
50 {
51 let migration_command = Self::parse();
52 migration_command.run(connection, migrator).await
53 }
54
55 pub async fn run<DB>(
60 &self,
61 connection: &mut <DB as Database>::Connection,
62 migrator: Box<dyn Migrate<DB>>,
63 ) -> Result<(), Error>
64 where
65 DB: Database,
66 {
67 self.sub_command
68 .handle_subcommand(migrator, connection)
69 .await?;
70 Ok(())
71 }
72}
73
74#[derive(Subcommand, Debug)]
75enum SubCommand {
76 #[command()]
78 Apply(Apply),
79 #[command()]
82 Drop,
83 #[command()]
86 List,
87 #[command()]
89 Revert(Revert),
90}
91
92impl SubCommand {
93 async fn handle_subcommand<DB>(
94 &self,
95 migrator: Box<dyn Migrate<DB>>,
96 connection: &mut <DB as Database>::Connection,
97 ) -> Result<(), Error>
98 where
99 DB: Database,
100 {
101 match self {
102 SubCommand::Apply(apply) => apply.run(connection, migrator).await?,
103 SubCommand::Drop => drop_migrations(connection, migrator).await?,
104 SubCommand::List => list_migrations(connection, migrator).await?,
105 SubCommand::Revert(revert) => revert.run(connection, migrator).await?,
106 }
107 Ok(())
108 }
109}
110
111async fn drop_migrations<DB>(
112 connection: &mut <DB as Database>::Connection,
113 migrator: Box<dyn Migrate<DB>>,
114) -> Result<(), Error>
115where
116 DB: Database,
117{
118 migrator.ensure_migration_table_exists(connection).await?;
119 if !migrator
120 .fetch_applied_migration_from_db(connection)
121 .await?
122 .is_empty()
123 {
124 return Err(Error::AppliedMigrationExists);
125 }
126 migrator.drop_migration_table_if_exists(connection).await?;
127 println!("Dropped migrations table");
128 Ok(())
129}
130
131async fn list_migrations<DB>(
132 connection: &mut <DB as Database>::Connection,
133 migrator: Box<dyn Migrate<DB>>,
134) -> Result<(), Error>
135where
136 DB: Database,
137{
138 let migration_plan = migrator.generate_migration_plan(connection, None).await?;
139
140 let apply_plan = migrator
141 .generate_migration_plan(connection, Some(&Plan::apply_all()))
142 .await?;
143 let applied_migrations = migrator.fetch_applied_migration_from_db(connection).await?;
144
145 let widths = [5, 10, 50, 10, 40];
146 let full_width = widths.iter().sum::<usize>() + widths.len() * 3;
147
148 let first_width = widths[0];
149 let second_width = widths[1];
150 let third_width = widths[2];
151 let fourth_width = widths[3];
152 let fifth_width = widths[4];
153
154 println!(
155 "{:^first_width$} | {:^second_width$} | {:^third_width$} | {:^fourth_width$} | \
156 {:^fifth_width$}",
157 "ID", "App", "Name", "Status", "Applied time"
158 );
159
160 println!("{:^full_width$}", "-".repeat(full_width));
161 for migration in migration_plan {
162 let mut id = String::from("N/A");
163 let mut status = "\u{2717}";
164 let mut applied_time = String::from("N/A");
165
166 let find_applied_migrations = applied_migrations
167 .iter()
168 .find(|&applied_migration| applied_migration == migration);
169
170 if let Some(sqlx_migration) = find_applied_migrations {
171 id = sqlx_migration.id().to_string();
172 status = "\u{2713}";
173 applied_time = sqlx_migration.applied_time().to_string();
174 } else if !apply_plan.contains(&migration) {
175 status = "\u{2194}";
176 }
177
178 println!(
179 "{:^first_width$} | {:^second_width$} | {:^third_width$} | {:^fourth_width$} | \
180 {:^fifth_width$}",
181 id,
182 migration.app(),
183 migration.name(),
184 status,
185 applied_time
186 );
187 }
188 Ok(())
189}
190
191#[derive(Parser, Debug)]
192#[expect(clippy::struct_excessive_bools)]
193struct Apply {
194 #[arg(long)]
197 app: Option<String>,
198 #[arg(long)]
200 check: bool,
201 #[arg(long, conflicts_with = "app")]
203 count: Option<usize>,
204 #[arg(long)]
206 fake: bool,
207 #[arg(long)]
210 force: bool,
211 #[arg(long, requires = "app")]
214 migration: Option<String>,
215 #[arg(long)]
217 plan: bool,
218}
219impl Apply {
220 async fn run<DB>(
221 &self,
222 connection: &mut <DB as Database>::Connection,
223 migrator: Box<dyn Migrate<DB>>,
224 ) -> Result<(), Error>
225 where
226 DB: Database,
227 {
228 let plan;
229 if let Some(count) = self.count {
230 plan = Plan::apply_count(count);
231 } else if let Some(app) = &self.app {
232 plan = Plan::apply_name(app, &self.migration);
233 } else {
234 plan = Plan::apply_all();
235 }
236 let plan = plan.fake(self.fake);
237 let migrations = migrator
238 .generate_migration_plan(connection, Some(&plan))
239 .await?;
240 if self.check && !migrations.is_empty() {
241 return Err(Error::PendingMigrationPresent);
242 }
243 if self.plan {
244 if migrations.is_empty() {
245 println!("No migration exists for applying");
246 } else {
247 let first_width = 10;
248 let second_width = 50;
249 let full_width = first_width + second_width + 3;
250 println!("{:^first_width$} | {:^second_width$}", "App", "Name");
251 println!("{:^full_width$}", "-".repeat(full_width));
252 for migration in migrations {
253 println!(
254 "{:^first_width$} | {:^second_width$}",
255 migration.app(),
256 migration.name(),
257 );
258 }
259 }
260 } else {
261 let destructible_migrations = migrations
262 .iter()
263 .filter(|m| m.operations().iter().any(|o| o.is_destructible()))
264 .collect::<Vec<_>>();
265 if !self.force && !destructible_migrations.is_empty() && !self.fake {
266 let mut input = String::new();
267 println!(
268 "Do you want to apply destructible migrations {} (y/N)",
269 destructible_migrations.len()
270 );
271 for (position, migration) in destructible_migrations.iter().enumerate() {
272 println!("{position}. {} : {}", migration.app(), migration.name());
273 }
274 std::io::stdout().flush()?;
275 std::io::stdin().read_line(&mut input)?;
276 let input_trimmed = input.trim().to_ascii_lowercase();
277 if !["y", "yes"].contains(&input_trimmed.as_str()) {
279 return Ok(());
280 }
281 }
282 migrator.run(connection, &plan).await?;
283 println!("Successfully applied migrations according to plan");
284 }
285 Ok(())
286 }
287}
288
289#[derive(Parser, Debug)]
290#[expect(clippy::struct_excessive_bools)]
291struct Revert {
292 #[arg(long, conflicts_with = "app")]
294 all: bool,
295 #[arg(long)]
298 app: Option<String>,
299 #[arg(long, conflicts_with_all = ["all", "app"])]
301 count: Option<usize>,
302 #[arg(long)]
304 fake: bool,
305 #[arg(long)]
307 force: bool,
308 #[arg(long, requires = "app")]
311 migration: Option<String>,
312 #[arg(long)]
314 plan: bool,
315}
316impl Revert {
317 async fn run<DB>(
318 &self,
319 connection: &mut <DB as Database>::Connection,
320 migrator: Box<dyn Migrate<DB>>,
321 ) -> Result<(), Error>
322 where
323 DB: Database,
324 {
325 let plan;
326 if let Some(count) = self.count {
327 plan = Plan::revert_count(count);
328 } else if let Some(app) = &self.app {
329 plan = Plan::revert_name(app, &self.migration);
330 } else if self.all {
331 plan = Plan::revert_all();
332 } else {
333 plan = Plan::revert_count(1);
334 }
335 let plan = plan.fake(self.fake);
336 let revert_migrations = migrator
337 .generate_migration_plan(connection, Some(&plan))
338 .await?;
339
340 if self.plan {
341 if revert_migrations.is_empty() {
342 println!("No migration exists for reverting");
343 } else {
344 let first_width = 10;
345 let second_width = 50;
346 let full_width = first_width + second_width + 3;
347 println!("{:^first_width$} | {:^second_width$}", "App", "Name");
348 println!("{:^full_width$}", "-".repeat(full_width));
349 for migration in revert_migrations {
350 println!(
351 "{:^first_width$} | {:^second_width$}",
352 migration.app(),
353 migration.name(),
354 );
355 }
356 }
357 } else {
358 if !self.force && !revert_migrations.is_empty() && !self.fake {
359 let mut input = String::new();
360 println!(
361 "Do you want to revert {} migrations (y/N)",
362 revert_migrations.len()
363 );
364 for (position, migration) in revert_migrations.iter().enumerate() {
365 println!("{position}. {} : {}", migration.app(), migration.name());
366 }
367 std::io::stdout().flush()?;
368 std::io::stdin().read_line(&mut input)?;
369 let input_trimmed = input.trim().to_ascii_lowercase();
370 if !["y", "yes"].contains(&input_trimmed.as_str()) {
372 return Ok(());
373 }
374 }
375 migrator.run(connection, &plan).await?;
376 println!("Successfully reverted migrations according to plan");
377 }
378 Ok(())
379 }
380}