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>
This commit is contained in:
parent
c8c34c118c
commit
2677c1457e
17 changed files with 282 additions and 16 deletions
|
|
@ -600,10 +600,23 @@ let manifest_parse (path : string) : (string * string) list =
|
||||||
if line = "" || (String.length line >= 1 && line.[0] = '#') then ()
|
if line = "" || (String.length line >= 1 && line.[0] = '#') then ()
|
||||||
else if line.[0] = '[' then begin
|
else if line.[0] = '[' then begin
|
||||||
if line.[String.length line - 1] <> ']' then fail !lineno "malformed section header";
|
if line.[String.length line - 1] <> ']' then fail !lineno "malformed section header";
|
||||||
section := String.sub line 1 (String.length line - 2);
|
(* accept `[[table.array]]` headers too (iteration 9c's
|
||||||
if !section <> "runtime" && !section <> "build" then
|
[[share.clients]]) by trimming the doubled brackets *)
|
||||||
fail !lineno (Printf.sprintf "unknown section [%s] (runtime and build exist)" !section)
|
let inner = String.sub line 1 (String.length line - 2) in
|
||||||
|
let inner =
|
||||||
|
if String.length inner >= 2 && inner.[0] = '[' && inner.[String.length inner - 1] = ']'
|
||||||
|
then String.sub inner 1 (String.length inner - 2)
|
||||||
|
else inner
|
||||||
|
in
|
||||||
|
section := inner;
|
||||||
|
if !section <> "runtime" && !section <> "build" && !section <> "share"
|
||||||
|
&& !section <> "share.clients"
|
||||||
|
then
|
||||||
|
fail !lineno
|
||||||
|
(Printf.sprintf "unknown section [%s] (runtime and build exist)" !section)
|
||||||
end
|
end
|
||||||
|
else if !section = "share" || !section = "share.clients" then
|
||||||
|
() (* iteration 9c manifest keys — parsed by the attach feature, ignored here *)
|
||||||
else
|
else
|
||||||
match String.index_opt line '=' with
|
match String.index_opt line '=' with
|
||||||
| None -> fail !lineno "expected `key = \"value\"`"
|
| None -> fail !lineno "expected `key = \"value\"`"
|
||||||
|
|
|
||||||
|
|
@ -1426,7 +1426,8 @@ let field_class_meta (p : pctx) (ty : Ast.field_ty) : int =
|
||||||
match name_of (Ast.Scalar e) with
|
match name_of (Ast.Scalar e) with
|
||||||
| Some n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none)
|
| Some n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none)
|
||||||
| None -> wob_none)
|
| None -> wob_none)
|
||||||
| Ast.Ref _ | Ast.Backlink _ | Ast.Nullable _ -> wob_none
|
| Ast.Ref n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none)
|
||||||
|
| Ast.Backlink _ | Ast.Nullable _ -> wob_none
|
||||||
|
|
||||||
let field_elem_meta (p : pctx) (ty : Ast.field_ty) : int =
|
let field_elem_meta (p : pctx) (ty : Ast.field_ty) : int =
|
||||||
match unwrap ty with
|
match unwrap ty with
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,12 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
||||||
case WO_B_DB_DELETE: {
|
case WO_B_DB_DELETE: {
|
||||||
uint32_t cid = (uint32_t)R[B];
|
uint32_t cid = (uint32_t)R[B];
|
||||||
uint64_t id = R[B + 1];
|
uint64_t id = R[B + 1];
|
||||||
|
/* FK restrict: refuse if another row still references this one
|
||||||
|
(iteration 9b) — nothing is removed, the statement traps */
|
||||||
|
if (wo_row_has_referrers(db, cid, id)) {
|
||||||
|
*msg = "row is still referenced (restrict)";
|
||||||
|
return WO_T_FK;
|
||||||
|
}
|
||||||
if (wo_row_remove(db, cid, id) != 0) {
|
if (wo_row_remove(db, cid, id) != 0) {
|
||||||
*msg = "no such row";
|
*msg = "no such row";
|
||||||
return WO_T_DB;
|
return WO_T_DB;
|
||||||
|
|
|
||||||
|
|
@ -703,6 +703,28 @@ int wo_row_update_field(wo_db *db, uint32_t class_id, uint64_t id, uint32_t fiel
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_remove(wo_db *db, uint32_t class_id, uint64_t id) {
|
int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id) {
|
||||||
if (class_id >= db->class_cnt) return -1;
|
if (class_id >= db->class_cnt) return -1;
|
||||||
db_table *t = &db->tables[class_id];
|
db_table *t = &db->tables[class_id];
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,14 @@ int wo_row_read(wo_db *db, wo_rt *rt, uint32_t class_id, uint64_t id,
|
||||||
* it. 0 ok, -1 no such row. */
|
* it. 0 ok, -1 no such row. */
|
||||||
int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id);
|
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,
|
/* 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 —
|
* swap it into the slot, keep every index containing that column honest —
|
||||||
* remove-old/add-new with the unique re-check running BEFORE anything
|
* remove-old/add-new with the unique re-check running BEFORE anything
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ that sequences its tasks. Read one, approve, then the next starts.
|
||||||
| 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/07b-inferred-gc-mark-sweep.md) | ⏸ off the workload's path (no `@gc`) |
|
| 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/07b-inferred-gc-mark-sweep.md) | ⏸ off the workload's path (no `@gc`) |
|
||||||
| 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ |
|
| 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ |
|
||||||
| 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | 🔄 engine complete (storage/WAL/indexes/insert-update-delete); reads land with 9b |
|
| 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | 🔄 engine complete (storage/WAL/indexes/insert-update-delete); reads land with 9b |
|
||||||
| 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | ⬜ spec + plan ready (2026-08-15) |
|
| 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | 🔄 query surface + relations + FK done (branch query-surface); group-by parked |
|
||||||
| 9c | [Cross-program tables](stories/language-runtime-database/09c-cross-program-tables.md) | 🔄 channel done (branch ipc-attach); manifest+binding pending |
|
| 9c | [Cross-program tables](stories/language-runtime-database/09c-cross-program-tables.md) | 🔄 channel done (branch ipc-attach); manifest+binding pending |
|
||||||
| 9d | [Keypair attach auth](stories/language-runtime-database/09d-keypair-attach-auth.md) | 🔄 crypto+handshake done (branch keypair-auth); manifest pending |
|
| 9d | [Keypair attach auth](stories/language-runtime-database/09d-keypair-attach-auth.md) | 🔄 crypto+handshake done (branch keypair-auth); manifest pending |
|
||||||
| 9e | [Durability, throughput, scale](stories/language-runtime-database/09e-durability-throughput-scale.md) | ⬜ needs a spec first |
|
| 9e | [Durability, throughput, scale](stories/language-runtime-database/09e-durability-throughput-scale.md) | ⬜ needs a spec first |
|
||||||
|
|
|
||||||
14
docs/examples/employee/justfile
Normal file
14
docs/examples/employee/justfile
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# docs/examples/employee — the database track's acceptance workload.
|
||||||
|
# `just employee::<recipe>` from the repo root, or plain `just <recipe>` here.
|
||||||
|
ROOT := source_directory() / "../../.."
|
||||||
|
|
||||||
|
default: accept
|
||||||
|
|
||||||
|
# build the standalone binary from wo.toml (needs woc-build + wovm-build once)
|
||||||
|
build:
|
||||||
|
{{ROOT}}/compiler/_build/default/bin/woc .
|
||||||
|
@ls -la target/employee
|
||||||
|
|
||||||
|
# the acceptance: compile + every mode against a WAL-durable database
|
||||||
|
accept:
|
||||||
|
{{ROOT}}/scripts/employee-accept.sh
|
||||||
|
|
@ -48,19 +48,34 @@ fn seed() -> Int {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
-- One GROUP BY after another: group-and-reduce lowers to a single hash pass
|
-- Per-department aggregates. The group-by SYNTAX
|
||||||
-- (no group objects), avg/min/max are ?Int because an empty group is data.
|
-- from e in Employee group e by e.dept into g order by avg(g.salary) desc
|
||||||
|
-- select { dept: g.key.name, headcount: count(g), avg_salary: avg(g.salary), ... }
|
||||||
|
-- is PARKED for a future iteration (compile-time group-and-reduce + projection
|
||||||
|
-- records). Until it lands, the same report is hand-rolled from the primitives
|
||||||
|
-- that DO exist — a scan of departments, a backlink scan of each one's staff,
|
||||||
|
-- and plain scalar accumulation. Same numbers, more lines; the group-by
|
||||||
|
-- version is the ergonomic upgrade, not a new capability.
|
||||||
fn report() -> Int {
|
fn report() -> Int {
|
||||||
let rows = from e in Employee
|
let payroll = 0;
|
||||||
group e by e.dept into g
|
for d in from x in Department order by x.name select x {
|
||||||
order by avg(g.salary) desc
|
let headcount = 0;
|
||||||
select { dept: g.key.name, headcount: count(g),
|
let total = 0;
|
||||||
avg_salary: avg(g.salary), min_salary: min(g.salary),
|
let smin = -1;
|
||||||
max_salary: max(g.salary) };
|
let smax = -1;
|
||||||
for r in rows {
|
for e in from s in d.staff select s {
|
||||||
print("DEPT ${r.dept} headcount=${r.headcount} avg=${r.avg_salary} min=${r.min_salary} max=${r.max_salary}");
|
headcount = headcount + 1;
|
||||||
|
total = total + e.salary;
|
||||||
|
payroll = payroll + e.salary;
|
||||||
|
if smin == -1 or e.salary < smin { smin = e.salary; }
|
||||||
|
if smax == -1 or e.salary > smax { smax = e.salary; }
|
||||||
|
}
|
||||||
|
if headcount == 0 {
|
||||||
|
print("DEPT ${d.name} headcount=0 avg=nil min=nil max=nil");
|
||||||
|
} else {
|
||||||
|
print("DEPT ${d.name} headcount=${headcount} avg=${total / headcount} min=${smin} max=${smax}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let payroll = sum(from e in Employee select e.salary);
|
|
||||||
print("PAYROLL ${payroll}");
|
print("PAYROLL ${payroll}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
docs/examples/employee/target/employee
Executable file
BIN
docs/examples/employee/target/employee
Executable file
Binary file not shown.
|
|
@ -132,6 +132,35 @@ validation (`WO-E102`), the Rust runtime already ships secondary indexes and
|
||||||
id rather than a pointer — so the relational vocabulary partly exists and this
|
id rather than a pointer — so the relational vocabulary partly exists and this
|
||||||
iteration makes it mean something in the C stack.
|
iteration makes it mean something in the C stack.
|
||||||
|
|
||||||
|
## Query surface landed (2026-08-16, branch `query-surface`)
|
||||||
|
|
||||||
|
The compiler-checked query surface runs end to end, proven by
|
||||||
|
`docs/examples/employee` (8-check acceptance, `scripts/employee-accept.sh`):
|
||||||
|
|
||||||
|
- **Queries**: `from <v> in <table|nav> where* [order by <k> [desc]] [take n]
|
||||||
|
select <v|v.field>`, lowered to bytecode loops over engine cursor builtins
|
||||||
|
(DB_SCAN / DB_GET_FIELD / DB_PROBE) — no SQL text, disassembly-provable.
|
||||||
|
- **Relations**: `ref C` forward navigation (`e.dept.name`, a point read),
|
||||||
|
`backlink C.f` reverse navigation (`d.staff`, an index probe); backlink
|
||||||
|
fields are virtual (no stored column).
|
||||||
|
- **Mutation**: update-through-row (`e.salary = v` → DB_UPDATE_FIELD),
|
||||||
|
`delete <row>`, and **FK restrict** — deleting a row a `ref` still points at
|
||||||
|
traps `WO_T_FK` (the compiler records the ref target in the class table's
|
||||||
|
field_class metadata; the engine scans referencing columns).
|
||||||
|
- **`@unique`** violations trap and are catchable; everything is WAL-durable
|
||||||
|
and survives a process restart (proven in the acceptance).
|
||||||
|
|
||||||
|
**PARKED to a future iteration (2026-08-16, user decision):** **group-by
|
||||||
|
aggregation** — the `group … by … into g … select { count(g), avg(g.salary),
|
||||||
|
… }` syntax, which needs projection-record synthesis (anonymous record types),
|
||||||
|
aggregate clause-functions, and two-phase hash aggregation. The employee
|
||||||
|
sample's `report` mode is hand-rolled from the shipped primitives meanwhile
|
||||||
|
(a scan of departments × a backlink scan of each one's staff × scalar
|
||||||
|
accumulation) — same numbers, and the group-by version is the ergonomic
|
||||||
|
upgrade, not a new capability. The relational vocabulary these queries used
|
||||||
|
(`ref`/`backlink`/`@unique`/restrict) is the "table relations and FK" half,
|
||||||
|
now complete.
|
||||||
|
|
||||||
## Proposed Solution
|
## Proposed Solution
|
||||||
|
|
||||||
- ~~Brainstorm a spec first~~ — **done 2026-08-15**; the spec settles all
|
- ~~Brainstorm a spec first~~ — **done 2026-08-15**; the spec settles all
|
||||||
|
|
|
||||||
5
justfile
5
justfile
|
|
@ -50,6 +50,11 @@ wovm-test:
|
||||||
# pass; the corpus below gates the individual behaviors underneath it.
|
# pass; the corpus below gates the individual behaviors underneath it.
|
||||||
mod log-watcher "docs/examples/log-watcher"
|
mod log-watcher "docs/examples/log-watcher"
|
||||||
|
|
||||||
|
# the database track's acceptance workload (iteration 9/9b): @table storage,
|
||||||
|
# ref/backlink relations + FK restrict, and the compiler-checked query surface
|
||||||
|
# (scan/where/select/order/take, update, delete). `just employee` runs it.
|
||||||
|
mod employee "docs/examples/employee"
|
||||||
|
|
||||||
# conformance harness (plan 3): walks tests/corpus/{run,compile-fail,trap},
|
# conformance harness (plan 3): walks tests/corpus/{run,compile-fail,trap},
|
||||||
# exact outcome per fixture kind — see docs/plan/oop-vm/02-corpus.md.
|
# exact outcome per fixture kind — see docs/plan/oop-vm/02-corpus.md.
|
||||||
# Fails loudly (and names the recipe to run) if woc or wovm isn't built.
|
# Fails loudly (and names the recipe to run) if woc or wovm isn't built.
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,9 @@ enum {
|
||||||
raised by the engine at the row choke point, catchable like any
|
raised by the engine at the row choke point, catchable like any
|
||||||
trap (the employee sample's SEED-DUP line) */
|
trap (the employee sample's SEED-DUP line) */
|
||||||
WO_T_UNIQUE = 10,
|
WO_T_UNIQUE = 10,
|
||||||
|
/* iteration 9b: deleting a row still referenced by a `ref` traps here
|
||||||
|
(restrict) — the employee sample's DROP-of-a-department-with-staff */
|
||||||
|
WO_T_FK = 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---- opcodes (spec section 5; semantics in the format doc) ---- */
|
/* ---- opcodes (spec section 5; semantics in the format doc) ---- */
|
||||||
|
|
|
||||||
106
scripts/employee-accept.sh
Executable file
106
scripts/employee-accept.sh
Executable file
|
|
@ -0,0 +1,106 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/employee-accept.sh — the database track's acceptance workload.
|
||||||
|
#
|
||||||
|
# docs/examples/employee must compile via `woc <dir>` and run all its modes
|
||||||
|
# against a real WAL-durable database: insert + @unique trap, per-department
|
||||||
|
# aggregates, ref/backlink navigation, update-through-row, FK restrict on
|
||||||
|
# delete, and persistence across a process restart. This is iteration 9/9b's
|
||||||
|
# acceptance the way log-watcher is iterations 1-7's.
|
||||||
|
#
|
||||||
|
# Group-by SYNTAX is parked (a future iteration); report is hand-rolled from
|
||||||
|
# the primitives, so the numbers below exercise the shipped query surface.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
WOC="$ROOT/compiler/_build/default/bin/woc"
|
||||||
|
WOVM="$ROOT/runtime/wovm"
|
||||||
|
SAMPLE="$ROOT/docs/examples/employee"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
ok() { echo "ok $1"; pass=$((pass + 1)); }
|
||||||
|
bad() { echo "FAIL $1 -- $2"; fail=$((fail + 1)); }
|
||||||
|
|
||||||
|
if [[ ! -x "$WOC" || ! -x "$WOVM" ]]; then
|
||||||
|
echo "employee-accept: build woc and wovm first (just woc-build; just wovm-build)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/emp-accept.XXXXXX")"
|
||||||
|
DATA="$WORK/data"
|
||||||
|
mkdir -p "$DATA"
|
||||||
|
IMG="$WORK/employee.wob"
|
||||||
|
cleanup() { [[ -n "${EMP_ACCEPT_KEEP:-}" ]] && echo "kept $WORK" || rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# ---- 1. compile ------------------------------------------------------
|
||||||
|
if "$WOC" --emit "$SAMPLE" -o "$IMG" >"$WORK/compile.out" 2>&1; then
|
||||||
|
ok "compile ($(stat -c%s "$IMG") bytes)"
|
||||||
|
else
|
||||||
|
bad "compile" "$(head -1 "$WORK/compile.out")"
|
||||||
|
echo; printf 'employee-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
run() { WO_DATA="$DATA" "$WOVM" "$IMG" "$@"; }
|
||||||
|
|
||||||
|
# ---- 2. seed (insert + WAL) ------------------------------------------
|
||||||
|
if run seed 2>&1 | grep -q "^SEEDED 3 departments, 6 employees"; then
|
||||||
|
ok "seed (insert, WAL-durable)"
|
||||||
|
else
|
||||||
|
bad "seed" "no SEEDED line"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 3. seed again: @unique trap, caught, across a process boundary --
|
||||||
|
out="$(run seed 2>&1)"; rc=$?
|
||||||
|
if [[ "$out" == *"SEED-DUP"* && $rc -eq 3 ]]; then
|
||||||
|
ok "unique violation caught on re-seed (persisted via replay)"
|
||||||
|
else
|
||||||
|
bad "unique re-seed" "got rc=$rc: $(printf '%s' "$out" | tr '\n' '|' | cut -c1-100)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 4. report: per-department aggregates + payroll ------------------
|
||||||
|
rep="$(run report 2>&1)"
|
||||||
|
if [[ "$rep" == *"DEPT Engineering headcount=3 avg=8200000 min=7300000 max=9200000"* \
|
||||||
|
&& "$rep" == *"DEPT Operations headcount=2 avg=6150000 min=5900000 max=6400000"* \
|
||||||
|
&& "$rep" == *"PAYROLL 45700000"* ]]; then
|
||||||
|
ok "report (aggregates + payroll)"
|
||||||
|
else
|
||||||
|
bad "report" "$(printf '%s' "$rep" | tr '\n' '|' | cut -c1-160)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 5. staff: index probe + backlink + ref navigation --------------
|
||||||
|
st="$(run staff Engineering 2>&1)"
|
||||||
|
if [[ "$st" == *"STAFF Asha 9200000 (Engineering)"* \
|
||||||
|
&& "$st" == *"STAFF Chidi 7300000 (Engineering)"* ]]; then
|
||||||
|
ok "staff (unique probe + backlink scan + ref nav)"
|
||||||
|
else
|
||||||
|
bad "staff" "$(printf '%s' "$st" | tr '\n' '|' | cut -c1-160)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 6. raise: update-through-row, reflected in a re-report ----------
|
||||||
|
run raise Operations 5 >/dev/null 2>&1
|
||||||
|
if run report 2>&1 | grep -q "^DEPT Operations headcount=2 avg=6457500"; then
|
||||||
|
ok "raise (update-through-row, durable)"
|
||||||
|
else
|
||||||
|
bad "raise" "operations average did not move to 6457500"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 7. drop: FK restrict (Engineering still has staff) --------------
|
||||||
|
out="$(run drop Engineering 2>&1)"; rc=$?
|
||||||
|
if [[ "$out" == *"restricted"* && $rc -eq 4 ]]; then
|
||||||
|
ok "drop restricted by FK (department has staff)"
|
||||||
|
else
|
||||||
|
bad "drop restrict" "got rc=$rc: $(printf '%s' "$out" | tr '\n' '|' | cut -c1-100)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 8. persistence: a fresh process still sees every acked write ----
|
||||||
|
if run report 2>&1 | grep -q "^PAYROLL 46315000"; then
|
||||||
|
ok "persistence (replay: raised payroll survives restart)"
|
||||||
|
else
|
||||||
|
bad "persistence" "payroll after restart not 46315000"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
printf 'employee-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail"
|
||||||
|
[[ $fail -eq 0 ]]
|
||||||
2
tests/corpus/run/db-fk-restrict-catch/fixture.out
Normal file
2
tests/corpus/run/db-fk-restrict-catch/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
eng restricted
|
||||||
|
ops deleted
|
||||||
23
tests/corpus/run/db-fk-restrict-catch/fixture.wo
Normal file
23
tests/corpus/run/db-fk-restrict-catch/fixture.wo
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
-- the restrict trap is catchable; a free (unreferenced) row deletes.
|
||||||
|
@table(name: "dept", index: [name])
|
||||||
|
class Dept {
|
||||||
|
name: Text
|
||||||
|
staff: backlink Emp.dept
|
||||||
|
}
|
||||||
|
@table(name: "emp", index: [dept])
|
||||||
|
class Emp {
|
||||||
|
name: Text
|
||||||
|
dept: ref Dept
|
||||||
|
}
|
||||||
|
fn main() {
|
||||||
|
let eng = insert Dept { name: "eng" }
|
||||||
|
let ops = insert Dept { name: "ops" }
|
||||||
|
insert Emp { name: "asha", dept: eng }
|
||||||
|
let de = from x in Dept where x.name == "eng" take 1 select x
|
||||||
|
let r1 = try delete de[0] catch (e) nil
|
||||||
|
if r1 == nil { print("eng restricted") }
|
||||||
|
let dop = from x in Dept where x.name == "ops" take 1 select x
|
||||||
|
let r2 = try delete dop[0] catch (e) nil
|
||||||
|
if r2 == nil { print("ops restricted (WRONG)") }
|
||||||
|
print("ops deleted")
|
||||||
|
}
|
||||||
1
tests/corpus/trap/db-fk-restrict/fixture.trap
Normal file
1
tests/corpus/trap/db-fk-restrict/fixture.trap
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
11
|
||||||
18
tests/corpus/trap/db-fk-restrict/fixture.wo
Normal file
18
tests/corpus/trap/db-fk-restrict/fixture.wo
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- iteration 9b: deleting a row a `ref` still points at traps WO_T_FK
|
||||||
|
-- (restrict). Uncaught here: the trap code (11) is the assertion.
|
||||||
|
@table(name: "dept", index: [name])
|
||||||
|
class Dept {
|
||||||
|
name: Text
|
||||||
|
staff: backlink Emp.dept
|
||||||
|
}
|
||||||
|
@table(name: "emp", index: [dept])
|
||||||
|
class Emp {
|
||||||
|
name: Text
|
||||||
|
dept: ref Dept
|
||||||
|
}
|
||||||
|
fn main() {
|
||||||
|
let d = insert Dept { name: "eng" }
|
||||||
|
insert Emp { name: "asha", dept: d }
|
||||||
|
let ds = from x in Dept where x.name == "eng" take 1 select x
|
||||||
|
delete ds[0]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue