Skip to main content

drizzle_cli/commands/
migrate.rs

1//! Migrate command implementation
2//!
3//! Runs pending migrations against the database.
4
5use crate::config::{Config, Driver};
6use crate::error::CliError;
7use crate::output;
8
9#[derive(clap::Args, Debug, Clone, Copy, Default)]
10pub struct MigrateOptions {
11    /// Verify migration consistency without applying changes
12    #[arg(long)]
13    pub verify: bool,
14
15    /// Print pending migration plan without applying changes
16    #[arg(long)]
17    pub plan: bool,
18
19    /// Verify first, then apply if checks pass
20    #[arg(long)]
21    pub safe: bool,
22}
23
24/// Run the migrate command.
25///
26/// # Errors
27///
28/// Returns [`CliError`] if mutually exclusive flags are combined, the database
29/// or credentials cannot be resolved, connecting to the database fails, or
30/// applying migrations fails.
31pub fn run(config: &Config, db_name: Option<&str>, opts: MigrateOptions) -> Result<(), CliError> {
32    validate_mutex_opts(opts)?;
33
34    let db = config.database(db_name)?;
35
36    crate::commands::harness::print_db_header(config, db_name);
37
38    println!("{}", output::heading(migrate_heading(opts)));
39    println!();
40
41    let out_dir = db.migrations_dir();
42
43    // Check if migrations directory exists
44    if !out_dir.exists() {
45        println!("  {}", output::warning("No migrations directory found."));
46        println!("  Run 'drizzle generate' to create your first migration.");
47        return Ok(());
48    }
49
50    // Codegen-only drivers (e.g. durable-sqlite) have no remote endpoint for the
51    // CLI to reach — migrations execute inside the DO runtime. Short-circuit
52    // with a pointed message instead of the generic "no credentials" fallback.
53    if matches!(db.driver, Some(Driver::DurableSqlite)) {
54        print_durable_sqlite_notice(out_dir);
55        return Ok(());
56    }
57
58    // Get credentials
59    let credentials = db.credentials()?;
60
61    let Some(credentials) = credentials else {
62        print_missing_credentials_help();
63        return Ok(());
64    };
65
66    let plan = if opts.verify || opts.plan || opts.safe {
67        Some(crate::db::verify_migrations(
68            &credentials,
69            db.dialect,
70            out_dir,
71            db.migrations_table(),
72            db.migrations_schema(),
73        )?)
74    } else {
75        None
76    };
77
78    if let Some(plan) = &plan
79        && handle_plan_short_circuit(plan, opts)
80    {
81        return Ok(());
82    }
83
84    // Run migrations
85    let result = crate::db::run_migrations(
86        &credentials,
87        db.dialect,
88        out_dir,
89        db.migrations_table(),
90        db.migrations_schema(),
91    )?;
92
93    print_migration_result(&result, opts.safe);
94    Ok(())
95}
96
97fn validate_mutex_opts(opts: MigrateOptions) -> Result<(), CliError> {
98    if opts.safe && opts.verify {
99        return Err(CliError::Other(
100            "--safe can't be combined with --verify".to_string(),
101        ));
102    }
103    if opts.safe && opts.plan {
104        return Err(CliError::Other(
105            "--safe can't be combined with --plan".to_string(),
106        ));
107    }
108    Ok(())
109}
110
111const fn migrate_heading(opts: MigrateOptions) -> &'static str {
112    if opts.verify {
113        "Verifying migrations..."
114    } else if opts.plan {
115        "Planning migrations..."
116    } else if opts.safe {
117        "Running safe migration flow..."
118    } else {
119        "Running migrations..."
120    }
121}
122
123fn print_durable_sqlite_notice(out_dir: &std::path::Path) {
124    println!(
125        "{}",
126        output::warning("Durable Objects SQLite runs inside the Workers runtime.")
127    );
128    println!();
129    println!("  The CLI can't apply migrations to a DO from outside.");
130    println!(
131        "  Apply them at `DurableObject` init time by importing `{}/migrations.js`",
132        out_dir.display()
133    );
134    println!("  and running each statement against `state.storage().sql()`.");
135    println!();
136    println!(
137        "  (This command only generates the SQL + JS bundle — run `drizzle generate` for that.)"
138    );
139}
140
141fn print_missing_credentials_help() {
142    println!("{}", output::warning("No database credentials configured."));
143    println!();
144    println!("Add credentials to your drizzle.config.toml:");
145    println!();
146    println!("  {}", output::muted("[dbCredentials]"));
147    println!("  {}", output::muted("url = \"./dev.db\""));
148    println!();
149    println!("Or use an environment variable:");
150    println!();
151    println!("  {}", output::muted("[dbCredentials]"));
152    println!("  {}", output::muted("url = { env = \"DATABASE_URL\" }"));
153}
154
155/// Print plan summary and return `true` if the caller should return early.
156fn handle_plan_short_circuit(plan: &crate::db::MigrationPlan, opts: MigrateOptions) -> bool {
157    println!(
158        "  {} {}",
159        output::label("Applied migrations:"),
160        plan.applied_count
161    );
162    println!(
163        "  {} {} ({} statement(s))",
164        output::label("Pending migrations:"),
165        plan.pending_count,
166        plan.pending_statements
167    );
168
169    if !plan.pending_migrations.is_empty() {
170        println!("  {}", output::label("Pending tags:"));
171        for tag in &plan.pending_migrations {
172            println!("    {} {}", output::label("->"), tag);
173        }
174    }
175    println!();
176
177    if opts.verify {
178        println!("{}", output::success("Migration verification passed."));
179        return true;
180    }
181
182    if opts.plan {
183        println!("{}", output::success("Migration plan complete."));
184        return true;
185    }
186
187    if opts.safe && plan.pending_count == 0 {
188        println!("  {}", output::success("No pending migrations."));
189        println!();
190        println!("{}", output::success("Safe migration complete!"));
191        return true;
192    }
193
194    false
195}
196
197fn print_migration_result(result: &crate::db::MigrationResult, safe: bool) {
198    if result.applied_count == 0 {
199        println!("  {}", output::success("No pending migrations."));
200    } else {
201        println!(
202            "  {} {} migration(s):",
203            output::success("Applied"),
204            result.applied_count
205        );
206        for hash in &result.applied_migrations {
207            println!("    {} {}", output::label("->"), hash);
208        }
209    }
210
211    println!();
212    if safe {
213        println!("{}", output::success("Safe migration complete!"));
214    } else {
215        println!("{}", output::success("Migrations complete!"));
216    }
217}