//! # SQLx CLI //! //! Command-line utility for the [SQLx](https://github.com/launchbadge/sqlx) ecosystem. //! //! This crate provides the core logic for the `sqlx` command-line interface, enabling database management, //! migrations, and offline query preparation for Rust projects using SQLx. //! //! ### Note: Semver Exempt API //! The API of this crate is not meant for general use and does *not* follow Semantic Versioning. //! The only crate that follows Semantic Versioning in the project is the `sqlx` crate itself. //! If you are building a custom SQLx driver, you should pin an exact version for `sqlx-cli` to //! avoid breakages: //! //! ```toml //! sqlx-cli = { version = "=0.9.0" } //! ``` //! //! And then make releases in lockstep with `sqlx-cli`. We recommend all driver crates, in-tree //! or otherwise, use the same version numbers as `sqlx-cli` to avoid confusion. use std::future::Future; use std::io; use std::time::Duration; use futures_util::TryFutureExt; use sqlx::AnyConnection; use tokio::{select, signal}; use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand}; pub mod database; pub mod metadata; // mod migration; // mod migrator; #[cfg(feature = "completions")] pub mod completions; pub mod migrate; pub mod opt; pub mod prepare; pub use crate::opt::Opt; pub use sqlx::_unstable::config::{self, Config}; /// Check arguments for `--no-dotenv` _before_ Clap parsing, and apply `.env` if not set. pub fn maybe_apply_dotenv() { if std::env::args().any(|arg| arg == "--no-dotenv") { return; } if let Err(e) = dotenvy::dotenv() { if !e.not_found() { eprintln!("Warning: error loading `.env` file: {e:?}"); } } } pub async fn run(opt: Opt) -> anyhow::Result<()> { // This `select!` is here so that when the process receives a `SIGINT` (CTRL + C), // the futures currently running on this task get dropped before the program exits. // This is currently necessary for the consumers of the `dialoguer` crate to restore // the user's terminal if the process is interrupted while a dialog is being displayed. let ctrlc_fut = signal::ctrl_c(); let do_run_fut = do_run(opt); select! { biased; _ = ctrlc_fut => { Ok(()) }, do_run_outcome = do_run_fut => { do_run_outcome } } } async fn do_run(opt: Opt) -> anyhow::Result<()> { match opt.command { Command::Migrate(migrate) => match migrate.command { MigrateCommand::Add(opts) => migrate::add(opts).await?, MigrateCommand::Run { source, config, dry_run, ignore_missing, mut connect_opts, target_version, } => { let config = config.load_config().await?; connect_opts.populate_db_url(&config)?; migrate::run( &config, &source, &connect_opts, dry_run, *ignore_missing, target_version, ) .await? } MigrateCommand::Revert { source, config, dry_run, ignore_missing, mut connect_opts, target_version, } => { let config = config.load_config().await?; connect_opts.populate_db_url(&config)?; migrate::revert( &config, &source, &connect_opts, dry_run, *ignore_missing, target_version, ) .await? } MigrateCommand::Info { source, config, mut connect_opts, } => { let config = config.load_config().await?; connect_opts.populate_db_url(&config)?; migrate::info(&config, &source, &connect_opts).await? } MigrateCommand::BuildScript { source, config, force, } => { let config = config.load_config().await?; migrate::build_script(&config, &source, force)? } }, Command::Database(database) => match database.command { DatabaseCommand::Create { config, mut connect_opts, } => { let config = config.load_config().await?; connect_opts.populate_db_url(&config)?; database::create(&connect_opts).await? } DatabaseCommand::Drop { confirmation, config, mut connect_opts, force, } => { let config = config.load_config().await?; connect_opts.populate_db_url(&config)?; database::drop(&connect_opts, !confirmation.yes, force).await? } DatabaseCommand::Reset { confirmation, source, config, mut connect_opts, force, } => { let config = config.load_config().await?; connect_opts.populate_db_url(&config)?; database::reset(&config, &source, &connect_opts, !confirmation.yes, force).await? } DatabaseCommand::Setup { source, config, mut connect_opts, } => { let config = config.load_config().await?; connect_opts.populate_db_url(&config)?; database::setup(&config, &source, &connect_opts).await? } }, Command::Prepare { check, all, workspace, mut connect_opts, args, config, } => { let config = config.load_config().await?; connect_opts.populate_db_url(&config)?; prepare::run(&config, check, all, workspace, connect_opts, args).await? } #[cfg(feature = "completions")] Command::Completions { shell } => completions::run(shell), }; Ok(()) } /// Attempt to connect to the database server, retrying up to `ops.connect_timeout`. async fn connect(config: &Config, opts: &ConnectOpts) -> anyhow::Result { retry_connect_errors(opts, move |url| { AnyConnection::connect_with_driver_config(url, &config.drivers) }) .await } /// Attempt an operation that may return errors like `ConnectionRefused`, /// retrying up until `ops.connect_timeout`. /// /// The closure is passed `&ops.database_url` for easy composition. async fn retry_connect_errors<'a, F, Fut, T>( opts: &'a ConnectOpts, mut connect: F, ) -> anyhow::Result where F: FnMut(&'a str) -> Fut, Fut: Future> + 'a, { let db_url = opts.expect_db_url()?; backoff::future::retry( backoff::ExponentialBackoffBuilder::new() .with_max_elapsed_time(Some(Duration::from_secs(opts.connect_timeout))) .build(), || { connect(db_url).map_err(|e| -> backoff::Error { if let sqlx::Error::Io(ref ioe) = e { match ioe.kind() { io::ErrorKind::ConnectionRefused | io::ErrorKind::ConnectionReset | io::ErrorKind::ConnectionAborted => { return backoff::Error::transient(e.into()); } _ => (), } } backoff::Error::permanent(e.into()) }) }, ) .await }