Skip to content

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.

05-transactions.zolo
// 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.

09-tagged-transaction.zolo
// 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.

enespt-br