drizzle_cli/commands/
migrate.rs1use crate::commands::overrides::{self, ConnectionOverrides};
4use crate::config::{Config, Dialect, Driver};
5use crate::error::CliError;
6use crate::output;
7
8#[derive(clap::Args, Debug, Clone, Copy, Default)]
9pub struct MigrateOptions {
10 #[arg(long)]
18 pub verify: bool,
19
20 #[arg(long, visible_alias = "dry-run")]
26 pub plan: bool,
27
28 #[arg(long)]
30 pub safe: bool,
31
32 #[arg(long)]
42 pub repair: bool,
43}
44
45pub fn run(config: &Config, db_name: Option<&str>, opts: MigrateOptions) -> Result<(), CliError> {
61 validate_mutex_opts(opts)?;
62
63 let db = config.database(db_name)?;
64
65 crate::commands::harness::print_db_header(config, db_name);
66
67 println!("{}", output::heading(migrate_heading(opts)));
68 println!();
69
70 let out_dir = db.migrations_dir();
71
72 if !out_dir.exists() {
75 return Err(CliError::Other(format!(
76 "Migrations directory not found: {}. Run `drizzle generate` to create your \
77 first migration, or check `out` in drizzle.config.toml.",
78 out_dir.display()
79 )));
80 }
81
82 if matches!(db.driver, Some(Driver::DurableSqlite)) {
86 print_durable_sqlite_notice(out_dir);
87 return Ok(());
88 }
89
90 let connection =
91 overrides::resolve_connection(db, db.dialect, &ConnectionOverrides::default())?;
92 let Some(connection) = connection else {
93 print_missing_credentials_help(db.dialect);
94 return Err(CliError::MissingCredentials("migrate"));
95 };
96
97 let plan = if opts.verify || opts.plan || opts.safe {
98 Some(crate::db::plan_migrations(
99 &connection,
100 out_dir,
101 db.migrations_table(),
102 db.migrations_schema(),
103 )?)
104 } else {
105 None
106 };
107
108 if let Some(plan) = &plan
109 && handle_plan_short_circuit(plan, opts)?
110 {
111 return Ok(());
112 }
113
114 let result = crate::db::run_migrations(
116 &connection,
117 out_dir,
118 db.migrations_table(),
119 db.migrations_schema(),
120 opts.repair,
121 )?;
122
123 print_migration_result(&result, opts.safe);
124 Ok(())
125}
126
127fn validate_mutex_opts(opts: MigrateOptions) -> Result<(), CliError> {
128 if opts.safe && opts.verify {
129 return Err(CliError::Other(
130 "--safe can't be combined with --verify".to_string(),
131 ));
132 }
133 if opts.safe && opts.plan {
134 return Err(CliError::Other(
135 "--safe can't be combined with --plan".to_string(),
136 ));
137 }
138 if opts.repair && opts.verify {
140 return Err(CliError::Other(
141 "--repair can't be combined with --verify".to_string(),
142 ));
143 }
144 if opts.repair && opts.plan {
145 return Err(CliError::Other(
146 "--repair can't be combined with --plan".to_string(),
147 ));
148 }
149 Ok(())
150}
151
152const fn migrate_heading(opts: MigrateOptions) -> &'static str {
153 if opts.verify {
154 "Verifying migrations..."
155 } else if opts.plan {
156 "Planning migrations..."
157 } else if opts.safe {
158 "Running safe migration flow..."
159 } else if opts.repair {
160 "Repairing and running migrations..."
161 } else {
162 "Running migrations..."
163 }
164}
165
166fn print_durable_sqlite_notice(out_dir: &std::path::Path) {
167 println!(
168 "{}",
169 output::warning("Durable Objects SQLite runs inside the Workers runtime.")
170 );
171 println!();
172 println!(" The CLI can't apply migrations to a DO from outside. Apply them when the");
173 println!(" object starts:");
174 println!(
175 " - Rust: embed them with `drizzle::include_migrations!(\"{}\")` and call",
176 out_dir.display()
177 );
178 println!(" `db.migrate(..)` in `DurableObject::new`.");
179 println!(
180 " - JavaScript: import `{}/migrations.js` and run each statement with",
181 out_dir.display()
182 );
183 println!(" `ctx.storage.sql.exec()` inside `ctx.storage.transactionSync()`.");
184 println!();
185 println!(
186 " (This command only generates the SQL + JS bundle — run `drizzle generate` for that.)"
187 );
188}
189
190fn print_missing_credentials_help(dialect: Dialect) {
192 println!("Add credentials to your drizzle.config.toml:");
193 println!();
194 println!(" {}", output::muted("[dbCredentials]"));
195 let example = match dialect {
196 Dialect::Sqlite => "url = \"./dev.db\"",
197 Dialect::Turso => "url = \"libsql://your-db.turso.io\"",
198 Dialect::Postgresql => "url = \"postgres://user:password@localhost:5432/mydb\"",
199 Dialect::Mysql => "url = \"mysql://user:password@localhost:3306/mydb\"",
200 };
201 println!(" {}", output::muted(example));
202 println!();
203 println!("Or use an environment variable:");
204 println!();
205 println!(" {}", output::muted("[dbCredentials]"));
206 println!(" {}", output::muted("url = { env = \"DATABASE_URL\" }"));
207}
208
209fn handle_plan_short_circuit(
214 plan: &crate::db::MigrationPlan,
215 opts: MigrateOptions,
216) -> Result<bool, CliError> {
217 println!(
218 " {} {}",
219 output::label("Applied migrations:"),
220 plan.applied_count
221 );
222 println!(
223 " {} {} ({} statement(s))",
224 output::label("Pending migrations:"),
225 plan.pending_count,
226 plan.pending_statements
227 );
228
229 if !plan.pending_migrations.is_empty() {
230 println!(" {}", output::label("Pending tags:"));
231 for tag in &plan.pending_migrations {
232 println!(" {} {}", output::label("->"), tag);
233 }
234 }
235
236 if !plan.findings.is_empty() {
237 println!();
238 println!(" {}", output::label("Integrity findings:"));
239 for finding in &plan.findings {
240 println!(" {} {}", output::warning("!"), finding);
241 }
242 }
243 println!();
244
245 if opts.verify || opts.safe {
246 if !plan.findings.is_empty() {
247 let mode = if opts.verify {
248 "verification"
249 } else {
250 "--safe"
251 };
252 return Err(CliError::MigrationError(format!(
253 "{} failed with {} integrity finding(s):\n - {}",
254 mode,
255 plan.findings.len(),
256 plan.findings.join("\n - ")
257 )));
258 }
259 if opts.verify {
260 println!(
261 "{}",
262 output::success(&format!(
263 "Migration verification passed: {} applied migration(s) match their \
264 local files; no drift, no interrupted rows.",
265 plan.applied_count
266 ))
267 );
268 return Ok(true);
269 }
270 }
271
272 if opts.plan {
273 println!("{}", output::success("Migration plan complete."));
274 return Ok(true);
275 }
276
277 if opts.safe && plan.pending_count == 0 {
278 println!(" {}", output::success("No pending migrations."));
279 println!();
280 println!("{}", output::success("Safe migration complete!"));
281 return Ok(true);
282 }
283
284 Ok(false)
285}
286
287fn print_migration_result(result: &crate::db::MigrationResult, safe: bool) {
288 if !result.repaired_migrations.is_empty() {
289 println!(
290 " {} {} interrupted migration(s):",
291 output::success("Repaired"),
292 result.repaired_migrations.len()
293 );
294 for tag in &result.repaired_migrations {
295 println!(" {} {}", output::label("->"), tag);
296 }
297 println!();
298 }
299
300 if result.applied_count == 0 {
301 println!(" {}", output::success("No pending migrations."));
302 } else {
303 println!(
304 " {} {} migration(s):",
305 output::success("Applied"),
306 result.applied_count
307 );
308 for hash in &result.applied_migrations {
309 println!(" {} {}", output::label("->"), hash);
310 }
311 }
312
313 println!();
314 if safe {
315 println!("{}", output::success("Safe migration complete!"));
316 } else {
317 println!("{}", output::success("Migrations complete!"));
318 }
319}