Skip to main content

sqlx_pool_registry/
lib.rs

1//! # sqlx_pool_registry
2//!
3//! A lightweight library for routing database operations to different SQLx PostgreSQL connection pools
4//! based on whether they're read or write operations.
5//!
6//! This enables load distribution by routing read-heavy operations to read replicas while ensuring
7//! write operations always go to the primary database.
8//!
9//! ## Features
10//!
11//! - **Zero-cost abstraction**: Trait-based design with no runtime overhead
12//! - **Explicit routing**: Read/write intent stays visible at each database call site
13//! - **Backward compatible**: `PgPool` implements `PoolProvider` for seamless integration
14//! - **Flexible**: Use single pool or separate primary/replica pools
15//! - **SQLx compatibility**: `with-sqlx-0_8` is enabled by default; select
16//!   `with-sqlx-0_9` with default features disabled to use SQLx 0.9. Exactly
17//!   one SQLx compatibility feature must be enabled. The selected crate is
18//!   re-exported as [`sqlx`].
19//! - **Named pools (optional)**: Group independent providers by name with the
20//!   `with-named-pools` feature
21//! - **Test helpers**: [`TestDbPools`] for testing with `#[sqlx::test]`
22//! - **Well-tested**: Comprehensive test suite with replica routing verification
23//!
24//! ## Quick Start
25//!
26//! ### Single Pool (Development)
27//!
28//! ```rust,no_run
29//! use sqlx_pool_registry::sqlx::{self, PgPool};
30//! use sqlx_pool_registry::PoolProvider;
31//!
32//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
33//! let pool = PgPool::connect("postgresql://localhost/mydb").await?;
34//!
35//! // PgPool implements PoolProvider automatically
36//! let result: (i32,) = sqlx::query_as("SELECT 1")
37//!     .fetch_one(pool.read())
38//!     .await?;
39//! # Ok(())
40//! # }
41//! ```
42//!
43//! ### Read/Write Separation (Production)
44//!
45//! ```rust,no_run
46//! use sqlx_pool_registry::sqlx::{self, postgres::PgPoolOptions};
47//! use sqlx_pool_registry::{DbPools, PoolProvider};
48//!
49//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
50//! let primary = PgPoolOptions::new()
51//!     .max_connections(5)
52//!     .connect("postgresql://primary-host/mydb")
53//!     .await?;
54//!
55//! let replica = PgPoolOptions::new()
56//!     .max_connections(10)
57//!     .connect("postgresql://replica-host/mydb")
58//!     .await?;
59//!
60//! let pools = DbPools::with_replica(primary, replica);
61//!
62//! // Reads go to replica
63//! let users: Vec<(i32, String)> = sqlx::query_as("SELECT id, name FROM users")
64//!     .fetch_all(pools.read())
65//!     .await?;
66//!
67//! // Writes go to primary
68//! sqlx::query("INSERT INTO users (name) VALUES ($1)")
69//!     .bind("Alice")
70//!     .execute(pools.write())
71//!     .await?;
72//! # Ok(())
73//! # }
74//! ```
75//!
76//! ### Named Pools (Optional)
77//!
78//! Enable the `with-named-pools` feature to use `PoolRegistry`. A lookup
79//! returns one independent provider and never changes registry-wide state.
80//! The full example is available on the `PoolRegistry` API documentation when
81//! the feature is enabled.
82//!
83//! ## Architecture
84//!
85//! ```text
86//! ┌─────────────┐
87//! │   DbPools   │
88//! └──────┬──────┘
89//!        │
90//!   ┌────┴────┐
91//!   ↓         ↓
92//! ┌─────┐  ┌─────────┐
93//! │Primary│  │ Replica │ (optional)
94//! └─────┘  └─────────┘
95//! ```
96//!
97//! ## Generic Programming
98//!
99//! Make your types generic over `PoolProvider` to support both single and multi-pool configurations:
100//!
101//! ```rust
102//! use sqlx_pool_registry::{sqlx, PoolProvider};
103//!
104//! struct Repository<P: PoolProvider> {
105//!     pools: P,
106//! }
107//!
108//! impl<P: PoolProvider> Repository<P> {
109//!     async fn get_user(&self, id: i64) -> Result<String, sqlx::Error> {
110//!         // Read from replica
111//!         sqlx::query_scalar("SELECT name FROM users WHERE id = $1")
112//!             .bind(id)
113//!             .fetch_one(self.pools.read())
114//!             .await
115//!     }
116//!
117//!     async fn create_user(&self, name: &str) -> Result<i64, sqlx::Error> {
118//!         // Write to primary
119//!         sqlx::query_scalar("INSERT INTO users (name) VALUES ($1) RETURNING id")
120//!             .bind(name)
121//!             .fetch_one(self.pools.write())
122//!             .await
123//!     }
124//! }
125//! ```
126//!
127//! ## Testing
128//!
129//! Use [`TestDbPools`] with `#[sqlx::test]` to make ordinary write-through-read
130//! routing mistakes fail during tests:
131//!
132//! ```rust,no_run
133//! use sqlx_pool_registry::sqlx::{self, PgPool};
134//! use sqlx_pool_registry::{PoolProvider, TestDbPools};
135//!
136//! #[sqlx::test]
137//! async fn test_repository(pool: PgPool) {
138//!     let pools = TestDbPools::new(pool).await.unwrap();
139//!
140//!     // Ordinary writes through .read() fail unless read-only mode is overridden
141//!     let result = sqlx::query("INSERT INTO users VALUES (1)")
142//!         .execute(pools.read())
143//!         .await;
144//!     assert!(result.is_err());
145//! }
146//! ```
147//!
148//! This helps catch routing bugs without needing a real replica database.
149//! `TestDbPools` is a testing aid, not a security boundary: clients can override
150//! PostgreSQL's read-only default for an individual transaction or session.
151
152#[cfg(all(feature = "with-sqlx-0_8", feature = "with-sqlx-0_9"))]
153compile_error!("features `with-sqlx-0_8` and `with-sqlx-0_9` are mutually exclusive");
154
155#[cfg(not(any(feature = "with-sqlx-0_8", feature = "with-sqlx-0_9")))]
156compile_error!("enable exactly one SQLx feature: `with-sqlx-0_8` or `with-sqlx-0_9`");
157
158#[cfg(all(feature = "with-sqlx-0_8", not(feature = "with-sqlx-0_9")))]
159pub extern crate sqlx_0_8 as sqlx;
160
161#[cfg(all(feature = "with-sqlx-0_9", not(feature = "with-sqlx-0_8")))]
162pub extern crate sqlx_0_9 as sqlx;
163
164mod db_pools;
165mod provider;
166#[cfg(feature = "with-named-pools")]
167mod registry;
168mod test_db_pools;
169
170pub use db_pools::DbPools;
171pub use provider::PoolProvider;
172#[cfg(feature = "with-named-pools")]
173pub use registry::{PoolRegistry, UnknownPool};
174pub use test_db_pools::TestDbPools;