Skip to content

29 · Database

Transactions

db.transaction guarantees atomicity: automatic commit on return, automatic rollback on error.

On this page

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::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::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: [Any] = 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.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br