writeonce/database/src/table.h
shoney.arickathil 65e5739492 feat: FK restrict on delete + employee sample runs; group-by parked (9b)
- FK restrict: deleting a row a non-nullable `ref` still points at traps
  WO_T_FK (11), catchable. The compiler now records a `ref` field's
  target class in the class-table field_class metadata; the engine
  (wo_row_has_referrers) scans referencing scalar columns before a
  delete. Correctness-first full scan; the backlink-index optimization
  is recorded for later
- docs/examples/employee now COMPILES AND RUNS all six modes against a
  WAL-durable database: seed (+@unique trap across restart), report
  (per-dept aggregates + payroll), staff (unique probe + backlink +
  ref nav), raise (update-through-row), drop (FK restrict), and
  persistence via replay
- group-by SYNTAX parked to a future iteration (user decision): the
  report mode is hand-rolled from the shipped primitives meanwhile
  (same numbers). "table relations and FK" is complete
- scripts/employee-accept.sh (8 checks) + a `just employee` module;
  manifest parser tolerates iteration 9c's [share]/[[share.clients]]
  sections so `woc .` builds the sample on this branch
- fixtures trap/db-fk-restrict (code 11) + run/db-fk-restrict-catch;
  oop-e2e 79/0, woc-test 566/0, 15 runtime suites, log-watcher 7/0,
  employee-accept 8/0
- 9b story + status board updated

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 18:37:23 +02:00

196 lines
8.3 KiB
C

/* table.h — class-shaped row storage (iteration 9, Task 1).
*
* The engine and the VM heap are two memory worlds crossed only by copy
* (the 9b design's section 6): a row stores NO VM pointer. Every field
* lands in one 8-byte slot, kind-driven:
*
* SCALAR the 8 bytes themselves (WO_NIL_SCALAR spells a ?scalar's nil)
* TEXT engine-owned db_text* (0 = nil)
* OWNED engine-owned db_rec* — the object flattened by value,
* recursively, through these same rules (0 = nil)
* MULTI engine-owned db_multi* — elements encoded element-wise
* MAP engine-owned db_map* — keys and values encoded pair-wise
* GCREF never stored: the compiler rejects it (the GC bulkhead);
* the engine refuses it defensively as an encode error
*
* `ref T` is a SCALAR at this layer — the target row's id, an ordinary
* number the compiler produced; the engine learns nothing about it until
* the FK checks (9b plan, Task 3).
*
* Row layout: a 16-byte header (id, class, flags) then field_cnt 8-byte
* slots — deliberately the VM object layout's shape, so encode/decode walk
* the same class-table kinds the VM walks. Rows live in per-class SLABS
* (fixed-count, malloc'd, never moved: a row's address is stable for its
* lifetime, which is what lets 9b hand out loop-scoped row views). A
* per-table bitmap tracks occupancy; removed slots go on a free list and
* are reused before any slab grows. The id->row map is an open-addressing
* hash owned by the table.
*
* Id discipline (the c-runtime plan's shipped behavior): per table, per
* shard, ids interleave — shard S of N allocates S+1, S+1+N, S+1+2N, … —
* so creation is coordination-free and a row's owner shard is (id-1) % N.
* Milestone runs at N=1 (iteration 8 not yet landed); everything here is
* N-parametric and degenerates cleanly.
*
* CHOKE POINT DOCTRINE: wo_row_insert / wo_row_remove are the only paths
* that touch storage. Task 4's secondary indexes hook exactly these two
* functions; anything else mutating a slab is a defect by definition.
*/
#ifndef WO_TABLE_H
#define WO_TABLE_H
#include "obj.h" /* wo_rt, wo_classdesc, kinds, wo_str, containers */
/* ---- engine-owned value shapes (all malloc'd, all reachable only from
* row slots, all freed through db_val_free) ---- */
typedef struct db_text {
uint32_t len;
char bytes[]; /* len bytes, no NUL */
} db_text;
typedef struct db_rec { /* an owned object flattened by value */
uint32_t class_id; /* index into the SAME class table the VM uses */
uint32_t _pad;
uint64_t slots[]; /* field_cnt slots, encoded by these rules */
} db_rec;
typedef struct db_multi {
uint8_t elem_kind;
uint32_t len;
uint64_t items[];
} db_multi;
typedef struct db_map {
uint8_t key_kind, val_kind;
uint32_t len;
uint64_t kv[]; /* len pairs: k0 v0 k1 v1 … */
} db_map;
/* ---- rows and tables ---- */
typedef struct db_row {
uint64_t id;
uint32_t class_id;
uint32_t flags; /* reserved (0) */
uint64_t slots[];
} db_row;
#define DB_SLAB_ROWS 256u
/* Secondary index (iteration 9, Task 4): built from the class table's v3
* metadata at first touch, maintained ONLY inside the row choke points.
* Hash multimap: bucket per column-value hash, ids within; equality is
* re-checked against the actual rows on the unique path (a hash is a hint,
* never an answer). */
typedef struct db_ibucket {
uint64_t hash;
uint64_t *ids;
uint32_t len, cap;
} db_ibucket;
typedef struct db_index {
uint32_t flags; /* bit0 = unique */
uint32_t col_cnt;
const uint32_t *cols; /* into the loader's idx pool */
db_ibucket *buckets; /* open addressing by hash; hash==0 stored as 1 */
size_t bcap, blen;
} db_index;
/* wo_row_insert failure classes — *msg carries the sentence, this carries
* the machine-readable kind so db.c maps to the right trap. */
enum { DB_ERR_NONE = 0, DB_ERR_OOM = 1, DB_ERR_BADKIND = 2, DB_ERR_UNIQUE = 3, DB_ERR_MISC = 4 };
typedef struct db_table {
uint32_t class_id;
size_t row_size; /* 16 + field_cnt * 8 */
/* slabs of DB_SLAB_ROWS rows each; addresses stable forever */
uint8_t **slabs;
uint32_t slab_cnt, slab_cap;
uint64_t *bitmap; /* one bit per slot, slab-major */
/* removed slots, reused LIFO before any slab grows */
uint32_t *free_slots;
uint32_t free_cnt, free_cap;
uint64_t next_id; /* next id THIS shard hands out for this table */
uint64_t count; /* live rows */
/* id -> (global slot + 1); 0 = empty. Open addressing, pow2. */
uint64_t *hkeys;
uint64_t *hvals;
size_t hcap, hlen;
/* secondary indexes, from the class table's v3 metadata */
db_index *indexes;
uint32_t index_cnt;
} db_table;
typedef struct wo_db {
const wo_classdesc *classes;
uint32_t class_cnt;
uint32_t shard, nshards; /* S of N; ids interleave S+1, S+1+N, … */
db_table *tables; /* class_cnt entries, created lazily on first insert */
} wo_db;
/* 0 ok, -1 alloc failure. nshards >= 1, shard < nshards. */
int wo_db_init(wo_db *db, const wo_classdesc *classes, uint32_t class_cnt,
uint32_t shard, uint32_t nshards);
void wo_db_destroy(wo_db *db);
/* Insert: encode field_cnt VM values (register words, kinds from the class
* table) into a fresh row. Returns the new id, or 0 with *msg set (OOM, or
* a GCREF field — which the compiler should have refused upstream). */
uint64_t wo_row_insert(wo_db *db, uint32_t class_id, const uint64_t *vals,
const char **msg, int *err_kind);
/* Read: decode the row's fields into VM values freshly allocated from
* [rt] — always copies, never a pointer into the slab (the out-gate).
* 0 ok, -1 no such row, -2 OOM (*msg set). */
int wo_row_read(wo_db *db, wo_rt *rt, uint32_t class_id, uint64_t id,
uint64_t *out_vals, const char **msg);
/* Remove: free the row's engine-owned field values, clear the slot, recycle
* it. 0 ok, -1 no such row. */
int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id);
/* iteration 9b FK restrict: 1 if some row in some class holds a non-nullable
* `ref` to [class_id] equal to [id] — i.e. deleting this row would dangle a
* reference. The compiler records a ref field's target class in the class
* table's field_class metadata; this scans those columns. Correctness-first
* (a full scan of referencing tables); the backlink index is the later
* optimization the spec records. */
int wo_row_has_referrers(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. */
db_row *wo_row_ptr(wo_db *db, uint32_t class_id, uint64_t id);
/* Engine-internal, for WAL replay only: create a row with a FIXED id,
* slots zeroed — the caller (wal.c) fills them with engine-encoded values
* it built while decoding. Advances the table's next_id past [id] when the
* id belongs to this shard, so post-replay inserts never collide. NULL =
* OOM or duplicate id (corruption beyond a torn tail). */
db_row *wo_row_create_raw(wo_db *db, uint32_t class_id, uint64_t id);
/* Engine-internal: free one engine-encoded slot value of [kind] (wal.c's
* decode error paths). */
void wo_db_val_free(wo_db *db, uint8_t kind, uint64_t v);
/* Decode one engine slot value to a FRESH VM value in [rt] (the out-gate:
* always a copy). The query builtins' field reads go through this. */
uint64_t wo_val_decode_vm(wo_db *db, wo_rt *rt, uint8_t kind, uint64_t engine_val,
int *ok, const char **msg);
/* Engine-internal, replay only: after wal.c fills a raw row's slots, this
* runs the index maintenance the normal insert runs inline — including the
* unique check, whose violation during replay is corruption, not data
* (0 ok, -1). */
int wo_row_raw_commit(wo_db *db, uint32_t class_id, db_row *r);
#endif /* WO_TABLE_H */