@supabase/pg-delta 1.0.0-alpha.17 → 1.0.0-alpha.19

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 (33) hide show
  1. package/dist/core/expand-replace-dependencies.js +69 -0
  2. package/dist/core/objects/index/index.model.js +12 -2
  3. package/dist/core/objects/procedure/procedure.diff.js +33 -20
  4. package/dist/core/objects/rls-policy/changes/rls-policy.create.js +23 -0
  5. package/dist/core/objects/rls-policy/rls-policy.model.d.ts +49 -0
  6. package/dist/core/objects/rls-policy/rls-policy.model.js +122 -1
  7. package/dist/core/objects/table/table.diff.js +1 -0
  8. package/dist/core/objects/table/table.model.d.ts +4 -0
  9. package/dist/core/objects/table/table.model.js +2 -0
  10. package/dist/core/plan/sql-format/fixtures.js +8 -0
  11. package/dist/core/post-diff-cycle-breaking.d.ts +7 -0
  12. package/dist/core/post-diff-cycle-breaking.js +69 -3
  13. package/package.json +1 -1
  14. package/src/core/catalog.snapshot.test.ts +2 -0
  15. package/src/core/expand-replace-dependencies.test.ts +118 -0
  16. package/src/core/expand-replace-dependencies.ts +78 -0
  17. package/src/core/objects/index/index.model.test.ts +83 -0
  18. package/src/core/objects/index/index.model.ts +13 -4
  19. package/src/core/objects/procedure/procedure.diff.test.ts +100 -2
  20. package/src/core/objects/procedure/procedure.diff.ts +39 -21
  21. package/src/core/objects/rls-policy/changes/rls-policy.alter.test.ts +16 -0
  22. package/src/core/objects/rls-policy/changes/rls-policy.create.test.ts +128 -0
  23. package/src/core/objects/rls-policy/changes/rls-policy.create.ts +27 -0
  24. package/src/core/objects/rls-policy/changes/rls-policy.drop.test.ts +2 -0
  25. package/src/core/objects/rls-policy/rls-policy.diff.test.ts +2 -0
  26. package/src/core/objects/rls-policy/rls-policy.model.ts +134 -1
  27. package/src/core/objects/table/changes/table.alter.test.ts +1 -0
  28. package/src/core/objects/table/table.diff.test.ts +102 -0
  29. package/src/core/objects/table/table.diff.ts +1 -0
  30. package/src/core/objects/table/table.model.ts +2 -0
  31. package/src/core/plan/sql-format/fixtures.ts +8 -0
  32. package/src/core/post-diff-cycle-breaking.test.ts +142 -0
  33. package/src/core/post-diff-cycle-breaking.ts +83 -2
@@ -112,6 +112,7 @@ describe.concurrent("table.diff", () => {
112
112
  validated: false,
113
113
  is_local: true,
114
114
  no_inherit: false,
115
+ is_temporal: false,
115
116
  is_partition_clone: false,
116
117
  parent_constraint_schema: null,
117
118
  parent_constraint_name: null,
@@ -328,6 +329,7 @@ describe.concurrent("table.diff", () => {
328
329
  validated: false,
329
330
  is_local: true,
330
331
  no_inherit: false,
332
+ is_temporal: false,
331
333
  is_partition_clone: false,
332
334
  parent_constraint_schema: null,
333
335
  parent_constraint_name: null,
@@ -453,6 +455,7 @@ describe.concurrent("table.diff", () => {
453
455
  validated: true,
454
456
  is_local: true,
455
457
  no_inherit: false,
458
+ is_temporal: false,
456
459
  is_partition_clone: false,
457
460
  parent_constraint_schema: null,
458
461
  parent_constraint_name: null,
@@ -531,6 +534,7 @@ describe.concurrent("table.diff", () => {
531
534
  validated: true,
532
535
  is_local: true,
533
536
  no_inherit: false,
537
+ is_temporal: false,
534
538
  is_partition_clone: false,
535
539
  parent_constraint_schema: null,
536
540
  parent_constraint_name: null,
@@ -606,6 +610,104 @@ describe.concurrent("table.diff", () => {
606
610
  );
607
611
  });
608
612
 
613
+ test("altered temporal constraint metadata triggers drop+add", () => {
614
+ const tMain = new Table({
615
+ ...base,
616
+ name: "t_temporal",
617
+ columns: [
618
+ {
619
+ name: "room_id",
620
+ position: 1,
621
+ data_type: "integer",
622
+ data_type_str: "integer",
623
+ is_custom_type: false,
624
+ custom_type_type: null,
625
+ custom_type_category: null,
626
+ custom_type_schema: null,
627
+ custom_type_name: null,
628
+ not_null: false,
629
+ is_identity: false,
630
+ is_identity_always: false,
631
+ is_generated: false,
632
+ collation: null,
633
+ default: null,
634
+ comment: null,
635
+ },
636
+ {
637
+ name: "booking_period",
638
+ position: 2,
639
+ data_type: "tstzrange",
640
+ data_type_str: "tstzrange",
641
+ is_custom_type: false,
642
+ custom_type_type: null,
643
+ custom_type_category: null,
644
+ custom_type_schema: null,
645
+ custom_type_name: null,
646
+ not_null: false,
647
+ is_identity: false,
648
+ is_identity_always: false,
649
+ is_generated: false,
650
+ collation: null,
651
+ default: null,
652
+ comment: null,
653
+ },
654
+ ],
655
+ constraints: [
656
+ {
657
+ name: "bookings_pkey",
658
+ constraint_type: "p",
659
+ deferrable: false,
660
+ initially_deferred: false,
661
+ validated: true,
662
+ is_local: true,
663
+ no_inherit: false,
664
+ is_temporal: false,
665
+ is_partition_clone: false,
666
+ parent_constraint_schema: null,
667
+ parent_constraint_name: null,
668
+ parent_table_schema: null,
669
+ parent_table_name: null,
670
+ key_columns: ["room_id", "booking_period"],
671
+ foreign_key_columns: null,
672
+ foreign_key_table: null,
673
+ foreign_key_schema: null,
674
+ foreign_key_table_is_partition: null,
675
+ foreign_key_parent_schema: null,
676
+ foreign_key_parent_table: null,
677
+ foreign_key_effective_schema: null,
678
+ foreign_key_effective_table: null,
679
+ on_update: null,
680
+ on_delete: null,
681
+ match_type: null,
682
+ check_expression: null,
683
+ owner: "o1",
684
+ definition: "PRIMARY KEY (room_id, booking_period)",
685
+ },
686
+ ],
687
+ });
688
+ const tBranch = new Table({
689
+ ...tMain,
690
+ constraints: [
691
+ {
692
+ ...tMain.constraints[0],
693
+ is_temporal: true,
694
+ definition: "PRIMARY KEY (room_id, booking_period WITHOUT OVERLAPS)",
695
+ },
696
+ ],
697
+ });
698
+ const changes = diffTables(
699
+ testContext,
700
+ { [tMain.stableId]: tMain },
701
+ { [tBranch.stableId]: tBranch },
702
+ );
703
+ expect(changes.some((c) => c instanceof AlterTableDropConstraint)).toBe(
704
+ true,
705
+ );
706
+ expect(changes.some((c) => c instanceof AlterTableAddConstraint)).toBe(
707
+ true,
708
+ );
709
+ });
710
+
609
711
  test("columns added/dropped/altered (type, default, not null)", () => {
610
712
  const main = new Table({ ...base, name: "t2", columns: [] });
611
713
  const withCol = new Table({
@@ -130,6 +130,7 @@ function createAlterConstraintChange(mainTable: Table, branchTable: Table) {
130
130
  mainC.validated !== branchC.validated ||
131
131
  mainC.is_local !== branchC.is_local ||
132
132
  mainC.no_inherit !== branchC.no_inherit ||
133
+ mainC.is_temporal !== branchC.is_temporal ||
133
134
  JSON.stringify(mainC.key_columns) !==
134
135
  JSON.stringify(branchC.key_columns) ||
135
136
  JSON.stringify(mainC.foreign_key_columns) !==
@@ -56,6 +56,7 @@ const tableConstraintPropsSchema = z.object({
56
56
  validated: z.boolean(),
57
57
  is_local: z.boolean(),
58
58
  no_inherit: z.boolean(),
59
+ is_temporal: z.boolean(),
59
60
  is_partition_clone: z.boolean(),
60
61
  parent_constraint_schema: z.string().nullable(),
61
62
  parent_constraint_name: z.string().nullable(),
@@ -284,6 +285,7 @@ select
284
285
  'validated', c.convalidated,
285
286
  'is_local', c.conislocal,
286
287
  'no_inherit', c.connoinherit,
288
+ 'is_temporal', coalesce((to_jsonb(c)->>'conperiod')::boolean, false),
287
289
 
288
290
  -- NEW: propagated-to-partition tagging (PG15+)
289
291
  'is_partition_clone', (c.conparentid <> 0::oid),
@@ -578,6 +578,7 @@ const pkConstraint = {
578
578
  validated: true,
579
579
  is_local: true,
580
580
  no_inherit: false,
581
+ is_temporal: false,
581
582
  is_partition_clone: false,
582
583
  parent_constraint_schema: null,
583
584
  parent_constraint_name: null,
@@ -609,6 +610,7 @@ const uniqueConstraint = {
609
610
  validated: true,
610
611
  is_local: true,
611
612
  no_inherit: false,
613
+ is_temporal: false,
612
614
  is_partition_clone: false,
613
615
  parent_constraint_schema: null,
614
616
  parent_constraint_name: null,
@@ -639,6 +641,7 @@ const fkConstraint = {
639
641
  validated: true,
640
642
  is_local: true,
641
643
  no_inherit: false,
644
+ is_temporal: false,
642
645
  is_partition_clone: false,
643
646
  parent_constraint_schema: null,
644
647
  parent_constraint_name: null,
@@ -670,6 +673,7 @@ const checkConstraint = {
670
673
  validated: true,
671
674
  is_local: true,
672
675
  no_inherit: true,
676
+ is_temporal: false,
673
677
  is_partition_clone: false,
674
678
  parent_constraint_schema: null,
675
679
  parent_constraint_name: null,
@@ -996,6 +1000,8 @@ const rlsPolicy = new RlsPolicy({
996
1000
  with_check_expression: null,
997
1001
  owner: "owner1",
998
1002
  comment: "rls policy comment",
1003
+ referenced_relations: [],
1004
+ referenced_procedures: [],
999
1005
  });
1000
1006
 
1001
1007
  const rlsPolicyRestrictive = new RlsPolicy({
@@ -1009,6 +1015,8 @@ const rlsPolicyRestrictive = new RlsPolicy({
1009
1015
  with_check_expression: "status <> 'locked'",
1010
1016
  owner: "owner1",
1011
1017
  comment: null,
1018
+ referenced_relations: [],
1019
+ referenced_procedures: [],
1012
1020
  });
1013
1021
 
1014
1022
  const index = new Index({
@@ -2,12 +2,15 @@ import { describe, expect, test } from "bun:test";
2
2
  import { Catalog, createEmptyCatalog } from "./catalog.model.ts";
3
3
  import type { Change } from "./change.types.ts";
4
4
  import {
5
+ AlterTableAddConstraint,
5
6
  AlterTableChangeOwner,
6
7
  AlterTableDropColumn,
7
8
  AlterTableDropConstraint,
8
9
  AlterTableEnableRowLevelSecurity,
9
10
  AlterTableSetReplicaIdentity,
11
+ AlterTableValidateConstraint,
10
12
  } from "./objects/table/changes/table.alter.ts";
13
+ import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.ts";
11
14
  import { CreateTable } from "./objects/table/changes/table.create.ts";
12
15
  import { DropTable } from "./objects/table/changes/table.drop.ts";
13
16
  import { GrantTablePrivileges } from "./objects/table/changes/table.privilege.ts";
@@ -77,6 +80,7 @@ describe("normalizePostDiffCycles", () => {
77
80
  validated: true,
78
81
  is_local: true,
79
82
  no_inherit: false,
83
+ is_temporal: false,
80
84
  is_partition_clone: false,
81
85
  parent_constraint_schema: null,
82
86
  parent_constraint_name: null,
@@ -117,6 +121,7 @@ describe("normalizePostDiffCycles", () => {
117
121
  validated: true,
118
122
  is_local: true,
119
123
  no_inherit: false,
124
+ is_temporal: false,
120
125
  is_partition_clone: false,
121
126
  parent_constraint_schema: null,
122
127
  parent_constraint_name: null,
@@ -237,6 +242,7 @@ describe("normalizePostDiffCycles", () => {
237
242
  validated: true,
238
243
  is_local: true,
239
244
  no_inherit: false,
245
+ is_temporal: false,
240
246
  is_partition_clone: false,
241
247
  parent_constraint_schema: null,
242
248
  parent_constraint_name: null,
@@ -314,4 +320,140 @@ describe("normalizePostDiffCycles", () => {
314
320
  expect(normalized).toContain(preExistingReplicaIdentity);
315
321
  expect(normalized).toContain(preExistingGrant);
316
322
  });
323
+
324
+ test("dedupes duplicate constraint Add/Validate/Comment on replaced tables keeping last occurrence", async () => {
325
+ const baseline = await createEmptyCatalog(170000, "postgres");
326
+ const branchChildren = new Table({
327
+ ...baseTableProps,
328
+ name: "children",
329
+ columns: [
330
+ { ...integerColumn("id", 1), not_null: true },
331
+ integerColumn("parent_ref", 2),
332
+ ],
333
+ });
334
+ const otherTable = new Table({
335
+ ...baseTableProps,
336
+ name: "other",
337
+ columns: [{ ...integerColumn("id", 1), not_null: true }],
338
+ });
339
+
340
+ const fkConstraint = {
341
+ name: "children_parent_ref_fkey",
342
+ constraint_type: "f" as const,
343
+ deferrable: false,
344
+ initially_deferred: false,
345
+ validated: false,
346
+ is_local: true,
347
+ no_inherit: false,
348
+ is_temporal: true,
349
+ is_partition_clone: false,
350
+ parent_constraint_schema: null,
351
+ parent_constraint_name: null,
352
+ parent_table_schema: null,
353
+ parent_table_name: null,
354
+ key_columns: ["parent_ref"],
355
+ foreign_key_columns: ["id"],
356
+ foreign_key_table: "parents",
357
+ foreign_key_schema: "public",
358
+ foreign_key_table_is_partition: false,
359
+ foreign_key_parent_schema: null,
360
+ foreign_key_parent_table: null,
361
+ foreign_key_effective_schema: "public",
362
+ foreign_key_effective_table: "parents",
363
+ on_update: "a" as const,
364
+ on_delete: "a" as const,
365
+ match_type: "s" as const,
366
+ check_expression: null,
367
+ owner: "postgres",
368
+ definition:
369
+ "FOREIGN KEY (parent_ref, PERIOD valid_period) REFERENCES public.parents(id, PERIOD valid_period)",
370
+ comment: "fk comment",
371
+ };
372
+ const otherConstraint = {
373
+ ...fkConstraint,
374
+ name: "other_unique",
375
+ constraint_type: "u" as const,
376
+ foreign_key_table: null,
377
+ foreign_key_schema: null,
378
+ foreign_key_effective_schema: null,
379
+ foreign_key_effective_table: null,
380
+ foreign_key_columns: [],
381
+ key_columns: ["id"],
382
+ definition: "UNIQUE (id)",
383
+ };
384
+
385
+ const diffTablesAdd = new AlterTableAddConstraint({
386
+ table: branchChildren,
387
+ constraint: fkConstraint,
388
+ });
389
+ const diffTablesValidate = new AlterTableValidateConstraint({
390
+ table: branchChildren,
391
+ constraint: fkConstraint,
392
+ });
393
+ const diffTablesComment = new CreateCommentOnConstraint({
394
+ table: branchChildren,
395
+ constraint: fkConstraint,
396
+ });
397
+ const expansionAdd = new AlterTableAddConstraint({
398
+ table: branchChildren,
399
+ constraint: fkConstraint,
400
+ });
401
+ const expansionValidate = new AlterTableValidateConstraint({
402
+ table: branchChildren,
403
+ constraint: fkConstraint,
404
+ });
405
+ const expansionComment = new CreateCommentOnConstraint({
406
+ table: branchChildren,
407
+ constraint: fkConstraint,
408
+ });
409
+ const soloOtherTableAdd = new AlterTableAddConstraint({
410
+ table: otherTable,
411
+ constraint: otherConstraint,
412
+ });
413
+
414
+ const changes: Change[] = [
415
+ new DropTable({ table: branchChildren }),
416
+ new CreateTable({ table: branchChildren }),
417
+ diffTablesAdd,
418
+ diffTablesValidate,
419
+ diffTablesComment,
420
+ soloOtherTableAdd,
421
+ expansionAdd,
422
+ expansionValidate,
423
+ expansionComment,
424
+ ];
425
+
426
+ const mainCatalog = new Catalog({
427
+ ...baseline,
428
+ tables: { [branchChildren.stableId]: branchChildren },
429
+ });
430
+
431
+ const normalized = normalizePostDiffCycles({
432
+ changes,
433
+ mainCatalog,
434
+ replacedTableIds: new Set([branchChildren.stableId]),
435
+ });
436
+
437
+ expect(normalized).not.toContain(diffTablesAdd);
438
+ expect(normalized).not.toContain(diffTablesValidate);
439
+ expect(normalized).not.toContain(diffTablesComment);
440
+ expect(normalized).toContain(expansionAdd);
441
+ expect(normalized).toContain(expansionValidate);
442
+ expect(normalized).toContain(expansionComment);
443
+ expect(normalized).toContain(soloOtherTableAdd);
444
+
445
+ expect(
446
+ normalized.filter((change) => change instanceof AlterTableAddConstraint),
447
+ ).toHaveLength(2);
448
+ expect(
449
+ normalized.filter(
450
+ (change) => change instanceof AlterTableValidateConstraint,
451
+ ),
452
+ ).toHaveLength(1);
453
+ expect(
454
+ normalized.filter(
455
+ (change) => change instanceof CreateCommentOnConstraint,
456
+ ),
457
+ ).toHaveLength(1);
458
+ });
317
459
  });
@@ -1,9 +1,12 @@
1
1
  import type { Catalog } from "./catalog.model.ts";
2
2
  import type { Change } from "./change.types.ts";
3
3
  import {
4
+ AlterTableAddConstraint,
4
5
  AlterTableDropColumn,
5
6
  AlterTableDropConstraint,
7
+ AlterTableValidateConstraint,
6
8
  } from "./objects/table/changes/table.alter.ts";
9
+ import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.ts";
7
10
  import { DropTable } from "./objects/table/changes/table.drop.ts";
8
11
  import { stableId } from "./objects/utils.ts";
9
12
 
@@ -52,6 +55,72 @@ function isSupersededByTableReplacement(
52
55
  return replacedTableIds.has(change.table.stableId);
53
56
  }
54
57
 
58
+ /**
59
+ * Drop earlier duplicates of `AlterTableAddConstraint` /
60
+ * `AlterTableValidateConstraint` / `CreateCommentOnConstraint` targeting
61
+ * replaced tables, keeping only the last occurrence of each
62
+ * `(changeType, table.stableId, constraint.name)`.
63
+ *
64
+ * When `expandReplaceDependencies()` promotes a table to a full
65
+ * `DropTable + CreateTable` pair, it also emits one
66
+ * `AlterTableAddConstraint` (plus optional `VALIDATE CONSTRAINT` /
67
+ * `COMMENT ON CONSTRAINT`) per branch constraint. If `diffTables()` already
68
+ * emitted the same change for a shape flip or a new constraint on that
69
+ * table, the plan ends up with two identical `ALTER TABLE ... ADD
70
+ * CONSTRAINT ...` statements and PostgreSQL fails at apply time with
71
+ * `constraint "..." for relation "..." already exists`. Because
72
+ * `expandReplaceDependencies()` appends its additions after the original
73
+ * `diffTables()` output, the last occurrence is the expansion's emission —
74
+ * keeping it preserves correctness while removing the duplicate.
75
+ */
76
+ function dropReplacedTableDuplicateConstraintChanges(
77
+ changes: Change[],
78
+ replacedTableIds: ReadonlySet<string>,
79
+ ): Change[] {
80
+ if (replacedTableIds.size === 0) return changes;
81
+
82
+ const keyFor = (change: Change): string | null => {
83
+ if (
84
+ !(change instanceof AlterTableAddConstraint) &&
85
+ !(change instanceof AlterTableValidateConstraint) &&
86
+ !(change instanceof CreateCommentOnConstraint)
87
+ ) {
88
+ return null;
89
+ }
90
+ if (!replacedTableIds.has(change.table.stableId)) return null;
91
+ const tag =
92
+ change instanceof AlterTableAddConstraint
93
+ ? "add"
94
+ : change instanceof AlterTableValidateConstraint
95
+ ? "validate"
96
+ : "comment";
97
+ return `${tag}:${constraintStableId(change.table, change.constraint.name)}`;
98
+ };
99
+
100
+ const seen = new Set<string>();
101
+ const reversedKept: Change[] = [];
102
+ let mutated = false;
103
+
104
+ // Walk backwards: the first encounter of each key corresponds to its LAST
105
+ // occurrence in the original order. `expandReplaceDependencies()` appends
106
+ // additions after the original changes, so "last wins" keeps the
107
+ // expansion's emission and drops the earlier diffTables duplicate.
108
+ for (let i = changes.length - 1; i >= 0; i--) {
109
+ const change = changes[i] as Change;
110
+ const key = keyFor(change);
111
+ if (key !== null) {
112
+ if (seen.has(key)) {
113
+ mutated = true;
114
+ continue;
115
+ }
116
+ seen.add(key);
117
+ }
118
+ reversedKept.push(change);
119
+ }
120
+
121
+ return mutated ? reversedKept.reverse() : changes;
122
+ }
123
+
55
124
  function collectExplicitConstraintDropIds(changes: Change[]) {
56
125
  const explicitConstraintDropIds = new Set<string>();
57
126
 
@@ -84,6 +153,13 @@ function hasSameEntries(
84
153
  * - If replace expansion added `DropTable(T)+CreateTable(T)`, targeted
85
154
  * `AlterTableDropColumn(T.*)` / `AlterTableDropConstraint(T.*)` changes are
86
155
  * redundant and create an unbreakable drop-phase cycle, so we elide them.
156
+ * - When the same `DropTable+CreateTable` pair is present, the expansion
157
+ * also emits one `AlterTableAddConstraint` / `AlterTableValidateConstraint`
158
+ * / `CreateCommentOnConstraint` per branch constraint, which may collide
159
+ * with the same change already emitted by `diffTables()` (for example on a
160
+ * shape flip or a new constraint). We dedupe these keeping only the last
161
+ * occurrence so the expansion's emission survives and the diffTables
162
+ * duplicate is removed.
87
163
  * - If two dropped tables reference each other via FK, we insert dedicated
88
164
  * `AlterTableDropConstraint` changes and teach the paired `DropTable`
89
165
  * changes not to claim those FK stable IDs.
@@ -100,10 +176,15 @@ export function normalizePostDiffCycles({
100
176
  mainCatalog: Catalog;
101
177
  replacedTableIds?: ReadonlySet<string>;
102
178
  }): Change[] {
179
+ const dedupedChanges = dropReplacedTableDuplicateConstraintChanges(
180
+ changes,
181
+ replacedTableIds,
182
+ );
183
+
103
184
  const structurallyNormalizedChanges =
104
185
  replacedTableIds.size === 0
105
- ? changes
106
- : changes.filter(
186
+ ? dedupedChanges
187
+ : dedupedChanges.filter(
107
188
  (change) => !isSupersededByTableReplacement(change, replacedTableIds),
108
189
  );
109
190