feat: make sqlx-cli aware of database-url-var

This commit is contained in:
Austin Bonander 2024-09-20 00:46:43 -07:00
parent 13f6ef0ab0
commit 65ef27f70c
5 changed files with 148 additions and 53 deletions

View File

@ -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?;
}
}

View File

@ -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")
}

View File

@ -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<String>,
/// 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(())
}
}

View File

@ -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<PathBuf, ConfigError>,
) -> 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<PathBuf, ConfigError>,
) -> &'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<Self, ConfigError> {
// The `toml` crate doesn't provide an incremental reader.
@ -238,3 +246,9 @@ impl Config {
}
}
}
fn get_crate_path() -> Result<PathBuf, ConfigError> {
let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
path.push("sqlx.toml");
Ok(path)
}

View File

@ -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"),