writeonce/database/src/wal.c
shoney.arickathil 0b9f09fc12 fix(db2-keys): stage the schema head before the first offset capture
- `seed` of docs/examples/residency (`resident: keys`) on a FRESH WO_DATA
  segfaulted rc 139 in both the dir and the file form; pre-existing.
- Root cause: the databasev2 12 head record was staged lazily INSIDE the
  first `wo_wal_append_*` (wal.c `stage()`), after db.c:78/293 had read
  `koff = wo_wal_next_offset(w)`; the first keys-resident row was re-pointed
  at the schema record and its first read folded "record header is
  malformed"; `wo_idx_probe` borrows with `msg == NULL` -> zero-page write.
- Fix: one helper `stage_schema_head` shared by `stage()`,
  `wo_wal_ensure_schema` and `wo_wal_next_offset` (no longer a pure inline):
  the head is staged before any caller observes `off + len`. Still lazy,
  never for a log that stays empty; head-stage OOM is `wo_wal_stage_fatal`.
  db.c untouched; compaction/migrate stage the head explicitly, unaffected.
- Failing test first: `test_keys_resident_fresh_log_first_row` (test_wal.c),
  the db.c:78 sequence call for call, then read-by-id, `wo_idx_probe`,
  replay. Pre-fix: `koff != 0` FAIL, `wo_row_read` -1 "record header is
  malformed", ASan SEGV `wo_wal_fold_row_at wal.c:1871` via `table.c:373`.
- Gates: test_wal 6650/0 (was 6629); `make -C runtime test` 21 suites
  8452/0 (was 8431); wovm-asan clean; residency `seed` + restart `order`
  under wovm_asan rc 0 on a fresh dir AND a fresh app.db; a control build
  with wal.c/wal.h reverted reproduces the SEGV.
- CODE-LOGIC §Schema migrations: "Head before any offset capture" bullet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 631007839451fb970e9dec83338d19c09b3043ab)
2026-09-15 01:16:13 +02:00

2119 lines
84 KiB
C

/* pread/pwrite/fdatasync/posix_fallocate under -std=c11 */
#define _POSIX_C_SOURCE 200809L
#include "wal.h"
#include <errno.h>
#include <time.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* ---- crc32 (poly 0xEDB88320) — ported from runtime/wo-rt.c ------------- */
static uint32_t crc_table[256];
static int crc_ready;
static void crc32_init(void) {
for (uint32_t i = 0; i < 256; i++) {
uint32_t c = i;
for (int k = 0; k < 8; k++) c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1;
crc_table[i] = c;
}
crc_ready = 1;
}
static uint32_t crc32(const void *buf, size_t len) {
if (!crc_ready) crc32_init();
const uint8_t *p = buf;
uint32_t c = 0xFFFFFFFFu;
while (len--) c = crc_table[(c ^ *p++) & 0xFF] ^ (c >> 8);
return c ^ 0xFFFFFFFFu;
}
/* ---- byte buffer -------------------------------------------------------- */
typedef struct {
uint8_t *b;
size_t len, cap;
int oom;
} wbuf;
static void wput(wbuf *w, const void *p, size_t n) {
if (w->oom) return;
if (w->len + n > w->cap) {
size_t nc = w->cap ? w->cap * 2 : 256;
while (nc < w->len + n) nc *= 2;
uint8_t *nb = realloc(w->b, nc);
if (!nb) {
w->oom = 1;
return;
}
w->b = nb;
w->cap = nc;
}
memcpy(w->b + w->len, p, n);
w->len += n;
}
static void wput_u8(wbuf *w, uint8_t v) { wput(w, &v, 1); }
static void wput_u32(wbuf *w, uint32_t v) { wput(w, &v, 4); }
static void wput_u64(wbuf *w, uint64_t v) { wput(w, &v, 8); }
/* bounds-checked reader */
typedef struct {
const uint8_t *p, *end;
int bad;
} rbuf;
static int rtake(rbuf *r, void *out, size_t n) {
if (r->bad || (size_t)(r->end - r->p) < n) {
r->bad = 1;
return -1;
}
memcpy(out, r->p, n);
r->p += n;
return 0;
}
static uint8_t rd_u8(rbuf *r) {
uint8_t v = 0;
rtake(r, &v, 1);
return v;
}
static uint32_t rd_u32(rbuf *r) {
uint32_t v = 0;
rtake(r, &v, 4);
return v;
}
static uint64_t rd_u64(rbuf *r) {
uint64_t v = 0;
rtake(r, &v, 8);
return v;
}
#define WAL_NIL_TEXT 0xFFFFFFFFu
/* ---- engine-value <-> bytes (kind-driven, mirrors table.c's encoding) --- */
static void enc_val(wbuf *w, const wo_classdesc *classes, uint8_t kind, uint64_t v) {
switch (kind) {
/* iteration 19: a Float is one word on the wire, its raw IEEE bits — no
decimal rendering anywhere in the durability path, so replay is
bit-exact for NaN, the infinities, and -0.0 alike. A Bytes is the same
length-prefixed blob a Text is; the class table's kind byte is what
says which one comes back out. */
case WO_K_SCALAR:
case WO_K_FLOAT: wput_u64(w, v); return;
case WO_K_TEXT:
case WO_K_BYTES: {
if (!v) {
wput_u32(w, WAL_NIL_TEXT);
return;
}
const db_text *t = (const db_text *)(uintptr_t)v;
wput_u32(w, t->len);
wput(w, t->bytes, t->len);
return;
}
case WO_K_OWNED: {
if (!v) {
wput_u8(w, 0);
return;
}
const db_rec *r = (const db_rec *)(uintptr_t)v;
wput_u8(w, 1);
wput_u32(w, r->class_id);
const wo_classdesc *c = &classes[r->class_id];
for (uint32_t i = 0; i < c->field_cnt; i++)
enc_val(w, classes, c->kinds[i], r->slots[i]);
return;
}
case WO_K_MULTI: {
if (!v) {
wput_u8(w, 0);
return;
}
const db_multi *m = (const db_multi *)(uintptr_t)v;
wput_u8(w, 1);
wput_u8(w, m->elem_kind);
wput_u32(w, m->len);
for (uint32_t i = 0; i < m->len; i++) enc_val(w, classes, m->elem_kind, m->items[i]);
return;
}
case WO_K_MAP: {
if (!v) {
wput_u8(w, 0);
return;
}
const db_map *m = (const db_map *)(uintptr_t)v;
wput_u8(w, 1);
wput_u8(w, m->key_kind);
wput_u8(w, m->val_kind);
wput_u32(w, m->len);
for (uint32_t i = 0; i < m->len; i++) {
enc_val(w, classes, m->key_kind, m->kv[2 * i]);
enc_val(w, classes, m->val_kind, m->kv[2 * i + 1]);
}
return;
}
default: return; /* GCREF never stored, so never logged */
}
}
/* Decode one value into an engine-owned allocation. Returns 0 on success
* with *out set (0 = genuine nil); -1 on truncation/corruption/OOM — the
* caller frees what it already built. */
static int dec_val(rbuf *r, wo_db *db, uint8_t kind, uint64_t *out) {
*out = 0;
switch (kind) {
case WO_K_SCALAR:
case WO_K_FLOAT: { /* iteration 19: the same word back, uninterpreted */
uint64_t v = rd_u64(r);
if (r->bad) return -1;
*out = v;
return 0;
}
case WO_K_TEXT:
case WO_K_BYTES: {
uint32_t len = rd_u32(r);
if (r->bad) return -1;
if (len == WAL_NIL_TEXT) return 0;
if ((size_t)(r->end - r->p) < len) return -1;
db_text *t = malloc(sizeof(db_text) + len);
if (!t) return -1;
t->len = len;
memcpy(t->bytes, r->p, len);
r->p += len;
*out = (uint64_t)(uintptr_t)t;
return 0;
}
case WO_K_OWNED: {
uint8_t tag = rd_u8(r);
if (r->bad) return -1;
if (!tag) return 0;
uint32_t cid = rd_u32(r);
if (r->bad || cid >= db->class_cnt) return -1;
const wo_classdesc *c = &db->classes[cid];
db_rec *rec = malloc(sizeof(db_rec) + (size_t)c->field_cnt * 8u);
if (!rec) return -1;
rec->class_id = cid;
rec->_pad = 0;
for (uint32_t i = 0; i < c->field_cnt; i++) {
if (dec_val(r, db, c->kinds[i], &rec->slots[i]) != 0) {
for (uint32_t j = 0; j < i; j++) wo_db_val_free(db, c->kinds[j], rec->slots[j]);
free(rec);
return -1;
}
}
*out = (uint64_t)(uintptr_t)rec;
return 0;
}
case WO_K_MULTI: {
uint8_t tag = rd_u8(r);
if (r->bad) return -1;
if (!tag) return 0;
uint8_t ek = rd_u8(r);
uint32_t len = rd_u32(r);
if (r->bad || ek > WO_K_MAX) return -1;
if (len > (size_t)(r->end - r->p)) return -1; /* each elem >= 1 byte */
db_multi *m = malloc(sizeof(db_multi) + (size_t)len * 8u);
if (!m) return -1;
m->elem_kind = ek;
m->len = len;
for (uint32_t i = 0; i < len; i++) {
if (dec_val(r, db, ek, &m->items[i]) != 0) {
for (uint32_t j = 0; j < i; j++) wo_db_val_free(db, ek, m->items[j]);
free(m);
return -1;
}
}
*out = (uint64_t)(uintptr_t)m;
return 0;
}
case WO_K_MAP: {
uint8_t tag = rd_u8(r);
if (r->bad) return -1;
if (!tag) return 0;
uint8_t kk = rd_u8(r), vk = rd_u8(r);
uint32_t len = rd_u32(r);
if (r->bad || kk > WO_K_MAX || vk > WO_K_MAX) return -1;
if (len > (size_t)(r->end - r->p)) return -1;
db_map *m = malloc(sizeof(db_map) + (size_t)len * 16u);
if (!m) return -1;
m->key_kind = kk;
m->val_kind = vk;
m->len = len;
for (uint32_t i = 0; i < len; i++) {
if (dec_val(r, db, kk, &m->kv[2 * i]) != 0 ||
dec_val(r, db, vk, &m->kv[2 * i + 1]) != 0) {
m->len = i; /* free only the fully-built pairs plus a possible key */
for (uint32_t j = 0; j < i; j++) {
wo_db_val_free(db, kk, m->kv[2 * j]);
wo_db_val_free(db, vk, m->kv[2 * j + 1]);
}
wo_db_val_free(db, kk, m->kv[2 * i]); /* 0 if the key failed */
free(m);
return -1;
}
}
*out = (uint64_t)(uintptr_t)m;
return 0;
}
default: return -1;
}
}
/* ---- record scan (shared by open, replay, check) ------------------------ */
/* Read the record at [off]. 0 = intact (*len_out = payload length, payload
* malloc'd into *payload_out if non-NULL); 1 = end of intact prefix (zero
* length, short read, bad crc, missing mark). */
static int scan_record(int fd, uint64_t off, uint32_t *len_out, uint8_t **payload_out) {
uint8_t hdr[8];
ssize_t n = pread(fd, hdr, 8, (off_t)off);
if (n != 8) return 1;
uint32_t len, crc;
memcpy(&len, hdr, 4);
memcpy(&crc, hdr + 4, 4);
if (len == 0 || len > (64u << 20)) return 1; /* preallocated tail or garbage */
uint8_t *payload = malloc(len + 4);
if (!payload) return 1;
n = pread(fd, payload, len + 4, (off_t)(off + 8));
if (n != (ssize_t)(len + 4)) {
free(payload);
return 1;
}
uint32_t mark;
memcpy(&mark, payload + len, 4);
if (mark != WO_WAL_MARK || crc32(payload, len) != crc) {
free(payload);
return 1;
}
*len_out = len;
if (payload_out) *payload_out = payload;
else free(payload);
return 0;
}
/* Task 4 follow-up (review finding): scan_record's exact framing, but read
* from the STAGED bytes in [w->off, w->off + w->len) instead of the file.
* A record staged behind this batch's own not-yet-run barrier is real,
* fully-framed data sitting in `w->buf` — preading its file offset would
* see whatever was on disk BEFORE this batch (the pre-allocated zero
* tail, ordinarily), which scan_record correctly, but unhelpfully, reads
* as "no record here yet". Used only by wo_wal_fold_row_at, and only for
* an offset a caller got from wo_wal_repoint_offset1 (a PENDING re-point,
* not yet flushed) — never for a durable offset, which stays on the
* scan_record/pread path unchanged. 0 = intact, 1 = short/bad, same as
* scan_record. */
static int scan_record_staged(const wo_wal *w, uint64_t off, uint32_t *len_out,
uint8_t **payload_out) {
if (off < w->off) return 1;
uint64_t rel = off - w->off;
if (rel + 8 > w->len) return 1;
uint32_t len, crc;
memcpy(&len, w->buf + rel, 4);
memcpy(&crc, w->buf + rel + 4, 4);
if (len == 0 || len > (64u << 20)) return 1; /* garbage: not a real frame */
if (rel + 8 + (uint64_t)len + 4 > w->len) return 1; /* short: not fully staged */
const uint8_t *payload = w->buf + rel + 8;
uint32_t mark;
memcpy(&mark, payload + len, 4);
if (mark != WO_WAL_MARK || crc32(payload, len) != crc) return 1;
if (payload_out) {
uint8_t *cp = malloc((size_t)len + 4);
if (!cp) return 1;
memcpy(cp, payload, (size_t)len + 4);
*payload_out = cp;
}
*len_out = len;
return 0;
}
/* ---- public API ---------------------------------------------------------- */
int wo_wal_open(wo_wal *w, const char *path, uint64_t prealloc) {
memset(w, 0, sizeof(*w));
w->fd = open(path, O_RDWR | O_CREAT, 0644);
if (w->fd < 0) return -1;
w->path = strdup(path); /* NULL is tolerated: the diagnostic degrades */
/* databasev2 3: remove a stale compaction temp before doing anything else.
* The only way one exists is a crash before the rename, which means its
* records were never authoritative — the live log below is the truth. It is
* deleted rather than ignored because a file full of well-formed records
* sitting beside the log is exactly the thing a future reader mistakes for
* data. */
{
char tmp[4096];
if ((size_t)snprintf(tmp, sizeof tmp, "%s%s", path, WO_WAL_TMP_SUFFIX) < sizeof tmp)
(void)unlink(tmp);
}
w->prealloc = prealloc;
if (prealloc) {
/* best-effort: a filesystem without fallocate still works */
(void)posix_fallocate(w->fd, 0, (off_t)prealloc);
}
/* position after the intact prefix: a torn tail is OVERWRITTEN by the
* next append, never appended after */
uint64_t off = 0;
uint32_t len;
while (scan_record(w->fd, off, &len, NULL) == 0) off += 8u + len + 4u;
w->off = off;
return 0;
}
/* dirname(3) without its allocation and locale baggage: "a/b" → "a", "/b" →
* "/", "b" → ".". The boot-time parent check (wo_wal_resolve_data_path) and
* the post-rename fsync (sync_parent_dir) MUST agree on what the parent is;
* this is the one place that decides. 0 ok, -1 when [path] does not fit. */
static int parent_dir_of(const char *path, char *dir, size_t cap) {
size_t n = strlen(path);
if (n >= cap) return -1;
memcpy(dir, path, n + 1);
char *slash = strrchr(dir, '/');
if (slash == dir) dir[1] = '\0';
else if (slash) *slash = '\0';
else memcpy(dir, ".", 2);
return 0;
}
int wo_wal_resolve_data_path(const char *wo_data, char *out, size_t cap) {
size_t n = strlen(wo_data);
struct stat st;
int have = stat(wo_data, &st) == 0;
if ((n && wo_data[n - 1] == '/') || (have && S_ISDIR(st.st_mode))) {
/* the directory form: byte for byte what main.c produced before this
* function existed, doubled slash after a trailing '/' included */
if ((size_t)snprintf(out, cap, "%s/shard-0.wal", wo_data) >= cap)
return WO_WAL_PATH_TOO_LONG;
return 0;
}
if (have && !S_ISREG(st.st_mode)) return WO_WAL_PATH_NOT_A_FILE;
if (!have) {
/* absent: wo_wal_open creates it, but only where a directory already
* is — the parent is handed back so the refusal can name it */
if (parent_dir_of(wo_data, out, cap) != 0) return WO_WAL_PATH_TOO_LONG;
struct stat pst;
if (stat(out, &pst) != 0 || !S_ISDIR(pst.st_mode)) return WO_WAL_PATH_NO_PARENT;
}
if (n >= cap) return WO_WAL_PATH_TOO_LONG;
memcpy(out, wo_data, n + 1);
return 0;
}
int wo_wal_pend_drop(wo_wal *w, uint32_t cid, uint64_t id, uint64_t off) {
if (w->pend_len == w->pend_cap) {
size_t nc = w->pend_cap ? w->pend_cap * 2 : 16;
struct wo_wal_pend *np = realloc(w->pend, nc * sizeof *np);
if (!np) return -1; /* the row stays resident: safe, just not dropped */
w->pend = np;
w->pend_cap = nc;
}
w->pend[w->pend_len].cid = cid;
w->pend[w->pend_len].id = id;
w->pend[w->pend_len].off = off;
w->pend_len++;
return 0;
}
/* Task 4 (keys-resident delta updates): pend_drop's counterpart for a
* re-point, own list, same reason (see the `repoint` field in wal.h). */
int wo_wal_pend_repoint(wo_wal *w, uint32_t cid, uint64_t id, uint64_t off) {
if (w->repoint_len == w->repoint_cap) {
size_t nc = w->repoint_cap ? w->repoint_cap * 2 : 16;
struct wo_wal_pend *np = realloc(w->repoint, nc * sizeof *np);
if (!np) return -1; /* the map stays where it was: see the header */
w->repoint = np;
w->repoint_cap = nc;
}
w->repoint[w->repoint_len].cid = cid;
w->repoint[w->repoint_len].id = id;
w->repoint[w->repoint_len].off = off;
w->repoint_len++;
return 0;
}
uint64_t wo_wal_repoint_offset1(const wo_wal *w, uint32_t cid, uint64_t id) {
/* backward: the LATEST entry for (cid, id) is the one still current if
this row was updated more than once behind the same barrier */
for (size_t i = w->repoint_len; i > 0; i--) {
if (w->repoint[i - 1].cid == cid && w->repoint[i - 1].id == id)
return w->repoint[i - 1].off + 1;
}
return 0;
}
void wo_db_flush_drops(wo_db *db, wo_wal *w) {
for (size_t i = 0; i < w->pend_len; i++)
(void)wo_row_drop_payload(db, w->pend[i].cid, w->pend[i].id, w->pend[i].off);
w->pend_len = 0;
/* Task 4: the request path's deferred update re-points, held behind the
same barrier as inserts' drops for the same reason — before the
commit above, these offsets pread zeros. */
for (size_t i = 0; i < w->repoint_len; i++)
(void)wo_row_set_offset(db, w->repoint[i].cid, w->repoint[i].id, w->repoint[i].off);
w->repoint_len = 0;
}
void wo_wal_close(wo_wal *w) {
free(w->schema);
w->schema = NULL;
w->schema_len = 0;
free(w->pend);
free(w->repoint);
if (w->fd >= 0) close(w->fd);
free(w->path);
free(w->buf);
memset(w, 0, sizeof(*w));
w->fd = -1;
}
static int stage(wo_wal *w, const wbuf *payload);
/* databasev2 12: the schema head is written LAZILY, ahead of the first real
* record — never for a log that stays empty. A program whose only tables are
* `durable: false` opens a WAL and appends nothing, and the documented
* contract is that such a run writes ZERO bytes; an eager head record broke
* that by 75 bytes and the residency gate caught it.
*
* Two callers, and the ORDER between them is the invariant: stage() (the
* first append) and wo_wal_next_offset (a caller capturing that first
* record's offset BEFORE appending it — db.c's koff/roff pattern). The head
* must be staged before either observes off + len; staged only inside the
* append, the captured offset named the head instead of the row, and the
* first keys-resident row of a fresh log read back as "record header is
* malformed" (2026-09-10). The flag, not the emptiness check, breaks the
* recursion through stage(). */
static int stage_schema_head(wo_wal *w) {
if (!w->schema || w->schema_written) return 0;
w->schema_written = 1;
if (w->off != 0 || w->len != 0) return 0; /* records exist: legacy until compaction */
wbuf sp = {0};
wput(&sp, w->schema, w->schema_len);
int rc = stage(w, &sp);
free(sp.b);
return rc;
}
uint64_t wo_wal_next_offset(wo_wal *w) {
/* a head-stage failure here is the death the following append would
* have taken for the same OOM — never answer with an offset the head
* would then displace */
if (stage_schema_head(w) != 0) wo_wal_stage_fatal(w);
return w->off + w->len;
}
/* frame one payload into the staged batch */
static int stage(wo_wal *w, const wbuf *payload) {
if (payload->oom) return -1;
if (stage_schema_head(w) != 0) return -1;
wbuf rec = {0};
wput_u32(&rec, (uint32_t)payload->len);
wput_u32(&rec, crc32(payload->b, payload->len));
wput(&rec, payload->b, payload->len);
wput_u32(&rec, WO_WAL_MARK);
if (rec.oom) {
free(rec.b);
return -1;
}
if (w->len + rec.len > w->cap) {
size_t nc = w->cap ? w->cap * 2 : 4096;
while (nc < w->len + rec.len) nc *= 2;
uint8_t *nb = realloc(w->buf, nc);
if (!nb) {
free(rec.b);
return -1;
}
w->buf = nb;
w->cap = nc;
}
memcpy(w->buf + w->len, rec.b, rec.len);
w->len += rec.len;
free(rec.b);
return 0;
}
/* ---- databasev2 12: the schema record -------------------------------- */
int wo_schema_encode(const wo_schema *sc, uint8_t **payload_out, uint32_t *len_out) {
wbuf p = {0};
wput_u8(&p, WO_WAL_SCHEMA);
wput_u32(&p, sc->class_cnt);
for (uint32_t c = 0; c < sc->class_cnt; c++) {
const wo_schema_class *k = &sc->classes[c];
wput_u32(&p, k->name_len);
wput(&p, k->name, k->name_len);
wput_u32(&p, k->flags);
wput_u32(&p, k->field_cnt);
for (uint32_t f = 0; f < k->field_cnt; f++) {
const wo_schema_field *fl = &k->fields[f];
wput_u32(&p, fl->name_len);
wput(&p, fl->name, fl->name_len);
wput_u8(&p, fl->kind);
wput_u32(&p, fl->fclass);
wput_u32(&p, fl->felem);
}
}
if (p.oom) {
free(p.b);
return -1;
}
*payload_out = p.b;
*len_out = (uint32_t)p.len;
return 0;
}
wo_schema *wo_schema_decode(const uint8_t *payload, uint32_t len) {
if (len < 5 || payload[0] != WO_WAL_SCHEMA) return NULL;
/* names point into one private copy of the bytes, so the schema outlives
* whatever buffer the caller hands in */
wo_schema *sc = calloc(1, sizeof *sc);
if (!sc) return NULL;
sc->owned = malloc(len);
if (!sc->owned) {
free(sc);
return NULL;
}
memcpy(sc->owned, payload, len);
rbuf r = {sc->owned + 1, sc->owned + len, 0};
sc->class_cnt = rd_u32(&r);
if (r.bad || sc->class_cnt > 65536) goto bad;
sc->classes = calloc(sc->class_cnt ? sc->class_cnt : 1, sizeof *sc->classes);
if (!sc->classes) goto bad;
for (uint32_t c = 0; c < sc->class_cnt; c++) {
wo_schema_class *k = &sc->classes[c];
k->name_len = rd_u32(&r);
if (r.bad || (size_t)(r.end - r.p) < k->name_len) goto bad;
k->name = r.p;
r.p += k->name_len;
k->flags = rd_u32(&r);
k->field_cnt = rd_u32(&r);
if (r.bad || k->field_cnt > 65536) goto bad;
k->fields = calloc(k->field_cnt ? k->field_cnt : 1, sizeof *k->fields);
if (!k->fields) goto bad;
for (uint32_t f = 0; f < k->field_cnt; f++) {
wo_schema_field *fl = &k->fields[f];
fl->name_len = rd_u32(&r);
if (r.bad || (size_t)(r.end - r.p) < fl->name_len) goto bad;
fl->name = r.p;
r.p += fl->name_len;
fl->kind = rd_u8(&r);
fl->fclass = rd_u32(&r);
fl->felem = rd_u32(&r);
if (r.bad) goto bad;
}
}
if (r.p != r.end) goto bad; /* trailing bytes = malformed */
return sc;
bad:
wo_schema_free(sc);
return NULL;
}
void wo_schema_free(wo_schema *sc) {
if (!sc) return;
if (sc->classes)
for (uint32_t c = 0; c < sc->class_cnt; c++) free(sc->classes[c].fields);
free(sc->classes);
free(sc->owned);
free(sc);
}
/* ---- databasev2 12: the boot diff ------------------------------------- */
static int sc_name_eq(const uint8_t *a, uint32_t al, const uint8_t *b, uint32_t bl) {
return al == bl && (al == 0 || memcmp(a, b, al) == 0);
}
static uint32_t sc_find_class(const wo_schema *sc, const uint8_t *name, uint32_t len) {
for (uint32_t c = 0; c < sc->class_cnt; c++)
if (sc_name_eq(sc->classes[c].name, sc->classes[c].name_len, name, len)) return c;
return WO_SCHEMA_NONE;
}
static uint32_t sc_find_field(const wo_schema_class *k, const uint8_t *name, uint32_t len) {
for (uint32_t f = 0; f < k->field_cnt; f++)
if (sc_name_eq(k->fields[f].name, k->fields[f].name_len, name, len)) return f;
return WO_SCHEMA_NONE;
}
/* fclass words number classes in their OWN schema, so under reordering the
* same referenced class carries different numbers — equality is by the NAME
* the number resolves to, never the number itself */
static int sc_ref_eq(const wo_schema *osc, uint32_t ofc, const wo_schema *nsc, uint32_t nfc) {
if (ofc == WO_SCHEMA_NONE || nfc == WO_SCHEMA_NONE) return ofc == nfc;
if (ofc >= osc->class_cnt || nfc >= nsc->class_cnt) return 0;
return sc_name_eq(osc->classes[ofc].name, osc->classes[ofc].name_len,
nsc->classes[nfc].name, nsc->classes[nfc].name_len);
}
static int sc_field_shape_eq(const wo_schema *osc, const wo_schema_field *of,
const wo_schema *nsc, const wo_schema_field *nf) {
return of->kind == nf->kind && of->felem == nf->felem &&
sc_ref_eq(osc, of->fclass, nsc, nf->fclass);
}
#if defined(__GNUC__)
__attribute__((format(printf, 1, 2)))
#endif
static char *sc_poisonf(const char *fmt, ...) {
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
return strdup(buf);
}
void wo_mig_plan_free(wo_mig_plan *plan) {
if (!plan->classes) return;
for (uint32_t c = 0; c < plan->old_class_cnt; c++) {
free(plan->classes[c].poison);
free(plan->classes[c].fmap);
}
free(plan->classes);
plan->classes = NULL;
}
int wo_schema_diff(const wo_schema *osc, const wo_schema *nsc, wo_mig_plan *plan) {
memset(plan, 0, sizeof *plan);
plan->old_class_cnt = osc->class_cnt;
plan->classes = calloc(osc->class_cnt ? osc->class_cnt : 1, sizeof *plan->classes);
if (!plan->classes) return -1;
for (uint32_t c = 0; c < osc->class_cnt; c++) {
const wo_schema_class *ok = &osc->classes[c];
wo_mig_class *mc = &plan->classes[c];
mc->old_field_cnt = ok->field_cnt;
mc->new_cid = sc_find_class(nsc, ok->name, ok->name_len);
if (mc->new_cid == WO_SCHEMA_NONE) {
mc->poison = sc_poisonf("class `%.*s` has stored rows this binary no "
"longer declares — restore the class, or delete "
"WO_DATA if the rows are expendable",
(int)ok->name_len, (const char *)ok->name);
continue;
}
const wo_schema_class *nk = &nsc->classes[mc->new_cid];
if (ok->flags != nk->flags) {
mc->new_cid = WO_SCHEMA_NONE;
mc->poison = sc_poisonf("class `%.*s` changed its storage declaration "
"(durable/resident) with rows in the log — v1 "
"migrates fields, not storage modes",
(int)ok->name_len, (const char *)ok->name);
continue;
}
mc->fmap = malloc((ok->field_cnt ? ok->field_cnt : 1) * sizeof *mc->fmap);
if (!mc->fmap) return -1;
uint32_t deleted = 0;
for (uint32_t f = 0; f < ok->field_cnt; f++) {
const wo_schema_field *of = &ok->fields[f];
uint32_t nf = sc_find_field(nk, of->name, of->name_len);
if (nf == WO_SCHEMA_NONE) {
mc->fmap[f] = -1;
deleted++;
continue;
}
if (!sc_field_shape_eq(osc, of, nsc, &nk->fields[nf])) {
free(mc->fmap);
mc->fmap = NULL;
mc->new_cid = WO_SCHEMA_NONE;
mc->poison = sc_poisonf("class `%.*s`: field `%.*s` changed its type "
"— v1 has no conversions; add a new field "
"and backfill instead",
(int)ok->name_len, (const char *)ok->name,
(int)of->name_len, (const char *)of->name);
break;
}
mc->fmap[f] = (int32_t)nf;
}
if (!mc->fmap) continue; /* poisoned above */
uint32_t added = 0;
for (uint32_t f = 0; f < nk->field_cnt; f++)
if (sc_find_field(ok, nk->fields[f].name, nk->fields[f].name_len) ==
WO_SCHEMA_NONE)
added++;
mc->changed = (deleted > 0 || added > 0);
/* a deleted and an added field of the SAME shape in one step is
* byte-for-byte indistinguishable from a rename, and the two
* readings differ by exactly one column of destroyed data */
if (deleted && added) {
for (uint32_t f = 0; f < ok->field_cnt && mc->fmap; f++) {
if (mc->fmap[f] != -1) continue;
for (uint32_t g = 0; g < nk->field_cnt; g++) {
if (sc_find_field(ok, nk->fields[g].name, nk->fields[g].name_len) !=
WO_SCHEMA_NONE)
continue;
if (sc_field_shape_eq(osc, &ok->fields[f], nsc, &nk->fields[g])) {
free(mc->fmap);
mc->fmap = NULL;
mc->new_cid = WO_SCHEMA_NONE;
mc->poison = sc_poisonf(
"class `%.*s`: `%.*s` was deleted and a field of the "
"same type added — a rename and a delete+add are "
"indistinguishable here and one of them destroys data. "
"Deploy the delete and the add as two separate steps",
(int)ok->name_len, (const char *)ok->name,
(int)ok->fields[f].name_len,
(const char *)ok->fields[f].name);
break;
}
}
if (!mc->fmap) break;
}
}
}
/* the embed closure: a class whose old records EMBED (owned or container
* values of) a class whose shape changed cannot be transcoded — the
* nested bytes are in the OLD sub-shape and v1 does not rewrite value
* trees recursively. Iterate to a fixpoint so chains of embedding
* poison through. */
for (int again = 1; again;) {
again = 0;
for (uint32_t c = 0; c < osc->class_cnt; c++) {
wo_mig_class *mc = &plan->classes[c];
if (mc->poison) continue;
for (uint32_t f = 0; f < osc->classes[c].field_cnt; f++) {
const wo_schema_field *of = &osc->classes[c].fields[f];
if (of->kind != WO_K_OWNED && of->kind != WO_K_MULTI &&
of->kind != WO_K_MAP)
continue;
int tainted = 0;
if (of->fclass != WO_SCHEMA_NONE && of->fclass < osc->class_cnt) {
const wo_mig_class *ref = &plan->classes[of->fclass];
tainted = ref->poison != NULL || ref->changed;
} else if (of->kind == WO_K_OWNED) {
/* an owned field with no recorded target class: assume the
* worst whenever anything at all changed */
for (uint32_t x = 0; x < osc->class_cnt && !tainted; x++)
tainted = plan->classes[x].poison != NULL ||
plan->classes[x].changed;
}
if (tainted) {
free(mc->fmap);
mc->fmap = NULL;
mc->new_cid = WO_SCHEMA_NONE;
mc->poison = sc_poisonf(
"class `%.*s`: field `%.*s` embeds a class whose shape "
"changed — its stored values carry the old sub-shape, "
"which v1 does not rewrite. Migrate the embedded class "
"on its own first",
(int)osc->classes[c].name_len,
(const char *)osc->classes[c].name, (int)of->name_len,
(const char *)of->name);
again = 1;
break;
}
}
}
}
plan->identity = 1;
for (uint32_t c = 0; c < osc->class_cnt; c++) {
const wo_mig_class *mc = &plan->classes[c];
/* a poison FORCES the transcode rather than being skipped: only the
* transcode can tell whether records of the class exist. With none,
* the pass runs clean and rewrites the head to the compiled shape —
* one time, since the next boot diffs equal. With records, it stops
* at the first one and hands back the poison text. Skipping instead
* would fall through to replay, which greets the shape mismatch
* with a generic "corruption" — the exact message this iteration
* exists to replace. Caught by the end-to-end retype smoke test. */
if (mc->poison || mc->new_cid != c || mc->changed) {
plan->identity = 0;
break;
}
}
return 0;
}
int wo_wal_set_schema(wo_wal *w, const wo_schema *sc) {
uint8_t *p;
uint32_t len;
if (wo_schema_encode(sc, &p, &len) != 0) return -1;
free(w->schema);
w->schema = p;
w->schema_len = len;
return 0;
}
int wo_wal_ensure_schema(wo_wal *w) {
if (!w->schema) return 0; /* never set: legacy behaviour */
if (w->off != 0 || w->len != 0) return 0; /* records exist or staged */
if (w->schema_written) return 0;
/* the same helper stage() and wo_wal_next_offset use, so no path can
* double-write the head */
if (stage_schema_head(w) != 0) return -1;
return wo_wal_commit(w);
}
int wo_wal_read_schema(const char *path, uint8_t **payload_out, uint32_t *len_out) {
int fd = open(path, O_RDONLY);
if (fd < 0) return errno == ENOENT ? 1 : -1;
uint32_t len;
uint8_t *payload;
int rc = scan_record(fd, 0, &len, &payload);
close(fd);
if (rc != 0) return 1; /* empty or torn head: a legacy log */
if (len < 1 || payload[0] != WO_WAL_SCHEMA) {
free(payload);
return 1;
}
if (payload_out) *payload_out = payload;
else free(payload);
if (len_out) *len_out = len;
return 0;
}
int wo_wal_append_insert(wo_wal *w, wo_db *db, uint32_t class_id, uint64_t id) {
/* wo_row_ptr, NOT wo_row_borrow: at append time a keys-resident row is
* still in its slab and the id map still holds a SLOT, not an offset —
* the drop happens after the commit. Borrowing here would read the log at
* a byte position that is really a slot number. (Compaction, which does
* face rows that live only in the log, moves their bytes instead — see
* copy_record.) */
db_row *r = wo_row_ptr(db, class_id, id);
if (!r) return -1; /* commit order: RAM apply comes FIRST */
wbuf p = {0};
wput_u8(&p, WO_WAL_INSERT);
wput_u32(&p, class_id);
wput_u64(&p, id);
const wo_classdesc *c = &db->classes[class_id];
for (uint32_t i = 0; i < c->field_cnt; i++) enc_val(&p, db->classes, c->kinds[i], r->slots[i]);
int rc = stage(w, &p);
free(p.b);
return rc;
}
int wo_wal_append_update(wo_wal *w, wo_db *db, uint32_t class_id, uint64_t id) {
/* wo_row_ptr is right here for the same reason as append_insert. A
* keys-resident update CAN succeed now (Task 3's row_apply_field_keys),
* but this function never runs for one: db.c routes a keys-resident
* update to a staged DELTA record instead (row_apply_field_keys), and
* never re-logs the whole row. This function stays reachable only for
* resident: all, guarded at both db.c call sites. */
db_row *r = wo_row_ptr(db, class_id, id);
if (!r) return -1;
wbuf p = {0};
wput_u8(&p, WO_WAL_UPDATE);
wput_u32(&p, class_id);
wput_u64(&p, id);
const wo_classdesc *c = &db->classes[class_id];
for (uint32_t i = 0; i < c->field_cnt; i++) enc_val(&p, db->classes, c->kinds[i], r->slots[i]);
int rc = stage(w, &p);
free(p.b);
return rc;
}
/* databasev2 11: the chain-terminating write. Same encode as append_insert,
* but from a row the caller already holds — a keys-resident row whose payload
* has been dropped has no slab image for wo_row_ptr to find, and the update
* path is the one caller that legitimately has the full, post-update values in
* hand because it folded them to maintain indexes.
*
* WO_WAL_UPDATE, not WO_WAL_INSERT, and the distinction is not cosmetic.
* Compaction's own flattening (stage_flattened_row) writes INSERT because it
* builds a FRESH log in which each row appears exactly once. This function
* appends into a LIVE log that already carries the row's original insert, so
* an INSERT here is a duplicate id, and replay correctly refuses a duplicate as
* corruption — caught by test_delta_chain_flatten_replays, which is the only
* way this could have been caught: the record reads back perfectly in-process
* and only fails on the next boot.
*
* UPDATE is exactly right anyway: replay applies it as remove-then-recreate,
* which is what replacing a row wholesale means, and the fold terminates on
* either full-row kind. */
int wo_wal_append_row_image(wo_wal *w, wo_db *db, uint32_t class_id, uint64_t id,
const db_row *r) {
if (!r) return -1;
wbuf p = {0};
wput_u8(&p, WO_WAL_UPDATE);
wput_u32(&p, class_id);
wput_u64(&p, id);
const wo_classdesc *c = &db->classes[class_id];
for (uint32_t i = 0; i < c->field_cnt; i++) enc_val(&p, db->classes, c->kinds[i], r->slots[i]);
int rc = stage(w, &p);
free(p.b);
return rc;
}
int wo_wal_append_delta(wo_wal *w, wo_db *db, uint32_t class_id, uint64_t id,
uint32_t field_idx, uint64_t back_off, uint64_t value) {
wbuf p = {0};
wput_u8(&p, WO_WAL_DELTA);
wput_u32(&p, class_id);
wput_u64(&p, id);
wput_u32(&p, field_idx);
wput_u64(&p, back_off);
const wo_classdesc *c = &db->classes[class_id];
enc_val(&p, db->classes, c->kinds[field_idx], value);
int rc = stage(w, &p);
free(p.b);
return rc;
}
int wo_wal_append_remove(wo_wal *w, uint32_t class_id, uint64_t id) {
wbuf p = {0};
wput_u8(&p, WO_WAL_REMOVE);
wput_u32(&p, class_id);
wput_u64(&p, id);
int rc = stage(w, &p);
free(p.b);
return rc;
}
int wo_wal_commit(wo_wal *w) {
if (!w->len) return 0; /* empty commits are not batches; do not count them */
if (w->len > w->stat_peak_staged) w->stat_peak_staged = w->len;
size_t at = 0;
while (at < w->len) {
ssize_t n = pwrite(w->fd, w->buf + at, w->len - at, (off_t)(w->off + at));
if (n < 0) {
if (errno == EINTR) continue;
return WO_WAL_ERR_WRITE;
}
at += (size_t)n;
}
if (fdatasync(w->fd) != 0) return WO_WAL_ERR_SYNC;
w->off += w->len;
w->len = 0; /* acked: the batch is durable */
return 0;
}
/* Nothing at either fatal point is recoverable: RAM holds changes the log
* does not, and this process can no longer serve reads that would survive a
* restart. Name what failed precisely enough to act on, then stop. */
static void wal_die(const wo_wal *w, const char *op, uint32_t nrec) {
fprintf(stderr,
"writeonce: DURABILITY FAILURE — %s failed on %s: %s\n"
" %u record(s) were NOT made durable and are not acknowledged.\n"
" The process is stopping: replay restores the last durable state.\n",
op, w->path ? w->path : "(the write-ahead log)", strerror(errno),
nrec);
exit(WO_EXIT_DURABILITY);
}
void wo_wal_stage_fatal(const wo_wal *w) { wal_die(w, "staging a record", 1); }
/* IMPORTANT 1 (review finding): unlike wo_wal_pend_drop's failure, a lost
* pend_repoint is not safe to shrug off — the delta it was meant to
* re-point to is already staged, RAM has already moved (table.c's own
* fatal-below-this-line rule), and a second same-drain update to the same
* row would otherwise chain its delta past the lost one, permanently. */
void wo_wal_repoint_fatal(const wo_wal *w) { wal_die(w, "recording a pending re-point", 1); }
void wo_wal_commit_fatal(wo_wal *w, uint32_t nrec) {
int staged = w->len != 0;
int rc = wo_wal_commit(w);
if (rc == 0) {
if (staged) { /* count the barrier that actually happened */
w->stat_batches++;
w->stat_records += nrec;
if (nrec > w->stat_peak_batch) w->stat_peak_batch = nrec;
}
return;
}
wal_die(w, rc == WO_WAL_ERR_SYNC ? "fdatasync" : "pwrite", nrec);
}
uint64_t wo_wal_ckpt_floor = 4u << 20; /* 4 MiB: below this there is nothing worth reclaiming */
uint32_t wo_wal_ckpt_ratio = 3u; /* 3x the live-set's own size is enough history */
int wo_wal_should_compact(uint64_t used, uint64_t last, uint64_t floor, uint32_t ratio) {
if (used < floor) return 0; /* a small log has nothing to reclaim */
if (last == 0) return 1; /* past the floor and never compacted: do it once
* to establish the denominator */
if (ratio == 0) return 0; /* a zero ratio disables the policy rather than
* dividing by nothing */
/* databasev2 11: an ABSOLUTE garbage term, and a ceiling on the
* proportional one.
*
* NOTE THE VOCABULARY TRAP this fixes. `floor` above SUPPRESSES compaction
* on a small log — the opposite of what the same word means in PostgreSQL,
* where autovacuum's `vac_base_thresh` (default 50) TRIGGERS cleanup on a
* small absolute problem that the proportional term would hide. We had the
* proportion and the suppressor and neither of the two guards that keep a
* size-based policy honest:
*
* - without a triggering term, garbage that is large in bytes but small
* relative to a big live set is never reclaimed;
* - a very large live set would otherwise defer compaction forever,
* which is what autovacuum_vacuum_max_threshold exists to stop.
*
* ONE term does both jobs here, and a separate ceiling was tried and
* removed as dead code. PostgreSQL needs two because its threshold counts
* TUPLES and its two constants sit at opposite ends (base 50, max 1e8).
* Ours counts BYTES, so "compact once garbage exceeds X" already caps
* deferral: any ceiling above X is unreachable, and any ceiling below it
* would be the trigger. Caught by trying to write a test that exercised
* the ceiling and finding none could. */
uint64_t garbage = used > last ? used - last : 0;
if (garbage >= WO_CKPT_ABS_BYTES) return 1;
return used > last * (uint64_t)ratio;
}
/* databasev2 3: how many records the dump stages before flushing.
*
* NOT unbounded: stage() grows the staging buffer by doubling and never
* shrinks it, so appending a whole store through one buffer would hold the
* entire store in RAM on top of the store itself — the unbounded growth
* databasev2 1 identified as how this engine dies. 256 records is a few tens
* of KiB per flush, which is large enough that the syscall cost is amortised
* and small enough that the buffer never matters. */
#define WO_WAL_COMPACT_FLUSH 256u
/* rename(2)'s atomicity is in-kernel: the new directory ENTRY is not durable
* until the parent directory is synced. Postgres does the same thing for the
* same reason. Best-effort — a filesystem that refuses to sync a directory
* still leaves a correct log, just one whose swap might not survive a power
* cut. */
static void sync_parent_dir(const char *path) {
char dir[4096];
if (parent_dir_of(path, dir, sizeof dir) != 0) return;
int fd = open(dir, O_RDONLY);
if (fd < 0) return;
(void)fsync(fd);
close(fd);
}
/* databasev2 3: write the staged bytes WITHOUT a durability barrier.
*
* Only compaction's dump uses this. Intermediate durability there is worthless:
* the temp file is not authoritative until the rename, and it is fsynced once
* immediately before that. Using wo_wal_commit for the dump instead cost one
* fdatasync per 256 records — measured, that was most of the stop-the-world
* pause (~22 MB/s, where the fixed cost plus ~150 redundant syncs dominated a
* 2 MB dump). */
static int wal_write_nosync(wo_wal *w) {
size_t at = 0;
while (at < w->len) {
ssize_t n = pwrite(w->fd, w->buf + at, w->len - at, (off_t)(w->off + at));
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
at += (size_t)n;
}
w->off += w->len;
w->len = 0;
return 0;
}
static uint64_t mono_us(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
return (uint64_t)ts.tv_sec * 1000000ull + (uint64_t)ts.tv_nsec / 1000ull;
}
/* ============================================================================
* OBLIGATION FOR WHOEVER IMPLEMENTS `resident: keys` (databasev2 2, tasks
* 5c/5d) — READ THIS BEFORE STORING WAL OFFSETS.
*
* Compaction rewrites the log and MOVES EVERY RECORD. Any WAL byte offset
* captured from the old file is meaningless afterwards — not stale-but-
* readable, but pointing at an arbitrary byte of a different file.
*
* `resident: keys` stores exactly such an offset per row and reads rows back
* through it. So the loop below, which knows each record's NEW position as it
* writes it, MUST also rebuild that map. It is the cheap direction and the only
* one that keeps both features usable together; the alternative is forbidding
* compaction whenever such a table is live, which would mean the feature for
* huge tables is incompatible with the feature that stops their log growing.
*
* Nothing fails today because that storage half does not exist yet. It will
* fail later, and it will look like data corruption rather than a design gap.
* ==========================================================================*/
/* databasev2 2 (5d): move one already-durable record from the old log into
* the new one, VERBATIM.
*
* Compaction cannot re-encode a keys-resident row the way it re-encodes a
* resident one. enc_val expects the ENGINE representation a slab row holds
* (db_text: len + bytes), while a row read back out of the log arrives in the
* VM representation (wo_str: len + data). The two are not the same struct, so
* feeding a borrowed row to enc_val reads the length out of the wrong field —
* ASan caught exactly that as a 4294967292-byte memcpy.
*
* Copying the bytes sidesteps the whole question, and is strictly better than
* decode-then-encode anyway: no allocation per row, no arena pressure, and the
* record that lands is bit-identical to the one that was acked. scan_record
* verifies the CRC, so a torn record is refused rather than propagated.
*
* The kind byte is normalised to INSERT: a live row's newest image may have
* been logged as an UPDATE, and the compacted log is supposed to read as one
* INSERT per live row. */
static int copy_record(wo_wal *nw, wo_wal *ow, uint64_t off, uint32_t want_cid,
uint64_t want_id, const char **why) {
uint32_t len = 0;
uint8_t *payload = NULL;
if (scan_record(ow->fd, off, &len, &payload) != 0) {
*why = "no intact record at a row's recorded offset";
return -1;
}
rbuf r = {payload, payload + len, 0};
uint8_t kind = rd_u8(&r);
uint32_t cid = rd_u32(&r);
uint64_t id = rd_u64(&r);
if (r.bad || cid != want_cid || id != want_id ||
(kind != WO_WAL_INSERT && kind != WO_WAL_UPDATE)) {
free(payload);
*why = "a row's recorded offset does not hold that row";
return -1;
}
payload[0] = WO_WAL_INSERT;
wbuf p = {0};
wput(&p, payload, len);
int rc = stage(nw, &p);
free(p.b);
free(payload);
if (rc != 0) *why = "staging a moved record failed";
return rc;
}
/* Task 5 (replay and compaction fold the same way): copy_record's
* counterpart for a row whose chain is NOT empty — a delta cannot be moved
* byte-for-byte the way copy_record moves a base row, because the whole
* point of compaction is to reset every chain to length zero. Fold the row
* (THE fold, same one reads and replay use) and re-encode it as a fresh
* INSERT via enc_val, exactly the representation the fold's own contract
* promises (ENGINE values, not the VM's) — so this, unlike copy_record,
* never touches wo_row_ptr/a slab at all. */
static int stage_flattened_row(wo_wal *nw, wo_wal *ow, wo_db *db, uint64_t off,
uint32_t class_id, uint64_t id, const char **why) {
const wo_classdesc *c = &db->classes[class_id];
uint64_t *vals = c->field_cnt ? calloc(c->field_cnt, sizeof *vals) : NULL;
if (c->field_cnt && !vals) {
*why = "out of memory flattening a delta chain";
return -1;
}
uint32_t got_cid = 0;
uint64_t got_id = 0;
const char *fmsg = "";
if (wo_wal_fold_row_at(ow, db, off, &got_cid, &got_id, vals, NULL, &fmsg) != 0 ||
got_cid != class_id || got_id != id) {
for (uint32_t i = 0; i < c->field_cnt; i++) wo_db_val_free(db, c->kinds[i], vals[i]);
free(vals);
*why = "a live row's delta chain does not fold cleanly";
return -1;
}
wbuf p = {0};
wput_u8(&p, WO_WAL_INSERT);
wput_u32(&p, class_id);
wput_u64(&p, id);
for (uint32_t i = 0; i < c->field_cnt; i++) enc_val(&p, db->classes, c->kinds[i], vals[i]);
int rc = stage(nw, &p);
free(p.b);
for (uint32_t i = 0; i < c->field_cnt; i++) wo_db_val_free(db, c->kinds[i], vals[i]);
free(vals);
if (rc != 0) *why = "staging a flattened row failed";
return rc;
}
int wo_wal_compact(wo_wal *w, wo_db *db) {
/* staged records would be written into a file about to be replaced */
if (!w->path || w->len != 0) return -1;
uint64_t t0 = mono_us();
int repointed = 0; /* has any keys-resident row been moved to the new log? */
const char *why = NULL; /* the reason a fail arm was taken, when it is known */
char tmp[4096];
if ((size_t)snprintf(tmp, sizeof tmp, "%s%s", w->path, WO_WAL_TMP_SUFFIX) >= sizeof tmp)
return -1;
(void)unlink(tmp); /* a stale one would otherwise be appended to */
wo_wal nw;
/* THE REPLACEMENT MUST BE PREALLOCATED LIKE THE ORIGINAL. The WAL is
* preallocated so appends never extend the file, which is precisely what
* makes fdatasync sufficient as the ack barrier — no file-size metadata
* has to reach disk for an acked record to be readable. Opening the
* replacement with prealloc 0 silently removed that property, and the
* crash battery caught it: records acked shortly before a kill went
* missing, with the log otherwise intact and self-consistent. */
if (wo_wal_open(&nw, tmp, w->prealloc) != 0) return -1;
/* databasev2 12: the replacement log's first record is the schema, so a
* compacted log is always self-describing — including the first
* compaction of a legacy log, which is how existing WO_DATA dirs become
* diffable without any migration step of their own. */
if (w->schema) {
wbuf sp = {0};
wput(&sp, w->schema, w->schema_len);
int src = stage(&nw, &sp);
free(sp.b);
if (src != 0) goto fail;
}
/* one INSERT per live row, in the existing grammar, through the existing
* append path — so replay needs no second decoder and ids are preserved
* exactly (wo_wal_append_insert takes the id and reads the row) */
uint32_t pending = 0;
/* databasev2 2 (5d): the walk is wo_row_next_id, not the bitmap. A
* keys-resident row has NO bitmap bit — its slot was returned to the free
* list when the payload was dropped — so a bitmap walk would omit every
* such row from the new log and call it compaction. That is silent data
* loss, and it is the failure the obligation note above was written for. */
for (uint32_t cid = 0; cid < db->class_cnt; cid++) {
db_table *t = &db->tables[cid];
if (!t->row_size) continue; /* tables are created lazily */
int keys = wo_table_is_keys_resident(db, cid);
size_t cur = 0;
uint64_t id;
while (wo_row_next_id(db, cid, &cur, &id)) {
if (!keys) {
if (wo_wal_append_insert(&nw, db, cid, id) != 0) goto fail;
} else {
/* read from the OLD log (still open, still the live file at
* this point), write into the new one, and re-point the map
* to where it landed. The offset is captured BEFORE staging:
* off is what has reached the file, len what is staged behind
* it, so their sum is the position of the next record. */
uint64_t o1 = wo_row_offset1(db, cid, id);
if (!o1) {
why = "a live keys-resident row has no recorded offset";
goto fail;
}
uint64_t at = nw.off + (uint64_t)nw.len;
/* Task 5: a chain flattens to ONE full row on every
* checkpoint — the bound the whole design relies on
* (without it, chains grow without limit). Peek the kind
* byte at the row's current record (position fixed by the
* frame header, valid whatever the record turns out to be)
* to choose: an empty chain keeps the existing byte-for-byte
* copy, faster and unchanged; a delta chain folds instead of
* being copied — copying it would just move the chain, not
* flatten it. */
uint8_t kind_byte = 0;
if (pread(w->fd, &kind_byte, 1, (off_t)(o1 - 1 + 8)) != 1) {
why = "cannot read a row's recorded offset";
goto fail;
}
if (kind_byte == WO_WAL_DELTA) {
if (stage_flattened_row(&nw, w, db, o1 - 1, cid, id, &why) != 0) goto fail;
} else if (copy_record(&nw, w, o1 - 1, cid, id, &why) != 0) {
goto fail;
}
/* value-only update: cannot rehash, so `cur` stays valid */
if (wo_row_set_offset(db, cid, id, at) != 0) {
why = "row vanished from the id map mid-compaction";
goto fail;
}
repointed = 1;
}
if (++pending >= WO_WAL_COMPACT_FLUSH) {
if (wal_write_nosync(&nw) != 0) goto fail;
pending = 0;
}
}
}
if (wal_write_nosync(&nw) != 0) goto fail; /* the tail batch */
/* THE dump's one and only barrier: everything above is just bytes in the
* page cache until this, and nothing reads the temp before the rename. */
if (fsync(nw.fd) != 0) goto fail;
uint64_t new_bytes = nw.off;
wo_wal_close(&nw);
/* THE SWITCH. Every crash point either side of this is safe. */
if (rename(tmp, w->path) != 0) {
(void)unlink(tmp);
return -1;
}
sync_parent_dir(w->path);
/* the old descriptor now refers to an unlinked inode */
if (w->fd >= 0) close(w->fd);
w->fd = open(w->path, O_RDWR);
if (w->fd < 0) return -1; /* the log is correct on disk; this process cannot go on */
w->off = new_bytes;
w->len = 0;
w->compacted_bytes = new_bytes;
{ /* the stop-the-world pause: nothing was served while this ran */
uint64_t el = mono_us() - t0;
w->stat_compactions++;
w->stat_compact_us_total += el;
if (el > w->stat_compact_us_max) w->stat_compact_us_max = el;
}
return 0;
fail:
wo_wal_close(&nw);
(void)unlink(tmp);
if (repointed) {
/* databasev2 2 (5d): rows already re-pointed name offsets inside the
* temp file just unlinked, so the id map now describes a file that no
* longer exists — reads would return another row's bytes or nothing.
* The log ON DISK is still the intact original, so replay rebuilds the
* map correctly; carrying on in this process cannot. Same doctrine as
* a failed commit barrier: stop rather than serve wrong rows. */
fprintf(stderr,
"writeonce: DURABILITY FAILURE — compaction of %s failed after "
"moving rows: %s\n"
" No data was lost: the original log is intact on disk.\n"
" The process is stopping: replay rebuilds the row offsets.\n",
w->path, why ? why : "write or sync error");
exit(WO_EXIT_DURABILITY);
}
return -1; /* the live log is untouched and still usable */
}
/* Task 5 (replay and compaction fold the same way): the DELTA replay arm.
*
* A delta cannot be applied like an insert/update — its body is one field,
* not the row — so it borrows WO_WAL_UPDATE's remove-then-recreate SHAPE
* instead: fold the row's PRE-delta state (THE fold, the same one reads and
* compaction use) via [back_off], overlay this delta's field onto it, then
* remove-and-recreate through the ordinary choke points. Recreating, rather
* than patching an index in place, is what re-indexes a changed INDEXED
* column for free — wo_row_remove takes the OLD value's entries out,
* wo_row_raw_commit puts the NEW value's entries in, exactly as a live
* update would have. The caller (wo_wal_replay_ex) still owns dropping the
* recreated row's payload back to the log, the same way it does for
* INSERT/UPDATE. */
/* ---- databasev2 12: the migration transcode ---------------------------- */
/* record-start offset -> record-start offset in the new log. Open-addressed;
* keys stored +1 so offset 0 (a legacy log's first record) is representable. */
typedef struct migmap {
uint64_t *k, *v;
size_t cap, len;
} migmap;
static int migmap_put(migmap *m, uint64_t key, uint64_t val) {
if (m->len * 2 >= m->cap) {
size_t nc = m->cap ? m->cap * 2 : 1024;
uint64_t *nk = calloc(nc, sizeof *nk);
uint64_t *nv = malloc(nc * sizeof *nv);
if (!nk || !nv) {
free(nk);
free(nv);
return -1;
}
for (size_t i = 0; i < m->cap; i++) {
if (!m->k[i]) continue;
size_t j = (size_t)(m->k[i] * 0x9E3779B97F4A7C15ull) & (nc - 1);
while (nk[j]) j = (j + 1) & (nc - 1);
nk[j] = m->k[i];
nv[j] = m->v[i];
}
free(m->k);
free(m->v);
m->k = nk;
m->v = nv;
m->cap = nc;
}
size_t j = (size_t)((key + 1) * 0x9E3779B97F4A7C15ull) & (m->cap - 1);
while (m->k[j] && m->k[j] != key + 1) j = (j + 1) & (m->cap - 1);
if (!m->k[j]) m->len++;
m->k[j] = key + 1;
m->v[j] = val;
return 0;
}
static int migmap_get(const migmap *m, uint64_t key, uint64_t *val) {
if (!m->cap) return -1;
size_t j = (size_t)((key + 1) * 0x9E3779B97F4A7C15ull) & (m->cap - 1);
while (m->k[j]) {
if (m->k[j] == key + 1) {
*val = m->v[j];
return 0;
}
j = (j + 1) & (m->cap - 1);
}
return -1;
}
/* remap the class ids EMBEDDED in a decoded value tree (owned values carry a
* cid on the wire). The tree's classes are all UNCHANGED — the embed closure
* poisoned anything else before a record could reach here — so only the
* NUMBERS move; shapes and kinds are the same on both sides. */
static int mig_fixup_cids(uint64_t v, uint8_t kind, const wo_schema *oldsc,
const wo_mig_plan *plan) {
if (!v) return 0;
switch (kind) {
case WO_K_OWNED: {
db_rec *rec = (db_rec *)(uintptr_t)v;
uint32_t oc = rec->class_id;
if (oc >= plan->old_class_cnt || plan->classes[oc].new_cid == WO_SCHEMA_NONE)
return -1;
const wo_schema_class *k = &oldsc->classes[oc];
for (uint32_t i = 0; i < k->field_cnt; i++)
if (mig_fixup_cids(rec->slots[i], k->fields[i].kind, oldsc, plan) != 0)
return -1;
rec->class_id = plan->classes[oc].new_cid;
return 0;
}
case WO_K_MULTI: {
db_multi *m = (db_multi *)(uintptr_t)v;
if (m->elem_kind != WO_K_OWNED && m->elem_kind != WO_K_MULTI &&
m->elem_kind != WO_K_MAP)
return 0;
for (uint32_t i = 0; i < m->len; i++)
if (mig_fixup_cids(m->items[i], m->elem_kind, oldsc, plan) != 0) return -1;
return 0;
}
case WO_K_MAP: {
db_map *m = (db_map *)(uintptr_t)v;
for (uint32_t i = 0; i < m->len; i++) {
if (mig_fixup_cids(m->kv[2 * i], m->key_kind, oldsc, plan) != 0) return -1;
if (mig_fixup_cids(m->kv[2 * i + 1], m->val_kind, oldsc, plan) != 0)
return -1;
}
return 0;
}
default: return 0;
}
}
int wo_wal_migrate(const char *path, wo_db *db, const wo_schema *oldsc,
const wo_mig_plan *plan, const wo_schema *newsc,
uint64_t prealloc, char **err_out) {
if (err_out) *err_out = NULL;
int rc = -1;
int ofd = open(path, O_RDONLY);
if (ofd < 0) return -1;
/* decode with the OLD shapes: a classdesc shim per stored class, kinds
* lifted from the schema record. dec_val touches nothing in the db but
* classes/class_cnt, so a stack shim is the whole "old database". */
wo_db olddb;
memset(&olddb, 0, sizeof olddb);
wo_classdesc *oldcls = calloc(oldsc->class_cnt ? oldsc->class_cnt : 1,
sizeof *oldcls);
uint8_t **oldkinds = calloc(oldsc->class_cnt ? oldsc->class_cnt : 1,
sizeof *oldkinds);
if (!oldcls || !oldkinds) goto out_nolog;
for (uint32_t c = 0; c < oldsc->class_cnt; c++) {
const wo_schema_class *k = &oldsc->classes[c];
oldkinds[c] = malloc(k->field_cnt ? k->field_cnt : 1);
if (!oldkinds[c]) goto out_nolog;
for (uint32_t f = 0; f < k->field_cnt; f++) oldkinds[c][f] = k->fields[f].kind;
oldcls[c].field_cnt = k->field_cnt;
oldcls[c].kinds = oldkinds[c];
oldcls[c].flags = k->flags;
}
olddb.classes = oldcls;
olddb.class_cnt = oldsc->class_cnt;
char tmp[4096];
if ((size_t)snprintf(tmp, sizeof tmp, "%s%s", path, WO_WAL_TMP_SUFFIX) >= sizeof tmp)
goto out_nolog;
(void)unlink(tmp);
wo_wal nw;
if (wo_wal_open(&nw, tmp, prealloc) != 0) goto out_nolog;
migmap map = {0};
uint64_t *vals = NULL;
/* the new log's first record: the compiled shape */
{
uint8_t *sp;
uint32_t slen;
if (wo_schema_encode(newsc, &sp, &slen) != 0) goto out;
wbuf b = {0};
wput(&b, sp, slen);
free(sp);
int src = stage(&nw, &b);
free(b.b);
if (src != 0) goto out;
}
uint32_t pending = 0;
for (uint64_t off = 0;;) {
uint32_t len;
uint8_t *payload;
if (scan_record(ofd, off, &len, &payload) != 0) break; /* intact prefix ends */
rbuf r = {payload, payload + len, 0};
uint8_t kind = rd_u8(&r);
uint64_t new_start = nw.off + (uint64_t)nw.len;
/* declared before any `goto corrupt` can fire, so the handler never
* frees an object whose initializer was jumped over */
wbuf b = {0};
const wo_schema_class *ok = NULL;
const wo_classdesc *nk = NULL;
if (kind == WO_WAL_SCHEMA) {
free(payload);
off += 8u + len + 4u;
continue; /* replaced by the head record above */
}
uint32_t cid = rd_u32(&r);
uint64_t id = rd_u64(&r);
if (r.bad || cid >= plan->old_class_cnt) goto corrupt;
const wo_mig_class *mc = &plan->classes[cid];
if (mc->new_cid == WO_SCHEMA_NONE) {
/* the poison bites: a record of this class actually exists */
if (err_out && mc->poison) *err_out = strdup(mc->poison);
rc = -2;
free(payload);
goto out;
}
ok = &oldsc->classes[cid];
nk = &db->classes[mc->new_cid];
if (kind == WO_WAL_INSERT || kind == WO_WAL_UPDATE) {
vals = calloc(ok->field_cnt ? ok->field_cnt : 1, sizeof *vals);
if (!vals) goto corrupt;
for (uint32_t f = 0; f < ok->field_cnt; f++)
if (dec_val(&r, &olddb, ok->fields[f].kind, &vals[f]) != 0) goto corrupt;
if ((size_t)(r.end - r.p) != 0) goto corrupt;
for (uint32_t f = 0; f < ok->field_cnt; f++)
if (mig_fixup_cids(vals[f], ok->fields[f].kind, oldsc, plan) != 0)
goto corrupt;
wput_u8(&b, kind);
wput_u32(&b, mc->new_cid);
wput_u64(&b, id);
for (uint32_t nf = 0; nf < nk->field_cnt; nf++) {
uint64_t v = 0; /* an added field: the kind's zero value */
for (uint32_t f = 0; f < ok->field_cnt; f++)
if (mc->fmap[f] == (int32_t)nf) {
v = vals[f];
break;
}
enc_val(&b, db->classes, nk->kinds[nf], v);
}
/* post-fixup the tree speaks NEW cids, so the compiled db frees it */
for (uint32_t f = 0; f < ok->field_cnt; f++)
wo_db_val_free(db, ok->fields[f].kind, vals[f]);
free(vals);
vals = NULL;
} else if (kind == WO_WAL_DELTA) {
uint32_t fidx = rd_u32(&r);
uint64_t back = rd_u64(&r);
if (r.bad || fidx >= ok->field_cnt) goto corrupt;
uint64_t nback;
if (migmap_get(&map, back, &nback) != 0) goto corrupt;
if (mc->fmap[fidx] < 0) {
/* a delta on a deleted field: splice the chain around it —
* anything pointing at THIS record re-points to where this
* record itself pointed */
free(payload);
if (migmap_put(&map, off, nback) != 0) goto out;
off += 8u + len + 4u;
continue;
}
uint64_t v = 0;
if (dec_val(&r, &olddb, ok->fields[fidx].kind, &v) != 0) goto corrupt;
if ((size_t)(r.end - r.p) != 0 ||
mig_fixup_cids(v, ok->fields[fidx].kind, oldsc, plan) != 0) {
wo_db_val_free(db, ok->fields[fidx].kind, v);
goto corrupt;
}
wput_u8(&b, WO_WAL_DELTA);
wput_u32(&b, mc->new_cid);
wput_u64(&b, id);
wput_u32(&b, (uint32_t)mc->fmap[fidx]);
wput_u64(&b, nback);
enc_val(&b, db->classes, nk->kinds[mc->fmap[fidx]], v);
wo_db_val_free(db, ok->fields[fidx].kind, v);
} else if (kind == WO_WAL_REMOVE) {
wput_u8(&b, WO_WAL_REMOVE);
wput_u32(&b, mc->new_cid);
wput_u64(&b, id);
} else {
goto corrupt;
}
free(payload);
payload = NULL;
{
int src = stage(&nw, &b);
free(b.b);
if (src != 0) goto out;
}
if (migmap_put(&map, off, new_start) != 0) goto out;
if (++pending >= WO_WAL_COMPACT_FLUSH) {
if (wal_write_nosync(&nw) != 0) goto out;
pending = 0;
}
off += 8u + len + 4u;
continue;
corrupt:
free(payload);
free(b.b);
if (vals && ok) {
for (uint32_t f = 0; f < ok->field_cnt; f++)
wo_db_val_free(db, ok->fields[f].kind, vals[f]);
free(vals);
vals = NULL;
}
rc = -1;
goto out;
}
if (wal_write_nosync(&nw) != 0) goto out;
if (fsync(nw.fd) != 0) goto out;
wo_wal_close(&nw);
if (rename(tmp, path) != 0) {
(void)unlink(tmp);
goto out_closed;
}
sync_parent_dir(path);
rc = 0;
goto out_closed;
out:
wo_wal_close(&nw);
(void)unlink(tmp);
out_closed:
free(map.k);
free(map.v);
out_nolog:
if (oldkinds)
for (uint32_t c = 0; c < oldsc->class_cnt; c++) free(oldkinds[c]);
free(oldkinds);
free(oldcls);
close(ofd);
return rc;
}
static int apply_delta(wo_db *db, uint32_t cid, uint64_t id, rbuf *r) {
uint32_t field_idx = rd_u32(r);
uint64_t back_off = rd_u64(r);
const wo_classdesc *c = &db->classes[cid];
if (r->bad || field_idx >= c->field_cnt) return -1;
/* IMPORTANT 2 (review finding): a DELTA can only be folded through
* db->rt->wal (wo_wal_replay_ex's lent view, at replay, or a live one
* otherwise) — refuse cleanly instead of dereferencing a NULL rt. */
if (!db->rt) return -1;
uint64_t nv;
if (dec_val(r, db, c->kinds[field_idx], &nv) != 0 || (size_t)(r->end - r->p) != 0)
return -1;
uint32_t field_cnt = c->field_cnt;
uint64_t *vals = field_cnt ? calloc(field_cnt, sizeof *vals) : NULL;
if (field_cnt && !vals) {
wo_db_val_free(db, c->kinds[field_idx], nv);
return -1;
}
uint32_t got_cid = 0;
uint64_t got_id = 0;
const char *fmsg = "";
if (wo_wal_fold_row_at((wo_wal *)db->rt->wal, db, back_off, &got_cid, &got_id, vals,
NULL, &fmsg) != 0 ||
got_cid != cid || got_id != id) {
for (uint32_t i = 0; i < field_cnt; i++) wo_db_val_free(db, c->kinds[i], vals[i]);
free(vals);
wo_db_val_free(db, c->kinds[field_idx], nv);
return -1;
}
wo_db_val_free(db, c->kinds[field_idx], vals[field_idx]);
vals[field_idx] = nv;
/* the row must exist (its base record precedes its deltas in a correct
log); anything else is corruption */
if (wo_row_remove(db, cid, id) != 0) {
for (uint32_t i = 0; i < field_cnt; i++) wo_db_val_free(db, c->kinds[i], vals[i]);
free(vals);
return -1;
}
db_row *row = wo_row_create_raw(db, cid, id);
if (!row) {
for (uint32_t i = 0; i < field_cnt; i++) wo_db_val_free(db, c->kinds[i], vals[i]);
free(vals);
return -1;
}
memcpy(row->slots, vals, (size_t)field_cnt * sizeof *vals);
free(vals);
/* a unique violation here is corruption, same reasoning as INSERT/UPDATE
below: the live update that produced this delta would have refused it */
if (wo_row_raw_commit(db, cid, row) != 0) {
wo_row_remove(db, cid, id);
return -1;
}
return 0;
}
static int apply_record(wo_db *db, const uint8_t *payload, uint32_t len) {
rbuf r = {payload, payload + len, 0};
uint8_t kind = rd_u8(&r);
/* databasev2 12: a schema record is descriptive, not a row — and it has
* no cid/id fields, so it must be skipped BEFORE those are read (its
* class count would be misread as a cid and bounds-refused). */
if (kind == WO_WAL_SCHEMA) return 0;
uint32_t cid = rd_u32(&r);
uint64_t id = rd_u64(&r);
if (r.bad || cid >= db->class_cnt) return -1;
/* databasev2 2: this log holds records for a class the CURRENT source
* declares `durable: false`. Not corruption — a real migration case (the
* table used to be durable). Refuse rather than convert, and refuse
* rather than silently resurrect rows into a table declared not to have
* any. -2 so the caller can say which of the two it is. */
if (db->classes[cid].flags & WO_CLASSF_VOLATILE) return -2;
if (kind == WO_WAL_REMOVE) return wo_row_remove(db, cid, id);
if (kind == WO_WAL_DELTA) return apply_delta(db, cid, id, &r);
if (kind != WO_WAL_INSERT && kind != WO_WAL_UPDATE) return -1;
if (kind == WO_WAL_UPDATE) {
/* replace: the row must exist (its insert precedes its update in a
correct log); anything else is corruption */
if (wo_row_remove(db, cid, id) != 0) return -1;
}
db_row *row = wo_row_create_raw(db, cid, id);
if (!row) return -1;
const wo_classdesc *c = &db->classes[cid];
for (uint32_t i = 0; i < c->field_cnt; i++) {
if (dec_val(&r, db, c->kinds[i], &row->slots[i]) != 0) {
/* a record that CRC-passed but does not decode is corruption,
* not a tear: fail loudly (the row's built slots are freed by
* wo_row_remove, which also unregisters the id) */
wo_row_remove(db, cid, id);
return -1;
}
}
if ((size_t)(r.end - r.p) != 0) { /* trailing bytes = corrupt */
wo_row_remove(db, cid, id);
return -1;
}
/* slots are real now: re-index (Task 4). A unique violation during
* replay is corruption — the live insert would have refused it. */
if (wo_row_raw_commit(db, cid, row) != 0) {
wo_row_remove(db, cid, id);
return -1;
}
return 0;
}
/* databasev2 2: materialise a row straight from a log offset.
*
* The offset twin of wo_row_read (table.c): same out-gate contract — every
* value handed back is a FRESH VM allocation, never a pointer into anything
* the engine owns — but resolved from a file position instead of the id hash.
* This is what a `resident: keys` table's read path will call once 5c gives
* it an id->offset map; nothing calls it yet, deliberately.
*
* Two decode stages, because the record and the VM speak different dialects:
* dec_val yields ENGINE-owned slots (db_text and friends, exactly what a slab
* row holds), then wo_val_decode_vm copies each into the VM. The engine slots
* are scratch and are always freed before returning, on every path. */
int wo_wal_read_row_at(wo_wal *w, wo_db *db, wo_rt *rt, uint64_t off,
uint32_t *class_out, uint64_t *id_out, uint64_t *out_vals,
const char **msg) {
uint32_t len = 0;
uint8_t *payload = NULL;
if (scan_record(w->fd, off, &len, &payload) != 0) {
*msg = "no intact record at that offset";
return -1;
}
rbuf r = {payload, payload + len, 0};
uint8_t kind = rd_u8(&r);
uint32_t cid = rd_u32(&r);
uint64_t id = rd_u64(&r);
if (r.bad || cid >= db->class_cnt) {
free(payload);
*msg = "record header is malformed";
return -1;
}
/* A REMOVE tombstone carries no field payload. Handing one back as a row
* would be the worst failure available here — the caller would read a
* deleted row as live — so it is refused explicitly, not decoded. */
if (kind != WO_WAL_INSERT && kind != WO_WAL_UPDATE) {
free(payload);
*msg = "record at that offset is a tombstone, not a row";
return -1;
}
const wo_classdesc *c = &db->classes[cid];
uint64_t *slots = c->field_cnt ? calloc(c->field_cnt, sizeof *slots) : NULL;
if (c->field_cnt && !slots) {
free(payload);
*msg = "out of memory reading a row";
return -2;
}
int rc = 0;
uint32_t done = 0;
for (; done < c->field_cnt; done++) {
if (dec_val(&r, db, c->kinds[done], &slots[done]) != 0) {
*msg = "record at that offset does not decode";
rc = -1;
break;
}
}
if (rc == 0 && (size_t)(r.end - r.p) != 0) { /* trailing bytes = corrupt */
*msg = "record at that offset has trailing bytes";
rc = -1;
}
if (rc == 0) {
int ok = 1;
for (uint32_t i = 0; i < c->field_cnt; i++) {
out_vals[i] = wo_val_decode_vm(db, rt, c->kinds[i], slots[i], &ok, msg);
if (!ok) {
rc = -2;
break;
}
}
}
/* engine slots are scratch: free every one that was built, on EVERY path */
for (uint32_t i = 0; i < done; i++) wo_db_val_free(db, c->kinds[i], slots[i]);
free(slots);
free(payload);
if (rc == 0) {
if (class_out) *class_out = cid;
if (id_out) *id_out = id;
}
return rc;
}
/* keys-resident delta updates, Task 2: THE fold. See wal.h. Reads, replay,
* and compaction all call this one function — never a second copy. */
int wo_wal_fold_row_at(wo_wal *w, wo_db *db, uint64_t off, uint32_t *class_out,
uint64_t *id_out, uint64_t *out_vals, uint32_t *hops_out,
const char **msg) {
uint32_t hops = 0; /* databasev2 11: DELTA records crossed */
if (hops_out) *hops_out = 0;
uint32_t cid = 0;
uint64_t id = 0;
uint32_t field_cnt = 0;
uint8_t *resolved = NULL; /* field idx -> a delta already claimed it */
uint64_t *resolved_val = NULL; /* field idx -> its remembered engine value */
uint64_t cur = off;
int rc = 0;
int first = 1;
for (;;) {
uint32_t len;
uint8_t *payload;
/* Task 4 follow-up: a hop into the currently-staged (not yet
durable) region reads from `w->buf`, not the file — see
scan_record_staged. Every other hop (the durable majority of
any real chain) is the original file read, unchanged. */
int src_rc = (cur >= w->off && cur < w->off + w->len)
? scan_record_staged(w, cur, &len, &payload)
: scan_record(w->fd, cur, &len, &payload);
if (src_rc != 0) {
*msg = "no intact record at that offset";
rc = -1;
break;
}
rbuf r = {payload, payload + len, 0};
uint8_t kind = rd_u8(&r);
uint32_t rec_cid = rd_u32(&r);
uint64_t rec_id = rd_u64(&r);
if (r.bad || rec_cid >= db->class_cnt) {
free(payload);
*msg = "record header is malformed";
rc = -1;
break;
}
if (first) {
first = 0;
cid = rec_cid;
id = rec_id;
field_cnt = db->classes[cid].field_cnt;
resolved = field_cnt ? calloc(field_cnt, 1) : NULL;
resolved_val = field_cnt ? calloc(field_cnt, sizeof *resolved_val) : NULL;
if (field_cnt && (!resolved || !resolved_val)) {
free(payload);
*msg = "out of memory folding a row";
rc = -2;
break;
}
} else if (rec_cid != cid || rec_id != id) {
free(payload);
*msg = "delta chain does not agree on row identity";
rc = -1;
break;
}
if (kind == WO_WAL_DELTA) {
uint32_t field_idx = rd_u32(&r);
uint64_t back_off = rd_u64(&r);
if (r.bad || field_idx >= field_cnt) {
free(payload);
*msg = "delta record is malformed";
rc = -1;
break;
}
/* THE cycle/forgery guard: a back-pointer names the row's
* PREVIOUS record, which by construction is earlier in the
* (append-only) log than the delta naming it. Anything else —
* a self-pointer, a forward pointer, a pointer that only forms
* a cycle several hops later — is corruption or forgery, and
* this is what actually rules all of those out: not a bound on
* how many records could exist, which a forward pointer to a
* genuine record satisfies trivially and a bound would then let
* straight through. */
if (back_off >= cur) {
free(payload);
*msg = "delta back-pointer does not point earlier in the log";
rc = -1;
break;
}
const wo_classdesc *c = &db->classes[cid];
uint64_t v;
if (dec_val(&r, db, c->kinds[field_idx], &v) != 0 ||
(size_t)(r.end - r.p) != 0) {
free(payload);
*msg = "delta record does not decode";
rc = -1;
break;
}
if (!resolved[field_idx]) {
resolved[field_idx] = 1;
resolved_val[field_idx] = v;
} else {
/* shadowed by a newer delta already seen: still validated
above, but its value loses, exactly like the base row's */
wo_db_val_free(db, c->kinds[field_idx], v);
}
free(payload);
hops++;
if (hops_out) *hops_out = hops;
cur = back_off;
continue;
}
if (kind != WO_WAL_INSERT && kind != WO_WAL_UPDATE) {
free(payload);
*msg = "record in a delta chain is a tombstone, not a row";
rc = -1;
break;
}
/* base row: decode every field positionally (a delta-shadowed
field's bytes are still there — dec_val must still walk past
them to keep the fields after it aligned), then overlay whatever
the walk resolved. */
const wo_classdesc *c = &db->classes[cid];
uint64_t *slots = field_cnt ? calloc(field_cnt, sizeof *slots) : NULL;
if (field_cnt && !slots) {
free(payload);
*msg = "out of memory folding a row";
rc = -2;
break;
}
uint32_t done = 0;
for (; done < field_cnt; done++) {
if (dec_val(&r, db, c->kinds[done], &slots[done]) != 0) {
*msg = "record at that offset does not decode";
rc = -1;
break;
}
}
if (rc == 0 && done == field_cnt && (size_t)(r.end - r.p) != 0) {
*msg = "record at that offset has trailing bytes";
rc = -1;
}
if (rc == 0 && done == field_cnt) {
for (uint32_t i = 0; i < field_cnt; i++) {
if (resolved[i]) {
wo_db_val_free(db, c->kinds[i], slots[i]);
slots[i] = resolved_val[i];
}
}
memcpy(out_vals, slots, (size_t)field_cnt * sizeof *out_vals);
} else {
for (uint32_t i = 0; i < done; i++) wo_db_val_free(db, c->kinds[i], slots[i]);
}
free(slots);
free(payload);
break;
}
if (rc != 0 && resolved && resolved_val) {
/* the walk resolved some fields but never reached a base row to
install them into: free what it was holding */
const wo_classdesc *c = &db->classes[cid];
for (uint32_t i = 0; i < field_cnt; i++)
if (resolved[i]) wo_db_val_free(db, c->kinds[i], resolved_val[i]);
}
free(resolved);
free(resolved_val);
if (rc == 0) {
if (class_out) *class_out = cid;
if (id_out) *id_out = id;
}
return rc;
}
int64_t wo_wal_replay_ex(const char *path, wo_db *db, uint32_t *volatile_cid) {
int fd = open(path, O_RDONLY);
if (fd < 0) return errno == ENOENT ? 0 : -1; /* no WAL yet = fresh boot */
uint64_t off = 0;
int64_t applied = 0;
/* databasev2 2: replay must be able to READ rows back, not only write them.
* A REMOVE (or an UPDATE or a DELTA, both of which replay as
* remove-then-recreate — see apply_delta, Task 5) on a keys-resident
* table reaches wo_row_remove, whose keys arm borrows the row out of the
* log to find its index entries — and a borrow reads through
* db->rt->wal. At boot that pointer is not wired yet: main.c replays first
* and assigns rt.wal afterwards, so the borrow found no log, the remove
* failed, and replay reported a perfectly good tombstone as CORRUPTION.
*
* Lend the runtime a read-only view over the fd already open here, for the
* duration of the replay only, and restore whatever was there. Nothing in
* this window appends, so a view carrying just the descriptor is enough.
*
* IMPORTANT 2 (review finding): this used to lend the view only when
* db->rt->wal was NOT already set — leaving a rt whose wal WAS already
* live to fold deltas against that LIVE wal's (possibly non-empty)
* staging buffer instead, which the design never allows. Install the
* throwaway view unconditionally whenever there is an rt to hold it;
* apply_delta refuses outright when there is no rt at all to install
* one into. */
wo_wal view;
memset(&view, 0, sizeof view);
view.fd = fd;
const void *saved_wal = NULL;
int lent = 0;
if (db->rt) {
saved_wal = db->rt->wal;
db->rt->wal = &view;
lent = 1;
}
#define REPLAY_RETURN(v) \
do { \
if (lent) db->rt->wal = (void *)saved_wal; \
return (v); \
} while (0)
for (;;) {
uint32_t len;
uint8_t *payload;
if (scan_record(fd, off, &len, &payload) != 0) break; /* intact prefix ends */
/* peek the class id before applying, so a -2 can name it */
uint32_t rec_cid = len >= 5u ? (uint32_t)payload[1] | ((uint32_t)payload[2] << 8)
| ((uint32_t)payload[3] << 16)
| ((uint32_t)payload[4] << 24)
: 0u;
/* databasev2 2 (5c): a keys-resident row must end boot pointing at the
* LOG, not at a slab. The record is applied normally (so indexes and
* uniqueness are built exactly as for any other table) and then its
* payload is dropped, leaving the id map holding THIS record's offset.
* For an update the later record wins, because each apply overwrites
* the map in order — which is the same rule replay already follows. */
uint8_t rec_kind = len >= 1u ? payload[0] : 0u;
uint64_t rec_id = 0;
if (len >= 13u)
for (int b = 0; b < 8; b++) rec_id |= (uint64_t)payload[5 + b] << (8 * b);
int rc = apply_record(db, payload, len);
free(payload);
if (rc != 0) {
close(fd);
if (rc == -2 && volatile_cid) *volatile_cid = rec_cid;
REPLAY_RETURN(rc == -2 ? -2 : -1);
}
if ((rec_kind == WO_WAL_INSERT || rec_kind == WO_WAL_UPDATE ||
rec_kind == WO_WAL_DELTA) &&
rec_id && wo_table_is_keys_resident(db, rec_cid))
(void)wo_row_drop_payload(db, rec_cid, rec_id, off);
off += 8u + len + 4u;
if (rec_kind != WO_WAL_SCHEMA) applied++;
}
close(fd);
REPLAY_RETURN(applied);
#undef REPLAY_RETURN
}
int64_t wo_wal_replay(const char *path, wo_db *db) {
return wo_wal_replay_ex(path, db, NULL);
}
int64_t wo_wal_check(const char *path, uint64_t *intact_bytes) {
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
uint64_t off = 0;
int64_t records = 0;
for (;;) {
uint32_t len;
if (scan_record(fd, off, &len, NULL) != 0) break;
off += 8u + len + 4u;
records++;
}
if (intact_bytes) *intact_bytes = off;
close(fd);
return records;
}