Skip to main content

ferriorm_runtime/
transaction.rs

1//! Database transaction support.
2//!
3//! Provides [`run_transaction`] for executing a closure within a database
4//! transaction, and [`TransactionClient`] as a wrapper around sqlx transaction
5//! handles. The transaction is automatically committed on success or rolled
6//! back on error.
7
8use crate::client::DatabaseClient;
9use crate::error::FerriormError;
10
11/// Execute a closure within a database transaction.
12///
13/// The closure receives a [`TransactionClient`] and must return it alongside the
14/// result on success so the transaction can be committed. If the closure returns
15/// `Err`, the transaction is rolled back automatically (sqlx rolls back on drop).
16///
17/// # Example
18///
19/// ```ignore
20/// let result = run_transaction(&client, |tx| async move {
21///     // ... use tx for queries ...
22///     Ok((value, tx))
23/// }).await?;
24/// ```
25pub async fn run_transaction<F, Fut, T>(client: &DatabaseClient, f: F) -> Result<T, FerriormError>
26where
27    F: FnOnce(TransactionClient) -> Fut,
28    Fut: std::future::Future<Output = Result<(T, TransactionClient), FerriormError>>,
29{
30    match client {
31        #[cfg(feature = "postgres")]
32        DatabaseClient::Postgres(pool) => {
33            let tx = pool.begin().await?;
34            let tx_client = TransactionClient::Postgres(tx);
35            match f(tx_client).await {
36                Ok((result, tx_client)) => {
37                    tx_client.commit().await?;
38                    Ok(result)
39                }
40                Err(e) => {
41                    // Transaction is dropped here, which triggers auto-rollback.
42                    Err(e)
43                }
44            }
45        }
46        #[cfg(feature = "sqlite")]
47        DatabaseClient::Sqlite(pool) => {
48            let tx = pool.begin().await?;
49            let tx_client = TransactionClient::Sqlite(tx);
50            match f(tx_client).await {
51                Ok((result, tx_client)) => {
52                    tx_client.commit().await?;
53                    Ok(result)
54                }
55                Err(e) => {
56                    // Transaction is dropped here, which triggers auto-rollback.
57                    Err(e)
58                }
59            }
60        }
61    }
62}
63
64/// A client wrapper for use within transactions.
65pub enum TransactionClient {
66    #[cfg(feature = "postgres")]
67    Postgres(sqlx::Transaction<'static, sqlx::Postgres>),
68    #[cfg(feature = "sqlite")]
69    Sqlite(sqlx::Transaction<'static, sqlx::Sqlite>),
70}
71
72impl TransactionClient {
73    /// Commit the transaction.
74    pub async fn commit(self) -> Result<(), FerriormError> {
75        match self {
76            #[cfg(feature = "postgres")]
77            Self::Postgres(tx) => tx.commit().await.map_err(FerriormError::from),
78            #[cfg(feature = "sqlite")]
79            Self::Sqlite(tx) => tx.commit().await.map_err(FerriormError::from),
80        }
81    }
82
83    /// Rollback the transaction.
84    pub async fn rollback(self) -> Result<(), FerriormError> {
85        match self {
86            #[cfg(feature = "postgres")]
87            Self::Postgres(tx) => tx.rollback().await.map_err(FerriormError::from),
88            #[cfg(feature = "sqlite")]
89            Self::Sqlite(tx) => tx.rollback().await.map_err(FerriormError::from),
90        }
91    }
92}