feat: Core's Module Database ABI v1 executor, proven against PostgreSQL (ADR 0050) #69

Merged
Zayin_Admin merged 2 commits from feat/module-database-abi-executor into main 2026-10-07 21:25:16 +00:00
Member

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

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)
ekklesia-bot added 1 commit 2026-10-07 21:02:02 +00:00
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
6db50516b7
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]>
Zayin_Admin added 1 commit 2026-10-07 21:12:04 +00:00
fix: ADR 0050 callback-error precedence, and the honest limits of transaction control
CI / verify (pull_request) Successful in 1m4s
CI / infrastructure-smoke (pull_request) Successful in 38s
CI / migration-smoke (pull_request) Successful in 1m54s
CI / image-build (pull_request) Successful in 22s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / deploy-smoke (split) (pull_request) Successful in 1m37s
317a4ce410
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]>
Zayin_Admin merged commit 4a3958c616 into main 2026-10-07 21:25:16 +00:00
Zayin_Admin deleted branch feat/module-database-abi-executor 2026-10-07 21:25:16 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Zayin/Ekklesia#69