docs: employee sample (target workload) + engine moves to database/
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
7f9332f5d2
commit
1dc2912047
6 changed files with 198 additions and 7 deletions
32
docs/examples/employee/README.md
Normal file
32
docs/examples/employee/README.md
Normal file
|
|
@ -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 <dept>` | 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 <dept> <pct>` | update through a query result; a missing department takes the empty-query path |
|
||||
| `employee drop <dept>` | 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.
|
||||
109
docs/examples/employee/main.wo
Normal file
109
docs/examples/employee/main.wo
Normal file
|
|
@ -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 <dept> relation navigation both directions + index probe
|
||||
-- raise <dept> <p> update through a query result; empty query => nil path
|
||||
-- drop <dept> 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: <pct> 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 <department> list a department's staff");
|
||||
print_err(" employee raise <department> <p> raise a department's salaries p%");
|
||||
print_err(" employee drop <department> 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;
|
||||
}
|
||||
24
docs/examples/employee/types.wo
Normal file
24
docs/examples/employee/types.wo
Normal file
|
|
@ -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
|
||||
}
|
||||
12
docs/examples/employee/wo.toml
Normal file
12
docs/examples/employee/wo.toml
Normal file
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue