diff --git a/sqlx-cli/sqlx-config.schema.json b/sqlx-cli/sqlx-config.schema.json new file mode 100644 index 0000000000..c72342a4fc --- /dev/null +++ b/sqlx-cli/sqlx-config.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "env_path": { + "type": "string", + "description": "Path to the .env file, this ignores the --database-url option when the command is run. If not provided, the .env file in the current directory is used if it exists, otherwise it gets the database url from the command line option \"--database-url.\"", + "default": ".env", + "examples": ["../.env", "../myapp/.env"] + } + } +} diff --git a/sqlx-cli/src/bin/cargo-sqlx.rs b/sqlx-cli/src/bin/cargo-sqlx.rs index 58f7b345f4..ced8426a32 100644 --- a/sqlx-cli/src/bin/cargo-sqlx.rs +++ b/sqlx-cli/src/bin/cargo-sqlx.rs @@ -14,6 +14,7 @@ enum Cli { #[tokio::main] async fn main() { dotenvy::dotenv().ok(); + let Cli::Sqlx(opt) = Cli::parse(); if let Err(error) = sqlx_cli::run(opt).await { diff --git a/sqlx-cli/src/bin/sqlx.rs b/sqlx-cli/src/bin/sqlx.rs index 59025cd7da..3d0bd6c25f 100644 --- a/sqlx-cli/src/bin/sqlx.rs +++ b/sqlx-cli/src/bin/sqlx.rs @@ -1,10 +1,15 @@ use clap::Parser; use console::style; -use sqlx_cli::Opt; +use sqlx_cli::{Opt, SqlxConfig}; #[tokio::main] async fn main() { dotenvy::dotenv().ok(); + SqlxConfig::read() + .ok() + .ok_or(anyhow::anyhow!("Error reading sqlx-config.file")) + .unwrap(); + // no special handling here if let Err(error) = sqlx_cli::run(Opt::parse()).await { println!("{} {}", style("error:").bold().red(), error); diff --git a/sqlx-cli/src/config.rs b/sqlx-cli/src/config.rs new file mode 100644 index 0000000000..e756e5272d --- /dev/null +++ b/sqlx-cli/src/config.rs @@ -0,0 +1,82 @@ +use serde::{Deserialize, Serialize}; +use std::fs::{self, File}; + +use crate::opt::ConnectOpts; + +/// Main struct for the sqlx-config.json +#[derive(Debug, Serialize, Deserialize)] +pub struct SqlxConfig { + #[serde(rename = "$schema")] + pub schema: String, + pub env_path: Option, +} + +impl Default for SqlxConfig { + fn default() -> Self { + SqlxConfig { + schema: "https://raw.githubusercontent.com/Dawsoncodes/sqlx/main/sqlx-cli/sqlx-config.schema.json" + .to_string(), + env_path: Some(".env".to_string()) + } + } +} + +// SqlxConfig::create +impl SqlxConfig { + pub fn create() -> anyhow::Result { + let default = SqlxConfig::default(); + + serde_json::to_writer_pretty(File::create("sqlx-config.json")?, &default)?; + + Ok(default) + } +} + +impl SqlxConfig { + pub fn read() -> anyhow::Result { + let file = fs::read_to_string("sqlx-config.json"); + match file { + Ok(f) => { + let config: SqlxConfig = serde_json::from_str(&f).unwrap(); + + Ok(config) + } + Err(_) => { + // Create the file + let default = SqlxConfig::default(); + + serde_json::to_writer_pretty(File::create("sqlx-config.json")?, &default)?; + + Ok(default) + } + } + } + + pub fn get_database_url() -> Option { + let sqlx_config = SqlxConfig::read().unwrap_or_default(); + + if let Some(env_path) = sqlx_config.env_path { + dotenvy::from_filename(env_path).ok(); + + let url = dotenvy::var("DATABASE_URL").ok(); + + if let Some(url) = url { + return Some(url); + } + } + + None + } +} + +/// If the `DATABASE_URL` was available in the `env_path` specified in the `sqlx-config.json` file, +/// then it will be returned, otherwise it will check the command line option `--database-url`. +pub fn get_database_url(connect_opts: &ConnectOpts) -> String { + let db_url_from_config = SqlxConfig::get_database_url(); + + if let Some(db_url_from_config) = db_url_from_config { + db_url_from_config + } else { + connect_opts.required_db_url().unwrap().to_string() + } +} diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs index 82dcfcfed9..91b2a4a687 100644 --- a/sqlx-cli/src/database.rs +++ b/sqlx-cli/src/database.rs @@ -1,3 +1,4 @@ +use crate::config::get_database_url; use crate::migrate; use crate::opt::ConnectOpts; use console::style; @@ -5,7 +6,7 @@ use promptly::{prompt, ReadlineError}; use sqlx::any::Any; use sqlx::migrate::MigrateDatabase; -pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> { +pub async fn create(connect_opts: &mut ConnectOpts) -> anyhow::Result<()> { // NOTE: only retry the idempotent action. // We're assuming that if this succeeds, then any following operations should also succeed. let exists = crate::retry_connect_errors(connect_opts, Any::database_exists).await?; @@ -17,14 +18,18 @@ pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> { std::sync::atomic::Ordering::Release, ); - Any::create_database(connect_opts.required_db_url()?).await?; + let database_url = get_database_url(connect_opts); + + Any::create_database(&database_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()?) { + let database_url = get_database_url(connect_opts); + + if confirm && !ask_to_continue_drop(&database_url) { return Ok(()); } @@ -33,10 +38,12 @@ pub async fn drop(connect_opts: &ConnectOpts, confirm: bool, force: bool) -> any let exists = crate::retry_connect_errors(connect_opts, Any::database_exists).await?; if exists { + let database_url = get_database_url(connect_opts); + if force { - Any::force_drop_database(connect_opts.required_db_url()?).await?; + Any::force_drop_database(&database_url).await?; } else { - Any::drop_database(connect_opts.required_db_url()?).await?; + Any::drop_database(&database_url).await?; } } @@ -45,7 +52,7 @@ pub async fn drop(connect_opts: &ConnectOpts, confirm: bool, force: bool) -> any pub async fn reset( migration_source: &str, - connect_opts: &ConnectOpts, + connect_opts: &mut ConnectOpts, confirm: bool, force: bool, ) -> anyhow::Result<()> { @@ -53,7 +60,7 @@ pub async fn reset( setup(migration_source, connect_opts).await } -pub async fn setup(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow::Result<()> { +pub async fn setup(migration_source: &str, connect_opts: &mut ConnectOpts) -> anyhow::Result<()> { create(connect_opts).await?; migrate::run(migration_source, connect_opts, false, false, None).await } diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index a0890fe053..f8a5b5ef34 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -14,11 +14,13 @@ mod metadata; // mod migrator; #[cfg(feature = "completions")] mod completions; +mod config; mod migrate; mod opt; mod prepare; pub use crate::opt::Opt; +pub use config::SqlxConfig; pub async fn run(opt: Opt) -> Result<()> { match opt.command { @@ -34,12 +36,12 @@ pub async fn run(opt: Opt) -> Result<()> { source, dry_run, ignore_missing, - connect_opts, + mut connect_opts, target_version, } => { migrate::run( &source, - &connect_opts, + &mut connect_opts, dry_run, *ignore_missing, target_version, @@ -50,12 +52,12 @@ pub async fn run(opt: Opt) -> Result<()> { source, dry_run, ignore_missing, - connect_opts, + mut connect_opts, target_version, } => { migrate::revert( &source, - &connect_opts, + &mut connect_opts, dry_run, *ignore_missing, target_version, @@ -64,28 +66,30 @@ pub async fn run(opt: Opt) -> Result<()> { } MigrateCommand::Info { source, - connect_opts, - } => migrate::info(&source, &connect_opts).await?, + mut connect_opts, + } => migrate::info(&source, &mut 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 } => { + database::create(&mut connect_opts).await? + } DatabaseCommand::Drop { confirmation, - connect_opts, + mut connect_opts, force, - } => database::drop(&connect_opts, !confirmation.yes, force).await?, + } => database::drop(&mut 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?, + } => database::reset(&source, &mut connect_opts, !confirmation.yes, force).await?, DatabaseCommand::Setup { source, - connect_opts, - } => database::setup(&source, &connect_opts).await?, + mut connect_opts, + } => database::setup(&source, &mut connect_opts).await?, }, Command::Prepare { @@ -103,7 +107,7 @@ pub async fn run(opt: Opt) -> Result<()> { } /// Attempt to connect to the database server, retrying up to `ops.connect_timeout`. -async fn connect(opts: &ConnectOpts) -> anyhow::Result { +async fn connect(opts: &mut ConnectOpts) -> anyhow::Result { retry_connect_errors(opts, AnyConnection::connect).await } @@ -116,19 +120,21 @@ async fn retry_connect_errors<'a, F, Fut, T>( mut connect: F, ) -> anyhow::Result where - F: FnMut(&'a str) -> Fut, + F: FnMut(&str) -> Fut, Fut: Future> + 'a, { sqlx::any::install_default_drivers(); + let opts_clone = opts.clone(); + let db_url = opts.required_db_url()?; backoff::future::retry( backoff::ExponentialBackoffBuilder::new() - .with_max_elapsed_time(Some(Duration::from_secs(opts.connect_timeout))) + .with_max_elapsed_time(Some(Duration::from_secs(opts_clone.connect_timeout))) .build(), || { - connect(db_url).map_err(|e| -> backoff::Error { + connect(&db_url).map_err(|e| -> backoff::Error { match e { sqlx::Error::Io(ref ioe) => match ioe.kind() { io::ErrorKind::ConnectionRefused diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index e939f5bec8..a3a6ed8c39 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -194,7 +194,7 @@ fn short_checksum(checksum: &[u8]) -> String { pub async fn info(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow::Result<()> { let migrator = Migrator::new(Path::new(migration_source)).await?; - let mut conn = crate::connect(&connect_opts).await?; + let mut conn = crate::connect(connect_opts).await?; conn.ensure_migrations_table().await?; @@ -380,7 +380,7 @@ pub async fn revert( } } - let mut conn = crate::connect(&connect_opts).await?; + let mut conn = crate::connect(connect_opts).await?; conn.ensure_migrations_table().await?; diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index be9ca6dac1..007111c74d 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -235,7 +235,7 @@ impl Deref for Source { } /// Argument for the database URL. -#[derive(Args, Debug)] +#[derive(Args, Debug, Clone)] 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)] @@ -246,6 +246,9 @@ pub struct ConnectOpts { #[clap(long, default_value = "10")] pub connect_timeout: u64, + #[clap(long, default_value = ".env")] + pub env_path: Option, + /// Set whether or not to create SQLite databases in Write-Ahead Log (WAL) mode: /// https://www.sqlite.org/wal.html /// @@ -263,6 +266,14 @@ impl ConnectOpts { /// Require a database URL to be provided, otherwise /// return an error. pub fn required_db_url(&self) -> anyhow::Result<&str> { + if self.env_path.is_some() { + dotenvy::from_filename(&self.env_path.as_deref().unwrap()).ok(); + + let url = dotenvy::var("DATABASE_URL").ok(); + + self.database_url = url; + } + self.database_url.as_deref().ok_or_else( || anyhow::anyhow!( "the `--database-url` option the or `DATABASE_URL` environment variable must be provided" diff --git a/sqlx-cli/src/prepare.rs b/sqlx-cli/src/prepare.rs index 18987048f9..db655fbc80 100644 --- a/sqlx-cli/src/prepare.rs +++ b/sqlx-cli/src/prepare.rs @@ -47,7 +47,7 @@ hint: This command only works in the manifest directory of a Cargo package or wo ); let metadata: Metadata = Metadata::from_current_directory(&cargo)?; - let ctx = PrepareCtx { + let mut ctx = PrepareCtx { workspace, cargo, cargo_args, @@ -56,15 +56,15 @@ hint: This command only works in the manifest directory of a Cargo package or wo }; if check { - prepare_check(&ctx).await + prepare_check(&mut ctx).await } else { - prepare(&ctx).await + prepare(&mut ctx).await } } -async fn prepare(ctx: &PrepareCtx) -> anyhow::Result<()> { +async fn prepare(ctx: &mut PrepareCtx) -> anyhow::Result<()> { if ctx.connect_opts.database_url.is_some() { - check_backend(&ctx.connect_opts).await?; + check_backend(&mut ctx.connect_opts).await?; } let prepare_dir = ctx.prepare_dir()?; @@ -90,9 +90,9 @@ async fn prepare(ctx: &PrepareCtx) -> anyhow::Result<()> { Ok(()) } -async fn prepare_check(ctx: &PrepareCtx) -> anyhow::Result<()> { +async fn prepare_check(ctx: &mut PrepareCtx) -> anyhow::Result<()> { if ctx.connect_opts.database_url.is_some() { - check_backend(&ctx.connect_opts).await?; + check_backend(&mut ctx.connect_opts).await?; } // Re-generate and store the queries in a separate directory from both the prepared @@ -348,7 +348,7 @@ fn load_json_file(path: impl AsRef) -> anyhow::Result { Ok(serde_json::from_slice(&file_bytes)?) } -async fn check_backend(opts: &ConnectOpts) -> anyhow::Result<()> { +async fn check_backend(opts: &mut ConnectOpts) -> anyhow::Result<()> { crate::connect(opts).await?.close().await?; Ok(()) } diff --git a/sqlx.config.json b/sqlx.config.json new file mode 100644 index 0000000000..37a8e30085 --- /dev/null +++ b/sqlx.config.json @@ -0,0 +1,3 @@ +{ + "$schema": "sqlx-cli/sqlx-config.schema.json" +}

AltStyle によって変換されたページ (->オリジナル) /