import { Pool } from "pg"; /** * Configuration for the Postgres connection pool. * * Sensible defaults are provided for production Cloud Run / single-instance * deployments. Override via environment variables or explicit config. */ export interface PostgresPoolConfig { /** Maximum number of connections in the pool (default: 20) */ max?: number; /** Close idle connections after this many ms (default: 30 000) */ idleTimeoutMillis?: number; /** Abort connection attempts after this many ms (default: 10 000) */ connectionTimeoutMillis?: number; /** Per-query timeout in ms (default: 30 000) */ queryTimeout?: number; /** Per-statement timeout in ms (default: 30 000) */ statementTimeout?: number; /** Enable TCP keep-alive (default: true) */ keepAlive?: boolean; /** * `search_path` pinned on every connection (default: `"public"`). * * Pass `false` to send no `search_path` at all and inherit whatever the * server/role defaults to. See {@link pinSearchPath} for why the default * is not "inherit". */ searchPath?: string | false; } /** * Pin `search_path` into a connection string, so unqualified SQL resolves to a * schema this framework chose rather than to one Postgres inferred. * * Postgres defaults `search_path` to `"$user", public`: the *first* candidate * is a schema named after the connecting role. Rebase creates a schema called * `rebase` (auth, history, api keys), and every template, compose file and * deployment doc names the database role `rebase` too — so `$user` resolves to * a schema that exists, and every unqualified statement lands there instead of * in `public`. The generated Drizzle schema emits bare `pgTable("posts", …)` * for any collection without an explicit `schema`, which makes the *runtime's* * own reads and writes unqualified; a developer's raw `rebase.sql(...)`, the * Studio SQL editor and any hand-written migration are unqualified too. The * result is collection tables created in, and served from, `rebase`. * * Drizzle cannot express the fix on its side: `pgSchema("public")` throws by * design ("just use pgTable() instead"), so there is no way to emit a * public-qualified table from the generator. The pin has to live on the * connection. * * Precedence is deliberate and verified against node-postgres: `options` in * the connection string wins over the `options` field passed to `Pool`, so * rewriting the URL — rather than setting the field — is what makes this * authoritative. Two escape hatches survive it: * * - an `options` that already mentions `search_path` is left untouched, so a * deployment that deliberately pins something else keeps it; * - `searchPath: false` (or an unparseable, non-URL connection string) sends * nothing and inherits the server default. * * Anything else in `options` (a `statement_timeout`, say) is preserved and the * `search_path` flag is appended to it. */ export declare function pinSearchPath(connectionString: string, searchPath?: string | false): string; /** * Destroy pool clients that are released while still inside a transaction. * * pg-pool returns a client to the idle list whenever `release()` is called * without an error — even if the connection is still mid-transaction (status * `T`/`E`). That happens in practice: drizzle's pool transaction releases in * a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails * (e.g. it was queued behind a statement that hit the client-side * query_timeout), the client goes back dirty. The next checkout then runs * its statements inside the zombie transaction — with the previous request's * `app.*` RLS GUCs still applied, which turns unrelated queries into * RLS-scoped ones (observed in production as registration failing with * SQLSTATE 42501 under a leaked anonymous context). * * pg-pool emits `release` before it consults its private `_expired` set, so * marking the client expired here makes `_release()` destroy it instead of * pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are * private APIs — feature-detect and fall back to loud logging so an upstream * change degrades to observability, never to silent corruption. */ export declare function guardPoolAgainstDirtyRelease(pool: Pool, label: string): void; /** * Create a Drizzle-backed Postgres connection with a production-grade * connection pool. * * @param connectionString Postgres connection URL * @param schema Optional Drizzle schema for the relational API * @param poolConfig Optional pool tuning (merged over defaults) * * @returns `{ db, pool, connectionString }` — the `pool` is exposed so * callers can register shutdown hooks (`pool.end()`) or monitor * pool metrics. */ export declare function createPostgresDatabaseConnection(connectionString: string, schema?: Record, poolConfig?: PostgresPoolConfig): { db: import("drizzle-orm/node-postgres").NodePgDatabase> & { $client: Pool; }; pool: Pool; connectionString: string; }; /** * Create a direct (non-pooled) connection for operations that require * session-level features incompatible with PgBouncer transaction mode, * such as LISTEN/NOTIFY, prepared statements, or advisory locks. * * Uses a smaller pool since this is only for specific use cases. */ export declare function createDirectDatabaseConnection(connectionString: string, schema?: Record, poolConfig?: PostgresPoolConfig): { db: import("drizzle-orm/node-postgres").NodePgDatabase> & { $client: Pool; }; pool: Pool; connectionString: string; }; /** * Create a read-only connection for routing read queries to replicas. * Uses a moderate pool size since reads are distributed across replicas. */ export declare function createReadReplicaConnection(connectionString: string, schema?: Record, poolConfig?: PostgresPoolConfig): { db: import("drizzle-orm/node-postgres").NodePgDatabase> & { $client: Pool; }; pool: Pool; connectionString: string; };