sqlx_transaction_manager/
lib.rs

1//! # sqlx-transaction-manager
2//!
3//! A type-safe transaction management wrapper for SQLx with automatic commit/rollback.
4//!
5//! ## Features
6//!
7//! - **Automatic Rollback**: Transactions automatically roll back on drop if not explicitly committed
8//! - **Type-Safe**: Transaction boundaries are enforced at compile time
9//! - **Ergonomic API**: Simple `with_transaction` function for common use cases
10//! - **Nested Transactions**: Support for savepoints to simulate nested transactions
11//! - **Zero Runtime Overhead**: Thin wrapper around SQLx's native transaction support
12//!
13//! ## Quick Start
14//!
15//! Add to your `Cargo.toml`:
16//!
17//! ```toml
18//! [dependencies]
19//! sqlx = { version = "0.8", features = ["mysql", "runtime-tokio"] }
20//! sqlx-transaction-manager = "0.1"
21//! ```
22//!
23//! ## Examples
24//!
25//! ### Basic Transaction
26//!
27//! ```rust,no_run
28//! use sqlx::MySqlPool;
29//! use sqlx_transaction_manager::with_transaction;
30//!
31//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
32//! let pool = MySqlPool::connect("mysql://localhost/test").await?;
33//!
34//! with_transaction(&pool, |tx| {
35//!     Box::pin(async move {
36//!         sqlx::query("INSERT INTO users (name) VALUES (?)")
37//!             .bind("Alice")
38//!             .execute(tx.as_executor())
39//!             .await?;
40//!         Ok::<_, sqlx::Error>(())
41//!     })
42//! }).await?;
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! ### Multiple Operations
48//!
49//! ```rust,no_run
50//! use sqlx::MySqlPool;
51//! use sqlx_transaction_manager::with_transaction;
52//!
53//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
54//! # let pool = MySqlPool::connect("mysql://localhost/test").await?;
55//! let user_id = with_transaction(&pool, |tx| {
56//!     Box::pin(async move {
57//!         // Insert user
58//!         let result = sqlx::query("INSERT INTO users (name) VALUES (?)")
59//!             .bind("Bob")
60//!             .execute(tx.as_executor())
61//!             .await?;
62//!
63//!         let user_id = result.last_insert_id() as i64;
64//!
65//!         // Insert profile (same transaction)
66//!         sqlx::query("INSERT INTO profiles (user_id, bio) VALUES (?, ?)")
67//!             .bind(user_id)
68//!             .bind("Software Developer")
69//!             .execute(tx.as_executor())
70//!             .await?;
71//!
72//!         // Both operations commit together
73//!         Ok::<_, sqlx::Error>(user_id)
74//!     })
75//! }).await?;
76//!
77//! println!("Created user with ID: {}", user_id);
78//! # Ok(())
79//! # }
80//! ```
81//!
82//! ### Using with sqlx-named-bind
83//!
84//! This library works seamlessly with `sqlx-named-bind`:
85//!
86//! ```rust,no_run
87//! use sqlx::MySqlPool;
88//! use sqlx_transaction_manager::with_transaction;
89//! use sqlx_named_bind::PreparedQuery;
90//!
91//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
92//! # let pool = MySqlPool::connect("mysql://localhost/test").await?;
93//! with_transaction(&pool, |tx| {
94//!     Box::pin(async move {
95//!         let name = "Charlie".to_string();
96//!         let age = 30;
97//!
98//!         let mut query = PreparedQuery::new(
99//!             "INSERT INTO users (name, age) VALUES (:name, :age)",
100//!             |q, key| match key {
101//!                 ":name" => q.bind(name.clone()),
102//!                 ":age" => q.bind(age),
103//!                 _ => q,
104//!             }
105//!         )?;
106//!
107//!         query.execute(tx.as_executor()).await?;
108//!         Ok::<_, sqlx_named_bind::Error>(())
109//!     })
110//! }).await?;
111//! # Ok(())
112//! # }
113//! ```
114//!
115//! ### Nested Transactions (Savepoints)
116//!
117//! ```rust,no_run
118//! use sqlx::MySqlPool;
119//! use sqlx_transaction_manager::{with_transaction, with_nested_transaction};
120//!
121//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
122//! # let pool = MySqlPool::connect("mysql://localhost/test").await?;
123//! with_transaction(&pool, |tx| {
124//!     Box::pin(async move {
125//!         // Main transaction operations
126//!         sqlx::query("INSERT INTO users (name) VALUES (?)")
127//!             .bind("David")
128//!             .execute(tx.as_executor())
129//!             .await?;
130//!
131//!         // Nested transaction with savepoint
132//!         let _ = with_nested_transaction(tx, |nested_tx| {
133//!             Box::pin(async move {
134//!                 sqlx::query("INSERT INTO audit_log (action) VALUES (?)")
135//!                     .bind("User created")
136//!                     .execute(nested_tx.as_executor())
137//!                     .await?;
138//!                 Ok::<_, sqlx::Error>(())
139//!             })
140//!         }).await; // If this fails, only the audit log is rolled back
141//!
142//!         Ok::<_, sqlx::Error>(())
143//!     })
144//! }).await?;
145//! # Ok(())
146//! # }
147//! ```
148//!
149//! ### Manual Transaction Control
150//!
151//! For more control, use `TransactionContext` directly:
152//!
153//! ```rust,no_run
154//! use sqlx::MySqlPool;
155//! use sqlx_transaction_manager::TransactionContext;
156//!
157//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
158//! # let pool = MySqlPool::connect("mysql://localhost/test").await?;
159//! let mut tx = TransactionContext::begin(&pool).await?;
160//!
161//! sqlx::query("INSERT INTO users (name) VALUES (?)")
162//!     .bind("Eve")
163//!     .execute(tx.as_executor())
164//!     .await?;
165//!
166//! // Explicitly commit
167//! tx.commit().await?;
168//! # Ok(())
169//! # }
170//! ```
171//!
172//! ## Error Handling
173//!
174//! When an error occurs inside a transaction, it automatically rolls back:
175//!
176//! ```rust,no_run
177//! use sqlx::MySqlPool;
178//! use sqlx_transaction_manager::with_transaction;
179//!
180//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
181//! # let pool = MySqlPool::connect("mysql://localhost/test").await?;
182//! let result = with_transaction(&pool, |tx| {
183//!     Box::pin(async move {
184//!         sqlx::query("INSERT INTO users (name) VALUES (?)")
185//!             .bind("Frank")
186//!             .execute(tx.as_executor())
187//!             .await?;
188//!
189//!         // This will cause a rollback
190//!         return Err(sqlx::Error::RowNotFound);
191//!
192//!         #[allow(unreachable_code)]
193//!         Ok::<_, sqlx::Error>(())
194//!     })
195//! }).await;
196//!
197//! assert!(result.is_err());
198//! // The INSERT was rolled back, "Frank" is not in the database
199//! # Ok(())
200//! # }
201//! ```
202//!
203//! ## How It Works
204//!
205//! 1. **TransactionContext**: Wraps SQLx's `Transaction` and tracks its state
206//! 2. **Automatic Cleanup**: Uncommitted transactions are rolled back on drop
207//! 3. **Type Safety**: Consumed transactions can't be reused (enforced at compile time)
208//! 4. **Executor Access**: Provides `&mut MySqlConnection` for use with SQLx queries
209//!
210//! ## Limitations
211//!
212//! - Currently only supports MySQL (PostgreSQL and SQLite support planned)
213//! - Nested transactions use savepoints (MySQL limitation)
214//! - Error type is `sqlx_transaction_manager::Error` (wraps `sqlx::Error`)
215//!
216//! ## License
217//!
218//! Licensed under either of Apache License, Version 2.0 or MIT license at your option.
219
220pub mod context;
221pub mod error;
222pub mod executor;
223
224pub use context::TransactionContext;
225pub use error::{Error, Result};
226pub use executor::{with_nested_transaction, with_transaction};
227
228/// Convenience re-exports for common use cases
229pub mod prelude {
230    pub use crate::context::TransactionContext;
231    pub use crate::error::{Error, Result};
232    pub use crate::executor::{with_nested_transaction, with_transaction};
233}