From ebcbf460dfc795484995b5b439a32d1bb73dc374 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sat, 15 Aug 2026 09:49:54 +0200 Subject: [PATCH] docs: employee sample (target workload) + engine moves to database/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/examples/employee: Department/Employee @table classes with @unique, [dept] and [dept, salary] indexes, ref/backlink pair; modes seed/report/staff/raise/drop per the 9b spec section 6 — written AHEAD of the features (sample-first, like log-watcher); README states it does not compile on today's toolchain and links the plans that compile toward it - report mode is the GroupBy showcase: group-and-reduce projection {headcount, avg, min, max} ordered by avg desc, whole-query sum for payroll; staff proves both navigation directions + index probe; drop proves delete restrict (trap asserted) - engine directory decision (user, 2026-08-15): database/ is its own top-level dir, statically linked into wovm — file structures updated in the iteration-9 plan and the 9b plan - gap found by writing the sample: iteration 9's subset lacks a `delete` statement and restrict needs one — added to 9b plan Task 3 - 9b plan Task 6 notes the sample is pre-authored and authoritative Co-Authored-By: Claude Opus 5 (1M context) --- docs/examples/employee/README.md | 32 +++++ docs/examples/employee/main.wo | 109 ++++++++++++++++++ docs/examples/employee/types.wo | 24 ++++ docs/examples/employee/wo.toml | 12 ++ .../2026-08-15-employee-relations-query.md | 21 +++- .../plans/2026-08-01-db-engine-binding.md | 7 +- 6 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 docs/examples/employee/README.md create mode 100644 docs/examples/employee/main.wo create mode 100644 docs/examples/employee/types.wo create mode 100644 docs/examples/employee/wo.toml diff --git a/docs/examples/employee/README.md b/docs/examples/employee/README.md new file mode 100644 index 0000000..49ac282 --- /dev/null +++ b/docs/examples/employee/README.md @@ -0,0 +1,32 @@ +# employee — the database track's acceptance workload + +> **Status: target workload — does not compile on today's toolchain.** +> This sample is written *ahead of* the features it exercises, exactly as +> log-watcher was written ahead of iterations 5–7: the sample is the test, +> and the plans compile toward it. It becomes buildable when iteration 9 +> (engine: [`2026-08-01-db-engine-binding.md`](../../superpowers/plans/2026-08-01-db-engine-binding.md)) +> and iteration 9b (query surface: +> [`2026-08-15-employee-relations-query.md`](../../plan/compiler/2026-08-15-employee-relations-query.md)) +> land. Normative semantics: +> [the 9b spec](../../superpowers/specs/2026-08-15-table-relations-query-design.md). + +Two `@table` classes and every 9b feature load-bearing: + +| Mode | What it proves | +| --- | --- | +| `employee seed` | `insert` + WAL-before-ack; a second run catches the `departments.name` `@unique` trap (`SEED-DUP`, exit 3) | +| `employee report` | `group … by … into g` lowered as one hash pass; `count`/`avg`/`min`/`max` per department; `order by avg(g.salary) desc`; whole-query `sum` for payroll | +| `employee staff ` | unique-name **index probe** (asserted via the engine's probe counter, not assumed), `backlink` scan one way, `e.dept.name` ref navigation the other | +| `employee raise ` | update through a query result; a missing department takes the empty-query path | +| `employee drop ` | delete **restrict**: a department with staff traps (exit 4); the trap code is the acceptance's assertion | + +The engine lives in `database/` (statically linked into `wovm` — still one +binary). Rows are RAM-authoritative, WAL-durable; a kill -9 between `seed` +and `report` followed by an identical `report` is part of the acceptance +script (iteration 9's replay, proven on this workload). + +Data shape: `Department { name @unique, staff: backlink Employee.dept }`, +`Employee { name, salary (cents), hired (epoch ms), dept: ref Department }`, +indexes `[name]`, `[dept]`, `[dept, salary]`. Salaries wrap like all language +arithmetic; `avg`/`min`/`max` are `?Int` because an empty group is data, not +a fault. diff --git a/docs/examples/employee/main.wo b/docs/examples/employee/main.wo new file mode 100644 index 0000000..9b6975b --- /dev/null +++ b/docs/examples/employee/main.wo @@ -0,0 +1,109 @@ +-- Employee management — iteration 9/9b's acceptance workload. +-- Four modes, each existing to make one feature load-bearing: +-- seed inserts (WAL, @unique trap on the second run) +-- report the GroupBy showcase (headcount/avg/min/max, payroll) +-- staff relation navigation both directions + index probe +-- raise

update through a query result; empty query => nil path +-- drop delete restrict: a department with staff must trap + +fn main(args: multi Text) -> Int { + if len(args) >= 1 and args[0] == "seed" { return seed(); } + if len(args) >= 1 and args[0] == "report" { return report(); } + if len(args) >= 2 and args[0] == "staff" { return staff(args[1]); } + if len(args) >= 3 and args[0] == "raise" { + let pct = parse_int(args[2]); + if pct == nil { print_err("raise: must be a number"); return 2; } + return raise(args[1], pct); + } + if len(args) >= 2 and args[0] == "drop" { return drop_dept(args[1]); } + print_err("usage:"); + print_err(" employee seed seed departments and employees"); + print_err(" employee report aggregates by department"); + print_err(" employee staff list a department's staff"); + print_err(" employee raise

raise a department's salaries p%"); + print_err(" employee drop delete a department (restrict demo)"); + return 1; +} + +-- Inserts are WAL-logged before acknowledgment (iteration 9). A second run +-- hits the departments.name @unique index and traps; catching it here is the +-- sample's unique-violation acceptance line. +fn seed() -> Int { + let eng = try insert Department { name: "Engineering" } catch (e) nil; + if eng == nil { + print("SEED-DUP departments already seeded (unique violation caught)"); + return 3; + } + let ops = insert Department { name: "Operations" }; + let sales = insert Department { name: "Sales" }; + + insert Employee { name: "Asha", salary: 9200000, hired: 1704067200000, dept: eng }; + insert Employee { name: "Bram", salary: 8100000, hired: 1706745600000, dept: eng }; + insert Employee { name: "Chidi", salary: 7300000, hired: 1709251200000, dept: eng }; + insert Employee { name: "Dora", salary: 6400000, hired: 1711929600000, dept: ops }; + insert Employee { name: "Emil", salary: 5900000, hired: 1714521600000, dept: ops }; + insert Employee { name: "Farah", salary: 8800000, hired: 1717200000000, dept: sales }; + + print("SEEDED 3 departments, 6 employees"); + return 0; +} + +-- One GROUP BY after another: group-and-reduce lowers to a single hash pass +-- (no group objects), avg/min/max are ?Int because an empty group is data. +fn report() -> Int { + let rows = 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), min_salary: min(g.salary), + max_salary: max(g.salary) }; + for r in rows { + print("DEPT ${r.dept} headcount=${r.headcount} avg=${r.avg_salary} min=${r.min_salary} max=${r.max_salary}"); + } + let payroll = sum(from e in Employee select e.salary); + print("PAYROLL ${payroll}"); + return 0; +} + +-- Both navigation directions on one screen: the department found by its +-- unique-name index (a probe, not a scan — the acceptance asserts the probe +-- counter), its `staff` backlink scanned, and each employee's forward +-- `e.dept.name` printed to prove ref navigation. +fn staff(name: Text) -> Int { + let ds = from d in Department where d.name == name take 1 select d; + if len(ds) == 0 { print_err("no such department: ${name}"); return 1; } + let d = ds[0]; + for e in from s in d.staff order by s.salary desc select s { + print("STAFF ${e.name} ${e.salary} (${e.dept.name})"); + } + return 0; +} + +-- Update through a query result; a missing department exercises the +-- empty-query path (the loop body never runs, nothing prints but the DONE). +fn raise(name: Text, pct: Int) -> Int { + let ds = from d in Department where d.name == name take 1 select d; + if len(ds) == 0 { print("RAISE ${name}: no such department (0 rows)"); return 0; } + let d = ds[0]; + let n = 0; + for e in from s in d.staff select s { + e.salary = e.salary + e.salary * pct / 100; + n = n + 1; + } + print("RAISE ${name} ${pct}% applied to ${n} employees"); + return 0; +} + +-- Restrict is the only FK action: deleting a department that employees still +-- reference traps, and the trap code is the acceptance's assertion. +fn drop_dept(name: Text) -> Int { + let ds = from d in Department where d.name == name take 1 select d; + if len(ds) == 0 { print_err("no such department: ${name}"); return 1; } + let ok = try delete ds[0] catch (e) nil; + if ok == nil { + print("DROP ${name}: restricted (staff still reference it)"); + return 4; + } + print("DROP ${name}: deleted"); + return 0; +} diff --git a/docs/examples/employee/types.wo b/docs/examples/employee/types.wo new file mode 100644 index 0000000..324074d --- /dev/null +++ b/docs/examples/employee/types.wo @@ -0,0 +1,24 @@ +-- The two tables the whole sample exists to relate. Every class IS a table; +-- @table only configures storage (name, indexes) — the language's oldest +-- doctrine. The [dept] index serves the `staff` backlink and the restrict +-- check; [dept, salary] serves the per-department salary queries and the +-- report's ordering inside a department. + +@table(name: "departments", index: [name]) +class Department { + name: Text @unique + + -- Not a stored column: the declared inverse of Employee.dept. Reading + -- `d.staff` is a secondary-index scan of employees.dept and yields + -- `multi Employee`. + staff: backlink Employee.dept +} + +@table(name: "employees", index: [dept], index: [dept, salary]) +class Employee { + name: Text + salary: Int -- cents; sum wraps like all language arithmetic + hired: Int -- epoch ms + dept: ref Department -- FK: stored as the department's row id, checked + -- by a primary-index probe on insert/update +} diff --git a/docs/examples/employee/wo.toml b/docs/examples/employee/wo.toml new file mode 100644 index 0000000..2b8e567 --- /dev/null +++ b/docs/examples/employee/wo.toml @@ -0,0 +1,12 @@ +name = "employee" +version = "0.1.0" +description = "Employee management — iteration 9/9b acceptance workload: @table storage, ref/backlink relations, compiler-checked GroupBy queries" + +[runtime] +wo = ">= 0.1" + +# `woc .` builds target/employee once iterations 9 (engine) and 9b (query +# surface) land; until then this sample is the target the plans compile +# toward, sample-first like log-watcher was. +[build] +runtime = "../../../runtime/wovm" diff --git a/docs/plan/compiler/2026-08-15-employee-relations-query.md b/docs/plan/compiler/2026-08-15-employee-relations-query.md index e1579b5..46f005b 100644 --- a/docs/plan/compiler/2026-08-15-employee-relations-query.md +++ b/docs/plan/compiler/2026-08-15-employee-relations-query.md @@ -19,8 +19,10 @@ to bytecode loops over engine cursor builtins, proven by a new `docs/examples/employee` sample whose acceptance script is the gate. **Architecture:** compiler front (`compiler/src/{lexer,parser,ast,types,owner,emit}.ml`) -for the query surface; runtime (`runtime/src/{table,db,builtin}.c` from -iteration 9, plus a new `query.c` for cursors and group hashing). The compiler +for the query surface; the database engine (`database/src/` — its own +top-level directory per the 2026-08-15 decision, statically linked into wovm; +`table.c`/`db.c` from iteration 9, plus a new `query.c` for cursors and group +hashing). The compiler is the planner: index selection happens at lowering, the VM never sees a plan tree. Reference semantics: System.Linq for operator meaning, PostgreSQL's nodeAgg/ri_triggers for execution and integrity vocabulary (both surveyed in @@ -50,8 +52,8 @@ new builtin ids appended to the format doc. compiler/src/lexer.ml parser.ml ast.ml query-expression grammar, clause AST (Task 1) compiler/src/types.ml range/group scopes, navigation, projection synthesis (Task 2) compiler/src/owner.ml emit.ml query ownership + lowering to cursor builtins (Task 5) -runtime/src/query.c query.h cursors, group hash, transition/finalize aggregates (Tasks 3, 4) -runtime/src/db.c table.c FK restrict checks at the row choke points (Task 3) +database/src/query.c query.h cursors, group hash, transition/finalize aggregates (Tasks 3, 4) +database/src/db.c table.c FK restrict checks at the row choke points (Task 3) docs/examples/employee/ the acceptance workload (Task 6) scripts/employee-accept.sh the gate (Task 6) docs/plan/oop-vm/00-wob-format.md appended builtin ids (Tasks 3-5) @@ -119,6 +121,10 @@ discarded — a check is an index probe, never query text). the ownership pass enforces it, mirror of the container-read borrow). - [ ] FK trap + restrict trap wired through the row API; a debug-build probe counter exposed for Task 5's index-selection proof. +- [ ] The `delete` statement (point delete of a row value) lands here too: + iteration 9's subset is insert/select/update-point, and restrict has + nothing to restrict without it — same typed-AST-plus-builtin shape as + insert, WAL remove record already specified by iteration 9's Task 2. - [ ] db-corpus fixtures from iteration 9's plan extended with one FK-violation and one restrict fixture (trap-code exact); ASan green; commit locally. @@ -176,8 +182,11 @@ index-probe proof), `raise` (update through a query; missing department ⇒ the empty-query nil path), the restrict-trap demo, and a kill -9 between `seed` and `report` proving replay on this workload. -- [ ] Sample written; `woc docs/examples/employee` compiles with zero - diagnostics; the binary's modes run. +- [ ] Sample source pre-authored 2026-08-15 (`docs/examples/employee/` — + the target workload, sample-first like log-watcher was); this task makes + `woc docs/examples/employee` compile it with zero diagnostics and the + binary's modes run. The sample is authoritative: divergence between it + and the spec is resolved in the spec's favor and committed. - [ ] `scripts/employee-accept.sh` + `just employee` module: every mode checked with exact expectations, trap codes asserted, crash step included; soak-style RSS/fd sampling reused from the log-watcher diff --git a/docs/superpowers/plans/2026-08-01-db-engine-binding.md b/docs/superpowers/plans/2026-08-01-db-engine-binding.md index d934919..81b4ef2 100644 --- a/docs/superpowers/plans/2026-08-01-db-engine-binding.md +++ b/docs/superpowers/plans/2026-08-01-db-engine-binding.md @@ -27,7 +27,8 @@ ## File Structure ``` -runtime/src/ +database/src/ the engine is its own top-level directory (user decision + 2026-08-15), statically linked into wovm — one binary, unchanged table.c table.h class-shaped row storage: slabs, slots, id alloc, indexes (Tasks 1, 4) wal.c wal.h typed-row WAL records, group commit, replay (Task 2) db.c db.h statement executors: insert/select/update-point (Tasks 3, 5) @@ -36,6 +37,10 @@ tests/corpus/db/ DB fixtures incl. crash/replay (Task 6) docs/plan/oop-vm/04-db-binding.md row format, WAL record layout, query subset (Task 1) ``` +The engine's headers are included by `runtime/src` (the VM calls the row API); +`runtime/Makefile` compiles `database/src/*.c` into every `wovm` target, +sanitizers included. `database/` gets its own CODE-LOGIC.md as code lands. + --- ### Task 1: Class-shaped row storage