writeonce/database/src/wal.h
shoney.arickathil 44c77ff5bf feat: update-point + delete engine half (iteration 9, Task 5 engine)
- wo_row_update_field: encode new value, unique re-check against a
  shadow BEFORE any mutation (violating update leaves the row
  untouched, DB_ERR_UNIQUE), index entries moved old-hash -> new-hash,
  old engine value freed; proven by test_table (unique refusal keeps
  the row, released key becomes insertable)
- WAL UPDATE record: full-row re-log, replay = replace (remove +
  re-create same id); prefix/suffix delta recorded as later
  optimization; test_wal replays insert+update to the updated state
- builtins 62 DB_UPDATE_FIELD (cid,id,field,value) and 63 DB_DELETE
  (cid,id), commit-before-ack like insert, WO_T_UNIQUE/WO_T_DB/WO_T_IO
  mapping; dispatch range 61..63; loader arities; runner mirror
- plan Task 5 marked superseded-in-part with the recorded deviation:
  the language surface (reads, queries, row views, delete statement)
  is 9b's, where the comprehension design put it -- no interim brace-
  select grammar to retire later
- gates: test_table 839/0, test_wal 102/0, 15 suites, oop-e2e 73/0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 13:01:05 +02:00

91 lines
4.1 KiB
C

/* wal.h — typed-row write-ahead log + boot replay (iteration 9, Task 2).
*
* The c-runtime plan's shipped pattern (phases D/E), generalized to typed
* rows. The commit order is doctrine, verbatim:
*
* RAM apply → wal_append (staged) → wal_commit (write + fdatasync)
* → only then is the write ACKNOWLEDGED
*
* Record framing — replay-whole-or-not-at-all:
*
* record := len u32 | crc u32 | payload | mark u32
* len = payload byte count (never 0; 0 = preallocated tail, stop)
* crc = CRC32 of payload
* mark = 0x574F4C31 "WOL1" — written LAST, so a record without its
* mark is torn by definition
* payload := kind u8 | class_id u32 | row_id u64 | body
* kind : 1 insert (body = the row's fields, engine encoding below)
* 2 remove (no body)
* 3 update (reserved for Task 5)
*
* Field encoding in a body walks the class table's kinds:
* SCALAR 8 bytes
* TEXT u32 len | bytes (0xFFFFFFFF = nil)
* OWNED u8 0 = nil, or u8 1 | u32 class_id | fields recursively
* MULTI u8 0 = nil, or u8 1 | u8 elem_kind | u32 len | elements
* MAP u8 0 = nil, or u8 1 | u8 kk | u8 vk | u32 len | k v pairs
*
* Replay decodes payloads STRAIGHT into engine-owned values — the VM heap
* is never involved (boot must not depend on a VM existing yet), and rows
* re-enter through the same choke-point row API, so Task 4's indexes are
* rebuilt for free. A torn tail (short record, bad CRC, missing mark) drops
* everything from the tear onward — never a partial record, never a record
* after a tear. Little-endian on-disk, matching the .wob loader's platform
* note.
*
* wo_wal_check is the offline oracle the crash battery verifies with: it
* walks a WAL file with no engine at all and reports how many records are
* intact and where the intact prefix ends. */
#ifndef WO_WAL_H
#define WO_WAL_H
#include "table.h"
#define WO_WAL_MARK 0x574F4C31u /* "WOL1" LE */
enum { WO_WAL_INSERT = 1, WO_WAL_REMOVE = 2, WO_WAL_UPDATE = 3 };
typedef struct wo_wal {
int fd;
uint64_t off; /* next write offset (the intact tail) */
/* staged batch: appended by wal_append_*, flushed by wal_commit */
uint8_t *buf;
size_t len, cap;
} wo_wal;
/* Open (create if missing) and preallocate [prealloc] bytes (best-effort;
* a filesystem without fallocate still works). Positions the write offset
* at the end of the INTACT record prefix — an existing file is scanned the
* same way replay scans it, so a torn tail is overwritten, not appended
* after. 0 ok, -1 errno-style failure. */
int wo_wal_open(wo_wal *w, const char *path, uint64_t prealloc);
void wo_wal_close(wo_wal *w);
/* Stage a record for the row that MUST already be applied to RAM (the
* commit-order doctrine). Insert/update read the row via wo_row_ptr.
* 0 ok, -1 OOM / no such row. */
int wo_wal_append_insert(wo_wal *w, wo_db *db, uint32_t class_id, uint64_t id);
int wo_wal_append_remove(wo_wal *w, uint32_t class_id, uint64_t id);
/* UPDATE re-logs the whole row (KISS: replay replaces — remove + re-create
* with the same id; the prefix/suffix delta trick from the survey is a
* later optimization, recorded). Call AFTER the RAM update. */
int wo_wal_append_update(wo_wal *w, wo_db *db, uint32_t class_id, uint64_t id);
/* Write the staged batch and fdatasync — the ack line. Empty batch = ok,
* no syscall. 0 ok, -1 write/sync failure (the batch stays staged). */
int wo_wal_commit(wo_wal *w);
/* Boot replay: apply every intact record to [db] in order. Ids re-enter
* exactly as logged; each table's next_id advances past the replayed ids
* that belong to this shard. Returns the number of records applied, or -1
* on open failure / a record naming an unknown class (corruption beyond
* what a torn tail explains). A torn tail is NOT an error: replay applies
* the intact prefix and reports it. */
int64_t wo_wal_replay(const char *path, wo_db *db);
/* Offline verification (no engine): scan [path], count intact records.
* *intact_bytes (optional) = where the intact prefix ends. -1 = open
* failure. */
int64_t wo_wal_check(const char *path, uint64_t *intact_bytes);
#endif /* WO_WAL_H */