Step 1 of 3 · Reading · ~4 min
Transaction Boundaries
Transactions & ACID
Grouping statements into one unit
Everything up to this chapter has treated each statement as its own independent event: an INSERT either happens or it doesn't, immediately. Real applications need more than that — "transfer money from account A to account B" is really two writes (debit A, credit B) that must succeed or fail together. That all-or-nothing grouping is what a transaction provides, and BEGIN / COMMIT / ROLLBACK are the commands that mark its boundaries.
Auto-commit: the implicit default
Without an explicit transaction, every statement is its own transaction — this is called auto-commit mode. INSERT INTO t VALUES (1) outside a BEGIN/COMMIT block takes effect immediately and permanently, as if it had been wrapped in an invisible BEGIN; INSERT ...; COMMIT;. This is the behavior every earlier lesson in this course has relied on. This lesson makes the transaction boundary explicit and controllable.
The three commands
BEGIN— starts an explicit transaction. From this point, writes are staged as "pending" rather than taking effect immediately — the visible effects of the transaction shouldn't be considered permanent untilCOMMIT.COMMIT— makes every change since the matchingBEGINpermanent, atomically, and ends the transaction. After this, the database is back in auto-commit mode.ROLLBACK— discards every change since the matchingBEGIN, as if none of it ever happened, and ends the transaction.
function begin():
if in_transaction:
return ERR "nested transaction"
in_transaction = true
snapshot = deep_copy(current_state) // or: start recording an undo log
return OK
function commit():
if not in_transaction:
return ERR "no transaction"
in_transaction = false
snapshot = None // changes are already applied; just stop tracking them
return OK
function rollback():
if not in_transaction:
return ERR "no transaction"
current_state = snapshot // restore pre-BEGIN state
in_transaction = false
return OK
Two implementation strategies
There are two common ways to make ROLLBACK actually undo changes, and either is reasonable for this exercise:
- Snapshot/copy-on-begin. When
BEGINruns, deep-copy whatever state your engine tracks (tables, rows). Writes during the transaction mutate the live state directly;COMMITsimply discards the snapshot (nothing to restore), andROLLBACKreplaces the live state with the snapshot. Simple to reason about, but costs memory/time proportional to database size on everyBEGIN— fine for a teaching exercise, not how real databases do it at scale. - Undo log. When
BEGINruns, start recording an undo entry for every write that happens (e.g., "this row didn't exist before" for an insert, or "this row had these old values" for an update/delete).COMMITdiscards the undo log.ROLLBACKreplays the undo log in reverse to restore prior state. This mirrors how real databases actually implement rollback, and connects directly to the WAL chapter — many engines log old values (undo records) right alongside the new values (redo records) in the same write-ahead log.
Either approach gets you correct BEGIN/COMMIT/ROLLBACK semantics for this exercise; the undo-log approach is worth understanding conceptually since it's the technique real systems use, and it's the natural extension point if this course's WAL work continues into transaction logging.
Nested BEGIN is an error, not a stack
The exercise spec is explicit: BEGIN while already inside a transaction is an error, not something that creates a nested/save-pointed sub-transaction. Real SQL does have SAVEPOINT for nested rollback points, but that's a separate, more advanced feature — for this lesson, one flat "am I in a transaction right now" boolean is exactly the right amount of state.
Edge cases
COMMITorROLLBACKwith no active transaction — should be a clean error, not a silent no-op and not a crash.- Auto-commit statements interacting with an open transaction — while inside
BEGIN...COMMIT, ordinaryINSERT/UPDATE/DELETEstatements should still each report their normalOK-style output, but their effects must only become permanent atCOMMIT(or vanish atROLLBACK) — don't let them auto-commit individually while a transaction is open. ROLLBACKcorrectly restoring reads — after rolling back, aSELECT/.countimmediately after should reflect the pre-BEGINstate exactly, not a partially-undone state.- A transaction with zero writes —
BEGINthenCOMMIT(orROLLBACK) with nothing in between should both succeed trivially, changing nothing.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…