Skip to content
AFM Studio
Personal ProjectDeveloper ToolWeb App

Isolation Anomaly

Hand-build two overlapping database transactions, watch exactly where they corrupt each other's data, then re-run the identical scenario against PostgreSQL, MySQL, SQL Server and Oracle to see the same code produce four different outcomes

All projects4 min read

Role

Solo Developer

Timeframe

Aug 2026

On this page

The problem

Every application developer configures a database isolation level — the setting that decides how much two simultaneous operations are allowed to interfere. Almost everyone accepts the default, and almost nobody can say what it protects them from. Three concrete gaps:

  • The failure modes are taught as a list of names — dirty read, non-repeatable read, phantom — recited in interviews and understood by few.
  • The most dangerous failure is missing from that list. Write skew: two transactions each read the same data, each check a rule ("someone else is still on call, so I can go off call"), each write a different row, and both commit. Nothing collides — no shared row, no lock contention — and the rule is broken by the combination. This silently empties on-call rosters, double-books rooms, and corrupts inventory. Snapshot isolation does not prevent it, and snapshot isolation is what most defaults amount to.
  • The level names mean different things in different engines, and no one says so. PostgreSQL's REPEATABLE READ is snapshot isolation; its READ UNCOMMITTED silently behaves as READ COMMITTED; Oracle's SERIALIZABLE is snapshot isolation, so it permits write skew despite the name. A developer who learns "REPEATABLE READ" from a textbook has learned something untrue of the database in front of them.

The approach

Real databases are the oracle, and they came first

The instinct with a project like this is to write the simulator from what you know about how databases work — which is exactly the failure mode, because a developer will make a production decision from what it shows. So before the executor existed, a harness started real PostgreSQL, MySQL, SQL Server, and Oracle in containers, executed every schedule against them, and recorded what actually happened — every value read, every error code, where execution blocked and until when, whether each transaction committed. Those 220 recordings are committed as fixtures; the simulator is built to match them, and when the two disagree, the simulator is wrong. Waits in particular are read from each engine's own instrumentation (pg_stat_activity, performance_schema.data_lock_waits, v$session.blocking_session) rather than inferred from a slow statement.

Engine differences are data, not code

There is one executor, one MVCC core, one lock manager. An engine is a JSON file of rules — what each level name actually maps to, when a lock is taken, what conflicts with what, what aborts and with which error code. There is no if (engine === 'postgres') anywhere. Two reasons: the cross-engine comparison is only meaningful if the machinery is shared (otherwise you're comparing implementations, not semantics), and a pack is reviewable by a Postgres expert who's never read the codebase. Every rule cites the vendor documentation and the build enforces it; the anomaly detector shares no code with the executor and evaluates each anomaly from its published definition, asserted in both directions (must appear at levels that permit it, must not at levels that prevent it).

Refuse rather than approximate

The clearest example: SQL Server picks its deadlock victim by cost estimate, so the same schedule loses T1 at one level and T2 at another. Rather than guess, its pack declares victim selection unmodelled and the executor refuses the whole run — with a test asserting each refusal is justified by a real deadlock in the recording. And a recording caught a rule the author had wrong (commit "a deadlock schedule, and the rule it caught me getting wrong") — the harness doing its job, which is the entire reason it was built first.

Outcome

Live and public, bilingual, fully static (the database drivers and Docker are development-only). Its signature view is the score — transactions as parallel staves on manuscript paper, operations as marks, bar lines at each step, so reading straight down a bar line shows what happened simultaneously; marks drag to re-interleave and re-run. Plus stepping with live MVCC version chains, held locks (range and gap included), and snapshot visibility; the conductor's mark — one red annotation on the exact step the anomaly became inevitable, cause not symptom, with a mechanism generated from the trace; the cross-engine matrix — one schedule against all five packs at every level (committed / aborted with error code / anomaly / refused), where a developer sees their default permits the thing they assumed impossible; a conflict graph cross-checked against brute-force serializability; and eleven framed scenarios (an on-call roster for write skew, a bank transfer for read skew).

Built solo over two days — ~9,255 lines of application code, 1,551 tests (1,215 of them simulator-vs-real-database comparisons), 220 recorded fixtures across five engine packs (PostgreSQL 16, MySQL 8.4 InnoDB, SQL Server 2022, SQL Server 2022 with RCSI, Oracle 23ai), 328 vendor citations, four runtime dependencies and no database, SQL-parser, or graph-layout library. Oracle earned its place: an engine whose level is literally named SERIALIZABLE yet permits write skew is the whole argument, made undeniable.

Outcomes

DB engines modelled from cited docs
5
Fixtures recorded from real databases
220
Tests — simulator vs the real engines
1,551
Scenarios, incl. the write-skew anomaly
11

Screenshots

Overview
Schedule
Scenarios
Matrix
Conflict Graph
Engines

Have a project like this?

If you need a system built with the same care — clear scope, solid execution — let's talk.

Start a project