Handmade PostgreSQL 2/5 — Data That Stays
Part two of the Handmade PostgreSQL campaign: the data directory earns
its keep. Everything the engine is told now lands in <datadir> and is
still there when the process comes back — and on top of that the engine
learns WHERE, UPDATE and DELETE.
The invocation has not moved an inch:
<run command> <datadir>
SQL on stdin, results on stdout, in the contract frozen in part one:
one acknowledgement line per statement, SELECT rows |-joined and closed
by SELECT <n>, failures as ERROR: <message> with execution carrying on
and the process exiting 0. UPDATE <n> and DELETE <n> join that contract
this part, with <n> the number of rows honestly touched.
This session continues your part-one codebase — start in the folder where part one was built (or in an empty one, and your previous work is fetched for you). The first check replays part one's contract before any new points are on the table.
Embedding SQLite, DuckDB, psql or any existing SQL engine is still not
building one. The storage format inside the data directory is yours too —
one file per table, pages, JSON, a log — as long as a brand-new process
pointed at the same directory sees everything a dead one wrote.
The ladder
- Set up, re-declare the commands, and prove part one still stands (10)
- Rows survive a restart (40)
- The schema survives too (20)
WHERE, the equality edition (20)WHERElearns to compare:<,>,<=,>=,<>(40)ANDandOR(20)UPDATEwith an honest count (40)DELETEand the survivors (40)- A fresh directory knows nothing (10)
1
Public
Reinvent the Wheel
handmade-postgresql-2-storage
20 min
~19 per session
No
10–40
- database
- sql
- handmade-postgresql
- campaign
1
Set up, re-declare the commands, and prove part one still stands
+10 pts per passing check · +10 for completing the task
10
pts / check
+10 pts per passing check · +10 for completing the task
Part two continues the engine you built in part one, in the same repo:
the invocation, the SQL dialect and the output contract are unchanged,
and this session's checks pick up where part one's left off. If the
folder is empty, fetch or recreate your part-one work first — the
engine must already speak SQL before any new points arrive.Re-declare the commands kept in session memory. AGENTS.md (or
README.md) carries arun:line with the exact command that starts
the engine, e.g.run: sh mydb.sh,run: python3 db.py, orrun: node db.js, and atest:line with the command that runs your
test suite, e.g.test: sh test.sh. AGENTS.md wins when both declare
one. From here on every check invokes exactly what you declared with
a data directory appended.One check replays part one's contract in a single script —
CREATE TABLE,INSERT, a planted error that must not stop the run, and aSELECTthat reads the row back. After this task the data directory
argument stops being decoration: everything that follows stores into
it.2
Rows survive a restart
+40 pts per passing check · +10 for completing the task
40
pts / check
+40 pts per passing check · +10 for completing the task
The headline feature of this part: persistence. One invocation of the
engine creates a table and inserts rows into its data directory; a
second invocation — a brand-new process pointed at the same directory
— selects them back, byte for byte, in insertion order.Where and how the data lives inside the directory is your call: one
file per table, a page file, JSON, a log. What is not your call is
the outcome — rows written by a process that has since exited are
readable by the next one, and reading them does not consume them: a
third invocation sees exactly what the second one saw. Worth 40
points.3
The schema survives too
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Persistence covers the catalog, not just the rows. One invocation only
creates a table and exits; the next invocation inserts into it without
re-creating it and reads the row back — column names, order and types
all come from the schema stored in the data directory.Unknown tables stay unknown: inserting into a table that no invocation
ever created prints anERROR:line, and the process still exits 0.
Worth 20 points.4
WHERE, the equality edition
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Implement
WHEREwith equality:SELECT ... FROM <table> WHERE <col> = <value>filters rows before they print, comparing an INT column
against a number and a TEXT column against a quoted string.Only the matching rows print, in insertion order, followed by
SELECT <n>counting the matches. A condition nothing satisfies is
not an error — it isSELECT 0. Worth 20 points.5
WHERE learns to compare
+40 pts per passing check · +10 for completing the task
40
pts / check
+40 pts per passing check · +10 for completing the task
Extend
WHEREwith the comparison operators on INT columns:<,>,<=,>=and<>. Numbers compare as numbers — 900 is less
than 1000 — and the same random threshold is probed from every side
in one script, with one row sitting exactly on it, so the strict and
inclusive variants must genuinely differ.The output shape is unchanged: matching rows in insertion order, then
SELECT <n>. Worth 40 points.6
AND and OR
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Combine two predicates:
WHERE <p> AND <q>keeps the rows both
predicates accept,WHERE <p> OR <q>keeps the rows either accepts.
A predicate on either side is any comparison from the previous tasks,
on an INT or a TEXT column — the checks mix them.The output shape is unchanged: matching rows in insertion order, then
SELECT <n>. Worth 20 points.7
UPDATE with an honest count
+40 pts per passing check · +10 for completing the task
40
pts / check
+40 pts per passing check · +10 for completing the task
Implement
UPDATE <table> SET <col> = <value> WHERE <cond>. The
acknowledgement isUPDATE <n>where<n>is exactly the number of
rows the condition matched — updating nothing printsUPDATE 0, and
that is a result, not an error.The change is durable: a later invocation against the same data
directory sees the new values, with untouched rows exactly as they
were and insertion order preserved. Worth 40 points.8
DELETE and the survivors
+40 pts per passing check · +10 for completing the task
40
pts / check
+40 pts per passing check · +10 for completing the task
Implement
DELETE FROM <table> WHERE <cond>. The acknowledgement isDELETE <n>with the honest count of removed rows — deleting nothing
printsDELETE 0, and that is a result, not an error.Deletion is durable: a later invocation against the same data
directory sees only the survivors, in their original insertion order,
and the closingSELECT <n>counts the shrunken table. Worth 40
points.9
A fresh directory knows nothing
+10 pts per passing check · +10 for completing the task
10
pts / check
+10 pts per passing check · +10 for completing the task
The data directory is the whole database. Run the engine against a
brand-new empty directory and it must know nothing: selecting the
table you created elsewhere prints anERROR:line — notSELECT 0
and not the other directory's rows — and the process still exits 0.Two directories are two independent worlds: the same table name may
exist in both with different contents and neither leaks into the
other. No global state lives outside the directory the engine was
pointed at. Worth 10 points.