Skip to content

Transactions

Run integrated SQL operations in an atomic datasource transaction.

Updated View as Markdown

transaction { ... } runs the integrated SQL operations in its body against one transaction.

model Account {
    @id
    id: int
    balance: int
}

fn transfer(from_id: int, to_id: int, amount: int): void ! QueryError {
    transaction {
        UPDATE Account
            SET balance = balance - amount
            WHERE id == from_id?
        UPDATE Account
            SET balance = balance + amount
            WHERE id == to_id?
    }
}

Normal fall-through and a successful return commit. An error exit rolls back. The runtime also rolls back when cancellation abandons an active transaction.

Lifecycle

Phase Behavior
enter resolve the route and begin a transaction
body run each query through the same transaction handle
success commit before leaving the block
error roll back before propagating the error
cancellation cleanup requests rollback and releases the handle

Begin, query, commit, and rollback are runtime operations. A begin or commit failure is reported as QueryError, so even a query-free transaction requires a fallible enclosing function.

Rules

  • All database operations in one block must resolve to the same datasource route.
  • A query-free block is permitted, but it still performs fallible begin/commit operations and therefore requires a fallible enclosing function.
  • Nested transaction blocks are rejected; savepoint syntax is not currently part of the language.
  • DataFusion routes are rejected because the current dataframe executor does not provide this transaction contract.
  • Reads observe writes already performed in the block.
  • Suspension is allowed, but holding a transaction while waiting on unrelated external work can increase contention.
  • The block is not a value expression. Fall through for void, or use a normal return to return from the surrounding function.
fn create_pair(): int ! QueryError {
    transaction {
        SAVE Item { id: 1, value: 10 }?
        SAVE Item { id: 2, value: 20 }?
        return 2
    }
}

Returning Err(...), using error, or propagating a failing query with ? leaves through the error path and rolls back. A commit failure itself is reported as QueryError.

Queries are routed by the model’s schema metadata. Every query in the block must resolve to the same route identity, not merely the same SQL dialect. This prevents a transaction from appearing atomic while crossing databases.

Returns and defers

A return inside the transaction commits before returning from the surrounding function:

fn create(item: Item): Item ! QueryError {
    transaction {
        saved := SAVE Item {
            id: item.id
            value: item.value
        }?
        return saved
    }
}

An error exit runs scope cleanup and rolls back. Keep defer cleanup idempotent and avoid performing a second database commit or rollback manually.

The transaction handle is compiler-managed. Application code cannot manually commit it, leak it from the block, or pass it to another task. A spawned task does not inherit ownership of the surrounding transaction.

External waits

Suspension is legal inside the block, including query execution itself. Waiting on unrelated network or task work while holding the transaction can extend locks and increase conflicts. Obtain external data before entering the block unless the operation’s consistency contract requires otherwise.

Retries

Serialization failures, deadlocks, connection loss, and selected timeouts can be transient. Retry the whole function containing the transaction, never only the last statement:

fn transfer_once(
    from_id: int,
    to_id: int,
    amount: int,
): void ! QueryError {
    transaction {
        UPDATE Account
            SET balance = balance - amount
            WHERE id == from_id?
        UPDATE Account
            SET balance = balance + amount
            WHERE id == to_id?
    }
}

The caller can match the returned QueryError and consult err.is_retryable(). Start a fresh transaction for each attempt so it observes a fresh snapshot, and apply bounded retries with backoff. Constraint, permission, read-only, and schema errors are not made correct by retrying.

Atoll currently has no nested transaction or savepoint syntax. Factor work into ordinary helper functions called from one outer transaction, and let errors propagate to that boundary.

Replicated datasources preserve the same source semantics through the cluster executor. The Raft proposal/capture protocol is runtime design and is documented outside the Language Guide.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close