@supabase/pg-delta 1.0.0-alpha.21 → 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 -3
- 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/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 +28 -4
- package/dist/core/objects/table/changes/table.alter.d.ts +12 -1
- package/dist/core/objects/table/changes/table.alter.js +20 -2
- package/dist/core/objects/table/table.diff.js +19 -15
- package/dist/core/objects/table/table.model.d.ts +6 -1
- package/dist/core/objects/table/table.model.js +40 -5
- 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-cycle-breaking.d.ts → post-diff-normalization.d.ts} +8 -1
- package/dist/core/post-diff-normalization.js +202 -0
- package/dist/core/sort/cycle-breakers.js +1 -1
- 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 -2
- package/src/core/catalog.model.ts +20 -8
- package/src/core/expand-replace-dependencies.test.ts +131 -0
- 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/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 +87 -0
- package/src/core/objects/sequence/sequence.diff.ts +31 -6
- package/src/core/objects/table/changes/table.alter.test.ts +13 -21
- package/src/core/objects/table/changes/table.alter.ts +30 -3
- package/src/core/objects/table/table.diff.ts +24 -19
- package/src/core/objects/table/table.model.test.ts +209 -0
- package/src/core/objects/table/table.model.ts +52 -7
- 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 -4
- package/src/core/post-diff-normalization.ts +260 -0
- package/src/core/sort/cycle-breakers.ts +1 -1
- package/src/core/sort/utils.ts +38 -0
- package/dist/core/post-diff-cycle-breaking.js +0 -100
- package/src/core/post-diff-cycle-breaking.ts +0 -138
|
@@ -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) {
|
|
@@ -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,
|
|
@@ -422,11 +452,16 @@ from
|
|
|
422
452
|
left join pg_attrdef ad on a.attrelid = ad.adrelid and a.attnum = ad.adnum
|
|
423
453
|
left join pg_type ty on ty.oid = a.atttypid
|
|
424
454
|
group by
|
|
425
|
-
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
|
|
426
456
|
order by
|
|
427
457
|
t.schema, t.name
|
|
428
458
|
`);
|
|
429
|
-
|
|
430
|
-
|
|
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
|
+
});
|
|
431
466
|
return validatedRows.map((row) => new Table(row));
|
|
432
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 {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Change } from "./change.types.ts";
|
|
2
|
+
import type { Table } from "./objects/table/table.model.ts";
|
|
2
3
|
/**
|
|
3
4
|
* Apply structural rewrites to the change list that are only obvious once
|
|
4
5
|
* every object diff has been collected. This pass does NOT prevent dependency
|
|
@@ -19,11 +20,17 @@ import type { Change } from "./change.types.ts";
|
|
|
19
20
|
* produced when `diffTables()` and `expandReplaceDependencies()` both
|
|
20
21
|
* emit the same constraint operation for a replaced table. Last write
|
|
21
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.
|
|
22
28
|
*
|
|
23
29
|
* Object-local PostgreSQL semantics (for example owned-sequence cascades)
|
|
24
30
|
* stay in the corresponding `diff*` function instead of this pass.
|
|
25
31
|
*/
|
|
26
|
-
export declare function
|
|
32
|
+
export declare function normalizePostDiffChanges({ changes, replacedTableIds, branchTables, }: {
|
|
27
33
|
changes: Change[];
|
|
28
34
|
replacedTableIds?: ReadonlySet<string>;
|
|
35
|
+
branchTables?: Record<string, Table>;
|
|
29
36
|
}): Change[];
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { CreateIndex } from "./objects/index/changes/index.create.js";
|
|
2
|
+
import { DropIndex } from "./objects/index/changes/index.drop.js";
|
|
3
|
+
import { AlterTableAddConstraint, AlterTableDropColumn, AlterTableDropConstraint, AlterTableSetReplicaIdentity, AlterTableValidateConstraint, } from "./objects/table/changes/table.alter.js";
|
|
4
|
+
import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.js";
|
|
5
|
+
import { stableId } from "./objects/utils.js";
|
|
6
|
+
function constraintStableId(table, constraintName) {
|
|
7
|
+
return stableId.constraint(table.schema, table.name, constraintName);
|
|
8
|
+
}
|
|
9
|
+
function isSupersededByTableReplacement(change, replacedTableIds) {
|
|
10
|
+
if (!(change instanceof AlterTableDropColumn) &&
|
|
11
|
+
!(change instanceof AlterTableDropConstraint)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return replacedTableIds.has(change.table.stableId);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Drop earlier duplicates of `AlterTableAddConstraint` /
|
|
18
|
+
* `AlterTableValidateConstraint` / `CreateCommentOnConstraint` targeting
|
|
19
|
+
* replaced tables, keeping only the last occurrence of each
|
|
20
|
+
* `(changeType, table.stableId, constraint.name)`.
|
|
21
|
+
*
|
|
22
|
+
* When `expandReplaceDependencies()` promotes a table to a full
|
|
23
|
+
* `DropTable + CreateTable` pair, it also emits one
|
|
24
|
+
* `AlterTableAddConstraint` (plus optional `VALIDATE CONSTRAINT` /
|
|
25
|
+
* `COMMENT ON CONSTRAINT`) per branch constraint. If `diffTables()` already
|
|
26
|
+
* emitted the same change for a shape flip or a new constraint on that
|
|
27
|
+
* table, the plan ends up with two identical `ALTER TABLE ... ADD
|
|
28
|
+
* CONSTRAINT ...` statements and PostgreSQL fails at apply time with
|
|
29
|
+
* `constraint "..." for relation "..." already exists`. Because
|
|
30
|
+
* `expandReplaceDependencies()` appends its additions after the original
|
|
31
|
+
* `diffTables()` output, the last occurrence is the expansion's emission —
|
|
32
|
+
* keeping it preserves correctness while removing the duplicate.
|
|
33
|
+
*/
|
|
34
|
+
function dropReplacedTableDuplicateConstraintChanges(changes, replacedTableIds) {
|
|
35
|
+
if (replacedTableIds.size === 0)
|
|
36
|
+
return changes;
|
|
37
|
+
const keyFor = (change) => {
|
|
38
|
+
if (!(change instanceof AlterTableAddConstraint) &&
|
|
39
|
+
!(change instanceof AlterTableValidateConstraint) &&
|
|
40
|
+
!(change instanceof CreateCommentOnConstraint)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (!replacedTableIds.has(change.table.stableId))
|
|
44
|
+
return null;
|
|
45
|
+
const tag = change instanceof AlterTableAddConstraint
|
|
46
|
+
? "add"
|
|
47
|
+
: change instanceof AlterTableValidateConstraint
|
|
48
|
+
? "validate"
|
|
49
|
+
: "comment";
|
|
50
|
+
return `${tag}:${constraintStableId(change.table, change.constraint.name)}`;
|
|
51
|
+
};
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const reversedKept = [];
|
|
54
|
+
let mutated = false;
|
|
55
|
+
// Walk backwards: the first encounter of each key corresponds to its LAST
|
|
56
|
+
// occurrence in the original order. `expandReplaceDependencies()` appends
|
|
57
|
+
// additions after the original changes, so "last wins" keeps the
|
|
58
|
+
// expansion's emission and drops the earlier diffTables duplicate.
|
|
59
|
+
for (let i = changes.length - 1; i >= 0; i--) {
|
|
60
|
+
const change = changes[i];
|
|
61
|
+
const key = keyFor(change);
|
|
62
|
+
if (key !== null) {
|
|
63
|
+
if (seen.has(key)) {
|
|
64
|
+
mutated = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
seen.add(key);
|
|
68
|
+
}
|
|
69
|
+
reversedKept.push(change);
|
|
70
|
+
}
|
|
71
|
+
return mutated ? reversedKept.reverse() : changes;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Re-emit `ALTER TABLE ... REPLICA IDENTITY USING INDEX <idx>` after any
|
|
75
|
+
* `DropIndex(idx) + CreateIndex(idx)` pair where `idx` is the replica-identity
|
|
76
|
+
* index of a branch table.
|
|
77
|
+
*
|
|
78
|
+
* Background: PostgreSQL silently flips a table's `relreplident` to `'d'`
|
|
79
|
+
* (DEFAULT) when the index it points to is dropped. `CREATE INDEX` cannot
|
|
80
|
+
* restore the marker — only `ALTER TABLE ... REPLICA IDENTITY USING INDEX`
|
|
81
|
+
* can. When both main and branch carry `replica_identity = 'i'` pointing at
|
|
82
|
+
* the same index name, `diffTables()` emits no replica-identity change of its
|
|
83
|
+
* own, so the marker would be lost on apply.
|
|
84
|
+
*
|
|
85
|
+
* This is a whole-plan interaction: `diffTables()` cannot detect it without
|
|
86
|
+
* also looking at index changes. Per the "whole-plan interactions belong in
|
|
87
|
+
* post-diff normalization" rule in the package CLAUDE.md, the restoration
|
|
88
|
+
* lives here.
|
|
89
|
+
*
|
|
90
|
+
* Insertion is idempotent: if `diffTables()` already emitted the same
|
|
91
|
+
* `AlterTableSetReplicaIdentity` for this table (e.g. when the user is also
|
|
92
|
+
* switching the replica-identity index name in the same migration), no
|
|
93
|
+
* duplicate is added.
|
|
94
|
+
*/
|
|
95
|
+
function restoreReplicaIdentityAfterIndexReplace(changes, branchTables) {
|
|
96
|
+
// Build the index-stable-id → owning-table map from branch state. Only
|
|
97
|
+
// tables in 'i' mode contribute, and only those whose configured index name
|
|
98
|
+
// is non-null (the extractor returns null for any other mode).
|
|
99
|
+
const replicaIdentityIndexToTable = new Map();
|
|
100
|
+
for (const table of Object.values(branchTables)) {
|
|
101
|
+
if (table.replica_identity !== "i" || !table.replica_identity_index) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const indexId = stableId.index(table.schema, table.name, table.replica_identity_index);
|
|
105
|
+
replicaIdentityIndexToTable.set(indexId, table);
|
|
106
|
+
}
|
|
107
|
+
if (replicaIdentityIndexToTable.size === 0)
|
|
108
|
+
return changes;
|
|
109
|
+
// Find the indexes that are both dropped AND created in this plan. A pure
|
|
110
|
+
// drop or a pure create is handled by `diffTables()` directly (the table's
|
|
111
|
+
// replica_identity / replica_identity_index fields will have changed). The
|
|
112
|
+
// hole is specifically the drop+create pair that recreates the same name.
|
|
113
|
+
const droppedIndexIds = new Set();
|
|
114
|
+
const createdIndexIds = new Set();
|
|
115
|
+
for (const change of changes) {
|
|
116
|
+
if (change instanceof DropIndex) {
|
|
117
|
+
droppedIndexIds.add(change.index.stableId);
|
|
118
|
+
}
|
|
119
|
+
else if (change instanceof CreateIndex) {
|
|
120
|
+
createdIndexIds.add(change.index.stableId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const replacedIndexIds = new Set();
|
|
124
|
+
for (const id of droppedIndexIds) {
|
|
125
|
+
if (createdIndexIds.has(id) && replicaIdentityIndexToTable.has(id)) {
|
|
126
|
+
replacedIndexIds.add(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (replacedIndexIds.size === 0)
|
|
130
|
+
return changes;
|
|
131
|
+
// Skip tables for which `diffTables()` already emitted a replica-identity
|
|
132
|
+
// setter — re-emitting would produce a redundant ALTER TABLE (harmless on
|
|
133
|
+
// apply, but noisy in plan output).
|
|
134
|
+
const tablesWithExistingReplicaIdentitySetter = new Set();
|
|
135
|
+
for (const change of changes) {
|
|
136
|
+
if (change instanceof AlterTableSetReplicaIdentity) {
|
|
137
|
+
tablesWithExistingReplicaIdentitySetter.add(change.table.stableId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Insert one `AlterTableSetReplicaIdentity` per replaced index, immediately
|
|
141
|
+
// after the matching `CreateIndex`. The change's `requires` already names
|
|
142
|
+
// both the table and the recreated index, so the topo sort orders it
|
|
143
|
+
// correctly relative to the surrounding DDL.
|
|
144
|
+
const result = [];
|
|
145
|
+
for (const change of changes) {
|
|
146
|
+
result.push(change);
|
|
147
|
+
if (!(change instanceof CreateIndex) ||
|
|
148
|
+
!replacedIndexIds.has(change.index.stableId)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const table = replicaIdentityIndexToTable.get(change.index.stableId);
|
|
152
|
+
if (!table)
|
|
153
|
+
continue;
|
|
154
|
+
if (tablesWithExistingReplicaIdentitySetter.has(table.stableId))
|
|
155
|
+
continue;
|
|
156
|
+
result.push(new AlterTableSetReplicaIdentity({
|
|
157
|
+
table,
|
|
158
|
+
mode: "i",
|
|
159
|
+
indexName: table.replica_identity_index,
|
|
160
|
+
}));
|
|
161
|
+
// Mark as emitted so a second replaced index on the same table — if that
|
|
162
|
+
// ever arises — doesn't double-emit.
|
|
163
|
+
tablesWithExistingReplicaIdentitySetter.add(table.stableId);
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Apply structural rewrites to the change list that are only obvious once
|
|
169
|
+
* every object diff has been collected. This pass does NOT prevent dependency
|
|
170
|
+
* cycles — that responsibility now lives in the sort phase, where
|
|
171
|
+
* `sortPhaseChanges` invokes `tryBreakCycleByChangeInjection` lazily on cycles
|
|
172
|
+
* that edge filtering can't break (FK SCC of dropped tables,
|
|
173
|
+
* AlterPublicationDropTables ↔ AlterTableDropColumn, …).
|
|
174
|
+
*
|
|
175
|
+
* Concretely, this pass:
|
|
176
|
+
*
|
|
177
|
+
* - Prunes `AlterTableDropColumn(T.*)` / `AlterTableDropConstraint(T.*)`
|
|
178
|
+
* changes that are made redundant by an expansion-emitted
|
|
179
|
+
* `DropTable(T) + CreateTable(T)` pair. Without this, the apply phase
|
|
180
|
+
* would try to drop a column that no longer exists in the freshly
|
|
181
|
+
* recreated table.
|
|
182
|
+
* - Dedupes duplicate `AlterTableAddConstraint` /
|
|
183
|
+
* `AlterTableValidateConstraint` / `CreateCommentOnConstraint` changes
|
|
184
|
+
* produced when `diffTables()` and `expandReplaceDependencies()` both
|
|
185
|
+
* emit the same constraint operation for a replaced table. Last write
|
|
186
|
+
* wins so the expansion's emission survives.
|
|
187
|
+
* - Re-emits `ALTER TABLE ... REPLICA IDENTITY USING INDEX <idx>` after any
|
|
188
|
+
* `DropIndex(idx) + CreateIndex(idx)` pair where `idx` is the replica
|
|
189
|
+
* identity index of a branch table — Postgres silently clears the marker
|
|
190
|
+
* when the underlying index is dropped, and `CREATE INDEX` cannot restore
|
|
191
|
+
* it.
|
|
192
|
+
*
|
|
193
|
+
* Object-local PostgreSQL semantics (for example owned-sequence cascades)
|
|
194
|
+
* stay in the corresponding `diff*` function instead of this pass.
|
|
195
|
+
*/
|
|
196
|
+
export function normalizePostDiffChanges({ changes, replacedTableIds = new Set(), branchTables = {}, }) {
|
|
197
|
+
const restoredChanges = restoreReplicaIdentityAfterIndexReplace(changes, branchTables);
|
|
198
|
+
const dedupedChanges = dropReplacedTableDuplicateConstraintChanges(restoredChanges, replacedTableIds);
|
|
199
|
+
if (replacedTableIds.size === 0)
|
|
200
|
+
return dedupedChanges;
|
|
201
|
+
return dedupedChanges.filter((change) => !isSupersededByTableReplacement(change, replacedTableIds));
|
|
202
|
+
}
|
|
@@ -245,7 +245,7 @@ function tryBreakPublicationColumnCycle(cycleNodeIndexes, phaseChanges) {
|
|
|
245
245
|
return null;
|
|
246
246
|
// Verify the table is NOT itself being dropped. If `DropTable(T)` is in
|
|
247
247
|
// the same phase, the existing structural rewrites in
|
|
248
|
-
// `post-diff-
|
|
248
|
+
// `post-diff-normalization.ts` (replace-expansion superseded filter)
|
|
249
249
|
// already prune the redundant `AlterTableDropColumn`, so we should not
|
|
250
250
|
// see this combination here. Be defensive and bail anyway — flipping
|
|
251
251
|
// `omitTableRequirement` when T is being dropped would let the column
|
|
@@ -19,5 +19,15 @@ export declare function isMetadataStableId(stableId: string): boolean;
|
|
|
19
19
|
* - ALTER operations with scope="privilege" → create_alter_object phase (metadata changes)
|
|
20
20
|
* - ALTER operations that drop actual objects → drop phase (destructive ALTER)
|
|
21
21
|
* - ALTER operations that don't drop objects → create_alter_object phase (non-destructive ALTER)
|
|
22
|
+
*
|
|
23
|
+
* Dependency-breaking ALTERs that remove a `pg_depend` edge to another
|
|
24
|
+
* object that may be dropped in the same plan (for example
|
|
25
|
+
* `ALTER COLUMN ... DROP DEFAULT` releasing a sequence reference, or
|
|
26
|
+
* `ALTER COLUMN ... TYPE <built-in>` releasing a user-defined type
|
|
27
|
+
* reference) are routed to the drop phase. The drop phase sorts in reverse
|
|
28
|
+
* dependency order using the main catalog, so the catalog edges already
|
|
29
|
+
* in `pg_depend` order the ALTER before any dependent `DROP TYPE` /
|
|
30
|
+
* `DROP SEQUENCE` / `DROP FUNCTION` and PostgreSQL no longer rejects the
|
|
31
|
+
* drop with error 2BP01.
|
|
22
32
|
*/
|
|
23
33
|
export declare function getExecutionPhase(change: Change): Phase;
|