All work

RustSQLite

A SQLite engine in Rust, no dependencies

Year
2026
Kind
Systems
Status
Finished

0 dependencies

A database engine written from scratch in Rust with only the standard library. It writes real SQLite 3 files that sqlite3 can open, and commits through a write-ahead log.

RustSQLite implements the storage engine and the SQL front end. Pages follow the SQLite 3 file format: the 100-byte header, B-tree pages, varint-encoded records and the sqlite_schema catalog on page 1. Commits go through a write-ahead log, and queries go through a hand-written recursive-descent parser.

The problem

RustSQLite is a database engine written from scratch in Rust, with only the standard library. One rule shapes it: its files must be real SQLite 3 files that the official sqlite3 tool can open. SQLite documents its format down to the byte, which makes sqlite3 an outside referee. If it reads the rows back, the pages are laid out right.

How it works

  1. 01

    Parse the SQL by hand

    A tokenizer and a recursive-descent parser turn SQL into a statement, one function per grammar rule. WHERE clauses go through parse_or_expr, then parse_and_expr, then parse_comparison, so AND binds tighter than OR without a precedence table.

  2. 02

    Encode each row as a SQLite record

    A row becomes a record: a header with one serial type per column, then the values. Integers take the smallest width that fits, and 0 and 1 store no bytes at all. Types and lengths are varints, SQLite's 1-to-9-byte integer encoding. Each record becomes a cell keyed by rowid, placed by walking the table B-tree from the root to the right leaf.

  3. 03

    Split a page when it fills

    Pages are 4 KB. Cells pack in from the end of the page while a sorted array of 2-byte cell pointers grows from the header. When a new cell no longer fits, the leaf splits: the lower half stays, the upper half moves to a new page, and a copy of the left half's largest key goes up to the parent as the separator. That is SQLite's own convention, so sqlite3's integrity check passes however many pages a table spans. If the root splits, the new root's page number is written back to sqlite_schema. Deletes run the other way: a page that falls below a third full is merged into a sibling, or shares rows with it, so no empty page is left behind, and freed pages go on SQLite's freelist to be reused.

  4. 04

    Keep SQLite's automatic indexes

    For every UNIQUE column, and every primary key that isn't an INTEGER PRIMARY KEY, SQLite keeps a hidden index named sqlite_autoindex_<table>_<n>. The engine writes the same one: a second B-tree in SQLite's index-page format, holding (value, rowid) records sorted the way SQLite compares them. Every insert, update and delete keeps it in step, and a duplicate is rejected by looking it up there. sqlite3 uses it for lookups, and its query plan says so.

  5. 05

    Commit through a write-ahead log

    Statements don't write to the database file. Changed pages collect in a dirty list, and reads check that list, then the log, then the file. On commit, each dirty page is appended to the write-ahead log as a frame, and the last frame carries a commit flag. Either the whole batch counts or none of it does. ROLLBACK just drops the list.

  6. 06

    Recover, then checkpoint

    On open, any frames after the last commit flag are discarded, so a transaction cut off mid-write disappears. A checkpoint copies the newest version of each page into the database file, updates the 100-byte header and empties the log. It runs once the log passes 1,000 committed frames, on .checkpoint, and when the database closes.

Results

0
dependencies outside the Rust standard library
86
tests passing: 48 unit, 37 integration (20 of them checked by sqlite3) and 1 doc test
5,000
rows across 118 pages pass sqlite3's PRAGMA integrity_check

Decisions

  • The real file format, not an invented one

    A private format could skip the 100-byte header, the sqlite_schema catalog on page 1 and SQLite's serial types. Implementing them means the output can be checked with an independent tool.

  • A simpler log than SQLite's

    SQLite's WAL uses checksummed 24-byte frame headers and a shared-memory index. This one uses an 8-byte header and an in-memory map. The cost: sqlite3 can't read the log and only sees changes after a checkpoint, so closing the database always checkpoints.

  • The B-tree doesn't know about the log

    B-tree code reads and writes pages through a small PageStore trait, and the engine decides what sits behind it. Splits, searches and scans never deal with durability.