@supabase/pg-delta 1.0.0-alpha.20 → 1.0.0-alpha.22
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/dist/core/catalog.diff.js +4 -4
- package/dist/core/catalog.model.d.ts +8 -1
- package/dist/core/catalog.model.js +9 -8
- package/dist/core/expand-replace-dependencies.js +23 -0
- package/dist/core/objects/extract-with-retry.d.ts +36 -0
- package/dist/core/objects/extract-with-retry.js +51 -0
- package/dist/core/objects/index/index.diff.js +0 -1
- package/dist/core/objects/index/index.model.d.ts +2 -3
- package/dist/core/objects/index/index.model.js +17 -6
- package/dist/core/objects/materialized-view/materialized-view.model.d.ts +2 -1
- package/dist/core/objects/materialized-view/materialized-view.model.js +20 -4
- package/dist/core/objects/procedure/procedure.model.d.ts +2 -1
- package/dist/core/objects/procedure/procedure.model.js +20 -4
- package/dist/core/objects/publication/changes/publication.alter.d.ts +1 -1
- package/dist/core/objects/rls-policy/rls-policy.diff.js +13 -1
- package/dist/core/objects/rule/rule.model.d.ts +2 -1
- package/dist/core/objects/rule/rule.model.js +20 -3
- package/dist/core/objects/sequence/sequence.diff.d.ts +2 -1
- package/dist/core/objects/sequence/sequence.diff.js +41 -9
- package/dist/core/objects/table/changes/table.alter.d.ts +16 -1
- package/dist/core/objects/table/changes/table.alter.js +39 -6
- package/dist/core/objects/table/table.diff.js +40 -17
- package/dist/core/objects/table/table.model.d.ts +6 -1
- package/dist/core/objects/table/table.model.js +50 -12
- package/dist/core/objects/trigger/trigger.model.d.ts +2 -1
- package/dist/core/objects/trigger/trigger.model.js +20 -4
- package/dist/core/objects/utils.d.ts +1 -0
- package/dist/core/objects/utils.js +3 -0
- package/dist/core/objects/view/view.model.d.ts +2 -1
- package/dist/core/objects/view/view.model.js +20 -4
- package/dist/core/plan/create.js +3 -1
- package/dist/core/plan/types.d.ts +8 -0
- package/dist/core/post-diff-normalization.d.ts +36 -0
- package/dist/core/post-diff-normalization.js +202 -0
- package/dist/core/sort/cycle-breakers.d.ts +15 -0
- package/dist/core/sort/cycle-breakers.js +269 -0
- package/dist/core/sort/sort-changes.js +97 -43
- package/dist/core/sort/utils.d.ts +10 -0
- package/dist/core/sort/utils.js +28 -0
- package/package.json +1 -1
- package/src/core/catalog.diff.ts +4 -3
- package/src/core/catalog.model.ts +20 -8
- package/src/core/expand-replace-dependencies.test.ts +139 -5
- package/src/core/expand-replace-dependencies.ts +24 -0
- package/src/core/objects/extract-with-retry.test.ts +143 -0
- package/src/core/objects/extract-with-retry.ts +87 -0
- package/src/core/objects/index/index.diff.ts +0 -1
- package/src/core/objects/index/index.model.test.ts +37 -1
- package/src/core/objects/index/index.model.ts +25 -6
- package/src/core/objects/materialized-view/materialized-view.model.test.ts +93 -0
- package/src/core/objects/materialized-view/materialized-view.model.ts +27 -4
- package/src/core/objects/procedure/procedure.model.test.ts +117 -0
- package/src/core/objects/procedure/procedure.model.ts +28 -5
- package/src/core/objects/publication/changes/publication.alter.ts +1 -1
- package/src/core/objects/rls-policy/rls-policy.diff.ts +19 -1
- package/src/core/objects/rule/rule.model.test.ts +99 -0
- package/src/core/objects/rule/rule.model.ts +28 -4
- package/src/core/objects/sequence/sequence.diff.test.ts +93 -1
- package/src/core/objects/sequence/sequence.diff.ts +43 -10
- package/src/core/objects/table/changes/table.alter.test.ts +26 -23
- package/src/core/objects/table/changes/table.alter.ts +66 -10
- package/src/core/objects/table/table.diff.test.ts +43 -0
- package/src/core/objects/table/table.diff.ts +52 -23
- package/src/core/objects/table/table.model.test.ts +209 -0
- package/src/core/objects/table/table.model.ts +62 -14
- package/src/core/objects/trigger/trigger.model.test.ts +113 -0
- package/src/core/objects/trigger/trigger.model.ts +28 -5
- package/src/core/objects/utils.ts +3 -0
- package/src/core/objects/view/view.model.test.ts +90 -0
- package/src/core/objects/view/view.model.ts +28 -5
- package/src/core/plan/create.ts +3 -1
- package/src/core/plan/types.ts +8 -0
- package/src/core/{post-diff-cycle-breaking.test.ts → post-diff-normalization.test.ts} +168 -160
- package/src/core/post-diff-normalization.ts +260 -0
- package/src/core/sort/cycle-breakers.test.ts +476 -0
- package/src/core/sort/cycle-breakers.ts +311 -0
- package/src/core/sort/sort-changes.ts +135 -50
- package/src/core/sort/utils.ts +38 -0
- package/dist/core/post-diff-cycle-breaking.d.ts +0 -29
- package/dist/core/post-diff-cycle-breaking.js +0 -209
- package/src/core/post-diff-cycle-breaking.ts +0 -317
|
@@ -216,16 +216,27 @@ export declare class AlterTableValidateConstraint extends AlterTableChange {
|
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
218
|
* ALTER TABLE ... REPLICA IDENTITY ...
|
|
219
|
+
*
|
|
220
|
+
* When `mode === "i"` (USING INDEX), `indexName` is the name of the index to
|
|
221
|
+
* use. The extractor populates `Table.replica_identity_index` from
|
|
222
|
+
* `pg_index.indisreplident` whenever `Table.replica_identity` is `'i'`, so
|
|
223
|
+
* callers that source their props from a `Table` instance can rely on the
|
|
224
|
+
* pair being consistent. The non-null assertions in `requires` / `serialize`
|
|
225
|
+
* below are justified by that data invariant — the same pattern the FK
|
|
226
|
+
* branch of `AlterTableAddConstraint` uses for `foreign_key_columns!` /
|
|
227
|
+
* `foreign_key_table!` / `foreign_key_schema!`.
|
|
219
228
|
*/
|
|
220
229
|
export declare class AlterTableSetReplicaIdentity extends AlterTableChange {
|
|
221
230
|
readonly table: Table;
|
|
222
231
|
readonly mode: "d" | "n" | "f" | "i";
|
|
232
|
+
readonly indexName: string | null;
|
|
223
233
|
readonly scope: "object";
|
|
224
234
|
constructor(props: {
|
|
225
235
|
table: Table;
|
|
226
236
|
mode: "d" | "n" | "f" | "i";
|
|
237
|
+
indexName?: string | null;
|
|
227
238
|
});
|
|
228
|
-
get requires():
|
|
239
|
+
get requires(): string[];
|
|
229
240
|
serialize(_options?: SerializeOptions): string;
|
|
230
241
|
}
|
|
231
242
|
/**
|
|
@@ -250,9 +261,11 @@ export declare class AlterTableDropColumn extends AlterTableChange {
|
|
|
250
261
|
readonly table: Table;
|
|
251
262
|
readonly column: ColumnProps;
|
|
252
263
|
readonly scope: "object";
|
|
264
|
+
readonly omitTableRequirement: boolean;
|
|
253
265
|
constructor(props: {
|
|
254
266
|
table: Table;
|
|
255
267
|
column: ColumnProps;
|
|
268
|
+
omitTableRequirement?: boolean;
|
|
256
269
|
});
|
|
257
270
|
get drops(): `column:${string}.${string}.${string}`[];
|
|
258
271
|
get requires(): (`column:${string}.${string}.${string}` | `table:${string}`)[];
|
|
@@ -264,10 +277,12 @@ export declare class AlterTableDropColumn extends AlterTableChange {
|
|
|
264
277
|
export declare class AlterTableAlterColumnType extends AlterTableChange {
|
|
265
278
|
readonly table: Table;
|
|
266
279
|
readonly column: ColumnProps;
|
|
280
|
+
readonly previousColumn?: ColumnProps;
|
|
267
281
|
readonly scope: "object";
|
|
268
282
|
constructor(props: {
|
|
269
283
|
table: Table;
|
|
270
284
|
column: ColumnProps;
|
|
285
|
+
previousColumn?: ColumnProps;
|
|
271
286
|
});
|
|
272
287
|
get requires(): `column:${string}.${string}.${string}`[];
|
|
273
288
|
serialize(_options?: SerializeOptions): string;
|
|
@@ -302,18 +302,35 @@ export class AlterTableValidateConstraint extends AlterTableChange {
|
|
|
302
302
|
}
|
|
303
303
|
/**
|
|
304
304
|
* ALTER TABLE ... REPLICA IDENTITY ...
|
|
305
|
+
*
|
|
306
|
+
* When `mode === "i"` (USING INDEX), `indexName` is the name of the index to
|
|
307
|
+
* use. The extractor populates `Table.replica_identity_index` from
|
|
308
|
+
* `pg_index.indisreplident` whenever `Table.replica_identity` is `'i'`, so
|
|
309
|
+
* callers that source their props from a `Table` instance can rely on the
|
|
310
|
+
* pair being consistent. The non-null assertions in `requires` / `serialize`
|
|
311
|
+
* below are justified by that data invariant — the same pattern the FK
|
|
312
|
+
* branch of `AlterTableAddConstraint` uses for `foreign_key_columns!` /
|
|
313
|
+
* `foreign_key_table!` / `foreign_key_schema!`.
|
|
305
314
|
*/
|
|
306
315
|
export class AlterTableSetReplicaIdentity extends AlterTableChange {
|
|
307
316
|
table;
|
|
308
317
|
mode;
|
|
318
|
+
indexName;
|
|
309
319
|
scope = "object";
|
|
310
320
|
constructor(props) {
|
|
311
321
|
super();
|
|
312
322
|
this.table = props.table;
|
|
313
323
|
this.mode = props.mode;
|
|
324
|
+
this.indexName = props.indexName ?? null;
|
|
314
325
|
}
|
|
315
326
|
get requires() {
|
|
316
|
-
|
|
327
|
+
const reqs = [this.table.stableId];
|
|
328
|
+
if (this.mode === "i") {
|
|
329
|
+
reqs.push(stableId.index(this.table.schema, this.table.name,
|
|
330
|
+
// biome-ignore lint/style/noNonNullAssertion: mode 'i' implies the extractor populated replica_identity_index
|
|
331
|
+
this.indexName));
|
|
332
|
+
}
|
|
333
|
+
return reqs;
|
|
317
334
|
}
|
|
318
335
|
serialize(_options) {
|
|
319
336
|
const clause = this.mode === "d"
|
|
@@ -322,7 +339,8 @@ export class AlterTableSetReplicaIdentity extends AlterTableChange {
|
|
|
322
339
|
? "NOTHING"
|
|
323
340
|
: this.mode === "f"
|
|
324
341
|
? "FULL"
|
|
325
|
-
:
|
|
342
|
+
: // biome-ignore lint/style/noNonNullAssertion: mode 'i' implies the extractor populated replica_identity_index
|
|
343
|
+
`USING INDEX ${this.indexName}`;
|
|
326
344
|
return [
|
|
327
345
|
"ALTER TABLE",
|
|
328
346
|
`${this.table.schema}.${this.table.name}`,
|
|
@@ -386,10 +404,16 @@ export class AlterTableDropColumn extends AlterTableChange {
|
|
|
386
404
|
table;
|
|
387
405
|
column;
|
|
388
406
|
scope = "object";
|
|
407
|
+
// Drop the implicit `requires(table)` edge. Only set by the lazy
|
|
408
|
+
// cycle-breaker for the publication↔column case, where the table survives
|
|
409
|
+
// the migration and the edge is therefore artificial. See
|
|
410
|
+
// `sort/cycle-breakers.ts` for the full justification.
|
|
411
|
+
omitTableRequirement;
|
|
389
412
|
constructor(props) {
|
|
390
413
|
super();
|
|
391
414
|
this.table = props.table;
|
|
392
415
|
this.column = props.column;
|
|
416
|
+
this.omitTableRequirement = props.omitTableRequirement ?? false;
|
|
393
417
|
}
|
|
394
418
|
get drops() {
|
|
395
419
|
return [
|
|
@@ -397,10 +421,8 @@ export class AlterTableDropColumn extends AlterTableChange {
|
|
|
397
421
|
];
|
|
398
422
|
}
|
|
399
423
|
get requires() {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
stableId.column(this.table.schema, this.table.name, this.column.name),
|
|
403
|
-
];
|
|
424
|
+
const colId = stableId.column(this.table.schema, this.table.name, this.column.name);
|
|
425
|
+
return this.omitTableRequirement ? [colId] : [this.table.stableId, colId];
|
|
404
426
|
}
|
|
405
427
|
serialize(_options) {
|
|
406
428
|
return [
|
|
@@ -417,11 +439,13 @@ export class AlterTableDropColumn extends AlterTableChange {
|
|
|
417
439
|
export class AlterTableAlterColumnType extends AlterTableChange {
|
|
418
440
|
table;
|
|
419
441
|
column;
|
|
442
|
+
previousColumn;
|
|
420
443
|
scope = "object";
|
|
421
444
|
constructor(props) {
|
|
422
445
|
super();
|
|
423
446
|
this.table = props.table;
|
|
424
447
|
this.column = props.column;
|
|
448
|
+
this.previousColumn = props.previousColumn;
|
|
425
449
|
}
|
|
426
450
|
get requires() {
|
|
427
451
|
return [
|
|
@@ -429,6 +453,12 @@ export class AlterTableAlterColumnType extends AlterTableChange {
|
|
|
429
453
|
];
|
|
430
454
|
}
|
|
431
455
|
serialize(_options) {
|
|
456
|
+
// previousColumn is optional so direct serializer tests/fixtures can keep
|
|
457
|
+
// emitting canonical ALTER TYPE SQL without forcing a USING expression.
|
|
458
|
+
// When provided, we can detect true type changes and add USING for casts
|
|
459
|
+
// PostgreSQL cannot perform automatically.
|
|
460
|
+
const hasTypeChangedWithPreviousDefinition = this.previousColumn?.data_type_str !== undefined &&
|
|
461
|
+
this.previousColumn.data_type_str !== this.column.data_type_str;
|
|
432
462
|
const parts = [
|
|
433
463
|
"ALTER TABLE",
|
|
434
464
|
`${this.table.schema}.${this.table.name}`,
|
|
@@ -440,6 +470,9 @@ export class AlterTableAlterColumnType extends AlterTableChange {
|
|
|
440
470
|
if (this.column.collation) {
|
|
441
471
|
parts.push("COLLATE", this.column.collation);
|
|
442
472
|
}
|
|
473
|
+
if (hasTypeChangedWithPreviousDefinition) {
|
|
474
|
+
parts.push("USING", `${this.column.name}::${this.column.data_type_str}`);
|
|
475
|
+
}
|
|
443
476
|
return parts.join(" ");
|
|
444
477
|
}
|
|
445
478
|
}
|
|
@@ -152,13 +152,11 @@ export function diffTables(ctx, main, branch) {
|
|
|
152
152
|
}
|
|
153
153
|
// REPLICA IDENTITY: If non-default, emit ALTER TABLE ... REPLICA IDENTITY
|
|
154
154
|
if (branchTable.replica_identity !== "d") {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}));
|
|
161
|
-
}
|
|
155
|
+
changes.push(new AlterTableSetReplicaIdentity({
|
|
156
|
+
table: branchTable,
|
|
157
|
+
mode: branchTable.replica_identity,
|
|
158
|
+
indexName: branchTable.replica_identity_index,
|
|
159
|
+
}));
|
|
162
160
|
}
|
|
163
161
|
changes.push(...createAlterConstraintChange(
|
|
164
162
|
// Create a dummy table with no constraints do diff constraints against
|
|
@@ -261,14 +259,20 @@ export function diffTables(ctx, main, branch) {
|
|
|
261
259
|
}
|
|
262
260
|
}
|
|
263
261
|
// REPLICA IDENTITY
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
262
|
+
// Re-emit when the mode changes, or when staying in 'i' mode but pointing
|
|
263
|
+
// at a different index. The index named on the branch must already exist
|
|
264
|
+
// before this ALTER runs; AlterTableSetReplicaIdentity declares that
|
|
265
|
+
// dependency in its `requires`.
|
|
266
|
+
const replicaIdentityChanged = mainTable.replica_identity !== branchTable.replica_identity ||
|
|
267
|
+
(branchTable.replica_identity === "i" &&
|
|
268
|
+
mainTable.replica_identity_index !==
|
|
269
|
+
branchTable.replica_identity_index);
|
|
270
|
+
if (replicaIdentityChanged) {
|
|
271
|
+
changes.push(new AlterTableSetReplicaIdentity({
|
|
272
|
+
table: mainTable,
|
|
273
|
+
mode: branchTable.replica_identity,
|
|
274
|
+
indexName: branchTable.replica_identity_index,
|
|
275
|
+
}));
|
|
272
276
|
}
|
|
273
277
|
// OWNER
|
|
274
278
|
if (mainTable.owner !== branchTable.owner) {
|
|
@@ -451,15 +455,30 @@ export function diffTables(ctx, main, branch) {
|
|
|
451
455
|
const branchCol = branchCols.get(name);
|
|
452
456
|
if (!branchCol)
|
|
453
457
|
continue;
|
|
458
|
+
const columnTypeChanged = mainCol.data_type_str !== branchCol.data_type_str;
|
|
459
|
+
const columnCollationChanged = mainCol.collation !== branchCol.collation;
|
|
460
|
+
const needsDefaultSafeFlow = columnTypeChanged && mainCol.default !== null;
|
|
454
461
|
// TYPE or COLLATION change
|
|
455
|
-
if (
|
|
456
|
-
mainCol.collation !== branchCol.collation) {
|
|
462
|
+
if (columnTypeChanged || columnCollationChanged) {
|
|
457
463
|
// Skip if parent has the same type/collation change
|
|
458
464
|
if (!parentHasSameColumnPropertyChange(name, "type")) {
|
|
465
|
+
if (needsDefaultSafeFlow) {
|
|
466
|
+
changes.push(new AlterTableAlterColumnDropDefault({
|
|
467
|
+
table: branchTable,
|
|
468
|
+
column: branchCol,
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
459
471
|
changes.push(new AlterTableAlterColumnType({
|
|
460
472
|
table: branchTable,
|
|
461
473
|
column: branchCol,
|
|
474
|
+
previousColumn: mainCol,
|
|
462
475
|
}));
|
|
476
|
+
if (needsDefaultSafeFlow && branchCol.default !== null) {
|
|
477
|
+
changes.push(new AlterTableAlterColumnSetDefault({
|
|
478
|
+
table: branchTable,
|
|
479
|
+
column: branchCol,
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
463
482
|
}
|
|
464
483
|
}
|
|
465
484
|
// PostgreSQL rejects SET DEFAULT while the column still has identity metadata,
|
|
@@ -476,6 +495,10 @@ export function diffTables(ctx, main, branch) {
|
|
|
476
495
|
if (mainCol.default !== branchCol.default) {
|
|
477
496
|
// Skip if parent has the same default change
|
|
478
497
|
if (!parentHasSameColumnPropertyChange(name, "default")) {
|
|
498
|
+
if (needsDefaultSafeFlow) {
|
|
499
|
+
// Defaults were already dropped/re-set in the type-change flow above.
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
479
502
|
if (branchCol.default === null) {
|
|
480
503
|
// Drop default value
|
|
481
504
|
changes.push(new AlterTableAlterColumnDropDefault({
|
|
@@ -2,6 +2,7 @@ import type { Pool } from "pg";
|
|
|
2
2
|
import z from "zod";
|
|
3
3
|
import { BasePgModel, type TableLikeObject } from "../base.model.ts";
|
|
4
4
|
import { type PrivilegeProps } from "../base.privilege-diff.ts";
|
|
5
|
+
import { type ExtractRetryOptions } from "../extract-with-retry.ts";
|
|
5
6
|
export declare const ReplicaIdentitySchema: z.ZodEnum<{
|
|
6
7
|
n: "n";
|
|
7
8
|
i: "i";
|
|
@@ -85,6 +86,7 @@ declare const tablePropsSchema: z.ZodObject<{
|
|
|
85
86
|
d: "d";
|
|
86
87
|
f: "f";
|
|
87
88
|
}>;
|
|
89
|
+
replica_identity_index: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
88
90
|
is_partition: z.ZodBoolean;
|
|
89
91
|
options: z.ZodNullable<z.ZodArray<z.ZodString>>;
|
|
90
92
|
partition_bound: z.ZodNullable<z.ZodString>;
|
|
@@ -187,6 +189,7 @@ export declare class Table extends BasePgModel implements TableLikeObject {
|
|
|
187
189
|
readonly has_subclasses: TableProps["has_subclasses"];
|
|
188
190
|
readonly is_populated: TableProps["is_populated"];
|
|
189
191
|
readonly replica_identity: TableProps["replica_identity"];
|
|
192
|
+
readonly replica_identity_index: TableProps["replica_identity_index"];
|
|
190
193
|
readonly is_partition: TableProps["is_partition"];
|
|
191
194
|
readonly options: TableProps["options"];
|
|
192
195
|
readonly partition_bound: TableProps["partition_bound"];
|
|
@@ -209,6 +212,7 @@ export declare class Table extends BasePgModel implements TableLikeObject {
|
|
|
209
212
|
row_security: boolean;
|
|
210
213
|
force_row_security: boolean;
|
|
211
214
|
replica_identity: "n" | "i" | "d" | "f";
|
|
215
|
+
replica_identity_index: string | null | undefined;
|
|
212
216
|
options: string[] | null;
|
|
213
217
|
parent_schema: string | null;
|
|
214
218
|
parent_name: string | null;
|
|
@@ -336,6 +340,7 @@ export declare class Table extends BasePgModel implements TableLikeObject {
|
|
|
336
340
|
row_security: boolean;
|
|
337
341
|
force_row_security: boolean;
|
|
338
342
|
replica_identity: "n" | "i" | "d" | "f";
|
|
343
|
+
replica_identity_index: string | null | undefined;
|
|
339
344
|
parent_schema: string | null;
|
|
340
345
|
parent_name: string | null;
|
|
341
346
|
partition_bound: string | null;
|
|
@@ -344,5 +349,5 @@ export declare class Table extends BasePgModel implements TableLikeObject {
|
|
|
344
349
|
};
|
|
345
350
|
};
|
|
346
351
|
}
|
|
347
|
-
export declare function extractTables(pool: Pool): Promise<Table[]>;
|
|
352
|
+
export declare function extractTables(pool: Pool, options?: ExtractRetryOptions): Promise<Table[]>;
|
|
348
353
|
export {};
|
|
@@ -3,6 +3,7 @@ import z from "zod";
|
|
|
3
3
|
import { BasePgModel, columnPropsSchema, normalizeColumns, } from "../base.model.js";
|
|
4
4
|
import { normalizePrivileges } from "../base.privilege.js";
|
|
5
5
|
import { privilegePropsSchema, } from "../base.privilege-diff.js";
|
|
6
|
+
import { extractWithDefinitionRetry, } from "../extract-with-retry.js";
|
|
6
7
|
const RelationPersistenceSchema = z.enum([
|
|
7
8
|
"p", // permanent
|
|
8
9
|
"u", // unlogged
|
|
@@ -65,6 +66,14 @@ const tableConstraintPropsSchema = z.object({
|
|
|
65
66
|
definition: z.string(),
|
|
66
67
|
comment: z.string().nullable().optional(),
|
|
67
68
|
});
|
|
69
|
+
// pg_get_constraintdef(oid, pretty) can return NULL under the same conditions
|
|
70
|
+
// as pg_get_indexdef: races with concurrent DDL, transient catalog
|
|
71
|
+
// inconsistencies, recovery edges. An unreadable constraint cannot be diffed,
|
|
72
|
+
// so we accept NULL here and filter the constraint out at extraction time
|
|
73
|
+
// rather than crashing the whole catalog parse with a ZodError.
|
|
74
|
+
const tableConstraintRowSchema = tableConstraintPropsSchema.extend({
|
|
75
|
+
definition: z.string().nullable(),
|
|
76
|
+
});
|
|
68
77
|
const tablePropsSchema = z.object({
|
|
69
78
|
schema: z.string(),
|
|
70
79
|
name: z.string(),
|
|
@@ -77,6 +86,7 @@ const tablePropsSchema = z.object({
|
|
|
77
86
|
has_subclasses: z.boolean(),
|
|
78
87
|
is_populated: z.boolean(),
|
|
79
88
|
replica_identity: ReplicaIdentitySchema,
|
|
89
|
+
replica_identity_index: z.string().nullable().optional(),
|
|
80
90
|
is_partition: z.boolean(),
|
|
81
91
|
options: z.array(z.string()).nullable(),
|
|
82
92
|
partition_bound: z.string().nullable(),
|
|
@@ -89,6 +99,9 @@ const tablePropsSchema = z.object({
|
|
|
89
99
|
constraints: z.array(tableConstraintPropsSchema).optional(),
|
|
90
100
|
privileges: z.array(privilegePropsSchema),
|
|
91
101
|
});
|
|
102
|
+
const tableRowSchema = tablePropsSchema.extend({
|
|
103
|
+
constraints: z.array(tableConstraintRowSchema).optional(),
|
|
104
|
+
});
|
|
92
105
|
export class Table extends BasePgModel {
|
|
93
106
|
schema;
|
|
94
107
|
name;
|
|
@@ -101,6 +114,7 @@ export class Table extends BasePgModel {
|
|
|
101
114
|
has_subclasses;
|
|
102
115
|
is_populated;
|
|
103
116
|
replica_identity;
|
|
117
|
+
replica_identity_index;
|
|
104
118
|
is_partition;
|
|
105
119
|
options;
|
|
106
120
|
partition_bound;
|
|
@@ -127,6 +141,7 @@ export class Table extends BasePgModel {
|
|
|
127
141
|
this.has_subclasses = props.has_subclasses;
|
|
128
142
|
this.is_populated = props.is_populated;
|
|
129
143
|
this.replica_identity = props.replica_identity;
|
|
144
|
+
this.replica_identity_index = props.replica_identity_index ?? null;
|
|
130
145
|
this.is_partition = props.is_partition;
|
|
131
146
|
this.options = props.options;
|
|
132
147
|
this.partition_bound = props.partition_bound;
|
|
@@ -155,6 +170,7 @@ export class Table extends BasePgModel {
|
|
|
155
170
|
row_security: this.row_security,
|
|
156
171
|
force_row_security: this.force_row_security,
|
|
157
172
|
replica_identity: this.replica_identity,
|
|
173
|
+
replica_identity_index: this.replica_identity_index,
|
|
158
174
|
options: this.options,
|
|
159
175
|
// Partition membership can be altered via ATTACH/DETACH
|
|
160
176
|
parent_schema: this.parent_schema,
|
|
@@ -185,8 +201,13 @@ export class Table extends BasePgModel {
|
|
|
185
201
|
};
|
|
186
202
|
}
|
|
187
203
|
}
|
|
188
|
-
export async function extractTables(pool) {
|
|
189
|
-
const
|
|
204
|
+
export async function extractTables(pool, options) {
|
|
205
|
+
const tableRows = await extractWithDefinitionRetry({
|
|
206
|
+
label: "table constraints",
|
|
207
|
+
options,
|
|
208
|
+
hasNullDefinition: (row) => row.constraints?.some((c) => c.definition === null) ?? false,
|
|
209
|
+
query: async () => {
|
|
210
|
+
const result = await pool.query(sql `
|
|
190
211
|
with extension_oids as (
|
|
191
212
|
select objid
|
|
192
213
|
from pg_depend d
|
|
@@ -205,6 +226,14 @@ with extension_oids as (
|
|
|
205
226
|
c.relhassubclass as has_subclasses,
|
|
206
227
|
c.relispopulated as is_populated,
|
|
207
228
|
c.relreplident as replica_identity,
|
|
229
|
+
(
|
|
230
|
+
select quote_ident(ri_class.relname)
|
|
231
|
+
from pg_index ri
|
|
232
|
+
join pg_class ri_class on ri_class.oid = ri.indexrelid
|
|
233
|
+
where ri.indrelid = c.oid
|
|
234
|
+
and ri.indisreplident is true
|
|
235
|
+
limit 1
|
|
236
|
+
) as replica_identity_index,
|
|
208
237
|
c.relispartition as is_partition,
|
|
209
238
|
c.reloptions as options,
|
|
210
239
|
pg_get_expr(c.relpartbound, c.oid) as partition_bound,
|
|
@@ -235,6 +264,7 @@ select
|
|
|
235
264
|
t.has_subclasses,
|
|
236
265
|
t.is_populated,
|
|
237
266
|
t.replica_identity,
|
|
267
|
+
t.replica_identity_index,
|
|
238
268
|
t.is_partition,
|
|
239
269
|
t.options,
|
|
240
270
|
t.partition_bound,
|
|
@@ -265,13 +295,16 @@ select
|
|
|
265
295
|
|
|
266
296
|
'key_columns',
|
|
267
297
|
case
|
|
268
|
-
when c.conkey is not null then (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
298
|
+
when c.conkey is not null then coalesce(
|
|
299
|
+
(
|
|
300
|
+
select json_agg(quote_ident(att.attname) order by pk.ordinality)
|
|
301
|
+
from unnest(c.conkey) with ordinality as pk(attnum, ordinality)
|
|
302
|
+
join pg_attribute att
|
|
303
|
+
on att.attrelid = c.conrelid
|
|
304
|
+
and att.attnum = pk.attnum
|
|
305
|
+
and att.attisdropped = false
|
|
306
|
+
),
|
|
307
|
+
'[]'::json
|
|
275
308
|
)
|
|
276
309
|
else '[]'::json
|
|
277
310
|
end,
|
|
@@ -419,11 +452,16 @@ from
|
|
|
419
452
|
left join pg_attrdef ad on a.attrelid = ad.adrelid and a.attnum = ad.adnum
|
|
420
453
|
left join pg_type ty on ty.oid = a.atttypid
|
|
421
454
|
group by
|
|
422
|
-
t.oid, t.schema, t.name, t.persistence, t.row_security, t.force_row_security, t.has_indexes, t.has_rules, t.has_triggers, t.has_subclasses, t.is_populated, t.replica_identity, t.is_partition, t.options, t.partition_bound, t.partition_by, t.owner, t.parent_schema, t.parent_name
|
|
455
|
+
t.oid, t.schema, t.name, t.persistence, t.row_security, t.force_row_security, t.has_indexes, t.has_rules, t.has_triggers, t.has_subclasses, t.is_populated, t.replica_identity, t.replica_identity_index, t.is_partition, t.options, t.partition_bound, t.partition_by, t.owner, t.parent_schema, t.parent_name
|
|
423
456
|
order by
|
|
424
457
|
t.schema, t.name
|
|
425
458
|
`);
|
|
426
|
-
|
|
427
|
-
|
|
459
|
+
return result.rows.map((row) => tableRowSchema.parse(row));
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
const validatedRows = tableRows.map((row) => {
|
|
463
|
+
const filteredConstraints = row.constraints?.filter((c) => c.definition !== null);
|
|
464
|
+
return { ...row, constraints: filteredConstraints };
|
|
465
|
+
});
|
|
428
466
|
return validatedRows.map((row) => new Table(row));
|
|
429
467
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Pool } from "pg";
|
|
2
2
|
import z from "zod";
|
|
3
3
|
import { BasePgModel } from "../base.model.ts";
|
|
4
|
+
import { type ExtractRetryOptions } from "../extract-with-retry.ts";
|
|
4
5
|
declare const triggerPropsSchema: z.ZodObject<{
|
|
5
6
|
schema: z.ZodString;
|
|
6
7
|
name: z.ZodString;
|
|
@@ -97,5 +98,5 @@ export declare class Trigger extends BasePgModel {
|
|
|
97
98
|
comment: string | null;
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
|
-
export declare function extractTriggers(pool: Pool): Promise<Trigger[]>;
|
|
101
|
+
export declare function extractTriggers(pool: Pool, options?: ExtractRetryOptions): Promise<Trigger[]>;
|
|
101
102
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sql } from "@ts-safeql/sql-tag";
|
|
2
2
|
import z from "zod";
|
|
3
3
|
import { BasePgModel } from "../base.model.js";
|
|
4
|
+
import { extractWithDefinitionRetry, } from "../extract-with-retry.js";
|
|
4
5
|
const TriggerEnabledSchema = z.enum([
|
|
5
6
|
"O", // ORIGIN - trigger fires in "origin" and "local" replica modes
|
|
6
7
|
"D", // DISABLED - trigger is disabled
|
|
@@ -41,6 +42,14 @@ const triggerPropsSchema = z.object({
|
|
|
41
42
|
definition: z.string(),
|
|
42
43
|
comment: z.string().nullable(),
|
|
43
44
|
});
|
|
45
|
+
// pg_get_triggerdef(oid, pretty) can return NULL when the trigger (its
|
|
46
|
+
// pg_trigger row) is dropped between catalog scan and resolution, or under
|
|
47
|
+
// transient catalog state. An unreadable trigger cannot be diffed, so we
|
|
48
|
+
// accept NULL here and filter the row out at extraction time rather than
|
|
49
|
+
// crashing the whole catalog parse with a ZodError.
|
|
50
|
+
const triggerRowSchema = triggerPropsSchema.extend({
|
|
51
|
+
definition: z.string().nullable(),
|
|
52
|
+
});
|
|
44
53
|
export class Trigger extends BasePgModel {
|
|
45
54
|
schema;
|
|
46
55
|
name;
|
|
@@ -139,8 +148,13 @@ export class Trigger extends BasePgModel {
|
|
|
139
148
|
};
|
|
140
149
|
}
|
|
141
150
|
}
|
|
142
|
-
export async function extractTriggers(pool) {
|
|
143
|
-
const
|
|
151
|
+
export async function extractTriggers(pool, options) {
|
|
152
|
+
const triggerRows = await extractWithDefinitionRetry({
|
|
153
|
+
label: "triggers",
|
|
154
|
+
options,
|
|
155
|
+
hasNullDefinition: (row) => row.definition === null,
|
|
156
|
+
query: async () => {
|
|
157
|
+
const result = await pool.query(sql `
|
|
144
158
|
with extension_trigger_oids as (
|
|
145
159
|
select objid
|
|
146
160
|
from pg_depend d
|
|
@@ -245,7 +259,9 @@ export async function extractTriggers(pool) {
|
|
|
245
259
|
|
|
246
260
|
order by 1, 2
|
|
247
261
|
`);
|
|
248
|
-
|
|
249
|
-
|
|
262
|
+
return result.rows.map((row) => triggerRowSchema.parse(row));
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const validatedRows = triggerRows.filter((row) => row.definition !== null);
|
|
250
266
|
return validatedRows.map((row) => new Trigger(row));
|
|
251
267
|
}
|
|
@@ -20,6 +20,7 @@ export declare const stableId: {
|
|
|
20
20
|
defacl(grantor: string, objtype: string, schema: string | null, grantee: string): `defacl:${string}:${string}:${string}:grantee:${string}`;
|
|
21
21
|
column(schema: string, table: string, column: string): `column:${string}.${string}.${string}`;
|
|
22
22
|
constraint(schema: string, table: string, constraint: string): `constraint:${string}.${string}.${string}`;
|
|
23
|
+
index(schema: string, table: string, indexName: string): `index:${string}.${string}.${string}`;
|
|
23
24
|
comment(objectStableId: string): `comment:${string}`;
|
|
24
25
|
role(role: string): `role:${string}`;
|
|
25
26
|
type(schema: string, name: string): `type:${string}.${string}`;
|
|
@@ -49,6 +49,9 @@ export const stableId = {
|
|
|
49
49
|
constraint(schema, table, constraint) {
|
|
50
50
|
return `constraint:${schema}.${table}.${constraint}`;
|
|
51
51
|
},
|
|
52
|
+
index(schema, table, indexName) {
|
|
53
|
+
return `index:${schema}.${table}.${indexName}`;
|
|
54
|
+
},
|
|
52
55
|
comment(objectStableId) {
|
|
53
56
|
return `comment:${objectStableId}`;
|
|
54
57
|
},
|
|
@@ -2,6 +2,7 @@ import type { Pool } from "pg";
|
|
|
2
2
|
import z from "zod";
|
|
3
3
|
import { BasePgModel, type TableLikeObject } from "../base.model.ts";
|
|
4
4
|
import { type PrivilegeProps } from "../base.privilege-diff.ts";
|
|
5
|
+
import { type ExtractRetryOptions } from "../extract-with-retry.ts";
|
|
5
6
|
declare const viewPropsSchema: z.ZodObject<{
|
|
6
7
|
schema: z.ZodString;
|
|
7
8
|
name: z.ZodString;
|
|
@@ -162,5 +163,5 @@ export declare class View extends BasePgModel implements TableLikeObject {
|
|
|
162
163
|
};
|
|
163
164
|
};
|
|
164
165
|
}
|
|
165
|
-
export declare function extractViews(pool: Pool): Promise<View[]>;
|
|
166
|
+
export declare function extractViews(pool: Pool, options?: ExtractRetryOptions): Promise<View[]>;
|
|
166
167
|
export {};
|
|
@@ -2,6 +2,7 @@ import { sql } from "@ts-safeql/sql-tag";
|
|
|
2
2
|
import z from "zod";
|
|
3
3
|
import { BasePgModel, columnPropsSchema, normalizeColumns, } from "../base.model.js";
|
|
4
4
|
import { privilegePropsSchema, } from "../base.privilege-diff.js";
|
|
5
|
+
import { extractWithDefinitionRetry, } from "../extract-with-retry.js";
|
|
5
6
|
import { ReplicaIdentitySchema } from "../table/table.model.js";
|
|
6
7
|
const viewPropsSchema = z.object({
|
|
7
8
|
schema: z.string(),
|
|
@@ -23,6 +24,14 @@ const viewPropsSchema = z.object({
|
|
|
23
24
|
columns: z.array(columnPropsSchema),
|
|
24
25
|
privileges: z.array(privilegePropsSchema),
|
|
25
26
|
});
|
|
27
|
+
// pg_get_viewdef(oid) can return NULL when the underlying view (or its
|
|
28
|
+
// pg_rewrite row) is dropped between catalog scan and resolution, or under
|
|
29
|
+
// transient catalog state during recovery. An unreadable view cannot be
|
|
30
|
+
// diffed, so we accept NULL here and filter the row out at extraction time
|
|
31
|
+
// rather than crashing the whole catalog parse with a ZodError.
|
|
32
|
+
const viewRowSchema = viewPropsSchema.extend({
|
|
33
|
+
definition: z.string().nullable(),
|
|
34
|
+
});
|
|
26
35
|
export class View extends BasePgModel {
|
|
27
36
|
schema;
|
|
28
37
|
name;
|
|
@@ -104,8 +113,13 @@ export class View extends BasePgModel {
|
|
|
104
113
|
};
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
|
-
export async function extractViews(pool) {
|
|
108
|
-
const
|
|
116
|
+
export async function extractViews(pool, options) {
|
|
117
|
+
const viewRows = await extractWithDefinitionRetry({
|
|
118
|
+
label: "views",
|
|
119
|
+
options,
|
|
120
|
+
hasNullDefinition: (row) => row.definition === null,
|
|
121
|
+
query: async () => {
|
|
122
|
+
const result = await pool.query(sql `
|
|
109
123
|
with extension_oids as (
|
|
110
124
|
select
|
|
111
125
|
objid
|
|
@@ -232,7 +246,9 @@ group by
|
|
|
232
246
|
order by
|
|
233
247
|
v.schema, v.name
|
|
234
248
|
`);
|
|
235
|
-
|
|
236
|
-
|
|
249
|
+
return result.rows.map((row) => viewRowSchema.parse(row));
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
const validatedRows = viewRows.filter((row) => row.definition !== null);
|
|
237
253
|
return validatedRows.map((row) => new View(row));
|
|
238
254
|
}
|
package/dist/core/plan/create.js
CHANGED
|
@@ -62,7 +62,9 @@ export async function createPlan(source, target, options = {}) {
|
|
|
62
62
|
}
|
|
63
63
|
const resolved = await resolvePool(input, label);
|
|
64
64
|
pools.push(resolved);
|
|
65
|
-
return extractCatalog(resolved.pool
|
|
65
|
+
return extractCatalog(resolved.pool, {
|
|
66
|
+
extractRetries: options.extractRetries,
|
|
67
|
+
});
|
|
66
68
|
};
|
|
67
69
|
const pools = [];
|
|
68
70
|
try {
|
|
@@ -141,5 +141,13 @@ export interface CreatePlanOptions {
|
|
|
141
141
|
* the output must be self-contained and not rely on statement execution order.
|
|
142
142
|
*/
|
|
143
143
|
skipDefaultPrivilegeSubtraction?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Number of retry attempts for catalog extractors when `pg_get_*def()`
|
|
146
|
+
* returns NULL for at least one row (a transient race with concurrent DDL).
|
|
147
|
+
* Total attempts is `extractRetries + 1`. When undefined, the value is read
|
|
148
|
+
* from the `PGDELTA_EXTRACT_RETRIES` environment variable, falling back to
|
|
149
|
+
* a default of 1 (i.e. the first attempt plus one retry, 2 attempts total).
|
|
150
|
+
*/
|
|
151
|
+
extractRetries?: number;
|
|
144
152
|
}
|
|
145
153
|
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Change } from "./change.types.ts";
|
|
2
|
+
import type { Table } from "./objects/table/table.model.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Apply structural rewrites to the change list that are only obvious once
|
|
5
|
+
* every object diff has been collected. This pass does NOT prevent dependency
|
|
6
|
+
* cycles — that responsibility now lives in the sort phase, where
|
|
7
|
+
* `sortPhaseChanges` invokes `tryBreakCycleByChangeInjection` lazily on cycles
|
|
8
|
+
* that edge filtering can't break (FK SCC of dropped tables,
|
|
9
|
+
* AlterPublicationDropTables ↔ AlterTableDropColumn, …).
|
|
10
|
+
*
|
|
11
|
+
* Concretely, this pass:
|
|
12
|
+
*
|
|
13
|
+
* - Prunes `AlterTableDropColumn(T.*)` / `AlterTableDropConstraint(T.*)`
|
|
14
|
+
* changes that are made redundant by an expansion-emitted
|
|
15
|
+
* `DropTable(T) + CreateTable(T)` pair. Without this, the apply phase
|
|
16
|
+
* would try to drop a column that no longer exists in the freshly
|
|
17
|
+
* recreated table.
|
|
18
|
+
* - Dedupes duplicate `AlterTableAddConstraint` /
|
|
19
|
+
* `AlterTableValidateConstraint` / `CreateCommentOnConstraint` changes
|
|
20
|
+
* produced when `diffTables()` and `expandReplaceDependencies()` both
|
|
21
|
+
* emit the same constraint operation for a replaced table. Last write
|
|
22
|
+
* wins so the expansion's emission survives.
|
|
23
|
+
* - Re-emits `ALTER TABLE ... REPLICA IDENTITY USING INDEX <idx>` after any
|
|
24
|
+
* `DropIndex(idx) + CreateIndex(idx)` pair where `idx` is the replica
|
|
25
|
+
* identity index of a branch table — Postgres silently clears the marker
|
|
26
|
+
* when the underlying index is dropped, and `CREATE INDEX` cannot restore
|
|
27
|
+
* it.
|
|
28
|
+
*
|
|
29
|
+
* Object-local PostgreSQL semantics (for example owned-sequence cascades)
|
|
30
|
+
* stay in the corresponding `diff*` function instead of this pass.
|
|
31
|
+
*/
|
|
32
|
+
export declare function normalizePostDiffChanges({ changes, replacedTableIds, branchTables, }: {
|
|
33
|
+
changes: Change[];
|
|
34
|
+
replacedTableIds?: ReadonlySet<string>;
|
|
35
|
+
branchTables?: Record<string, Table>;
|
|
36
|
+
}): Change[];
|