diff --git a/docs/stories/databasev2/14-shop-workload.md b/docs/stories/databasev2/14-shop-workload.md new file mode 100644 index 0000000..bc7b177 --- /dev/null +++ b/docs/stories/databasev2/14-shop-workload.md @@ -0,0 +1,176 @@ +--- +track: databasev2 +iteration: "14" +status: pending +readiness: refine +--- + +# databasev2 14 — the shop workload: what an order-taking web app needs from the store + +> **Status:** ⬜ pending · design `refine` — see the +> [status board](../00-status.md). Part of +> [Story — databasev2: the database beyond RAM](00-story.md). +> +> **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`](../../examples/shop/README.md), 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`](../../superpowers/specs/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](09-cross-program-tables.md)'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 + `ref`s 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.