From 7e3bf31d362b5ea352f104c36f5e6b101175b7c7 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sat, 15 Aug 2026 13:01:05 +0200 Subject: [PATCH] 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) --- compiler/test/runner.ml | 4 +- database/src/db.c | 34 ++++++++ database/src/table.c | 87 +++++++++++++++++++ database/src/table.h | 8 ++ database/src/wal.c | 21 ++++- database/src/wal.h | 4 + .../plans/2026-08-01-db-engine-binding.md | 16 +++- runtime/src/builtin.c | 2 +- runtime/src/loader.c | 2 + runtime/src/wob.h | 9 +- runtime/test/test_table.c | 61 +++++++++++++ runtime/test/test_wal.c | 37 +++++++- 12 files changed, 278 insertions(+), 7 deletions(-) diff --git a/compiler/test/runner.ml b/compiler/test/runner.ml index ba3cfd0..f120542 100644 --- a/compiler/test/runner.ml +++ b/compiler/test/runner.ml @@ -2604,7 +2604,7 @@ let validate_image (img : string) : string list = golden lowering suite actually emits; 61 = DB_INSERT (arity 1: the class-id slot — field slots are runtime-validated, same as the C loader) *) - if c > 12 && c <> 61 then + if c > 12 && (c < 61 || c > 63) then fail (Printf.sprintf "method %d pc %d: builtin out of range" i pc) else if c = 4 then begin if b > 5 then fail (Printf.sprintf "method %d pc %d: bad element kind" i pc) @@ -2621,6 +2621,8 @@ let validate_image (img : string) : string list = | 5 | 6 | 11 | 12 -> 2 | 10 -> 3 | 61 -> 1 + | 62 -> 4 + | 63 -> 2 | _ -> 0 in if arity > 0 then begin diff --git a/database/src/db.c b/database/src/db.c index 4347fbb..790a21b 100644 --- a/database/src/db.c +++ b/database/src/db.c @@ -34,6 +34,40 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { R[A] = id; return 0; } + case WO_B_DB_UPDATE_FIELD: { + uint32_t cid = (uint32_t)R[B]; + uint64_t id = R[B + 1]; + uint32_t field = (uint32_t)R[B + 2]; + int ek = 0; + if (wo_row_update_field(db, cid, id, field, R[B + 3], msg, &ek) != 0) + return ek == DB_ERR_UNIQUE ? WO_T_UNIQUE : ek == DB_ERR_OOM ? WO_T_OOM : WO_T_DB; + wo_wal *w = (wo_wal *)vm->rt.wal; + if (w) { + if (wo_wal_append_update(w, db, cid, id) != 0 || wo_wal_commit(w) != 0) { + *msg = "wal commit failed"; /* RAM ahead of disk: trap, do not ack */ + return WO_T_IO; + } + } + R[A] = 0; + return 0; + } + case WO_B_DB_DELETE: { + uint32_t cid = (uint32_t)R[B]; + uint64_t id = R[B + 1]; + if (wo_row_remove(db, cid, id) != 0) { + *msg = "no such row"; + return WO_T_DB; + } + wo_wal *w = (wo_wal *)vm->rt.wal; + if (w) { + if (wo_wal_append_remove(w, cid, id) != 0 || wo_wal_commit(w) != 0) { + *msg = "wal commit failed"; + return WO_T_IO; + } + } + R[A] = 0; + return 0; + } default: *msg = "unknown db builtin"; return WO_T_DB; diff --git a/database/src/table.c b/database/src/table.c index 5da75c0..feaf104 100644 --- a/database/src/table.c +++ b/database/src/table.c @@ -610,6 +610,93 @@ void wo_db_val_free(wo_db *db, uint8_t kind, uint64_t v) { db_val_free(kind, v); } +int wo_row_update_field(wo_db *db, uint32_t class_id, uint64_t id, uint32_t field, + uint64_t vm_val, const char **msg, int *err_kind) { + if (err_kind) *err_kind = DB_ERR_MISC; + db_row *r = wo_row_ptr(db, class_id, id); + if (!r) { + *msg = "no such row"; + return -1; + } + const wo_classdesc *c = &db->classes[class_id]; + if (field >= c->field_cnt) { + *msg = "no such field"; + return -1; + } + db_table *t = &db->tables[class_id]; + int ok = 1; + uint64_t nv = db_val_encode(db->classes, c->kinds[field], vm_val, &ok, msg); + if (!ok) { + if (err_kind) *err_kind = DB_ERR_BADKIND; + return -1; + } + /* indexes containing this column: unique checks against the NEW value + run first, against a shadow of the row, before anything mutates */ + uint64_t old = r->slots[field]; + r->slots[field] = nv; + for (uint32_t x = 0; x < t->index_cnt; x++) { + db_index *ix = &t->indexes[x]; + if (!(ix->flags & 1u)) continue; + int touches = 0; + for (uint32_t i = 0; i < ix->col_cnt; i++) + if (ix->cols[i] == field) touches = 1; + if (!touches) continue; + db_ibucket *b = idx_bucket(ix, idx_hash(c, ix, r), 0); + if (!b) continue; + for (uint32_t i = 0; i < b->len; i++) { + if (b->ids[i] == id) continue; + db_row *other = wo_row_ptr(db, class_id, b->ids[i]); + if (other && idx_cols_equal(c, ix, r, other)) { + r->slots[field] = old; /* untouched, promised */ + db_val_free(c->kinds[field], nv); + if (err_kind) *err_kind = DB_ERR_UNIQUE; + *msg = "unique index violation"; + return -1; + } + } + } + /* commit: fix every index containing the column (old entry out under + the OLD value's hash, new entry in), then free the old value */ + r->slots[field] = old; + for (uint32_t x = 0; x < t->index_cnt; x++) { + db_index *ix = &t->indexes[x]; + int touches = 0; + for (uint32_t i = 0; i < ix->col_cnt; i++) + if (ix->cols[i] == field) touches = 1; + if (!touches) continue; + db_ibucket *b = idx_bucket(ix, idx_hash(c, ix, r), 0); + if (b) + for (uint32_t i = 0; i < b->len; i++) + if (b->ids[i] == id) { + b->ids[i] = b->ids[--b->len]; + break; + } + } + r->slots[field] = nv; + for (uint32_t x = 0; x < t->index_cnt; x++) { + db_index *ix = &t->indexes[x]; + int touches = 0; + for (uint32_t i = 0; i < ix->col_cnt; i++) + if (ix->cols[i] == field) touches = 1; + if (!touches) continue; + db_ibucket *b = idx_bucket(ix, idx_hash(c, ix, r), 1); + if (b) { + if (b->len == b->cap) { + uint32_t ncap = b->cap ? b->cap * 2 : 4; + uint64_t *ni = realloc(b->ids, (size_t)ncap * 8u); + if (ni) { + b->ids = ni; + b->cap = ncap; + } + } + if (b->len < b->cap) b->ids[b->len++] = id; + } + } + db_val_free(c->kinds[field], old); + if (err_kind) *err_kind = DB_ERR_NONE; + return 0; +} + int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id) { if (class_id >= db->class_cnt) return -1; db_table *t = &db->tables[class_id]; diff --git a/database/src/table.h b/database/src/table.h index 3890957..c99e0d0 100644 --- a/database/src/table.h +++ b/database/src/table.h @@ -150,6 +150,14 @@ int wo_row_read(wo_db *db, wo_rt *rt, uint32_t class_id, uint64_t id, * it. 0 ok, -1 no such row. */ int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id); +/* Update one field in place (iteration 9 Task 5): encode the VM value, + * swap it into the slot, keep every index containing that column honest — + * remove-old/add-new with the unique re-check running BEFORE anything + * mutates, so a violating update leaves the row untouched. 0 ok, -1 no + * such row / bad field, DB_ERR_* codes via *err_kind like insert. */ +int wo_row_update_field(wo_db *db, uint32_t class_id, uint64_t id, uint32_t field, + uint64_t vm_val, const char **msg, int *err_kind); + /* Borrowed row pointer for engine-internal callers (the WAL writes a row's * encoded bytes; indexes read key slots). NULL = no such row. NEVER handed * to the VM. */ diff --git a/database/src/wal.c b/database/src/wal.c index 2e8858c..4f58161 100644 --- a/database/src/wal.c +++ b/database/src/wal.c @@ -356,6 +356,20 @@ int wo_wal_append_insert(wo_wal *w, wo_db *db, uint32_t class_id, uint64_t id) { return rc; } +int wo_wal_append_update(wo_wal *w, wo_db *db, uint32_t class_id, uint64_t id) { + 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; +} + int wo_wal_append_remove(wo_wal *w, uint32_t class_id, uint64_t id) { wbuf p = {0}; wput_u8(&p, WO_WAL_REMOVE); @@ -390,7 +404,12 @@ static int apply_record(wo_db *db, const uint8_t *payload, uint32_t len) { uint64_t id = rd_u64(&r); if (r.bad || cid >= db->class_cnt) return -1; if (kind == WO_WAL_REMOVE) return wo_row_remove(db, cid, id); - if (kind != WO_WAL_INSERT) return -1; /* UPDATE lands with Task 5 */ + 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]; diff --git a/database/src/wal.h b/database/src/wal.h index 0a05a47..4acbaf2 100644 --- a/database/src/wal.h +++ b/database/src/wal.h @@ -66,6 +66,10 @@ void wo_wal_close(wo_wal *w); * 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). */ diff --git a/docs/superpowers/plans/2026-08-01-db-engine-binding.md b/docs/superpowers/plans/2026-08-01-db-engine-binding.md index b1efe24..f81d34f 100644 --- a/docs/superpowers/plans/2026-08-01-db-engine-binding.md +++ b/docs/superpowers/plans/2026-08-01-db-engine-binding.md @@ -1,6 +1,6 @@ # DB Engine Binding Implementation Plan -> **Status: 🔄 in progress — Tasks 1–3 done 2026-08-15** (story iteration 9) — class-shaped tables, typed WAL + recovery, `insert`/`select` execution. Story iteration 9b (`@table` relations + language-integrated query) follows it and needs a spec brainstormed first. Board: [00-status.md](../../00-status.md) +> **Status: 🔄 in progress — Tasks 1–4 done, Task 5 engine half done 2026-08-15** (story iteration 9) — class-shaped tables, typed WAL + recovery, `insert`/`select` execution. Story iteration 9b (`@table` relations + language-integrated query) follows it and needs a spec brainstormed first. Board: [00-status.md](../../00-status.md) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > @@ -113,7 +113,19 @@ sanitizers included. `database/` gets its own CODE-LOGIC.md as code lands. - [ ] Implement; green. - [ ] Record commit draft: `feat(runtime): secondary indexes — per-shard hash indexes maintained only inside the row API, @unique constraint trap, replay rebuild; doctrine enforced by construction.` -### Task 5: `select` subset + cross-shard point reads +### Task 5 🔄: `select` subset + cross-shard point reads — **superseded in part (2026-08-15)** + +> **Deviation, recorded:** the 9b design (spec'd after this plan was +> written) makes compiler-lowered comprehension queries THE select +> surface — a brace-select subset built here would be a second grammar +> retired months later. So Task 5's ENGINE half landed now (update-point +> through the choke point with unique re-check, delete, the WAL UPDATE +> record with replace-on-replay, builtins 62/63 — `test_table` 839/0, +> `test_wal` 102/0), and the LANGUAGE half (reads, queries, row views, +> the delete statement) is the 9b plan's Tasks 1–5, where it belongs. +> Cross-shard point reads wait for iteration 8's mailboxes, as written. + + **Concept & reason:** the milestone query subset, semantics per the C++ `wo-db` reference where they overlap: select-by-id; select with a WHERE conjunction over indexed fields (index-backed) or a full shard scan (explicitly allowed, explicitly slower); dotted-path field access in the projection; results materialize as VM objects (rows decode back through the field-encoding rules — the Task-1 doc's table read in reverse). Local rows resolve inline; a by-id read of a foreign row hops once via the plan-4 mailbox (point ops hop once; the requesting job pumps its inbox while waiting — never blocks). List queries stay shard-local in this plan; scatter-gather fan-out is future work, stated in the doc. `update` limited to point-by-id field sets (the method-transaction pattern the pricing demo uses); no joins, no aggregations beyond the existing builtins, no RETURNING chaining — all named as out-of-scope in the binding doc. diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index 37f4677..019a99c 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -70,7 +70,7 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { if (C == WO_B_JSON_ENCODE || C == WO_B_JSON_DECODE) return wo_builtin_json(vm, R, ins, msg); if (C >= WO_B_SYS_FIRST && C <= WO_B_PROC_RUN) return wo_builtin_sys(vm, R, ins, msg); - if (C == WO_B_DB_INSERT) return wo_builtin_db(vm, R, ins, msg); + if (C >= WO_B_DB_INSERT && C <= WO_B_DB_DELETE) return wo_builtin_db(vm, R, ins, msg); switch (C) { case WO_B_NOW: { /* wall-clock milliseconds */ struct timespec ts; diff --git a/runtime/src/loader.c b/runtime/src/loader.c index ecb3985..81da8d5 100644 --- a/runtime/src/loader.c +++ b/runtime/src/loader.c @@ -42,6 +42,8 @@ static const uint8_t b_arity[WO_B_MAX + 1] = { the class table (db.c / wo_row_insert). Same trust level as the kind-immediate builtins' B nibble. */ [WO_B_DB_INSERT] = 1, + [WO_B_DB_UPDATE_FIELD] = 4, + [WO_B_DB_DELETE] = 2, [WO_B_NOW] = 0, [WO_B_PRINT] = 1, [WO_B_PRINT_INT] = 1, [WO_B_WORDS] = 1, [WO_B_MULTI_NEW] = 0, [WO_B_MULTI_PUSH] = 2, [WO_B_MULTI_GET] = 2, [WO_B_COUNT] = 1, [WO_B_LATEST] = 1, diff --git a/runtime/src/wob.h b/runtime/src/wob.h index 757f3fc..4146cbd 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -295,8 +295,15 @@ enum { * failures trap WO_T_DB; a failed WAL commit traps WO_T_IO (the write * was applied to RAM but never acknowledged). */ WO_B_DB_INSERT = 61, + /* DB_UPDATE_FIELD: R[B] = class id, R[B+1] = row id, R[B+2] = field + * index, R[B+3] = the value. R[A] = 0. Unique violation traps + * WO_T_UNIQUE with the row untouched. */ + WO_B_DB_UPDATE_FIELD = 62, + /* DB_DELETE: R[B] = class id, R[B+1] = row id. R[A] = 0. A missing row + * traps WO_T_DB (deleting what is not there is a fault, not a no-op). */ + WO_B_DB_DELETE = 63, }; -#define WO_B_MAX 61u +#define WO_B_MAX 63u /* ids at or above this one live in sysio.c, not builtin.c */ #define WO_B_SYS_FIRST WO_B_FS_EXISTS diff --git a/runtime/test/test_table.c b/runtime/test/test_table.c index 90c5941..805e943 100644 --- a/runtime/test/test_table.c +++ b/runtime/test/test_table.c @@ -161,6 +161,66 @@ static void test_slab_growth_and_reuse(void) { wo_rt_destroy(&rt); } +/* Task 5: single-field update through the choke point — value swapped, + * indexes moved, unique violations leave the row untouched. Class 3 in a + * local table: User { email: Text @unique-ish } via idx metadata. */ +static const uint8_t user_kinds[] = {WO_K_TEXT, WO_K_SCALAR}; +static const uint32_t user_idx_meta[] = {1 /*unique*/, 1, 0 /*col: email*/, + 0 /*non-unique*/, 1, 1 /*col: n*/}; +static const wo_classdesc UCLASSES[] = { + {.name = 0, .flags = 0, .field_cnt = 2, .kinds = user_kinds, .idx_cnt = 2, + .idx_meta = user_idx_meta}, +}; + +static void test_update_field(void) { + wo_rt rt; + T_EQ(wo_rt_init(&rt, 1 << 20, UCLASSES, 1), 0); + wo_db db; + T_EQ(wo_db_init(&db, UCLASSES, 1, 0, 1), 0); + const char *msg = ""; + int ek = 0; + wo_str *e1 = wo_str_new(&rt, "a@x", 3); + wo_str *e2 = wo_str_new(&rt, "b@x", 3); + uint64_t v1[2] = {(uint64_t)(uintptr_t)e1, 10}; + uint64_t v2[2] = {(uint64_t)(uintptr_t)e2, 20}; + uint64_t a = wo_row_insert(&db, 0, v1, &msg, NULL); + uint64_t b = wo_row_insert(&db, 0, v2, &msg, NULL); + T_CHECK(a && b); + + /* scalar update: value moves, non-unique index follows */ + T_EQ(wo_row_update_field(&db, 0, a, 1, 99, &msg, &ek), 0); + uint64_t out[2]; + T_EQ(wo_row_read(&db, &rt, 0, a, out, &msg), 0); + T_EQ(out[1], 99); + wo_str_free(&rt, (wo_str *)(uintptr_t)out[0]); + + /* unique violation: updating a's email to b's must refuse, row untouched */ + wo_str *dupe = wo_str_new(&rt, "b@x", 3); + T_EQ(wo_row_update_field(&db, 0, a, 0, (uint64_t)(uintptr_t)dupe, &msg, &ek), -1); + T_EQ(ek, DB_ERR_UNIQUE); + T_EQ(wo_row_read(&db, &rt, 0, a, out, &msg), 0); + wo_str *still = (wo_str *)(uintptr_t)out[0]; + T_CHECK(still->len == 3 && memcmp(still->data, "a@x", 3) == 0); + wo_str_free(&rt, still); + + /* legal text update: old engine value freed (ASan), index moved — the + old email becomes free for someone else */ + wo_str *fresh = wo_str_new(&rt, "c@x", 3); + T_EQ(wo_row_update_field(&db, 0, a, 0, (uint64_t)(uintptr_t)fresh, &msg, &ek), 0); + wo_str *take_a = wo_str_new(&rt, "a@x", 3); + uint64_t v3[2] = {(uint64_t)(uintptr_t)take_a, 30}; + uint64_t cid = wo_row_insert(&db, 0, v3, &msg, &ek); + T_CHECK(cid != 0); /* "a@x" released by the update */ + + wo_drop_obj(&rt, (wo_hdr *)e1); + wo_drop_obj(&rt, (wo_hdr *)e2); + wo_drop_obj(&rt, (wo_hdr *)dupe); + wo_drop_obj(&rt, (wo_hdr *)fresh); + wo_drop_obj(&rt, (wo_hdr *)take_a); + wo_db_destroy(&db); + wo_rt_destroy(&rt); +} + static void test_misuse(void) { const char *msg = ""; wo_db db; @@ -177,6 +237,7 @@ int main(void) { test_roundtrip_all_kinds(); test_id_interleave_across_shards(); test_slab_growth_and_reuse(); + test_update_field(); test_misuse(); return t_report("test_table"); } diff --git a/runtime/test/test_wal.c b/runtime/test/test_wal.c index fc6e4ac..87bd03d 100644 --- a/runtime/test/test_wal.c +++ b/runtime/test/test_wal.c @@ -14,6 +14,7 @@ #include #include +#include "gc.h" #include "obj.h" #include "t.h" #include "table.h" @@ -73,13 +74,47 @@ static void test_roundtrip_replay(void) { uint64_t fresh = wo_row_insert(&db2, 0, vals, &msg, NULL); T_CHECK(fresh > ids[2]); wo_db_destroy(&db2); - wo_rt_destroy(&rt); + + /* update record: re-log, replay replaces */ + { + char upath[128]; + snprintf(upath, sizeof upath, "%s/upd.wal", g_dir); + wo_db du; + T_EQ(wo_db_init(&du, CLASSES, 1, 0, 1), 0); + wo_wal wu; + T_EQ(wo_wal_open(&wu, upath, 0), 0); + wo_str *s1 = wo_str_new(&rt, "old", 3); + uint64_t uv[2] = {7, (uint64_t)(uintptr_t)s1}; + uint64_t uid = wo_row_insert(&du, 0, uv, &msg, NULL); + T_EQ(wo_wal_append_insert(&wu, &du, 0, uid), 0); + int ek = 0; + wo_str *s2 = wo_str_new(&rt, "new!", 4); + T_EQ(wo_row_update_field(&du, 0, uid, 1, (uint64_t)(uintptr_t)s2, &msg, &ek), 0); + T_EQ(wo_row_update_field(&du, 0, uid, 0, 8, &msg, &ek), 0); + T_EQ(wo_wal_append_update(&wu, &du, 0, uid), 0); + T_EQ(wo_wal_commit(&wu), 0); + wo_wal_close(&wu); + wo_db_destroy(&du); + wo_db db4; + T_EQ(wo_db_init(&db4, CLASSES, 1, 0, 1), 0); + T_EQ(wo_wal_replay(upath, &db4), 2); + uint64_t uo[2]; + T_EQ(wo_row_read(&db4, &rt, 0, uid, uo, &msg), 0); + T_EQ(uo[0], 8); + wo_str *us = (wo_str *)(uintptr_t)uo[1]; + T_CHECK(us->len == 4 && memcmp(us->data, "new!", 4) == 0); + wo_str_free(&rt, us); + wo_drop_obj(&rt, (wo_hdr *)s1); + wo_drop_obj(&rt, (wo_hdr *)s2); + wo_db_destroy(&db4); + } /* replay of a missing file is a fresh boot, not an error */ wo_db db3; T_EQ(wo_db_init(&db3, CLASSES, 1, 0, 1), 0); T_EQ(wo_wal_replay("/nonexistent/nope.wal", &db3), 0); wo_db_destroy(&db3); + wo_rt_destroy(&rt); } static void test_torn_tail(void) {