#include "table.h" #include #include #include "cont.h" #include "gc.h" /* databasev2 2 (5c): VM-side drops for materialised rows */ #include "wal.h" /* databasev2 2 (5c): a keys-resident borrow reads the log */ /* ---- engine-owned value encode / free / decode ------------------------- */ /* Free one encoded slot value of [kind]. Recursion mirrors encoding. */ static void db_val_free(uint8_t kind, uint64_t v); static void db_rec_free(db_rec *r, const wo_classdesc *classes) { const wo_classdesc *c = &classes[r->class_id]; for (uint32_t i = 0; i < c->field_cnt; i++) db_val_free(c->kinds[i], r->slots[i]); free(r); } /* db_val_free needs the class table for nested records; a file-static is * the honest signature here — one engine per process today (N=1), and the * pointer is set once at init. Revisit when iteration 8 brings N>1 shards * (each shard's wo_db shares the same immutable class table anyway). */ static const wo_classdesc *g_classes; static void db_val_free(uint8_t kind, uint64_t v) { if (!v) return; switch (kind) { /* iteration 19: FLOAT is a word in the slot, nothing to free. BYTES is stored in the same db_text blob a Text is, so the same free serves. */ case WO_K_SCALAR: case WO_K_FLOAT: return; case WO_K_TEXT: case WO_K_BYTES: free((db_text *)(uintptr_t)v); return; case WO_K_OWNED: db_rec_free((db_rec *)(uintptr_t)v, g_classes); return; case WO_K_MULTI: { db_multi *m = (db_multi *)(uintptr_t)v; for (uint32_t i = 0; i < m->len; i++) db_val_free(m->elem_kind, m->items[i]); free(m); return; } case WO_K_MAP: { db_map *m = (db_map *)(uintptr_t)v; for (uint32_t i = 0; i < m->len; i++) { db_val_free(m->key_kind, m->kv[2 * i]); db_val_free(m->val_kind, m->kv[2 * i + 1]); } free(m); return; } default: return; /* GCREF never stored */ } } /* Encode one VM value into an engine-owned slot value. 0-with-*ok=0 means * failure (OOM or a GCREF); a genuine nil encodes as 0 with *ok=1. */ static uint64_t db_val_encode(const wo_classdesc *classes, uint8_t kind, uint64_t v, int *ok, const char **msg) { *ok = 1; switch (kind) { /* iteration 19: the f64's bits go in the slot unexamined. NaN, the infinities, and -0.0 all store and read back bit-exact because nothing here interprets the word — the kind byte is what tells json and the WAL how to read it later. */ case WO_K_SCALAR: case WO_K_FLOAT: return v; case WO_K_TEXT: case WO_K_BYTES: { if (!v) return 0; const wo_str *s = (const wo_str *)(uintptr_t)v; db_text *t = malloc(sizeof(db_text) + s->len); if (!t) goto oom; t->len = s->len; memcpy(t->bytes, s->data, s->len); return (uint64_t)(uintptr_t)t; } case WO_K_OWNED: { if (!v) return 0; const wo_hdr *o = (const wo_hdr *)(uintptr_t)v; const wo_classdesc *c = &classes[o->class_id]; db_rec *r = malloc(sizeof(db_rec) + (size_t)c->field_cnt * 8u); if (!r) goto oom; r->class_id = o->class_id; r->_pad = 0; const uint64_t *f = (const uint64_t *)(const void *)(o + 1); for (uint32_t i = 0; i < c->field_cnt; i++) { r->slots[i] = db_val_encode(classes, c->kinds[i], f[i], ok, msg); if (!*ok) { /* free what we built so far, then fail upward */ for (uint32_t j = 0; j < i; j++) db_val_free(c->kinds[j], r->slots[j]); free(r); return 0; } } return (uint64_t)(uintptr_t)r; } case WO_K_MULTI: { if (!v) return 0; const wo_multi *m = (const wo_multi *)(uintptr_t)v; db_multi *d = malloc(sizeof(db_multi) + (size_t)m->len * 8u); if (!d) goto oom; d->elem_kind = m->elem_kind; d->len = m->len; for (uint32_t i = 0; i < m->len; i++) { d->items[i] = db_val_encode(classes, m->elem_kind, m->items[i], ok, msg); if (!*ok) { for (uint32_t j = 0; j < i; j++) db_val_free(d->elem_kind, d->items[j]); free(d); return 0; } } return (uint64_t)(uintptr_t)d; } case WO_K_MAP: { if (!v) return 0; const wo_map *m = (const wo_map *)(uintptr_t)v; db_map *d = malloc(sizeof(db_map) + (size_t)m->len * 16u); if (!d) goto oom; d->key_kind = m->key_kind; d->val_kind = m->val_kind; d->len = m->len; for (uint32_t i = 0; i < m->len; i++) { d->kv[2 * i] = db_val_encode(classes, m->key_kind, m->keys[i], ok, msg); uint64_t dv = 0; if (*ok) dv = db_val_encode(classes, m->val_kind, m->vals[i], ok, msg); d->kv[2 * i + 1] = dv; if (!*ok) { for (uint32_t j = 0; j <= i; j++) { db_val_free(d->key_kind, d->kv[2 * j]); db_val_free(d->val_kind, d->kv[2 * j + 1]); } free(d); return 0; } } return (uint64_t)(uintptr_t)d; } default: *ok = 0; *msg = "a garbage-collected value cannot be stored in a table field"; return 0; } oom: *ok = 0; *msg = "out of memory encoding a row"; return 0; } /* Decode one engine slot back into a fresh VM value (the out-gate: always * a copy). 0-with-*ok=0 = OOM; nil decodes as 0 with *ok=1. */ static uint64_t db_val_decode(wo_rt *rt, uint8_t kind, uint64_t v, int *ok, const char **msg) { *ok = 1; switch (kind) { case WO_K_SCALAR: case WO_K_FLOAT: return v; /* iteration 19: bits back out unchanged */ case WO_K_TEXT: case WO_K_BYTES: { if (!v) return 0; const db_text *t = (const db_text *)(uintptr_t)v; /* the out-gate decides the KIND: a Bytes column must hand back a Bytes, or a Text builtin would happily accept the row's value and the distinct type would be a fiction at the storage boundary */ wo_str *s = kind == WO_K_BYTES ? wo_bytes_new(rt, t->bytes, t->len) : wo_str_new(rt, t->bytes, t->len); if (!s) goto oom; return (uint64_t)(uintptr_t)s; } case WO_K_OWNED: { if (!v) return 0; const db_rec *r = (const db_rec *)(uintptr_t)v; wo_hdr *o = wo_obj_new(rt, r->class_id); if (!o) goto oom; const wo_classdesc *c = &rt->classes[r->class_id]; uint64_t *f = wo_fields(o); for (uint32_t i = 0; i < c->field_cnt; i++) { f[i] = db_val_decode(rt, c->kinds[i], r->slots[i], ok, msg); if (!*ok) return 0; /* partial object: rt teardown reclaims (test scope) */ } return (uint64_t)(uintptr_t)o; } case WO_K_MULTI: { if (!v) return 0; const db_multi *d = (const db_multi *)(uintptr_t)v; wo_multi *m = wo_multi_new(rt, d->elem_kind); if (!m) goto oom; for (uint32_t i = 0; i < d->len; i++) { uint64_t ev = db_val_decode(rt, d->elem_kind, d->items[i], ok, msg); if (!*ok || wo_multi_push(m, ev) != 0) goto oom; } return (uint64_t)(uintptr_t)m; } case WO_K_MAP: { if (!v) return 0; const db_map *d = (const db_map *)(uintptr_t)v; wo_map *m = wo_map_new(rt, d->key_kind, d->val_kind); if (!m) goto oom; for (uint32_t i = 0; i < d->len; i++) { uint64_t kv = db_val_decode(rt, d->key_kind, d->kv[2 * i], ok, msg); uint64_t vv = 0; if (*ok) vv = db_val_decode(rt, d->val_kind, d->kv[2 * i + 1], ok, msg); uint64_t old; if (!*ok || wo_map_set(m, kv, vv, &old) < 0) goto oom; } return (uint64_t)(uintptr_t)m; } default: return 0; /* GCREF never stored, so never decoded */ } oom: *ok = 0; *msg = "out of memory decoding a row"; return 0; } /* ---- id hash (open addressing, pow2, id -> global slot + 1) ----------- */ static uint64_t hmix(uint64_t x) { /* splitmix64 finalizer */ x += 0x9e3779b97f4a7c15ull; x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ull; x = (x ^ (x >> 27)) * 0x94d049bb133111ebull; return x ^ (x >> 31); } /* Ids are never 0 (0 spells "empty bucket") and never reused, so all-ones * can never collide with a live id — it marks a deleted bucket that probes * walk straight past. */ #define H_DELETED ((uint64_t)-1) static int hgrow(db_table *t) { size_t ncap = t->hcap ? t->hcap * 2 : 64; uint64_t *nk = calloc(ncap, 8), *nv = calloc(ncap, 8); if (!nk || !nv) { free(nk); free(nv); return -1; } for (size_t i = 0; i < t->hcap; i++) { if (!t->hkeys[i] || t->hkeys[i] == H_DELETED) continue; size_t j = hmix(t->hkeys[i]) & (ncap - 1); while (nk[j]) j = (j + 1) & (ncap - 1); nk[j] = t->hkeys[i]; nv[j] = t->hvals[i]; } free(t->hkeys); free(t->hvals); t->hkeys = nk; t->hvals = nv; t->hcap = ncap; return 0; } static int hput(db_table *t, uint64_t id, uint64_t slot1) { if (t->hlen * 10 >= t->hcap * 7 && hgrow(t) != 0) return -1; size_t j = hmix(id) & (t->hcap - 1); while (t->hkeys[j] && t->hkeys[j] != id) j = (j + 1) & (t->hcap - 1); if (!t->hkeys[j]) t->hlen++; t->hkeys[j] = id; t->hvals[j] = slot1; return 0; } static uint64_t hget(const db_table *t, uint64_t id) { if (!t->hcap) return 0; size_t j = hmix(id) & (t->hcap - 1); while (t->hkeys[j]) { if (t->hkeys[j] == id) return t->hvals[j]; j = (j + 1) & (t->hcap - 1); } return 0; } static void hdel(db_table *t, uint64_t id) { if (!t->hcap) return; size_t j = hmix(id) & (t->hcap - 1); while (t->hkeys[j]) { if (t->hkeys[j] == id) { t->hkeys[j] = H_DELETED; t->hvals[j] = 0; return; } j = (j + 1) & (t->hcap - 1); } } /* ---- tables and rows ---------------------------------------------------- */ /* ---- secondary indexes (Task 4) ---------------------------------------- */ /* iteration 19: an index key for a FLOAT column is the value's CANONICAL * bits, not its raw bits. Two values that the total order calls equal must * hash and compare equal, and raw bits break that twice: -0.0 and +0.0 are * equal but differ in the sign bit, and two NaNs with different payloads are * equal (both "last") but differ everywhere. Without this a `unique` Float * column would accept both -0.0 and 0.0, and a probe for one would miss a row * stored as the other. */ static uint64_t idx_float_key(uint64_t bits) { double d = wo_f64(bits); if (d != d) return 0x7FF8000000000000ull; /* every NaN -> the canonical one */ if (d == 0.0) return 0; /* -0.0 -> +0.0 */ return bits; } /* hash of one row's index columns: kind-driven, never trusted for equality */ static uint64_t idx_hash(const wo_classdesc *c, const db_index *ix, const db_row *r) { uint64_t h = 0x9e3779b97f4a7c15ull; for (uint32_t i = 0; i < ix->col_cnt; i++) { uint32_t col = ix->cols[i]; uint64_t v = r->slots[col]; if (c->kinds[col] == WO_K_TEXT) { const db_text *t = (const db_text *)(uintptr_t)v; uint64_t th = 1469598103934665603ull; /* FNV-1a over bytes; nil = 0 */ if (t) for (uint32_t b = 0; b < t->len; b++) th = (th ^ (uint8_t)t->bytes[b]) * 1099511628211ull; else th = 0; v = th; } else if (c->kinds[col] == WO_K_FLOAT) v = idx_float_key(v); /* iteration 19 */ h ^= hmix(v + i); } return h ? h : 1; /* 0 marks an empty bucket */ } static db_ibucket *idx_bucket(db_index *ix, uint64_t h, int create); /* One KEY's bucket hash — must reproduce idx_hash's result for a * single-column index bit for bit (same FNV, same float folding, same * position mix at i == 0), or probes and maintenance disagree on the * bucket and rows silently vanish from reads. */ static uint64_t idx_hash_key1(uint8_t kind, uint64_t key_scalar, const void *key_bytes, uint32_t key_len) { uint64_t v; if (kind == WO_K_TEXT) { if (key_bytes) { uint64_t th = 1469598103934665603ull; const uint8_t *p = (const uint8_t *)key_bytes; for (uint32_t b = 0; b < key_len; b++) th = (th ^ p[b]) * 1099511628211ull; v = th; } else v = 0; /* nil text, exactly as idx_hash spells it */ } else if (kind == WO_K_FLOAT) v = idx_float_key(key_scalar); else v = key_scalar; uint64_t h = 0x9e3779b97f4a7c15ull; h ^= hmix(v + 0); return h ? h : 1; } int wo_idx_probe(wo_db *db, uint32_t class_id, uint32_t index, uint64_t key_scalar, const void *key_bytes, uint32_t key_len, uint64_t **out_ids, uint32_t *out_cnt) { *out_ids = NULL; *out_cnt = 0; if (class_id >= db->class_cnt) return 0; db_table *t = &db->tables[class_id]; if (!t->row_size || index >= t->index_cnt) return 0; db_index *ix = &t->indexes[index]; if (ix->col_cnt != 1) return 0; /* composite: the caller keeps its scan */ uint32_t col = ix->cols[0]; uint8_t kind = db->classes[class_id].kinds[col]; db_ibucket *b = idx_bucket(ix, idx_hash_key1(kind, key_scalar, key_bytes, key_len), 0); if (!b || !b->len) return 1; /* probed: genuinely empty */ uint64_t *ids = malloc((size_t)b->len * 8u); if (!ids) return -1; uint32_t n = 0; for (uint32_t i = 0; i < b->len; i++) { /* databasev2 2 (5d): THE unique shadow — the site the plan called the * real coupling, because it needs a row it cannot get from a slab. For * a keys-resident table each candidate costs a pread and a * materialisation: the disclosed price of `@unique` there, bounded by * the bucket rather than the table. */ db_row *r = wo_row_borrow(db, class_id, b->ids[i], NULL); if (!r) continue; int eq; if (kind == WO_K_TEXT) { const db_text *have = (const db_text *)(uintptr_t)r->slots[col]; eq = (!key_bytes && !have) || (key_bytes && have && have->len == key_len && memcmp(have->bytes, key_bytes, key_len) == 0); } else /* raw-word equality for scalars AND floats — the slab walk's * exact comparison, so probe results never differ from scan * results (the hash canonicalized only to FIND the bucket) */ eq = r->slots[col] == key_scalar; wo_row_release(db, class_id, r); /* before any use of the result */ if (eq) ids[n++] = b->ids[i]; } if (!n) { free(ids); return 1; } *out_ids = ids; *out_cnt = n; return 1; } static int idx_cols_equal(const wo_classdesc *c, const db_index *ix, const db_row *a, const db_row *b) { for (uint32_t i = 0; i < ix->col_cnt; i++) { uint32_t col = ix->cols[i]; if (c->kinds[col] == WO_K_TEXT) { const db_text *x = (const db_text *)(uintptr_t)a->slots[col]; const db_text *y = (const db_text *)(uintptr_t)b->slots[col]; if (!x || !y) { if (x != y) return 0; } else if (x->len != y->len || memcmp(x->bytes, y->bytes, x->len) != 0) return 0; } else if (c->kinds[col] == WO_K_FLOAT) { /* iteration 19: compare canonicalized, matching idx_hash */ if (idx_float_key(a->slots[col]) != idx_float_key(b->slots[col])) return 0; } else if (a->slots[col] != b->slots[col]) return 0; } return 1; } static db_ibucket *idx_bucket(db_index *ix, uint64_t h, int create) { if (ix->bcap == 0) { if (!create) return NULL; ix->buckets = calloc(64, sizeof(db_ibucket)); if (!ix->buckets) return NULL; ix->bcap = 64; } if (create && ix->blen * 10 >= ix->bcap * 7) { size_t ncap = ix->bcap * 2; db_ibucket *nb = calloc(ncap, sizeof(db_ibucket)); if (!nb) return NULL; for (size_t i = 0; i < ix->bcap; i++) { if (!ix->buckets[i].hash) continue; size_t j = ix->buckets[i].hash & (ncap - 1); while (nb[j].hash) j = (j + 1) & (ncap - 1); nb[j] = ix->buckets[i]; } free(ix->buckets); ix->buckets = nb; ix->bcap = ncap; } size_t j = h & (ix->bcap - 1); while (ix->buckets[j].hash) { if (ix->buckets[j].hash == h) return &ix->buckets[j]; j = (j + 1) & (ix->bcap - 1); } if (!create) return NULL; ix->buckets[j].hash = h; ix->blen++; return &ix->buckets[j]; } /* Add [r] to every index; unique violation reports which without mutating * anything (checks run before any add). 0 ok, DB_ERR_* otherwise. */ static int idx_add_row(wo_db *db, db_table *t, db_row *r) { const wo_classdesc *c = &db->classes[t->class_id]; for (uint32_t x = 0; x < t->index_cnt; x++) { db_index *ix = &t->indexes[x]; if (!(ix->flags & 1u)) 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++) { /* databasev2 2: borrow, never peek at a slab. For a keys-table the * conflicting row may not be resident, and a unique check that * silently skipped non-resident rows would be a correctness hole, * not a limitation. */ const char *bmsg = ""; db_row *other = wo_row_borrow(db, t->class_id, b->ids[i], &bmsg); int clash = other && idx_cols_equal(c, ix, r, other); wo_row_release(db, t->class_id, other); if (clash) return DB_ERR_UNIQUE; } } for (uint32_t x = 0; x < t->index_cnt; x++) { db_index *ix = &t->indexes[x]; db_ibucket *b = idx_bucket(ix, idx_hash(c, ix, r), 1); if (!b) return DB_ERR_OOM; 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) return DB_ERR_OOM; b->ids = ni; b->cap = ncap; } b->ids[b->len++] = r->id; } return 0; } static void idx_remove_row(wo_db *db, db_table *t, db_row *r) { const wo_classdesc *c = &db->classes[t->class_id]; for (uint32_t x = 0; x < t->index_cnt; x++) { db_index *ix = &t->indexes[x]; 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] == r->id) { b->ids[i] = b->ids[--b->len]; break; } } } int wo_db_init(wo_db *db, const wo_classdesc *classes, uint32_t class_cnt, uint32_t shard, uint32_t nshards) { if (!nshards || shard >= nshards) return -1; memset(db, 0, sizeof(*db)); db->classes = classes; db->class_cnt = class_cnt; db->shard = shard; db->nshards = nshards; db->tables = calloc(class_cnt ? class_cnt : 1, sizeof(db_table)); if (!db->tables) return -1; g_classes = classes; return 0; } static void table_destroy(wo_db *db, db_table *t) { free(t->scratch); /* databasev2 2 */ t->scratch = NULL; t->scratch_cap = 0; /* free every live row's engine-owned values, then the slabs */ const wo_classdesc *c = &db->classes[t->class_id]; for (uint32_t s = 0; s < t->slab_cnt; s++) { for (uint32_t i = 0; i < DB_SLAB_ROWS; i++) { uint32_t g = s * DB_SLAB_ROWS + i; if (!(t->bitmap[g >> 6] & (1ull << (g & 63)))) continue; db_row *r = (db_row *)(t->slabs[s] + (size_t)i * t->row_size); for (uint32_t f = 0; f < c->field_cnt; f++) db_val_free(c->kinds[f], r->slots[f]); } free(t->slabs[s]); } free(t->slabs); free(t->bitmap); free(t->free_slots); free(t->hkeys); free(t->hvals); for (uint32_t x = 0; x < t->index_cnt; x++) { for (size_t b = 0; b < t->indexes[x].bcap; b++) free(t->indexes[x].buckets[b].ids); free(t->indexes[x].buckets); } free(t->indexes); } void wo_db_destroy(wo_db *db) { if (!db->tables) return; for (uint32_t i = 0; i < db->class_cnt; i++) if (db->tables[i].slab_cnt || db->tables[i].hkeys) table_destroy(db, &db->tables[i]); free(db->tables); db->tables = NULL; } static db_table *table_of(wo_db *db, uint32_t class_id) { if (class_id >= db->class_cnt) return NULL; db_table *t = &db->tables[class_id]; if (!t->row_size) { /* lazy init on first touch */ const wo_classdesc *c = &db->classes[class_id]; t->class_id = class_id; t->row_size = sizeof(db_row) + (size_t)c->field_cnt * 8u; t->next_id = db->shard + 1; /* S+1, then += N: interleaved, local-only */ if (c->idx_cnt) { t->indexes = calloc(c->idx_cnt, sizeof(db_index)); if (!t->indexes) return NULL; const uint32_t *im = c->idx_meta; for (uint32_t x = 0; x < c->idx_cnt; x++) { t->indexes[x].flags = im[0]; t->indexes[x].col_cnt = im[1]; t->indexes[x].cols = im + 2; im += 2 + im[1]; } t->index_cnt = c->idx_cnt; } } return t; } static db_row *slot_row(db_table *t, uint32_t g) { return (db_row *)(t->slabs[g / DB_SLAB_ROWS] + (size_t)(g % DB_SLAB_ROWS) * t->row_size); } /* Pick the slot a new row lands in: recycled first, else the next free bit, * else grow a slab. Returns the global slot or UINT32_MAX on OOM. */ static uint32_t slot_alloc(db_table *t) { if (t->free_cnt) return t->free_slots[--t->free_cnt]; uint32_t total = t->slab_cnt * DB_SLAB_ROWS; for (uint32_t g = 0; g < total; g++) /* cheap at slab granularity: only reached when free list is empty, and the bitmap scan is bounded by one word test per 64 slots */ if (!(t->bitmap[g >> 6] & (1ull << (g & 63)))) return g; /* grow */ if (t->slab_cnt == t->slab_cap) { uint32_t ncap = t->slab_cap ? t->slab_cap * 2 : 4; uint8_t **ns = realloc(t->slabs, (size_t)ncap * sizeof(uint8_t *)); if (!ns) return UINT32_MAX; t->slabs = ns; t->slab_cap = ncap; } uint8_t *slab = malloc((size_t)DB_SLAB_ROWS * t->row_size); if (!slab) return UINT32_MAX; size_t nwords = ((size_t)(t->slab_cnt + 1) * DB_SLAB_ROWS + 63) / 64; uint64_t *nb = realloc(t->bitmap, nwords * 8); if (!nb) { free(slab); return UINT32_MAX; } memset(nb + ((size_t)t->slab_cnt * DB_SLAB_ROWS) / 64, 0, (nwords - ((size_t)t->slab_cnt * DB_SLAB_ROWS) / 64) * 8); t->bitmap = nb; t->slabs[t->slab_cnt] = slab; return t->slab_cnt++ * DB_SLAB_ROWS; } uint64_t wo_row_insert(wo_db *db, uint32_t class_id, const uint64_t *vals, const char **msg, int *err_kind) { if (err_kind) *err_kind = DB_ERR_MISC; db_table *t = table_of(db, class_id); if (!t) { *msg = "no such class"; return 0; } const wo_classdesc *c = &db->classes[class_id]; uint32_t g = slot_alloc(t); if (g == UINT32_MAX) { *msg = "out of memory growing a table"; return 0; } db_row *r = slot_row(t, g); r->class_id = class_id; r->flags = 0; int ok = 1; uint32_t i = 0; for (; i < c->field_cnt; i++) { r->slots[i] = db_val_encode(db->classes, c->kinds[i], vals[i], &ok, msg); if (!ok) { if (err_kind) *err_kind = DB_ERR_BADKIND; break; } } if (!ok) { for (uint32_t j = 0; j < i; j++) db_val_free(c->kinds[j], r->slots[j]); /* slot never became live: recycle it (bitmap bit was never set) */ if (t->free_cnt == t->free_cap) { uint32_t ncap = t->free_cap ? t->free_cap * 2 : 16; uint32_t *nf = realloc(t->free_slots, (size_t)ncap * 4); if (nf) { t->free_slots = nf; t->free_cap = ncap; } } if (t->free_cnt < t->free_cap) t->free_slots[t->free_cnt++] = g; return 0; } r->id = t->next_id; t->next_id += db->nshards; if (hput(t, r->id, (uint64_t)g + 1) != 0) { for (uint32_t j = 0; j < c->field_cnt; j++) db_val_free(c->kinds[j], r->slots[j]); if (err_kind) *err_kind = DB_ERR_OOM; *msg = "out of memory indexing a row"; return 0; } t->bitmap[g >> 6] |= 1ull << (g & 63); t->count++; /* THE index hook (Task 4): inside the choke point, never anywhere else. A unique violation un-applies the row entirely — id never handed out twice matters less than the row never having existed. */ int irc = idx_add_row(db, t, r); if (irc != 0) { t->bitmap[g >> 6] &= ~(1ull << (g & 63)); hdel(t, r->id); t->count--; t->next_id -= db->nshards; /* the id was never observable: reclaim it */ for (uint32_t j = 0; j < c->field_cnt; j++) db_val_free(c->kinds[j], r->slots[j]); if (t->free_cnt < t->free_cap) t->free_slots[t->free_cnt++] = g; if (err_kind) *err_kind = irc; *msg = irc == DB_ERR_UNIQUE ? "unique index violation" : "out of memory indexing a row"; return 0; } if (err_kind) *err_kind = DB_ERR_NONE; return r->id; } uint64_t wo_row_insert_slots(wo_db *db, uint32_t class_id, const uint64_t *slots, const char **msg, int *err_kind) { if (err_kind) *err_kind = DB_ERR_MISC; db_table *t = table_of(db, class_id); const wo_classdesc *c = class_id < db->class_cnt ? &db->classes[class_id] : NULL; if (!t || !c) { /* slot kinds unknowable without the class: the values leak rather than die by the wrong kind (defensive; the requester validated) */ *msg = "no such class"; return 0; } uint32_t g = slot_alloc(t); if (g == UINT32_MAX) { for (uint32_t j = 0; j < c->field_cnt; j++) db_val_free(c->kinds[j], slots[j]); if (err_kind) *err_kind = DB_ERR_OOM; *msg = "out of memory growing a table"; return 0; } db_row *r = slot_row(t, g); r->class_id = class_id; r->flags = 0; memcpy(r->slots, slots, (size_t)c->field_cnt * 8u); r->id = t->next_id; t->next_id += db->nshards; if (hput(t, r->id, (uint64_t)g + 1) != 0) { for (uint32_t j = 0; j < c->field_cnt; j++) db_val_free(c->kinds[j], r->slots[j]); t->next_id -= db->nshards; if (t->free_cnt < t->free_cap) t->free_slots[t->free_cnt++] = g; if (err_kind) *err_kind = DB_ERR_OOM; *msg = "out of memory indexing a row"; return 0; } t->bitmap[g >> 6] |= 1ull << (g & 63); t->count++; int irc = idx_add_row(db, t, r); if (irc != 0) { t->bitmap[g >> 6] &= ~(1ull << (g & 63)); hdel(t, r->id); t->count--; t->next_id -= db->nshards; /* the id was never observable: reclaim it */ for (uint32_t j = 0; j < c->field_cnt; j++) db_val_free(c->kinds[j], r->slots[j]); if (t->free_cnt < t->free_cap) t->free_slots[t->free_cnt++] = g; if (err_kind) *err_kind = irc; *msg = irc == DB_ERR_UNIQUE ? "unique index violation" : "out of memory indexing a row"; return 0; } if (err_kind) *err_kind = DB_ERR_NONE; return r->id; } db_row *wo_row_ptr(wo_db *db, uint32_t class_id, uint64_t id) { if (class_id >= db->class_cnt) return NULL; db_table *t = &db->tables[class_id]; if (!t->row_size) return NULL; uint64_t s1 = hget(t, id); if (!s1) return NULL; /* databasev2 2 (5d): on a keys-resident table the map value means one of * two things — a SLOT while the row is still in its slab (between the * insert and the post-barrier drop, which is when wo_wal_append_insert * legitimately calls this) and a LOG OFFSET afterwards. Nothing in the * value distinguishes them, so this function refuses to guess: an index * past the slabs, or one whose bitmap bit is clear, is an offset and the * row is not in RAM. Without this a caller that had not read 5d got * slot_row() applied to a byte offset — slot_row does no bounds check — * and a wild pointer that was then freed. Callers already handle NULL. */ if (wo_table_is_keys_resident(db, class_id)) { uint64_t g = s1 - 1; uint64_t total = (uint64_t)t->slab_cnt * DB_SLAB_ROWS; if (g >= total) return NULL; if (!(t->bitmap[g >> 6] & (1ull << (g & 63)))) return NULL; } return slot_row(t, (uint32_t)(s1 - 1)); } /* keys-resident fold: reads the row at [off] (the row's current record) and * folds it into [buf] (t->row_size bytes, caller-owned) — the piece * wo_row_borrow and a unique shadow-check's candidate probe both need, * factored out because they cannot share a buffer: wo_row_borrow writes into * t->scratch and holds it busy for the whole life of the borrow, so a * shadow-check that needs to look at OTHER rows of the SAME table while the * row under test is still borrowed must use a buffer of its own, never * t->scratch. [id] is checked against what the fold actually names, same as * wo_row_borrow always did. NULL on any failure, *msg set. * * table.h's opening doctrine: "the engine and the VM heap are two memory * worlds crossed only by copy... a row stores NO VM pointer." wo_wal_fold_row_at * hands back ENGINE-owned values (dec_val's representation, exactly what a * slab row's own slots hold, per wal.h) — those land straight in r->slots, * with no VM decode stage, so a keys-resident borrow matches wo_row_ptr's * contract exactly instead of a second, divergent one. Every existing * out-gate (wo_row_read, db.c's GET_FIELD/PROBE, idx_hash/idx_cols_equal/ * wo_idx_probe) already decodes engine->VM itself on the assumption that a * borrowed row is engine-encoded; a decode done AGAIN here used to hand them * a VM wo_str* reinterpreted as an engine db_text* — same bug either * direction, invisible for scalars (decode is identity there) and silent * wrong-bytes for Text/Bytes, which is exactly what stayed unexercised. */ static db_row *keys_fold_into(wo_db *db, uint32_t class_id, uint64_t id, uint64_t off, uint8_t *buf, const char **msg) { const wo_classdesc *c = &db->classes[class_id]; db_row *r = (db_row *)buf; uint32_t got_cid = 0; uint64_t got_id = 0; /* keys-resident delta updates, Task 2: the fold, not a single-record * read — a row's current offset may point at a delta, not a base row. * Folds straight into r->slots: field_cnt uint64_t slots is exactly * what out_vals expects, and what a db_row already provides. */ if (wo_wal_fold_row_at((wo_wal *)db->rt->wal, db, off, &got_cid, &got_id, r->slots, msg) != 0) return NULL; if (got_cid != class_id || got_id != id) { /* the offset pointed at someone else's record — a compaction that * moved records without rebuilding this map would land here, which is * exactly the obligation recorded at wo_wal_compact */ for (uint32_t i = 0; i < c->field_cnt; i++) wo_db_val_free(db, c->kinds[i], r->slots[i]); if (msg) *msg = "log offset does not hold the expected row"; return NULL; } r->id = id; r->class_id = class_id; r->flags = 0; return r; } db_row *wo_row_borrow(wo_db *db, uint32_t class_id, uint64_t id, const char **msg) { /* Fully-resident tables: exactly today's lookup, and releasing is a no-op. * The hot path pays one predicate. */ if (!wo_table_is_keys_resident(db, class_id)) return wo_row_ptr(db, class_id, id); /* Keys-resident: the id map holds the record's LOG OFFSET (off + 1), not a * slot, so the row is materialised into the table's scratch. */ db_table *t = &db->tables[class_id]; if (!t->row_size) return NULL; uint64_t o1 = hget(t, id); if (!o1) return NULL; if (!db->rt || !db->rt->wal) { /* a keys-resident table cannot exist without a log to read from; the * loader refuses the annotation outright, so this is a defensive arm */ if (msg) *msg = "resident: keys table without a write-ahead log"; return NULL; } if (t->scratch_busy) { /* One scratch per TABLE, so two live borrows on the same table would * hand back the same buffer. A unique shadow-check that needs OTHER * rows of this table while one is already borrowed uses its OWN * throwaway buffer (row_apply_field_keys), never this one — say so * rather than corrupting the first borrow silently. */ if (msg) *msg = "nested borrow on one table"; return NULL; } if (t->scratch_cap < t->row_size) { uint8_t *nb = realloc(t->scratch, t->row_size); if (!nb) { if (msg) *msg = "out of memory"; return NULL; } t->scratch = nb; t->scratch_cap = t->row_size; } db_row *r = keys_fold_into(db, class_id, id, o1 - 1, t->scratch, msg); if (!r) return NULL; t->scratch_busy = 1; return r; } void wo_row_release(wo_db *db, uint32_t class_id, db_row *r) { if (!r || class_id >= db->class_cnt) return; db_table *t = &db->tables[class_id]; if (!t->scratch_busy || (uint8_t *)r != t->scratch) return; /* slab-backed */ const wo_classdesc *c = &db->classes[class_id]; /* These are ENGINE values, exactly what a slab row holds (keys_fold_into's * contract) — freed the same way table_destroy frees a slab row's fields, * not through the runtime. */ for (uint32_t i = 0; i < c->field_cnt; i++) db_val_free(c->kinds[i], r->slots[i]); t->scratch_busy = 0; } int wo_row_read(wo_db *db, wo_rt *rt, uint32_t class_id, uint64_t id, uint64_t *out_vals, const char **msg) { db_row *r = wo_row_borrow(db, class_id, id, msg); if (!r) return -1; const wo_classdesc *c = &db->classes[class_id]; int ok = 1; for (uint32_t i = 0; i < c->field_cnt; i++) { /* decode out of the row BEFORE releasing: a keys-resident row's slots * point into the scratch that release frees */ out_vals[i] = db_val_decode(rt, c->kinds[i], r->slots[i], &ok, msg); if (!ok) { wo_row_release(db, class_id, r); return -2; } } wo_row_release(db, class_id, r); return 0; } db_row *wo_row_create_raw(wo_db *db, uint32_t class_id, uint64_t id) { db_table *t = table_of(db, class_id); if (!t || !id) return NULL; if (hget(t, id)) return NULL; /* duplicate id: corruption, not a tear */ uint32_t g = slot_alloc(t); if (g == UINT32_MAX) return NULL; db_row *r = slot_row(t, g); r->id = id; r->class_id = class_id; r->flags = 0; memset(r->slots, 0, t->row_size - sizeof(db_row)); if (hput(t, id, (uint64_t)g + 1) != 0) return NULL; t->bitmap[g >> 6] |= 1ull << (g & 63); t->count++; /* keep the interleave: only ids this shard owns move its counter */ if ((id - 1) % db->nshards == db->shard && id >= t->next_id) t->next_id = id + db->nshards; /* indexes: NOT here — the slots are still zero. wal.c fills them and then calls wo_row_raw_commit, which is where replayed rows re-index. */ return r; } int wo_row_raw_commit(wo_db *db, uint32_t class_id, db_row *r) { db_table *t = &db->tables[class_id]; return idx_add_row(db, t, r) == 0 ? 0 : -1; } void wo_db_val_free(wo_db *db, uint8_t kind, uint64_t v) { (void)db; db_val_free(kind, v); } uint64_t wo_db_val_encode(const wo_classdesc *classes, uint8_t kind, uint64_t vm_val, int *ok, const char **msg) { return db_val_encode(classes, kind, vm_val, ok, msg); } /* Deep engine-to-engine copy; shapes mirror db_val_free's recursion. */ uint64_t wo_db_val_clone(const wo_classdesc *classes, uint8_t kind, uint64_t v, int *ok) { *ok = 1; if (!v) return 0; switch (kind) { case WO_K_SCALAR: case WO_K_FLOAT: return v; case WO_K_TEXT: case WO_K_BYTES: { const db_text *s = (const db_text *)(uintptr_t)v; db_text *t = malloc(sizeof(db_text) + s->len); if (!t) goto oom; t->len = s->len; memcpy(t->bytes, s->bytes, s->len); return (uint64_t)(uintptr_t)t; } case WO_K_OWNED: { const db_rec *s = (const db_rec *)(uintptr_t)v; const wo_classdesc *c = &classes[s->class_id]; db_rec *r = malloc(sizeof(db_rec) + (size_t)c->field_cnt * 8u); if (!r) goto oom; r->class_id = s->class_id; r->_pad = 0; for (uint32_t i = 0; i < c->field_cnt; i++) { r->slots[i] = wo_db_val_clone(classes, c->kinds[i], s->slots[i], ok); if (!*ok) { for (uint32_t j = 0; j < i; j++) db_val_free(c->kinds[j], r->slots[j]); free(r); return 0; } } return (uint64_t)(uintptr_t)r; } case WO_K_MULTI: { const db_multi *s = (const db_multi *)(uintptr_t)v; db_multi *d = malloc(sizeof(db_multi) + (size_t)s->len * 8u); if (!d) goto oom; d->elem_kind = s->elem_kind; d->len = s->len; for (uint32_t i = 0; i < s->len; i++) { d->items[i] = wo_db_val_clone(classes, s->elem_kind, s->items[i], ok); if (!*ok) { for (uint32_t j = 0; j < i; j++) db_val_free(d->elem_kind, d->items[j]); free(d); return 0; } } return (uint64_t)(uintptr_t)d; } case WO_K_MAP: { const db_map *s = (const db_map *)(uintptr_t)v; db_map *d = malloc(sizeof(db_map) + (size_t)s->len * 16u); if (!d) goto oom; d->key_kind = s->key_kind; d->val_kind = s->val_kind; d->len = s->len; for (uint32_t i = 0; i < s->len; i++) { d->kv[2 * i] = wo_db_val_clone(classes, s->key_kind, s->kv[2 * i], ok); uint64_t dv = 0; if (*ok) dv = wo_db_val_clone(classes, s->val_kind, s->kv[2 * i + 1], ok); d->kv[2 * i + 1] = dv; if (!*ok) { for (uint32_t j = 0; j <= i; j++) { db_val_free(d->key_kind, d->kv[2 * j]); db_val_free(d->val_kind, d->kv[2 * j + 1]); } free(d); return 0; } } return (uint64_t)(uintptr_t)d; } default: return v; /* GCREF never stored; nothing to clone */ } oom: *ok = 0; return 0; } uint64_t wo_val_decode_vm(wo_db *db, wo_rt *rt, uint8_t kind, uint64_t engine_val, int *ok, const char **msg) { (void)db; return db_val_decode(rt, kind, engine_val, ok, msg); } static int row_apply_field_slot(wo_db *db, db_table *t, const wo_classdesc *c, db_row *r, uint32_t class_id, uint64_t id, uint32_t field, uint64_t nv, const char **msg, int *err_kind); static int row_apply_field_keys(wo_db *db, uint32_t class_id, uint64_t id, uint32_t field, uint64_t nv, const char **msg, int *err_kind); 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; /* databasev2 3 (keys-resident delta updates): a keys-resident row lives * in the LOG, so there is no slab slot to mutate — row_apply_field_keys * does read-modify-APPEND instead of a slot swap. wo_row_offset1 is the * cheap existence check wo_row_ptr would otherwise give us. */ if (wo_table_is_keys_resident(db, class_id)) { if (!wo_row_offset1(db, class_id, id)) { *msg = "no such row"; return -1; } const wo_classdesc *kc = &db->classes[class_id]; if (field >= kc->field_cnt) { *msg = "no such field"; return -1; } int kok = 1; uint64_t knv = db_val_encode(db->classes, kc->kinds[field], vm_val, &kok, msg); if (!kok) { if (err_kind) *err_kind = DB_ERR_BADKIND; return -1; } return row_apply_field_keys(db, class_id, id, field, knv, msg, err_kind); } 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; } return row_apply_field_slot(db, t, c, r, class_id, id, field, nv, msg, err_kind); } /* The post-encode half of an update: unique shadow-check, index fix-up, * slot swap. Consumes [nv] (installed on success, freed on failure) — * shared by the VM-value wrapper above and the RPC slot path. */ static int row_apply_field_slot(wo_db *db, db_table *t, const wo_classdesc *c, db_row *r, uint32_t class_id, uint64_t id, uint32_t field, uint64_t nv, const char **msg, int *err_kind) { /* 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; const char *bmsg = ""; db_row *other = wo_row_borrow(db, class_id, b->ids[i], &bmsg); int clash = other && idx_cols_equal(c, ix, r, other); wo_row_release(db, class_id, other); if (clash) { 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; } /* keys-resident counterpart of row_apply_field_slot. There is no slab slot * to swap — the row lives in the log — so the shape is read-modify-APPEND: * borrow (folds), append a delta with the row's current offset as the * back-pointer. [nv] is already engine-encoded (same convention as * row_apply_field_slot); consumed on every path. * * The borrow's materialised row holds ENGINE values now (keys_fold_into's * contract matches wo_row_ptr's), so [nv] lands in r->slots[field] directly — * no VM decode stage, same representation the WAL record and the index * functions already expect. * * Task 4 (keys-resident delta updates) ruling: this function stages the * delta but does NOT commit and does NOT move the id map — mirroring * insert, where table.c applies RAM and db.c owns staging/commit and the * post-barrier map move (wo_wal_pend_drop / wo_db_flush_drops for insert; * wo_wal_pend_repoint / wo_db_flush_drops for this). The caller re-points * using the offset it captured via wo_wal_next_offset() BEFORE calling in * here — insert's own `koff` pattern — since nothing between that capture * and the wo_wal_append_delta call below stages any other bytes on [w]. * * Ordering: the unique shadow-check (against a shadow of the row, mirroring * row_apply_field_slot's promise that a rejected update leaves the row * untouched) is the only SOFT-trap gate and runs first, before anything * moves. Once it passes, the index swap is RAM apply and happens * unconditionally, mirroring wo_row_insert's doctrine order (RAM, then * log) — from that point a failure to even STAGE the delta is fatal, * exactly like insert's own append, because RAM has already moved and * there is no undo. * * back_off checks a PENDING re-point first (wo_wal_repoint_offset1) before * falling back to the durable wo_row_offset1: a second update to this same * row, staged behind the same barrier as a first, must chain to the * first's delta — the id map won't move until the barrier, but the delta * itself is already staged and its offset already fixed. */ static int row_apply_field_keys(wo_db *db, uint32_t class_id, uint64_t id, uint32_t field, uint64_t nv, const char **msg, int *err_kind) { const wo_classdesc *c = &db->classes[class_id]; db_table *t = &db->tables[class_id]; const char *bmsg = "no such row"; db_row *r = wo_row_borrow(db, class_id, id, &bmsg); if (!r) { db_val_free(c->kinds[field], nv); *msg = bmsg; return -1; } /* successful borrow proves db->rt and db->rt->wal are both set */ wo_wal *w = (wo_wal *)db->rt->wal; uint64_t pending1 = wo_wal_repoint_offset1(w, class_id, id); uint64_t back_off = (pending1 ? pending1 : wo_row_offset1(db, class_id, id)) - 1; /* unique shadow-check: run with the NEW value before anything durable or indexed moves, exactly row_apply_field_slot's promise. CRITICAL: candidates are probed into a THROWAWAY buffer, never t->scratch. r (the row under update) already lives in t->scratch and wo_row_borrow refuses ANY nested borrow on the same table's scratch — reusing it here would make every candidate probe return NULL, so a clash could never be detected (a silent hole: keys-resident @unique would accept duplicates). Candidates are always in this same, keys-resident table, so keys_fold_into (bypassing wo_row_borrow and its scratch_busy gate) is safe to call directly. */ uint64_t old_eng = r->slots[field]; r->slots[field] = nv; uint8_t *cand_buf = NULL; 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; if (!cand_buf) { cand_buf = malloc(t->row_size); if (!cand_buf) { r->slots[field] = old_eng; wo_row_release(db, class_id, r); db_val_free(c->kinds[field], nv); if (err_kind) *err_kind = DB_ERR_OOM; *msg = "out of memory"; return -1; } } for (uint32_t i = 0; i < b->len; i++) { if (b->ids[i] == id) continue; /* Task 4 follow-up (review finding): a candidate updated earlier in this SAME, not-yet-committed drain has its re-point only PENDING — the durable wo_row_offset1 would still fold its PRE-update value, letting a real unique clash through uncaught. Unlike back_off (a pure number), keys_fold_into DOES need to read this record's bytes to compare values — which is why wo_wal_fold_row_at now reads the staging buffer for an offset in the not-yet-durable range (see scan_record_staged in wal.c); a plain wo_row_offset1 substitution here is not enough on its own. */ uint64_t cand_off1 = wo_wal_repoint_offset1(w, class_id, b->ids[i]); if (!cand_off1) cand_off1 = wo_row_offset1(db, class_id, b->ids[i]); if (!cand_off1) continue; /* stale bucket entry: no row, no clash */ const char *obmsg = ""; db_row *other = keys_fold_into(db, class_id, b->ids[i], cand_off1 - 1, cand_buf, &obmsg); int clash = other && idx_cols_equal(c, ix, r, other); /* keys_fold_into decoded fresh ENGINE values for EVERY field, same as a real borrow — nobody else owns them, so drop them here the same way wo_row_release would */ if (other) for (uint32_t k = 0; k < c->field_cnt; k++) db_val_free(c->kinds[k], other->slots[k]); if (clash) { r->slots[field] = old_eng; /* untouched, promised */ free(cand_buf); wo_row_release(db, class_id, r); db_val_free(c->kinds[field], nv); if (err_kind) *err_kind = DB_ERR_UNIQUE; *msg = "unique index violation"; return -1; } } } free(cand_buf); r->slots[field] = old_eng; /* restored: still the OLD row until applied */ /* RAM apply (Task 4 ruling): the borrowed row is the OLD row — out of every index under the OLD value, then in again under the NEW one. Unconditional from here: a failure below is fatal, not a trap. */ idx_remove_row(db, t, r); r->slots[field] = nv; (void)idx_add_row(db, t, r); /* cannot violate uniqueness: the shadow check above already cleared it */ if (wo_wal_append_delta(w, db, class_id, id, field, back_off, nv) != 0) wo_wal_stage_fatal(w); /* RAM already moved; see the insert arm */ db_val_free(c->kinds[field], old_eng); /* old value done: r now holds nv */ wo_row_release(db, class_id, r); /* frees r's slots, including nv, as engine values */ if (err_kind) *err_kind = DB_ERR_NONE; return 0; } int wo_row_update_field_slot(wo_db *db, uint32_t class_id, uint64_t id, uint32_t field, uint64_t slot, const char **msg, int *err_kind) { if (err_kind) *err_kind = DB_ERR_MISC; /* bounds first: the RPC requester validated cid/field to encode at all, so these are defensive; the slot's kind is unknowable on a class violation and the value leaks rather than dies by the wrong kind */ if (class_id >= db->class_cnt) { *msg = "no such class"; return -1; } const wo_classdesc *c = &db->classes[class_id]; if (field >= c->field_cnt) { *msg = "no such field"; return -1; } /* databasev2 3 (keys-resident delta updates): a keys-resident row has no * slab slot to mutate — row_apply_field_keys does read-modify-APPEND. */ if (wo_table_is_keys_resident(db, class_id)) return row_apply_field_keys(db, class_id, id, field, slot, msg, err_kind); db_row *r = wo_row_ptr(db, class_id, id); if (!r) { db_val_free(c->kinds[field], slot); *msg = "no such row"; return -1; } return row_apply_field_slot(db, &db->tables[class_id], c, r, class_id, id, field, slot, msg, err_kind); } int wo_row_has_referrers(wo_db *db, uint32_t class_id, uint64_t id) { if (!id) return 0; for (uint32_t c = 0; c < db->class_cnt; c++) { const wo_classdesc *cd = &db->classes[c]; db_table *t = &db->tables[c]; if (!t->row_size || !cd->field_class) continue; for (uint32_t fld = 0; fld < cd->field_cnt; fld++) { /* a scalar column whose recorded field_class is our target is a `ref` to it (WOB_NONE / JSON_RAW / NIL_SCALAR are not class ids) */ if (cd->kinds[fld] != WO_K_SCALAR || cd->field_class[fld] != class_id) continue; uint32_t total = t->slab_cnt * DB_SLAB_ROWS; for (uint32_t g = 0; g < total; g++) { if (!(t->bitmap[g >> 6] & (1ull << (g & 63)))) continue; db_row *r = (db_row *)(t->slabs[g / DB_SLAB_ROWS] + (size_t)(g % DB_SLAB_ROWS) * t->row_size); if (r->slots[fld] == id) return 1; } } } return 0; } int wo_row_next_id(const wo_db *db, uint32_t class_id, size_t *cursor, uint64_t *id_out) { if (class_id >= db->class_cnt) return 0; const db_table *t = &db->tables[class_id]; if (!t->row_size) return 0; if (wo_table_is_keys_resident(db, class_id)) { /* the id map IS the live set here: hkeys non-zero, hvals holding an * offset + 1 */ for (size_t j = *cursor; j < t->hcap; j++) { if (t->hkeys[j] && t->hvals[j]) { *id_out = t->hkeys[j]; *cursor = j + 1; return 1; } } *cursor = t->hcap; return 0; } { /* resident: the bitmap, in slab order, exactly as before */ uint32_t total = t->slab_cnt * DB_SLAB_ROWS; for (size_t g = *cursor; g < total; g++) { if (!(t->bitmap[g >> 6] & (1ull << (g & 63)))) continue; *id_out = slot_row((db_table *)t, (uint32_t)g)->id; *cursor = g + 1; return 1; } *cursor = total; return 0; } } int wo_table_is_keys_resident(const wo_db *db, uint32_t class_id) { if (class_id >= db->class_cnt) return 0; return (db->classes[class_id].flags & WO_CLASSF_RESIDENT_KEYS) != 0u; } int wo_row_drop_payload(wo_db *db, uint32_t class_id, uint64_t id, uint64_t wal_off) { if (class_id >= db->class_cnt) return -1; db_table *t = &db->tables[class_id]; if (!t->row_size) return -1; uint64_t s1 = hget(t, id); if (!s1) return -1; uint32_t g = (uint32_t)(s1 - 1); db_row *r = slot_row(t, g); /* the values are engine-owned; the log holds their bytes now */ const wo_classdesc *c = &db->classes[class_id]; for (uint32_t i = 0; i < c->field_cnt; i++) db_val_free(c->kinds[i], r->slots[i]); t->bitmap[g >> 6] &= ~(1ull << (g & 63)); /* the id STAYS, now pointing at the log rather than at a slab. No * idx_remove_row and no count change: the row is live, only its backing * moved. */ if (hput(t, id, wal_off + 1) != 0) return -1; if (t->free_cnt == t->free_cap) { uint32_t ncap = t->free_cap ? t->free_cap * 2 : 16; uint32_t *nf = realloc(t->free_slots, (size_t)ncap * 4); if (!nf) return 0; /* slot simply not recycled; the bitmap still frees it */ t->free_slots = nf; t->free_cap = ncap; } t->free_slots[t->free_cnt++] = g; 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]; if (!t->row_size) return -1; uint64_t s1 = hget(t, id); if (!s1) return -1; /* databasev2 2 (5d): a keys-resident row's map entry is a LOG OFFSET, not * a slot. Falling through to the slab path below would index t->slabs[] * with a byte offset — slot_row does no bounds check — and then free * whatever it landed on. That is memory corruption, not a missing feature, * which is why the loader still refuses the annotation. * * The row has no slab slot, no bitmap bit and no free-list entry to give * back; only the indexes and the id map know about it. The index hook * needs the row's column VALUES to find its bucket, and those live in the * log, so the row is borrowed for exactly as long as that takes. */ if (wo_table_is_keys_resident(db, class_id)) { db_row *r = wo_row_borrow(db, class_id, id, NULL); if (!r) return -1; idx_remove_row(db, t, r); wo_row_release(db, class_id, r); /* frees the materialised values */ hdel(t, id); t->count--; return 0; } uint32_t g = (uint32_t)(s1 - 1); db_row *r = slot_row(t, g); /* the index hook's remove side: before the row's values die, while the columns are still comparable */ idx_remove_row(db, t, r); const wo_classdesc *c = &db->classes[class_id]; for (uint32_t i = 0; i < c->field_cnt; i++) db_val_free(c->kinds[i], r->slots[i]); t->bitmap[g >> 6] &= ~(1ull << (g & 63)); hdel(t, id); t->count--; if (t->free_cnt == t->free_cap) { uint32_t ncap = t->free_cap ? t->free_cap * 2 : 16; uint32_t *nf = realloc(t->free_slots, (size_t)ncap * 4); if (!nf) return 0; /* slot simply not recycled; bitmap still frees it */ t->free_slots = nf; t->free_cap = ncap; } t->free_slots[t->free_cnt++] = g; return 0; } /* databasev2 2 (5d): re-point a keys-resident row at a NEW log offset. * * Deliberately not hput(): hput runs the load-factor check and can rehash, * which would reorder hkeys/hvals underneath a wo_row_next_id cursor. This * only ever overwrites the value of a key that already exists, so the table's * shape cannot change and a walk in progress stays valid. That property is * what lets compaction re-point rows as it writes them instead of buffering * one (cid, id, offset) triple per live row. Returns -1 if the id is absent. */ int wo_row_set_offset(wo_db *db, uint32_t class_id, uint64_t id, uint64_t wal_off) { if (class_id >= db->class_cnt) return -1; db_table *t = &db->tables[class_id]; if (!t->hcap) return -1; size_t j = hmix(id) & (t->hcap - 1); while (t->hkeys[j]) { if (t->hkeys[j] == id) { t->hvals[j] = wal_off + 1; return 0; } j = (j + 1) & (t->hcap - 1); } return -1; } /* databasev2 2 (5d): the log offset a keys-resident row currently reads from, * as stored (off + 1), so 0 means "no such row". Compaction needs the raw * offset to copy the record without materialising it. */ uint64_t wo_row_offset1(const wo_db *db, uint32_t class_id, uint64_t id) { if (class_id >= db->class_cnt) return 0; const db_table *t = &db->tables[class_id]; if (!t->hcap) return 0; return hget(t, id); }