@rebasepro/server-postgres 0.23.0 → 0.24.1-canary.g56af4a4

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 (180) hide show
  1. package/dist/{BranchService-DGPL6G_C.js → BranchService-Bsn2wfaj.js} +36 -18
  2. package/dist/BranchService-Bsn2wfaj.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +166 -13
  4. package/dist/auth/ensure-tables.d.ts +12 -0
  5. package/dist/auth/services.d.ts +78 -14
  6. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  7. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  8. package/dist/backup/backup-cli.d.ts +14 -0
  9. package/dist/backup/backup-service.d.ts +2 -2
  10. package/dist/backup/pg-binary.d.ts +5 -0
  11. package/dist/{backup-cli-24v4OlSp.js → backup-cli-aUfRv16h.js} +37 -29
  12. package/dist/backup-cli-aUfRv16h.js.map +1 -0
  13. package/dist/{backup-service-HQ9GC4tN.js → backup-service-BTBB7RXn.js} +60 -28
  14. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  15. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  16. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  17. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  18. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  19. package/dist/cli-errors.d.ts +93 -5
  20. package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
  21. package/dist/cli-flags-B-Dawocl.js.map +1 -0
  22. package/dist/cli-helpers.d.ts +52 -12
  23. package/dist/cli-output.d.ts +16 -28
  24. package/dist/cli-push.d.ts +87 -0
  25. package/dist/cli.js +625 -190
  26. package/dist/cli.js.map +1 -1
  27. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-drZIsSNf.js} +42 -26
  28. package/dist/column-plan-helpers-drZIsSNf.js.map +1 -0
  29. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
  30. package/dist/connection-BPoNILnD.js.map +1 -0
  31. package/dist/connection.d.ts +9 -1
  32. package/dist/data-transformer.d.ts +0 -8
  33. package/dist/databasePoolManager-BkEy2w18.js +119 -0
  34. package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
  35. package/dist/deferred-index-build-DP1_SNFo.js +1051 -0
  36. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  37. package/dist/{doctor-Cb2thZ8s.js → doctor-C4WiftHm.js} +279 -87
  38. package/dist/doctor-C4WiftHm.js.map +1 -0
  39. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  40. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  41. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  42. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  43. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-CHsgyLqg.js} +178 -75
  44. package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
  45. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  46. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  47. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  48. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  49. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-B74WN37S.js} +28 -10
  50. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  51. package/dist/generate-files-CdEckX6c.js +199 -0
  52. package/dist/generate-files-CdEckX6c.js.map +1 -0
  53. package/dist/generate-postgres-ddl-logic-B64O2oZF.js +152 -0
  54. package/dist/generate-postgres-ddl-logic-B64O2oZF.js.map +1 -0
  55. package/dist/generated-sql.d.ts +34 -0
  56. package/dist/history/HistoryService.d.ts +38 -6
  57. package/dist/history/ensure-history-table.d.ts +42 -4
  58. package/dist/index.es.js +5557 -7612
  59. package/dist/index.es.js.map +1 -1
  60. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-C-Di8dA9.js} +562 -350
  61. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  62. package/dist/introspect-db-queries-D6kAqReI.js +339 -0
  63. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  64. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  65. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  66. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-B1YF_tYq.js} +278 -533
  67. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  68. package/dist/{policy-drift-DljYdrpW.js → policy-drift-Dm8i-s1J.js} +24 -7
  69. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  70. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  71. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  72. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  73. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  74. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CtC3y9Ol.js} +17 -151
  75. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  76. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1046
  77. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  78. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-DvzqlaL1.js} +137 -17
  79. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  80. package/dist/schema/auth-schema.d.ts +172 -2
  81. package/dist/schema/boot-ddl.d.ts +47 -0
  82. package/dist/schema/classify-change.d.ts +29 -1
  83. package/dist/schema/collection-index.d.ts +45 -0
  84. package/dist/schema/column-plan-helpers.d.ts +38 -21
  85. package/dist/schema/deferred-index-build.d.ts +131 -0
  86. package/dist/schema/destructive-sql.d.ts +27 -1
  87. package/dist/schema/doctor-cli.js +110 -6
  88. package/dist/schema/doctor-cli.js.map +1 -1
  89. package/dist/schema/doctor-search-scale.d.ts +45 -0
  90. package/dist/schema/doctor.d.ts +53 -1
  91. package/dist/schema/ensure-collection-policies.d.ts +95 -35
  92. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  93. package/dist/schema/enum-labels-kept.d.ts +26 -0
  94. package/dist/schema/generate-drizzle-schema.js +1 -1
  95. package/dist/schema/generate-files.d.ts +44 -0
  96. package/dist/schema/generate-next-step.d.ts +0 -13
  97. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  98. package/dist/schema/generate-postgres-ddl.js +10 -81
  99. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  100. package/dist/schema/generate-schema-commit.d.ts +12 -0
  101. package/dist/schema/introspect-db-logic.d.ts +43 -0
  102. package/dist/schema/introspect-db-queries.d.ts +12 -2
  103. package/dist/schema/introspect-db-search.d.ts +22 -0
  104. package/dist/schema/introspect-db-storage.d.ts +83 -0
  105. package/dist/schema/introspect-db-structure.d.ts +10 -0
  106. package/dist/schema/introspect-db.js +17 -6
  107. package/dist/schema/introspect-db.js.map +1 -1
  108. package/dist/schema/migration-post-process.d.ts +34 -0
  109. package/dist/schema/plan/diff-plan.d.ts +127 -1
  110. package/dist/schema/plan/plan-schema.d.ts +16 -8
  111. package/dist/schema/plan/render-ddl.d.ts +6 -0
  112. package/dist/schema/plan/types.d.ts +45 -11
  113. package/dist/schema/push-index-prebuild.d.ts +25 -0
  114. package/dist/schema/push-legacy-renames.d.ts +20 -0
  115. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  116. package/dist/search-column-BM-GV6vH.js +442 -0
  117. package/dist/search-column-BM-GV6vH.js.map +1 -0
  118. package/dist/security/policy-drift.d.ts +1 -1
  119. package/dist/security/rls-enforcement.d.ts +39 -4
  120. package/dist/services/BranchService.d.ts +22 -1
  121. package/dist/services/FetchService.d.ts +63 -12
  122. package/dist/services/PersistService.d.ts +15 -0
  123. package/dist/services/RelationService.d.ts +106 -3
  124. package/dist/services/cdc/CdcListener.d.ts +26 -10
  125. package/dist/services/cdc/identity-columns.d.ts +19 -0
  126. package/dist/services/cdc/trigger-cdc.d.ts +180 -18
  127. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  128. package/dist/services/channel-history.d.ts +32 -5
  129. package/dist/services/collection-helpers.d.ts +26 -1
  130. package/dist/services/field-viewer.d.ts +15 -1
  131. package/dist/services/pg-notify-listener.d.ts +95 -6
  132. package/dist/services/realtimeService.d.ts +467 -53
  133. package/dist/services/row-pipeline.d.ts +16 -9
  134. package/dist/services/socket-liveness.d.ts +85 -0
  135. package/dist/services/soft-delete.d.ts +12 -0
  136. package/dist/services/sql-script.d.ts +275 -0
  137. package/dist/services/vector-iterative-scan.d.ts +40 -0
  138. package/dist/services/write-depth.d.ts +22 -0
  139. package/dist/services/write-transaction-scope.d.ts +15 -3
  140. package/dist/socket-liveness-D3UD5_BW.js +7570 -0
  141. package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
  142. package/dist/socket-opt-ins.d.ts +64 -0
  143. package/dist/source-DBDbBTL4.js +811 -0
  144. package/dist/source-DBDbBTL4.js.map +1 -0
  145. package/dist/tsx-loader.d.ts +14 -0
  146. package/dist/utils/drizzle-conditions.d.ts +63 -3
  147. package/dist/utils/error-cause.d.ts +7 -0
  148. package/dist/utils/pg-error-utils.d.ts +2 -4
  149. package/dist/utils/result-rows.d.ts +10 -0
  150. package/dist/websocket.d.ts +63 -18
  151. package/package.json +7 -7
  152. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  153. package/dist/backup-cli-24v4OlSp.js.map +0 -1
  154. package/dist/backup-service-HQ9GC4tN.js.map +0 -1
  155. package/dist/cli-errors-B8qHg02P.js +0 -798
  156. package/dist/cli-errors-B8qHg02P.js.map +0 -1
  157. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  158. package/dist/cli-output-CNdMql-L.js +0 -47
  159. package/dist/cli-output-CNdMql-L.js.map +0 -1
  160. package/dist/collection-index-DdnsxiJ_.js +0 -286
  161. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  162. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  163. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  164. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  165. package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
  166. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  167. package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
  168. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  169. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  170. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  171. package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
  172. package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
  173. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  174. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  175. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  176. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  177. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  178. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
  179. package/dist/source-Br7L7GOI.js +0 -410
  180. package/dist/source-Br7L7GOI.js.map +0 -1
@@ -1,7 +1,8 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
- import { d as extractPgError, u as extractCauseMessage } from "./cli-errors-B8qHg02P.js";
4
+ import { K as extractCauseMessage } from "./cli-errors-CaI4ntyD.js";
5
+ import { n as extractPgError } from "./pg-error-utils-vJezlXdI.js";
5
6
  import { sql } from "drizzle-orm";
6
7
  import { revokeInternalTableSql } from "@rebasepro/common";
7
8
  //#region src/services/BranchService.ts
@@ -106,9 +107,20 @@ function toBranchDbName(name) {
106
107
  var BranchService = class {
107
108
  db;
108
109
  poolManager;
109
- constructor(db, poolManager) {
110
+ home;
111
+ /**
112
+ * @param db A connection on the server the branches are made on. Branch
113
+ * DDL runs here.
114
+ * @param home Where branches are kept. Without one, the database the
115
+ * pool manager's connection string names, and `db`'s registry.
116
+ */
117
+ constructor(db, poolManager, home) {
110
118
  this.db = db;
111
119
  this.poolManager = poolManager;
120
+ this.home = home ?? {
121
+ database: async () => poolManager.defaultDatabaseName,
122
+ registry: async () => db
123
+ };
112
124
  }
113
125
  /**
114
126
  * Refuse a branch mutation the connected server cannot honour.
@@ -130,8 +142,9 @@ var BranchService = class {
130
142
  * Idempotent — safe to call on every startup.
131
143
  */
132
144
  async ensureBranchMetadataTable() {
133
- await this.db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
134
- await this.db.execute(sql.raw(`
145
+ const registry = await this.home.registry();
146
+ await registry.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
147
+ await registry.execute(sql.raw(`
135
148
  CREATE TABLE IF NOT EXISTS ${BRANCHES_TABLE} (
136
149
  name TEXT PRIMARY KEY,
137
150
  db_name TEXT NOT NULL UNIQUE,
@@ -140,7 +153,7 @@ var BranchService = class {
140
153
  metadata JSONB DEFAULT '{}'
141
154
  );
142
155
  `));
143
- await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "branches")));
156
+ await registry.execute(sql.raw(revokeInternalTableSql("rebase", "branches")));
144
157
  }
145
158
  /**
146
159
  * Create a new branch database by templating the source database.
@@ -155,8 +168,8 @@ var BranchService = class {
155
168
  await this.assertBranchingSupported();
156
169
  if (options?.source) validateIdentifier(options.source, "source database name");
157
170
  const dbName = toBranchDbName(name);
158
- const sourceDb = options?.source || this.poolManager.defaultDatabaseName;
159
- if ((await this.db.execute(sql`SELECT name FROM rebase.branches WHERE name = ${name} OR db_name = ${dbName}`)).rows.length > 0) throw new Error(`Branch "${name}" already exists.`);
171
+ const sourceDb = options?.source || await this.home.database();
172
+ 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.`);
160
173
  await this.poolManager.disconnectDatabase(sourceDb);
161
174
  if (options?.force) {
162
175
  if (await this.terminateConnections(sourceDb) > 0) await new Promise((resolve) => setTimeout(resolve, 250));
@@ -170,10 +183,11 @@ var BranchService = class {
170
183
  throw describeBranchDdlError(err, dbName);
171
184
  }
172
185
  const now = /* @__PURE__ */ new Date();
173
- await this.db.execute(sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
186
+ await (await this.home.registry()).execute(sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
174
187
  VALUES (${name}, ${dbName}, ${sourceDb}, ${now.toISOString()})`);
175
188
  return {
176
189
  name,
190
+ database: dbName,
177
191
  parentDatabase: sourceDb,
178
192
  createdAt: now
179
193
  };
@@ -185,11 +199,12 @@ var BranchService = class {
185
199
  async deleteBranch(name, options) {
186
200
  await this.assertBranchingSupported();
187
201
  assertValidBranchName(name);
188
- if (toBranchDbName(name) === this.poolManager.defaultDatabaseName) throw new Error("Cannot delete the main database.");
189
- const existingRows = (await this.db.execute(sql`SELECT db_name FROM rebase.branches WHERE name = ${name}`)).rows;
202
+ const mainDatabase = await this.home.database();
203
+ if (toBranchDbName(name) === mainDatabase) throw new Error("Cannot delete the main database.");
204
+ const existingRows = (await (await this.home.registry()).execute(sql`SELECT db_name FROM rebase.branches WHERE name = ${name}`)).rows;
190
205
  if (existingRows.length === 0) throw new Error(`Branch "${name}" not found.`);
191
206
  const dbName = existingRows[0].db_name;
192
- if (dbName === this.poolManager.defaultDatabaseName) throw new Error("Cannot delete the main database.");
207
+ if (dbName === mainDatabase) throw new Error("Cannot delete the main database.");
193
208
  await this.poolManager.disconnectDatabase(dbName);
194
209
  if (options?.force) {
195
210
  if (await this.terminateConnections(dbName) > 0) await new Promise((resolve) => setTimeout(resolve, 250));
@@ -201,7 +216,7 @@ var BranchService = class {
201
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));
202
217
  throw describeBranchDdlError(err, dbName);
203
218
  }
204
- await this.db.execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);
219
+ await (await this.home.registry()).execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);
205
220
  }
206
221
  /**
207
222
  * Who is holding the database open, in words a developer can act on.
@@ -268,7 +283,7 @@ var BranchService = class {
268
283
  */
269
284
  async pruneCandidates() {
270
285
  return {
271
- rows: (await this.db.execute(sql.raw(`
286
+ rows: (await (await this.home.registry()).execute(sql.raw(`
272
287
  SELECT name, db_name, created_at FROM ${BRANCHES_TABLE} ORDER BY created_at
273
288
  `))).rows.map((row) => ({
274
289
  name: row.name,
@@ -282,7 +297,7 @@ var BranchService = class {
282
297
  }
283
298
  /** Remove a metadata row whose database is already gone. */
284
299
  async forgetBranchRow(name) {
285
- await this.db.execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);
300
+ await (await this.home.registry()).execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);
286
301
  }
287
302
  /**
288
303
  * Drop a database by name, with no metadata row required.
@@ -295,7 +310,7 @@ var BranchService = class {
295
310
  */
296
311
  async dropDatabase(dbName) {
297
312
  validateIdentifier(dbName, "database name");
298
- if (dbName === this.poolManager.defaultDatabaseName) throw new Error("Cannot delete the main database.");
313
+ if (dbName === await this.home.database()) throw new Error("Cannot delete the main database.");
299
314
  await this.poolManager.disconnectDatabase(dbName);
300
315
  try {
301
316
  await this.db.execute(sql.raw(`DROP DATABASE "${dbName.replace(/"/g, "\"\"")}"`));
@@ -308,9 +323,10 @@ var BranchService = class {
308
323
  * Optionally fetches database sizes from pg_database.
309
324
  */
310
325
  async listBranches() {
311
- return (await this.db.execute(sql.raw(`
326
+ return (await (await this.home.registry()).execute(sql.raw(`
312
327
  SELECT
313
328
  b.name,
329
+ b.db_name,
314
330
  b.parent_db,
315
331
  b.created_at,
316
332
  pg_database_size(b.db_name) as size_bytes
@@ -319,6 +335,7 @@ var BranchService = class {
319
335
  ORDER BY b.created_at DESC
320
336
  `))).rows.map((row) => ({
321
337
  name: row.name,
338
+ database: row.db_name,
322
339
  parentDatabase: row.parent_db,
323
340
  createdAt: new Date(row.created_at),
324
341
  sizeBytes: row.size_bytes != null ? Number(row.size_bytes) : void 0
@@ -329,7 +346,7 @@ var BranchService = class {
329
346
  */
330
347
  async getBranchInfo(name) {
331
348
  assertValidBranchName(name);
332
- const rows = (await this.db.execute(sql`
349
+ const rows = (await (await this.home.registry()).execute(sql`
333
350
  SELECT
334
351
  b.name,
335
352
  b.db_name,
@@ -348,6 +365,7 @@ var BranchService = class {
348
365
  } catch {}
349
366
  return {
350
367
  name: row.name,
368
+ database: row.db_name,
351
369
  parentDatabase: row.parent_db,
352
370
  createdAt: new Date(row.created_at),
353
371
  sizeBytes
@@ -357,4 +375,4 @@ var BranchService = class {
357
375
  //#endregion
358
376
  export { BranchService_exports as n, BranchingUnsupportedError as r, BranchService as t };
359
377
 
360
- //# sourceMappingURL=BranchService-DGPL6G_C.js.map
378
+ //# sourceMappingURL=BranchService-Bsn2wfaj.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BranchService-Bsn2wfaj.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/**\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 * 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 if (options?.force) {\n const closed = await this.terminateConnections(sourceDb);\n if (closed > 0) {\n // Postgres reports the backend as gone before its slot is\n // actually free, and CREATE DATABASE then fails on a connection\n // that no longer exists. One short settle beats a confusing retry.\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n }\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 try {\n await this.db.execute(\n sql.raw(`CREATE DATABASE \"${safeDbName}\" TEMPLATE \"${safeSourceDb}\"`)\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}\" has active connections.\\n`\n + await this.describeBlockingConnections(sourceDb)\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 if (options?.force) {\n const closed = await this.terminateConnections(dbName);\n if (closed > 0) await new Promise((resolve) => setTimeout(resolve, 250));\n }\n\n // Drop the database\n const safeDbName = dbName.replace(/\"/g, '\"\"');\n try {\n await this.db.execute(sql.raw(`DROP DATABASE \"${safeDbName}\"`));\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 has active connections.\\n`\n + await this.describeBlockingConnections(dbName)\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): 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 return ` Connected right now:\\n${listed}\\n`\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 \" Close other clients and connections, or re-run with --force.\";\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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"}
@@ -3,7 +3,7 @@ import { BranchService } from "./services/BranchService.js";
3
3
  import { RealtimeService, type SubscriptionAuthContext } from "./services/realtimeService.js";
4
4
  import { DatabasePoolManager } from "./databasePoolManager.js";
5
5
  import { DrizzleClient } from "./interfaces.js";
6
- import { DatabaseAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseCallContext, RebaseServerClient, RebaseSdkData, RestFetchService, BatchWriteProps, SaveManyProps, SaveProps, UpdateManyProps, UpdateRelationPivotProps, DeleteManyProps, TableMetadata, User } from "@rebasepro/types";
6
+ import { DatabaseAdmin, SQLAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseCallContext, RebaseServerClient, RebaseSdkData, RestFetchService, BatchWriteProps, SaveManyProps, SaveProps, SqlCursor, SqlScriptResult, UpdateManyProps, UpdateRelationPivotProps, DeleteManyProps, TableMetadata, User } from "@rebasepro/types";
7
7
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry.js";
8
8
  import { HistoryService } from "./history/HistoryService.js";
9
9
  /**
@@ -20,6 +20,16 @@ import { HistoryService } from "./history/HistoryService.js";
20
20
  * not be fixed alone and left disagreeing with the rest.
21
21
  */
22
22
  export declare function isRoleSwitchingOptedOut(): boolean;
23
+ /**
24
+ * The main database, when this process serves a branch of it.
25
+ *
26
+ * `rebase dev` on a checkout switched to a branch connects to the branch, and
27
+ * names the database it was branched from in `REBASE_BRANCH_PARENT_DATABASE`.
28
+ * Branches are kept there — the registry, and the default source of a new
29
+ * one — as they are when the main database itself is served: a branch's own
30
+ * `rebase.branches` is a snapshot, taken before its row was written.
31
+ */
32
+ export declare function branchParentDatabase(): string | undefined;
23
33
  /**
24
34
  * The role a statement will actually have run as, given the role it asked for.
25
35
  *
@@ -31,6 +41,17 @@ export declare function isRoleSwitchingOptedOut(): boolean;
31
41
  export declare function effectiveSqlRole(requestedRole?: string): string;
32
42
  /** How {@link effectiveSqlRole} names "whatever role the connection holds". */
33
43
  export declare const CONNECTION_OWNER = "<connection owner>";
44
+ /**
45
+ * The role a console statement ran as, given the role it asked for — said to
46
+ * the console with its rows, so it can say when that is not the role picked.
47
+ *
48
+ * The requested role whenever role switching is on: a statement that cannot
49
+ * assume its role is refused, never run as anyone else. With
50
+ * `DISABLE_DB_ROLE_SWITCHING`, every statement runs as the connection owner,
51
+ * named by asking a session of its own on the same database — a role the
52
+ * session already held is then still the one requested.
53
+ */
54
+ export declare function sqlRoleRanAs(admin: Pick<SQLAdmin, "executeSql">, requestedRole: string, database: string | undefined): Promise<string>;
34
55
  /**
35
56
  * A statement named a database role the connection cannot assume.
36
57
  *
@@ -51,6 +72,8 @@ export declare class PostgresBackendDriver implements DataDriver {
51
72
  poolManager?: DatabasePoolManager | undefined;
52
73
  key: string;
53
74
  initialised: boolean;
75
+ /** `save` and `delete` read the row they address and run a caller's precondition on it. */
76
+ readonly checksRowPreconditions = true;
54
77
  dataService: DataService;
55
78
  realtimeService: RealtimeService;
56
79
  historyService?: HistoryService;
@@ -105,6 +128,19 @@ export declare class PostgresBackendDriver implements DataDriver {
105
128
  * allowing test spies applied after construction to take effect.
106
129
  */
107
130
  get admin(): DatabaseAdmin;
131
+ /**
132
+ * `SQLAdmin.listen`: a `LISTEN` on a connection of its own, through the
133
+ * listener CDC and the channel bus use, which reconnects and heartbeats.
134
+ *
135
+ * The connection is found the way the realtime listener finds its own:
136
+ * `DATABASE_DIRECT_URL` when set — a transaction-mode pooler such as
137
+ * PgBouncer does not keep a `LISTEN` — else the pool's own URL. The
138
+ * in-process development database has neither, and is refused, as is a
139
+ * first connect that fails: the caller polls instead.
140
+ */
141
+ listen(channel: string, onNotify: (payload: string) => void, options?: {
142
+ onReconnect?: () => void;
143
+ }): Promise<() => Promise<void>>;
108
144
  /**
109
145
  * The catalogue-reading shim the schema planner wants.
110
146
  *
@@ -169,6 +205,13 @@ export declare class PostgresBackendDriver implements DataDriver {
169
205
  * collection's afterRead (so `post.author.email` is masked by the authors
170
206
  * collection, not left raw).
171
207
  */
208
+ /**
209
+ * One stored row as `restFetchService.fetchOneForRest` serves it: through
210
+ * `afterRead`, on a copy, so the row the write itself goes on with (its
211
+ * previous values, its history) stays the stored one. A precondition
212
+ * compares against what the caller was served — an `ETag` is a hash of that.
213
+ */
214
+ private asRestServes;
172
215
  applyAfterReadForRest(rows: Record<string, unknown>[], path: string): Promise<Record<string, unknown>[]>;
173
216
  fetchCollection<M extends Record<string, unknown>>({ path, collection, startAfter, ...query }: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;
174
217
  /**
@@ -187,7 +230,7 @@ export declare class PostgresBackendDriver implements DataDriver {
187
230
  * `null` when it is not there — deleted, never created, or not readable by
188
231
  * the subscriber. Reads as {@link listenCollection} does.
189
232
  */
190
- listenOne<M extends Record<string, unknown>>({ path, id, onUpdate, onError }: ListenOneProps<M>, authContext?: SubscriptionAuthContext | undefined): () => void;
233
+ listenOne<M extends Record<string, unknown>>({ path, id, include, onUpdate, onError }: ListenOneProps<M>, authContext?: SubscriptionAuthContext | undefined): () => void;
191
234
  /**
192
235
  * How many membership rows a single write will read to answer "which
193
236
  * tenants is this caller in".
@@ -223,27 +266,43 @@ export declare class PostgresBackendDriver implements DataDriver {
223
266
  * collection is readable by its own members in the first place.
224
267
  */
225
268
  private resolveCallerTenants;
269
+ /**
270
+ * Where an update reads the row it addresses before writing it.
271
+ *
272
+ * The path itself, except through a many-to-many relation: there the path
273
+ * names the parent's set, and an update may *link* a row that exists but is
274
+ * not in that set yet (the persistence layer attaches it). Read through the
275
+ * path, such a row is absent, so it is read from its own collection.
276
+ */
277
+ private storedRowPath;
226
278
  /**
227
279
  * The stored row an upsert's key names, if this caller can address it —
228
280
  * with its address and the values to update it with.
229
281
  *
230
- * Read as the caller reads: their policies and `beforeQuery` scope, and
231
- * soft-deleted rows hidden, so a row found here is one an update may
232
- * target. On the trusted plane for its fields only, like
233
- * {@link resolveCallerTenants}: the key columns are the server's to see.
282
+ * Read as the caller reads: their policies and `beforeQuery` scope. On the
283
+ * trusted plane for its fields only, like {@link resolveCallerTenants}:
284
+ * the key columns are the server's to see.
285
+ *
286
+ * Soft-deleted rows are read too, and a key one of them holds is refused
287
+ * with `ROW_IN_TRASH` (see {@link rowInTrashError}). Hidden, it was not
288
+ * found here, the write went down the create pipeline, and the INSERT's
289
+ * conflict branch wrote into the trashed row: `201 Created`, hooks told
290
+ * "new", a second `create` in history, and the row still in the trash.
234
291
  *
235
292
  * `undefined` when the key is incomplete (the write is a plain insert,
236
293
  * which is the persistence layer's rule too), when a key value is not a
237
294
  * plain value, or when no such row is visible. None of those can write a
238
295
  * stored row wrongly: the statement's own conflict branch sets only what
239
- * the caller sent, and only on a row in their scope.
296
+ * the caller sent, only on a row in their scope, and never on one in the
297
+ * trash.
240
298
  *
241
299
  * The values drop the key and the conflict target, which the stored row
242
300
  * already has: a natural-key upsert carrying a key the caller invented
243
301
  * must not move the row onto it.
244
302
  */
245
303
  private findUpsertTarget;
246
- save<M extends Record<string, unknown>>({ path, id, values, collection, status, upsert, onConflict }: SaveProps<M>): Promise<Record<string, unknown>>;
304
+ save<M extends Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>>;
305
+ private saveInFrame;
247
306
  /**
248
307
  * Write many rows through the same pipeline as {@link save}.
249
308
  *
@@ -256,6 +315,17 @@ export declare class PostgresBackendDriver implements DataDriver {
256
315
  * Rows are applied in order, so a batch that touches the same key twice ends
257
316
  * with the last write winning, exactly as separate calls would.
258
317
  */
318
+ /**
319
+ * Run a batch in one transaction of its own, on a driver bound to it, and
320
+ * tell realtime subscribers about its rows only once it has committed.
321
+ *
322
+ * Inside a request's transaction the events already wait for that one to
323
+ * commit, and the batch adds to its list. On the server's own driver they
324
+ * did not wait at all: each row was announced as it was written, before
325
+ * the COMMIT — a subscriber's refetch found nothing yet, and a batch that
326
+ * then rolled back had announced rows that never existed.
327
+ */
328
+ private inBatchTransaction;
259
329
  private bindToTransaction;
260
330
  /**
261
331
  * The callback context for work already running inside `tx` on behalf of
@@ -318,11 +388,40 @@ export declare class PostgresBackendDriver implements DataDriver {
318
388
  * body that cannot work never costs a rollback.
319
389
  */
320
390
  batchWrite<M extends Record<string, unknown>>({ operations }: BatchWriteProps<M>): Promise<(Record<string, unknown> | null)[]>;
321
- delete<M extends Record<string, unknown>>({ row, collection, hard }: DeleteProps<M>): Promise<void>;
391
+ delete<M extends Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;
392
+ private deleteInFrame;
393
+ /**
394
+ * The child lists a save rewrote through the parent, as notifications —
395
+ * when nothing else will announce them.
396
+ *
397
+ * `save("posts", "p-1", { tags: [...] })` rewrites the junction, and
398
+ * `{ comments: [...] }` on a `hasMany` re-points the children's foreign
399
+ * keys: either way the contents of `posts/p-1/<relation>` changed, and the
400
+ * row notification names the parent, whose paths land on `posts`. Under
401
+ * change capture the junction's or the children's own triggers announce
402
+ * it. Without capture these did not reach anyone: a live list of a post's
403
+ * tags kept the old set. Addressed to every row of the list, which also
404
+ * reaches the target collection's own lists — a `hasMany` changed those
405
+ * rows' foreign keys.
406
+ */
407
+ private childListNotifications;
322
408
  deleteAll(path: string): Promise<void>;
323
409
  updateRelationPivot({ path, targetId, pivot }: UpdateRelationPivotProps): Promise<void>;
324
410
  checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean>;
325
411
  count<M extends Record<string, unknown>>({ path, collection, filter, logical, searchString, vectorSearch, withDeleted }: FetchCollectionProps<M>): Promise<number>;
412
+ /** The answer to {@link appDatabaseName}, once it has been given. */
413
+ private appDatabase?;
414
+ /**
415
+ * The database this driver's own connection is on: the app's, which every
416
+ * data request and the CMS read and write.
417
+ *
418
+ * Asked of the connection rather than read off the admin connection
419
+ * string, which can name another database — the server's `postgres`, or
420
+ * the main database while this process runs on a branch. The SQL console
421
+ * preselects this name, a statement sent to it runs on this connection,
422
+ * and branching copies it by default.
423
+ */
424
+ appDatabaseName(): Promise<string>;
326
425
  private getTargetDb;
327
426
  /**
328
427
  * Build one statement, binding `$n` placeholders as real parameters.
@@ -337,7 +436,40 @@ export declare class PostgresBackendDriver implements DataDriver {
337
436
  role?: string;
338
437
  params?: unknown[];
339
438
  isolateSession?: boolean;
439
+ maxResultBytes?: number;
340
440
  }): Promise<Record<string, unknown>[]>;
441
+ /**
442
+ * Run a script a person wrote — the Studio console — and describe its
443
+ * result. See `SQLAdmin.runSqlScript` and `services/sql-script.ts`.
444
+ */
445
+ runSqlScript(sqlText: string, options?: {
446
+ database?: string;
447
+ role?: string;
448
+ signal?: AbortSignal;
449
+ }): Promise<SqlScriptResult>;
450
+ /**
451
+ * Read the whole result of one query a person wrote, a batch at a time —
452
+ * see `SQLAdmin.openSqlCursor` and `openSqlCursorOnPool`. The database's
453
+ * refusal of the query (a write, a second statement, a syntax error, a
454
+ * role it may not take) is the caller's to read, as on the socket.
455
+ */
456
+ openSqlCursor(sqlText: string, options?: {
457
+ database?: string;
458
+ role?: string;
459
+ batchSize?: number;
460
+ }): Promise<SqlCursor>;
461
+ /**
462
+ * `SET ROLE` for the rest of a session of a script's own — not `SET LOCAL`
463
+ * inside a transaction, which a `COMMIT` in the script ends, and every
464
+ * statement after it ran as the connection owner. The session is reset
465
+ * after the script, so the role does not outlive it.
466
+ *
467
+ * The same refusals as {@link executeSqlOn}: a role the session already
468
+ * has is no switch; `DISABLE_DB_ROLE_SWITCHING` runs as the owner on
469
+ * purpose; and a connection that may not `SET ROLE` refuses, never falls
470
+ * back to the owner.
471
+ */
472
+ private assumeSessionRole;
341
473
  /**
342
474
  * Run `fn` on a session of its own, and put that session back the way it
343
475
  * was handed out before anything else can use it.
@@ -350,10 +482,15 @@ export declare class PostgresBackendDriver implements DataDriver {
350
482
  * connection in N, until a restart. `SET SESSION AUTHORIZATION DEFAULT`
351
483
  * clears the role as well, which `RESET ALL` does not.
352
484
  *
353
- * A connection that cannot be reset — the statement left a transaction
354
- * open or aborted — is destroyed rather than returned. A handle that is
355
- * already one session (PGlite in process, a single client) is reset in
356
- * place.
485
+ * On a pool, `role` is assumed for the whole session, and a transaction
486
+ * the SQL leaves open is rolled back and the run refused — see
487
+ * `onOwnSession`. `fn` is then handed no role: there is nothing left to
488
+ * switch. A handle that is already one session (PGlite in process, a
489
+ * single client) is reset in place, and `fn` switches the role itself.
490
+ *
491
+ * With `maxResultBytes`, a result past it is refused — see
492
+ * `readWithinBudget`: on a pool the read stops there, in process the rows
493
+ * are measured once read.
357
494
  */
358
495
  private onIsolatedSession;
359
496
  private executeSqlOn;
@@ -400,6 +537,8 @@ export declare class AuthenticatedPostgresBackendDriver implements DataDriver {
400
537
  delegate: PostgresBackendDriver;
401
538
  key: string;
402
539
  initialised: boolean;
540
+ /** Its delegate's, run inside this caller's transaction. */
541
+ readonly checksRowPreconditions = true;
403
542
  user: User;
404
543
  data: RebaseSdkData;
405
544
  constructor(delegate: PostgresBackendDriver, user: User);
@@ -408,6 +547,20 @@ export declare class AuthenticatedPostgresBackendDriver implements DataDriver {
408
547
  */
409
548
  admin: DatabaseAdmin;
410
549
  get restFetchService(): RestFetchService;
550
+ /**
551
+ * Several reads in one read-only transaction — see
552
+ * {@link DataDriver.readTogether}. The same transaction every read on this
553
+ * driver opens for itself, with the same identity, role and field viewer;
554
+ * `work` is handed the transaction's driver, whose reads run on it directly
555
+ * instead of each opening another.
556
+ *
557
+ * `REPEATABLE READ`, because the point of reading together is reading one
558
+ * snapshot: under the default `READ COMMITTED` each statement takes its
559
+ * own, and a count could still describe a different set from the page
560
+ * beside it. A read-only transaction cannot fail serialization at this
561
+ * level, so it costs nothing.
562
+ */
563
+ readTogether<T>(work: (driver: DataDriver) => Promise<T>): Promise<T>;
411
564
  private withTransaction;
412
565
  fetchCollection<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;
413
566
  /**
@@ -1,5 +1,17 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import type { CollectionConfig } from "@rebasepro/types";
3
+ /**
4
+ * One statement that drops each named index — but only one that covers a
5
+ * single column that a valid, unique, non-partial btree on the same table also
6
+ * leads with. That is the UNIQUE (token_hash) constraint's index, which serves
7
+ * every `token_hash = $1` lookup; a table that somehow lacks it keeps its
8
+ * plain index, so no lookup is ever left without one.
9
+ *
10
+ * `DROP INDEX` takes the table's ACCESS EXCLUSIVE lock, on the tables every
11
+ * sign-in and token refresh touches, so it runs under the boot lock budget
12
+ * (`lock_timeout`, transaction-local to this statement — see `boot-ddl`).
13
+ */
14
+ export declare function dropDuplicateTokenHashIndexesSql(schema: string, names: readonly string[]): string;
3
15
  /**
4
16
  * Auto-create auth tables if they don't exist.
5
17
  *