# sqlx-record Transaction Skill Guide to the transaction! macro for ergonomic transactions. ## Triggers - "transaction", "transactions" - "commit", "rollback" - "atomic", "transactional" ## Overview The `transaction!` macro provides ergonomic transaction handling with automatic commit on success and rollback on error. ## Basic Syntax ```rust use sqlx_record::transaction; let result = transaction!(&pool, |tx| { // Operations using &mut *tx as executor user.insert(&mut *tx).await?; order.insert(&mut *tx).await?; Ok::<_, sqlx::Error>(order.id) // Return value type annotation }).await?; ``` ## Key Points 1. **Automatic commit**: Transaction commits if closure returns `Ok` 2. **Automatic rollback**: Transaction rolls back if closure returns `Err` or panics 3. **Return values**: The closure can return any value wrapped in `Result` 4. **Executor access**: Use `&mut *tx` to pass the transaction as an executor ## Usage Examples ### Basic Transaction ```rust use sqlx_record::{transaction, prelude::*}; async fn create_user_with_profile(pool: &Pool, user: User, profile: Profile) -> Result { transaction!(&pool, |tx| { let user_id = user.insert(&mut *tx).await?; let mut profile = profile; profile.user_id = user_id; profile.insert(&mut *tx).await?; Ok::<_, sqlx::Error>(user_id) }).await } ``` ### Multiple Operations ```rust async fn transfer_funds( pool: &Pool, from_id: &Uuid, to_id: &Uuid, amount: i64 ) -> Result<(), sqlx::Error> { transaction!(&pool, |tx| { // Debit from source Account::update_by_id(&mut *tx, from_id, Account::update_form().eval_balance(UpdateExpr::Sub(amount.into())) ).await?; // Credit to destination Account::update_by_id(&mut *tx, to_id, Account::update_form().eval_balance(UpdateExpr::Add(amount.into())) ).await?; // Create transfer record let transfer = Transfer { id: new_uuid(), from_account: *from_id, to_account: *to_id, amount, created_at: chrono::Utc::now().timestamp_millis(), }; transfer.insert(&mut *tx).await?; Ok::<_, sqlx::Error>(()) }).await } ``` ### With Error Handling ```rust async fn create_order(pool: &Pool, cart: Cart) -> Result { transaction!(&pool, |tx| { // Verify stock for item in &cart.items { let product = Product::get_by_id(&mut *tx, &item.product_id).await? .ok_or(sqlx::Error::RowNotFound)?; if product.stock < item.quantity { return Err(sqlx::Error::Protocol("Insufficient stock".into())); } } // Create order let order = Order { id: new_uuid(), user_id: cart.user_id, status: OrderStatus::PENDING.into(), total: cart.total(), created_at: chrono::Utc::now().timestamp_millis(), }; order.insert(&mut *tx).await?; // Create order items and decrement stock for item in cart.items { let order_item = OrderItem { id: new_uuid(), order_id: order.id, product_id: item.product_id, quantity: item.quantity, price: item.price, }; order_item.insert(&mut *tx).await?; Product::update_by_id(&mut *tx, &item.product_id, Product::update_form().eval_stock(UpdateExpr::Sub(item.quantity.into())) ).await?; } Ok::<_, sqlx::Error>(order) }).await.map_err(AppError::from) } ``` ### Nested Operations (Not Nested Transactions) ```rust // Helper function that accepts any executor async fn create_audit_log<'a, E>(executor: E, action: &str, entity_id: Uuid) -> Result<(), sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::MySql>, { let log = AuditLog { id: new_uuid(), action: action.into(), entity_id, created_at: chrono::Utc::now().timestamp_millis(), }; log.insert(executor).await?; Ok(()) } // Use in transaction transaction!(&pool, |tx| { user.insert(&mut *tx).await?; create_audit_log(&mut *tx, "user_created", user.id).await?; Ok::<_, sqlx::Error>(()) }).await?; ``` ## Type Annotation The closure must have an explicit return type annotation: ```rust // Correct - with type annotation Ok::<_, sqlx::Error>(value) // Also correct Ok::(42) // Incorrect - missing annotation (won't compile) // Ok(value) ``` ## Comparison with Manual Transactions ```rust // Manual approach let mut tx = pool.begin().await?; match async { user.insert(&mut *tx).await?; order.insert(&mut *tx).await?; Ok::<_, sqlx::Error>(order.id) }.await { Ok(result) => { tx.commit().await?; Ok(result) } Err(e) => { tx.rollback().await?; Err(e) } } // With transaction! macro - cleaner transaction!(&pool, |tx| { user.insert(&mut *tx).await?; order.insert(&mut *tx).await?; Ok::<_, sqlx::Error>(order.id) }).await ``` ## Notes - The macro works with all supported databases (MySQL, PostgreSQL, SQLite) - Transactions use the pool's default isolation level - For custom isolation levels, use sqlx's native transaction API - The closure is async - use `.await` for all database operations