Skip to main content

rorm_db/transaction/
mod.rs

1//! This module holds the definition of transactions
2
3use std::error::Error as StdError;
4use std::fmt;
5use std::future::Future;
6use std::ops::{Deref, DerefMut};
7
8use rorm_sql::DBImpl;
9use tracing::debug;
10
11use crate::internal::any::AnyTransaction;
12pub use crate::transaction::hook::TransactionHook;
13use crate::transaction::hook_closure::{ClosureHook, OnRollback, PostCommit, PreCommit};
14use crate::transaction::hook_storage::HookStorage;
15use crate::Error;
16
17mod hook;
18mod hook_closure;
19mod hook_storage;
20
21/// Transactions can be used to provide a safe way to execute multiple SQL operations
22/// after another with a way to go back to the start without something changed in the
23/// database.
24///
25/// Can be obtained using [`Database::start_transaction`](crate::Database::start_transaction).
26#[must_use = "A transaction needs to be committed."]
27pub struct Transaction {
28    pub(crate) sqlx: AnyTransaction,
29    hooks: Option<HookStorage>,
30}
31
32impl Transaction {
33    pub(crate) fn new(sqlx: AnyTransaction) -> Self {
34        Self { sqlx, hooks: None }
35    }
36
37    /// Gets database's sql dialect.
38    pub fn dialect(&self) -> DBImpl {
39        match self.sqlx {
40            #[cfg(feature = "postgres")]
41            AnyTransaction::Postgres(_) => DBImpl::Postgres,
42            #[cfg(feature = "sqlite")]
43            AnyTransaction::Sqlite(_) => DBImpl::SQLite,
44        }
45    }
46
47    /// This function commits the transaction.
48    pub async fn commit(mut self) -> Result<(), TransactionError> {
49        let mut hooks = self.hooks.take();
50
51        if let Some(hooks) = hooks.as_mut() {
52            hooks.pre_commit(&mut self).await?;
53
54            if let Some(invalid_hooks) = self.hooks.as_mut() {
55                debug!("Some transaction hook added additional hooks during pre-commit. This is not supported and will be ignored.");
56
57                // Prevent `Drop` impl from calling `on_rollback`.
58                invalid_hooks.clear();
59            }
60        }
61
62        let result = self.sqlx.commit().await;
63
64        if let Some(hooks) = hooks.as_mut() {
65            if result.is_ok() {
66                hooks.post_commit();
67
68                // Prevent `Drop` impl from calling `on_rollback`.
69                hooks.clear();
70            }
71        }
72
73        result
74            .map_err(Error::SqlxError)
75            .map_err(TransactionError::Database)
76    }
77
78    /// Use this function to abort the transaction.
79    pub async fn rollback(self) -> Result<(), Error> {
80        self.sqlx.rollback().await.map_err(Error::SqlxError)
81    }
82}
83
84// This impl should be on `Transaction` itself.
85// However, the `sqlx` field has to be consumed by ownership
86// which prevents `Transaction` from implementing `Drop`.
87impl Drop for HookStorage {
88    fn drop(&mut self) {
89        // `Transaction::commit` will clear all hooks, so this call would become a no-op.
90        self.on_rollback();
91    }
92}
93
94impl Transaction {
95    /// Accesses the simple API for adding hooks to the transaction
96    ///
97    /// If you reach the API's limits, consider [`Transaction::adv_hooks`].
98    pub fn hooks(&mut self) -> SimpleHooksApi<'_> {
99        SimpleHooksApi(self.hooks.get_or_insert_default())
100    }
101
102    /// Accesses the advanced API for adding hooks to the transaction
103    ///
104    /// If you're new to transaction hooks, consider [`Transaction::hooks`].
105    pub fn adv_hooks(&mut self) -> AdvancedHooksApi<'_> {
106        AdvancedHooksApi(self.hooks.get_or_insert_default())
107    }
108}
109
110/// Simple API for adding hooks to [`Transaction`]s
111///
112/// A hook is a closure which is called before or after a transaction has been commited.
113pub struct SimpleHooksApi<'a>(&'a mut HookStorage);
114impl SimpleHooksApi<'_> {
115    /// Adds an async closure which is run before the transaction is commited.
116    ///
117    /// Note, the transaction could still fail due to a database error or a hook error.
118    pub fn pre_commit<F>(&mut self, hook: impl FnOnce() -> F + Send + 'static) -> &mut Self
119    where
120        F: Future<Output = Result<(), TransactionError>> + Send,
121    {
122        self.0
123            .get_or_insert()
124            .push(ClosureHook::new(hook, PreCommit));
125        self
126    }
127
128    /// Adds a closure which is run after the transaction has been commited successfully.
129    pub fn post_commit(&mut self, hook: impl FnOnce() + Send + 'static) -> &mut Self {
130        self.0
131            .get_or_insert()
132            .push(ClosureHook::new(hook, PostCommit));
133        self
134    }
135
136    /// Adds a closure which is run when the transaction is rolled back.
137    ///
138    /// It MAY be called before, during or after the actual database operation.
139    pub fn on_rollback(&mut self, hook: impl FnOnce() + Send + 'static) -> &mut Self {
140        self.0
141            .get_or_insert()
142            .push(ClosureHook::new(hook, OnRollback));
143        self
144    }
145}
146
147/// Advanced API for adding hooks to [`Transaction`]s
148///
149/// A [`TransactionHook`] is a type which is called before and after a transaction has been finished.
150///
151/// A `Transaction` can store many instances of many `TransactionHook` types.
152///
153/// This API provides convenience methods for two common patters:
154/// - [`push`](Self::push) for adding many instances (potentially of the same type)
155/// - [`get_or_insert_default`](Self::get_or_insert_default) and [`get_or_insert_with`](Self::get_or_insert_with)
156///   when you only want a single instance of your hook type but want to extend it several times.
157///
158/// If these APIs are not flexible enough, you can use [`get_all`](Self::get_all) to access the raw
159/// storage of `TransactionHook`s of a single type.
160pub struct AdvancedHooksApi<'a>(&'a mut HookStorage);
161impl AdvancedHooksApi<'_> {
162    /// Adds a hook which is called if the transaction has been finished.
163    pub fn push<T: TransactionHook>(&mut self, hook: T) {
164        self.get_all().push(hook);
165    }
166
167    /// Gets the hook of type `T`.
168    ///
169    /// Adds its [`Default`] value if no value has been added yet.
170    pub fn get_or_insert_default<T: TransactionHook + Default>(&mut self) -> &mut T {
171        self.get_or_insert_with(T::default)
172    }
173
174    /// Gets the hook of type `T`.
175    ///
176    /// Calls `init` to add a value if no value has been added yet.
177    pub fn get_or_insert_with<T: TransactionHook>(&mut self, init: impl FnOnce() -> T) -> &mut T {
178        let vec = self.get_all();
179        if vec.is_empty() {
180            vec.push(init());
181        }
182        &mut vec[0]
183    }
184
185    /// Gets all hooks of type `T`.
186    pub fn get_all<T: TransactionHook>(&mut self) -> &mut Vec<T> {
187        self.0.get_or_insert()
188    }
189}
190
191/// Error for committing a [`Transaction`]
192#[derive(Debug)]
193pub enum TransactionError {
194    /// Error returned by the database
195    Database(Error),
196
197    /// Arbitrary error returned by a hook
198    Hook(HookError),
199}
200/// Arbitrary error returned by a hook
201pub type HookError = Box<dyn StdError + Send + Sync>;
202
203impl From<Error> for TransactionError {
204    fn from(value: Error) -> Self {
205        Self::Database(value)
206    }
207}
208impl From<HookError> for TransactionError {
209    fn from(value: HookError) -> Self {
210        Self::Hook(value)
211    }
212}
213impl fmt::Display for TransactionError {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        match self {
216            TransactionError::Database(x) => fmt::Display::fmt(x, f),
217            TransactionError::Hook(x) => fmt::Display::fmt(x, f),
218        }
219    }
220}
221impl StdError for TransactionError {
222    fn source(&self) -> Option<&(dyn StdError + 'static)> {
223        match self {
224            TransactionError::Database(x) => Some(x),
225            TransactionError::Hook(x) => Some(x.as_ref()),
226        }
227    }
228}
229
230/// Type alias to the old name
231#[deprecated(note = "Use `MaybeOwnedTransaction` instead")]
232pub type TransactionGuard<'a> = MaybeOwnedTransaction<'a>;
233
234/// Either an owned or borrowed [`Transaction`].
235#[must_use = "The owned variant needs to be committed."]
236pub enum MaybeOwnedTransaction<'a> {
237    /// An owned transaction
238    Owned(Transaction),
239
240    /// A borrowed transaction
241    Borrowed(&'a mut Transaction),
242}
243
244impl MaybeOwnedTransaction<'_> {
245    /// (Re-) Borrows the transaction
246    pub fn as_ref(&self) -> &Transaction {
247        &*self
248    }
249
250    /// (Re-) Borrows the transaction
251    pub fn as_mut(&mut self) -> &mut Transaction {
252        &mut *self
253    }
254
255    /// Get a reference to the guarded transaction
256    #[deprecated(note = "Use deref instead")]
257    pub fn get_transaction(&mut self) -> &mut Transaction {
258        &mut *self
259    }
260
261    /// Commits the potentially owned transaction.
262    pub async fn commit_if_owned(self) -> Result<(), TransactionError> {
263        if let Self::Owned(tr) = self {
264            tr.commit().await
265        } else {
266            Ok(())
267        }
268    }
269
270    /// Rolls back the potentially owned transaction.
271    pub async fn rollback_if_owned(self) -> Result<(), Error> {
272        if let Self::Owned(tr) = self {
273            tr.rollback().await
274        } else {
275            Ok(())
276        }
277    }
278
279    /// Consume the guard, committing the potentially owned transaction.
280    #[deprecated(note = "Use `commit_if_owned` instead")]
281    pub async fn commit(self) -> Result<(), TransactionError> {
282        self.commit_if_owned().await
283    }
284}
285
286impl Deref for MaybeOwnedTransaction<'_> {
287    type Target = Transaction;
288
289    fn deref(&self) -> &Self::Target {
290        match self {
291            Self::Owned(x) => x,
292            Self::Borrowed(x) => x,
293        }
294    }
295}
296
297impl DerefMut for MaybeOwnedTransaction<'_> {
298    fn deref_mut(&mut self) -> &mut Self::Target {
299        match self {
300            Self::Owned(x) => x,
301            Self::Borrowed(x) => x,
302        }
303    }
304}