@ultimat3/db 29.4.0 → 30.0.0

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 (96) hide show
  1. package/CLAUDE.md +74 -110
  2. package/README.md +163 -15
  3. package/package.json +13 -2
  4. package/src/app-relation.ts +7 -2
  5. package/src/catalog-fold.ts +5 -0
  6. package/src/catalog-objects.ts +20 -24
  7. package/src/catalog-policies.ts +81 -0
  8. package/src/catalog-relations.ts +31 -10
  9. package/src/catalog.ts +40 -1
  10. package/src/check-ddl.ts +34 -7
  11. package/src/client.ts +132 -48
  12. package/src/column-alter.ts +42 -1
  13. package/src/concurrent-index.ts +20 -0
  14. package/src/concurrent-plan.ts +135 -17
  15. package/src/connection-url.ts +16 -4
  16. package/src/db-diagnostics-deadline.ts +43 -0
  17. package/src/db-diagnostics-redact.ts +58 -0
  18. package/src/db-diagnostics-rows.ts +166 -0
  19. package/src/db-diagnostics-sql.ts +92 -0
  20. package/src/db-diagnostics.ts +178 -0
  21. package/src/destructive.ts +17 -3
  22. package/src/diagnostics-entry.ts +15 -0
  23. package/src/drift-errors.ts +9 -4
  24. package/src/drift-findings.ts +47 -62
  25. package/src/drift-missing.ts +203 -0
  26. package/src/drift-row-security.ts +88 -0
  27. package/src/drift-triggers.ts +54 -0
  28. package/src/drift.ts +88 -21
  29. package/src/entity-shape.ts +67 -0
  30. package/src/errors.ts +77 -9
  31. package/src/expected-loop.ts +34 -4
  32. package/src/failure-store-pg.ts +157 -0
  33. package/src/foreign-key-plan.ts +75 -23
  34. package/src/foreign-key.ts +76 -5
  35. package/src/generate-counter.ts +85 -0
  36. package/src/generate-history.ts +142 -0
  37. package/src/generate-row-security.ts +51 -0
  38. package/src/generate-snapshot.ts +82 -0
  39. package/src/generate-steps.ts +6 -1
  40. package/src/generate-triggers.ts +119 -0
  41. package/src/generate-view.ts +97 -0
  42. package/src/generate.ts +105 -79
  43. package/src/index.ts +129 -6
  44. package/src/introspect-catalog.ts +26 -6
  45. package/src/introspect-rows.ts +82 -0
  46. package/src/introspect.ts +108 -50
  47. package/src/keyset.ts +182 -0
  48. package/src/lifecycle-errors.ts +15 -0
  49. package/src/local-settings.ts +38 -0
  50. package/src/marker-line.ts +12 -4
  51. package/src/migrate-lock-retry.ts +60 -0
  52. package/src/migrate-rollback.ts +45 -6
  53. package/src/migrate.ts +45 -4
  54. package/src/migration-errors.ts +27 -1
  55. package/src/migration-ledger.ts +38 -4
  56. package/src/migration-step.ts +50 -22
  57. package/src/partition-order.ts +56 -0
  58. package/src/partitioned-tables.ts +24 -0
  59. package/src/pool-gate.ts +120 -0
  60. package/src/pool-listeners.ts +143 -0
  61. package/src/pool-pins.ts +68 -0
  62. package/src/pool-profile.ts +23 -7
  63. package/src/pool-reserve.ts +11 -4
  64. package/src/pool-retire.ts +22 -0
  65. package/src/pool-sweep.ts +115 -0
  66. package/src/pool-timer.ts +10 -0
  67. package/src/pooler-fix.ts +53 -0
  68. package/src/pooler.ts +161 -0
  69. package/src/read-transaction.ts +59 -0
  70. package/src/readonly-query.ts +4 -4
  71. package/src/replica-client.ts +54 -22
  72. package/src/replica-position.ts +89 -0
  73. package/src/replica-scope.ts +25 -4
  74. package/src/retired.ts +131 -0
  75. package/src/retype-dependents.ts +2 -0
  76. package/src/rolling-errors.ts +97 -0
  77. package/src/rolling.ts +212 -0
  78. package/src/row-secure-client.ts +54 -0
  79. package/src/row-security-catalog.ts +26 -0
  80. package/src/row-security-migrator.ts +48 -0
  81. package/src/row-security-plan.ts +58 -0
  82. package/src/row-security-role.ts +60 -0
  83. package/src/row-security-roles.ts +64 -0
  84. package/src/row-security.ts +59 -0
  85. package/src/same-tenant.ts +173 -0
  86. package/src/schema-dump-policies.ts +26 -0
  87. package/src/schema-dump-table.ts +21 -6
  88. package/src/schema-dump.ts +13 -2
  89. package/src/snapshot-parse.ts +42 -2
  90. package/src/sqlstate.ts +4 -0
  91. package/src/statement-bound.ts +82 -0
  92. package/src/transaction-errors.ts +14 -0
  93. package/src/transaction-guard.ts +40 -0
  94. package/src/transaction-settings.ts +24 -0
  95. package/src/transaction.ts +74 -19
  96. package/src/ungeneratable.ts +30 -0
package/src/generate.ts CHANGED
@@ -4,15 +4,19 @@
4
4
  // field. Every generated migration must be reversible; a drop that loses data refuses instead.
5
5
 
6
6
  import { systemClock } from '@ultimat3/core';
7
- import { checkClauses, checkPlan, declaredChecks } from './check-ddl';
8
- import { alterColumnInPlace } from './column-alter';
7
+ import { checkClauses, checkPlan } from './check-ddl';
8
+ import { alterColumnInPlace, retypeOverDefault, wantedDefault } from './column-alter';
9
9
  import { defaultExpression } from './column-default';
10
10
  import { type ConcurrentPlan, emptyConcurrentPlan } from './concurrent-plan';
11
11
  import { dropOrder } from './drop-order';
12
12
  import type { ColumnDescriptionLike, EntityDescriptionLike } from './entity-shape';
13
- import { type ConstraintPlans, foreignKeyPlan, foreignKeysOf, type Plan } from './foreign-key-plan';
13
+ import { type ConstraintPlans, foreignKeyPlan, type Plan } from './foreign-key-plan';
14
14
  import { appendOnlyBackfillRefused, appendOnlyPlan } from './generate-append-only';
15
+ import { type RowSecurityOption, rowSecurityInput } from './generate-row-security';
16
+ import { snapshotOf } from './generate-snapshot';
15
17
  import { assembleSteps } from './generate-steps';
18
+ import { recordTriggers, triggerPlan } from './generate-triggers';
19
+ import { splitViews, viewPlan } from './generate-view';
16
20
  import type { Regeneration } from './generated-column';
17
21
  import { generatedClause, isGenerated, regenerate } from './generated-column';
18
22
  import { createIndex, impliedByColumnClause } from './index-ddl';
@@ -28,9 +32,13 @@ import { migrationIrreversible } from './migration-errors';
28
32
  import { addChangedKey, dropChangedKey, migrationNameArg } from './primary-key';
29
33
  import type { ReplicaIdentityInput } from './replica-identity';
30
34
  import { replicaIdentityFullAfter, replicaIdentityPlan } from './replica-identity';
35
+ import { contractsMarkerFor, recordRetired, retirePlan } from './retired';
31
36
  import type { MovedAside } from './retype-dependents';
32
37
  import { moveDependentsAside } from './retype-dependents';
33
38
  import { moveKeysAside, retypedColumns, retypedIn } from './retype-keys';
39
+ import { columnDropNotRetired } from './rolling-errors';
40
+ import { rowSecurityAfter, rowSecurityPlan } from './row-security-plan';
41
+ import { withSameTenantIndexes } from './same-tenant';
34
42
  import { identifier } from './sql';
35
43
  import { sqlType } from './sql-type';
36
44
  import { type UnrenderedDeclaration, unrenderedOf } from './unrendered';
@@ -60,67 +68,6 @@ function columnClause(column: ColumnDescriptionLike): string {
60
68
  return parts.join(' ');
61
69
  }
62
70
 
63
- /** No table subscribed to, which is what every caller outside `generateMigration` describes. */
64
- const NONE: ReadonlySet<string> = new Set();
65
-
66
- export function snapshotOf(
67
- entities: readonly EntityDescriptionLike[],
68
- replicaIdentityFull: ReadonlySet<string> = NONE,
69
- ): SchemaDescription {
70
- const tables = [...entities]
71
- .sort((a, b) => (a.table < b.table ? -1 : 1))
72
- .map((entity): TableDescription => {
73
- const columns: ColumnDescription[] = [...entity.columns]
74
- .sort((a, b) => (a.column < b.column ? -1 : 1))
75
- .map((column, index) => ({
76
- name: column.column,
77
- dataType: sqlType(column.kind),
78
- nullable: !column.notNull,
79
- default: defaultExpression(column),
80
- position: index + 1,
81
- // Only when one was declared — absent stays absent, so no snapshot written before this
82
- // field existed gains a key and no app's sidecar regenerates over a fact already true.
83
- ...(column.generated === undefined ? {} : { generated: column.generated }),
84
- }));
85
- const checks = declaredChecks(entity);
86
- return {
87
- schema: 'public',
88
- name: entity.table,
89
- columns,
90
- primaryKey: [...entity.primaryKey],
91
- // Whole, never partly: a snapshot that recorded the name and dropped the predicate made
92
- // the next generation blind to a `where` or an `order` changing, and a partial index
93
- // silently kept as a total one is a constraint the entity no longer declares.
94
- //
95
- // `declaredIndexes`, so a `unique` invariant's index is recorded exactly like an entity's
96
- // own — a statement emitted and not recorded is `42P07` on the very next `x db gen`.
97
- indexes: declaredIndexes(entity).map((index) => ({
98
- name: index.name,
99
- columns: [...index.columns],
100
- unique: index.unique,
101
- primary: false,
102
- where: index.where,
103
- order: index.order,
104
- // Only when one was declared. Writing `using: 'btree'` out for every index would rewrite
105
- // every sidecar in every app on the next `x db gen` — a diff on every file for a fact
106
- // that was already true, which `indexMethodOf` reads out of the absence anyway.
107
- ...(index.using === undefined ? {} : { using: index.using }),
108
- })),
109
- foreignKeys: foreignKeysOf(entity),
110
- // Absent, never `[]`, on a table declaring none: a sidecar that predates this field must
111
- // read as "nothing recorded" so the next generation adds the constraints the database is
112
- // genuinely missing — the rule `using` and `generated` already state one field up.
113
- ...(checks.length === 0 ? {} : { checks }),
114
- // The same rule once more: `true` or absent, never `false`. This is what makes the ALTER
115
- // beside it a one-time statement rather than a line every `x db gen` writes again.
116
- ...(replicaIdentityFull.has(entity.table) ? { replicaIdentityFull: true as const } : {}),
117
- // `true` or absent once more — the record that makes the trigger a one-time statement.
118
- ...(entity.appendOnly === true ? { appendOnly: true as const } : {}),
119
- };
120
- });
121
- return { tables };
122
- }
123
-
124
71
  function createTable(entity: EntityDescriptionLike): readonly string[] {
125
72
  const clauses = entity.columns.map(columnClause);
126
73
  if (entity.primaryKey.length > 0) {
@@ -177,8 +124,8 @@ function retypeColumn(
177
124
  const alter = (type: string): string =>
178
125
  `alter table ${identifier(table).text} alter column ${identifier(column.column).text} ` +
179
126
  `type ${type} using ${identifier(column.column).text}::${type};`;
180
- plan.up.push(alter(wanted));
181
- plan.down.push(alter(recorded.dataType));
127
+ retypeOverDefault(table, column, recorded, plan, alter(wanted), alter(recorded.dataType));
128
+ moved.defaults.add(column.column);
182
129
  return 'altered';
183
130
  }
184
131
 
@@ -195,16 +142,27 @@ function diffTable(
195
142
  // over it has to be stated again even though its own definition never moved.
196
143
  const rebuilt = new Set<string>();
197
144
  // What a retype dropped ahead of itself, read by the two arms below.
198
- const moved: MovedAside = { indexes: new Set(), checks: new Set() };
145
+ const moved: MovedAside = { indexes: new Set(), checks: new Set(), defaults: new Set() };
199
146
  for (const column of entity.columns) {
200
147
  const recorded = existing.get(column.column);
201
148
  if (recorded !== undefined) {
202
149
  if (retypeColumn(live, column, recorded, plan, moved, retyped) === 'rebuilt') {
203
150
  rebuilt.add(column.column);
204
151
  } else {
205
- // After any retype, so a new default is set against the column's new type. A rebuilt
206
- // column was re-added carrying its whole clause, so it has nothing left to move.
207
- alterColumnInPlace(entity, column, recorded, plan);
152
+ // After any retype, so nullability moves on the column's new type. A rebuilt column was
153
+ // re-added carrying its whole clause, so it has nothing left to move. A retype already
154
+ // moved the default, across the type (`column-alter.ts`).
155
+ alterColumnInPlace(
156
+ entity,
157
+ column,
158
+ moved.defaults.has(column.column)
159
+ ? {
160
+ ...recorded,
161
+ default: wantedDefault(column, recorded),
162
+ }
163
+ : recorded,
164
+ plan,
165
+ );
208
166
  }
209
167
  continue;
210
168
  }
@@ -241,9 +199,11 @@ function diffTable(
241
199
  // that does not exist yet is `42703`. `check-ddl.ts` owns which of them move; `rebuilt` because a
242
200
  // column dropped and re-added lost its constraint while the snapshot still records it, and
243
201
  // `moved.checks` because a retype already dropped the ones written against the old type.
244
- checkPlan(entity, live, plan, rebuilt, moved.checks);
202
+ checkPlan(entity, live, plan, rebuilt, moved.checks, concurrent);
245
203
  }
246
204
 
205
+ export { snapshotOf } from './generate-snapshot';
206
+
247
207
  export interface GenerateOptions {
248
208
  readonly entities: readonly EntityDescriptionLike[];
249
209
  /** The schema migrations already declare — `expectedSchema(migrations, ledger)`. */
@@ -252,6 +212,12 @@ export interface GenerateOptions {
252
212
  readonly now?: Date | undefined;
253
213
  /** Allow a DROP COLUMN whose down cannot restore the data. `x db gen --allow-destructive`. */
254
214
  readonly allowDestructive?: boolean | undefined;
215
+ /**
216
+ * Drop a column that was never retired, for an app that stops before it starts: one instance, or
217
+ * `strategy: Recreate`. `x db gen --rolling-unsafe`. Consent to the outage and not to the loss,
218
+ * so `allowDestructive` is still asked of it.
219
+ */
220
+ readonly rollingUnsafe?: boolean | undefined;
255
221
  /**
256
222
  * Tables a live query subscribes to, so logical replication carries the old row —
257
223
  * `@ultimat3/realtime` refuses a subscription without it. Absent or empty emits nothing.
@@ -263,6 +229,12 @@ export interface GenerateOptions {
263
229
  * is the one caller that can answer it.
264
230
  */
265
231
  readonly replicaIdentityFull?: readonly string[] | undefined;
232
+ /**
233
+ * `database.rowSecurity` and each tenant table's column (`describeTenancy()`): a parameter for
234
+ * `replicaIdentityFull`'s reason — a config fact no entity carries. Absent is off, so a recorded
235
+ * policy is dropped: `@ultimat3/cli`'s `db-generate.ts` passes it on every run.
236
+ */
237
+ readonly rowSecurity?: RowSecurityOption | undefined;
266
238
  }
267
239
 
268
240
  export interface GeneratedMigration {
@@ -278,6 +250,11 @@ export interface GeneratedMigration {
278
250
  * cannot be written unmarked and then refused by `x verify` for lacking the mark.
279
251
  */
280
252
  readonly destructive: boolean;
253
+ /**
254
+ * `true` when `--rolling-unsafe` let this `up` drop a column it never retired. Absent otherwise.
255
+ * `x db gen` writes it as `-- rolling-unsafe: true`, the line the `drift` step's rail reads.
256
+ */
257
+ readonly rollingUnsafe?: boolean | undefined;
281
258
  /**
282
259
  * Every declaration this generator could not write down. Empty on a migration that carries the
283
260
  * whole schema, which is what makes it readable as a verdict rather than as noise — and the same
@@ -310,7 +287,12 @@ export function slugify(name: string): string {
310
287
  .replace(/^_+|_+$/g, '');
311
288
  }
312
289
 
313
- export function generateMigration(options: GenerateOptions): GeneratedMigration {
290
+ export function generateMigration(given: GenerateOptions): GeneratedMigration {
291
+ // One list for every arm below: a target's same-tenant index is declared by whoever points at it,
292
+ // and neither `createTable`, `diffTable` nor a snapshot may hold a different answer.
293
+ // Views leave here, before any table arm: none of them is a table (`generate-view.ts`).
294
+ const { tables, views } = splitViews(given.entities);
295
+ const options = { ...given, entities: withSameTenantIndexes(tables, given.current) };
314
296
  const current = options.current ?? { tables: [] };
315
297
  const plan: Plan = { up: [], down: [] };
316
298
  // Applied AFTER `plan`, outside its transaction, in a step of its own (`concurrent-plan.ts`).
@@ -336,11 +318,11 @@ export function generateMigration(options: GenerateOptions): GeneratedMigration
336
318
  // and the key that a retype of its column breaks is recorded on whichever table OWNS the key.
337
319
  const retyped = retypedColumns(options.entities, current);
338
320
  const predropped = moveKeysAside(current, retyped, doomed, preAlters);
339
- const plans: ConstraintPlans = { constraints, preDrops, doomed, predropped };
321
+ const plans: ConstraintPlans = { constraints, preDrops, doomed, predropped, concurrent };
322
+ let droppedUnretired = false;
340
323
 
341
324
  for (const entity of options.entities) {
342
325
  const live = findTable(current, entity.table);
343
- foreignKeyPlan(entity, live, plans);
344
326
  if (live === undefined) {
345
327
  plan.up.push(...createTable(entity));
346
328
  plan.down.push(`drop table ${identifier(entity.table).text};`);
@@ -349,17 +331,36 @@ export function generateMigration(options: GenerateOptions): GeneratedMigration
349
331
  }
350
332
  // The key first and last, around every column statement of the table (`primary-key.ts`): the
351
333
  // snapshot below records `entity.primaryKey`, and until this arm existed nothing produced it.
334
+ retirePlan(entity, live, plan, options.entities);
352
335
  dropChangedKey(entity, live, current, plan, options.name);
353
336
  diffTable(entity, live, plan, retypedIn(retyped, entity.table), concurrent);
354
- const kept = new Set(entity.columns.map((column) => column.column));
337
+ const kept = new Set([
338
+ ...entity.columns.map((column) => column.column),
339
+ ...(entity.retired ?? []),
340
+ ]);
355
341
  for (const column of live.columns) {
356
342
  if (kept.has(column.name)) continue;
343
+ // Order matters: the release boundary first, with the flag or without it. Retiring is what
344
+ // earns the next question; it is never consent to losing the rows, so that one still stands.
345
+ if (column.retired !== true && options.rollingUnsafe !== true) {
346
+ throw columnDropNotRetired(
347
+ entity.name,
348
+ entity.table,
349
+ column.name,
350
+ migrationNameArg(options.name),
351
+ );
352
+ }
357
353
  if (options.allowDestructive !== true) {
358
354
  throw migrationIrreversible(
359
355
  `dropping "${entity.table}"."${column.name}" discards its rows and cannot be undone`,
360
- `x db gen ${migrationNameArg(options.name)} --allow-destructive # or keep the column and deprecate it`,
356
+ `x db gen ${migrationNameArg(options.name)} --allow-destructive # or put "${column.name}" back in columns and keep it`,
361
357
  );
362
358
  }
359
+ if (column.retired === true) {
360
+ plan.up.push(contractsMarkerFor(entity.table, column.name));
361
+ } else {
362
+ droppedUnretired = true;
363
+ }
363
364
  plan.up.push(
364
365
  `alter table ${identifier(entity.table).text} drop column ${identifier(column.name).text};`,
365
366
  );
@@ -372,6 +373,11 @@ export function generateMigration(options: GenerateOptions): GeneratedMigration
372
373
  addChangedKey(entity, live, plan, options.name);
373
374
  }
374
375
 
376
+ // After every table's diff, so the deferred step already says which target indexes it builds.
377
+ for (const entity of options.entities) {
378
+ foreignKeyPlan(entity, findTable(current, entity.table), plans);
379
+ }
380
+
375
381
  const order = dropOrder(current.tables.filter((table) => !wanted.has(table.name)));
376
382
  for (const table of order.tables) {
377
383
  if (options.allowDestructive !== true) {
@@ -397,6 +403,11 @@ export function generateMigration(options: GenerateOptions): GeneratedMigration
397
403
 
398
404
  // After every table exists, so a `create table` in this same migration is already above it.
399
405
  appendOnlyPlan(plan, options.entities, current, created);
406
+ // History, its guard and counters, the same way: after every table they name exists.
407
+ triggerPlan(plan, options.entities, current);
408
+ // Then each tenant table's policy, both directions (`row-security-plan.ts`), on tables that exist.
409
+ const rowSecurity = rowSecurityInput(options.entities, current, options.rowSecurity);
410
+ rowSecurityPlan(plan, rowSecurity);
400
411
 
401
412
  // Dead last in `up`, and it is the only placement that is right for every arm: the table has to
402
413
  // exist, and a `create table` in this same migration is the reason it might not. It is ordered
@@ -424,16 +435,31 @@ export function generateMigration(options: GenerateOptions): GeneratedMigration
424
435
  // `current`, not the entities alone: an `assert` whose CHECK a previous migration recorded is a
425
436
  // loss only because THIS plan drops it, and the recorded schema is the only thing that knows.
426
437
  const unrendered = unrenderedOf(options.entities, current);
438
+ // Dropped ahead of every table statement and created after the last one: a view compiled against
439
+ // a column this migration retypes or drops is `0A000`/`2BP01` otherwise.
440
+ const viewed = viewPlan(views, current, [...preAlters.up, ...plan.up]);
427
441
  return assembleSteps({
428
442
  id,
429
443
  name: options.name,
430
- up: [...preAlters.up, ...plan.up],
444
+ up: [...viewed.before.up, ...preAlters.up, ...plan.up, ...viewed.after.up],
431
445
  // Reverse order: the last thing created is the first thing dropped. `preAlters` goes in at the
432
446
  // FRONT here precisely so reversal puts it last — a key is added back only once both of its
433
447
  // ends have been retyped back, which is every other statement in the script.
434
- down: [...preAlters.down, ...plan.down].reverse(),
448
+ down: [...viewed.before.down, ...preAlters.down, ...plan.down, ...viewed.after.down].reverse(),
435
449
  concurrent,
436
- snapshot: snapshotOf(options.entities, replicaIdentityFullAfter(replicaIdentity)),
450
+ snapshot: recordTriggers(
451
+ recordRetired(
452
+ snapshotOf(
453
+ [...options.entities, ...views],
454
+ replicaIdentityFullAfter(replicaIdentity),
455
+ rowSecurityAfter(rowSecurity),
456
+ ),
457
+ options.entities,
458
+ current,
459
+ ),
460
+ options.entities,
461
+ ),
437
462
  unrendered,
463
+ ...(droppedUnretired ? { rollingUnsafe: true } : {}),
438
464
  });
439
465
  }
package/src/index.ts CHANGED
@@ -27,13 +27,21 @@ export type {
27
27
  PostgresClientOptions,
28
28
  ReservableClient,
29
29
  } from './client';
30
- export { baseClient, db, isReservable, postgresClient, setDbClient } from './client';
30
+ export {
31
+ baseClient,
32
+ db,
33
+ dbClientInstalled,
34
+ isReservable,
35
+ postgresClient,
36
+ setDbClient,
37
+ } from './client';
31
38
  export type { ColumnDefaultLike } from './column-default';
32
39
  export { defaultExpression } from './column-default';
33
40
  export {
34
41
  concurrentIndexTarget,
35
42
  isConcurrentIndexStatement,
36
43
  isNonTransactional,
44
+ isPartitionedBuildRefusal,
37
45
  NO_TRANSACTION_MARKER,
38
46
  } from './concurrent-index';
39
47
  export { dbExecutor } from './db-executor';
@@ -65,11 +73,16 @@ export {
65
73
  FRAMEWORK_TABLE_PREFIX,
66
74
  } from './drift';
67
75
  export { dbDrift } from './drift-errors';
76
+ export type { RowSecurityDifference } from './drift-row-security';
77
+ export { rowSecurityDifferences, rowSecurityDrift } from './drift-row-security';
68
78
  export type {
69
79
  ColumnDescriptionLike,
80
+ CounterDescriptionLike,
70
81
  EntityDescriptionLike,
82
+ HistoryDescriptionLike,
71
83
  IndexDescriptionLike,
72
84
  InvariantDescriptionLike,
85
+ RetainDescriptionLike,
73
86
  } from './entity-shape';
74
87
  export type { DbErrorCode, DbErrorInit } from './errors';
75
88
  export {
@@ -88,11 +101,39 @@ export {
88
101
  serializationExhausted,
89
102
  sqlUnsafe,
90
103
  } from './errors';
91
- export { expectedQueryLoop, expectedQueryLoopReason } from './expected-loop';
104
+ export {
105
+ expectedQueryLoop,
106
+ expectedQueryLoopReason,
107
+ expectedSeqScan,
108
+ expectedSeqScanReason,
109
+ } from './expected-loop';
110
+ export type { FailureSummary, PostgresFailureStore } from './failure-store-pg';
111
+ export {
112
+ FAILURES_TABLE,
113
+ failurePurgeTarget,
114
+ MAX_FAILURE_LIST,
115
+ postgresFailureStore,
116
+ SQL_FAILURES_TABLE,
117
+ } from './failure-store-pg';
92
118
  export type { RecordedStatement, RecordingClient, StubResponse } from './fake';
93
119
  export { recordingClient } from './fake';
94
120
  export type { GeneratedMigration, GenerateOptions } from './generate';
95
121
  export { generateMigration, migrationStamp, slugify, snapshotOf } from './generate';
122
+ export {
123
+ counterTrigger,
124
+ counterTriggerName,
125
+ recountDriftSql,
126
+ recountSql,
127
+ } from './generate-counter';
128
+ export {
129
+ ACTOR_SETTING,
130
+ HISTORY_GUARD_TRIGGER,
131
+ HISTORY_TRIGGER,
132
+ PURGE_SETTING,
133
+ REQUEST_SETTING,
134
+ } from './generate-history';
135
+ export type { RowSecurityOption, TenantColumnLike } from './generate-row-security';
136
+ export { declaredTriggers } from './generate-triggers';
96
137
  export type { IndexMethod } from './index-method';
97
138
  export {
98
139
  declaredMethod,
@@ -109,6 +150,8 @@ export type {
109
150
  IntrospectOptions,
110
151
  SchemaDescription,
111
152
  TableDescription,
153
+ TriggerRecord,
154
+ ViewDescription,
112
155
  } from './introspect';
113
156
  export { buildSchema, findTable, introspectSchema } from './introspect';
114
157
  export {
@@ -118,8 +161,13 @@ export {
118
161
  uniqueColumns,
119
162
  } from './invariant-ddl';
120
163
  export { constraintExpressionUnsafe, constraintNameUnsafe } from './invariant-errors';
164
+ export type { Keyset, KeysetKey, KeysetKind, KeysetOptions, KeysetRow } from './keyset';
165
+ export { keyset } from './keyset';
166
+ export { retainInvalid } from './lifecycle-errors';
121
167
  export type { DbSubscription, ListeningClient } from './listen';
122
168
  export { canListen } from './listen';
169
+ export type { LocalSettings } from './local-settings';
170
+ export { readLocal, setLocal } from './local-settings';
123
171
  export type {
124
172
  AppliedMigration,
125
173
  LedgerRow,
@@ -147,6 +195,7 @@ export {
147
195
  export {
148
196
  migrateConcurrent,
149
197
  migrationConcurrentInTransaction,
198
+ migrationConcurrentPartitioned,
150
199
  migrationConflict,
151
200
  migrationDestructive,
152
201
  migrationIrreversible,
@@ -179,7 +228,23 @@ export type { LinkedExtensions, PgliteExtensionLoader } from './pglite-extension
179
228
  export { linkPgliteExtensions } from './pglite-extensions';
180
229
  export { pgliteVersion } from './pglite-snapshot';
181
230
  export type { PoolProfile } from './pool-profile';
182
- export { POOL_MAX_ENV, POOL_PROFILES, poolMaxFromEnv, poolProfileFor } from './pool-profile';
231
+ export {
232
+ POOL_MAX_ENV,
233
+ POOL_PROFILES,
234
+ poolMaxFromEnv,
235
+ poolProfileFor,
236
+ } from './pool-profile';
237
+ export type { PoolerMode, StatementBound } from './pooler';
238
+ export {
239
+ lostStatementBound,
240
+ POOLER_ENV,
241
+ POOLER_WAIT_MS,
242
+ poolerFromEnv,
243
+ poolerSessionRequired,
244
+ statementBound,
245
+ } from './pooler';
246
+ export type { ReadTransactionOptions } from './read-transaction';
247
+ export { withReadTransaction } from './read-transaction';
183
248
  export type { ReadOnlyQueryOptions, ReadOnlyQueryResult } from './readonly-query';
184
249
  export { READONLY_TIMEOUT_MS, readOnlyQuery } from './readonly-query';
185
250
  export type { ReadOnlyRoleOptions } from './readonly-role';
@@ -190,9 +255,53 @@ export type {
190
255
  ReplicatedClientOptions,
191
256
  } from './replica-client';
192
257
  export { BREAKER_COOLDOWN_MS, BREAKER_FAILURES, replicatedClient } from './replica-client';
258
+ export type { ReplayReader } from './replica-position';
259
+ export {
260
+ formatPosition,
261
+ POSITION_CACHE_MS,
262
+ parsePosition,
263
+ primaryPosition,
264
+ replayReader,
265
+ } from './replica-position';
193
266
  export { type DbNode, isPlainRead } from './replica-route';
194
- export type { ReplicaScope } from './replica-scope';
267
+ export type { ReplicaReadsOptions, ReplicaScope } from './replica-scope';
195
268
  export { markScopeWrote, replicaScope, withReplicaReads } from './replica-scope';
269
+ export { recordRetired } from './retired';
270
+ export type { RollingUnsafeKind, RollingUnsafeStatement } from './rolling';
271
+ export {
272
+ hasRollingUnsafeMarker,
273
+ ROLLING_UNSAFE_MARKER,
274
+ retiredColumnsIn,
275
+ rollingUnsafeStatements,
276
+ unretiredColumnsIn,
277
+ } from './rolling';
278
+ export { migrationRollingUnsafe } from './rolling-errors';
279
+ export { rowSecureClient } from './row-secure-client';
280
+ export {
281
+ CROSS_TENANT_SETTING,
282
+ dropRowSecuritySql,
283
+ enableRowSecuritySql,
284
+ ROW_SECURITY_POLICY,
285
+ TENANT_SETTING,
286
+ } from './row-security';
287
+ export { holdMigratorRole, migratorOwnershipSql } from './row-security-migrator';
288
+ export type { RowSecurityPlanInput, TenantTable } from './row-security-plan';
289
+ export { rowSecurityAfter, rowSecurityPlan } from './row-security-plan';
290
+ export type { RowSecurityRole } from './row-security-role';
291
+ export {
292
+ assertRowSecurityBinds,
293
+ rowSecurityExemption,
294
+ rowSecurityRole,
295
+ } from './row-security-role';
296
+ export {
297
+ APP_ROLE,
298
+ assumeRole,
299
+ ensureRowSecurityRoles,
300
+ MIGRATOR_ROLE,
301
+ rowSecurityRolesSql,
302
+ serveAsAppRole,
303
+ } from './row-security-roles';
304
+ export { sameTenantKeyName } from './same-tenant';
196
305
  export { SIBLING_SCOPE_WAIT_MS } from './sibling-turn';
197
306
  export { snapshotJson } from './snapshot-json';
198
307
  export { parseSnapshot } from './snapshot-parse';
@@ -210,13 +319,27 @@ export { stripSqlNoise } from './sql-noise';
210
319
  export { dollarTagAt, endOfBlockComment } from './sql-scan';
211
320
  export type { DbSqlStateCode } from './sqlstate';
212
321
  export { DB_SQLSTATE_CODES, isRetryableState, SQLSTATE, sqlState, sqlStateCode } from './sqlstate';
322
+ export type { StatementBoundOptions } from './statement-bound';
323
+ export { withStatementTimeout } from './statement-bound';
213
324
  export { statementFingerprint, statementKind, statementVerb } from './statement-shape';
214
325
  export { STATEMENT_ATTRIBUTE } from './statement-span';
215
326
  export { statementsOf } from './statement-split';
216
- export { currentTx, liveTxConnection, outsideTransaction, withTransaction } from './transaction';
217
- export { commitUnknown, siblingScopeTimeout, transactionAborted } from './transaction-errors';
327
+ export {
328
+ completeTransaction,
329
+ currentTx,
330
+ liveTxConnection,
331
+ outsideTransaction,
332
+ withTransaction,
333
+ } from './transaction';
334
+ export {
335
+ commitUnknown,
336
+ siblingScopeTimeout,
337
+ transactionAborted,
338
+ } from './transaction-errors';
339
+ export { refuseAbandoned } from './transaction-guard';
218
340
  export type { DbTx, IsolationLevel, TransactionOptions } from './transaction-options';
219
341
  export { beginStatement } from './transaction-options';
342
+ export { setTransactionSettings, transactionSettings } from './transaction-settings';
220
343
  export type { GeneratableForm } from './ungeneratable';
221
344
  export { GENERATABLE_FORMS, ungeneratableStatements } from './ungeneratable';
222
345
  export type { UnrenderedDeclaration } from './unrendered';
@@ -15,6 +15,7 @@ import {
15
15
  unrenderedRows,
16
16
  viewRows,
17
17
  } from './catalog-objects';
18
+ import { policiesOf, policyRows, rowSecurityOf, rowSecurityRows } from './catalog-policies';
18
19
  import {
19
20
  columnRows,
20
21
  constraintRows,
@@ -23,6 +24,7 @@ import {
23
24
  tableRows,
24
25
  } from './catalog-relations';
25
26
  import { type DbClient, db } from './client';
27
+ import { indexDefinitionOf } from './partition-order';
26
28
 
27
29
  export interface IntrospectCatalogOptions {
28
30
  readonly client?: DbClient | undefined;
@@ -60,7 +62,7 @@ async function typesOf(client: DbClient, schema: string): Promise<readonly Catal
60
62
  }
61
63
 
62
64
  /**
63
- * Eleven round trips, sequential: a pinned transaction connection answers one statement at a
65
+ * Fifteen round trips, sequential: a pinned transaction connection answers one statement at a
64
66
  * time, and this runs once per `x db gen`, never per request.
65
67
  */
66
68
  export async function introspectCatalog(
@@ -77,11 +79,15 @@ export async function introspectCatalog(
77
79
  const functions = await functionRows(client, schema);
78
80
  const triggers = await triggerRows(client, schema);
79
81
  const unrendered = await unrenderedRows(client, schema);
82
+ const security = await rowSecurityRows(client, schema);
83
+ const policies = await policyRows(client, schema);
80
84
  const known = new Set(tables.map((table) => table.name));
85
+ // A policy loads only onto a table the dump creates — the rule `loadable` states for a trigger.
86
+ const onKnown = (row: { readonly table_name: string }): boolean => known.has(row.table_name);
81
87
  const materialized = new Set(views.filter((view) => view.kind === 'm').map((view) => view.name));
82
- // A trigger loads only onto a relation the dump creates: a plain table, or a view (`instead
83
- // of`). One on a partitioned table or a partition was filed under `09_triggers/` for a table no
84
- // file creates, and the load refused the whole dump with `X_SCHEMA_DUMP_DRIFT`.
88
+ // A trigger loads only onto a relation the dump creates: a table, or a view (`instead of`). One
89
+ // on an inheritance child was filed under `09_triggers/` for a table no file creates, and the
90
+ // load refused the whole dump with `X_SCHEMA_DUMP_DRIFT`.
85
91
  const viewNames = new Set(views.map((view) => view.name));
86
92
  const loadable = (trigger: { readonly table_name: string }): boolean =>
87
93
  known.has(trigger.table_name) || viewNames.has(trigger.table_name);
@@ -99,10 +105,14 @@ export async function introspectCatalog(
99
105
  tables: tables
100
106
  .map((table) => tableOf(table, columns, constraints, sequences))
101
107
  .sort(by((table) => table.name)),
102
- // An index on a relation this reading does not render (a partition) has nowhere to load.
108
+ // An index on a relation this reading does not render (an inheritance child) has nowhere to load.
103
109
  indexes: indexes
104
110
  .filter((index) => known.has(index.table_name) || materialized.has(index.table_name))
105
- .map((row) => ({ table: row.table_name, name: row.name, definition: row.definition }))
111
+ .map((row) => ({
112
+ table: row.table_name,
113
+ name: row.name,
114
+ definition: indexDefinitionOf(row.definition),
115
+ }))
106
116
  .sort(
107
117
  by(
108
118
  (index) => index.table,
@@ -124,6 +134,7 @@ export async function introspectCatalog(
124
134
  materialized: row.kind === 'm',
125
135
  options: row.options,
126
136
  definition: row.definition,
137
+ columns: [...row.columns],
127
138
  }))
128
139
  .sort(by((view) => view.name)),
129
140
  functions: functions
@@ -148,8 +159,17 @@ export async function introspectCatalog(
148
159
  (trigger) => trigger.name,
149
160
  ),
150
161
  ),
162
+ rowSecurity: rowSecurityOf(security.filter(onKnown)),
163
+ policies: policiesOf(policies.filter(onKnown)),
151
164
  unrendered: [
152
165
  ...unrendered,
166
+ ...policies
167
+ .filter((policy) => !onKnown(policy))
168
+ .map((policy) => ({
169
+ kind: 'row security policy',
170
+ name: policy.name,
171
+ table_name: policy.table_name,
172
+ })),
153
173
  // Named rather than dropped, the rule `CatalogUnrendered` states.
154
174
  ...triggers
155
175
  .filter((trigger) => !loadable(trigger))