migratio-cli 0.4.2

CLI support library for migratio database migrations
Documentation
//! CLI support library for migratio database migrations.
//!
//! This crate provides the runtime support for migration runner binaries
//! generated by `cargo-migratio`. It handles CLI argument parsing and
//! executes migration commands against the database.
//!
//! # Example
//!
//! This crate is typically used by auto-generated runner code, but can
//! also be used directly:
//!
//! ```ignore
//! use migratio_cli::{CliArgs, run_sqlite};
//! use clap::Parser;
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     let database_url = std::env::var("DATABASE_URL")?;
//!     let migrator = my_app::migrations::get_migrator();
//!     let args = CliArgs::parse();
//!     run_sqlite(migrator, &database_url, args)
//! }
//! ```
//!
//! For PostgreSQL:
//!
//! ```ignore
//! use migratio_cli::{CliArgs, run_postgres};
//! use clap::Parser;
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     let database_url = std::env::var("DATABASE_URL")?;
//!     let migrator = my_app::migrations::get_postgres_migrator();
//!     let args = CliArgs::parse();
//!     run_postgres(migrator, &database_url, args)
//! }
//! ```

use clap::{Parser, Subcommand};

/// CLI arguments for the migration runner.
#[derive(Parser, Debug)]
#[command(name = "migratio-runner")]
#[command(about = "Database migration runner")]
pub struct CliArgs {
    #[command(subcommand)]
    pub command: Commands,
}

/// Available migration commands for the runner binary.
#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Show current migration status (requires database)
    Status,
    /// Run pending migrations (requires database)
    Upgrade {
        /// Target version (default: latest)
        #[arg(long)]
        to: Option<u32>,
    },
    /// Rollback migrations (requires database)
    Downgrade {
        /// Target version to rollback to
        #[arg(long)]
        to: u32,
    },
    /// Show migration history (requires database)
    History,
    /// Preview pending migrations without running them (requires database)
    Preview,
    /// List all migrations defined in the migrator (no database required)
    List,
}

#[cfg(feature = "sqlite")]
pub use sqlite::run_sqlite;

#[cfg(feature = "sqlite")]
mod sqlite {
    use super::{CliArgs, Commands};
    use migratio::sqlite::SqliteMigrator;
    use rusqlite::Connection;

    /// Run the CLI with a SQLite migrator.
    ///
    /// This function handles all CLI commands for SQLite databases.
    ///
    /// # Arguments
    ///
    /// * `migrator` - The configured SQLite migrator
    /// * `database_url` - Path to the SQLite database file
    /// * `args` - Parsed CLI arguments
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` on success, or an error if the operation fails.
    pub fn run_sqlite(
        migrator: SqliteMigrator,
        database_url: &str,
        args: CliArgs,
    ) -> Result<(), Box<dyn std::error::Error>> {
        // Check if the database file exists before opening
        // (SQLite will create a new file if it doesn't exist, which we don't want)
        if !std::path::Path::new(database_url).exists() {
            return Err(format!(
                "Database file not found: {}\n\nTo create a new database, first create the file manually or use your application's initialization logic.",
                database_url
            ).into());
        }

        let mut conn = Connection::open(database_url)?;

        match args.command {
            Commands::Status => {
                let version = migrator.get_current_version(&mut conn)?;
                let pending = migrator.preview_upgrade(&mut conn)?;
                println!("Current version: {}", version);
                println!("Pending migrations: {}", pending.len());
                for m in pending {
                    println!("  - {} (v{})", m.name(), m.version());
                }
            }
            Commands::Upgrade { to } => {
                let report = match to {
                    Some(target) => migrator.upgrade_to(&mut conn, target)?,
                    None => migrator.upgrade(&mut conn)?,
                };
                if report.migrations_run.is_empty() {
                    println!("No migrations to run.");
                } else {
                    println!("Migrations run: {:?}", report.migrations_run);
                }
            }
            Commands::Downgrade { to } => {
                let report = migrator.downgrade(&mut conn, to)?;
                if report.migrations_run.is_empty() {
                    println!("No migrations to roll back.");
                } else {
                    println!("Migrations rolled back: {:?}", report.migrations_run);
                }
            }
            Commands::History => {
                let history = migrator.get_migration_history(&mut conn)?;
                if history.is_empty() {
                    println!("No migrations have been applied yet.");
                } else {
                    println!("Migration history:");
                    for entry in history {
                        println!(
                            "  v{}: {} [{}] (applied {})",
                            entry.version, entry.name, entry.migration_type, entry.applied_at
                        );
                    }
                }
            }
            Commands::Preview => {
                let pending = migrator.preview_upgrade(&mut conn)?;
                if pending.is_empty() {
                    println!("No pending migrations.");
                } else {
                    println!("Pending migrations:");
                    for m in pending {
                        println!("  - {} (v{})", m.name(), m.version());
                        if let Some(desc) = m.description() {
                            println!("    {}", desc);
                        }
                    }
                }
            }
            Commands::List => {
                // This command is handled before run_sqlite is called.
                // If we get here, something went wrong.
                unreachable!("List command should be handled before run_sqlite");
            }
        }

        Ok(())
    }
}

#[cfg(feature = "mysql")]
pub use mysql_support::run_mysql;

#[cfg(feature = "postgres")]
pub use postgres_support::run_postgres;

#[cfg(feature = "mysql")]
mod mysql_support {
    use super::{CliArgs, Commands};
    use migratio::mysql::MysqlMigrator;
    use mysql::{Conn, Opts};

    /// Run the CLI with a MySQL migrator.
    ///
    /// This function handles all CLI commands for MySQL databases.
    ///
    /// # Arguments
    ///
    /// * `migrator` - The configured MySQL migrator
    /// * `database_url` - MySQL connection URL
    /// * `args` - Parsed CLI arguments
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` on success, or an error if the operation fails.
    pub fn run_mysql(
        migrator: MysqlMigrator,
        database_url: &str,
        args: CliArgs,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let opts = Opts::from_url(database_url)?;
        let mut conn = Conn::new(opts)?;

        match args.command {
            Commands::Status => {
                let version = migrator.get_current_version(&mut conn)?;
                let pending = migrator.preview_upgrade(&mut conn)?;
                println!("Current version: {}", version);
                println!("Pending migrations: {}", pending.len());
                for m in pending {
                    println!("  - {} (v{})", m.name(), m.version());
                }
            }
            Commands::Upgrade { to } => {
                let report = match to {
                    Some(target) => migrator.upgrade_to(&mut conn, target)?,
                    None => migrator.upgrade(&mut conn)?,
                };
                if report.migrations_run.is_empty() {
                    println!("No migrations to run.");
                } else {
                    println!("Migrations run: {:?}", report.migrations_run);
                }
            }
            Commands::Downgrade { to } => {
                let report = migrator.downgrade(&mut conn, to)?;
                if report.migrations_run.is_empty() {
                    println!("No migrations to roll back.");
                } else {
                    println!("Migrations rolled back: {:?}", report.migrations_run);
                }
            }
            Commands::History => {
                let history = migrator.get_migration_history(&mut conn)?;
                if history.is_empty() {
                    println!("No migrations have been applied yet.");
                } else {
                    println!("Migration history:");
                    for entry in history {
                        println!(
                            "  v{}: {} [{}] (applied {})",
                            entry.version, entry.name, entry.migration_type, entry.applied_at
                        );
                    }
                }
            }
            Commands::Preview => {
                let pending = migrator.preview_upgrade(&mut conn)?;
                if pending.is_empty() {
                    println!("No pending migrations.");
                } else {
                    println!("Pending migrations:");
                    for m in pending {
                        println!("  - {} (v{})", m.name(), m.version());
                        if let Some(desc) = m.description() {
                            println!("    {}", desc);
                        }
                    }
                }
            }
            Commands::List => {
                // This command is handled before run_mysql is called.
                // If we get here, something went wrong.
                unreachable!("List command should be handled before run_mysql");
            }
        }

        Ok(())
    }
}

#[cfg(feature = "postgres")]
mod postgres_support {
    use super::{CliArgs, Commands};
    use migratio::postgres::PostgresMigrator;
    use postgres::{Client, NoTls};

    /// Run the CLI with a PostgreSQL migrator.
    ///
    /// This function handles all CLI commands for PostgreSQL databases.
    ///
    /// # Arguments
    ///
    /// * `migrator` - The configured PostgreSQL migrator
    /// * `database_url` - PostgreSQL connection URL
    /// * `args` - Parsed CLI arguments
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` on success, or an error if the operation fails.
    pub fn run_postgres(
        migrator: PostgresMigrator,
        database_url: &str,
        args: CliArgs,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let mut client = Client::connect(database_url, NoTls)?;

        match args.command {
            Commands::Status => {
                let version = migrator.get_current_version(&mut client)?;
                let pending = migrator.preview_upgrade(&mut client)?;
                println!("Current version: {}", version);
                println!("Pending migrations: {}", pending.len());
                for m in pending {
                    println!("  - {} (v{})", m.name(), m.version());
                }
            }
            Commands::Upgrade { to } => {
                let report = match to {
                    Some(target) => migrator.upgrade_to(&mut client, target)?,
                    None => migrator.upgrade(&mut client)?,
                };
                if report.migrations_run.is_empty() {
                    println!("No migrations to run.");
                } else {
                    println!("Migrations run: {:?}", report.migrations_run);
                }
            }
            Commands::Downgrade { to } => {
                let report = migrator.downgrade(&mut client, to)?;
                if report.migrations_run.is_empty() {
                    println!("No migrations to roll back.");
                } else {
                    println!("Migrations rolled back: {:?}", report.migrations_run);
                }
            }
            Commands::History => {
                let history = migrator.get_migration_history(&mut client)?;
                if history.is_empty() {
                    println!("No migrations have been applied yet.");
                } else {
                    println!("Migration history:");
                    for entry in history {
                        println!(
                            "  v{}: {} [{}] (applied {})",
                            entry.version, entry.name, entry.migration_type, entry.applied_at
                        );
                    }
                }
            }
            Commands::Preview => {
                let pending = migrator.preview_upgrade(&mut client)?;
                if pending.is_empty() {
                    println!("No pending migrations.");
                } else {
                    println!("Pending migrations:");
                    for m in pending {
                        println!("  - {} (v{})", m.name(), m.version());
                        if let Some(desc) = m.description() {
                            println!("    {}", desc);
                        }
                    }
                }
            }
            Commands::List => {
                // This command is handled before run_postgres is called.
                // If we get here, something went wrong.
                unreachable!("List command should be handled before run_postgres");
            }
        }

        Ok(())
    }
}