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}