Storage
4 KiB pages, WAL,
and crash recovery.
The storage path is synchronous. The current model is single-writer, STEAL, NO-FORCE, and WAL-protected. It supports synchronous physical runtime rollback and startup crash recovery. Reads have no snapshot and may observe an active writer.
Executor
↓
HeapStorage
↘ BTree persistence
↓
TransactionManager + WAL
↓
BufferPool + PageGuards
↓
PageManager
↓
Database filePage format v5
Data pages are a fixed 4096 bytes. Page 0 is still legacy container / heap metadata and is not interpreted as Page v5. Heap metadata is a separate NBD1 version-2 layout and stores the canonical table fingerprint. Versions 1 through 4 are rejected rather than guessed or migrated.
| Offset | Field | Meaning |
|---|---|---|
| 0..4 | NBP1 | Page magic |
| 4..6 | u16 version | Format version 5 |
| 6 | u8 type | 2 Heap · 3 BTreeMeta · 4 Internal · 5 Leaf |
| 8..10 | slot count | Slot-directory entries |
| 10..14 | free bounds | Free-space lower / upper bounds |
| 16..24 | pageLSN | 0 means no WAL record yet |
| 24..28 | CRC32C | Whole-page checksum bound to the expected PageId |
| 28.. | slots | u16 offset + u16 length + u32 generation |
Tuple bytes pack from the end of the page backward. Every allocated slot has a nonzero generation. The reserved pair (offset = 0, length = 65535) means Deleted and retains the generation. DELETE compacts tuple bytes without renumbering slots; a later insert may reuse the lowest eligible tombstone after a checked increment.
RowId is the versioned physical locator PageId + SlotId + generation. It is not a business key and is never exposed as a SQL column. Before reuse, an old locator reports RowDeleted; afterward it reports StaleRowId and cannot access the new occupant.
Buffer pool and write order
The buffer pool owns a bounded set of raw page frames, uses simple round-robin eviction, pins pages while guards are alive, and refuses to evict pinned pages. Before writing a dirty data page it makes the WAL durable through that page's pageLSN. If the WAL flush fails, the data-page write is not attempted.
construct after-image with pageLSN
→ append PageUpdate
→ publish dirty buffer frame
→ flush WAL through pageLSN
→ write data pageWAL
Each database uses two alternating slots: <database>-wal and <database>-wal.next. Logical LSNs and physical offsets are deliberately different:
logical LSN = generation base_lsn + (physical record offset - 48)
The WAL header is 48 bytes, format version 3, with a whole-header CRC32C. Record headers are 40 bytes, format version 2; the type determines the only valid total length. Record types are Begin, PageUpdate, Commit, Abort, and RollbackComplete. PageUpdate carries complete 4 KiB before/after images.
A physically complete record whose checksum fails is corruption and is never truncated as a crash tail, even at EOF. Only an incomplete final record whose available header passes structural checks may be discarded at the recovery boundary.
Transactions and the single writer
Writer ownership is acquired lazily on the first write; read-only transactions do not reserve it. Commit releases ownership only after the Commit record reaches durable storage. Rollback first makes Abort durable, follows the prevLSN chain backward, installs validated before-images, then durably records RollbackComplete.
Dropping an unfinished dirty writer does not silently release it: later writes require recovery, and close reports an error. flush remains legal during an active transaction because the engine uses STEAL and WAL-orders every page write; flush success is not commit.
Startup recovery
Open data file + WAL
→ Analysis
├── Winners (Commit exists)
├── Completed rollback (RollbackComplete)
└── Losers (incomplete or Abort-only)
→ Redo non-rolled-back PageUpdates in ascending LSN
→ Undo losers in descending global LSN
→ Sync undo + durably finalize recovered losersRecovery completes synchronously before the buffer pool is exposed. pageLSN may skip redo only after the whole page validates. A checksum-invalid current page is a hard error. Recovery does not reconstruct the page from WAL that a checkpoint may already have recycled.
Checkpoints
Checkpoints are explicit, synchronous, and quiescent. They return a typed error instead of waiting whenever a transaction handle remains, a writer is active/pending, or runtime health requires startup recovery. A successful checkpoint flushes the WAL, WAL-orders and syncs every dirty page, then creates and syncs the next WAL generation. Clean-shutdown markers are omitted on purpose: open already scans one bounded current generation.
Persistent B+Tree
Heap and B+Tree pages share one database file, buffer pool, transaction chain, WAL, recovery pass, and checkpoint. Index pages are ordinary checksummed Page v5 pages with exactly one generation-1 payload slot.
netbadb-index owns ordering, nodes, and versioned codecs, with no dependency on storage, SQL, or the executor. BTreeHandle is a stable metadata-page identity; a root split can replace the root without changing the handle. Registered indexes are backfilled by create_index, maintained by heap and SQL DML, and visible to the planner as IndexScan. SQL index DDL is not available.
Integrity, not authentication
Page CRC and WAL CRC detect persistent corruption. They neither repair it nor provide cryptographic authentication. Decoder fuzzing covers WAL recovery, Page v5, and B+Tree nodes.