Field note · July 30, 2024
Writing a SQL engine that fits in a browser tab
Tokeniser, Pratt parser, and the decision to be strict rather than forgiving.
The SQL playground is live. It is a real engine — tokeniser, parser, evaluator — running entirely in the page, about 700 lines.
Three decisions worth writing down.
A Pratt parser, not a grammar generator. Operator-precedence parsing is a good fit for SQL expressions and it fits in a page of code you can read. Precedence is a lookup table; IS NULL, IN, LIKE and BETWEEN are handled as special cases at the binding loop. Nothing exotic.
Strict rather than forgiving. The engine refuses things it cannot do properly instead of approximating them. select * alongside an aggregate is an error rather than a guess. An unsupported function names the ones that exist. A join that produces more than 400,000 rows stops and says so, with a note that a fan-out this large is usually a grain bug.
That last one started as a hang-prevention measure and became a teaching feature. The example query that self-joins world_indicators on country to demonstrate a fan-out is now one of the presets.
Hash joins where possible. A nested loop across two eight-thousand-row tables is 64 million comparisons, which is a frozen tab. When the join condition is a plain a.x = b.y the engine builds a hash index on the right side. Anything more complex falls back to nested loops with the row cap.
What it does not support: window functions, subqueries in FROM, CTEs, set operations. Those are all real omissions and the error messages say so by name rather than failing mysteriously.
The design principle throughout: a query that runs here should run in Postgres. The reverse is explicitly not guaranteed, and saying so is more useful than pretending to be complete.