Atomicity: Undo Logging & Rollback
Building a Database Engine: Transactions & Concurrency
Chapter 4 · Atomicity: Undo Logging & Rollback
This course opened with a real, verified bug: a two-step insert — write the row, then update the index — that leaves the table and its own index silently disagreeing if anything interrupts it between the two steps. Chapters 2 and 3 closed the durability half of that story: a crash before a write is applied is now recoverable. But Chapter 1's own original scenario wasn't a crash — it was one step succeeding and the next one failing outright. This chapter closes that half: wrap the whole operation in a transaction, and if any part of it fails, undo everything that already happened.
A Real Undo Log
Where Chapter 2's WAL recorded each page's new content, an undo log records each page's old content — captured the first time, and only the first time, a transaction touches it:
Making the write side of every table transparently participate needs no changes at all to Course 1's own HeapTable — only a thin wrapper implementing the exact same interface, so HeapTable genuinely has no idea it's being run inside a transaction:
Finding 1: Commit Keeps Changes, Rollback Fully Undoes Them
rollback() is called instead of commit(). Both pages come back byte-for-byte identical to their state before the transaction began — the writes are genuinely undone, not merely left inconsistent.
Finding 2: First-Touch Capture Restores the TRUE Original, Not an Intermediate State
A single transaction writes to the same page twice — once with content V1, then again with different content V2.
self.touched and skips capturing anything. After rollback(), the page matches its true pre-transaction state exactly, and is confirmed not equal to V1 — the intermediate state the page passed through partway through the transaction. Capturing on first touch only means rollback always reaches all the way back to where the transaction began, no matter how many times a page was rewritten along the way.
Finding 3: The Payoff — Genuinely Resolving Chapter 1's Own Bug
Chapter 1's own opening scenario, unmodified: insert a row, then update an index for it. This time, both steps run inside a transaction, and the index update is made to fail on purpose.
rollback(), a real scan() of the table finds zero rows. Compare this against Chapter 1's own Finding 1, where the exact same interrupted-operation shape left a row genuinely, permanently present but unindexed. Here, the row insertion itself is undone along with everything else — the table is back to exactly where it was before the attempt, with no inconsistency at all. Running the same operation with should_fail=False and calling commit() instead confirms the successful path still works correctly too: the row is present, and the index correctly points at its real location.
Finding 4: Why a Non-Deduped Undo Log Must Replay in Reverse
This chapter's own Transaction class sidesteps a subtle ordering problem entirely, by construction, since it never keeps more than one undo record per page. A log that captures every write — not just the first one per page — doesn't get that guarantee for free.
V1, then V2) are logged without deduplication — two separate undo entries: (before V1) and (before V2, i.e. V1). Replaying them in the order they were captured — the natural-looking first instinct — applies (before V1) first, then (before V2), landing the page on V1, not the true original. Replaying the exact same two records in reverse order lands correctly on the true pre-transaction state. "Undo" means walking history backward — a log that isn't already deduplicated to one entry per page depends on that being done explicitly, not assumed.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Genuinely atomic insert-plus-index-update | Chapter 1 (this course) — the exact scenario opened there is fully resolved here, closing the loop this entire course started with |
| Reverse-order replay for a non-deduped log | Chapter 3's own recover() — a structurally similar "replay records in the right order" concern, though redo (Chapter 3) and undo (this chapter) need opposite orderings for the same underlying reason |
| A newly-allocated page that can't be shrunk back on rollback | An honest, deliberate scope limitation of this engine — real systems solve this with a free-page/deallocation map, not attempted here |
Hands-On Exercises
Within a transaction, allocate a brand-new page (via TransactionalHeapFile.allocate_page()) and write a row to it, then call rollback(). Confirm num_pages() does not shrink back down (the file itself can't reclaim the space), but the page's own content is blanked and a real scan() finds no rows there.
Within a transaction, read a page (via TransactionalHeapFile.read_page()) but never write to it, then write to a different page and roll back. Confirm the undo log contains exactly one entry — for the written page only — and that the read-only page's own real content is completely unaffected by the rollback.
Write to the same page three times within one transaction (three genuinely different contents), then roll back. Confirm the undo log still contains only one entry (not three), and that the page ends up matching its true pre-transaction state, not any of the three intermediate versions.
📄 View solutionChapter 4 Quick Reference
- Undo log: captures each page's before-image, but only on its first touch within a transaction
- rollback(): restores every touched page to its pre-transaction state, in reverse order
- Verified Finding 1: rollback fully undoes writes across multiple pages
- Verified Finding 2: first-touch capture restores the TRUE original, not an intermediate state, no matter how many times a page was rewritten
- Verified Finding 3 — the payoff: wrapping Chapter 1's own insert-plus-index-update in a transaction and rolling back on failure leaves the table genuinely, completely unchanged — no more silent index/data inconsistency
- Verified Finding 4: a non-deduped undo log MUST replay in reverse order, or it lands on an intermediate state instead of the true original
- Next chapter: Isolation, Part 1 — locking, and a real, verified deadlock