1use crate::error::{SqlxError, SqlxResult};
4use crate::pool::SqlxPool;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum IsolationLevel {
9 ReadUncommitted,
11 ReadCommitted,
13 RepeatableRead,
15 Serializable,
17}
18
19impl IsolationLevel {
20 pub fn as_sql(&self) -> &'static str {
22 match self {
23 IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
24 IsolationLevel::ReadCommitted => "READ COMMITTED",
25 IsolationLevel::RepeatableRead => "REPEATABLE READ",
26 IsolationLevel::Serializable => "SERIALIZABLE",
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum AccessMode {
34 ReadWrite,
36 ReadOnly,
38}
39
40impl AccessMode {
41 pub fn as_sql(&self) -> &'static str {
43 match self {
44 AccessMode::ReadWrite => "READ WRITE",
45 AccessMode::ReadOnly => "READ ONLY",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Default)]
52pub struct TransactionOptions {
53 pub isolation_level: Option<IsolationLevel>,
55 pub access_mode: Option<AccessMode>,
57 pub deferrable: Option<bool>,
59}
60
61impl TransactionOptions {
62 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn isolation_level(mut self, level: IsolationLevel) -> Self {
69 self.isolation_level = Some(level);
70 self
71 }
72
73 pub fn access_mode(mut self, mode: AccessMode) -> Self {
75 self.access_mode = Some(mode);
76 self
77 }
78
79 pub fn deferrable(mut self, deferrable: bool) -> Self {
81 self.deferrable = Some(deferrable);
82 self
83 }
84
85 pub fn read_only() -> Self {
87 Self::new().access_mode(AccessMode::ReadOnly)
88 }
89
90 pub fn serializable() -> Self {
92 Self::new().isolation_level(IsolationLevel::Serializable)
93 }
94}
95
96#[cfg(feature = "postgres")]
113pub async fn with_transaction_pg<F, T>(pool: &sqlx::PgPool, f: F) -> SqlxResult<T>
114where
115 F: for<'c> FnOnce(
116 &'c mut sqlx::Transaction<'static, sqlx::Postgres>,
117 ) -> futures::future::BoxFuture<'c, SqlxResult<T>>,
118{
119 let mut tx = pool.begin().await?;
120 match f(&mut tx).await {
121 Ok(value) => {
122 tx.commit().await?;
123 Ok(value)
124 }
125 Err(err) => {
126 if let Err(rb) = tx.rollback().await {
127 tracing::warn!(error = %rb, "transaction rollback failed; connection drop will abort server-side");
128 }
129 Err(err)
130 }
131 }
132}
133
134#[cfg(feature = "mysql")]
140pub async fn with_transaction_mysql<F, T>(pool: &sqlx::MySqlPool, f: F) -> SqlxResult<T>
141where
142 F: for<'c> FnOnce(
143 &'c mut sqlx::Transaction<'static, sqlx::MySql>,
144 ) -> futures::future::BoxFuture<'c, SqlxResult<T>>,
145{
146 let mut tx = pool.begin().await?;
147 match f(&mut tx).await {
148 Ok(value) => {
149 tx.commit().await?;
150 Ok(value)
151 }
152 Err(err) => {
153 if let Err(rb) = tx.rollback().await {
154 tracing::warn!(error = %rb, "transaction rollback failed; connection drop will abort server-side");
155 }
156 Err(err)
157 }
158 }
159}
160
161#[cfg(feature = "sqlite")]
167pub async fn with_transaction_sqlite<F, T>(pool: &sqlx::SqlitePool, f: F) -> SqlxResult<T>
168where
169 F: for<'c> FnOnce(
170 &'c mut sqlx::Transaction<'static, sqlx::Sqlite>,
171 ) -> futures::future::BoxFuture<'c, SqlxResult<T>>,
172{
173 let mut tx = pool.begin().await?;
174 match f(&mut tx).await {
175 Ok(value) => {
176 tx.commit().await?;
177 Ok(value)
178 }
179 Err(err) => {
180 if let Err(rb) = tx.rollback().await {
181 tracing::warn!(error = %rb, "transaction rollback failed; connection drop will abort server-side");
182 }
183 Err(err)
184 }
185 }
186}
187
188pub async fn with_transaction<F, T>(pool: &SqlxPool, f: F) -> SqlxResult<T>
190where
191 F: FnOnce(&SqlxPool) -> futures::future::BoxFuture<'_, Result<T, SqlxError>>,
192{
193 f(pool).await
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_isolation_level_sql() {
205 assert_eq!(IsolationLevel::ReadUncommitted.as_sql(), "READ UNCOMMITTED");
206 assert_eq!(IsolationLevel::ReadCommitted.as_sql(), "READ COMMITTED");
207 assert_eq!(IsolationLevel::RepeatableRead.as_sql(), "REPEATABLE READ");
208 assert_eq!(IsolationLevel::Serializable.as_sql(), "SERIALIZABLE");
209 }
210
211 #[test]
212 fn test_access_mode_sql() {
213 assert_eq!(AccessMode::ReadWrite.as_sql(), "READ WRITE");
214 assert_eq!(AccessMode::ReadOnly.as_sql(), "READ ONLY");
215 }
216
217 #[test]
218 fn test_transaction_options_builder() {
219 let opts = TransactionOptions::new()
220 .isolation_level(IsolationLevel::Serializable)
221 .access_mode(AccessMode::ReadOnly)
222 .deferrable(true);
223
224 assert_eq!(opts.isolation_level, Some(IsolationLevel::Serializable));
225 assert_eq!(opts.access_mode, Some(AccessMode::ReadOnly));
226 assert_eq!(opts.deferrable, Some(true));
227 }
228
229 #[test]
230 fn test_transaction_options_presets() {
231 let read_only = TransactionOptions::read_only();
232 assert_eq!(read_only.access_mode, Some(AccessMode::ReadOnly));
233
234 let serializable = TransactionOptions::serializable();
235 assert_eq!(
236 serializable.isolation_level,
237 Some(IsolationLevel::Serializable)
238 );
239 }
240
241 #[cfg(feature = "sqlite")]
242 #[tokio::test]
243 async fn test_with_transaction_sqlite_commits_on_ok() {
244 let pool = sqlx::sqlite::SqlitePoolOptions::new()
247 .max_connections(1)
248 .connect("sqlite::memory:")
249 .await
250 .expect("in-memory sqlite needs no server");
251 sqlx::query("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
252 .execute(&pool)
253 .await
254 .unwrap();
255
256 with_transaction_sqlite(&pool, |tx| {
257 Box::pin(async move {
258 sqlx::query("INSERT INTO t (name) VALUES ('a')")
259 .execute(&mut **tx)
260 .await?;
261 Ok::<_, SqlxError>(())
262 })
263 })
264 .await
265 .expect("transaction commits");
266
267 let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM t")
268 .fetch_one(&pool)
269 .await
270 .unwrap();
271 assert_eq!(count, 1, "committed row must be visible after commit");
272 }
273
274 #[cfg(feature = "sqlite")]
275 #[tokio::test]
276 async fn test_with_transaction_sqlite_rolls_back_on_err() {
277 let pool = sqlx::sqlite::SqlitePoolOptions::new()
278 .max_connections(1)
279 .connect("sqlite::memory:")
280 .await
281 .expect("in-memory sqlite needs no server");
282 sqlx::query("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
283 .execute(&pool)
284 .await
285 .unwrap();
286
287 let result: SqlxResult<()> = with_transaction_sqlite(&pool, |tx| {
288 Box::pin(async move {
289 sqlx::query("INSERT INTO t (name) VALUES ('a')")
290 .execute(&mut **tx)
291 .await?;
292 Err(SqlxError::Internal("boom".into()))
293 })
294 })
295 .await;
296 assert!(result.is_err(), "closure error must propagate");
297
298 let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM t")
299 .fetch_one(&pool)
300 .await
301 .unwrap();
302 assert_eq!(count, 0, "rolled-back write must not persist");
303 }
304}