@rebasepro/server-postgres 0.24.1-canary.gcb918fc → 0.25.0-canary.g70dad01

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 (142) hide show
  1. package/dist/{BranchService-Bsn2wfaj.js → BranchService-DO_3_CXt.js} +118 -18
  2. package/dist/BranchService-DO_3_CXt.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +13 -0
  4. package/dist/PostgresBootstrapper.d.ts +10 -0
  5. package/dist/backup/backup-cli.d.ts +120 -2
  6. package/dist/backup/backup-service.d.ts +74 -1
  7. package/dist/backup/pg-binary.d.ts +2 -2
  8. package/dist/backup/pg-tools.d.ts +65 -0
  9. package/dist/backup-cli-Ap2WX2uW.js +647 -0
  10. package/dist/backup-cli-Ap2WX2uW.js.map +1 -0
  11. package/dist/{backup-service-BTBB7RXn.js → backup-service-DytMimsL.js} +414 -24
  12. package/dist/backup-service-DytMimsL.js.map +1 -0
  13. package/dist/{boot-ddl-DVpsFN6r.js → boot-ddl-D1lMCZLd.js} +3 -3
  14. package/dist/{boot-ddl-DVpsFN6r.js.map → boot-ddl-D1lMCZLd.js.map} +1 -1
  15. package/dist/branch-argv.d.ts +18 -6
  16. package/dist/branch-prune.d.ts +18 -0
  17. package/dist/cli-console-CQGsBzSY.js +262 -0
  18. package/dist/cli-console-CQGsBzSY.js.map +1 -0
  19. package/dist/cli-console.d.ts +11 -0
  20. package/dist/{cli-errors-CaI4ntyD.js → cli-errors-CJTKAKax.js} +499 -107
  21. package/dist/cli-errors-CJTKAKax.js.map +1 -0
  22. package/dist/cli-errors.d.ts +42 -5
  23. package/dist/{cli-flags-JNI-pwSf.js → cli-flags-D-b0qL9T.js} +71 -6
  24. package/dist/cli-flags-D-b0qL9T.js.map +1 -0
  25. package/dist/cli-flags.d.ts +21 -0
  26. package/dist/cli-helpers.d.ts +53 -24
  27. package/dist/{arg-Dni7MzLB.js → cli-prompt-egtNKkZl.js} +49 -3
  28. package/dist/cli-prompt-egtNKkZl.js.map +1 -0
  29. package/dist/cli-prompt.d.ts +19 -0
  30. package/dist/cli-scratch-database.d.ts +47 -8
  31. package/dist/cli.js +615 -100
  32. package/dist/cli.js.map +1 -1
  33. package/dist/collections/validate-relations.d.ts +8 -2
  34. package/dist/{column-plan-helpers-drZIsSNf.js → column-plan-helpers-D_V4oaZh.js} +2 -2
  35. package/dist/{column-plan-helpers-drZIsSNf.js.map → column-plan-helpers-D_V4oaZh.js.map} +1 -1
  36. package/dist/{connection-C9mT8bgf.js → connection-C938nR3E.js} +2 -2
  37. package/dist/{connection-C9mT8bgf.js.map → connection-C938nR3E.js.map} +1 -1
  38. package/dist/{databasePoolManager-BIuewidb.js → databasePoolManager-Yqdw14wY.js} +4 -4
  39. package/dist/{databasePoolManager-BIuewidb.js.map → databasePoolManager-Yqdw14wY.js.map} +1 -1
  40. package/dist/{deferred-index-build-DP1_SNFo.js → deferred-index-build-BiTqxqdl.js} +8 -8
  41. package/dist/{deferred-index-build-DP1_SNFo.js.map → deferred-index-build-BiTqxqdl.js.map} +1 -1
  42. package/dist/{doctor-C4WiftHm.js → doctor-N3J88BN_.js} +8 -8
  43. package/dist/doctor-N3J88BN_.js.map +1 -0
  44. package/dist/{ensure-collection-policies-CXpbeNag.js → ensure-collection-policies-CVP6kIPT.js} +125 -18
  45. package/dist/ensure-collection-policies-CVP6kIPT.js.map +1 -0
  46. package/dist/{ensure-collection-tables-BhsU6qUE.js → ensure-collection-tables-1WVNALOW.js} +10 -10
  47. package/dist/{ensure-collection-tables-BhsU6qUE.js.map → ensure-collection-tables-1WVNALOW.js.map} +1 -1
  48. package/dist/{ensure-tables-CVm_yvzb.js → ensure-tables-D-Nm2k3S.js} +5 -5
  49. package/dist/{ensure-tables-CVm_yvzb.js.map → ensure-tables-D-Nm2k3S.js.map} +1 -1
  50. package/dist/{enum-labels-kept-D8fI1MoX.js → enum-labels-kept-e_93v2Ic.js} +3 -3
  51. package/dist/{enum-labels-kept-D8fI1MoX.js.map → enum-labels-kept-e_93v2Ic.js.map} +1 -1
  52. package/dist/{generate-drizzle-schema-Bz52jH2h.js → generate-drizzle-schema-C3EAP9HE.js} +3 -3
  53. package/dist/{generate-drizzle-schema-Bz52jH2h.js.map → generate-drizzle-schema-C3EAP9HE.js.map} +1 -1
  54. package/dist/{generate-drizzle-schema-logic-B74WN37S.js → generate-drizzle-schema-logic-DA3zM8zc.js} +4 -4
  55. package/dist/{generate-drizzle-schema-logic-B74WN37S.js.map → generate-drizzle-schema-logic-DA3zM8zc.js.map} +1 -1
  56. package/dist/{generate-files-CdEckX6c.js → generate-files-ClmOUHXj.js} +6 -47
  57. package/dist/generate-files-ClmOUHXj.js.map +1 -0
  58. package/dist/generate-next-step-DCeS4LrJ.js +74 -0
  59. package/dist/generate-next-step-DCeS4LrJ.js.map +1 -0
  60. package/dist/{generate-postgres-ddl-logic-B64O2oZF.js → generate-postgres-ddl-logic-4eUP4t8w.js} +4 -4
  61. package/dist/{generate-postgres-ddl-logic-B64O2oZF.js.map → generate-postgres-ddl-logic-4eUP4t8w.js.map} +1 -1
  62. package/dist/{generated-schema-staleness-Di9c4ZxN.js → generated-schema-staleness-FlxM2i4i.js} +102 -12
  63. package/dist/{generated-schema-staleness-Di9c4ZxN.js.map → generated-schema-staleness-FlxM2i4i.js.map} +1 -1
  64. package/dist/index.es.js +11 -11
  65. package/dist/{introspect-db-logic-C-Di8dA9.js → introspect-db-logic-CI5CU_n0.js} +145 -9
  66. package/dist/introspect-db-logic-CI5CU_n0.js.map +1 -0
  67. package/dist/{introspect-db-queries-D6kAqReI.js → introspect-db-queries-DevUrzll.js} +41 -6
  68. package/dist/introspect-db-queries-DevUrzll.js.map +1 -0
  69. package/dist/{open-rebase-CbOur0TU.js → open-rebase-Cwnqo5LU.js} +116 -51
  70. package/dist/open-rebase-Cwnqo5LU.js.map +1 -0
  71. package/dist/open-rebase.d.ts +12 -4
  72. package/dist/{pg-error-utils-vJezlXdI.js → pg-error-utils-Cmo8wZPy.js} +19 -5
  73. package/dist/pg-error-utils-Cmo8wZPy.js.map +1 -0
  74. package/dist/{plan-schema-B1YF_tYq.js → plan-schema-CXOucCw-.js} +4 -4
  75. package/dist/{plan-schema-B1YF_tYq.js.map → plan-schema-CXOucCw-.js.map} +1 -1
  76. package/dist/{policy-drift-Dm8i-s1J.js → policy-drift-vm0BcvcE.js} +28 -14
  77. package/dist/{policy-drift-Dm8i-s1J.js.map → policy-drift-vm0BcvcE.js.map} +1 -1
  78. package/dist/{push-index-prebuild-Db4AQF4i.js → push-index-prebuild-SXRlWBjc.js} +4 -4
  79. package/dist/{push-index-prebuild-Db4AQF4i.js.map → push-index-prebuild-SXRlWBjc.js.map} +1 -1
  80. package/dist/{push-legacy-renames-BK6ilQq2.js → push-legacy-renames-B37yKDac.js} +4 -4
  81. package/dist/{push-legacy-renames-BK6ilQq2.js.map → push-legacy-renames-B37yKDac.js.map} +1 -1
  82. package/dist/push-policy-plan-jXzHAWSu.js +127 -0
  83. package/dist/push-policy-plan-jXzHAWSu.js.map +1 -0
  84. package/dist/{render-ddl-CtC3y9Ol.js → render-ddl-CQNwZdse.js} +10 -9
  85. package/dist/render-ddl-CQNwZdse.js.map +1 -0
  86. package/dist/{rls-bootstrap-sql-RQUljnJ1.js → rls-bootstrap-sql-Bd-hJk08.js} +3 -58
  87. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +1 -0
  88. package/dist/{rls-enforcement-DvzqlaL1.js → rls-enforcement-D7P4-ZYX.js} +2 -2
  89. package/dist/{rls-enforcement-DvzqlaL1.js.map → rls-enforcement-D7P4-ZYX.js.map} +1 -1
  90. package/dist/{__vite-browser-external-BnuHet1e.js → rolldown-runtime-V7rFbMRe.js} +1 -8
  91. package/dist/schema/atlas-apply-report.d.ts +23 -0
  92. package/dist/schema/atlas-argv.d.ts +24 -1
  93. package/dist/schema/atlas-exclude.d.ts +42 -0
  94. package/dist/schema/atlas-revisions.d.ts +4 -0
  95. package/dist/schema/carved-out-migration.d.ts +5 -4
  96. package/dist/schema/destructive-sql.d.ts +23 -1
  97. package/dist/schema/doctor-cli.js +5 -5
  98. package/dist/schema/ensure-collection-policies.d.ts +74 -0
  99. package/dist/schema/generate-drizzle-schema.js +1 -1
  100. package/dist/schema/generate-next-step.d.ts +23 -0
  101. package/dist/schema/generate-postgres-ddl.js +2 -2
  102. package/dist/schema/generated-schema-diff.d.ts +7 -0
  103. package/dist/schema/generated-schema-staleness.d.ts +7 -1
  104. package/dist/schema/introspect-db-logic.d.ts +32 -1
  105. package/dist/schema/introspect-db-queries.d.ts +18 -5
  106. package/dist/schema/introspect-db.js +55 -53
  107. package/dist/schema/introspect-db.js.map +1 -1
  108. package/dist/schema/migrate-baseline.d.ts +51 -0
  109. package/dist/schema/plan/render-ddl.d.ts +2 -1
  110. package/dist/schema/push-policy-plan.d.ts +70 -0
  111. package/dist/schema/undeclarable-constraints.d.ts +59 -0
  112. package/dist/security/policy-drift.d.ts +5 -0
  113. package/dist/services/BranchService.d.ts +38 -0
  114. package/dist/{source-DBDbBTL4.js → source-Bv5KPeIx.js} +109 -4
  115. package/dist/source-Bv5KPeIx.js.map +1 -0
  116. package/dist/utils/connection-string.d.ts +47 -0
  117. package/dist/{vector-index-CMJKb3C9.js → vector-index-BFhyEaiz.js} +2 -2
  118. package/dist/{vector-index-CMJKb3C9.js.map → vector-index-BFhyEaiz.js.map} +1 -1
  119. package/dist/{websocket-BLGc5yhs.js → websocket-BT85Yv_G.js} +41 -13
  120. package/dist/websocket-BT85Yv_G.js.map +1 -0
  121. package/package.json +7 -7
  122. package/dist/BranchService-Bsn2wfaj.js.map +0 -1
  123. package/dist/__vite-browser-external-BnuHet1e.js.map +0 -1
  124. package/dist/arg-Dni7MzLB.js.map +0 -1
  125. package/dist/backup-cli-DnpJi6I7.js +0 -420
  126. package/dist/backup-cli-DnpJi6I7.js.map +0 -1
  127. package/dist/backup-service-BTBB7RXn.js.map +0 -1
  128. package/dist/cli-console-Ca6ZlAVU.js +0 -154
  129. package/dist/cli-console-Ca6ZlAVU.js.map +0 -1
  130. package/dist/cli-errors-CaI4ntyD.js.map +0 -1
  131. package/dist/cli-flags-JNI-pwSf.js.map +0 -1
  132. package/dist/doctor-C4WiftHm.js.map +0 -1
  133. package/dist/ensure-collection-policies-CXpbeNag.js.map +0 -1
  134. package/dist/generate-files-CdEckX6c.js.map +0 -1
  135. package/dist/introspect-db-logic-C-Di8dA9.js.map +0 -1
  136. package/dist/introspect-db-queries-D6kAqReI.js.map +0 -1
  137. package/dist/open-rebase-CbOur0TU.js.map +0 -1
  138. package/dist/pg-error-utils-vJezlXdI.js.map +0 -1
  139. package/dist/render-ddl-CtC3y9Ol.js.map +0 -1
  140. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +0 -1
  141. package/dist/source-DBDbBTL4.js.map +0 -1
  142. package/dist/websocket-BLGc5yhs.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
- import { K as extractCauseMessage } from "./cli-errors-CaI4ntyD.js";
5
- import { n as extractPgError } from "./pg-error-utils-vJezlXdI.js";
3
+ import { n as __exportAll } from "./rolldown-runtime-V7rFbMRe.js";
4
+ import { tt as extractCauseMessage } from "./cli-errors-CJTKAKax.js";
5
+ import { n as extractPgError } from "./pg-error-utils-Cmo8wZPy.js";
6
6
  import { sql } from "drizzle-orm";
7
7
  import { revokeInternalTableSql } from "@rebasepro/common";
8
8
  //#region src/services/BranchService.ts
@@ -24,6 +24,10 @@ var BRANCH_DB_PREFIX = "rb_";
24
24
  var PG_DUPLICATE_DATABASE = "42P04";
25
25
  /** `object_in_use` — the database still has connections attached. */
26
26
  var PG_OBJECT_IN_USE = "55006";
27
+ /** How many times `--force` disconnects the other sessions before it reports one still there. */
28
+ var FORCE_ATTEMPTS = 3;
29
+ /** The database every Postgres server has, from which another one can be closed and opened. */
30
+ var MAINTENANCE_DATABASE = "postgres";
27
31
  /**
28
32
  * Describe a failed branch DDL statement in terms a user can act on.
29
33
  *
@@ -138,6 +142,16 @@ var BranchService = class {
138
142
  if (await this.poolManager.servesOneDatabase()) throw new BranchingUnsupportedError(BRANCHING_UNSUPPORTED_MESSAGE);
139
143
  }
140
144
  /**
145
+ * Whether the `rebase.branches` registry exists. A database that has never
146
+ * had a branch made on it may not have it, and reading it there is an
147
+ * empty answer, not an error — nor a reason to create it. The CLI's `list`
148
+ * and `info` ask this rather than running the DDL that creates it.
149
+ */
150
+ async hasBranchRegistry() {
151
+ const [row] = (await (await this.home.registry()).execute(sql`SELECT to_regclass('rebase.branches') IS NOT NULL AS present`)).rows;
152
+ return row !== void 0 && Reflect.get(row, "present") === true;
153
+ }
154
+ /**
141
155
  * Ensure the `rebase.branches` metadata table exists in the default database.
142
156
  * Idempotent — safe to call on every startup.
143
157
  */
@@ -171,15 +185,16 @@ var BranchService = class {
171
185
  const sourceDb = options?.source || await this.home.database();
172
186
  if ((await (await this.home.registry()).execute(sql`SELECT name FROM rebase.branches WHERE name = ${name} OR db_name = ${dbName}`)).rows.length > 0) throw new Error(`Branch "${name}" already exists.`);
173
187
  await this.poolManager.disconnectDatabase(sourceDb);
174
- if (options?.force) {
175
- if (await this.terminateConnections(sourceDb) > 0) await new Promise((resolve) => setTimeout(resolve, 250));
176
- }
177
188
  const safeDbName = dbName.replace(/"/g, "\"\"");
178
189
  const safeSourceDb = sourceDb.replace(/"/g, "\"\"");
179
- try {
190
+ const copy = async () => {
180
191
  await this.db.execute(sql.raw(`CREATE DATABASE "${safeDbName}" TEMPLATE "${safeSourceDb}"`));
192
+ };
193
+ try {
194
+ if (options?.force) await this.whileClosedToNewConnections(sourceDb, () => this.disconnectingOthers(sourceDb, copy));
195
+ else await copy();
181
196
  } catch (err) {
182
- if (extractPgError(err)?.code === PG_OBJECT_IN_USE) throw new Error(`Cannot create branch: the source database "${sourceDb}" has active connections.\n` + await this.describeBlockingConnections(sourceDb));
197
+ if (extractPgError(err)?.code === PG_OBJECT_IN_USE) throw new Error(`Cannot create branch: the source database "${sourceDb}" ${options?.force ? "still had connections after they were disconnected" : "has active connections"}.\n` + await this.describeBlockingConnections(sourceDb, { forced: options?.force === true }));
183
198
  throw describeBranchDdlError(err, dbName);
184
199
  }
185
200
  const now = /* @__PURE__ */ new Date();
@@ -206,14 +221,15 @@ var BranchService = class {
206
221
  const dbName = existingRows[0].db_name;
207
222
  if (dbName === mainDatabase) throw new Error("Cannot delete the main database.");
208
223
  await this.poolManager.disconnectDatabase(dbName);
209
- if (options?.force) {
210
- if (await this.terminateConnections(dbName) > 0) await new Promise((resolve) => setTimeout(resolve, 250));
211
- }
212
224
  const safeDbName = dbName.replace(/"/g, "\"\"");
213
- try {
225
+ const drop = async () => {
214
226
  await this.db.execute(sql.raw(`DROP DATABASE "${safeDbName}"`));
227
+ };
228
+ try {
229
+ if (options?.force) await this.disconnectingOthers(dbName, drop);
230
+ else await drop();
215
231
  } catch (err) {
216
- if (extractPgError(err)?.code === PG_OBJECT_IN_USE) throw new Error(`Cannot delete branch "${name}": the database has active connections.\n` + await this.describeBlockingConnections(dbName));
232
+ if (extractPgError(err)?.code === PG_OBJECT_IN_USE) throw new Error(`Cannot delete branch "${name}": the database ${options?.force ? "still had connections after they were disconnected" : "has active connections"}.\n` + await this.describeBlockingConnections(dbName, { forced: options?.force === true }));
217
233
  throw describeBranchDdlError(err, dbName);
218
234
  }
219
235
  await (await this.home.registry()).execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);
@@ -237,7 +253,7 @@ var BranchService = class {
237
253
  * Best-effort by construction: this runs while reporting a failure, and a
238
254
  * diagnostic that throws would replace a real error with its own.
239
255
  */
240
- async describeBlockingConnections(dbName) {
256
+ async describeBlockingConnections(dbName, options = {}) {
241
257
  try {
242
258
  const rows = (await this.db.execute(sql`
243
259
  SELECT coalesce(nullif(application_name, ''), '(unnamed)') AS app,
@@ -249,10 +265,94 @@ var BranchService = class {
249
265
  ORDER BY 2 DESC, 1
250
266
  `)).rows;
251
267
  if (rows.length === 0) return " Retry — whatever was connected has since disconnected.";
252
- return ` Connected right now:\n${rows.map((row) => ` ${row.n} × ${String(row.app)}`).join("\n")}\n A running \`rebase dev\` is the usual one — stop it, or re-run with --force to
253
- disconnect them for you.`;
268
+ return ` Connected right now:\n${rows.map((row) => ` ${row.n} × ${String(row.app)}`).join("\n")}\n` + (options.forced ? " They reconnected as fast as they were disconnected. A running `rebase dev` is the\n usual one — stop it, then run this again." : " A running `rebase dev` is the usual one — stop it, or re-run with --force to\n disconnect them for you.");
254
269
  } catch {
255
- return " Close other clients and connections, or re-run with --force.";
270
+ return options.forced ? " Stop the clients connected to it — a running `rebase dev` is the usual one — then run this again." : " Close other clients and connections, or re-run with --force.";
271
+ }
272
+ }
273
+ /**
274
+ * Run `operation` with no other session on `dbName`, disconnecting them
275
+ * first: what `--force` asks for.
276
+ *
277
+ * Disconnected, a client that reconnects — a running `rebase dev`'s
278
+ * realtime listener does — can be back before `CREATE DATABASE … TEMPLATE`
279
+ * or `DROP DATABASE` counts the sessions, and it failed on one about once
280
+ * in seven tries, saying to pass the `--force` that had been passed. So a
281
+ * refusal on a session still connected is retried, disconnecting again, a
282
+ * few times before it is reported.
283
+ */
284
+ async disconnectingOthers(dbName, operation) {
285
+ for (let attempt = 1;; attempt++) {
286
+ if (await this.terminateConnections(dbName) > 0) await new Promise((resolve) => setTimeout(resolve, 250));
287
+ try {
288
+ await operation();
289
+ return;
290
+ } catch (err) {
291
+ if (attempt >= FORCE_ATTEMPTS || extractPgError(err)?.code !== PG_OBJECT_IN_USE) throw err;
292
+ }
293
+ }
294
+ }
295
+ /**
296
+ * Run `operation` with `dbName` refusing new connections, and open it
297
+ * again after, whatever happens.
298
+ *
299
+ * A client disconnected from the source — a running `rebase dev` holds a
300
+ * pool and a realtime listener — cannot then reconnect before the copy has
301
+ * counted the sessions on it. Postgres refuses `ALLOW_CONNECTIONS false` on
302
+ * the database a session is connected to, so both statements run from the
303
+ * server's `postgres` database, and they need the database's owner. Where
304
+ * either is missing, the retries of {@link disconnectingOthers} are what is
305
+ * left. A database that already refused connections is left as it was.
306
+ *
307
+ * Interrupted (Ctrl-C) while closed, it opens the database again before
308
+ * the process exits; one it could not open is named in the error, with the
309
+ * statement that does.
310
+ */
311
+ async whileClosedToNewConnections(dbName, operation) {
312
+ const safe = dbName.replace(/"/g, "\"\"");
313
+ const maintenance = dbName === MAINTENANCE_DATABASE ? null : this.poolManager.getDrizzle(MAINTENANCE_DATABASE);
314
+ let closed = false;
315
+ try {
316
+ if (maintenance) {
317
+ const [row] = (await maintenance.execute(sql`SELECT datallowconn FROM pg_database WHERE datname = ${dbName}`)).rows;
318
+ if (row?.datallowconn === true) {
319
+ await maintenance.execute(sql.raw(`ALTER DATABASE "${safe}" ALLOW_CONNECTIONS false`));
320
+ closed = true;
321
+ }
322
+ }
323
+ } catch {}
324
+ if (!closed) {
325
+ await this.poolManager.disconnectDatabase(MAINTENANCE_DATABASE);
326
+ await operation();
327
+ return;
328
+ }
329
+ let reopening;
330
+ const reopen = () => {
331
+ reopening ??= this.reopenToConnections(dbName);
332
+ return reopening;
333
+ };
334
+ const onSignal = (signal) => {
335
+ reopen().finally(() => process.kill(process.pid, signal));
336
+ };
337
+ process.once("SIGINT", onSignal);
338
+ process.once("SIGTERM", onSignal);
339
+ try {
340
+ await operation();
341
+ } finally {
342
+ process.removeListener("SIGINT", onSignal);
343
+ process.removeListener("SIGTERM", onSignal);
344
+ await reopen();
345
+ }
346
+ }
347
+ /** `ALLOW_CONNECTIONS true` again, from the maintenance database, or an error that says how to do it by hand. */
348
+ async reopenToConnections(dbName) {
349
+ const statement = `ALTER DATABASE "${dbName.replace(/"/g, "\"\"")}" ALLOW_CONNECTIONS true`;
350
+ try {
351
+ await this.poolManager.getDrizzle(MAINTENANCE_DATABASE).execute(sql.raw(statement));
352
+ } catch (err) {
353
+ throw new Error(`"${dbName}" was closed to new connections for the copy and could not be opened again (${extractPgError(err)?.message ?? extractCauseMessage(err) ?? String(err)}). Nothing can connect to it until you run, from another database: ${statement};`);
354
+ } finally {
355
+ await this.poolManager.disconnectDatabase(MAINTENANCE_DATABASE);
256
356
  }
257
357
  }
258
358
  /**
@@ -375,4 +475,4 @@ var BranchService = class {
375
475
  //#endregion
376
476
  export { BranchService_exports as n, BranchingUnsupportedError as r, BranchService as t };
377
477
 
378
- //# sourceMappingURL=BranchService-Bsn2wfaj.js.map
478
+ //# sourceMappingURL=BranchService-DO_3_CXt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BranchService-DO_3_CXt.js","names":[],"sources":["../src/services/BranchService.ts"],"sourcesContent":["/**\n * BranchService\n *\n * Manages database branching by creating/deleting PostgreSQL databases\n * using `CREATE DATABASE ... TEMPLATE`. Branch metadata is stored in the\n * `rebase.branches` table in the default (main) database, following the\n * same `rebase` schema convention used by entity_history, auth, etc.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { BranchInfo } from \"@rebasepro/types\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { DrizzleClient } from \"../interfaces\";\nimport { BranchRow } from \"../branch-prune\";\nimport { DatabasePoolManager } from \"../databasePoolManager\";\nimport { extractPgError, extractCauseMessage } from \"../utils/pg-error-utils\";\n\n/** Internal prefix applied to branch database names to avoid collisions. */\nconst BRANCH_DB_PREFIX = \"rb_\";\n\n/** `duplicate_database` — the target database name is already taken. */\nconst PG_DUPLICATE_DATABASE = \"42P04\";\n\n/** `object_in_use` — the database still has connections attached. */\nconst PG_OBJECT_IN_USE = \"55006\";\n\n/** How many times `--force` disconnects the other sessions before it reports one still there. */\nconst FORCE_ATTEMPTS = 3;\n\n/** The database every Postgres server has, from which another one can be closed and opened. */\nconst MAINTENANCE_DATABASE = \"postgres\";\n\n/**\n * Describe a failed branch DDL statement in terms a user can act on.\n *\n * Drizzle reports failures as `Failed query: <sql> params:` and hides the real\n * PostgreSQL error in the `cause` chain, so matching on `err.message` never sees\n * the actual problem. Match on the PG error code instead — it survives wrapping\n * and, unlike the message text, is not locale-dependent.\n */\nfunction describeBranchDdlError(err: unknown, fallbackContext: string): Error {\n const pgError = extractPgError(err);\n\n if (pgError?.code === PG_DUPLICATE_DATABASE) {\n return new Error(`Database \"${fallbackContext}\" already exists on the server. Choose a different branch name.`);\n }\n if (pgError?.code === PG_OBJECT_IN_USE) {\n return new Error(\n `Cannot complete the operation: the database \"${fallbackContext}\" has active connections. ` +\n \"Close other clients or connections and try again.\"\n );\n }\n\n // Unknown failure: surface the real PG message rather than the Drizzle\n // wrapper, which would otherwise show the raw SQL and no reason at all.\n const detail = pgError?.message ?? extractCauseMessage(err);\n if (detail) return new Error(detail);\n return err instanceof Error ? err : new Error(String(err));\n}\n\n/** Fully-qualified metadata table in the rebase schema. */\nconst BRANCHES_TABLE = \"rebase.branches\";\n\n/**\n * The connected server cannot branch, so nothing was done.\n *\n * A refusal, not a fault: typed so the transports can forward it intact.\n * `websocket.ts` answers it with its own code and message rather than letting\n * it fall into the generic handler, which suppresses the text in production —\n * and the text is the only part that says what to do instead.\n */\nexport class BranchingUnsupportedError extends Error {\n /** Wire code, forwarded verbatim to the client. */\n readonly code = \"BRANCHING_UNSUPPORTED\";\n\n constructor(message: string) {\n super(message);\n this.name = \"BranchingUnsupportedError\";\n }\n}\n\n/**\n * What to say when the server serves one database.\n *\n * Deliberately the same argument the CLI's `refuseBranchOnManagedDatabase`\n * makes, because it is the same refusal — it just used to be available only to\n * whoever came in through argv. One paragraph, because this one has to fit in a\n * toast as well as a terminal.\n */\nconst BRANCHING_UNSUPPORTED_MESSAGE =\n \"Branching does not work on the managed development database. It copies a database with \"\n + \"CREATE DATABASE ... TEMPLATE, and the managed database is PGlite, which serves exactly one — \"\n + \"the copy would be the original, and every write you meant to sandbox would land in it. \"\n + \"Branching needs a real Postgres: run `rebase dev --docker`, or uncomment DATABASE_URL in .env \"\n + \"to point at your own.\";\n\n/**\n * Validate that a user-provided identifier only contains safe characters.\n * Throws if the value contains characters outside [a-zA-Z0-9_-].\n */\nfunction validateIdentifier(value: string, label: string): void {\n if (!/^[a-zA-Z0-9_-]+$/.test(value)) {\n throw new Error(`Invalid ${label}: only letters, digits, underscores, and hyphens are allowed.`);\n }\n}\n\n/**\n * Postgres truncates identifiers at NAMEDATALEN-1 = 63 bytes, and does it\n * silently. The prefix comes out of the same budget.\n */\nconst MAX_BRANCH_NAME_LENGTH = 63 - BRANCH_DB_PREFIX.length;\n\n/**\n * Check a user-provided branch name, and otherwise leave it exactly as given.\n *\n * This used to strip everything outside [a-zA-Z0-9_], so `my-feature` was\n * quietly created as `myfeature`: the name you typed was not the name `list`\n * gave back. Nothing ever needed that. Every identifier this service builds is\n * double-quoted (see `CREATE DATABASE` below), which is what makes a hyphen\n * safe, and `validateIdentifier` has always accepted hyphens for the `--from`\n * source database — the two disagreed about the same character class.\n *\n * Refusing a name we cannot represent is better than representing a different\n * one, which is also why the length is checked here rather than left to\n * Postgres, whose answer to an over-long identifier is a silent rename.\n */\nfunction assertValidBranchName(name: string): void {\n validateIdentifier(name, \"branch name\");\n if (name.length > MAX_BRANCH_NAME_LENGTH) {\n throw new Error(\n `Branch name \"${name}\" is too long: ${name.length} characters, maximum ${MAX_BRANCH_NAME_LENGTH}. ` +\n \"Postgres truncates identifiers past 63 bytes, which would give the branch a name you did not choose.\"\n );\n }\n}\n\n/**\n * Convert a user-facing branch name to the actual PostgreSQL database name.\n */\nfunction toBranchDbName(name: string): string {\n assertValidBranchName(name);\n return `${BRANCH_DB_PREFIX}${name}`;\n}\n\n/**\n * Where a server's branches are kept: the main database — what a branch is\n * copied from unless told otherwise, and what may never be dropped — and the\n * connection whose `rebase.branches` records them.\n */\nexport interface BranchHome {\n /** The main database's name. */\n database(): Promise<string>;\n /**\n * A connection on the database the registry is in. Asked for at each use:\n * a pool to the main database is closed before it is copied.\n */\n registry(): Promise<DrizzleClient>;\n}\n\nexport class BranchService {\n private readonly home: BranchHome;\n\n /**\n * @param db A connection on the server the branches are made on. Branch\n * DDL runs here.\n * @param home Where branches are kept. Without one, the database the\n * pool manager's connection string names, and `db`'s registry.\n */\n constructor(\n private db: DrizzleClient,\n private poolManager: DatabasePoolManager,\n home?: BranchHome\n ) {\n this.home = home ?? {\n database: async () => poolManager.defaultDatabaseName,\n registry: async () => db\n };\n }\n\n /**\n * Refuse a branch mutation the connected server cannot honour.\n *\n * The check lives here rather than in the CLI's argv parsing because the\n * CLI is not the only caller: Studio reaches `createBranch` over the\n * websocket, and before this it got \"Branch created successfully\" and a\n * detail pane promising isolation for a database that is the parent.\n *\n * Reads only, and only mutations are guarded: `listBranches` on a managed\n * database is an honest empty list, and refusing it would turn the Branches\n * pane into a failure with a \"Try again\" that can never succeed.\n */\n private async assertBranchingSupported(): Promise<void> {\n if (await this.poolManager.servesOneDatabase()) {\n throw new BranchingUnsupportedError(BRANCHING_UNSUPPORTED_MESSAGE);\n }\n }\n\n /**\n * Whether the `rebase.branches` registry exists. A database that has never\n * had a branch made on it may not have it, and reading it there is an\n * empty answer, not an error — nor a reason to create it. The CLI's `list`\n * and `info` ask this rather than running the DDL that creates it.\n */\n async hasBranchRegistry(): Promise<boolean> {\n const result = await (await this.home.registry()).execute(\n sql`SELECT to_regclass('rebase.branches') IS NOT NULL AS present`\n );\n const [row] = result.rows;\n return row !== undefined && Reflect.get(row, \"present\") === true;\n }\n\n /**\n * Ensure the `rebase.branches` metadata table exists in the default database.\n * Idempotent — safe to call on every startup.\n */\n async ensureBranchMetadataTable(): Promise<void> {\n // Create the rebase schema (idempotent — may already exist from auth/history init)\n const registry = await this.home.registry();\n await registry.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);\n\n await registry.execute(sql.raw(`\n CREATE TABLE IF NOT EXISTS ${BRANCHES_TABLE} (\n name TEXT PRIMARY KEY,\n db_name TEXT NOT NULL UNIQUE,\n parent_db TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n metadata JSONB DEFAULT '{}'\n );\n `));\n\n // Not a collection, so no RLS — and it names every branch database on\n // this server. The driver's schema-wide grant reaches it, so revoke.\n await registry.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"branches\")));\n }\n\n /**\n * Create a new branch database by templating the source database.\n *\n * Uses `CREATE DATABASE ... TEMPLATE` for an instant, full-fidelity copy\n * of both schema and data.\n *\n * @param name User-facing branch name (e.g., \"feature_auth\")\n * @param options.source Source database to clone; defaults to the main database.\n */\n async createBranch(\n name: string,\n options?: { source?: string; force?: boolean }\n ): Promise<BranchInfo> {\n await this.assertBranchingSupported();\n\n if (options?.source) {\n validateIdentifier(options.source, \"source database name\");\n }\n\n const dbName = toBranchDbName(name);\n const sourceDb = options?.source || await this.home.database();\n\n // Check if branch already exists\n const existing = await (await this.home.registry()).execute(\n sql`SELECT name FROM rebase.branches WHERE name = ${name} OR db_name = ${dbName}`\n );\n if ((existing.rows as unknown[]).length > 0) {\n throw new Error(`Branch \"${name}\" already exists.`);\n }\n\n // Disconnect any idle pools to the source DB so TEMPLATE works.\n // CREATE DATABASE ... TEMPLATE requires no other connections to the template.\n //\n // This only reaches pools inside *this* process. The CLI runs as its own\n // process, so a `rebase dev` holding the database open is untouched by\n // it — which is why the failure below has to name what is connected, and\n // why `--force` exists.\n await this.poolManager.disconnectDatabase(sourceDb);\n\n // Create the database using the source as a template.\n // Note: Identifiers must be double-quoted, not parameterized.\n const safeDbName = dbName.replace(/\"/g, '\"\"');\n const safeSourceDb = sourceDb.replace(/\"/g, '\"\"');\n const copy = async (): Promise<void> => {\n await this.db.execute(sql.raw(`CREATE DATABASE \"${safeDbName}\" TEMPLATE \"${safeSourceDb}\"`));\n };\n try {\n if (options?.force) {\n await this.whileClosedToNewConnections(sourceDb, () => this.disconnectingOthers(sourceDb, copy));\n } else {\n await copy();\n }\n } catch (err) {\n const pgError = extractPgError(err);\n if (pgError?.code === PG_OBJECT_IN_USE) {\n // The template — not the new database — is the one still in use.\n throw new Error(\n `Cannot create branch: the source database \"${sourceDb}\" ${options?.force\n ? \"still had connections after they were disconnected\"\n : \"has active connections\"}.\\n`\n + await this.describeBlockingConnections(sourceDb, { forced: options?.force === true })\n );\n }\n throw describeBranchDdlError(err, dbName);\n }\n\n // Record metadata in the registry — asked for again, as copying the\n // main database closed any pool on it.\n const now = new Date();\n await (await this.home.registry()).execute(\n sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)\n VALUES (${name}, ${dbName}, ${sourceDb}, ${now.toISOString()})`\n );\n\n return {\n name,\n database: dbName,\n parentDatabase: sourceDb,\n createdAt: now\n };\n }\n\n /**\n * Delete a branch database and remove its metadata.\n * Cannot delete the main/default database.\n */\n async deleteBranch(name: string, options?: { force?: boolean }): Promise<void> {\n await this.assertBranchingSupported();\n\n assertValidBranchName(name);\n\n // Safety, first pass: a request that would target the default database\n // is refused before any metadata is read, so the answer costs nothing\n // and cannot depend on what a row happens to say.\n const mainDatabase = await this.home.database();\n if (toBranchDbName(name) === mainDatabase) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n // Verify the branch exists, and take the database name from the row\n // rather than deriving it again. A branch created before names stopped\n // being stripped is recorded as `rb_myfeature` while `my-feature` now\n // derives `rb_my-feature`; re-deriving would drop a database this row\n // never named — or, if that name happened to exist, somebody else's.\n // The stored value is the only one that is true by construction.\n const existing = await (await this.home.registry()).execute(\n sql`SELECT db_name FROM rebase.branches WHERE name = ${name}`\n );\n const existingRows = existing.rows as Record<string, unknown>[];\n if (existingRows.length === 0) {\n throw new Error(`Branch \"${name}\" not found.`);\n }\n const dbName = existingRows[0].db_name as string;\n\n // Safety, second pass: the first checked a name we derived, this checks\n // the one we are about to drop. They differ for any row written under\n // the old scheme, and it is this one that governs.\n if (dbName === mainDatabase) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n // Disconnect any pools to this branch before dropping. Same limit as\n // `createBranch`: in-process pools only.\n await this.poolManager.disconnectDatabase(dbName);\n\n // Drop the database\n const safeDbName = dbName.replace(/\"/g, '\"\"');\n const drop = async (): Promise<void> => {\n await this.db.execute(sql.raw(`DROP DATABASE \"${safeDbName}\"`));\n };\n try {\n if (options?.force) await this.disconnectingOthers(dbName, drop);\n else await drop();\n } catch (err) {\n const pgError = extractPgError(err);\n if (pgError?.code === PG_OBJECT_IN_USE) {\n throw new Error(\n `Cannot delete branch \"${name}\": the database ${options?.force\n ? \"still had connections after they were disconnected\"\n : \"has active connections\"}.\\n`\n + await this.describeBlockingConnections(dbName, { forced: options?.force === true })\n );\n }\n throw describeBranchDdlError(err, dbName);\n }\n\n // Remove metadata\n await (await this.home.registry()).execute(\n sql`DELETE FROM rebase.branches WHERE name = ${name}`\n );\n }\n\n /**\n * Who is holding the database open, in words a developer can act on.\n *\n * `CREATE DATABASE ... TEMPLATE` and `DROP DATABASE` both require that no\n * other session is connected, and the one thing the old message could not\n * say is the only thing worth knowing: *which* session. \"Close other\n * clients or connections and try again\" is advice you cannot follow when\n * you do not know there is a `rebase dev` in another terminal — and that is\n * the common case, because wanting a branch and running the app are the\n * same moment.\n *\n * `application_name` is the useful column: node-postgres sends none by\n * default, so a Rebase process shows as `(unnamed)` while DBeaver, pgAdmin\n * and psql all name themselves. Counting by it separates \"my own dev\n * server\" from \"the GUI I forgot about\" without any guessing.\n *\n * Best-effort by construction: this runs while reporting a failure, and a\n * diagnostic that throws would replace a real error with its own.\n */\n private async describeBlockingConnections(dbName: string, options: { forced?: boolean } = {}): Promise<string> {\n try {\n const result = await this.db.execute(sql`\n SELECT coalesce(nullif(application_name, ''), '(unnamed)') AS app,\n count(*)::int AS n\n FROM pg_stat_activity\n WHERE datname = ${dbName}\n AND pid <> pg_backend_pid()\n GROUP BY 1\n ORDER BY 2 DESC, 1\n `);\n const rows = result.rows as Record<string, unknown>[];\n if (rows.length === 0) {\n // The blocker went away between the failure and this query, or\n // it is a session Postgres does not attribute to this database.\n return \" Retry — whatever was connected has since disconnected.\";\n }\n\n const listed = rows\n .map((row) => ` ${row.n} × ${String(row.app)}`)\n .join(\"\\n\");\n\n // With --force they were disconnected already, and came back: the\n // one thing left to say is what to stop, not to pass --force.\n return ` Connected right now:\\n${listed}\\n`\n + (options.forced\n ? \" They reconnected as fast as they were disconnected. A running `rebase dev` is the\\n\"\n + \" usual one — stop it, then run this again.\"\n : \" A running `rebase dev` is the usual one — stop it, or re-run with --force to\\n\"\n + \" disconnect them for you.\");\n } catch {\n return options.forced\n ? \" Stop the clients connected to it — a running `rebase dev` is the usual one — then run this again.\"\n : \" Close other clients and connections, or re-run with --force.\";\n }\n }\n\n /**\n * Run `operation` with no other session on `dbName`, disconnecting them\n * first: what `--force` asks for.\n *\n * Disconnected, a client that reconnects — a running `rebase dev`'s\n * realtime listener does — can be back before `CREATE DATABASE … TEMPLATE`\n * or `DROP DATABASE` counts the sessions, and it failed on one about once\n * in seven tries, saying to pass the `--force` that had been passed. So a\n * refusal on a session still connected is retried, disconnecting again, a\n * few times before it is reported.\n */\n private async disconnectingOthers(dbName: string, operation: () => Promise<void>): Promise<void> {\n for (let attempt = 1; ; attempt++) {\n const closed = await this.terminateConnections(dbName);\n if (closed > 0) {\n // Postgres reports the backend as gone before its slot is\n // actually free, and the operation then fails on a connection\n // that no longer exists. A short settle first.\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n try {\n await operation();\n return;\n } catch (err) {\n if (attempt >= FORCE_ATTEMPTS || extractPgError(err)?.code !== PG_OBJECT_IN_USE) throw err;\n }\n }\n }\n\n /**\n * Run `operation` with `dbName` refusing new connections, and open it\n * again after, whatever happens.\n *\n * A client disconnected from the source — a running `rebase dev` holds a\n * pool and a realtime listener — cannot then reconnect before the copy has\n * counted the sessions on it. Postgres refuses `ALLOW_CONNECTIONS false` on\n * the database a session is connected to, so both statements run from the\n * server's `postgres` database, and they need the database's owner. Where\n * either is missing, the retries of {@link disconnectingOthers} are what is\n * left. A database that already refused connections is left as it was.\n *\n * Interrupted (Ctrl-C) while closed, it opens the database again before\n * the process exits; one it could not open is named in the error, with the\n * statement that does.\n */\n private async whileClosedToNewConnections(dbName: string, operation: () => Promise<void>): Promise<void> {\n const safe = dbName.replace(/\"/g, '\"\"');\n const maintenance = dbName === MAINTENANCE_DATABASE ? null : this.poolManager.getDrizzle(MAINTENANCE_DATABASE);\n let closed = false;\n try {\n if (maintenance) {\n const state = await maintenance.execute(sql`SELECT datallowconn FROM pg_database WHERE datname = ${dbName}`);\n const [row] = state.rows as Record<string, unknown>[];\n if (row?.datallowconn === true) {\n await maintenance.execute(sql.raw(`ALTER DATABASE \"${safe}\" ALLOW_CONNECTIONS false`));\n closed = true;\n }\n }\n } catch {\n // No `postgres` database to run it from, or not the owner.\n }\n if (!closed) {\n await this.poolManager.disconnectDatabase(MAINTENANCE_DATABASE);\n await operation();\n return;\n }\n\n let reopening: Promise<void> | undefined;\n const reopen = (): Promise<void> => {\n reopening ??= this.reopenToConnections(dbName);\n return reopening;\n };\n const onSignal = (signal: NodeJS.Signals): void => {\n void reopen().finally(() => process.kill(process.pid, signal));\n };\n process.once(\"SIGINT\", onSignal);\n process.once(\"SIGTERM\", onSignal);\n try {\n await operation();\n } finally {\n process.removeListener(\"SIGINT\", onSignal);\n process.removeListener(\"SIGTERM\", onSignal);\n await reopen();\n }\n }\n\n /** `ALLOW_CONNECTIONS true` again, from the maintenance database, or an error that says how to do it by hand. */\n private async reopenToConnections(dbName: string): Promise<void> {\n const statement = `ALTER DATABASE \"${dbName.replace(/\"/g, '\"\"')}\" ALLOW_CONNECTIONS true`;\n try {\n await this.poolManager.getDrizzle(MAINTENANCE_DATABASE).execute(sql.raw(statement));\n } catch (err) {\n throw new Error(\n `\"${dbName}\" was closed to new connections for the copy and could not be opened again ` +\n `(${extractPgError(err)?.message ?? extractCauseMessage(err) ?? String(err)}). ` +\n `Nothing can connect to it until you run, from another database: ${statement};`\n );\n } finally {\n await this.poolManager.disconnectDatabase(MAINTENANCE_DATABASE);\n }\n }\n\n /**\n * Disconnect every other session on a database, and say how many.\n *\n * `pg_terminate_backend` rather than `pg_cancel_backend`: cancelling a\n * query leaves the session connected, and a connected session is exactly\n * what blocks the operation.\n *\n * The current backend is excluded — terminating the connection running this\n * statement would abort the command that asked for it.\n */\n private async terminateConnections(dbName: string): Promise<number> {\n const result = await this.db.execute(sql`\n SELECT pg_terminate_backend(pid)\n FROM pg_stat_activity\n WHERE datname = ${dbName}\n AND pid <> pg_backend_pid()\n `);\n\n return (result.rows as unknown[]).length;\n }\n\n /**\n * Everything prune needs to decide, in two queries.\n *\n * Reads the rows and the server's database list rather than joining them,\n * so the *disagreements* between the two are visible — a row whose database\n * is gone and a database with no row are the two things prune exists to\n * find, and a join hides both.\n */\n async pruneCandidates(): Promise<{ rows: BranchRow[]; databases: string[] }> {\n const rowResult = await (await this.home.registry()).execute(sql.raw(`\n SELECT name, db_name, created_at FROM ${BRANCHES_TABLE} ORDER BY created_at\n `));\n const rows = (rowResult.rows as Record<string, unknown>[]).map((row) => ({\n name: row.name as string,\n dbName: row.db_name as string,\n createdAt: new Date(row.created_at as string)\n }));\n\n const dbResult = await this.db.execute(sql`\n SELECT datname FROM pg_database WHERE NOT datistemplate\n `);\n const databases = (dbResult.rows as Record<string, unknown>[]).map((row) => String(row.datname));\n\n return { rows, databases };\n }\n\n /** Remove a metadata row whose database is already gone. */\n async forgetBranchRow(name: string): Promise<void> {\n await (await this.home.registry()).execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);\n }\n\n /**\n * Drop a database by name, with no metadata row required.\n *\n * `deleteBranch` deliberately takes its database name from the row, because\n * the row is the only value that is true by construction. Prune has the\n * opposite job — the cases it handles are exactly the ones where row and\n * database disagree — so it needs to name a database directly. Guarded the\n * same way regardless: the main database is never droppable.\n */\n async dropDatabase(dbName: string): Promise<void> {\n validateIdentifier(dbName, \"database name\");\n if (dbName === await this.home.database()) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n await this.poolManager.disconnectDatabase(dbName);\n try {\n await this.db.execute(sql.raw(`DROP DATABASE \"${dbName.replace(/\"/g, '\"\"')}\"`));\n } catch (err) {\n throw describeBranchDdlError(err, dbName);\n }\n }\n\n /**\n * List all branches recorded in the metadata table.\n * Optionally fetches database sizes from pg_database.\n */\n async listBranches(): Promise<BranchInfo[]> {\n const result = await (await this.home.registry()).execute(sql.raw(`\n SELECT \n b.name,\n b.db_name,\n b.parent_db,\n b.created_at,\n pg_database_size(b.db_name) as size_bytes\n FROM ${BRANCHES_TABLE} b\n JOIN pg_database d ON d.datname = b.db_name\n ORDER BY b.created_at DESC\n `));\n\n return (result.rows as Record<string, unknown>[]).map((row) => ({\n name: row.name as string,\n database: row.db_name as string,\n parentDatabase: row.parent_db as string,\n createdAt: new Date(row.created_at as string),\n sizeBytes: row.size_bytes != null ? Number(row.size_bytes) : undefined\n }));\n }\n\n /**\n * Get info about a specific branch.\n */\n async getBranchInfo(name: string): Promise<BranchInfo | undefined> {\n assertValidBranchName(name);\n\n const result = await (await this.home.registry()).execute(sql`\n SELECT\n b.name,\n b.db_name,\n b.parent_db,\n b.created_at\n FROM rebase.branches b\n WHERE b.name = ${name}\n `);\n\n const rows = result.rows as Record<string, unknown>[];\n if (rows.length === 0) return undefined;\n\n const row = rows[0];\n\n // Attempt to get size — may fail if the DB was externally dropped.\n // Same reason as `deleteBranch`: the size of a re-derived name is the\n // size of some other database, or of nothing.\n let sizeBytes: number | undefined;\n try {\n const dbName = row.db_name as string;\n const sizeResult = await this.db.execute(\n sql`SELECT pg_database_size(${dbName}) as size_bytes`\n );\n const sizeRows = sizeResult.rows as Record<string, unknown>[];\n if (sizeRows.length > 0 && sizeRows[0].size_bytes != null) {\n sizeBytes = Number(sizeRows[0].size_bytes);\n }\n } catch {\n // Database might not exist anymore\n }\n\n return {\n name: row.name as string,\n database: row.db_name as string,\n parentDatabase: row.parent_db as string,\n createdAt: new Date(row.created_at as string),\n sizeBytes\n };\n }\n}\n"],"mappings":"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"}
@@ -283,6 +283,19 @@ export declare class PostgresBackendDriver implements DataDriver {
283
283
  * path, such a row is absent, so it is read from its own collection.
284
284
  */
285
285
  private storedRowPath;
286
+ /**
287
+ * The row an update addresses, as this caller reads it there: what a
288
+ * caller's precondition (the REST routes' 404 and `If-Match`) judges.
289
+ *
290
+ * `stored` is the row read from {@link storedRowPath}. Off a many-to-many
291
+ * path that is the address itself. Through one, the row is judged as a
292
+ * member of the parent's set when it is one. When it is not yet, this
293
+ * update is the link that makes it one (the persistence layer writes the
294
+ * junction row), so the row is judged as it is stored — provided the caller
295
+ * can read the parent it is being linked to. A parent that is not there, or
296
+ * that their policies hide, is no set to join, and the row reads as absent.
297
+ */
298
+ private rowAnUpdateAddresses;
286
299
  /**
287
300
  * The stored row an upsert's key names, if this caller can address it —
288
301
  * with its address and the values to update it with.
@@ -96,6 +96,16 @@ export interface PostgresDriverInternals {
96
96
  * driver's path, not only while it opens.
97
97
  */
98
98
  provisioned: boolean;
99
+ /**
100
+ * How authenticated requests are isolated on this connection, in one
101
+ * line: run as the restricted role, or filtered natively.
102
+ *
103
+ * Returned rather than logged by {@link openPostgresDriver}. Announcing it
104
+ * is the server boot's business — `initializeDriver` logs it at info — and
105
+ * a script's `openRebase` logs it at debug, so a script's stdout holds what
106
+ * the script prints.
107
+ */
108
+ rlsEnforcement?: string;
99
109
  /**
100
110
  * Attach CDC triggers to tables that did not exist when the driver
101
111
  * bootstrapped. Only set when database-level capture is actually active.
@@ -3,7 +3,7 @@
3
3
  * `rebase db backups list`. Kept out of the main `cli.ts` dispatcher so the
4
4
  * backup surface stays self-contained.
5
5
  */
6
- import arg from "arg";
6
+ import type arg from "arg";
7
7
  import type { StorageController } from "@rebasepro/server";
8
8
  import { BackupDestination } from "./pg-tools.js";
9
9
  /**
@@ -18,8 +18,74 @@ import { BackupDestination } from "./pg-tools.js";
18
18
  * Exported for its test.
19
19
  */
20
20
  export declare function resolveStorageForDestination(dest: BackupDestination, env: Record<string, string | undefined>): Promise<StorageController | null>;
21
+ /**
22
+ * The directory `rebase` was run from, which `rebase` puts here for the driver.
23
+ *
24
+ * The driver runs in the project's `backend/`, so a relative path resolved
25
+ * against its own working directory named a different place from the one the
26
+ * user typed it in: `rebase db backup --out ./backups` wrote to `<root>/backups`
27
+ * (the CLI resolves explicit paths itself) while `rebase db backups list`
28
+ * looked in `<root>/backend/backups`. Spelled the same in
29
+ * `packages/cli/src/commands/db.ts`.
30
+ */
31
+ export declare const INVOCATION_CWD_ENV = "REBASE_INVOCATION_CWD";
32
+ /** Where a relative path the user typed is relative to: where they ran `rebase`. */
33
+ export declare function invocationDir(env?: Readonly<Record<string, string | undefined>>): string;
34
+ /**
35
+ * Where `db backup` writes and `db backups list` reads, by one rule.
36
+ *
37
+ * - `--out` is relative to where `rebase` was run, like any path typed on a
38
+ * command line.
39
+ * - `BACKUP_DESTINATION` is relative to the backend directory, because the
40
+ * server reads the same variable there: the Backups panel and the scheduled
41
+ * backup have to find what the CLI wrote.
42
+ * - With neither, `./backups` where `rebase` was run, as the help says.
43
+ *
44
+ * An `s3://` or `gs://` destination is returned as given.
45
+ */
46
+ export declare function resolveBackupLocation(opts: {
47
+ flag?: string;
48
+ configured?: string;
49
+ invocationDir: string;
50
+ backendDir: string;
51
+ }): string;
52
+ /**
53
+ * Parse a `rebase db backup …` line. An undeclared flag is refused (see
54
+ * `parseCommandFlags`), and so is a positional: `rebase db backup ./backups`
55
+ * means `--out ./backups`, and taken as nothing it wrote to the default
56
+ * directory.
57
+ */
58
+ export declare function parseBackupLine(rawArgs: string[]): arg.Result<{
59
+ "--database-url": StringConstructor;
60
+ "--docker": BooleanConstructor;
61
+ "--debug": BooleanConstructor;
62
+ "--help": BooleanConstructor;
63
+ "-h": string;
64
+ } & {
65
+ "--out": StringConstructor;
66
+ "--output": string;
67
+ "--exclude-schema": [StringConstructor];
68
+ "--no-owner": BooleanConstructor;
69
+ "--enable-row-security": BooleanConstructor;
70
+ "--row-security-role": StringConstructor;
71
+ "-o": string;
72
+ }>;
73
+ /**
74
+ * Whether a `pg_dump --exclude-schema` pattern matches the `rebase` schema, by
75
+ * pg_dump's own rules: `*` and `?` are wildcards, an unquoted name is folded
76
+ * to lower case and a double-quoted one is taken as written, and a
77
+ * `<database>.<schema>` pattern is matched on its schema.
78
+ */
79
+ export declare function excludesRebaseSchema(pattern: string): boolean;
21
80
  export declare function backupCommand(rawArgs: string[]): Promise<void>;
22
- /** Parse a `rebase db restore …` line; the first positional is the backup. */
81
+ /**
82
+ * Parse a `rebase db restore …` line; the one positional is the backup.
83
+ *
84
+ * Strict, because this is the command where a typo costs most: parsed
85
+ * permissively, `--targt-db x` became a second positional, the target fell
86
+ * back to DATABASE_URL's own database, and the restore ran against the live
87
+ * one.
88
+ */
23
89
  export declare function parseRestoreLine(rawArgs: string[]): arg.Result<{
24
90
  "--database-url": StringConstructor;
25
91
  "--docker": BooleanConstructor;
@@ -35,5 +101,57 @@ export declare function parseRestoreLine(rawArgs: string[]): arg.Result<{
35
101
  "--yes": BooleanConstructor;
36
102
  "-y": string;
37
103
  }>;
104
+ /**
105
+ * The restore's one question, asked before anything can change.
106
+ *
107
+ * `--yes` skips it. On a terminal it asks for a typed 'yes'. With no terminal
108
+ * nobody can answer, and the refusal says to pass `--yes`, as `db reset`,
109
+ * `db pull` and `db branch prune` do: "Aborted. No changes were made." alone
110
+ * read as though someone had answered no.
111
+ *
112
+ * @returns whether to go on. When not, it has said why.
113
+ */
114
+ export declare function confirmRestore(opts: {
115
+ target: string;
116
+ yes: boolean;
117
+ interactive: boolean;
118
+ ask: (question: string) => Promise<boolean>;
119
+ }): Promise<boolean>;
120
+ /**
121
+ * What `db restore` says when the target already holds what the backup
122
+ * creates, and the two ways round it: `--clean` replaces them with the
123
+ * backup's copy, a new database restores beside them.
124
+ *
125
+ * `line` is the rest of what the user typed — the database it names, `--yes`,
126
+ * `--no-owner` — which both commands keep, so the one copied runs against the
127
+ * same database and does not stop again to ask. `--target-db` goes on the
128
+ * `--clean` one only: the other names a new database.
129
+ */
130
+ export declare function describeRestoreConflict(target: string, backupArg: string, conflicts: readonly string[], line?: {
131
+ targetDb?: string;
132
+ carried?: readonly string[];
133
+ }): {
134
+ message: string;
135
+ hint: string;
136
+ };
137
+ /**
138
+ * What `restore --clean` says, before it asks, about what it drops that the
139
+ * backup does not hold — see `findCleanExtras`. Nothing when there is none.
140
+ */
141
+ export declare function describeCleanExtras(schemas: readonly string[], extras: readonly string[]): string[];
142
+ /**
143
+ * What a `restore --clean` that stopped says: it ran as one transaction, so
144
+ * nothing changed. The cluster roles the sidecar recreated are the exception,
145
+ * and are said to be.
146
+ */
147
+ export declare function describeCleanRestoreFailure(target: string, failure: {
148
+ tool: "pg_restore" | "psql";
149
+ lockTimeout: boolean;
150
+ }, rolesApplied: number): {
151
+ message: string;
152
+ hint?: string;
153
+ };
38
154
  export declare function restoreCommand(rawArgs: string[]): Promise<void>;
155
+ /** What `db backups <action>` says of an action it does not have: the sentence every `db` command says. */
156
+ export declare function unknownBackupsAction(action: string): string;
39
157
  export declare function backupsCommand(rawArgs: string[]): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import type { StorageController } from "@rebasepro/server";
2
2
  import { resolvePgBinary } from "./pg-binary.js";
3
- import { BackupDestination, type RowSecurityIdentity, VersionCompatibility } from "./pg-tools.js";
3
+ import { BackupDestination, type RowSecurityIdentity, type DumpObjects, VersionCompatibility } from "./pg-tools.js";
4
4
  import { BackupObject, RetentionOptions } from "./retention.js";
5
5
  /**
6
6
  * Remove a dump artifact abandoned by a failed tool run. See
@@ -17,6 +17,24 @@ export { resolvePgBinary };
17
17
  export declare function detectToolMajor(bin: string): Promise<number | null>;
18
18
  /** Query the server for its major version via `server_version_num`. */
19
19
  export declare function getServerVersionMajor(connectionString: string): Promise<number | null>;
20
+ /**
21
+ * Put back what a client tool left set on a database that is one session
22
+ * every connection shares, and do nothing on any other.
23
+ *
24
+ * The managed development database is PGlite: one session behind a socket
25
+ * server, so what `pg_dump` sets for itself — `search_path` emptied,
26
+ * `row_security` off, `PREPARE dumpEnumType …` — stays set for every client
27
+ * after it, a running `rebase dev` included. One `rebase db backup` there left
28
+ * every API request answering 500 "table does not exist", a restart did not
29
+ * help (the database outlives `rebase dev`), and the next backup failed on
30
+ * `prepared statement "dumpenumtype" already exists`.
31
+ *
32
+ * `RESET ALL` and `DEALLOCATE ALL`, not `DISCARD ALL`: that would also drop
33
+ * the `LISTEN` a running `rebase dev`'s realtime holds on the same session.
34
+ * Best effort: it runs after the tool, whose own outcome is what the caller
35
+ * reports.
36
+ */
37
+ export declare function resetSharedSession(connectionString: string): Promise<void>;
20
38
  export interface PreflightResult extends VersionCompatibility {
21
39
  bin: string;
22
40
  toolMajor: number | null;
@@ -114,6 +132,61 @@ export declare function restoreDump(opts: {
114
132
  inheritStdio?: boolean;
115
133
  env?: Record<string, string | undefined>;
116
134
  }): Promise<void>;
135
+ /**
136
+ * A `restore --clean` that stopped, and so was rolled back whole.
137
+ *
138
+ * `tool` is the one that stopped it: `pg_restore`, writing the dump's script,
139
+ * or `psql`, running it. `lockTimeout` when what `psql` stopped on was the
140
+ * wait for a lock — `CLEAN_LOCK_TIMEOUT` in `pg-tools.ts`.
141
+ */
142
+ export declare class CleanRestoreFailed extends Error {
143
+ readonly tool: "pg_restore" | "psql";
144
+ readonly exitCode: number | undefined;
145
+ readonly lockTimeout: boolean;
146
+ constructor(tool: "pg_restore" | "psql", exitCode: number | undefined, lockTimeout: boolean);
147
+ }
148
+ /**
149
+ * Restore a dump over what the database holds, as one transaction:
150
+ * {@link cleanRestorePrelude} first, then the dump as `pg_restore` writes it
151
+ * for `psql`, and the `COMMIT` only after the last of it.
152
+ *
153
+ * One unit, so it either replaces what the dump holds or changes nothing:
154
+ * `pg_restore --clean` runs each statement on its own, so the drops it starts
155
+ * with had all happened when a later one failed — and its plain `DROP SCHEMA
156
+ * rebase` fails on any table the backup lacks, which left a database with
157
+ * every table dropped and nothing restored. Here a failure anywhere —
158
+ * `pg_restore` cannot read the archive, a statement fails, the connection
159
+ * drops — ends the transaction without a `COMMIT`, and Postgres rolls it back.
160
+ *
161
+ * Throws {@link CleanRestoreFailed} when it did not commit.
162
+ */
163
+ export declare function restoreDumpReplacing(opts: {
164
+ connectionString: string;
165
+ inputFile: string;
166
+ objects: DumpObjects;
167
+ noOwner?: boolean;
168
+ inheritStdio?: boolean;
169
+ env?: Record<string, string | undefined>;
170
+ }): Promise<void>;
171
+ /**
172
+ * What `restore --clean` drops that the backup does not hold, so it can say
173
+ * so before it runs: the tables and views in a schema the backup replaces
174
+ * whole that it has no copy of, and the views and foreign keys elsewhere that
175
+ * reference one of the relations it replaces. As `<kind> <name>`.
176
+ *
177
+ * None for a database that does not exist yet.
178
+ */
179
+ export declare function findCleanExtras(connectionString: string, objects: DumpObjects): Promise<string[]>;
180
+ /** The schemas and tables a dump creates, from `pg_restore --list`. See {@link parseDumpObjects}. */
181
+ export declare function readDumpObjects(inputFile: string, env?: Record<string, string | undefined>): Promise<DumpObjects>;
182
+ /**
183
+ * Which of a dump's schemas and tables the target database already holds,
184
+ * as `schema <name>` / `table <schema>.<name>`: what a restore without
185
+ * `--clean` would stop on. `public` is left out of the schemas — every
186
+ * database has it, and pg_restore does not create it. None for a database
187
+ * that does not exist yet.
188
+ */
189
+ export declare function findRestoreConflicts(connectionString: string, objects: DumpObjects): Promise<string[]>;
117
190
  /**
118
191
  * Create a database (if absent) by connecting to the maintenance
119
192
  * `postgres` database. Used by `restore --create-db`.
@@ -1,5 +1,5 @@
1
- export type PgTool = "pg_dump" | "pg_restore" | "pg_dumpall";
1
+ export type PgTool = "pg_dump" | "pg_restore" | "pg_dumpall" | "psql";
2
2
  /** The variable that points at `tool` when it is not on the PATH. */
3
3
  export declare function pgToolVariable(tool: PgTool): string;
4
- /** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
4
+ /** Locate `pg_dump` / `pg_restore` / `pg_dumpall` / `psql`, honouring an env override. */
5
5
  export declare function resolvePgBinary(tool: PgTool, env?: Record<string, string | undefined>): string | null;
@@ -161,6 +161,71 @@ export declare function buildPgRestoreArgs(opts: {
161
161
  * a cheap integrity check that the file isn't truncated or corrupt.
162
162
  */
163
163
  export declare function buildPgRestoreListArgs(inputFile: string): string[];
164
+ /** The schemas and tables a dump creates, read off its table of contents. */
165
+ export interface DumpObjects {
166
+ schemas: string[];
167
+ tables: {
168
+ schema: string;
169
+ table: string;
170
+ }[];
171
+ /**
172
+ * Every relation it creates — tables, views, materialized views, foreign
173
+ * tables and sequences — by schema and name: what `restore --clean` can
174
+ * tell apart from what the target holds besides.
175
+ */
176
+ relations: {
177
+ schema: string;
178
+ name: string;
179
+ }[];
180
+ }
181
+ /**
182
+ * The schemas and tables in `pg_restore --list` output: the objects a
183
+ * restore without `--clean` fails on when the target already has them.
184
+ *
185
+ * A TOC line ends in the object's owner, after its namespace and its name;
186
+ * a schema's namespace is `-`. The name is what lies between the two, so a
187
+ * name with a space in it (`Odd Name`) reads whole. `TABLE DATA` and every
188
+ * other kind of entry are not objects of their own here.
189
+ */
190
+ export declare function parseDumpObjects(list: string): DumpObjects;
191
+ /**
192
+ * How long `restore --clean` waits for a lock before it gives up.
193
+ *
194
+ * A `rebase dev` serving from the database holds connections, and one inside a
195
+ * transaction would otherwise make the restore wait without a word. The dump's
196
+ * own script sets `lock_timeout = 0` after this, for the objects it creates.
197
+ */
198
+ export declare const CLEAN_LOCK_TIMEOUT = "15s";
199
+ /**
200
+ * What `restore --clean` runs first, in the transaction the restore runs in:
201
+ * every schema the dump creates, dropped with `CASCADE`, and every table it
202
+ * creates in a schema it does not (`public`), the same.
203
+ *
204
+ * `CASCADE`, which `pg_restore --clean` never uses: a table the backup lacks
205
+ * in one of its schemas — `rebase.atlas_schema_revisions` from `rebase db
206
+ * migrate`, a table a newer runtime added — is exactly what stopped its plain
207
+ * `DROP SCHEMA rebase`, and a table that references one of the backup's is what
208
+ * stops a plain `DROP TABLE`. The dump's script follows, with its own `DROP …
209
+ * IF EXISTS` for everything else it creates.
210
+ */
211
+ export declare function cleanRestorePrelude(objects: DumpObjects): string[];
212
+ /**
213
+ * `pg_restore` writing the dump as a SQL script on stdout, with the `DROP …
214
+ * IF EXISTS` that `--clean` adds, for `psql` to run. See {@link buildPsqlScriptArgs}.
215
+ */
216
+ export declare function buildPgRestoreScriptArgs(opts: {
217
+ inputFile: string;
218
+ noOwner?: boolean;
219
+ }): string[];
220
+ /**
221
+ * `psql` running a script read from stdin, stopping at its first error.
222
+ *
223
+ * `-X`, so no `.psqlrc` changes what runs; `-q`, so it prints errors and
224
+ * warnings, not a command tag per statement. The transaction is the script's
225
+ * own: `restore --clean` writes the `BEGIN`, and the `COMMIT` only once
226
+ * `pg_restore` has written the whole dump.
227
+ */
228
+ export declare function buildPsqlScriptArgs(connectionString: string): string[];
164
229
  /**
165
230
  * Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other
166
231
  * cluster-wide objects) live outside any single database, so a per-database