feat: update-point + delete engine half (iteration 9, Task 5 engine)
- wo_row_update_field: encode new value, unique re-check against a shadow BEFORE any mutation (violating update leaves the row untouched, DB_ERR_UNIQUE), index entries moved old-hash -> new-hash, old engine value freed; proven by test_table (unique refusal keeps the row, released key becomes insertable) - WAL UPDATE record: full-row re-log, replay = replace (remove + re-create same id); prefix/suffix delta recorded as later optimization; test_wal replays insert+update to the updated state - builtins 62 DB_UPDATE_FIELD (cid,id,field,value) and 63 DB_DELETE (cid,id), commit-before-ack like insert, WO_T_UNIQUE/WO_T_DB/WO_T_IO mapping; dispatch range 61..63; loader arities; runner mirror - plan Task 5 marked superseded-in-part with the recorded deviation: the language surface (reads, queries, row views, delete statement) is 9b's, where the comprehension design put it -- no interim brace- select grammar to retire later - gates: test_table 839/0, test_wal 102/0, 15 suites, oop-e2e 73/0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
47db5b6cfe
commit
7e3bf31d36
12 changed files with 278 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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). */
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue