@supabase/pg-delta 1.0.0-alpha.14 → 1.0.0-alpha.16
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/README.md +7 -1
- package/dist/core/catalog.diff.js +7 -1
- package/dist/core/expand-replace-dependencies.d.ts +8 -2
- package/dist/core/expand-replace-dependencies.js +24 -10
- package/dist/core/objects/sequence/sequence.diff.js +14 -6
- package/dist/core/objects/table/changes/table.drop.d.ts +12 -0
- package/dist/core/objects/table/changes/table.drop.js +20 -3
- package/dist/core/post-diff-cycle-breaking.d.ts +22 -0
- package/dist/core/post-diff-cycle-breaking.js +143 -0
- package/dist/core/postgres-config.d.ts +31 -1
- package/dist/core/postgres-config.js +57 -2
- package/package.json +1 -1
- package/src/core/catalog.diff.ts +7 -1
- package/src/core/expand-replace-dependencies.test.ts +247 -8
- package/src/core/expand-replace-dependencies.ts +33 -5
- package/src/core/objects/sequence/sequence.diff.test.ts +110 -8
- package/src/core/objects/sequence/sequence.diff.ts +16 -6
- package/src/core/objects/table/changes/table.drop.ts +27 -4
- package/src/core/post-diff-cycle-breaking.test.ts +317 -0
- package/src/core/post-diff-cycle-breaking.ts +236 -0
- package/src/core/postgres-config.test.ts +95 -0
- package/src/core/postgres-config.ts +57 -3
|
@@ -7,10 +7,21 @@ import { CreateSequence } from "./objects/sequence/changes/sequence.create.ts";
|
|
|
7
7
|
import { DropSequence } from "./objects/sequence/changes/sequence.drop.ts";
|
|
8
8
|
import { diffSequences } from "./objects/sequence/sequence.diff.ts";
|
|
9
9
|
import { Sequence } from "./objects/sequence/sequence.model.ts";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
AlterTableAlterColumnSetDefault,
|
|
12
|
+
AlterTableChangeOwner,
|
|
13
|
+
AlterTableDropColumn,
|
|
14
|
+
AlterTableDropConstraint,
|
|
15
|
+
AlterTableEnableRowLevelSecurity,
|
|
16
|
+
AlterTableSetReplicaIdentity,
|
|
17
|
+
} from "./objects/table/changes/table.alter.ts";
|
|
11
18
|
import { CreateTable } from "./objects/table/changes/table.create.ts";
|
|
12
19
|
import { DropTable } from "./objects/table/changes/table.drop.ts";
|
|
20
|
+
import { GrantTablePrivileges } from "./objects/table/changes/table.privilege.ts";
|
|
13
21
|
import { Table } from "./objects/table/table.model.ts";
|
|
22
|
+
import { CreateEnum } from "./objects/type/enum/changes/enum.create.ts";
|
|
23
|
+
import { DropEnum } from "./objects/type/enum/changes/enum.drop.ts";
|
|
24
|
+
import { Enum } from "./objects/type/enum/enum.model.ts";
|
|
14
25
|
|
|
15
26
|
function mockChange(overrides: {
|
|
16
27
|
creates?: string[];
|
|
@@ -43,8 +54,9 @@ describe("expandReplaceDependencies", () => {
|
|
|
43
54
|
mainCatalog: catalog,
|
|
44
55
|
branchCatalog: catalog,
|
|
45
56
|
});
|
|
46
|
-
expect(result).toHaveLength(1);
|
|
47
|
-
expect(result).toBe(changes);
|
|
57
|
+
expect(result.changes).toHaveLength(1);
|
|
58
|
+
expect(result.changes).toBe(changes);
|
|
59
|
+
expect(result.replacedTableIds.size).toBe(0);
|
|
48
60
|
});
|
|
49
61
|
|
|
50
62
|
test("returns changes unchanged when replace roots have no dependents in catalog", async () => {
|
|
@@ -60,8 +72,9 @@ describe("expandReplaceDependencies", () => {
|
|
|
60
72
|
mainCatalog: catalog,
|
|
61
73
|
branchCatalog: catalog,
|
|
62
74
|
});
|
|
63
|
-
expect(result).toHaveLength(1);
|
|
64
|
-
expect(result[0]).toBe(changes[0]);
|
|
75
|
+
expect(result.changes).toHaveLength(1);
|
|
76
|
+
expect(result.changes[0]).toBe(changes[0]);
|
|
77
|
+
expect(result.replacedTableIds.size).toBe(0);
|
|
65
78
|
});
|
|
66
79
|
|
|
67
80
|
test("returns same array reference when replaceRoots.size is 0", async () => {
|
|
@@ -74,7 +87,8 @@ describe("expandReplaceDependencies", () => {
|
|
|
74
87
|
mainCatalog: catalog,
|
|
75
88
|
branchCatalog: catalog,
|
|
76
89
|
});
|
|
77
|
-
expect(result).toBe(changes);
|
|
90
|
+
expect(result.changes).toBe(changes);
|
|
91
|
+
expect(result.replacedTableIds.size).toBe(0);
|
|
78
92
|
});
|
|
79
93
|
|
|
80
94
|
test("does not replace the owning table for an owned sequence recreation", async () => {
|
|
@@ -187,9 +201,234 @@ describe("expandReplaceDependencies", () => {
|
|
|
187
201
|
expect(changes[0]).toBeInstanceOf(DropSequence);
|
|
188
202
|
expect(changes[1]).toBeInstanceOf(CreateSequence);
|
|
189
203
|
expect(changes[3]).toBeInstanceOf(AlterTableAlterColumnSetDefault);
|
|
190
|
-
expect(expanded.some((change) => change instanceof DropTable)).toBe(
|
|
191
|
-
expect(expanded.some((change) => change instanceof CreateTable)).toBe(
|
|
204
|
+
expect(expanded.changes.some((change) => change instanceof DropTable)).toBe(
|
|
192
205
|
false,
|
|
193
206
|
);
|
|
207
|
+
expect(
|
|
208
|
+
expanded.changes.some((change) => change instanceof CreateTable),
|
|
209
|
+
).toBe(false);
|
|
210
|
+
expect(expanded.replacedTableIds.size).toBe(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("reports replaced tables for downstream post-diff normalization", async () => {
|
|
214
|
+
// Reproduction guard for the enum-replacement expansion case: the expander
|
|
215
|
+
// must report which dependent tables it promoted to DropTable+CreateTable,
|
|
216
|
+
// but the pruning of same-table AlterTableDropColumn/DropConstraint belongs
|
|
217
|
+
// to the later post-diff normalization pass, not this expansion step.
|
|
218
|
+
const baseline = await createEmptyCatalog(170000, "postgres");
|
|
219
|
+
const mainEnum = new Enum({
|
|
220
|
+
schema: "public",
|
|
221
|
+
name: "item_status",
|
|
222
|
+
owner: "postgres",
|
|
223
|
+
labels: [
|
|
224
|
+
{ sort_order: 1, label: "draft" },
|
|
225
|
+
{ sort_order: 2, label: "published" },
|
|
226
|
+
{ sort_order: 3, label: "archived" },
|
|
227
|
+
],
|
|
228
|
+
comment: null,
|
|
229
|
+
privileges: [],
|
|
230
|
+
});
|
|
231
|
+
const branchEnum = new Enum({
|
|
232
|
+
...mainEnum,
|
|
233
|
+
labels: [
|
|
234
|
+
{ sort_order: 1, label: "draft" },
|
|
235
|
+
{ sort_order: 2, label: "published" },
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
const columnTemplate = {
|
|
239
|
+
data_type: "integer" as const,
|
|
240
|
+
data_type_str: "integer",
|
|
241
|
+
is_custom_type: false as const,
|
|
242
|
+
custom_type_type: null,
|
|
243
|
+
custom_type_category: null,
|
|
244
|
+
custom_type_schema: null,
|
|
245
|
+
custom_type_name: null,
|
|
246
|
+
not_null: false,
|
|
247
|
+
is_identity: false,
|
|
248
|
+
is_identity_always: false,
|
|
249
|
+
is_generated: false,
|
|
250
|
+
collation: null,
|
|
251
|
+
default: null,
|
|
252
|
+
comment: null,
|
|
253
|
+
};
|
|
254
|
+
const mainChildren = new Table({
|
|
255
|
+
schema: "public",
|
|
256
|
+
name: "children",
|
|
257
|
+
persistence: "p",
|
|
258
|
+
row_security: false,
|
|
259
|
+
force_row_security: false,
|
|
260
|
+
has_indexes: false,
|
|
261
|
+
has_rules: false,
|
|
262
|
+
has_triggers: false,
|
|
263
|
+
has_subclasses: false,
|
|
264
|
+
is_populated: true,
|
|
265
|
+
replica_identity: "d",
|
|
266
|
+
is_partition: false,
|
|
267
|
+
options: null,
|
|
268
|
+
partition_bound: null,
|
|
269
|
+
partition_by: null,
|
|
270
|
+
owner: "postgres",
|
|
271
|
+
comment: null,
|
|
272
|
+
parent_schema: null,
|
|
273
|
+
parent_name: null,
|
|
274
|
+
columns: [
|
|
275
|
+
{ ...columnTemplate, name: "id", position: 1, not_null: true },
|
|
276
|
+
{ ...columnTemplate, name: "parent_ref", position: 2 },
|
|
277
|
+
{
|
|
278
|
+
...columnTemplate,
|
|
279
|
+
name: "status",
|
|
280
|
+
position: 3,
|
|
281
|
+
data_type: "item_status",
|
|
282
|
+
data_type_str: "public.item_status",
|
|
283
|
+
is_custom_type: true,
|
|
284
|
+
custom_type_type: "e",
|
|
285
|
+
custom_type_category: "E",
|
|
286
|
+
custom_type_schema: "public",
|
|
287
|
+
custom_type_name: "item_status",
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
privileges: [],
|
|
291
|
+
});
|
|
292
|
+
const branchChildren = new Table({
|
|
293
|
+
...mainChildren,
|
|
294
|
+
columns: [
|
|
295
|
+
{ ...columnTemplate, name: "id", position: 1, not_null: true },
|
|
296
|
+
{
|
|
297
|
+
...columnTemplate,
|
|
298
|
+
name: "status",
|
|
299
|
+
position: 2,
|
|
300
|
+
data_type: "item_status",
|
|
301
|
+
data_type_str: "public.item_status",
|
|
302
|
+
is_custom_type: true,
|
|
303
|
+
custom_type_type: "e",
|
|
304
|
+
custom_type_category: "E",
|
|
305
|
+
custom_type_schema: "public",
|
|
306
|
+
custom_type_name: "item_status",
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Pre-existing planner output: the enum replacement from diffEnums plus
|
|
312
|
+
// targeted ALTER TABLE statements from diffTables. The two cycle-forming
|
|
313
|
+
// ALTERs (drop-column, drop-constraint) must be elided. The privilege
|
|
314
|
+
// ALTER and the owner / RLS / replica-identity ALTERs must all survive.
|
|
315
|
+
const droppedColumn = mainChildren.columns.find(
|
|
316
|
+
(c) => c.name === "parent_ref",
|
|
317
|
+
);
|
|
318
|
+
if (!droppedColumn) throw new Error("test setup: parent_ref missing");
|
|
319
|
+
const preExistingDropColumn = new AlterTableDropColumn({
|
|
320
|
+
table: mainChildren,
|
|
321
|
+
column: droppedColumn,
|
|
322
|
+
});
|
|
323
|
+
const preExistingDropConstraint = new AlterTableDropConstraint({
|
|
324
|
+
table: mainChildren,
|
|
325
|
+
constraint: {
|
|
326
|
+
name: "children_parent_ref_fkey",
|
|
327
|
+
constraint_type: "f",
|
|
328
|
+
deferrable: false,
|
|
329
|
+
initially_deferred: false,
|
|
330
|
+
validated: true,
|
|
331
|
+
is_local: true,
|
|
332
|
+
no_inherit: false,
|
|
333
|
+
is_partition_clone: false,
|
|
334
|
+
parent_constraint_schema: null,
|
|
335
|
+
parent_constraint_name: null,
|
|
336
|
+
parent_table_schema: null,
|
|
337
|
+
parent_table_name: null,
|
|
338
|
+
key_columns: ["parent_ref"],
|
|
339
|
+
foreign_key_columns: ["id"],
|
|
340
|
+
foreign_key_table: "parents",
|
|
341
|
+
foreign_key_schema: "public",
|
|
342
|
+
foreign_key_table_is_partition: false,
|
|
343
|
+
foreign_key_parent_schema: null,
|
|
344
|
+
foreign_key_parent_table: null,
|
|
345
|
+
foreign_key_effective_schema: "public",
|
|
346
|
+
foreign_key_effective_table: "parents",
|
|
347
|
+
on_update: "a",
|
|
348
|
+
on_delete: "a",
|
|
349
|
+
match_type: "s",
|
|
350
|
+
check_expression: null,
|
|
351
|
+
owner: "postgres",
|
|
352
|
+
definition: "FOREIGN KEY (parent_ref) REFERENCES public.parents(id)",
|
|
353
|
+
comment: null,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
const preExistingChangeOwner = new AlterTableChangeOwner({
|
|
357
|
+
table: branchChildren,
|
|
358
|
+
owner: "new_owner",
|
|
359
|
+
});
|
|
360
|
+
const preExistingEnableRls = new AlterTableEnableRowLevelSecurity({
|
|
361
|
+
table: branchChildren,
|
|
362
|
+
});
|
|
363
|
+
const preExistingReplicaIdentity = new AlterTableSetReplicaIdentity({
|
|
364
|
+
table: branchChildren,
|
|
365
|
+
mode: "f",
|
|
366
|
+
});
|
|
367
|
+
const preExistingGrant = new GrantTablePrivileges({
|
|
368
|
+
table: branchChildren,
|
|
369
|
+
grantee: "reader",
|
|
370
|
+
privileges: [{ privilege: "SELECT", grantable: false }],
|
|
371
|
+
});
|
|
372
|
+
const changes: Change[] = [
|
|
373
|
+
new DropEnum({ enum: mainEnum }),
|
|
374
|
+
new CreateEnum({ enum: branchEnum }),
|
|
375
|
+
preExistingDropColumn,
|
|
376
|
+
preExistingDropConstraint,
|
|
377
|
+
preExistingChangeOwner,
|
|
378
|
+
preExistingEnableRls,
|
|
379
|
+
preExistingReplicaIdentity,
|
|
380
|
+
preExistingGrant,
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
const mainCatalog = new Catalog({
|
|
384
|
+
...baseline,
|
|
385
|
+
enums: { [mainEnum.stableId]: mainEnum },
|
|
386
|
+
tables: { [mainChildren.stableId]: mainChildren },
|
|
387
|
+
// pg_depend: column children.status depends on type item_status.
|
|
388
|
+
depends: [
|
|
389
|
+
{
|
|
390
|
+
dependent_stable_id: "column:public.children.status",
|
|
391
|
+
referenced_stable_id: mainEnum.stableId,
|
|
392
|
+
deptype: "n",
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
});
|
|
396
|
+
const branchCatalog = new Catalog({
|
|
397
|
+
...baseline,
|
|
398
|
+
enums: { [branchEnum.stableId]: branchEnum },
|
|
399
|
+
tables: { [branchChildren.stableId]: branchChildren },
|
|
400
|
+
depends: [],
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const expanded = expandReplaceDependencies({
|
|
404
|
+
changes,
|
|
405
|
+
mainCatalog,
|
|
406
|
+
branchCatalog,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// The replace-table pair was added.
|
|
410
|
+
expect(expanded.changes.some((c) => c instanceof DropTable)).toBe(true);
|
|
411
|
+
expect(expanded.changes.some((c) => c instanceof CreateTable)).toBe(true);
|
|
412
|
+
expect(expanded.replacedTableIds.has(mainChildren.stableId)).toBe(true);
|
|
413
|
+
// Expansion itself keeps the pre-existing ALTERs; the post-diff cycle pass
|
|
414
|
+
// decides which of them are superseded by the replacement.
|
|
415
|
+
expect(expanded.changes).toContain(preExistingDropColumn);
|
|
416
|
+
expect(expanded.changes).toContain(preExistingDropConstraint);
|
|
417
|
+
expect(
|
|
418
|
+
expanded.changes.some((c) => c instanceof AlterTableDropColumn),
|
|
419
|
+
).toBe(true);
|
|
420
|
+
expect(
|
|
421
|
+
expanded.changes.some((c) => c instanceof AlterTableDropConstraint),
|
|
422
|
+
).toBe(true);
|
|
423
|
+
// The enum replace roots are still present.
|
|
424
|
+
expect(expanded.changes.some((c) => c instanceof DropEnum)).toBe(true);
|
|
425
|
+
expect(expanded.changes.some((c) => c instanceof CreateEnum)).toBe(true);
|
|
426
|
+
// Non-cycle object-scope ALTERs are carried through untouched.
|
|
427
|
+
expect(expanded.changes).toContain(preExistingChangeOwner);
|
|
428
|
+
expect(expanded.changes).toContain(preExistingEnableRls);
|
|
429
|
+
expect(expanded.changes).toContain(preExistingReplicaIdentity);
|
|
430
|
+
// Privilege-scope ALTER on the recreated table survives.
|
|
431
|
+
expect(expanded.changes).toContain(preExistingGrant);
|
|
432
|
+
expect(expanded.replacedTableIds.has("table:public.parents")).toBe(false);
|
|
194
433
|
});
|
|
195
434
|
});
|
|
@@ -70,8 +70,14 @@ type ResolvedObject =
|
|
|
70
70
|
* replaced so that destructive drops succeed. Uses dependency edges from pg_depend
|
|
71
71
|
* (already captured in Catalog.depends) plus change metadata (creates/drops/requires).
|
|
72
72
|
*
|
|
73
|
-
* New changes are appended; ordering
|
|
73
|
+
* New changes are appended; ordering and any multi-statement cycle normalization
|
|
74
|
+
* are handled later by post-diff helpers and the sorter.
|
|
74
75
|
*/
|
|
76
|
+
interface ExpandReplaceDependenciesResult {
|
|
77
|
+
changes: Change[];
|
|
78
|
+
replacedTableIds: ReadonlySet<string>;
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
export function expandReplaceDependencies({
|
|
76
82
|
changes,
|
|
77
83
|
mainCatalog,
|
|
@@ -80,7 +86,7 @@ export function expandReplaceDependencies({
|
|
|
80
86
|
changes: Change[];
|
|
81
87
|
mainCatalog: Catalog;
|
|
82
88
|
branchCatalog: Catalog;
|
|
83
|
-
}):
|
|
89
|
+
}): ExpandReplaceDependenciesResult {
|
|
84
90
|
const createdIds = new Set<string>();
|
|
85
91
|
const droppedIds = new Set<string>();
|
|
86
92
|
|
|
@@ -97,7 +103,10 @@ export function expandReplaceDependencies({
|
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
if (replaceRoots.size === 0) {
|
|
100
|
-
return
|
|
106
|
+
return {
|
|
107
|
+
changes,
|
|
108
|
+
replacedTableIds: new Set<string>(),
|
|
109
|
+
};
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
// Build referenced -> dependents adjacency from main catalog dependencies.
|
|
@@ -115,6 +124,12 @@ export function expandReplaceDependencies({
|
|
|
115
124
|
const visitedTargets = new Set<string>();
|
|
116
125
|
const visitedRefs = new Set<string>(replaceRoots);
|
|
117
126
|
const queue: string[] = [...replaceRoots];
|
|
127
|
+
// Tables being replaced by an expansion-added DropTable+CreateTable pair.
|
|
128
|
+
// Any pre-existing targeted AlterTable*(T) object-scope change is superseded
|
|
129
|
+
// by the replacement and must be removed to avoid contradictions (e.g. an
|
|
130
|
+
// AlterTableDropColumn on a table that is about to be dropped) and the
|
|
131
|
+
// associated drop-phase cycle with the catalog constraint→column edge.
|
|
132
|
+
const tablesReplacedByExpansion = new Set<string>();
|
|
118
133
|
|
|
119
134
|
while (queue.length > 0) {
|
|
120
135
|
const refId = queue.shift() as string;
|
|
@@ -178,6 +193,13 @@ export function expandReplaceDependencies({
|
|
|
178
193
|
|
|
179
194
|
additions.push(...replacementChanges);
|
|
180
195
|
|
|
196
|
+
// If we added a DropTable(T) for an existing table, mark T so any
|
|
197
|
+
// pre-existing object-scope AlterTable*(T) changes get dropped below —
|
|
198
|
+
// the DropTable+CreateTable pair supersedes all structural alterations.
|
|
199
|
+
if (resolved.kind === "table" && addDrop) {
|
|
200
|
+
tablesReplacedByExpansion.add(targetId);
|
|
201
|
+
}
|
|
202
|
+
|
|
181
203
|
// Track new creates/drops so we don't duplicate work for downstream dependents.
|
|
182
204
|
for (const change of replacementChanges) {
|
|
183
205
|
for (const id of change.creates ?? []) createdIds.add(id);
|
|
@@ -187,10 +209,16 @@ export function expandReplaceDependencies({
|
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
if (additions.length === 0) {
|
|
190
|
-
return
|
|
212
|
+
return {
|
|
213
|
+
changes,
|
|
214
|
+
replacedTableIds: tablesReplacedByExpansion,
|
|
215
|
+
};
|
|
191
216
|
}
|
|
192
217
|
|
|
193
|
-
return
|
|
218
|
+
return {
|
|
219
|
+
changes: [...changes, ...additions],
|
|
220
|
+
replacedTableIds: tablesReplacedByExpansion,
|
|
221
|
+
};
|
|
194
222
|
}
|
|
195
223
|
|
|
196
224
|
function isOwnedSequenceColumnDependency(
|
|
@@ -198,28 +198,130 @@ describe.concurrent("sequence.diff", () => {
|
|
|
198
198
|
expect(changes).toHaveLength(0);
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
-
test("generate DROP SEQUENCE when owned by table that still exists", () => {
|
|
201
|
+
test("generate DROP SEQUENCE when owned by table/column that still exists", () => {
|
|
202
202
|
const ownedSequence = new Sequence({
|
|
203
203
|
...base,
|
|
204
204
|
owned_by_schema: "public",
|
|
205
205
|
owned_by_table: "users",
|
|
206
206
|
owned_by_column: "id",
|
|
207
207
|
});
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
const branchTable = new Table({
|
|
209
|
+
schema: "public",
|
|
210
|
+
name: "users",
|
|
211
|
+
persistence: "p",
|
|
212
|
+
row_security: false,
|
|
213
|
+
force_row_security: false,
|
|
214
|
+
has_indexes: false,
|
|
215
|
+
has_rules: false,
|
|
216
|
+
has_triggers: false,
|
|
217
|
+
has_subclasses: false,
|
|
218
|
+
is_populated: true,
|
|
219
|
+
replica_identity: "d",
|
|
220
|
+
is_partition: false,
|
|
221
|
+
options: null,
|
|
222
|
+
partition_bound: null,
|
|
223
|
+
partition_by: null,
|
|
224
|
+
owner: "test",
|
|
225
|
+
comment: null,
|
|
226
|
+
parent_schema: null,
|
|
227
|
+
parent_name: null,
|
|
228
|
+
columns: [
|
|
229
|
+
{
|
|
230
|
+
name: "id",
|
|
231
|
+
position: 1,
|
|
232
|
+
data_type: "bigint",
|
|
233
|
+
data_type_str: "bigint",
|
|
234
|
+
is_custom_type: false,
|
|
235
|
+
custom_type_type: null,
|
|
236
|
+
custom_type_category: null,
|
|
237
|
+
custom_type_schema: null,
|
|
238
|
+
custom_type_name: null,
|
|
239
|
+
not_null: true,
|
|
240
|
+
is_identity: false,
|
|
241
|
+
is_identity_always: false,
|
|
242
|
+
is_generated: false,
|
|
243
|
+
collation: null,
|
|
244
|
+
default: null,
|
|
245
|
+
comment: null,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
privileges: [],
|
|
249
|
+
});
|
|
250
|
+
// When the owning table AND the owning column still exist in branch catalog,
|
|
251
|
+
// DROP SEQUENCE should be generated (sequence is orphaned and needs explicit drop).
|
|
210
252
|
const changes = diffSequences(
|
|
211
253
|
testContext,
|
|
212
254
|
{ [ownedSequence.stableId]: ownedSequence },
|
|
213
|
-
{},
|
|
214
|
-
{
|
|
215
|
-
"table:public.users": {} as Table, // table still exists
|
|
216
|
-
},
|
|
255
|
+
{},
|
|
256
|
+
{ [branchTable.stableId]: branchTable },
|
|
217
257
|
);
|
|
218
|
-
// Should generate DROP SEQUENCE since table still exists
|
|
219
258
|
expect(changes).toHaveLength(1);
|
|
220
259
|
expect(changes[0]).toBeInstanceOf(DropSequence);
|
|
221
260
|
});
|
|
222
261
|
|
|
262
|
+
test("skip DROP SEQUENCE when owning column is dropped but table survives", () => {
|
|
263
|
+
// Reproduction guard for the DropSequence ↔ AlterTableDropColumn cycle:
|
|
264
|
+
// dropping a SERIAL column on a surviving table leaves PG to cascade-drop
|
|
265
|
+
// the owned sequence. Emitting DROP SEQUENCE here would both fail at apply
|
|
266
|
+
// time AND form an unbreakable drop-phase cycle with AlterTableDropColumn.
|
|
267
|
+
const ownedSequence = new Sequence({
|
|
268
|
+
...base,
|
|
269
|
+
owned_by_schema: "public",
|
|
270
|
+
owned_by_table: "widgets",
|
|
271
|
+
owned_by_column: "id",
|
|
272
|
+
});
|
|
273
|
+
const branchTable = new Table({
|
|
274
|
+
schema: "public",
|
|
275
|
+
name: "widgets",
|
|
276
|
+
persistence: "p",
|
|
277
|
+
row_security: false,
|
|
278
|
+
force_row_security: false,
|
|
279
|
+
has_indexes: false,
|
|
280
|
+
has_rules: false,
|
|
281
|
+
has_triggers: false,
|
|
282
|
+
has_subclasses: false,
|
|
283
|
+
is_populated: true,
|
|
284
|
+
replica_identity: "d",
|
|
285
|
+
is_partition: false,
|
|
286
|
+
options: null,
|
|
287
|
+
partition_bound: null,
|
|
288
|
+
partition_by: null,
|
|
289
|
+
owner: "test",
|
|
290
|
+
comment: null,
|
|
291
|
+
parent_schema: null,
|
|
292
|
+
parent_name: null,
|
|
293
|
+
// `id` column has been dropped in branch; only `label` remains.
|
|
294
|
+
columns: [
|
|
295
|
+
{
|
|
296
|
+
name: "label",
|
|
297
|
+
position: 1,
|
|
298
|
+
data_type: "text",
|
|
299
|
+
data_type_str: "text",
|
|
300
|
+
is_custom_type: false,
|
|
301
|
+
custom_type_type: null,
|
|
302
|
+
custom_type_category: null,
|
|
303
|
+
custom_type_schema: null,
|
|
304
|
+
custom_type_name: null,
|
|
305
|
+
not_null: false,
|
|
306
|
+
is_identity: false,
|
|
307
|
+
is_identity_always: false,
|
|
308
|
+
is_generated: false,
|
|
309
|
+
collation: null,
|
|
310
|
+
default: null,
|
|
311
|
+
comment: null,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
privileges: [],
|
|
315
|
+
});
|
|
316
|
+
const changes = diffSequences(
|
|
317
|
+
testContext,
|
|
318
|
+
{ [ownedSequence.stableId]: ownedSequence },
|
|
319
|
+
{},
|
|
320
|
+
{ [branchTable.stableId]: branchTable },
|
|
321
|
+
);
|
|
322
|
+
expect(changes).toHaveLength(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
223
325
|
test("create with comment emits CreateCommentOnSequence", () => {
|
|
224
326
|
const s = new Sequence({ ...base, comment: "my seq" });
|
|
225
327
|
const changes = diffSequences(testContext, {}, { [s.stableId]: s });
|
|
@@ -116,18 +116,28 @@ export function diffSequences(
|
|
|
116
116
|
|
|
117
117
|
for (const sequenceId of dropped) {
|
|
118
118
|
const sequence = main[sequenceId];
|
|
119
|
-
// Skip generating DROP SEQUENCE if the sequence is owned by a table that's being dropped.
|
|
120
|
-
// PostgreSQL automatically
|
|
121
|
-
//
|
|
119
|
+
// Skip generating DROP SEQUENCE if the sequence is owned by a table/column that's being dropped.
|
|
120
|
+
// PostgreSQL automatically cascades owned sequences when the owning table OR the owning
|
|
121
|
+
// column is dropped (via OWNED BY). Emitting DROP SEQUENCE in those cases would either
|
|
122
|
+
// fail at apply time (sequence already gone) or — in the column-drop case — create an
|
|
123
|
+
// unbreakable DropSequence ↔ AlterTableDropColumn cycle in the drop-phase sort graph.
|
|
122
124
|
if (
|
|
123
125
|
sequence.owned_by_schema &&
|
|
124
126
|
sequence.owned_by_table &&
|
|
125
127
|
sequence.owned_by_column
|
|
126
128
|
) {
|
|
127
129
|
const ownedByTableId = `table:${sequence.owned_by_schema}.${sequence.owned_by_table}`;
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
if (!
|
|
130
|
+
const ownedByTable = branchTables[ownedByTableId];
|
|
131
|
+
// Owning table is dropped → PG auto-drops the owned sequence.
|
|
132
|
+
if (!ownedByTable) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Owning column is dropped (table survives) → PG still auto-drops the owned
|
|
136
|
+
// sequence as part of the column drop, so we must not emit DROP SEQUENCE.
|
|
137
|
+
const ownedByColumnExists = ownedByTable.columns?.some(
|
|
138
|
+
(col) => col.name === sequence.owned_by_column,
|
|
139
|
+
);
|
|
140
|
+
if (!ownedByColumnExists) {
|
|
131
141
|
continue;
|
|
132
142
|
}
|
|
133
143
|
}
|
|
@@ -16,10 +16,31 @@ import { DropTableChange } from "./table.base.ts";
|
|
|
16
16
|
export class DropTable extends DropTableChange {
|
|
17
17
|
public readonly table: Table;
|
|
18
18
|
public readonly scope = "object" as const;
|
|
19
|
+
/**
|
|
20
|
+
* Names of constraints on this table that are dropped explicitly by a
|
|
21
|
+
* separate `AlterTableDropConstraint` change. Those constraints must not be
|
|
22
|
+
* claimed by `DropTable.drops` / `.requires`, otherwise catalog edges tied
|
|
23
|
+
* to the constraint stableId will attach to this DropTable node instead of
|
|
24
|
+
* the dedicated AlterTableDropConstraint node. When two tables with mutual
|
|
25
|
+
* FK references are dropped in the same phase, that misattribution
|
|
26
|
+
* produces an unbreakable cycle between the two DropTable changes.
|
|
27
|
+
*/
|
|
28
|
+
public readonly externallyDroppedConstraints: ReadonlySet<string>;
|
|
19
29
|
|
|
20
|
-
constructor(props: {
|
|
30
|
+
constructor(props: {
|
|
31
|
+
table: Table;
|
|
32
|
+
externallyDroppedConstraints?: ReadonlySet<string>;
|
|
33
|
+
}) {
|
|
21
34
|
super();
|
|
22
35
|
this.table = props.table;
|
|
36
|
+
this.externallyDroppedConstraints =
|
|
37
|
+
props.externallyDroppedConstraints ?? new Set();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private get claimedConstraints() {
|
|
41
|
+
return this.table.constraints.filter(
|
|
42
|
+
(constraint) => !this.externallyDroppedConstraints.has(constraint.name),
|
|
43
|
+
);
|
|
23
44
|
}
|
|
24
45
|
|
|
25
46
|
get drops() {
|
|
@@ -29,8 +50,10 @@ export class DropTable extends DropTableChange {
|
|
|
29
50
|
stableId.column(this.table.schema, this.table.name, column.name),
|
|
30
51
|
),
|
|
31
52
|
// Include constraint stableIds so FK relationships that only exist at the
|
|
32
|
-
// constraint level still affect whole-table drop ordering.
|
|
33
|
-
|
|
53
|
+
// constraint level still affect whole-table drop ordering. Skip any
|
|
54
|
+
// constraint that the diff layer is dropping via a dedicated
|
|
55
|
+
// AlterTableDropConstraint change — that node owns the stableId.
|
|
56
|
+
...this.claimedConstraints.map((constraint) =>
|
|
34
57
|
stableId.constraint(
|
|
35
58
|
this.table.schema,
|
|
36
59
|
this.table.name,
|
|
@@ -48,7 +71,7 @@ export class DropTable extends DropTableChange {
|
|
|
48
71
|
),
|
|
49
72
|
// Mirror the dropped constraint ids in requires so drop-phase graph
|
|
50
73
|
// consumers can connect catalog FK edges back to this table drop.
|
|
51
|
-
...this.
|
|
74
|
+
...this.claimedConstraints.map((constraint) =>
|
|
52
75
|
stableId.constraint(
|
|
53
76
|
this.table.schema,
|
|
54
77
|
this.table.name,
|