From 82f2933690d80e3e9df79889d417cce6e3ef883a Mon Sep 17 00:00:00 2001 From: Matjaz Domen Pecan Date: Wed, 25 Feb 2026 19:01:45 +0100 Subject: [PATCH 1/3] feat(postgres): add `locking` attribute to `#[sqlx::test]` Add a `locking` parameter to `#[sqlx::test]` that controls whether an advisory lock is acquired before creating the test database schema. Defaults to `true` to preserve existing PostgreSQL behavior. Setting `locking = false` allows `#[sqlx::test]` to work with databases that speak the PostgreSQL wire protocol but do not implement advisory locks, such as CockroachDB. When disabled, migrator locking is also skipped so the entire test setup is advisory-lock-free. Follows the same pattern as `Migrator::set_locking()` (PR #2063). --- sqlx-core/src/testing/mod.rs | 25 ++++++++++++++- sqlx-macros-core/src/test_attr.rs | 30 ++++++++++++++++- sqlx-postgres/src/testing/mod.rs | 53 +++++++++++++++++++------------ src/macros/test.md | 32 ++++++++++++++++++- tests/postgres/test-attr.rs | 23 ++++++++++++++ 5 files changed, 140 insertions(+), 23 deletions(-) diff --git a/sqlx-core/src/testing/mod.rs b/sqlx-core/src/testing/mod.rs index 17022b4652..9a6b9b1c13 100644 --- a/sqlx-core/src/testing/mod.rs +++ b/sqlx-core/src/testing/mod.rs @@ -66,6 +66,7 @@ pub struct TestArgs { pub test_path: &'static str, pub migrator: Option<&'static Migrator>, pub fixtures: &'static [TestFixture], + pub locking: bool, } pub trait TestFn { @@ -158,6 +159,7 @@ impl TestArgs { test_path, migrator: None, fixtures: &[], + locking: true, } } @@ -168,6 +170,10 @@ impl TestArgs { pub fn fixtures(&mut self, fixtures: &'static [TestFixture]) { self.fixtures = fixtures; } + + pub fn set_locking(&mut self, locking: bool) { + self.locking = locking; + } } impl TestTermination for () { @@ -255,7 +261,24 @@ async fn setup_test_db( .await .expect("failed to connect to test database"); - if let Some(migrator) = args.migrator { + if let Some(static_migrator) = args.migrator { + // When test locking is disabled, also disable locking in the migrator. + // This is required for databases that don't support advisory locks (e.g. CockroachDB). + let owned_migrator; + let migrator = if args.locking { + static_migrator + } else { + owned_migrator = Migrator { + locking: false, + migrations: static_migrator.migrations.clone(), + ignore_missing: static_migrator.ignore_missing, + no_tx: static_migrator.no_tx, + table_name: static_migrator.table_name.clone(), + create_schemas: static_migrator.create_schemas.clone(), + }; + &owned_migrator + }; + migrator .run_direct(None, &mut conn) .await diff --git a/sqlx-macros-core/src/test_attr.rs b/sqlx-macros-core/src/test_attr.rs index 046ff5c2fb..a03a527558 100644 --- a/sqlx-macros-core/src/test_attr.rs +++ b/sqlx-macros-core/src/test_attr.rs @@ -6,6 +6,7 @@ use syn::parse::Parser; struct Args { fixtures: Vec<(FixturesType, Vec)>, migrations: MigrationsOpt, + locking: Option, } #[cfg(feature = "migrate")] @@ -143,6 +144,11 @@ fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> crate::Result { let migrator = crate::migrate::expand(Some(path))?; @@ -180,6 +186,8 @@ fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> crate::Result _ = #name; @@ -197,6 +205,7 @@ fn parse_args(attr_args: AttributeArgs) -> syn::Result { let mut fixtures = Vec::new(); let mut migrations = MigrationsOpt::InferredPath; + let mut locking = None; for arg in attr_args { let path = arg.path().clone(); @@ -292,10 +301,28 @@ fn parse_args(attr_args: AttributeArgs) -> syn::Result { migrations = MigrationsOpt::ExplicitMigrator(lit.parse()?); } + Meta::NameValue(MetaNameValue { value, .. }) if path.is_ident("locking") => { + if locking.is_some() { + return Err(syn::Error::new_spanned( + path, + "cannot have more than one `locking` arg", + )); + } + + let Expr::Lit(syn::ExprLit { + lit: Lit::Bool(lit), + .. + }) = value + else { + return Err(syn::Error::new_spanned(path, "expected `true` or `false`")); + }; + + locking = Some(lit.value); + } arg => { return Err(syn::Error::new_spanned( arg, - r#"expected `fixtures("", ...)` or `migrations = "" | false` or `migrator = ""`"#, + r#"expected `fixtures("", ...)` or `migrations = "" | false` or `migrator = ""` or `locking = true | false`"#, )) } } @@ -304,6 +331,7 @@ fn parse_args(attr_args: AttributeArgs) -> syn::Result { Ok(Args { fixtures, migrations, + locking, }) } diff --git a/sqlx-postgres/src/testing/mod.rs b/sqlx-postgres/src/testing/mod.rs index 3e1cf0ddf7..4db31f97cc 100644 --- a/sqlx-postgres/src/testing/mod.rs +++ b/sqlx-postgres/src/testing/mod.rs @@ -125,31 +125,44 @@ async fn test_context(args: &TestArgs) -> Result, Error> { let mut conn = master_pool.acquire().await?; - // language=PostgreSQL - conn.execute( - // Explicit lock avoids this latent bug: https://stackoverflow.com/a/29908840 - // I couldn't find a bug on the mailing list for `CREATE SCHEMA` specifically, - // but a clearly related bug with `CREATE TABLE` has been known since 2007: - // https://www.postgresql.org/message-id/200710222037.l9MKbCJZ098744%40wwwmaster.postgresql.org - // magic constant 8318549251334697844 is just 8 ascii bytes 'sqlxtest'. + // Explicit lock avoids this latent bug: https://stackoverflow.com/a/29908840 + // I couldn't find a bug on the mailing list for `CREATE SCHEMA` specifically, + // but a clearly related bug with `CREATE TABLE` has been known since 2007: + // https://www.postgresql.org/message-id/200710222037.l9MKbCJZ098744%40wwwmaster.postgresql.org + // magic constant 8318549251334697844 is just 8 ascii bytes 'sqlxtest'. + // + // The lock and DDL must be in a single `execute` call so they share one implicit + // transaction — `pg_advisory_xact_lock` is released at transaction end. + // + // Can be disabled with `#[sqlx::test(locking = false)]` for databases that do not + // implement advisory locks, such as CockroachDB. + let lock_sql = if args.locking { + "select pg_advisory_xact_lock(8318549251334697844);" + } else { + "" + }; + + let setup_sql = format!( + // language=PostgreSQL r#" - select pg_advisory_xact_lock(8318549251334697844); + {lock_sql} + + create schema if not exists _sqlx_test; - create schema if not exists _sqlx_test; + create table if not exists _sqlx_test.databases ( + db_name text primary key, + test_path text not null, + created_at timestamptz not null default now() + ); - create table if not exists _sqlx_test.databases ( - db_name text primary key, - test_path text not null, - created_at timestamptz not null default now() - ); + create index if not exists databases_created_at + on _sqlx_test.databases(created_at); - create index if not exists databases_created_at - on _sqlx_test.databases(created_at); + create sequence if not exists _sqlx_test.database_ids; + "# + ); - create sequence if not exists _sqlx_test.database_ids; - "#, - ) - .await?; + conn.execute(AssertSqlSafe(setup_sql)).await?; let db_name = Postgres::db_name(args); do_cleanup(&mut conn, &db_name).await?; diff --git a/src/macros/test.md b/src/macros/test.md index ec3cee90b0..05372112df 100644 --- a/src/macros/test.md +++ b/src/macros/test.md @@ -226,4 +226,34 @@ Multiple `fixtures` attributes can be used to combine different operating modes. 3Ordering for test fixtures is entirely up to the application, and each test may choose which fixtures to apply and which to omit. However, since each fixture is applied separately (sent as a single command string, so wrapped in an implicit `BEGIN` and `COMMIT`), you will want to make sure to order the fixtures such that foreign key -requirements are always satisfied, or else you might get errors. +requirements are always satisfied, or else you might get errors. + +### Disabling Advisory Locking (requires `migrate` feature) + +By default, `#[sqlx::test]` acquires a PostgreSQL advisory lock (`pg_advisory_xact_lock`) before creating the test +database schema and also locks during migrations. This prevents a race condition when multiple tests run concurrently. + +Some databases speak the PostgreSQL wire protocol but do not implement advisory locks. +For example, CockroachDB does not support `pg_advisory_xact_lock` +(see [cockroachdb/cockroach#13546](https://github.com/cockroachdb/cockroach/issues/13546)). + +You can disable locking with the `locking` attribute: + +```rust,no_run +# #[cfg(all(feature = "migrate", feature = "postgres"))] +# mod example { +use sqlx::PgPool; + +#[sqlx::test(locking = false)] +async fn test_on_cockroachdb(pool: PgPool) -> sqlx::Result<()> { + // The test database was created without advisory locks. + // Migrations also run without advisory locks. + Ok(()) +} +# } +``` + +This follows the same pattern as [`Migrator::set_locking(false)`](crate::migrate::Migrator::set_locking). + +**Note:** Disabling locking means concurrent test processes may race during schema setup. The DDL statements +use `IF NOT EXISTS` so this is generally safe, but you should be aware of the trade-off. diff --git a/tests/postgres/test-attr.rs b/tests/postgres/test-attr.rs index 78a8b1f59a..6c6205cc7a 100644 --- a/tests/postgres/test-attr.rs +++ b/tests/postgres/test-attr.rs @@ -199,3 +199,26 @@ macro_rules! macro_using_test { }; } macro_using_test!("tests/postgres/migrations"); + +// Verify that `locking = false` compiles and produces a valid test. +// +// This test is NOT safe to run concurrently with other `#[sqlx::test]` tests against the +// same Postgres server because the advisory lock exists to guard a race in +// `CREATE SCHEMA IF NOT EXISTS`. The `locking = false` flag is intended for databases +// like CockroachDB that serialize DDL and don't support advisory locks. +// +// We use `migrations = false` and `#[ignore]` so this compiles (proving the attribute +// parses correctly) but doesn't race with other tests in CI. +#[sqlx::test(migrations = false, locking = false)] +#[ignore] +async fn it_works_without_locking(pool: PgPool) -> sqlx::Result<()> { + let mut conn = pool.acquire().await?; + + let db_name: String = sqlx::query_scalar("SELECT current_database()") + .fetch_one(&mut *conn) + .await?; + + assert!(db_name.starts_with("_sqlx_test"), "dbname: {db_name:?}"); + + Ok(()) +} From 085164caba5155edcc107c2bcf10e8a56c31bac1 Mon Sep 17 00:00:00 2001 From: Matjaz Domen Pecan Date: Sat, 28 Feb 2026 12:43:43 +0100 Subject: [PATCH 2/3] feat(postgres): move advisory locking opt-out to DATABASE_URL parameter Replace the `#[sqlx::test(locking = false)]` macro attribute with a connection string parameter `?sqlx-advisory-locking=false`. This is a better design because: - Configuration lives where the database identity is declared - Users set it once in DATABASE_URL rather than annotating every test - Follows the existing `statement-cache-capacity` precedent - Named generically to leave the door open for Migrator support When `sqlx-advisory-locking=false` is set, both the test schema setup and migrations skip advisory locks, allowing `#[sqlx::test]` to work with CockroachDB and similar databases. --- sqlx-core/src/testing/mod.rs | 14 +++++--------- sqlx-macros-core/src/test_attr.rs | 30 +----------------------------- sqlx-mysql/src/testing/mod.rs | 1 + sqlx-postgres/src/options/doc.md | 1 + sqlx-postgres/src/options/mod.rs | 20 ++++++++++++++++++++ sqlx-postgres/src/options/parse.rs | 20 ++++++++++++++++++++ sqlx-postgres/src/testing/mod.rs | 8 +++++--- sqlx-sqlite/src/testing/mod.rs | 1 + src/macros/test.md | 24 +++++++++--------------- tests/postgres/test-attr.rs | 15 +++++++-------- 10 files changed, 70 insertions(+), 64 deletions(-) diff --git a/sqlx-core/src/testing/mod.rs b/sqlx-core/src/testing/mod.rs index 9a6b9b1c13..1683e0fdea 100644 --- a/sqlx-core/src/testing/mod.rs +++ b/sqlx-core/src/testing/mod.rs @@ -66,7 +66,6 @@ pub struct TestArgs { pub test_path: &'static str, pub migrator: Option<&'static Migrator>, pub fixtures: &'static [TestFixture], - pub locking: bool, } pub trait TestFn { @@ -83,6 +82,7 @@ pub struct TestContext { pub pool_opts: PoolOptions, pub connect_opts: ::Options, pub db_name: String, + pub locking: bool, } impl TestFn for fn(Pool) -> Fut @@ -159,7 +159,6 @@ impl TestArgs { test_path, migrator: None, fixtures: &[], - locking: true, } } @@ -170,10 +169,6 @@ impl TestArgs { pub fn fixtures(&mut self, fixtures: &'static [TestFixture]) { self.fixtures = fixtures; } - - pub fn set_locking(&mut self, locking: bool) { - self.locking = locking; - } } impl TestTermination for () { @@ -232,7 +227,7 @@ where .await .expect("failed to connect to setup test database"); - setup_test_db::(&test_context.connect_opts, &args).await; + setup_test_db::(&test_context.connect_opts, &args, test_context.locking).await; let res = test_fn(test_context.pool_opts, test_context.connect_opts).await; @@ -252,6 +247,7 @@ where async fn setup_test_db( copts: &::Options, args: &TestArgs, + locking: bool, ) where DB::Connection: Migrate + Sized, for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>, @@ -262,10 +258,10 @@ async fn setup_test_db( .expect("failed to connect to test database"); if let Some(static_migrator) = args.migrator { - // When test locking is disabled, also disable locking in the migrator. + // When advisory locking is disabled, also disable it in the migrator. // This is required for databases that don't support advisory locks (e.g. CockroachDB). let owned_migrator; - let migrator = if args.locking { + let migrator = if locking { static_migrator } else { owned_migrator = Migrator { diff --git a/sqlx-macros-core/src/test_attr.rs b/sqlx-macros-core/src/test_attr.rs index a03a527558..046ff5c2fb 100644 --- a/sqlx-macros-core/src/test_attr.rs +++ b/sqlx-macros-core/src/test_attr.rs @@ -6,7 +6,6 @@ use syn::parse::Parser; struct Args { fixtures: Vec<(FixturesType, Vec)>, migrations: MigrationsOpt, - locking: Option, } #[cfg(feature = "migrate")] @@ -144,11 +143,6 @@ fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> crate::Result { let migrator = crate::migrate::expand(Some(path))?; @@ -186,8 +180,6 @@ fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> crate::Result _ = #name; @@ -205,7 +197,6 @@ fn parse_args(attr_args: AttributeArgs) -> syn::Result { let mut fixtures = Vec::new(); let mut migrations = MigrationsOpt::InferredPath; - let mut locking = None; for arg in attr_args { let path = arg.path().clone(); @@ -301,28 +292,10 @@ fn parse_args(attr_args: AttributeArgs) -> syn::Result { migrations = MigrationsOpt::ExplicitMigrator(lit.parse()?); } - Meta::NameValue(MetaNameValue { value, .. }) if path.is_ident("locking") => { - if locking.is_some() { - return Err(syn::Error::new_spanned( - path, - "cannot have more than one `locking` arg", - )); - } - - let Expr::Lit(syn::ExprLit { - lit: Lit::Bool(lit), - .. - }) = value - else { - return Err(syn::Error::new_spanned(path, "expected `true` or `false`")); - }; - - locking = Some(lit.value); - } arg => { return Err(syn::Error::new_spanned( arg, - r#"expected `fixtures("", ...)` or `migrations = "" | false` or `migrator = ""` or `locking = true | false`"#, + r#"expected `fixtures("", ...)` or `migrations = "" | false` or `migrator = ""`"#, )) } } @@ -331,7 +304,6 @@ fn parse_args(attr_args: AttributeArgs) -> syn::Result { Ok(Args { fixtures, migrations, - locking, }) } diff --git a/sqlx-mysql/src/testing/mod.rs b/sqlx-mysql/src/testing/mod.rs index f509f9da45..09f24b8e27 100644 --- a/sqlx-mysql/src/testing/mod.rs +++ b/sqlx-mysql/src/testing/mod.rs @@ -178,6 +178,7 @@ async fn test_context(args: &TestArgs) -> Result, Error> { .clone() .database(&db_name), db_name, + locking: true, }) } diff --git a/sqlx-postgres/src/options/doc.md b/sqlx-postgres/src/options/doc.md index 33dd63b7a8..f1002ebbbb 100644 --- a/sqlx-postgres/src/options/doc.md +++ b/sqlx-postgres/src/options/doc.md @@ -48,6 +48,7 @@ Instead, the name is linked to the method to set the value. | Parameter | Default | |--------------------------------------------------------------|-------------------------------| | [`statement-cache-capacity`][Self::statement_cache_capacity] | `100` | +| [`sqlx-advisory-locking`][Self::advisory_locking] | `true` | # Example URLs ```text diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index 21e6628cae..9ee3723b14 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -30,6 +30,7 @@ pub struct PgConnectOptions { pub(crate) log_settings: LogSettings, pub(crate) extra_float_digits: Option>, pub(crate) options: Option, + pub(crate) advisory_locking: bool, } impl Default for PgConnectOptions { @@ -97,6 +98,7 @@ impl PgConnectOptions { extra_float_digits: Some("2".into()), log_settings: Default::default(), options: var("PGOPTIONS").ok(), + advisory_locking: true, } } @@ -452,6 +454,19 @@ impl PgConnectOptions { self } + /// Sets whether `#[sqlx::test]` should use advisory locking during test database setup. + /// + /// Defaults to `true`. Set to `false` for databases that speak the PostgreSQL wire + /// protocol but do not implement `pg_advisory_xact_lock`, such as CockroachDB. + /// + /// When disabled, advisory locking is also skipped for the migrator during test setup. + /// + /// Can be set in the connection URL: `postgres://localhost/mydb?sqlx-advisory-locking=false` + pub fn advisory_locking(mut self, advisory_locking: bool) -> Self { + self.advisory_locking = advisory_locking; + self + } + /// We try using a socket if hostname starts with `/` or if socket parameter /// is specified. pub(crate) fn fetch_socket(&self) -> Option { @@ -580,6 +595,11 @@ impl PgConnectOptions { pub fn get_options(&self) -> Option<&str> { self.options.as_deref() } + + /// Get whether advisory locking is enabled for test database setup. + pub fn get_advisory_locking(&self) -> bool { + self.advisory_locking + } } fn default_host(port: u16) -> String { diff --git a/sqlx-postgres/src/options/parse.rs b/sqlx-postgres/src/options/parse.rs index e911305698..05039099d3 100644 --- a/sqlx-postgres/src/options/parse.rs +++ b/sqlx-postgres/src/options/parse.rs @@ -104,6 +104,10 @@ impl PgConnectOptions { } } + "sqlx-advisory-locking" => { + options = options.advisory_locking(value.parse().map_err(Error::config)?); + } + _ => tracing::warn!(%key, %value, "ignoring unrecognized connect parameter"), } } @@ -340,3 +344,19 @@ fn built_url_can_be_parsed() { assert!(parsed.is_ok()); } + +#[test] +fn it_parses_advisory_locking_correctly() { + let url = "postgres://localhost?sqlx-advisory-locking=false"; + let opts = PgConnectOptions::from_str(url).unwrap(); + assert!(!opts.advisory_locking); + + let url = "postgres://localhost?sqlx-advisory-locking=true"; + let opts = PgConnectOptions::from_str(url).unwrap(); + assert!(opts.advisory_locking); + + // Default is true + let url = "postgres://localhost"; + let opts = PgConnectOptions::from_str(url).unwrap(); + assert!(opts.advisory_locking); +} diff --git a/sqlx-postgres/src/testing/mod.rs b/sqlx-postgres/src/testing/mod.rs index 4db31f97cc..f9e3a1acc2 100644 --- a/sqlx-postgres/src/testing/mod.rs +++ b/sqlx-postgres/src/testing/mod.rs @@ -93,6 +93,7 @@ async fn test_context(args: &TestArgs) -> Result, Error> { let url = dotenvy::var("DATABASE_URL").expect("DATABASE_URL must be set"); let master_opts = PgConnectOptions::from_str(&url).expect("failed to parse DATABASE_URL"); + let advisory_locking = master_opts.advisory_locking; let pool = PoolOptions::new() // Postgres' normal connection limit is 100 plus 3 superuser connections @@ -134,9 +135,9 @@ async fn test_context(args: &TestArgs) -> Result, Error> { // The lock and DDL must be in a single `execute` call so they share one implicit // transaction — `pg_advisory_xact_lock` is released at transaction end. // - // Can be disabled with `#[sqlx::test(locking = false)]` for databases that do not - // implement advisory locks, such as CockroachDB. - let lock_sql = if args.locking { + // Can be disabled with `?sqlx-advisory-locking=false` in DATABASE_URL for databases + // that do not implement advisory locks, such as CockroachDB. + let lock_sql = if advisory_locking { "select pg_advisory_xact_lock(8318549251334697844);" } else { "" @@ -196,6 +197,7 @@ async fn test_context(args: &TestArgs) -> Result, Error> { .clone() .database(&db_name), db_name, + locking: advisory_locking, }) } diff --git a/sqlx-sqlite/src/testing/mod.rs b/sqlx-sqlite/src/testing/mod.rs index 058a24c52b..c0a815e8e0 100644 --- a/sqlx-sqlite/src/testing/mod.rs +++ b/sqlx-sqlite/src/testing/mod.rs @@ -58,6 +58,7 @@ async fn test_context(args: &TestArgs) -> Result, Error> { // The main limitation is going to be the number of concurrent running tests. pool_opts: PoolOptions::new().max_connections(1000), db_name: db_path, + locking: true, }) } diff --git a/src/macros/test.md b/src/macros/test.md index 05372112df..6cae5a358b 100644 --- a/src/macros/test.md +++ b/src/macros/test.md @@ -228,7 +228,7 @@ apply and which to omit. However, since each fixture is applied separately (sent in an implicit `BEGIN` and `COMMIT`), you will want to make sure to order the fixtures such that foreign key requirements are always satisfied, or else you might get errors. -### Disabling Advisory Locking (requires `migrate` feature) +### Disabling Advisory Locking By default, `#[sqlx::test]` acquires a PostgreSQL advisory lock (`pg_advisory_xact_lock`) before creating the test database schema and also locks during migrations. This prevents a race condition when multiple tests run concurrently. @@ -237,23 +237,17 @@ Some databases speak the PostgreSQL wire protocol but do not implement advisory For example, CockroachDB does not support `pg_advisory_xact_lock` (see [cockroachdb/cockroach#13546](https://github.com/cockroachdb/cockroach/issues/13546)). -You can disable locking with the `locking` attribute: +You can disable advisory locking by adding `sqlx-advisory-locking=false` to your `DATABASE_URL`: -```rust,no_run -# #[cfg(all(feature = "migrate", feature = "postgres"))] -# mod example { -use sqlx::PgPool; - -#[sqlx::test(locking = false)] -async fn test_on_cockroachdb(pool: PgPool) -> sqlx::Result<()> { - // The test database was created without advisory locks. - // Migrations also run without advisory locks. - Ok(()) -} -# } +```text +DATABASE_URL="postgres://localhost/mydb?sqlx-advisory-locking=false" ``` -This follows the same pattern as [`Migrator::set_locking(false)`](crate::migrate::Migrator::set_locking). +This affects all `#[sqlx::test]` tests: both the test database schema setup and migrations +will skip advisory locks. No per-test annotation is needed. + +See also [`PgConnectOptions::advisory_locking()`](crate::postgres::PgConnectOptions::advisory_locking) +and [`Migrator::set_locking()`](crate::migrate::Migrator::set_locking). **Note:** Disabling locking means concurrent test processes may race during schema setup. The DDL statements use `IF NOT EXISTS` so this is generally safe, but you should be aware of the trade-off. diff --git a/tests/postgres/test-attr.rs b/tests/postgres/test-attr.rs index 6c6205cc7a..79bb84be7b 100644 --- a/tests/postgres/test-attr.rs +++ b/tests/postgres/test-attr.rs @@ -200,16 +200,15 @@ macro_rules! macro_using_test { } macro_using_test!("tests/postgres/migrations"); -// Verify that `locking = false` compiles and produces a valid test. +// Verifies that `?sqlx-advisory-locking=false` in DATABASE_URL is respected. // -// This test is NOT safe to run concurrently with other `#[sqlx::test]` tests against the -// same Postgres server because the advisory lock exists to guard a race in -// `CREATE SCHEMA IF NOT EXISTS`. The `locking = false` flag is intended for databases -// like CockroachDB that serialize DDL and don't support advisory locks. +// This test is #[ignore] because it cannot safely run concurrently with locked tests on +// the same Postgres server. The advisory lock guards a DDL race that only manifests under +// concurrency on standard PostgreSQL. The feature is intended for databases like CockroachDB +// that serialize DDL and don't support advisory locks. // -// We use `migrations = false` and `#[ignore]` so this compiles (proving the attribute -// parses correctly) but doesn't race with other tests in CI. -#[sqlx::test(migrations = false, locking = false)] +// To run: DATABASE_URL="postgres://...?sqlx-advisory-locking=false" cargo test -- --ignored it_works_without_locking +#[sqlx::test(migrations = false)] #[ignore] async fn it_works_without_locking(pool: PgPool) -> sqlx::Result<()> { let mut conn = pool.acquire().await?; From e7dc5f39f9cf61301cd7b7953c4f04c4d5f868ef Mon Sep 17 00:00:00 2001 From: Matjaz Domen Pecan Date: Sat, 28 Feb 2026 13:27:33 +0100 Subject: [PATCH 3/3] test(postgres): add multi-parameter URL case for advisory locking --- sqlx-postgres/src/options/parse.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sqlx-postgres/src/options/parse.rs b/sqlx-postgres/src/options/parse.rs index 05039099d3..54cdefdfde 100644 --- a/sqlx-postgres/src/options/parse.rs +++ b/sqlx-postgres/src/options/parse.rs @@ -355,6 +355,12 @@ fn it_parses_advisory_locking_correctly() { let opts = PgConnectOptions::from_str(url).unwrap(); assert!(opts.advisory_locking); + // Works after other parameters + let url = "postgres://localhost?application_name=myapp&sqlx-advisory-locking=false"; + let opts = PgConnectOptions::from_str(url).unwrap(); + assert!(!opts.advisory_locking); + assert_eq!(opts.application_name.as_deref(), Some("myapp")); + // Default is true let url = "postgres://localhost"; let opts = PgConnectOptions::from_str(url).unwrap();