Skip to main content

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}