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}