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 file

Page 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.

OffsetFieldMeaning
0..4NBP1Page magic
4..6u16 versionFormat version 5
6u8 type2 Heap · 3 BTreeMeta · 4 Internal · 5 Leaf
8..10slot countSlot-directory entries
10..14free boundsFree-space lower / upper bounds
16..24pageLSN0 means no WAL record yet
24..28CRC32CWhole-page checksum bound to the expected PageId
28..slotsu16 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 page

WAL

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 losers

Recovery 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.