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    /// Revert the latest migration with a down file.
251    Revert {
252        #[clap(flatten)]
253        source: MigrationSourceOpt,
254
255        #[clap(flatten)]
256        config: ConfigOpt,
257
258        /// List the migration to be reverted without applying
259        #[clap(long)]
260        dry_run: bool,
261
262        #[clap(flatten)]
263        ignore_missing: IgnoreMissing,
264
265        #[clap(flatten)]
266        connect_opts: ConnectOpts,
267
268        /// Revert migrations down to the specified version. If unspecified, revert
269        /// only the last migration. Set to 0 to revert all migrations. If already
270        /// at the target version, then no-op.
271        #[clap(long)]
272        target_version: Option<i64>,
273    },
274
275    /// List all available migrations.
276    Info {
277        #[clap(flatten)]
278        source: MigrationSourceOpt,
279
280        #[clap(flatten)]
281        config: ConfigOpt,
282
283        #[clap(flatten)]
284        connect_opts: ConnectOpts,
285    },
286
287    /// Generate a `build.rs` to trigger recompilation when a new migration is added.
288    ///
289    /// Must be run in a Cargo project root.
290    BuildScript {
291        #[clap(flatten)]
292        source: MigrationSourceOpt,
293
294        #[clap(flatten)]
295        config: ConfigOpt,
296
297        /// Overwrite the build script if it already exists.
298        #[clap(long)]
299        force: bool,
300    },
301}
302
303#[derive(Args, Debug)]
304pub struct AddMigrationOpts {
305    pub description: String,
306
307    #[clap(flatten)]
308    pub source: MigrationSourceOpt,
309
310    #[clap(flatten)]
311    pub config: ConfigOpt,
312
313    /// If set, create an up-migration only. Conflicts with `--reversible`.
314    #[clap(long, conflicts_with = "reversible")]
315    simple: bool,
316
317    /// If set, create a pair of up and down migration files with same version.
318    ///
319    /// Conflicts with `--simple`.
320    #[clap(short, long, conflicts_with = "simple")]
321    reversible: bool,
322
323    /// If set, use timestamp versioning for the new migration. Conflicts with `--sequential`.
324    ///
325    /// Timestamp format: `YYYYMMDDHHMMSS`
326    #[clap(short, long, conflicts_with = "sequential")]
327    timestamp: bool,
328
329    /// If set, use sequential versioning for the new migration. Conflicts with `--timestamp`.
330    #[clap(short, long, conflicts_with = "timestamp")]
331    sequential: bool,
332}
333
334/// Argument for the migration scripts source.
335#[derive(Args, Debug)]
336pub struct MigrationSourceOpt {
337    /// Path to folder containing migrations.
338    ///
339    /// Defaults to `migrations/` if not specified, but a different default may be set by `sqlx.toml`.
340    #[clap(long)]
341    pub source: Option<String>,
342}
343
344impl MigrationSourceOpt {
345    pub fn resolve_path<'a>(&'a self, config: &'a Config) -> &'a str {
346        if let Some(source) = &self.source {
347            return source;
348        }
349
350        config.migrate.migrations_dir()
351    }
352
353    pub async fn resolve(&self, config: &Config) -> Result<Migrator, MigrateError> {
354        Migrator::new(ResolveWith(
355            self.resolve_path(config),
356            config.migrate.to_resolve_config(),
357        ))
358        .await
359    }
360}
361
362/// Argument for the database URL.
363#[derive(Args, Debug)]
364pub struct ConnectOpts {
365    #[clap(flatten)]
366    pub no_dotenv: NoDotenvOpt,
367
368    /// Location of the DB, by default will be read from the DATABASE_URL env var or `.env` files.
369    #[clap(long, short = 'D')]
370    pub database_url: Option<String>,
371
372    /// The maximum time, in seconds, to try connecting to the database server before
373    /// returning an error.
374    #[clap(long, default_value = "10")]
375    pub connect_timeout: u64,
376
377    /// Set whether or not to create SQLite databases in Write-Ahead Log (WAL) mode:
378    /// https://www.sqlite.org/wal.html
379    ///
380    /// WAL mode is enabled by default for SQLite databases created by `sqlx-cli`.
381    ///
382    /// However, if your application sets a `journal_mode` on `SqliteConnectOptions` to something
383    /// other than `Wal`, then it will have to take the database file out of WAL mode on connecting,
384    /// which requires an exclusive lock and may return a `database is locked` (`SQLITE_BUSY`) error.
385    #[cfg(feature = "_sqlite")]
386    #[clap(long, action = clap::ArgAction::Set, default_value = "true")]
387    pub sqlite_create_db_wal: bool,
388}
389
390#[derive(Args, Debug)]
391pub struct NoDotenvOpt {
392    /// Do not automatically load `.env` files.
393    #[clap(long)]
394    // Parsing of this flag is actually handled _before_ calling Clap,
395    // by `crate::maybe_apply_dotenv()`.
396    #[allow(unused)] // TODO: switch to `#[expect]`
397    pub no_dotenv: bool,
398}
399
400#[derive(Args, Debug)]
401pub struct ConfigOpt {
402    /// Override the path to the config file.
403    ///
404    /// Defaults to `sqlx.toml` in the current directory, if it exists.
405    ///
406    /// Configuration file loading may be bypassed with `--config=/dev/null` on Linux,
407    /// or `--config=NUL` on Windows.
408    ///
409    /// Config file loading is enabled by the `sqlx-toml` feature.
410    #[clap(long)]
411    pub config: Option<PathBuf>,
412}
413
414impl ConnectOpts {
415    /// Require a database URL to be provided, otherwise
416    /// return an error.
417    pub fn expect_db_url(&self) -> anyhow::Result<&str> {
418        self.database_url
419            .as_deref()
420            .context("BUG: database_url not populated")
421    }
422
423    /// Populate `database_url` from the environment, if not set.
424    pub fn populate_db_url(&mut self, config: &Config) -> anyhow::Result<()> {
425        if self.database_url.is_some() {
426            return Ok(());
427        }
428
429        let var = config.common.database_url_var();
430
431        let context = if var != "DATABASE_URL" {
432            " (`common.database-url-var` in `sqlx.toml`)"
433        } else {
434            ""
435        };
436
437        match env::var(var) {
438            Ok(url) => {
439                if !context.is_empty() {
440                    eprintln!("Read database url from `{var}`{context}");
441                }
442
443                self.database_url = Some(url)
444            }
445            Err(env::VarError::NotPresent) => {
446                anyhow::bail!("`--database-url` or `{var}`{context} must be set")
447            }
448            Err(env::VarError::NotUnicode(_)) => {
449                anyhow::bail!("`{var}`{context} is not valid UTF-8");
450            }
451        }
452
453        Ok(())
454    }
455}
456
457impl ConfigOpt {
458    pub async fn load_config(&self) -> anyhow::Result<Config> {
459        let path = self.config.clone();
460
461        // Tokio does file I/O on a background task anyway
462        tokio::task::spawn_blocking(|| {
463            if let Some(path) = path {
464                let err_str = format!("error reading config from {path:?}");
465                Config::try_from_path(path).context(err_str)
466            } else {
467                let path = PathBuf::from("sqlx.toml");
468
469                if path.exists() {
470                    eprintln!("Found `sqlx.toml` in current directory; reading...");
471                    Ok(Config::try_from_path(path)?)
472                } else {
473                    Ok(Config::default())
474                }
475            }
476        })
477        .await
478        .context("unexpected error loading config")?
479    }
480}
481
482/// Argument for automatic confirmation.
483#[derive(Args, Copy, Clone, Debug)]
484pub struct Confirmation {
485    /// Automatic confirmation. Without this option, you will be prompted before dropping
486    /// your database.
487    #[clap(short)]
488    pub yes: bool,
489}
490
491/// Argument for ignoring applied migrations that were not resolved.
492#[derive(Args, Copy, Clone, Debug)]
493pub struct IgnoreMissing {
494    /// Ignore applied migrations that are missing in the resolved migrations
495    #[clap(long)]
496    ignore_missing: bool,
497}
498
499impl Deref for IgnoreMissing {
500    type Target = bool;
501
502    fn deref(&self) -> &Self::Target {
503        &self.ignore_missing
504    }
505}
506
507impl Not for IgnoreMissing {
508    type Output = bool;
509
510    fn not(self) -> Self::Output {
511        !self.ignore_missing
512    }
513}
514
515impl AddMigrationOpts {
516    pub fn reversible(&self, config: &Config, migrator: &Migrator) -> bool {
517        if self.reversible {
518            return true;
519        }
520        if self.simple {
521            return false;
522        }
523
524        match config.migrate.defaults.migration_type {
525            DefaultMigrationType::Inferred => migrator
526                .iter()
527                .last()
528                .is_some_and(|m| m.migration_type.is_reversible()),
529            DefaultMigrationType::Simple => false,
530            DefaultMigrationType::Reversible => true,
531        }
532    }
533
534    pub fn version_prefix(&self, config: &Config, migrator: &Migrator) -> String {
535        let default_versioning = &config.migrate.defaults.migration_versioning;
536
537        match (self.timestamp, self.sequential, default_versioning) {
538            (true, false, _) | (false, false, DefaultVersioning::Timestamp) => next_timestamp(),
539            (false, true, _) | (false, false, DefaultVersioning::Sequential) => fmt_sequential(
540                migrator
541                    .migrations
542                    .last()
543                    .map_or(1, |migration| migration.version + 1),
544            ),
545            (false, false, DefaultVersioning::Inferred) => {
546                migrator
547                    .migrations
548                    .rchunks(2)
549                    .next()
550                    .and_then(|migrations| {
551                        match migrations {
552                            [previous, latest] => {
553                                // If the latest two versions differ by 1, infer sequential.
554                                (latest.version - previous.version == 1)
555                                    .then_some(latest.version + 1)
556                            }
557                            [latest] => {
558                                // If only one migration exists and its version is 0 or 1, infer sequential
559                                matches!(latest.version, 0 | 1).then_some(latest.version + 1)
560                            }
561                            _ => unreachable!(),
562                        }
563                    })
564                    .map_or_else(next_timestamp, fmt_sequential)
565            }
566            (true, true, _) => unreachable!("BUG: Clap should have rejected this case"),
567        }
568    }
569}
570
571fn next_timestamp() -> String {
572    Utc::now().format("%Y%m%d%H%M%S").to_string()
573}
574
575fn fmt_sequential(version: i64) -> String {
576    format!("{version:04}")
577}