TestDbPools

Struct TestDbPools 

Source
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

Source

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 separation

Trait Implementations§

Source§

impl Clone for TestDbPools

Source§

fn clone(&self) -> TestDbPools

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for TestDbPools

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl PoolProvider for TestDbPools

Source§

fn read(&self) -> &Pool<Postgres>

Get a pool for read operations. Read more
Source§

fn write(&self) -> &Pool<Postgres>

Get a pool for write operations. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

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
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,