Implements the first ADR 0050 slice: Core's Module Database ABI v1 executor and its PostgreSQL integration tests. It is not wired to any module yet. The legacy ModuleDatabase facade is unchanged.
Out of scope: SDK adapters, raw-SQL helper, manifest v8 / database.abi, plain schema metadata, packager/verifier changes, the template, Signage, legacy removal, and migrations.
What it adds
@ekklesia/contracts → module-database-abi.ts (no imports):
ABI version 1, and query/result/field types.
Executor, transaction and savepoint types, each with exactly the ABI's members.
ModuleDatabaseError { code, sqlState? } with eight stable codes, plus a structural isModuleDatabaseError.
@ekklesia/core/modules → createModuleDatabaseExecutor(pool, { moduleId }), over the API's existing pool.
Key decisions
One statement per query. Every statement uses pg 8.22's queryMode: "extended", so PostgreSQL itself refuses a second statement with 42601. There is no SQL parser. Semicolons in literals, $$ strings, quoted identifiers and comments produce no false positives (tested).
Decoding.types returns raw text, so pg's global, mutable parser registry is never consulted. Core then decodes from text:
int8, numeric, temporal types and JSON stay exact text.
bytea becomes a plain Uint8Array.
Built-in arrays decode element by element.
Session pinning and reset. Each operation pins TimeZone/DateStyle/IntervalStyle/bytea_output/extra_float_digits, and ends with DISCARD ALL before the connection goes back to the pool. A connection that can't be reset, or isn't idle, is destroyed.
Forgotten await. A callback that settles with work outstanding is rolled back, never committed or released. Already-started statements are allowed to settle first.
Transaction control through query. Forbidden by ADR 0050 as a conformance rule, not a security guarantee. Core does not parse SQL, so PostgreSQL runs such a statement before Core sees it, and a module's own COMMIT has committed. Core detects the violation from the command tag or transaction status, then:
refuses every later statement in that transaction without sending it;
never commits the transaction itself;
destroys the connection.
Callback-error precedence. A rejected callback keeps its own error, such as one failed half of a Promise.all. Only a callback that resolves with work outstanding gets SCOPE_WORK_OUTSTANDING. Either way the scope rolls back.
Duplicate object-mode columns. These are found only after execution, so a mutation may already have applied. Run it in a transaction if it must be atomic.
Verification
pnpm test:migrations:integration: 15 files, 396 tests passed, including the new suite's 88.
The new suite runs against a database with hostile defaults: Chicago time zone, SQL, DMY, sql_standard, escape bytea, reduced float digits.
pnpm verify: passes.
The first run hit the four known import-safety timeouts under parallel load. They pass in isolation in about 1.6 s, and the rerun passed in full.
Deploy config, apps/api and startup are untouched, so deploy:config and the smoke tests don't apply.
Implements the first ADR 0050 slice: **Core's Module Database ABI v1 executor and its PostgreSQL integration tests**. It is not wired to any module yet. The legacy `ModuleDatabase` facade is unchanged.
**Out of scope:** SDK adapters, raw-SQL helper, manifest v8 / `database.abi`, plain schema metadata, packager/verifier changes, the template, Signage, legacy removal, and migrations.
## What it adds
- **`@ekklesia/contracts` → `module-database-abi.ts`** (no imports):
- ABI version 1, and query/result/field types.
- Executor, transaction and savepoint types, each with exactly the ABI's members.
- `ModuleDatabaseError { code, sqlState? }` with eight stable codes, plus a structural `isModuleDatabaseError`.
- **`@ekklesia/core/modules` → `createModuleDatabaseExecutor(pool, { moduleId })`**, over the API's existing pool.
## Key decisions
- **One statement per `query`.** Every statement uses pg 8.22's `queryMode: "extended"`, so PostgreSQL itself refuses a second statement with `42601`. There is no SQL parser. Semicolons in literals, `$$` strings, quoted identifiers and comments produce no false positives (tested).
- **Decoding.** `types` returns raw text, so pg's global, mutable parser registry is never consulted. Core then decodes from text:
- `int8`, `numeric`, temporal types and JSON stay exact text.
- `bytea` becomes a plain `Uint8Array`.
- Built-in arrays decode element by element.
- **Session pinning and reset.** Each operation pins `TimeZone`/`DateStyle`/`IntervalStyle`/`bytea_output`/`extra_float_digits`, and ends with `DISCARD ALL` before the connection goes back to the pool. A connection that can't be reset, or isn't idle, is destroyed.
- **Forgotten `await`.** A callback that settles with work outstanding is rolled back, never committed or released. Already-started statements are allowed to settle first.
- **Transaction control through `query`.** Forbidden by ADR 0050 as a conformance rule, not a security guarantee. Core does not parse SQL, so PostgreSQL runs such a statement before Core sees it, and a module's own `COMMIT` has committed. Core detects the violation from the command tag or transaction status, then:
- refuses every later statement in that transaction without sending it;
- never commits the transaction itself;
- destroys the connection.
- **Callback-error precedence.** A rejected callback keeps its own error, such as one failed half of a `Promise.all`. Only a callback that *resolves* with work outstanding gets `SCOPE_WORK_OUTSTANDING`. Either way the scope rolls back.
- **Duplicate object-mode columns.** These are found only after execution, so a mutation may already have applied. Run it in a transaction if it must be atomic.
## Verification
- `pnpm test:migrations:integration`: **15 files, 396 tests passed**, including the new suite's 88.
- The new suite runs against a database with hostile defaults: Chicago time zone, `SQL, DMY`, `sql_standard`, `escape` bytea, reduced float digits.
- `pnpm verify`: passes.
- The first run hit the four known import-safety timeouts under parallel load. They pass in isolation in about 1.6 s, and the rerun passed in full.
- Deploy config, `apps/api` and startup are untouched, so `deploy:config` and the smoke tests don't apply.
ADR 0050 → **Accepted**.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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]>
Narrow contract and implementation adjustments from review. No SDK, manifest,
packaging, or Signage work.
Callback-error precedence:
- A transaction or savepoint callback that rejects keeps its own error, even
while sibling work it started is still settling (one failed half of a
Promise.all). Core drains that work, rolls back, and never commits or
releases.
- A callback that resolves with work outstanding (a forgotten await) still
fails with MODULE_DATABASE_SCOPE_WORK_OUTSTANDING and rolls back.
Transaction-control SQL:
- ADR 0050 forbids it through query() as a conformance rule for trusted code,
not a security guarantee. Core does not parse SQL, so PostgreSQL has run
the statement before Core sees it. Core detects the violation from the
command tag or the transaction status, and claims nothing it cannot do: a
module's own COMMIT has committed.
- After a violation, the transaction's scopes refuse every later statement
without sending it (it would otherwise autocommit). Core never commits the
transaction itself, and the connection is destroyed rather than reused,
for top-level violations too.
- The commit and rollback guarantees apply to conforming use.
Duplicate object-mode columns: documented as found only after execution, so a
mutation can have taken effect; atomicity needs an ABI transaction.
Tests prove each of these against PostgreSQL: Promise.all failures in a
transaction and a savepoint keep 23505; a forgotten await still rolls back; a
module COMMIT's prior work is committed while nothing after it runs; a backend
PID changes after every violation; duplicate RETURNING columns persist at the
top level and roll back in a transaction.
ADR 0050 stays Accepted: the executor conforms to the revised text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements the first ADR 0050 slice: Core's Module Database ABI v1 executor and its PostgreSQL integration tests. It is not wired to any module yet. The legacy
ModuleDatabasefacade is unchanged.Out of scope: SDK adapters, raw-SQL helper, manifest v8 /
database.abi, plain schema metadata, packager/verifier changes, the template, Signage, legacy removal, and migrations.What it adds
@ekklesia/contracts→module-database-abi.ts(no imports):ModuleDatabaseError { code, sqlState? }with eight stable codes, plus a structuralisModuleDatabaseError.@ekklesia/core/modules→createModuleDatabaseExecutor(pool, { moduleId }), over the API's existing pool.Key decisions
query. Every statement uses pg 8.22'squeryMode: "extended", so PostgreSQL itself refuses a second statement with42601. There is no SQL parser. Semicolons in literals,$$strings, quoted identifiers and comments produce no false positives (tested).typesreturns raw text, so pg's global, mutable parser registry is never consulted. Core then decodes from text:int8,numeric, temporal types and JSON stay exact text.byteabecomes a plainUint8Array.TimeZone/DateStyle/IntervalStyle/bytea_output/extra_float_digits, and ends withDISCARD ALLbefore the connection goes back to the pool. A connection that can't be reset, or isn't idle, is destroyed.await. A callback that settles with work outstanding is rolled back, never committed or released. Already-started statements are allowed to settle first.query. Forbidden by ADR 0050 as a conformance rule, not a security guarantee. Core does not parse SQL, so PostgreSQL runs such a statement before Core sees it, and a module's ownCOMMIThas committed. Core detects the violation from the command tag or transaction status, then:Promise.all. Only a callback that resolves with work outstanding getsSCOPE_WORK_OUTSTANDING. Either way the scope rolls back.Verification
pnpm test:migrations:integration: 15 files, 396 tests passed, including the new suite's 88.SQL, DMY,sql_standard,escapebytea, reduced float digits.pnpm verify: passes.apps/apiand startup are untouched, sodeploy:configand the smoke tests don't apply.ADR 0050 → Accepted.
🤖 Generated with Claude Code
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]>