- 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>
226 lines
13 KiB
Markdown
226 lines
13 KiB
Markdown
# `@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`](../../superpowers/plans/2026-08-01-db-engine-binding.md)):
|
||
> Tasks 3–6 below consume its row storage, WAL, indexes and select subset.
|
||
> Board: [00-status.md](../../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`](../../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.
|