@rebasepro/server-postgres 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/BranchService-ByLfM_Tt.js +360 -0
  2. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  3. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  4. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  5. package/dist/arg-Dni7MzLB.js +87 -0
  6. package/dist/arg-Dni7MzLB.js.map +1 -0
  7. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  8. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  9. package/dist/backup-cli-oM9WQVlV.js +408 -0
  10. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  11. package/dist/backup-service-Br3tqwsy.js +715 -0
  12. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  13. package/dist/cli-errors-DxN4yzdH.js +657 -0
  14. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  15. package/dist/cli-output-CNdMql-L.js +47 -0
  16. package/dist/cli-output-CNdMql-L.js.map +1 -0
  17. package/dist/cli.js +2020 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/{collection-index-C7LopMyX.js → collection-index-BHgT_TKd.js} +32 -3
  20. package/dist/{collection-index-C7LopMyX.js.map → collection-index-BHgT_TKd.js.map} +1 -1
  21. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  22. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  23. package/dist/databasePoolManager-Bj5FbeAs.js +349 -0
  24. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  25. package/dist/doctor-D7HWmGzs.js +974 -0
  26. package/dist/doctor-D7HWmGzs.js.map +1 -0
  27. package/dist/{ensure-collection-policies-CagOzEPS.js → ensure-collection-policies-2c2NV8zX.js} +5 -25
  28. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  29. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  30. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  31. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  32. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  33. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  34. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  35. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  36. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  37. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +1312 -0
  38. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  39. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  40. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  41. package/dist/index.es.js +849 -10807
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  44. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  45. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  46. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  47. package/dist/rls-bootstrap-sql-DLDnUjtx.js +7092 -0
  48. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  49. package/dist/{rls-enforcement-ClJoTboT.js → rls-enforcement-DeYEjYLt.js} +14 -3
  50. package/dist/{rls-enforcement-ClJoTboT.js.map → rls-enforcement-DeYEjYLt.js.map} +1 -1
  51. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  52. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  53. package/dist/schema/doctor-cli.js +129 -0
  54. package/dist/schema/doctor-cli.js.map +1 -0
  55. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  56. package/dist/schema/generate-drizzle-schema.js +24 -0
  57. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  58. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  59. package/dist/schema/generate-postgres-ddl.js +138 -0
  60. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  61. package/dist/schema/introspect-db.js +866 -0
  62. package/dist/schema/introspect-db.js.map +1 -0
  63. package/dist/source-Br7L7GOI.js +410 -0
  64. package/dist/source-Br7L7GOI.js.map +1 -0
  65. package/dist/vector-index-DkWxm2de.js +309 -0
  66. package/dist/vector-index-DkWxm2de.js.map +1 -0
  67. package/package.json +7 -7
  68. package/dist/ensure-collection-policies-CagOzEPS.js.map +0 -1
  69. package/dist/ensure-collection-tables-DbAhrjnG.js +0 -2732
  70. package/dist/ensure-collection-tables-DbAhrjnG.js.map +0 -1
@@ -1,5 +1,4 @@
1
1
  import { createRequire as __createRequire } from "module";
2
- import "process";
3
2
  __createRequire(import.meta.url);
4
3
  import { sql } from "drizzle-orm";
5
4
  //#region src/schema/collections-schema-version.ts
@@ -78,4 +77,4 @@ async function stampCollectionsSchemaVersion(db, metaSchema, version) {
78
77
  //#endregion
79
78
  export { readCollectionsSchemaVersion, stampCollectionsSchemaVersion };
80
79
 
81
- //# sourceMappingURL=collections-schema-version-BMeu3cgv.js.map
80
+ //# sourceMappingURL=collections-schema-version-T6cGvrDi.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"collections-schema-version-BMeu3cgv.js","names":[],"sources":["../src/schema/collections-schema-version.ts"],"sourcesContent":["/**\n * The collections schema version, stamped into the database.\n *\n * Deliberately the same table shape, the same key/value convention and the same\n * \"unstamped is null, and null is not an error\" rule as `auth/schema-version.ts`.\n * They answer the same kind of question — \"was this database provisioned by\n * something that agrees with me\" — and inventing a second convention for the\n * second one would be a second thing to find, back up and reason about.\n *\n * It does *not* follow the auth stamp's schema. `resolveAuthSchema` tracks the\n * users collection, so a project that puts its users in a custom schema moves\n * the auth stamp with them — correct there, because that stamp describes those\n * tables. This one describes the collections as a whole and belongs with the\n * runtime's own internal state, which is always `rebase`. Usually they land in\n * the same table anyway; when they do not, that is the reason.\n *\n * The value is opaque here on purpose. It is a hash produced by\n * `computeSchemaVersion` in `@rebasepro/types`, and this module's only job is to\n * put a string in and take the same string out. Comparing them, and deciding\n * what a difference means, is `boot/schema-stamp.ts` in the runtime — where it\n * can be tested without a database.\n *\n * Unlike the auth version, this is **not** ordered. The auth stamp is an integer\n * whose comparison direction carries meaning (a database newer than the runtime\n * is unrecoverable, older is the ordinary upgrade path). A schema hash has no\n * order at all: it can say the two disagree and never which is ahead. That is a\n * deliberate limit, and the reason the runtime's response to a mismatch is to\n * describe it rather than to decide who is wrong.\n */\nimport { sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\n/** Key under which the version is stored in the meta table. */\nconst VERSION_KEY = \"collections_schema_version\";\n\n/**\n * Anything that can run SQL for us.\n *\n * Narrower than `NodePgDatabase` so the provisioning handle — which is what the\n * boot path actually has in hand — satisfies it without a cast at every call\n * site.\n */\nexport interface SchemaMetaQueryable {\n execute(query: unknown): Promise<{ rows: Record<string, unknown>[] }>;\n}\n\n/**\n * Read the stamped version, or `null` when this database has never been stamped.\n *\n * `to_regclass` rather than a plain `SELECT` so a missing schema or table is a\n * `null` instead of a thrown 42P01 — the overwhelmingly common case on a fresh\n * database is that neither exists yet, and that is not news.\n */\nexport async function readCollectionsSchemaVersion(\n db: SchemaMetaQueryable | NodePgDatabase,\n metaSchema: string\n): Promise<string | null> {\n const qualified = `\"${metaSchema}\".\"schema_meta\"`;\n const exists = await (db as SchemaMetaQueryable).execute(\n sql`SELECT to_regclass(${qualified}) IS NOT NULL AS present`\n );\n if (!(exists.rows[0] as { present: boolean } | undefined)?.present) return null;\n\n const result = await (db as SchemaMetaQueryable).execute(sql`\n SELECT value FROM ${sql.raw(qualified)} WHERE key = ${VERSION_KEY}\n `);\n const raw = (result.rows[0] as { value: string } | undefined)?.value;\n\n // An empty string is not a version anybody computed, and treating it as one\n // would make every process disagree with the database forever. Unstamped.\n return raw === undefined || raw.trim() === \"\" ? null : raw;\n}\n\n/**\n * Record the version this runtime just applied.\n *\n * Creates the meta table if the auth stamp has not already — the two are\n * independent halves of one boot and either may run first, so neither can assume\n * the table is there. `CREATE TABLE IF NOT EXISTS` is not atomic against a\n * concurrent identical statement, which is exactly the race a split deployment\n * arranges; it is tolerated here for the same reason it is elsewhere in the boot\n * path, and the caller treats any failure as a warning rather than a fatal.\n */\nexport async function stampCollectionsSchemaVersion(\n db: SchemaMetaQueryable | NodePgDatabase,\n metaSchema: string,\n version: string\n): Promise<void> {\n const qualified = `\"${metaSchema}\".\"schema_meta\"`;\n await (db as SchemaMetaQueryable).execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.identifier(metaSchema)}`);\n await (db as SchemaMetaQueryable).execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(qualified)} (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n )\n `);\n await (db as SchemaMetaQueryable).execute(sql`\n INSERT INTO ${sql.raw(qualified)} (key, value)\n VALUES (${VERSION_KEY}, ${version})\n ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()\n `);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAM,cAAc;;;;;;;;AAoBpB,eAAsB,6BAClB,IACA,YACsB;CACtB,MAAM,YAAY,IAAI,WAAW;CAIjC,IAAI,EAAE,MAHgB,GAA2B,QAC7C,GAAG,sBAAsB,UAAU,yBACvC,EAAA,CACa,KAAK,EAAE,EAAuC,SAAS,OAAO;CAK3E,MAAM,OAAO,MAHS,GAA2B,QAAQ,GAAG;4BACpC,IAAI,IAAI,SAAS,EAAE,eAAe,YAAY;KACrE,EAAA,CACmB,KAAK,EAAE,EAAoC;CAI/D,OAAO,QAAQ,KAAA,KAAa,IAAI,KAAK,MAAM,KAAK,OAAO;AAC3D;;;;;;;;;;;AAYA,eAAsB,8BAClB,IACA,YACA,SACa;CACb,MAAM,YAAY,IAAI,WAAW;CACjC,MAAO,GAA2B,QAAQ,GAAG,+BAA+B,IAAI,WAAW,UAAU,GAAG;CACxG,MAAO,GAA2B,QAAQ,GAAG;qCACZ,IAAI,IAAI,SAAS,EAAE;;;;;KAKnD;CACD,MAAO,GAA2B,QAAQ,GAAG;sBAC3B,IAAI,IAAI,SAAS,EAAE;kBACvB,YAAY,IAAI,QAAQ;;KAErC;AACL"}
1
+ {"version":3,"file":"collections-schema-version-T6cGvrDi.js","names":[],"sources":["../src/schema/collections-schema-version.ts"],"sourcesContent":["/**\n * The collections schema version, stamped into the database.\n *\n * Deliberately the same table shape, the same key/value convention and the same\n * \"unstamped is null, and null is not an error\" rule as `auth/schema-version.ts`.\n * They answer the same kind of question — \"was this database provisioned by\n * something that agrees with me\" — and inventing a second convention for the\n * second one would be a second thing to find, back up and reason about.\n *\n * It does *not* follow the auth stamp's schema. `resolveAuthSchema` tracks the\n * users collection, so a project that puts its users in a custom schema moves\n * the auth stamp with them — correct there, because that stamp describes those\n * tables. This one describes the collections as a whole and belongs with the\n * runtime's own internal state, which is always `rebase`. Usually they land in\n * the same table anyway; when they do not, that is the reason.\n *\n * The value is opaque here on purpose. It is a hash produced by\n * `computeSchemaVersion` in `@rebasepro/types`, and this module's only job is to\n * put a string in and take the same string out. Comparing them, and deciding\n * what a difference means, is `boot/schema-stamp.ts` in the runtime — where it\n * can be tested without a database.\n *\n * Unlike the auth version, this is **not** ordered. The auth stamp is an integer\n * whose comparison direction carries meaning (a database newer than the runtime\n * is unrecoverable, older is the ordinary upgrade path). A schema hash has no\n * order at all: it can say the two disagree and never which is ahead. That is a\n * deliberate limit, and the reason the runtime's response to a mismatch is to\n * describe it rather than to decide who is wrong.\n */\nimport { sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\n/** Key under which the version is stored in the meta table. */\nconst VERSION_KEY = \"collections_schema_version\";\n\n/**\n * Anything that can run SQL for us.\n *\n * Narrower than `NodePgDatabase` so the provisioning handle — which is what the\n * boot path actually has in hand — satisfies it without a cast at every call\n * site.\n */\nexport interface SchemaMetaQueryable {\n execute(query: unknown): Promise<{ rows: Record<string, unknown>[] }>;\n}\n\n/**\n * Read the stamped version, or `null` when this database has never been stamped.\n *\n * `to_regclass` rather than a plain `SELECT` so a missing schema or table is a\n * `null` instead of a thrown 42P01 — the overwhelmingly common case on a fresh\n * database is that neither exists yet, and that is not news.\n */\nexport async function readCollectionsSchemaVersion(\n db: SchemaMetaQueryable | NodePgDatabase,\n metaSchema: string\n): Promise<string | null> {\n const qualified = `\"${metaSchema}\".\"schema_meta\"`;\n const exists = await (db as SchemaMetaQueryable).execute(\n sql`SELECT to_regclass(${qualified}) IS NOT NULL AS present`\n );\n if (!(exists.rows[0] as { present: boolean } | undefined)?.present) return null;\n\n const result = await (db as SchemaMetaQueryable).execute(sql`\n SELECT value FROM ${sql.raw(qualified)} WHERE key = ${VERSION_KEY}\n `);\n const raw = (result.rows[0] as { value: string } | undefined)?.value;\n\n // An empty string is not a version anybody computed, and treating it as one\n // would make every process disagree with the database forever. Unstamped.\n return raw === undefined || raw.trim() === \"\" ? null : raw;\n}\n\n/**\n * Record the version this runtime just applied.\n *\n * Creates the meta table if the auth stamp has not already — the two are\n * independent halves of one boot and either may run first, so neither can assume\n * the table is there. `CREATE TABLE IF NOT EXISTS` is not atomic against a\n * concurrent identical statement, which is exactly the race a split deployment\n * arranges; it is tolerated here for the same reason it is elsewhere in the boot\n * path, and the caller treats any failure as a warning rather than a fatal.\n */\nexport async function stampCollectionsSchemaVersion(\n db: SchemaMetaQueryable | NodePgDatabase,\n metaSchema: string,\n version: string\n): Promise<void> {\n const qualified = `\"${metaSchema}\".\"schema_meta\"`;\n await (db as SchemaMetaQueryable).execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.identifier(metaSchema)}`);\n await (db as SchemaMetaQueryable).execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(qualified)} (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n )\n `);\n await (db as SchemaMetaQueryable).execute(sql`\n INSERT INTO ${sql.raw(qualified)} (key, value)\n VALUES (${VERSION_KEY}, ${version})\n ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()\n `);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAM,cAAc;;;;;;;;AAoBpB,eAAsB,6BAClB,IACA,YACsB;CACtB,MAAM,YAAY,IAAI,WAAW;CAIjC,IAAI,EAAE,MAHgB,GAA2B,QAC7C,GAAG,sBAAsB,UAAU,yBACvC,EAAA,CACa,KAAK,EAAE,EAAuC,SAAS,OAAO;CAK3E,MAAM,OAAO,MAHS,GAA2B,QAAQ,GAAG;4BACpC,IAAI,IAAI,SAAS,EAAE,eAAe,YAAY;KACrE,EAAA,CACmB,KAAK,EAAE,EAAoC;CAI/D,OAAO,QAAQ,KAAA,KAAa,IAAI,KAAK,MAAM,KAAK,OAAO;AAC3D;;;;;;;;;;;AAYA,eAAsB,8BAClB,IACA,YACA,SACa;CACb,MAAM,YAAY,IAAI,WAAW;CACjC,MAAO,GAA2B,QAAQ,GAAG,+BAA+B,IAAI,WAAW,UAAU,GAAG;CACxG,MAAO,GAA2B,QAAQ,GAAG;qCACZ,IAAI,IAAI,SAAS,EAAE;;;;;KAKnD;CACD,MAAO,GAA2B,QAAQ,GAAG;sBAC3B,IAAI,IAAI,SAAS,EAAE;kBACvB,YAAY,IAAI,QAAQ;;KAErC;AACL"}
@@ -0,0 +1,349 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
+ import { Pool } from "pg";
5
+ import { drizzle } from "drizzle-orm/node-postgres";
6
+ import { logger } from "@rebasepro/server";
7
+ //#region src/connection.ts
8
+ var connection_exports = /* @__PURE__ */ __exportAll({
9
+ cappedPoolMax: () => cappedPoolMax,
10
+ createDirectDatabaseConnection: () => createDirectDatabaseConnection,
11
+ createPostgresDatabaseConnection: () => createPostgresDatabaseConnection,
12
+ createReadReplicaConnection: () => createReadReplicaConnection,
13
+ guardPoolAgainstDirtyRelease: () => guardPoolAgainstDirtyRelease,
14
+ pinSearchPath: () => pinSearchPath,
15
+ poolMaxCeiling: () => poolMaxCeiling
16
+ });
17
+ /**
18
+ * Ceiling on pool size, from the environment.
19
+ *
20
+ * Exists for the managed development database. PGlite is a single session
21
+ * behind a multiplexing socket server, and two pooled clients holding
22
+ * overlapping transactions deadlock there — which a request-per-transaction
23
+ * server produces the moment two requests overlap. One connection turns that
24
+ * deadlock into ordinary queueing.
25
+ *
26
+ * A ceiling rather than a value, so it constrains every pool in the process
27
+ * without any of them having to know why. Ignored when unset or unparseable,
28
+ * because a malformed limit must not silently serialize a production server.
29
+ */
30
+ function poolMaxCeiling(env = process.env) {
31
+ const raw = env.REBASE_DB_POOL_MAX;
32
+ if (typeof raw !== "string") return null;
33
+ if (!/^[0-9]+$/.test(raw.trim())) return null;
34
+ const parsed = Number(raw.trim());
35
+ return parsed >= 1 ? parsed : null;
36
+ }
37
+ /** Apply {@link poolMaxCeiling} to a requested pool size. */
38
+ function cappedPoolMax(requested, env = process.env) {
39
+ const ceiling = poolMaxCeiling(env);
40
+ return ceiling === null ? requested : Math.min(requested, ceiling);
41
+ }
42
+ var DEFAULT_POOL = {
43
+ max: 20,
44
+ searchPath: "public",
45
+ idleTimeoutMillis: 3e4,
46
+ connectionTimeoutMillis: 1e4,
47
+ queryTimeout: 6e4,
48
+ statementTimeout: 3e4,
49
+ keepAlive: true
50
+ };
51
+ /** ReadyForQuery status byte: `I` idle, `T` in transaction, `E` failed transaction. */
52
+ var TX_IDLE = "I";
53
+ /**
54
+ * Pin `search_path` into a connection string, so unqualified SQL resolves to a
55
+ * schema this framework chose rather than to one Postgres inferred.
56
+ *
57
+ * Postgres defaults `search_path` to `"$user", public`: the *first* candidate
58
+ * is a schema named after the connecting role. Rebase creates a schema called
59
+ * `rebase` (auth, history, api keys), and every template, compose file and
60
+ * deployment doc names the database role `rebase` too — so `$user` resolves to
61
+ * a schema that exists, and every unqualified statement lands there instead of
62
+ * in `public`. The generated Drizzle schema emits bare `pgTable("posts", …)`
63
+ * for any collection without an explicit `schema`, which makes the *runtime's*
64
+ * own reads and writes unqualified; a developer's raw `rebase.sql(...)`, the
65
+ * Studio SQL editor and any hand-written migration are unqualified too. The
66
+ * result is collection tables created in, and served from, `rebase`.
67
+ *
68
+ * Drizzle cannot express the fix on its side: `pgSchema("public")` throws by
69
+ * design ("just use pgTable() instead"), so there is no way to emit a
70
+ * public-qualified table from the generator. The pin has to live on the
71
+ * connection.
72
+ *
73
+ * Precedence is deliberate and verified against node-postgres: `options` in
74
+ * the connection string wins over the `options` field passed to `Pool`, so
75
+ * rewriting the URL — rather than setting the field — is what makes this
76
+ * authoritative. Two escape hatches survive it:
77
+ *
78
+ * - an `options` that already mentions `search_path` is left untouched, so a
79
+ * deployment that deliberately pins something else keeps it;
80
+ * - `searchPath: false` (or an unparseable, non-URL connection string) sends
81
+ * nothing and inherits the server default.
82
+ *
83
+ * Anything else in `options` (a `statement_timeout`, say) is preserved and the
84
+ * `search_path` flag is appended to it.
85
+ */
86
+ function pinSearchPath(connectionString, searchPath = "public") {
87
+ if (searchPath === false) return connectionString;
88
+ let url;
89
+ try {
90
+ url = new URL(connectionString);
91
+ } catch {
92
+ return connectionString;
93
+ }
94
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return connectionString;
95
+ const existing = url.searchParams.get("options");
96
+ if (existing && /(^|\s)-c\s*search_path\s*=/.test(existing)) return connectionString;
97
+ const flag = `-c search_path=${searchPath}`;
98
+ url.searchParams.set("options", existing ? `${existing} ${flag}` : flag);
99
+ url.search = Array.from(url.searchParams.entries()).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
100
+ return url.toString();
101
+ }
102
+ /**
103
+ * Destroy pool clients that are released while still inside a transaction.
104
+ *
105
+ * pg-pool returns a client to the idle list whenever `release()` is called
106
+ * without an error — even if the connection is still mid-transaction (status
107
+ * `T`/`E`). That happens in practice: drizzle's pool transaction releases in
108
+ * a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails
109
+ * (e.g. it was queued behind a statement that hit the client-side
110
+ * query_timeout), the client goes back dirty. The next checkout then runs
111
+ * its statements inside the zombie transaction — with the previous request's
112
+ * `app.*` RLS GUCs still applied, which turns unrelated queries into
113
+ * RLS-scoped ones (observed in production as registration failing with
114
+ * SQLSTATE 42501 under a leaked anonymous context).
115
+ *
116
+ * pg-pool emits `release` before it consults its private `_expired` set, so
117
+ * marking the client expired here makes `_release()` destroy it instead of
118
+ * pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are
119
+ * private APIs — feature-detect and fall back to loud logging so an upstream
120
+ * change degrades to observability, never to silent corruption.
121
+ */
122
+ function guardPoolAgainstDirtyRelease(pool, label) {
123
+ pool.on("release", (err, client) => {
124
+ if (err) return;
125
+ const txStatus = client?._txStatus;
126
+ if (typeof txStatus !== "string" || txStatus === TX_IDLE) return;
127
+ const expired = pool._expired;
128
+ if (expired && typeof expired.add === "function" && typeof expired.has === "function" && client && typeof client === "object") {
129
+ expired.add(client);
130
+ logger.error(`[${label}] Client released back to the pool while still in a transaction (status '${txStatus}') — destroying it so the open transaction and its session state (RLS GUCs) cannot leak into the next request.`);
131
+ } else logger.error(`[${label}] Client released mid-transaction (status '${txStatus}') but the pool's internal expiry set is unavailable (pg-pool internals changed?). The connection may leak its open transaction into subsequent requests.`);
132
+ });
133
+ }
134
+ /**
135
+ * Create a Drizzle-backed Postgres connection with a production-grade
136
+ * connection pool.
137
+ *
138
+ * @param connectionString Postgres connection URL
139
+ * @param schema Optional Drizzle schema for the relational API
140
+ * @param poolConfig Optional pool tuning (merged over defaults)
141
+ *
142
+ * @returns `{ db, pool, connectionString }` — the `pool` is exposed so
143
+ * callers can register shutdown hooks (`pool.end()`) or monitor
144
+ * pool metrics.
145
+ */
146
+ function createPostgresDatabaseConnection(connectionString, schema, poolConfig) {
147
+ const opts = {
148
+ ...DEFAULT_POOL,
149
+ ...poolConfig
150
+ };
151
+ connectionString = pinSearchPath(connectionString, opts.searchPath);
152
+ const pool = new Pool({
153
+ connectionString,
154
+ max: cappedPoolMax(opts.max),
155
+ idleTimeoutMillis: opts.idleTimeoutMillis,
156
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
157
+ query_timeout: opts.queryTimeout,
158
+ statement_timeout: opts.statementTimeout,
159
+ keepAlive: opts.keepAlive,
160
+ keepAliveInitialDelayMillis: 0
161
+ });
162
+ pool.on("error", (err) => {
163
+ logger.error("[pg-pool] Unexpected pool error", { detail: err.message });
164
+ if (err.message.includes("ETIMEDOUT")) logger.warn("[pg-pool] Connection timeout detected — pool will auto-retry");
165
+ });
166
+ guardPoolAgainstDirtyRelease(pool, "pg-pool");
167
+ return {
168
+ db: schema ? drizzle(pool, { schema }) : drizzle(pool),
169
+ pool,
170
+ connectionString
171
+ };
172
+ }
173
+ /**
174
+ * Create a direct (non-pooled) connection for operations that require
175
+ * session-level features incompatible with PgBouncer transaction mode,
176
+ * such as LISTEN/NOTIFY, prepared statements, or advisory locks.
177
+ *
178
+ * Uses a smaller pool since this is only for specific use cases.
179
+ */
180
+ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
181
+ const opts = {
182
+ ...DEFAULT_POOL,
183
+ max: 5,
184
+ ...poolConfig
185
+ };
186
+ connectionString = pinSearchPath(connectionString, opts.searchPath);
187
+ const pool = new Pool({
188
+ connectionString,
189
+ max: cappedPoolMax(opts.max),
190
+ idleTimeoutMillis: opts.idleTimeoutMillis,
191
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
192
+ query_timeout: opts.queryTimeout,
193
+ statement_timeout: opts.statementTimeout,
194
+ keepAlive: opts.keepAlive,
195
+ keepAliveInitialDelayMillis: 0
196
+ });
197
+ pool.on("error", (err) => {
198
+ logger.error("[pg-direct-pool] Unexpected pool error", { detail: err.message });
199
+ });
200
+ guardPoolAgainstDirtyRelease(pool, "pg-direct-pool");
201
+ return {
202
+ db: schema ? drizzle(pool, { schema }) : drizzle(pool),
203
+ pool,
204
+ connectionString
205
+ };
206
+ }
207
+ /**
208
+ * Create a read-only connection for routing read queries to replicas.
209
+ * Uses a moderate pool size since reads are distributed across replicas.
210
+ */
211
+ function createReadReplicaConnection(connectionString, schema, poolConfig) {
212
+ const opts = {
213
+ ...DEFAULT_POOL,
214
+ max: 10,
215
+ ...poolConfig
216
+ };
217
+ connectionString = pinSearchPath(connectionString, opts.searchPath);
218
+ const pool = new Pool({
219
+ connectionString,
220
+ max: cappedPoolMax(opts.max),
221
+ idleTimeoutMillis: opts.idleTimeoutMillis,
222
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
223
+ query_timeout: opts.queryTimeout,
224
+ statement_timeout: opts.statementTimeout,
225
+ keepAlive: opts.keepAlive,
226
+ keepAliveInitialDelayMillis: 0
227
+ });
228
+ pool.on("error", (err) => {
229
+ logger.error("[pg-replica-pool] Unexpected pool error", { detail: err.message });
230
+ });
231
+ guardPoolAgainstDirtyRelease(pool, "pg-replica-pool");
232
+ return {
233
+ db: schema ? drizzle(pool, { schema }) : drizzle(pool),
234
+ pool,
235
+ connectionString
236
+ };
237
+ }
238
+ //#endregion
239
+ //#region src/databasePoolManager.ts
240
+ var databasePoolManager_exports = /* @__PURE__ */ __exportAll({ DatabasePoolManager: () => DatabasePoolManager });
241
+ var DatabasePoolManager = class {
242
+ pools = /* @__PURE__ */ new Map();
243
+ drizzleInstances = /* @__PURE__ */ new Map();
244
+ defaultDatabaseName;
245
+ rootConnectionString;
246
+ constructor(adminConnectionString) {
247
+ this.rootConnectionString = adminConnectionString;
248
+ try {
249
+ const url = new URL(adminConnectionString);
250
+ this.defaultDatabaseName = url.pathname.slice(1);
251
+ } catch (e) {
252
+ throw new Error(`Invalid adminConnectionString provided: ${e}`);
253
+ }
254
+ }
255
+ getDrizzle(databaseName) {
256
+ const existing = this.drizzleInstances.get(databaseName);
257
+ if (existing) return existing;
258
+ const db = drizzle(this.getPool(databaseName));
259
+ this.drizzleInstances.set(databaseName, db);
260
+ return db;
261
+ }
262
+ getPool(databaseName) {
263
+ if (this.pools.has(databaseName)) return this.pools.get(databaseName);
264
+ const url = new URL(this.rootConnectionString);
265
+ url.pathname = `/${databaseName}`;
266
+ const pool = new Pool({
267
+ connectionString: pinSearchPath(url.toString()),
268
+ max: cappedPoolMax(10),
269
+ idleTimeoutMillis: 1e4,
270
+ allowExitOnIdle: true
271
+ });
272
+ pool.on("error", (err) => {
273
+ logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
274
+ });
275
+ guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);
276
+ this.pools.set(databaseName, pool);
277
+ return pool;
278
+ }
279
+ /**
280
+ * Disconnect and remove the pool for a specific database.
281
+ * Required before `CREATE DATABASE ... TEMPLATE` or `DROP DATABASE`,
282
+ * which need exclusive access to the target database.
283
+ */
284
+ async disconnectDatabase(databaseName) {
285
+ const pool = this.pools.get(databaseName);
286
+ if (pool) {
287
+ await pool.end();
288
+ this.pools.delete(databaseName);
289
+ this.drizzleInstances.delete(databaseName);
290
+ }
291
+ }
292
+ /** Check if a pool exists for a given database name. */
293
+ hasPool(databaseName) {
294
+ return this.pools.has(databaseName);
295
+ }
296
+ /**
297
+ * Does this server serve exactly one database?
298
+ *
299
+ * The managed development database is PGlite, and PGlite serves exactly
300
+ * one. `CREATE DATABASE "rb_feature" TEMPLATE "postgres"` there writes a
301
+ * `pg_database` catalogue row and nothing else: nothing errors, the row is
302
+ * listable, `pg_database_size` answers — and connecting to the "branch"
303
+ * lands you in the parent. Every write made in the belief that it is
304
+ * sandboxed goes to the developer's real development database.
305
+ *
306
+ * The CLI already refuses `rebase db branch` on this ground, but it does it
307
+ * by parsing argv, so Studio — which reaches `BranchService` over the
308
+ * websocket — walked straight past it. The connection is the only thing
309
+ * both callers share, so the answer belongs here.
310
+ *
311
+ * Measured, not assumed: `select version()` over the PGlite socket returns
312
+ * `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32-…`, which is how a wasm build
313
+ * announces itself and what `dev-db/constraints.ts` documents. A real
314
+ * Postgres never carries `(PGlite `.
315
+ *
316
+ * Cached, because a live connection cannot change engine underneath us, and
317
+ * a `PostgresBackendDriver` — which builds a `BranchService` — is
318
+ * constructed per transaction.
319
+ */
320
+ servesOneDatabase() {
321
+ this.oneDatabaseProbe ??= this.probeOneDatabase();
322
+ return this.oneDatabaseProbe;
323
+ }
324
+ oneDatabaseProbe;
325
+ async probeOneDatabase() {
326
+ try {
327
+ const result = await this.getPool(this.defaultDatabaseName).query("SELECT version() AS version");
328
+ return /\(PGlite\b/i.test(result.rows[0]?.version ?? "");
329
+ } catch (err) {
330
+ this.oneDatabaseProbe = void 0;
331
+ logger.warn("[DatabasePoolManager] Could not read the server version", { error: err });
332
+ return false;
333
+ }
334
+ }
335
+ async shutdown() {
336
+ const promises = [];
337
+ for (const [dbName, pool] of this.pools.entries()) {
338
+ logger.info(`[DatabasePoolManager] Shutting down pool for ${dbName}`);
339
+ promises.push(pool.end());
340
+ }
341
+ await Promise.all(promises);
342
+ this.pools.clear();
343
+ this.drizzleInstances.clear();
344
+ }
345
+ };
346
+ //#endregion
347
+ export { createDirectDatabaseConnection as a, guardPoolAgainstDirtyRelease as c, connection_exports as i, pinSearchPath as l, databasePoolManager_exports as n, createPostgresDatabaseConnection as o, cappedPoolMax as r, createReadReplicaConnection as s, DatabasePoolManager as t, poolMaxCeiling as u };
348
+
349
+ //# sourceMappingURL=databasePoolManager-Bj5FbeAs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"databasePoolManager-Bj5FbeAs.js","names":[],"sources":["../src/connection.ts","../src/databasePoolManager.ts"],"sourcesContent":["import { Pool, PoolConfig } from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Configuration for the Postgres connection pool.\n *\n * Sensible defaults are provided for production Cloud Run / single-instance\n * deployments. Override via environment variables or explicit config.\n */\nexport interface PostgresPoolConfig {\n /** Maximum number of connections in the pool (default: 20) */\n max?: number;\n /** Close idle connections after this many ms (default: 30 000) */\n idleTimeoutMillis?: number;\n /** Abort connection attempts after this many ms (default: 10 000) */\n connectionTimeoutMillis?: number;\n /** Per-query timeout in ms (default: 30 000) */\n queryTimeout?: number;\n /** Per-statement timeout in ms (default: 30 000) */\n statementTimeout?: number;\n /** Enable TCP keep-alive (default: true) */\n keepAlive?: boolean;\n /**\n * `search_path` pinned on every connection (default: `\"public\"`).\n *\n * Pass `false` to send no `search_path` at all and inherit whatever the\n * server/role defaults to. See {@link pinSearchPath} for why the default\n * is not \"inherit\".\n */\n searchPath?: string | false;\n}\n\n/**\n * Ceiling on pool size, from the environment.\n *\n * Exists for the managed development database. PGlite is a single session\n * behind a multiplexing socket server, and two pooled clients holding\n * overlapping transactions deadlock there — which a request-per-transaction\n * server produces the moment two requests overlap. One connection turns that\n * deadlock into ordinary queueing.\n *\n * A ceiling rather than a value, so it constrains every pool in the process\n * without any of them having to know why. Ignored when unset or unparseable,\n * because a malformed limit must not silently serialize a production server.\n */\nexport function poolMaxCeiling(env: NodeJS.ProcessEnv = process.env): number | null {\n const raw = env.REBASE_DB_POOL_MAX;\n if (typeof raw !== \"string\") return null;\n\n // Plain decimal digits only. `Number` would also accept `1e3`, `0x10` and\n // ` 4 ` — none of which anyone writes on purpose in a config value, and all\n // of which would mean this silently did something other than it appears to.\n if (!/^[0-9]+$/.test(raw.trim())) return null;\n\n const parsed = Number(raw.trim());\n\n return parsed >= 1 ? parsed : null;\n}\n\n/** Apply {@link poolMaxCeiling} to a requested pool size. */\nexport function cappedPoolMax(requested: number, env: NodeJS.ProcessEnv = process.env): number {\n const ceiling = poolMaxCeiling(env);\n\n return ceiling === null ? requested : Math.min(requested, ceiling);\n}\n\nconst DEFAULT_POOL: Required<PostgresPoolConfig> = {\n max: 20,\n searchPath: \"public\",\n idleTimeoutMillis: 30_000,\n connectionTimeoutMillis: 10_000,\n // The client-side read timeout MUST be comfortably above the server-side\n // statement_timeout. When the client timer fires first, node-postgres\n // abandons the in-flight statement but keeps the connection — inside a\n // transaction that leaves the tx open (and any pending ROLLBACK is\n // spliced out of the client queue before it ever reaches the wire), so\n // the pooled connection is returned still in-transaction with its RLS\n // GUCs set. The server abort (SQLSTATE 57014) is the clean path; the\n // client timeout is only a backstop for a dead network.\n queryTimeout: 60_000,\n statementTimeout: 30_000,\n keepAlive: true\n};\n\n/** ReadyForQuery status byte: `I` idle, `T` in transaction, `E` failed transaction. */\nconst TX_IDLE = \"I\";\n\n/**\n * Pin `search_path` into a connection string, so unqualified SQL resolves to a\n * schema this framework chose rather than to one Postgres inferred.\n *\n * Postgres defaults `search_path` to `\"$user\", public`: the *first* candidate\n * is a schema named after the connecting role. Rebase creates a schema called\n * `rebase` (auth, history, api keys), and every template, compose file and\n * deployment doc names the database role `rebase` too — so `$user` resolves to\n * a schema that exists, and every unqualified statement lands there instead of\n * in `public`. The generated Drizzle schema emits bare `pgTable(\"posts\", …)`\n * for any collection without an explicit `schema`, which makes the *runtime's*\n * own reads and writes unqualified; a developer's raw `rebase.sql(...)`, the\n * Studio SQL editor and any hand-written migration are unqualified too. The\n * result is collection tables created in, and served from, `rebase`.\n *\n * Drizzle cannot express the fix on its side: `pgSchema(\"public\")` throws by\n * design (\"just use pgTable() instead\"), so there is no way to emit a\n * public-qualified table from the generator. The pin has to live on the\n * connection.\n *\n * Precedence is deliberate and verified against node-postgres: `options` in\n * the connection string wins over the `options` field passed to `Pool`, so\n * rewriting the URL — rather than setting the field — is what makes this\n * authoritative. Two escape hatches survive it:\n *\n * - an `options` that already mentions `search_path` is left untouched, so a\n * deployment that deliberately pins something else keeps it;\n * - `searchPath: false` (or an unparseable, non-URL connection string) sends\n * nothing and inherits the server default.\n *\n * Anything else in `options` (a `statement_timeout`, say) is preserved and the\n * `search_path` flag is appended to it.\n */\nexport function pinSearchPath(connectionString: string, searchPath: string | false = \"public\"): string {\n if (searchPath === false) return connectionString;\n\n let url: URL;\n try {\n url = new URL(connectionString);\n } catch {\n // Key/value DSNs and anything else we cannot parse are returned as\n // given: a connection that works unpinned beats one we corrupted.\n return connectionString;\n }\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") return connectionString;\n\n const existing = url.searchParams.get(\"options\");\n if (existing && /(^|\\s)-c\\s*search_path\\s*=/.test(existing)) return connectionString;\n\n const flag = `-c search_path=${searchPath}`;\n url.searchParams.set(\"options\", existing ? `${existing} ${flag}` : flag);\n // Re-serialize by hand. `URLSearchParams` writes a space as `+`, which\n // node-postgres happens to decode but libpq does not — and this same string\n // is handed to `pg_dump`/`psql` for backups. Percent-encoding is the form\n // both agree on, and is what the scaffolded `.env` already ships.\n url.search = Array.from(url.searchParams.entries())\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join(\"&\");\n return url.toString();\n}\n\n/**\n * Destroy pool clients that are released while still inside a transaction.\n *\n * pg-pool returns a client to the idle list whenever `release()` is called\n * without an error — even if the connection is still mid-transaction (status\n * `T`/`E`). That happens in practice: drizzle's pool transaction releases in\n * a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails\n * (e.g. it was queued behind a statement that hit the client-side\n * query_timeout), the client goes back dirty. The next checkout then runs\n * its statements inside the zombie transaction — with the previous request's\n * `app.*` RLS GUCs still applied, which turns unrelated queries into\n * RLS-scoped ones (observed in production as registration failing with\n * SQLSTATE 42501 under a leaked anonymous context).\n *\n * pg-pool emits `release` before it consults its private `_expired` set, so\n * marking the client expired here makes `_release()` destroy it instead of\n * pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are\n * private APIs — feature-detect and fall back to loud logging so an upstream\n * change degrades to observability, never to silent corruption.\n */\nexport function guardPoolAgainstDirtyRelease(pool: Pool, label: string): void {\n pool.on(\"release\", (err: Error | undefined, client: unknown) => {\n if (err) return; // errored clients are already destroyed by pg-pool\n const txStatus = (client as { _txStatus?: string | null })?._txStatus;\n if (typeof txStatus !== \"string\" || txStatus === TX_IDLE) return;\n\n // pg-pool keeps expired clients in a WeakSet (a plain object works too\n // if upstream ever changes it — duck-type on add/has).\n const expired = (pool as unknown as { _expired?: { add(c: object): unknown; has(c: object): boolean } })._expired;\n if (expired && typeof expired.add === \"function\" && typeof expired.has === \"function\" && client && typeof client === \"object\") {\n expired.add(client);\n logger.error(\n `[${label}] Client released back to the pool while still in a transaction ` +\n `(status '${txStatus}') — destroying it so the open transaction and its ` +\n `session state (RLS GUCs) cannot leak into the next request.`\n );\n } else {\n logger.error(\n `[${label}] Client released mid-transaction (status '${txStatus}') but the ` +\n `pool's internal expiry set is unavailable (pg-pool internals changed?). ` +\n `The connection may leak its open transaction into subsequent requests.`\n );\n }\n });\n}\n\n/**\n * Create a Drizzle-backed Postgres connection with a production-grade\n * connection pool.\n *\n * @param connectionString Postgres connection URL\n * @param schema Optional Drizzle schema for the relational API\n * @param poolConfig Optional pool tuning (merged over defaults)\n *\n * @returns `{ db, pool, connectionString }` — the `pool` is exposed so\n * callers can register shutdown hooks (`pool.end()`) or monitor\n * pool metrics.\n */\nexport function createPostgresDatabaseConnection(\n connectionString: string,\n schema?: Record<string, unknown>,\n poolConfig?: PostgresPoolConfig\n) {\n const opts = { ...DEFAULT_POOL,\n...poolConfig };\n connectionString = pinSearchPath(connectionString, opts.searchPath);\n\n const pgPoolConfig: PoolConfig = {\n connectionString,\n max: cappedPoolMax(opts.max),\n idleTimeoutMillis: opts.idleTimeoutMillis,\n connectionTimeoutMillis: opts.connectionTimeoutMillis,\n query_timeout: opts.queryTimeout,\n statement_timeout: opts.statementTimeout,\n keepAlive: opts.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n\n const pool = new Pool(pgPoolConfig);\n\n // ── Pool event logging ────────────────────────────────────────────────\n // Uses console.* because the structured logger lives in server\n // (a separate package). The caller can replace these with the structured\n // logger if desired via pool.on() after creation.\n pool.on(\"error\", (err) => {\n logger.error(\"[pg-pool] Unexpected pool error\", { detail: err.message });\n if (err.message.includes(\"ETIMEDOUT\")) {\n logger.warn(\"[pg-pool] Connection timeout detected — pool will auto-retry\");\n }\n });\n guardPoolAgainstDirtyRelease(pool, \"pg-pool\");\n\n // Create drizzle instance — pass schema when available to enable db.query relational API\n const db = schema ? drizzle(pool, { schema }) : drizzle(pool);\n\n return { db,\npool,\nconnectionString };\n}\n\n/**\n * Create a direct (non-pooled) connection for operations that require\n * session-level features incompatible with PgBouncer transaction mode,\n * such as LISTEN/NOTIFY, prepared statements, or advisory locks.\n *\n * Uses a smaller pool since this is only for specific use cases.\n */\nexport function createDirectDatabaseConnection(\n connectionString: string,\n schema?: Record<string, unknown>,\n poolConfig?: PostgresPoolConfig\n) {\n const opts = {\n ...DEFAULT_POOL,\n max: 5,\n ...poolConfig\n };\n connectionString = pinSearchPath(connectionString, opts.searchPath);\n\n const pgPoolConfig: PoolConfig = {\n connectionString,\n max: cappedPoolMax(opts.max),\n idleTimeoutMillis: opts.idleTimeoutMillis,\n connectionTimeoutMillis: opts.connectionTimeoutMillis,\n query_timeout: opts.queryTimeout,\n statement_timeout: opts.statementTimeout,\n keepAlive: opts.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n\n const pool = new Pool(pgPoolConfig);\n\n pool.on(\"error\", (err) => {\n logger.error(\"[pg-direct-pool] Unexpected pool error\", { detail: err.message });\n });\n guardPoolAgainstDirtyRelease(pool, \"pg-direct-pool\");\n\n const db = schema ? drizzle(pool, { schema }) : drizzle(pool);\n\n return { db,\npool,\nconnectionString };\n}\n\n/**\n * Create a read-only connection for routing read queries to replicas.\n * Uses a moderate pool size since reads are distributed across replicas.\n */\nexport function createReadReplicaConnection(\n connectionString: string,\n schema?: Record<string, unknown>,\n poolConfig?: PostgresPoolConfig\n) {\n const opts = {\n ...DEFAULT_POOL,\n max: 10,\n ...poolConfig\n };\n connectionString = pinSearchPath(connectionString, opts.searchPath);\n\n const pgPoolConfig: PoolConfig = {\n connectionString,\n max: cappedPoolMax(opts.max),\n idleTimeoutMillis: opts.idleTimeoutMillis,\n connectionTimeoutMillis: opts.connectionTimeoutMillis,\n query_timeout: opts.queryTimeout,\n statement_timeout: opts.statementTimeout,\n keepAlive: opts.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n\n const pool = new Pool(pgPoolConfig);\n\n pool.on(\"error\", (err) => {\n logger.error(\"[pg-replica-pool] Unexpected pool error\", { detail: err.message });\n });\n guardPoolAgainstDirtyRelease(pool, \"pg-replica-pool\");\n\n const db = schema ? drizzle(pool, { schema }) : drizzle(pool);\n\n return { db,\npool,\nconnectionString };\n}\n","import { Pool } from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { guardPoolAgainstDirtyRelease, pinSearchPath, cappedPoolMax } from \"./connection\";\n\nexport class DatabasePoolManager {\n private pools: Map<string, Pool> = new Map();\n private drizzleInstances: Map<string, NodePgDatabase> = new Map();\n public readonly defaultDatabaseName: string;\n private readonly rootConnectionString: string;\n\n constructor(adminConnectionString: string) {\n this.rootConnectionString = adminConnectionString;\n try {\n const url = new URL(adminConnectionString);\n this.defaultDatabaseName = url.pathname.slice(1);\n } catch (e) {\n throw new Error(`Invalid adminConnectionString provided: ${e}`);\n }\n }\n\n public getDrizzle(databaseName: string): NodePgDatabase<Record<string, never>> {\n const existing = this.drizzleInstances.get(databaseName);\n if (existing) {\n return existing;\n }\n\n const pool = this.getPool(databaseName);\n const db = drizzle(pool);\n this.drizzleInstances.set(databaseName, db);\n return db;\n }\n\n public getPool(databaseName: string): Pool {\n if (this.pools.has(databaseName)) {\n return this.pools.get(databaseName)!;\n }\n\n const url = new URL(this.rootConnectionString);\n url.pathname = `/${databaseName}`;\n\n const pool = new Pool({\n // Same pin as the primary pool: these are branch/multi-database\n // connections to the *same* server, so they inherit the same\n // `\"$user\"` hazard. See `pinSearchPath`.\n connectionString: pinSearchPath(url.toString()),\n // Capped by REBASE_DB_POOL_MAX, which the managed development\n // database sets to 1: PGlite multiplexes onto a single session and\n // overlapping transactions deadlock there.\n max: cappedPoolMax(10),\n idleTimeoutMillis: 10000, // Reduced from 30000 for aggressive cleanup\n allowExitOnIdle: true // Prevent idle clients from hanging the Node.js process\n });\n\n // Prevent idle client errors from crashing the Node.js process\n pool.on(\"error\", (err) => {\n logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });\n });\n guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);\n\n this.pools.set(databaseName, pool);\n return pool;\n }\n\n /**\n * Disconnect and remove the pool for a specific database.\n * Required before `CREATE DATABASE ... TEMPLATE` or `DROP DATABASE`,\n * which need exclusive access to the target database.\n */\n public async disconnectDatabase(databaseName: string): Promise<void> {\n const pool = this.pools.get(databaseName);\n if (pool) {\n await pool.end();\n this.pools.delete(databaseName);\n this.drizzleInstances.delete(databaseName);\n }\n }\n\n /** Check if a pool exists for a given database name. */\n public hasPool(databaseName: string): boolean {\n return this.pools.has(databaseName);\n }\n\n /**\n * Does this server serve exactly one database?\n *\n * The managed development database is PGlite, and PGlite serves exactly\n * one. `CREATE DATABASE \"rb_feature\" TEMPLATE \"postgres\"` there writes a\n * `pg_database` catalogue row and nothing else: nothing errors, the row is\n * listable, `pg_database_size` answers — and connecting to the \"branch\"\n * lands you in the parent. Every write made in the belief that it is\n * sandboxed goes to the developer's real development database.\n *\n * The CLI already refuses `rebase db branch` on this ground, but it does it\n * by parsing argv, so Studio — which reaches `BranchService` over the\n * websocket — walked straight past it. The connection is the only thing\n * both callers share, so the answer belongs here.\n *\n * Measured, not assumed: `select version()` over the PGlite socket returns\n * `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32-…`, which is how a wasm build\n * announces itself and what `dev-db/constraints.ts` documents. A real\n * Postgres never carries `(PGlite `.\n *\n * Cached, because a live connection cannot change engine underneath us, and\n * a `PostgresBackendDriver` — which builds a `BranchService` — is\n * constructed per transaction.\n */\n public servesOneDatabase(): Promise<boolean> {\n this.oneDatabaseProbe ??= this.probeOneDatabase();\n return this.oneDatabaseProbe;\n }\n\n private oneDatabaseProbe?: Promise<boolean>;\n\n private async probeOneDatabase(): Promise<boolean> {\n try {\n const result = await this.getPool(this.defaultDatabaseName)\n .query<{ version: string }>(\"SELECT version() AS version\");\n return /\\(PGlite\\b/i.test(result.rows[0]?.version ?? \"\");\n } catch (err) {\n // Fail open, and do not cache the failure. The cost of a wrong\n // \"yes\" is refusing branching on a real Postgres that momentarily\n // could not answer; the cost of a wrong \"no\" is one fake branch,\n // which is what this exists to prevent — but a server that cannot\n // run `select version()` is not one we can conclude anything about.\n this.oneDatabaseProbe = undefined;\n logger.warn(\"[DatabasePoolManager] Could not read the server version\", { error: err });\n return false;\n }\n }\n\n public async shutdown(): Promise<void> {\n const promises = [];\n for (const [dbName, pool] of this.pools.entries()) {\n logger.info(`[DatabasePoolManager] Shutting down pool for ${dbName}`);\n promises.push(pool.end());\n }\n await Promise.all(promises);\n this.pools.clear();\n this.drizzleInstances.clear();\n }\n}\n"],"mappings":"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"}