Skip to main content

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//!
24//! For PostgreSQL:
25//!
26//! ```ignore
27//! use migratio_cli::{CliArgs, run_postgres};
28//! use clap::Parser;
29//!
30//! fn main() -> Result<(), Box<dyn std::error::Error>> {
31//!     let database_url = std::env::var("DATABASE_URL")?;
32//!     let migrator = my_app::migrations::get_postgres_migrator();
33//!     let args = CliArgs::parse();
34//!     run_postgres(migrator, &database_url, args)
35//! }
36//! ```
37
38use clap::{Parser, Subcommand};
39
40/// CLI arguments for the migration runner.
41#[derive(Parser, Debug)]
42#[command(name = "migratio-runner")]
43#[command(about = "Database migration runner")]
44pub struct CliArgs {
45    #[command(subcommand)]
46    pub command: Commands,
47}
48
49/// Available migration commands for the runner binary.
50#[derive(Subcommand, Debug)]
51pub enum Commands {
52    /// Show current migration status (requires database)
53    Status,
54    /// Run pending migrations (requires database)
55    Upgrade {
56        /// Target version (default: latest)
57        #[arg(long)]
58        to: Option<u32>,
59    },
60    /// Rollback migrations (requires database)
61    Downgrade {
62        /// Target version to rollback to
63        #[arg(long)]
64        to: u32,
65    },
66    /// Show migration history (requires database)
67    History,
68    /// Preview pending migrations without running them (requires database)
69    Preview,
70    /// List all migrations defined in the migrator (no database required)
71    List,
72}
73
74#[cfg(feature = "sqlite")]
75pub use sqlite::run_sqlite;
76
77#[cfg(feature = "sqlite")]
78mod sqlite {
79    use super::{CliArgs, Commands};
80    use migratio::sqlite::SqliteMigrator;
81    use rusqlite::Connection;
82
83    /// Run the CLI with a SQLite migrator.
84    ///
85    /// This function handles all CLI commands for SQLite databases.
86    ///
87    /// # Arguments
88    ///
89    /// * `migrator` - The configured SQLite migrator
90    /// * `database_url` - Path to the SQLite database file
91    /// * `args` - Parsed CLI arguments
92    ///
93    /// # Returns
94    ///
95    /// Returns `Ok(())` on success, or an error if the operation fails.
96    pub fn run_sqlite(
97        migrator: SqliteMigrator,
98        database_url: &str,
99        args: CliArgs,
100    ) -> Result<(), Box<dyn std::error::Error>> {
101        // Check if the database file exists before opening
102        // (SQLite will create a new file if it doesn't exist, which we don't want)
103        if !std::path::Path::new(database_url).exists() {
104            return Err(format!(
105                "Database file not found: {}\n\nTo create a new database, first create the file manually or use your application's initialization logic.",
106                database_url
107            ).into());
108        }
109
110        let mut conn = Connection::open(database_url)?;
111
112        match args.command {
113            Commands::Status => {
114                let version = migrator.get_current_version(&mut conn)?;
115                let pending = migrator.preview_upgrade(&mut conn)?;
116                println!("Current version: {}", version);
117                println!("Pending migrations: {}", pending.len());
118                for m in pending {
119                    println!("  - {} (v{})", m.name(), m.version());
120                }
121            }
122            Commands::Upgrade { to } => {
123                let report = match to {
124                    Some(target) => migrator.upgrade_to(&mut conn, target)?,
125                    None => migrator.upgrade(&mut conn)?,
126                };
127                if report.migrations_run.is_empty() {
128                    println!("No migrations to run.");
129                } else {
130                    println!("Migrations run: {:?}", report.migrations_run);
131                }
132            }
133            Commands::Downgrade { to } => {
134                let report = migrator.downgrade(&mut conn, to)?;
135                if report.migrations_run.is_empty() {
136                    println!("No migrations to roll back.");
137                } else {
138                    println!("Migrations rolled back: {:?}", report.migrations_run);
139                }
140            }
141            Commands::History => {
142                let history = migrator.get_migration_history(&mut conn)?;
143                if history.is_empty() {
144                    println!("No migrations have been applied yet.");
145                } else {
146                    println!("Migration history:");
147                    for entry in history {
148                        println!(
149                            "  v{}: {} [{}] (applied {})",
150                            entry.version, entry.name, entry.migration_type, entry.applied_at
151                        );
152                    }
153                }
154            }
155            Commands::Preview => {
156                let pending = migrator.preview_upgrade(&mut conn)?;
157                if pending.is_empty() {
158                    println!("No pending migrations.");
159                } else {
160                    println!("Pending migrations:");
161                    for m in pending {
162                        println!("  - {} (v{})", m.name(), m.version());
163                        if let Some(desc) = m.description() {
164                            println!("    {}", desc);
165                        }
166                    }
167                }
168            }
169            Commands::List => {
170                // This command is handled before run_sqlite is called.
171                // If we get here, something went wrong.
172                unreachable!("List command should be handled before run_sqlite");
173            }
174        }
175
176        Ok(())
177    }
178}
179
180#[cfg(feature = "mysql")]
181pub use mysql_support::run_mysql;
182
183#[cfg(feature = "postgres")]
184pub use postgres_support::run_postgres;
185
186#[cfg(feature = "mysql")]
187mod mysql_support {
188    use super::{CliArgs, Commands};
189    use migratio::mysql::MysqlMigrator;
190    use mysql::{Conn, Opts};
191
192    /// Run the CLI with a MySQL migrator.
193    ///
194    /// This function handles all CLI commands for MySQL databases.
195    ///
196    /// # Arguments
197    ///
198    /// * `migrator` - The configured MySQL migrator
199    /// * `database_url` - MySQL connection URL
200    /// * `args` - Parsed CLI arguments
201    ///
202    /// # Returns
203    ///
204    /// Returns `Ok(())` on success, or an error if the operation fails.
205    pub fn run_mysql(
206        migrator: MysqlMigrator,
207        database_url: &str,
208        args: CliArgs,
209    ) -> Result<(), Box<dyn std::error::Error>> {
210        let opts = Opts::from_url(database_url)?;
211        let mut conn = Conn::new(opts)?;
212
213        match args.command {
214            Commands::Status => {
215                let version = migrator.get_current_version(&mut conn)?;
216                let pending = migrator.preview_upgrade(&mut conn)?;
217                println!("Current version: {}", version);
218                println!("Pending migrations: {}", pending.len());
219                for m in pending {
220                    println!("  - {} (v{})", m.name(), m.version());
221                }
222            }
223            Commands::Upgrade { to } => {
224                let report = match to {
225                    Some(target) => migrator.upgrade_to(&mut conn, target)?,
226                    None => migrator.upgrade(&mut conn)?,
227                };
228                if report.migrations_run.is_empty() {
229                    println!("No migrations to run.");
230                } else {
231                    println!("Migrations run: {:?}", report.migrations_run);
232                }
233            }
234            Commands::Downgrade { to } => {
235                let report = migrator.downgrade(&mut conn, to)?;
236                if report.migrations_run.is_empty() {
237                    println!("No migrations to roll back.");
238                } else {
239                    println!("Migrations rolled back: {:?}", report.migrations_run);
240                }
241            }
242            Commands::History => {
243                let history = migrator.get_migration_history(&mut conn)?;
244                if history.is_empty() {
245                    println!("No migrations have been applied yet.");
246                } else {
247                    println!("Migration history:");
248                    for entry in history {
249                        println!(
250                            "  v{}: {} [{}] (applied {})",
251                            entry.version, entry.name, entry.migration_type, entry.applied_at
252                        );
253                    }
254                }
255            }
256            Commands::Preview => {
257                let pending = migrator.preview_upgrade(&mut conn)?;
258                if pending.is_empty() {
259                    println!("No pending migrations.");
260                } else {
261                    println!("Pending migrations:");
262                    for m in pending {
263                        println!("  - {} (v{})", m.name(), m.version());
264                        if let Some(desc) = m.description() {
265                            println!("    {}", desc);
266                        }
267                    }
268                }
269            }
270            Commands::List => {
271                // This command is handled before run_mysql is called.
272                // If we get here, something went wrong.
273                unreachable!("List command should be handled before run_mysql");
274            }
275        }
276
277        Ok(())
278    }
279}
280
281#[cfg(feature = "postgres")]
282mod postgres_support {
283    use super::{CliArgs, Commands};
284    use migratio::postgres::PostgresMigrator;
285    use postgres::{Client, NoTls};
286
287    /// Run the CLI with a PostgreSQL migrator.
288    ///
289    /// This function handles all CLI commands for PostgreSQL databases.
290    ///
291    /// # Arguments
292    ///
293    /// * `migrator` - The configured PostgreSQL migrator
294    /// * `database_url` - PostgreSQL connection URL
295    /// * `args` - Parsed CLI arguments
296    ///
297    /// # Returns
298    ///
299    /// Returns `Ok(())` on success, or an error if the operation fails.
300    pub fn run_postgres(
301        migrator: PostgresMigrator,
302        database_url: &str,
303        args: CliArgs,
304    ) -> Result<(), Box<dyn std::error::Error>> {
305        let mut client = Client::connect(database_url, NoTls)?;
306
307        match args.command {
308            Commands::Status => {
309                let version = migrator.get_current_version(&mut client)?;
310                let pending = migrator.preview_upgrade(&mut client)?;
311                println!("Current version: {}", version);
312                println!("Pending migrations: {}", pending.len());
313                for m in pending {
314                    println!("  - {} (v{})", m.name(), m.version());
315                }
316            }
317            Commands::Upgrade { to } => {
318                let report = match to {
319                    Some(target) => migrator.upgrade_to(&mut client, target)?,
320                    None => migrator.upgrade(&mut client)?,
321                };
322                if report.migrations_run.is_empty() {
323                    println!("No migrations to run.");
324                } else {
325                    println!("Migrations run: {:?}", report.migrations_run);
326                }
327            }
328            Commands::Downgrade { to } => {
329                let report = migrator.downgrade(&mut client, to)?;
330                if report.migrations_run.is_empty() {
331                    println!("No migrations to roll back.");
332                } else {
333                    println!("Migrations rolled back: {:?}", report.migrations_run);
334                }
335            }
336            Commands::History => {
337                let history = migrator.get_migration_history(&mut client)?;
338                if history.is_empty() {
339                    println!("No migrations have been applied yet.");
340                } else {
341                    println!("Migration history:");
342                    for entry in history {
343                        println!(
344                            "  v{}: {} [{}] (applied {})",
345                            entry.version, entry.name, entry.migration_type, entry.applied_at
346                        );
347                    }
348                }
349            }
350            Commands::Preview => {
351                let pending = migrator.preview_upgrade(&mut client)?;
352                if pending.is_empty() {
353                    println!("No pending migrations.");
354                } else {
355                    println!("Pending migrations:");
356                    for m in pending {
357                        println!("  - {} (v{})", m.name(), m.version());
358                        if let Some(desc) = m.description() {
359                            println!("    {}", desc);
360                        }
361                    }
362                }
363            }
364            Commands::List => {
365                // This command is handled before run_postgres is called.
366                // If we get here, something went wrong.
367                unreachable!("List command should be handled before run_postgres");
368            }
369        }
370
371        Ok(())
372    }
373}