Skip to main content

TestDbPools

Struct TestDbPools 

Source
pub struct TestDbPools { /* private fields */ }
Expand description

Test pool provider with a replica that is read-only by default.

This creates two separate connection pools from the same database:

  • Primary pool for writes (normal permissions)
  • Replica pool for reads (sets default_transaction_read_only = on)

This helps tests catch bugs where ordinary write operations are incorrectly routed through .read(). PostgreSQL will reject writes to non-temporary tables by default with errors such as: “cannot execute INSERT/UPDATE/DELETE in a read-only transaction”

This helper is not a security boundary. A client can override the default for an individual transaction or session, and PostgreSQL read-only transactions do not prohibit every possible write. See PostgreSQL’s SET TRANSACTION documentation for the exact restrictions.

§Usage with #[sqlx::test]

use sqlx_pool_registry::sqlx::{self, PgPool};
use sqlx_pool_registry::{PoolProvider, TestDbPools};

#[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 ordinary routing mistakes easier to detect in tests.

§Example

use sqlx_pool_registry::sqlx::{self, PgPool};
use sqlx_pool_registry::{PoolProvider, TestDbPools};

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: PgPool) -> Result<Self, 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 by default

The replica pool sets default_transaction_read_only = on, so ordinary writes to non-temporary tables fail unless the client overrides that default. This is a testing aid, not an authorization mechanism.

§Example
use sqlx_pool_registry::sqlx::{self, PgPool};
use sqlx_pool_registry::TestDbPools;

let pools = TestDbPools::new(pool).await?;

// Now ordinary write-through-read mistakes fail during tests

Trait Implementations§

Source§

impl Clone for TestDbPools

Source§

fn clone(&self) -> TestDbPools

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · 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

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

impl PoolProvider for TestDbPools

Source§

fn read(&self) -> &PgPool

Get a pool for read operations. Read more
Source§

fn write(&self) -> &PgPool

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<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

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> 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> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

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