sqlx_cli/opt.rs
1use crate::config::migrate::{DefaultMigrationType, DefaultVersioning};
2use crate::config::Config;
3use anyhow::Context;
4use chrono::Utc;
5use clap::{
6 builder::{styling::AnsiColor, Styles},
7 Args, Parser,
8};
9#[cfg(feature = "completions")]
10use clap_complete::Shell;
11use sqlx::migrate::{MigrateError, Migrator, ResolveWith};
12use std::env;
13use std::ops::{Deref, Not};
14use std::path::PathBuf;
15
16const HELP_STYLES: Styles = Styles::styled()
17 .header(AnsiColor::Blue.on_default().bold())
18 .usage(AnsiColor::Blue.on_default().bold())
19 .literal(AnsiColor::White.on_default())
20 .placeholder(AnsiColor::Green.on_default());
21
22#[derive(Parser, Debug)]
23#[clap(version, about, author, styles = HELP_STYLES)]
24pub struct Opt {
25 // https://github.com/launchbadge/sqlx/pull/3724 placed this here,
26 // but the intuitive place would be in the arguments for each subcommand.
27 #[clap(flatten)]
28 pub no_dotenv: NoDotenvOpt,
29
30 #[clap(subcommand)]
31 pub command: Command,
32}
33
34#[derive(Parser, Debug)]
35pub enum Command {
36 #[clap(alias = "db")]
37 Database(DatabaseOpt),
38
39 /// Generate query metadata to support offline compile-time verification.
40 ///
41 /// Saves metadata for all invocations of `query!` and related macros to a `.sqlx` directory
42 /// in the current directory (or workspace root with `--workspace`), overwriting if needed.
43 ///
44 /// During project compilation, the absence of the `DATABASE_URL` environment variable or
45 /// the presence of `SQLX_OFFLINE` (with a value of `true` or `1`) will constrain the
46 /// compile-time verification to only read from the cached query metadata.
47 #[clap(alias = "prep")]
48 Prepare {
49 /// Run in 'check' mode. Exits with 0 if the query metadata is up-to-date. Exits with
50 /// 1 if the query metadata needs updating.
51 #[clap(long)]
52 check: bool,
53
54 /// Prepare query macros in dependencies that exist outside the current crate or workspace.
55 #[clap(long)]
56 all: bool,
57
58 /// Generate a single workspace-level `.sqlx` folder.
59 ///
60 /// This option is intended for workspaces where multiple crates use SQLx. If there is only
61 /// one, it is better to run `cargo sqlx prepare` without this option inside that crate.
62 #[clap(long)]
63 workspace: bool,
64
65 /// Arguments to be passed to `cargo rustc ...`.
66 #[clap(last = true)]
67 args: Vec<String>,
68
69 #[clap(flatten)]
70 connect_opts: ConnectOpts,
71
72 #[clap(flatten)]
73 config: ConfigOpt,
74 },
75
76 #[clap(alias = "mig")]
77 Migrate(MigrateOpt),
78
79 #[cfg(feature = "completions")]
80 /// Generate shell completions for the specified shell
81 Completions { shell: Shell },
82}
83
84/// Group of commands for creating and dropping your database.
85#[derive(Parser, Debug)]
86pub struct DatabaseOpt {
87 #[clap(subcommand)]
88 pub command: DatabaseCommand,
89}
90
91#[derive(Parser, Debug)]
92pub enum DatabaseCommand {
93 /// Creates the database specified in your DATABASE_URL.
94 Create {
95 #[clap(flatten)]
96 connect_opts: ConnectOpts,
97
98 #[clap(flatten)]
99 config: ConfigOpt,
100 },
101
102 /// Drops the database specified in your DATABASE_URL.
103 Drop {
104 #[clap(flatten)]
105 confirmation: Confirmation,
106
107 #[clap(flatten)]
108 config: ConfigOpt,
109
110 #[clap(flatten)]
111 connect_opts: ConnectOpts,
112
113 /// PostgreSQL only: force drops the database.
114 #[clap(long, short, default_value = "false")]
115 force: bool,
116 },
117
118 /// Drops the database specified in your DATABASE_URL, re-creates it, and runs any pending migrations.
119 Reset {
120 #[clap(flatten)]
121 confirmation: Confirmation,
122
123 #[clap(flatten)]
124 source: MigrationSourceOpt,
125
126 #[clap(flatten)]
127 config: ConfigOpt,
128
129 #[clap(flatten)]
130 connect_opts: ConnectOpts,
131
132 /// PostgreSQL only: force drops the database.
133 #[clap(long, short, default_value = "false")]
134 force: bool,
135 },
136
137 /// Creates the database specified in your DATABASE_URL and runs any pending migrations.
138 Setup {
139 #[clap(flatten)]
140 source: MigrationSourceOpt,
141
142 #[clap(flatten)]
143 config: ConfigOpt,
144
145 #[clap(flatten)]
146 connect_opts: ConnectOpts,
147 },
148}
149
150/// Group of commands for creating and running migrations.
151#[derive(Parser, Debug)]
152pub struct MigrateOpt {
153 #[clap(subcommand)]
154 pub command: MigrateCommand,
155}
156
157#[derive(Parser, Debug)]
158pub enum MigrateCommand {
159 /// Create a new migration with the given description.
160 ///
161 /// --------------------------------
162 ///
163 /// Migrations may either be simple, or reversible.
164 ///
165 /// Reversible migrations can be reverted with `sqlx migrate revert`, simple migrations cannot.
166 ///
167 /// Reversible migrations are created as a pair of two files with the same filename but
168 /// extensions `.up.sql` and `.down.sql` for the up-migration and down-migration, respectively.
169 ///
170 /// The up-migration should contain the commands to be used when applying the migration,
171 /// while the down-migration should contain the commands to reverse the changes made by the
172 /// up-migration.
173 ///
174 /// When writing down-migrations, care should be taken to ensure that they
175 /// do not leave the database in an inconsistent state.
176 ///
177 /// Simple migrations have just `.sql` for their extension and represent an up-migration only.
178 ///
179 /// Note that reverting a migration is **destructive** and will likely result in data loss.
180 /// Reverting a migration will not restore any data discarded by commands in the up-migration.
181 ///
182 /// It is recommended to always back up the database before running migrations.
183 ///
184 /// --------------------------------
185 ///
186 /// For convenience, this command attempts to detect if reversible migrations are in-use.
187 ///
188 /// If the latest existing migration is reversible, the new migration will also be reversible.
189 ///
190 /// Otherwise, a simple migration is created.
191 ///
192 /// This behavior can be overridden by `--simple` or `--reversible`, respectively.
193 ///
194 /// The default type to use can also be set in `sqlx.toml`.
195 ///
196 /// --------------------------------
197 ///
198 /// A version number will be automatically assigned to the migration.
199 ///
200 /// Migrations are applied in ascending order by version number.
201 /// Version numbers do not need to be strictly consecutive.
202 ///
203 /// The migration process will abort if SQLx encounters a migration with a version number
204 /// less than _any_ previously applied migration.
205 ///
206 /// Migrations should only be created with increasing version number.
207 ///
208 /// --------------------------------
209 ///
210 /// For convenience, this command will attempt to detect if sequential versioning is in use,
211 /// and if so, continue the sequence.
212 ///
213 /// Sequential versioning is inferred if:
214 ///
215 /// * The version numbers of the last two migrations differ by exactly 1, or:
216 ///
217 /// * only one migration exists and its version number is either 0 or 1.
218 ///
219 /// Otherwise, timestamp versioning (`YYYYMMDDHHMMSS`) is assumed.
220 ///
221 /// This behavior can be overridden by `--timestamp` or `--sequential`, respectively.
222 ///
223 /// The default versioning to use can also be set in `sqlx.toml`.
224 Add(AddMigrationOpts),
225
226 /// Run all pending migrations.
227 Run {
228 #[clap(flatten)]
229 source: MigrationSourceOpt,
230
231 #[clap(flatten)]
232 config: ConfigOpt,
233
234 /// List all the migrations to be run without applying
235 #[clap(long)]
236 dry_run: bool,
237
238 #[clap(flatten)]
239 ignore_missing: IgnoreMissing,
240
241 #[clap(flatten)]
242 connect_opts: ConnectOpts,
243
244 /// Apply migrations up to the specified version. If unspecified, apply all
245 /// pending migrations. If already at the target version, then no-op.
246 #[clap(long)]
247 target_version: Option<i64>,
248 },
249
250 /// Override migration state, potentially dangerous operations.
251 Override {
252 #[clap(subcommand)]
253 command: OverrideCommand,
254 },
255
256 /// Revert the latest migration with a down file.
257 Revert {
258 #[clap(flatten)]
259 source: MigrationSourceOpt,
260
261 #[clap(flatten)]
262 config: ConfigOpt,
263
264 /// List the migration to be reverted without applying
265 #[clap(long)]
266 dry_run: bool,
267
268 #[clap(flatten)]
269 ignore_missing: IgnoreMissing,
270
271 #[clap(flatten)]
272 connect_opts: ConnectOpts,
273
274 /// Revert migrations down to the specified version. If unspecified, revert
275 /// only the last migration. Set to 0 to revert all migrations. If already
276 /// at the target version, then no-op.
277 #[clap(long)]
278 target_version: Option<i64>,
279 },
280
281 /// List all available migrations.
282 Info {
283 #[clap(flatten)]
284 source: MigrationSourceOpt,
285
286 #[clap(flatten)]
287 config: ConfigOpt,
288
289 #[clap(flatten)]
290 connect_opts: ConnectOpts,
291 },
292
293 /// Generate a `build.rs` to trigger recompilation when a new migration is added.
294 ///
295 /// Must be run in a Cargo project root.
296 BuildScript {
297 #[clap(flatten)]
298 source: MigrationSourceOpt,
299
300 #[clap(flatten)]
301 config: ConfigOpt,
302
303 /// Overwrite the build script if it already exists.
304 #[clap(long)]
305 force: bool,
306 },
307}
308
309#[derive(clap::Subcommand, Debug)]
310pub enum OverrideCommand {
311 /// Skip all pending migrations without running them.
312 Skip {
313 #[clap(flatten)]
314 source: MigrationSourceOpt,
315
316 #[clap(flatten)]
317 config: ConfigOpt,
318
319 #[clap(flatten)]
320 connect_opts: ConnectOpts,
321
322 /// List all the migrations to be skipped without marking them as applied.
323 #[clap(long)]
324 dry_run: bool,
325
326 #[clap(flatten)]
327 ignore_missing: IgnoreMissing,
328
329 /// Apply migrations up to the specified version. If unspecified, apply all
330 /// pending migrations. If already at the target version, then no-op.
331 #[clap(long)]
332 target_version: Option<i64>,
333 },
334}
335
336#[derive(Args, Debug)]
337pub struct AddMigrationOpts {
338 pub description: String,
339
340 #[clap(flatten)]
341 pub source: MigrationSourceOpt,
342
343 #[clap(flatten)]
344 pub config: ConfigOpt,
345
346 /// If set, create an up-migration only. Conflicts with `--reversible`.
347 #[clap(long, conflicts_with = "reversible")]
348 simple: bool,
349
350 /// If set, create a pair of up and down migration files with same version.
351 ///
352 /// Conflicts with `--simple`.
353 #[clap(short, long, conflicts_with = "simple")]
354 reversible: bool,
355
356 /// If set, use timestamp versioning for the new migration. Conflicts with `--sequential`.
357 ///
358 /// Timestamp format: `YYYYMMDDHHMMSS`
359 #[clap(short, long, conflicts_with = "sequential")]
360 timestamp: bool,
361
362 /// If set, use sequential versioning for the new migration. Conflicts with `--timestamp`.
363 #[clap(short, long, conflicts_with = "timestamp")]
364 sequential: bool,
365}
366
367/// Argument for the migration scripts source.
368#[derive(Args, Debug)]
369pub struct MigrationSourceOpt {
370 /// Path to folder containing migrations.
371 ///
372 /// Defaults to `migrations/` if not specified, but a different default may be set by `sqlx.toml`.
373 #[clap(long)]
374 pub source: Option<String>,
375}
376
377impl MigrationSourceOpt {
378 pub fn resolve_path<'a>(&'a self, config: &'a Config) -> &'a str {
379 if let Some(source) = &self.source {
380 return source;
381 }
382
383 config.migrate.migrations_dir()
384 }
385
386 pub async fn resolve(&self, config: &Config) -> Result<Migrator, MigrateError> {
387 Migrator::new(ResolveWith(
388 self.resolve_path(config),
389 config.migrate.to_resolve_config(),
390 ))
391 .await
392 }
393}
394
395/// Argument for the database URL.
396#[derive(Args, Debug)]
397pub struct ConnectOpts {
398 #[clap(flatten)]
399 pub no_dotenv: NoDotenvOpt,
400
401 /// Location of the DB, by default will be read from the DATABASE_URL env var or `.env` files.
402 #[clap(long, short = 'D')]
403 pub database_url: Option<String>,
404
405 /// The maximum time, in seconds, to try connecting to the database server before
406 /// returning an error.
407 #[clap(long, default_value = "10")]
408 pub connect_timeout: u64,
409
410 /// Set whether or not to create SQLite databases in Write-Ahead Log (WAL) mode:
411 /// https://www.sqlite.org/wal.html
412 ///
413 /// WAL mode is enabled by default for SQLite databases created by `sqlx-cli`.
414 ///
415 /// However, if your application sets a `journal_mode` on `SqliteConnectOptions` to something
416 /// other than `Wal`, then it will have to take the database file out of WAL mode on connecting,
417 /// which requires an exclusive lock and may return a `database is locked` (`SQLITE_BUSY`) error.
418 #[cfg(feature = "_sqlite")]
419 #[clap(long, action = clap::ArgAction::Set, default_value = "true")]
420 pub sqlite_create_db_wal: bool,
421}
422
423#[derive(Args, Debug)]
424pub struct NoDotenvOpt {
425 /// Do not automatically load `.env` files.
426 #[clap(long)]
427 // Parsing of this flag is actually handled _before_ calling Clap,
428 // by `crate::maybe_apply_dotenv()`.
429 #[allow(unused)] // TODO: switch to `#[expect]`
430 pub no_dotenv: bool,
431}
432
433#[derive(Args, Debug)]
434pub struct ConfigOpt {
435 /// Override the path to the config file.
436 ///
437 /// Defaults to `sqlx.toml` in the current directory, if it exists.
438 ///
439 /// Configuration file loading may be bypassed with `--config=/dev/null` on Linux,
440 /// or `--config=NUL` on Windows.
441 ///
442 /// Config file loading is enabled by the `sqlx-toml` feature.
443 #[clap(long)]
444 pub config: Option<PathBuf>,
445}
446
447impl ConnectOpts {
448 /// Require a database URL to be provided, otherwise
449 /// return an error.
450 pub fn expect_db_url(&self) -> anyhow::Result<&str> {
451 self.database_url
452 .as_deref()
453 .context("BUG: database_url not populated")
454 }
455
456 /// Populate `database_url` from the environment, if not set.
457 pub fn populate_db_url(&mut self, config: &Config) -> anyhow::Result<()> {
458 if self.database_url.is_some() {
459 return Ok(());
460 }
461
462 let var = config.common.database_url_var();
463
464 let context = if var != "DATABASE_URL" {
465 " (`common.database-url-var` in `sqlx.toml`)"
466 } else {
467 ""
468 };
469
470 match env::var(var) {
471 Ok(url) => {
472 if !context.is_empty() {
473 eprintln!("Read database url from `{var}`{context}");
474 }
475
476 self.database_url = Some(url)
477 }
478 Err(env::VarError::NotPresent) => {
479 anyhow::bail!("`--database-url` or `{var}`{context} must be set")
480 }
481 Err(env::VarError::NotUnicode(_)) => {
482 anyhow::bail!("`{var}`{context} is not valid UTF-8");
483 }
484 }
485
486 Ok(())
487 }
488}
489
490impl ConfigOpt {
491 pub async fn load_config(&self) -> anyhow::Result<Config> {
492 let path = self.config.clone();
493
494 // Tokio does file I/O on a background task anyway
495 tokio::task::spawn_blocking(|| {
496 if let Some(path) = path {
497 let err_str = format!("error reading config from {path:?}");
498 Config::try_from_path(path).context(err_str)
499 } else {
500 let path = PathBuf::from("sqlx.toml");
501
502 if path.exists() {
503 eprintln!("Found `sqlx.toml` in current directory; reading...");
504 Ok(Config::try_from_path(path)?)
505 } else {
506 Ok(Config::default())
507 }
508 }
509 })
510 .await
511 .context("unexpected error loading config")?
512 }
513}
514
515/// Argument for automatic confirmation.
516#[derive(Args, Copy, Clone, Debug)]
517pub struct Confirmation {
518 /// Automatic confirmation. Without this option, you will be prompted before dropping
519 /// your database.
520 #[clap(short)]
521 pub yes: bool,
522}
523
524/// Argument for ignoring applied migrations that were not resolved.
525#[derive(Args, Copy, Clone, Debug)]
526pub struct IgnoreMissing {
527 /// Ignore applied migrations that are missing in the resolved migrations
528 #[clap(long)]
529 ignore_missing: bool,
530}
531
532impl Deref for IgnoreMissing {
533 type Target = bool;
534
535 fn deref(&self) -> &Self::Target {
536 &self.ignore_missing
537 }
538}
539
540impl Not for IgnoreMissing {
541 type Output = bool;
542
543 fn not(self) -> Self::Output {
544 !self.ignore_missing
545 }
546}
547
548impl AddMigrationOpts {
549 pub fn reversible(&self, config: &Config, migrator: &Migrator) -> bool {
550 if self.reversible {
551 return true;
552 }
553 if self.simple {
554 return false;
555 }
556
557 match config.migrate.defaults.migration_type {
558 DefaultMigrationType::Inferred => migrator
559 .iter()
560 .last()
561 .is_some_and(|m| m.migration_type.is_reversible()),
562 DefaultMigrationType::Simple => false,
563 DefaultMigrationType::Reversible => true,
564 }
565 }
566
567 pub fn version_prefix(&self, config: &Config, migrator: &Migrator) -> String {
568 let default_versioning = &config.migrate.defaults.migration_versioning;
569
570 match (self.timestamp, self.sequential, default_versioning) {
571 (true, false, _) | (false, false, DefaultVersioning::Timestamp) => next_timestamp(),
572 (false, true, _) | (false, false, DefaultVersioning::Sequential) => fmt_sequential(
573 migrator
574 .migrations
575 .last()
576 .map_or(1, |migration| migration.version + 1),
577 ),
578 (false, false, DefaultVersioning::Inferred) => {
579 migrator
580 .migrations
581 .rchunks(2)
582 .next()
583 .and_then(|migrations| {
584 match migrations {
585 [previous, latest] => {
586 // If the latest two versions differ by 1, infer sequential.
587 (latest.version - previous.version == 1)
588 .then_some(latest.version + 1)
589 }
590 [latest] => {
591 // If only one migration exists and its version is 0 or 1, infer sequential
592 matches!(latest.version, 0 | 1).then_some(latest.version + 1)
593 }
594 _ => unreachable!(),
595 }
596 })
597 .map_or_else(next_timestamp, fmt_sequential)
598 }
599 (true, true, _) => unreachable!("BUG: Clap should have rejected this case"),
600 }
601 }
602}
603
604fn next_timestamp() -> String {
605 Utc::now().format("%Y%m%d%H%M%S").to_string()
606}
607
608fn fmt_sequential(version: i64) -> String {
609 format!("{version:04}")
610}