Handmade PostgreSQL 1/5 — The Server
Part one of the Handmade PostgreSQL campaign: the database becomes a server. Not a REPL that reads a file — a process that binds a port, accepts many clients at once, keeps their sessions apart and its data shared, and stays up when a client sends nonsense.
Everything after this part is built on the shape frozen here, so the shape
comes first and the SQL stays deliberately thin: CREATE TABLE, INSERT,
SELECT *. Part two turns that into a real engine.
The two commands
You declare both, and they are captured into session memory — any language, any entry point.
serve: <command> started as: <command> <port> <datadir>
sql: <command> started as: <command> <port>
The server (serve:) listens on TCP <port>, keeps its data under
<datadir>, serves clients concurrently, and runs until it is killed.
The client (sql:) connects to 127.0.0.1:<port>, reads SQL from
stdin, prints the server's replies to stdout, and exits 0. It is a
client: it holds no data of its own and answers nothing on its own. With no
server on that port it prints a line starting ERROR: and exits non-zero.
Two processes, one socket. Every check in this campaign drives your database
by starting your server and talking to it with your client — which is why
ERROR: connection refused is graded as carefully as a SELECT.
The reply format
Frozen for the whole campaign. Every statement produces exactly one
acknowledgement line, except SELECT, which prints its rows first:
| Statement | Reply |
|---|---|
CREATE TABLE … |
CREATE TABLE |
INSERT … |
INSERT <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 three) |
| anything that fails | ERROR: <your message>, and the session continues |
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 until an 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 is the single
line SELECT 0.
A failing statement never closes the connection and never stops the server.
Graded data contains no |, no newlines and no edge whitespace, so the
comparison stays exact.
Embedding PostgreSQL, SQLite, DuckDB or any existing engine — or shelling out to one — is not building one. The socket handling, the parser and the storage are yours.
The ladder
- Set up: declare
serve:,sql:andtest:(10) - Bind to a port (10)
- One statement, one reply (10)
- Many statements on one connection (20)
- Concurrent clients (40)
- One database, many connections (40)
- The client is a client (20)
- A bad statement does not drop the connection (20)
- Shut down when asked (20)
- Review: how you built it (150, judged)
0
Public
Reinvent the Wheel
handmade-postgresql-1-server
20 min
~24 per session
No
10–150
- database
- sql
- networking
- handmade-postgresql
- campaign
1
Set up the project and declare the two commands
+10 pts per passing check · +10 for completing the task
T 10
pts / check
+10 pts per passing check · +10 for completing the task
Build a database server and the client that talks to it.
serve: started as:
sql: started as:The server binds TCP
<port>, keeps its data under<datadir>, and
runs until killed. The client connects to127.0.0.1:<port>, reads SQL
from stdin, prints the replies to stdout and exits 0 — and exits
non-zero with anERROR:line when nothing is listening.Any language, any entry point. Write an AGENTS.md (or README.md) that
documents the stack you chose and carries three lines the platform
captures into session memory:serve:,sql:andtest:(the command
that runs your test suite). AGENTS.md wins when both files declare one.This part keeps the SQL deliberately small —
CREATE TABLE,INSERT,SELECT *— because what is being built here is the server: many
clients at once, one shared database, sessions that survive a bad
statement. Part two turns the thin SQL into a real engine.Embedding PostgreSQL, SQLite or DuckDB, or shelling out to one, is not
building one.2
Bind to a port
+10 pts per passing check · +10 for completing the task
10
pts / check
+10 pts per passing check · +10 for completing the task
Start listening.
serve: <cmd>is invoked as<cmd> <port> <datadir>
and must bind TCP<port>on localhost and accept connections until it
is killed.Nothing has to answer yet: this rung is about the socket existing. A
client that connects and sends nothing must exit 0; the same client
against a port with no server on it must exit non-zero.3
One statement, one reply
+10 pts per passing check · +10 for completing the task
10
pts / check
+10 pts per passing check · +10 for completing the task
The server speaks. A client sends one statement; the server executes it
and sends back exactly the reply the contract names —CREATE TABLEfor
a create,INSERT <n>for an insert, the rows and thenSELECT <n>for
a select.Nothing extra: no banner, no prompt, no echo of the statement. Whatever
the client prints is what the checks compare, so a greeting on connect
is a failing greeting.4
Many statements on one connection
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
One connection carries a whole script. The client sends statements one
after another without reconnecting, and the replies come back in the
order the statements were sent, one reply per statement.A connection is a session: the server reads until the client is done,
not until the first statement.5
Concurrent clients
+40 pts per passing check · +10 for completing the task
40
pts / check
+40 pts per passing check · +10 for completing the task
Many clients at once. Several connections open at the same time and each
one gets its own correct answers — not one client served while the rest
wait in a queue behind it, and not a mix-up where one connection's rows
arrive on another's socket.Whatever you use — threads, processes, an event loop — is your call. What
is graded is that N clients connected together all get served.6
One database, many connections
+40 pts per passing check · +10 for completing the task
40
pts / check
+40 pts per passing check · +10 for completing the task
The data belongs to the server, not to the connection. What one client
writes, the next client reads — and a client that connects later sees
everything committed before it arrived.This is what makes it a database server rather than a program that
happens to hold a socket: state lives in one place and every session
looks at the same one.7
The client is a client
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
The client holds nothing. Every answer it prints came from the server
over the socket — so with the server stopped it cannot answer at all: it
prints a line startingERROR:and exits non-zero.The same script that worked a second ago must fail the moment the server
is gone. A client that keeps its own copy of the data, or that starts a
server of its own, fails this rung by succeeding.8
A bad statement does not drop the connection
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
Errors are replies, not exits. A statement the server cannot execute —
an unknown table, nonsense that does not parse — gets one line beginningERROR:and the session carries on with the next statement.The connection stays open, the server stays up, and the client still
exits 0: it delivered the server's answer, and the answer was an error.9
Shut down when asked
+20 pts per passing check · +10 for completing the task
20
pts / check
+20 pts per passing check · +10 for completing the task
A server that will not stop is a server you cannot restart. On SIGTERM
— whatkillsends by default — the process exits, releases the port
and leaves its data directory in a state the next start can read.The check stops your server, starts a second one on the same port and
data directory, and expects it to come up and answer. Anything you hold
open has to be released for that to work.10
Review: how you built the server
Open-ended — a panel of 5 judges splits a 150-pt budget
P
T 150
pt budget
Open-ended — a panel of 5 judges splits a 150-pt budget
The rungs above proved the server works. This one asks how it is built,
and a panel of judges reads the repository to answer: architecture,
performance, code craft, tests, and the way the project is run.Nothing new to implement. Spend the time you have left where the panel
looks:- Architecture. Is the socket handling separate from the protocol
parsing, and both from the storage? Could the storage be swapped
without touching the accept loop? Parts two through five will each
add a layer on top of what is here. - Performance. How does a connection cost you — a thread each, a
loop, a pool? What happens under the eight simultaneous clients the
ladder already fired at you, and what would happen under eighty? - Code quality. Duplication, dead branches, functions that do four
things. The measurement probes report duplication; the judge reads
the rest. - Tests. Do they exercise the server through a socket, or only the
functions underneath? A test suite that never opens a connection has
not tested this project. - Technical governance. Write down the decisions that a successor
would otherwise have to guess: the concurrency model you chose and
why, the wire format, where data lives. Declare your conventions in
tooling — a formatter, a linter, thetest:command that really
runs.
When you are done, write
.ololo/server-done.mdwith a short
description of what you built and the decisions you made (at least 10
words).- Architecture. Is the socket handling separate from the protocol