Skip to content

Commit 014bb58

Browse files
committed
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).
1 parent d9b3340 commit 014bb58

File tree

5 files changed

+115
-11
lines changed

5 files changed

+115
-11
lines changed

sqlx-core/src/testing/mod.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub struct TestArgs {
6666
pub test_path: &'static str,
6767
pub migrator: Option<&'static Migrator>,
6868
pub fixtures: &'static [TestFixture],
69+
pub locking: bool,
6970
}
7071

7172
pub trait TestFn {
@@ -158,6 +159,7 @@ impl TestArgs {
158159
test_path,
159160
migrator: None,
160161
fixtures: &[],
162+
locking: true,
161163
}
162164
}
163165

@@ -168,6 +170,10 @@ impl TestArgs {
168170
pub fn fixtures(&mut self, fixtures: &'static [TestFixture]) {
169171
self.fixtures = fixtures;
170172
}
173+
174+
pub fn set_locking(&mut self, locking: bool) {
175+
self.locking = locking;
176+
}
171177
}
172178

173179
impl TestTermination for () {
@@ -255,7 +261,24 @@ async fn setup_test_db<DB: Database>(
255261
.await
256262
.expect("failed to connect to test database");
257263

258-
if let Some(migrator) = args.migrator {
264+
if let Some(static_migrator) = args.migrator {
265+
// When test locking is disabled, also disable locking in the migrator.
266+
// This is required for databases that don't support advisory locks (e.g. CockroachDB).
267+
let owned_migrator;
268+
let migrator = if args.locking {
269+
static_migrator
270+
} else {
271+
owned_migrator = Migrator {
272+
locking: false,
273+
migrations: static_migrator.migrations.clone(),
274+
ignore_missing: static_migrator.ignore_missing,
275+
no_tx: static_migrator.no_tx,
276+
table_name: static_migrator.table_name.clone(),
277+
create_schemas: static_migrator.create_schemas.clone(),
278+
};
279+
&owned_migrator
280+
};
281+
259282
migrator
260283
.run_direct(None, &mut conn)
261284
.await

sqlx-macros-core/src/test_attr.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use syn::parse::Parser;
66
struct Args {
77
fixtures: Vec<(FixturesType, Vec<syn::LitStr>)>,
88
migrations: MigrationsOpt,
9+
locking: Option<bool>,
910
}
1011

1112
#[cfg(feature = "migrate")]
@@ -143,6 +144,11 @@ fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> crate::Result<Tok
143144
fixtures.append(&mut res)
144145
}
145146

147+
let locking = args
148+
.locking
149+
.map(|value| quote! { args.set_locking(#value); })
150+
.unwrap_or_default();
151+
146152
let migrations = match args.migrations {
147153
MigrationsOpt::ExplicitPath(path) => {
148154
let migrator = crate::migrate::expand(Some(path))?;
@@ -180,6 +186,8 @@ fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> crate::Result<Tok
180186

181187
args.fixtures(&[#(#fixtures),*]);
182188

189+
#locking
190+
183191
// We need to give a coercion site or else we get "unimplemented trait" errors.
184192
let f: fn(#(#fn_arg_types),*) -> _ = #name;
185193

@@ -197,6 +205,7 @@ fn parse_args(attr_args: AttributeArgs) -> syn::Result<Args> {
197205

198206
let mut fixtures = Vec::new();
199207
let mut migrations = MigrationsOpt::InferredPath;
208+
let mut locking = None;
200209

201210
for arg in attr_args {
202211
let path = arg.path().clone();
@@ -292,10 +301,28 @@ fn parse_args(attr_args: AttributeArgs) -> syn::Result<Args> {
292301

293302
migrations = MigrationsOpt::ExplicitMigrator(lit.parse()?);
294303
}
304+
Meta::NameValue(MetaNameValue { value, .. }) if path.is_ident("locking") => {
305+
if locking.is_some() {
306+
return Err(syn::Error::new_spanned(
307+
path,
308+
"cannot have more than one `locking` arg",
309+
));
310+
}
311+
312+
let Expr::Lit(syn::ExprLit {
313+
lit: Lit::Bool(lit),
314+
..
315+
}) = value
316+
else {
317+
return Err(syn::Error::new_spanned(path, "expected `true` or `false`"));
318+
};
319+
320+
locking = Some(lit.value);
321+
}
295322
arg => {
296323
return Err(syn::Error::new_spanned(
297324
arg,
298-
r#"expected `fixtures("<filename>", ...)` or `migrations = "<path>" | false` or `migrator = "<rust path>"`"#,
325+
r#"expected `fixtures("<filename>", ...)` or `migrations = "<path>" | false` or `migrator = "<rust path>"` or `locking = true | false`"#,
299326
))
300327
}
301328
}
@@ -304,6 +331,7 @@ fn parse_args(attr_args: AttributeArgs) -> syn::Result<Args> {
304331
Ok(Args {
305332
fixtures,
306333
migrations,
334+
locking,
307335
})
308336
}
309337

sqlx-postgres/src/testing/mod.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,23 @@ async fn test_context(args: &TestArgs) -> Result<TestContext<Postgres>, Error> {
125125

126126
let mut conn = master_pool.acquire().await?;
127127

128+
// Explicit lock avoids this latent bug: https://stackoverflow.com/a/29908840
129+
// I couldn't find a bug on the mailing list for `CREATE SCHEMA` specifically,
130+
// but a clearly related bug with `CREATE TABLE` has been known since 2007:
131+
// https://www.postgresql.org/message-id/200710222037.l9MKbCJZ098744%40wwwmaster.postgresql.org
132+
// magic constant 8318549251334697844 is just 8 ascii bytes 'sqlxtest'.
133+
//
134+
// Can be disabled with `#[sqlx::test(locking = false)]` for databases that do not
135+
// implement advisory locks, such as CockroachDB.
136+
if args.locking {
137+
// language=PostgreSQL
138+
conn.execute("select pg_advisory_xact_lock(8318549251334697844)")
139+
.await?;
140+
}
141+
128142
// language=PostgreSQL
129143
conn.execute(
130-
// Explicit lock avoids this latent bug: https://stackoverflow.com/a/29908840
131-
// I couldn't find a bug on the mailing list for `CREATE SCHEMA` specifically,
132-
// but a clearly related bug with `CREATE TABLE` has been known since 2007:
133-
// https://www.postgresql.org/message-id/200710222037.l9MKbCJZ098744%40wwwmaster.postgresql.org
134-
// magic constant 8318549251334697844 is just 8 ascii bytes 'sqlxtest'.
135144
r#"
136-
select pg_advisory_xact_lock(8318549251334697844);
137-
138145
create schema if not exists _sqlx_test;
139146
140147
create table if not exists _sqlx_test.databases (
@@ -143,7 +150,7 @@ async fn test_context(args: &TestArgs) -> Result<TestContext<Postgres>, Error> {
143150
created_at timestamptz not null default now()
144151
);
145152
146-
create index if not exists databases_created_at
153+
create index if not exists databases_created_at
147154
on _sqlx_test.databases(created_at);
148155
149156
create sequence if not exists _sqlx_test.database_ids;

src/macros/test.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,34 @@ Multiple `fixtures` attributes can be used to combine different operating modes.
226226
<sup>3</sup>Ordering for test fixtures is entirely up to the application, and each test may choose which fixtures to
227227
apply and which to omit. However, since each fixture is applied separately (sent as a single command string, so wrapped
228228
in an implicit `BEGIN` and `COMMIT`), you will want to make sure to order the fixtures such that foreign key
229-
requirements are always satisfied, or else you might get errors.
229+
requirements are always satisfied, or else you might get errors.
230+
231+
### Disabling Advisory Locking (requires `migrate` feature)
232+
233+
By default, `#[sqlx::test]` acquires a PostgreSQL advisory lock (`pg_advisory_xact_lock`) before creating the test
234+
database schema and also locks during migrations. This prevents a race condition when multiple tests run concurrently.
235+
236+
Some databases speak the PostgreSQL wire protocol but do not implement advisory locks.
237+
For example, CockroachDB does not support `pg_advisory_xact_lock`
238+
(see [cockroachdb/cockroach#13546](https://github.com/cockroachdb/cockroach/issues/13546)).
239+
240+
You can disable locking with the `locking` attribute:
241+
242+
```rust,no_run
243+
# #[cfg(all(feature = "migrate", feature = "postgres"))]
244+
# mod example {
245+
use sqlx::PgPool;
246+
247+
#[sqlx::test(locking = false)]
248+
async fn test_on_cockroachdb(pool: PgPool) -> sqlx::Result<()> {
249+
// The test database was created without advisory locks.
250+
// Migrations also run without advisory locks.
251+
Ok(())
252+
}
253+
# }
254+
```
255+
256+
This follows the same pattern as [`Migrator::set_locking(false)`](crate::migrate::Migrator::set_locking).
257+
258+
**Note:** Disabling locking means concurrent test processes may race during schema setup. The DDL statements
259+
use `IF NOT EXISTS` so this is generally safe, but you should be aware of the trade-off.

tests/postgres/test-attr.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,19 @@ macro_rules! macro_using_test {
199199
};
200200
}
201201
macro_using_test!("tests/postgres/migrations");
202+
203+
// This should work without acquiring an advisory lock during test database setup.
204+
// Useful for databases that speak the PostgreSQL protocol but do not implement
205+
// pg_advisory_xact_lock, such as CockroachDB.
206+
#[sqlx::test(migrations = "tests/postgres/migrations", locking = false)]
207+
async fn it_works_without_locking(pool: PgPool) -> sqlx::Result<()> {
208+
let mut conn = pool.acquire().await?;
209+
210+
let db_name: String = sqlx::query_scalar("SELECT current_database()")
211+
.fetch_one(&mut *conn)
212+
.await?;
213+
214+
assert!(db_name.starts_with("_sqlx_test"), "dbname: {db_name:?}");
215+
216+
Ok(())
217+
}

0 commit comments

Comments
 (0)