writeonce/docs/plan/compiler/2026-08-15-employee-relations-query.md
shoney.arickathil 1749440351 docs: borrow/GC analysis for the database engine, into the iterations
- 9b spec gains section 6 "Ownership, borrows, and GC across the
  engine boundary": two one-way copy gates (no VM pointer enters a
  row, everything a select returns is copied out), so the collector
  never traces engine memory and the engine never touches refcounts
- row views are borrows WITHOUT a runtime net: rows share the VM's
  field encoding but not its header, so no borrow word backs them --
  the compile-time escape rule is load-bearing alone
- cursor stability settled: scans materialize their id list before
  the body, row updates through the view stay legal (raise mode
  updates an indexed column mid-scan and is the proving fixture),
  insert/delete on a table with an open cursor is a new WO-E5xx
- GC-pause interaction recorded: collector runs between statements,
  a long scan delays slices -- accepted, documented
- iteration-7b ordering constraint: GC inference must classify before
  table-field validation, diagnostic names the inference reason --
  noted in 7b story, iteration-9 plan constraints, 9b plan tasks
- stories 09/09b Info sections point at the analysis; 9b plan Tasks
  3/5 carry the enforceable checkboxes (ASan boundary assertion)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 09:57:53 +02:00

13 KiB
Raw Blame History

@table, Relations, Query — Implementation Plan (employee sample)

Status: ⬜ pending (story iteration 9b) — blocked on iteration 9's engine plan (2026-08-01-db-engine-binding.md): Tasks 3–6 below consume its row storage, WAL, indexes and select subset. Board: 00-status.md

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Style rule (user convention): concept, reason, and required behavior in words only; the executor writes the code.

Spec: docs/superpowers/specs/2026-08-15-table-relations-query-design.md (normative: the settled forks, the clause grammar, the aggregate semantics table, the lowering model), amended by the systems-track spec's program-mode contract for the sample's CLI.

Goal: @table classes queried in the language: comprehension queries with where/join/group/order/take/select, typed ref/backlink navigation with restrict integrity, and the LINQ aggregate vocabulary (count/sum/avg/min/max, GroupBy as group-and-reduce) — all lowered 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; 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 the spec, fork 3).

Tech Stack: OCaml stdlib (compiler), C11 libc (runtime). No new opcodes; new builtin ids appended to the format doc.

Global Constraints

  • The sample is the test. docs/examples/employee plus its acceptance script is 9b's gate; the only corpus additions are the db-corpus fixtures iteration 9 already plans. just oop-e2e, just woc-test, just wovm-test stay green as regression after every task.
  • Index doctrine (iteration 9, verbatim): secondary indexes are maintained only through the engine's row choke points; FK checks and backlink reads are index probes, never storage walks.
  • No SQL text anywhere — the 9b acceptance's disassembly criterion.
  • No function values. Every predicate/projection is expression syntax; a query value is never deferred or passed around.
  • Plans/specs in words; commits local only, never push; docs under docs/; CODE-LOGIC.md updated beside changed code.

File Structure

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)
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)

Task 1: Query expressions parse

Concept & reason: the comprehension grammar from spec section 3 — from/where/join/group…into/order by/take/skip/select, clause order fixed, aggregates as clause functions — becomes lexer keywords (contextual, so from/group/order stay legal identifiers outside a query), a clause AST, and parse diagnostics in a new WO-E5xx range (clause out of order, missing select, aggregate named outside a query). Fixed clause order is a parse rule, not a type rule, so the error lands on the exact token.

  • Grammar and AST for every clause; contextual keywords verified against the existing samples (no .wo file in the tree breaks).
  • WO-E5xx diagnostics with golden coverage in the existing woc-test suite (dump-ast goldens for well-formed queries, diagnostic goldens for each malformed shape).
  • Gates green; commit locally.

Task 2: Queries typecheck

Concept & reason: the surface's whole promise is compile-time checking. from e in Employee opens a scope where e's fields are the class's; ref navigation substitutes the target class's field set (e.dept.name); backlink reads type as multi of the source class; group … into g closes the range scope and opens the group scope, where g.key has the key's type and g.f is legal only inside an aggregate call; select { … } synthesizes an anonymous record type so later use of a dropped column is a compile error. Aggregate result types follow the spec's table exactly — count is Int, sum Int, avg/min/max are ?T because an empty group is data. Unknown table, unknown column (naming the class), navigation through a non-ref, bare group-member column, join sides not separable: each is its own WO-E5xx with a golden.

  • Scope machinery for range/group variables; navigation typing both directions; projection record synthesis interned like other class shapes.
  • Aggregate typing per the spec table; ?T results force the existing nil-handling style at use sites.
  • Diagnostic goldens for every error class named above; gates green; commit locally.

Task 3: Cursors and integrity in the engine

Concept & reason: the runtime side queries need, built on iteration 9's storage: a table-scan cursor (open by class id, advance, borrowed row view), a primary-index point read (ref navigation), a secondary-index cursor with longest-prefix probe (backlink reads, indexed where), all as builtins appended to the format doc. Integrity lands at the row choke points where the indexes already live: inserting/updating a non-nil ref probes the referenced primary index and traps on a miss (foreign-key violation, sibling of the unique trap); deleting a row still referenced traps (restrict) via the same secondary index a backlink requires — one probe, no new structure. Nil ref never probes (the MATCH SIMPLE rule); an update that leaves the key unchanged skips the check (both from the PostgreSQL RI survey, mechanism discarded — a check is an index probe, never query text).

  • Cursor builtins + borrowed-row-view lifetime rules written into the binding doc (a row view never escapes the loop that opened the cursor — the ownership pass enforces it, mirror of the container-read borrow). Rows have no borrow word (they share the VM's field encoding, not its header), so unlike every VM-heap borrow there is no runtime trap behind this rule — the compile-time check is load-bearing alone, which is why it gets its own diagnostic and goldens rather than riding on E30x.
  • Cursor stability per spec section 6: scans materialize their id list before the body runs and point-read per iteration; updates through the row view stay legal (exclusive row borrow, index maintenance at the row API); insert/delete targeting a table with an open cursor is a new WO-E5xx (the ownership pass carries the open-cursor table set through the loop body); read-only nested queries over the same table stay legal. The raise mode — updating an indexed column mid-scan — is the fixture that proves the materialized-id semantics.
  • 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.

Task 4: Group hash + aggregate execution

Concept & reason: the AggregateBy shape from the spec — group-and-reduce in one pass, no group objects. A group hash table builtin set: create (keyed by the group key's kind, nil legal), upsert-advance (locate-or-create the group, advance each aggregate's transition slot), drain (iterate groups, finalize, hand key + finals to the compiled projection loop). Transition/ finalize is PostgreSQL's split: avg carries sum+count in its transition state and divides at finalize; the "no row seen yet" state is distinct from "transition value is nil" so nil-skipping aggregates need no first-row special case. Aggregate semantics are the spec's table: empty ⇒ nil (or 0 for count/sum), nil elements skipped, sum wraps, avg truncates.

  • Builtins implemented; the input projected to only the columns the aggregates read before hashing (the nodeAgg memory lesson).
  • Fixture-level verification through iteration 9's db corpus (one grouped query, exact output) plus the sample in Task 6; ASan green; commit locally.

Task 5: Lowering — the compiler is the planner

Concept & reason: a query desugars to the bytecode loops the language already has. Scan or probe chosen at compile time: leading where equality conjuncts matched against declared indexes longest-prefix-first, probe emitted on a hit, scan otherwise — and the choice is demonstrated via Task 3's probe counter in the acceptance, not assumed. join builds a transient hash on the inner side and probes with the outer (nil never inserted, never probes). group emits the two-phase hash-aggregation loop from Task 4. order by materializes and stable-sorts (original-index tiebreak — the LINQ stability guarantee); take/skip slice the result. Ownership: row views stay loop-bound borrows; everything select emits is copied/built at the boundary under the established Text-copy and fresh-value rules — queries add no new ownership classes, and the ownership pass's existing drop machinery covers the query's temporaries because the lowering IS ordinary loops.

  • Desugar + lowering for every clause; disassembly of the sample's report mode shows loops and builtins, no plan tree, no text. The select boundary is the ownership bulkhead (spec section 6): everything a query returns is copied or freshly built, so no value anywhere points into a row slab after the query ends — asserted under ASan by mutating rows after a query and re-reading the query's results.
  • Index selection proven: the acceptance asserts probe-counter deltas for the indexed staff <department> path versus a full-scan query.
  • oop-e2e, woc-test, ASan-corpus gates green; commit locally.

Task 6: The employee sample is the acceptance

Concept & reason: spec section 7 verbatim — docs/examples/employee with Department/Employee (@table, @unique name, composite [dept, salary] index, ref/backlink pair), wo.toml manifest so woc . builds it, a just module beside it (the log-watcher convention), and scripts/employee-accept.sh as the gate: seed (insert + WAL + duplicate-department trap on the second run), report (headcount/avg/min/max by department, total payroll, ordered by average salary — byte-exact lines), staff (both navigation directions + 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 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 script's pattern for the report loop.
  • ASan run of the full acceptance: zero leaks (the log-watcher bar).
  • Docs: README beside the sample, CODE-LOGIC.md updates beside changed compiler/runtime code, format-doc builtin table final, board + story rows updated with measured numbers; commit locally.

Out of scope — deferred by name

  • Everything spec section 8 lists: set operators, outer joins, subqueries, composite group keys, groups as values, FK cascade/set-nil, deferred checks, SQL text in any role, cross-shard queries, LIVE, migrations, cost-based planning.
  • Sorted-grouping and partial-sort optimizations (recorded LINQ/postgres precedents; hash + full stable sort are this plan's only strategies).
  • The ecommerce sample's query rewrite (9b's fifth acceptance criterion) — it lands as its own follow-up once the employee gate is green, so this plan's blast radius stays one new sample.