writeonce/docs/stories/databasev2/07-single-file-db.md
shoney.arickathil 9b5498fafe docs(db2-7): story closeout — acceptance met per task, progress table, WO_DATA is always a path
- frontmatter `status: done`, `readiness: ready`, `review_pending` (the
  nonexistent-path rule settled under autonomy 2026-09-10)
- every criterion met with its evidence: task 1 smokes A–D (`b31bd40`),
  residency section 8 checks i–vii (`aaea6b2`), db-bench `--wo-data-file`
  181 checks / 5 failures — the same 5 as the directory form
  (`residency.keys.fit` rc 74, databasev2 13's sibling, not this defect),
  temps-beside-the-log test plus its mutation control (`ccee2d0`)
- the fork, settled: existing dir or trailing `/` → <dir>/shard-0.wal,
  byte-identical; otherwise the path IS the log, created behind an existing
  parent; a missing parent or a non-regular non-directory path refuses,
  exit 2, naming path and parent — never a silent mkdir -p
- `WO_DATA` stays a path: ephemerality is WO_EPHEMERAL=1 (databasev2 2 task
  6a), so the file-vs-directory parse carries no `:memory:` sentinel
- progress table with the four hashes; history

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

9 KiB
Raw Blame History

track iteration was_language_iteration status readiness review_pending
databasev2 7 33 done ready fork settled 2026-09-10 for autonomous execution (nonexistent-path rule below) — developer second review before close

databasev2 7 — WO_DATA=<path>.db: the persistent store as one file

Moved 2026-08-26 from the language track, where this was iteration 33. Part of Story — databasev2: the database beyond RAM. Content unchanged by the move; its dependencies are restated in that track index.

Format: product/story-iteration-template. Part of Story — one language, one runtime, one database, one binary — the track this iteration was authored in before the 2026-08-26 move.

Inserted 2026-08-22 (developer ask: "can the persistent db be in file.db form?"). The truth is already almost there: WO_DATA=<dir> holds exactly ONE file (shard-0.wal) — the entire persistent state, since the log is the whole store and no data pages exist. This iteration makes the surface say so: point WO_DATA at a file and THAT file is the store. Small, driver-only, independent of the concurrency chain.

Goals

  • WO_DATA=<path> accepts a file path. A path that is not an existing directory is treated as THE wal file (app.db, store.wo.db — the name is the operator's). The directory form stays and keeps meaning <dir>/shard-0.wal — every existing deployment and gate is byte-identical.
  • One file remains the whole truth at any core count — stage 3 made the WAL owner-shard-only (shard 0 is the sole writer), so nothing multi-shard ever adds a second file.
  • The contract says it out loud: 04-db-binding.md documents the file form, including that compaction (databasev2 3, landed 2026-08-29) and schema migration (databasev2 12) both rewrite the log through a temp file + rename beside it and fsync the parent directory — so the file form must derive that directory from the file path for both.
  • WO_DATA is always a path. Ephemerality is a separate variable — WO_EPHEMERAL=1, decided with databasev2 2's task 6a on 2026-09-09 — so the file-vs-directory parse here never special-cases a sentinel such as :memory:. Both variables set together is already a startup refusal there; the file form inherits it unchanged.

Acceptance Criteria

Met:

  • Given WO_DATA=/tmp/app.db (no such directory), when a program seeds, restarts, and verifies, then replay is byte-true and /tmp/app.db is the only artifact on disk. ✅ task 1 smoke A (b31bd40): seed creates the file, restart+order prints the same lines as the directory form, ls shows app.db as the only artifact. Confirmed at gate level by residency-accept.sh section 8 check (i) (aaea6b2): mix.wob seed → restart prints kept=1 scratch=0; find f8/a -mindepth 1 -printf %P == app.db.
  • Given WO_DATA=<dir> (existing directory), when the same program runs, then behavior is byte-identical to today — <dir>/shard-0.wal, every standing gate unchanged. ✅ task 1 smoke D ($W/d1 and $W/d2/ both produce shard-0.wal inside the dir, unchanged form); section 8 check (iv) pins the same as a regression.
  • Given the db-bench durability legs pointed at the file form, when the restart proof and kill -9 battery run, then every guarantee holds identically (the file IS the same WAL, only named by the operator). ✅ ./scripts/db-bench.py --quick --wo-data-file: db-bench: 181 checks, 5 failures — the same 5 as the directory form (residency.keys.fit rc 74, not this defect; see databasev2 13); restart.s1.file, crash.s1.0.file, restart.sN.file, crash.sN.0.file all ok. residency-accept.sh section 8 check (vi): inline onefile.wo kill -9 battery, N=1e6, killed at 0.20–0.49s, asserts rc 137 (the kill landed) before verifying — landed after 45781 acks on tmpfs, all replayed.
  • Given WO_DATA=/tmp/app.db where /tmp/app.db does not exist and /tmp does, when the program starts, then the file is created and used as the log; given WO_DATA=/tmp/app.db/ (trailing slash) or an existing directory, then the directory form applies; given a path whose parent directory does not exist, then startup refuses with exit 2 naming the path and the parent — never a silent mkdir -p. ✅ task 1 smoke B (missing parent: rc 2, its parent <path>/nope is not an existing directory; create it first) and C (fifo: rc 2, neither a regular file nor a directory); section 8 checks (ii) missing parent (rc 2, message matches its parent .*/nope is not an existing directory + nope/app.db, asserts ! -e nope) and (iii) fifo (rc 2 under timeout 10, so a wrong resolution blocking on the fifo also fails); section 8 check (iv) nodir/ (trailing slash, missing dir) → rc 2 cannot open .*/nodir//shard-0.wal, byte-identical to pre-7.
  • Given the file form, when compaction (databasev2 3) or a schema migration (databasev2 12) rewrites the log, then the temp file is <file><suffix> beside it and the fsync'd parent is the file's own directory — pinned by a test, since both paths today derive from the log path rather than from WO_DATA. ✅ test_file_form_temps_beside_log (ccee2d0, test_wal: 6629 pass, 0 fail, +57 assertions), teeth proven by a mutation control (compaction temp redirected into a decoy dir): 14 assertions turn red. Gate-level: section 8 check (vii) (WO_CHECKPOINT_BYTES=1 WO_WAL_STATS=1 wal 300): compactions= >= 1 asserted (5 measured), app.db still the only artifact, verify 300 rc 0.

Out Of Scope

  • A paged database file (SQLite's shape) — still rejected; the disk story is the WAL, full stop.
  • Checkpoint/compaction — iteration 32's; its rename-swap (write snapshot+tail to a NEW file, fsync, rename() over the old) is exactly what keeps the single-file promise crash-safe when it lands. 33 before or after 32 works; landing 33 first means 32's spec inherits the file form as a stated constraint. (2026-09-10: 32 is databasev2 3, landed 2026-08-29; this iteration lands after it and pins the constraint with a test — see Progress task 2.)
  • Multiple stores per process, attach-by-file — held iteration 20's territory.

Info

  • The whole change is runtime/src/main.c's hardcoded snprintf("%s/shard-0.wal", dir) growing a stat-based fork (directory → today's path; otherwise → the path itself), plus a gate check and the binding-doc note. No engine, no WAL format, no compiler.
  • Fork for the (tiny) spec: what does a NONEXISTENT path mean? Leaning: a path whose parent exists and that does not end in / is a file to create; a trailing / or existing directory keeps the directory form. Refusing ambiguity loudly (WO exit 2) beats guessing. Settled 2026-09-10 (auto-approved for autonomous execution, review_pending): the leaning IS the rule. Existing directory or trailing / → <dir>/shard-0.wal (byte-identical to today). Otherwise the path is the log file: created if absent, opened if a regular file. A parent directory that does not exist → startup refusal, exit 2, one stderr line naming the path and the missing parent (no mkdir -p: a typo must not create a store somewhere unexpected). A path that exists but is neither a regular file nor a directory (fifo, socket, device) → the same refusal shape. WO_EPHEMERAL interplay is inherited from databasev2 2 task 6a unchanged. Compaction and migration already build their temp as <log path><suffix> and sync the log's parent directory (database/src/wal.c sync_parent_dir), so the file form inherits crash safety without engine changes; a test pins that.

Progress

# Task State
1 runtime/src/main.c: stat-based resolution of WO_DATA (dir / trailing slash → shard-0.wal; else the file), the two refusals (missing parent; not a regular file or directory), message names path and parent ✅ b31bd40
2 runtime/test: pin that compaction and migration temps land beside an arbitrary log path and the parent of THAT path is synced (file-form path with a sibling directory as decoy) ✅ ccee2d0
3 Contract + memory: docs/plan/oop-vm/04-db-binding.md file form; database/src/CODE-LOGIC.md or runtime/src/CODE-LOGIC.md note on path resolution ✅ f1985ba
4 Gate: scripts/residency-accept.sh or a db-bench durability leg pointed at the file form — seed, restart, kill -9 battery, only-artifact check; refusal checks for the missing parent (codd-cyril) ✅ e274f4a + aaea6b2

Proposed Solution

Small enough for a bounded slice: brainstorm the one fork in chat, implement driver + db-bench file-form leg + docs in one pass, gates green. No plan document needed unless it grows.

History

  • 2026-09-10 tasks 1–3 landed; 4 in flight.
  • 2026-09-10 closed: task 4 landed, residency 32/0.