feat: secondary indexes + @unique trap (iteration 9, Task 4; wob v3)
- .wob v3: class records carry an index tail (flags bit0 = unique, col_cnt, columns) -- @table(index:[a,b]) entries plus one unique single-column entry per @unique field; loader validates columns in range and scalar/Text-kinded; emitter validates the declarations (unknown column, un-indexable kind => diagnostic) - engine: db_index hash multimap per table, built from the class table at first touch, maintained ONLY inside wo_row_insert/ wo_row_remove; unique checks re-compare actual column values (a hash is a hint); replay re-indexes via wo_row_raw_commit AFTER slots are filled, so recovered tables carry their indexes - WO_T_UNIQUE = 10; a violating insert is un-applied whole (bitmap, hash, count, and the never-observable id reclaimed) and traps catchably -- the employee SEED-DUP pattern - wo_row_insert gains err_kind so db.c maps UNIQUE/OOM/other to the right trap; test images and the runner's loader mirror speak v3 - fixtures: trap/db-unique-violation (code 10 exact) and run/db-unique-catch (catchable dup, composite index accepts duplicates, next id dense after a refusal) - gates: oop-e2e 73/0, all 15 runtime suites, woc-test green, log-watcher 7/0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2e1041daed
commit
6f2f9b6f0a
17 changed files with 441 additions and 42 deletions
|
|
@ -164,7 +164,7 @@ let dump (img : string) : string =
|
|||
let line fmt = Buffer.add_string out (fmt ^ "\n") in
|
||||
if u32 img 0 <> magic then raise (Bad "bad magic");
|
||||
let ver = u32 img 4 in
|
||||
if ver <> 2 then raise (Bad (Printf.sprintf "unsupported version %d" ver));
|
||||
if ver <> 3 then raise (Bad (Printf.sprintf "unsupported version %d" ver));
|
||||
let coff = u32 img 8 and ccnt = u32 img 12 in
|
||||
let koff = u32 img 16 and kcnt = u32 img 20 in
|
||||
let ioff = u32 img 24 and icnt = u32 img 28 in
|
||||
|
|
@ -213,6 +213,14 @@ let dump (img : string) : string =
|
|||
renders as keys, so a wrong one is worth seeing. *)
|
||||
let names = List.init fcnt (fun j -> u32 img (!o + (j * 4))) in
|
||||
o := !o + (fcnt * 12);
|
||||
(* v3 index tail: walk past (the disassembly prints class shape, not
|
||||
indexes — dump goldens stay byte-stable across the version bump) *)
|
||||
let icnt = u32 img !o in
|
||||
o := !o + 4;
|
||||
for _ = 1 to icnt do
|
||||
let ccnt = u32 img (!o + 4) in
|
||||
o := !o + 8 + (ccnt * 4)
|
||||
done;
|
||||
let fields =
|
||||
List.map2
|
||||
(fun nmk k -> if nmk = 0xFFFFFFFF then k else Printf.sprintf "%s:%s" (kname nmk) k)
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ let stdlib_not_linked_code = Diag.emitter_prefix ^ "06"
|
|||
============================================================ *)
|
||||
|
||||
let wob_magic = 0x31424F57 (* "WOB1" read as an LE u32 *)
|
||||
let wob_version = 2 (* v2: per-field class-table metadata *)
|
||||
let wob_version = 3 (* v3: v2 + per-class secondary-index metadata *)
|
||||
let wob_hdr_size = 44
|
||||
let wob_none = 0xFFFFFFFF
|
||||
let k_int = 0
|
||||
|
|
@ -349,6 +349,11 @@ type clsrec = {
|
|||
cr_gc : bool;
|
||||
cr_fields : (string * Ast.field_ty) array;
|
||||
cr_methods : string list; (* method names, declaration order *)
|
||||
(* iteration 9 Task 4: (unique, column indices) per secondary index —
|
||||
`@table(index: [a, b])` entries (non-unique, composite) plus one
|
||||
unique single-column entry per `@unique` field. Serialized as the v3
|
||||
class-record tail; the engine builds its runtime indexes from this. *)
|
||||
cr_indexes : (bool * int array) list;
|
||||
}
|
||||
|
||||
type ifacerec = {
|
||||
|
|
@ -3923,6 +3928,18 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
|||
~(module_syms : (string, Types.symbols) Hashtbl.t) (coll : Diag.Collector.t) (units : input list) :
|
||||
string =
|
||||
let colliding = compute_colliding_fn_names ~module_of units in
|
||||
(* iteration 9 Task 4: index-declaration problems found while building
|
||||
clsrecs — reported once a file/pos-bearing context exists below *)
|
||||
let index_col_err : (Ast.pos * string) option ref = ref None in
|
||||
let index_err_file = ref "" in
|
||||
let ref_index_of_name (fnames : string list) (n : string) : int =
|
||||
let rec go i = function
|
||||
| [] -> 0 (* unknown column: the caller records the diagnostic *)
|
||||
| x :: tl -> if x = n then i else go (i + 1) tl
|
||||
in
|
||||
go 0 fnames
|
||||
in
|
||||
let p_syms_for_indexes = syms in
|
||||
(* ---- pass 1: declarations, in discovery then declaration order ---- *)
|
||||
let classes = ref [] and class_id = ref SM.empty and nclasses = ref 0 in
|
||||
let ifaces = ref [] and iface_id = ref SM.empty and nifaces = ref 0 and nslots = ref 0 in
|
||||
|
|
@ -3961,6 +3978,7 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
|||
(function
|
||||
| Ast.Class (c : Ast.class_decl) ->
|
||||
if not (SM.mem c.name !class_id) then begin
|
||||
(if !index_col_err = None then index_err_file := u.file);
|
||||
let shape = if c.is_record then Some (record_shape_key c) else None in
|
||||
let alias_of =
|
||||
match shape with Some key -> Hashtbl.find_opt record_shape key | None -> None
|
||||
|
|
@ -3978,10 +3996,58 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
|||
| Some key -> Hashtbl.replace record_shape key cid
|
||||
| None -> ());
|
||||
classes :=
|
||||
{ cr_name = c.name; cr_gc = c.is_gc;
|
||||
cr_fields =
|
||||
Array.of_list (List.map (fun (fl : Ast.field) -> (fl.name, fl.ty)) c.fields);
|
||||
cr_methods = List.map (fun (m : Ast.method_decl) -> m.name) c.methods }
|
||||
(let fnames = List.map (fun (fl : Ast.field) -> fl.Ast.name) c.fields in
|
||||
let col_of n = ref_index_of_name fnames n in
|
||||
let is_indexable (fl : Ast.field) =
|
||||
match Types.wob_kind_of_typ p_syms_for_indexes (Types.typ_of_field_ty (unwrap fl.Ast.ty)) with
|
||||
| Types.WO_K_SCALAR | Types.WO_K_TEXT -> true
|
||||
| _ -> false
|
||||
in
|
||||
let table_indexes =
|
||||
match c.Ast.table with
|
||||
| None -> []
|
||||
| Some cfg ->
|
||||
List.map
|
||||
(fun cols -> (false, Array.of_list (List.map col_of cols)))
|
||||
cfg.Ast.indexes
|
||||
in
|
||||
let unique_indexes =
|
||||
List.concat_map
|
||||
(fun (fl : Ast.field) ->
|
||||
if List.mem "unique" fl.Ast.annotations then begin
|
||||
if not (is_indexable fl) then
|
||||
index_col_err := Some (c.Ast.pos, Printf.sprintf
|
||||
"`@unique` on `%s.%s`: only scalar and Text fields can be indexed"
|
||||
c.Ast.name fl.Ast.name);
|
||||
[ (true, [| col_of fl.Ast.name |]) ]
|
||||
end
|
||||
else [])
|
||||
c.fields
|
||||
in
|
||||
(match c.Ast.table with
|
||||
| Some cfg ->
|
||||
List.iter
|
||||
(fun cols ->
|
||||
List.iter
|
||||
(fun cn ->
|
||||
match List.find_opt (fun (fl : Ast.field) -> fl.Ast.name = cn) c.fields with
|
||||
| None ->
|
||||
index_col_err := Some (c.Ast.pos, Printf.sprintf
|
||||
"`@table(index: ...)` on `%s` names `%s`, which is not a field"
|
||||
c.Ast.name cn)
|
||||
| Some fl ->
|
||||
if not (is_indexable fl) then
|
||||
index_col_err := Some (c.Ast.pos, Printf.sprintf
|
||||
"`@table(index: ...)` on `%s`: `%s` is not a scalar or Text field"
|
||||
c.Ast.name cn))
|
||||
cols)
|
||||
cfg.Ast.indexes
|
||||
| None -> ());
|
||||
{ cr_name = c.name; cr_gc = c.is_gc;
|
||||
cr_fields =
|
||||
Array.of_list (List.map (fun (fl : Ast.field) -> (fl.name, fl.ty)) c.fields);
|
||||
cr_methods = List.map (fun (m : Ast.method_decl) -> m.name) c.methods;
|
||||
cr_indexes = table_indexes @ unique_indexes })
|
||||
:: !classes
|
||||
end
|
||||
| Ast.Union (ud : Ast.union_decl) ->
|
||||
|
|
@ -4001,7 +4067,7 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
|||
class_id := SM.add key cid !class_id;
|
||||
incr nclasses;
|
||||
classes :=
|
||||
{ cr_name = key; cr_gc = false;
|
||||
{ cr_name = key; cr_gc = false; cr_indexes = [];
|
||||
cr_fields = Array.of_list vd.Ast.v_fields;
|
||||
cr_methods = [] }
|
||||
:: !classes
|
||||
|
|
@ -4044,11 +4110,18 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
|||
class_id := SM.add name cid !class_id;
|
||||
incr nclasses;
|
||||
classes :=
|
||||
{ cr_name = name; cr_gc = false; cr_fields = Array.of_list fields; cr_methods = [] }
|
||||
{ cr_name = name; cr_gc = false; cr_fields = Array.of_list fields; cr_methods = [];
|
||||
cr_indexes = [] }
|
||||
:: !classes
|
||||
end)
|
||||
Types.predeclared_records;
|
||||
let class_id = !class_id in
|
||||
(match !index_col_err with
|
||||
| Some (pos, msg) ->
|
||||
Diag.Collector.add coll
|
||||
(Diag.error ~code:cannot_lower_code ~file:!index_err_file ~line:pos.Ast.line
|
||||
~col:pos.Ast.col ~message:msg ())
|
||||
| None -> ());
|
||||
let p_classes = Array.of_list (List.rev !classes) in
|
||||
let p_ifaces = Array.of_list (List.rev !ifaces) in
|
||||
List.iter
|
||||
|
|
@ -4220,7 +4293,16 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
|||
needs when decode creates one. *)
|
||||
Array.iter (fun kidx -> Buf.u32 cls kidx) class_field_names.(cid);
|
||||
Array.iter (fun (_, ty) -> Buf.u32 cls (field_class_meta p ty)) c.cr_fields;
|
||||
Array.iter (fun (_, ty) -> Buf.u32 cls (field_elem_meta p ty)) c.cr_fields)
|
||||
Array.iter (fun (_, ty) -> Buf.u32 cls (field_elem_meta p ty)) c.cr_fields;
|
||||
(* v3 tail (iteration 9 Task 4): the class's secondary indexes —
|
||||
index_cnt, then per index: flags (bit0 unique), col_cnt, cols *)
|
||||
Buf.u32 cls (List.length c.cr_indexes);
|
||||
List.iter
|
||||
(fun (uniq, cols) ->
|
||||
Buf.u32 cls (if uniq then 1 else 0);
|
||||
Buf.u32 cls (Array.length cols);
|
||||
Array.iter (fun ci -> Buf.u32 cls ci) cols)
|
||||
c.cr_indexes)
|
||||
p_classes;
|
||||
let ifs = Buf.create () in
|
||||
Array.iteri
|
||||
|
|
|
|||
|
|
@ -2382,7 +2382,7 @@ let validate_image (img : string) : string list =
|
|||
let u64 o = if ok 8 o then String.get_int64_le img o else 0L in
|
||||
let none = 0xFFFFFFFF in
|
||||
if u32 0 <> 0x31424F57 then fail "bad magic";
|
||||
if u32 4 <> 2 then fail "unsupported version";
|
||||
if u32 4 <> 3 then fail "unsupported version";
|
||||
let coff = u32 8 and ccnt = u32 12 in
|
||||
let koff = u32 16 and kcnt = u32 20 in
|
||||
let ioff = u32 24 and icnt = u32 28 in
|
||||
|
|
@ -2419,6 +2419,7 @@ let validate_image (img : string) : string list =
|
|||
if flags land lnot 0x01 <> 0 then fail (Printf.sprintf "class %d: unknown flags" i);
|
||||
if fcnt > 65535 then fail (Printf.sprintf "class %d: too many fields" i);
|
||||
class_fields.(i) <- fcnt;
|
||||
let kco = !o in (* the kind bytes' offset: the v3 index walk re-reads them *)
|
||||
for j = 0 to fcnt - 1 do
|
||||
if u8 (!o + j) > 5 then fail (Printf.sprintf "class %d field %d: bad kind" i j)
|
||||
done;
|
||||
|
|
@ -2435,6 +2436,25 @@ let validate_image (img : string) : string list =
|
|||
fail (Printf.sprintf "class %d field %d: field class out of range" i j)
|
||||
done;
|
||||
o := !o + (fcnt * 12);
|
||||
(* v3: the index tail — flags (bit0 only), col_cnt 1..8, columns in
|
||||
range and scalar/Text-kinded. Mirrors loader.c's checks. *)
|
||||
let icnt_x = u32 !o in
|
||||
o := !o + 4;
|
||||
if icnt_x > 64 then fail (Printf.sprintf "class %d: too many indexes" i);
|
||||
for x = 0 to icnt_x - 1 do
|
||||
let ifl = u32 !o and ccnt = u32 (!o + 4) in
|
||||
o := !o + 8;
|
||||
if ifl land lnot 1 <> 0 then fail (Printf.sprintf "class %d index %d: unknown flags" i x);
|
||||
if ccnt = 0 || ccnt > 8 then fail (Printf.sprintf "class %d index %d: bad column count" i x);
|
||||
for c = 0 to ccnt - 1 do
|
||||
let col = u32 !o in
|
||||
o := !o + 4;
|
||||
if col >= fcnt then fail (Printf.sprintf "class %d index %d: column out of range" i x);
|
||||
let kind = u8 (kco + col) in
|
||||
if kind <> 0 && kind <> 3 then
|
||||
fail (Printf.sprintf "class %d index %d: column %d is not scalar or Text" i x c)
|
||||
done
|
||||
done;
|
||||
if !o > len then fail (Printf.sprintf "class %d: truncated" i)
|
||||
done;
|
||||
(* interfaces + vtable rows *)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,12 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
switch (C) {
|
||||
case WO_B_DB_INSERT: {
|
||||
uint32_t cid = (uint32_t)R[B];
|
||||
uint64_t id = wo_row_insert(db, cid, &R[B + 1], msg);
|
||||
if (!id) return WO_T_DB; /* *msg already set (OOM / bad kind) */
|
||||
int ek = 0;
|
||||
uint64_t id = wo_row_insert(db, cid, &R[B + 1], msg, &ek);
|
||||
if (!id)
|
||||
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) {
|
||||
/* RAM applied, record staged, ONE commit before the ack (the
|
||||
|
|
|
|||
|
|
@ -266,6 +266,120 @@ static void hdel(db_table *t, uint64_t id) {
|
|||
|
||||
/* ---- tables and rows ---------------------------------------------------- */
|
||||
|
||||
/* ---- secondary indexes (Task 4) ---------------------------------------- */
|
||||
|
||||
/* 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;
|
||||
}
|
||||
h ^= hmix(v + i);
|
||||
}
|
||||
return h ? h : 1; /* 0 marks an empty bucket */
|
||||
}
|
||||
|
||||
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 (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++) {
|
||||
db_row *other = wo_row_ptr(db, t->class_id, b->ids[i]);
|
||||
if (other && idx_cols_equal(c, ix, r, other)) 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;
|
||||
|
|
@ -298,6 +412,11 @@ static void table_destroy(wo_db *db, db_table *t) {
|
|||
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) {
|
||||
|
|
@ -312,9 +431,22 @@ 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)db->classes[class_id].field_cnt * 8u;
|
||||
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;
|
||||
}
|
||||
|
|
@ -356,7 +488,8 @@ static uint32_t slot_alloc(db_table *t) {
|
|||
}
|
||||
|
||||
uint64_t wo_row_insert(wo_db *db, uint32_t class_id, const uint64_t *vals,
|
||||
const char **msg) {
|
||||
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";
|
||||
|
|
@ -375,7 +508,10 @@ uint64_t wo_row_insert(wo_db *db, uint32_t class_id, const uint64_t *vals,
|
|||
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) break;
|
||||
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]);
|
||||
|
|
@ -395,13 +531,28 @@ uint64_t wo_row_insert(wo_db *db, uint32_t class_id, const uint64_t *vals,
|
|||
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++;
|
||||
/* INDEX HOOK (Task 4): secondary indexes update here, inside the choke
|
||||
point, never anywhere else. */
|
||||
/* 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;
|
||||
}
|
||||
|
||||
|
|
@ -444,12 +595,16 @@ db_row *wo_row_create_raw(wo_db *db, uint32_t class_id, uint64_t id) {
|
|||
/* 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;
|
||||
/* INDEX HOOK (Task 4): replayed rows re-index here, same as inserts —
|
||||
the caller fills slots BEFORE indexes exist on them (Task 4 will move
|
||||
the hook to a post-fill call, recorded in the binding doc). */
|
||||
/* 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);
|
||||
|
|
@ -463,8 +618,9 @@ int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id) {
|
|||
if (!s1) return -1;
|
||||
uint32_t g = (uint32_t)(s1 - 1);
|
||||
db_row *r = slot_row(t, g);
|
||||
/* INDEX HOOK (Task 4): secondary indexes remove here, before the row's
|
||||
values die. */
|
||||
/* 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));
|
||||
|
|
|
|||
|
|
@ -78,6 +78,29 @@ typedef struct 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 */
|
||||
|
|
@ -94,6 +117,9 @@ typedef struct db_table {
|
|||
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 {
|
||||
|
|
@ -112,7 +138,7 @@ void wo_db_destroy(wo_db *db);
|
|||
* 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);
|
||||
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).
|
||||
|
|
@ -140,4 +166,10 @@ 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);
|
||||
|
||||
/* 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 */
|
||||
|
|
|
|||
|
|
@ -403,7 +403,17 @@ static int apply_record(wo_db *db, const uint8_t *payload, uint32_t len) {
|
|||
return -1;
|
||||
}
|
||||
}
|
||||
return (size_t)(r.end - r.p) == 0 ? 0 : -1; /* trailing bytes = corrupt */
|
||||
if ((size_t)(r.end - r.p) != 0) { /* trailing bytes = corrupt */
|
||||
wo_row_remove(db, cid, id);
|
||||
return -1;
|
||||
}
|
||||
/* slots are real now: re-index (Task 4). A unique violation during
|
||||
* replay is corruption — the live insert would have refused it. */
|
||||
if (wo_row_raw_commit(db, cid, row) != 0) {
|
||||
wo_row_remove(db, cid, id);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t wo_wal_replay(const char *path, wo_db *db) {
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
|||
m->classes = calloc(kcnt, sizeof(wo_classdesc));
|
||||
if (!m->classes) BAIL("out of memory");
|
||||
}
|
||||
size_t pool_len = 0, meta_pool = 0;
|
||||
size_t pool_len = 0, meta_pool = 0, idx_pool = 0;
|
||||
for (uint32_t i = 0; i < kcnt; i++) {
|
||||
uint32_t name, flags, fcnt;
|
||||
if (rd_u32(&k, &name) || rd_u32(&k, &flags) || rd_u32(&k, &fcnt))
|
||||
|
|
@ -203,6 +203,36 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
|||
m->classes[i].field_elem = (const uint32_t *)(uintptr_t)(meta_pool + 2u * (size_t)fcnt);
|
||||
pool_len += fcnt ? fcnt : 1;
|
||||
meta_pool += meta_words ? meta_words : 1;
|
||||
/* v3 tail: secondary indexes — flags/col_cnt/cols per index, columns
|
||||
bounded and scalar/Text-kinded (the only indexable kinds) */
|
||||
uint32_t icnt;
|
||||
if (rd_u32(&k, &icnt)) BAIL("class %u: truncated index count", (unsigned)i);
|
||||
if (icnt > 64) BAIL("class %u: too many indexes", (unsigned)i);
|
||||
m->classes[i].idx_cnt = icnt;
|
||||
m->classes[i].idx_meta = (const uint32_t *)(uintptr_t)idx_pool;
|
||||
for (uint32_t x = 0; x < icnt; x++) {
|
||||
uint32_t iflags, ccnt;
|
||||
if (rd_u32(&k, &iflags) || rd_u32(&k, &ccnt))
|
||||
BAIL("class %u index %u: truncated", (unsigned)i, (unsigned)x);
|
||||
if (iflags & ~1u) BAIL("class %u index %u: unknown flags", (unsigned)i, (unsigned)x);
|
||||
if (ccnt == 0 || ccnt > 8)
|
||||
BAIL("class %u index %u: bad column count", (unsigned)i, (unsigned)x);
|
||||
uint32_t *ip = realloc(m->idxpool, (idx_pool + 2 + ccnt) * sizeof(uint32_t));
|
||||
if (!ip) BAIL("out of memory");
|
||||
m->idxpool = ip;
|
||||
m->idxpool[idx_pool++] = iflags;
|
||||
m->idxpool[idx_pool++] = ccnt;
|
||||
for (uint32_t cix = 0; cix < ccnt; cix++) {
|
||||
uint32_t col;
|
||||
if (rd_u32(&k, &col)) BAIL("class %u index %u: truncated column", (unsigned)i, (unsigned)x);
|
||||
if (col >= fcnt) BAIL("class %u index %u: column out of range", (unsigned)i, (unsigned)x);
|
||||
uint8_t kind = m->kindpool[(uintptr_t)m->classes[i].kinds + col];
|
||||
if (kind != WO_K_SCALAR && kind != WO_K_TEXT)
|
||||
BAIL("class %u index %u: column %u is not scalar or Text", (unsigned)i,
|
||||
(unsigned)x, (unsigned)col);
|
||||
m->idxpool[idx_pool++] = col;
|
||||
}
|
||||
}
|
||||
m->class_cnt = i + 1;
|
||||
}
|
||||
for (uint32_t i = 0; i < m->class_cnt; i++) {
|
||||
|
|
@ -210,6 +240,8 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
|||
m->classes[i].field_names = m->metapool + (uintptr_t)m->classes[i].field_names;
|
||||
m->classes[i].field_class = m->metapool + (uintptr_t)m->classes[i].field_class;
|
||||
m->classes[i].field_elem = m->metapool + (uintptr_t)m->classes[i].field_elem;
|
||||
m->classes[i].idx_meta =
|
||||
m->idxpool ? m->idxpool + (uintptr_t)m->classes[i].idx_meta : NULL;
|
||||
}
|
||||
|
||||
/* ---- interfaces + vtable rows (expanded to sorted triples) ---- */
|
||||
|
|
@ -532,6 +564,7 @@ void wo_module_free(wo_module *m) {
|
|||
free(m->classes);
|
||||
free(m->kindpool);
|
||||
free(m->metapool);
|
||||
free(m->idxpool);
|
||||
free(m->vtabs);
|
||||
for (uint32_t i = 0; i < m->method_cnt; i++) {
|
||||
free(m->methods[i].code);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ typedef struct wo_module {
|
|||
/* pooled per-field metadata (v2): names, referenced class ids, element
|
||||
kinds — see wob.h's "class-table field metadata" note */
|
||||
uint32_t *metapool;
|
||||
uint32_t *idxpool; /* v3 pooled per-class index metadata (flags/cols) */
|
||||
uint32_t slot_cnt; /* total interface slots across all interfaces */
|
||||
wo_vtabent *vtabs;
|
||||
uint32_t vtab_cnt;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
/* ---- file header (44 bytes, absolute offsets) ---- */
|
||||
#define WOB_MAGIC 0x31424F57u /* "WOB1" read as LE u32 */
|
||||
#define WOB_VERSION 2u /* v2 adds per-field names/types to the class table */
|
||||
#define WOB_VERSION 3u /* v3: v2's field metadata + per-class index metadata */
|
||||
#define WOB_HDR_SIZE 44u
|
||||
#define WOB_OFF_MAGIC 0u
|
||||
#define WOB_OFF_VERSION 4u
|
||||
|
|
@ -121,6 +121,10 @@ enum {
|
|||
* message rides along in the error record, and `try ... catch` is how
|
||||
* a program that expects the failure handles it. */
|
||||
WO_T_IO = 9,
|
||||
/* iteration 9 Task 4: a unique-index violation on insert/update —
|
||||
raised by the engine at the row choke point, catchable like any
|
||||
trap (the employee sample's SEED-DUP line) */
|
||||
WO_T_UNIQUE = 10,
|
||||
};
|
||||
|
||||
/* ---- opcodes (spec section 5; semantics in the format doc) ---- */
|
||||
|
|
@ -330,6 +334,11 @@ typedef struct wo_classdesc {
|
|||
const uint32_t *field_names; /* constant index of each field's name */
|
||||
const uint32_t *field_class; /* referenced class id / JSON_RAW / NONE */
|
||||
const uint32_t *field_elem; /* container element kinds */
|
||||
/* v3 (iteration 9 Task 4): the class's secondary indexes, flat-encoded
|
||||
[flags, col_cnt, col...]* — flags bit0 = unique. idx_cnt entries.
|
||||
Columns are field indices, scalar/Text kinds only (loader-checked). */
|
||||
uint32_t idx_cnt;
|
||||
const uint32_t *idx_meta;
|
||||
} wo_classdesc;
|
||||
#define WO_CLASSF_GC 0x01u
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ static void test_roundtrip_all_kinds(void) {
|
|||
uint64_t vals[5] = {(uint64_t)(uintptr_t)name, 9200000,
|
||||
(uint64_t)(uintptr_t)addr, (uint64_t)(uintptr_t)tags,
|
||||
(uint64_t)(uintptr_t)meta};
|
||||
uint64_t id = wo_row_insert(&db, 1, vals, &msg);
|
||||
uint64_t id = wo_row_insert(&db, 1, vals, &msg, NULL);
|
||||
T_EQ(id, 1); /* shard 0 of 1: first id is 1 */
|
||||
|
||||
/* the row stored COPIES: mutate the VM originals, then read back */
|
||||
|
|
@ -74,7 +74,7 @@ static void test_roundtrip_all_kinds(void) {
|
|||
|
||||
/* nil TEXT / nil OWNED / WO_NIL_SCALAR round-trip */
|
||||
uint64_t nilvals[5] = {0, WO_NIL_SCALAR, 0, 0, 0};
|
||||
uint64_t id2 = wo_row_insert(&db, 1, nilvals, &msg);
|
||||
uint64_t id2 = wo_row_insert(&db, 1, nilvals, &msg, NULL);
|
||||
T_EQ(id2, 2);
|
||||
uint64_t out2[5] = {(uint64_t)-1, 0, (uint64_t)-1, (uint64_t)-1, (uint64_t)-1};
|
||||
T_EQ(wo_row_read(&db, &rt, 1, id2, out2, &msg), 0);
|
||||
|
|
@ -103,12 +103,12 @@ static void test_id_interleave_across_shards(void) {
|
|||
T_EQ(wo_db_init(&b, CLASSES, 3, 1, 3), 0);
|
||||
T_EQ(wo_db_init(&c, CLASSES, 3, 2, 3), 0);
|
||||
uint64_t v[1] = {42};
|
||||
T_EQ(wo_row_insert(&a, 2, v, &msg), 1); /* shard 0: 1, 4, 7 */
|
||||
T_EQ(wo_row_insert(&a, 2, v, &msg), 4);
|
||||
T_EQ(wo_row_insert(&b, 2, v, &msg), 2); /* shard 1: 2, 5 */
|
||||
T_EQ(wo_row_insert(&b, 2, v, &msg), 5);
|
||||
T_EQ(wo_row_insert(&c, 2, v, &msg), 3); /* shard 2: 3, 6 */
|
||||
T_EQ(wo_row_insert(&c, 2, v, &msg), 6);
|
||||
T_EQ(wo_row_insert(&a, 2, v, &msg, NULL), 1); /* shard 0: 1, 4, 7 */
|
||||
T_EQ(wo_row_insert(&a, 2, v, &msg, NULL), 4);
|
||||
T_EQ(wo_row_insert(&b, 2, v, &msg, NULL), 2); /* shard 1: 2, 5 */
|
||||
T_EQ(wo_row_insert(&b, 2, v, &msg, NULL), 5);
|
||||
T_EQ(wo_row_insert(&c, 2, v, &msg, NULL), 3); /* shard 2: 3, 6 */
|
||||
T_EQ(wo_row_insert(&c, 2, v, &msg, NULL), 6);
|
||||
/* owner-shard discipline: (id-1) % N names the shard */
|
||||
T_EQ((4 - 1) % 3, 0);
|
||||
T_EQ((5 - 1) % 3, 1);
|
||||
|
|
@ -133,7 +133,7 @@ static void test_slab_growth_and_reuse(void) {
|
|||
uint64_t ids[N];
|
||||
for (uint32_t i = 0; i < N; i++) {
|
||||
uint64_t v[1] = {i};
|
||||
ids[i] = wo_row_insert(&db, 2, v, &msg);
|
||||
ids[i] = wo_row_insert(&db, 2, v, &msg, NULL);
|
||||
T_CHECK(ids[i] == i + 1);
|
||||
}
|
||||
T_EQ(db.tables[2].slab_cnt, 4);
|
||||
|
|
@ -152,7 +152,7 @@ static void test_slab_growth_and_reuse(void) {
|
|||
T_EQ(wo_row_read(&db, &rt, 2, ids[100], out, &msg), -1); /* gone */
|
||||
T_EQ(wo_row_remove(&db, 2, ids[100]), -1); /* twice = miss */
|
||||
uint64_t v[1] = {777};
|
||||
uint64_t fresh = wo_row_insert(&db, 2, v, &msg);
|
||||
uint64_t fresh = wo_row_insert(&db, 2, v, &msg, NULL);
|
||||
T_CHECK(fresh > (uint64_t)N); /* ids never reused ... */
|
||||
db_row *fresh_row = wo_row_ptr(&db, 2, fresh);
|
||||
T_CHECK(fresh_row == victim); /* ... but the SLOT is */
|
||||
|
|
@ -166,7 +166,7 @@ static void test_misuse(void) {
|
|||
wo_db db;
|
||||
T_EQ(wo_db_init(&db, CLASSES, 3, 0, 1), 0);
|
||||
uint64_t v[1] = {1};
|
||||
T_EQ(wo_row_insert(&db, 99, v, &msg), 0); /* unknown class */
|
||||
T_EQ(wo_row_insert(&db, 99, v, &msg, NULL), 0); /* unknown class */
|
||||
T_CHECK(wo_row_ptr(&db, 99, 1) == NULL);
|
||||
T_CHECK(wo_row_ptr(&db, 2, 1) == NULL); /* table never touched */
|
||||
T_EQ(wo_row_remove(&db, 2, 1), -1);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ static void test_roundtrip_replay(void) {
|
|||
for (int i = 0; i < 3; i++) {
|
||||
wo_str *s = wo_str_new(&rt, "abcXYZ" + i, 3); /* "abc","bcX","cXY" */
|
||||
uint64_t vals[2] = {(uint64_t)(i * 10), (uint64_t)(uintptr_t)s};
|
||||
ids[i] = wo_row_insert(&db, 0, vals, &msg);
|
||||
ids[i] = wo_row_insert(&db, 0, vals, &msg, NULL);
|
||||
T_CHECK(ids[i] != 0);
|
||||
T_EQ(wo_wal_append_insert(&w, &db, 0, ids[i]), 0);
|
||||
wo_str_free(&rt, s);
|
||||
|
|
@ -70,7 +70,7 @@ static void test_roundtrip_replay(void) {
|
|||
wo_str_free(&rt, (wo_str *)(uintptr_t)out[1]);
|
||||
/* next_id advanced past the replayed ids: a fresh insert never collides */
|
||||
uint64_t vals[2] = {99, 0};
|
||||
uint64_t fresh = wo_row_insert(&db2, 0, vals, &msg);
|
||||
uint64_t fresh = wo_row_insert(&db2, 0, vals, &msg, NULL);
|
||||
T_CHECK(fresh > ids[2]);
|
||||
wo_db_destroy(&db2);
|
||||
wo_rt_destroy(&rt);
|
||||
|
|
@ -92,7 +92,7 @@ static void test_torn_tail(void) {
|
|||
const char *msg = "";
|
||||
for (int i = 0; i < 5; i++) {
|
||||
uint64_t vals[2] = {(uint64_t)i, 0};
|
||||
uint64_t id = wo_row_insert(&db, 0, vals, &msg);
|
||||
uint64_t id = wo_row_insert(&db, 0, vals, &msg, NULL);
|
||||
T_EQ(wo_wal_append_insert(&w, &db, 0, id), 0);
|
||||
T_EQ(wo_wal_commit(&w), 0);
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ static void test_torn_tail(void) {
|
|||
T_EQ(wo_wal_open(&w2, path, 0), 0);
|
||||
T_EQ(w2.off, intact_end);
|
||||
uint64_t vals[2] = {100, 0};
|
||||
uint64_t id = wo_row_insert(&db3, 0, vals, &msg);
|
||||
uint64_t id = wo_row_insert(&db3, 0, vals, &msg, NULL);
|
||||
T_EQ(wo_wal_append_insert(&w2, &db3, 0, id), 0);
|
||||
T_EQ(wo_wal_commit(&w2), 0);
|
||||
wo_wal_close(&w2);
|
||||
|
|
@ -151,7 +151,7 @@ static void battery_child(const char *path, int ack_fd) {
|
|||
int n = snprintf(label, sizeof label, "row-%llu", (unsigned long long)i);
|
||||
wo_str *s = wo_str_new(&rt, label, (uint32_t)n);
|
||||
uint64_t vals[2] = {i * 3 + 1, (uint64_t)(uintptr_t)s};
|
||||
uint64_t id = wo_row_insert(&db, 0, vals, &msg);
|
||||
uint64_t id = wo_row_insert(&db, 0, vals, &msg, NULL);
|
||||
wo_str_free(&rt, s);
|
||||
if (!id) _exit(9);
|
||||
if (wo_wal_append_insert(&w, &db, 0, id) != 0) _exit(9);
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ uint32_t wb_class(wb_t *b, uint32_t name_const, uint32_t flags,
|
|||
for (uint32_t j = 0; j < field_cnt; j++) put_u32(&b->classes, WOB_NONE);
|
||||
for (uint32_t j = 0; j < field_cnt; j++) put_u32(&b->classes, WOB_NONE);
|
||||
for (uint32_t j = 0; j < field_cnt; j++) put_u32(&b->classes, 0);
|
||||
put_u32(&b->classes, 0); /* v3: no secondary indexes in hand-built images */
|
||||
return b->class_cnt++;
|
||||
}
|
||||
|
||||
|
|
|
|||
4
tests/corpus/run/db-unique-catch/fixture.out
Normal file
4
tests/corpus/run/db-unique-catch/fixture.out
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
1
|
||||
DUP-REFUSED
|
||||
EVENTS-OK
|
||||
2
|
||||
26
tests/corpus/run/db-unique-catch/fixture.wo
Normal file
26
tests/corpus/run/db-unique-catch/fixture.wo
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- iteration 9 Task 4, the other half: the unique trap is catchable (the
|
||||
-- employee sample's SEED-DUP pattern), a NON-unique @table composite index
|
||||
-- accepts duplicates, and a distinct key inserts freely beside the taken
|
||||
-- one. (Key release after delete joins the corpus with Task 5's delete.)
|
||||
@table(name: "events", index: [kind, at])
|
||||
class Event {
|
||||
kind: Text
|
||||
at: Int
|
||||
}
|
||||
|
||||
class Account {
|
||||
email: Text @unique
|
||||
balance: Int
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a = insert Account { email: "a@x", balance: 100 }
|
||||
print_int(a)
|
||||
let dup = try insert Account { email: "a@x", balance: 200 } catch (e) nil
|
||||
if dup == nil { print("DUP-REFUSED") }
|
||||
insert Event { kind: "warn", at: 1 }
|
||||
insert Event { kind: "warn", at: 1 }
|
||||
print("EVENTS-OK")
|
||||
let b = insert Account { email: "b@x", balance: 300 }
|
||||
print_int(b)
|
||||
}
|
||||
1
tests/corpus/trap/db-unique-violation/fixture.trap
Normal file
1
tests/corpus/trap/db-unique-violation/fixture.trap
Normal file
|
|
@ -0,0 +1 @@
|
|||
10
|
||||
12
tests/corpus/trap/db-unique-violation/fixture.wo
Normal file
12
tests/corpus/trap/db-unique-violation/fixture.wo
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- iteration 9 Task 4: a duplicate insert into a @unique column traps with
|
||||
-- the unique-violation code (WO_T_UNIQUE = 10), raised at the engine's row
|
||||
-- choke point. Uncaught here on purpose: the trap code is the assertion.
|
||||
class Account {
|
||||
email: Text @unique
|
||||
balance: Int
|
||||
}
|
||||
|
||||
fn main() {
|
||||
insert Account { email: "a@x", balance: 100 }
|
||||
insert Account { email: "a@x", balance: 200 }
|
||||
}
|
||||
Loading…
Reference in a new issue