pub struct TestDbPools { /* private fields */ }Expand description
Test pool provider with read-only replica enforcement.
This creates two separate connection pools from the same database:
- Primary pool for writes (normal permissions)
- Replica pool for reads (enforces
default_transaction_read_only = on)
This ensures tests catch bugs where write operations are incorrectly
routed through .read(). PostgreSQL will reject writes with:
“cannot execute INSERT/UPDATE/DELETE in a read-only transaction”
§Usage with #[sqlx::test]
use sqlx::PgPool;
use sqlx_pool_router::{TestDbPools, PoolProvider};
#[sqlx::test]
async fn test_read_write_routing(pool: PgPool) {
let pools = TestDbPools::new(pool).await.unwrap();
// Write operations work on .write()
sqlx::query("CREATE TEMP TABLE users (id INT)")
.execute(pools.write())
.await
.expect("Write pool should allow writes");
// Write operations FAIL on .read()
let result = sqlx::query("INSERT INTO users VALUES (1)")
.execute(pools.read())
.await;
assert!(result.is_err(), "Read pool should reject writes");
// Read operations work on .read()
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(pools.read())
.await
.expect("Read pool should allow reads");
}§Why This Matters
Without this test helper, you might accidentally route write operations through
.read() and not catch the bug until production when you have an actual replica
with replication lag. This helper makes the bug obvious immediately in tests.
§Example
use sqlx::PgPool;
use sqlx_pool_router::{TestDbPools, PoolProvider};
struct Repository<P: PoolProvider> {
pools: P,
}
impl<P: PoolProvider> Repository<P> {
async fn get_user(&self, id: i64) -> Result<String, sqlx::Error> {
sqlx::query_scalar("SELECT name FROM users WHERE id = $1")
.bind(id)
.fetch_one(self.pools.read())
.await
}
async fn create_user(&self, name: &str) -> Result<i64, sqlx::Error> {
sqlx::query_scalar("INSERT INTO users (name) VALUES ($1) RETURNING id")
.bind(name)
.fetch_one(self.pools.write())
.await
}
}
#[sqlx::test]
async fn test_repository_routing(pool: PgPool) {
let pools = TestDbPools::new(pool).await.unwrap();
let repo = Repository { pools };
// Test will fail if create_user incorrectly uses .read()
sqlx::query("CREATE TEMP TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
.execute(repo.pools.write())
.await
.unwrap();
let user_id = repo.create_user("Alice").await.unwrap();
let name = repo.get_user(user_id).await.unwrap();
assert_eq!(name, "Alice");
}Implementations§
Source§impl TestDbPools
impl TestDbPools
Sourcepub async fn new(pool: Pool<Postgres>) -> Result<TestDbPools, Error>
pub async fn new(pool: Pool<Postgres>) -> Result<TestDbPools, Error>
Create test pools from a single database pool.
This creates:
- A primary pool (clone of input) for writes
- A replica pool (new connection) configured as read-only
The replica pool enforces default_transaction_read_only = on,
so any write operations will fail with a PostgreSQL error.
§Example
use sqlx::PgPool;
use sqlx_pool_router::TestDbPools;
let pools = TestDbPools::new(pool).await?;
// Now you have pools that enforce read/write separationTrait Implementations§
Source§impl Clone for TestDbPools
impl Clone for TestDbPools
Source§fn clone(&self) -> TestDbPools
fn clone(&self) -> TestDbPools
Returns a duplicate of the value. Read more
1.0.0 · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreSource§impl Debug for TestDbPools
impl Debug for TestDbPools
Auto Trait Implementations§
impl Freeze for TestDbPools
impl !RefUnwindSafe for TestDbPools
impl Send for TestDbPools
impl Sync for TestDbPools
impl Unpin for TestDbPools
impl !UnwindSafe for TestDbPools
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
Converts
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
Converts
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more