Skip to main content

ferro_rs/database/
transaction.rs

1//! Transaction helpers for database operations
2//!
3//! Provides convenient ways to wrap database operations in transactions.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use ferro_rs::database::transaction;
9//!
10//! // Simple transaction
11//! let result = transaction(|txn| async move {
12//!     User::insert_one_with(user_data, &txn).await?;
13//!     Profile::insert_one_with(profile_data, &txn).await?;
14//!     Ok(())
15//! }).await?;
16//!
17//! // With return value
18//! let user = transaction(|txn| async move {
19//!     let user = User::insert_one_with(user_data, &txn).await?;
20//!     Ok(user)
21//! }).await?;
22//! ```
23
24use async_trait::async_trait;
25use sea_orm::{
26    AccessMode, DatabaseConnection, DatabaseTransaction, IsolationLevel, TransactionTrait,
27};
28use std::future::Future;
29
30use crate::database::DB;
31use crate::error::FrameworkError;
32
33/// Execute a closure within a database transaction
34///
35/// The transaction is automatically committed if the closure returns `Ok`,
36/// and rolled back if it returns `Err` or panics.
37///
38/// # Example
39///
40/// ```rust,ignore
41/// use ferro_rs::database::transaction;
42///
43/// // Create multiple records atomically
44/// transaction(|txn| async move {
45///     let user = user::ActiveModel {
46///         email: Set("test@example.com".to_string()),
47///         ..Default::default()
48///     };
49///     user.insert(txn).await?;
50///
51///     let profile = profile::ActiveModel {
52///         user_id: Set(user.id),
53///         ..Default::default()
54///     };
55///     profile.insert(txn).await?;
56///
57///     Ok(())
58/// }).await?;
59/// ```
60pub async fn transaction<F, T, Fut>(f: F) -> Result<T, FrameworkError>
61where
62    F: FnOnce(&DatabaseTransaction) -> Fut,
63    Fut: Future<Output = Result<T, FrameworkError>>,
64{
65    let db = DB::connection()?;
66    let txn = db
67        .inner()
68        .begin()
69        .await
70        .map_err(|e| FrameworkError::database(format!("Failed to begin transaction: {e}")))?;
71
72    match f(&txn).await {
73        Ok(result) => {
74            txn.commit()
75                .await
76                .map_err(|e| FrameworkError::database(format!("Failed to commit: {e}")))?;
77            Ok(result)
78        }
79        Err(e) => {
80            // Rollback is automatic when txn is dropped without commit
81            Err(e)
82        }
83    }
84}
85
86/// Execute a closure within a transaction with custom isolation level
87///
88/// # Example
89///
90/// ```rust,ignore
91/// use ferro_rs::database::transaction_with;
92/// use sea_orm::IsolationLevel;
93///
94/// transaction_with(IsolationLevel::Serializable, |txn| async move {
95///     // Operations that require serializable isolation
96///     Ok(())
97/// }).await?;
98/// ```
99pub async fn transaction_with<F, T, Fut>(
100    isolation_level: IsolationLevel,
101    f: F,
102) -> Result<T, FrameworkError>
103where
104    F: FnOnce(&DatabaseTransaction) -> Fut,
105    Fut: Future<Output = Result<T, FrameworkError>>,
106{
107    let db = DB::connection()?;
108    let txn = db
109        .inner()
110        .begin_with_config(Some(isolation_level), Some(AccessMode::ReadWrite))
111        .await
112        .map_err(|e| FrameworkError::database(format!("Failed to begin transaction: {e}")))?;
113
114    match f(&txn).await {
115        Ok(result) => {
116            txn.commit()
117                .await
118                .map_err(|e| FrameworkError::database(format!("Failed to commit: {e}")))?;
119            Ok(result)
120        }
121        Err(e) => Err(e),
122    }
123}
124
125/// Extension trait for running transactions on a database connection
126#[async_trait]
127pub trait TransactionExt {
128    /// Execute a closure within a transaction
129    ///
130    /// # Example
131    ///
132    /// ```rust,ignore
133    /// use ferro_rs::database::{DB, TransactionExt};
134    ///
135    /// DB::connection()?.transaction(|txn| async move {
136    ///     // Your transactional operations
137    ///     Ok(())
138    /// }).await?;
139    /// ```
140    async fn transaction<F, T, Fut>(&self, f: F) -> Result<T, FrameworkError>
141    where
142        F: FnOnce(&DatabaseTransaction) -> Fut + Send,
143        Fut: Future<Output = Result<T, FrameworkError>> + Send,
144        T: Send;
145
146    /// Execute a closure within a transaction with custom isolation level
147    async fn transaction_with<F, T, Fut>(
148        &self,
149        isolation_level: IsolationLevel,
150        f: F,
151    ) -> Result<T, FrameworkError>
152    where
153        F: FnOnce(&DatabaseTransaction) -> Fut + Send,
154        Fut: Future<Output = Result<T, FrameworkError>> + Send,
155        T: Send;
156}
157
158#[async_trait]
159impl TransactionExt for DatabaseConnection {
160    async fn transaction<F, T, Fut>(&self, f: F) -> Result<T, FrameworkError>
161    where
162        F: FnOnce(&DatabaseTransaction) -> Fut + Send,
163        Fut: Future<Output = Result<T, FrameworkError>> + Send,
164        T: Send,
165    {
166        let txn = self
167            .begin()
168            .await
169            .map_err(|e| FrameworkError::database(format!("Failed to begin transaction: {e}")))?;
170
171        match f(&txn).await {
172            Ok(result) => {
173                txn.commit()
174                    .await
175                    .map_err(|e| FrameworkError::database(format!("Failed to commit: {e}")))?;
176                Ok(result)
177            }
178            Err(e) => Err(e),
179        }
180    }
181
182    async fn transaction_with<F, T, Fut>(
183        &self,
184        isolation_level: IsolationLevel,
185        f: F,
186    ) -> Result<T, FrameworkError>
187    where
188        F: FnOnce(&DatabaseTransaction) -> Fut + Send,
189        Fut: Future<Output = Result<T, FrameworkError>> + Send,
190        T: Send,
191    {
192        let txn = self
193            .begin_with_config(Some(isolation_level), Some(AccessMode::ReadWrite))
194            .await
195            .map_err(|e| FrameworkError::database(format!("Failed to begin transaction: {e}")))?;
196
197        match f(&txn).await {
198            Ok(result) => {
199                txn.commit()
200                    .await
201                    .map_err(|e| FrameworkError::database(format!("Failed to commit: {e}")))?;
202                Ok(result)
203            }
204            Err(e) => Err(e),
205        }
206    }
207}
208
209/// Macro for cleaner transaction syntax
210///
211/// # Example
212///
213/// ```rust,ignore
214/// use ferro_rs::txn;
215///
216/// txn! {
217///     User::insert_one(user_data).await?;
218///     Profile::insert_one(profile_data).await?;
219///     Ok(())
220/// }
221/// ```
222#[macro_export]
223macro_rules! txn {
224    ($($body:tt)*) => {
225        $crate::database::transaction(|_txn| async move {
226            $($body)*
227        }).await
228    };
229}
230
231pub use txn;