sqlx/sqlx-cli/src/lib.rs
djarb f7ef1ed1e9
feat(sqlx.toml): support SQLite extensions in macros and sqlx-cli (#3917)
* feat: create `sqlx.toml` format

* feat: add support for ignored_chars config to sqlx_core::migrate

* chore: test ignored_chars with `U+FEFF` (ZWNBSP/BOM)

https://en.wikipedia.org/wiki/Byte_order_mark

* refactor: make `Config` always compiled

simplifies usage while still making parsing optional for less generated code

* refactor: add origin information to `Column`

* feat(macros): implement `type_override` and `column_override` from `sqlx.toml`

* refactor(sqlx.toml): make all keys kebab-case, create `macros.preferred-crates`

* feat: make macros aware of `macros.preferred-crates`

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

* feat: teach macros about `migrate.table-name`, `migrations-dir`

* feat: teach macros about `migrate.ignored-chars`

* feat: teach `sqlx-cli` about `migrate.defaults`

* feat: teach `sqlx-cli` about `migrate.migrations-dir`

* feat: teach `sqlx-cli` about `migrate.table-name`

* feat: introduce `migrate.create-schemas`

* WIP feat: create multi-tenant database example

* SQLite extension loading via sqlx.toml for CLI and query macros

* fix: allow start_database to function when the SQLite database file does not already exist

* Added example demonstrating migration and compile-time checking with SQLite extensions

* remove accidentally included db file

* Update sqlx-core/src/config/common.rs

Doc formatting tweak

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>

* feat: create `sqlx.toml` format

* feat: add support for ignored_chars config to sqlx_core::migrate

* chore: test ignored_chars with `U+FEFF` (ZWNBSP/BOM)

https://en.wikipedia.org/wiki/Byte_order_mark

* refactor: make `Config` always compiled

simplifies usage while still making parsing optional for less generated code

* refactor: add origin information to `Column`

* feat(macros): implement `type_override` and `column_override` from `sqlx.toml`

* refactor(sqlx.toml): make all keys kebab-case, create `macros.preferred-crates`

* feat: make macros aware of `macros.preferred-crates`

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

* feat: teach macros about `migrate.table-name`, `migrations-dir`

* feat: teach macros about `migrate.ignored-chars`

* feat: teach `sqlx-cli` about `migrate.defaults`

* feat: teach `sqlx-cli` about `migrate.migrations-dir`

* feat: teach `sqlx-cli` about `migrate.table-name`

* feat: introduce `migrate.create-schemas`

* fix(postgres): don't fetch `ColumnOrigin` for transparently-prepared statements

* feat: progress on axum-multi-tenant example

* feat(config): better errors for mislabeled fields

* WIP feat: filling out axum-multi-tenant example

* feat: multi-tenant example

No longer Axum-based because filling out the request routes would have distracted from the purpose of the example.

* chore(ci): test multi-tenant example

* fixup after merge

* fix: CI, README for `multi-tenant`

* fix: clippy warnings

* fix: multi-tenant README

* fix: sequential versioning inference for migrations

* fix: migration versioning with explicit overrides

* fix: only warn on ambiguous crates if the invocation relies on it

* fix: remove unused imports

* fix: `sqlx mig add` behavior and tests

* fix: restore original type-checking order

* fix: deprecation warning in `tests/postgres/macros.rs`

* feat: create postgres/multi-database example

* fix: examples/postgres/multi-database

* fix: cargo fmt

* chore: add tests for config `migrate.defaults`

* fix: sqlx-cli/tests/add.rs

* feat(cli): add `--config` override to all relevant commands

* chore: run `sqlx mig add` test with `RUST_BACKTRACE=1`

* fix: properly canonicalize config path for `sqlx mig add` test

* fix: get `sqlx mig add` test passing

* fix(cli): test `migrate.ignored-chars`, fix bugs

* feat: create `macros.preferred-crates` example

* fix(examples): use workspace `sqlx`

* fix: examples

* fix: run `cargo fmt`

* fix: more example fixes

* fix(ci): preferred-crates setup

* fix: axum-multi-tenant example locked to specific sqlx version

* import anyhow::Context trait in sqlx-cli/src/lib.rs since it was being used and causing a compile error

* rebased on upstream/main

* make cargo fmt happy

* make clippy happy

* make clippy happier still

* fix: improved error reporting, added parsing test, removed sqlx-toml flag use

* switched to kebab-case for the config key

* switched to kebab-case for the config key

---------

Co-authored-by: Austin Bonander <austin.bonander@gmail.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2025-07-08 01:20:46 -07:00

253 lines
7.6 KiB
Rust

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};
mod database;
mod metadata;
// mod migration;
// mod migrator;
#[cfg(feature = "completions")]
mod completions;
mod migrate;
mod opt;
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;
}
dotenvy::dotenv().ok();
}
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(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(opts: &ConnectOpts) -> anyhow::Result<AnyConnection> {
retry_connect_errors(opts, move |url| {
// This only handles the default case. For good support of
// the new command line options, we need to work out some
// way to make the appropriate ConfigOpt available here. I
// suspect that that infrastructure would be useful for
// other things in the future, as well, but it also seems
// like an extensive and intrusive change.
//
// On the other hand, the compile-time checking macros
// can't be configured to use a different config file at
// all, so I believe this is okay for the time being.
let config = Some(std::path::PathBuf::from("sqlx.toml")).and_then(|p| {
if p.exists() {
Some(p)
} else {
None
}
});
async move { AnyConnection::connect_with_config(url, config.clone()).await }
})
.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<T>
where
F: FnMut(&'a str) -> Fut,
Fut: Future<Output = sqlx::Result<T>> + '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<anyhow::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
}