@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.g19596ef

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 (193) hide show
  1. package/README.md +16 -0
  2. package/dist/{BranchService-BRt78gfa.js → BranchService-UgKa416x.js} +14 -3
  3. package/dist/BranchService-UgKa416x.js.map +1 -0
  4. package/dist/PostgresBackendDriver.d.ts +104 -2
  5. package/dist/PostgresBootstrapper.d.ts +101 -2
  6. package/dist/auth/ensure-tables.d.ts +12 -0
  7. package/dist/auth/services.d.ts +6 -0
  8. package/dist/backup/backup-cli.d.ts +75 -2
  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-DW5p9_zv.js → backup-cli-yiKd1GsC.js} +168 -75
  12. package/dist/backup-cli-yiKd1GsC.js.map +1 -0
  13. package/dist/{backup-service-EwcDVG-8.js → backup-service-AZWc2wvI.js} +99 -28
  14. package/dist/backup-service-AZWc2wvI.js.map +1 -0
  15. package/dist/boot-ddl-B9ZmBJ75.js +105 -0
  16. package/dist/boot-ddl-B9ZmBJ75.js.map +1 -0
  17. package/dist/cli-console-DGee3Zl7.js +225 -0
  18. package/dist/cli-console-DGee3Zl7.js.map +1 -0
  19. package/dist/cli-console.d.ts +39 -0
  20. package/dist/cli-errors-oVUuD8k0.js +2180 -0
  21. package/dist/cli-errors-oVUuD8k0.js.map +1 -0
  22. package/dist/cli-errors.d.ts +91 -9
  23. package/dist/{cli-flags-BglvpjHv.js → cli-flags-DnPHXjRH.js} +56 -6
  24. package/dist/cli-flags-DnPHXjRH.js.map +1 -0
  25. package/dist/cli-flags.d.ts +15 -0
  26. package/dist/cli-helpers.d.ts +76 -22
  27. package/dist/cli-output.d.ts +16 -28
  28. package/dist/{arg-Dni7MzLB.js → cli-prompt-egtNKkZl.js} +49 -3
  29. package/dist/cli-prompt-egtNKkZl.js.map +1 -0
  30. package/dist/cli-prompt.d.ts +19 -0
  31. package/dist/cli-push.d.ts +87 -0
  32. package/dist/cli-scratch-database.d.ts +33 -8
  33. package/dist/cli.js +764 -222
  34. package/dist/cli.js.map +1 -1
  35. package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-D_V4oaZh.js} +11 -5
  36. package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-D_V4oaZh.js.map} +1 -1
  37. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C938nR3E.js} +25 -113
  38. package/dist/connection-C938nR3E.js.map +1 -0
  39. package/dist/connection.d.ts +13 -2
  40. package/dist/databasePoolManager-DZgnBOgj.js +119 -0
  41. package/dist/databasePoolManager-DZgnBOgj.js.map +1 -0
  42. package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-BqmEHGaD.js} +485 -347
  43. package/dist/deferred-index-build-BqmEHGaD.js.map +1 -0
  44. package/dist/{doctor-C-sYWbmt.js → doctor-CUAAOFZm.js} +65 -54
  45. package/dist/doctor-CUAAOFZm.js.map +1 -0
  46. package/dist/ensure-collection-policies-Bb51V2OV.js +541 -0
  47. package/dist/ensure-collection-policies-Bb51V2OV.js.map +1 -0
  48. package/dist/ensure-collection-tables-egjGBjhv.js +532 -0
  49. package/dist/ensure-collection-tables-egjGBjhv.js.map +1 -0
  50. package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-DMPWhJ6R.js} +174 -78
  51. package/dist/ensure-tables-DMPWhJ6R.js.map +1 -0
  52. package/dist/enum-labels-kept-Msq5gMcb.js +56 -0
  53. package/dist/enum-labels-kept-Msq5gMcb.js.map +1 -0
  54. package/dist/generate-drizzle-schema-DrA-YgXv.js +57 -0
  55. package/dist/generate-drizzle-schema-DrA-YgXv.js.map +1 -0
  56. package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-BaT0H7Gq.js} +6 -6
  57. package/dist/generate-drizzle-schema-logic-BaT0H7Gq.js.map +1 -0
  58. package/dist/generate-files-OoRJLEdk.js +199 -0
  59. package/dist/generate-files-OoRJLEdk.js.map +1 -0
  60. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B4k9djAV.js} +4 -4
  61. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B4k9djAV.js.map} +1 -1
  62. package/dist/{generated-schema-staleness-Di9c4ZxN.js → generated-schema-staleness-BWN_PsMi.js} +2 -2
  63. package/dist/{generated-schema-staleness-Di9c4ZxN.js.map → generated-schema-staleness-BWN_PsMi.js.map} +1 -1
  64. package/dist/generated-sql.d.ts +8 -2
  65. package/dist/history/HistoryService.d.ts +64 -5
  66. package/dist/history/ensure-history-table.d.ts +63 -4
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.es.js +139 -19236
  69. package/dist/index.es.js.map +1 -1
  70. package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-BCc1Jym2.js} +136 -9
  71. package/dist/introspect-db-logic-BCc1Jym2.js.map +1 -0
  72. package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-BhhJg0b8.js} +48 -1
  73. package/dist/introspect-db-queries-BhhJg0b8.js.map +1 -0
  74. package/dist/open-rebase-CAjWpPjs.js +20895 -0
  75. package/dist/open-rebase-CAjWpPjs.js.map +1 -0
  76. package/dist/open-rebase.d.ts +94 -0
  77. package/dist/pg-error-utils-Bs_Ue4wn.js +395 -0
  78. package/dist/pg-error-utils-Bs_Ue4wn.js.map +1 -0
  79. package/dist/{plan-schema-DU9exq6C.js → plan-schema-CsNgK0qi.js} +152 -42
  80. package/dist/plan-schema-CsNgK0qi.js.map +1 -0
  81. package/dist/{policy-drift-B-J2hhm0.js → policy-drift-BKC50y2p.js} +32 -8
  82. package/dist/policy-drift-BKC50y2p.js.map +1 -0
  83. package/dist/push-index-prebuild-C_qhyoJa.js +45 -0
  84. package/dist/push-index-prebuild-C_qhyoJa.js.map +1 -0
  85. package/dist/push-legacy-renames-5g9h3HOa.js +34 -0
  86. package/dist/push-legacy-renames-5g9h3HOa.js.map +1 -0
  87. package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CrfEe__B.js} +15 -10
  88. package/dist/render-ddl-CrfEe__B.js.map +1 -0
  89. package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-Bd-hJk08.js} +125 -1325
  90. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +1 -0
  91. package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-D7P4-ZYX.js} +130 -18
  92. package/dist/rls-enforcement-D7P4-ZYX.js.map +1 -0
  93. package/dist/{__vite-browser-external-BnuHet1e.js → rolldown-runtime-V7rFbMRe.js} +1 -8
  94. package/dist/schema/atlas-exclude.d.ts +42 -0
  95. package/dist/schema/auth-schema.d.ts +2 -2
  96. package/dist/schema/boot-ddl.d.ts +47 -0
  97. package/dist/schema/carved-out-migration.d.ts +5 -4
  98. package/dist/schema/collection-index.d.ts +45 -0
  99. package/dist/schema/column-plan-helpers.d.ts +5 -0
  100. package/dist/schema/deferred-index-build.d.ts +131 -0
  101. package/dist/schema/destructive-sql.d.ts +30 -1
  102. package/dist/schema/doctor-cli.js +110 -6
  103. package/dist/schema/doctor-cli.js.map +1 -1
  104. package/dist/schema/doctor-search-scale.d.ts +45 -0
  105. package/dist/schema/doctor.d.ts +20 -1
  106. package/dist/schema/ensure-collection-policies.d.ts +73 -35
  107. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  108. package/dist/schema/enum-labels-kept.d.ts +26 -0
  109. package/dist/schema/generate-drizzle-schema.js +1 -1
  110. package/dist/schema/generate-files.d.ts +44 -0
  111. package/dist/schema/generate-next-step.d.ts +0 -13
  112. package/dist/schema/generate-postgres-ddl.js +10 -81
  113. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  114. package/dist/schema/generated-schema-diff.d.ts +7 -0
  115. package/dist/schema/introspect-db-logic.d.ts +32 -0
  116. package/dist/schema/introspect-db-queries.d.ts +20 -1
  117. package/dist/schema/introspect-db-structure.d.ts +10 -0
  118. package/dist/schema/introspect-db.js +16 -20
  119. package/dist/schema/introspect-db.js.map +1 -1
  120. package/dist/schema/migrate-baseline.d.ts +33 -0
  121. package/dist/schema/migration-post-process.d.ts +34 -0
  122. package/dist/schema/plan/diff-plan.d.ts +126 -0
  123. package/dist/schema/plan/render-ddl.d.ts +2 -1
  124. package/dist/schema/plan/types.d.ts +8 -0
  125. package/dist/schema/push-index-prebuild.d.ts +25 -0
  126. package/dist/schema/push-legacy-renames.d.ts +20 -0
  127. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  128. package/dist/schema/undeclarable-constraints.d.ts +59 -0
  129. package/dist/security/rls-enforcement.d.ts +32 -4
  130. package/dist/services/BranchService.d.ts +7 -0
  131. package/dist/services/FetchService.d.ts +58 -8
  132. package/dist/services/PersistService.d.ts +15 -0
  133. package/dist/services/RelationService.d.ts +88 -3
  134. package/dist/services/cdc/CdcListener.d.ts +17 -6
  135. package/dist/services/cdc/trigger-cdc.d.ts +151 -27
  136. package/dist/services/channel-bus/index.d.ts +1 -1
  137. package/dist/services/channel-history.d.ts +15 -4
  138. package/dist/services/collection-helpers.d.ts +15 -0
  139. package/dist/services/field-viewer.d.ts +15 -1
  140. package/dist/services/pg-notify-listener.d.ts +7 -0
  141. package/dist/services/realtimeService.d.ts +314 -14
  142. package/dist/services/row-pipeline.d.ts +16 -9
  143. package/dist/services/socket-liveness.d.ts +42 -2
  144. package/dist/services/sql-script.d.ts +207 -3
  145. package/dist/services/vector-iterative-scan.d.ts +40 -0
  146. package/dist/services/write-depth.d.ts +6 -0
  147. package/dist/services/write-transaction-scope.d.ts +9 -0
  148. package/dist/socket-opt-ins.d.ts +64 -0
  149. package/dist/source-DB6xJv3p.js +873 -0
  150. package/dist/source-DB6xJv3p.js.map +1 -0
  151. package/dist/tsx-loader.d.ts +14 -0
  152. package/dist/utils/connection-string.d.ts +31 -0
  153. package/dist/utils/drizzle-conditions.d.ts +21 -1
  154. package/dist/utils/error-cause.d.ts +7 -0
  155. package/dist/utils/pg-error-utils.d.ts +2 -4
  156. package/dist/utils/result-rows.d.ts +10 -0
  157. package/dist/{vector-index-CMJKb3C9.js → vector-index-BFhyEaiz.js} +2 -2
  158. package/dist/{vector-index-CMJKb3C9.js.map → vector-index-BFhyEaiz.js.map} +1 -1
  159. package/dist/websocket-DUiiwQd2.js +1632 -0
  160. package/dist/websocket-DUiiwQd2.js.map +1 -0
  161. package/dist/websocket.d.ts +13 -0
  162. package/package.json +7 -7
  163. package/dist/BranchService-BRt78gfa.js.map +0 -1
  164. package/dist/__vite-browser-external-BnuHet1e.js.map +0 -1
  165. package/dist/arg-Dni7MzLB.js.map +0 -1
  166. package/dist/backup-cli-DW5p9_zv.js.map +0 -1
  167. package/dist/backup-service-EwcDVG-8.js.map +0 -1
  168. package/dist/cli-errors-DsA-K9uP.js +0 -893
  169. package/dist/cli-errors-DsA-K9uP.js.map +0 -1
  170. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  171. package/dist/cli-output-CNdMql-L.js +0 -47
  172. package/dist/cli-output-CNdMql-L.js.map +0 -1
  173. package/dist/collection-index-DdnsxiJ_.js +0 -286
  174. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  175. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  176. package/dist/doctor-C-sYWbmt.js.map +0 -1
  177. package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
  178. package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
  179. package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
  180. package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
  181. package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
  182. package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
  183. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
  184. package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
  185. package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
  186. package/dist/plan-schema-DU9exq6C.js.map +0 -1
  187. package/dist/policy-drift-B-J2hhm0.js.map +0 -1
  188. package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
  189. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
  190. package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
  191. package/dist/source-Br7L7GOI.js +0 -410
  192. package/dist/source-Br7L7GOI.js.map +0 -1
  193. package/dist/utils/sql-redaction.d.ts +0 -21
@@ -31,8 +31,9 @@ import { REBASE_USER_ROLE } from "@rebasepro/common";
31
31
  * SELECT/INSERT/UPDATE/DELETE grants (+ default privileges so future
32
32
  * tables stay covered).
33
33
  * 3. {@link applyAuthContext} — per-transaction: set the `app.*` GUCs the
34
- * policies read (`rebase.uid()` etc.) and `SET LOCAL ROLE rebase_user` so
35
- * RLS binds. Transaction-scoped, so it composes with poolers.
34
+ * policies read (`rebase.uid()` etc.) and the transaction-local role
35
+ * `rebase_user` so RLS binds, in one statement. Transaction-scoped, so it
36
+ * composes with poolers.
36
37
  *
37
38
  * Provisioning runs from the framework's own bootstrap/migrate (which already
38
39
  * self-creates the `auth` schema and functions) — enforcement is default-on,
@@ -203,11 +204,38 @@ export declare function appRoleSetupInstructions(connectionRole: string, schemas
203
204
  * connection cannot create it.
204
205
  */
205
206
  export declare function ensureAppRole(run: RawSqlRunner, schemas: string[]): Promise<void>;
207
+ /**
208
+ * The grant tier of {@link ensureAppRole} on its own: `USAGE` on each existing
209
+ * schema in `schemas`, DML on its tables and `USAGE, SELECT` on its
210
+ * sequences, the default privileges that cover what this role creates later,
211
+ * and Rebase's own tables taken back — for a role and membership already
212
+ * provisioned.
213
+ *
214
+ * Every statement is issued only when the catalogue says it would change
215
+ * something. They used to run unconditionally, twice per boot (at connect, and
216
+ * again in the final posture check): a `GRANT … ON ALL TABLES` rewrites every
217
+ * table's `pg_class` row whether or not the ACL changes, which invalidates
218
+ * every table's cached plans on every connection, and the internal-table sweep
219
+ * ran 23 `REVOKE`s per schema. A boot against a database whose grants already
220
+ * hold now reads one catalogue query and writes nothing; one that is missing
221
+ * any of them runs exactly the statements it always ran.
222
+ *
223
+ * Table owners (the expected non-superuser posture) can always grant on their
224
+ * own objects, so this needs no extra privilege.
225
+ */
226
+ export declare function ensureAppRoleGrants(run: RawSqlRunner, schemas: string[]): Promise<void>;
227
+ /**
228
+ * Rebase's own RLS-off tables in `schema` that a `revokeInternalTableAccess`
229
+ * sweep would change — the user role holds a privilege on them, or their ACL
230
+ * has never been written. One read; the sweep it narrows is one `DO` per
231
+ * table name.
232
+ */
233
+ export declare function internalTablesGrantedToUserRole(run: RawSqlRunner, schema: string): Promise<string[]>;
206
234
  /**
207
235
  * Apply the authenticated context to a transaction: the `app.*` GUCs that RLS
208
236
  * policies read via `rebase.uid()` / `rebase.roles()` / `rebase.jwt()`, and — when
209
- * `userRole` is set — `SET LOCAL ROLE` so RLS binds every statement in this
210
- * transaction (reads *and* writes).
237
+ * `userRole` is set — the `LOCAL` role switch, in the same statement, so RLS
238
+ * binds every statement in this transaction (reads *and* writes).
211
239
  *
212
240
  * GUCs are set with `is_local = true` and the role switch is `LOCAL`: both
213
241
  * reset at commit/rollback, so pooled connections are never polluted.
@@ -61,6 +61,13 @@ export declare class BranchService {
61
61
  * pane into a failure with a "Try again" that can never succeed.
62
62
  */
63
63
  private assertBranchingSupported;
64
+ /**
65
+ * Whether the `rebase.branches` registry exists. A database that has never
66
+ * had a branch made on it may not have it, and reading it there is an
67
+ * empty answer, not an error — nor a reason to create it. The CLI's `list`
68
+ * and `info` ask this rather than running the DDL that creates it.
69
+ */
70
+ hasBranchRegistry(): Promise<boolean>;
64
71
  /**
65
72
  * Ensure the `rebase.branches` metadata table exists in the default database.
66
73
  * Idempotent — safe to call on every startup.
@@ -1,5 +1,5 @@
1
1
  import { SQL } from "drizzle-orm";
2
- import { PgTable } from "drizzle-orm/pg-core";
2
+ import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
3
  import { FilterValues, OrderByTuple, LogicalCondition } from "@rebasepro/types";
4
4
  import type { IncludeSpec, VectorSearchParams } from "@rebasepro/types";
5
5
  import { RelationService } from "./RelationService.js";
@@ -218,6 +218,18 @@ export declare class FetchService {
218
218
  * no related row, so a typo in an `include` looked like empty data.
219
219
  */
220
220
  private resolveIncludeLevel;
221
+ /**
222
+ * Resolve every level of an include tree against the collections, without
223
+ * loading anything — the 400 for an unknown relation, decided before the
224
+ * read rather than by it.
225
+ *
226
+ * {@link loadIncludes} resolves a level only once it has rows to load it
227
+ * onto. On its own that made the refusal depend on the data: a typo in
228
+ * `include` against an empty table, under a filter that matched nothing,
229
+ * or below a relation that loaded no rows answered 200, and the same
230
+ * request became a 400 the first time a row matched.
231
+ */
232
+ private assertIncludeResolvable;
221
233
  /**
222
234
  * The extra `WHERE` an include's own `where`/`logical` puts on the target.
223
235
  *
@@ -227,14 +239,25 @@ export declare class FetchService {
227
239
  */
228
240
  private includeNarrowing;
229
241
  /**
230
- * Sort, cap and project the rows of ONE to-many relation, per parent.
242
+ * Refuse a `count` the loader cannot serve honestly, before anything runs.
231
243
  *
232
- * `orderBy` and `limit` are applied here rather than in SQL because a limit
233
- * on a batched relation load is *per parent*, and expressing that needs a
234
- * LATERAL or a window function per relation kind — four of them, each with
235
- * its own join shape. The rows were already narrowed by
236
- * {@link includeNarrowing} in SQL, so what this sorts and cuts is the set
237
- * the caller asked for and not the table.
244
+ * - On a to-one relation: there is nothing to count past a row or none,
245
+ * and answering 0 or 1 under `_count` would invent a shape for it.
246
+ * - On a collection with a property named `_count`: the total would be
247
+ * written over that column's value. A silent overwrite answers 200 with
248
+ * the column gone, so it is a 400 naming why.
249
+ */
250
+ private assertCountsAllowed;
251
+ /**
252
+ * Cap and project the rows of ONE relation, per parent.
253
+ *
254
+ * The order and the limit are Postgres's: the batch loader ranks each
255
+ * parent's related rows by the include's `orderBy` and reads back only the
256
+ * ones within its `limit` (see
257
+ * {@link RelationService.batchFetchRelatedEntitiesMany}), so `rows`
258
+ * arrives sorted and already cut. The slice here is what holds the limit
259
+ * where a `via` chain reaches one target along two paths — those rows share
260
+ * a rank — and it costs nothing where nothing is left to cut.
238
261
  */
239
262
  private shapeRelatedRows;
240
263
  /**
@@ -646,6 +669,33 @@ export declare class FetchService {
646
669
  /** See `FetchCollectionProps.withDeleted`. */
647
670
  withDeleted?: WithDeleted;
648
671
  }): Promise<Record<string, unknown> | null>;
672
+ /**
673
+ * {@link fetchOneForRest}'s read of a row an INSERT just wrote, taken from
674
+ * the INSERT's own `RETURNING` instead of a second statement — or
675
+ * `undefined` when the two could answer differently.
676
+ *
677
+ * A write answers with `fetchOneForRest(slug, id, undefined, …, { withDeleted: true })`
678
+ * (see `PersistService.save`): the row's visible columns, rendered by
679
+ * `toRestRow`. For a row the statement itself wrote, with nothing written
680
+ * after it, `RETURNING` those same columns is that row — the same values
681
+ * through the same column decoders, in the same order, rendered the same
682
+ * way. Two things narrow the read and not the statement, and either one
683
+ * keeps the read:
684
+ *
685
+ * - a `beforeQuery` adds conditions to the read (and runs application
686
+ * code): a row outside them is no answer today, and would be one from
687
+ * `RETURNING`;
688
+ * - per-field read access, which projects the read for the viewer.
689
+ *
690
+ * Row-level security is the same either way. `INSERT … RETURNING` holds
691
+ * the new row to the table's SELECT policies and refuses one they hide
692
+ * (42501) — which the INSERT's `RETURNING <key>` already did, so returning
693
+ * more columns changes nothing about who may write or read what.
694
+ */
695
+ insertReturning(collectionPath: string): {
696
+ columns: Record<string, AnyPgColumn>;
697
+ render: (returned: Record<string, unknown>) => Record<string, unknown>;
698
+ } | undefined;
649
699
  /**
650
700
  * Fetch raw rows without any relation processing (for REST fast path)
651
701
  */
@@ -75,6 +75,21 @@ export declare class PersistService {
75
75
  * for why a zero-row write cannot be reported as success.
76
76
  */
77
77
  private explainZeroRowWrite;
78
+ /**
79
+ * Lock one row against every other write to it until this transaction
80
+ * ends: `FOR NO KEY UPDATE`, the lock an UPDATE of it takes anyway.
81
+ *
82
+ * For a conditional write (`If-Match`): taken before the row is read and
83
+ * compared, so a write that lands meanwhile is either already committed and
84
+ * read, or waits for this one. Without it two writes carrying the same tag
85
+ * both read the old row, both passed, and the second overwrote the first.
86
+ *
87
+ * Under row-level security the lock reads through the caller's UPDATE
88
+ * policies as well as their SELECT ones, so a row they may read but not
89
+ * update is simply not locked — and their write is refused by the database
90
+ * all the same. A nested address locks the row it ends in.
91
+ */
92
+ lockRow(collectionPath: string, id: string | number): Promise<void>;
78
93
  /**
79
94
  * Delete an row by ID
80
95
  */
@@ -1,5 +1,5 @@
1
1
  import { SQL } from "drizzle-orm";
2
- import { PgTable } from "drizzle-orm/pg-core";
2
+ import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
3
  import { DrizzleClient } from "../interfaces.js";
4
4
  import { CollectionConfig, FilterValues, OrderByTuple, ResolvedRelation, ResolvedHasMany, ResolvedHasOne } from "@rebasepro/types";
5
5
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
@@ -34,6 +34,52 @@ export declare function joinStepCondition(currentTable: PgTable, joinTable: PgTa
34
34
  } | {
35
35
  missing: string;
36
36
  };
37
+ /**
38
+ * What an include asks of ONE to-many relation's rows, per parent: how many,
39
+ * and in what order. See {@link RelationService.batchFetchRelatedEntitiesMany}.
40
+ */
41
+ export interface RelatedRowWindow {
42
+ /** Rows to keep per parent. */
43
+ limit?: number;
44
+ /** Their order, by the target's own columns. */
45
+ orderBy?: OrderByTuple[];
46
+ /**
47
+ * Filled, when given and a `limit` is set, with each parent's number of
48
+ * matching related rows *before* the limit — what `include: { x: { limit,
49
+ * count: true } }` serves under `_count`. Counted in the ranking subquery,
50
+ * so it is the same statement, the same conditions and the same RLS
51
+ * context as the rows. A parent with no related row is absent (zero).
52
+ * Without a limit nothing is cut, and the caller counts what it got.
53
+ */
54
+ totals?: Map<string, number>;
55
+ }
56
+ /**
57
+ * How the batch loaders render a target row.
58
+ *
59
+ * - `"admin"` — the view model the admin panel reads: declared types parsed
60
+ * (`parseDataFromServer`), the target's own to-one relations as
61
+ * `{ __type: "relation" }` refs, undeclared columns dropped.
62
+ * - `"rest"` — the row the target's own route serves (`toRestRow`): what an
63
+ * `include` embeds. The REST contract is that a developer never sees a ref,
64
+ * and an included row is rendered exactly as the same row is at the top
65
+ * level.
66
+ */
67
+ export type RelatedRowStyle = "admin" | "rest";
68
+ /**
69
+ * The order one parent's related rows come in: the include's own keys, then
70
+ * the target's key ascending.
71
+ *
72
+ * The key ascending is the order an unordered batch read handed rows back in
73
+ * on an append-only table, and so the rows a `limit` with no `orderBy` kept —
74
+ * stated now rather than left to the plan. A key that names no column of the
75
+ * target sorts nothing, as it sorted nothing when the sort ran over the
76
+ * returned rows in JavaScript; the caller's field access on it was already
77
+ * judged at the route.
78
+ *
79
+ * NULLs go where a top-level `orderBy` puts them: the stated placement, else
80
+ * Postgres's own (last ascending, first descending).
81
+ */
82
+ export declare function relatedRowOrder(targetTable: PgTable, targetKeys: AnyPgColumn[], orderBy: OrderByTuple[] | undefined): SQL[];
37
83
  /**
38
84
  * Service for handling all relation-related operations.
39
85
  * Handles fetching, updating, and managing row relations.
@@ -119,6 +165,21 @@ export declare class RelationService {
119
165
  * nobody was comparing.
120
166
  */
121
167
  private toRelatedRow;
168
+ /**
169
+ * A target row as its own REST route serves it, for the `"rest"` style.
170
+ *
171
+ * The loaders read the target with `SELECT *`, so the columns a REST read
172
+ * never selects — a generated search vector — are dropped first; then
173
+ * `toRestRow` renders the rest, which applies the target's field access.
174
+ * Built once per batch: the hidden set is a property of the table.
175
+ *
176
+ * It replaces a second pass. An include used to render each target row as
177
+ * the admin's view model (`parseDataFromServer`, which also minted a
178
+ * `{ __type: "relation" }` ref for the target's own to-one relations) and
179
+ * then re-render that for REST: 3.4 µs a row against 1.4 µs for the row
180
+ * itself, and an envelope in a response whose contract has none.
181
+ */
182
+ private restRendering;
122
183
  /**
123
184
  * The junction row's own columns, as the caller may see them.
124
185
  *
@@ -242,13 +303,37 @@ export declare class RelationService {
242
303
  * the rows that come back, because filtering afterwards reads every related
243
304
  * row of every parent in order to discard most of them.
244
305
  */
245
- batchFetchRelatedEntities(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation, narrow?: SQL): Promise<Map<string, RelatedRow<Record<string, unknown>>>>;
306
+ batchFetchRelatedEntities(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation, narrow?: SQL, options?: {
307
+ /** How each target row is rendered; see {@link RelatedRowStyle}. */
308
+ style?: RelatedRowStyle;
309
+ /**
310
+ * The parent rows themselves, in `parentIds` order, when the caller
311
+ * holds them. A `belongsTo` reads each one's foreign key off its
312
+ * row rather than selecting it again.
313
+ */
314
+ parentRows?: Record<string, unknown>[];
315
+ }): Promise<Map<string, RelatedRow<Record<string, unknown>>>>;
246
316
  /**
247
317
  * Batch fetch many-cardinality related rows for multiple parent rows.
248
318
  * Returns a Map<parentId, RelatedRow[]> instead of Map<parentId, RelatedRow>.
249
319
  * Uses a single SQL query with IN clause to avoid N+1.
320
+ *
321
+ * `window` is an include's own `limit` and `orderBy`, and both are applied
322
+ * by Postgres. A limit is *per parent*, so it is a rank within each
323
+ * parent's rows rather than a `LIMIT`: a subquery ranks the keys of every
324
+ * matching row — under the same relation condition, `narrow`, soft delete
325
+ * and `beforeQuery` the unlimited read applies — and the read joins back to
326
+ * the ones ranked within the limit. It used to read every related row of
327
+ * every parent and cut the list in JavaScript: 200 rows served out of
328
+ * 200,000 read, parsed and rendered for `{ messages: { limit: 2 } }` on a
329
+ * page of 100 threads. Rows come back in the window's order, so the
330
+ * per-parent lists are already sorted.
250
331
  */
251
332
  batchFetchRelatedEntitiesMany(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation,
252
333
  /** An extra condition on the target — see {@link batchFetchRelatedEntities}. */
253
- narrow?: SQL): Promise<Map<string, RelatedRow<Record<string, unknown>>[]>>;
334
+ narrow?: SQL, window?: RelatedRowWindow,
335
+ /** `style` — how each target row is rendered; see {@link RelatedRowStyle}. */
336
+ options?: {
337
+ style?: RelatedRowStyle;
338
+ }): Promise<Map<string, RelatedRow<Record<string, unknown>>[]>>;
254
339
  }
@@ -1,12 +1,13 @@
1
1
  import { type PgNotifyListenerStatus } from "../pg-notify-listener.js";
2
2
  /**
3
3
  * A single database change captured by the CDC triggers and delivered over the
4
- * `rebase_cdc` NOTIFY channel.
4
+ * `rebase_cdc` NOTIFY channel — one row of a notification, which may name many.
5
5
  */
6
6
  export interface CdcChangeEvent {
7
7
  schema: string;
8
8
  table: string;
9
- op: "INSERT" | "UPDATE" | "DELETE";
9
+ /** `TRUNCATE` changed every row, and `row` is empty. */
10
+ op: "INSERT" | "UPDATE" | "DELETE" | "TRUNCATE";
10
11
  /**
11
12
  * The changed row's identity, by column: its key, and for a junction table
12
13
  * the two ids naming the child list (from NEW for insert/update, OLD for
@@ -18,10 +19,20 @@ export interface CdcChangeEvent {
18
19
  truncated?: boolean;
19
20
  }
20
21
  /**
21
- * Parse a `rebase_cdc` NOTIFY payload. Returns `null` for anything malformed so
22
- * a single bad message can never crash the listener.
22
+ * Parse a `rebase_cdc` NOTIFY payload into the changes it names, one per row.
23
+ * Returns `null` for anything malformed so a single bad message can never
24
+ * crash the listener.
25
+ *
26
+ * Reads every shape a capture trigger sends, whichever version installed it —
27
+ * during a rolling deploy the database's triggers and this instance need not
28
+ * be the same version:
29
+ * - `{ schema, table, op, row }` — one row: the row trigger, and a statement
30
+ * that changed one row;
31
+ * - `{ schema, table, op, cols, keys }` — a statement's rows in a batch. With
32
+ * one column a key is the value; with several, an array in `cols` order;
33
+ * - `{ schema, table, op: "TRUNCATE" }`.
23
34
  */
24
- export declare function parseCdcPayload(payload: string): CdcChangeEvent | null;
35
+ export declare function parseCdcPayload(payload: string): CdcChangeEvent[] | null;
25
36
  /**
26
37
  * Dedicated Postgres LISTEN client for database-level CDC.
27
38
  *
@@ -38,7 +49,7 @@ export declare class CdcListener {
38
49
  * drop. Every change committed in the gap was notified to nobody, so
39
50
  * this is where subscribers are told to look again.
40
51
  */
41
- constructor(connectionString: string, onEvent: (event: CdcChangeEvent) => void | Promise<void>, onReconnect?: () => void);
52
+ constructor(connectionString: string, onEvents: (events: CdcChangeEvent[]) => void | Promise<void>, onReconnect?: () => void, applicationName?: string);
42
53
  /**
43
54
  * Connect and begin listening. Idempotent.
44
55
  *
@@ -6,9 +6,13 @@ import type { RawSqlRunner } from "../../security/rls-enforcement.js";
6
6
  * — like Supabase Realtime — sees *every* commit regardless of how it was made.
7
7
  * When logical replication is unavailable (managed Postgres without
8
8
  * `wal_level=logical`, no replication privilege, no `REPLICA IDENTITY`), this
9
- * trigger-based fallback provides the same guarantee at the row level:
9
+ * trigger-based fallback provides the same guarantee:
10
10
  *
11
- * AFTER INSERT/UPDATE/DELETE trigger → pg_notify('rebase_cdc', payload)
11
+ * AFTER INSERT/UPDATE/DELETE/TRUNCATE statement trigger → pg_notify('rebase_cdc', payload)
12
+ *
13
+ * once per statement, naming the changed rows by key in batches of about a
14
+ * thousand — a 1M-row UPDATE is ~1,000 notifications, where a row trigger sent
15
+ * a million into Postgres's shared notification queue, to every instance.
12
16
  *
13
17
  * A single dedicated LISTEN client per backend instance consumes the channel
14
18
  * (see {@link CdcListener}) and feeds the change into the existing
@@ -21,24 +25,88 @@ import type { RawSqlRunner } from "../../security/rls-enforcement.js";
21
25
  */
22
26
  /** Postgres NOTIFY channel carrying database-level change events. */
23
27
  export declare const CDC_CHANNEL = "rebase_cdc";
24
- /** Schema-qualified name of the generic trigger function. */
28
+ /**
29
+ * Schema-qualified name of the row-level trigger function: one notification
30
+ * per changed row. What every version before statement-level capture attached
31
+ * everywhere, and still what a partitioned or foreign table gets — see
32
+ * {@link levelForRelkind}.
33
+ */
25
34
  export declare const CDC_TRIGGER_FUNCTION = "rebase.rebase_cdc_notify";
26
- /** Name of the per-table trigger (unqualified — triggers are namespaced by table). */
35
+ /**
36
+ * Schema-qualified name of the statement-level trigger function: one
37
+ * notification per ~1,000 changed rows. See {@link buildCdcFunctionSql}.
38
+ *
39
+ * A function of its own rather than a new body for the row function, because
40
+ * an instance of an older version replaces the row function's body every time
41
+ * it boots: a statement trigger calling it would then run a row trigger's body,
42
+ * for every write, until the next boot of this version.
43
+ */
44
+ export declare const CDC_STATEMENT_FUNCTION = "rebase.rebase_cdc_notify_statement";
45
+ /** Name of the per-table row-level trigger (unqualified — triggers are namespaced by table). */
27
46
  export declare const CDC_TRIGGER_NAME = "rebase_cdc_trigger";
47
+ /** How a table is instrumented: one notification per statement's ~1,000 rows, or one per row. */
48
+ export type CdcTriggerLevel = "statement" | "row";
49
+ /** One trigger this module creates, as `pg_trigger` describes it. */
50
+ interface CdcTriggerSpec {
51
+ name: string;
52
+ /** The function it calls. */
53
+ fn: "row" | "statement";
54
+ /** `CREATE TRIGGER` between the name and `ON`. */
55
+ timing: string;
56
+ /** `CREATE TRIGGER` between the table and `EXECUTE`. */
57
+ forEach: string;
58
+ /** `tgtype`: ROW (1), BEFORE (2), INSERT (4), DELETE (8), UPDATE (16), TRUNCATE (32), INSTEAD (64). */
59
+ type: number;
60
+ /** `tgnewtable` / `tgoldtable`: the transition tables it captures. */
61
+ newTable: string | null;
62
+ oldTable: string | null;
63
+ }
64
+ /**
65
+ * The statement-level triggers a plain table gets. One per event, because
66
+ * Postgres refuses transition tables on a trigger with more than one event.
67
+ * The TRUNCATE one is new with them: a row trigger never fires on `TRUNCATE`,
68
+ * so a truncated table used to be announced to nobody.
69
+ */
70
+ export declare const CDC_STATEMENT_TRIGGERS: readonly CdcTriggerSpec[];
71
+ /**
72
+ * The level a table is instrumented at, by `pg_class.relkind`.
73
+ *
74
+ * A plain table gets statement triggers. A partitioned table keeps the row
75
+ * trigger: Postgres clones a row trigger onto every partition, so a write made
76
+ * straight to a partition is still captured, while a statement trigger on the
77
+ * parent fires only for statements against the parent. A foreign table
78
+ * cannot have transition tables at all.
79
+ */
80
+ export declare const levelForRelkind: (relkind: string | null | undefined) => CdcTriggerLevel;
81
+ /**
82
+ * How long after a boot first converts a table from the row trigger, the
83
+ * statement triggers still announce every change one row per notification.
84
+ *
85
+ * The triggers are the database's, and an instance of the previous version
86
+ * still running beside this one — a rolling deploy, a rollback — reads the
87
+ * same channel with a parser that knows one row per notification: given a
88
+ * batch it sees no key, so its lists refetch but its single-row and
89
+ * many-to-many subscribers hear nothing. While any such instance may be up,
90
+ * the statement triggers speak its protocol; after this window they batch.
91
+ * Every version that batches reads both. A boot of the older version re-attaches its row trigger (it
92
+ * creates a missing one), and the next boot of this one converts it back and
93
+ * re-opens the window, so a rollback is covered the same way.
94
+ */
95
+ export declare const CDC_ROW_COMPAT_MS: number;
28
96
  /**
29
- * SQL that (re)creates the generic CDC trigger function. Safe to run repeatedly:
97
+ * SQL that (re)creates both CDC trigger functions. Safe to run repeatedly:
30
98
  * `CREATE OR REPLACE` updates in place without dropping dependent triggers —
31
- * which is also how a database instrumented by an older version gets this body
32
- * on its next boot, for every table at once, re-attached or not.
33
- *
34
- * The function emits `{ schema, table, op, row }`, and `row` is an **identity,
35
- * never the tuple**. Postgres puts no privilege on `LISTEN`: any role that can
36
- * connect can listen on this channel, whatever it may `SELECT`. When `row` was
37
- * `to_jsonb(NEW)`, a login with no grant on anything received every changed row
38
- * of every instrumented table — `password_hash` and verification tokens from
39
- * the auth table included — past RLS and column grants. The consumer never
40
- * read anything but the key: every subscriber re-reads the row under its own
41
- * scope.
99
+ * which is also how a database instrumented by an older version gets these
100
+ * bodies on its next boot, for every table at once, re-attached or not.
101
+ *
102
+ * Both functions emit `{ schema, table, op, ... }`, and the rows are named by
103
+ * **identity, never the tuple**. Postgres puts no privilege on `LISTEN`: any
104
+ * role that can connect can listen on this channel, whatever it may `SELECT`.
105
+ * When `row` was `to_jsonb(NEW)`, a login with no grant on anything received
106
+ * every changed row of every instrumented table — `password_hash` and
107
+ * verification tokens from the auth table included — past RLS and column
108
+ * grants. The consumer never read anything but the key: every subscriber
109
+ * re-reads the row under its own scope.
42
110
  *
43
111
  * The identity is, in order:
44
112
  * - the columns the trigger was attached with (`TG_ARGV`) — the key the
@@ -49,18 +117,26 @@ export declare const CDC_TRIGGER_NAME = "rebase_cdc_trigger";
49
117
  * maps any more;
50
118
  * - otherwise `id`, if the table has one, and else nothing: a change the
51
119
  * consumer can only treat as collection-wide.
120
+ *
121
+ * `compatUntil` is the end of the window in which the statement function
122
+ * still sends one row per notification — see {@link CDC_ROW_COMPAT_MS}. A
123
+ * timestamp, or `-infinity` for none.
52
124
  */
53
- export declare function buildCdcFunctionSql(): string;
125
+ export declare function buildCdcFunctionSql(options?: {
126
+ compatUntil?: string;
127
+ }): string;
54
128
  /**
55
- * SQL that (re)attaches the CDC trigger to a single table. `DROP ... IF EXISTS`
56
- * before `CREATE` keeps it idempotent and picks up any function signature change.
129
+ * SQL that (re)attaches the CDC triggers to a single table, at `level`.
130
+ * Every trigger this module owns is dropped first, whichever level it was
131
+ * created at, so a table moves between levels — a row trigger left by an
132
+ * older version becomes the statement triggers — in one transaction, with no
133
+ * moment at which a write is announced by neither.
57
134
  *
58
- * `identityColumns` are what the payload names the changed row by — see
135
+ * `identityColumns` are what the payload names the changed rows by — see
59
136
  * {@link buildCdcFunctionSql}. Passed as trigger arguments, so one shared
60
- * function serves every table without a catalogue read per row. Empty means
61
- * "the primary key".
137
+ * function serves every table. Empty means "the primary key".
62
138
  */
63
- export declare function buildCdcTriggerSql(schema: string, table: string, identityColumns?: readonly string[]): string;
139
+ export declare function buildCdcTriggerSql(schema: string, table: string, identityColumns?: readonly string[], level?: CdcTriggerLevel): string;
64
140
  export interface CdcTableRef {
65
141
  schema: string;
66
142
  table: string;
@@ -80,7 +156,7 @@ export interface ProvisionResult {
80
156
  }>;
81
157
  }
82
158
  /**
83
- * Replace an already-installed trigger function with the current body, and
159
+ * Replace the already-installed trigger functions with the current bodies, and
84
160
  * install nothing where there is none.
85
161
  *
86
162
  * For a process that owns the schema but is not provisioning capture on this
@@ -88,15 +164,63 @@ export interface ProvisionResult {
88
164
  * earlier boot attached keep firing whether anything consumes them or not, so
89
165
  * a database instrumented by a version whose function put whole rows on the
90
166
  * channel kept doing so, for every login that cares to `LISTEN`, until capture
91
- * was switched back on. Returns whether a function was there to replace.
167
+ * was switched back on. The statement function keeps its compatibility
168
+ * window. Returns whether a function was there to replace.
92
169
  */
93
170
  export declare function refreshInstalledCdcFunction(run: RawSqlRunner): Promise<boolean>;
94
171
  /**
95
- * Idempotently install the CDC trigger function and per-table triggers.
172
+ * Take change capture off the database: detach every capture trigger and drop
173
+ * both functions. Returns how many triggers were attached.
174
+ *
175
+ * For `REALTIME_CDC=off`. Capture switched off used to leave the triggers in
176
+ * place, so every write to every instrumented table still ran one and sent a
177
+ * notification on a channel nothing listened to. Nothing depends on them once
178
+ * capture is off: a process without capture announces its own writes on the
179
+ * cross-instance channel, and the instances that serve realtime listen there.
180
+ *
181
+ * `DROP FUNCTION … CASCADE` takes the triggers with it in one statement. It
182
+ * waits for each table's lock like any trigger DDL, so it runs with the boot's
183
+ * lock budget and is retried on a lock timeout; a database still contended
184
+ * after that keeps its triggers and the caller reports it.
185
+ */
186
+ export declare function removeTriggerCdc(run: RawSqlRunner): Promise<number>;
187
+ /**
188
+ * What a boot that does not provision capture does with capture an earlier
189
+ * boot installed.
190
+ *
191
+ * - `capture: "off"` — `REALTIME_CDC=off` — removes it ({@link removeTriggerCdc}).
192
+ * - `capture: "not-provisioned"` — capture is wanted but this boot could not
193
+ * install it (no direct URL, `auto` fell back) — leaves the triggers and
194
+ * brings the functions up to date ({@link refreshInstalledCdcFunction}),
195
+ * since they keep firing.
196
+ */
197
+ export declare function settleInstalledCapture(run: RawSqlRunner, options: {
198
+ capture: "off" | "not-provisioned";
199
+ }): Promise<{
200
+ removed: number;
201
+ } | {
202
+ refreshed: boolean;
203
+ }>;
204
+ /**
205
+ * Idempotently install the CDC trigger functions and per-table triggers.
206
+ *
207
+ * A plain table gets the statement-level triggers, a partitioned or foreign
208
+ * one the row trigger ({@link levelForRelkind}). A table whose triggers are
209
+ * already exactly the ones this would create — same function, same events,
210
+ * enabled, same identity arguments — is left alone; one with a trigger
211
+ * missing, disabled, attached with different arguments or at the other level
212
+ * is (re-)attached, all of it in one transaction.
213
+ *
214
+ * Converting a table from a row trigger an older version attached opens the
215
+ * statement function's compatibility window ({@link CDC_ROW_COMPAT_MS}): the
216
+ * instances that attached it may still be listening.
96
217
  *
97
218
  * Runs as the owner (server) connection at bootstrap. A table that does not yet
98
219
  * exist in the database (schema drift) is skipped with a warning rather than
99
220
  * aborting the whole install, so one un-migrated collection cannot disable CDC
100
221
  * for the rest.
101
222
  */
102
- export declare function provisionTriggerCdc(run: RawSqlRunner, tables: CdcTableRef[]): Promise<ProvisionResult>;
223
+ export declare function provisionTriggerCdc(run: RawSqlRunner, tables: CdcTableRef[], options?: {
224
+ now?: number;
225
+ }): Promise<ProvisionResult>;
226
+ export {};
@@ -36,7 +36,7 @@ export interface ChannelBusDeps {
36
36
  * transports this package knows how to construct, so honouring it there would
37
37
  * mean silently discarding the object the application handed us.
38
38
  */
39
- export declare function resolveChannelBusSetting(configured?: ChannelBusSetting): ChannelBusSetting;
39
+ export declare function resolveChannelBusSetting(configured?: ChannelBusSetting, env?: NodeJS.ProcessEnv): ChannelBusSetting;
40
40
  /**
41
41
  * Produce the bus a setting asks for.
42
42
  *
@@ -79,10 +79,21 @@ export interface RetainedAnnouncement {
79
79
  export declare class ChannelHistoryStore {
80
80
  private db;
81
81
  private rules;
82
- /** Resolved rule per channel name, so the match runs once per channel. */
83
- private resolved;
84
- /** Channel → timestamp of its last prune, for {@link PRUNE_THROTTLE_MS}. */
82
+ /**
83
+ * Each rule with its retention resolved, once. Matched per call rather
84
+ * than cached per channel name: a cache keyed by the names clients invent
85
+ * grew by every one of them for the life of the process, and matching a
86
+ * handful of prefixes costs less than the hashing a lookup does.
87
+ */
88
+ private resolvedRules;
89
+ /**
90
+ * Channel → timestamp of its last prune, for {@link PRUNE_THROTTLE_MS}.
91
+ * An entry older than the window throttles nothing, so they are swept once
92
+ * a window: the map holds the channels pruned recently, not every channel
93
+ * ever pruned.
94
+ */
85
95
  private lastPruned;
96
+ private nextPruneSweepAt;
86
97
  private tablesReady;
87
98
  constructor(db: NodePgDatabase<Record<string, unknown>>, rules?: ChannelRetentionRule[]);
88
99
  /** Whether any channel retains anything at all. */
@@ -140,6 +151,6 @@ export declare class ChannelHistoryStore {
140
151
  * volume, which is what makes retention affordable on a hot channel.
141
152
  */
142
153
  prune(channel: string, retention: ResolvedRetention): Promise<number>;
143
- /** Forget throttle and match caches. Called on shutdown. */
154
+ /** Forget the prune throttle. Called on shutdown. */
144
155
  clear(): void;
145
156
  }
@@ -143,6 +143,21 @@ export declare function requirePrimaryKeys(collection: CollectionConfig, registr
143
143
  * @param collectionPath named in the error.
144
144
  */
145
145
  export declare function primaryKeyColumns(table: PgTable, primaryKeys: PrimaryKeyInfo[], collectionPath: string): AnyPgColumn[];
146
+ /**
147
+ * The column a `joinPath` step names, on a table that may key it either way.
148
+ *
149
+ * A `JoinStep` is written in database terms — `on: { from: "author_id" }` —
150
+ * because there is no collection to translate through: the step names a raw
151
+ * table. A table the boot reads back out of the catalogue is keyed by the
152
+ * *wire* name, `authorId`, while a hand-written Drizzle schema may key it by
153
+ * the column name. Looking the step's name up as a key therefore found
154
+ * nothing on every booted table, and a `via` relation loaded nothing at all.
155
+ *
156
+ * The key first, as the step may already name one; then the column whose SQL
157
+ * name it is. `undefined` when the table has neither, which the caller turns
158
+ * into an error naming the step.
159
+ */
160
+ export declare function columnNamed(table: PgTable, name: string): AnyPgColumn | undefined;
146
161
  /**
147
162
  * The column on the *source* table that a `hasOne`/`hasMany` link points at.
148
163
  *