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