drizzle-cli 0.1.7

Command-line interface for drizzle-rs migrations
Documentation
//! Migrate command implementation
//!
//! Runs pending migrations against the database.

use crate::config::{Config, Driver};
use crate::error::CliError;
use crate::output;

#[derive(clap::Args, Debug, Clone, Copy, Default)]
pub struct MigrateOptions {
    /// Verify migration consistency without applying changes
    #[arg(long)]
    pub verify: bool,

    /// Print pending migration plan without applying changes
    #[arg(long)]
    pub plan: bool,

    /// Verify first, then apply if checks pass
    #[arg(long)]
    pub safe: bool,
}

/// Run the migrate command.
///
/// # Errors
///
/// Returns [`CliError`] if mutually exclusive flags are combined, the database
/// or credentials cannot be resolved, connecting to the database fails, or
/// applying migrations fails.
pub fn run(config: &Config, db_name: Option<&str>, opts: MigrateOptions) -> Result<(), CliError> {
    validate_mutex_opts(opts)?;

    let db = config.database(db_name)?;

    crate::commands::harness::print_db_header(config, db_name);

    println!("{}", output::heading(migrate_heading(opts)));
    println!();

    let out_dir = db.migrations_dir();

    // Check if migrations directory exists
    if !out_dir.exists() {
        println!("  {}", output::warning("No migrations directory found."));
        println!("  Run 'drizzle generate' to create your first migration.");
        return Ok(());
    }

    // Codegen-only drivers (e.g. durable-sqlite) have no remote endpoint for the
    // CLI to reach — migrations execute inside the DO runtime. Short-circuit
    // with a pointed message instead of the generic "no credentials" fallback.
    if matches!(db.driver, Some(Driver::DurableSqlite)) {
        print_durable_sqlite_notice(out_dir);
        return Ok(());
    }

    // Get credentials
    let credentials = db.credentials()?;

    let Some(credentials) = credentials else {
        print_missing_credentials_help();
        return Ok(());
    };

    let plan = if opts.verify || opts.plan || opts.safe {
        Some(crate::db::verify_migrations(
            &credentials,
            db.dialect,
            out_dir,
            db.migrations_table(),
            db.migrations_schema(),
        )?)
    } else {
        None
    };

    if let Some(plan) = &plan
        && handle_plan_short_circuit(plan, opts)
    {
        return Ok(());
    }

    // Run migrations
    let result = crate::db::run_migrations(
        &credentials,
        db.dialect,
        out_dir,
        db.migrations_table(),
        db.migrations_schema(),
    )?;

    print_migration_result(&result, opts.safe);
    Ok(())
}

fn validate_mutex_opts(opts: MigrateOptions) -> Result<(), CliError> {
    if opts.safe && opts.verify {
        return Err(CliError::Other(
            "--safe can't be combined with --verify".to_string(),
        ));
    }
    if opts.safe && opts.plan {
        return Err(CliError::Other(
            "--safe can't be combined with --plan".to_string(),
        ));
    }
    Ok(())
}

const fn migrate_heading(opts: MigrateOptions) -> &'static str {
    if opts.verify {
        "Verifying migrations..."
    } else if opts.plan {
        "Planning migrations..."
    } else if opts.safe {
        "Running safe migration flow..."
    } else {
        "Running migrations..."
    }
}

fn print_durable_sqlite_notice(out_dir: &std::path::Path) {
    println!(
        "{}",
        output::warning("Durable Objects SQLite runs inside the Workers runtime.")
    );
    println!();
    println!("  The CLI can't apply migrations to a DO from outside.");
    println!(
        "  Apply them at `DurableObject` init time by importing `{}/migrations.js`",
        out_dir.display()
    );
    println!("  and running each statement against `state.storage().sql()`.");
    println!();
    println!(
        "  (This command only generates the SQL + JS bundle — run `drizzle generate` for that.)"
    );
}

fn print_missing_credentials_help() {
    println!("{}", output::warning("No database credentials configured."));
    println!();
    println!("Add credentials to your drizzle.config.toml:");
    println!();
    println!("  {}", output::muted("[dbCredentials]"));
    println!("  {}", output::muted("url = \"./dev.db\""));
    println!();
    println!("Or use an environment variable:");
    println!();
    println!("  {}", output::muted("[dbCredentials]"));
    println!("  {}", output::muted("url = { env = \"DATABASE_URL\" }"));
}

/// Print plan summary and return `true` if the caller should return early.
fn handle_plan_short_circuit(plan: &crate::db::MigrationPlan, opts: MigrateOptions) -> bool {
    println!(
        "  {} {}",
        output::label("Applied migrations:"),
        plan.applied_count
    );
    println!(
        "  {} {} ({} statement(s))",
        output::label("Pending migrations:"),
        plan.pending_count,
        plan.pending_statements
    );

    if !plan.pending_migrations.is_empty() {
        println!("  {}", output::label("Pending tags:"));
        for tag in &plan.pending_migrations {
            println!("    {} {}", output::label("->"), tag);
        }
    }
    println!();

    if opts.verify {
        println!("{}", output::success("Migration verification passed."));
        return true;
    }

    if opts.plan {
        println!("{}", output::success("Migration plan complete."));
        return true;
    }

    if opts.safe && plan.pending_count == 0 {
        println!("  {}", output::success("No pending migrations."));
        println!();
        println!("{}", output::success("Safe migration complete!"));
        return true;
    }

    false
}

fn print_migration_result(result: &crate::db::MigrationResult, safe: bool) {
    if result.applied_count == 0 {
        println!("  {}", output::success("No pending migrations."));
    } else {
        println!(
            "  {} {} migration(s):",
            output::success("Applied"),
            result.applied_count
        );
        for hash in &result.applied_migrations {
            println!("    {} {}", output::label("->"), hash);
        }
    }

    println!();
    if safe {
        println!("{}", output::success("Safe migration complete!"));
    } else {
        println!("{}", output::success("Migrations complete!"));
    }
}