diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index 843fdc1..49b1a6b 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -620,7 +620,7 @@ the language arc as v1 history. | # | Iteration | State | | --- | --- | --- | | 1 | [RAM ceiling: measure the breaking point](databasev2/01-ram-ceiling-measurement.md) | โฌœ **first, and startable today** โ€” nobody here can say what happens at 90% RAM. Curve not cliff: swap onset, latency departure, the three exits (checked trap / swap thrash / OOM killer), and `kill -9` durability *at exhaustion*. Output is `perf-targets.md` + baseline rows, not prose | -| 2 | [`@table` storage modes](databasev2/02-table-storage-modes.md) | โฌœ **the language enrichment** โ€” `mode: ram \| durable \| cold` per table, replacing the global switch. `durable` defaults so nothing changes silently; the compiler refuses a `durable` row holding a `ref` into a `ram` table. `.wob` format change. Grammar is small (`Ast.table_cfg` gains a key); semantics are the iteration | +| 2 | [per-table storage: `durable` and `resident`](databasev2/02-table-storage-modes.md) | ๐Ÿ”„ **the language enrichment โ€” the `durable` half is DONE and usable.** Two optional `@table` keys, `durable: true\|false` and `resident: all\|keys`, both defaulting to today's behaviour (all 28 existing declarations compile unchanged, no golden moved). Landed: the grammar, WO-E224 (a durable `ref` into a volatile table is refused), `.wob` v7 carrying both properties in spare `flags` bits, `durable: false` actually skipping the WAL (measured: 50 inserts โ†’ 1500 bytes durable, **0** volatile) with a mode-mismatch startup refusal, plus offset capture and read-a-row-from-an-offset. Outstanding: 5c/5d (the idโ†’offset map and rewiring `wo_row_ptr`'s 11 call sites, slab scans and `@unique`/FK across the boundary โ€” not yet written up), the two runtime refusals, and closeout. [spec](../superpowers/specs/2026-08-26-table-residency-design.md) ยท [plan](../superpowers/plans/2026-08-26-table-residency.md) | | 3 | [WAL checkpoint](databasev2/03-wal-checkpoint.md) *(was 32)* | โฌœ snapshot + truncate: disk reclaimed, replay bounded | | 4 | [io_uring group commit](databasev2/04-io-uring-commit.md) *(was 23)* | โฌœ close the 66ร— gap iteration 22 measured (durable 4.5k vs ram 297k inserts/s) | | 5 | [Bounded tables and eviction](databasev2/05-bounded-tables-eviction.md) | โฌœ a declared capacity + refuse/evict/back-pressure, and a process-level pressure signal that sheds **before** the allocator or OS gets involved โ€” turning the invisible failure into a managed one | diff --git a/docs/stories/databasev2/00-story.md b/docs/stories/databasev2/00-story.md index a4be9ab..ba8731e 100644 --- a/docs/stories/databasev2/00-story.md +++ b/docs/stories/databasev2/00-story.md @@ -76,42 +76,49 @@ disposable*; an orders table wants *resident and durable*; an audit log wants *durable and rarely read*. One global switch cannot express that, so it forces either "everything is precious" or "nothing is". -Extending `@table` with a storage mode moves the decision into the language, -where the compiler can act on it: +Extending `@table` moves the decision into the language, where the compiler can +act on it. **Two keys, not one enum** โ€” the developer is answering two +independent questions, and an enum would need a name for every combination: -- **`ram`** โ€” resident, never WAL-logged, gone on restart. The compiler knows - no durability code is needed; the engine knows these rows are the first - candidates to shed under pressure; and โ€” the part that matters โ€” a program - that expects a `ram` table to survive a restart is now stating something the - compiler can refuse. -- **`durable`** โ€” today's behaviour: resident and WAL-logged, ack after fsync. -- **`cold`** โ€” durable, and *not* required to be resident. This is the mode that - actually raises the ceiling, and it is the one with real design work behind it - (iteration [6](06-cold-tiering.md)). +- **`durable: true | false`** (default `true`). `false` skips the WAL append + entirely: no record, no fsync, ack from RAM, table empty after restart. The + compiler can then refuse a program that stores a durable `ref` into such a + table, because that id would dangle across a restart (WO-E224). +- **`resident: all | keys`** (default `all`). `keys` keeps the id map, the + secondary indexes and the unique shadows resident and reads rows back from + the log by offset. This is the key that raises the ceiling โ€” and the + arithmetic is why it works: 240M rows ร— 16 B of index โ‰ˆ 3.8 GB resident for a + 120 GB table. -The grammar change is small and the surface is already the right shape: -`Ast.table_cfg` is `{ table_name; indexes }`, the parser's argument match -already rejects unknown keys with a catalogued diagnostic -(`unknown @table argument ... (supported: name, index)`), and adding one more -key follows the path `index` already cut. The *semantics* are the work, not the -syntax โ€” which is exactly why it gets its own iteration -([2](02-table-storage-modes.md)) and why it comes after the measurement. +`durable: false` with `resident: keys` is refused: rows would be neither logged +nor resident, so there would be nowhere to read them from. -This is also the honest answer to "does this break principle 7?" It does not. -RAM stays authoritative **for the tables that say so**. `cold` is a declared -exception a developer opts into per table, with the trade written at the -declaration site rather than buried in an operations runbook. +The grammar change was small, as predicted โ€” `Ast.table_cfg` gained two fields +and the parser's argument match two arms. The *semantics* were the work, which +is why iteration 2 is 7 tasks rather than one. + +**Does this break principle 7?** It amends it, deliberately, and the amendment +is applied: the **log** is authoritative and residency is a declared per-table +policy. Durability is untouched and unconditional โ€” ack after fsync, replay +whole-or-nothing, torn tails dropped by CRC. What stays rejected is a *second* +engine: a paged B-tree with its own buffer pool. Reading rows from the log we +already write is not that. + +An earlier draft of this section proposed a three-valued `mode:` enum including +`cold`. That name conflated durability with residency and could not be defined +before its mechanism existed; the history is in +[iteration 2](02-table-storage-modes.md). ## The sequence | # | Iteration | Delivers | Needs | | --- | --- | --- | --- | | 1 | [RAM ceiling: measure the breaking point](01-ram-ceiling-measurement.md) | what actually happens from 50% RAM to OOM โ€” swap onset, latency cliff, trap behaviour, `kill -9` survival | nothing; extends iteration 22's harness | -| 2 | [`@table` storage modes](02-table-storage-modes.md) | the grammar: `mode: ram \| durable \| cold`, per table, replacing the global `WO_DATA` all-or-nothing | 1 for its defaults | +| 2 | [per-table storage](02-table-storage-modes.md) | the grammar: `durable: true\|false` and `resident: all\|keys`, per table, replacing the global `WO_DATA` all-or-nothing. **In progress โ€” the `durable` half is done** | 1 for the budget default | | 3 | [WAL checkpoint](03-wal-checkpoint.md) *(was language 32)* | snapshot + truncate: disk reclaimed, replay bounded | 4 composes | | 4 | [io_uring group commit](04-io-uring-commit.md) *(was language 23)* | close the 66ร— durable/RAM write gap (4.5k vs 297k inserts/s) | the arc (landed) | | 5 | [Bounded tables and eviction](05-bounded-tables-eviction.md) | a capacity a `ram` table may not exceed, and what happens when it does | 2 | -| 6 | [Cold tiering](06-cold-tiering.md) | rows that leave RAM and come back โ€” the iteration that raises the ceiling | 2, 3, 5 | +| 6 | [Cold tiering](06-cold-tiering.md) | โš  **largely superseded by 2** โ€” `resident: keys` is the ceiling-raiser. Its premise (a user-space resident working set) was rejected in favour of the kernel page cache. Revisit only with a measurement showing the page cache insufficient | โ€” | | 7 | [Single-file store](07-single-file-db.md) *(was language 33)* | `WO_DATA=.db` โ€” a file path IS the store | independent | | 8 | [Query grammar from corpora](08-query-grammar-corpus.md) *(was language 27)* | whole-query `count`, `exists` | independent | | 9 | [Cross-program tables](09-cross-program-tables.md) *(was language 20)* | attach to a running program's database over local IPC | independent | @@ -125,11 +132,15 @@ declaration site rather than buried in an operations runbook. 9 โ”€โ”€โ–ถ 10 ``` -Order rationale: **1 before 2** because a mode's default should follow from a -measurement, not a guess. **3 and 4 before 6** because tiering onto a log that -never truncates would make the disk problem worse, not better. **5 before 6** -because eviction from a bounded resident table is the simpler half of the same -mechanism, and getting the policy right there de-risks the hard half. +Order rationale: **1 before 2** because the budget default should follow from a +measurement, not a guess. **3 and 4 matter to 2** for the same reason tiering +onto a never-truncating log would have: `resident: keys` rebuilds its offset map +by scanning the whole log at boot until 3's snapshot persists it. + +Amended 2026-08-27: the original rationale sequenced **6** as the ceiling-raiser +after 3, 4 and 5. `resident: keys` took that role into iteration 2, so 6 is +largely superseded and 5 is no longer a prerequisite for anything on the +critical path. ## What this track does NOT own diff --git a/docs/stories/databasev2/02-table-storage-modes.md b/docs/stories/databasev2/02-table-storage-modes.md index df0cfcc..95eeaa4 100644 --- a/docs/stories/databasev2/02-table-storage-modes.md +++ b/docs/stories/databasev2/02-table-storage-modes.md @@ -1,196 +1,180 @@ --- track: databasev2 iteration: "2" -status: refine +status: in-progress --- -# databasev2 2 โ€” `@table` storage modes: durability becomes a language decision +# databasev2 2 โ€” per-table storage: `durable` and `resident` > Part of [Story โ€” databasev2: the database beyond RAM](00-story.md). -> Needs [1](01-ram-ceiling-measurement.md) โ€” a mode's default should follow from -> a measurement, not a preference. +> Spec: [`2026-08-26-table-residency-design.md`](../../superpowers/specs/2026-08-26-table-residency-design.md) +> ยท plan: [`2026-08-26-table-residency.md`](../../superpowers/plans/2026-08-26-table-residency.md). > -> **The developer's ask, and the language enrichment this track exists for.** -> Today durability is one environment variable for a whole process: `WO_DATA` is -> set and every `@table` is WAL-logged, or it is not and none are -> (`runtime/src/main.c`). Real applications are not uniform. A session table, a -> rate-limit counter and a page cache are resident and disposable; an orders -> table is resident and precious; an audit log is precious and rarely read. One -> global switch forces "everything is precious" or "nothing is", and the -> developer pays for the wrong one either way. +> **The language enrichment this track exists for.** Before this, durability was +> one environment variable for a whole process: `WO_DATA` set and every `@table` +> is WAL-logged, or unset and none are (`runtime/src/main.c`, and `db.c` guards +> each append on the WAL pointer). Real applications are not uniform โ€” a session +> table and a rate-limit counter are disposable, an orders table is precious, a +> 120 GB audit table does not fit in RAM at all. One global switch forces +> "everything is precious" or "nothing is", and the developer pays for the wrong +> one either way. +> +> **Rewritten 2026-08-27** to match what was designed and built. Two earlier +> drafts of this file described a three-valued `mode:` enum including `cold`; +> that design was replaced during the brainstorm and the history is at the +> bottom. -> **โš  Superseded in part, 2026-08-26.** The brainstorm settled on a different -> shape than this document describes: one log-structured engine where the WAL -> *is* the row store, with residency declared per table (`resident: all` / -> `resident: keys`) rather than a three-valued `mode:` enum including `cold`. -> Principle 7 was amended accordingly โ€” the log is authoritative, residency is -> the declaration. This file is rewritten once the grammar is approved; read the -> [track index](00-story.md) and `docs/00-principles.md` ยง7 as current. +## The design, as built -## Goals +Two optional `@table` arguments, because the developer is answering two +independent questions โ€” *do I need this after a restart?* and *does it fit in +RAM?* A single enum would have forced a name for every combination, which is +what made the third value unwriteable before its mechanism existed. -- **Move the storage decision to the declaration site.** `@table(mode: ...)`, - chosen per table, in the source, where the person who knows what the data is - worth is already writing. An operations runbook is the wrong place for a fact - the compiler could hold. -- **Three modes, each earning its existence.** `ram` โ€” resident, never logged, - gone on restart. `durable` โ€” today's behaviour, resident and ack-after-fsync, - and the **default** so every existing program is byte-identical. `cold` โ€” - durable and not required to be resident, declared here and *implemented* in - [6](06-cold-tiering.md), because a mode with no engine behind it is a promise. -- **Make the compiler enforce what the mode means.** This is the part that makes - it a language feature rather than a config key. A program that inserts into a - `ram` table and expects the row after a restart is stating a contradiction, and - the compiler is the right place to say so โ€” at minimum for the cases it can - see statically, with the diagnostic catalogued like every other. -- **Let the engine act on it.** `ram` tables skip the WAL write entirely, which - is not merely a saving โ€” it is the 66ร— gap iteration 22 measured (4.5k durable - vs 297k RAM inserts/s) becoming available per table instead of per process. - They also become the first candidates to shed under pressure, which is what - [5](05-bounded-tables-eviction.md) builds on. -- **Keep principle 7 intact and say why.** RAM stays authoritative for every - table that says so. `cold` is a declared, per-table exception a developer opts - into with the trade visible at the declaration. +| Argument | Values | Default | Meaning | +| --- | --- | --- | --- | +| `durable` | `true`, `false` | `true` | `false` skips the WAL append entirely: no record, no fsync, ack from RAM, table empty after restart | +| `resident` | `all`, `keys` | `all` | `keys` keeps the id map, secondary indexes and unique shadows resident; rows are read back from the log by offset | -## Phases +Both default to the pre-existing behaviour, which is why all 28 `@table` +declarations in the repository compiled unchanged and no golden moved. +`durable: false` with `resident: keys` is refused โ€” rows would be neither +logged nor resident, so there would be nowhere to read them from. -### Phase A โ€” the grammar +The engine stays **one log-structured store**. The WAL already held every row; +this iteration stops discarding the payload. No second engine, no user-space row +cache โ€” the kernel page cache is the hot copy, which is the position +`exploration/postgresql/buffer-and-checkpoint.md` already argued and the reason +the engine avoids `O_DIRECT`. -- Extend the `@table` argument parser with `mode:`. The path is already cut: - the argument loop matches `name` and `index` and rejects anything else with a - catalogued diagnostic (`unknown @table argument ... (supported: name, index)`), - so this is one more arm plus an updated message. -- Extend `Ast.table_cfg` โ€” today `{ table_name; indexes }` โ€” with the mode, and - give it the default so every existing `@table` keeps its meaning. -- Reject the incoherent cases at parse time: `mode` given twice, an unknown mode - name. Both belong in the same diagnostic family as the existing `@table` - errors, and both go in the error catalog **in this change**, not later โ€” that - catalog went stale once by exactly that omission. -- Verify: golden AST fixtures for each mode; `woc-test` green; every existing - `@table` in every sample parses unchanged. +Principle 7 was amended for this: the log is authoritative, residency is a +declared per-table policy. Durability is untouched and unconditional. -### Phase B โ€” the mode reaches the image and the engine +## Progress -- Carry the mode through the class descriptor into the `.wob` image so the - runtime knows it without re-deriving anything. This is a format change, so it - moves `WOB_VERSION` and the format contract in the same commit as the code. -- The engine consults it at the one choke point that already exists: the - `INDEX HOOK` / WAL staging site in `wo_row_insert` / `wo_row_remove`, which - `database/src/CODE-LOGIC.md` names as the only place storage may be mutated. - A `ram` table stages nothing. -- Replay must skip records for tables that are now `ram` โ€” a WAL written when a - table was `durable` and replayed after the source changed is a real - migration case, and silently resurrecting rows into a `ram` table would be - worse than refusing. -- Verify: a `ram` table's inserts produce no WAL growth (measured, not assumed); - a `durable` table is byte-identical to today; a mode change across a restart - is handled explicitly rather than by accident. +| # | Task | State | +| --- | --- | --- | +| 1 | grammar: both arguments, defaults preserve behaviour | โœ… `69b7ce2` | +| 2 | WO-E224: refuse a durable `ref` into a volatile table | โœ… `753e6c4` | +| 3 | `.wob` v7: the class descriptor carries both properties | โœ… `7e68c99` | +| 4 | `durable: false` skips the WAL append and replay | โœ… `dd67e31` | +| 5a | `wo_wal_next_offset` โ€” exact record offsets | โœ… `ac7d8af` | +| 5b | `wo_wal_read_row_at` โ€” a row from a log offset | โœ… `d0c370c` | +| 5c | the idโ†’offset map + drop-payload-keep-index | โฌœ **not written up** | +| 5d | rewiring `wo_row_ptr`'s call sites, slab scans, `@unique`/FK across the boundary | โฌœ not written up | +| 6 | the two runtime refusals (no-`WO_DATA`, the byte budget) | โฌœ | +| 7 | measure, gate, document, close out | โฌœ | -### Phase C โ€” the compiler's enforcement +**The `durable` half is complete and usable.** A volatile table is a full table +in-process โ€” same indexes, same `@unique`, same FK restrict, same query surface +โ€” and is simply empty after a restart. That is what +[porch 1โ€“3](../porch/01-store-backed-middleware.md) need for sessions, +rate-limit counters and idempotency keys. -- Decide how far static checking goes (fork 2) and implement that much. The - floor: `WO_DATA` set with every table `ram` is a program that asked for a data - directory it will never write to โ€” worth a warning at least. -- The interesting case is a `ram` table participating in a `ref`/`backlink` - relation with a `durable` one. A durable row holding a foreign key into a - table that evaporates on restart is a dangling reference by construction, and - FK-restrict cannot save it. This is the check most worth having, and it is - statically visible from the class table. -- Verify: corpus `compile-fail` fixtures for each refusal; each carries the - exact `WO-E###` the catalog now documents. - -### Phase D โ€” prove it on a real workload - -- Give `docs/examples/employee` or the `db-bench` sample a mixed schema โ€” at - least one `ram` table and one `durable` โ€” and gate the distinction: after a - restart, the durable rows are present and the ram rows are gone. That single - assertion is the whole feature. -- Extend the durability legs of `just db-bench` so the per-table write-path - saving appears as a baseline number, not a claim. -- Verify: `just employee`, `just db-actor`, `just db-bench`, `oop-accept` green. - -### Phase E โ€” document the contract - -- The `@table` mode surface in the language-surface guide and the db-binding - contract; the new diagnostics in the error catalog; the mode's effect on - replay in `database/src/CODE-LOGIC.md`. -- Verify: `just linkcheck` clean; the language-surface guide's `@table` row - matches what the parser actually accepts. +**The `resident: keys` half has its read path but no storage behind it.** +Offsets can be captured and rows can be read back from them; nothing yet stores +a table that way. ## Acceptance Criteria -- **Given** every existing `@table` declaration in the repository, **when** it is - compiled after this change, **then** behaviour is byte-identical โ€” `durable` - is the default and nothing opts in silently. -- **Given** a table declared `mode: ram`, **when** rows are inserted with - `WO_DATA` set, **then** the WAL does not grow, and after a restart the table is - empty while `durable` tables in the same program replay intact. -- **Given** `mode: ram` and a measured insert workload, **when** it runs against - the same shape as a `durable` table, **then** the write-path saving is visible - in `bench/baseline.json` โ€” the per-table half of iteration 22's 66ร— gap. -- **Given** an unknown mode name or `mode:` given twice, **when** it is compiled, - **then** it fails with the catalogued diagnostic naming the legal modes. -- **Given** a `durable` table holding a `ref` into a `ram` table, **when** it is - compiled, **then** the compiler refuses (or warns, per fork 2) โ€” a persistent - row cannot reference one that evaporates. -- **Given** a WAL containing records for a table whose source now says `ram`, - **when** the program starts, **then** the situation is handled explicitly - (refuse, or skip and report) and never by silently loading rows into a table - declared not to have any. -- **Given** the `.wob` format change, **when** an image from the previous version - is loaded, **then** the loader refuses it clearly on the version rather than - misreading a descriptor. +Met: + +- **Given** every existing `@table` declaration, **when** compiled, **then** + behaviour is byte-identical. โœ… verified as `git diff` over + `compiler/test/golden/` being empty after a `WOC_BLESS` run โ€” a green test + run alone proves nothing, since blessing rewrites every golden. +- **Given** `durable: false` with `WO_DATA` set, **when** rows are inserted, + **then** the WAL does not grow and the table is empty after a restart while + durable siblings replay. โœ… measured: 50 inserts wrote 1500 bytes durable and + **0** volatile. Measured against the file's non-zero prefix, because the file + is `fallocate`'d to 1 MiB and its size proves nothing. +- **Given** an unknown value, a repeated argument, a retired design word, or the + refused combination, **when** compiled, **then** WO-E102 with a message + naming what to write instead. โœ… +- **Given** a `durable` table holding a `ref` into a volatile one, **when** + compiled, **then** WO-E224 naming both classes and both escapes. โœ… The + reverse direction and every `backlink` shape stay legal, pinned by a run + fixture so the check cannot grow over-broad. +- **Given** a WAL holding records for a class the source now declares volatile, + **when** the program starts, **then** it refuses, exits 2, names the class, + and is **not** reported as corruption. โœ… +- **Given** a v6 image, **when** loaded, **then** refused on version rather + than misread. โœ… + +Outstanding: + +- **Given** a `resident: keys` table larger than any plausible resident budget, + **when** rows are read by id and scanned, **then** every row is byte-identical + including heap-valued columns. *(needs 5c/5d)* +- **Given** `@unique` on a `resident: keys` table, **when** a duplicate arrives + whose conflicting row is not resident, **then** it is refused. *(5d โ€” the + correctness core; a constraint that silently checks only resident rows must + never ship)* +- **Given** `durable: true` and no `WO_DATA`, **when** the program starts, + **then** it refuses. *(task 6 โ€” today this combination silently discards + every write)* +- **Given** the resident footprint crossing the budget, **when** it does, + **then** a refusal naming the table and the annotation. *(task 6)* +- **Given** the `resident: all` read baseline, **when** re-measured, **then** + inside tolerance โ€” no cost for a feature not used. *(task 7)* ## Out Of Scope -- **Implementing `cold`.** Declared here so the mode set is settled and the - format carries it; the engine behaviour is [6](06-cold-tiering.md). Until then - a `cold` declaration must be refused rather than silently treated as - `durable` โ€” accepting a mode that does nothing is how a feature becomes a lie. -- **Per-table capacity limits and eviction** โ€” [5](05-bounded-tables-eviction.md). - This iteration says what a table *is*; that one says how much of it there may - be. -- **`@table` feature flags and `transaction { }`** โ€” language - [iteration 18](../language-runtime-database/18-memory-db-features.md), whose - spec is approved and deliberately left whole. -- **Per-table WAL files.** One log, one writer, shard 0 โ€” the invariant stage 3 - established and [7](07-single-file-db.md) depends on. Modes decide *whether* a - table logs, never *where*. -- **Migrating an existing dataset between modes.** A schema-change story, and - the repo already records destructive migrations as a recorded future. -- **Encryption at rest, compression of the WAL.** Neither has a consumer. +- **Checkpoint and compaction** โ€” [3](03-wal-checkpoint.md). Boot rebuilds the + offset map by scanning the log until that lands, which is O(all history); + 3's snapshot should persist the map. +- **Eviction and a resident row cache** โ€” [5](05-bounded-tables-eviction.md). + This iteration's tables are either fully resident or keys-only. +- **io_uring on the read path** โ€” a real question that only exists after this; + noted in [4](04-io-uring-commit.md), deliberately not folded in. +- **`transaction { }` and `@table` feature flags** โ€” language + [iteration 18](../language-runtime-database/18-memory-db-features.md), + approved spec, left whole. +- **Per-shard residency for volatile tables** โ€” a volatile table has no WAL, so + it arguably need not live on the owner shard at all. Faster, and a different + consistency story. Recorded as a candidate, not decided. +- **Converting an existing dataset between settings.** Refuse on mismatch, do + not convert โ€” implemented in task 4. -## Info +## Info โ€” the forks, settled -The grammar surface this touches, read from the source: the parser's `@table` -argument loop and its `unknown @table argument` failure; `Ast.table_cfg` as -`{ table_name : string option; indexes : string list list }`; and the class -descriptor in `runtime/src/wob.h` that the loader validates. Adding a key is -genuinely small โ€” the semantics are the iteration. +1. **Two keys, not one enum.** An enum needs a name per *combination*, and the + brainstorm demonstrated the third name is unwriteable before its mechanism is + decided. +2. **`keys`, not `index`.** `index:` is already an argument key, so + `@table(index: [c], resident: index)` read badly. `all`/`keys` also put both + values on one axis โ€” what row data stays resident. `resident: none` was + rejected as overclaiming, since the indexes are very much resident. +3. **Optional with `durable` defaulting true**, not mandatory. Mandatory would + have touched 28 declarations, 13 corpus fixtures and 3 goldens; the README + already says nothing is API-stable, so making it mandatory at 1.0 stays + available. +4. **`@unique` on `resident: keys` is allowed**, with its index unconditionally + resident. Roughly doubles the resident index; stated at the declaration so + the cost is visible. +5. **The budget is bytes, not rows** โ€” a text-heavy row and an Int-only row + differ by an order of magnitude, so a row count cannot bound RAM. -Forks the spec must settle: +## History โ€” two corrections worth keeping -1. **What are the modes called?** `ram` / `durable` / `cold` is descriptive of - mechanism. `scratch` / `persistent` / `archived` is descriptive of intent and - is what a developer reasons about. The names are the API and are hard to - change later; leaning the intent-shaped set for the first two if a - short-enough pair can be found, since a developer choosing a mode is thinking - about what the data is *for*, not about where it sits. -2. **How hard does the compiler push?** Three levels: warn on the suspicious - cases; refuse the provably-broken ones (a `durable`โ†’`ram` `ref`); or a full - dataflow check that an insert into a `ram` table is never expected to persist. - The third is not statically decidable in general. Leaning: refuse the - relation case (provable, high value), warn on the `WO_DATA`-with-no-durable- - table case, and stop there. -3. **What is the default, and does it depend on iteration 1?** `durable` keeps - every existing program identical, which is nearly decisive. But if iteration - 1's numbers show the WAL write dominating a workload nobody wanted durable, - there is an argument for making the choice mandatory โ€” no default, every - `@table` states its mode. That is a bigger source change and a better - language; the fork is whether the churn is worth it now or at 1.0. -4. **Does `mode: ram` imply anything about the actor/DB-actor path?** Stage 3 - marshals worker-shard statements to shard 0 because the owner shard holds the - store and the WAL. A `ram` table has no WAL โ€” so does it still need to live on - the owner? A per-shard `ram` table would be dramatically faster and a - different consistency story. Tempting, out of scope here, and worth recording - as a candidate rather than deciding in passing. +**The three-mode design was replaced.** Earlier drafts had +`mode: ram | durable | cold`. `cold` conflated two independent properties and +could not be named honestly before its mechanism existed, and the developer's +120 GB-on-32 GB case showed the real axis was residency. Replaced by two keys, +and principle 7 amended rather than worked around. + +**The "one real rewrite" was fiction.** The spec claimed the on-disk record was +pointer-bearing and that re-encoding it was this iteration's substantive +engineering. That came from reading `table.c`'s `db_val_encode` โ€” which builds +the *in-memory slot* โ€” and inferring the file format from it. `wal.c`'s +`enc_val` has been flat since iteration 9. The task was deleted, not reduced. + +**The opposite half then turned out to be genuinely deep.** With the format +fine, the plan's storage steps still read as plumbing. Measured instead: +`wo_row_ptr` returns a `db_row *` into a slab and has 11 call sites, `table.c` +has 37 slab references, `db.c:105-181` walks slabs for scans, `enc_val` +serialises *from* the slab, and **no operation exists that drops a row's payload +while keeping its index entries**. Hence the 5aโ€“5d split. 5c and 5d need their +own write-ups, and the two open design questions for 5c are whether the id hash +stores offsets in place of slot indices or gains a parallel map, and what the +new operation does about the unique shadows, which currently point at slots. diff --git a/docs/stories/databasev2/06-cold-tiering.md b/docs/stories/databasev2/06-cold-tiering.md index 0ac6994..cd4a27a 100644 --- a/docs/stories/databasev2/06-cold-tiering.md +++ b/docs/stories/databasev2/06-cold-tiering.md @@ -11,6 +11,28 @@ status: refine > [3](03-wal-checkpoint.md) so the log this builds on does not grow forever, > and [5](05-bounded-tables-eviction.md) for the policy machinery. > +> **โš  LARGELY SUPERSEDED 2026-08-27 by [iteration 2](02-table-storage-modes.md).** +> This iteration was written to implement a `cold` mode. That mode no longer +> exists: the brainstorm replaced it with `resident: all | keys`, and +> **`resident: keys` is the ceiling-raising mechanism** โ€” indexes resident, rows +> read from the log by offset. It is iteration 2's tasks 5c/5d, not this file's. +> +> Its premise was also specifically *rejected*, not merely relocated. This +> iteration assumed a **user-space resident working set** with faulting and an +> eviction policy from [iteration 5](05-bounded-tables-eviction.md). The spec +> chose the opposite: no user-space row cache at all, because the kernel page +> cache already is one and a `pread` against a cached page is a memcpy โ€” the +> position `exploration/postgresql/buffer-and-checkpoint.md` already argued and +> the reason the engine avoids `O_DIRECT`. +> +> **What may still be left:** if measurement after 5c/5d shows the page cache +> insufficient for some workload, a user-space working set becomes arguable +> again โ€” but only with that number in hand, which is the opposite of how this +> file was written. Until then treat the design questions below as answered +> elsewhere and the phases as void. Its genuinely durable contribution is its +> fork list, especially "does the language surface the fault cost at the *use* +> site" โ€” still open, and still the largest question about what writeonce is. +> > **The iteration that actually raises the ceiling, and the one most likely to > go wrong.** Everything before it makes the limit visible, declared and > managed. This one removes it โ€” for tables that opt in โ€” and in doing so