Christopher Hotchkiss

Christopher Hotchkiss

Crafting Solutions, Shaping Products: From Concept to Code

FeOphant

A PostgreSQL-compatible database server I wrote from scratch in Rust — point psql at it and run a query, and the rows come back. Built by hand in 2021, before Copilot existed.

It speaks the real Postgres wire protocol, so the proof is one command: connect with psql (or pgbench, or any Postgres driver), CREATE TABLE, INSERT, SELECT — and the data is still there after you restart the server. Source on GitHub (AGPL-3.0, 27 stars).

I'm leading with the year on purpose. This is ~10,000 lines of database internals written in 2021 — no LLM in the loop, because there wasn't one. If you want a sample of how I actually think about a hard system with no assistant holding my hand, this is it.

Why a whole database

The honest trigger was a quibble. Postgres's vacuum and transaction-ID wraparound always struck me as a design tax you shouldn't have to pay, and I wanted to see whether 64/128-bit transaction IDs could just make it go away. But you can't second-guess a database's design from the outside — reading the Postgres source teaches you the WHAT; reimplementing the page format, the B-tree and the visibility rules teaches you WHY each one is shaped the way it is. So I built the smallest real Postgres I could and kept going until a query went in one end and the right rows came out the other.

What's actually under it

Not a SQL-string-to-HashMap toy — the real subsystems, built from scratch:

  • The Postgres v3 wire protocol — a hand-written decoder/encoder, startup + SSL negotiation, so real Postgres clients connect with no shim in between.
  • A heap storage engine — 8KB pages laid out like Postgres's own (page header, item pointers, per-row info_mask hint bits, a null bitmap), rows written to disk and read back.
  • A B-tree index, from scratch — leaf and branch nodes, node splits, the search down to a leaf — which is what enforces the primary key.
  • MVCC — a transaction manager modeled on Postgres's clog, snapshot visibility off the hint bits, isolation levels, with its OWN visibility test suite (because that's the part that's easy to get subtly, silently wrong).
  • The system catalogspg_class, pg_attribute, pg_index, pg_constraint — emulated, because that's how a real client introspects a schema.

The layers stack the way my design doc draws them — Trigger → Security → Constraint → Visible-Row (MVCC) → Row → I/O Manager — with the Transaction Manager feeding visibility and the constraint types feeding the constraint layer. One query walks the whole stack:

direction: down

client: "Postgres client (psql / tokio-postgres)"
wire: "Wire codec + SQL parser + engine"
trigger: "Trigger Manager"
security: "Security Manager"
cmgr: "Constraint Manager"
visible: "Visible-Row Manager (MVCC)"
row: "Row Manager (heap tuples, hint bits)"
io: "I/O Manager (pages, free-space, locks)"
disk: "8KB pages on disk" {shape: cylinder}

client -> wire: "v3 wire protocol"
wire -> trigger
trigger -> security
security -> cmgr
cmgr -> visible
visible -> row
row -> io
io -> disk

ctypes: "Null / Unique / Custom constraints"
txn: "Transaction Manager"
btree: "B-tree Index Manager"

ctypes -> cmgr: {style.stroke-dash: 4}
txn -> visible: {style.stroke-dash: 4}
btree -> visible: {style.stroke-dash: 4}

(The Trigger and Security managers are in the designed layering; the path that actually runs a query today is Constraint → Visible-Row → Row → I/O.)

Two deliberate divergences from Postgres, because copying it exactly wasn't the point:

  • UUIDs instead of OIDs, and 64/128-bit transaction IDs — specifically to sidestep the vacuum-wraparound problem that haunts real Postgres.
  • async/Tokio instead of multi-process shared memory — a different concurrency model on purpose, not an accident of the language.

What's real, and where I stopped

Real and tested — and the tests are the proof, not my say-so:

Where I stopped, in 2021: it is not crash-safe — there's no WAL, so a kill -9 mid-write can leave the on-disk format inconsistent, and that format isn't stable across versions. Queries are single-table — no joins yet. And the honest reason the momentum died: I got stuck trying to upstream a parser combinator I needed — take_until_parser_matches, for cleanly scanning forward across multiple SQL statements — into nom (rust-bakery/nom#1441). The PR stalled, I was building on my own fork, and the side project lost its thread there. So it's a real database CORE, not a finished database — I'd rather say it flat out than dress it up.

Get it

  • Source: github.com/chotchki/feophant (AGPL-3.0).
  • Try it: clone, cargo run, then psql -h 127.0.0.1 -p 50000 and run a query against a database that's entirely my code, top to bottom.