feat: Core's Module Database ABI v1 executor, proven against PostgreSQL (ADR 0050)
CI / verify (pull_request) Successful in 59s
CI / infrastructure-smoke (pull_request) Successful in 27s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 23s
CI / deploy-smoke (same-origin) (pull_request) Successful in 41s
CI / deploy-smoke (split) (pull_request) Successful in 1m29s

Implements the first ADR 0050 slice: Core's ORM-neutral executor and its
PostgreSQL evidence. Nothing is wired to modules yet; manifest v8, SDK
adapters, schema metadata, packaging, and Signage are later slices. The
legacy ModuleDatabase facade is unchanged.

Contracts (@ekklesia/contracts, no imports at all):
- ABI version 1; query { text, values, rowMode } returning
  { command, rowCount, rows, fields: [{ name, typeOid }] };
- executor, transaction, and savepoint types with exactly the ABI's
  members; parameter and result value types;
- ModuleDatabaseError { code, sqlState? }: eight stable codes, and a
  SQLSTATE only when PostgreSQL supplied one; static messages, no cause,
  frozen; isModuleDatabaseError recognises it by shape.

Core (createModuleDatabaseExecutor over the API's existing pool):
- one statement per query, enforced by forcing the extended query
  protocol so PostgreSQL itself refuses a second statement (42601);
  no SQL parsing;
- parameters: null, boolean, number, bigint, string, Uint8Array, and
  rectangular arrays of them; everything else refused before sending;
- a Core-owned decoding table that never consults pg's global parser
  registry: int8, numeric, temporal, and JSON as exact text; bytea as a
  plain Uint8Array; built-in arrays element by element;
- every operation pins TimeZone, DateStyle, IntervalStyle, bytea_output,
  and extra_float_digits, then resets with DISCARD ALL before returning
  the connection, or destroys it when it cannot be reset cleanly;
- callback-scoped transactions with explicit isolation and access mode;
  a COMMIT that PostgreSQL answers with ROLLBACK is reported as failed;
- savepoints named by Core; only the innermost scope is active; scopes
  are revoked when their callback settles;
- a callback that settles with work outstanding is rolled back, never
  committed or released;
- transaction-control statements sent through query are refused by
  command tag and transaction status, and fail the transaction;
- the module ID is carried for Core's attribution only.

Tests:
- 83 integration tests on a database with hostile defaults: queries,
  the one-statement rule, parameters, decoding, errors, transactions,
  savepoints, forgotten awaits, pool safety after every failure class,
  and session state;
- unit tests for encoding, decoding, options, sanitization, and scope
  bookkeeping;
- type-level and source checks that no ORM or driver object crosses
  the boundary.

ADR 0050 is Accepted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-07 16:01:46 -05:00
co-authored by Claude Opus 5.5
parent d43031997b
commit 6db50516b7
13 changed files with 3498 additions and 4 deletions
+25
View File
@@ -509,6 +509,31 @@ an `authenticated` baseline, from platform ownership, or from a user grant. Its
answer "yes" to every human question, so any accidental fall-through into a human path would flip
those assertions.
A further suite (`core/tests/module-database-abi.integration.ts`, same harness) proves Core's Module
Database ABI v1 executor against real PostgreSQL
([ADR 0050](../architecture/decisions/0050-module-database-abi-v1.md)). Nothing it asserts is mocked,
because every claim is a property of PostgreSQL or of the protocol:
- one statement per `query`, enforced by PostgreSQL's extended protocol rather than by parsing, so
semicolons inside literals, identifiers, and comments are never mistaken for a second statement;
- every allowed parameter category bound exactly, and every refused one refused before PostgreSQL
sees it;
- the decoding table for each type the ADR names;
- PostgreSQL's SQLSTATE preserved while its message, detail, constraint, and values are hidden;
- commit, rollback, isolation levels, read-only, no retries, and an aborted transaction never being
committed;
- callback-scoped savepoints;
- a forgotten `await` rolling back instead of committing;
- revoked executors reaching nothing.
The suite's database is created with deliberately hostile defaults: a non-UTC time zone, a non-ISO
date style, `sql_standard` intervals, `escape` bytea output, and reduced float digits. So every
decoding assertion also proves the executor pins its own session settings. The pool-safety cases use
a one-connection pool, which proves the next borrower gets a connection with exactly the database's
defaults after every failure class: no transaction, setting, temporary table, listener, or advisory
lock carries over. The same is true after a connection the server terminated. Parameter validation,
decoding from text, option validation, error sanitization, and scope bookkeeping are also unit-tested
without a database in `core/tests/module-database-abi.test.ts`.
Outbound email is proven against a **real SMTP server** rather than a mock
(`core/tests/mail-smtp.integration.test.ts`, [ADR 0037](../architecture/decisions/0037-host-provided-outbound-email.md)).
`smtp-server` — the reference implementation from the same project as the client — runs in-process,