Skip to content

feat(sqlx.toml): support SQLite extensions in macros and sqlx-cli #3917

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 78 commits into from
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
39e5a5a
feat: create `sqlx.toml` format
abonander Jul 26, 2024
fdce6b8
feat: add support for ignored_chars config to sqlx_core::migrate
abonander Sep 9, 2024
44db470
chore: test ignored_chars with `U+FEFF` (ZWNBSP/BOM)
abonander Sep 9, 2024
62c0c6f
refactor: make `Config` always compiled
abonander Sep 18, 2024
d7b6cd2
refactor: add origin information to `Column`
abonander Sep 18, 2024
c0ed389
feat(macros): implement `type_override` and `column_override` from `s…
abonander Sep 19, 2024
ff3b532
refactor(sqlx.toml): make all keys kebab-case, create `macros.preferr…
abonander Sep 20, 2024
0791e06
feat: make macros aware of `macros.preferred-crates`
abonander Sep 20, 2024
6b0df6d
feat: make `sqlx-cli` aware of `database-url-var`
abonander Sep 20, 2024
c377de7
feat: teach macros about `migrate.table-name`, `migrations-dir`
abonander Sep 23, 2024
d41aa07
feat: teach macros about `migrate.ignored-chars`
abonander Sep 23, 2024
57c75b4
feat: teach `sqlx-cli` about `migrate.defaults`
abonander Oct 5, 2024
b4efb9e
feat: teach `sqlx-cli` about `migrate.migrations-dir`
abonander Jan 15, 2025
528a7d4
feat: teach `sqlx-cli` about `migrate.table-name`
abonander Jan 22, 2025
2ec3698
feat: introduce `migrate.create-schemas`
abonander Jan 22, 2025
a8d9ac2
WIP feat: create multi-tenant database example
abonander Jan 26, 2025
f415b08
SQLite extension loading via sqlx.toml for CLI and query macros
djarb Jan 29, 2025
2231769
fix: allow start_database to function when the SQLite database file d…
djarb Jan 31, 2025
6e55a88
Added example demonstrating migration and compile-time checking with …
djarb Feb 3, 2025
8126ae1
remove accidentally included db file
djarb Feb 3, 2025
d36b9f9
Update sqlx-core/src/config/common.rs
djarb May 1, 2025
15c837e
feat: create `sqlx.toml` format
abonander Jul 26, 2024
f5ed721
feat: add support for ignored_chars config to sqlx_core::migrate
abonander Sep 9, 2024
0a7dfea
chore: test ignored_chars with `U+FEFF` (ZWNBSP/BOM)
abonander Sep 9, 2024
2a35d6a
refactor: make `Config` always compiled
abonander Sep 18, 2024
2bed2ee
refactor: add origin information to `Column`
abonander Sep 18, 2024
d687017
feat(macros): implement `type_override` and `column_override` from `s…
abonander Sep 19, 2024
3a22958
refactor(sqlx.toml): make all keys kebab-case, create `macros.preferr…
abonander Sep 20, 2024
a6947b2
feat: make macros aware of `macros.preferred-crates`
abonander Sep 20, 2024
ff5374e
feat: make `sqlx-cli` aware of `database-url-var`
abonander Sep 20, 2024
f069470
feat: teach macros about `migrate.table-name`, `migrations-dir`
abonander Sep 23, 2024
77db4af
feat: teach macros about `migrate.ignored-chars`
abonander Sep 23, 2024
199429d
feat: teach `sqlx-cli` about `migrate.defaults`
abonander Oct 5, 2024
217742a
feat: teach `sqlx-cli` about `migrate.migrations-dir`
abonander Jan 15, 2025
c6fea5b
feat: teach `sqlx-cli` about `migrate.table-name`
abonander Jan 22, 2025
b43a957
feat: introduce `migrate.create-schemas`
abonander Jan 22, 2025
396b23a
fix(postgres): don't fetch `ColumnOrigin` for transparently-prepared …
abonander Feb 2, 2025
59cd288
feat: progress on axum-multi-tenant example
abonander Feb 2, 2025
08b2364
feat(config): better errors for mislabeled fields
abonander Feb 21, 2025
bda547c
WIP feat: filling out axum-multi-tenant example
abonander Feb 26, 2025
abff167
feat: multi-tenant example
abonander Feb 28, 2025
85b4507
chore(ci): test multi-tenant example
abonander Feb 28, 2025
2b1a271
fixup after merge
abonander Feb 28, 2025
d998be1
fix: CI, README for `multi-tenant`
abonander Feb 28, 2025
7ab599c
fix: clippy warnings
abonander Feb 28, 2025
0694026
fix: multi-tenant README
abonander Feb 28, 2025
ec20110
fix: sequential versioning inference for migrations
abonander Feb 28, 2025
1893336
fix: migration versioning with explicit overrides
abonander Feb 28, 2025
52103a0
fix: only warn on ambiguous crates if the invocation relies on it
abonander Mar 30, 2025
0342c6f
fix: remove unused imports
abonander Mar 30, 2025
8f1a8b0
fix: `sqlx mig add` behavior and tests
abonander Mar 31, 2025
41df655
fix: restore original type-checking order
abonander Mar 31, 2025
57b711f
fix: deprecation warning in `tests/postgres/macros.rs`
abonander Mar 31, 2025
0d87749
feat: create postgres/multi-database example
abonander Apr 11, 2025
19d1e4a
fix: examples/postgres/multi-database
abonander Apr 13, 2025
9b60b10
fix: cargo fmt
abonander Apr 13, 2025
d903aef
chore: add tests for config `migrate.defaults`
abonander May 3, 2025
502d2b3
fix: sqlx-cli/tests/add.rs
abonander May 3, 2025
21bd242
feat(cli): add `--config` override to all relevant commands
abonander May 3, 2025
635670f
chore: run `sqlx mig add` test with `RUST_BACKTRACE=1`
abonander May 3, 2025
12b823e
fix: properly canonicalize config path for `sqlx mig add` test
abonander May 3, 2025
b4cc0d3
fix: get `sqlx mig add` test passing
abonander May 3, 2025
2d007c0
fix(cli): test `migrate.ignored-chars`, fix bugs
abonander May 16, 2025
5453589
feat: create `macros.preferred-crates` example
abonander Jun 2, 2025
929939a
fix(examples): use workspace `sqlx`
abonander Jun 2, 2025
aa43ced
fix: examples
abonander Jun 2, 2025
35b6a79
fix: run `cargo fmt`
abonander Jun 2, 2025
f57ee86
fix: more example fixes
abonander Jun 2, 2025
daca7a0
fix(ci): preferred-crates setup
abonander Jun 2, 2025
756ee0c
fix: axum-multi-tenant example locked to specific sqlx version
djarb Jun 2, 2025
1956256
import anyhow::Context trait in sqlx-cli/src/lib.rs since it was bein…
djarb Jun 2, 2025
94c93d8
rebased on upstream/main
djarb Jul 1, 2025
89c63ae
make cargo fmt happy
djarb Jul 1, 2025
4bf945a
make clippy happy
djarb Jul 1, 2025
6d4db03
make clippy happier still
djarb Jul 1, 2025
c04c328
fix: improved error reporting, added parsing test, removed sqlx-toml …
djarb Jul 2, 2025
fe9916f
switched to kebab-case for the config key
djarb Jul 5, 2025
55567c3
switched to kebab-case for the config key
djarb Jul 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ members = [
"examples/postgres/todos",
"examples/postgres/transaction",
"examples/sqlite/todos",
"examples/sqlite/extension",
]

[workspace.package]
Expand Down Expand Up @@ -65,7 +66,7 @@ macros = ["derive", "sqlx-macros/macros"]
migrate = ["sqlx-core/migrate", "sqlx-macros?/migrate", "sqlx-mysql?/migrate", "sqlx-postgres?/migrate", "sqlx-sqlite?/migrate"]

# Enable parsing of `sqlx.toml` for configuring macros and migrations.
sqlx-toml = ["sqlx-core/sqlx-toml", "sqlx-macros?/sqlx-toml"]
sqlx-toml = ["sqlx-core/sqlx-toml", "sqlx-macros?/sqlx-toml", "sqlx-sqlite?/sqlx-toml"]

# intended mainly for CI and docs
all-databases = ["mysql", "sqlite", "postgres", "any"]
Expand Down
17 changes: 17 additions & 0 deletions examples/sqlite/extension/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "sqlx-example-sqlite-extension"
version = "0.1.0"
license.workspace = true
edition.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
authors.workspace = true

[dependencies]
sqlx = { path = "../../../", features = [ "sqlite", "runtime-tokio", "tls-native-tls", "sqlx-toml"] }
tokio = { version = "1.20.0", features = ["rt", "macros"]}
anyhow = "1.0"

[lints]
workspace = true
9 changes: 9 additions & 0 deletions examples/sqlite/extension/download-extension.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

# This grabs a pre-compiled version of the extension used in this
# example, and stores it in a temporary directory. That's a bit
# unusual. Normally, any extensions you need will be installed into a
# directory on the library search path, either by using the system
# package manager or by compiling and installing it yourself.

mkdir /tmp/sqlite3-lib && wget -O /tmp/sqlite3-lib/ipaddr.so https://github.com/nalgeon/sqlean/releases/download/0.15.2/ipaddr.so
25 changes: 25 additions & 0 deletions examples/sqlite/extension/migrations/20250203094951_addresses.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
create table addresses (address text, family integer);

-- The `ipfamily` function is provided by the
-- [ipaddr](https://github.com/nalgeon/sqlean/blob/main/docs/ipaddr.md)
-- sqlite extension, and so this migration can not run if that
-- extension is not loaded.
insert into addresses (address, family) values
('fd04:3d29:9f41::1', ipfamily('fd04:3d29:9f41::1')),
('10.0.0.1', ipfamily('10.0.0.1')),
('10.0.0.2', ipfamily('10.0.0.2')),
('fd04:3d29:9f41::2', ipfamily('fd04:3d29:9f41::2')),
('fd04:3d29:9f41::3', ipfamily('fd04:3d29:9f41::3')),
('10.0.0.3', ipfamily('10.0.0.3')),
('fd04:3d29:9f41::4', ipfamily('fd04:3d29:9f41::4')),
('fd04:3d29:9f41::5', ipfamily('fd04:3d29:9f41::5')),
('fd04:3d29:9f41::6', ipfamily('fd04:3d29:9f41::6')),
('10.0.0.4', ipfamily('10.0.0.4')),
('10.0.0.5', ipfamily('10.0.0.5')),
('10.0.0.6', ipfamily('10.0.0.6')),
('10.0.0.7', ipfamily('10.0.0.7')),
('fd04:3d29:9f41::7', ipfamily('fd04:3d29:9f41::7')),
('fd04:3d29:9f41::8', ipfamily('fd04:3d29:9f41::8')),
('10.0.0.8', ipfamily('10.0.0.8')),
('fd04:3d29:9f41::9', ipfamily('fd04:3d29:9f41::9')),
('10.0.0.9', ipfamily('10.0.0.9'));
12 changes: 12 additions & 0 deletions examples/sqlite/extension/sqlx.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[common.drivers.sqlite]
# Including the full path to the extension is somewhat unusual,
# because normally an extension will be installed in a standard
# directory which is part of the library search path. If that were the
# case here, the load-extensions value could just be `["ipaddr"]`
#
# When the extension file is installed in a non-standard location, as
# in this example, there are two options:
# * Provide the full path the the extension, as seen below.
# * Add the non-standard location to the library search path, which on
# Linux means adding it to the LD_LIBRARY_PATH environment variable.
load-extensions = ["/tmp/sqlite3-lib/ipaddr"]
47 changes: 47 additions & 0 deletions examples/sqlite/extension/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use std::str::FromStr;

use sqlx::{
query,
sqlite::{SqliteConnectOptions, SqlitePool},
};

#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let opts = SqliteConnectOptions::from_str(&std::env::var("DATABASE_URL")?)?
// The sqlx.toml file controls loading extensions for the CLI
// and for the query checking macros, *not* for the
// application while it's running. Thus, if we want the
// extension to be available during program execution, we need
// to load it.
//
// Note that while in this case the extension path is the same
// when checking the program (sqlx.toml) and when running it
// (here), this is not required. The runtime environment can
// be entirely different from the development one.
//
// The extension can be described with a full path, as seen
// here, but in many cases that will not be necessary. As long
// as the extension is installed in a directory on the library
// search path, it is sufficient to just provide the extension
// name, like "ipaddr"
.extension("/tmp/sqlite3-lib/ipaddr");

let db = SqlitePool::connect_with(opts).await?;

// We're not running the migrations here, for the sake of brevity
// and to confirm that the needed extension was loaded during the
// CLI migrate operation. It would not be unusual to run the
// migrations here as well, though, using the database connection
// we just configured.

query!(
"insert into addresses (address, family) values (?1, ipfamily(?1))",
"10.0.0.10"
)
.execute(&db)
.await?;

println!("Query which requires the extension was successfully executed.");

Ok(())
}
1 change: 1 addition & 0 deletions examples/x.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ def project(name, database=None, driver=None):
project("mysql/todos", driver="mysql_8", database="todos")
project("postgres/todos", driver="postgres_12", database="todos")
project("sqlite/todos", driver="sqlite", database="todos.db")
project("sqlite/extension", driver="sqlite", database="extension.db")
25 changes: 23 additions & 2 deletions sqlx-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::time::Duration;

use futures_util::TryFutureExt;

use sqlx::{AnyConnection, Connection};
use sqlx::AnyConnection;
use tokio::{select, signal};

use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand};
Expand Down Expand Up @@ -189,7 +189,28 @@ async fn do_run(opt: Opt) -> anyhow::Result<()> {

/// 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, AnyConnection::connect).await
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
}
});
Comment on lines +192 to +209
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got a better idea of how to do this but it's a nontrivial change so I'm going to go ahead and merge. Just know that this might look a bit different in the final release.


async move { AnyConnection::connect_with_config(url, config.clone()).await }
})
.await
}

/// Attempt an operation that may return errors like `ConnectionRefused`,
Expand Down
18 changes: 18 additions & 0 deletions sqlx-core/src/any/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ impl AnyConnection {
})
}

/// UNSTABLE: for use with `sqlx-cli`
///
/// Connect to the database, and instruct the nested driver to
/// read options from the sqlx.toml file as appropriate.
#[cfg(feature = "sqlx-toml")]
#[doc(hidden)]
pub fn connect_with_config(
url: &str,
path: Option<std::path::PathBuf>,
) -> BoxFuture<'static, Result<Self, Error>>
where
Self: Sized,
{
let options: Result<AnyConnectOptions, Error> = url.parse();

Box::pin(async move { Self::connect_with(&options?.with_config_file(path)).await })
}

pub(crate) fn connect_with_db<DB: Database>(
options: &AnyConnectOptions,
) -> BoxFuture<'_, crate::Result<Self>>
Expand Down
15 changes: 15 additions & 0 deletions sqlx-core/src/any/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use url::Url;
pub struct AnyConnectOptions {
pub database_url: Url,
pub log_settings: LogSettings,
pub enable_config: Option<std::path::PathBuf>,
}
impl FromStr for AnyConnectOptions {
type Err = Error;
Expand All @@ -29,6 +30,7 @@ impl FromStr for AnyConnectOptions {
.parse::<Url>()
.map_err(|e| Error::Configuration(e.into()))?,
log_settings: LogSettings::default(),
enable_config: None,
})
}
}
Expand All @@ -40,6 +42,7 @@ impl ConnectOptions for AnyConnectOptions {
Ok(AnyConnectOptions {
database_url: url.clone(),
log_settings: LogSettings::default(),
enable_config: None,
})
}

Expand All @@ -63,3 +66,15 @@ impl ConnectOptions for AnyConnectOptions {
self
}
}

impl AnyConnectOptions {
/// UNSTABLE: for use with `sqlx-cli`
///
/// Allow nested drivers to extract configuration information from
/// the sqlx.toml file.
#[doc(hidden)]
pub fn with_config_file(mut self, path: Option<impl Into<std::path::PathBuf>>) -> Self {
self.enable_config = path.map(|p| p.into());
self
}
}
39 changes: 39 additions & 0 deletions sqlx-core/src/config/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,49 @@ pub struct Config {
/// The query macros used in `foo` will use `FOO_DATABASE_URL`,
/// and the ones used in `bar` will use `BAR_DATABASE_URL`.
pub database_url_var: Option<String>,

/// Settings for specific database drivers.
///
/// These settings apply when checking queries, or when applying
/// migrations via `sqlx-cli`. These settings *do not* apply when
/// applying migrations via the macro, as that uses the run-time
/// database connection configured by the application.
pub drivers: Drivers,
}

impl Config {
pub fn database_url_var(&self) -> &str {
self.database_url_var.as_deref().unwrap_or("DATABASE_URL")
}
}

/// Configuration for specific database drivers.
#[derive(Debug, Default)]
#[cfg_attr(
feature = "sqlx-toml",
derive(serde::Deserialize),
serde(default, rename_all = "kebab-case", deny_unknown_fields)
)]
pub struct Drivers {
/// Specify options for the SQLite driver.
pub sqlite: SQLite,
}

/// Configuration for the SQLite database driver.
#[derive(Debug, Default)]
#[cfg_attr(
feature = "sqlx-toml",
derive(serde::Deserialize),
serde(default, rename_all = "kebab-case", deny_unknown_fields)
)]
pub struct SQLite {
/// Specify extensions to load.
///
/// # Example: Load the "uuid" and "vsv" extensions
/// `sqlx.toml`:
/// ```toml
/// [common.drivers.sqlite]
/// load-extensions = ["uuid", "vsv"]
/// ```
pub load_extensions: Vec<String>,
}
6 changes: 6 additions & 0 deletions sqlx-core/src/config/reference.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
# If not specified, defaults to `DATABASE_URL`
database-url-var = "FOO_DATABASE_URL"

[common.drivers.sqlite]
# Load extensions into SQLite when running macros or migrations
#
# Defaults to an empty list, which has no effect.
load-extensions = ["uuid", "vsv"]

###############################################################################################

# Configuration for the `query!()` family of macros.
Expand Down
1 change: 1 addition & 0 deletions sqlx-core/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ fn reference_parses_as_config() {

fn assert_common_config(config: &config::common::Config) {
assert_eq!(config.database_url_var.as_deref(), Some("FOO_DATABASE_URL"));
assert_eq!(config.drivers.sqlite.load_extensions[1].as_str(), "vsv");
}

fn assert_macros_config(config: &config::macros::Config) {
Expand Down
4 changes: 4 additions & 0 deletions sqlx-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ pub struct UnexpectedNullError;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// Error occurred while reading configuration file
#[error("error reading configuration file: {0}")]
ConfigFile(#[source] crate::config::ConfigError),

/// Error occurred while parsing a connection string.
#[error("error with configuration: {0}")]
Configuration(#[source] BoxDynError),
Expand Down
2 changes: 1 addition & 1 deletion sqlx-macros-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ derive = []
macros = []
migrate = ["sqlx-core/migrate"]

sqlx-toml = ["sqlx-core/sqlx-toml"]
sqlx-toml = ["sqlx-core/sqlx-toml", "sqlx-sqlite?/sqlx-toml"]

# database
mysql = ["sqlx-mysql"]
Expand Down
2 changes: 2 additions & 0 deletions sqlx-sqlite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ preupdate-hook = ["libsqlite3-sys/preupdate_hook"]
bundled = ["libsqlite3-sys/bundled"]
unbundled = ["libsqlite3-sys/buildtime_bindgen"]

sqlx-toml = ["sqlx-core/sqlx-toml"]

# Note: currently unused, only to satisfy "unexpected `cfg` condition" lint
bigdecimal = []
rust_decimal = []
Expand Down
17 changes: 17 additions & 0 deletions sqlx-sqlite/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,23 @@ impl<'a> TryFrom<&'a AnyConnectOptions> for SqliteConnectOptions {
fn try_from(opts: &'a AnyConnectOptions) -> Result<Self, Self::Error> {
let mut opts_out = SqliteConnectOptions::from_url(&opts.database_url)?;
opts_out.log_settings = opts.log_settings.clone();

if let Some(ref path) = opts.enable_config {
if path.exists() {
let config = match sqlx_core::config::Config::try_from_path(path.to_path_buf()) {
Ok(cfg) => cfg,
Err(sqlx_core::config::ConfigError::NotFound { path: _ }) => {
return Ok(opts_out)
}
Err(err) => return Err(Self::Error::ConfigFile(err)),
};

for extension in config.common.drivers.sqlite.load_extensions.iter() {
opts_out = opts_out.extension(extension.to_owned());
}
}
}

Ok(opts_out)
}
}
Expand Down
Loading
Loading