sqlx_migrator/
cli.rs

1//! Module for creating and running cli with help of migrator
2//!
3//! CLI Command can directly used or extended
4//!
5//! For direct usage you can run `parse_and_run` function for `MigrationCommand`
6//!
7//! OR
8//!
9//! If you want to extend your own clap based cli then you can add migrator to
10//! sub command enum and then run migrator
11//! ```rust,no_run
12//! #[derive(clap::Parser)]
13//! struct Cli {
14//!     #[command(subcommand)]
15//!     sub_command: CliSubcommand,
16//! }
17//!
18//! #[derive(clap::Subcommand)]
19//! enum CliSubcommand {
20//!     #[command()]
21//!     Migrator(sqlx_migrator::cli::MigrationCommand),
22//! }
23//! ```
24use std::io::Write;
25
26use clap::{Parser, Subcommand};
27use sqlx::Database;
28
29use crate::error::Error;
30use crate::migrator::{Migrate, Plan};
31
32/// Migration command for performing rust based sqlx migrations
33#[derive(Parser, Debug)]
34pub struct MigrationCommand {
35    #[command(subcommand)]
36    sub_command: SubCommand,
37}
38
39impl MigrationCommand {
40    /// Parse [`MigrationCommand`] and run migration command line interface
41    ///
42    /// # Errors
43    /// If migration command fails to complete and raise some issue
44    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    /// Run migration command line interface
56    ///
57    /// # Errors
58    /// If migration command fails to complete and raise some issue
59    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    /// Apply migrations
77    #[command()]
78    Apply(Apply),
79    /// Drop migration information table. Needs all migrations to be
80    /// reverted else raises error
81    #[command()]
82    Drop,
83    /// List migrations along with their status and time applied if migrations
84    /// is already applied
85    #[command()]
86    List,
87    /// Revert migrations
88    #[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    /// App name up to which migration needs to be applied. If migration option
195    /// is also present than only till migration is applied
196    #[arg(long)]
197    app: Option<String>,
198    /// Check for pending migration
199    #[arg(long)]
200    check: bool,
201    /// Number of migration to apply. Conflicts with app args
202    #[arg(long, conflicts_with = "app")]
203    count: Option<usize>,
204    /// Make migration applied without running migration operations
205    #[arg(long)]
206    fake: bool,
207    /// Force run apply operation without asking question if migration is
208    /// destructible
209    #[arg(long)]
210    force: bool,
211    /// Apply migration till provided migration. Requires app options to be
212    /// present
213    #[arg(long, requires = "app")]
214    migration: Option<String>,
215    /// Show plan
216    #[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 answer is not y or yes then return
278                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    /// Revert all migration. Conflicts with app args
293    #[arg(long, conflicts_with = "app")]
294    all: bool,
295    /// Revert migration till app migrations is reverted. If it is present
296    /// alongside migration options than only till migration is reverted
297    #[arg(long)]
298    app: Option<String>,
299    /// Number of migration to revert. Conflicts with all and app args
300    #[arg(long, conflicts_with_all = ["all", "app"])]
301    count: Option<usize>,
302    /// Make migration reverted without running revert operation
303    #[arg(long)]
304    fake: bool,
305    /// Force run revert operation without asking question
306    #[arg(long)]
307    force: bool,
308    /// Revert migration till provided migration. Requires app options to be
309    /// present
310    #[arg(long, requires = "app")]
311    migration: Option<String>,
312    /// Show plan
313    #[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 answer is not y or yes then return
371                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}