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
use crate::;
/// 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`](https://www.postgresql.org/docs/current/sql-set-transaction.html)
/// documentation for the exact restrictions.
///
/// # Usage with `#[sqlx::test]`
///
/// ```rust,no_run
/// 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
///
/// ```rust,no_run
/// 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");
/// }
/// ```