@ultimat3/db 29.4.0 → 30.1.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.
- package/CLAUDE.md +74 -110
- package/README.md +163 -15
- package/package.json +13 -2
- package/src/app-relation.ts +7 -2
- package/src/catalog-fold.ts +5 -0
- package/src/catalog-objects.ts +20 -24
- package/src/catalog-policies.ts +81 -0
- package/src/catalog-relations.ts +31 -10
- package/src/catalog.ts +40 -1
- package/src/check-ddl.ts +34 -7
- package/src/client.ts +132 -48
- package/src/column-alter.ts +42 -1
- package/src/concurrent-index.ts +20 -0
- package/src/concurrent-plan.ts +135 -17
- package/src/connection-url.ts +16 -4
- package/src/db-diagnostics-deadline.ts +43 -0
- package/src/db-diagnostics-redact.ts +58 -0
- package/src/db-diagnostics-rows.ts +166 -0
- package/src/db-diagnostics-sql.ts +92 -0
- package/src/db-diagnostics.ts +178 -0
- package/src/destructive.ts +17 -3
- package/src/diagnostics-entry.ts +15 -0
- package/src/drift-errors.ts +9 -4
- package/src/drift-findings.ts +47 -62
- package/src/drift-missing.ts +203 -0
- package/src/drift-row-security.ts +88 -0
- package/src/drift-triggers.ts +54 -0
- package/src/drift.ts +88 -21
- package/src/entity-shape.ts +67 -0
- package/src/errors.ts +77 -9
- package/src/expected-loop.ts +34 -4
- package/src/failure-store-pg.ts +157 -0
- package/src/foreign-key-plan.ts +75 -23
- package/src/foreign-key.ts +76 -5
- package/src/generate-counter.ts +85 -0
- package/src/generate-history.ts +142 -0
- package/src/generate-row-security.ts +51 -0
- package/src/generate-snapshot.ts +82 -0
- package/src/generate-steps.ts +6 -1
- package/src/generate-triggers.ts +119 -0
- package/src/generate-view.ts +97 -0
- package/src/generate.ts +105 -79
- package/src/index.ts +129 -6
- package/src/introspect-catalog.ts +26 -6
- package/src/introspect-rows.ts +82 -0
- package/src/introspect.ts +108 -50
- package/src/keyset.ts +182 -0
- package/src/lifecycle-errors.ts +15 -0
- package/src/local-settings.ts +38 -0
- package/src/marker-line.ts +12 -4
- package/src/migrate-lock-retry.ts +60 -0
- package/src/migrate-rollback.ts +45 -6
- package/src/migrate.ts +45 -4
- package/src/migration-errors.ts +27 -1
- package/src/migration-ledger.ts +38 -4
- package/src/migration-step.ts +50 -22
- package/src/partition-order.ts +56 -0
- package/src/partitioned-tables.ts +24 -0
- package/src/pool-gate.ts +120 -0
- package/src/pool-listeners.ts +143 -0
- package/src/pool-pins.ts +68 -0
- package/src/pool-profile.ts +23 -7
- package/src/pool-reserve.ts +11 -4
- package/src/pool-retire.ts +22 -0
- package/src/pool-sweep.ts +115 -0
- package/src/pool-timer.ts +10 -0
- package/src/pooler-fix.ts +53 -0
- package/src/pooler.ts +161 -0
- package/src/read-transaction.ts +59 -0
- package/src/readonly-query.ts +4 -4
- package/src/replica-client.ts +54 -22
- package/src/replica-position.ts +89 -0
- package/src/replica-scope.ts +25 -4
- package/src/retired.ts +131 -0
- package/src/retype-dependents.ts +2 -0
- package/src/rolling-errors.ts +97 -0
- package/src/rolling.ts +212 -0
- package/src/row-secure-client.ts +54 -0
- package/src/row-security-catalog.ts +26 -0
- package/src/row-security-migrator.ts +48 -0
- package/src/row-security-plan.ts +58 -0
- package/src/row-security-role.ts +60 -0
- package/src/row-security-roles.ts +64 -0
- package/src/row-security.ts +59 -0
- package/src/same-tenant.ts +173 -0
- package/src/schema-dump-policies.ts +26 -0
- package/src/schema-dump-table.ts +21 -6
- package/src/schema-dump.ts +13 -2
- package/src/snapshot-parse.ts +42 -2
- package/src/sqlstate.ts +4 -0
- package/src/statement-bound.ts +82 -0
- package/src/transaction-errors.ts +14 -0
- package/src/transaction-guard.ts +40 -0
- package/src/transaction-settings.ts +24 -0
- package/src/transaction.ts +74 -19
- 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
|
|
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,
|
|
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
|
|
181
|
-
|
|
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
|
|
206
|
-
//
|
|
207
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
217
|
-
|
|
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
|
-
*
|
|
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
|
|
83
|
-
//
|
|
84
|
-
//
|
|
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 (
|
|
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) => ({
|
|
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))
|