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::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.conn.expect("transaction already consumed");
89            conn.commit_transaction().await
90        })
91    }
92
93    fn rollback(self: Box<Self>) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'static>> {
94        Box::pin(async move {
95            let mut conn = self.conn.expect("transaction already consumed");
96            conn.rollback_transaction().await
97        })
98    }
99
100    fn create_point<'a>(
101        &'a mut self,
102        name: &'a str,
103    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>> {
104        Box::pin(async move {
105            self.conn
106                .as_mut()
107                .ok_or_else(|| crate::error::EFError::transaction("transaction consumed"))?
108                .create_savepoint(name)
109                .await
110        })
111    }
112
113    fn release_point<'a>(
114        &'a mut self,
115        name: &'a str,
116    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>> {
117        Box::pin(async move {
118            self.conn
119                .as_mut()
120                .ok_or_else(|| crate::error::EFError::transaction("transaction consumed"))?
121                .release_savepoint(name)
122                .await
123        })
124    }
125
126    fn rollback_point<'a>(
127        &'a mut self,
128        name: &'a str,
129    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>> {
130        Box::pin(async move {
131            self.conn
132                .as_mut()
133                .ok_or_else(|| crate::error::EFError::transaction("transaction consumed"))?
134                .rollback_to_savepoint(name)
135                .await
136        })
137    }
138
139    fn set_isolation<'a>(
140        &'a mut self,
141        level: IsolationLevel,
142    ) -> Pin<Box<dyn Future<Output = EFResult<()>> + Send + 'a>> {
143        Box::pin(async move {
144            self.conn
145                .as_mut()
146                .ok_or_else(|| crate::error::EFError::transaction("transaction consumed"))?
147                .set_transaction_isolation(level)
148                .await
149        })
150    }
151
152    fn connection(&mut self) -> &mut (dyn IAsyncConnection + Send) {
153        self.conn
154            .as_mut()
155            .expect("transaction already consumed")
156            .as_mut()
157    }
158}