migratio_cli/
lib.rs

1//! CLI support library for migratio database migrations.
2//!
3//! This crate provides the runtime support for migration runner binaries
4//! generated by `cargo-migratio`. It handles CLI argument parsing and
5//! executes migration commands against the database.
6//!
7//! # Example
8//!
9//! This crate is typically used by auto-generated runner code, but can
10//! also be used directly:
11//!
12//! ```ignore
13//! use migratio_cli::{CliArgs, run_sqlite};
14//! use clap::Parser;
15//!
16//! fn main() -> Result<(), Box<dyn std::error::Error>> {
17//!     let database_url = std::env::var("DATABASE_URL")?;
18//!     let migrator = my_app::migrations::get_migrator();
19//!     let args = CliArgs::parse();
20//!     run_sqlite(migrator, &database_url, args)
21//! }
22//! ```
23
24use clap::{Parser, Subcommand};
25
26/// CLI arguments for the migration runner.
27#[derive(Parser, Debug)]
28#[command(name = "migratio-runner")]
29#[command(about = "Database migration runner")]
30pub struct CliArgs {
31    #[command(subcommand)]
32    pub command: Commands,
33}
34
35/// Available migration commands for the runner binary.
36#[derive(Subcommand, Debug)]
37pub enum Commands {
38    /// Show current migration status (requires database)
39    Status,
40    /// Run pending migrations (requires database)
41    Upgrade {
42        /// Target version (default: latest)
43        #[arg(long)]
44        to: Option<u32>,
45    },
46    /// Rollback migrations (requires database)
47    Downgrade {
48        /// Target version to rollback to
49        #[arg(long)]
50        to: u32,
51    },
52    /// Show migration history (requires database)
53    History,
54    /// Preview pending migrations without running them (requires database)
55    Preview,
56    /// List all migrations defined in the migrator (no database required)
57    List,
58}
59
60#[cfg(feature = "sqlite")]
61pub use sqlite::run_sqlite;
62
63#[cfg(feature = "sqlite")]
64mod sqlite {
65    use super::{CliArgs, Commands};
66    use migratio::sqlite::SqliteMigrator;
67    use rusqlite::Connection;
68
69    /// Run the CLI with a SQLite migrator.
70    ///
71    /// This function handles all CLI commands for SQLite databases.
72    ///
73    /// # Arguments
74    ///
75    /// * `migrator` - The configured SQLite migrator
76    /// * `database_url` - Path to the SQLite database file
77    /// * `args` - Parsed CLI arguments
78    ///
79    /// # Returns
80    ///
81    /// Returns `Ok(())` on success, or an error if the operation fails.
82    pub fn run_sqlite(
83        migrator: SqliteMigrator,
84        database_url: &str,
85        args: CliArgs,
86    ) -> Result<(), Box<dyn std::error::Error>> {
87        // Check if the database file exists before opening
88        // (SQLite will create a new file if it doesn't exist, which we don't want)
89        if !std::path::Path::new(database_url).exists() {
90            return Err(format!(
91                "Database file not found: {}\n\nTo create a new database, first create the file manually or use your application's initialization logic.",
92                database_url
93            ).into());
94        }
95
96        let mut conn = Connection::open(database_url)?;
97
98        match args.command {
99            Commands::Status => {
100                let version = migrator.get_current_version(&mut conn)?;
101                let pending = migrator.preview_upgrade(&mut conn)?;
102                println!("Current version: {}", version);
103                println!("Pending migrations: {}", pending.len());
104                for m in pending {
105                    println!("  - {} (v{})", m.name(), m.version());
106                }
107            }
108            Commands::Upgrade { to } => {
109                let report = match to {
110                    Some(target) => migrator.upgrade_to(&mut conn, target)?,
111                    None => migrator.upgrade(&mut conn)?,
112                };
113                if report.migrations_run.is_empty() {
114                    println!("No migrations to run.");
115                } else {
116                    println!("Migrations run: {:?}", report.migrations_run);
117                }
118            }
119            Commands::Downgrade { to } => {
120                let report = migrator.downgrade(&mut conn, to)?;
121                if report.migrations_run.is_empty() {
122                    println!("No migrations to roll back.");
123                } else {
124                    println!("Migrations rolled back: {:?}", report.migrations_run);
125                }
126            }
127            Commands::History => {
128                let history = migrator.get_migration_history(&mut conn)?;
129                if history.is_empty() {
130                    println!("No migrations have been applied yet.");
131                } else {
132                    println!("Migration history:");
133                    for entry in history {
134                        println!(
135                            "  v{}: {} (applied {})",
136                            entry.version, entry.name, entry.applied_at
137                        );
138                    }
139                }
140            }
141            Commands::Preview => {
142                let pending = migrator.preview_upgrade(&mut conn)?;
143                if pending.is_empty() {
144                    println!("No pending migrations.");
145                } else {
146                    println!("Pending migrations:");
147                    for m in pending {
148                        println!("  - {} (v{})", m.name(), m.version());
149                        if let Some(desc) = m.description() {
150                            println!("    {}", desc);
151                        }
152                    }
153                }
154            }
155            Commands::List => {
156                // This command is handled before run_sqlite is called.
157                // If we get here, something went wrong.
158                unreachable!("List command should be handled before run_sqlite");
159            }
160        }
161
162        Ok(())
163    }
164}
165
166#[cfg(feature = "mysql")]
167pub use mysql_support::run_mysql;
168
169#[cfg(feature = "mysql")]
170mod mysql_support {
171    use super::{CliArgs, Commands};
172    use migratio::mysql::MysqlMigrator;
173    use mysql::{Conn, Opts};
174
175    /// Run the CLI with a MySQL migrator.
176    ///
177    /// This function handles all CLI commands for MySQL databases.
178    ///
179    /// # Arguments
180    ///
181    /// * `migrator` - The configured MySQL migrator
182    /// * `database_url` - MySQL connection URL
183    /// * `args` - Parsed CLI arguments
184    ///
185    /// # Returns
186    ///
187    /// Returns `Ok(())` on success, or an error if the operation fails.
188    pub fn run_mysql(
189        migrator: MysqlMigrator,
190        database_url: &str,
191        args: CliArgs,
192    ) -> Result<(), Box<dyn std::error::Error>> {
193        let opts = Opts::from_url(database_url)?;
194        let mut conn = Conn::new(opts)?;
195
196        match args.command {
197            Commands::Status => {
198                let version = migrator.get_current_version(&mut conn)?;
199                let pending = migrator.preview_upgrade(&mut conn)?;
200                println!("Current version: {}", version);
201                println!("Pending migrations: {}", pending.len());
202                for m in pending {
203                    println!("  - {} (v{})", m.name(), m.version());
204                }
205            }
206            Commands::Upgrade { to } => {
207                let report = match to {
208                    Some(target) => migrator.upgrade_to(&mut conn, target)?,
209                    None => migrator.upgrade(&mut conn)?,
210                };
211                if report.migrations_run.is_empty() {
212                    println!("No migrations to run.");
213                } else {
214                    println!("Migrations run: {:?}", report.migrations_run);
215                }
216            }
217            Commands::Downgrade { to } => {
218                let report = migrator.downgrade(&mut conn, to)?;
219                if report.migrations_run.is_empty() {
220                    println!("No migrations to roll back.");
221                } else {
222                    println!("Migrations rolled back: {:?}", report.migrations_run);
223                }
224            }
225            Commands::History => {
226                let history = migrator.get_migration_history(&mut conn)?;
227                if history.is_empty() {
228                    println!("No migrations have been applied yet.");
229                } else {
230                    println!("Migration history:");
231                    for entry in history {
232                        println!(
233                            "  v{}: {} (applied {})",
234                            entry.version, entry.name, entry.applied_at
235                        );
236                    }
237                }
238            }
239            Commands::Preview => {
240                let pending = migrator.preview_upgrade(&mut conn)?;
241                if pending.is_empty() {
242                    println!("No pending migrations.");
243                } else {
244                    println!("Pending migrations:");
245                    for m in pending {
246                        println!("  - {} (v{})", m.name(), m.version());
247                        if let Some(desc) = m.description() {
248                            println!("    {}", desc);
249                        }
250                    }
251                }
252            }
253            Commands::List => {
254                // This command is handled before run_mysql is called.
255                // If we get here, something went wrong.
256                unreachable!("List command should be handled before run_mysql");
257            }
258        }
259
260        Ok(())
261    }
262}