feat(db2-delta): WAL delta record kind and encoder
- enum: add WO_WAL_DELTA = 4, existing 1/2/3 untouched (on-disk logs) - wal.h: document kind 4's payload shape in the format docblock - wal.h: declare wo_wal_append_delta(w, db, class_id, id, field_idx, back_off, value) — back-pointer taken as a parameter, not looked up, keeping the encoder ignorant of table/map state - wal.c: implement it, modeled on wo_wal_append_insert's shape — wput_u8/u32/u64 the header fields, enc_val the one field, stage() - test_wal.c: new test_delta_record — stages a delta after an insert, commits, then preads the raw record and asserts kind/class/id/ field_idx/back-pointer/value all round-trip; registered in main() - nothing reads deltas back yet — decode/apply is a later task Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 9c6f832c0534a59e644c53b7cd2850581da12159)
This commit is contained in:
parent
048633bf27
commit
f1cf2d2f85
3 changed files with 88 additions and 1 deletions
|
|
@ -426,6 +426,21 @@ int wo_wal_append_update(wo_wal *w, wo_db *db, uint32_t class_id, uint64_t id) {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
* kind : 1 insert (body = the row's fields, engine encoding below)
|
||||
* 2 remove (no body)
|
||||
* 3 update (reserved for Task 5)
|
||||
* 4 delta (single-field update; body = field_idx u32 |
|
||||
* back-pointer offset u64 | the one field's value, engine
|
||||
* encoding below — keys-resident tables only)
|
||||
*
|
||||
* Field encoding in a body walks the class table's kinds:
|
||||
* SCALAR 8 bytes
|
||||
|
|
@ -43,7 +46,7 @@
|
|||
|
||||
#define WO_WAL_MARK 0x574F4C31u /* "WOL1" LE */
|
||||
|
||||
enum { WO_WAL_INSERT = 1, WO_WAL_REMOVE = 2, WO_WAL_UPDATE = 3 };
|
||||
enum { WO_WAL_INSERT = 1, WO_WAL_REMOVE = 2, WO_WAL_UPDATE = 3, WO_WAL_DELTA = 4 };
|
||||
|
||||
typedef struct wo_wal {
|
||||
int fd;
|
||||
|
|
@ -126,6 +129,13 @@ int wo_wal_append_remove(wo_wal *w, uint32_t class_id, uint64_t id);
|
|||
* 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);
|
||||
/* DELTA logs one field change, for a keys-resident row whose payload may
|
||||
* already be gone from RAM (so there is no whole row to re-log). back_off
|
||||
* is the row's PREVIOUS record's offset (insert or an earlier delta) — a
|
||||
* caller parameter, not looked up here, so the encoder stays ignorant of
|
||||
* table/map state. */
|
||||
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);
|
||||
|
||||
/* databasev2 4: which half of the barrier failed. A pwrite failure and an
|
||||
* fdatasync failure are different operational problems (a short write vs a
|
||||
|
|
|
|||
|
|
@ -574,6 +574,67 @@ static void test_keys_resident_round_trip(void) {
|
|||
wo_rt_destroy(&rt);
|
||||
}
|
||||
|
||||
/* databasev2 (task 1 of keys-resident delta updates): the delta record kind.
|
||||
* A keys-resident row cannot be rewritten whole to update one field (its
|
||||
* payload may already be gone from RAM), so a delta logs just the changed
|
||||
* field plus a back-pointer to the row's previous record. Nothing reads
|
||||
* deltas back yet — this only proves the encoder's bytes are what the format
|
||||
* says: kind, class, id, field index, back-pointer, value. */
|
||||
static void test_delta_record(void) {
|
||||
char path[128];
|
||||
snprintf(path, sizeof path, "%s/delta.wal", g_dir);
|
||||
wo_rt rt;
|
||||
T_EQ(wo_rt_init(&rt, 1 << 20, KEYS_CLASSES, 1), 0);
|
||||
wo_db db;
|
||||
T_EQ(wo_db_init(&db, KEYS_CLASSES, 1, 0, 1), 0);
|
||||
wo_wal w;
|
||||
T_EQ(wo_wal_open(&w, path, 1 << 16), 0);
|
||||
db.rt = &rt; rt.wal = &w; rt.db = &db;
|
||||
const char *msg = "";
|
||||
|
||||
wo_str *s = wo_str_new(&rt, "hi", 2);
|
||||
uint64_t vals[2] = {111, (uint64_t)(uintptr_t)s};
|
||||
uint64_t id = wo_row_insert(&db, 0, vals, &msg, NULL);
|
||||
T_CHECK(id != 0);
|
||||
uint64_t base_off = wo_wal_next_offset(&w);
|
||||
T_EQ(wo_wal_append_insert(&w, &db, 0, id), 0);
|
||||
T_EQ(wo_wal_commit(&w), 0);
|
||||
|
||||
/* field 0 (scalar) changes from 111 to 999; back-pointer is the insert
|
||||
* record this delta supersedes */
|
||||
uint64_t delta_off = wo_wal_next_offset(&w);
|
||||
T_EQ(wo_wal_append_delta(&w, &db, 0, id, 0, base_off, 999), 0);
|
||||
T_EQ(wo_wal_commit(&w), 0);
|
||||
|
||||
/* payload: kind u8 | class u32 | id u64 | field_idx u32 | back_off u64 |
|
||||
* value u64 (scalar) — 33 bytes, after the 8-byte len+crc head */
|
||||
uint8_t head[8], body[33];
|
||||
T_EQ((int)pread(w.fd, head, 8, (off_t)delta_off), 8);
|
||||
uint32_t len;
|
||||
memcpy(&len, head, 4);
|
||||
T_EQ(len, 33u);
|
||||
T_EQ((int)pread(w.fd, body, 33, (off_t)(delta_off + 8)), 33);
|
||||
T_EQ(body[0], WO_WAL_DELTA);
|
||||
uint32_t cid;
|
||||
uint64_t rid, back, val;
|
||||
uint32_t fidx;
|
||||
memcpy(&cid, body + 1, 4);
|
||||
memcpy(&rid, body + 5, 8);
|
||||
memcpy(&fidx, body + 13, 4);
|
||||
memcpy(&back, body + 17, 8);
|
||||
memcpy(&val, body + 25, 8);
|
||||
T_EQ(cid, 0u);
|
||||
T_EQ(rid, id);
|
||||
T_EQ(fidx, 0u);
|
||||
T_EQ(back, base_off);
|
||||
T_EQ(val, 999u);
|
||||
|
||||
wo_str_free(&rt, s);
|
||||
wo_wal_close(&w);
|
||||
wo_db_destroy(&db);
|
||||
wo_rt_destroy(&rt);
|
||||
}
|
||||
|
||||
/* databasev2 2 (5c): boot. A keys-resident store must come back from replay
|
||||
* with its rows readable FROM THE LOG — the map rebuilt to offsets, not slabs.
|
||||
* This is the half the round-trip test cannot cover: it runs in a fresh db,
|
||||
|
|
@ -1217,6 +1278,7 @@ int main(void) {
|
|||
test_commit_failure_detected();
|
||||
test_compact_shortens_and_replays_equal();
|
||||
test_keys_resident_round_trip();
|
||||
test_delta_record();
|
||||
test_keys_resident_replay();
|
||||
test_keys_resident_survives_compaction();
|
||||
test_keys_resident_delete();
|
||||
|
|
|
|||
Loading…
Reference in a new issue