Step 1 of 4 · Reading · ~4 min
Learn
Transactions, Grouping, NULL
Transactions and ACID
By the end you can make several statements succeed or fail together, undo part of a transaction without losing the rest, and say what each letter of ACID guarantees. In SQL Fundamentals a constraint violation stopped one statement; here the question is what happens to the ones around it.
Autocommit: one statement, one transaction
A transaction is a group of statements treated as one unit. Outside an explicit one, SQLite runs each statement in its own transaction and commits it (makes it permanent) at once: autocommit. The sqlite3 shell prints a failing statement as Error: near line N: ... on stderr and carries on with the next, so a two-statement transfer can be half done:
CREATE TABLE wallets (owner TEXT PRIMARY KEY, coins INTEGER NOT NULL CHECK (coins >= 0));
INSERT INTO wallets VALUES ('ana', 50), ('bo', 10);
UPDATE wallets SET coins = coins + 70 WHERE owner = 'bo';
UPDATE wallets SET coins = coins - 70 WHERE owner = 'ana';
SELECT * FROM wallets ORDER BY owner;
Error: near line 4: CHECK constraint failed: wallets
ana|50
bo|80
Seventy coins appeared from nowhere: the credit was already committed when the debit failed.
BEGIN, COMMIT, ROLLBACK
BEGIN opens a transaction. Its changes stay provisional until COMMIT makes them permanent or ROLLBACK throws them all away. The same transfer, from ana 50, bo 10:
BEGIN;
UPDATE wallets SET coins = coins + 70 WHERE owner = 'bo';
UPDATE wallets SET coins = coins - 70 WHERE owner = 'ana';
SELECT * FROM wallets ORDER BY owner;
ROLLBACK;
SELECT * FROM wallets ORDER BY owner;
Error: near line 3: CHECK constraint failed: wallets
ana|50
bo|80
ana|50
bo|10
Lines 2-3 are the table before ROLLBACK, lines 4-5 after it. The failed statement was undone alone; the transaction stayed open with the credit in it. SQLite does not cancel it for you, so the script must notice and choose ROLLBACK; a COMMIT there would have stored bo's 80.
Table changes are transactional too. CREATE TABLE name AS SELECT ... makes a table filled with a query's rows; sqlite_master is SQLite's built-in catalog, one row per table, index and view:
BEGIN;
CREATE TABLE scratch AS SELECT owner, coins FROM wallets;
ROLLBACK;
SELECT COUNT(*) FROM sqlite_master WHERE name = 'scratch';
SELECT COUNT(*) FROM scratch;
0
Error: near line 5: no such table: scratch
Savepoints: undo only part
SAVEPOINT name marks a point inside a transaction. ROLLBACK TO name undoes everything since that mark but leaves the mark and the transaction open. RELEASE name forgets the mark and keeps the work. Marks nest, and rolling back to an outer mark discards every mark made after it. Only the outermost COMMIT makes anything permanent.
CREATE TABLE log (id INTEGER PRIMARY KEY, msg TEXT);
BEGIN;
INSERT INTO log (msg) VALUES ('start');
SAVEPOINT outer_sp;
INSERT INTO log (msg) VALUES ('a');
SAVEPOINT inner_sp;
INSERT INTO log (msg) VALUES ('b');
ROLLBACK TO inner_sp;
INSERT INTO log (msg) VALUES ('c');
SELECT group_concat(msg) FROM log;
ROLLBACK TO outer_sp;
SELECT group_concat(msg) FROM log;
COMMIT;
start,a,c
start
Rolling back to the inner mark dropped only b; rolling back to the outer one then dropped a and c together. The mark you name decides how far back you go.
ACID: what each letter guarantees
| Letter | Promise | How SQLite provides it |
|---|---|---|
| Atomicity | all or nothing | it saves a copy of the original data first, and restores it on ROLLBACK or after a crash |
| Consistency | only valid states | constraints are checked per statement; rules you cannot declare are yours |
| Isolation | no half-done work visible | a write lock, held by one connection at a time, orders the writers |
| Durability | committed data survives a crash | the file is synced to disk at COMMIT |
Isolation anomalies
| Anomaly | Timeline |
|---|---|
| Dirty read | B reads A's change before A commits |
| Non-repeatable read | A reads a row twice; B commits an update between |
| Phantom | A runs a range query twice; B commits an insert between |
| Lost update | A and B both read 10 and write back their own results; the second overwrites the first |
PostgreSQL's default (READ COMMITTED) prevents only the dirty read. SQLite acts as SERIALIZABLE, meaning as if transactions ran one after another, so it prevents all four; you choose only when the write lock is taken: plain BEGIN at the first write, BEGIN IMMEDIATE at once.
Common mistakes
ROLLBACKwhereROLLBACK TOwas meant - the whole transaction is gone, earlier work included, and a laterRELEASEfails withno such savepoint.- Rolling back to the wrong mark -
ROLLBACK TOan inner mark keeps everything before it, so half an order survives. - Logging the rejection before
ROLLBACK TO- the log row sits inside the undone span and vanishes with it.
Your exercise
The starter reserves stock for three orders and runs a price dry run; you supply the transaction control at nine TODO markers. For each oversell, decide how far back it must go: the reserved lines show whether you went too far or not far enough.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…