diff --git a/tasks/suite-column-sqlite.md b/tasks/suite-column-sqlite.md index eb63193..31cd8c5 100644 --- a/tasks/suite-column-sqlite.md +++ b/tasks/suite-column-sqlite.md @@ -1,7 +1,7 @@ --- id: suite-column-sqlite name: Conformance suite — the sqlite column (the first SQL-backed column) -status: pending +status: completed depends_on: [store-sweep-durable-window] scope: moderate risk: medium @@ -103,6 +103,81 @@ the default build (feature `sqlite` is default-on). > To be filled by implementation agent +The column as it landed (`tests/conformance/sqlite_column.rs`, 17 tests): + +- **Factories**: `dual-tier sqlite+local` (the default-build + production shape, `DualTierSqliteFactory`) and `kv-only(sqlite)` + (`KvOnlySqliteFactory`), each instance-tracking the temp sites it + vends (a private temp directory per open holding `pool.db` and the + local root; teardown `remove_dir_all`s it — idempotent, NotFound + tolerated, panicking-row-safe, one runner per instance), with the + column-declared threshold (1024) and the **test tuning of the + window delay (`Duration::ZERO` — execute-on-next-sweep)**. The + module is `#[cfg(feature = "sqlite")]` — **default-on**, so the + column runs in every default `cargo test`. +- **The medium oracle distinguishes by medium**: the dual-tier rows + count the sqlite file's `blobs` rows (the kv tier) vs the local + root's committed files (the large tier), plus `gc_window` rows (the + durable schedule observer) — via the test's own independent + connection. The shared helpers (`committed_file_count`, + `staging_file_count`, `slice_digest`, `LatchProtect`) moved to + `rows.rs` (`pub(crate)`, the columns' shared fixtures) so the + sqlite column reuses them without the mem gate. +- **Mirrored verbatim** (form-independent, from `rows.rs`): INV-1 + round-trips, INV-6 stat discipline, the `NoLivenessSources`/ + `ProtectFailed` abort shapes, and the kv-only rejection row + (against `kv-only(sqlite)`; unchanged, threshold-agnostic by + discovery). +- **Durable-schedule rows** (the column's own, concrete-factory, + ADR-stamped, traced): INV-2 dedup and INV-3 stage hygiene with + counts read against the staged schedule (the sweep-counting rows' + amendment), the two-sweep exact-count row over the full protection + set (both tiers, the window observed), the race across the window + delay (a re-put landing during the window survives the executing + sweep via execution-time re-arbitration; the unpinned twin + deletes; both put paths in-body over both tiers), the + holder-stages-only lease shape (the reference `SweeperLock` row's + schedule-relative form), and INV-5 list correctness through + durable-schedule outcomes. +- **Dispatch rows** mirror the local column's family (pure-function + routing, threshold boundaries at the tuned + shipped-default + thresholds, fall-through per op) with the placement oracle adapted + by medium. **Virgin-store row**: reads are no-ops on a fresh file + (pool, window, and committed tree all empty); the no-tables-yet + tolerance itself is engine-instrumented (src/sqlite.rs — the + constructor always ensures the schema), this row being the facade + form. **Structural factory tests**: fresh isolated opens + (distinct private sqlite files and roots, medium-observable) and + teardown-against-a-panicking-row (files really removed). +- **README amendments**: §The schedule-relative reading for + SQL-backed columns (the mirror convention's amendment, citing + ADR-012 §6.7 — staged-this-sweep / executed-next-sweep, which rows + carry it, which mirror verbatim), the sqlite rows table in §Columns + beyond the reference set, and Current state (three columns; the + default-build column). main.rs's module doc names the column. + ## Summary -> To be filled on completion \ No newline at end of file +> To be filled on completion + +The sqlite column — the suite's first SQL-backed column — is green: +the default-build production shape (`kv = sqlite` + `large = local`, +feature `sqlite` default-on) runs the form-independent reference rows +verbatim against its factories, grows the durable-schedule sweep rows +whose exact counts are asserted relative to the staged schedule +(ADR-012 §6.7: staged this sweep, executed by the next — two-sweep +counts over the full protection set, the race across the window delay, +the holder-stages-only lease shape, INV-5 through outcomes, and the +schedule-relative INV-2/INV-3 readings), and the dispatch rows with +the medium oracle distinguishing the sqlite file's rows from the +root's committed files. Every row ADR-stamped and traced; the +schedule-relative INV-4/INV-5 reading is recorded in the README (the +mirror convention's amendment); factory isolation (fresh site per +open) and idempotent panicking-row-safe teardown hold structurally. +The column never skips and runs in the default build (17 tests). + +Verification: `cargo test` green (default: 69 lib + 19 conformance + +3 git-interop), `cargo test --features mem` green (124 lib + 54 +conformance), `cargo check --all-features --tests` green, +`cargo clippy --all-targets -- -D warnings` green, `cargo fmt --check` +green (fmt applied). \ No newline at end of file diff --git a/tests/conformance/README.md b/tests/conformance/README.md index 94b369e..2b9e7bc 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -185,6 +185,38 @@ families' first shapes (the `SweeperLock` abort) complete the three `GcAbortCause` variants within the single-node exact-count family (ADR-013 §2's lease, its in-process ADR-012 §5 form). +### The schedule-relative reading for SQL-backed columns (the mirror convention's amendment) + +The reference set's single-sweep exact-count rows are the **inline-form +host's** (mem): there a completed sweep's +`candidates_staged == deleted + cancelled_by_arbitration` holds over +its same-sweep staged set. The durable form's semantics differ — +**staged this sweep, executed by the next** (ADR-012 §6.7) — so +SQL-backed columns read every exact-count assertion **relative to the +staged schedule**: the staging sweep reports `candidates_staged` as +the newly dead set and deletes nothing; its executor (the next sweep) +reports `deleted + cancelled_by_arbitration` summing exactly to that +staged set. Recorded, citing ADR-012 §6.7: + +- **INV-4 and INV-5** run the **durable-schedule equivalents** on + SQL-backed columns — the two-sweep exact-count rows over the full + protection set, the sweep-vs-put race across the window delay, and + list correctness through durable-schedule outcomes (README §Columns + beyond the reference set grows the rows table). The `SweeperLock` + abort shape is form-independent (abort before staging) but its + reference holder-completion leg is inline-form; SQL-backed columns + run the holder-stages-only form (its staged set waits for the next + sweep to execute). +- **INV-2 and INV-3** — whose mirror rows observe their invariants + through sweep outcomes — likewise assert their counts relative to + the schedule: zero residue after failures is still staged-0; a + success's exactly-one-entry is staged-1 then deleted-1 across the + two sweeps. INV-1, INV-6, and the constructor-mode rows consume no + entry-dependent sweep counts and mirror verbatim. +- **Aborts are form-independent**: `NoLivenessSources` and + `ProtectFailed` abort before staging or executing — counts all zero + on the durable form too — so those rows mirror directly. + ## Columns beyond the reference set (INV-7's mirror + extension) The constructor-mode rows of the reference set run wherever the mode @@ -228,6 +260,41 @@ and kv-only mode rows do not mirror onto the dual-tier factories (the precondition `mode()` shapes they assert are not this column's mode); the dispatch rows are the dual-tier mode's facade-contract coverage. +**The sqlite column's own rows** (tests/conformance/sqlite_column.rs, +the first SQL-backed column — the default-build production shape, +`kv = sqlite` + `large = local`, feature `sqlite` default-on, +ADR-007/013 §4): the dispatch-coverage rows with the **placement +oracle adapted by medium** — both tiers are on disk now, kv entries +live in the sqlite file (rows in the `blobs` table), large entries +under the local root — and the **durable-schedule sweep rows** (§The +schedule-relative reading for SQL-backed columns; ADR-012 §6.7; the +window's `gc_window` rows are the schedule observer). The +form-independent reference rows mirror verbatim: the INV-1 round-trip +family and the INV-6 stat-discipline row against the dual-tier +factory, the `NoLivenessSources`/`ProtectFailed` abort shapes +(form-independent — abort before staging), and the kv-only rejection +row against a `kv-only(sqlite)` factory (threshold-agnostic by +discovery, unchanged; the threshold materializable per the +configuration contract). + +| Row (`sqlite_column.rs`) | Traces to | +|---|---| +| `dedup_identical_content_is_one_pool_entry` | INV-2 (same-path and cross-path legs; counts read against the durable schedule, one row-oracle leg) | +| `stage_hygiene_zero_residue_on_failure_exactly_one_entry_on_success` | INV-3 (the degenerate kv stage form through the injectable failure modes; schedule-relative counts) | +| `two_sweep_exact_counts_over_the_full_protection_set` | INV-4/INV-5 (the full protection set across two sweeps — staged this sweep, executed by the next; the both-tiers form) | +| `a_put_landing_during_the_window_survives_the_executing_sweep` | INV-4 (the sweep-vs-put race across the window delay; execution-time re-arbitration) | +| `the_contended_second_sweeper_aborts_as_data_and_the_holder_stages_only` | INV-4 (the `SweeperLock` abort shape — abort before staging; the holder's durable-form completion: stages only) | +| `list_correctness_is_observable_through_durable_schedule_outcomes` | INV-5 (exact counts, never list comparison) | +| `routing_is_a_pure_function_of_length_across_put_paths_and_re_puts` | ADR-003 §Dispatch policy (purity/no-migration); ADR-012 §6.3; ADR-012 §6.7 (schedule-relative dedup legs); ADR-007; INV-7 facade-contract row | +| `threshold_boundary_lengths_route_deterministically` | ADR-003 §Dispatch policy (at/below/above, tuned + shipped-default thresholds); ADR-012 §6.3; ADR-007 | +| `kv_tier_misses_fall_through_to_the_large_tier_per_op` | ADR-012 §6.1 (fall-through per op); ADR-012 §6.5 ruling 5 (miss shapes); ADR-006; ADR-012 §6.3; ADR-007 | +| `virgin_store_reads_are_no_ops_on_a_fresh_file` | store-api.md §Error model (virgin-store semantics); ADR-012 §6.5; ADR-007 (the no-tables-yet read tolerance — engine-instrumented, the facade's fresh-file form) | + +The sqlite column's window delay is the **test tuning** (ADR-008 §2's +legal degenerate tuning, `Duration::ZERO` — execute-on-next-sweep): +the schedule the durable rows assert against is exactly the +next-sweep executor form. + ## Column plan **mem is the contract-reference engine** (ADR-011 §2): wave 2 lands @@ -240,15 +307,15 @@ and first run in wave 6. ## Current state -The harness is green on **two columns** (wave 2). The **mem column** -is the contract-reference column: the factory binding is the concrete -`alkblobs::Store` facade (see §The factory contract — the binding -demonstrator remains in `binding.rs`), the reference row set is landed -(§The reference row set — `rows.rs`, every row ADR-stamped and traced -to the backlog), the column's factories (`mem-only`, the reference; -`kv-only(mem)` at a column-declared threshold) vend fresh isolated -stores with idempotent teardown (`mem_column.rs`, the structural -factory-contract tests included), and mem never skips. +The harness is green on **three columns** (waves 2–3). The **mem +column** is the contract-reference column: the factory binding is the +concrete `alkblobs::Store` facade (see §The factory contract — the +binding demonstrator remains in `binding.rs`), the reference row set +is landed (§The reference row set — `rows.rs`, every row ADR-stamped +and traced to the backlog), the column's factories (`mem-only`, the +reference; `kv-only(mem)` at a column-declared threshold) vend fresh +isolated stores with idempotent teardown (`mem_column.rs`, the +structural factory-contract tests included), and mem never skips. The **local column** (the suite's first dual-tier column, the dispatch-coverage shape ADR-012 §6.3 names) mirrors the reference INV @@ -269,4 +336,28 @@ constructor rides feature `mem` because mem is the only constructible kv engine until wave 3 (the gate is the kv pairing's, not the local engine's, which is ungated in `src/`) — the column runs under `cargo test --features mem`, with the reference row set compiling ungated so -it stays type-checked in default builds. \ No newline at end of file +it stays type-checked in default builds. + +The **sqlite column** (the suite's first SQL-backed column — the +default-build production shape, `kv = sqlite` + `large = local`, +ADR-007/013 §4) mirrors the form-independent reference rows (INV-1, +INV-6, the `NoLivenessSources`/`ProtectFailed` abort shapes, and the +kv-only rejection row against `kv-only(sqlite)`) and grows the +**durable-schedule sweep rows** (§The schedule-relative reading for +SQL-backed columns — the two-sweep exact counts, the race across the +window delay, the holder-stages-only lease shape, list correctness +through durable-schedule outcomes, and the schedule-relative INV-2/3 +readings) plus the dispatch rows with the placement oracle adapted by +medium (both tiers on disk: the sqlite file's rows vs the root's +committed files) and the virgin-store row — the recorded inventory is +in §Columns beyond the reference set. Its factories declare the +column-tuned threshold and the test tuning of the window delay +(`Duration::ZERO` — execute-on-next-sweep), vend a fresh temp site +per open (a private directory holding the sqlite file and the local +root; idempotent teardown removing the site, panicking-row-safe, each +runner owning its instance), and never skip. The column is +feature-gated on `sqlite` — **default-on**, so the column runs in the +default build: every `cargo test` run exercises it. The sqlite +engine's no-tables-yet read tolerance is engine-instrumented +(src/sqlite.rs's tests; the constructor-vs-lazy timing design point +there), the column's virgin-store row being its facade form. \ No newline at end of file diff --git a/tests/conformance/local_column.rs b/tests/conformance/local_column.rs index e90ac41..fecacfb 100644 --- a/tests/conformance/local_column.rs +++ b/tests/conformance/local_column.rs @@ -46,13 +46,14 @@ #![allow(dead_code)] use std::io::Cursor; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; -use sha2::{Digest as _, Sha256}; - use crate::factory::StoreFactory; -use crate::rows::{self, assert_exact_counts, patterned, Chunked, Failing, SetProtect, SetSource}; +use crate::rows::{ + self, assert_exact_counts, committed_file_count, patterned, slice_digest, staging_file_count, + Chunked, Failing, SetProtect, SetSource, +}; use alkblobs::{Error, Key, Store, StoreMode, DEFAULT_DISPATCH_THRESHOLD}; // ------------------------------------------------------------------ @@ -159,55 +160,12 @@ impl StoreFactory for DualTierLocalFactory { } // ------------------------------------------------------------------ -// On-disk observation helpers (the dispatch-coverage shape's medium -// oracle; kv = mem never touches this disk, so these counts are -// exactly the large tier's committed/staging residency). +// On-disk observation helpers live in `rows` (the medium-backed +// columns' shared oracle); kv = mem never touches this disk, so the +// counts below are exactly the large tier's committed/staging +// residency. // ------------------------------------------------------------------ -/// Every regular file under `root` outside any `staging` directory — -/// with `kv = mem`, exactly one per committed large-tier entry. -fn committed_file_count(root: &Path) -> usize { - fn walk(dir: &Path, out: &mut Vec) { - let Ok(entries) = std::fs::read_dir(dir) else { - return; - }; - for entry in entries.flatten() { - let Ok(kind) = entry.file_type() else { - continue; - }; - let path = entry.path(); - if kind.is_dir() { - if path.file_name().and_then(|n| n.to_str()) == Some("staging") { - continue; - } - walk(&path, out); - } else if kind.is_file() { - out.push(path); - } - } - } - let mut files = Vec::new(); - walk(root, &mut files); - files.len() -} - -/// Files in the large tier's staging area — the residue counter of the -/// exact-count hygiene row (zero after every failure form and every -/// success form; a staged artifact lives there only mid-put). -fn staging_file_count(root: &Path) -> usize { - match std::fs::read_dir(root.join("staging")) { - Ok(entries) => entries.count(), - Err(_) => 0, - } -} - -/// The ADR-006 slice digest, computed independently of the crate — -/// SHA-256 over exactly the returned bytes (the out-of-band -/// verification the convention requires; callers verify the same way). -fn slice_digest(bytes: &[u8]) -> [u8; 32] { - Sha256::digest(bytes).into() -} - // ------------------------------------------------------------------ // The column's own rows — dispatch coverage (the coverage shape's // reason to exist) and the local-specific properties. Every row takes diff --git a/tests/conformance/main.rs b/tests/conformance/main.rs index d9cc769..9f87773 100644 --- a/tests/conformance/main.rs +++ b/tests/conformance/main.rs @@ -25,7 +25,12 @@ //! [`local_column`] — the suite's first dual-tier column (the //! `kv = mem` + `large = local` dispatch-coverage shape ADR-012 §6.3 //! names), which mirrors the reference set and grows the -//! dispatch-coverage and local-specific rows. +//! dispatch-coverage and local-specific rows; [`sqlite_column`] — the +//! suite's first SQL-backed column (the default-build production +//! shape, `kv = sqlite` + `large = local`), which mirrors the +//! form-independent reference rows and grows the durable-schedule +//! sweep rows (README §The schedule-relative reading for SQL-backed +//! columns). //! //! Items beyond the tests below are the later columns' surface, hence //! the scoped `#![allow(dead_code)]` — it shrinks as columns land. @@ -41,6 +46,8 @@ mod local_column; #[cfg(feature = "mem")] mod mem_column; mod rows; +#[cfg(feature = "sqlite")] +mod sqlite_column; mod stamp; #[test] diff --git a/tests/conformance/rows.rs b/tests/conformance/rows.rs index 53a7909..e3c36cc 100644 --- a/tests/conformance/rows.rs +++ b/tests/conformance/rows.rs @@ -32,12 +32,14 @@ use std::collections::BTreeSet; use std::io::Cursor; +use std::path::{Path, PathBuf}; use std::pin::Pin as StdPin; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; use parking_lot::Mutex; +use sha2::{Digest as _, Sha256}; use tokio::io::{AsyncRead, ReadBuf}; use crate::factory::StoreFactory; @@ -111,9 +113,10 @@ impl ProtectCallback for SetProtect { /// A protect callback that blocks mid-consult — the fixture for the /// sweep-lease contended window: it announces its entry on a channel, /// waits on a second, then answers with an empty protection set. -struct LatchProtect { - entered: std::sync::mpsc::Sender<()>, - release: Mutex>>, +/// `pub(crate)`: the suite's other columns reuse it. +pub(crate) struct LatchProtect { + pub(crate) entered: std::sync::mpsc::Sender<()>, + pub(crate) release: Mutex>>, } impl ProtectCallback for LatchProtect { @@ -267,6 +270,57 @@ pub(crate) fn assert_exact_counts(report: &alkblobs::SweepReport) { ); } +/// Every regular file under `root` outside any `staging` directory — +/// the committed-tree counter of the medium-backed columns' on-disk +/// placement oracle. With a kv tier that never touches this disk it is +/// exactly one per committed large-tier entry; a SQL-backed kv tier +/// distinguishes by medium instead (its entries live in the database +/// file, so this count is still exactly the large tier's residency). +/// `pub(crate)`: the suite's other columns reuse it. +pub(crate) fn committed_file_count(root: &Path) -> usize { + fn walk(dir: &Path, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let Ok(kind) = entry.file_type() else { + continue; + }; + let path = entry.path(); + if kind.is_dir() { + if path.file_name().and_then(|n| n.to_str()) == Some("staging") { + continue; + } + walk(&path, out); + } else if kind.is_file() { + out.push(path); + } + } + } + let mut files = Vec::new(); + walk(root, &mut files); + files.len() +} + +/// Files in the large tier's staging area — the residue counter of the +/// on-disk stage-hygiene exact-count form (zero after every failure +/// form and every success form; a staged artifact lives there only +/// mid-put). `pub(crate)`: the suite's other columns reuse it. +pub(crate) fn staging_file_count(root: &Path) -> usize { + match std::fs::read_dir(root.join("staging")) { + Ok(entries) => entries.count(), + Err(_) => 0, + } +} + +/// The ADR-006 slice digest, computed independently of the crate — +/// SHA-256 over exactly the returned bytes (the out-of-band +/// verification the convention requires; callers verify the same way). +/// `pub(crate)`: the suite's other columns reuse it. +pub(crate) fn slice_digest(bytes: &[u8]) -> [u8; 32] { + Sha256::digest(bytes).into() +} + /// INV-1: whole-blob put/get round-trips byte-identical under the /// canonical derivation — the content matrix loops both put paths /// (known-length, unknown-length) in-body; every put's key is the diff --git a/tests/conformance/sqlite_column.rs b/tests/conformance/sqlite_column.rs new file mode 100644 index 0000000..095ea5a --- /dev/null +++ b/tests/conformance/sqlite_column.rs @@ -0,0 +1,1797 @@ +//! The **sqlite column** — the suite's first SQL-backed column: the +//! reference row set run against the default-build production shape +//! (`kv = sqlite` + `large = local`; feature `sqlite` is default-on, +//! ADR-007, ADR-013 §4), extended with the **durable-schedule sweep +//! rows** and the dispatch rows (README §Columns beyond the reference +//! set; INV-7: every engine's rows mirror the reference column). +//! +//! The sweep rows are **schedule-relative on this column** (the +//! README §The schedule-relative reading for SQL-backed columns is +//! the recorded amendment, citing ADR-012 §6.7): the reference set's +//! single-sweep exact-count rows are the inline-form host's — the +//! durable form stages its candidates this sweep and executes them by +//! the next, so the exact counts here are asserted against the staged +//! schedule. The form-independent rows mirror verbatim +//! ([`crate::rows`]: the round-trip family, the stat-discipline row, +//! the no-sources and failed-protect abort shapes, and the kv-only +//! rejection row against the kv-only factory); the schedule-relative +//! properties grew their own rows below (dedup and stage hygiene +//! observe their invariants through sweep outcomes, so they read +//! against the schedule too). Mem-only never mirrors here: the mode +//! is not constructible without mem. +//! +//! The factories (README §The factory contract): `open()` vends the +//! concrete [`alkblobs::Store`] facade over a fresh temp site per +//! call — a private temporary directory holding the sqlite pool file +//! and the large engine's private root — so two opens never share +//! state. Teardown removes every site this factory instance vended +//! (idempotent: a repeated teardown is a no-op and an already-removed +//! directory is tolerated; safe against abandoned instances — each +//! site is private to its open). The local column's instance-tracks- +//! its-own-root pattern: every runner constructs its own factory, so +//! concurrent rows never touch each other's sites. The column never +//! skips (README §SKIP posture — server-less engines' columns must +//! always be green in every environment). +//! +//! The dual-tier dispatch rows mirror the local column's family with +//! the placement oracle adapted: both tiers are on disk now — kv +//! entries live in the sqlite file (the oracle counts its pool rows), +//! large entries under the local root — so the oracle distinguishes +//! by medium. + +#![allow(dead_code)] + +use std::io::Cursor; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use rusqlite::Connection; + +use crate::factory::StoreFactory; +use crate::rows::{ + self, committed_file_count, patterned, slice_digest, staging_file_count, Failing, LatchProtect, + SetProtect, SetSource, +}; +use alkblobs::{Error, Key, Store, StoreMode, DEFAULT_DISPATCH_THRESHOLD}; + +// ------------------------------------------------------------------ +// The factories +// ------------------------------------------------------------------ + +/// The column's declared dual-tier threshold — small, so the shared +/// content matrix and the race fixtures cross it and both tiers' +/// bodies are exercised (sub-threshold rows land in the sqlite file; +/// over-threshold rows stage-commit into the local root). +pub(crate) const TUNED_THRESHOLD: u64 = 1024; + +/// The column's declared kv-only rejection threshold — small, so every +/// leg of the rejection row stays trivially materializable. +pub(crate) const KV_ONLY_THRESHOLD: u64 = 1024; + +/// The **test tuning of the window delay** (ADR-008 §2's legal +/// degenerate tuning): `0` — execute-on-next-sweep. The durable +/// schedule's staging sweep stages the dead set; the *next* sweep is +/// its executor, which is exactly the schedule the sweep rows assert +/// against. +pub(crate) const WINDOW_DELAY: Duration = Duration::ZERO; + +/// One vended backing site: a fresh private temporary directory +/// holding the sqlite pool file and the large engine's root. +#[derive(Clone)] +pub(crate) struct Site { + /// The whole private directory — what teardown removes. + base: PathBuf, + /// The sqlite pool file — the kv tier's medium (the dispatch + /// oracle's kv-side counter and the durable window's rows table). + database: PathBuf, + /// The local root — the large tier's medium (the dispatch + /// oracle's file-side counter rides [`committed_file_count`]). + root: PathBuf, +} + +fn fresh_site(tag: &str) -> Site { + use std::sync::atomic::{AtomicU64, Ordering}; + use std::time::{SystemTime, UNIX_EPOCH}; + + static SEQ: AtomicU64 = AtomicU64::new(0); + let seq = SEQ.fetch_add(1, Ordering::Relaxed); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0); + let base = std::env::temp_dir().join(format!( + "alkblobs-sqlite-column-{tag}-{}-{:x}-{seq}", + std::process::id(), + nanos + )); + std::fs::create_dir_all(&base).expect("temp site created"); + Site { + base, + database: PathBuf::new(), + root: PathBuf::new(), + } +} + +impl Site { + fn with_pool(mut self) -> Site { + self.database = self.base.join("pool.db"); + self.root = self.base.join("local-root"); + self + } + + /// The sqlite pool file path — captured before teardown so a + /// structural test can assert the removal. + pub(crate) fn database(&self) -> &Path { + &self.database + } + + /// The local root path — for the file oracle's callers. + pub(crate) fn root(&self) -> &Path { + &self.root + } + + fn remove(self) -> Result<(), Error> { + match std::fs::remove_dir_all(&self.base) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(Error::Io(e.to_string())), + } + } +} + +/// The dispatch-coverage factory: `kv = sqlite` + `large = local` at +/// the threshold this column declares and the test tuning of the +/// window delay (see [`TUNED_THRESHOLD`], [`WINDOW_DELAY`], and the +/// default-threshold sibling `DualTierSqliteFactory::new( +/// DEFAULT_DISPATCH_THRESHOLD)` the boundary row runs against). +/// +/// **Why a fresh instance per runner (not a shared static):** teardown +/// removes the sites *this instance* vended; a shared instance's +/// teardown would race concurrently running rows' sites. Every runner +/// constructs its own and owns its lifecycle. +pub(crate) struct DualTierSqliteFactory { + /// The declared dispatch threshold — the routing boundary this + /// column's rows run at (constructor-tunable; ADR-003). Declared + /// configuration, like the local column's (the dual-tier store + /// never rejects, so there is nothing to probe). + threshold: u64, + /// The declared window delay — the durable schedule's cadence knob + /// the sweep rows run at (the test tuning: execute-on-next-sweep). + window_delay: Duration, + /// Every temp site this instance vended, for its own teardown. + sites: Mutex>, +} + +impl DualTierSqliteFactory { + pub(crate) fn new(threshold: u64) -> DualTierSqliteFactory { + DualTierSqliteFactory { + threshold, + window_delay: WINDOW_DELAY, + sites: Mutex::new(Vec::new()), + } + } + + /// Open + track the vended site: the shape the column's own rows + /// use (they observe the backing medium, so they need the site + /// alongside the store). + pub(crate) async fn open_tracked(&self) -> Result<(Store, Site), Error> { + let site = fresh_site("dual-tier").with_pool(); + let store = Store::dual_tier_sqlite_local( + &site.database, + &site.root, + self.threshold, + self.window_delay, + ) + .await?; + self.sites + .lock() + .unwrap_or_else(|e| e.into_inner()) + .push(site.clone()); + Ok((store, site)) + } + + /// The database files this instance vended, for the structural + /// tests' teardown assertions. + pub(crate) fn tracked_databases(&self) -> Vec { + self.sites + .lock() + .unwrap_or_else(|e| e.into_inner()) + .iter() + .map(|site| site.database.clone()) + .collect() + } +} + +impl StoreFactory for DualTierSqliteFactory { + fn name(&self) -> &'static str { + "dual-tier sqlite+local" + } + + async fn open(&self) -> Result { + self.open_tracked().await.map(|(store, _)| store) + } + + async fn teardown(&self) -> Result<(), Error> { + for site in self + .sites + .lock() + .unwrap_or_else(|e| e.into_inner()) + .drain(..) + { + site.remove()?; + } + Ok(()) + } +} + +/// The kv-only(sqlite) factory (ADR-011 §4; ADR-012 §6.2): no large +/// tier; the threshold is the put-time rejection point the kv-only +/// row discovers by probing. The durable single-node medium. +pub(crate) struct KvOnlySqliteFactory { + /// The rejection point this column's configuration declares. The + /// kv-only row discovers it behaviorally and requires it + /// materializable for its boundary legs. + pub(crate) threshold: u64, + /// The declared window delay — the durable schedule's cadence knob. + window_delay: Duration, + /// Every temp site this instance vended, for its own teardown. + sites: Mutex>, +} + +impl KvOnlySqliteFactory { + pub(crate) fn new(threshold: u64) -> KvOnlySqliteFactory { + KvOnlySqliteFactory { + threshold, + window_delay: WINDOW_DELAY, + sites: Mutex::new(Vec::new()), + } + } +} + +impl StoreFactory for KvOnlySqliteFactory { + fn name(&self) -> &'static str { + "kv-only(sqlite)" + } + + async fn open(&self) -> Result { + let site = fresh_site("kv-only").with_pool(); + let store = + Store::kv_only_sqlite(&site.database, self.threshold, self.window_delay).await?; + self.sites + .lock() + .unwrap_or_else(|e| e.into_inner()) + .push(site); + Ok(store) + } + + async fn teardown(&self) -> Result<(), Error> { + for site in self + .sites + .lock() + .unwrap_or_else(|e| e.into_inner()) + .drain(..) + { + site.remove()?; + } + Ok(()) + } +} + +// ------------------------------------------------------------------ +// The medium oracle (the dispatch rows' instrument, adapted from the +// local column's): both tiers are on disk now, so the oracle +// distinguishes by medium — the sqlite file's pool and window tables +// vs the root's committed files. +// ------------------------------------------------------------------ + +/// Rows in one of the sqlite pool file's tables — the kv tier's pool +/// (`blobs`) or the durable window's staged candidates (`gc_window`), +/// counted from the test's own independent connection to the file. +fn sqlite_table_rows(database: &Path, table: &str) -> usize { + let conn = Connection::open(database).expect("the oracle connection opens"); + let count: i64 = conn + .query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |row| { + row.get(0) + }) + .expect("the oracle query succeeds"); + count.max(0) as usize +} + +/// The sqlite kv tier's pool residency — exactly one row per committed +/// kv-tier entry (the CAS table's rows). +fn sqlite_pool_rows(site: &Site) -> usize { + sqlite_table_rows(site.database(), "blobs") +} + +/// The durable window's staged-candidate residency — the schedule +/// observer the durable rows assert against (staged rows land here; +/// the executing sweep's take empties them). +fn sqlite_window_rows(site: &Site) -> usize { + sqlite_table_rows(site.database(), "gc_window") +} + +// ------------------------------------------------------------------ +// The column's own rows — the durable-schedule sweep family, the +// schedule-relative readings of the sweep-counting INV rows, and the +// local-specific forms. Every row takes this column's concrete +// factory (it observes the backing medium), is ADR-stamped per the +// convention, and traces its family. +// ------------------------------------------------------------------ + +/// INV-2 on the durable schedule: identical content put twice — +/// through the same put path and across the two paths — is one pool +/// entry (one row in the sqlite file), and the re-put's `Pin` +/// protects exactly as a fresh put's does. One-pool-entry is asserted +/// through durable-schedule outcomes (the INV-5 posture): the pins +/// held stage nothing; once unpinned, sweep 1 stages exactly the one +/// entry and removes nothing, and its executor sweep deletes exactly +/// that staged set — never a list comparison, and never a single-sweep +/// count (README §The schedule-relative reading for SQL-backed +/// columns). +/// +/// Contract stamp: store-api.md §Invariants 2; ADR-003 (whole-value +/// put is CAS first-writer-wins); ADR-005 (a re-put refcounts the +/// existing entry under the same arbitration lock); ADR-008 §2 (the +/// staged re-arbitrated delete); ADR-012 §6.7 (the executor is the +/// next sweep — the schedule these counts read against). +pub async fn dedup_identical_content_is_one_pool_entry(factory: &DualTierSqliteFactory) { + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + SetSource::register(&store); + + let content = b"deduplicated content".to_vec(); + let canonical = Key::derive(&content, content.len() as u64); + + let (key, first) = store + .put(Some(content.len() as u64), Cursor::new(content.as_slice())) + .await + .expect("the fresh put publishes"); + let (reput, second) = store + .put(Some(content.len() as u64), Cursor::new(content.as_slice())) + .await + .expect("the same-path re-put publishes"); + assert_eq!(key, canonical); + assert_eq!(reput, canonical, "one derivation, one address"); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(report.aborted, None); + assert_eq!( + report.candidates_staged, 0, + "one pool entry, both refcounts composing under the same lock" + ); + assert_eq!( + sqlite_pool_rows(&site), + 1, + "one pool entry, both refcounts — one row in the sqlite file" + ); + drop(first); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + report.candidates_staged, 0, + "the re-put's pin protects exactly as a fresh put's" + ); + drop(second); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + (report.candidates_staged, report.deleted), + (1, 0), + "exactly one pool entry all along — staged this sweep, not executed" + ); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + (report.deleted, report.cancelled_by_arbitration), + (1, 0), + "the executing sweep deleted exactly the staged entry" + ); + assert!(!store.has(&canonical.as_bytes()).await.unwrap()); + assert_eq!(sqlite_pool_rows(&site), 0, "the executed delete landed"); + + let cross = b"deduplicated across the put paths".to_vec(); + let cross_canonical = Key::derive(&cross, cross.len() as u64); + let (key, first) = store + .put(Some(cross.len() as u64), Cursor::new(cross.as_slice())) + .await + .expect("the known-length put publishes"); + let (reput, second) = store + .put(None, Cursor::new(cross.as_slice())) + .await + .expect("the unknown-length re-put publishes"); + assert_eq!(key, cross_canonical); + assert_eq!(reput, cross_canonical, "one pool entry across the paths"); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(report.candidates_staged, 0, "both refcounts compose"); + drop(second); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + report.candidates_staged, 0, + "the re-put's pin protects exactly as a fresh put's (other drop order)" + ); + drop(first); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + (report.candidates_staged, report.deleted), + (1, 0), + "exactly one pool entry all along" + ); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(report.deleted, 1, "executed exactly the staged set"); + assert!(!store.has(&cross_canonical.as_bytes()).await.unwrap()); + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +/// INV-3 on the durable schedule: stage hygiene — every put-path +/// failure leaves zero residue and every success leaves exactly one +/// committed entry, over both put paths and both injectable failure +/// modes (a mid-stream stream failure and a declared-length +/// mismatch). The kv tier's stage form is degenerate (nothing stages +/// outside a flush), so residue is the pool itself — the sqlite file's +/// rows, which the failures never touch — observed through +/// durable-schedule sweep outcomes: zero staged after every failure, +/// and a success staged exactly once, then executed exactly once. +/// +/// Contract stamp: store-api.md §Invariants 3; ADR-010 §1 (the +/// staged-put contract: commit publishes, every failure path +/// discards); ADR-003 (stage-then-commit, the kv form degenerate); +/// ADR-012 §6.7 (the schedule-relative reading the success counts +/// follow). +pub async fn stage_hygiene_zero_residue_on_failure_exactly_one_entry_on_success( + factory: &DualTierSqliteFactory, +) { + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + SetSource::register(&store); + + let content = patterned(200, 167); + let canonical = Key::derive(&content, content.len() as u64); + + let err = store + .put(Some(content.len() as u64), Failing::new(&content, 40)) + .await + .expect_err("the failed mid-stream put rejects"); + assert!( + matches!(err, Error::Io(_)), + "the stream failure surfaces as Io: {err}" + ); + let err = store + .put(None, Failing::new(&content, 40)) + .await + .expect_err("the failed unknown-length put rejects"); + assert!( + matches!(err, Error::Io(_)), + "the stream failure surfaces as Io: {err}" + ); + let err = store + .put(Some(100), Cursor::new(content.as_slice())) + .await + .expect_err("the over-fed mismatch rejects"); + assert_eq!(err, Error::Verification, "declared-len mismatch rejects"); + let err = store + .put(Some(400), Cursor::new(content.as_slice())) + .await + .expect_err("the under-fed mismatch rejects"); + assert_eq!(err, Error::Verification, "declared-len mismatch rejects"); + + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(report.aborted, None); + assert_eq!( + ( + report.candidates_staged, + report.deleted, + report.cancelled_by_arbitration + ), + (0, 0, 0), + "zero residue after every failure, both put paths, both failure modes" + ); + assert_eq!( + sqlite_pool_rows(&site), + 0, + "the pool table holds zero entries after every failure" + ); + + let (key, pin) = store + .put(Some(content.len() as u64), Cursor::new(content.as_slice())) + .await + .expect("the known-length put publishes"); + assert_eq!(key, canonical); + drop(pin); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + (report.candidates_staged, report.deleted), + (1, 0), + "exactly one committed entry staged on success" + ); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + report.deleted, 1, + "the executing sweep removed exactly that one entry" + ); + assert!(!store.has(&canonical.as_bytes()).await.unwrap()); + + let (key, pin) = store + .put(None, Cursor::new(content.as_slice())) + .await + .expect("the unknown-length put publishes"); + assert_eq!(key, canonical); + drop(pin); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + (report.candidates_staged, report.deleted), + (1, 0), + "exactly one committed entry staged on the other put path too" + ); + let report = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(report.deleted, 1, "and executed exactly that staged set"); + assert!(!store.has(&canonical.as_bytes()).await.unwrap()); + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +/// INV-4, the durable-schedule happy shape: the full protection set +/// spanning both tiers (a source-live kv entry, a pinned large-tier +/// entry, a protect-named large-tier entry) plus one dead entry per +/// tier — sweep 1 stages exactly the two dead candidates and deletes +/// nothing (staging is not executing: the pool and the medium hold +/// everything and the window holds the staged rows); sweep 2 executes +/// exactly that staged set (`deleted + cancelled == N` against the +/// staged set), and the protected entries stay byte-identical. The +/// exact staged candidates also make this the INV-5 posture's +/// both-tiers form: list correctness (complete and well-formed across +/// both tiers' enumerations) is observable only through these +/// outcomes. The medium oracle distinguishes the tiers (the sqlite +/// file's pool rows vs the root's committed files) and watches the +/// schedule itself (the window's rows). +/// +/// Contract stamp: store-api.md §Invariants 4 (exact counts with +/// correct liveness) and §Invariants 5 (list correctness through +/// sweep outcomes); ADR-005 (three liveness marks; delete windows +/// with arbitrate-at-delete-time); ADR-008 §2 (the staged +/// re-arbitrated delete; the constructor-tunable delay); ADR-012 §1 +/// (sweeps are never errors — the report is the outcome record); +/// ADR-012 §6.7 (the executor is the next sweep — the schedule these +/// counts read against); ADR-007 (sqlite, the default-build kv +/// engine). +pub async fn two_sweep_exact_counts_over_the_full_protection_set(factory: &DualTierSqliteFactory) { + let threshold = factory.threshold; + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + let source = SetSource::register(&store); + let protect = Arc::new(SetProtect::default()); + store.register_protect_callback(protect.clone()); + + let source_live = patterned((threshold - 1) as usize, 191); + let (key_live, _) = store + .put( + Some(source_live.len() as u64), + Cursor::new(source_live.as_slice()), + ) + .await + .expect("the source-live entry publishes"); + source.add(key_live.clone()); + + let pinned = patterned(2 * threshold as usize + 7, 173); + let (key_pinned, pin_pinned) = store + .put(None, Cursor::new(pinned.as_slice())) + .await + .expect("the pinned entry publishes"); + + let protected = patterned(2 * threshold as usize + 11, 167); + let (key_protected, _) = store + .put( + Some(protected.len() as u64), + Cursor::new(protected.as_slice()), + ) + .await + .expect("the protect-named entry publishes"); + protect.set_keys(vec![key_protected.clone()]); + + let dead_kv = patterned((threshold - 3) as usize, 149); + let (key_dead_kv, pin) = store + .put(Some(dead_kv.len() as u64), Cursor::new(dead_kv.as_slice())) + .await + .expect("the kv-tier dead entry publishes"); + drop(pin); + let dead_large = patterned(2 * threshold as usize + 13, 151); + let (key_dead_large, pin) = store + .put(None, Cursor::new(dead_large.as_slice())) + .await + .expect("the large-tier dead entry publishes"); + drop(pin); + + assert_eq!( + (sqlite_pool_rows(&site), committed_file_count(site.root())), + (2, 3), + "before the sweep, both tiers hold their entries exactly \ + (two pool rows in the sqlite file, three committed files \ + under the root)" + ); + + let first = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(first.aborted, None); + assert_eq!( + ( + first.candidates_staged, + first.deleted, + first.cancelled_by_arbitration + ), + (2, 0, 0), + "sweep 1 staged exactly the two dead entries and deleted \ + nothing — staging is not executing" + ); + assert_eq!( + sqlite_window_rows(&site), + 2, + "the staged set waits in the durable window" + ); + for (key, content) in [ + (&key_live, &source_live), + (&key_pinned, &pinned), + (&key_protected, &protected), + ] { + let (len, blob) = store + .get(&key.as_bytes()) + .await + .expect("get succeeds") + .expect("the protected entry is present"); + assert_eq!( + blob.read_at(0, len).await.expect("the window reads"), + content.as_slice(), + "the protected entry is byte-identical after the staging sweep" + ); + } + assert!( + store.has(&key_live.as_bytes()).await.unwrap() + && store.has(&key_pinned.as_bytes()).await.unwrap() + && store.has(&key_protected.as_bytes()).await.unwrap(), + "the staged sweep deleted nothing: every entry intact" + ); + + let second = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(second.aborted, None); + assert_eq!( + second.candidates_staged, 0, + "nothing newly dead since the staging sweep" + ); + assert_eq!( + second.deleted + second.cancelled_by_arbitration, + 2, + "sweep 2 executed exactly the staged set" + ); + assert_eq!( + (second.deleted, second.cancelled_by_arbitration), + (2, 0), + "with correct liveness, every executed candidate deleted" + ); + assert!(!store.has(&key_dead_kv.as_bytes()).await.unwrap()); + assert!(!store.has(&key_dead_large.as_bytes()).await.unwrap()); + assert_eq!( + (sqlite_pool_rows(&site), committed_file_count(site.root())), + (1, 2), + "the executed deletes landed on both media exactly" + ); + assert_eq!( + sqlite_window_rows(&site), + 0, + "the executed rows left the durable window" + ); + + drop(pin_pinned); + let third = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + (third.candidates_staged, third.deleted), + (1, 0), + "with the pin dropped, the formerly pinned entry stages" + ); + let fourth = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + fourth.deleted, 1, + "and the next sweep executes it — exactly one deletion" + ); + assert!(!store.has(&key_pinned.as_bytes()).await.unwrap()); + assert!(store.has(&key_live.as_bytes()).await.unwrap()); + assert!(store.has(&key_protected.as_bytes()).await.unwrap()); + assert_eq!( + committed_file_count(site.root()), + 1, + "exactly the protect-named entry's file remains" + ); + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +/// INV-4, the race, in its durable-schedule form: a put landing +/// *during the window* survives the executing sweep — the re-put's +/// pin is minted (pin-before-publish) between the staging sweep and +/// the executing sweep, so the execution-time re-arbitration cancels +/// the pinned candidate while the unpinned twin deletes; once the pin +/// drops, the twin re-stages and executes exactly once. Both put +/// paths race in-body over both tiers (a kv twin and a large twin). +/// +/// Contract stamp: store-api.md §Invariants 4; ADR-005 (pin-before- +/// publish ordering; the delete window closes the sweep-vs-put race); +/// ADR-008 §2 (the staged re-arbitrated delete — the race holds +/// across the window delay); ADR-012 §6.7 (execution-time +/// re-arbitration is the executor sweep's posture). +pub async fn a_put_landing_during_the_window_survives_the_executing_sweep( + factory: &DualTierSqliteFactory, +) { + let threshold = factory.threshold; + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + SetSource::register(&store); + + let rounds: [(String, Vec, bool, bool); 2] = [ + ( + "the kv twin (known-path re-put)".to_owned(), + patterned((threshold - 1) as usize, 191), + true, + true, + ), + ( + "the large twin (unknown-path re-put)".to_owned(), + patterned(2 * threshold as usize + 5, 173), + false, + false, + ), + ]; + for (index, (tag, content, original_known, reput_known)) in rounds.iter().enumerate() { + let len = content.len() as u64; + let twin = Key::derive(content, len); + let witness = format!("the dead witness of round {index}").into_bytes(); + let witness_key = Key::derive(&witness, witness.len() as u64); + + let original = if *original_known { Some(len) } else { None }; + let (_, pin) = store + .put(original, Cursor::new(content.as_slice())) + .await + .expect("the twin publishes"); + drop(pin); + let (_, pin) = store + .put(None, Cursor::new(witness.as_slice())) + .await + .expect("the witness publishes"); + drop(pin); + + let staged = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + ( + staged.aborted, + staged.candidates_staged, + staged.deleted, + staged.cancelled_by_arbitration + ), + (None, 2, 0, 0), + "{tag}: the staging sweep stages exactly the two dead \ + twins and deletes nothing" + ); + + // The put lands during the window (the re-put dedups into the + // existing twin and refcounts its pin): protected across the + // delay, pin-before-publish. + let reput = if *reput_known { Some(len) } else { None }; + let (_, pin) = store + .put(reput, Cursor::new(content.as_slice())) + .await + .expect("the re-put publishes"); + let executed = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(executed.aborted, None, "{tag}"); + assert_eq!( + executed.candidates_staged, 0, + "{tag}: nothing newly dead since the staging sweep" + ); + assert_eq!( + executed.deleted + executed.cancelled_by_arbitration, + 2, + "{tag}: the executing sweep ran exactly the staged set" + ); + assert_eq!(executed.deleted, 1, "{tag}: the unpinned witness deleted"); + assert_eq!( + executed.cancelled_by_arbitration, 1, + "{tag}: the pinned twin's candidate cancelled at execution time" + ); + assert!( + !store.has(&witness_key.as_bytes()).await.unwrap(), + "{tag}: the unpinned twin is gone" + ); + let (got_len, blob) = store + .get(&twin.as_bytes()) + .await + .expect("get succeeds") + .expect("{tag}: the put landing during the window survived"); + assert_eq!(got_len, len, "{tag}"); + assert_eq!( + blob.read_at(0, len).await.expect("the window reads"), + content.as_slice(), + "{tag}: the surviving twin is byte-identical" + ); + drop(pin); + + let restaged = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + (restaged.candidates_staged, restaged.deleted), + (1, 0), + "{tag}: the pin dropped — the twin re-stages" + ); + let executed = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + executed.deleted, 1, + "{tag}: the re-staged candidate executes exactly once" + ); + assert!(!store.has(&twin.as_bytes()).await.unwrap()); + assert_eq!( + sqlite_window_rows(&site), + 0, + "{tag}: the window holds nothing after the executed round" + ); + } + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +/// INV-4, abort shape: the sweep is single-flight on the durable form +/// too — the in-process sweep lease (ADR-013 §2's single-node analog). +/// The contended second sweeper aborts with `SweeperLock` in a +/// completed `Ok(SweepReport)` — nothing executed, nothing staged, +/// nothing deleted (abort before staging: form-independent) — while +/// the lease holder's sweep completes by **staging only**: its staged +/// set waits in the window for the next sweep to execute (the +/// durable form's schedule-relative replacement for the reference +/// row's inline holder-completion leg). +/// +/// Contract stamp: store-api.md §Invariants 4; ADR-012 §1 (abort is +/// report data, never an error); ADR-013 §2 (the named exclusive +/// sweep lease — one sweeper per pool, holdership ends at release); +/// ADR-012 §6.7 (the executor is the next sweep — the holder stages, +/// it does not execute). +pub async fn the_contended_second_sweeper_aborts_as_data_and_the_holder_stages_only( + factory: &DualTierSqliteFactory, +) { + let threshold = factory.threshold; + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + SetSource::register(&store); + + let (entered_tx, entered_rx) = std::sync::mpsc::channel::<()>(); + let (release_tx, release_rx) = std::sync::mpsc::channel::<()>(); + store.register_protect_callback(Arc::new(LatchProtect { + entered: entered_tx, + release: parking_lot::Mutex::new(Some(release_rx)), + })); + + let content = patterned((threshold - 1) as usize, 167); + let (key, pin) = store + .put(Some(content.len() as u64), Cursor::new(content.as_slice())) + .await + .expect("the dead witness publishes"); + drop(pin); + + // The lease holder sweeps on its own task (its protect consult + // blocks that task's thread mid-callback); this task contends and + // then releases the holder. + let store = Arc::new(store); + let holder = tokio::spawn({ + let store = Arc::clone(&store); + async move { store.sweep().await } + }); + + while entered_rx.recv_timeout(Duration::from_millis(50)).is_err() { + tokio::task::yield_now().await; + } + + let second = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + second.aborted, + Some(alkblobs::GcAbortCause::SweeperLock), + "the contended sweeper aborts as report data" + ); + assert_eq!( + ( + second.candidates_staged, + second.deleted, + second.cancelled_by_arbitration + ), + (0, 0, 0), + "the contended sweep executes, stages, and deletes nothing" + ); + assert!( + store.has(&key.as_bytes()).await.unwrap(), + "the contended sweep deleted nothing" + ); + assert_eq!( + sqlite_window_rows(&site), + 0, + "the aborted sweep staged nothing into the window" + ); + release_tx.send(()).ok(); + + let report = holder + .await + .expect("the holder task joined") + .expect("sweeps are never errors"); + assert_eq!(report.aborted, None, "the lease holder completes"); + assert_eq!( + (report.candidates_staged, report.deleted), + (1, 0), + "the holder stages exactly the dead set — it does not execute \ + (the durable form's schedule)" + ); + assert_eq!( + sqlite_window_rows(&site), + 1, + "the holder's staged set waits in the durable window" + ); + assert!( + store.has(&key.as_bytes()).await.unwrap(), + "the holder staged, did not execute: the entry is intact" + ); + + let executed = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + executed.deleted, 1, + "the next sweep is the executor of the staged set" + ); + assert!(!store.has(&key.as_bytes()).await.unwrap()); + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +/// INV-5 on the durable schedule: `list()` correctness is observable +/// only through GC — the enumeration is complete and well-formed in +/// both directions, proven by exact durable-schedule outcomes: the +/// staging sweep stages exactly the dead set (neither more nor fewer +/// candidates), the executing sweep deletes exactly that staged set, +/// and a liveness flip re-stages exactly the newly dead entry. Never +/// a direct list comparison. +/// +/// Contract stamp: store-api.md §Invariants 5; ADR-003 (`list()` is +/// complete and well-formed by contract — load-bearing for GC); +/// ADR-005 (the sweep's live-set diff rides `list()`); ADR-012 §6.7 +/// (the executor is the next sweep — the schedule these counts read +/// against). +pub async fn list_correctness_is_observable_through_durable_schedule_outcomes( + factory: &DualTierSqliteFactory, +) { + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + let source = SetSource::register(&store); + + let mut keys = Vec::new(); + for (index, content) in [ + b"kept live by the source, entry one".to_vec(), + b"kept live by the source, entry two".to_vec(), + b"dead at the first staging, entry three".to_vec(), + b"dead at the first staging, entry four".to_vec(), + b"dead at the first staging, entry five".to_vec(), + ] + .into_iter() + .enumerate() + { + let declared = if index % 2 == 0 { + Some(content.len() as u64) + } else { + None + }; + let (key, pin) = store + .put(declared, Cursor::new(content.as_slice())) + .await + .expect("the entry publishes"); + drop(pin); + keys.push(key); + } + source.add(keys[0].clone()); + source.add(keys[1].clone()); + + let first = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(first.aborted, None); + assert_eq!( + (first.candidates_staged, first.deleted), + (3, 0), + "the enumeration staged exactly the three dead entries — \ + complete and well-formed in both directions" + ); + for key in &keys { + assert!( + store.has(&key.as_bytes()).await.unwrap(), + "staging deleted nothing: every enumerated entry is intact" + ); + } + assert_eq!( + sqlite_window_rows(&site), + 3, + "the staged schedule holds the three candidates" + ); + + let second = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + (second.candidates_staged, second.deleted), + (0, 3), + "the executing sweep removed exactly the staged set" + ); + assert!(store.has(&keys[0].as_bytes()).await.unwrap()); + assert!(store.has(&keys[1].as_bytes()).await.unwrap()); + for key in &keys[2..] { + assert!(!store.has(&key.as_bytes()).await.unwrap()); + } + + source.remove(&keys[1]); + let third = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + (third.candidates_staged, third.deleted), + (1, 0), + "the newly dead entry is staged exactly" + ); + let fourth = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(fourth.deleted, 1, "and executed exactly by the next sweep"); + assert!(!store.has(&keys[1].as_bytes()).await.unwrap()); + assert!(store.has(&keys[0].as_bytes()).await.unwrap()); + + source.remove(&keys[0]); + let fifth = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(fifth.candidates_staged, 1, "the last entry stages exactly"); + let sixth = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(sixth.deleted, 1, "and executes exactly"); + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +/// Dispatch row: **routing is a pure function of length** — at, below, +/// and over the declared threshold, the same content lands on exactly +/// the same tier through both put paths (same placement every time; +/// nothing moves, nothing duplicates), identical content through both +/// paths is one pool entry, and every read is byte-identical from +/// wherever that tier holds it. +/// +/// Tier placement is observed through the backing medium (the module +/// doc's oracle, adapted): kv entries hold a row in the sqlite file +/// and none under the root; large entries hold a committed file under +/// the root and no pool row — the facade cannot and must not expose +/// tier residency (it is engine-blind). +/// +/// Contract stamp: store-api.md §Invariants 7 (facade-contract row); +/// ADR-003 §Dispatch policy (routing is a pure function of length; +/// no migration; CAS first-writer-wins makes re-puts one entry); +/// ADR-005 (re-put refcounts); ADR-007 (sqlite, the default-build +/// kv engine); ADR-012 §6.3 (the dual-tier dispatch-coverage shape); +/// ADR-012 §6.7 (the schedule-relative cleanup the dedup legs read +/// against). +pub async fn routing_is_a_pure_function_of_length_across_put_paths_and_re_puts( + factory: &DualTierSqliteFactory, +) { + let threshold = factory.threshold; + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + SetSource::register(&store); + + for content in [ + patterned((threshold - 1) as usize, 191), + patterned(threshold as usize, 173), + patterned((threshold + 1) as usize, 167), + patterned(4 * threshold as usize + 9, 151), + ] { + let len = content.len() as u64; + let canonical = Key::derive(&content, len); + let expected_rows = if len > threshold { 0 } else { 1 }; + let expected_files = if len > threshold { 1 } else { 0 }; + + for (path_tag, declared) in [("the known path", Some(len)), ("the unknown path", None)] { + let (key, pin) = store + .put(declared, Cursor::new(content.as_slice())) + .await + .expect("put publishes"); + assert_eq!( + key, canonical, + "the canonical derivation addresses the put on {path_tag}" + ); + assert_eq!( + sqlite_pool_rows(&site), + expected_rows, + "length {len} on {path_tag}: the placement is exactly the \ + routing function's answer and never moves or duplicates \ + (the sqlite medium holds {expected_rows} kv rows)" + ); + assert_eq!( + committed_file_count(site.root()), + expected_files, + "length {len} on {path_tag}: the large-tier medium holds \ + {expected_files} committed files" + ); + assert_eq!( + staging_file_count(site.root()), + 0, + "a published put leaves no stage file on {path_tag}" + ); + let (got_len, blob) = store + .get(&key.as_bytes()) + .await + .expect("get succeeds") + .expect("the entry is present"); + assert_eq!( + got_len, len, + "the read serves the entry from its placed tier" + ); + assert_eq!( + blob.read_at(0, len).await.expect("the whole window reads"), + content, + "the entry is byte-identical from its placed tier across paths" + ); + drop(pin); + } + + let staged = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + staged.candidates_staged, 1, + "identical content through both put paths is one pool entry" + ); + let executed = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + executed.deleted, 1, + "the executing sweep removed exactly the one entry (durable \ + schedule: staged this sweep, executed by the next)" + ); + assert_eq!( + (sqlite_pool_rows(&site), committed_file_count(site.root())), + (0, 0), + "both media are back to zero" + ); + } + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +/// Dispatch row: **threshold boundary lengths route deterministically** +/// — at/below/above the factory's declared threshold, on both put +/// paths, the placement is the routing function's answer every time +/// (the medium oracle distinguishes the sqlite file's rows from the +/// root's committed files); and the default-threshold boundary +/// re-routes with the tuning (the constructor-tuned threshold +/// deterministically places the default boundary's length on the tier +/// its own threshold names). Run against two factories by the runner +/// — the column's tuned threshold and the shipped default — the same +/// row shape at two declared thresholds being the tunable re-route +/// demonstration. The durable cleanup: the staged sweep stages exactly +/// the six entries' dead set and the next sweep executes it. +/// +/// Contract stamp: store-api.md §Invariants 7 (facade-contract row); +/// ADR-003 §Dispatch policy (default 128 KiB at-or-below routes kv; +/// constructor-tunable without API change); ADR-007 (sqlite, the +/// default-build kv engine); ADR-012 §6.3 (the dual-tier +/// dispatch-coverage shape); ADR-012 §6.7 (schedule-relative cleanup). +pub async fn threshold_boundary_lengths_route_deterministically(factory: &DualTierSqliteFactory) { + let threshold = factory.threshold; + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + SetSource::register(&store); + + let boundary = [ + (threshold - 1, 191u32, false), + (threshold, 173, false), + (threshold, 167, false), + (threshold + 1, 151, true), + (threshold + 1, 139, true), + ]; + let mut expected_rows = 0u64; + let mut expected_files = 0u64; + for (len, modulus, lands_large) in boundary { + let content = patterned(len as usize, modulus); + for declared in [Some(content.len() as u64), None] { + let (key, pin) = store + .put(declared, Cursor::new(content.as_slice())) + .await + .expect("put publishes"); + assert_eq!( + key, + Key::derive(&content, len), + "the canonical derivation addresses the put" + ); + assert_eq!( + sqlite_pool_rows(&site) as u64, + expected_rows + if lands_large { 0 } else { 1 }, + "boundary length {len}: at/below the threshold lands kv \ + (one row in the sqlite file), over it lands large (a \ + committed file instead), both put paths" + ); + assert_eq!( + committed_file_count(site.root()) as u64, + expected_files + if lands_large { 1 } else { 0 }, + "boundary length {len}: the large-tier medium matches the \ + routing answer" + ); + drop(pin); + } + if lands_large { + expected_files += 1; + } else { + expected_rows += 1; + } + } + + // The default boundary re-routes with the constructor's tuning: + // the shipped default's length belongs to the tier this + // configuration's own threshold puts it on. + let at_default = patterned(DEFAULT_DISPATCH_THRESHOLD as usize, 199); + let default_lands_large = DEFAULT_DISPATCH_THRESHOLD > threshold; + let (key, pin) = store + .put( + Some(DEFAULT_DISPATCH_THRESHOLD), + Cursor::new(at_default.as_slice()), + ) + .await + .expect("the default-boundary put publishes"); + assert_eq!( + key, + Key::derive(&at_default, DEFAULT_DISPATCH_THRESHOLD), + "the canonical derivation addresses the put" + ); + assert_eq!( + sqlite_pool_rows(&site) as u64, + expected_rows + if default_lands_large { 0 } else { 1 }, + "the default boundary's length routes to the tier this \ + configuration's tuning names (the sqlite medium's answer)" + ); + assert_eq!( + committed_file_count(site.root()) as u64, + expected_files + if default_lands_large { 1 } else { 0 }, + "the default boundary's length routes to the tier this \ + configuration's tuning names (the large medium's answer)" + ); + drop(pin); + + let staged = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + staged.candidates_staged, 6, + "each leg's content was exactly one pool entry" + ); + let executed = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + executed.deleted, 6, + "the executing sweep removed every entry, tier notwithstanding" + ); + assert_eq!( + (sqlite_pool_rows(&site), committed_file_count(site.root())), + (0, 0), + "both media are back to zero" + ); + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +/// Dispatch row: **a kv-tier miss falls through to the large tier on +/// every key-addressed op** — content the routing function placed on +/// the large tier (one committed file under the root; the sqlite +/// medium holds no pool row — the kv tier misses) serves `get`, +/// `stat`, `has`, and `read_range` whole, exact, and digest-correct; +/// the mirror-position leg proves the kv tier is the *first* consult +/// (sub-threshold content, one sqlite row and no committed file, +/// reads still served); and both-tier misses resolve per the error +/// model's per-op shapes (reached here by the durable schedule's +/// staged-then-executed deletion of the subject entry). +/// +/// Contract stamp: store-api.md §Read path (fall-through consult +/// order); ADR-012 §6.1 (every key-addressed op consults kv first, +/// then large; the general rule); ADR-012 §6.5 ruling 5 (both-tier +/// miss shapes: `None` on get/stat, `Err(Missing)` on read_range); +/// ADR-006 (the slice digest); ADR-007 (sqlite, the default-build +/// kv engine); ADR-012 §6.3 (the dispatch-coverage shape). +pub async fn kv_tier_misses_fall_through_to_the_large_tier_per_op(factory: &DualTierSqliteFactory) { + let threshold = factory.threshold; + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + SetSource::register(&store); + + // Over-threshold content — the kv tier misses, the large tier + // serves every op. Both put paths hold the shape. + for (content, declared) in [ + ( + patterned(3 * threshold as usize + 41, 181), + Some(3 * threshold + 41), + ), + (patterned(3 * threshold as usize + 43, 197), None), + ] { + let len = content.len() as u64; + let (key, pin) = store + .put(declared, Cursor::new(content.as_slice())) + .await + .expect("put publishes"); + assert_eq!( + (sqlite_pool_rows(&site), committed_file_count(site.root())), + (0, 1), + "the content lives on exactly the large tier — the kv tier \ + misses" + ); + + let (got_len, blob) = store + .get(&key.as_bytes()) + .await + .expect("get succeeds") + .expect("the large tier served the kv miss"); + assert_eq!(got_len, len); + assert_eq!( + blob.read_at(0, len).await.expect("the whole window reads"), + content, + "the fall-through read is byte-identical" + ); + assert_eq!( + store.stat(&key.as_bytes()).await.expect("stat succeeds"), + Some(alkblobs::EntryMeta { + len, + algorithm: key.algorithm(), + }), + "stat falls through and carries the length plus algorithm" + ); + assert!( + store.has(&key.as_bytes()).await.unwrap(), + "has falls through" + ); + let mid = len / 2; + let (slice, digest) = store + .read_range(&key.as_bytes(), mid, 17) + .await + .expect("read_range falls through"); + assert_eq!( + slice, + content[(mid as usize)..(mid as usize + 17)], + "the range slice is exact" + ); + assert_eq!( + digest, + slice_digest(&slice), + "the slice digest is SHA-256 over exactly the returned bytes" + ); + + // Sweep the entry away (the pin dropped): the miss shapes now + // resolve per op — the large tier misses too. The durable + // schedule: staged this sweep, executed by the next. + drop(pin); + let staged = store.sweep().await.expect("sweeps are never errors"); + assert_eq!((staged.candidates_staged, staged.deleted), (1, 0)); + let executed = store.sweep().await.expect("sweeps are never errors"); + assert_eq!( + executed.deleted, 1, + "exactly the one entry died across the two sweeps" + ); + assert!( + store + .get(&key.as_bytes()) + .await + .expect("get succeeds") + .is_none(), + "a both-tier miss on get is Ok(None)" + ); + assert_eq!( + store.stat(&key.as_bytes()).await.expect("stat succeeds"), + None, + "a both-tier miss on stat is Ok(None)" + ); + assert!( + !store.has(&key.as_bytes()).await.unwrap(), + "a both-tier miss on has is false" + ); + assert_eq!( + store + .read_range(&key.as_bytes(), 0, 4) + .await + .expect_err("the both-tier miss errs"), + Error::Missing, + "a both-tier miss on read_range is Err(Missing)" + ); + } + + // The mirror-position leg: sub-threshold content — the kv tier is + // the first consult and serves before the large tier is reached + // (one row in the sqlite file; no committed file ever lands). + let small = patterned((threshold - 1) as usize, 149); + let (key, pin) = store + .put(Some(small.len() as u64), Cursor::new(small.as_slice())) + .await + .expect("the sub-threshold put publishes"); + assert_eq!( + (sqlite_pool_rows(&site), committed_file_count(site.root())), + (1, 0), + "the kv tier holds the sub-threshold entry — the large tier was \ + never consulted on the write, never reached on the read" + ); + let (len, blob) = store + .get(&key.as_bytes()) + .await + .expect("get succeeds") + .expect("the kv tier served"); + assert_eq!(len, small.len() as u64); + assert_eq!( + blob.read_at(0, len).await.expect("the whole window reads"), + small + ); + + drop(pin); + + // Both-tier miss shapes for a key that never existed at all, per + // op, over the fresh both-tier store. + let absent = b"never put here".to_vec(); + let absent = Key::derive(&absent, absent.len() as u64); + assert!( + store + .get(&absent.as_bytes()) + .await + .expect("get succeeds") + .is_none(), + "never-put: get is Ok(None)" + ); + assert_eq!( + store.stat(&absent.as_bytes()).await.expect("stat succeeds"), + None, + "never-put: stat is Ok(None)" + ); + assert!( + !store.has(&absent.as_bytes()).await.unwrap(), + "never-put: has is false" + ); + assert_eq!( + store + .read_range(&absent.as_bytes(), 0, 4) + .await + .expect_err("never-put read_range errs"), + Error::Missing, + "never-put: read_range is Err(Missing)" + ); + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +/// Virgin-store row: **reads are no-ops on a fresh file** — an empty +/// `list`, absent reads per op (`None` on get/stat, `false` on has, +/// `Err(Missing)` on read_range), and nothing on the fresh medium +/// itself (zero pool rows, zero window rows, zero committed files). +/// The sqlite engine's no-tables-yet read tolerance (every read +/// against a genuinely tableless database file is a no-op, never a +/// "table does not exist" error) is instrumented engine-side — a +/// store constructor always ensures the schema — so the facade's +/// fresh-file form of the tolerance is this row's assertions. +/// +/// Contract stamp: store-api.md §Error model (virgin-store semantics: +/// reads on a fresh store see absent/empty, never errors); ADR-012 +/// §6.5 ruling 5 (the per-op miss shapes); ADR-007 (the sqlite +/// engine — the read tolerance's engine-level posture and the +/// schema-creation timing design point). +pub async fn virgin_store_reads_are_no_ops_on_a_fresh_file(factory: &DualTierSqliteFactory) { + let (store, site) = factory + .open_tracked() + .await + .expect("open vends a fresh, isolated store"); + assert_eq!( + store.mode(), + StoreMode::DualTier, + "the column's factory is the dispatch-coverage shape" + ); + + let absent = b"nothing was ever put".to_vec(); + let absent = Key::derive(&absent, absent.len() as u64); + + assert_eq!( + store.list().await.expect("list succeeds"), + Vec::::new(), + "the virgin pool lists empty" + ); + assert!( + store + .get(&absent.as_bytes()) + .await + .expect("get succeeds") + .is_none(), + "absent get is Ok(None)" + ); + assert_eq!( + store.stat(&absent.as_bytes()).await.expect("stat succeeds"), + None, + "absent stat is Ok(None)" + ); + assert!( + !store.has(&absent.as_bytes()).await.unwrap(), + "absent has is false" + ); + assert_eq!( + store + .read_range(&absent.as_bytes(), 0, 4) + .await + .expect_err("absent read_range errs"), + Error::Missing, + "absent read_range is Err(Missing)" + ); + assert_eq!( + ( + sqlite_pool_rows(&site), + sqlite_window_rows(&site), + committed_file_count(site.root()) + ), + (0, 0, 0), + "the fresh medium holds nothing (pool, durable window, and \ + committed tree all empty)" + ); + + drop(store); + factory.teardown().await.expect("teardown is idempotent"); +} + +// ------------------------------------------------------------------ +// The runners — the column's tests: the form-independent reference +// rows mirrored verbatim against the dual-tier factory (INV-1, INV-6, +// and the two no-op abort shapes), the durable-schedule rows above, +// the kv-only rejection row against the kv-only factory (unchanged — +// threshold-agnostic by discovery), and the factory contract's +// structural shapes. +// ------------------------------------------------------------------ + +#[tokio::test] +async fn sqlite_inv1_round_trip_puts_and_gets_are_byte_identical() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + rows::round_trip_puts_and_gets_are_byte_identical(&factory).await; +} + +#[tokio::test] +async fn sqlite_inv2_dedup_identical_content_is_one_pool_entry() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + dedup_identical_content_is_one_pool_entry(&factory).await; +} + +#[tokio::test] +async fn sqlite_inv3_stage_hygiene_zero_residue_one_entry_on_success() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + stage_hygiene_zero_residue_on_failure_exactly_one_entry_on_success(&factory).await; +} + +#[tokio::test] +async fn sqlite_inv4_two_sweep_exact_counts_over_the_full_protection_set() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + two_sweep_exact_counts_over_the_full_protection_set(&factory).await; +} + +#[tokio::test] +async fn sqlite_inv4_a_put_landing_during_the_window_survives_the_executing_sweep() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + a_put_landing_during_the_window_survives_the_executing_sweep(&factory).await; +} + +#[tokio::test] +async fn sqlite_inv4_sweep_without_liveness_sources_aborts_and_deletes_nothing() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + rows::sweep_without_liveness_sources_aborts_and_deletes_nothing(&factory).await; +} + +#[tokio::test] +async fn sqlite_inv4_sweep_with_a_failed_protect_callback_aborts_and_deletes_nothing() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + rows::sweep_with_a_failed_protect_callback_aborts_and_deletes_nothing(&factory).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn sqlite_inv4_the_contended_second_sweeper_aborts_as_data_and_the_holder_stages_only() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + the_contended_second_sweeper_aborts_as_data_and_the_holder_stages_only(&factory).await; +} + +#[tokio::test] +async fn sqlite_inv5_list_correctness_through_durable_schedule_outcomes() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + list_correctness_is_observable_through_durable_schedule_outcomes(&factory).await; +} + +#[tokio::test] +async fn sqlite_inv6_stat_reports_len_and_algorithm_without_fetching_content() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + rows::stat_reports_len_and_algorithm_without_fetching_content(&factory).await; +} + +#[tokio::test] +async fn sqlite_virgin_store_reads_are_no_ops_on_a_fresh_file() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + virgin_store_reads_are_no_ops_on_a_fresh_file(&factory).await; +} + +#[tokio::test] +async fn sqlite_kv_only_mode_rejects_over_threshold_puts_at_put_time() { + let factory = KvOnlySqliteFactory::new(KV_ONLY_THRESHOLD); + rows::kv_only_mode_rejects_over_threshold_puts_at_put_time(&factory).await; +} + +#[tokio::test] +async fn dispatch_routing_is_a_pure_function_of_length_across_put_paths_and_re_puts() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + routing_is_a_pure_function_of_length_across_put_paths_and_re_puts(&factory).await; +} + +#[tokio::test] +async fn dispatch_threshold_boundary_lengths_route_deterministically() { + let tuned = DualTierSqliteFactory::new(TUNED_THRESHOLD); + threshold_boundary_lengths_route_deterministically(&tuned).await; + let default = DualTierSqliteFactory::new(DEFAULT_DISPATCH_THRESHOLD); + threshold_boundary_lengths_route_deterministically(&default).await; +} + +#[tokio::test] +async fn dispatch_kv_tier_misses_fall_through_to_the_large_tier_per_op() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + kv_tier_misses_fall_through_to_the_large_tier_per_op(&factory).await; +} + +// ------------------------------------------------------------------ +// The factory contract's structural shapes (the mem and local +// columns' pattern, carried to the SQL-backed medium-backed factory): +// fresh isolated opens (fresh site per open, nothing shared) and +// idempotent teardown safe against a panicking row. Not conformance +// rows — no backlog entry — the factory contract's own wiring-level +// assertions. +// ------------------------------------------------------------------ + +/// Every `open()` vends a fresh, isolated store: no shared state +/// between islands, observable on the facade *and* on the fresh media +/// (each open's sqlite file and root are private and start empty). +#[tokio::test] +async fn factory_open_vends_fresh_isolated_sqlite_stores() { + let factory = DualTierSqliteFactory::new(TUNED_THRESHOLD); + + let (a, site_a) = factory.open_tracked().await.expect("open vends a store"); + let (b, site_b) = factory.open_tracked().await.expect("open vends another"); + assert_ne!( + site_a.database(), + site_b.database(), + "two opens never share a sqlite file" + ); + assert_ne!( + site_a.root(), + site_b.root(), + "two opens never share a local root" + ); + + let content_a = patterned(2 * TUNED_THRESHOLD as usize + 5, 167); + let content_b = patterned(TUNED_THRESHOLD as usize - 5, 191); + let (key_a, _) = a + .put( + Some(content_a.len() as u64), + Cursor::new(content_a.as_slice()), + ) + .await + .expect("island a's put publishes"); + let (key_b, _) = b + .put(None, Cursor::new(content_b.as_slice())) + .await + .expect("island b's put publishes"); + + assert!(a.has(&key_a.as_bytes()).await.unwrap()); + assert!( + !a.has(&key_b.as_bytes()).await.unwrap(), + "island a sees nothing of island b" + ); + assert!(b.has(&key_b.as_bytes()).await.unwrap()); + assert!( + !b.has(&key_a.as_bytes()).await.unwrap(), + "island b sees nothing of island a" + ); + assert_eq!( + ( + sqlite_pool_rows(&site_a), + committed_file_count(site_a.root()) + ), + (0, 1), + "island a's media hold exactly its own entry (a's content routes large)" + ); + assert_eq!( + ( + sqlite_pool_rows(&site_b), + committed_file_count(site_b.root()) + ), + (1, 0), + "island b's media hold exactly its own entry (b's content routes kv)" + ); + + let (c, site_c) = factory.open_tracked().await.expect("a third vends fresh"); + assert_eq!( + ( + sqlite_pool_rows(&site_c), + committed_file_count(site_c.root()) + ), + (0, 0), + "a third open's media are virgin" + ); + assert!( + !c.has(&key_a.as_bytes()).await.unwrap(), + "a third open shares nothing with the first" + ); + assert!( + !c.has(&key_b.as_bytes()).await.unwrap(), + "a third open shares nothing with the second" + ); + + factory + .teardown() + .await + .expect("teardown removes the sites"); + factory.teardown().await.expect("teardown repeats cleanly"); +} + +/// A failing assertion panics the row before teardown runs — and +/// teardown must make sense over the abandoned instance anyway: it is +/// idempotent against the panicked row's path, the vended files are +/// really gone, and the next open vends a fresh store on a fresh +/// (empty) medium (README §The factory contract). +#[tokio::test] +async fn factory_teardown_is_safe_against_a_panicking_row() { + let content = patterned(TUNED_THRESHOLD as usize - 7, 173); + let abandoned = Key::derive(&content, content.len() as u64); + let factory = Arc::new(DualTierSqliteFactory::new(TUNED_THRESHOLD)); + + let task = { + let factory = Arc::clone(&factory); + tokio::spawn(async move { + let (store, _) = factory.open_tracked().await.expect("open vends the store"); + let (_, pin) = store + .put(Some(content.len() as u64), Cursor::new(content.as_slice())) + .await + .expect("the put publishes"); + drop(pin); + panic!("a failing assertion panics the row (the factory contract's early exit)"); + }) + }; + assert!( + task.await.is_err(), + "the row's panic surfaced as its failure" + ); + + let databases = factory.tracked_databases(); + assert!(!databases.is_empty(), "the panicked row vended a site"); + + factory + .teardown() + .await + .expect("teardown makes sense over the abandoned instance"); + factory.teardown().await.expect("teardown is idempotent"); + + for database in databases { + assert!( + !database.exists(), + "teardown removed the panicked row's sqlite file: {database:?}" + ); + } + + let (store, site) = factory + .open_tracked() + .await + .expect("the next open still vends a store"); + assert!( + !store.has(&abandoned.as_bytes()).await.unwrap(), + "no residue from the panicked row — isolation is factory-owned" + ); + assert_eq!( + (sqlite_pool_rows(&site), committed_file_count(site.root())), + (0, 0), + "and its media carry nothing of the abandoned run" + ); + factory + .teardown() + .await + .expect("teardown removes the last site"); +}