- 28 story files gain YAML frontmatter: iteration id, status (mirrors folder), chain position (7 files, positions 1-6) - board-views.md: Dataview queries (not-done, by-status lanes, chain order, active); Kanban caveat — view only, frontmatter is source of truth, folder move + status key change together - board points at the views Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.9 KiB
| iteration | status |
|---|---|
| 27 | hold |
Iteration 27 — query grammar, driven by real embedded-DB corpora
Format: fiberloom
product/story-iteration-template. Part of Story — one language, one runtime, one database, one binary.Inserted 2026-08-16. A query-surface iteration in the 9b family: the language-integrated query grows to cover the grammar that real applications backed by an embedded SQL database actually use — measured, not guessed, by cataloguing a real app and adding only the constructs it depends on. The method is the postgres/System.Linq reference pattern applied to a whole application: an embedded-SQLite app is a grammar corpus; each one analysed drives a grammar increment.
No spec exists yet. The forks in Info are genuine decisions.
Why this iteration exists
The 9b query surface ships scan / where / select / order by / take,
ref/backlink navigation, insert / update / delete, @unique, and FK
restrict (all running, docs/examples/employee). What it does NOT yet cover
is everything past that — and "everything" is unbounded, so the sensible way
to choose the next grammar is to point at a real program that uses an
embedded SQL database and add exactly what it needs.
Corpus #1: ~/projects/skillhost (a C++ MCP host, embedded SQLite as its
in-memory skill catalog; surveyed 2026-08-16). Its entire SQL footprint is
one file (src/catalog/catalog.cpp, 172 lines): one table + index, a
single-row parameterized INSERT, and four SELECTs. Mapping each statement
to the writeonce query surface:
| skillhost statement | writeonce today |
|---|---|
INSERT INTO skills (…) VALUES (?,…) |
✅ insert Skill { … } |
SELECT … WHERE name = ? |
✅ from s in Skill where s.name == n select s (unique-index probe) |
SELECT … ORDER BY name |
✅ from s in Skill order by s.name select s |
SELECT COUNT(*) FROM skills |
❌ whole-query count |
SELECT … WHERE NOT EXISTS (SELECT 1 FROM skills c WHERE c.parent = s.name) ORDER BY name |
❌ correlated not exists subquery |
So the real grammar gap this corpus demands is two constructs, and — importantly — neither is the parked full group-by/projection machinery. Everything else SQLite offers (JOIN, HAVING, LIMIT/OFFSET, DISTINCT, CTE, window functions, UNION, UPSERT, RETURNING, JSON1, FTS5, triggers, generated columns) skillhost does not touch, so none of it is in scope here.
Goals
- Whole-query
count:count(from s in Table [where …] select …)yields anInt— the trivial, group-free special case of aggregation (materialize the query, take its length). It is a stepping stone toward, and independent of, the parked group-by aggregation. - Existence subqueries:
exists(<query>)andnot exists(<query>)as a boolean, usable in awhereguard, where the inner query may reference the outer range variable (a correlated subquery — skillhost's roots-of-the- tree query). Short-circuits: existence needs only the first matching row. - Parity, proven by translation: a new
docs/examples/skill-catalogsample mirrors skillhost's schema and expresses all five of its statements in writeonce, producing results identical to what skillhost's SQLite returns for the same data.
Acceptance Criteria
- What to achieve?
- Given
count(from s in Skill select s)andcount(from s in Skill where s.parent == nil select s), - when compiled and run,
- then each yields the correct row count as an
Int, lowered to a materialize-then-length over the existing scan/where loop — no group machinery, provable by disassembly.
- Given
- What to achieve?
- Given
from s in Skill where not exists(from c in Skill where c.parent == s.name select c) order by s.name select s— the roots of the skill tree, - when run over a catalog with parent/child skills,
- then it returns exactly the childless skills in name order, and the
inner query correctly sees the outer
s(correlation), matching skillhost'sNOT EXISTSresult row-for-row.
- Given
- What to achieve?
- Given the
skill-catalogsample seeded with the same rows a skillhost session would load, - when each of skillhost's five catalog operations is run through the writeonce translation,
- then every result matches, and the sample's README records the one translation choice made (see fork 1).
- Given the
Out Of Scope
- Full group-by aggregation (
group … by … into g … select { count(g), avg(g.f) }) — still parked (9b's deferral). Whole-querycounthere is the degenerate case, not the general one;sum/avg/min/maxas query aggregates ride with the group-by iteration. - Every SQL construct skillhost does not use: JOIN, HAVING, LIMIT/OFFSET
(writeonce has
take;skip/offset waits for a workload), DISTINCT, CTE /WITH RECURSIVE, window functions, UNION/INTERSECT/EXCEPT, UPSERT /ON CONFLICT,RETURNING, multi-rowVALUES,INSERT … SELECT, JSON1 operators, FTS5, triggers, generated columns, explicit collation. Each enters only when a corpus demands it — that is this iteration's whole method. - A resident SQL parser — the doctrine stands: skillhost is a grammar corpus to translate against, never a syntax writeonce adopts. No SQL text in a compiled image.
- Subqueries in general beyond correlated
exists/not exists(e.g. a subquery producing a value,IN (subquery), scalar subqueries) — add when a corpus uses them.
Info
Forks the spec must settle:
1. Does skillhost's NOT EXISTS even need a subquery, or does a backlink
express it? skillhost's skills table is self-referential (parent → a
name), and its roots query is "skills no other skill names as parent." In
writeonce that is naturally a backlink emptiness: give Skill a
children: backlink Skill.parent and write where len(s.children) == 0 — no
subquery at all, using machinery that already exists (backlink probe) plus a
len on the result. So the corpus may be fully expressible today once
count/len over a query lands, making exists strictly optional for
skillhost. Leaning: ship whole-query count/len (needed regardless), and
add exists/not exists as the general construct for correlations a backlink
cannot express (a correlation on a non-relation column) — but let the
skill-catalog sample use the idiomatic backlink form for its roots query and
record the subquery form as the alternative. This keeps the new surface
minimal and honest about what the corpus actually forces.
2. count vs len. writeonce already has len/count builtins on a
multi. A query yields a multi, so len(from … select …) may already work
with no new surface at all — the "gap" could be purely that a bare query in
argument position typechecks and lowers. Leaning: verify len(<query>) works
end to end first; if it does, whole-query count is a documentation/alias
matter, not new code, and the only real new construct in this iteration is the
existence subquery (fork 1's optional half). The spec must confirm this
against the running compiler before committing scope.
3. Correlated-subquery execution. If exists lands, the inner query
references the outer row, so it re-evaluates per outer row (a nested loop) or
uses the referenced index. Leaning: nested-loop for correctness first (the
data is small; skillhost's catalog is dozens of skills), index-backed probe as
the optimization the spec records — mirroring how 9b did scans before index
selection.
Method note (the durable part): this iteration establishes the pattern for all future query-grammar growth — catalogue a real embedded-DB application, add only the constructs it uses, translate its statements 1:1 as the acceptance, and park the rest by name. skillhost is corpus #1 and, tellingly, needs almost nothing beyond what 9b already shipped — which is itself the strongest evidence that the 9b surface was scoped right.
Proposed Solution
- Brainstorm the spec settling the three forks — especially forks 1/2,
which may collapse the iteration to "confirm
len(<query>)works + addexists," a very small increment. - Acceptance workload:
docs/examples/skill-catalog—Skill { name: Text @unique, description: Text, location: Text, root: Text, parent: ?ref Skill, children: backlink Skill.parent }and a CLI mirroring skillhost's catalog operations (add, get-by-name, list, list-roots, count), each a direct translation of the corresponding SQLite statement, with an acceptance script asserting the same results skillhost produces. - Expected shape: small parser/type/emit additions for
exists/not exists(a query in boolean position, correlated), whole-querycount/lenconfirmed or wired, and the sample + script. No engine format change beyond what 9b already appended.