@supabase/pg-delta 1.0.0-alpha.20 → 1.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/core/catalog.diff.js +0 -1
  2. package/dist/core/objects/publication/changes/publication.alter.d.ts +1 -1
  3. package/dist/core/objects/sequence/sequence.diff.js +13 -5
  4. package/dist/core/objects/table/changes/table.alter.d.ts +4 -0
  5. package/dist/core/objects/table/changes/table.alter.js +19 -4
  6. package/dist/core/objects/table/table.diff.js +21 -2
  7. package/dist/core/objects/table/table.model.js +10 -7
  8. package/dist/core/post-diff-cycle-breaking.d.ts +21 -21
  9. package/dist/core/post-diff-cycle-breaking.js +24 -133
  10. package/dist/core/sort/cycle-breakers.d.ts +15 -0
  11. package/dist/core/sort/cycle-breakers.js +269 -0
  12. package/dist/core/sort/sort-changes.js +97 -43
  13. package/package.json +1 -1
  14. package/src/core/catalog.diff.ts +0 -1
  15. package/src/core/expand-replace-dependencies.test.ts +8 -5
  16. package/src/core/objects/publication/changes/publication.alter.ts +1 -1
  17. package/src/core/objects/sequence/sequence.diff.test.ts +6 -1
  18. package/src/core/objects/sequence/sequence.diff.ts +12 -4
  19. package/src/core/objects/table/changes/table.alter.test.ts +13 -2
  20. package/src/core/objects/table/changes/table.alter.ts +36 -7
  21. package/src/core/objects/table/table.diff.test.ts +43 -0
  22. package/src/core/objects/table/table.diff.ts +28 -4
  23. package/src/core/objects/table/table.model.ts +10 -7
  24. package/src/core/post-diff-cycle-breaking.test.ts +0 -156
  25. package/src/core/post-diff-cycle-breaking.ts +23 -202
  26. package/src/core/sort/cycle-breakers.test.ts +476 -0
  27. package/src/core/sort/cycle-breakers.ts +311 -0
  28. package/src/core/sort/sort-changes.ts +135 -50
@@ -0,0 +1,476 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { Change } from "../change.types.ts";
3
+ import {
4
+ AlterPublicationDropTables,
5
+ AlterPublicationSetOwner,
6
+ } from "../objects/publication/changes/publication.alter.ts";
7
+ import { Publication } from "../objects/publication/publication.model.ts";
8
+ import {
9
+ AlterTableDropColumn,
10
+ AlterTableDropConstraint,
11
+ } from "../objects/table/changes/table.alter.ts";
12
+ import { DropTable } from "../objects/table/changes/table.drop.ts";
13
+ import { Table } from "../objects/table/table.model.ts";
14
+ import { stableId } from "../objects/utils.ts";
15
+ import { tryBreakCycleByChangeInjection } from "./cycle-breakers.ts";
16
+
17
+ const baseTableProps = {
18
+ schema: "public",
19
+ persistence: "p" as const,
20
+ row_security: false,
21
+ force_row_security: false,
22
+ has_indexes: false,
23
+ has_rules: false,
24
+ has_triggers: false,
25
+ has_subclasses: false,
26
+ is_populated: true,
27
+ replica_identity: "d" as const,
28
+ is_partition: false,
29
+ options: null,
30
+ partition_bound: null,
31
+ partition_by: null,
32
+ owner: "postgres",
33
+ comment: null,
34
+ parent_schema: null,
35
+ parent_name: null,
36
+ privileges: [],
37
+ };
38
+
39
+ function integerColumn(name: string, position: number) {
40
+ return {
41
+ name,
42
+ position,
43
+ data_type: "integer" as const,
44
+ data_type_str: "integer",
45
+ is_custom_type: false as const,
46
+ custom_type_type: null,
47
+ custom_type_category: null,
48
+ custom_type_schema: null,
49
+ custom_type_name: null,
50
+ not_null: false,
51
+ is_identity: false,
52
+ is_identity_always: false,
53
+ is_generated: false,
54
+ collation: null,
55
+ default: null,
56
+ comment: null,
57
+ };
58
+ }
59
+
60
+ function fkConstraint(props: {
61
+ name: string;
62
+ fkColumn: string;
63
+ targetSchema: string;
64
+ targetTable: string;
65
+ }) {
66
+ return {
67
+ name: props.name,
68
+ constraint_type: "f" as const,
69
+ deferrable: false,
70
+ initially_deferred: false,
71
+ validated: true,
72
+ is_local: true,
73
+ no_inherit: false,
74
+ is_temporal: false,
75
+ is_partition_clone: false,
76
+ parent_constraint_schema: null,
77
+ parent_constraint_name: null,
78
+ parent_table_schema: null,
79
+ parent_table_name: null,
80
+ key_columns: [props.fkColumn],
81
+ foreign_key_columns: ["id"],
82
+ foreign_key_table: props.targetTable,
83
+ foreign_key_schema: props.targetSchema,
84
+ foreign_key_table_is_partition: false,
85
+ foreign_key_parent_schema: null,
86
+ foreign_key_parent_table: null,
87
+ foreign_key_effective_schema: props.targetSchema,
88
+ foreign_key_effective_table: props.targetTable,
89
+ on_update: "a" as const,
90
+ on_delete: "a" as const,
91
+ match_type: "s" as const,
92
+ check_expression: null,
93
+ owner: "postgres",
94
+ definition: `FOREIGN KEY (${props.fkColumn}) REFERENCES ${props.targetSchema}.${props.targetTable}(id)`,
95
+ comment: null,
96
+ };
97
+ }
98
+
99
+ describe("tryBreakCycleByChangeInjection", () => {
100
+ test("FK 2-cycle: injects one constraint drop per FK and updates externallyDroppedConstraints", () => {
101
+ // Schema:
102
+ // DROP TABLE a; DROP TABLE b;
103
+ // where a.b_id REFERENCES b, b.a_id REFERENCES a
104
+ // Cycle is over [DropTable(a), DropTable(b)] — both tables drop while
105
+ // their FKs still bind to each other.
106
+ const tableA = new Table({
107
+ ...baseTableProps,
108
+ name: "a",
109
+ columns: [
110
+ { ...integerColumn("id", 1), not_null: true },
111
+ integerColumn("b_id", 2),
112
+ ],
113
+ constraints: [
114
+ fkConstraint({
115
+ name: "a_b_fkey",
116
+ fkColumn: "b_id",
117
+ targetSchema: "public",
118
+ targetTable: "b",
119
+ }),
120
+ ],
121
+ });
122
+ const tableB = new Table({
123
+ ...baseTableProps,
124
+ name: "b",
125
+ columns: [
126
+ { ...integerColumn("id", 1), not_null: true },
127
+ integerColumn("a_id", 2),
128
+ ],
129
+ constraints: [
130
+ fkConstraint({
131
+ name: "b_a_fkey",
132
+ fkColumn: "a_id",
133
+ targetSchema: "public",
134
+ targetTable: "a",
135
+ }),
136
+ ],
137
+ });
138
+ const changes: Change[] = [
139
+ new DropTable({ table: tableA }),
140
+ new DropTable({ table: tableB }),
141
+ ];
142
+
143
+ const broken = tryBreakCycleByChangeInjection([0, 1], changes);
144
+ if (broken === null) throw new Error("expected breaker to fire");
145
+
146
+ const injectedDrops = broken.filter(
147
+ (change): change is AlterTableDropConstraint =>
148
+ change instanceof AlterTableDropConstraint,
149
+ );
150
+ expect(injectedDrops).toHaveLength(2);
151
+ expect(injectedDrops.map((d) => d.constraint.name).sort()).toEqual([
152
+ "a_b_fkey",
153
+ "b_a_fkey",
154
+ ]);
155
+
156
+ const dropA = broken.find(
157
+ (change): change is DropTable =>
158
+ change instanceof DropTable &&
159
+ change.table.stableId === tableA.stableId,
160
+ );
161
+ const dropB = broken.find(
162
+ (change): change is DropTable =>
163
+ change instanceof DropTable &&
164
+ change.table.stableId === tableB.stableId,
165
+ );
166
+ if (!dropA || !dropB) throw new Error("expected both DropTables in result");
167
+
168
+ expect(dropA.externallyDroppedConstraints.has("a_b_fkey")).toBe(true);
169
+ expect(dropB.externallyDroppedConstraints.has("b_a_fkey")).toBe(true);
170
+ expect(
171
+ dropA.requires.includes(stableId.constraint("public", "a", "a_b_fkey")),
172
+ ).toBe(false);
173
+ expect(
174
+ dropB.requires.includes(stableId.constraint("public", "b", "b_a_fkey")),
175
+ ).toBe(false);
176
+ });
177
+
178
+ test("FK 3-cycle: injects three constraint drops and frees all three tables", () => {
179
+ // Schema:
180
+ // DROP TABLE a; DROP TABLE b; DROP TABLE c;
181
+ // where a.b_id REFERENCES b, b.c_id REFERENCES c, c.a_id REFERENCES a
182
+ // No mutual edges — would have stalled the old eager mutual-only
183
+ // breaker. The lazy dispatcher uses the cycle node-set directly.
184
+ const tableA = new Table({
185
+ ...baseTableProps,
186
+ name: "a",
187
+ columns: [
188
+ { ...integerColumn("id", 1), not_null: true },
189
+ integerColumn("b_id", 2),
190
+ ],
191
+ constraints: [
192
+ fkConstraint({
193
+ name: "a_b_fkey",
194
+ fkColumn: "b_id",
195
+ targetSchema: "public",
196
+ targetTable: "b",
197
+ }),
198
+ ],
199
+ });
200
+ const tableB = new Table({
201
+ ...baseTableProps,
202
+ name: "b",
203
+ columns: [
204
+ { ...integerColumn("id", 1), not_null: true },
205
+ integerColumn("c_id", 2),
206
+ ],
207
+ constraints: [
208
+ fkConstraint({
209
+ name: "b_c_fkey",
210
+ fkColumn: "c_id",
211
+ targetSchema: "public",
212
+ targetTable: "c",
213
+ }),
214
+ ],
215
+ });
216
+ const tableC = new Table({
217
+ ...baseTableProps,
218
+ name: "c",
219
+ columns: [
220
+ { ...integerColumn("id", 1), not_null: true },
221
+ integerColumn("a_id", 2),
222
+ ],
223
+ constraints: [
224
+ fkConstraint({
225
+ name: "c_a_fkey",
226
+ fkColumn: "a_id",
227
+ targetSchema: "public",
228
+ targetTable: "a",
229
+ }),
230
+ ],
231
+ });
232
+ const changes: Change[] = [
233
+ new DropTable({ table: tableA }),
234
+ new DropTable({ table: tableB }),
235
+ new DropTable({ table: tableC }),
236
+ ];
237
+
238
+ const broken = tryBreakCycleByChangeInjection([0, 1, 2], changes);
239
+ if (broken === null) throw new Error("expected breaker to fire");
240
+
241
+ const injectedDrops = broken.filter(
242
+ (change): change is AlterTableDropConstraint =>
243
+ change instanceof AlterTableDropConstraint,
244
+ );
245
+ expect(injectedDrops).toHaveLength(3);
246
+ expect(injectedDrops.map((d) => d.constraint.name).sort()).toEqual([
247
+ "a_b_fkey",
248
+ "b_c_fkey",
249
+ "c_a_fkey",
250
+ ]);
251
+
252
+ for (const t of [tableA, tableB, tableC]) {
253
+ const dropChange = broken.find(
254
+ (change): change is DropTable =>
255
+ change instanceof DropTable && change.table.stableId === t.stableId,
256
+ );
257
+ if (!dropChange) throw new Error(`missing DropTable for ${t.name}`);
258
+ expect(dropChange.externallyDroppedConstraints.size).toBe(1);
259
+ }
260
+ });
261
+
262
+ test("FK breaker skips when an explicit AlterTableDropConstraint already exists", () => {
263
+ // Diff layer emitted the constraint drop explicitly — breaker shouldn't
264
+ // duplicate it. Returns null (no change to make).
265
+ const tableA = new Table({
266
+ ...baseTableProps,
267
+ name: "a",
268
+ columns: [
269
+ { ...integerColumn("id", 1), not_null: true },
270
+ integerColumn("b_id", 2),
271
+ ],
272
+ constraints: [
273
+ fkConstraint({
274
+ name: "a_b_fkey",
275
+ fkColumn: "b_id",
276
+ targetSchema: "public",
277
+ targetTable: "b",
278
+ }),
279
+ ],
280
+ });
281
+ const tableB = new Table({
282
+ ...baseTableProps,
283
+ name: "b",
284
+ columns: [
285
+ { ...integerColumn("id", 1), not_null: true },
286
+ integerColumn("a_id", 2),
287
+ ],
288
+ constraints: [
289
+ fkConstraint({
290
+ name: "b_a_fkey",
291
+ fkColumn: "a_id",
292
+ targetSchema: "public",
293
+ targetTable: "a",
294
+ }),
295
+ ],
296
+ });
297
+ const changes: Change[] = [
298
+ new AlterTableDropConstraint({
299
+ table: tableA,
300
+ constraint: tableA.constraints[0],
301
+ }),
302
+ new AlterTableDropConstraint({
303
+ table: tableB,
304
+ constraint: tableB.constraints[0],
305
+ }),
306
+ new DropTable({ table: tableA }),
307
+ new DropTable({ table: tableB }),
308
+ ];
309
+
310
+ // Cycle reported by sort phase only includes the DropTables (the
311
+ // existing constraint drops are at indices 0 and 1, but the cycle is
312
+ // between the DropTables at 2 and 3).
313
+ const broken = tryBreakCycleByChangeInjection([2, 3], changes);
314
+ expect(broken).toBeNull();
315
+ });
316
+
317
+ test("publication-column on surviving table: rebuilds AlterTableDropColumn with omitTableRequirement", () => {
318
+ // Schema:
319
+ // CREATE PUBLICATION p FOR TABLE labs (id, summary);
320
+ // ALTER TABLE labs DROP COLUMN summary;
321
+ // Diff:
322
+ // AlterPublicationDropTables(p, [labs])
323
+ // AlterTableDropColumn(labs.summary)
324
+ // Cycle: pub→col (catalog) and col→table (explicit requires). `labs`
325
+ // survives; breaker should rewrite the column drop to drop the
326
+ // table-requirement edge.
327
+ const tableLabs = new Table({
328
+ ...baseTableProps,
329
+ name: "labs",
330
+ columns: [
331
+ { ...integerColumn("id", 1), not_null: true },
332
+ integerColumn("summary", 2),
333
+ ],
334
+ });
335
+ const summaryColumn = tableLabs.columns.find(
336
+ (column) => column.name === "summary",
337
+ );
338
+ if (!summaryColumn) throw new Error("test setup: summary column missing");
339
+
340
+ const publication = new Publication({
341
+ name: "p",
342
+ owner: "postgres",
343
+ comment: null,
344
+ all_tables: false,
345
+ publish_insert: true,
346
+ publish_update: true,
347
+ publish_delete: true,
348
+ publish_truncate: true,
349
+ publish_via_partition_root: false,
350
+ tables: [
351
+ { schema: "public", name: "labs", columns: ["id"], row_filter: null },
352
+ ],
353
+ schemas: [],
354
+ });
355
+
356
+ const changes: Change[] = [
357
+ new AlterPublicationDropTables({
358
+ publication,
359
+ tables: [
360
+ {
361
+ schema: "public",
362
+ name: "labs",
363
+ columns: ["id", "summary"],
364
+ row_filter: null,
365
+ },
366
+ ],
367
+ }),
368
+ new AlterTableDropColumn({
369
+ table: tableLabs,
370
+ column: summaryColumn,
371
+ }),
372
+ ];
373
+
374
+ const broken = tryBreakCycleByChangeInjection([0, 1], changes);
375
+ if (broken === null) throw new Error("expected breaker to fire");
376
+
377
+ const rewrittenDropColumn = broken.find(
378
+ (change): change is AlterTableDropColumn =>
379
+ change instanceof AlterTableDropColumn,
380
+ );
381
+ if (!rewrittenDropColumn) throw new Error("missing AlterTableDropColumn");
382
+
383
+ expect(rewrittenDropColumn.omitTableRequirement).toBe(true);
384
+ expect(rewrittenDropColumn.requires.includes(tableLabs.stableId)).toBe(
385
+ false,
386
+ );
387
+ expect(
388
+ rewrittenDropColumn.requires.includes(
389
+ stableId.column("public", "labs", "summary"),
390
+ ),
391
+ ).toBe(true);
392
+
393
+ // The publication change passes through untouched.
394
+ expect(broken[0]).toBe(changes[0]);
395
+ });
396
+
397
+ test("publication-column when table is also being dropped: returns null (don't interfere)", () => {
398
+ // If `labs` itself is being dropped, the existing structural
399
+ // rewrites in post-diff handle the redundant column drop. Flipping
400
+ // omitTableRequirement here would let the column drop reorder
401
+ // against the table drop and is unsafe.
402
+ const tableLabs = new Table({
403
+ ...baseTableProps,
404
+ name: "labs",
405
+ columns: [
406
+ { ...integerColumn("id", 1), not_null: true },
407
+ integerColumn("summary", 2),
408
+ ],
409
+ });
410
+ const summaryColumn = tableLabs.columns.find(
411
+ (column) => column.name === "summary",
412
+ );
413
+ if (!summaryColumn) throw new Error("test setup: summary column missing");
414
+ const publication = new Publication({
415
+ name: "p",
416
+ owner: "postgres",
417
+ comment: null,
418
+ all_tables: false,
419
+ publish_insert: true,
420
+ publish_update: true,
421
+ publish_delete: true,
422
+ publish_truncate: true,
423
+ publish_via_partition_root: false,
424
+ tables: [],
425
+ schemas: [],
426
+ });
427
+
428
+ const changes: Change[] = [
429
+ new AlterPublicationDropTables({
430
+ publication,
431
+ tables: [
432
+ {
433
+ schema: "public",
434
+ name: "labs",
435
+ columns: ["id"],
436
+ row_filter: null,
437
+ },
438
+ ],
439
+ }),
440
+ new AlterTableDropColumn({
441
+ table: tableLabs,
442
+ column: summaryColumn,
443
+ }),
444
+ new DropTable({ table: tableLabs }),
445
+ ];
446
+
447
+ const broken = tryBreakCycleByChangeInjection([0, 1, 2], changes);
448
+ expect(broken).toBeNull();
449
+ });
450
+
451
+ test("returns null for a cycle with no recognised pattern (e.g. publication-only)", () => {
452
+ // Cycle of `AlterPublicationSetOwner` changes — neither FK nor
453
+ // publication-column shape. Breaker must bail so the formatted
454
+ // CycleError surfaces instead of an unsafe rewrite.
455
+ const publication = new Publication({
456
+ name: "p",
457
+ owner: "postgres",
458
+ comment: null,
459
+ all_tables: false,
460
+ publish_insert: true,
461
+ publish_update: true,
462
+ publish_delete: true,
463
+ publish_truncate: true,
464
+ publish_via_partition_root: false,
465
+ tables: [],
466
+ schemas: [],
467
+ });
468
+ const changes: Change[] = [
469
+ new AlterPublicationSetOwner({ publication, owner: "alice" }),
470
+ new AlterPublicationSetOwner({ publication, owner: "bob" }),
471
+ ];
472
+
473
+ const broken = tryBreakCycleByChangeInjection([0, 1], changes);
474
+ expect(broken).toBeNull();
475
+ });
476
+ });