Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Transactions

A transaction groups multiple database operations so they either all succeed or all fail. Toasty supports interactive transactions on SQL databases (SQLite, PostgreSQL, MySQL).

Tip: If you just need multiple operations to execute atomically, consider using batch operations first. Batch operations are atomic and more efficient — they can be sent as a single statement, avoiding the extra round-trips that interactive transactions require (begin, execute, commit). Use interactive transactions when you need to read data and branch on the results within the same atomic scope.

Starting a transaction

Call db.transaction() to begin a transaction:

#![allow(unused)]
fn main() {
use toasty::{Model, Executor};
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut tx = db.transaction().await?;

toasty::create!(User { name: "Alice" }).exec(&mut tx).await?;
toasty::create!(User { name: "Bob" }).exec(&mut tx).await?;

tx.commit().await?;
Ok(())
}
}

The transaction borrows &mut Db, preventing other operations on the same Db handle while the transaction is open. Pass &mut tx to query builders the same way you pass &mut db.

Running queries in a transaction

All the same operations work inside a transaction — creates, queries, updates, and deletes:

let mut tx = db.transaction().await?;

// Create
let user = toasty::create!(User { name: "Alice" }).exec(&mut tx).await?;

// Query
let users = User::all().exec(&mut tx).await?;

// Update
user.update().name("Bob").exec(&mut tx).await?;

// Delete
User::filter_by_id(user.id).delete().exec(&mut tx).await?;

tx.commit().await?;

Reads inside a transaction see the writes made earlier in the same transaction, even before commit:

let mut tx = db.transaction().await?;

toasty::create!(User { name: "Alice" }).exec(&mut tx).await?;

// This sees the record we just created
let users = User::all().exec(&mut tx).await?;
assert_eq!(users.len(), 1);

tx.commit().await?;

Commit and rollback

Call .commit() to save all changes made in the transaction:

let mut tx = db.transaction().await?;
toasty::create!(User { name: "Alice" }).exec(&mut tx).await?;
tx.commit().await?;

// The record is now visible outside the transaction
let users = User::all().exec(&mut db).await?;
assert_eq!(users.len(), 1);

Call .rollback() to discard all changes:

let mut tx = db.transaction().await?;
toasty::create!(User { name: "Alice" }).exec(&mut tx).await?;
tx.rollback().await?;

// The record was never persisted
let users = User::all().exec(&mut db).await?;
assert!(users.is_empty());

Auto-rollback on drop

If a transaction is dropped without calling .commit() or .rollback(), it automatically rolls back. This means you don’t need explicit rollback when an error occurs — just let the transaction go out of scope:

let mut tx = db.transaction().await?;
toasty::create!(User { name: "Alice" }).exec(&mut tx).await?;
// tx is dropped here without commit — changes are rolled back

This is useful in functions that return Result. If an operation inside the transaction fails with ?, the transaction is dropped and rolled back:

async fn transfer(db: &mut Db) -> toasty::Result<()> {
    let mut tx = db.transaction().await?;

    // If this fails, tx is dropped and rolled back
    let user = User::get_by_id(&mut tx, &1).await?;
    user.update().balance(user.balance - 100).exec(&mut tx).await?;

    let other = User::get_by_id(&mut tx, &2).await?;
    other.update().balance(other.balance + 100).exec(&mut tx).await?;

    tx.commit().await?;
    Ok(())
}

Nested transactions

Call .transaction() on an existing transaction to create a nested transaction. Nested transactions use database savepoints:

let mut tx = db.transaction().await?;
toasty::create!(User { name: "Alice" }).exec(&mut tx).await?;

{
    let mut nested = tx.transaction().await?;
    toasty::create!(User { name: "Bob" }).exec(&mut nested).await?;
    nested.commit().await?; // releases the savepoint
}

tx.commit().await?; // commits both Alice and Bob

Rolling back a nested transaction only undoes the work done inside it. The outer transaction continues:

let mut tx = db.transaction().await?;
toasty::create!(User { name: "Alice" }).exec(&mut tx).await?;

{
    let mut nested = tx.transaction().await?;
    toasty::create!(User { name: "Bob" }).exec(&mut nested).await?;
    nested.rollback().await?; // rolls back to savepoint — Bob is discarded
}

tx.commit().await?; // only Alice is committed

Nested transactions also auto-rollback on drop, just like top-level transactions.

Transaction options

Use transaction_builder() to configure a transaction before starting it:

use toasty::IsolationLevel;

let mut tx = db.transaction_builder()
    .isolation(IsolationLevel::Serializable)
    .read_only(true)
    .begin()
    .await?;

Isolation levels

Toasty supports four isolation levels:

LevelDescription
ReadUncommittedAllows dirty reads
ReadCommittedOnly reads committed data
RepeatableReadConsistent reads within the transaction
SerializableFull isolation between transactions

Driver support varies. SQLite only supports Serializable. PostgreSQL and MySQL support all four levels.

Read-only transactions

Set .read_only(true) to create a read-only transaction. The database rejects write operations inside a read-only transaction.