{"scenario":"mysql/01-basics/aborted-transaction.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"After an error inside a transaction, MySQL keeps the transaction alive — only the failed statement is rolled back, and everything else commits normally.","sessions":["A"],"errors":["1062"]}
{"scenario":"mysql/01-basics/atomicity.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"A transaction that fails halfway leaves zero partial writes — even statements that already succeeded inside it are undone.","sessions":["A","B"],"errors":["3819"]}
{"scenario":"mysql/01-basics/autocommit-visibility.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Without BEGIN, every statement commits instantly and is immediately visible to everyone; inside BEGIN, changes stay private until COMMIT.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/01-basics/savepoint-nesting.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Rolling back to an outer savepoint discards inner savepoints along with their work; RELEASE keeps the changes but forfeits the rollback point.","sessions":["A"],"errors":["1305"]}
{"scenario":"mysql/01-basics/savepoint-recovery.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"ROLLBACK TO SAVEPOINT discards only the work done after the savepoint — the rest of the transaction commits normally.","sessions":["A"],"errors":["1062"]}
{"scenario":"mysql/02-isolation/circular-information-flow.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"At READ UNCOMMITTED two concurrent transactions can each read the other's uncommitted write (Adya's G1c, \"circular information flow\") — an exchange no serial order can explain. READ COMMITTED makes it impossible.","sessions":["A","B"],"errors":[],"anomaly":"G1c"}
{"scenario":"mysql/02-isolation/current-reads.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"A REPEATABLE READ transaction's UPDATE operates on the CURRENT committed row, not on its snapshot — and afterwards the transaction sees its own write, so the 'repeatable' read changes.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/02-isolation/dirty-read.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"At READ UNCOMMITTED, MySQL serves other transactions' uncommitted changes — including values that are later rolled back and thus never existed.","sessions":["A","B"],"errors":[],"anomaly":"G1a","isolation":"READ UNCOMMITTED"}
{"scenario":"mysql/02-isolation/dirty-write.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"InnoDB always takes exclusive row locks for writes, so two transactions repricing the same rows cannot interleave their writes (Adya's G0, \"dirty write\") at any isolation level — even the one that allows dirty reads.","sessions":["A","B"],"errors":[],"anomaly":"G0","isolation":"READ UNCOMMITTED"}
{"scenario":"mysql/02-isolation/intermediate-read.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"At READ UNCOMMITTED a reader can observe an intermediate value the writer later overwrites before committing (Adya's G1b, \"intermediate read\") — a number that is never part of any committed state. READ COMMITTED stops this.","sessions":["A","B"],"errors":[],"anomaly":"G1b"}
{"scenario":"mysql/02-isolation/lost-update-read-committed.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Two read-modify-write transactions at READ COMMITTED can silently overwrite each other: two deposits of 10 grow the balance by only 10.","sessions":["A","B"],"errors":[],"anomaly":"P4","isolation":"READ COMMITTED"}
{"scenario":"mysql/02-isolation/lost-update-repeatable-read.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"The interleaving that loses an update at READ COMMITTED loses it at REPEATABLE READ too — MySQL raises no error, unlike PostgreSQL's 40001. The fix must be a locking read or an atomic UPDATE.","sessions":["A","B"],"errors":[],"anomaly":"P4"}
{"scenario":"mysql/02-isolation/non-repeatable-read.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"At READ COMMITTED, a transaction can read two different values for the same row — other transactions' commits become visible between its statements.","sessions":["A","B"],"errors":[],"anomaly":"P2","isolation":"READ COMMITTED"}
{"scenario":"mysql/02-isolation/observed-transaction-vanishes.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"At READ UNCOMMITTED a reader can see a later transaction's uncommitted overwrite of one row while relying on the overwritten transaction's other row (Adya's OTV, \"observed transaction vanishes\") — the committed transaction is visible only in pieces. READ COMMITTED restores atomic visibility.","sessions":["A","B","C"],"errors":[],"anomaly":"OTV"}
{"scenario":"mysql/02-isolation/phantom-read.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"At READ COMMITTED, re-running the same range query inside one transaction can return rows that weren't there before — phantoms.","sessions":["A","B"],"errors":[],"anomaly":"PMP","isolation":"READ COMMITTED"}
{"scenario":"mysql/02-isolation/read-skew.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"At READ COMMITTED, a transaction reading two rows around a concurrent transfer observes money that never existed (Adya's G-single, \"read skew\"); REPEATABLE READ's transaction-wide snapshot keeps plain SELECTs consistent.","sessions":["A","B"],"errors":[],"anomaly":"G-single"}
{"scenario":"mysql/02-isolation/stable-snapshot.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"At REPEATABLE READ — MySQL's default — plain SELECTs use a single snapshot for the entire transaction: no non-repeatable reads and no phantoms.","sessions":["A","B"],"errors":[],"anomaly":"P2"}
{"scenario":"mysql/02-isolation/update-recheck.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"An UPDATE at READ COMMITTED that waited for a lock re-evaluates its WHERE clause against the new row version — rows that no longer match are silently skipped.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/02-isolation/write-predicate-skew.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Under REPEATABLE READ a write's WHERE clause is evaluated against the CURRENT committed data while SELECTs keep showing the snapshot — so a transaction can \"delete every row matching X\", delete nothing, and still see rows matching X (Hermitage's G-single write-predicate anomaly).","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/02-isolation/write-skew-rr.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Two REPEATABLE READ transactions can each validate an invariant against their snapshots, write to different rows, and both commit — leaving the invariant broken. This is write skew.","sessions":["A","B"],"errors":[],"anomaly":"G2-item"}
{"scenario":"mysql/02-isolation/write-skew-serializable.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Under MySQL's SERIALIZABLE, plain SELECTs take shared locks, so the write-skew interleaving deadlocks: one transaction is rolled back with errno 1213, and the invariant survives.","sessions":["A","B"],"errors":["1213"],"anomaly":"G2","isolation":"SERIALIZABLE"}
{"scenario":"mysql/03-locking/alter-table-outage.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"An ALTER TABLE queued behind one long transaction blocks every later query on that table — even plain SELECTs — because they must queue behind its exclusive metadata-lock request.","sessions":["A","B","C","M"],"errors":[]}
{"scenario":"mysql/03-locking/ddl-lock-timeout.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"With lock_wait_timeout set, a migration that can't get its metadata lock fails fast (errno 1205) instead of queueing — and other sessions' queries are never blocked behind it.","sessions":["A","B","C"],"errors":["1205"]}
{"scenario":"mysql/03-locking/deadlock-avoidance.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"The same two opposite-direction transfers cannot deadlock if both transactions lock the rows in the same (id) order first — the second simply waits its turn.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/03-locking/deadlock.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Two transactions locking the same rows in opposite order deadlock; InnoDB detects the cycle instantly and rolls back one of them with errno 1213 so the other can finish.","sessions":["A","B"],"errors":["1213"]}
{"scenario":"mysql/03-locking/fk-shared-lock.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"INSERTing a child row locks the referenced parent row with a plain shared lock: even a non-key UPDATE of the parent blocks until the child commits (PostgreSQL's FOR KEY SHARE would allow it), and deleting a referenced parent fails outright.","sessions":["A","B"],"errors":["1451"]}
{"scenario":"mysql/03-locking/for-update-blocks.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"A row locked with SELECT FOR UPDATE can still be read by everyone, but any write to it waits until the locking transaction ends.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/03-locking/gap-locks.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Under REPEATABLE READ a locking read on a range takes next-key locks — the matching rows AND the gaps between them — so INSERTs into the range block until the reader finishes. Under READ COMMITTED only the found rows are locked and the same INSERT sails through.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/03-locking/lock-mode-matrix.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"InnoDB row locks come in exactly two strengths — S locks coexist with each other but block writers, X locks block everything. There is no PostgreSQL-style four-mode ladder.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/03-locking/lock-queue.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Sessions waiting for the same row lock pile up behind the holder, visible live in sys.innodb_lock_waits — and every queued update lands once the holder commits.","sessions":["A","B","C","M"],"errors":[]}
{"scenario":"mysql/03-locking/lock-timeout.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"With innodb_lock_wait_timeout set, a statement waits for a row lock only that long, then fails with errno 1205 — only the statement is rolled back, the transaction survives.","sessions":["A","B"],"errors":["1205"]}
{"scenario":"mysql/03-locking/monitoring-locks.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"data_locks shows every lock a transaction holds — a single-row UPDATE takes an intention lock on the table plus a record lock on the row — and a waiter shows up as WAITING, findable via sys.innodb_lock_waits.","sessions":["A","B","M"],"errors":[]}
{"scenario":"mysql/03-locking/nowait.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"SELECT ... FOR UPDATE NOWAIT refuses to wait: if the row is locked it fails immediately with errno 3572 instead of joining the lock queue.","sessions":["A","B"],"errors":["3572"]}
{"scenario":"mysql/03-locking/skip-locked.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"SELECT ... FOR UPDATE SKIP LOCKED silently skips locked rows, so concurrent workers each grab a different row without ever waiting — the backbone of SQL job queues.","sessions":["A","B","C","D"],"errors":[]}
{"scenario":"mysql/04-mvcc/history-list-length.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"While any read view stays open, purge cannot remove the undo history behind it: every committed write transaction adds to the history list, and the history list length — InnoDB's bloat metric — grows for as long as the reader lives.","sessions":["A","R"],"errors":[]}
{"scenario":"mysql/04-mvcc/read-views.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"BEGIN does not take the snapshot — the transaction's first read does. START TRANSACTION WITH CONSISTENT SNAPSHOT takes it immediately. And until it writes, a transaction doesn't even get a real transaction ID — readers are cheap by design.","sessions":["A","R"],"errors":[]}
{"scenario":"mysql/04-mvcc/undo-logs.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"A committed DELETE removes rows only from the current version of the table. A transaction whose read view predates it keeps reading the old versions, rebuilt on demand from the undo log — long after the \"real\" rows are gone.","sessions":["A","R"],"errors":[]}
{"scenario":"mysql/05-patterns/advisory-locks.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"GET_LOCK serializes application-defined work with no table involved: timeout 0 gives an instant yes/no, a positive timeout gives a bounded wait, and the lock ignores transactions entirely — only RELEASE_LOCK (or disconnecting) lets go.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/05-patterns/check-then-insert-race.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"\"SELECT first, INSERT if absent\" cannot enforce uniqueness: two concurrent requests both see 0 rows, both insert, and the duplicate lands without any error.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/05-patterns/fix-lost-update-atomic.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"UPDATE ... SET balance = balance + 10 reads and writes in one statement: the second writer waits for the first and stacks on top — both deposits survive, at any isolation level.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/05-patterns/fix-lost-update-for-update.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"When the new value must be computed in application code, SELECT ... FOR UPDATE serializes the read-modify-write: the second reader waits and then reads the committed 110 — a locking read pierces the snapshot by design — so both deposits land.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/05-patterns/fix-lost-update-version-column.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"A version column makes the lost update detectable instead of silent: the stale write matches 0 rows, and retrying against the new version lands both deposits — no locks held while the app thinks.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/05-patterns/idempotency-key.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"An INSERT on an idempotency key reports 1 affected row exactly once: the first request charges, every retry — even one racing the original in flight — gets 0 affected rows and charges nothing.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/05-patterns/implicit-commit.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Every DDL statement performs an implicit COMMIT of the current transaction first: after a mid-transaction CREATE INDEX, the \"uncommitted\" work is permanently committed and a later ROLLBACK undoes nothing.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/05-patterns/job-queue.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"The full worker loop — claim with FOR UPDATE SKIP LOCKED, work, mark done, commit — never double-processes a job and never loses one: a worker crash returns its job to the queue automatically.","sessions":["A","B"],"errors":[]}
{"scenario":"mysql/05-patterns/on-duplicate-key.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"A UNIQUE constraint makes the concurrent duplicate wait for the first insert's fate and then fail with errno 1062 — and ON DUPLICATE KEY UPDATE turns that error into a clean upsert (INSERT IGNORE absorbs it, at a price).","sessions":["A","B"],"errors":["1062"]}
{"scenario":"mysql/05-patterns/retry-deadlocks.ts","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Errno 1213 is transient, not fatal: the victim's transaction rerun from the top sees the survivor's committed state and succeeds — withRetry needs exactly two attempts here.","sessions":["A","B"],"errors":["1213"]}
{"scenario":"mysql/06-distributed/dual-write-problem.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Two systems, two writes, no shared transaction: whichever write goes first, a failure between them leaves the database and the broker permanently disagreeing.","sessions":["App"],"errors":["3819"]}
{"scenario":"mysql/06-distributed/saga-compensation.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"A saga replaces one distributed transaction with a chain of local ones: every step commits immediately and is visible to everyone, and a failed step is undone by a new compensating transaction — not by ROLLBACK.","sessions":["Saga","Reader"],"errors":[]}
{"scenario":"mysql/06-distributed/transactional-outbox.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Writing the order and its event in one transaction makes them atomic, and a SKIP LOCKED relay gives at-least-once delivery: a crashed relay redelivers the event instead of losing it.","sessions":["App","Relay"],"errors":[]}
{"scenario":"mysql/06-distributed/xa-transactions.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"XA PREPARE detaches a transaction from its session: it survives the session's death, keeps holding its locks, and any later session can finish it by name with XA COMMIT.","sessions":["A","B","M"],"errors":["3572","ERR_MYSQL_CONNECTION_CLOSED"]}
{"scenario":"mysql/08-production/deadlock-counter.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Every deadlock increments the server-wide lock_deadlocks counter in INNODB_METRICS — deadlocks are countable and alertable even if nobody saw the error, because the counter only ever goes up.","sessions":["A","B","M"],"errors":["1213"]}
{"scenario":"mysql/08-production/find-long-transactions.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Two joins on information_schema.innodb_trx find the forgotten transaction: the oldest open transaction by trx_started, and the session that holds one open while doing nothing — including the read-only kind that never shows up in write metrics but still pins purge.","sessions":["A","M"],"errors":[]}
{"scenario":"mysql/08-production/history-list-health.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"Two queries make undo bloat diagnosable: INNODB_METRICS' history list length says purge is falling behind, and a join on innodb_trx names the session whose old read view is holding it back.","sessions":["R","A","M"],"errors":[]}
{"scenario":"mysql/08-production/timeout-guardrails.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"max_execution_time cuts a runaway SELECT short without touching the session, and wait_timeout kills a connection that goes idle — rolling back whatever transaction it silently held open.","sessions":["A","B"],"errors":["ERR_MYSQL_CONNECTION_CLOSED"]}
{"scenario":"mysql/08-production/who-is-blocking-whom.yaml","engine":"mysql","product":"MySQL","version":"8.4.10","claim":"One query on sys.innodb_lock_waits names the waiter and the blocker, and a join to the processlist shows the blocker is sitting idle in transaction — and killing it drains the queue.","sessions":["A","B","M"],"errors":[]}
{"scenario":"postgres/01-basics/aborted-transaction.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"After any error inside a transaction, PostgreSQL rejects every further statement with 25P02 until you ROLLBACK.","sessions":["A"],"errors":["22012","25P02"]}
{"scenario":"postgres/01-basics/atomicity.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A transaction that fails halfway leaves zero partial writes — even statements that already succeeded inside it are undone.","sessions":["A","B"],"errors":["23514"]}
{"scenario":"postgres/01-basics/autocommit-visibility.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Without BEGIN, every statement commits instantly and is immediately visible to everyone; inside BEGIN, changes stay private until COMMIT.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/01-basics/ddl-rollback.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Ordinary DDL participates in the transaction like any other statement: a CREATE INDEX and a CREATE TABLE stay invisible until COMMIT, and a ROLLBACK removes the schema they created along with the rows.","sessions":["A","B"],"errors":["42P01"]}
{"scenario":"postgres/01-basics/savepoint-nesting.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Rolling back to an outer savepoint discards inner savepoints along with their work; RELEASE keeps the changes but forfeits the rollback point.","sessions":["A"],"errors":["3B001"]}
{"scenario":"postgres/01-basics/savepoint-recovery.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"ROLLBACK TO SAVEPOINT un-aborts a failed transaction, discarding only the work done after the savepoint — the rest commits normally.","sessions":["A"],"errors":["23505"]}
{"scenario":"postgres/02-isolation/circular-information-flow.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Two concurrent transactions cannot read each other's uncommitted writes (Adya's G1c, \"circular information flow\"): each sees the pre-existing value, as one serial order or the other would dictate.","sessions":["A","B"],"errors":[],"anomaly":"G1c"}
{"scenario":"postgres/02-isolation/concurrent-update-40001.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A REPEATABLE READ transaction that tries to UPDATE a row modified since its snapshot fails with SQLSTATE 40001 — immediately if the change is committed, or after waiting if it isn't.","sessions":["A","B"],"errors":["40001"],"anomaly":"P4","isolation":"REPEATABLE READ"}
{"scenario":"postgres/02-isolation/dirty-write.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Two transactions repricing the same rows cannot interleave their writes (Adya's G0, \"dirty write\"): the second writer waits for the first to finish, so the final state is one transaction's prices, never a mix.","sessions":["A","B"],"errors":[],"anomaly":"G0"}
{"scenario":"postgres/02-isolation/intermediate-read.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A transaction that changes a value twice exposes neither draft to other transactions (Adya's G1b, \"intermediate read\"): readers see the old value until commit, then only the final one.","sessions":["A","B"],"errors":[],"anomaly":"G1b"}
{"scenario":"postgres/02-isolation/lost-update-read-committed.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Two read-modify-write transactions at READ COMMITTED can silently overwrite each other: two deposits of 10 grow the balance by only 10.","sessions":["A","B"],"errors":[],"anomaly":"P4"}
{"scenario":"postgres/02-isolation/lost-update-repeatable-read.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"The interleaving that silently loses an update at READ COMMITTED fails loudly with SQLSTATE 40001 at REPEATABLE READ — no data is lost, the loser just retries.","sessions":["A","B"],"errors":["40001"],"anomaly":"P4","isolation":"REPEATABLE READ"}
{"scenario":"postgres/02-isolation/no-dirty-reads.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"PostgreSQL accepts READ UNCOMMITTED syntax but behaves as READ COMMITTED: uncommitted changes from other transactions are never visible.","sessions":["A","B"],"errors":[],"anomaly":"G1a","isolation":"READ UNCOMMITTED"}
{"scenario":"postgres/02-isolation/non-repeatable-read.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"At READ COMMITTED, a transaction can read two different values for the same row — other transactions' commits become visible between its statements.","sessions":["A","B"],"errors":[],"anomaly":"P2","isolation":"READ COMMITTED"}
{"scenario":"postgres/02-isolation/observed-transaction-vanishes.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Once a transaction commits, every reader sees ALL of its writes or none (Adya's OTV, \"observed transaction vanishes\"): a later overwrite of one row cannot make the rest of the committed transaction disappear.","sessions":["A","B","C"],"errors":[],"anomaly":"OTV"}
{"scenario":"postgres/02-isolation/phantom-read.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"At READ COMMITTED, re-running the same range query inside one transaction can return rows that weren't there before — phantoms.","sessions":["A","B"],"errors":[],"anomaly":"PMP","isolation":"READ COMMITTED"}
{"scenario":"postgres/02-isolation/read-only-anomaly.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"At REPEATABLE READ, even a read-only transaction can observe a state no serial execution could produce; at SERIALIZABLE, PostgreSQL aborts the offending writer instead (SQLSTATE 40001).","sessions":["Cashier","Closer","Report"],"errors":["40001"]}
{"scenario":"postgres/02-isolation/read-skew.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"At READ COMMITTED, a transaction reading two rows around a concurrent transfer observes money that never existed (Adya's G-single, \"read skew\"); REPEATABLE READ's single snapshot makes the same interleaving consistent.","sessions":["A","B"],"errors":[],"anomaly":"G-single","isolation":"REPEATABLE READ"}
{"scenario":"postgres/02-isolation/stable-snapshot.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"At REPEATABLE READ, PostgreSQL uses a single snapshot for the entire transaction — no non-repeatable reads AND no phantoms (stronger than the SQL standard requires).","sessions":["A","B"],"errors":[],"anomaly":"P2","isolation":"REPEATABLE READ"}
{"scenario":"postgres/02-isolation/update-recheck.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"An UPDATE at READ COMMITTED that waited for a lock re-evaluates its WHERE clause against the new row version — rows that no longer match are silently skipped.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/02-isolation/write-skew-rr.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Two REPEATABLE READ transactions can each validate an invariant against their snapshots, write to different rows, and both commit — leaving the invariant broken. This is write skew.","sessions":["A","B"],"errors":[],"anomaly":"G2-item","isolation":"REPEATABLE READ"}
{"scenario":"postgres/02-isolation/write-skew-serializable.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"The exact interleaving that breaks the invariant under REPEATABLE READ fails with SQLSTATE 40001 under SERIALIZABLE — one transaction commits, the other must retry.","sessions":["A","B"],"errors":["40001"],"anomaly":"G2","isolation":"SERIALIZABLE"}
{"scenario":"postgres/03-locking/alter-table-outage.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"An ALTER TABLE queued behind one long transaction blocks every later query on that table — even plain SELECTs — because they must queue behind its ACCESS EXCLUSIVE request.","sessions":["A","B","C","M"],"errors":[]}
{"scenario":"postgres/03-locking/ddl-lock-timeout.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"With lock_timeout set, a migration that can't get its lock fails fast (55P03) instead of queueing — and other sessions' queries are never blocked behind it.","sessions":["A","B","C"],"errors":["55P03"]}
{"scenario":"postgres/03-locking/deadlock-avoidance.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"The same two opposite-direction transfers cannot deadlock if both transactions lock the rows in the same (id) order first — the second simply waits its turn.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/03-locking/deadlock.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Two transactions locking the same rows in opposite order deadlock; PostgreSQL detects the cycle and aborts one of them with SQLSTATE 40P01 so the other can finish.","sessions":["A","B"],"errors":["40P01"]}
{"scenario":"postgres/03-locking/fk-key-share.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"INSERTing a child row locks the referenced parent row with FOR KEY SHARE: updating the parent's other columns still works, but deleting it blocks — and then fails the moment the child commits.","sessions":["A","B"],"errors":["23503"]}
{"scenario":"postgres/03-locking/for-update-blocks.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A row locked with SELECT FOR UPDATE can still be read by everyone, but any write to it waits until the locking transaction ends.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/03-locking/lock-mode-matrix.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"FOR KEY SHARE < FOR SHARE < FOR NO KEY UPDATE < FOR UPDATE: two share-mode locks coexist, but a share lock still stops writers, and FOR UPDATE stops everything.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/03-locking/lock-queue-fifo.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Sessions waiting for the same row lock queue up: when the holder commits, the lock goes to the first waiter, and everyone else keeps waiting behind it.","sessions":["A","B","C","M"],"errors":[]}
{"scenario":"postgres/03-locking/lock-timeout.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"With lock_timeout set, a statement waits for a lock only that long, then fails with SQLSTATE 55P03 — the same code NOWAIT raises immediately.","sessions":["A","B"],"errors":["55P03"]}
{"scenario":"postgres/03-locking/monitoring-locks.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"pg_locks shows every lock a transaction holds — a single-row UPDATE takes four — and a waiter shows up as granted = false, findable via pg_blocking_pids().","sessions":["A","B","M"],"errors":[]}
{"scenario":"postgres/03-locking/nowait.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"SELECT ... FOR UPDATE NOWAIT refuses to wait: if the row is locked it fails immediately with SQLSTATE 55P03 instead of joining the lock queue.","sessions":["A","B"],"errors":["55P03"]}
{"scenario":"postgres/03-locking/skip-locked.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"SELECT ... FOR UPDATE SKIP LOCKED silently skips locked rows, so concurrent workers each grab a different row without ever waiting — the backbone of Postgres job queues.","sessions":["A","B","C","D"],"errors":[]}
{"scenario":"postgres/04-mvcc/dead-tuples-and-bloat.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Every UPDATE leaves a dead tuple behind and every DELETE leaves the row on disk — so a table's file only ever grows, no matter how much data you delete.","sessions":["A"],"errors":[]}
{"scenario":"postgres/04-mvcc/long-transactions.ts","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"VACUUM can only remove tuples no snapshot can still see — one long-running transaction holds the horizon back for every table, and VACUUM silently reclaims nothing until it ends.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/04-mvcc/row-versions.ts","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"UPDATE never modifies a row in place — it writes a new version stamped with its xid (xmin) and stamps the old one's xmax; DELETE only stamps xmax. Both versions stay on disk, visible with pageinspect.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/04-mvcc/snapshots-under-the-hood.ts","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A snapshot is three numbers — xmin, xmax, and the in-progress list — and visibility is pure arithmetic on them: committed before xmax and not in xip = visible. Commit order doesn't matter; the snapshot already decided.","sessions":["A","B","C"],"errors":[]}
{"scenario":"postgres/04-mvcc/vacuum.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"VACUUM turns dead tuples into reusable free space inside the table's file — new rows land in the freed slots — but it does not shrink the file; only VACUUM FULL rewrites the table to its minimal size.","sessions":["A"],"errors":[]}
{"scenario":"postgres/05-patterns/advisory-locks.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Advisory locks serialize application-defined work with no table involved: try-lock answers instantly, session-level locks survive COMMIT and need an explicit unlock, transaction-level locks vanish at COMMIT.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/05-patterns/check-then-insert-race.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"\"SELECT first, INSERT if absent\" cannot enforce uniqueness: two concurrent requests both see 0 rows, both insert, and the duplicate lands without any error.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/05-patterns/fix-lost-update-atomic.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"UPDATE ... SET balance = balance + 10 reads and writes in one statement: the second writer waits for the first and stacks on top — both deposits survive.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/05-patterns/fix-lost-update-for-update.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"When the new value must be computed in application code, SELECT ... FOR UPDATE serializes the read-modify-write: the second reader waits and then reads the committed 110, so both deposits land.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/05-patterns/fix-lost-update-version-column.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A version column makes the lost update detectable instead of silent: the stale write matches 0 rows (UPDATE 0), and retrying against the new version lands both deposits.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/05-patterns/idempotency-key.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"INSERT ... ON CONFLICT DO NOTHING RETURNING on an idempotency key makes retries safe: the first request charges, every retry — even one racing the original in flight — gets 0 rows back and charges nothing.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/05-patterns/idle-in-transaction-timeout.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A session that goes idle mid-transaction (the classic ORM failure mode) is killed by idle_in_transaction_session_timeout: its work is rolled back and the app discovers the corpse on its next statement.","sessions":["A","M"],"errors":["ERR_POSTGRES_CONNECTION_CLOSED"]}
{"scenario":"postgres/05-patterns/job-queue.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"The full worker loop — claim with FOR UPDATE SKIP LOCKED, work, mark done, commit — never double-processes a job and never loses one: a worker crash returns its job to the queue automatically.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/05-patterns/on-conflict.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A UNIQUE constraint makes the concurrent duplicate wait for the first insert's fate and then fail with 23505 — and ON CONFLICT turns that error into a clean no-op or upsert.","sessions":["A","B"],"errors":["23505"]}
{"scenario":"postgres/05-patterns/retry-serialization-failures.ts","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A 40001 is transient, not fatal: rerunning the identical transaction reads the fresh state and succeeds — withRetry needs exactly two attempts here.","sessions":["A","B"],"errors":["40001"]}
{"scenario":"postgres/06-distributed/dual-write-problem.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Two systems, two writes, no shared transaction: whichever write goes first, a failure between them leaves the database and the broker permanently disagreeing.","sessions":["App"],"errors":["23514"]}
{"scenario":"postgres/06-distributed/listen-notify.ts","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"NOTIFY delivers nothing until COMMIT, a rolled-back NOTIFY is never delivered, and identical notifications within one transaction are folded into one.","sessions":["A"],"errors":[]}
{"scenario":"postgres/06-distributed/saga-compensation.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A saga replaces one distributed transaction with a chain of local ones: every step commits immediately and is visible to everyone, and a failed step is undone by a new compensating transaction — not by ROLLBACK.","sessions":["Saga","Reader"],"errors":[]}
{"scenario":"postgres/06-distributed/transactional-outbox.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Writing the order and its event in one transaction makes them atomic, and a SKIP LOCKED relay gives at-least-once delivery: a crashed relay redelivers the event instead of losing it.","sessions":["App","Relay"],"errors":[]}
{"scenario":"postgres/06-distributed/two-phase-commit.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"PREPARE TRANSACTION detaches a transaction from its session: it survives the session's death, keeps holding its locks, holds the xid horizon back so VACUUM reclaims nothing in any table, and any later session can finish it by name with COMMIT PREPARED.","sessions":["A","B","M"],"errors":["55P03","ERR_POSTGRES_CONNECTION_CLOSED"]}
{"scenario":"postgres/07-pitfalls/queue-bloat.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"A stuck worker's open transaction pins the vacuum horizon, so every claim/complete cycle the queue drains during the hang leaves a dead row version VACUUM cannot reclaim — the queue table grows with its throughput, not its backlog, and the identical VACUUM frees all of it the moment the worker's transaction ends.","sessions":["A","B"],"errors":[]}
{"scenario":"postgres/08-production/deadlock-counter.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Every deadlock increments pg_stat_database.deadlocks — a counter you can alert on even when nobody was watching the error logs.","sessions":["A","B","M"],"errors":["40P01"]}
{"scenario":"postgres/08-production/find-long-transactions.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"Three pg_stat_activity queries find the forgotten transaction: the oldest xact_start, sessions sitting idle in transaction, and the backend_xmin that pins VACUUM's horizon.","sessions":["A","M"],"errors":[]}
{"scenario":"postgres/08-production/timeout-guardrails.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"statement_timeout cancels a runaway statement (SQLSTATE 57014) but keeps the session; transaction_timeout kills the whole transaction — connection and all — and its work rolls back.","sessions":["A","M"],"errors":["57014","ERR_POSTGRES_CONNECTION_CLOSED"]}
{"scenario":"postgres/08-production/vacuum-health.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"pg_stat_user_tables shows dead tuples accumulating and when vacuum last ran, and age(datfrozenxid) vs autovacuum_freeze_max_age is the wraparound alert.","sessions":["A","M"],"errors":[]}
{"scenario":"postgres/08-production/who-is-blocking-whom.yaml","engine":"postgres","product":"PostgreSQL","version":"18.4","claim":"One join on pg_stat_activity + pg_blocking_pids() names the blocker, shows that it's sitting idle in transaction, and shows the last statement it ran — and pg_terminate_backend() drains the queue.","sessions":["A","B","M"],"errors":[]}
