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