From 0e6a4aaa93d1d72ecbe57b86d0e4529fc5c1c12a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Tue, 20 Oct 2026 09:39:39 +0000 Subject: [PATCH] Record that this client cannot run behind pgCat 1.2.0, and stop lying about it A real pooled database became available, so the deployment assumption in src/db/index.ts - "points at pgCat/PgBouncer, so keep prepared statements disabled" - was finally testable. It does not hold. Through pgCat 1.2.0 an unparameterised `select 1` succeeds, and so does `select count(*) from organisations`, but any query carrying a bind parameter hangs forever. That is true with prepare:false, with prepare:true, with fetch_types:false, and inside an explicit transaction. The same queries straight at Postgres all pass, and psql's \bind through the same pool returns its row, so the fault is specific to how postgres-js frames those messages rather than to extended protocol. Every Drizzle query binds parameters, so no subset of this app works there. drizzle-kit hung at commit for the same reason - it builds its own client and drizzle.config.ts cannot pass prepare:false - having executed all 14 statements of 0000, which then rolled back. Transaction-mode pooling itself was confirmed, not assumed: in one client session consecutive statements hopped between server backends while statements inside BEGIN..COMMIT stayed pinned to one. So the comment now records what was measured, names PgBouncer as the pooler this configuration actually suits, and says migrations go straight at Postgres. dev-env.sh's note gets the same correction. Two real bugs found on the way there: - e2e-authz.sh and e2e-edge.sh hardcoded `psql -h localhost -U continuum -d continuum` while both read DATABASE_URL for the backend under test. Point DATABASE_URL elsewhere and the suite seeds fixtures into one database while exercising another - and its own failure message says "no database at $DATABASE_URL", naming a URL nothing ever tried. Both now use `psql "$DATABASE_URL"`. - drizzle.config.ts fell back to port 6432, where nothing listens here, so a bare db:migrate failed confusingly. It now matches dev-env.sh's DEV_DATABASE_URL. bun test 128 pass / 0 fail; e2e-authz.sh PASS with 68 checks against local Postgres after the psql_q change. e2e-edge.sh was not re-run because it drives the real printers, and its change is the same line. --- drizzle.config.ts | 6 +++++- scripts/dev-env.sh | 6 ++++-- scripts/e2e-authz.sh | 6 +++++- scripts/e2e-edge.sh | 6 +++++- src/db/index.ts | 23 +++++++++++++++++++++-- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/drizzle.config.ts b/drizzle.config.ts index 2928e91..e21e9c2 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -5,7 +5,11 @@ export default defineConfig({ out: "./drizzle", dialect: "postgresql", dbCredentials: { - url: process.env.DATABASE_URL ?? "postgres://continuum:continuum@localhost:6432/continuum", + // Matches scripts/dev-env.sh's DEV_DATABASE_URL. It pointed at 6432 (a + // pooler) where nothing listens in this environment, so a bare db:migrate + // failed confusingly. Migrations want Postgres directly in any case: see + // the note in src/db/index.ts about parameterised queries through pgCat. + url: process.env.DATABASE_URL ?? "postgres://continuum:continuum@localhost:5432/continuum", }, verbose: true, strict: true, diff --git a/scripts/dev-env.sh b/scripts/dev-env.sh index fe1e4f6..1d8f55e 100755 --- a/scripts/dev-env.sh +++ b/scripts/dev-env.sh @@ -35,8 +35,10 @@ cd "$REPO_ROOT" PG_VERSION=16 PG_PORT=5432 VALKEY_PORT=6379 -# Straight at Postgres, not pgCat on 6432: .env.example points at the pooler for -# production, and neither backend here runs one. +# Straight at Postgres, not pgCat: .env.example points at the pooler for +# production, and neither backend here runs one. Keep it that way even where a +# pooler is reachable - parameterised queries from postgres-js hang through +# pgCat 1.2.0, and drizzle-kit cannot migrate through it. See src/db/index.ts. DEV_DATABASE_URL="postgres://continuum:continuum@localhost:${PG_PORT}/continuum" DEV_VALKEY_URL="redis://localhost:${VALKEY_PORT}" diff --git a/scripts/e2e-authz.sh b/scripts/e2e-authz.sh index 0358e3e..8a63db3 100755 --- a/scripts/e2e-authz.sh +++ b/scripts/e2e-authz.sh @@ -50,7 +50,11 @@ new_uuid() { fi } -psql_q() { PGPASSWORD=continuum psql -h localhost -U continuum -d continuum -tAq -c "$1"; } +# Uses DATABASE_URL rather than a hardcoded localhost. The backend under test +# reads that variable, so a psql pointed anywhere else seeds fixtures into a +# different database than the one being exercised - and the failure message +# below names DATABASE_URL, which would then be naming a URL nothing tried. +psql_q() { psql "$DATABASE_URL" -tAq -c "$1"; } cleanup() { local rc=$? diff --git a/scripts/e2e-edge.sh b/scripts/e2e-edge.sh index 74210d3..29c4509 100755 --- a/scripts/e2e-edge.sh +++ b/scripts/e2e-edge.sh @@ -62,7 +62,11 @@ new_uuid() { fi } -psql_q() { PGPASSWORD=continuum psql -h localhost -U continuum -d continuum -tAq -c "$1"; } +# Uses DATABASE_URL rather than a hardcoded localhost. The backend under test +# reads that variable, so a psql pointed anywhere else seeds fixtures into a +# different database than the one being exercised - and the failure message +# below names DATABASE_URL, which would then be naming a URL nothing tried. +psql_q() { psql "$DATABASE_URL" -tAq -c "$1"; } # The farm's operator. A real Supabase user id would come from the project; # here it only has to be a uuid that the token's `sub` and the org_members row diff --git a/src/db/index.ts b/src/db/index.ts index 8ba70e6..5ac2a2f 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -2,8 +2,27 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; -// Points at pgCat/PgBouncer, not Postgres directly, so keep prepared -// statements disabled and cap the pool per-instance connections low. +// `prepare: false` because this is meant to sit behind a transaction-mode +// pooler, where a named statement prepared on one server connection is not +// there on the next one. `max` is low for the same reason: the pooler +// multiplexes, so a high per-instance count buys nothing and starves peers. +// +// Measured 2026-10-20, and the reason to read this before assuming the +// deployment works: against pgCat 1.2.0 this client cannot run at all. +// Through that pooler an unparameterised `select 1` succeeds, and so does +// `select count(*) from organisations`, but *any* query carrying a bind +// parameter hangs forever - with `prepare: false`, with `prepare: true`, with +// `fetch_types: false`, and inside an explicit transaction alike. The same +// queries straight at Postgres are fine, and psql's `\bind` through the same +// pool is fine, so it is specific to how postgres-js frames those messages, +// not to extended protocol in general. Every Drizzle query binds parameters, +// so there is no subset of this app that works there. +// +// So `prepare: false` is necessary but nowhere near sufficient, and running +// behind pgCat is an unverified claim rather than a tested path. PgBouncer is +// the pooler this configuration is actually known to suit. Migrations go +// straight at Postgres regardless - drizzle.config.ts has no way to pass +// `prepare: false`, and drizzle-kit hung at commit for the same reason. const connectionString = process.env.DATABASE_URL; if (!connectionString) { throw new Error("DATABASE_URL is not set");