use crate::commands::overrides::{self, ConnectionOverrides};
use crate::config::{Config, Dialect, 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, visible_alias = "dry-run")]
pub plan: bool,
#[arg(long)]
pub safe: bool,
#[arg(long)]
pub repair: 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() {
return Err(CliError::Other(format!(
"Migrations directory not found: {}. Run `drizzle generate` to create your \
first migration, or check `out` in drizzle.config.toml.",
out_dir.display()
)));
}
if matches!(db.driver, Some(Driver::DurableSqlite)) {
print_durable_sqlite_notice(out_dir);
return Ok(());
}
let connection =
overrides::resolve_connection(db, db.dialect, &ConnectionOverrides::default())?;
let Some(connection) = connection else {
print_missing_credentials_help(db.dialect);
return Err(CliError::MissingCredentials("migrate"));
};
let plan = if opts.verify || opts.plan || opts.safe {
Some(crate::db::plan_migrations(
&connection,
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(
&connection,
out_dir,
db.migrations_table(),
db.migrations_schema(),
opts.repair,
)?;
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(),
));
}
if opts.repair && opts.verify {
return Err(CliError::Other(
"--repair can't be combined with --verify".to_string(),
));
}
if opts.repair && opts.plan {
return Err(CliError::Other(
"--repair 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 if opts.repair {
"Repairing and running migrations..."
} 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. Apply them when the");
println!(" object starts:");
println!(
" - Rust: embed them with `drizzle::include_migrations!(\"{}\")` and call",
out_dir.display()
);
println!(" `db.migrate(..)` in `DurableObject::new`.");
println!(
" - JavaScript: import `{}/migrations.js` and run each statement with",
out_dir.display()
);
println!(" `ctx.storage.sql.exec()` inside `ctx.storage.transactionSync()`.");
println!();
println!(
" (This command only generates the SQL + JS bundle — run `drizzle generate` for that.)"
);
}
fn print_missing_credentials_help(dialect: Dialect) {
println!("Add credentials to your drizzle.config.toml:");
println!();
println!(" {}", output::muted("[dbCredentials]"));
let example = match dialect {
Dialect::Sqlite => "url = \"./dev.db\"",
Dialect::Turso => "url = \"libsql://your-db.turso.io\"",
Dialect::Postgresql => "url = \"postgres://user:password@localhost:5432/mydb\"",
Dialect::Mysql => "url = \"mysql://user:password@localhost:3306/mydb\"",
};
println!(" {}", output::muted(example));
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,
) -> Result<bool, CliError> {
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);
}
}
if !plan.findings.is_empty() {
println!();
println!(" {}", output::label("Integrity findings:"));
for finding in &plan.findings {
println!(" {} {}", output::warning("!"), finding);
}
}
println!();
if opts.verify || opts.safe {
if !plan.findings.is_empty() {
let mode = if opts.verify {
"verification"
} else {
"--safe"
};
return Err(CliError::MigrationError(format!(
"{} failed with {} integrity finding(s):\n - {}",
mode,
plan.findings.len(),
plan.findings.join("\n - ")
)));
}
if opts.verify {
println!(
"{}",
output::success(&format!(
"Migration verification passed: {} applied migration(s) match their \
local files; no drift, no interrupted rows.",
plan.applied_count
))
);
return Ok(true);
}
}
if opts.plan {
println!("{}", output::success("Migration plan complete."));
return Ok(true);
}
if opts.safe && plan.pending_count == 0 {
println!(" {}", output::success("No pending migrations."));
println!();
println!("{}", output::success("Safe migration complete!"));
return Ok(true);
}
Ok(false)
}
fn print_migration_result(result: &crate::db::MigrationResult, safe: bool) {
if !result.repaired_migrations.is_empty() {
println!(
" {} {} interrupted migration(s):",
output::success("Repaired"),
result.repaired_migrations.len()
);
for tag in &result.repaired_migrations {
println!(" {} {}", output::label("->"), tag);
}
println!();
}
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!"));
}
}