Goal: understand storage engines, indexing, and query execution well enough to build a minimal but real database — not a toy that only handles one hardcoded query.
Phase 0 — Study the references
Read/skim these before writing code. Don’t over-invest here — enough to steal the right mental models, not enough to stall.
- Read db_tutorial — the file format, B-tree logic, and overall shape of a minimal DB (this is the backbone the rest of the plan follows)
- Skim KISSDB — how minimal a disk-based hash map can actually be
- Skim GroundUpDb or MiniDB — C++-specific patterns: memory mapping, class structure for pages/buffers
Output: a short note ([[db-notes]]) on file format + page layout decisions you’re stealing/adapting.
Phase 1 — Interface Layer (REPL)
Build this first — it’s your test harness for everything after it.
- Build a command-line REPL: read a line, dispatch it, print result or error
- Distinguish meta-commands (
.exit,.help, etc.) from actual statements - Basic input buffer handling (db_tutorial covers this early —
InputBuffer/Tablestructs)
Phase 2 — Storage Engine (raw file I/O)
Get data onto disk before you get clever about indexing it.
- Design a fixed-size page format (row layout, page header)
- Implement raw read/write to a data file using
std::fstreamormmap - Simple append-only insert + full-scan read to prove the format works end-to-end
Phase 3 — Indexing (B+ Tree or LSM)
- Implement a B+ Tree or a simple LSM-tree in C++ on top of Phase 2’s file I/O
- Support insert / search / (delete if B+ Tree)
- Swap your Phase 2 full-scan reads for index lookups
Phase 4 — Buffer Pool Manager
Raw fstream/mmap calls on every operation don’t scale — this is the bridge between your C++ objects and the disk.
- Wrap your file I/O in a class managing a fixed array of in-memory pages
- Implement pin/unpin and dirty-page tracking
- Implement a page replacement policy (start with LRU)
- Route all B+ Tree / LSM page access through the buffer pool instead of directly through
fstream/mmap
Phase 5 — Query Processor (Parser + Executor)
Don’t write a full SQL parser. Support a deliberately small surface area first.
- Tokenizer: split raw input into tokens
- Parser: tokens → a
Statementstruct. Support onlyINSERT,SELECT,UPDATE,DELETE, with simpleWHEREclauses (single equality condition is enough at first) - Executor: takes a
Statement, calls the index’s insert/search/etc. via the Buffer Pool - Translate a
SELECTscan into iteration over B+ Tree leaf nodes (or LSM levels)
Phase 6 — Durability (Write-Ahead Log)
- Append every mutation to a sequential log file before it touches the B+ Tree / data pages
- Write a recovery routine: on startup, replay the log to reconstruct state after a crash
- Test it — kill the process mid-write, confirm recovery actually works
Phase 7 — Concurrency
- Start with a single global mutex around writes (correctness first, speed later)
- Only after that works: explore fine-grained per-page latching
Phase 8 — Stretch: compare against real parsers
Not for direct use early on — these are references once your hand-rolled parser feels limiting, to see how a production-grade SQL parser is structured.