Handmade PostgreSQL 2/5 — Data That Stays

Public Archived

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)
  • WHERE learns to compare: <, >, <=, >=, <> (40)
  • AND and OR (20)
  • UPDATE with an honest count (40)
  • DELETE and the survivors (40)
  • A fresh directory knows nothing (10)
Sessions

1

Visibility

Public

Category

Reinvent the Wheel

Slug

handmade-postgresql-2-storage

Duration

20 min

Judge reviews

~19 per session

Active session

No

Points

10–40

Tags
  • database
  • sql
  • handmade-postgresql
  • campaign
  • 1

    Set up, re-declare the commands, and prove part one still stands

    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 a run: line with the exact command that starts
    the engine, e.g. run: sh mydb.sh, run: python3 db.py, or
    run: node db.js, and a test: 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 a
    SELECT that 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 / 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 / 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 an ERROR: line, and the process still exits 0.
    Worth 20 points.

  • 4

    WHERE, the equality edition

    20

    pts / check

    +20 pts per passing check · +10 for completing the task

    Implement WHERE with 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 is SELECT 0. Worth 20 points.

  • 5

    WHERE learns to compare

    40

    pts / check

    +40 pts per passing check · +10 for completing the task

    Extend WHERE with 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 / 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 / check

    +40 pts per passing check · +10 for completing the task

    Implement UPDATE <table> SET <col> = <value> WHERE <cond>. The
    acknowledgement is UPDATE <n> where <n> is exactly the number of
    rows the condition matched — updating nothing prints UPDATE 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 / check

    +40 pts per passing check · +10 for completing the task

    Implement DELETE FROM <table> WHERE <cond>. The acknowledgement is
    DELETE <n> with the honest count of removed rows — deleting nothing
    prints DELETE 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 closing SELECT <n> counts the shrunken table. Worth 40
    points.

  • 9

    A fresh directory knows nothing

    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 an ERROR: line — not SELECT 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.