feat(database): query-read engine builtins (iteration 9b foundation)

- DB_SCAN(64): class -> multi<Int> of every row id, materialized up
  front (the 9b cursor-stability rule: the loop body point-reads, so a
  row updated mid-loop cannot disturb iteration)
- DB_GET_FIELD(65): class,id,field -> the field decoded to a fresh VM
  value (the out-gate copy); a table-class value IS its row id at
  runtime, so this is how a compiled query reads a column, and a ref
  field decodes to the target id for navigation
- DB_PROBE(66): class,index,key -> multi<Int> of ids whose first
  indexed column equals key (backlink + indexed where)
- wo_val_decode_vm wrapper exposed; dispatch range 61..66, loader
  arities, runner mirror updated
- 15 runtime suites green, oop-e2e 73/0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-15 22:19:13 +02:00
parent 71d3985e81
commit 048c242a01
7 changed files with 117 additions and 3 deletions

View file

@ -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 || c > 63) then
if c > 12 && (c < 61 || c > 66) 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)
@ -2623,6 +2623,9 @@ let validate_image (img : string) : string list =
| 61 -> 1
| 62 -> 4
| 63 -> 2
| 64 -> 1
| 65 -> 3
| 66 -> 3
| _ -> 0
in
if arity > 0 then begin

View file

@ -1,5 +1,8 @@
#include "db.h"
#include <string.h>
#include "cont.h"
#include "table.h"
#include "wal.h"
@ -68,6 +71,85 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
R[A] = 0;
return 0;
}
case WO_B_DB_SCAN: {
uint32_t cid = (uint32_t)R[B];
if (cid >= db->class_cnt) {
*msg = "no such class";
return WO_T_DB;
}
wo_multi *ids = wo_multi_new(&vm->rt, WO_K_SCALAR);
if (!ids) return WO_T_OOM;
/* materialize the id list up front — the 9b cursor-stability rule:
* the loop body then point-reads each id, so a row updated mid-loop
* (even an indexed column) cannot disturb the iteration */
db_table *t = &db->tables[cid];
if (t->row_size) {
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 *row =
(db_row *)(t->slabs[g / DB_SLAB_ROWS] + (size_t)(g % DB_SLAB_ROWS) * t->row_size);
if (wo_multi_push(ids, row->id) != 0) return WO_T_OOM;
}
}
R[A] = (uint64_t)(uintptr_t)ids;
return 0;
}
case WO_B_DB_GET_FIELD: {
uint32_t cid = (uint32_t)R[B];
uint64_t id = R[B + 1];
uint32_t field = (uint32_t)R[B + 2];
if (cid >= db->class_cnt || field >= db->classes[cid].field_cnt) {
*msg = "no such field";
return WO_T_DB;
}
db_row *row = wo_row_ptr(db, cid, id);
if (!row) {
*msg = "no such row";
return WO_T_DB;
}
int ok = 1;
uint64_t v = wo_val_decode_vm(db, &vm->rt, db->classes[cid].kinds[field],
row->slots[field], &ok, msg);
if (!ok) return WO_T_OOM;
R[A] = v;
return 0;
}
case WO_B_DB_PROBE: {
uint32_t cid = (uint32_t)R[B];
uint32_t index = (uint32_t)R[B + 1];
if (cid >= db->class_cnt) {
*msg = "no such class";
return WO_T_DB;
}
wo_multi *ids = wo_multi_new(&vm->rt, WO_K_SCALAR);
if (!ids) return WO_T_OOM;
db_table *t = &db->tables[cid];
if (t->row_size && index < t->index_cnt) {
db_index *ix = &t->indexes[index];
uint32_t col = ix->cols[0];
uint8_t kind = db->classes[cid].kinds[col];
uint64_t key = R[B + 2];
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 *row =
(db_row *)(t->slabs[g / DB_SLAB_ROWS] + (size_t)(g % DB_SLAB_ROWS) * t->row_size);
int eq;
if (kind == WO_K_TEXT) {
const wo_str *want = (const wo_str *)(uintptr_t)key;
const db_text *have = (const db_text *)(uintptr_t)row->slots[col];
eq = (!want && !have) ||
(want && have && want->len == have->len &&
memcmp(want->data, have->bytes, have->len) == 0);
} else
eq = row->slots[col] == key;
if (eq && wo_multi_push(ids, row->id) != 0) return WO_T_OOM;
}
}
R[A] = (uint64_t)(uintptr_t)ids;
return 0;
}
default:
*msg = "unknown db builtin";
return WO_T_DB;

View file

@ -610,6 +610,12 @@ void wo_db_val_free(wo_db *db, uint8_t kind, uint64_t v) {
db_val_free(kind, v);
}
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);
}
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;

View file

@ -174,6 +174,11 @@ db_row *wo_row_create_raw(wo_db *db, uint32_t class_id, uint64_t id);
* 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

View file

@ -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 && C <= WO_B_DB_DELETE) return wo_builtin_db(vm, R, ins, msg);
if (C >= WO_B_DB_INSERT && C <= WO_B_DB_PROBE) return wo_builtin_db(vm, R, ins, msg);
switch (C) {
case WO_B_NOW: { /* wall-clock milliseconds */
struct timespec ts;

View file

@ -44,6 +44,9 @@ static const uint8_t b_arity[WO_B_MAX + 1] = {
[WO_B_DB_INSERT] = 1,
[WO_B_DB_UPDATE_FIELD] = 4,
[WO_B_DB_DELETE] = 2,
[WO_B_DB_SCAN] = 1,
[WO_B_DB_GET_FIELD] = 3,
[WO_B_DB_PROBE] = 3,
[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,

View file

@ -302,8 +302,23 @@ enum {
/* 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,
/* the query surface's reads (iteration 9b). A table-class value IS its
* row id at runtime (the "objects are rows" model), so these are how the
* compiled query loop touches storage:
* DB_SCAN (64): R[B] = class -> R[A] = multi<Int> of every id
* DB_GET_FIELD(65): R[B]=class, R[B+1]=id, R[B+2]=field
* -> R[A] = that field, decoded to a VM value (a
* Text field decodes to a fresh Text; a ref field
* decodes to the target id). Missing row traps
* WO_T_DB.
* DB_PROBE (66): R[B]=class, R[B+1]=index, R[B+2]=key
* -> R[A] = multi<Int> of ids whose first indexed
* column equals key (backlink + indexed where). */
WO_B_DB_SCAN = 64,
WO_B_DB_GET_FIELD = 65,
WO_B_DB_PROBE = 66,
};
#define WO_B_MAX 63u
#define WO_B_MAX 66u
/* ids at or above this one live in sysio.c, not builtin.c */
#define WO_B_SYS_FIRST WO_B_FS_EXISTS