writeonce/docs/stories/databasev2/14-shop-workload.md
shoney.arickathil 96af299663 docs(db2-14): the shop workload story — what an order-taking web app needs from the store (refine)
- inserted 2026-09-12 from a developer question ("would I build an
  e-commerce site on writeonce?"): the load is a non-question — a hundred
  orders a minute is under two durable writes a second against an engine
  that group-commits thousands; what the developer hits is the SHAPE of the
  query and schema surface, measured against PostgreSQL habits
- goals, each its own future slice: range queries and ordering through an
  ordered index (today's indexes are equality-only hash buckets); `skip`
  beside `take` (specced 2026-08-15, never built); group-by aggregation for
  the reports (parser accepts, types.ml refuses — owner: language track);
  composite unique, check rules, on-delete policy beside FK restrict;
  export/import and a read-only attach (databasev2 9)
- Given/When/Then per page or report of docs/examples/shop; out of scope;
  the forks left open for a brainstorm before any slice starts; progress
  and history empty — `status: pending`, `readiness: refine`

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit f33ae982c954d5478d549a6dc5f15463de43ab1b)
2026-09-15 01:16:24 +02:00

10 KiB

track iteration status readiness
databasev2 14 pending refine

databasev2 14 — the shop workload: what an order-taking web app needs from the store

Status: ⬜ pending · design refine — see the status board. Part of Story — databasev2: the database beyond RAM.

Inserted 2026-09-12 from a developer-facing question: "would I build an e-commerce site on writeonce?" The load side is a non-question — a hundred orders a minute is under two durable writes a second against an engine that group-commits thousands. What the developer hits is the SHAPE of the query and schema surface, measured against what the PostgreSQL tree under .dev/reference/postgresql gives them by habit. This iteration names those gaps as one workload and lets each land as its own slice.

The user

A developer building a shop on porch: a Product catalogue, Order rows with a ref Product, a stock count to decrement, a listing page, an order history page, and a weekly revenue report. Their instinct is PostgreSQL. They will keep writeonce only if the everyday statements are as short as SQL and the mistakes SQL catches for them are caught here too. The driving workload is docs/examples/shop, extended as this iteration lands; every criterion below is a page or a report that sample must serve.

Goals

  • Range queries and ordering through an index. A product listing "under 20 euros, cheapest first" and an order history "last 30 days, newest first" read an ordered index and stop when the range ends, instead of scanning the table and sorting the result. The PostgreSQL shape is access/nbtree behind executor/nodeIndexscan.c; writeonce's indexes today are hash buckets that answer equality only (database/src/table.c, wo_idx_probe).
  • Pagination. skip joins take so a listing can show page three. The spec already names it (2026-08-15-table-relations-query-design.md section 3, clause 6); only take was built.
  • Reports through group-by aggregation. "Revenue per day" and "top ten products" as one query with group … by … into g and sum, count, avg, min, max. Parked from language 9b on 2026-08-16: the parser accepts the clause, types.ml refuses it. This half belongs to the language track (compiler surface, anonymous projection records); the iteration lists it because the workload needs it, and names the owner.
  • Schema-level validity rules. A composite unique key (sku together with warehouse), a check rule (qty > 0, price >= 0), and a declared on-delete policy for a ref (cascade the order lines, or set the reference to nil) beside today's FK restrict. Each is one line in the class today and a forgotten if in a handler tomorrow. PostgreSQL's shapes are the constraint machinery under catalog/ and commands/.
  • Operations while the shop runs. An export of the store to a text form a human can read and a fresh binary can import, and a read-only attach for a second process (the support tool, the report script) — the two habits pg_dump and a second psql session give a PostgreSQL user. Attach is databasev2 9's territory and stays there; this iteration owns export/import and states the attach dependency.

Acceptance Criteria

Met: none yet.

Outstanding:

  • Given Product declared with an ordered index on price, when the listing asks for products under a price sorted ascending with take, then the query lowers to an index-range probe (visible in the disassembly as the range builtin, not DB_SCAN), returns exactly the rows a full scan plus sort returns, and its cost grows with the result size, not the table size — measured on docs/examples/shop at ten thousand products.
  • Given an order history query with order by created_at desc take 20 skip 40, when it runs, then it returns page three exactly as PostgreSQL's OFFSET 40 LIMIT 20 would over the same rows, and skip without order by is a compile error (an unordered page is a bug).
  • Given the shop's revenue report written as a group-by query over Order with sum(g.total) and count(g) per day, when compiled and run, then the numbers equal the hand-rolled loop the sample uses today, and referencing a dropped column after the projection is a compile error.
  • Given a class with a composite unique key and a check rule, when a duplicate pair or a violating value is inserted or updated, then the statement traps by name before RAM moves, catchable like @unique, and the same violation on replay is refused as corruption — the engine never trusts a log a rule would have rejected.
  • Given a ref declared with an on-delete policy, when the target row is deleted, then cascade removes the referencing rows in the same barrier and set-nil clears the references in the same barrier, both inside one implicit transaction, and the default stays restrict — byte-identical behaviour for every class declared before this landed.
  • Given a running shop with a populated store, when the export command runs against a copy of its log, then it writes a text form that round-trips through import into a fresh store whose replayed rows are byte-identical, and a human can read a row in it without tooling.
  • Given every standing gate, when re-run, then unchanged: the index kind, the clauses and the rules are opt-in, and a class or query written before this iteration compiles and runs byte-identically.

Out Of Scope

  • A second index type beyond one ordered kind (no GIN, GiST, BRIN, no full-text search) — the ordered index covers the shop; text search is its own question.
  • Isolation levels and MVCC — the engine is single-writer on shard 0 and transaction { } (language 18) already gives one-barrier atomicity; there is nothing to isolate from.
  • Replication, read replicas, high availability — the single-binary story stands.
  • Money as a decimal type — Int cents is the doctrine; a Float price in the sample is the sample's problem and this iteration corrects it in the shop, not the language.
  • Attach for a second process — databasev2 9, referenced not absorbed.

Info — the forks (open, brainstorm before any slice starts)

  1. The ordered index's storage. Options: a sorted array per table rebuilt on change (simple, O(n) insert), a skip list or B-tree in RAM rebuilt from the log at boot (PostgreSQL's nbtree is on-disk and page-based, which the log-is-the-store doctrine rejects), or a sorted run per slab merged at query time. Keys-resident tables (databasev2 2) hold no row bodies in RAM, so the index must carry the key itself. Decide after measuring the shop at ten thousand and one million products.
  2. Grammar for the index kind. @table(index: [price]) today means hash; options: a second argument key (ordered: [price]), a per-column marker, or making every index ordered and dropping hash (simpler surface, slower equality probes — measure).
  3. Range probe surface. Whether where comparisons on an indexed column lower automatically to the probe (the 9b precedent for equality) or the query must name the index; and how order by on the indexed column proves it walks the index (the disassembly, as 9b did).
  4. Composite unique and check rules: where they live. @table(unique: [sku, warehouse]) beside index:, and a check as a class annotation over a boolean expression of fields — evaluated where? Compile-time constant folding is impossible for a row; the check must be a small engine-side interpreter or a compiled method the engine calls, and the latter crosses the two-memory-worlds boundary. The lightest honest option may be a compiled predicate method the STATEMENT calls before the builtin, which the engine cannot verify on replay — state the trade-off before picking.
  5. On-delete cascade inside the log. A cascade deletes N rows for one statement: one kind-6 record with the members (language 18's block, opened implicitly), or N plain records under one barrier. The block is honest about atomicity; decide whether an implicit block may open while an explicit one is already open (fork 3c of language 18 says one per process).
  6. Export format. Options: one JSON object per line with class name and fields (readable, the json builtins already exist), a .wo source file of insert statements (importable by compiling it — no import tool at all), or the WAL's own record dump. The insert-source form needs no new surface and is grep-able; ids would be reassigned on import, which breaks refs unless the export orders classes by dependency.
  7. Group-by ownership. Language track iteration, with the shop as a second acceptance workload beside employee; this iteration only carries the criterion. Confirm the number with the language track before writing the story.

Progress

# Slice Owner Size State
A skip clause (grammar, typing, lowering; skip without order by refused) language track / codd-zack S ⬜
B ordered index: storage, grammar, range probe, order by walk, shop listing legs codd-shoney brainstorm → codd-zack L ⬜
C composite unique + check rules + replay refusal codd-shoney brainstorm → codd-zack M ⬜
D on-delete policy (cascade, set-nil) as an implicit block after language 18 T7 M ⬜
E export / import codd-shoney brainstorm → codd-zack M ⬜
F group-by aggregation language track (own story) M ⬜
G shop sample grows the pages and the report; codd-cyril gate legs codd-cyril M ⬜

History

  • 2026-09-12: story created from the "would I build a shop on this" analysis; seven forks open, readiness: refine. Attach stays databasev2 9; group-by stays the language track's.