Skip to content

feat(db): Postgres Db adapter — pg driver, pooler-safe schema scoping (HT-20) - #18

Merged
zaridan merged 4 commits into
mainfrom
feat/ht-20-postgres-db-adapter
Jul 11, 2026
Merged

feat(db): Postgres Db adapter — pg driver, pooler-safe schema scoping (HT-20)#18
zaridan merged 4 commits into
mainfrom
feat/ht-20-postgres-db-adapter

Conversation

@zaridan

@zaridan zaridan commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What

A real Postgres implementation of the Db/Queryable seam (src/db/postgres.ts), alongside the existing PGlite adapter. Closes the last code gap before the first deploy: the engine can now run against Supabase (or any Postgres) via a standard connection string.

Jira: HT-20

Design

  • Wraps pg (node-postgres, MIT). Only unnamed prepared statements — compatible with transaction-mode poolers (Supabase Supavisor :6543, the serverless-correct connection).
  • schema option contains every table in a dedicated Postgres schema (e.g. helpthread inside a shared database) with zero changes to the stores' unqualified SQL. Enforcement is per-transaction (set_config('search_path', …, is_local => true) after BEGIN on a pinned client): a session-level SET does not survive transaction pooling, where even consecutive autocommit statements can land on different backends. query() in schema mode rides a single-statement transaction for the same reason.
  • ensureSchema handles both production shapes: schema pre-created by an admin for a scoped role (existence checked first, no DDL attempted), or created on first boot (concurrent-create race tolerated, then existence re-checked). Afterwards it verifies USAGE + CREATE via has_schema_privilege — a schema without USAGE is silently skipped in search_path resolution, so mis-granted deploys fail loud at construction instead of with "relation does not exist" later.
  • Schema name validated against a strict lowercase-identifier whitelist (it participates in CREATE SCHEMA DDL); pg_ prefix rejected.
  • Pool max defaults to 2, not pg's 10 — serverless multiplies the default by warm-instance count.
  • Uint8Array params copied to Buffer so bytea binds behave identically to PGlite (pg would JSON-stringify a raw Uint8Array).
  • migrate() needed no changes: its advisory lock is already pg_advisory_xact_lock (transaction-scoped — the pooler-safe flavor), and it runs entirely inside db.transaction(), so _migrations and all tables land in the configured schema.

Tests — real driver, real Postgres, no mocks

src/db/postgres.test.ts (20 tests) exposes a PGlite instance on a loopback TCP port via @electric-sql/pglite-socket (Apache-2.0, devDep) and connects with the actual pg driver: wire-protocol parameter binding, commit/rollback (including a mid-transaction statement failure proving the pooled client is returned clean), bytea round-trip, schema placement verified through the catalog bypassing the adapter, cross-schema isolation, idempotent re-construction, and the real migrations run into a named schema. What the harness can't simulate — Supavisor's backend shuffling — is exactly what the transaction-local design defends against structurally (see module docs).

247 tests total, typecheck + Biome clean.

Review

Codex adversarial pass (standing rule for the connection/DDL path): initial DO-NOT-SHIP with 5 findings (HIGH: pool default; MEDIUM: privilege validation, race postcondition; LOW: commit-uncertainty doc, zero-copy Buffer view) — all fixed in the second commit; re-review verdict SHIP, no new defects.

Licenses

pg MIT (dependency), @types/pg MIT + @electric-sql/pglite-socket Apache-2.0 (devDependencies).

🤖 Generated with Claude Code

https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b

Summary by CodeRabbit

  • New Features
    • Added a PostgreSQL-backed database adapter with query and transaction support.
    • Supports schema-scoped operation, including automatic schema setup and isolation across schemas.
    • Includes connection pooling controls, SSL configuration, and correct binary (Uint8Array) parameter handling.
    • Exposes Postgres options and a factory for creating Postgres-backed database instances.
  • Tests
    • Added end-to-end integration tests validating commits, rollbacks, error handling, schema validation, migrations, and pool closing.
  • Chores
    • Added required PostgreSQL dependencies for runtime and development.

zaridan and others added 2 commits July 10, 2026 21:30
… (HT-20)

createPostgresDb wraps node-postgres behind the same Db/Queryable seam as
PGlite. The schema option creates the schema if absent (tolerating the
concurrent-create race and the pre-created-by-admin shape) and enforces
search_path per-transaction via set_config(..., is_local => true), because
session SET does not survive a transaction-mode pooler (Supabase 6543).
query() in schema mode rides a single-statement transaction for the same
reason. Uint8Array params are bridged to Buffer so bytea binds match
PGlite. Tests drive the real pg driver over TCP against real Postgres
(PGlite behind @electric-sql/pglite-socket) — no mocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
- Default pool max to 2 (pg's 10 is per-instance-multiplied in serverless)
- Verify USAGE+CREATE on the schema after ensure (missing USAGE silently
  drops the schema from search_path resolution — fail loud at boot instead)
- Recheck schema existence after a swallowed create-race error rather than
  trusting the error code
- Copy Uint8Array params into a fresh Buffer (no shared-memory view)
- Document the commit-uncertain rejection window in the module doc

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f81c518a-b661-4c76-a79e-77bb8cf382de

📥 Commits

Reviewing files that changed from the base of the PR and between 2c90cd7 and 132b1f7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

📝 Walkthrough

Walkthrough

Adds a Postgres-backed Db adapter using pg.Pool, with transaction handling, schema provisioning, binary parameter support, public factory exports, and integration tests against PGlite over a loopback socket.

Changes

Postgres adapter

Layer / File(s) Summary
Public exports and package setup
package.json, src/db/index.ts, src/db/postgres.ts
Adds Postgres dependencies, documents backend boundaries, and exposes PostgresDbOptions and createPostgresDb without re-exporting the class.
Query and transaction execution
src/db/postgres.ts
Implements pooled queries, transaction-scoped clients, local search_path, Uint8Array parameter conversion, rollback handling, and pool closure.
Schema validation and provisioning
src/db/postgres.ts
Validates schema names, creates or reuses schemas, checks USAGE and CREATE privileges, and constructs configured pools.
Real Postgres integration coverage
src/db/postgres.test.ts
Tests queries, commits, rollbacks, pool reuse, binary parameters, closure, schema isolation, validation, and migrations using the real pg driver.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PostgresDb
  participant pgPool
  participant pgClient
  participant PostgreSQL
  PostgresDb->>pgPool: connect()
  pgPool->>pgClient: provide pooled client
  PostgresDb->>pgClient: BEGIN
  PostgresDb->>pgClient: set transaction-local search_path
  PostgresDb->>pgClient: execute callback queries
  PostgresDb->>pgClient: COMMIT or ROLLBACK
  pgClient->>pgPool: release client
  pgPool->>PostgreSQL: execute SQL
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a Postgres Db adapter with pg driver and schema scoping support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ht-20-postgres-db-adapter

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/db/index.ts`:
- Line 16: The public barrel export exposes PostgresDb and allows callers to
bypass createPostgresDb validation and provisioning. Update the export in
src/db/index.ts to expose only createPostgresDb, while keeping PostgresDb
available internally within the postgres module.

In `@src/db/postgres.test.ts`:
- Around line 230-246: Update the test “does not leak search_path onto pooled
connections used without schema” to construct both schema-scoped and schema-less
PostgresDb adapters over the same pg.Pool configured with max: 1, rather than
calling openDb() twice. Ensure the scoped operation completes before querying
through the plain adapter, and clean up the shared pool afterward so the test
verifies reuse of the same backend connection.

In `@src/db/postgres.ts`:
- Around line 119-133: Add a configurable connection acquisition timeout to the
pool options and pass it through when constructing the pool in the relevant
PostgreSQL client setup. Also register a pool error listener so idle-client
errors are handled and logged rather than becoming uncaught process-level
errors; update the connection options type and pool initialization symbols
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 685b51b4-e774-4c67-8e82-3d954e92ec14

📥 Commits

Reviewing files that changed from the base of the PR and between 2f90bc6 and ac6027a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • package.json
  • src/db/index.ts
  • src/db/postgres.test.ts
  • src/db/postgres.ts

Comment thread src/db/index.ts Outdated
Comment thread src/db/postgres.test.ts Outdated
Comment thread src/db/postgres.ts
…sharper leak test

- Barrel exports only createPostgresDb; the PostgresDb constructor skips
  schema validation/provisioning so it stays internal to src/db
- pool.on('error') handler: an idle client dying (pooler cull, backend
  restart) is an uncaught 'error' event that crashes the process otherwise
- connectionTimeoutMillis defaults to 10s (pg's default waits forever)
- The search_path leak test now multiplexes a schema-mode and a plain Db
  over ONE max-1 pool — the same physical session — so a regression from
  transaction-local to session-local set_config would actually fail it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/db/postgres.ts (1)

356-364: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Emit a low-level log from the pool error handler.
The no-op avoids the crash, but it also hides idle-client failures from backend restarts or pooler recycling, which makes connectivity issues harder to diagnose. A single log line keeps the runtime behavior unchanged while preserving a signal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/db/postgres.ts` around lines 356 - 364, The pool error handler currently
suppresses idle-client failures without recording them. Update the
pool.on('error') handler in the pool initialization flow to emit one low-level
log line containing the error details, while preserving the existing
non-throwing behavior and avoiding additional recovery logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/db/postgres.ts`:
- Around line 356-364: The pool error handler currently suppresses idle-client
failures without recording them. Update the pool.on('error') handler in the pool
initialization flow to emit one low-level log line containing the error details,
while preserving the existing non-throwing behavior and avoiding additional
recovery logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b33ffb5-ad15-401e-9402-0bc087157720

📥 Commits

Reviewing files that changed from the base of the PR and between ac6027a and 2c90cd7.

📒 Files selected for processing (3)
  • src/db/index.ts
  • src/db/postgres.test.ts
  • src/db/postgres.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/db/postgres.test.ts

@zaridan
zaridan merged commit 5634d5d into main Jul 11, 2026
5 checks passed
@zaridan
zaridan deleted the feat/ht-20-postgres-db-adapter branch August 2, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant