Skip to main content

rust_ef/
transaction.rs

1//! Transaction handle abstraction.
2//!
3//! Decouples transaction capabilities from `DbContext` so callers can hold a
4//! typed handle (`Box<dyn ITransaction>`) returned by
5//! `DbContext::begin_transaction()`. The handle exposes `commit` / `rollback`
6//! (which consume it), savepoint operations, isolation level control, and a
7//! `connection()` accessor used internally by `save_changes()` to reuse the
8//! ambient transaction's connection.
9//!
10//! ## Two control modes
11//!
12//! - **Manual** — `let txn = ctx.begin_transaction().await?; ... txn.commit().await?;`
13//!   The handle is returned without registering an ambient; `save_changes()`
14//!   will self-manage its own transaction. Use this when you need the handle
15//!   for explicit savepoint / isolation control.
16//! - **Scoped** — `ctx.use_transaction(|ctx| Box::pin(async move { ... })).await?`
17//!   Registers the handle as ambient; `save_changes()` calls inside the
18//!   closure reuse the same transaction. Commits on `Ok`, rolls back on `Err`.
19
20use crate::error::{EFError, EFResult};
21use crate::provider::{IAsyncConnection, IsolationLevel};
22use std::future::Future;
23use std::pin::Pin;
24
25/// Transaction handle abstracting the Priority 2 connection-level transaction
26/// methods.
27///
28/// `commit` / `rollback` consume `self: Box<Self>` to make use-after-commit
29/// impossible at the type level. Savepoint and isolation methods take
30/// `&mut self` and return a boxed future borrowing the handle.
31///
32/// `Send + Sync` is required so that `Box<dyn ITransaction>` can be stored in
33/// `DbContext` (which DI registers as `Scoped`, requiring `Send + Sync`).
34pub trait ITransaction: Send + Sync {
35    /// Commits the transaction and consumes the handle.
36    fn commit(self: Box<Self>) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'static>>;
37
38    /// Rolls back the transaction and consumes the handle.
39    fn rollback(self: Box<Self>) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'static>>;
40
41    /// Creates a savepoint within the current transaction.
42    fn create_point<'a>(
43        &'a mut self,
44        name: &'a str,
45    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>>;
46
47    /// Releases (commits) a previously created savepoint.
48    fn release_point<'a>(
49        &'a mut self,
50        name: &'a str,
51    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>>;
52
53    /// Rolls back to the named savepoint, preserving the outer transaction.
54    fn rollback_point<'a>(
55        &'a mut self,
56        name: &'a str,
57    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>>;
58
59    /// Sets the isolation level of the current transaction.
60    fn set_isolation<'a>(
61        &'a mut self,
62        level: IsolationLevel,
63    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>>;
64
65    /// Exposes the underlying connection so `save_changes()` can reuse the
66    /// ambient transaction's connection.
67    fn connection(&mut self) -> &mut (dyn IAsyncConnection + Send);
68}
69
70/// Default `ITransaction` implementation wrapping an `IAsyncConnection`.
71///
72/// The connection is held in `Option` so `commit` / `rollback` can take it
73/// out by consuming the handle; subsequent savepoint/isolation calls on a
74/// consumed handle return `EFError::Transaction`.
75pub struct DbTransaction {
76    conn: Option<Box<dyn IAsyncConnection>>,
77}
78
79impl DbTransaction {
80    pub fn new(conn: Box<dyn IAsyncConnection>) -> Self {
81        Self { conn: Some(conn) }
82    }
83}
84
85impl ITransaction for DbTransaction {
86    fn commit(self: Box<Self>) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'static>> {
87        Box::pin(async move {
88            let mut conn = self
89                .conn
90                .ok_or_else(|| EFError::transaction("transaction already consumed"))?;
91            conn.commit_transaction().await
92        })
93    }
94
95    fn rollback(self: Box<Self>) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'static>> {
96        Box::pin(async move {
97            let mut conn = self
98                .conn
99                .ok_or_else(|| EFError::transaction("transaction already consumed"))?;
100            conn.rollback_transaction().await
101        })
102    }
103
104    fn create_point<'a>(
105        &'a mut self,
106        name: &'a str,
107    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>> {
108        Box::pin(async move {
109            self.conn
110                .as_mut()
111                .ok_or_else(|| crate::error::EFError::transaction("transaction consumed"))?
112                .create_savepoint(name)
113                .await
114        })
115    }
116
117    fn release_point<'a>(
118        &'a mut self,
119        name: &'a str,
120    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>> {
121        Box::pin(async move {
122            self.conn
123                .as_mut()
124                .ok_or_else(|| crate::error::EFError::transaction("transaction consumed"))?
125                .release_savepoint(name)
126                .await
127        })
128    }
129
130    fn rollback_point<'a>(
131        &'a mut self,
132        name: &'a str,
133    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>> {
134        Box::pin(async move {
135            self.conn
136                .as_mut()
137                .ok_or_else(|| crate::error::EFError::transaction("transaction consumed"))?
138                .rollback_to_savepoint(name)
139                .await
140        })
141    }
142
143    fn set_isolation<'a>(
144        &'a mut self,
145        level: IsolationLevel,
146    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>> {
147        Box::pin(async move {
148            self.conn
149                .as_mut()
150                .ok_or_else(|| crate::error::EFError::transaction("transaction consumed"))?
151                .set_transaction_isolation(level)
152                .await
153        })
154    }
155
156    fn connection(&mut self) -> &mut (dyn IAsyncConnection + Send) {
157        self.conn
158            .as_mut()
159            .expect("transaction already consumed")
160            .as_mut()
161    }
162}