use crate::config::{Config, Driver};
use crate::error::CliError;
use crate::output;
#[derive(clap::Args, Debug, Clone, Copy, Default)]
pub struct MigrateOptions {
#[arg(long)]
pub verify: bool,
#[arg(long)]
pub plan: bool,
#[arg(long)]
pub safe: bool,
}
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();
if !out_dir.exists() {
println!(" {}", output::warning("No migrations directory found."));
println!(" Run 'drizzle generate' to create your first migration.");
return Ok(());
}
if matches!(db.driver, Some(Driver::DurableSqlite)) {
print_durable_sqlite_notice(out_dir);
return Ok(());
}
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(());
}
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\" }"));
}
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!"));
}
}