Handmade PostgreSQL 1/5 — Speak SQL
Part one of the Handmade PostgreSQL campaign: teach a program to speak
SQL. By the end of this session it parses statements, keeps tables in memory,
answers SELECT, and reports errors without falling over.
The engine is invoked as:
<run command> <datadir>
SQL arrives on stdin, statement by statement, terminated by ;. Results
go to stdout. You declare the run command yourself in a run: line and it
is captured into session memory, so any language and any entry point works
(run: sh mydb.sh, run: python3 db.py, run: node db.js, …). The data
directory argument is passed from the first check onwards — this part may
ignore it, part two will not, and freezing the invocation now means the
interface never breaks underneath you.
Embedding SQLite, DuckDB, psql or any existing SQL engine is not building
one. The parser and the executor are yours.
The output contract
This is the whole grading surface, and it does not change for the rest of the
campaign. Every statement prints exactly one acknowledgement line, except
SELECT, which prints its rows first:
| Statement | Output |
|---|---|
CREATE TABLE … |
CREATE TABLE |
INSERT … |
INSERT <n> — n rows inserted |
SELECT … |
one line per row, then SELECT <n> |
UPDATE … / DELETE … |
UPDATE <n> / DELETE <n> (part two) |
BEGIN; / COMMIT; / ROLLBACK; |
BEGIN / COMMIT / ROLLBACK (part five) |
| anything that fails | ERROR: <your message>, then keep going |
Rows print their values joined by a single | — no header, no padding, no
trailing delimiter. Column order is the SELECT list; * means declared
order. Row order is insertion order unless ORDER BY says otherwise. Two
types exist: INT prints as plain decimal, TEXT prints verbatim and
unquoted. NULL prints as the empty string. An empty result set is just the
line SELECT 0.
A failing statement prints its ERROR: line and execution continues with
the next statement. The process exits 0 either way — a script of ten
statements with one bad one still runs the other nine.
Graded data never contains |, newlines, or leading and trailing spaces, so
the comparison stays exact.
The ladder
- Set up the project and declare how to run it (10)
CREATE TABLE, and saying no twice (10)INSERTandSELECT *in insertion order (20)- Multi-row
INSERTand honest counts (20) - Projection: pick columns, in your order (20)
- TEXT comes back verbatim (20)
- An error does not stop the script (20)
- The transcript gauntlet (40)
1
Public
Reinvent the Wheel
handmade-postgresql-1-repl
15 min
~17 per session
No
10–40
- database
- sql
- handmade-postgresql
- campaign
1
Set up the project and declare how to run it
+10 pts per passing check · +10 for completing the task
10
pts / check
+10 pts per passing check · +10 for completing the task
Build a SQL engine invoked as:
It reads SQL from stdin, executes statements in order, prints results
to stdout and exits 0. Any language and any entry point works — you
decide. Write an AGENTS.md (or README.md) describing your stack
(language, tooling, layout) and carrying the commands kept in session
memory, starting with arun:line declaring the exact command that
starts the engine, e.g.run: sh mydb.sh,run: python3 db.py, orrun: node db.js. That command is captured into session memory: from
here on every check invokes exactly what you declared with a data
directory appended. AGENTS.md wins when both declare one. Declare atest:line too — the command that runs your test suite (e.g.test: sh test.sh).This part may ignore the data directory argument (tables can live in
memory); part two stores everything in it. Accept it from day one so
the interface never changes.Embedding SQLite, DuckDB or any existing SQL engine is not building
one — the parser and the executor are yours.The output contract is frozen for the whole campaign:
CREATE TABLE
acknowledges withCREATE TABLE,INSERTwithINSERT <n>, aSELECTprints one|-joined row per line and thenSELECT <n>, a
failing statement printsERROR: <message>and execution continues.2
CREATE TABLE, and saying no twice
+10 pts per passing check · +10 for completing the task
10
pts / check
+10 pts per passing check · +10 for completing the task
Implement
CREATE TABLE <name> (<col> <type>, ...)with two types:INTandTEXT. A successful statement prints exactlyCREATE TABLE.Then implement refusal. Creating a table that already exists prints an
ERROR:line instead of a secondCREATE TABLE, and selecting from a
table that was never created prints anERROR:line too. Either way
the process still exits 0 — an error is a result, not a crash.3
INSERT and SELECT * in insertion order
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Implement
INSERT INTO <table> VALUES (...)for a single row andSELECT * FROM <table>.Each single-row insert prints
INSERT 1.SELECT *prints one line
per row — values joined by a single|, columns in declared order, no
header and no padding — followed bySELECT <n>. Rows come back in
insertion order: nothing sorts them yet.An empty table prints just
SELECT 0. Worth 20 points.4
Multi-row INSERT and honest counts
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Extend
INSERTto accept several tuples in one statement:INSERT INTO t VALUES (1, 'a'), (2, 'b'), (3, 'c');
The acknowledgement carries the count of rows actually inserted —
INSERT 3for the statement above. The count is not the number of
statements and not always 1: it is what the statement did. All the
rows land, in the order they were written. Worth 20 points.5
Projection — pick columns, in your order
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Implement an explicit select list:
SELECT c3, c1 FROM t;.The output carries only the named columns, in the order the query
names them — not the declared order.SELECT *keeps meaning "every
column, as declared". Selecting a column the table does not have
prints anERROR:line. Worth 20 points.6
TEXT comes back verbatim
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Text values round-trip exactly as written. A value with spaces in it
keeps its spaces; a value that looks like a number ('42','007') is still text and must print exactly as stored — not
renumbered, not stripped of a leading zero, and never quoted in the
output.Values are printed bare: the quotes belong to the SQL, not to the
result. Worth 20 points.7
An error does not stop the script
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
A script is a sequence, not a transaction. When one statement fails,
print itsERROR: <message>line and carry straight on with the next
statement; the ones after a bad one still run, and the process still
exits 0.Syntax that makes no sense at all is an error too — an unparseable
statement gets the same treatment as a semantic one. Worth 20 points.8
The transcript gauntlet
+40 pts per passing check · +10 for completing the task
40
pts / check
+40 pts per passing check · +10 for completing the task
The whole contract at once. One randomized script across two tables:
creates, single- and multi-row inserts, projections, an empty-table
read, and one planted error — graded as a single exact transcript,
line for line, top to bottom.Nothing new to implement here if the earlier rungs are honest. What
this rung catches is drift: an extra blank line, a stray header, a
count that is right in isolation and wrong in company. Worth 40
points.