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