rent
Concepts

Transactions and rollback

Use an async closure for the common case:

let user = client
    .transaction(async |tx| {
        let user = tx.create_user().name("Grace").save().await?;
        tx.create_profile()
            .user_id(user.id)
            .bio("Compiler pioneer")
            .save()
            .await?;

        Ok(user)
    })
    .await?;

Rent commits when the closure returns Ok and rolls back when it returns Err.

Generated writes inside an existing transaction use savepoints. A failed write rolls back its own database changes before returning an error, including nested relationship changes and after-write hook failures. Catch a recoverable error to continue the transaction, or propagate it with ? to roll back the whole transaction. The transactions tutorial demonstrates both choices.

Each successful mutation scope adds SAVEPOINT and RELEASE SAVEPOINT commands; rollback adds ROLLBACK TO SAVEPOINT. Compound writes can contain nested scopes. These boundaries add latency, especially over a network, but do not add savepoints to ordinary pool-backed single-statement writes. Reads do not open mutation savepoints. Operations sharing a connection wait outside an active nested scope so one rollback cannot erase another operation's successful changes.

If a nested operation is cancelled or its savepoint cannot be finalized, the enclosing transaction cannot commit. Start a new transaction. Use the executor supplied to a nested runtime callback, not a captured parent executor that is waiting for that callback to finish.

Application services often need to return their own error type. transaction_app keeps that error intact:

let order = client
    .transaction_app(async |tx| {
        let order = create_order(tx, input).await?;

        inventory_service.reserve(order.id).await?;

        Ok(order)
    })
    .await?;

The result uses TransactionError<E> to distinguish a database failure, the application error E, and the rare case where both the application operation and rollback fail.

Choose explicit transaction characteristics when a workflow needs stronger consistency or a database-enforced read-only boundary:

use rent::driver::{IsolationLevel, TransactionOptions};

let options = TransactionOptions::new()
    .isolation(IsolationLevel::Serializable)
    .read_only()
    .deferrable();

let posts = client
    .transaction_with(options, async |tx| {
        tx.post()
            .published_eq(true)
            .all()
            .await
    })
    .await?;

The exact support is explicit:

CapabilityPostgreSQLMySQL and MariaDBSQLite
Read uncommittedYesYesNo
Read committedYesYesNo
Repeatable readYesYesNo
SerializableYesYesImmediate transaction
Read onlyYesYesNo
DeferrableYesNoNo

Rent applies MySQL and MariaDB isolation to the same pooled connection that begins the transaction. Unsupported combinations return a configuration error; Rent never silently weakens the requested boundary.

Retry a complete transaction after serialization failures or deadlocks:

let receipt = client
    .transaction_retry(3, async |tx| {
        reserve_inventory(tx, order_id).await
    })
    .await?;

Rent rolls back before retrying, uses bounded exponential backoff, and retries only errors the database marks as safe for a whole-transaction retry. The attempt budget includes the first attempt, and each retry starts a new transaction. Failures reported by BEGIN, the operation, or COMMIT are eligible; a rollback failure stops immediately because a clean boundary cannot be guaranteed.

Database characteristics and retry behavior are separate controls. Use transaction_retry_with for explicit TransactionOptions with the default backoff, or transaction_retry_with_policy to provide a TransactionRetryPolicy with custom attempt, delay, multiplier, ceiling, and jitter settings. The convenience policy starts at 10 ms, doubles up to one second, and applies full jitter.

Use the explicit API when the boundary itself is conditional:

let transaction = client.tx().await?;
let tx = transaction.client();
tx.create_audit_event()
    .message("reviewed")
    .save()
    .await?;

transaction.rollback().await?;

The lower-level driver transaction exposes named savepoints, rollback-to-savepoint, release, commit, and rollback.