Transactions
db.transaction(|tx| { ... }) wraps a block of operations in an ACID
transaction. The tx handle is interchangeable with db — you call
tx.execute, tx.query, etc. exactly as you would outside the transaction.
At the end of the block, Zolo performs an automatic commit. If a panic or unhandled error occurs inside the callback, the transaction is rolled back and the database returns to its previous state.
Atomic bank transfer: debit and credit on two accounts inside a single transaction.
// Feature: Database — transactions with `db.transaction(fn)`
// Syntax: the callback receives the same db; auto-commit on return,
// auto-rollback on panic or error.
// When to use: multi-step operations that must be atomic (bank
// transfer, batch insert).
use std::Database
let db = Database.open("sqlite://:memory:").unwrap()
defer db.close()
db.execute("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance REAL)").unwrap()
db.execute("INSERT INTO accounts VALUES (1, 100.0)").unwrap()
db.execute("INSERT INTO accounts VALUES (2, 50.0)").unwrap()
// Transfer: debit 1, credit 2 — atomic.
fn transfer(tx, from_id: int, to_id: int, amount: float) {
tx.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", [amount, from_id]).unwrap()
tx.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", [amount, to_id]).unwrap()
}
db.transaction(|tx| {
transfer(tx, 1, 2, 30.0)
})
let rows = db.query("SELECT id, balance FROM accounts ORDER BY id").unwrap()
for row in rows {
print("account {row.id}: {row.balance}")
}
// expected:
// account 1: 70
// account 2: 80
Requires the Zolo CLI/host — open in the playground or run locally.
The same transfer using sql"..." with interpolation — the captured values
are bound, not concatenated.
// Feature: `db.transaction(|tx| { ... })` mixed with `sql"..."`
// Syntax: the callback receives a tx handle interchangeable with `db`
// for `sql"..."` execution. Auto-commits on return; auto-rollback on
// panic or error.
// When to use: multi-step writes that must be atomic — bank transfers,
// inventory updates, rename + foreign-key fix-ups. Compare with the
// older form in `05-transactions.zolo` (positional binding).
use std::Database
let db = Database.open("sqlite://:memory:").unwrap()
defer db.close()
db.execute(/* sql */ "CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)").unwrap()
db.execute(/* sql */ "INSERT INTO accounts (id, balance) VALUES (1, 100)").unwrap()
db.execute(/* sql */ "INSERT INTO accounts (id, balance) VALUES (2, 50)").unwrap()
let from_id = 1
let to_id = 2
let amount = 30
// Both UPDATEs use `sql"..."` interpolation — captured values are bound.
db.transaction(|tx| {
sql"UPDATE accounts SET balance = balance - {amount} WHERE id = {from_id}".execute(tx).unwrap()
sql"UPDATE accounts SET balance = balance + {amount} WHERE id = {to_id}".execute(tx).unwrap()
})
let rows = sql"SELECT id, balance FROM accounts ORDER BY id".query(db).unwrap()
for row in rows {
print("{row.id}: {row.balance}")
}
// expected:
// 1: 70
// 2: 80
Requires the Zolo CLI/host — open in the playground or run locally.
Challenge
Simulate a failure in the middle of the transaction (e.g. error("insufficient balance"))
and verify that the balances remain unchanged after the rollback.
See also