feat: add PostgreSQL backend #1

Merged
codinget merged 2 commits from worktree-bridge-cse_01EBD4LP89U8QdJtzcsHHvZ3 into master 2026-07-20 21:56:03 +02:00
Owner

Summary

Adds a PostgreSQL backend that mirrors the node:sqlite sub-backend structure, using native pg types and full migration support.

New: src/db/postgres/

  • sql.ts — Template tag using ? placeholders internally (same pattern as the sqlite side); toPositional() converts ?$1,$2… only at execution time, so catSql/joinSql composition works identically to sqlite with no renumbering.
  • cast.ts — Row converters for pg's native return types: UUIDs as strings (no buffer conversion), JSONB columns as already-parsed objects, TIMESTAMPTZ columns as Date.toISOString().
  • pool.tsWrappedPgClient interface with two implementations: WrappedPool (wraps pg.Pool) and WrappedPgTx (dedicated PoolClient for transactions). multi() runs BEGIN/COMMIT/ROLLBACK on a checked-out client. rethrow maps pg error codes 23505ConflictAbodeError, 23503NotFoundAbodeError.
  • query.ts — Async SELECT helpers mirroring sqlite/query.ts; no json("flags") wrapping needed since JSONB comes back parsed.
  • url.ts — Parses postgres:// and postgresql:// URLs; strips the readonly query param before passing the connection string to pg.
  • PostgresInterface.ts — Implements BackendDbInterface. All db calls are awaited; multi() receives a tx: WrappedPgClient which is threaded through to private helpers (#getUserById(uid, tx), etc.). Timestamps use NOW() / NOW() + INTERVAL '7 days'.
  • PostgresMigrator.ts — Implements Migrator. Table existence checked via information_schema.tables (vs catching a "no such table" error in sqlite). Each migration runs in its own transaction on a dedicated pool client.
  • getdb.static.ts — Factory registered in dbSources.static.ts for postgres: and postgresql: protocols.
  • migrations/ — Schema DDL using proper pg types: UUID, JSONB, TIMESTAMPTZ, NOW(), INTERVAL '7 days', '{}'::jsonb.

Modified

  • src/db/dbSources.static.ts — registers getPgStatic
  • package.json — adds @types/pg ^8.20.0 devDependency

Test plan

  • tsc --noEmit passes (verified clean)
  • npm run abode-migrate -- --url postgres://localhost/abode_test available lists 3 migrations
  • npm run abode-migrate -- --url postgres://localhost/abode_test migrate 3 runs all migrations cleanly
  • npm run abode-migrate -- --url postgres://localhost/abode_test current shows migration 3 applied
  • Web server starts with --url postgres://... and user CRUD works via API
  • JSONB flags and permissions round-trip correctly
  • Session expiry and renewal work with INTERVAL arithmetic
## Summary Adds a PostgreSQL backend that mirrors the `node:sqlite` sub-backend structure, using native pg types and full migration support. ### New: `src/db/postgres/` - **`sql.ts`** — Template tag using `?` placeholders internally (same pattern as the sqlite side); `toPositional()` converts `?` → `$1,$2…` only at execution time, so `catSql`/`joinSql` composition works identically to sqlite with no renumbering. - **`cast.ts`** — Row converters for pg's native return types: UUIDs as strings (no buffer conversion), JSONB columns as already-parsed objects, `TIMESTAMPTZ` columns as `Date` → `.toISOString()`. - **`pool.ts`** — `WrappedPgClient` interface with two implementations: `WrappedPool` (wraps `pg.Pool`) and `WrappedPgTx` (dedicated `PoolClient` for transactions). `multi()` runs `BEGIN`/`COMMIT`/`ROLLBACK` on a checked-out client. `rethrow` maps pg error codes `23505` → `ConflictAbodeError`, `23503` → `NotFoundAbodeError`. - **`query.ts`** — Async SELECT helpers mirroring `sqlite/query.ts`; no `json("flags")` wrapping needed since JSONB comes back parsed. - **`url.ts`** — Parses `postgres://` and `postgresql://` URLs; strips the `readonly` query param before passing the connection string to pg. - **`PostgresInterface.ts`** — Implements `BackendDbInterface`. All db calls are awaited; `multi()` receives a `tx: WrappedPgClient` which is threaded through to private helpers (`#getUserById(uid, tx)`, etc.). Timestamps use `NOW()` / `NOW() + INTERVAL '7 days'`. - **`PostgresMigrator.ts`** — Implements `Migrator`. Table existence checked via `information_schema.tables` (vs catching a "no such table" error in sqlite). Each migration runs in its own transaction on a dedicated pool client. - **`getdb.static.ts`** — Factory registered in `dbSources.static.ts` for `postgres:` and `postgresql:` protocols. - **`migrations/`** — Schema DDL using proper pg types: `UUID`, `JSONB`, `TIMESTAMPTZ`, `NOW()`, `INTERVAL '7 days'`, `'{}'::jsonb`. ### Modified - `src/db/dbSources.static.ts` — registers `getPgStatic` - `package.json` — adds `@types/pg ^8.20.0` devDependency ## Test plan - [ ] `tsc --noEmit` passes (verified clean) - [ ] `npm run abode-migrate -- --url postgres://localhost/abode_test available` lists 3 migrations - [ ] `npm run abode-migrate -- --url postgres://localhost/abode_test migrate 3` runs all migrations cleanly - [ ] `npm run abode-migrate -- --url postgres://localhost/abode_test current` shows migration 3 applied - [ ] Web server starts with `--url postgres://...` and user CRUD works via API - [ ] JSONB flags and permissions round-trip correctly - [ ] Session expiry and renewal work with `INTERVAL` arithmetic
codinget added the Kind/FeatureAgentic
Agent
Sonnet
labels 2026-06-30 12:43:02 +02:00
Author
Owner

test comment from review

test comment from review
Author
Owner

Review: approve, with one bug worth fixing

(Ignore the stray "test comment from review" above — that was me debugging the CLI, not part of this review.)

Reviewed the postgres backend implementation (tsc clean, checked against the sqlite backend for parity). Overall this faithfully mirrors SqliteInterface's semantics — readonly checks, error-code mapping (23505→Conflict, 23503→NotFound), transaction handling, and cast/type coercion all check out. No SQL injection risk found; all values go through the sql tagged template's parameter binding.

Bug worth fixing before/soon after merge:

  • src/db/postgres/PostgresMigrator.ts (~L97-122): in the per-migration loop, JS-defined migration parts run via part.apply(this.#pool) — passing the pool, not the transactional client. Compare to SqliteMigrator.ts, which correctly passes the single active connection (part.apply(this.#db)). No migration currently uses a JS apply() part (all are .sql files), so this is latent, but the first JS-based migration part added will run outside the transaction on a separate pooled connection — breaking atomicity silently. Suggest threading the transactional WrappedPgClient through instead.

Non-blocking notes:

  • sql.ts's toPositional() naively replaces every literal ? with $n — fine today, but Postgres's JSONB ?/?|/?& operators would break this if ever used, given how JSONB-heavy this schema is. Worth a comment/guard.
  • WrappedPgTx/WrappedPool in pool.ts duplicate all/get/run — could share a small base/helper.
  • pool.ts rollback-on-error doesn't guard against the rollback call itself throwing (would mask the original error).

This PR is fully additive (new src/db/postgres/ dir + two small touch points), so it's low-conflict and safe to merge independently of #2/#3.

**Review: approve, with one bug worth fixing** (Ignore the stray "test comment from review" above — that was me debugging the CLI, not part of this review.) Reviewed the postgres backend implementation (tsc clean, checked against the sqlite backend for parity). Overall this faithfully mirrors SqliteInterface's semantics — readonly checks, error-code mapping (23505→Conflict, 23503→NotFound), transaction handling, and cast/type coercion all check out. No SQL injection risk found; all values go through the `sql` tagged template's parameter binding. **Bug worth fixing before/soon after merge:** - `src/db/postgres/PostgresMigrator.ts` (~L97-122): in the per-migration loop, JS-defined migration parts run via `part.apply(this.#pool)` — passing the pool, not the transactional `client`. Compare to `SqliteMigrator.ts`, which correctly passes the single active connection (`part.apply(this.#db)`). No migration currently uses a JS `apply()` part (all are `.sql` files), so this is latent, but the first JS-based migration part added will run outside the transaction on a separate pooled connection — breaking atomicity silently. Suggest threading the transactional `WrappedPgClient` through instead. **Non-blocking notes:** - `sql.ts`'s `toPositional()` naively replaces every literal `?` with `$n` — fine today, but Postgres's JSONB `?`/`?|`/`?&` operators would break this if ever used, given how JSONB-heavy this schema is. Worth a comment/guard. - `WrappedPgTx`/`WrappedPool` in `pool.ts` duplicate `all`/`get`/`run` — could share a small base/helper. - `pool.ts` rollback-on-error doesn't guard against the rollback call itself throwing (would mask the original error). This PR is fully additive (new `src/db/postgres/` dir + two small touch points), so it's low-conflict and safe to merge independently of #2/#3.
codinget added 2 commits 2026-07-20 04:03:50 +02:00
Mirrors the node:sqlite sub-backend structure with full migration support.
Uses native pg types (UUID, JSONB, TIMESTAMPTZ) and $1/$2 parameterisation
via internal ? placeholders converted at execution time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PostgresMigrator: run JS migration parts on the transactional client
  (WrappedPgTx) instead of the pool, preserving migration atomicity
- pool.ts: guard ROLLBACK so a failing rollback no longer masks the
  original error (also applied in the migrator loop)
- pool.ts: share all/get/run between WrappedPool and WrappedPgTx via a
  common base class
- sql.ts: document that toPositional precludes JSONB ?/?|/?& operators
- PostgresInterface: implement deleteSession, required by
  BackendDbInterface since the logout-invalidation change on master

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codinget force-pushed worktree-bridge-cse_01EBD4LP89U8QdJtzcsHHvZ3 from bc787a208a to b9fa79ff1f 2026-07-20 04:03:50 +02:00 Compare
codinget merged commit b9fa79ff1f into master 2026-07-20 21:56:03 +02:00
codinget deleted branch worktree-bridge-cse_01EBD4LP89U8QdJtzcsHHvZ3 2026-07-20 21:56:03 +02:00
Sign in to join this conversation.