The 'postgres' kv-tier engine (ADR-007, feature 'postgres', default-off) against the wave-1 trait: the sqlite engine's structural twin on a Pooled-Postgres driver cluster. - One engine-owned schema (default 'alkblobs'; quoted-identifier boundary via the alkstore quote_identifier pattern) holding the B5 row shape — key bytea PK / value bytea NOT NULL / size bigint NOT NULL, WITH (fillfactor = 90) — idempotently bootstrapped at construction; autovacuum and the ~40x small-tier storage overhead recorded as posture, not silence - deadpool pool with DEFAULT_MAX_SIZE = 8 (load-bearing, POC #5 B2); per-connection prepare_cached discipline (B5: statements die with sessions); the synchronous_commit parity knob as connect options on every pooled connection (default 'on', configurable, alkstore src/opts.rs mechanics) - CAS first-writer-wins (INSERT ... ON CONFLICT DO NOTHING) on both put forms; metadata-only size probe riding the PK index (EXPLAIN gate-tested); EXISTS has; materializing get cursor; degenerate in-memory stage with both hygiene forms (fail_commit injection) - list: the single-statement MVCC-snapshot cursor (portal-batched row stream, keys only); crate-internal batch-delete helper (DELETE ... WHERE key = ANY() over bytea[]), scope-noted until wave 6; virgin-store read-only split via SQLSTATE 42P01 mapping - 15 in-module unit tests, env-driven (ALKBLOBS_PG_*), skipping cleanly server-less; green against the dockerized pglo-poc harness server (:15432) Verified: cargo test; cargo test --features postgres (84 lib tests); cargo test --all-features (139); clippy all-targets, default + features + all-features, -D warnings; fmt --check; doc --no-deps.
13 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | |||
|---|---|---|---|---|---|---|---|---|---|---|---|
| engine-postgres | postgres kv engine — the fleet-capable engine | completed | narrow | medium | component | implementation |
|
Description
Implement the postgres kv-tier engine (feature postgres,
default-off; ADR-007; REQ-2/REQ-3) against the wave-1 trait
(src/backend.rs) — the second SQL-backed engine and the fleet host
(ADR-012 §4). The sqlite engine (src/sqlite.rs) is the structural
twin: same contract, different driver cluster and posture deltas.
- Backend impl (
src/postgres.rs, behind#[cfg(feature = "postgres")]): the seven methods plusput_staged, opaque byte keys/values,name() -> "postgres".- Schema bootstrap — one engine-owned PostgreSQL schema
(recorded design point: the default schema name, the
quoted-identifier boundary for every identifier entering SQL text
— the alkstore
quote_identifierpattern,alkstore-postgressrc/schema.rs) holding the POC #5 finding B5 row shape:blobs (key bytea PRIMARY KEY, value bytea NOT NULL, size bigint NOT NULL) WITH (fillfactor = 90). Idempotent DDL (CREATE … IF NOT EXISTS) at the constructor; no migration machinery. The dedicatedsizecolumn is the metadata-onlysizeprobe:SELECT size … WHERE key = $1rides the PK index and never loads the value blob (ADR-008's size discipline). - Pool — deadpool-postgres over tokio-postgres
(
deadpool_postgres::Pool); pool size configurable with a named shipped default (recorded design point; the alkstoreDEFAULT_MAX_SIZE = 8precedent). Connection pooling is load-bearing, not an optimization (POC #5 B2: fresh sessions cost ~20 ms) — the module doc records the posture. - Per-connection prepared-statement discipline — prepared
statements die with sessions (POC #5 B5 hit the "prepared
statement s_1 does not exist" error exactly when a pool-recycled
connection lost its statement); every engine statement rides
prepare_cachedon the pooled client so the cache is per-connection by construction. Recorded design point. synchronous_commitparity knob — configurable per engine instance, wired on every pooled connection (the alkstore POC-verified per-session connect-options mechanics,alkstore-postgressrc/opts.rs). Defaulton(the ship config);offis the documented sqlite-NORMAL-alike latency trade (B5: latency moves ~2-3× across settings). This is the parity knob with sqlite'ssynchronous=NORMAL-WAL shipped tier — recorded in the module doc as posture, not hidden.- CAS rows —
INSERT … ON CONFLICT DO NOTHINGfirst-writer-wins on both put forms; a re-put of an existing key is a no-op, never an overwrite (ADR-007 §3: the sqliteINSERT OR IGNOREmapping). has— an EXISTS probe (SELECT EXISTS(…)) — never a value load.get— the materializing cursor body (kv family): the whole value loaded once (bounded by tier policy),len()from the row's size column, reads slice the materialized value composingbackend.rs'sclamped.put_staged— the degenerate in-memory stage (ADR-010 §1, the sqlite engine's shape):appendextends the buffer,commitpublishes via the sameON CONFLICT DO NOTHINGclause and self-discards on failure,discarddrops the buffer with zero residue.delete— per-key rows-affected discipline (Err(Error::Missing)when absent). Plus the crate-internal batch-delete helper —DELETE … WHERE key = ANY($1)over abytea[]parameter (ADR-007 §5's sweep-safety mechanics): the sweep's batched delete form, contract-tested in-module; its store-core consumer is wave 6's fleet sweep (scope-noted#![allow(dead_code)]until then — the wave-1 seam-shape discipline).list— complete and well-formed as a stable cursor over a vacuuming table (ADR-007 §5): one statement's MVCC snapshot is stable for its duration (ghost rows from concurrent vacuuming never appear); the natural realization is the singleSELECT key … ORDER BY keystatement with tokio-postgres's row-streaming portal batches (bounded memory on huge pools) — the exact fetch shape is a recorded design point.- Virgin-store posture — the constructor creates the schema and
tables; the tolerance is the sqlite engine's recorded read-only
split (reads on a schema-less database map to absent/empty, writes
surface the engine error — never silently creating schema). The
test shape (a fixture connecting without running the bootstrap)
mirrors
src/sqlite.rs'snew_tablelesspattern. Recorded design point. - Autovacuum/fillfactor posture — stated in the module doc, not silence (POC #5 B5: autovacuum is a real maintenance surface for a churning CAS pool; fillfactor 90 measured; the ~40× small-tier storage overhead accepted per B6 — documented, not hidden). The B5 tuning numbers are POC-derived posture defaults (ADR-007 §6's deferred-cost pattern): the first real pg deployment verifies them against its media.
- Schema bootstrap — one engine-owned PostgreSQL schema
(recorded design point: the default schema name, the
quoted-identifier boundary for every identifier entering SQL text
— the alkstore
- Natively async — tokio-postgres is async: no
spawn_blockingseam (the sqlite engine's blocking discipline does not carry; the pool is the concurrency posture). No blocking I/O on the async path; nounwrap/expectoutside tests; thiserror errors surface asError::Iovia the engine's mapping discipline (Missingvia the rows-affected/virgin-store mapping). - Unit tests in-module: the method contract (virgin-store no-ops
incl. the schema-less form, CAS re-put, Missing discipline, size
exactness + metadata-only), the staged-put happy path and both
hygiene forms (discard → zero residue; commit failure →
self-discard — a
#[cfg(test)]fail-commit knob standing in for media failure, the mem/sqlite pattern), list stability and completeness, the batch-delete helper, thesynchronous_commitwiring. Tests need a live server — the alkstore harness convention: connection settings ride the environment (ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB), tests skip cleanly server-less, acceptance requires green against the server. The harness server: dockerizedpglo-poc(postgres:16-alpine) on :15432, dbblobs, user/passwordpostgres/poc(verified live 2026-10-12).
The engine-state clause families (EngineWindowState +
EngineFleetState) are engine-postgres-fleet (separate task); the
constructor wiring is store-postgres-construction. This task ships
the engine and its in-module unit tests only.
Acceptance Criteria
src/postgres.rsimplementsBackendcompletely (seven methods +put_staged) behind featurepostgres(default-off), with the materializing cursor body and the degenerate staging body- One engine-owned schema (quoted-identifier boundary), the B5 row shape with fillfactor 90, idempotent bootstrap at the constructor; autovacuum/storage-overhead posture recorded in the module doc
- deadpool pool with a named shipped default size;
per-connection
prepare_cacheddiscipline; thesynchronous_commitparity knob wired on every pooled connection, configurable, defaulton - CAS first-writer-wins on both put forms; re-put never overwrites
sizereads the dedicated size column via the PK index, never the value blob (metadata-only, unit-tested)listcomplete and well-formed as a stable MVCC-snapshot cursor (design point recorded); the batch-delete helper (key = ANY($1)) contract-tested, scope-noted until wave 6- Stage hygiene holds in both forms (discard → zero residue; failed commit → self-discard), unit-tested
- Virgin-store reads are no-ops including the schema-less form (unit-tested); writes surface the error (the read-only split)
- Unit tests env-driven (
ALKBLOBS_PG_*), skipping cleanly server-less; green against the harness server at acceptance cargo test(default — postgres is default-off, the engine compiles out) green;cargo test --features postgresgreen server-less (skipping);cargo clippy --all-targets -- -D warningsandcargo fmt --checkgreen (default +--features postgres)
References
- docs/architecture/backends-and-dispatch.md (§kv tier — postgres engine)
- docs/architecture/decisions/007-two-kv-engines-sqlite-and-postgres.md (§4's B5 deltas as requirements; §5's sweep-safety proof; §6's deferred-cost pattern)
- docs/architecture/decisions/003-backend-contract-and-dispatch.md (the trait contract; the three numbered put rules' kv side)
- docs/architecture/decisions/010-backend-io-seams-and-fleet-state-home.md §1 (the whole-value put's pg body; the materializing cursor)
- docs/research/poc-postgres-kv-findings.md (findings B1-B6 — the row shape, the pooling discipline, the durability knob, the storage overhead)
- /workspace/@alkdev/alkstore/alkstore-postgres (the sibling harness patterns: src/opts.rs's PgOpts + per-session synchronous_commit wiring, src/schema.rs's bootstrap + quote_identifier, the suite factory's fresh-schema-per-open)
- src/backend.rs (the trait + seam contracts this implements)
- src/sqlite.rs (the structural twin's shapes this mirrors)
Notes
Implemented in
src/postgres.rs(wave 4, this task's only code change;src/lib.rs's#[cfg(feature = "postgres")] pub mod postgres;gate pre-existed). Recorded design points, as resolved:
- Opts shape — a crate-internal
PgOpts(schema/max_size/synchronous_commit), mirroring alkstore'ssrc/opts.rs; the constructor takes the tokio-postgresConfig-parseable connection string first,PgOptssecond.max_size = 0fails at entry, before any round trip; an empty schema string selects the default.- Schema name — default
"alkblobs"; every statement composes the table throughquote_identifier(the alkstore boundary), so a hostile schema name (embedded", reserved-word-shaped) is tested green.size/value/keyare quoted in DDL per the alkstore hazard list, plain in DML (key,value,sizenon-reserved).- Pool default —
DEFAULT_MAX_SIZE = 8,RecyclingMethod::Fast(alkstore precedent); deadpool-postgres 0.14 / tokio-postgres 0.7.- list fetch shape — single
SELECT key … ORDER BY keythrough the deadpool client: one statement = one MVCC snapshot (vacuum-ghost-stable); tokio-postgres's row stream drains the server-side portal in bounded batches and the collection materializes only the key column. Ordering by key is engine-local (the trait leaves ordering unspecified).- Virgin-store discriminant — SQLSTATE 42P01 (
is_undefined_table), mapped on reads (absent/empty), delete (Missing), anddelete_batch(Ok(0)); writes and staged commits surfaceError::Io. The schema-less fixture connects via a#[cfg(test)] new_schemalessconstructor (the sqlitenew_tablelesspattern) against a schema that was never bootstrapped.- Stage hygiene mechanics — the staged buffer is
std::mem::taken before the pool checkout, so a failed commit (injection or engine error) strands no staged bytes; the#[cfg(test)] fail_commitknob mirrors sqlite/mem.- No new dependencies —
postgres = ["dep:tokio-postgres", "dep:deadpool-postgres"]was already inCargo.toml;listuses the client's collected query (row-stream under the hood) rather than pulling futures-util forquery_raw.- Test harness quirks —
simple_query("SHOW …")returnsSimpleQueryMessages (skip to theRowvariant);size's EXPLAIN plan gate embeds the key as a'\x…'literal (PREPARE cannot wrap EXPLAIN) and assertsIndex Scan+blobs_pkey.- Isolation convention: one fresh schema per test (
abt_{tag}_{pid}_{seq}), the alkstore suite-factory pattern, on the shared harness database.
Summary
Completed 2026-10-12 (wave 4).
src/postgres.rsimplements the seven-methodBackend+put_stagedbehind featurepostgres(default-off) — deadpool pool (default 8, load-bearing posture recorded), per-connectionprepare_cacheddiscipline, thesynchronous_commitparity knob wired as connect options on every pooled connection (defaulton, configurable), one engine-owned schema (alkblobs, quoted-identifier boundary) holding the B5 row shape withfillfactor = 90idempotently bootstrapped at construction, CAS first-writer-wins viaON CONFLICT DO NOTHINGon both put forms, the metadata-onlysizeprobe (PK-index-tested via EXPLAIN), the degenerate in-memory stage with both hygiene forms, the single-MVCC-snapshotlistcursor, and the crate-internaldelete_batch(key = ANY($1)overbytea[], scope-noted until wave 6). Natively async — no spawn_blocking. 15 in-module unit tests, env-driven (ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB), skipping cleanly server-less; all green against the dockerized pglo-poc harness server (:15432). Verified:cargo test(69/19/3 ok),cargo test --features postgres(84/19/3 ok, server live),cargo test --all-features(139/54/4 ok),cargo clippy --all-targetsand--all-features --all-targets -- -D warningsgreen,cargo fmt --checkgreen,cargo doc --no-depsclean. Engine-state clause families remainengine-postgres-fleet; constructor wiring remainsstore-postgres-construction.