Add ROLLBACK. Before a transaction changes a page for the first time, keep a copy of the page as it was: its before-image. ROLLBACK puts every before-image back, forgets the pages the transaction allocated past the end of the file, and leaves the database exactly as it was at BEGIN, tables created or dropped inside it included. Use the same copies to undo a single statement that fails after it changed pages, so a failed statement changes nothing even inside a transaction.
In the last stage a transaction could already be thrown away, by 01303 or by ending the session. You probably did it by forgetting the pages in memory and letting the next read fetch them from the file again, which works because nothing uncommitted ever reaches the file. That is a way out, not an answer. The user has no way to say "I changed my mind", and a statement that fails half way through still leaves its first half behind.
ROLLBACK is the user saying it. The answer this stage teaches is the one every engine uses in some form: before you change something, keep what it was. Here the something is a page, and the copy is its before-image. ROLLBACK puts the copies back.
It is worth doing properly now because the same copies solve a second problem. A statement that fails after it changed some pages, such as an UPDATE that reaches a damaged leaf after rewriting three healthy ones, has to change nothing. Inside a transaction you cannot throw the whole transaction away for that. You need to undo exactly one statement, and before-images taken at the right moment are exactly that.
The first time a transaction is about to change a page, it takes a copy. Later changes to the same page take nothing: the copy that matters is the page as it was at BEGIN.
ROLLBACK walks the transaction's pages. A page with a before-image gets it back. A page the transaction allocated past the end of the file has no image: it simply leaves, and the page count goes back to what it was. Nothing is written to the file: thanks to no-steal it never saw the changes. Do not get the old bytes back by reading the file again, though. It works today, and stops working when commits stop writing the file (the checkpoint stage): then the page at BEGIN may hold a commit the file does not have yet. The before-image always has it.
Since everything in droid is a page, this one mechanism undoes everything:
| Changed inside the transaction | Where it lives | Undone by |
|---|---|---|
| rows | leaves | their before-images |
| a split or a merge | leaves, internal nodes, new pages | images, and new pages leaving |
| the free list | the header and free pages | their images |
| CREATE TABLE, DROP TABLE, TRUNCATE | the catalog's tree, the table's pages | their images |
What is not a page is a copy of something a page holds, kept in memory for speed: the page count, a table's root or the catalog's root, the head and length of the free list, anything a statement cached about the schema. After ROLLBACK every such copy must agree with the restored pages again, either because it was put back with them or because it is read from them afresh. The transaction's own bookkeeping (its set of pages, its images, its state) is the other thing outside the pages, and ROLLBACK clears it.
PostgreSQL never needs before-images for rollback: an aborted transaction's row versions simply stay invisible. InnoDB keeps an undo log of row changes instead of whole pages. SQLite's rollback journal is the closest to this stage: before a page is first changed in a transaction, its original goes to the journal.
A statement is a small transaction of its own. Before it runs, note what the transaction had changed so far. If it fails part way, put back what it changed, then print its error: the transaction carries on with the statements before it, as if the failed one had never run.
One copy per page per transaction, taken just in time.
Put every page back as it was at BEGIN.
A statement is a transaction inside the transaction.
The full definition is on the output contract page, sections 32 and 34.
| Input | Output |
|---|---|
rollback; | Rollback statement executed successfully |
rollback; with no transaction open | [ERROR:01302] <message> |
| a statement that fails after it changed pages | its error, and nothing it did is left |
tokenize ROLLBACK; | [KEYWORD_ROLLBACK - ROLLBACK], [SYMBOL - ;] |
ast rollback; | Statement: ROLLBACK |
explain rollback; | [PLAN] rollback, [PLAN] └── Rollback |
After ROLLBACK, pager status shows the same total_pages as at BEGIN, and db header, db freelist, catalog list and btree structure print what they printed then.
$droid --db shop.dbWelcome to droid!>create table items (id int, label varchar(40));Create table statement executed successfully>insert into items (id, label) values (1, 'lamp');Insert statement executed successfully (1 rows)>begin;Begin statement executed successfully>update items set label = 'broken lamp' where id = 1;Update statement executed successfully (1 rows)[EXEC] rows_examined=1>create table orders (id int, item int);Create table statement executed successfully>catalog list[CATALOG] Tables: 2[CATALOG] items root=2 (id INT, label VARCHAR(40))[CATALOG] orders root=3 (id INT, item INT)>rollback;Rollback statement executed successfully>catalog list[CATALOG] Tables: 1[CATALOG] items root=2 (id INT, label VARCHAR(40))>select * from items;id | label1 | lamp(1 rows)[EXEC] rows_examined=1>rollback;[ERROR:01302] No transaction is open>.exitexiting.. good bye!
The UPDATE and the new table went back together: the catalog is a tree of pages, and its leaf had a before-image like any other.
total_pages is too large and the next allocation skips a page.The page above is the same for every language. These are the only language-specific hints.
memcpy into a buffer you own. A small array of (page number, buffer) per transaction is enough at this scale; free the buffers on commit and on rollback, or every transaction leaks 4 KB a page.