Charlie KrugThe Build Log

← All posts

It passed the sample and failed a database you can't see

Terseql is a daily SQL puzzle scored in UTF-8 bytes, ascending. Real SQLite runs in your tab, and hidden fixture databases decide whether your shortcut generalizes or just fit the preview.

Day one asks for each customer's total spend, highest first, skipping anyone who never ordered. Here is the query you would put in a pull request, and it is correct:

SELECT c.name, SUM(o.amount) AS total
FROM customers c JOIN orders o ON o.customer_id = c.id
GROUP BY c.id
ORDER BY total DESC

126 bytes. That is not the answer, that is the starting line.

The alias exists only so ORDER BY can name the column, and ORDER BY 2 points at it by position instead, so the alias goes. JOIN ... ON becomes a comma join with a WHERE, which is the same thing in fewer characters. Then the whitespace the parser never needed:

SELECT name,SUM(amount)FROM customers c,orders WHERE customer_id=c.id GROUP BY 1 ORDER BY 2 DESC

96 bytes. Same rows. Somewhere under that is a query I have not thought of.

Terseql is one SQL puzzle a day, scored by the UTF-8 byte length of your query, board sorted ascending. It is for people who write SQL well enough that "it works" stopped being interesting. Practice tools are mostly a tutorial with a check button, and generic code-golf sites know nothing about SQL specifically: no editor, no engine, no schema. This is the Wordle loop pointed at the one thing where "I got it in 61 bytes" is a real sentence people say to each other.

The hidden fixtures are the actual puzzle

Byte golf against a single visible dataset is trivially cheatable. If I can see the sample, I can write a query that hardcodes its way to the right answer in 30 bytes and learns nothing.

So the sample you experiment against is not what grades you. Every puzzle ships extra seeded databases you never see, built to cover what the preview conveniently lacks: empty groups, ties, NULLs, negative amounts. Your query has to produce expected output on every one of them. When it does not, you are told that it passed the sample and failed a hidden case, and you are not told which one.

That withholding is deliberate, and it is where the game lives. SUM(amount) over a customer with no orders returns NULL, not 0. Two customers tied at the top have no guaranteed order unless you gave them one. A HAVING you dropped to save four bytes was load-bearing for exactly one row in a database you cannot open. Working out which invariant you quietly broke, from nothing but a failure, is the same skill as reading a production incident, compressed into a tab.

Everything runs in your tab

There is no backend in the solve loop at all. sql.js is SQLite compiled to WebAssembly, so the engine executing your query is the actual engine, running locally. It starts compiling while you are still reading the prompt, so the first Run is as fast as the tenth. Type a character, the byte counter rolls and the result table redraws, with no latency to design around, which is the whole reason the trimming loop feels like a game instead of a form submission.

A side effect I like: each Run gets a fresh database. Write DROP TABLE orders if you feel like it. The next Run starts from the same ground truth, and grading uses its own copies regardless.

Two honest notes. Bytes, not characters, is a real decision rather than a detail: TextEncoder counts the encoded length, so a clever multi-byte glyph costs what it actually costs instead of displaying as short. And the leaderboard: a shared cross-player board needs a server, which is the one piece not built yet. Today src/leaderboard.js keeps your personal bests in localStorage, and the app reads a leaderboard URL at build time when there is one to point at. Five puzzles are authored. Both of those are small numbers and I would rather print them than imply otherwise.

Try it

Open Terseql, read the schema, and write the boring correct query first. Hit Run, watch the table fill, then start cutting: the aliases, the ON, the spaces around the operators. Watch the counter fall. When you pass, you get a share card of your descent, 126 to 113 to 101 to 96, carrying no query text, so posting it in a group chat cannot spoil the day for anyone. The source is on GitHub.

Terseql is live. Free, in your browser, no signup.

This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.

Comments

Loading comments…