From a4e1551b69f98ee732a2a1dd5a395b7f21b7b980 Mon Sep 17 00:00:00 2001 From: Niclas Klugmann Date: Thu, 14 Aug 2025 10:47:33 +0200 Subject: [PATCH 01/16] fix documentation for rustls native root certificates --- sqlx-postgres/src/options/doc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/options/doc.md b/sqlx-postgres/src/options/doc.md index 15c2459c81..33dd63b7a8 100644 --- a/sqlx-postgres/src/options/doc.md +++ b/sqlx-postgres/src/options/doc.md @@ -86,7 +86,7 @@ See `default_host()` in the same source file as this method for the current beha If `sslrootcert` is not set, the default root certificates used depends on Cargo features: * If `tls-native-tls` is enabled, the system root certificates are used. -* If `tls-rustls-native-roots` is enabled, the system root certificates are used. +* If `tls-rustls-ring-native-roots` is enabled, the system root certificates are used. * Otherwise, TLS roots are populated using the [`webpki-roots`] crate. ## Environment Variables From 2bd3a502a9ccf54cb2c3684a15f2ff47a775b614 Mon Sep 17 00:00:00 2001 From: Niclas Klugmann Date: Tue, 16 Sep 2025 13:08:46 +0200 Subject: [PATCH 02/16] postgres kerberos compatibility --- Cargo.lock | 181 +++++++++++++++++++- sqlx-core/src/error.rs | 3 + sqlx-postgres/Cargo.toml | 1 + sqlx-postgres/src/connection/establish.rs | 5 + sqlx-postgres/src/connection/gssapi.rs | 45 +++++ sqlx-postgres/src/connection/mod.rs | 1 + sqlx-postgres/src/message/authentication.rs | 13 ++ sqlx-postgres/src/message/gssapi.rs | 20 +++ sqlx-postgres/src/message/mod.rs | 4 +- sqlx-postgres/src/options/mod.rs | 8 + 10 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 sqlx-postgres/src/connection/gssapi.rs create mode 100644 sqlx-postgres/src/message/gssapi.rs diff --git a/Cargo.lock b/Cargo.lock index 79906bb607..a5f7355fb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -337,7 +337,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "923ded50f602b3007e5e63e3f094c479d9c8a9b42d7f4034e4afe456aa48bfd2" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -482,12 +482,32 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.96", "which", ] +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.7.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.96", +] + [[package]] name = "bit-vec" version = "0.6.3" @@ -991,6 +1011,19 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "cross-krb5" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4ddf7139e64dc916b11d434421031bcc5ba02e521a49a011652a0f68775188" +dependencies = [ + "anyhow", + "bitflags 2.7.0", + "bytes", + "libgssapi", + "windows", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1716,7 +1749,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -2039,6 +2072,28 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libgssapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "834339e86b2561169d45d3b01741967fee3e5716c7d0b6e33cd4e3b34c9558cd" +dependencies = [ + "bitflags 2.7.0", + "bytes", + "lazy_static", + "libgssapi-sys", +] + +[[package]] +name = "libgssapi-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7518e6902e94f92e7c7271232684b60988b4bd813529b4ef9d97aead96956ae8" +dependencies = [ + "bindgen 0.71.1", + "pkg-config", +] + [[package]] name = "libloading" version = "0.8.6" @@ -2072,7 +2127,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "pkg-config", "vcpkg", @@ -2989,6 +3044,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.43" @@ -3814,6 +3875,7 @@ dependencies = [ "byteorder", "chrono", "crc", + "cross-krb5", "dotenvy", "etcetera", "futures-channel", @@ -4832,6 +4894,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -4841,6 +4925,86 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4899,6 +5063,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index c6652aef75..69036f5d37 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -52,6 +52,9 @@ pub enum Error { #[error("error occurred while attempting to establish a TLS connection: {0}")] Tls(#[source] BoxDynError), + #[error("error occured during gssapi negotiation: {0}")] + GssApi(#[from] BoxDynError), + /// Unexpected or invalid data encountered while communicating with the database. /// /// This should indicate there is a programming error in a SQLx driver or there diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index a70fb37d72..e3c22239cb 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -71,6 +71,7 @@ whoami = { version = "1.2.1", default-features = false } serde = { version = "1.0.144", features = ["derive"] } serde_json = { version = "1.0.85", features = ["raw_value"] } +cross-krb5 = { version = "0.4.2" } [dependencies.sqlx-core] workspace = true diff --git a/sqlx-postgres/src/connection/establish.rs b/sqlx-postgres/src/connection/establish.rs index 634b71de4b..cdacf3333e 100644 --- a/sqlx-postgres/src/connection/establish.rs +++ b/sqlx-postgres/src/connection/establish.rs @@ -1,3 +1,4 @@ +use crate::connection::gssapi; use crate::HashMap; use crate::common::StatementCache; @@ -97,6 +98,10 @@ impl PgConnection { .await?; } + Authentication::Gss => { + gssapi::authenticate(&mut stream, options).await?; + } + Authentication::Sasl(body) => { sasl::authenticate(&mut stream, options, body).await?; } diff --git a/sqlx-postgres/src/connection/gssapi.rs b/sqlx-postgres/src/connection/gssapi.rs new file mode 100644 index 0000000000..4f33dfc080 --- /dev/null +++ b/sqlx-postgres/src/connection/gssapi.rs @@ -0,0 +1,45 @@ +use std::borrow::Cow; + +use crate::error::Error; +use cross_krb5::InitiateFlags; + +use crate::{ + connection::PgStream, + message::{Authentication, AuthenticationGss, GssResponse}, + PgConnectOptions, +}; + +pub async fn authenticate(stream: &mut PgStream, options: &PgConnectOptions) -> Result<(), Error> { + let PgConnectOptions { + host, + gssapi_target_principal: gssapi_principal, + .. + } = options; + let principal = gssapi_principal + .as_ref() + .map(Cow::Borrowed) + .unwrap_or(Cow::Owned(format!("postgres/{host}"))); + let (mut ctx, token) = + cross_krb5::ClientCtx::new(InitiateFlags::empty(), None, &principal, None) + .map_err(|e| Error::GssApi(e.into()))?; + let msg = GssResponse { token: &token }; + stream.send(msg).await?; + loop { + let token = match stream.recv_expect().await? { + Authentication::GssContinue(AuthenticationGss { token }) => token, + other => return Err(err_protocol!("expected GssContinue but receiver {other:?}")), + }; + match ctx.step(&token).map_err(|e| Error::GssApi(e.into()))? { + cross_krb5::Step::Finished((_context, last_token)) => { + if let Some(last_token) = last_token { + stream.send(GssResponse { token: &last_token }).await?; + } + return Ok(()); + } + cross_krb5::Step::Continue((pending, token)) => { + ctx = pending; + stream.send(GssResponse { token: &token }).await?; + } + } + } +} diff --git a/sqlx-postgres/src/connection/mod.rs b/sqlx-postgres/src/connection/mod.rs index 4e05cd867b..8ef916467c 100644 --- a/sqlx-postgres/src/connection/mod.rs +++ b/sqlx-postgres/src/connection/mod.rs @@ -26,6 +26,7 @@ pub use self::stream::PgStream; pub(crate) mod describe; mod establish; mod executor; +mod gssapi; mod sasl; mod stream; mod tls; diff --git a/sqlx-postgres/src/message/authentication.rs b/sqlx-postgres/src/message/authentication.rs index 3a3cf7ff6e..6e071762fb 100644 --- a/sqlx-postgres/src/message/authentication.rs +++ b/sqlx-postgres/src/message/authentication.rs @@ -36,6 +36,12 @@ pub enum Authentication { /// again using the 4-byte random salt. Md5Password(AuthenticationMd5Password), + /// The frontend must initiate GSSAPI negotiation + Gss, + + /// GSSAPI token reponse for continuing the security context + GssContinue(AuthenticationGss), + /// The frontend must now initiate a SASL negotiation, /// using one of the SASL mechanisms listed in the message. /// @@ -75,6 +81,8 @@ impl BackendMessage for Authentication { Authentication::Md5Password(AuthenticationMd5Password { salt }) } + 7 => Authentication::Gss, + 8 => Authentication::GssContinue(AuthenticationGss { token: buf }), 10 => Authentication::Sasl(AuthenticationSasl(buf)), 11 => Authentication::SaslContinue(AuthenticationSaslContinue::decode(buf)?), @@ -191,3 +199,8 @@ impl ProtocolDecode<'_> for AuthenticationSaslFinal { Ok(Self { verifier }) } } + +#[derive(Debug)] +pub struct AuthenticationGss { + pub token: Bytes, +} diff --git a/sqlx-postgres/src/message/gssapi.rs b/sqlx-postgres/src/message/gssapi.rs new file mode 100644 index 0000000000..6076c6b928 --- /dev/null +++ b/sqlx-postgres/src/message/gssapi.rs @@ -0,0 +1,20 @@ +use crate::message::{FrontendMessage, FrontendMessageFormat}; + +pub struct GssResponse<'g> { + pub(crate) token: &'g [u8], +} +impl<'g> FrontendMessage for GssResponse<'g> { + const FORMAT: FrontendMessageFormat = FrontendMessageFormat::PasswordPolymorphic; + + fn body_size_hint(&self) -> std::num::Saturating { + let mut size = std::num::Saturating(0); + size += 4; + size += self.token.len(); + size + } + + fn encode_body(&self, buf: &mut Vec) -> Result<(), sqlx_core::Error> { + buf.extend_from_slice(&self.token); + Ok(()) + } +} diff --git a/sqlx-postgres/src/message/mod.rs b/sqlx-postgres/src/message/mod.rs index e62f9bebb3..5aaab1efa8 100644 --- a/sqlx-postgres/src/message/mod.rs +++ b/sqlx-postgres/src/message/mod.rs @@ -14,6 +14,7 @@ mod data_row; mod describe; mod execute; mod flush; +mod gssapi; mod notification; mod parameter_description; mod parameter_status; @@ -30,7 +31,7 @@ mod startup; mod sync; mod terminate; -pub use authentication::{Authentication, AuthenticationSasl}; +pub use authentication::{Authentication, AuthenticationGss, AuthenticationSasl}; pub use backend_key_data::BackendKeyData; pub use bind::Bind; pub use close::Close; @@ -41,6 +42,7 @@ pub use describe::Describe; pub use execute::Execute; #[allow(unused_imports)] pub use flush::Flush; +pub use gssapi::GssResponse; pub use notification::Notification; pub use parameter_description::ParameterDescription; pub use parameter_status::ParameterStatus; diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index efbc43989b..de616e1b4d 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -21,6 +21,7 @@ pub struct PgConnectOptions { pub(crate) username: String, pub(crate) password: Option, pub(crate) database: Option, + pub(crate) gssapi_target_principal: Option, pub(crate) ssl_mode: PgSslMode, pub(crate) ssl_root_cert: Option, pub(crate) ssl_client_cert: Option, @@ -75,6 +76,7 @@ impl PgConnectOptions { username, password: var("PGPASSWORD").ok(), database, + gssapi_target_principal: var("PGPRINCIPAL").ok(), ssl_root_cert: var("PGSSLROOTCERT").ok().map(CertificateInput::from), ssl_client_cert: var("PGSSLCERT").ok().map(CertificateInput::from), // As of writing, the implementation of `From` only looks for @@ -336,6 +338,12 @@ impl PgConnectOptions { self } + /// Sets the targeted principal in case of attempted Kerberos negotiation + /// If left out and Kerberos is challenged, uses 'postgres/' + pub fn gssapi_target_principal(mut self, target_principal: &str) -> Self { + self.gssapi_target_principal = Some(target_principal.to_owned()); + self + } /// Sets the capacity of the connection's statement cache in a number of stored /// distinct statements. Caching is handled using LRU, meaning when the /// amount of queries hits the defined limit, the oldest statement will get From 83e26150726eb827dbf10541cfaaed97db411952 Mon Sep 17 00:00:00 2001 From: Niclas Klugmann Date: Tue, 16 Sep 2025 13:37:33 +0200 Subject: [PATCH 03/16] fix lockfile --- Cargo.lock | 90 ++++++++++++------------------------------------------ 1 file changed, 20 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f402784ab..0ba2a3bd78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -567,7 +567,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.9.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -578,7 +578,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.96", + "syn 2.0.104", ] [[package]] @@ -1085,7 +1085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d4ddf7139e64dc916b11d434421031bcc5ba02e521a49a011652a0f68775188" dependencies = [ "anyhow", - "bitflags 2.7.0", + "bitflags 2.9.1", "bytes", "libgssapi", "windows", @@ -1813,7 +1813,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core", ] [[package]] @@ -2143,7 +2143,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "834339e86b2561169d45d3b01741967fee3e5716c7d0b6e33cd4e3b34c9558cd" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.9.1", "bytes", "lazy_static", "libgssapi-sys", @@ -4925,7 +4925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core", "windows-future", "windows-link", "windows-numerics", @@ -4937,66 +4937,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link", + "windows-core", ] [[package]] @@ -5018,7 +4959,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link", "windows-threading", ] @@ -5031,7 +4972,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.104", ] [[package]] @@ -5042,7 +4983,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.104", ] [[package]] @@ -5057,7 +4998,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link", ] @@ -5162,6 +5103,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" From 064d649abdfd1742e5fdcc20176a6b415b9c25d3 Mon Sep 17 00:00:00 2001 From: David Uebler Date: Tue, 23 Sep 2025 14:23:01 +0000 Subject: [PATCH 04/16] native tls handshake: build TlsConnector in blocking threadpool (#4027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build TlsConnector in blocking threadpool The openssl TlsConnector synchronously loads certificates from files. Loading these files can block for tens of milliseconds. * Update sqlx-core/src/net/tls/tls_native_tls.rs --------- Co-authored-by: David Übler Co-authored-by: Austin Bonander --- sqlx-core/src/net/tls/tls_native_tls.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sqlx-core/src/net/tls/tls_native_tls.rs b/sqlx-core/src/net/tls/tls_native_tls.rs index 1c40b4b01f..3423e48f8c 100644 --- a/sqlx-core/src/net/tls/tls_native_tls.rs +++ b/sqlx-core/src/net/tls/tls_native_tls.rs @@ -4,6 +4,7 @@ use crate::io::ReadBuf; use crate::net::tls::util::StdSocket; use crate::net::tls::TlsConfig; use crate::net::Socket; +use crate::rt; use crate::Error; use native_tls::{HandshakeError, Identity}; @@ -61,7 +62,11 @@ pub async fn handshake( builder.identity(identity); } - let connector = builder.build().map_err(Error::tls)?; + // The openssl TlsConnector synchronously loads certificates from files. + // Loading these files can block for tens of milliseconds. + let connector = rt::spawn_blocking(move || builder.build()) + .await + .map_err(Error::tls)?; let mut mid_handshake = match connector.connect(config.hostname, StdSocket::new(socket)) { Ok(tls_stream) => return Ok(NativeTlsSocket { stream: tls_stream }), From 388c424f486bf20542a8a37d296dbcf86bb6dffd Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Tue, 14 Oct 2025 17:31:12 -0700 Subject: [PATCH 05/16] fix(macros): smarter `.env` loading, caching, and invalidation (#4053) * fix(macros): smarter `.env` loading, caching, and invalidation * feat(mysql): test `.env` loading in CI * feat(postgres): test `.env` loading in CI * feat(macros): allow `DATABASE_URL` to be empty * fix(examples/postgres): make `cargo-sqlx` executable * fix(examples/postgres): `cargo sqlx` invocation * feat(examples/postgres): check offline prepare on more examples * fix(examples/postgres): the name of this step * fix(cli): don't suppress error from `dotenv()` * fix(ci/examples/postgres): don't use heredoc in this step * fix(ci/examples/postgres): multi-tenant * fix(ci/examples/sqlite): test `.env` loading * chore: add CHANGELOG entry --- .github/workflows/examples.yml | 149 ++++++++++++++- CHANGELOG.md | 10 + Cargo.lock | 42 +++-- Cargo.toml | 1 + sqlx-cli/src/lib.rs | 6 +- sqlx-core/Cargo.toml | 11 +- sqlx-core/src/config/mod.rs | 11 +- sqlx-macros-core/Cargo.toml | 3 +- sqlx-macros-core/clippy.toml | 3 + sqlx-macros-core/src/common.rs | 13 +- sqlx-macros-core/src/lib.rs | 29 ++- sqlx-macros-core/src/query/cache.rs | 97 ++++++++++ sqlx-macros-core/src/query/data.rs | 125 +++++++------ sqlx-macros-core/src/query/metadata.rs | 162 +++++++++++++++++ sqlx-macros-core/src/query/mod.rs | 242 +++++-------------------- sqlx-mysql/Cargo.toml | 5 +- sqlx-postgres/Cargo.toml | 5 +- sqlx-sqlite/Cargo.toml | 3 +- 18 files changed, 621 insertions(+), 296 deletions(-) create mode 100644 sqlx-macros-core/clippy.toml create mode 100644 sqlx-macros-core/src/query/cache.rs create mode 100644 sqlx-macros-core/src/query/metadata.rs diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 4b405dfa02..5e90588619 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -26,7 +26,6 @@ jobs: - run: > cargo build -p sqlx-cli - --bin sqlx --release --no-default-features --features mysql,postgres,sqlite,sqlx-toml @@ -34,7 +33,9 @@ jobs: - uses: actions/upload-artifact@v4 with: name: sqlx-cli - path: target/release/sqlx + path: | + target/release/sqlx + target/release/cargo-sqlx mysql: name: MySQL Examples @@ -42,6 +43,10 @@ jobs: needs: sqlx-cli timeout-minutes: 30 + strategy: + matrix: + offline: ['', 'offline'] + services: mysql: image: mysql:latest @@ -60,7 +65,7 @@ jobs: - run: | ls -R /home/runner/.local/bin - chmod +x /home/runner/.local/bin/sqlx + chmod +x /home/runner/.local/bin/sqlx /home/runner/.local/bin/cargo-sqlx echo /home/runner/.local/bin >> $GITHUB_PATH sleep 10 @@ -77,9 +82,32 @@ jobs: DATABASE_URL: mysql://root:password@localhost:3306/todos?ssl-mode=disabled run: sqlx db setup + - name: Todos (Prepare) + if: ${{ matrix.offline }} + working-directory: examples/mysql/todos + env: + DATABASE_URL: mysql://root:password@localhost:3306/todos?ssl-mode=disabled + run: cargo sqlx prepare + + - name: Todos (Check Offline) + if: ${{ matrix.offline }} + run: | + cargo clean -p sqlx-example-mysql-todos + cargo check -p sqlx-example-mysql-todos + + - name: Todos (Prepare from .env) + if: ${{ matrix.offline }} + working-directory: examples/mysql/todos + run: | + echo "DATABASE_URL=mysql://root:password@localhost:3306/todos?ssl-mode=disabled" > .env + cargo clean -p sqlx-example-mysql-todos + cargo sqlx prepare + rm .env + - name: Todos (Run) env: DATABASE_URL: mysql://root:password@localhost:3306/todos?ssl-mode=disabled + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo run -p sqlx-example-mysql-todos postgres: @@ -88,6 +116,10 @@ jobs: needs: sqlx-cli timeout-minutes: 30 + strategy: + matrix: + offline: ['', 'offline'] + services: postgres: image: postgres:latest @@ -106,6 +138,7 @@ jobs: - run: | ls -R /home/runner/.local/bin chmod +x $HOME/.local/bin/sqlx + chmod +x $HOME/.local/bin/cargo-sqlx echo $HOME/.local/bin >> $GITHUB_PATH sleep 10 @@ -120,14 +153,32 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/axum-social run: sqlx db setup - - name: Axum Social with Tests (Check) + # Test `cargo sqlx prepare` setting `DATABASE_URL` both directly and in `.env` + # This doesn't need to be done for every single example here, but should at least cover potential problem cases. + - name: Axum Social with Tests (Prepare) + if: ${{ matrix.offline }} env: DATABASE_URL: postgres://postgres:password@localhost:5432/axum-social - run: cargo check -p sqlx-example-postgres-axum-social + run: cargo sqlx prepare -- -p sqlx-example-postgres-axum-social + + - name: Axum Social with Tests (Check Offline) + if: ${{ matrix.offline }} + run: | + cargo clean -p sqlx-example-postgres-axum-social + cargo check -p sqlx-example-postgres-axum-social + + - name: Axum Social with Tests (Prepare from .env) + if: ${{ matrix.offline }} + run: | + echo "DATABASE_URL=postgres://postgres:password@localhost:5432/axum-social" > .env + cargo clean -p sqlx-example-postgres-axum-social + cargo sqlx prepare -- -p sqlx-example-postgres-axum-social + rm .env - name: Axum Social with Tests (Test) env: DATABASE_URL: postgres://postgres:password@localhost:5432/axum-social + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo test -p sqlx-example-postgres-axum-social # The Chat example has an interactive TUI which is not trivial to test automatically, @@ -190,11 +241,47 @@ jobs: (cd payments && sqlx db setup) sqlx db setup + - name: Multi-Database (Prepare) + if: ${{ matrix.offline }} + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database + ACCOUNTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-accounts + PAYMENTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-payments + run: | + cargo clean -p sqlx-example-postgres-multi-database-accounts + cargo clean -p sqlx-example-postgres-multi-database-payments + cargo clean -p sqlx-example-postgres-multi-database + # should include -accounts and -payments + cargo sqlx prepare -- -p sqlx-example-postgres-multi-database + + - name: Multi-Database (Check Offline) + if: ${{ matrix.offline }} + run: | + cargo clean -p sqlx-example-postgres-multi-database + cargo check -p sqlx-example-postgres-multi-database + + - name: Multi-Database (Prepare from .env) + if: ${{ matrix.offline }} + run: | + # Tried to get this to work with heredocs but had trouble writing tabs in YAML + echo 'DATABASE_URL=postgres://postgres:password@localhost:5432/multi-database' >.env + # Important: append, don't truncate + echo 'ACCOUNTS_DATABASE_URL=postgres://postgres:password@localhost:5432/multi-database-accounts' >> .env + echo 'PAYMENTS_DATABASE_URL=postgres://postgres:password@localhost:5432/multi-database-payments' >> .env + + cargo clean -p sqlx-example-postgres-multi-database-accounts + cargo clean -p sqlx-example-postgres-multi-database-payments + cargo clean -p sqlx-example-postgres-multi-database + cargo sqlx prepare -- -p sqlx-example-postgres-multi-database + + rm .env + - name: Multi-Database (Run) env: DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database ACCOUNTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-accounts PAYMENTS_DATABASE_URL: postgres://postgres:password@localhost:5432/multi-database-payments + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo run -p sqlx-example-postgres-multi-database - name: Multi-Tenant (Setup) @@ -206,9 +293,38 @@ jobs: (cd payments && sqlx migrate run) sqlx migrate run + - name: Multi-Tenant (Prepare) + if: ${{ matrix.offline }} + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/multi-tenant + run: | + cargo clean -p sqlx-example-postgres-multi-tenant-accounts + cargo clean -p sqlx-example-postgres-multi-tenant-payments + cargo clean -p sqlx-example-postgres-multi-tenant + # should include -accounts and -payments + cargo sqlx prepare -- -p sqlx-example-postgres-multi-tenant + + - name: Multi-Tenant (Check Offline) + if: ${{ matrix.offline }} + run: cargo check -p sqlx-example-postgres-multi-tenant + + - name: Multi-Tenant (Prepare from .env) + if: ${{ matrix.offline }} + run: | + echo "DATABASE_URL=postgres://postgres:password@localhost:5432/multi-tenant" > .env + + cargo clean -p sqlx-example-postgres-multi-tenant-accounts + cargo clean -p sqlx-example-postgres-multi-tenant-payments + cargo clean -p sqlx-example-postgres-multi-tenant + # should include -accounts and -payments + cargo sqlx prepare -- -p sqlx-example-postgres-multi-tenant + + rm .env + - name: Multi-Tenant (Run) env: DATABASE_URL: postgres://postgres:password@localhost:5432/multi-tenant + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo run -p sqlx-example-postgres-multi-tenant - name: Preferred-Crates (Setup) @@ -217,7 +333,7 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/preferred-crates run: sqlx db setup - - name: Multi-Tenant (Run) + - name: Preferred-Crates (Run) env: DATABASE_URL: postgres://postgres:password@localhost:5432/preferred-crates run: cargo run -p sqlx-example-postgres-preferred-crates @@ -275,7 +391,28 @@ jobs: DATABASE_URL: sqlite://todos.sqlite run: sqlx db setup --source=examples/sqlite/todos/migrations + - name: Todos (Prepare) + if: ${{ matrix.offline }} + env: + DATABASE_URL: sqlite://todos.sqlite + run: cargo sqlx prepare -- -p sqlx-example-sqlite-todos + + - name: Todos (Check Offline) + if: ${{ matrix.offline }} + run: | + cargo clean -p sqlx-example-sqlite-todos + cargo check -p sqlx-example-sqlite-todos + + - name: Todos (Prepare from .env) + if: ${{ matrix.offline }} + run: | + echo "DATABASE_URL=sqlite://todos.sqlite" > .env + cargo clean -p sqlx-example-sqlite-todos + cargo sqlx prepare -- -p sqlx-example-sqlite-todos + rm .env + - name: TODOs (Run) env: DATABASE_URL: sqlite://todos.sqlite + SQLX_OFFLINE: ${{ matrix.offline == 'offline' }} run: cargo run -p sqlx-example-sqlite-todos diff --git a/CHANGELOG.md b/CHANGELOG.md index 9036a38d09..98b78d86f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,12 +43,21 @@ This section will be replaced in subsequent alpha releases. See the Git history * Significant changes to the `Migrate` trait * `sqlx::migrate::resolve_blocking()` is now `#[doc(hidden)]` and thus SemVer-exempt. +### Fixed + +* [[#4053]]: fix(macros): smarter `.env` loading, caching, and invalidation [[@abonander]] + * Additional credit to [[@AlexTMjugador]] ([[#4018]]) and [[@Diggsey]] ([[#4039]]) for their proposed solutions + which served as a useful comparison. + [seaorm-2600]: https://github.com/SeaQL/sea-orm/issues/2600 [feature unification]: https://doc.rust-lang.org/cargo/reference/features.html#feature-unification [preferred-crates]: examples/postgres/preferred-crates [#3821]: https://github.com/launchbadge/sqlx/pull/3821 [#3383]: https://github.com/launchbadge/sqlx/pull/3383 +[#4018]: https://github.com/launchbadge/sqlx/pull/4018 +[#4039]: https://github.com/launchbadge/sqlx/pull/4039 +[#4053]: https://github.com/launchbadge/sqlx/pull/4053 ## 0.8.6 - 2025-05-19 @@ -2951,3 +2960,4 @@ Fix docs.rs build by enabling a runtime feature in the docs.rs metadata in `Carg [@dyc3]: https://github.com/dyc3 [@ThomWright]: https://github.com/ThomWright [@duhby]: https://github.com/duhby +[@AlexTMjugador]: https://github.com/AlexTMjugador diff --git a/Cargo.lock b/Cargo.lock index 61d2e7d7b6..d611267ccc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1405,6 +1405,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1642,7 +1648,18 @@ checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -3559,7 +3576,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.4", + "hashbrown 0.16.0", "hashlink", "indexmap 2.10.0", "ipnet", @@ -3578,7 +3595,7 @@ dependencies = [ "smallvec", "smol", "sqlx", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -3613,7 +3630,7 @@ dependencies = [ "serde_json", "serde_with", "sqlx", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tower", @@ -3866,6 +3883,7 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn 2.0.104", + "thiserror 2.0.17", "tokio", "url", ] @@ -3908,7 +3926,7 @@ dependencies = [ "sqlx", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "uuid", @@ -3953,7 +3971,7 @@ dependencies = [ "sqlx", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "uuid", @@ -3980,7 +3998,7 @@ dependencies = [ "serde_urlencoded", "sqlx", "sqlx-core", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "url", @@ -4164,11 +4182,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -4184,9 +4202,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b24b59cfa0..00d5d656c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -190,6 +190,7 @@ uuid = "1.1.2" # Common utility crates cfg-if = "1.0.0" dotenvy = { version = "0.15.0", default-features = false } +thiserror = { version = "2.0.17", default-features = false, features = ["std"] } # Runtimes [workspace.dependencies.async-global-executor] diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index bd3f11137c..7a2e41b16f 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -49,7 +49,11 @@ pub fn maybe_apply_dotenv() { return; } - dotenvy::dotenv().ok(); + if let Err(e) = dotenvy::dotenv() { + if !e.not_found() { + eprintln!("Warning: error loading `.env` file: {e:?}"); + } + } } pub async fn run(opt: Opt) -> anyhow::Result<()> { diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index 58c5b67e05..e22c6d4fc3 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -62,6 +62,7 @@ rustls-native-certs = { version = "0.8.0", optional = true } # Type Integrations bit-vec = { workspace = true, optional = true } bigdecimal = { workspace = true, optional = true } +chrono = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } time = { workspace = true, optional = true } ipnet = { workspace = true, optional = true } @@ -69,15 +70,14 @@ ipnetwork = { workspace = true, optional = true } mac_address = { workspace = true, optional = true } uuid = { workspace = true, optional = true } +# work around bug in async-fs 2.0.0, which references futures-lite dependency wrongly, see https://github.com/launchbadge/sqlx/pull/3791#issuecomment-3043363281 +async-fs = { version = "2.1", optional = true } async-io = { version = "2.4.1", optional = true } async-task = { version = "4.7.1", optional = true } -# work around bug in async-fs 2.0.0, which references futures-lite dependency wrongly, see https://github.com/launchbadge/sqlx/pull/3791#issuecomment-3043363281 -async-fs = { version = "2.1", optional = true } base64 = { version = "0.22.0", default-features = false, features = ["std"] } bytes = "1.1.0" cfg-if = { workspace = true } -chrono = { version = "0.4.34", default-features = false, features = ["clock"], optional = true } crc = { version = "3", optional = true } crossbeam-queue = "0.3.2" either = "1.6.1" @@ -93,7 +93,6 @@ serde_json = { version = "1.0.73", features = ["raw_value"], optional = true } toml = { version = "0.8.16", optional = true } sha2 = { version = "0.10.0", default-features = false, optional = true } #sqlformat = "0.2.0" -thiserror = "2.0.0" tokio-stream = { version = "0.1.8", features = ["fs"], optional = true } tracing = { version = "0.1.37", features = ["log"] } smallvec = "1.7.0" @@ -102,7 +101,9 @@ bstr = { version = "1.0", default-features = false, features = ["std"], optional hashlink = "0.10.0" indexmap = "2.0" event-listener = "5.2.0" -hashbrown = "0.15.0" +hashbrown = "0.16.0" + +thiserror.workspace = true [dev-dependencies] sqlx = { workspace = true, features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] } diff --git a/sqlx-core/src/config/mod.rs b/sqlx-core/src/config/mod.rs index 267a2f1ed1..960d0f434f 100644 --- a/sqlx-core/src/config/mod.rs +++ b/sqlx-core/src/config/mod.rs @@ -158,7 +158,16 @@ impl Config { /// * If the file exists but could not be read or parsed. /// * If the file exists but the `sqlx-toml` feature is disabled. pub fn try_from_crate_or_default() -> Result { - Self::read_from(get_crate_path()?).or_else(|e| { + Self::try_from_path_or_default(get_crate_path()?) + } + + /// Attempt to read `Config` from the path given, or return `Config::default()` if it does not exist. + /// + /// # Errors + /// * If the file exists but could not be read or parsed. + /// * If the file exists but the `sqlx-toml` feature is disabled. + pub fn try_from_path_or_default(path: PathBuf) -> Result { + Self::read_from(path).or_else(|e| { if let ConfigError::NotFound { .. } = e { Ok(Config::default()) } else { diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml index 3bcbede6f4..8702555086 100644 --- a/sqlx-macros-core/Cargo.toml +++ b/sqlx-macros-core/Cargo.toml @@ -26,7 +26,7 @@ _sqlite = [] # SQLx features derive = [] -macros = [] +macros = ["thiserror"] migrate = ["sqlx-core/migrate"] sqlx-toml = ["sqlx-core/sqlx-toml", "sqlx-sqlite?/sqlx-toml"] @@ -66,6 +66,7 @@ tokio = { workspace = true, optional = true } cfg-if = { workspace = true} dotenvy = { workspace = true } +thiserror = { workspace = true, optional = true } hex = { version = "0.4.3" } heck = { version = "0.5" } diff --git a/sqlx-macros-core/clippy.toml b/sqlx-macros-core/clippy.toml new file mode 100644 index 0000000000..f303803661 --- /dev/null +++ b/sqlx-macros-core/clippy.toml @@ -0,0 +1,3 @@ +[[disallowed-methods]] +path = "std::env::var" +reason = "use `crate::env()` instead, which optionally calls `proc_macro::tracked_env::var()`" diff --git a/sqlx-macros-core/src/common.rs b/sqlx-macros-core/src/common.rs index b195a9ffd0..662dc12eff 100644 --- a/sqlx-macros-core/src/common.rs +++ b/sqlx-macros-core/src/common.rs @@ -1,5 +1,4 @@ use proc_macro2::Span; -use std::env; use std::path::{Path, PathBuf}; pub(crate) fn resolve_path(path: impl AsRef, err_span: Span) -> syn::Result { @@ -25,13 +24,9 @@ pub(crate) fn resolve_path(path: impl AsRef, err_span: Span) -> syn::Resul )); } - let base_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| { - syn::Error::new( - err_span, - "CARGO_MANIFEST_DIR is not set; please use Cargo to build", - ) - })?; - let base_dir_path = Path::new(&base_dir); + let mut out_path = crate::manifest_dir().map_err(|e| syn::Error::new(err_span, e))?; + + out_path.push(path); - Ok(base_dir_path.join(path)) + Ok(out_path) } diff --git a/sqlx-macros-core/src/lib.rs b/sqlx-macros-core/src/lib.rs index 9d4204f814..7722bf2e02 100644 --- a/sqlx-macros-core/src/lib.rs +++ b/sqlx-macros-core/src/lib.rs @@ -20,13 +20,14 @@ )] use cfg_if::cfg_if; +use std::path::PathBuf; #[cfg(feature = "macros")] use crate::query::QueryDriver; pub type Error = Box; -pub type Result = std::result::Result; +pub type Result = std::result::Result; mod common; pub mod database; @@ -84,3 +85,29 @@ where } } } + +pub fn env(var: &str) -> Result { + env_opt(var)? + .ok_or_else(|| format!("env var {var:?} must be set to use the query macros").into()) +} + +#[allow(clippy::disallowed_methods)] +pub fn env_opt(var: &str) -> Result> { + use std::env::VarError; + + #[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))] + let res: Result = proc_macro::tracked_env::var(var); + + #[cfg(not(any(sqlx_macros_unstable, procmacro2_semver_exempt)))] + let res: Result = std::env::var(var); + + match res { + Ok(val) => Ok(Some(val)), + Err(VarError::NotPresent) => Ok(None), + Err(VarError::NotUnicode(_)) => Err(format!("env var {var:?} is not valid UTF-8").into()), + } +} + +pub fn manifest_dir() -> Result { + Ok(env("CARGO_MANIFEST_DIR")?.into()) +} diff --git a/sqlx-macros-core/src/query/cache.rs b/sqlx-macros-core/src/query/cache.rs new file mode 100644 index 0000000000..f44366bac2 --- /dev/null +++ b/sqlx-macros-core/src/query/cache.rs @@ -0,0 +1,97 @@ +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::SystemTime; + +/// A cached value derived from one or more files, which is automatically invalidated +/// if the modified-time of any watched file changes. +pub struct MtimeCache { + inner: Mutex>>, +} + +pub struct MtimeCacheBuilder { + file_mtimes: Vec<(PathBuf, Option)>, +} + +struct MtimeCacheInner { + builder: MtimeCacheBuilder, + cached: T, +} + +impl MtimeCache { + pub fn new() -> Self { + MtimeCache { + inner: Mutex::new(None), + } + } + + /// Get the cached value, or (re)initialize it if it does not exist or a file's mtime has changed. + pub fn get_or_try_init( + &self, + init: impl FnOnce(&mut MtimeCacheBuilder) -> Result, + ) -> Result { + let mut inner = self.inner.lock().unwrap_or_else(|e| { + // Reset the cache on-panic. + let mut locked = e.into_inner(); + *locked = None; + locked + }); + + if let Some(inner) = &*inner { + if !inner.builder.any_modified() { + return Ok(inner.cached.clone()); + } + } + + let mut builder = MtimeCacheBuilder::new(); + + let value = init(&mut builder)?; + + *inner = Some(MtimeCacheInner { + builder, + cached: value.clone(), + }); + + Ok(value) + } +} + +impl MtimeCacheBuilder { + fn new() -> Self { + MtimeCacheBuilder { + file_mtimes: Vec::new(), + } + } + + /// Add a file path to watch. + /// + /// The cached value will be automatically invalidated if the modified-time of the file changes, + /// or if the file does not exist but is created sometime after this call. + pub fn add_path(&mut self, path: PathBuf) { + let mtime = get_mtime(&path); + + #[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))] + { + proc_macro::tracked_path::path(&path); + } + + self.file_mtimes.push((path, mtime)); + } + + fn any_modified(&self) -> bool { + for (path, expected_mtime) in &self.file_mtimes { + let actual_mtime = get_mtime(path); + + if expected_mtime != &actual_mtime { + return true; + } + } + + false + } +} + +fn get_mtime(path: &Path) -> Option { + std::fs::metadata(path) + .and_then(|metadata| metadata.modified()) + .ok() +} diff --git a/sqlx-macros-core/src/query/data.rs b/sqlx-macros-core/src/query/data.rs index 470f86f973..912236ae37 100644 --- a/sqlx-macros-core/src/query/data.rs +++ b/sqlx-macros-core/src/query/data.rs @@ -1,17 +1,18 @@ -use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; use std::fs; use std::io::Write as _; use std::marker::PhantomData; use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, Mutex}; +use std::sync::{Arc, LazyLock, Mutex}; use serde::{Serialize, Serializer}; use sqlx_core::database::Database; use sqlx_core::describe::Describe; +use sqlx_core::HashMap; use crate::database::DatabaseExt; +use crate::query::cache::MtimeCache; #[derive(serde::Serialize)] #[serde(bound(serialize = "Describe: serde::Serialize"))] @@ -64,7 +65,7 @@ impl Serialize for SerializeDbName { } } -static OFFLINE_DATA_CACHE: LazyLock>> = +static OFFLINE_DATA_CACHE: LazyLock>>>> = LazyLock::new(Default::default); /// Offline query data @@ -79,47 +80,33 @@ pub struct DynQueryData { impl DynQueryData { /// Loads a query given the path to its "query-.json" file. Subsequent calls for the same /// path are retrieved from an in-memory cache. - pub fn from_data_file(path: impl AsRef, query: &str) -> crate::Result { - let path = path.as_ref(); - - let mut cache = OFFLINE_DATA_CACHE + pub fn from_data_file(path: &Path, query: &str) -> crate::Result { + let cache = OFFLINE_DATA_CACHE .lock() // Just reset the cache on error .unwrap_or_else(|poison_err| { let mut guard = poison_err.into_inner(); *guard = Default::default(); guard - }); - if let Some(cached) = cache.get(path).cloned() { - if query != cached.query { - return Err("hash collision for saved query data".into()); - } - return Ok(cached); - } - - #[cfg(procmacro2_semver_exempt)] - { - let path = path.as_ref().canonicalize()?; - let path = path.to_str().ok_or_else(|| { - format!( - "query-.json path cannot be represented as a string: {:?}", - path - ) - })?; + }) + .entry_ref(path) + .or_insert_with(|| Arc::new(MtimeCache::new())) + .clone(); - proc_macro::tracked_path::path(path); - } + cache.get_or_try_init(|builder| { + builder.add_path(path.into()); - let offline_data_contents = fs::read_to_string(path) - .map_err(|e| format!("failed to read saved query path {}: {}", path.display(), e))?; - let dyn_data: DynQueryData = serde_json::from_str(&offline_data_contents)?; + let offline_data_contents = fs::read_to_string(path).map_err(|e| { + format!("failed to read saved query path {}: {}", path.display(), e) + })?; + let dyn_data: DynQueryData = serde_json::from_str(&offline_data_contents)?; - if query != dyn_data.query { - return Err("hash collision for saved query data".into()); - } + if query != dyn_data.query { + return Err("hash collision for saved query data".into()); + } - let _ = cache.insert(path.to_owned(), dyn_data.clone()); - Ok(dyn_data) + Ok(dyn_data) + }) } } @@ -149,41 +136,71 @@ where } } - pub(super) fn save_in(&self, dir: impl AsRef) -> crate::Result<()> { + pub(super) fn save_in(&self, dir: &Path) -> crate::Result<()> { use std::io::ErrorKind; - let path = dir.as_ref().join(format!("query-{}.json", self.hash)); - match std::fs::remove_file(&path) { - Ok(()) => {} - Err(err) - if matches!( - err.kind(), - ErrorKind::NotFound | ErrorKind::PermissionDenied, - ) => {} - Err(err) => return Err(format!("failed to delete {path:?}: {err:?}").into()), + let path = dir.join(format!("query-{}.json", self.hash)); + + if let Err(err) = fs::remove_file(&path) { + match err.kind() { + ErrorKind::NotFound | ErrorKind::PermissionDenied => (), + ErrorKind::NotADirectory => { + return Err(format!( + "sqlx offline path exists, but is not a directory: {dir:?}" + ) + .into()); + } + _ => return Err(format!("failed to delete {path:?}: {err:?}").into()), + } } - let mut file = match std::fs::OpenOptions::new() + + // Prevent tearing from concurrent invocations possibly trying to write the same file + // by using the existence of the file itself as a mutex. + // + // By deleting the file first and then using `.create_new(true)`, + // we guarantee that this only succeeds if another invocation hasn't concurrently + // re-created the file. + let mut file = match fs::OpenOptions::new() .write(true) .create_new(true) .open(&path) { Ok(file) => file, - // We overlapped with a concurrent invocation and the other one succeeded. - Err(err) if matches!(err.kind(), ErrorKind::AlreadyExists) => return Ok(()), Err(err) => { - return Err(format!("failed to exclusively create {path:?}: {err:?}").into()) + return match err.kind() { + // We overlapped with a concurrent invocation and the other one succeeded. + ErrorKind::AlreadyExists => Ok(()), + ErrorKind::NotFound => { + Err(format!("sqlx offline path does not exist: {dir:?}").into()) + } + ErrorKind::NotADirectory => Err(format!( + "sqlx offline path exists, but is not a directory: {dir:?}" + ) + .into()), + _ => Err(format!("failed to exclusively create {path:?}: {err:?}").into()), + }; } }; - let data = serde_json::to_string_pretty(self) - .map_err(|err| format!("failed to serialize query data: {err:?}"))?; - file.write_all(data.as_bytes()) - .map_err(|err| format!("failed to write query data to file: {err:?}"))?; + // From a quick survey of the files generated by `examples/postgres/axum-social-with-tests`, + // which are generally in the 1-2 KiB range, this seems like a safe bet to avoid + // lots of reallocations without using too much memory. + // + // As of writing, `serde_json::to_vec_pretty()` only allocates 128 bytes up-front. + let mut data = Vec::with_capacity(4096); + + serde_json::to_writer_pretty(&mut data, self).expect("BUG: failed to serialize query data"); // Ensure there is a newline at the end of the JSON file to avoid // accidental modification by IDE and make github diff tool happier. - file.write_all(b"\n") - .map_err(|err| format!("failed to append a newline to file: {err:?}"))?; + data.push(b'\n'); + + // This ideally writes the data in as few syscalls as possible. + file.write_all(&data) + .map_err(|err| format!("failed to write query data to file {path:?}: {err:?}"))?; + + // We don't really need to call `.sync_data()` since it's trivial to re-run the macro + // in the event a power loss results in incomplete flushing of the data to disk. Ok(()) } diff --git a/sqlx-macros-core/src/query/metadata.rs b/sqlx-macros-core/src/query/metadata.rs new file mode 100644 index 0000000000..b3b31c0eb5 --- /dev/null +++ b/sqlx-macros-core/src/query/metadata.rs @@ -0,0 +1,162 @@ +use sqlx_core::config::Config; +use std::hash::{BuildHasherDefault, DefaultHasher}; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use crate::query::cache::{MtimeCache, MtimeCacheBuilder}; +use sqlx_core::HashMap; + +pub struct Metadata { + pub manifest_dir: PathBuf, + pub config: Config, + env: MtimeCache>, + workspace_root: Arc>>, +} + +pub struct MacrosEnv { + pub database_url: Option, + pub offline_dir: Option, + pub offline: Option, +} + +impl Metadata { + pub fn env(&self) -> crate::Result> { + self.env + .get_or_try_init(|builder| load_env(&self.manifest_dir, &self.config, builder)) + } + + pub fn workspace_root(&self) -> PathBuf { + let mut root = self.workspace_root.lock().unwrap(); + if root.is_none() { + use serde::Deserialize; + use std::process::Command; + + let cargo = crate::env("CARGO").unwrap(); + + let output = Command::new(cargo) + .args(["metadata", "--format-version=1", "--no-deps"]) + .current_dir(&self.manifest_dir) + .env_remove("__CARGO_FIX_PLZ") + .output() + .expect("Could not fetch metadata"); + + #[derive(Deserialize)] + struct CargoMetadata { + workspace_root: PathBuf, + } + + let metadata: CargoMetadata = + serde_json::from_slice(&output.stdout).expect("Invalid `cargo metadata` output"); + + *root = Some(metadata.workspace_root); + } + root.clone().unwrap() + } +} + +pub fn try_for_crate() -> crate::Result> { + /// The `MtimeCache` in this type covers the config itself, + /// any changes to which will indirectly invalidate the loaded env vars as well. + #[expect(clippy::type_complexity)] + static METADATA: Mutex< + HashMap>>, BuildHasherDefault>, + > = Mutex::new(HashMap::with_hasher(BuildHasherDefault::new())); + + let manifest_dir = crate::env("CARGO_MANIFEST_DIR")?; + + let cache = METADATA + .lock() + .expect("BUG: we shouldn't panic while holding this lock") + .entry_ref(&manifest_dir) + .or_insert_with(|| Arc::new(MtimeCache::new())) + .clone(); + + cache.get_or_try_init(|builder| { + let manifest_dir = PathBuf::from(manifest_dir); + let config_path = manifest_dir.join("sqlx.toml"); + + builder.add_path(config_path.clone()); + + let config = Config::try_from_path_or_default(config_path)?; + + Ok(Arc::new(Metadata { + manifest_dir, + config, + env: MtimeCache::new(), + workspace_root: Default::default(), + })) + }) +} + +fn load_env( + manifest_dir: &Path, + config: &Config, + builder: &mut MtimeCacheBuilder, +) -> crate::Result> { + #[derive(thiserror::Error, Debug)] + #[error("error reading dotenv file {path:?}")] + struct DotenvError { + path: PathBuf, + #[source] + error: dotenvy::Error, + } + + let mut from_dotenv = MacrosEnv { + database_url: None, + offline_dir: None, + offline: None, + }; + + for dir in manifest_dir.ancestors() { + let path = dir.join(".env"); + + let dotenv = match dotenvy::from_path_iter(&path) { + Ok(iter) => { + builder.add_path(path.clone()); + iter + } + Err(dotenvy::Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => { + builder.add_path(dir.to_path_buf()); + continue; + } + Err(e) => { + builder.add_path(path.clone()); + return Err(DotenvError { path, error: e }.into()); + } + }; + + for res in dotenv { + let (name, val) = res.map_err(|e| DotenvError { + path: path.clone(), + error: e, + })?; + + match &*name { + "SQLX_OFFLINE_DIR" => from_dotenv.offline_dir = Some(val.into()), + "SQLX_OFFLINE" => from_dotenv.offline = Some(is_truthy_bool(&val)), + _ if name == config.common.database_url_var() => { + from_dotenv.database_url = Some(val) + } + _ => continue, + } + } + } + + Ok(Arc::new(MacrosEnv { + // Make set variables take precedent + database_url: crate::env_opt(config.common.database_url_var())? + .or(from_dotenv.database_url), + offline_dir: crate::env_opt("SQLX_OFFLINE_DIR")? + .map(PathBuf::from) + .or(from_dotenv.offline_dir), + offline: crate::env_opt("SQLX_OFFLINE")? + .map(|val| is_truthy_bool(&val)) + .or(from_dotenv.offline), + })) +} + +/// Returns `true` if `val` is `"true"`, +fn is_truthy_bool(val: &str) -> bool { + val.eq_ignore_ascii_case("true") || val == "1" +} diff --git a/sqlx-macros-core/src/query/mod.rs b/sqlx-macros-core/src/query/mod.rs index 060a24b847..84461f22b8 100644 --- a/sqlx-macros-core/src/query/mod.rs +++ b/sqlx-macros-core/src/query/mod.rs @@ -1,7 +1,4 @@ -use std::collections::{hash_map, HashMap}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, LazyLock, Mutex}; -use std::{fs, io}; use proc_macro2::TokenStream; use syn::Type; @@ -14,20 +11,25 @@ use sqlx_core::{column::Column, describe::Describe, type_info::TypeInfo}; use crate::database::DatabaseExt; use crate::query::data::{hash_string, DynQueryData, QueryData}; use crate::query::input::RecordType; +use crate::query::metadata::MacrosEnv; use either::Either; +use metadata::Metadata; use sqlx_core::config::Config; use url::Url; mod args; +mod cache; mod data; mod input; +mod metadata; mod output; #[derive(Copy, Clone)] pub struct QueryDriver { db_name: &'static str, url_schemes: &'static [&'static str], - expand: fn(&Config, QueryMacroInput, QueryDataSource) -> crate::Result, + expand: + fn(&Config, QueryMacroInput, QueryDataSource, Option<&Path>) -> crate::Result, } impl QueryDriver { @@ -68,138 +70,64 @@ impl<'a> QueryDataSource<'a> { } } } - -struct Metadata { - #[allow(unused)] - manifest_dir: PathBuf, - offline: bool, - database_url: Option, - offline_dir: Option, - config: Config, - workspace_root: Arc>>, -} - -impl Metadata { - pub fn workspace_root(&self) -> PathBuf { - let mut root = self.workspace_root.lock().unwrap(); - if root.is_none() { - use serde::Deserialize; - use std::process::Command; - - let cargo = env("CARGO").expect("`CARGO` must be set"); - - let output = Command::new(cargo) - .args(["metadata", "--format-version=1", "--no-deps"]) - .current_dir(&self.manifest_dir) - .env_remove("__CARGO_FIX_PLZ") - .output() - .expect("Could not fetch metadata"); - - #[derive(Deserialize)] - struct CargoMetadata { - workspace_root: PathBuf, - } - - let metadata: CargoMetadata = - serde_json::from_slice(&output.stdout).expect("Invalid `cargo metadata` output"); - - *root = Some(metadata.workspace_root); - } - root.clone().unwrap() - } -} - -static METADATA: LazyLock>> = LazyLock::new(Default::default); - -// If we are in a workspace, lookup `workspace_root` since `CARGO_MANIFEST_DIR` won't -// reflect the workspace dir: https://github.com/rust-lang/cargo/issues/3946 -fn init_metadata(manifest_dir: &String) -> crate::Result { - let manifest_dir: PathBuf = manifest_dir.into(); - - let (database_url, offline, offline_dir) = load_dot_env(&manifest_dir); - - let offline = env("SQLX_OFFLINE") - .ok() - .or(offline) - .map(|s| s.eq_ignore_ascii_case("true") || s == "1") - .unwrap_or(false); - - let offline_dir = env("SQLX_OFFLINE_DIR").ok().or(offline_dir); - - let config = Config::try_from_crate_or_default()?; - - let database_url = env(config.common.database_url_var()).ok().or(database_url); - - Ok(Metadata { - manifest_dir, - offline, - database_url, - offline_dir, - config, - workspace_root: Arc::new(Mutex::new(None)), - }) -} - pub fn expand_input<'a>( input: QueryMacroInput, drivers: impl IntoIterator, ) -> crate::Result { - let manifest_dir = env("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` must be set"); - - let mut metadata_lock = METADATA - .lock() - // Just reset the metadata on error - .unwrap_or_else(|poison_err| { - let mut guard = poison_err.into_inner(); - *guard = Default::default(); - guard - }); + let metadata = metadata::try_for_crate()?; - let metadata = match metadata_lock.entry(manifest_dir) { - hash_map::Entry::Occupied(occupied) => occupied.into_mut(), - hash_map::Entry::Vacant(vacant) => { - let metadata = init_metadata(vacant.key())?; - vacant.insert(metadata) - } - }; + let metadata_env = metadata.env()?; - let data_source = match &metadata { - Metadata { - offline: false, + let data_source = match &*metadata_env { + MacrosEnv { + offline: None | Some(false), database_url: Some(db_url), .. - } => QueryDataSource::live(db_url)?, - Metadata { offline, .. } => { + } + // Allow `DATABASE_URL=''` + if !db_url.is_empty() => QueryDataSource::live(db_url)?, + MacrosEnv { + offline, + offline_dir, + .. + } => { // Try load the cached query metadata file. let filename = format!("query-{}.json", hash_string(&input.sql)); // Check SQLX_OFFLINE_DIR, then local .sqlx, then workspace .sqlx. let dirs = [ - |meta: &Metadata| meta.offline_dir.as_deref().map(PathBuf::from), - |meta: &Metadata| Some(meta.manifest_dir.join(".sqlx")), - |meta: &Metadata| Some(meta.workspace_root().join(".sqlx")), + |_: &Metadata, offline_dir: Option<&Path>| offline_dir.map(PathBuf::from), + |meta: &Metadata, _: Option<&Path>| Some(meta.manifest_dir.join(".sqlx")), + |meta: &Metadata, _: Option<&Path>| Some(meta.workspace_root().join(".sqlx")), ]; + let Some(data_file_path) = dirs .iter() - .filter_map(|path| path(metadata)) + .filter_map(|path| path(&metadata, offline_dir.as_deref())) .map(|path| path.join(&filename)) .find(|path| path.exists()) else { return Err( - if *offline { + if offline.unwrap_or(false) { "`SQLX_OFFLINE=true` but there is no cached data for this query, run `cargo sqlx prepare` to update the query cache or unset `SQLX_OFFLINE`" } else { "set `DATABASE_URL` to use query macros online, or run `cargo sqlx prepare` to update the query cache" }.into() ); }; + QueryDataSource::Cached(DynQueryData::from_data_file(&data_file_path, &input.sql)?) } }; for driver in drivers { if data_source.matches_driver(driver) { - return (driver.expand)(&metadata.config, input, data_source); + return (driver.expand)( + &metadata.config, + input, + data_source, + metadata_env.offline_dir.as_deref(), + ); } } @@ -224,19 +152,21 @@ fn expand_with( config: &Config, input: QueryMacroInput, data_source: QueryDataSource, + offline_dir: Option<&Path>, ) -> crate::Result where Describe: DescribeExt, { - let (query_data, offline): (QueryData, bool) = match data_source { - QueryDataSource::Cached(dyn_data) => (QueryData::from_dyn_data(dyn_data)?, true), + let (query_data, save_dir): (QueryData, Option<&Path>) = match data_source { + // If the build is offline, the cache is our input so it's pointless to also write data for it. + QueryDataSource::Cached(dyn_data) => (QueryData::from_dyn_data(dyn_data)?, None), QueryDataSource::Live { database_url, .. } => { let describe = DB::describe_blocking(&input.sql, database_url, &config.drivers)?; - (QueryData::from_describe(&input.sql, describe), false) + (QueryData::from_describe(&input.sql, describe), offline_dir) } }; - expand_with_data(config, input, query_data, offline) + expand_with_data(config, input, query_data, save_dir) } // marker trait for `Describe` that lets us conditionally require it to be `Serialize + Deserialize` @@ -257,7 +187,7 @@ fn expand_with_data( config: &Config, input: QueryMacroInput, data: QueryData, - offline: bool, + save_dir: Option<&Path>, ) -> crate::Result where Describe: DescribeExt, @@ -380,99 +310,9 @@ where } }; - // Store query metadata only if offline support is enabled but the current build is online. - // If the build is offline, the cache is our input so it's pointless to also write data for it. - if !offline { - // Only save query metadata if SQLX_OFFLINE_DIR is set manually or by `cargo sqlx prepare`. - // Note: in a cargo workspace this path is relative to the root. - if let Ok(dir) = env("SQLX_OFFLINE_DIR") { - let path = PathBuf::from(&dir); - - match fs::metadata(&path) { - Err(e) => { - if e.kind() != io::ErrorKind::NotFound { - // Can't obtain information about .sqlx - return Err(format!("{e}: {dir}").into()); - } - // .sqlx doesn't exist. - return Err(format!("sqlx offline path does not exist: {dir}").into()); - } - Ok(meta) => { - if !meta.is_dir() { - return Err(format!( - "sqlx offline path exists, but is not a directory: {dir}" - ) - .into()); - } - - // .sqlx exists and is a directory, store data. - data.save_in(path)?; - } - } - } + if let Some(save_dir) = save_dir { + data.save_in(save_dir)?; } Ok(ret_tokens) } - -/// Get the value of an environment variable, telling the compiler about it if applicable. -fn env(name: &str) -> Result { - #[cfg(procmacro2_semver_exempt)] - { - proc_macro::tracked_env::var(name) - } - - #[cfg(not(procmacro2_semver_exempt))] - { - std::env::var(name) - } -} - -/// Get `DATABASE_URL`, `SQLX_OFFLINE` and `SQLX_OFFLINE_DIR` from the `.env`. -fn load_dot_env(manifest_dir: &Path) -> (Option, Option, Option) { - let mut env_path = manifest_dir.join(".env"); - - // If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this, - // otherwise fallback to default dotenv file. - #[cfg_attr(not(procmacro2_semver_exempt), allow(unused_variables))] - let env_file = if env_path.exists() { - let res = dotenvy::from_path_iter(&env_path); - match res { - Ok(iter) => Some(iter), - Err(e) => panic!("failed to load environment from {env_path:?}, {e}"), - } - } else { - #[allow(unused_assignments)] - { - env_path = PathBuf::from(".env"); - } - dotenvy::dotenv_iter().ok() - }; - - let mut offline = None; - let mut database_url = None; - let mut offline_dir = None; - - if let Some(env_file) = env_file { - // tell the compiler to watch the `.env` for changes. - #[cfg(procmacro2_semver_exempt)] - if let Some(env_path) = env_path.to_str() { - proc_macro::tracked_path::path(env_path); - } - - for item in env_file { - let Ok((key, value)) = item else { - continue; - }; - - match key.as_str() { - "DATABASE_URL" => database_url = Some(value), - "SQLX_OFFLINE" => offline = Some(value), - "SQLX_OFFLINE_DIR" => offline_dir = Some(value), - _ => {} - }; - } - } - - (database_url, offline, offline_dir) -} diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index 52717c4207..2ed0738a24 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -55,7 +55,6 @@ base64 = { version = "0.22.0", default-features = false, features = ["std"] } bitflags = { version = "2", default-features = false, features = ["serde"] } byteorder = { version = "1.4.3", default-features = false, features = ["std"] } bytes = "1.1.0" -dotenvy = "0.15.5" either = "1.6.1" generic-array = { version = "0.14.4", default-features = false } hex = "0.4.3" @@ -65,10 +64,12 @@ memchr = { version = "2.4.1", default-features = false } percent-encoding = "2.1.0" smallvec = "1.7.0" stringprep = "0.1.2" -thiserror = "2.0.0" tracing = { version = "0.1.37", features = ["log"] } whoami = { version = "1.2.1", default-features = false } +dotenvy.workspace = true +thiserror.workspace = true + serde = { version = "1.0.144", optional = true } [dev-dependencies] diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index a70fb37d72..4abd252357 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -56,7 +56,6 @@ atoi = "2.0" base64 = { version = "0.22.0", default-features = false, features = ["std"] } bitflags = { version = "2", default-features = false } byteorder = { version = "1.4.3", default-features = false, features = ["std"] } -dotenvy = { workspace = true } hex = "0.4.3" home = "0.5.5" itoa = "1.0.1" @@ -65,10 +64,12 @@ memchr = { version = "2.4.1", default-features = false } num-bigint = { version = "0.4.3", optional = true } smallvec = { version = "1.7.0", features = ["serde"] } stringprep = "0.1.2" -thiserror = "2.0.0" tracing = { version = "0.1.37", features = ["log"] } whoami = { version = "1.2.1", default-features = false } +dotenvy.workspace = true +thiserror.workspace = true + serde = { version = "1.0.144", features = ["derive"] } serde_json = { version = "1.0.85", features = ["raw_value"] } diff --git a/sqlx-sqlite/Cargo.toml b/sqlx-sqlite/Cargo.toml index 4508e19ff6..6e04f1c2a6 100644 --- a/sqlx-sqlite/Cargo.toml +++ b/sqlx-sqlite/Cargo.toml @@ -85,7 +85,8 @@ atoi = "2.0" log = "0.4.18" tracing = { version = "0.1.37", features = ["log"] } -thiserror = "2.0.0" + +thiserror.workspace = true serde = { version = "1.0.145", features = ["derive"], optional = true } regex = { version = "1.5.5", optional = true } From 1b2b19fe8e840f8b49dab9c954e7697e94ce00df Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Tue, 14 Oct 2025 18:47:11 -0700 Subject: [PATCH 06/16] chore: update CHANGELOG for 0.9.0-alpha-1 (#4057) --- CHANGELOG.md | 230 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 222 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b78d86f8..8f00d170d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 0.9.0-alpha.1 - 2025-05-19 +## 0.9.0-alpha.1 - 2025-10-14 Accumulated changes since the beginning of the alpha cycle. Effectively a draft CHANGELOG for the 0.9.0 release. @@ -13,12 +13,6 @@ This section will be replaced in subsequent alpha releases. See the Git history ### Breaking -* [[#3821]]: Groundwork for 0.9.0-alpha.1 [[@abonander]] - * Increased MSRV to 1.86 and set rust-version - * Deleted deprecated combination runtime+TLS features (e.g. `runtime-tokio-native-tls`) - * Deleted re-export of unstable `TransactionManager` trait in `sqlx`. - * Not technically a breaking change because it's `#[doc(hidden)]`, - but [it _will_ break SeaORM][seaorm-2600] if not proactively fixed. * [[#3383]]: feat: create `sqlx.toml` format [[@abonander]] * SQLx and `sqlx-cli` now support per-crate configuration files (`sqlx.toml`) * New functionality includes, but is not limited to: @@ -42,9 +36,136 @@ This section will be replaced in subsequent alpha releases. See the Git history * **Breaking changes**: * Significant changes to the `Migrate` trait * `sqlx::migrate::resolve_blocking()` is now `#[doc(hidden)]` and thus SemVer-exempt. +* [[#3486]]: fix(logs): Correct spelling of aquired_after_secs tracing field [[@iamjpotts]] + * Breaking behavior change: implementations parsing `tracing` logs from SQLx will need to update the spelling. +* [[#3495]]: feat(postgres): remove lifetime from `PgAdvisoryLockGuard` [[@bonsairobo]] +* [[#3526]]: Return &mut Self from the migrator set_ methods [[@nipunn1313]] + * Minor breaking change: `Migrator::set_ignore_missing` and `set_locking` now return `&mut Self` instead of `&Self` + which may break code in rare circumstances. +* [[#3541]]: Postgres: force generic plan for better nullability inference. [[@joeydewaal]] + * Breaking change: may alter the output of the `query!()` macros for certain queries in Postgres. +* [[#3613]]: fix: `RawSql` lifetime issues [[@abonander]] + * Breaking change: adds `DB` type parameter to all methods of `RawSql` +* [[#3670]]: Bump ipnetwork to v0.21.1 [[@BeauGieskens]] +* [[#3674]]: Implement `Decode`, `Encode` and `Type` for `Box`, `Arc`, `Cow` and `Rc` [[@joeydewaal]] + * Breaking change: `impl Decode for Cow` now always decodes `Cow::Owned`, lifetime is unlinked + * See this discussion for motivation: https://github.com/launchbadge/sqlx/pull/3674#discussion_r2008611502 +* [[#3723]]: Add SqlStr [[@joeydewaal]] + * Breaking change: all `query*()` functions now take `impl SqlSafeStr` + which is only implemented for `&'static str` and `AssertSqlSafe`. + For all others, wrap in `AssertSqlSafe()`. + * This, along with [[#3960]], finally allows returning owned queries as the type will be `Query<'static, DB>`. + * `SqlSafeStr` trait is deliberately similar to `std::panic::UnwindSafe`, + serving as a speedbump to warn users about naïvely building queries with `format!()` + while allowing a workaround for advanced usage that is easy to spot on code review. +* [[#3800]]: Escape PostgreSQL Options [[@V02460]] + * Breaking behavior change: options passed to `PgConnectOptions::options()` are now automatically escaped. + Manual escaping of options is no longer necessary and may cause incorrect behavior. +* [[#3821]]: Groundwork for 0.9.0-alpha.1 [[@abonander]] + * Increased MSRV to 1.86 and set rust-version + * Deleted deprecated combination runtime+TLS features (e.g. `runtime-tokio-native-tls`) + * Deleted re-export of unstable `TransactionManager` trait in `sqlx`. + * Not technically a breaking change because it's `#[doc(hidden)]`, + but [it _will_ break SeaORM][seaorm-2600] if not proactively fixed. +* [[#3924]]: breaking(mysql): assume all non-binary collations compatible with `str` [[@abonander]] + * Text (or text-like) columns which previously were inferred to be `Vec` will be inferred to be `String` + (this should ultimately fix more code than it breaks). + * `SET NAMES utf8mb4 COLLATE utf8_general_ci` is no longer sent by default; instead, `SET NAMES utf8mb4` is sent to + allow the server to select the appropriate default collation (since this is version- and configuration-dependent). + * `MySqlConnectOptions::charset()` and `::collation()` now imply `::set_names(true)` because they don't do anything otherwise. + * Setting `charset` doesn't change what's sent in the `Protocol::HandshakeResponse41` packet as that normally only + matters for error messages before `SET NAMES` is sent. + The default collation if `set_names = false` is `utf8mb4_general_ci`. + * See [this comment](https://github.com/launchbadge/sqlx/blob/388c424f486bf20542a8a37d296dbcf86bb6dffd/sqlx-mysql/src/collation.rs#L1-L37) for details. + * Incidental breaking change: `RawSql::fetch_optional()` now returns `sqlx::Result>` + instead of `sqlx::Result`. Whoops. +* [[#3928]]: breaking(sqlite): `libsqlite3-sys` versioning, feature flags, safety changes [[@abonander]] + * SemVer policy changes: `libsqlite3-sys` version is now specified using a range. + The maximum of the range may now be increased in any backwards-compatible release. + The minimum of the range may only be increased in major releases. + If you have `libsqlite3-sys` in your dependencies, Cargo should choose a compatible version automatically. + If otherwise unconstrained, Cargo should choose the latest version supported. + * SQLite extension loading (including through the new `sqlx-toml` feature) is now `unsafe`. + * Added new **non-default** features corresponding to conditionally compiled SQLite APIs: + * `sqlite-deserialize` enabling `SqliteConnection::serialize()` and `SqliteConnection::deserialize()` + * `sqlite-load-extension` enabling `SqliteConnectOptions::extension()` and `::extension_with_entrypoint()` + * `sqlite-unlock-notify` enables internal use of `sqlite3_unlock_notify()` + * `SqliteValue` and `SqliteValueRef` changes: + * The [`sqlite3_value*` interface](https://www.sqlite.org/c3ref/value_blob.html) reserves the right to be stateful. + Without protection, any call could theoretically invalidate values previously returned, leading to dangling pointers. + * `SqliteValue` is now `!Sync` and `SqliteValueRef` is `!Send` to prevent data races from concurrent accesses. + * Instead, clone or wrap the `SqliteValue` in `Mutex`, or convert the `SqliteValueRef` to an owned value. + * `SqliteValue` and any derived `SqliteValueRef`s now internally track if that value has been used to decode a + borrowed `&[u8]` or `&str` and errors if it's used to decode any other type. + * This is not expected to affect the vast majority of usages, which should only decode a single type + per `SqliteValue`/`SqliteValueRef`. + * See new docs on `SqliteValue` for details. +* [[#3949]]: Postgres: move `PgLTree::from` to `From>` implementation [[@JerryQ17]] +* [[#3957]]: refactor(sqlite): do not borrow bound values, delete lifetime on `SqliteArguments` [[@iamjpotts]] +* [[#3958]]: refactor(any): Remove lifetime parameter from AnyArguments [[@iamjpotts]] +* [[#3960]]: refactor(core): Remove lifetime parameter from Arguments trait [[@iamjpotts]] +* [[#4008]]: make `#[derive(sqlx::Type)]` automatically generate `impl PgHasArrayType` by default for newtype structs [[@papaj-na-wrotkach]] + * Manual implementations of PgHasArrayType for newtypes will conflict with the generated one. + Delete the manual impl or add `#[sqlx(no_pg_array)]` where conflicts occur. -### Fixed +### Added +* [[#3641]]: feat(Postgres): support nested domain types [[@joeydewaal]] +* [[#3651]]: Add PgBindIter for encoding and use it as the implementation encoding &[T] [[@tylerhawkes]] +* [[#3675]]: feat: implement Encode, Decode, Type for `Arc` and `Arc<[u8]>` (and `Rc` equivalents) [[@joeydewaal]] +* [[#3791]]: Smol+async global executor 1.80 dev [[@martin-kolarik]] + * Adds `runtime-smol` and `runtime-async-global-executor` features to replace usages of the deprecated `async-std` crate. +* [[#3859]]: Add more JsonRawValue encode/decode impls. [[@Dirbaio]] +* [[#3881]]: CLi: made cli-lib modules publicly available for other crates [[@silvestrpredko]] +* [[#3889]]: Compile-time support for external drivers [[@bobozaur]] +* [[#3917]]: feat(sqlx.toml): support SQLite extensions in macros and sqlx-cli [[@djarb]] +* [[#3918]]: Feature: Add exclusion violation error kind [[@barskern]] +* [[#3971]]: Allow single-field named structs to be transparent [[@Xiretza]] +* [[#4015]]: feat(sqlite): `no_tx` migration support [[@AlexTMjugador]] +* [[#4020]]: Add `Migrator::with_migrations()` constructor [[@xb284524239]] + +### Changed +* [[#3525]]: Remove unnecessary boxfutures [[@joeydewaal]] +* [[#3867]]: sqlx-postgres: Bump etcetera to 0.10.0 [[@miniduikboot]] +* [[#3709]]: chore: replace once_cell `OnceCell`/`Lazy` with std `OnceLock`/`LazyLock` [[@paolobarbolini]] +* [[#3890]]: feat: Unify `Debug` implementations across `PgRow`, `MySqlRow` and `SqliteRow` [[@davidcornu]] +* [[#3911]]: chore: upgrade async-io to v2.4.1 [[@zebrapurring]] +* [[#3938]]: Move `QueryLogger` back [[@joeydewaal]] +* [[#3956]]: chore(sqlite): Remove unused test of removed git2 feature [[@iamjpotts]] +* [[#3962]]: Give SQLX_OFFLINE_DIR from environment precedence in macros [[@psionic-k]] +* [[#3968]]: chore(ci): Add timeouts to ci jobs [[@iamjpotts]] +* [[#4002]]: sqlx-postgres(tests): cleanup 2 unit tests. [[@joeydewaal]] +* [[#4022]]: refactor: tweaks after #3791 [[@abonander]] +### Fixed +* [[#3840]]: Fix docs.rs build of sqlx-sqlite [[@gferon]] +* [[#3848]]: fix(macros): don't mutate environment variables [[@joeydewaal]] +* [[#3856]]: fix(macros): slightly improve unsupported type error message [[@dyc3]] +* [[#3857]]: fix(mysql): validate parameter count for prepared statements [[@cvzx]] +* [[#3861]]: Fix NoHostnameTlsVerifier for rustls 0.23.24 and above [[@elichai]] +* [[#3863]]: Use unnamed statement in pg when not persistent [[@ThomWright]] +* [[#3874]]: Further reduce dependency on `futures` and `futures-util` [[@paolobarbolini]] +* [[#3886]]: fix: use Executor::fetch in QueryAs::fetch [[@bobozaur]] +* [[#3910]]: feat(ok): add correct handling of ok packets in MYSQL implementation [[@0xfourzerofour]] +* [[#3914]]: fix: regenerate test certificates [[@abonander]] +* [[#3915]]: fix: spec_error is used by try_from derive [[@saiintbrisson]] +* [[#3919]]: fix[sqlx-postgres]: do a checked_mul to prevent panic'ing [[@nhatcher-frequenz]] +* [[#3923]]: sqlx-mysql: Fix bug in cleanup test db's. [[@joeydewaal]] +* [[#3950]]: chore: Fix warnings for custom postgres_## cfg flags [[@iamjpotts]] +* [[#3952]]: `Pool.close`: close all connections before returning [[@jpmelos]] +* [[#3975]]: fix documentation for rustls native root certificates [[@2ndDerivative]] +* [[#3977]]: refactor(ci): Use separate job for postgres ssl auth tests [[@iamjpotts]] +* [[#3980]]: Correctly `ROLLBACK` transaction when dropped during `BEGIN`. [[@kevincox]] +* [[#3981]]: SQLite: fix transaction level accounting with bad custom command. [[@kevincox]] +* [[#3986]]: chore(core): Fix docstring for Query::try_bind [[@iamjpotts]] +* [[#3987]]: chore(deps): Resolve deprecation warning for chrono Date and ymd methods [[@iamjpotts]] +* [[#3988]]: refactor(sqlite): Resolve duplicate test target warning for macros.rs [[@iamjpotts]] +* [[#3989]]: chore(deps): Set default-features=false on sqlx in workspace.dependencies [[@iamjpotts]] +* [[#3991]]: fix(sqlite): regression when decoding nulls [[@abonander]] +* [[#4006]]: PostgreSQL SASL – run SHA256 in a blocking executor [[@ThomWright]] +* [[#4007]]: fix(compose): use OS-assigned ports for all conatiners [[@papaj-na-wrotkach]] +* [[#4009]]: Drop cached db connections in macros upon hitting an error [[@swlynch99]] +* [[#4024]]: fix(sqlite) Migrate revert with no-transaction [[@Dosenpfand]] +* [[#4027]]: native tls handshake: build TlsConnector in blocking threadpool [[@daviduebler]] * [[#4053]]: fix(macros): smarter `.env` loading, caching, and invalidation [[@abonander]] * Additional credit to [[@AlexTMjugador]] ([[#4018]]) and [[@Diggsey]] ([[#4039]]) for their proposed solutions which served as a useful comparison. @@ -55,7 +176,76 @@ This section will be replaced in subsequent alpha releases. See the Git history [#3821]: https://github.com/launchbadge/sqlx/pull/3821 [#3383]: https://github.com/launchbadge/sqlx/pull/3383 +[#3486]: https://github.com/launchbadge/sqlx/pull/3486 +[#3495]: https://github.com/launchbadge/sqlx/pull/3495 +[#3525]: https://github.com/launchbadge/sqlx/pull/3525 +[#3526]: https://github.com/launchbadge/sqlx/pull/3526 +[#3541]: https://github.com/launchbadge/sqlx/pull/3541 +[#3613]: https://github.com/launchbadge/sqlx/pull/3613 +[#3641]: https://github.com/launchbadge/sqlx/pull/3641 +[#3651]: https://github.com/launchbadge/sqlx/pull/3651 +[#3670]: https://github.com/launchbadge/sqlx/pull/3670 +[#3674]: https://github.com/launchbadge/sqlx/pull/3674 +[#3675]: https://github.com/launchbadge/sqlx/pull/3675 +[#3709]: https://github.com/launchbadge/sqlx/pull/3709 +[#3723]: https://github.com/launchbadge/sqlx/pull/3723 +[#3791]: https://github.com/launchbadge/sqlx/pull/3791 +[#3800]: https://github.com/launchbadge/sqlx/pull/3800 +[#3821]: https://github.com/launchbadge/sqlx/pull/3821 +[#3840]: https://github.com/launchbadge/sqlx/pull/3840 +[#3848]: https://github.com/launchbadge/sqlx/pull/3848 +[#3856]: https://github.com/launchbadge/sqlx/pull/3856 +[#3857]: https://github.com/launchbadge/sqlx/pull/3857 +[#3859]: https://github.com/launchbadge/sqlx/pull/3859 +[#3861]: https://github.com/launchbadge/sqlx/pull/3861 +[#3863]: https://github.com/launchbadge/sqlx/pull/3863 +[#3867]: https://github.com/launchbadge/sqlx/pull/3867 +[#3874]: https://github.com/launchbadge/sqlx/pull/3874 +[#3881]: https://github.com/launchbadge/sqlx/pull/3881 +[#3886]: https://github.com/launchbadge/sqlx/pull/3886 +[#3889]: https://github.com/launchbadge/sqlx/pull/3889 +[#3890]: https://github.com/launchbadge/sqlx/pull/3890 +[#3910]: https://github.com/launchbadge/sqlx/pull/3910 +[#3911]: https://github.com/launchbadge/sqlx/pull/3911 +[#3914]: https://github.com/launchbadge/sqlx/pull/3914 +[#3915]: https://github.com/launchbadge/sqlx/pull/3915 +[#3917]: https://github.com/launchbadge/sqlx/pull/3917 +[#3918]: https://github.com/launchbadge/sqlx/pull/3918 +[#3919]: https://github.com/launchbadge/sqlx/pull/3919 +[#3923]: https://github.com/launchbadge/sqlx/pull/3923 +[#3924]: https://github.com/launchbadge/sqlx/pull/3924 +[#3928]: https://github.com/launchbadge/sqlx/pull/3928 +[#3938]: https://github.com/launchbadge/sqlx/pull/3938 +[#3949]: https://github.com/launchbadge/sqlx/pull/3949 +[#3950]: https://github.com/launchbadge/sqlx/pull/3950 +[#3952]: https://github.com/launchbadge/sqlx/pull/3952 +[#3956]: https://github.com/launchbadge/sqlx/pull/3956 +[#3957]: https://github.com/launchbadge/sqlx/pull/3957 +[#3958]: https://github.com/launchbadge/sqlx/pull/3958 +[#3960]: https://github.com/launchbadge/sqlx/pull/3960 +[#3962]: https://github.com/launchbadge/sqlx/pull/3962 +[#3968]: https://github.com/launchbadge/sqlx/pull/3968 +[#3971]: https://github.com/launchbadge/sqlx/pull/3971 +[#3975]: https://github.com/launchbadge/sqlx/pull/3975 +[#3977]: https://github.com/launchbadge/sqlx/pull/3977 +[#3980]: https://github.com/launchbadge/sqlx/pull/3980 +[#3981]: https://github.com/launchbadge/sqlx/pull/3981 +[#3986]: https://github.com/launchbadge/sqlx/pull/3986 +[#3987]: https://github.com/launchbadge/sqlx/pull/3987 +[#3988]: https://github.com/launchbadge/sqlx/pull/3988 +[#3989]: https://github.com/launchbadge/sqlx/pull/3989 +[#3991]: https://github.com/launchbadge/sqlx/pull/3991 +[#4002]: https://github.com/launchbadge/sqlx/pull/4002 +[#4006]: https://github.com/launchbadge/sqlx/pull/4006 +[#4007]: https://github.com/launchbadge/sqlx/pull/4007 +[#4008]: https://github.com/launchbadge/sqlx/pull/4008 +[#4009]: https://github.com/launchbadge/sqlx/pull/4009 +[#4015]: https://github.com/launchbadge/sqlx/pull/401 [#4018]: https://github.com/launchbadge/sqlx/pull/4018 +[#4020]: https://github.com/launchbadge/sqlx/pull/4020 +[#4022]: https://github.com/launchbadge/sqlx/pull/4022 +[#4024]: https://github.com/launchbadge/sqlx/pull/4024 +[#4027]: https://github.com/launchbadge/sqlx/pull/4027 [#4039]: https://github.com/launchbadge/sqlx/pull/4039 [#4053]: https://github.com/launchbadge/sqlx/pull/4053 @@ -2960,4 +3150,28 @@ Fix docs.rs build by enabling a runtime feature in the docs.rs metadata in `Carg [@dyc3]: https://github.com/dyc3 [@ThomWright]: https://github.com/ThomWright [@duhby]: https://github.com/duhby +[@V02460]: https://github.com/V02460 +[@nipunn1313]: https://github.com/nipunn1313 +[@miniduikboot]: https://github.com/miniduikboot +[@0xfourzerofour]: https://github.com/0xfourzerofour [@AlexTMjugador]: https://github.com/AlexTMjugador +[@martin-kolarik]: https://github.com/martin-kolarik +[@cvzx]: https://github.com/cvzx +[@Dirbaio]: https://github.com/Dirbaio +[@elichai]: https://github.com/elichai +[@silvestrpredko]: https://github.com/silvestrpredko +[@davidcornu]: https://github.com/davidcornu +[@zebrapurring]: https://github.com/zebrapurring +[@djarb]: https://github.com/djarb +[@barskern]: https://github.com/barskern +[@nhatcher-frequenz]: https://github.com/nhatcher-frequenz +[@JerryQ17]: https://github.com/JerryQ17 +[@jpmelos]: https://github.com/jpmelos +[@psionic-k]: https://github.com/psionic-k +[@Xiretza]: https://github.com/Xiretza +[@2ndDerivative]: https://github.com/2ndDerivative +[@kevincox]: https://github.com/kevincox +[@papaj-na-wrotkach]: https://github.com/papaj-na-wrotkach +[@xb284524239]: https://github.com/xb284524239 +[@Dosenpfand]: https://github.com/Dosenpfand +[@daviduebler]: https://github.com/daviduebler From 946b6d4d162410df65c8eadd73b9ae7eef7db7a4 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Tue, 14 Oct 2025 19:08:15 -0700 Subject: [PATCH 07/16] fix: break dev-dependency cycle because of rust-lang/cargo#15622 --- sqlx-core/Cargo.toml | 8 +++++++- sqlx-mysql/Cargo.toml | 3 ++- sqlx-postgres/Cargo.toml | 4 +++- sqlx-sqlite/Cargo.toml | 8 ++++++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index e22c6d4fc3..f2c0d58e36 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -106,8 +106,14 @@ hashbrown = "0.16.0" thiserror.workspace = true [dev-dependencies] -sqlx = { workspace = true, features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] } tokio = { version = "1", features = ["rt"] } +[dev-dependencies.sqlx] +# FIXME: https://github.com/rust-lang/cargo/issues/15622 +# workspace = true +path = ".." +default-features = false +features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] + [lints] workspace = true diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index 2ed0738a24..ee9512b61e 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -73,7 +73,8 @@ thiserror.workspace = true serde = { version = "1.0.144", optional = true } [dev-dependencies] -sqlx = { workspace = true, features = ["mysql"] } +# FIXME: https://github.com/rust-lang/cargo/issues/15622 +sqlx = { path = "..", default-features = false, features = ["mysql"] } [lints] workspace = true diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index 4abd252357..ceec9eb648 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -79,7 +79,9 @@ workspace = true features = ["json"] [dev-dependencies.sqlx] -workspace = true +# FIXME: https://github.com/rust-lang/cargo/issues/15622 +# workspace = true +path = ".." features = ["postgres", "derive"] [target.'cfg(target_os = "windows")'.dependencies] diff --git a/sqlx-sqlite/Cargo.toml b/sqlx-sqlite/Cargo.toml index 6e04f1c2a6..4f56c3592f 100644 --- a/sqlx-sqlite/Cargo.toml +++ b/sqlx-sqlite/Cargo.toml @@ -94,8 +94,12 @@ regex = { version = "1.5.5", optional = true } [dependencies.sqlx-core] workspace = true -[dev-dependencies] -sqlx = { workspace = true, features = ["macros", "runtime-tokio", "tls-none", "sqlite"] } +[dev-dependencies.sqlx] +# FIXME: https://github.com/rust-lang/cargo/issues/15622 +# workspace = true +path = ".." +default-features = false +features = ["macros", "runtime-tokio", "tls-none", "sqlite"] [lints] workspace = true From a802da0e674e3b890d2fa3580ee843d687f2e32f Mon Sep 17 00:00:00 2001 From: Kevin R Date: Wed, 22 Oct 2025 10:40:50 -0700 Subject: [PATCH 08/16] Fix typo in migration example from 'uesrs' to 'users' (#4068) --- sqlx-core/src/migrate/migrator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-core/src/migrate/migrator.rs b/sqlx-core/src/migrate/migrator.rs index 375c2af3fd..53295c92d0 100644 --- a/sqlx-core/src/migrate/migrator.rs +++ b/sqlx-core/src/migrate/migrator.rs @@ -80,7 +80,7 @@ impl Migrator { /// // Define your migrations. /// // You can also use include_str!("./xxx.sql") instead of hard-coded SQL statements. /// let migrations = vec![ - /// Migration::new(1, "user".into(), ReversibleUp, "create table uesrs ( ... )".into_sql_str(), false), + /// Migration::new(1, "user".into(), ReversibleUp, "create table users ( ... )".into_sql_str(), false), /// Migration::new(2, "post".into(), ReversibleUp, "create table posts ( ... )".into_sql_str(), false), /// // add more... /// ]; From 2a1eedd29962ce81a581f858337016edac160100 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Tue, 28 Oct 2025 04:44:02 -0700 Subject: [PATCH 09/16] fix(CHANGELOG): correct link to #4015 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f00d170d1..ed472e2148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -240,7 +240,7 @@ This section will be replaced in subsequent alpha releases. See the Git history [#4007]: https://github.com/launchbadge/sqlx/pull/4007 [#4008]: https://github.com/launchbadge/sqlx/pull/4008 [#4009]: https://github.com/launchbadge/sqlx/pull/4009 -[#4015]: https://github.com/launchbadge/sqlx/pull/401 +[#4015]: https://github.com/launchbadge/sqlx/pull/4015 [#4018]: https://github.com/launchbadge/sqlx/pull/4018 [#4020]: https://github.com/launchbadge/sqlx/pull/4020 [#4022]: https://github.com/launchbadge/sqlx/pull/4022 From e4afbd4ca127358391bf13ddf921f8f671633ae2 Mon Sep 17 00:00:00 2001 From: Andreas Molitor <62615378+anmolitor@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:46:25 +0100 Subject: [PATCH 10/16] chore: update hashlink to v0.11.0 (#4072) Co-authored-by: amolitor --- Cargo.lock | 6 +++--- sqlx-core/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d611267ccc..78e40f0c12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1664,11 +1664,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.16.0", ] [[package]] diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index f2c0d58e36..fff4ef3d24 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -98,7 +98,7 @@ tracing = { version = "0.1.37", features = ["log"] } smallvec = "1.7.0" url = { version = "2.2.2" } bstr = { version = "1.0", default-features = false, features = ["std"], optional = true } -hashlink = "0.10.0" +hashlink = "0.11.0" indexmap = "2.0" event-listener = "5.2.0" hashbrown = "0.16.0" From e8384f2a00173c2b120eea72e99d120557fced8b Mon Sep 17 00:00:00 2001 From: Joey de Waal <99046430+joeydewaal@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:18:47 +0100 Subject: [PATCH 11/16] fix spelling (#4069) --- examples/postgres/axum-social-with-tests/src/http/error.rs | 2 +- examples/postgres/files/src/main.rs | 2 +- sqlx-cli/tests/ignored-chars/sqlx.toml | 2 +- sqlx-core/src/config/reference.toml | 2 +- sqlx-core/src/error.rs | 2 +- sqlx-mysql/src/migrate.rs | 4 ++-- sqlx-mysql/src/protocol/statement/row.rs | 2 +- sqlx-mysql/src/protocol/text/column.rs | 2 +- sqlx-postgres/src/any.rs | 4 ++-- sqlx-postgres/src/message/sasl.rs | 2 +- sqlx-postgres/src/migrate.rs | 2 +- sqlx-postgres/src/options/pgpass.rs | 4 ++-- sqlx-postgres/src/types/geometry/polygon.rs | 2 +- tests/postgres/postgres.rs | 4 ++-- 14 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/postgres/axum-social-with-tests/src/http/error.rs b/examples/postgres/axum-social-with-tests/src/http/error.rs index fc4fd5ea1f..51e0208e5a 100644 --- a/examples/postgres/axum-social-with-tests/src/http/error.rs +++ b/examples/postgres/axum-social-with-tests/src/http/error.rs @@ -11,7 +11,7 @@ pub enum Error { /// A SQLx call returned an error. /// /// The exact error contents are not reported to the user in order to avoid leaking - /// information about databse internals. + /// information about database internals. #[error("an internal database error occurred")] Sqlx(#[from] sqlx::Error), diff --git a/examples/postgres/files/src/main.rs b/examples/postgres/files/src/main.rs index 65e92190d8..194d823fd7 100644 --- a/examples/postgres/files/src/main.rs +++ b/examples/postgres/files/src/main.rs @@ -30,7 +30,7 @@ impl Display for PostWithAuthorQuery { async fn main() -> anyhow::Result<()> { let pool = PgPool::connect(&dotenvy::var("DATABASE_URL")?).await?; - // we can use a tranditional wrapper around the `query!()` macro using files + // we can use a traditional wrapper around the `query!()` macro using files query_file!("queries/insert_seed_data.sql") .execute(&pool) .await?; diff --git a/sqlx-cli/tests/ignored-chars/sqlx.toml b/sqlx-cli/tests/ignored-chars/sqlx.toml index e5278d283f..f331e5b7a4 100644 --- a/sqlx-cli/tests/ignored-chars/sqlx.toml +++ b/sqlx-cli/tests/ignored-chars/sqlx.toml @@ -1,5 +1,5 @@ [migrate] -# Ignore common whitespace characters (beware syntatically significant whitespace!) +# Ignore common whitespace characters (beware syntactically significant whitespace!) # Space, tab, CR, LF, zero-width non-breaking space (U+FEFF) # # U+FEFF is added by some editors as a magic number at the beginning of a text file indicating it is UTF-8 encoded, diff --git a/sqlx-core/src/config/reference.toml b/sqlx-core/src/config/reference.toml index f0acbdbe6a..00f0af9285 100644 --- a/sqlx-core/src/config/reference.toml +++ b/sqlx-core/src/config/reference.toml @@ -206,7 +206,7 @@ migrations-dir = "foo/migrations" # Note that the TOML format requires double-quoted strings to process escapes. # ignored-chars = ["\r"] -# Ignore common whitespace characters (beware syntatically significant whitespace!) +# Ignore common whitespace characters (beware syntactically significant whitespace!) # Space, tab, CR, LF, zero-width non-breaking space (U+FEFF) # # U+FEFF is added by some editors as a magic number at the beginning of a text file indicating it is UTF-8 encoded, diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index c6652aef75..8c6f424cdf 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -84,7 +84,7 @@ pub enum Error { source: BoxDynError, }, - /// Error occured while encoding a value. + /// Error occurred while encoding a value. #[error("error occurred while encoding a value: {0}")] Encode(#[source] BoxDynError), diff --git a/sqlx-mysql/src/migrate.rs b/sqlx-mysql/src/migrate.rs index 0176f93c26..7424dfe27f 100644 --- a/sqlx-mysql/src/migrate.rs +++ b/sqlx-mysql/src/migrate.rs @@ -193,7 +193,7 @@ CREATE TABLE IF NOT EXISTS {table_name} ( migration: &'e Migration, ) -> BoxFuture<'e, Result> { Box::pin(async move { - // Use a single transaction for the actual migration script and the essential bookeeping so we never + // Use a single transaction for the actual migration script and the essential bookkeeping so we never // execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966. // The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for // data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1 @@ -268,7 +268,7 @@ CREATE TABLE IF NOT EXISTS {table_name} ( migration: &'e Migration, ) -> BoxFuture<'e, Result> { Box::pin(async move { - // Use a single transaction for the actual migration script and the essential bookeeping so we never + // Use a single transaction for the actual migration script and the essential bookkeeping so we never // execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966. let mut tx = self.begin().await?; let start = Instant::now(); diff --git a/sqlx-mysql/src/protocol/statement/row.rs b/sqlx-mysql/src/protocol/statement/row.rs index 3007884c72..a55701fe19 100644 --- a/sqlx-mysql/src/protocol/statement/row.rs +++ b/sqlx-mysql/src/protocol/statement/row.rs @@ -18,7 +18,7 @@ impl<'de> ProtocolDecode<'de, &'de [MySqlColumn]> for BinaryRow { let header = buf.get_u8(); if header != 0 { return Err(err_protocol!( - "exepcted 0x00 (ROW) but found 0x{:02x}", + "expected 0x00 (ROW) but found 0x{:02x}", header )); } diff --git a/sqlx-mysql/src/protocol/text/column.rs b/sqlx-mysql/src/protocol/text/column.rs index 6ff65b3fcc..6e33713880 100644 --- a/sqlx-mysql/src/protocol/text/column.rs +++ b/sqlx-mysql/src/protocol/text/column.rs @@ -41,7 +41,7 @@ bitflags! { /// Field is an enumeration. const ENUM = 256; - /// Field is an auto-incement field. + /// Field is an auto-increment field. const AUTO_INCREMENT = 512; /// Field is a timestamp. diff --git a/sqlx-postgres/src/any.rs b/sqlx-postgres/src/any.rs index 51eb15d2a7..1f248505ae 100644 --- a/sqlx-postgres/src/any.rs +++ b/sqlx-postgres/src/any.rs @@ -136,8 +136,8 @@ impl AnyConnectionBackend for PgConnection { ) -> BoxFuture<'c, sqlx_core::Result> { Box::pin(async move { let statement = Executor::prepare_with(self, sql, &[]).await?; - let colunn_names = statement.metadata.column_names.clone(); - AnyStatement::try_from_statement(statement, colunn_names) + let column_names = statement.metadata.column_names.clone(); + AnyStatement::try_from_statement(statement, column_names) }) } diff --git a/sqlx-postgres/src/message/sasl.rs b/sqlx-postgres/src/message/sasl.rs index 9d393189bf..5593a9367a 100644 --- a/sqlx-postgres/src/message/sasl.rs +++ b/sqlx-postgres/src/message/sasl.rs @@ -41,7 +41,7 @@ impl FrontendMessage for SaslInitialResponse<'_> { let response_len = i32::try_from(self.response.len()).map_err(|_| { err_protocol!( - "SASL Initial Response length too long for protcol: {}", + "SASL Initial Response length too long for protocol: {}", self.response.len() ) })?; diff --git a/sqlx-postgres/src/migrate.rs b/sqlx-postgres/src/migrate.rs index 49104672c7..f4e55a36e6 100644 --- a/sqlx-postgres/src/migrate.rs +++ b/sqlx-postgres/src/migrate.rs @@ -231,7 +231,7 @@ CREATE TABLE IF NOT EXISTS {table_name} ( if migration.no_tx { execute_migration(self, table_name, migration).await?; } else { - // Use a single transaction for the actual migration script and the essential bookeeping so we never + // Use a single transaction for the actual migration script and the essential bookkeeping so we never // execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966. // The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for // data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1 diff --git a/sqlx-postgres/src/options/pgpass.rs b/sqlx-postgres/src/options/pgpass.rs index bf16559548..930c5f0f51 100644 --- a/sqlx-postgres/src/options/pgpass.rs +++ b/sqlx-postgres/src/options/pgpass.rs @@ -173,8 +173,8 @@ fn find_next_field<'a>(line: &mut &'a str) -> Option> { let mut escaped_string = None; let mut last_added = 0; - let char_indicies = line.char_indices(); - for (idx, c) in char_indicies { + let char_indices = line.char_indices(); + for (idx, c) in char_indices { if c == ':' && !escaping { let (field, rest) = line.split_at(idx); *line = &rest[1..]; diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index e612b93499..5ca97477ea 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -15,7 +15,7 @@ const BYTE_WIDTH: usize = mem::size_of::(); /// Description: Polygon (similar to closed polygon) /// Representation: `((x1,y1),...)` /// -/// Polygons are represented by lists of points (the vertexes of the polygon). Polygons are very similar to closed paths; the essential semantic difference is that a polygon is considered to include the area within it, while a path is not. +/// Polygons are represented by lists of points (the vertices of the polygon). Polygons are very similar to closed paths; the essential semantic difference is that a polygon is considered to include the area within it, while a path is not. /// An important implementation difference between polygons and paths is that the stored representation of a polygon includes its smallest bounding box. This speeds up certain search operations, although computing the bounding box adds overhead while constructing new polygons. /// Values of type polygon are specified using any of the following syntaxes: /// diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index 8324982b67..06adf0ca7f 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -417,7 +417,7 @@ async fn copy_can_work_with_failed_transactions() -> anyhow::Result<()> { tx.rollback().await?; - // conn should be usable again, as we explictly rolled back the transaction + // conn should be usable again, as we explicitly rolled back the transaction let got: i32 = sqlx::query_scalar("SELECT 1") .fetch_one(conn.as_mut()) .await?; @@ -445,7 +445,7 @@ async fn it_can_work_with_failed_transactions() -> anyhow::Result<()> { .is_err()); tx.rollback().await?; - // conn should be usable again, as we explictly rolled back the transaction + // conn should be usable again, as we explicitly rolled back the transaction let got: i32 = sqlx::query_scalar("SELECT 1") .fetch_one(conn.as_mut()) .await?; From a2a219f175bf052f70e054b4345e4e644f468acd Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Wed, 24 Dec 2025 21:40:53 -0800 Subject: [PATCH 12/16] fix CI: replace removed macOS runner, deprecated use of `Command::cargo_bin()` (#4134) * fix(ci): update macOS intel runner version * fix(cli/tests): replace use of deprecated `Command::cargo_bin()` --- .github/workflows/sqlx-cli.yml | 6 +++--- Cargo.lock | 11 ++--------- sqlx-cli/Cargo.toml | 2 +- sqlx-cli/tests/add.rs | 4 ++-- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.github/workflows/sqlx-cli.yml b/.github/workflows/sqlx-cli.yml index 927616c69b..5686cb5059 100644 --- a/.github/workflows/sqlx-cli.yml +++ b/.github/workflows/sqlx-cli.yml @@ -45,7 +45,7 @@ jobs: - ubuntu-latest # FIXME: migrations tests fail on Windows for whatever reason # - windows-latest - - macOS-13 + - macOS-15-intel - macOS-latest timeout-minutes: 30 @@ -302,7 +302,7 @@ jobs: os: - ubuntu-latest - windows-latest - - macOS-13 + - macOS-15-intel - macOS-latest include: - os: ubuntu-latest @@ -312,7 +312,7 @@ jobs: - os: windows-latest target: x86_64-pc-windows-msvc bin: target/debug/cargo-sqlx.exe - - os: macOS-13 + - os: macOS-15-intel target: x86_64-apple-darwin bin: target/debug/cargo-sqlx - os: macOS-latest diff --git a/Cargo.lock b/Cargo.lock index 78e40f0c12..7a2979fc80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,13 +151,12 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert_cmd" -version = "2.0.17" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" dependencies = [ "anstyle", "bstr", - "doc-comment", "libc", "predicates 3.1.3", "predicates-core", @@ -1229,12 +1228,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "dotenvy" version = "0.15.7" diff --git a/sqlx-cli/Cargo.toml b/sqlx-cli/Cargo.toml index d69048e698..89dc93477e 100644 --- a/sqlx-cli/Cargo.toml +++ b/sqlx-cli/Cargo.toml @@ -76,7 +76,7 @@ sqlx-toml = ["sqlx/sqlx-toml"] _sqlite = [] [dev-dependencies] -assert_cmd = "2.0.11" +assert_cmd = "2.1.1" tempfile = "3.10.1" [lints] diff --git a/sqlx-cli/tests/add.rs b/sqlx-cli/tests/add.rs index cebbb51d53..bf9085b85e 100644 --- a/sqlx-cli/tests/add.rs +++ b/sqlx-cli/tests/add.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use assert_cmd::Command; +use assert_cmd::cargo_bin_cmd; use std::cmp::Ordering; use std::fs::read_dir; use std::ops::Index; @@ -117,7 +117,7 @@ impl AddMigrations { sequential: bool, expect_success: bool, ) -> anyhow::Result<&'_ Self> { - let cmd_result = Command::cargo_bin("cargo-sqlx")? + let cmd_result = cargo_bin_cmd!("cargo-sqlx") .current_dir(&self.tempdir) .args( [ From 452da1acf549e94a6358a770e7513433f15b5f0a Mon Sep 17 00:00:00 2001 From: M Bakhtiyar Ali <95485961+ZennoZenith@users.noreply.github.com> Date: Wed, 31 Dec 2025 03:23:59 +0530 Subject: [PATCH 13/16] Bump bit-vec to v0.8 (#4094) --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a2979fc80..9c397c555d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -562,9 +562,9 @@ dependencies = [ [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" diff --git a/Cargo.toml b/Cargo.toml index 00d5d656c1..8726be4f95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,7 +178,7 @@ sqlx = { version = "=0.9.0-alpha.1", path = ".", default-features = false } # Common type integrations shared by multiple driver crates. # These are optional unless enabled in a workspace crate. bigdecimal = "0.4.0" -bit-vec = "0.6.3" +bit-vec = "0.8" chrono = { version = "0.4.34", default-features = false, features = ["std", "clock"] } ipnet = "2.3.0" ipnetwork = "0.21.1" From 93bafbefd6232e5148b7765ae30b3cee7de173f8 Mon Sep 17 00:00:00 2001 From: chrxn1c <90571420+chrxn1c@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:40:53 +0300 Subject: [PATCH 14/16] feat: add self-extractor for JSON type (#4123) --- sqlx-core/src/types/json.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sqlx-core/src/types/json.rs b/sqlx-core/src/types/json.rs index 3b94d8729b..6616ce1a7d 100644 --- a/sqlx-core/src/types/json.rs +++ b/sqlx-core/src/types/json.rs @@ -86,6 +86,13 @@ use crate::types::Type; #[serde(transparent)] pub struct Json(pub T); +impl Json { + /// Extract the inner value. + pub fn into_inner(self) -> T { + self.0 + } +} + impl From for Json { fn from(value: T) -> Self { Self(value) From 1dd526a2ed67fa763766e670c30b1ce3b152a42e Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 18 Jan 2026 11:02:10 +0800 Subject: [PATCH 15/16] Bump whoami to v2 (#4143) * Bump whoami to v2 Signed-off-by: tison * Update sqlx-postgres/src/options/mod.rs --------- Signed-off-by: tison --- Cargo.lock | 31 ++++++++++++++++--------------- sqlx-mysql/Cargo.toml | 1 - sqlx-postgres/Cargo.toml | 2 +- sqlx-postgres/src/options/mod.rs | 9 ++++++++- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c397c555d..74947c4835 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2132,13 +2132,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.4" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.9.1", "libc", - "redox_syscall", + "redox_syscall 0.7.0", ] [[package]] @@ -2547,7 +2547,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.13", "smallvec", "windows-targets 0.52.6", ] @@ -2970,6 +2970,15 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.9.1", +] + [[package]] name = "regex" version = "1.11.1" @@ -3923,7 +3932,6 @@ dependencies = [ "time", "tracing", "uuid", - "whoami", ] [[package]] @@ -4709,12 +4717,6 @@ dependencies = [ "wit-bindgen-rt", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -4828,12 +4830,11 @@ dependencies = [ [[package]] name = "whoami" -version = "1.6.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +checksum = "ace4d5c7b5ab3d99629156d4e0997edbe98a4beb6d5ba99e2cae830207a81983" dependencies = [ - "redox_syscall", - "wasite", + "libredox", ] [[package]] diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index ee9512b61e..8a7c9cef38 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -65,7 +65,6 @@ percent-encoding = "2.1.0" smallvec = "1.7.0" stringprep = "0.1.2" tracing = { version = "0.1.37", features = ["log"] } -whoami = { version = "1.2.1", default-features = false } dotenvy.workspace = true thiserror.workspace = true diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index ceec9eb648..89016c8088 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -65,7 +65,7 @@ num-bigint = { version = "0.4.3", optional = true } smallvec = { version = "1.7.0", features = ["serde"] } stringprep = "0.1.2" tracing = { version = "0.1.37", features = ["log"] } -whoami = { version = "1.2.1", default-features = false } +whoami = { version = "2.0.2", default-features = false } dotenvy.workspace = true thiserror.workspace = true diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index efbc43989b..21e6628cae 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -64,7 +64,14 @@ impl PgConnectOptions { .or_else(|| var("PGHOST").ok()) .unwrap_or_else(|| default_host(port)); - let username = var("PGUSER").ok().unwrap_or_else(whoami::username); + let username = if let Ok(username) = var("PGUSER") { + username + } else if let Ok(username) = whoami::username() { + username + } else { + // keep the same fallback as previous version + "unknown".to_string() + }; let database = var("PGDATABASE").ok(); From 89c399c296a0206978bb6ccf955c8e8abbc676c4 Mon Sep 17 00:00:00 2001 From: Niclas Klugmann Date: Mon, 19 Jan 2026 19:53:29 +0100 Subject: [PATCH 16/16] lazily construct principal name --- sqlx-postgres/src/connection/gssapi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/connection/gssapi.rs b/sqlx-postgres/src/connection/gssapi.rs index 4f33dfc080..07ec4db729 100644 --- a/sqlx-postgres/src/connection/gssapi.rs +++ b/sqlx-postgres/src/connection/gssapi.rs @@ -18,7 +18,7 @@ pub async fn authenticate(stream: &mut PgStream, options: &PgConnectOptions) -> let principal = gssapi_principal .as_ref() .map(Cow::Borrowed) - .unwrap_or(Cow::Owned(format!("postgres/{host}"))); + .unwrap_or_else(|| Cow::Owned(format!("postgres/{host}"))); let (mut ctx, token) = cross_krb5::ClientCtx::new(InitiateFlags::empty(), None, &principal, None) .map_err(|e| Error::GssApi(e.into()))?;