sqlx_transaction_manager/
context.rs

1use sqlx::{MySql, MySqlConnection, MySqlPool, Transaction};
2use std::ops::DerefMut;
3
4/// Transaction context wrapper providing type-safe transaction boundaries.
5///
6/// This struct wraps SQLx's `Transaction` and provides automatic rollback on drop
7/// if `commit()` is not explicitly called.
8///
9/// # Safety
10///
11/// If this struct is dropped without calling `commit()`, the transaction will be
12/// automatically rolled back. This prevents accidental commits when errors occur.
13///
14/// # Examples
15///
16/// ```rust,no_run
17/// use sqlx::MySqlPool;
18/// use sqlx_transaction_manager::TransactionContext;
19///
20/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21/// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
22/// let mut tx = TransactionContext::begin(&pool).await?;
23///
24/// // Perform database operations using tx.as_executor()
25/// // sqlx::query("INSERT INTO ...").execute(tx.as_executor()).await?;
26///
27/// // Explicitly commit the transaction
28/// tx.commit().await?;
29/// # Ok(())
30/// # }
31/// ```
32pub struct TransactionContext<'tx> {
33    tx: Option<Transaction<'tx, MySql>>,
34}
35
36impl<'tx> TransactionContext<'tx> {
37    /// Begins a new transaction from the connection pool.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the database connection fails or transaction cannot be started.
42    ///
43    /// # Examples
44    ///
45    /// ```rust,no_run
46    /// use sqlx::MySqlPool;
47    /// use sqlx_transaction_manager::TransactionContext;
48    ///
49    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
50    /// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
51    /// let mut tx = TransactionContext::begin(&pool).await?;
52    /// // Use the transaction...
53    /// tx.commit().await?;
54    /// # Ok(())
55    /// # }
56    /// ```
57    pub async fn begin(pool: &MySqlPool) -> crate::Result<Self> {
58        Ok(Self {
59            tx: Some(pool.begin().await?),
60        })
61    }
62
63    /// Commits the transaction.
64    ///
65    /// After calling this method, the `TransactionContext` is consumed and cannot be used.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the commit operation fails.
70    ///
71    /// # Examples
72    ///
73    /// ```rust,no_run
74    /// use sqlx::MySqlPool;
75    /// use sqlx_transaction_manager::TransactionContext;
76    ///
77    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
78    /// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
79    /// let mut tx = TransactionContext::begin(&pool).await?;
80    /// // ... perform operations
81    /// tx.commit().await?;
82    /// # Ok(())
83    /// # }
84    /// ```
85    pub async fn commit(mut self) -> crate::Result<()> {
86        if let Some(tx) = self.tx.take() {
87            tx.commit().await?;
88        }
89        Ok(())
90    }
91
92    /// Explicitly rolls back the transaction.
93    ///
94    /// Normally, rollback happens automatically when the `TransactionContext` is dropped
95    /// without calling `commit()`. This method allows explicit rollback for error handling.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the rollback operation fails.
100    ///
101    /// # Examples
102    ///
103    /// ```rust,no_run
104    /// use sqlx::MySqlPool;
105    /// use sqlx_transaction_manager::TransactionContext;
106    ///
107    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
108    /// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
109    /// let mut tx = TransactionContext::begin(&pool).await?;
110    /// // ... if something goes wrong
111    /// tx.rollback().await?;
112    /// # Ok(())
113    /// # }
114    /// ```
115    pub async fn rollback(mut self) -> crate::Result<()> {
116        if let Some(tx) = self.tx.take() {
117            tx.rollback().await?;
118        }
119        Ok(())
120    }
121
122    /// Returns a mutable reference to the underlying connection for use as an Executor.
123    ///
124    /// This method provides access to `&mut MySqlConnection`, which implements SQLx's
125    /// `Executor` trait. Use this when calling SQLx query methods or other libraries
126    /// that accept an executor.
127    ///
128    /// # Panics
129    ///
130    /// Panics if the transaction has already been consumed (committed or rolled back).
131    ///
132    /// # Examples
133    ///
134    /// ```rust,no_run
135    /// use sqlx::MySqlPool;
136    /// use sqlx_transaction_manager::TransactionContext;
137    ///
138    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
139    /// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
140    /// let mut tx = TransactionContext::begin(&pool).await?;
141    ///
142    /// sqlx::query("INSERT INTO users (name) VALUES (?)")
143    ///     .bind("Alice")
144    ///     .execute(tx.as_executor())
145    ///     .await?;
146    ///
147    /// tx.commit().await?;
148    /// # Ok(())
149    /// # }
150    /// ```
151    pub fn as_executor(&mut self) -> &mut MySqlConnection {
152        self.tx
153            .as_mut()
154            .expect("Transaction has already been consumed")
155            .deref_mut()
156    }
157
158    /// Consumes the context and returns the underlying SQLx `Transaction`.
159    ///
160    /// This is useful when you need direct access to SQLx's transaction API.
161    /// After calling this method, the `TransactionContext` cannot be used.
162    ///
163    /// # Panics
164    ///
165    /// Panics if the transaction has already been consumed.
166    ///
167    /// # Examples
168    ///
169    /// ```rust,no_run
170    /// use sqlx::MySqlPool;
171    /// use sqlx_transaction_manager::TransactionContext;
172    ///
173    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
174    /// # let pool = MySqlPool::connect("mysql://localhost/test").await?;
175    /// let tx_ctx = TransactionContext::begin(&pool).await?;
176    /// let tx = tx_ctx.into_inner();
177    /// // Use raw SQLx transaction...
178    /// tx.commit().await?;
179    /// # Ok(())
180    /// # }
181    /// ```
182    #[allow(dead_code)]
183    pub fn into_inner(mut self) -> Transaction<'tx, MySql> {
184        self.tx
185            .take()
186            .expect("Transaction has already been consumed")
187    }
188}
189
190impl<'tx> Drop for TransactionContext<'tx> {
191    /// Automatically rolls back the transaction if not committed.
192    ///
193    /// This ensures that uncommitted transactions are always rolled back,
194    /// preventing accidental commits when errors occur or when the transaction
195    /// context goes out of scope.
196    fn drop(&mut self) {
197        // If tx is Some, it means commit() was not called.
198        // SQLx's Transaction automatically rolls back on drop,
199        // so we don't need to do anything here.
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_transaction_context_can_be_created() {
209        // This test just ensures the struct can be instantiated
210        // Actual database tests require a connection pool
211    }
212}