diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs index 7a2056ab..53834c11 100644 --- a/sqlx-cli/src/database.rs +++ b/sqlx-cli/src/database.rs @@ -17,14 +17,14 @@ pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> { std::sync::atomic::Ordering::Release, ); - Any::create_database(connect_opts.required_db_url()?).await?; + Any::create_database(connect_opts.expect_db_url()?).await?; } Ok(()) } pub async fn drop(connect_opts: &ConnectOpts, confirm: bool, force: bool) -> anyhow::Result<()> { - if confirm && !ask_to_continue_drop(connect_opts.required_db_url()?) { + if confirm && !ask_to_continue_drop(connect_opts.expect_db_url()?) { return Ok(()); } @@ -34,9 +34,9 @@ pub async fn drop(connect_opts: &ConnectOpts, confirm: bool, force: bool) -> any if exists { if force { - Any::force_drop_database(connect_opts.required_db_url()?).await?; + Any::force_drop_database(connect_opts.expect_db_url()?).await?; } else { - Any::drop_database(connect_opts.required_db_url()?).await?; + Any::drop_database(connect_opts.expect_db_url()?).await?; } } diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index bfd71e4b..d08b949c 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -1,7 +1,8 @@ use std::io; +use std::path::{Path, PathBuf}; use std::time::Duration; -use anyhow::Result; +use anyhow::{Context, Result}; use futures::{Future, TryFutureExt}; use sqlx::{AnyConnection, Connection}; @@ -20,7 +21,12 @@ mod prepare; pub use crate::opt::Opt; +pub use sqlx::_unstable::config; +use crate::config::Config; + pub async fn run(opt: Opt) -> Result<()> { + let config = config_from_current_dir()?; + match opt.command { Command::Migrate(migrate) => match migrate.command { MigrateCommand::Add { @@ -34,9 +40,11 @@ pub async fn run(opt: Opt) -> Result<()> { source, dry_run, ignore_missing, - connect_opts, + mut connect_opts, target_version, } => { + connect_opts.populate_db_url(config)?; + migrate::run( &source, &connect_opts, @@ -50,9 +58,11 @@ pub async fn run(opt: Opt) -> Result<()> { source, dry_run, ignore_missing, - connect_opts, + mut connect_opts, target_version, } => { + connect_opts.populate_db_url(config)?; + migrate::revert( &source, &connect_opts, @@ -64,37 +74,56 @@ pub async fn run(opt: Opt) -> Result<()> { } MigrateCommand::Info { source, - connect_opts, - } => migrate::info(&source, &connect_opts).await?, + mut connect_opts, + } => { + connect_opts.populate_db_url(config)?; + + migrate::info(&source, &connect_opts).await? + }, MigrateCommand::BuildScript { source, force } => migrate::build_script(&source, force)?, }, Command::Database(database) => match database.command { - DatabaseCommand::Create { connect_opts } => database::create(&connect_opts).await?, + DatabaseCommand::Create { mut connect_opts } => { + connect_opts.populate_db_url(config)?; + database::create(&connect_opts).await? + }, DatabaseCommand::Drop { confirmation, - connect_opts, + mut connect_opts, force, - } => database::drop(&connect_opts, !confirmation.yes, force).await?, + } => { + connect_opts.populate_db_url(config)?; + database::drop(&connect_opts, !confirmation.yes, force).await? + }, DatabaseCommand::Reset { confirmation, source, - connect_opts, + mut connect_opts, force, - } => database::reset(&source, &connect_opts, !confirmation.yes, force).await?, + } => { + connect_opts.populate_db_url(config)?; + database::reset(&source, &connect_opts, !confirmation.yes, force).await? + }, DatabaseCommand::Setup { source, - connect_opts, - } => database::setup(&source, &connect_opts).await?, + mut connect_opts, + } => { + connect_opts.populate_db_url(config)?; + database::setup(&source, &connect_opts).await? + }, }, Command::Prepare { check, all, workspace, - connect_opts, + mut connect_opts, args, - } => prepare::run(check, all, workspace, connect_opts, args).await?, + } => { + connect_opts.populate_db_url(config)?; + prepare::run(check, all, workspace, connect_opts, args).await? + }, #[cfg(feature = "completions")] Command::Completions { shell } => completions::run(shell), @@ -122,7 +151,7 @@ where { sqlx::any::install_default_drivers(); - let db_url = opts.required_db_url()?; + let db_url = opts.expect_db_url()?; backoff::future::retry( backoff::ExponentialBackoffBuilder::new() @@ -147,3 +176,18 @@ where ) .await } + +async fn config_from_current_dir() -> anyhow::Result<&'static Config> { + // Tokio does file I/O on a background task anyway + tokio::task::spawn_blocking(|| { + let path = PathBuf::from("sqlx.toml"); + + if path.exists() { + eprintln!("Found `sqlx.toml` in current directory; reading..."); + } + + Config::read_with_or_default(move || Ok(path)) + }) + .await + .context("unexpected error loading config") +} diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index d5fe3152..a37fda18 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -1,8 +1,10 @@ +use std::env; use std::ops::{Deref, Not}; - +use anyhow::Context; use clap::{Args, Parser}; #[cfg(feature = "completions")] use clap_complete::Shell; +use sqlx::config::Config; #[derive(Parser, Debug)] #[clap(version, about, author)] @@ -242,7 +244,7 @@ impl Deref for Source { #[derive(Args, Debug)] pub struct ConnectOpts { /// Location of the DB, by default will be read from the DATABASE_URL env var or `.env` files. - #[clap(long, short = 'D', env)] + #[clap(long, short = 'D')] pub database_url: Option, /// The maximum time, in seconds, to try connecting to the database server before @@ -266,12 +268,41 @@ pub struct ConnectOpts { impl ConnectOpts { /// Require a database URL to be provided, otherwise /// return an error. - pub fn required_db_url(&self) -> anyhow::Result<&str> { - self.database_url.as_deref().ok_or_else( - || anyhow::anyhow!( - "the `--database-url` option or the `DATABASE_URL` environment variable must be provided" - ) - ) + pub fn expect_db_url(&self) -> anyhow::Result<&str> { + self.database_url.as_deref().context("BUG: database_url not populated") + } + + /// Populate `database_url` from the environment, if not set. + pub fn populate_db_url(&mut self, config: &Config) -> anyhow::Result<()> { + if self.database_url.is_some() { + return Ok(()); + } + + let var = config.common.database_url_var(); + + let context = if var != "DATABASE_URL" { + " (`common.database-url-var` in `sqlx.toml`)" + } else { + "" + }; + + match env::var(var) { + Ok(url) => { + if !context.is_empty() { + eprintln!("Read database url from `{var}`{context}"); + } + + self.database_url = Some(url) + }, + Err(env::VarError::NotPresent) => { + anyhow::bail!("`--database-url` or `{var}`{context} must be set") + } + Err(env::VarError::NotUnicode(_)) => { + anyhow::bail!("`{var}`{context} is not valid UTF-8"); + } + } + + Ok(()) } } diff --git a/sqlx-core/src/config/mod.rs b/sqlx-core/src/config/mod.rs index b3afd9ea..02bde20f 100644 --- a/sqlx-core/src/config/mod.rs +++ b/sqlx-core/src/config/mod.rs @@ -152,25 +152,7 @@ impl Config { /// ### Panics /// If the file exists but an unrecoverable error was encountered while parsing it. pub fn from_crate() -> &'static Self { - Self::try_from_crate().unwrap_or_else(|e| { - match e { - ConfigError::NotFound { path } => { - // Non-fatal - tracing::debug!("Not reading config, file {path:?} not found"); - CACHE.get_or_init(Config::default) - } - // FATAL ERRORS BELOW: - // In the case of migrations, - // we can't proceed with defaults as they may be completely wrong. - e @ ConfigError::ParseDisabled { .. } => { - // Only returned if the file exists but the feature is not enabled. - panic!("{e}") - } - e => { - panic!("failed to read sqlx config: {e}") - } - } - }) + Self::read_with_or_default(get_crate_path) } /// Get the cached config, or to read `$CARGO_MANIFEST_DIR/sqlx.toml`. @@ -179,11 +161,7 @@ impl Config { /// /// Errors if `CARGO_MANIFEST_DIR` is not set, or if the config file could not be read. pub fn try_from_crate() -> Result<&'static Self, ConfigError> { - Self::try_get_with(|| { - let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); - path.push("sqlx.toml"); - Ok(path) - }) + Self::try_read_with(get_crate_path) } /// Get the cached config, or attempt to read `sqlx.toml` from the current working directory. @@ -192,7 +170,7 @@ impl Config { /// /// Errors if the config file does not exist, or could not be read. pub fn try_from_current_dir() -> Result<&'static Self, ConfigError> { - Self::try_get_with(|| Ok("sqlx.toml".into())) + Self::try_read_with(|| Ok("sqlx.toml".into())) } /// Get the cached config, or attempt to read it from the path returned by the closure. @@ -200,7 +178,7 @@ impl Config { /// On success, the config is cached in a `static` and returned by future calls. /// /// Errors if the config file does not exist, or could not be read. - pub fn try_get_with( + pub fn try_read_with( make_path: impl FnOnce() -> Result, ) -> Result<&'static Self, ConfigError> { CACHE.get_or_try_init(|| { @@ -209,6 +187,36 @@ impl Config { }) } + /// Get the cached config, or attempt to read it from the path returned by the closure. + /// + /// On success, the config is cached in a `static` and returned by future calls. + /// + /// Returns `Config::default()` if the file does not exist. + pub fn read_with_or_default( + make_path: impl FnOnce() -> Result, + ) -> &'static Self { + CACHE.get_or_init(|| { + match make_path().and_then(Self::read_from) { + Ok(config) => config, + Err(ConfigError::NotFound { path }) => { + // Non-fatal + tracing::debug!("Not reading config, file {path:?} not found"); + Config::default() + } + // FATAL ERRORS BELOW: + // In the case of migrations, + // we can't proceed with defaults as they may be completely wrong. + Err(e @ ConfigError::ParseDisabled { .. }) => { + // Only returned if the file exists but the feature is not enabled. + panic!("{e}") + } + Err(e) => { + panic!("failed to read sqlx config: {e}") + } + } + }) + } + #[cfg(feature = "sqlx-toml")] fn read_from(path: PathBuf) -> Result { // The `toml` crate doesn't provide an incremental reader. @@ -238,3 +246,9 @@ impl Config { } } } + +fn get_crate_path() -> Result { + let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); + path.push("sqlx.toml"); + Ok(path) +} diff --git a/src/lib.rs b/src/lib.rs index 7c10b852..aaa0e819 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,6 +169,12 @@ pub mod prelude { #[cfg(feature = "_unstable-doc")] pub use sqlx_core::config; +// NOTE: APIs exported in this module are SemVer-exempt. +#[doc(hidden)] +pub mod _unstable { + pub use sqlx_core::config; +} + #[doc(hidden)] #[cfg_attr( all(feature = "chrono", feature = "time"),