From cddcea3cfa9368e0f763817771d4bf62fbc18529 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Mon, 12 Oct 2026 08:04:19 +0000 Subject: [PATCH] =?UTF-8?q?feat(store):=20the=20pg-lo=20constructor=20wiri?= =?UTF-8?q?ng=20=E2=80=94=20the=20consolidation=20shape=20and=20the=20wind?= =?UTF-8?q?ow-host=20completion=20(wave=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LargeEngine::PgLo slots in behind `pg-lo` + all six large-op dispatch arms; the large cursor/staging bodies land as the LargeCursor/LargeStaging sub-enums behind AnyCursor/AnyStaging's one large arm (the recorded KvCursor pattern's large-tier twin) - dual_tier_postgres_pglo (the consolidation posture; two DSN+opts pairs, implicitly non-fleet, completing the fleet-valid engine set) and dual_tier_mem_pglo (the one-window-host rule's named test topology), both carrying the constructor-tunable delete-window delay - ensure_gc_schema_at_construction: the one-window-host rule's store-core dispatch (kv host on dual-SQL — pg-lo's window clauses dormant; pg-lo host on kv=mem); the sweep's mem arm gains the large-host delegation, the branch keyed off the engine representation never a flag - stale scope notes updated to the landed state (store.rs module doc, LargeEngine, the sweep docs, PUT_CHUNK, pg_lo.rs, gc.rs) - 7 new env-driven constructor tests (dedicated-database janitor discipline; consolidation + mem-pglo both shapes Verified live against the pglo-poc harness server: cargo test default 69/19/3, --features pg-lo 92/19/3, --features "pg-lo,mem" 151/54/4 (server-less 124/37/4), --no-default-features "postgres,pg-lo" 102/19/3 (both live and server-less), --all-features 188/71/4; clippy -D warnings and fmt --check green on all four configurations; cargo doc clean. The literal kv-less pg-lo config re-confirmed structurally uncompilable (d69d9a3's finding; not a gate). --- src/gc.rs | 12 +- src/pg_lo.rs | 35 +- src/store.rs | 1247 +++++++++++++++++++++++++++-- tasks/store-pg-lo-construction.md | 126 ++- 4 files changed, 1305 insertions(+), 115 deletions(-) diff --git a/src/gc.rs b/src/gc.rs index 682c196..5663b69 100644 --- a/src/gc.rs +++ b/src/gc.rs @@ -66,11 +66,13 @@ //! //! The `#![allow(dead_code)]` below covers the companion-seam scope: //! the window family has its in-tree callers (store construction runs -//! `ensure_gc_schema` on every SQL-backed kv engine; the sweep's durable -//! form drives `window_stage`/`window_take`), while the fleet family -//! waits for wave 6 — its postgres clause bodies landed with wave 4, -//! but the store core consumes the family only under the `fleet: true` -//! declaration. It shrinks as that lands. +//! `ensure_gc_schema` on the window's host — the kv engine when it is +//! SQL-backed, pg-lo when the kv engine is not (the one-window-host +//! rule, ADR-012 §5); the sweep's durable form drives +//! `window_stage`/`window_take` on that same host), while the fleet +//! family waits for wave 6 — its postgres clause bodies landed with +//! wave 4, but the store core consumes the family only under the +//! `fleet: true` declaration. It shrinks as that lands. #![allow(dead_code)] diff --git a/src/pg_lo.rs b/src/pg_lo.rs index 48a1df9..b8d9dd0 100644 --- a/src/pg_lo.rs +++ b/src/pg_lo.rs @@ -52,9 +52,9 @@ //! 39–43 MB/s from 512 KiB chunks and collapse below (6 MB/s @8 KiB). //! [`LOWRITE_CHUNK`] is the named shipped constant (C2's citation), //! covering the whole-value put directly and the staging cursor's -//! append aggregation (the store core's put loop hands -//! `crate::dispatch::PUT_CHUNK`-sized 64 KiB windows; the engine -//! aggregates appends into 512 KiB `lowrite` statements). [`READ_CHUNK`] +//! append aggregation (the store core's put loop hands 64 KiB windows; +//! the engine aggregates appends into 512 KiB `lowrite` statements). +//! [`READ_CHUNK`] //! bounds the `lo_get` window: larger windows don't beat the //! server-side per-statement ceiling (~65–70 MB/s per in-flight //! statement, C2/C3) and the int4 length parameter caps at 2 GiB. @@ -175,16 +175,15 @@ //! Nothing of [`crate::gc::EngineFleetState`] exists on this engine //! (ADR-012 §5's pg-lo row: pin clauses **no** — fleet pins live on //! the kv host; sweep lock **no**). The window-family seam clauses -//! ([`crate::gc::EngineWindowState`]) are implemented here (the -//! engine-pg-lo-window task): `ensure_gc_schema` creates `gc_window` -//! only — never `gc_pins`, never the embedder's liveness table — and -//! the window clauses are conditional per the one-window-host rule -//! (dormant on a dual-SQL instance — kv=sqlite/postgres + large=pg-lo -//! stages on the kv engine; active on the kv=mem + large=pg-lo shape). -//! Whether the store core drives them at construction is the -//! constructor wiring's business (`store-pg-lo-construction`); this -//! module ships the clauses, their engine-level contract tests, and -//! the rest of the engine (unit tests only). +//! ([`crate::gc::EngineWindowState`]) are implemented here and wired +//! by construction (landed with the constructor wiring): the store +//! core's construction call follows the one-window-host rule — active +//! on the kv=mem + large=pg-lo shape (this module's `ensure_gc_schema` +//! runs at construction; its `gc_window` hosts the durable window), +//! dormant on a dual-SQL instance (kv=sqlite/postgres + large=pg-lo: +//! the kv host's call runs, this schema carries no `gc_window`); +//! `ensure_gc_schema` creates `gc_window` only — never `gc_pins`, +//! never the embedder's liveness table. //! //! Tests need a live server: connection settings ride the environment //! (`ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB`); tests skip cleanly @@ -455,11 +454,11 @@ impl EngineWindowState for PgLoBackend { /// `gc_pins` is never created here (ADR-012 §5: fleet pins live on /// the kv host) and the embedder's liveness table is never created /// in any case (ADR-008's schema-ownership rule). Idempotent, so - /// the twice-at-open call form is a no-op. Whether the store core - /// calls this at construction is the one-window-host rule's - /// business (`store-pg-lo-construction` wires the active host) — - /// the clause body exists and is contract-tested per applicability - /// regardless of dormancy. + /// the twice-at-open call form is a no-op. The store core calls + /// this at construction exactly when this engine hosts the window + /// (the one-window-host rule's large-host arm — kv = mem + + /// large = pg-lo); on a dual-SQL instance the kv host's call runs + /// and this stays dormant, never invoked at construction. async fn ensure_gc_schema(&self) -> Result<(), Error> { let client = self.pool.get().await.map_err(err)?; client diff --git a/src/store.rs b/src/store.rs index 6be34c7..d430161 100644 --- a/src/store.rs +++ b/src/store.rs @@ -28,9 +28,9 @@ //! only through named constructors, feature-gated with their engine //! (`mem_only` needs feature `mem`; the sqlite constructors are the //! default build's — wave 3; the postgres constructors arrived with -//! wave 4 behind feature `postgres`; pg-lo slots in at wave 5 — and -//! each slotting is exactly adding an enum variant and a named -//! constructor, without reshaping). +//! wave 4 behind feature `postgres`; the pg-lo constructors arrived +//! with wave 5 behind feature `pg-lo` — and each slotting is exactly +//! adding an enum variant and a named constructor, without reshaping). //! //! **Constructor modes** (ADR-011 §4; ADR-012 §6.3): //! @@ -43,8 +43,16 @@ //! default-on, ADR-013 §4); the wave-4 shape is `kv = postgres` + //! `large = local` — the **fleet-capable-kv production shape** (the //! engine is the fleet host, ADR-012 §4, but the constructor is the -//! non-fleet declaration until wave 6). Over-threshold content -//! routes to the large tier; at-or-below to kv. +//! non-fleet declaration until wave 6). The wave-5 shapes complete +//! the dual-tier constructor set: `kv = postgres` + `large = pg-lo` +//! — the **consolidation posture** (ADR-009's "one engine to +//! operate"; that constructor completes the fleet-valid engine +//! set, the module doc's recorded forward reference) — and `kv = +//! mem` + `large = pg-lo` — the **one-window-host rule's named +//! test topology** (ADR-012 §5: the durable window hosts on the +//! large engine when the kv engine is not SQL-backed). +//! Over-threshold content routes to the large tier; at-or-below to +//! kv. //! - **kv-only**: no large tier; over-threshold puts reject **at put //! time** (ADR-012 §6.2 — known-length immediately; unknown-length at //! mid-stream threshold overflow). The rejection behavior itself @@ -52,11 +60,18 @@ //! rejection point's value. The wave-2 engine is mem (testing); //! the wave-3 engine is sqlite (the durable single-node medium); the //! wave-4 engine is postgres (the fleet host's kv-only shape). +//! pg-lo is not a kv engine — kv-only modes take kv engines only, +//! and no `kv_only(pg-lo)` constructor exists. //! - **mem-only**: the mem engine alone, **no dispatch threshold in //! effect** (everything is local and small) — a testing/embedder- //! ephemeral posture, never production. Structurally distinct from //! kv-only precisely by that threshold absence. //! +//! pg-lo constructors are dual-tier only (pg-lo is a large engine; +//! the pairing is a valid non-fleet construction per ADR-012 §5), +//! and no sqlite + pg-lo constructor is named this wave — added on +//! demand. +//! //! Modes are validated structurally — unrepresentable states are //! unrepresentable by construction: dual-tier's signature constructs //! the large engine itself, mem-only's constructor exists only with @@ -76,9 +91,11 @@ //! returns the pinned `(len, stream)` shape as //! `Option<(u64, Blob)>` (store-api.md §Read path) — [`Blob`] is a //! bounded, re-entrant windowed reader over the crate-internal -//! `AnyCursor`, which dispatches the engine cursor bodies by tier -//! (mem's materializing body for kv, `local`'s pread body for -//! large). Windowed reads are the uniform stream shape across +//! [`AnyCursor`], which dispatches the engine cursor bodies by tier +//! — the kv materializing bodies behind [`KvCursor`] on the one +//! arm, the large streaming bodies behind [`LargeCursor`] on the +//! other (both sub-enums the recorded design points). Windowed +//! reads are the uniform stream shape across //! tiers; no chunk-iterator machinery was adopted — the cursor //! bodies *are* the streaming abstraction, and per-chunk `Stream`- //! wrapping would re-shape them without changing the underlying @@ -187,12 +204,18 @@ //! **The fleet declaration defers to wave 6.** ADR-012 §3 rules fleet //! mode a constructor declaration (`fleet: true`), never inferred — //! but it is a constructor *error* for every non-fleet-valid -//! configuration, and no fleet-valid configuration is constructible -//! yet (mem is never fleet-valid; `local` needs the shared-media -//! declaration of ADR-008 §3; pg-lo's wave-5 constructor completes the -//! fleet-valid engine set). The fleet-capable engines' constructors -//! — postgres's pair, landed wave 4 — are **implicitly `fleet: false`** -//! (ADR-012 §3): the ADR-005 in-process protocol applies verbatim +//! configuration, and the fleet-valid engine set is now complete in +//! the constructors (pg-lo's consolidation constructor landed wave 5, +//! the module doc's recorded forward reference — there is no +//! fleet-valid engine left to land): mem is never fleet-valid, +//! `local` needs the shared-media declaration of ADR-008 §3, and +//! pg-lo requires the kv host's same-instance clause (ADR-012 §4) — +//! so wave 6 is the declaration's predicate and constructor +//! parameter slotting in, nothing more. The fleet-capable engines' +//! constructors — postgres's pair (wave 4) and the pg-lo +//! consolidation constructor (wave 5) — are **implicitly +//! `fleet: false`** (ADR-012 §3): the ADR-005 in-process protocol +//! applies verbatim //! regardless of engine — pins are in-process guards, the sweep lease //! is the in-process lock, and none of the `EngineFleetState` clauses //! run — with the posture recorded in each constructor's doc so wave @@ -209,33 +232,39 @@ //! crate-internal) //! the gc-protocol task fills. //! -//! **The durable delete window hosts on the kv engine when it is -//! SQL-backed (ADR-012 §5's one-window-host rule).** Sqlite and -//! postgres — the SQL-backed kv engines — host: their constructors run -//! the GC schema's engine-neutral call (`ensure_gc_schema` on the -//! `EngineWindowState` seam) at construction, dispatched on the -//! kv-engine enum — a mem arm is structurally nothing, the in-process -//! engines execute the ADR-005 window protocol inline, so there is no -//! schema to ensure — and the sweep's SQL-backed form (**the durable -//! form**, ADR-008 §2 staged re-arbitrated delete; ADR-012 §6.7 the -//! executor is the next sweep) stages its candidates on the kv engine -//! the constructor built. The form branch keys off the engine -//! representation (`sweep_window`'s kv-engine-enum match — each -//! SQL-backed arm runs the durable form on its host, never a flag): -//! the inline form verbatim on mem/local, so wave 5's pg-lo dormancy -//! (the dual-SQL instance's large-engine window clauses are dormant -//! when the kv engine already hosts) slots in without reshaping. Every -//! SQL-backed constructor carries the delete-window delay +//! **The durable delete window hosts on exactly one SQL-backed engine +//! per store instance (ADR-012 §5's one-window-host rule) — the kv +//! engine when it is SQL-backed, otherwise the large engine.** Landed +//! in three constructor stages. Sqlite and postgres — the SQL-backed +//! kv engines — host: their constructors run the GC schema's +//! engine-neutral call (`ensure_gc_schema` on the `EngineWindowState` +//! seam) at construction, dispatched off the engine representation +//! ([`ensure_gc_schema_at_construction`]). Pg-lo's dormancy landed +//! with the wave-5 wiring: on a dual-SQL instance (kv = +//! sqlite/postgres + large = pg-lo) the kv host's call runs and +//! pg-lo's window clauses are **dormant** (the pg-lo schema carries +//! no `gc_window`), while on the kv = mem + large = pg-lo shape the +//! **pg-lo host's** call +//! runs (`gc_window` in the pg-lo schema) — the rule's large-host +//! arm, the window's completion. The sweep's form branch keys off +//! the same representation (`sweep_window`'s kv-engine-enum match, +//! whose mem arm consults the large engine precisely when it hosts +//! the window — never a flag): the **durable form** (ADR-008 §2 +//! staged re-arbitrated delete; ADR-012 §6.7 the executor is the +//! next sweep) on every window host, the inline form verbatim where +//! the window is in-process (mem/local, ADR-005 protocol inline). +//! Every SQL-backed-host constructor carries the delete-window delay //! (ADR-008 §2: default 5 min, constructor-tunable per deployment — //! [`DEFAULT_DELETE_WINDOW_DELAY`]; `0` is a legal degenerate tuning, //! execute-on-next-sweep) as the cadence knob the durable form's -//! staging consumes (`visible_after = mark_end + window`); the -//! mem/local constructors' inline form has no delay in effect. +//! staging consumes (`visible_after = mark_end + window`) — on +//! either host; the mem/local constructors' inline form has no delay +//! in effect. //! //! The `#![allow(dead_code)]` below marks the work still ahead, not -//! the GC protocol (which is driven below — the durable window's -//! cadence knob is consumed by the durable form's staging). The fleet -//! clauses hold the remaining scope until the fleet engine's wave. +//! the GC protocol (the window host's construction call and the +//! durable form are driven below on either host). The fleet clauses +//! hold the remaining scope until wave 6's fleet declaration. #![allow(dead_code)] @@ -258,8 +287,12 @@ use crate::local::{LocalBackend, LocalCursor, LocalStaging}; #[cfg(feature = "mem")] use crate::mem::{MemBackend, MemCursor}; +#[cfg(any(feature = "postgres", feature = "pg-lo"))] +use crate::pg_common::PgOpts; +#[cfg(feature = "pg-lo")] +use crate::pg_lo::{PgLoBackend, PgLoCursor, PgLoStaging}; #[cfg(feature = "postgres")] -use crate::postgres::{PgOpts, PostgresBackend, PostgresCursor}; +use crate::postgres::{PostgresBackend, PostgresCursor}; #[cfg(feature = "sqlite")] use crate::sqlite::{SqliteBackend, SqliteCursor}; @@ -301,11 +334,13 @@ pub struct Store { /// threshold in effect" is the mode's ruling (ADR-011 §4), not a /// value absence. threshold: u64, - /// The delete-window delay the constructor took (ADR-008 §2) — the - /// durable window's cadence knob the sweep's SQL-backed form + /// The delete-window delay the constructor took (ADR-008 §2) — + /// the durable window's cadence knob the sweep's durable form /// consumes (`window_stage`'s `visible_after = mark_end + window` - /// on the `EngineWindowState` seam). The mem/local inline form - /// has no delay in effect: those constructors carry + /// on the `EngineWindowState` seam, on either host — the kv + /// engine's or the large engine's). The in-process inline form + /// has no delay in effect: those constructors (mem-only, + /// kv-only(mem), dual-tier mem+local) carry /// [`DEFAULT_DELETE_WINDOW_DELAY`], never consulted. window_delay: Duration, /// The constructor mode; the dispatch/put/read/gc paths branch on it. @@ -360,10 +395,57 @@ pub(crate) enum KvEngine { } /// The large-tier engine set. `local` is ungated (dependency-free, -/// always present, ADR-013 §4); pg-lo slots in at wave 5. +/// always present, ADR-013 §4); pg-lo landed behind feature `pg-lo` +/// (wave 5) — its slotting is exactly the enum-variant-plus-named- +/// constructor pattern the module doc pins. pub(crate) enum LargeEngine { /// The sharded-filesystem engine (ADR-003/008). Local(LocalBackend), + /// The pg-lo large-object engine (ADR-009): postgres Large + /// Objects hold the bytes, the companion table is the addressing + /// authority. Feature `pg-lo`, default-off (it does not imply + /// feature `postgres`, ADR-013 §4). + #[cfg(feature = "pg-lo")] + PgLo(PgLoBackend), +} + +/// **The large-arm shape over multiple large engines (named design +/// point): a `LargeCursor` sub-enum behind [`AnyCursor`]'s one large +/// arm — the recorded [`KvCursor`] sub-enum pattern's large-tier +/// twin, on the same bounded-memory framing as the kv side.** The +/// alternative — one `AnyCursor` variant per large engine — would +/// copy the large streaming arm into the tier dispatch per engine, +/// and the tier dispatch's variant count must stay bounded so +/// `Blob`-tier consumers (the verification posture's arm shapes in +/// `Store::get`) stay a two-arm tier dispatch while large engines +/// multiply inside this one. Recorded for the review gate and landed +/// with wave 5: pg-lo slotted one arm in here (exactly the shape the +/// design point named, no reshaping). +pub(crate) enum LargeCursor { + /// The pread body over the committed file (ADR-003/008). + Local(LocalCursor), + /// The descriptorless window body over the companion row — + /// `lo_get` windows (ADR-009). + #[cfg(feature = "pg-lo")] + PgLo(PgLoCursor), +} + +impl ReadCursor for LargeCursor { + fn len(&self) -> u64 { + match self { + Self::Local(cursor) => cursor.len(), + #[cfg(feature = "pg-lo")] + Self::PgLo(cursor) => cursor.len(), + } + } + + async fn read_at(&self, offset: u64, len: u64) -> Result, Error> { + match self { + Self::Local(cursor) => cursor.read_at(offset, len).await, + #[cfg(feature = "pg-lo")] + Self::PgLo(cursor) => cursor.read_at(offset, len).await, + } + } } /// **The kv-arm shape over multiple kv engines (named design point): @@ -420,14 +502,16 @@ impl ReadCursor for KvCursor { /// streaming body — behind one crate-internal value that [`Blob`] holds /// and drives. Crate-internal: engine cursors never appear on the /// public surface; the facade's stream shape is this enum's uniform -/// [`ReadCursor`] view (`Blob`). The kv bodies live behind -/// [`KvCursor`] (the recorded design point above). +/// [`ReadCursor`] view (`Blob`). The per-tier body families live +/// behind the sub-enums — [`KvCursor`] for the kv arm, [`LargeCursor`] +/// for the large arm (both the recorded design points). pub(crate) enum AnyCursor { /// The kv body family: the whole value materialized in memory /// (bounded by tier policy). Kv(KvCursor), - /// The large body family: a pread cursor over the committed file. - Large(LocalCursor), + /// The large body family: streaming readers over the committed + /// home, behind the large-tier sub-enum. + Large(LargeCursor), } impl ReadCursor for AnyCursor { @@ -453,10 +537,12 @@ impl ReadCursor for AnyCursor { /// settled form); a kv-degenerate staged body variant arrives only if /// the conformance suite demands the staged form through the facade. /// Crate-internal — engine cursors never leak onto the public surface. +/// The stage bodies live behind [`LargeStaging`] (the sub-enum design +/// point's staging-side twin). pub(crate) enum AnyStaging { - /// The large body family: a stage file under the stage-hygiene - /// protocol, published by commit-rename. - Large(LocalStaging), + /// The large body family: stage bodies behind the large-tier + /// sub-enum. + Large(LargeStaging), } impl ReadCursor for AnyStaging { @@ -473,6 +559,71 @@ impl ReadCursor for AnyStaging { } } +/// The staged arm's sub-enum — the [`LargeCursor`]/staging-side twin +/// of the `KvCursor` pattern: the local stage-file body and pg-lo's +/// held-transaction body behind [`AnyStaging`]'s one large arm rather +/// than one `AnyStaging` variant per large engine (the same +/// bounded-variant argument; landed with wave 5). The variant payloads +/// are structurally dissimilar by design (the file-descriptor posture +/// vs the held pooled checkout + tx) and every dispatch rides the enum +/// by value-consumption or `&self`, so the size difference costs +/// nothing — the recorded `KvEngine` enum's allow posture. +#[allow(clippy::large_enum_variant)] +pub(crate) enum LargeStaging { + /// The stage-file body under the stage-hygiene protocol, + /// published by commit-rename (ADR-003/008). + Local(LocalStaging), + /// The held-transaction body: the stage lives in uncommitted LO + /// pages, published by the companion-row insert + COMMIT + /// (ADR-009). + #[cfg(feature = "pg-lo")] + PgLo(PgLoStaging), +} + +impl ReadCursor for LargeStaging { + fn len(&self) -> u64 { + match self { + Self::Local(cursor) => cursor.len(), + #[cfg(feature = "pg-lo")] + Self::PgLo(cursor) => cursor.len(), + } + } + + async fn read_at(&self, offset: u64, len: u64) -> Result, Error> { + match self { + Self::Local(cursor) => cursor.read_at(offset, len).await, + #[cfg(feature = "pg-lo")] + Self::PgLo(cursor) => cursor.read_at(offset, len).await, + } + } +} + +impl StagingCursor for LargeStaging { + async fn append(&mut self, chunk: &[u8]) -> Result<(), Error> { + match self { + Self::Local(cursor) => cursor.append(chunk).await, + #[cfg(feature = "pg-lo")] + Self::PgLo(cursor) => cursor.append(chunk).await, + } + } + + async fn commit(self, key: &[u8]) -> Result<(), Error> { + match self { + Self::Local(cursor) => cursor.commit(key).await, + #[cfg(feature = "pg-lo")] + Self::PgLo(cursor) => cursor.commit(key).await, + } + } + + async fn discard(self) { + match self { + Self::Local(cursor) => cursor.discard().await, + #[cfg(feature = "pg-lo")] + Self::PgLo(cursor) => cursor.discard().await, + } + } +} + impl StagingCursor for AnyStaging { async fn append(&mut self, chunk: &[u8]) -> Result<(), Error> { match self { @@ -525,15 +676,19 @@ impl KvEngine { } } - /// The GC schema's engine-neutral store-core call (ADR-010 §2's - /// companion seam; ADR-012 §5's applicability table), run at - /// construction: a SQL-backed kv engine creates the tables its - /// applicability row names — sqlite's body creates `gc_window` - /// only, postgres's creates `gc_pins` + `gc_window` (the fleet - /// host's both-tables row) — and an in-process kv engine is - /// structurally nothing (the + /// The GC schema's engine-neutral kv-host arm (ADR-010 §2's + /// companion seam; ADR-012 §5's applicability table): a SQL-backed + /// kv engine creates the tables its applicability row names — + /// sqlite's body creates `gc_window` only, postgres's creates + /// `gc_pins` + `gc_window` (the fleet host's both-tables row) — and + /// an in-process kv engine is structurally nothing (the /// local/mem row of the table: those engines execute the window - /// protocol inline, ADR-005, and carry no GC schema). + /// protocol inline, ADR-005, and carry no GC schema). The + /// store-core construction call is + /// [`ensure_gc_schema_at_construction`] — the one-window-host + /// rule's dispatch, which consults this method on the SQL-backed + /// kv arms and the large tier only when the kv engine is not + /// SQL-backed. async fn ensure_gc_schema(&self) -> Result<(), Error> { match self { #[cfg(feature = "mem")] @@ -596,37 +751,75 @@ impl LargeEngine { /// staging cursor's commit. async fn put_staged(&self) -> Result { match self { - LargeEngine::Local(engine) => Ok(engine.put_staged().await.map(AnyStaging::Large)?), + LargeEngine::Local(engine) => Ok(engine + .put_staged() + .await + .map(|c| AnyStaging::Large(LargeStaging::Local(c)))?), + #[cfg(feature = "pg-lo")] + LargeEngine::PgLo(engine) => Ok(engine + .put_staged() + .await + .map(|c| AnyStaging::Large(LargeStaging::PgLo(c)))?), } } async fn get(&self, key: &[u8]) -> Result, Error> { match self { - LargeEngine::Local(engine) => Ok(engine.get(key).await?.map(AnyCursor::Large)), + LargeEngine::Local(engine) => Ok(engine + .get(key) + .await? + .map(|c| AnyCursor::Large(LargeCursor::Local(c)))), + #[cfg(feature = "pg-lo")] + LargeEngine::PgLo(engine) => Ok(engine + .get(key) + .await? + .map(|c| AnyCursor::Large(LargeCursor::PgLo(c)))), } } async fn has(&self, key: &[u8]) -> Result { match self { LargeEngine::Local(engine) => engine.has(key).await, + #[cfg(feature = "pg-lo")] + LargeEngine::PgLo(engine) => engine.has(key).await, } } async fn size(&self, key: &[u8]) -> Result, Error> { match self { LargeEngine::Local(engine) => engine.size(key).await, + #[cfg(feature = "pg-lo")] + LargeEngine::PgLo(engine) => engine.size(key).await, } } async fn delete(&self, key: &[u8]) -> Result<(), Error> { match self { LargeEngine::Local(engine) => engine.delete(key).await, + #[cfg(feature = "pg-lo")] + LargeEngine::PgLo(engine) => engine.delete(key).await, } } async fn list(&self) -> Result>, Error> { match self { LargeEngine::Local(engine) => engine.list().await, + #[cfg(feature = "pg-lo")] + LargeEngine::PgLo(engine) => engine.list().await, + } + } + + /// The large tier's arm of the one-window-host rule's construction + /// call: the pg-lo host's `ensure_gc_schema` (creating `gc_window` + /// in the pg-lo schema), consulted by + /// [`ensure_gc_schema_at_construction`] only when the kv engine is + /// not SQL-backed. No arm exists for `local` — it is + /// seam-excluded by construction (ADR-010/012 §5). + #[cfg(feature = "pg-lo")] + async fn ensure_gc_schema(&self) -> Result<(), Error> { + match self { + LargeEngine::PgLo(engine) => engine.ensure_gc_schema().await, + LargeEngine::Local(_) => Ok(()), } } } @@ -647,6 +840,40 @@ fn typed_keys(raws: Vec>) -> Result, Error> { raws.into_iter().map(|raw| boundary_key(&raw)).collect() } +/// **The GC schema's engine-neutral construction call under the +/// one-window-host rule (ADR-012 §5)** — the rule's store-core +/// dispatch, run at construction exactly once per instance: the kv +/// engine's arm when the kv engine is SQL-backed (its +/// `ensure_gc_schema` creates its applicability row's tables; the +/// dual-SQL instance's pg-lo window clauses stay dormant — never +/// consulted here), the large tier's arm only when the kv engine is +/// not SQL-backed (kv = mem + large = pg-lo — the pg-lo host's call +/// creating `gc_window` in the pg-lo schema). The in-process +/// combinations (mem alone, local large) are structurally nothing: no +/// GC schema exists on their side of the rule. The dispatch keys off +/// the engine representation, never a flag. +async fn ensure_gc_schema_at_construction( + kv: &KvEngine, + large: Option<&LargeEngine>, +) -> Result<(), Error> { + // `large` is consulted only by the mem arm (the one-window-host + // rule's large-host arm); absent feature `mem` no arm reads it. + #[cfg(not(feature = "mem"))] + let _ = large; + match kv { + #[cfg(feature = "mem")] + KvEngine::Mem(_) => match large { + #[cfg(feature = "pg-lo")] + Some(LargeEngine::PgLo(engine)) => engine.ensure_gc_schema().await, + _ => Ok(()), + }, + #[cfg(feature = "postgres")] + KvEngine::Postgres(engine) => engine.ensure_gc_schema().await, + #[cfg(feature = "sqlite")] + KvEngine::Sqlite(engine) => engine.ensure_gc_schema().await, + } +} + impl Store { /// **mem-only mode** (ADR-011 §4): the mem engine alone, no /// dispatch threshold in effect — a testing/embedder-ephemeral @@ -734,7 +961,7 @@ impl Store { window_delay: Duration, ) -> Result { let kv = KvEngine::Sqlite(SqliteBackend::new(database).await?); - kv.ensure_gc_schema().await?; + ensure_gc_schema_at_construction(&kv, None).await?; Ok(Store { kv, large: None, @@ -773,10 +1000,11 @@ impl Store { window_delay: Duration, ) -> Result { let kv = KvEngine::Sqlite(SqliteBackend::new(database).await?); - kv.ensure_gc_schema().await?; + let large = LargeEngine::Local(LocalBackend::new(root).await?); + ensure_gc_schema_at_construction(&kv, Some(&large)).await?; Ok(Store { kv, - large: Some(LargeEngine::Local(LocalBackend::new(root).await?)), + large: Some(large), threshold, window_delay, mode: DualTier, @@ -824,7 +1052,7 @@ impl Store { window_delay: Duration, ) -> Result { let kv = KvEngine::Postgres(PostgresBackend::new(config, opts).await?); - kv.ensure_gc_schema().await?; + ensure_gc_schema_at_construction(&kv, None).await?; Ok(Store { kv, large: None, @@ -881,10 +1109,127 @@ impl Store { window_delay: Duration, ) -> Result { let kv = KvEngine::Postgres(PostgresBackend::new(config, opts).await?); - kv.ensure_gc_schema().await?; + let large = LargeEngine::Local(LocalBackend::new(root).await?); + ensure_gc_schema_at_construction(&kv, Some(&large)).await?; Ok(Store { kv, - large: Some(LargeEngine::Local(LocalBackend::new(root).await?)), + large: Some(large), + threshold, + window_delay, + mode: DualTier, + gc: GcHome::default(), + }) + } + + /// **dual-tier mode** (the default, ADR-011 §4) in the + /// **consolidation posture** (ADR-009: "one engine to operate") — + /// `kv = postgres` + `large = pg-lo`: size-threshold dispatch at + /// `threshold` (constructor-tunable; + /// [`crate::dispatch::DEFAULT_DISPATCH_THRESHOLD`] is the shipped + /// default), at-or-below on the kv tier, over-threshold staged + /// into the large tier's held-transaction staging cursor. + /// + /// **Connection-settings design point (two pairs):** this + /// constructor takes a DSN + [`PgOpts`] pair per engine (kv first, + /// large second). Cross-instance kv=postgres + large=pg-lo is a + /// valid non-fleet construction (ADR-012 §4); the consolidation + /// shape passes **the same DSN and opts to both** — the pool + /// content lives in one pg instance the kv tier rides (the + /// recorded corollary for a future conformance-suite factory: + /// minting one engine-owned schema and passing it to both engines + /// is equally valid — both engines' table names are distinct, so + /// the one-schema form holds `blobs`, `lo_entries`, and the GC + /// tables together; the two-schema form splits them — either is a + /// legitimate mint for this constructor's shape). + /// + /// Fallible on I/O — engine opens (config parse, pool build, + /// connect, the bootstrap DDL) and the GC schema call + /// ([`ensure_gc_schema_at_construction`]: the kv host runs per the + /// one-window-host rule, creating `gc_pins` + `gc_window` in the + /// kv schema — the pg-lo window clauses are **dormant** on this + /// dual-SQL instance, so the pg-lo schema carries no `gc_window`). + /// A construction failure is `Err(Error::Io)` per the + /// fallible-constructor pattern `dual_tier_mem_local` set. + /// + /// `window_delay` (ADR-008 §2) is the durable window's cadence + /// knob — [`DEFAULT_DELETE_WINDOW_DELAY`] is the shipped default, + /// `Duration::ZERO` the legal degenerate tuning. + /// + /// **Non-fleet (ADR-012 §3):** this constructor is implicitly + /// `fleet: false` — the ADR-005 in-process protocol applies + /// verbatim regardless of engine (pins are in-process guards, the + /// sweep lease is the in-process lock, none of the + /// `EngineFleetState` clauses run). The `fleet: true` declaration + /// is wave 6 (the composite fleet-validity predicate plus the + /// constructor declarations) and slots in without reshaping — no + /// fleet parameter is carried here. **This constructor completes + /// the fleet-valid engine set** — the module doc's recorded + /// forward reference: with pg-lo's landing, every fleet-valid + /// engine has its named constructor, and wave 6 is the + /// declaration's predicate and parameter alone. + #[cfg(all(feature = "postgres", feature = "pg-lo"))] + pub async fn dual_tier_postgres_pglo( + config: &str, + opts: PgOpts, + large_config: &str, + large_opts: PgOpts, + threshold: u64, + window_delay: Duration, + ) -> Result { + let kv = KvEngine::Postgres(PostgresBackend::new(config, opts).await?); + let large = LargeEngine::PgLo(PgLoBackend::new(large_config, large_opts).await?); + ensure_gc_schema_at_construction(&kv, Some(&large)).await?; + Ok(Store { + kv, + large: Some(large), + threshold, + window_delay, + mode: DualTier, + gc: GcHome::default(), + }) + } + + /// **dual-tier mode** (the default, ADR-011 §4) in the + /// one-window-host rule's **named test topology** (ADR-012 §5) — + /// `kv = mem` + `large = pg-lo`: the durable delete window hosts + /// on the **large engine** when the kv engine is not SQL-backed; + /// the constructor runs the pg-lo host's `ensure_gc_schema` call + /// ([`ensure_gc_schema_at_construction`]'s large-host arm — + /// `gc_window` in the pg-lo schema) and its sweeps run the + /// durable form on the pg-lo host, with the inline form nowhere in + /// this shape's story (the mem kv arm of the form branch consults + /// the large engine precisely when it hosts the window; + /// ADR-012 §5's non-SQL inline form exists for mem/local *without* + /// a large SQL host). + /// + /// `config` is the connection settings — the tokio-postgres + /// [`tokio_postgres::Config`]-parseable DSN form; `opts` carries + /// the engine-configuration knobs (schema name, pool size, + /// `synchronous_commit`), riding the constructor parameters, never + /// invented defaults. The kv tier is the mem engine — fallible + /// only on the pg-lo engine's I/O (config parse, pool build, + /// connect, the bootstrap DDL, the GC schema call): a construction + /// failure is `Err(Error::Io)` per the fallible-constructor + /// pattern `dual_tier_mem_local` set. + /// + /// `window_delay` (ADR-008 §2) is the durable window's cadence + /// knob on the pg-lo host — [`DEFAULT_DELETE_WINDOW_DELAY`] is the + /// shipped default, `Duration::ZERO` the legal degenerate tuning. + /// + /// Not fleet-anything: mem is never fleet-valid (ADR-011 §4). + #[cfg(all(feature = "mem", feature = "pg-lo"))] + pub async fn dual_tier_mem_pglo( + config: &str, + opts: PgOpts, + threshold: u64, + window_delay: Duration, + ) -> Result { + let kv = KvEngine::Mem(MemBackend::default()); + let large = LargeEngine::PgLo(PgLoBackend::new(config, opts).await?); + ensure_gc_schema_at_construction(&kv, Some(&large)).await?; + Ok(Store { + kv, + large: Some(large), threshold, window_delay, mode: DualTier, @@ -1095,8 +1440,10 @@ impl Store { /// The write path's streaming chunk granularity — every put loop reads /// the stream in windows of this size. Sized inside the kv tier's /// economics regime (bounded memory per iteration; engine bodies -/// aggregate upstream — pg-lo's measured 512 KiB `lowrite` knee is that -/// engine wave's concern). +/// aggregate upstream — the pg-lo engine's measured 512 KiB +/// `lowrite` knee, [`crate::pg_lo::LOWRITE_CHUNK`], aggregates these +/// windows into its C2 statement shape; that engine's constant is the +/// authority there, landed with its wave). const PUT_CHUNK: usize = 64 * 1024; /// The write path (store-api.md §Write path): `put` — the two internal @@ -1537,10 +1884,11 @@ impl Store { /// **`sweep()`** — the explicit mark-and-sweep with the delete /// window (ADR-005 §Sweep), in two forms under one signature: the - /// **durable form** runs where the kv engine is SQL-backed and hosts - /// the window (ADR-012 §5's one-window-host rule — sqlite and - /// postgres in today's set; the branch keys off the engine enum, - /// never a flag), and the + /// **durable form** runs on the delete window's SQL-backed host + /// (ADR-012 §5's one-window-host rule — the SQL-backed kv engine + /// when it is SQL-backed: sqlite and postgres; the pg-lo large + /// host otherwise: kv = mem + large = pg-lo; the branch keys off + /// the engine representation, never a flag), and the /// **inline form** (ADR-012 §5: `local`/`mem` stage candidates /// in-process and execute the window protocol inline in the same /// sweep) runs verbatim elsewhere. Sweeps are never errors: @@ -1629,10 +1977,13 @@ impl Store { /// The sweep body, under the sweep lease: the protect consult /// (whose abort shape is [`GcAbortCause::ProtectFailed`] — and whose /// coverage extends to the durable form's execution phase), then - /// the form branch on the delete window's host — the kv-engine - /// enum itself (ADR-012 §5's one-window-host rule), never a flag: - /// the **durable form** on a SQL-backed kv engine, the **inline - /// form** verbatim on `local`/`mem`. Operational failures (engine + /// the form branch on the delete window's host — the engine + /// representation itself (ADR-012 §5's one-window-host rule), + /// never a flag: the **durable form** on a SQL-backed kv engine + /// (its generic host arm shared by every `EngineWindowState` + /// engine) and on the pg-lo large host when the kv engine is mem + /// (the mem arm's large-host completion), the **inline form** + /// verbatim on mem/local otherwise. Operational failures (engine /// I/O, a malformed pool or window list) are the `Err` half of the /// return shape (ADR-012 §1); every deliberate no-op is report data /// instead. @@ -1658,7 +2009,13 @@ impl Store { match &self.kv { #[cfg(feature = "mem")] - KvEngine::Mem(_) => self.sweep_window_inline(live, started).await, + KvEngine::Mem(_) => match self.large() { + #[cfg(feature = "pg-lo")] + Some(LargeEngine::PgLo(host)) => { + self.sweep_window_durable(host, live, started).await + } + _ => self.sweep_window_inline(live, started).await, + }, #[cfg(feature = "postgres")] KvEngine::Postgres(engine) => self.sweep_window_durable(engine, live, started).await, #[cfg(feature = "sqlite")] @@ -1666,12 +2023,13 @@ impl Store { } } - /// The durable form's body, shared by every SQL-backed kv-engine - /// arm of the form branch (the branch keys off the kv-engine - /// enum, ADR-012 §5's one-window-host rule — the host this runs - /// on is the kv engine the constructor built). The overdue staged - /// rows execute first — each re-arbitrated at execution time — - /// then this sweep's own mark and stage at + /// The durable form's body, shared by every window-host arm of + /// the form branch (the branch keys off the engine + /// representation, ADR-012 §5's one-window-host rule — the host + /// this runs on is the SQL-backed engine the window lives on: the + /// kv engine's SQL-backed arms and pg-lo's large-host arm). The + /// overdue staged rows execute first — each re-arbitrated at + /// execution time — then this sweep's own mark and stage at /// `visible_after = mark_end + window`. async fn sweep_window_durable( &self, @@ -2047,6 +2405,8 @@ mod tests { async fn plant_large(store: &Store, key: &[u8], value: &[u8]) { match store.large() { Some(LargeEngine::Local(engine)) => engine.put(key, value).await.expect("large plant"), + #[cfg(feature = "pg-lo")] + Some(LargeEngine::PgLo(_)) => panic!("these tests construct local-tier stores"), None => panic!("planting into a mode with no large tier"), } } @@ -4414,6 +4774,8 @@ mod sqlite_window_tests { .put(&key.as_bytes(), value) .await .expect("large plant"), + #[cfg(feature = "pg-lo")] + Some(LargeEngine::PgLo(_)) => panic!("these tests construct local-tier stores"), None => panic!("planting into a mode with no large tier"), } } @@ -5303,3 +5665,724 @@ mod postgres_construction_tests { b"a dead candidate under the durable form".to_vec() } } + +/// The wave-5 constructor wiring's tests (features `pg-lo` + `mem`): +/// the one-window-host rule's named test topology — the durable +/// window's large host. The mode constructs and introspects correctly, +/// the GC schema ran on the **pg-lo host** at construction (`gc_window` +/// in the pg-lo schema; `gc_pins` never), the window-delay default is +/// the pinned constant and is constructor-tunable (`0` legal), the +/// shape round-trips end-to-end through both tiers with LO placement +/// asserted (kv entries never in the pg database), and the sweep's +/// form branch runs the durable form on the pg-lo host. Env-driven +/// (`ALKBLOBS_PG_*`), skipping cleanly server-less. +#[cfg(all(test, feature = "pg-lo", feature = "mem"))] +mod mem_pglo_construction_tests { + use super::*; + use crate::backend::Backend; + use crate::pg_common::test_support::DatabaseJanitor; + use crate::pg_lo::PgLoBackend; + use std::io::Cursor; + use std::sync::atomic::{AtomicU64, Ordering}; + use tokio_postgres::NoTls; + + static DB_SEQ: AtomicU64 = AtomicU64::new(0); + + fn env_nonempty(name: &str) -> Option { + std::env::var(name).ok().filter(|v| !v.is_empty()) + } + + /// The harness connection settings ride the environment + /// (`ALKBLOBS_PG_HOST` required; port/user/password/db default to + /// the harness server's values). Absent → the caller skips. + fn harness_dsn() -> Option { + let host = env_nonempty("ALKBLOBS_PG_HOST")?; + Some(format!( + "host={host} port={} user={} password={} dbname={}", + env_nonempty("ALKBLOBS_PG_PORT").unwrap_or_else(|| "15432".to_owned()), + env_nonempty("ALKBLOBS_PG_USER").unwrap_or_else(|| "postgres".to_owned()), + env_nonempty("ALKBLOBS_PG_PASSWORD").unwrap_or_else(|| "poc".to_owned()), + env_nonempty("ALKBLOBS_PG_DB").unwrap_or_else(|| "blobs".to_owned()), + )) + } + + /// The skip note — printed exactly once per skipped test. + fn skip_note() { + eprintln!( + "skipping (no postgres harness server): set ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB" + ); + } + + /// A fresh dedicated database per test (LOs are database-level — + /// the C7 lesson): the test's constructor opens its engine inside + /// it, and a `DatabaseJanitor` drop-guard (`WITH (FORCE)`) removes + /// the whole database at test end — success, early return, or + /// unwind — zero residue on the shared harness database. + struct TestDatabase { + /// The DSN into the dedicated database (the constructor's + /// connection settings). + dsn: String, + _janitor: DatabaseJanitor, + } + + fn admin_dsn(dsn: &str) -> String { + let base = dsn.rsplit_once("dbname=").expect("dbname").0; + format!("{base} dbname=postgres") + } + + fn db_dsn(dsn: &str, db: &str) -> String { + let base = dsn.rsplit_once("dbname=").expect("dbname").0; + format!("{base} dbname={db}") + } + + async fn fresh_db(tag: &str) -> Option { + let dsn = harness_dsn()?; + let db = format!( + "abtmplo_{}_{}_{:x}", + tag, + std::process::id(), + DB_SEQ.fetch_add(1, Ordering::Relaxed) + ); + let admin = admin_dsn(&dsn); + { + let (client, conn) = tokio_postgres::connect(&admin, NoTls) + .await + .expect("admin connect"); + tokio::spawn(async move { + let _ = conn.await; + }); + client + .execute( + &format!( + "CREATE DATABASE {}", + crate::pg_common::quote_identifier(&db) + ), + &[], + ) + .await + .expect("create test database"); + } + Some(TestDatabase { + dsn: db_dsn(&dsn, &db), + _janitor: DatabaseJanitor::new(&admin, &db), + }) + } + + fn pglo_engine(store: &Store) -> &PgLoBackend { + match store.large() { + Some(LargeEngine::PgLo(engine)) => engine, + Some(LargeEngine::Local(_)) | None => { + panic!("these tests construct pg-lo-tier stores") + } + } + } + + fn key_for(content: &[u8]) -> Key { + Key::derive(content, content.len() as u64) + } + + /// The one-window-host rule's named test topology constructs and + /// introspects: both tiers present, the dispatch threshold active, + /// and `ensure_gc_schema` ran at construction on the **pg-lo host** + /// (`gc_window` in the pg-lo schema; `gc_pins` never — the pg-lo + /// row of ADR-012 §5's applicability table). + #[tokio::test] + async fn dual_tier_mem_pglo_constructs_and_validates() { + let Some(site) = fresh_db("mem-pglo").await else { + skip_note(); + return; + }; + let store = Store::dual_tier_mem_pglo( + &site.dsn, + PgOpts::default(), + 1024, + DEFAULT_DELETE_WINDOW_DELAY, + ) + .await + .expect("mem+pglo constructs"); + + assert_eq!(store.mode(), DualTier); + assert_eq!(store.threshold(), Some(1024)); + assert_eq!(store.window_delay(), DEFAULT_DELETE_WINDOW_DELAY); + assert!(store.large().is_some(), "both tiers present"); + + for (len, tier) in [ + (0u64, Tier::Kv), + (1023, Tier::Kv), + (1024, Tier::Kv), + (1025, Tier::Large), + (u64::MAX, Tier::Large), + ] { + assert_eq!( + store.tier_for(len), + tier, + "length {len} routes deterministically" + ); + } + + let lo = pglo_engine(&store); + assert!( + lo.table_exists("lo_entries").await.unwrap(), + "the pg-lo engine's companion table bootstrapped at construction" + ); + assert!( + lo.table_exists("gc_window").await.unwrap(), + "ensure_gc_schema ran at construction on the pg-lo host: \ + gc_window in the pg-lo schema (the one-window-host rule's \ + large-host arm)" + ); + assert!( + !lo.table_exists("gc_pins").await.unwrap(), + "gc_pins is never created by pg-lo (fleet pins live on the kv host)" + ); + } + + /// The mem-pglo shape round-trips end-to-end: a sub-threshold + /// put lands on the kv tier (mem) — never in the pg database — + /// an over-threshold put stages into the pg-lo tier (the + /// companion row + LO present), both get back byte-identical, + /// and the pool list is the sorted union of both tiers. + #[tokio::test] + async fn dual_tier_mem_pglo_shape_round_trips_with_lo_placement() { + let Some(site) = fresh_db("mem-pglo-rt").await else { + skip_note(); + return; + }; + let store = Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::ZERO) + .await + .expect("mem+pglo constructs"); + + let small = b"a sub-threshold blob"; + let (key, pin) = store + .put(Some(small.len() as u64), Cursor::new(&small[..])) + .await + .expect("kv put publishes"); + assert_eq!(key, key_for(small)); + assert!( + store.kv().has(&key.as_bytes()).await.unwrap(), + "landed on the kv tier (mem)" + ); + let lo = pglo_engine(&store); + assert!( + !lo.has(&key.as_bytes()).await.unwrap(), + "kv entries never land in the pg database" + ); + drop(pin); + + let big = vec![7u8; 200]; + let (key, pin) = store + .put(Some(200), Cursor::new(&big[..])) + .await + .expect("staged put publishes"); + assert_eq!(key, key_for(&big)); + assert!( + lo.has(&key.as_bytes()).await.unwrap(), + "landed on the pg-lo tier: the companion row is present" + ); + assert_eq!( + lo.size(&key.as_bytes()).await.unwrap(), + Some(200), + "the companion row's size is exact" + ); + assert!( + !store.kv().has(&key.as_bytes()).await.unwrap(), + "no kv copy on the large route" + ); + let (len, blob) = store + .get(&key.as_bytes()) + .await + .unwrap() + .expect("entry present"); + assert_eq!(len, 200); + assert_eq!( + blob.read_at(0, len).await.unwrap(), + big, + "the LO's content round-trips byte-identical (large placement)" + ); + drop(pin); + + let mut expected: Vec = [key_for(small), key_for(&big)].to_vec(); + expected.sort(); + assert_eq!( + store.list().await.expect("list succeeds"), + expected, + "the pool list is the sorted union of both tiers" + ); + } + + /// The window-delay parameter on the pg-lo host: the shipped + /// default is the pinned 5-minute constant, the constructor's + /// value passes through the store's introspection, and + /// `Duration::ZERO` is a legal degenerate tuning (execute-on- + /// next-sweep). + #[tokio::test] + async fn window_delay_default_is_pinned_and_constructor_tunable() { + assert_eq!( + DEFAULT_DELETE_WINDOW_DELAY, + Duration::from_secs(5 * 60), + "the shipped default is 5 minutes (ADR-008 §2)" + ); + + let Some(site) = fresh_db("mem-pglo-delay").await else { + skip_note(); + return; + }; + let store = Store::dual_tier_mem_pglo( + &site.dsn, + PgOpts::default(), + 64, + DEFAULT_DELETE_WINDOW_DELAY, + ) + .await + .expect("constructs at the shipped default"); + assert_eq!(store.window_delay(), Duration::from_secs(300)); + + let store = + Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::from_secs(90)) + .await + .expect("constructs at a custom tuning"); + assert_eq!(store.window_delay(), Duration::from_secs(90)); + + let store = Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::ZERO) + .await + .expect("the degenerate zero tuning is legal"); + assert_eq!(store.window_delay(), Duration::ZERO); + + Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::from_secs(6000)) + .await + .expect("a long delay is a legal tuning too"); + } + + /// A set-backed liveness source — the construction tests' shared + /// sweep instrument. + #[derive(Default)] + struct SetSource { + live: parking_lot::Mutex>, + } + + impl LivenessSource for SetSource { + fn live_keys(&self) -> Vec { + self.live.lock().iter().cloned().collect() + } + + fn contains(&self, key: &Key) -> bool { + self.live.lock().contains(key) + } + } + + /// The sweep's form branch runs the **durable form on the pg-lo + /// host** (the branch keyed off the engine representation — the mem + /// arm consulting the large engine, never a flag, ADR-012 §5): a + /// zero-delay sweep stages this sweep's dead candidate into the + /// pg-lo schema's `gc_window` and executes none of them (the + /// inline form — mem kv, no large host — would delete in the same + /// sweep, so this is the form-branch-is-large-host proof), and the + /// *next* sweep executes the overdue set re-arbitrated at + /// execution time. + #[tokio::test] + async fn sweep_runs_the_durable_form_on_the_pg_lo_host() { + let Some(site) = fresh_db("mem-pglo-sweep").await else { + skip_note(); + return; + }; + let store = Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::ZERO) + .await + .expect("mem+pglo constructs"); + store.register_liveness_source(Arc::new(SetSource::default())); + + let content = b"a dead candidate under the pg-lo host's window"; + let (dead, _) = store + .put(Some(content.len() as u64), Cursor::new(&content[..])) + .await + .expect("the candidate's put publishes"); + assert_eq!(store.list().await.unwrap(), vec![dead.clone()]); + + let first = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(first.aborted, None); + assert_eq!( + first.candidates_staged, 1, + "the dead candidate staged into the pg-lo schema's gc_window" + ); + assert_eq!( + first.deleted, 0, + "the durable form executes nothing in its own staging sweep \ + (the form-branch-is-large-host proof)" + ); + assert!( + store.has(&dead.as_bytes()).await.unwrap(), + "the staged candidate still waits out its window" + ); + + let second = store.sweep().await.expect("sweeps are never errors"); + assert_eq!(second.aborted, None); + assert_eq!( + second.candidates_staged, 0, + "nothing newly dead after the execution" + ); + assert_eq!( + second.deleted, 1, + "the next sweep executed the overdue set, re-arbitrated at \ + execution time" + ); + assert!(!store.has(&dead.as_bytes()).await.unwrap()); + } +} + +/// The wave-5 constructor wiring's tests (features `pg-lo` + +/// `postgres`): the **consolidation posture** (ADR-009) — the +/// constructor completes the fleet-valid engine set. The mode +/// constructs and introspects correctly, the GC schema ran on the +/// **kv host** (both GC tables in the kv schema; the pg-lo window +/// clauses dormant — no `gc_window` in the pg-lo schema), the +/// window-delay default is the pinned constant and is +/// constructor-tunable (`0` legal), and the consolidation shape +/// round-trips end-to-end through both tiers with LO placement +/// asserted. Env-driven (`ALKBLOBS_PG_*`), skipping cleanly +/// server-less. +#[cfg(all(test, feature = "pg-lo", feature = "postgres"))] +mod postgres_pglo_construction_tests { + use super::*; + use crate::pg_common::test_support::DatabaseJanitor; + use std::io::Cursor; + use std::sync::atomic::{AtomicU64, Ordering}; + use tokio_postgres::NoTls; + + static DB_SEQ: AtomicU64 = AtomicU64::new(0); + + fn env_nonempty(name: &str) -> Option { + std::env::var(name).ok().filter(|v| !v.is_empty()) + } + + /// The harness connection settings ride the environment + /// (`ALKBLOBS_PG_HOST` required; port/user/password/db default to + /// the harness server's values). Absent → the caller skips. + fn harness_dsn() -> Option { + let host = env_nonempty("ALKBLOBS_PG_HOST")?; + Some(format!( + "host={host} port={} user={} password={} dbname={}", + env_nonempty("ALKBLOBS_PG_PORT").unwrap_or_else(|| "15432".to_owned()), + env_nonempty("ALKBLOBS_PG_USER").unwrap_or_else(|| "postgres".to_owned()), + env_nonempty("ALKBLOBS_PG_PASSWORD").unwrap_or_else(|| "poc".to_owned()), + env_nonempty("ALKBLOBS_PG_DB").unwrap_or_else(|| "blobs".to_owned()), + )) + } + + /// The skip note — printed exactly once per skipped test. + fn skip_note() { + eprintln!( + "skipping (no postgres harness server): set ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB" + ); + } + + /// A fresh dedicated database per test (LOs are database-level — + /// the C7 lesson): the drop-guard removes the whole database — + /// both engines' schemas with it — at test end in every exit path. + struct TestDatabase { + /// The DSN into the dedicated database (the constructor's + /// connection settings for both engines). + dsn: String, + _janitor: DatabaseJanitor, + } + + fn admin_dsn(dsn: &str) -> String { + let base = dsn.rsplit_once("dbname=").expect("dbname").0; + format!("{base} dbname=postgres") + } + + fn db_dsn(dsn: &str, db: &str) -> String { + let base = dsn.rsplit_once("dbname=").expect("dbname").0; + format!("{base} dbname={db}") + } + + async fn fresh_db(tag: &str) -> Option { + let dsn = harness_dsn()?; + let db = format!( + "abtmpglo_{}_{}_{:x}", + tag, + std::process::id(), + DB_SEQ.fetch_add(1, Ordering::Relaxed) + ); + let admin = admin_dsn(&dsn); + { + let (client, conn) = tokio_postgres::connect(&admin, NoTls) + .await + .expect("admin connect"); + tokio::spawn(async move { + let _ = conn.await; + }); + client + .execute( + &format!( + "CREATE DATABASE {}", + crate::pg_common::quote_identifier(&db) + ), + &[], + ) + .await + .expect("create test database"); + } + Some(TestDatabase { + dsn: db_dsn(&dsn, &db), + _janitor: DatabaseJanitor::new(&admin, &db), + }) + } + + /// A fresh engine-owned schema per test (the host-split test's + /// instrument: two distinct schemas so the dormancy assertion + /// observes the pg-lo schema genuinely without `gc_window`). + fn fresh_schema(tag: &str, seq: u64) -> String { + format!("abtmpglo_{tag}_{}_{seq:x}", std::process::id()) + } + + fn pglo_engine(store: &Store) -> &crate::pg_lo::PgLoBackend { + match store.large() { + Some(LargeEngine::PgLo(engine)) => engine, + Some(LargeEngine::Local(_)) | None => { + panic!("these tests construct pg-lo-tier stores") + } + } + } + + fn postgres_engine(store: &Store) -> &PostgresBackend { + match store.kv() { + #[cfg(feature = "postgres")] + KvEngine::Postgres(engine) => engine, + #[cfg(feature = "mem")] + KvEngine::Mem(_) => panic!("these tests construct postgres stores"), + #[cfg(feature = "sqlite")] + KvEngine::Sqlite(_) => panic!("these tests construct postgres stores"), + } + } + + fn key_for(content: &[u8]) -> Key { + Key::derive(content, content.len() as u64) + } + + /// The consolidation constructor constructs and introspects; the + /// GC schema ran on the **kv host** (both GC tables in the kv + /// schema) and the pg-lo window clauses are **dormant** (no + /// `gc_window` in the pg-lo schema — two distinct schemas here so + /// the dormancy assertion observes genuine absence, ADR-012 §5's + /// dual-SQL shape). + #[tokio::test] + async fn dual_tier_postgres_pglo_constructs_with_the_kv_host_winning_the_window() { + let Some(site) = fresh_db("consolidation").await else { + skip_note(); + return; + }; + let seq = DB_SEQ.fetch_add(1, Ordering::Relaxed); + let kv_schema = fresh_schema("kv", seq); + let lo_schema = fresh_schema("lo", seq); + let store = Store::dual_tier_postgres_pglo( + &site.dsn, + PgOpts { + schema: kv_schema, + ..Default::default() + }, + &site.dsn, + PgOpts { + schema: lo_schema, + ..Default::default() + }, + 1024, + DEFAULT_DELETE_WINDOW_DELAY, + ) + .await + .expect("consolidation constructs"); + + assert_eq!(store.mode(), DualTier); + assert_eq!(store.threshold(), Some(1024)); + assert_eq!(store.window_delay(), DEFAULT_DELETE_WINDOW_DELAY); + assert!(store.large().is_some(), "both tiers present"); + + for (len, tier) in [ + (0u64, Tier::Kv), + (1023, Tier::Kv), + (1024, Tier::Kv), + (1025, Tier::Large), + (u64::MAX, Tier::Large), + ] { + assert_eq!( + store.tier_for(len), + tier, + "length {len} routes deterministically" + ); + } + + let kv = postgres_engine(&store); + assert!( + kv.table_exists("blobs").await.unwrap(), + "the pool table exists at construction" + ); + assert!( + kv.table_exists("gc_window").await.unwrap(), + "the kv host's call ran at construction: gc_window in the kv schema" + ); + assert!( + kv.table_exists("gc_pins").await.unwrap(), + "the fleet host's both-tables row: gc_pins in the kv schema" + ); + + let lo = pglo_engine(&store); + assert!( + lo.table_exists("lo_entries").await.unwrap(), + "the pg-lo engine's companion table bootstrapped at construction" + ); + assert!( + !lo.table_exists("gc_window").await.unwrap(), + "the pg-lo window clauses are dormant on the dual-SQL instance \ + (ADR-012 §5): the pg-lo schema carries no gc_window" + ); + assert!( + !lo.table_exists("gc_pins").await.unwrap(), + "gc_pins is never created by pg-lo (fleet pins live on the kv host)" + ); + } + + /// The consolidation shape round-trips end-to-end in the recorded + /// corollary form — the same DSN and opts to both engines (one + /// minted schema holding `blobs`, `lo_entries`, and the GC tables + ///; ADR-012 §4's co-tenancy): a sub-threshold put lands on the kv + /// tier and never in the pg-lo companion table; an over-threshold + /// put stages into the pg-lo tier (the companion row + LO + /// present); both get back byte-identical; the pool list is the + /// sorted union. + #[tokio::test] + async fn consolidation_shape_round_trips_end_to_end_through_both_tiers() { + let Some(site) = fresh_db("consolidation-rt").await else { + skip_note(); + return; + }; + let opts = PgOpts::default(); + let store = Store::dual_tier_postgres_pglo( + &site.dsn, + opts.clone(), + &site.dsn, + opts, + 64, + Duration::ZERO, + ) + .await + .expect("consolidation constructs"); + + let small = b"a sub-threshold blob"; + let (key, pin) = store + .put(Some(small.len() as u64), Cursor::new(&small[..])) + .await + .expect("kv put publishes"); + assert_eq!(key, key_for(small)); + assert!( + store.kv().has(&key.as_bytes()).await.unwrap(), + "landed on the kv tier (postgres)" + ); + let lo = pglo_engine(&store); + assert!( + !lo.has(&key.as_bytes()).await.unwrap(), + "the sub-threshold key never entered the pg-lo companion table" + ); + drop(pin); + + let big = vec![42u8; 200]; + let (key, pin) = store + .put(Some(200), Cursor::new(&big[..])) + .await + .expect("staged put publishes"); + assert_eq!(key, key_for(&big)); + assert!( + lo.has(&key.as_bytes()).await.unwrap(), + "landed on the pg-lo tier: the companion row is present" + ); + assert_eq!( + lo.size(&key.as_bytes()).await.unwrap(), + Some(200), + "the companion row's size is exact" + ); + assert!( + !store.kv().has(&key.as_bytes()).await.unwrap(), + "no kv copy on the large route" + ); + let (len, blob) = store + .get(&key.as_bytes()) + .await + .unwrap() + .expect("entry present"); + assert_eq!(len, 200); + assert_eq!( + blob.read_at(0, len).await.unwrap(), + big, + "the LO's content round-trips byte-identical (LO placement)" + ); + drop(pin); + + let mut expected: Vec = [key_for(small), key_for(&big)].to_vec(); + expected.sort(); + assert_eq!( + store.list().await.expect("list succeeds"), + expected, + "the pool list is the sorted union of both tiers" + ); + } + + /// The window-delay parameter on the consolidation constructor: + /// the shipped default is the pinned 5-minute constant, tunable, + /// `0` legal. + #[tokio::test] + async fn window_delay_default_is_pinned_and_constructor_tunable() { + assert_eq!( + DEFAULT_DELETE_WINDOW_DELAY, + Duration::from_secs(5 * 60), + "the shipped default is 5 minutes (ADR-008 §2)" + ); + + let Some(site) = fresh_db("consolidation-delay").await else { + skip_note(); + return; + }; + let store = Store::dual_tier_postgres_pglo( + &site.dsn, + PgOpts::default(), + &site.dsn, + PgOpts::default(), + 64, + DEFAULT_DELETE_WINDOW_DELAY, + ) + .await + .expect("constructs at the shipped default"); + assert_eq!(store.window_delay(), Duration::from_secs(300)); + + let store = Store::dual_tier_postgres_pglo( + &site.dsn, + PgOpts::default(), + &site.dsn, + PgOpts::default(), + 64, + Duration::from_secs(90), + ) + .await + .expect("constructs at a custom tuning"); + assert_eq!(store.window_delay(), Duration::from_secs(90)); + + let store = Store::dual_tier_postgres_pglo( + &site.dsn, + PgOpts::default(), + &site.dsn, + PgOpts::default(), + 64, + Duration::ZERO, + ) + .await + .expect("the degenerate zero tuning is legal"); + assert_eq!(store.window_delay(), Duration::ZERO); + + Store::dual_tier_postgres_pglo( + &site.dsn, + PgOpts::default(), + &site.dsn, + PgOpts::default(), + 64, + Duration::from_secs(6000), + ) + .await + .expect("a long delay is a legal tuning too"); + } +} diff --git a/tasks/store-pg-lo-construction.md b/tasks/store-pg-lo-construction.md index d16b32b..f1db9b3 100644 --- a/tasks/store-pg-lo-construction.md +++ b/tasks/store-pg-lo-construction.md @@ -1,7 +1,7 @@ --- id: store-pg-lo-construction name: pg-lo constructor wiring — the consolidation shape and the window-host completion -status: pending +status: completed depends_on: [engine-pg-lo, engine-pg-lo-window] scope: moderate risk: medium @@ -125,27 +125,27 @@ the durable window gains its large-engine host arm. ## Acceptance Criteria -- [ ] `LargeEngine::PgLo` slots in; all six large ops dispatch; the +- [x] `LargeEngine::PgLo` slots in; all six large ops dispatch; the large cursor/staging bodies slot through the recorded sub-enum design point (the store.rs notes updated to the landed state) -- [ ] `dual_tier_postgres_pglo` + `dual_tier_mem_pglo` constructors +- [x] `dual_tier_postgres_pglo` + `dual_tier_mem_pglo` constructors exist behind feature `pg-lo`, take connection settings + opts + threshold + window delay, fallible where I/O is fallible; the consolidation constructor's doc records the implicitly-non-fleet posture and the fleet-valid-set completion -- [ ] `ensure_gc_schema` follows the one-window-host rule at +- [x] `ensure_gc_schema` follows the one-window-host rule at construction (kv host on dual-SQL — pg-lo schema carries no `gc_window`; pg-lo host on kv=mem — contract-tested both shapes) -- [ ] The sweep form branch runs the durable form on the pg-lo host +- [x] The sweep form branch runs the durable form on the pg-lo host for kv=mem + large=pg-lo and the inline form otherwise; the branch keys off the engine representation, never a flag -- [ ] The connection-settings design point (two DSN+opts pairs; the +- [x] The connection-settings design point (two DSN+opts pairs; the consolidation corollary) decided and recorded -- [ ] The pg-lo pure builds (`--no-default-features --features +- [x] The pg-lo pure builds (`--no-default-features --features "pg-lo,mem"` and `--features "postgres,pg-lo"`) compile clean (the literal kv-less config is not a gate — see the Description bullet) -- [ ] `cargo test` (default) green; `cargo test --features pg-lo` and +- [x] `cargo test` (default) green; `cargo test --features pg-lo` and `--features "pg-lo,mem"` green server-less (skipping); `cargo clippy --all-targets -- -D warnings` and `cargo fmt --check` green (default + the pg-lo feature variants) @@ -174,8 +174,114 @@ the durable window gains its large-engine host arm. ## Notes -> To be filled by implementation agent +> Implemented in `src/store.rs` + `src/pg_lo.rs` + `src/gc.rs` (wave 5, +> following `store-postgres-construction` — the structural twin — point +> for point). Recorded design points, as resolved: +> +> - **The large-tier sub-enums landed** — `LargeCursor` and +> `LargeStaging` behind `AnyCursor`/`AnyStaging`'s one large arm +> (the `KvCursor` pattern's large-tier twin, on the same +> bounded-variant argument; recorded for the review gate): local + +> pg-lo variants, `LargeStaging` carrying a scoped +> `#[allow(clippy::large_enum_variant)]` (the file-descriptor +> posture vs the held pooled checkout + tx — payloads structurally +> dissimilar, dispatch by reference/consumption; the `KvEngine` +> enum's recorded allow posture). No reshaping of `AnyCursor`/ +> `AnyStaging`/`Blob`. +> - **The one-window-host rule's store-core completion** — +> `ensure_gc_schema_at_construction(kv, large)`: the store-core +> dispatch the constructors call, keyed off the engine +> representation (the kv arm on a SQL-backed kv engine; the large +> arm consulted only when the kv engine is not SQL-backed; in- +> process combinations structurally nothing). The sqlite and +> postgres constructors were re-routed through it (`kv_only_*` pass +> `None`; the dual-tier locals pass `Some(&large)`) — their +> semantics unchanged, and the mem arm's large-host branch is where +> the rule completes. The sweep's form branch gained exactly that +> arm: `KvEngine::Mem` matches on `self.large()` — `LargeEngine::PgLo` +> runs `sweep_window_durable` on the pg-lo host (already generic +> over `W: EngineWindowState`, so the arm is a delegation), anything +> else stays inline; sqlite/postgres arms untouched. +> - **Connection-settings design point** — the consolidation +> constructor takes **two DSN + opts pairs** (kv first, large +> second): cross-instance is valid non-fleet (ADR-012 §4), and the +> consolidation shape passes the same DSN/opts to both. Recorded +> with it: the suite factory's corollary (one minted schema passed +> to both engines vs two schemas — both engines' table names are +> distinct, so either mint is legitimate). The mem-pglo constructor +> takes one pair (kv = mem takes none). +> - **No `kv_only(pg-lo)`** (pg-lo is a large engine) and **no +> sqlite+pg-lo constructor this wave** (valid non-fleet per +> ADR-012 §5, not named by the plan) — both recorded in the +> store.rs module doc's constructor-modes section. +> - **Stale scope notes updated to the landed state** — the +> engine-representation doc's wave references, the dual-tier +> shapes' wave bullets, the fleet-declaration paragraph (the +> fleet-valid engine set is complete in the constructors — wave 6 +> is the declaration's predicate and parameter alone), the +> one-window-host paragraph (dormancy landed; the large-host arm is +> the reshaping the rule always named), the `LargeEngine` doc, the +> `KvEngine::ensure_gc_schema` doc (now the kv-host arm the helper +> consumes), the sweep's docs (two forms' host sets), the +> `PUT_CHUNK` note (the engine's `LOWRITE_CHUNK` constant is the +> authority — landed), and the pg_lo/gc.rs module docs' forward +> references. The batch-delete helper's wave-6 note stays. +> - **Clippy scope note** — one `doc list item without indentation` +> fixup during the gate: a doc line beginning `+ large=pg-lo)` +> re-wrapped (the `+` was a markdown list marker). +> - **Tests (7 new, env-driven `ALKBLOBS_PG_*`, skipping cleanly +> server-less, janitor discipline from the start — amendment A2)** — +> two modules keyed to the constructor feature gates. The mem-pglo +> module (4 tests, on per-test dedicated databases — the C7 lesson's +> shape — with `DatabaseJanitor`): construction + introspection + +> the pg-lo-host schema assertions (`gc_window` present, `gc_pins` +> never); the end-to-end round-trip with LO placement asserted (kv +> entries never in the pg database, companion row present, size +> exact, LO content byte-exact); the window-delay +> default/tunings/zero; and the durable-form sweep smoke (zero +> delay: first sweep stages into the pg-lo schema's `gc_window` and +> executes nothing — the form-branch-is-large-host proof — second +> sweep executes the overdue set re-arbitrated at execution time). +> The consolidation module (3 tests, same dedicated-database +> discipline): construction + the kv-host-wins assertions (both GC +> tables in the kv schema, no `gc_window` in the pg-lo schema — two +> distinct schemas so the dormancy assertion observes genuine +> absence); the round-trip in the recorded corollary form (the same +> DSN + one minted schema to both engines, LO placement asserted); +> and the window-delay tunings. The test-side `plant_large` helpers +> in the existing test modules gained `PgLo` arms (the same +> all-features-exhaustiveness pattern as wave 4's `KvEngine` arms). +> - **The literal kv-less config (`--no-default-features --features +> pg-lo`) remains structurally uncompilable** (E0004 on the empty +> kv set, by-reference match arms requiring inhabitated variants) +> — the d69d9a3 finding, not a gate; verification used the two +> meaningful pure builds, both clean. ## Summary -> To be filled on completion \ No newline at end of file +> Completed 2026-10-12 (wave 5). pg-lo slotted into the facade: +> `LargeEngine::PgLo` with all six large-op dispatch arms, the +> `LargeCursor`/`LargeStaging` sub-enums behind `AnyCursor`/`AnyStaging`'s +> one large arm (the recorded `KvCursor` pattern's large-tier twin), +> the one-window-host rule's store-core completion +> (`ensure_gc_schema_at_construction` — kv host on dual-SQL, pg-lo +> host on kv=mem; the sweep's mem arm gains the large-host delegation, +> the branch keyed off the engine representation never a flag), and +> the two named constructors (`dual_tier_postgres_pglo` — the +> consolidation posture, two DSN+opts pairs, implicitly non-fleet, +> completing the fleet-valid engine set — and `dual_tier_mem_pglo` — +> the one-window-host rule's named test topology), both carrying the +> constructor-tunable delete-window delay (`DEFAULT_DELETE_WINDOW_ +> DELAY` pinned at 5 min, `0` legal). Stale scope notes updated to the +> landed state (store.rs module doc, `LargeEngine`, the sweep docs, +> `PUT_CHUNK`, pg_lo.rs, gc.rs). Verification (harness server live +> 2026-10-12): `cargo test` 69/19/3 ok (default); `cargo test +> --features pg-lo` 92/19/3 ok live and green server-less; `cargo test +> --features "pg-lo,mem"` 151/54/4 ok live, 124/37/4 ok server-less; +> `cargo test --no-default-features --features "postgres,pg-lo"` +> 102/19/3 ok live and server-less; `cargo test --all-features` +> 188/71/4 ok (7 new constructor tests green live); clippy `-D +> warnings` green on default, all-features, `pg-lo,mem`, and +> `postgres,pg-lo`; `cargo fmt --check` green; `cargo doc --no-deps` +> clean. The literal kv-less config re-confirmed structurally +> uncompilable (d69d9a3's finding; not a gate). \ No newline at end of file