@supabase/pg-delta 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (37) hide show
  1. package/README.md +7 -1
  2. package/dist/core/catalog.diff.js +7 -1
  3. package/dist/core/connection-url.d.ts +32 -0
  4. package/dist/core/connection-url.js +77 -0
  5. package/dist/core/expand-replace-dependencies.d.ts +8 -2
  6. package/dist/core/expand-replace-dependencies.js +24 -10
  7. package/dist/core/integrations/supabase.js +1 -0
  8. package/dist/core/objects/procedure/procedure.diff.js +8 -0
  9. package/dist/core/objects/sequence/sequence.diff.js +14 -6
  10. package/dist/core/objects/table/changes/table.alter.js +4 -1
  11. package/dist/core/objects/table/changes/table.drop.d.ts +12 -0
  12. package/dist/core/objects/table/changes/table.drop.js +20 -3
  13. package/dist/core/objects/table/table.diff.js +7 -2
  14. package/dist/core/post-diff-cycle-breaking.d.ts +22 -0
  15. package/dist/core/post-diff-cycle-breaking.js +143 -0
  16. package/dist/core/postgres-config.d.ts +27 -0
  17. package/dist/core/postgres-config.js +99 -7
  18. package/package.json +2 -1
  19. package/src/core/catalog.diff.ts +7 -1
  20. package/src/core/connection-url.test.ts +142 -0
  21. package/src/core/connection-url.ts +82 -0
  22. package/src/core/expand-replace-dependencies.test.ts +247 -8
  23. package/src/core/expand-replace-dependencies.ts +33 -5
  24. package/src/core/integrations/supabase.ts +1 -0
  25. package/src/core/objects/procedure/procedure.diff.test.ts +25 -0
  26. package/src/core/objects/procedure/procedure.diff.ts +12 -0
  27. package/src/core/objects/sequence/sequence.diff.test.ts +110 -8
  28. package/src/core/objects/sequence/sequence.diff.ts +16 -6
  29. package/src/core/objects/table/changes/table.alter.test.ts +14 -0
  30. package/src/core/objects/table/changes/table.alter.ts +4 -1
  31. package/src/core/objects/table/changes/table.drop.ts +27 -4
  32. package/src/core/objects/table/table.diff.test.ts +55 -0
  33. package/src/core/objects/table/table.diff.ts +10 -2
  34. package/src/core/post-diff-cycle-breaking.test.ts +317 -0
  35. package/src/core/post-diff-cycle-breaking.ts +236 -0
  36. package/src/core/postgres-config.test.ts +241 -0
  37. package/src/core/postgres-config.ts +127 -16
@@ -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: { table: Table }) {
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
- ...this.table.constraints.map((constraint) =>
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.table.constraints.map((constraint) =>
74
+ ...this.claimedConstraints.map((constraint) =>
52
75
  stableId.constraint(
53
76
  this.table.schema,
54
77
  this.table.name,
@@ -835,6 +835,61 @@ describe.concurrent("table.diff", () => {
835
835
  ).toBe(true);
836
836
  });
837
837
 
838
+ test("postgres 17+ recreates a column when switching from regular to generated", () => {
839
+ const pg17Context = {
840
+ ...testContext,
841
+ version: 170000,
842
+ };
843
+
844
+ const regularColumn = {
845
+ name: "confirmed_at",
846
+ position: 1,
847
+ data_type: "timestamp with time zone",
848
+ data_type_str: "timestamp with time zone",
849
+ is_custom_type: false,
850
+ custom_type_type: null,
851
+ custom_type_category: null,
852
+ custom_type_schema: null,
853
+ custom_type_name: null,
854
+ not_null: false,
855
+ is_identity: false,
856
+ is_identity_always: false,
857
+ is_generated: false,
858
+ collation: null,
859
+ default: null,
860
+ comment: null,
861
+ };
862
+
863
+ const generatedColumn = {
864
+ ...regularColumn,
865
+ is_generated: true,
866
+ default: "LEAST(email_confirmed_at, phone_confirmed_at)",
867
+ };
868
+
869
+ const mainTable = new Table({
870
+ ...base,
871
+ name: "auth_users_like",
872
+ columns: [regularColumn],
873
+ });
874
+ const branchTable = new Table({
875
+ ...base,
876
+ name: "auth_users_like",
877
+ columns: [generatedColumn],
878
+ });
879
+
880
+ const changes = diffTables(
881
+ pg17Context,
882
+ { [mainTable.stableId]: mainTable },
883
+ { [branchTable.stableId]: branchTable },
884
+ );
885
+
886
+ expect(changes.some((c) => c instanceof AlterTableDropColumn)).toBe(true);
887
+ expect(changes.some((c) => c instanceof AlterTableAddColumn)).toBe(true);
888
+ expect(
889
+ changes.some((c) => c instanceof AlterTableAlterColumnSetDefault),
890
+ ).toBe(false);
891
+ });
892
+
838
893
  test("created table with privileges emits grant changes", () => {
839
894
  const t = new Table({
840
895
  ...base,
@@ -745,10 +745,18 @@ export function diffTables(
745
745
  // Set new default value
746
746
  const isGeneratedColumn = branchCol.is_generated;
747
747
  const isPostgresLowerThan17 = ctx.version < 170000;
748
+ const generatedStatusChanged =
749
+ mainCol.is_generated !== branchCol.is_generated;
748
750
 
749
- if (isGeneratedColumn && isPostgresLowerThan17) {
751
+ if (
752
+ isGeneratedColumn &&
753
+ (isPostgresLowerThan17 || generatedStatusChanged)
754
+ ) {
750
755
  // For generated columns in < PostgreSQL 17, we need to drop and recreate
751
- // instead of using SET EXPRESSION AS for computed columns
756
+ // instead of using SET EXPRESSION AS for computed columns. We also
757
+ // need to recreate the column when switching between regular and
758
+ // generated states because SET EXPRESSION only applies to existing
759
+ // generated columns.
752
760
  // cf: https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=5d06e99a3
753
761
  // cf: https://www.postgresql.org/docs/release/17.0/
754
762
  // > Allow ALTER TABLE to change a column's generation expression
@@ -0,0 +1,317 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Catalog, createEmptyCatalog } from "./catalog.model.ts";
3
+ import type { Change } from "./change.types.ts";
4
+ import {
5
+ AlterTableChangeOwner,
6
+ AlterTableDropColumn,
7
+ AlterTableDropConstraint,
8
+ AlterTableEnableRowLevelSecurity,
9
+ AlterTableSetReplicaIdentity,
10
+ } from "./objects/table/changes/table.alter.ts";
11
+ import { CreateTable } from "./objects/table/changes/table.create.ts";
12
+ import { DropTable } from "./objects/table/changes/table.drop.ts";
13
+ import { GrantTablePrivileges } from "./objects/table/changes/table.privilege.ts";
14
+ import { Table } from "./objects/table/table.model.ts";
15
+ import { stableId } from "./objects/utils.ts";
16
+ import { normalizePostDiffCycles } from "./post-diff-cycle-breaking.ts";
17
+
18
+ const baseTableProps = {
19
+ schema: "public",
20
+ persistence: "p" as const,
21
+ row_security: false,
22
+ force_row_security: false,
23
+ has_indexes: false,
24
+ has_rules: false,
25
+ has_triggers: false,
26
+ has_subclasses: false,
27
+ is_populated: true,
28
+ replica_identity: "d" as const,
29
+ is_partition: false,
30
+ options: null,
31
+ partition_bound: null,
32
+ partition_by: null,
33
+ owner: "postgres",
34
+ comment: null,
35
+ parent_schema: null,
36
+ parent_name: null,
37
+ privileges: [],
38
+ };
39
+
40
+ function integerColumn(name: string, position: number) {
41
+ return {
42
+ name,
43
+ position,
44
+ data_type: "integer" as const,
45
+ data_type_str: "integer",
46
+ is_custom_type: false as const,
47
+ custom_type_type: null,
48
+ custom_type_category: null,
49
+ custom_type_schema: null,
50
+ custom_type_name: null,
51
+ not_null: false,
52
+ is_identity: false,
53
+ is_identity_always: false,
54
+ is_generated: false,
55
+ collation: null,
56
+ default: null,
57
+ comment: null,
58
+ };
59
+ }
60
+
61
+ describe("normalizePostDiffCycles", () => {
62
+ test("injects explicit FK drops for mutually dependent dropped tables", async () => {
63
+ const baseline = await createEmptyCatalog(170000, "postgres");
64
+ const tableA = new Table({
65
+ ...baseTableProps,
66
+ name: "a",
67
+ columns: [
68
+ { ...integerColumn("id", 1), not_null: true },
69
+ integerColumn("b_id", 2),
70
+ ],
71
+ constraints: [
72
+ {
73
+ name: "a_b_fkey",
74
+ constraint_type: "f",
75
+ deferrable: false,
76
+ initially_deferred: false,
77
+ validated: true,
78
+ is_local: true,
79
+ no_inherit: false,
80
+ is_partition_clone: false,
81
+ parent_constraint_schema: null,
82
+ parent_constraint_name: null,
83
+ parent_table_schema: null,
84
+ parent_table_name: null,
85
+ key_columns: ["b_id"],
86
+ foreign_key_columns: ["id"],
87
+ foreign_key_table: "b",
88
+ foreign_key_schema: "public",
89
+ foreign_key_table_is_partition: false,
90
+ foreign_key_parent_schema: null,
91
+ foreign_key_parent_table: null,
92
+ foreign_key_effective_schema: "public",
93
+ foreign_key_effective_table: "b",
94
+ on_update: "a",
95
+ on_delete: "a",
96
+ match_type: "s",
97
+ check_expression: null,
98
+ owner: "postgres",
99
+ definition: "FOREIGN KEY (b_id) REFERENCES public.b(id)",
100
+ comment: null,
101
+ },
102
+ ],
103
+ });
104
+ const tableB = new Table({
105
+ ...baseTableProps,
106
+ name: "b",
107
+ columns: [
108
+ { ...integerColumn("id", 1), not_null: true },
109
+ integerColumn("a_id", 2),
110
+ ],
111
+ constraints: [
112
+ {
113
+ name: "b_a_fkey",
114
+ constraint_type: "f",
115
+ deferrable: false,
116
+ initially_deferred: false,
117
+ validated: true,
118
+ is_local: true,
119
+ no_inherit: false,
120
+ is_partition_clone: false,
121
+ parent_constraint_schema: null,
122
+ parent_constraint_name: null,
123
+ parent_table_schema: null,
124
+ parent_table_name: null,
125
+ key_columns: ["a_id"],
126
+ foreign_key_columns: ["id"],
127
+ foreign_key_table: "a",
128
+ foreign_key_schema: "public",
129
+ foreign_key_table_is_partition: false,
130
+ foreign_key_parent_schema: null,
131
+ foreign_key_parent_table: null,
132
+ foreign_key_effective_schema: "public",
133
+ foreign_key_effective_table: "a",
134
+ on_update: "a",
135
+ on_delete: "a",
136
+ match_type: "s",
137
+ check_expression: null,
138
+ owner: "postgres",
139
+ definition: "FOREIGN KEY (a_id) REFERENCES public.a(id)",
140
+ comment: null,
141
+ },
142
+ ],
143
+ });
144
+ const mainCatalog = new Catalog({
145
+ ...baseline,
146
+ tables: {
147
+ [tableA.stableId]: tableA,
148
+ [tableB.stableId]: tableB,
149
+ },
150
+ });
151
+ const changes: Change[] = [
152
+ new DropTable({ table: tableA }),
153
+ new DropTable({ table: tableB }),
154
+ ];
155
+
156
+ const normalized = normalizePostDiffCycles({
157
+ changes,
158
+ mainCatalog,
159
+ });
160
+
161
+ const explicitConstraintDrops = normalized.filter(
162
+ (change) => change instanceof AlterTableDropConstraint,
163
+ );
164
+ expect(explicitConstraintDrops).toHaveLength(2);
165
+
166
+ const normalizedDropTableA = normalized.find(
167
+ (change) =>
168
+ change instanceof DropTable &&
169
+ change.table.stableId === tableA.stableId,
170
+ );
171
+ const normalizedDropTableB = normalized.find(
172
+ (change) =>
173
+ change instanceof DropTable &&
174
+ change.table.stableId === tableB.stableId,
175
+ );
176
+ if (!(normalizedDropTableA instanceof DropTable)) {
177
+ throw new Error("expected normalized DropTable(public.a)");
178
+ }
179
+ if (!(normalizedDropTableB instanceof DropTable)) {
180
+ throw new Error("expected normalized DropTable(public.b)");
181
+ }
182
+
183
+ expect(
184
+ normalizedDropTableA.externallyDroppedConstraints.has("a_b_fkey"),
185
+ ).toBe(true);
186
+ expect(
187
+ normalizedDropTableB.externallyDroppedConstraints.has("b_a_fkey"),
188
+ ).toBe(true);
189
+ expect(
190
+ normalizedDropTableA.requires.includes(
191
+ stableId.constraint("public", "a", "a_b_fkey"),
192
+ ),
193
+ ).toBe(false);
194
+ expect(
195
+ normalizedDropTableB.requires.includes(
196
+ stableId.constraint("public", "b", "b_a_fkey"),
197
+ ),
198
+ ).toBe(false);
199
+ });
200
+
201
+ test("prunes same-table drop-column and drop-constraint ALTERs for replaced tables only", async () => {
202
+ const baseline = await createEmptyCatalog(170000, "postgres");
203
+ const mainChildren = new Table({
204
+ ...baseTableProps,
205
+ name: "children",
206
+ columns: [
207
+ { ...integerColumn("id", 1), not_null: true },
208
+ integerColumn("parent_ref", 2),
209
+ integerColumn("status", 3),
210
+ ],
211
+ });
212
+ const branchChildren = new Table({
213
+ ...baseTableProps,
214
+ name: "children",
215
+ columns: [
216
+ { ...integerColumn("id", 1), not_null: true },
217
+ integerColumn("status", 2),
218
+ ],
219
+ });
220
+
221
+ const droppedColumn = mainChildren.columns.find(
222
+ (column) => column.name === "parent_ref",
223
+ );
224
+ if (!droppedColumn) throw new Error("test setup: parent_ref missing");
225
+
226
+ const preExistingDropColumn = new AlterTableDropColumn({
227
+ table: mainChildren,
228
+ column: droppedColumn,
229
+ });
230
+ const preExistingDropConstraint = new AlterTableDropConstraint({
231
+ table: mainChildren,
232
+ constraint: {
233
+ name: "children_parent_ref_fkey",
234
+ constraint_type: "f",
235
+ deferrable: false,
236
+ initially_deferred: false,
237
+ validated: true,
238
+ is_local: true,
239
+ no_inherit: false,
240
+ is_partition_clone: false,
241
+ parent_constraint_schema: null,
242
+ parent_constraint_name: null,
243
+ parent_table_schema: null,
244
+ parent_table_name: null,
245
+ key_columns: ["parent_ref"],
246
+ foreign_key_columns: ["id"],
247
+ foreign_key_table: "parents",
248
+ foreign_key_schema: "public",
249
+ foreign_key_table_is_partition: false,
250
+ foreign_key_parent_schema: null,
251
+ foreign_key_parent_table: null,
252
+ foreign_key_effective_schema: "public",
253
+ foreign_key_effective_table: "parents",
254
+ on_update: "a",
255
+ on_delete: "a",
256
+ match_type: "s",
257
+ check_expression: null,
258
+ owner: "postgres",
259
+ definition: "FOREIGN KEY (parent_ref) REFERENCES public.parents(id)",
260
+ comment: null,
261
+ },
262
+ });
263
+ const preExistingChangeOwner = new AlterTableChangeOwner({
264
+ table: branchChildren,
265
+ owner: "new_owner",
266
+ });
267
+ const preExistingEnableRls = new AlterTableEnableRowLevelSecurity({
268
+ table: branchChildren,
269
+ });
270
+ const preExistingReplicaIdentity = new AlterTableSetReplicaIdentity({
271
+ table: branchChildren,
272
+ mode: "f",
273
+ });
274
+ const preExistingGrant = new GrantTablePrivileges({
275
+ table: branchChildren,
276
+ grantee: "reader",
277
+ privileges: [{ privilege: "SELECT", grantable: false }],
278
+ });
279
+ const changes: Change[] = [
280
+ new DropTable({ table: mainChildren }),
281
+ new CreateTable({ table: branchChildren }),
282
+ preExistingDropColumn,
283
+ preExistingDropConstraint,
284
+ preExistingChangeOwner,
285
+ preExistingEnableRls,
286
+ preExistingReplicaIdentity,
287
+ preExistingGrant,
288
+ ];
289
+ const mainCatalog = new Catalog({
290
+ ...baseline,
291
+ tables: { [mainChildren.stableId]: mainChildren },
292
+ });
293
+
294
+ const normalized = normalizePostDiffCycles({
295
+ changes,
296
+ mainCatalog,
297
+ replacedTableIds: new Set([mainChildren.stableId]),
298
+ });
299
+
300
+ expect(normalized.some((change) => change instanceof DropTable)).toBe(true);
301
+ expect(normalized.some((change) => change instanceof CreateTable)).toBe(
302
+ true,
303
+ );
304
+ expect(normalized).not.toContain(preExistingDropColumn);
305
+ expect(normalized).not.toContain(preExistingDropConstraint);
306
+ expect(
307
+ normalized.some((change) => change instanceof AlterTableDropColumn),
308
+ ).toBe(false);
309
+ expect(
310
+ normalized.some((change) => change instanceof AlterTableDropConstraint),
311
+ ).toBe(false);
312
+ expect(normalized).toContain(preExistingChangeOwner);
313
+ expect(normalized).toContain(preExistingEnableRls);
314
+ expect(normalized).toContain(preExistingReplicaIdentity);
315
+ expect(normalized).toContain(preExistingGrant);
316
+ });
317
+ });
@@ -0,0 +1,236 @@
1
+ import type { Catalog } from "./catalog.model.ts";
2
+ import type { Change } from "./change.types.ts";
3
+ import {
4
+ AlterTableDropColumn,
5
+ AlterTableDropConstraint,
6
+ } from "./objects/table/changes/table.alter.ts";
7
+ import { DropTable } from "./objects/table/changes/table.drop.ts";
8
+ import { stableId } from "./objects/utils.ts";
9
+
10
+ function constraintStableId(
11
+ table: { schema: string; name: string },
12
+ constraintName: string,
13
+ ) {
14
+ return stableId.constraint(table.schema, table.name, constraintName);
15
+ }
16
+
17
+ /**
18
+ * Yield FK constraints on `table` whose referenced table is also dropped in the
19
+ * final plan. Self-references are left alone because the sort phase already
20
+ * handles the resulting self-loop correctly.
21
+ */
22
+ function* iterCrossDropFkConstraints(
23
+ table: Catalog["tables"][string],
24
+ droppedSet: ReadonlySet<string>,
25
+ ) {
26
+ for (const constraint of table.constraints) {
27
+ if (constraint.constraint_type !== "f") continue;
28
+ if (constraint.is_partition_clone) continue;
29
+ if (!constraint.foreign_key_schema || !constraint.foreign_key_table) {
30
+ continue;
31
+ }
32
+ const referencedId = stableId.table(
33
+ constraint.foreign_key_schema,
34
+ constraint.foreign_key_table,
35
+ );
36
+ if (referencedId === table.stableId) continue;
37
+ if (!droppedSet.has(referencedId)) continue;
38
+ yield { constraint, referencedId };
39
+ }
40
+ }
41
+
42
+ function isSupersededByTableReplacement(
43
+ change: Change,
44
+ replacedTableIds: ReadonlySet<string>,
45
+ ): boolean {
46
+ if (
47
+ !(change instanceof AlterTableDropColumn) &&
48
+ !(change instanceof AlterTableDropConstraint)
49
+ ) {
50
+ return false;
51
+ }
52
+ return replacedTableIds.has(change.table.stableId);
53
+ }
54
+
55
+ function collectExplicitConstraintDropIds(changes: Change[]) {
56
+ const explicitConstraintDropIds = new Set<string>();
57
+
58
+ for (const change of changes) {
59
+ if (!(change instanceof AlterTableDropConstraint)) continue;
60
+ explicitConstraintDropIds.add(
61
+ constraintStableId(change.table, change.constraint.name),
62
+ );
63
+ }
64
+
65
+ return explicitConstraintDropIds;
66
+ }
67
+
68
+ function hasSameEntries(
69
+ left: ReadonlySet<string>,
70
+ right: ReadonlySet<string>,
71
+ ): boolean {
72
+ if (left.size !== right.size) return false;
73
+ for (const value of left) {
74
+ if (!right.has(value)) return false;
75
+ }
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Normalize change-list cycles that only become apparent after all object
81
+ * diffs have been collected.
82
+ *
83
+ * This pass intentionally handles whole-plan interactions only:
84
+ * - If replace expansion added `DropTable(T)+CreateTable(T)`, targeted
85
+ * `AlterTableDropColumn(T.*)` / `AlterTableDropConstraint(T.*)` changes are
86
+ * redundant and create an unbreakable drop-phase cycle, so we elide them.
87
+ * - If two dropped tables reference each other via FK, we insert dedicated
88
+ * `AlterTableDropConstraint` changes and teach the paired `DropTable`
89
+ * changes not to claim those FK stable IDs.
90
+ *
91
+ * Object-local PostgreSQL semantics (for example owned-sequence cascades) stay
92
+ * in the corresponding `diff*` function instead of this pass.
93
+ */
94
+ export function normalizePostDiffCycles({
95
+ changes,
96
+ mainCatalog,
97
+ replacedTableIds = new Set<string>(),
98
+ }: {
99
+ changes: Change[];
100
+ mainCatalog: Catalog;
101
+ replacedTableIds?: ReadonlySet<string>;
102
+ }): Change[] {
103
+ const structurallyNormalizedChanges =
104
+ replacedTableIds.size === 0
105
+ ? changes
106
+ : changes.filter(
107
+ (change) => !isSupersededByTableReplacement(change, replacedTableIds),
108
+ );
109
+
110
+ const dropTableChanges = structurallyNormalizedChanges.filter(
111
+ (change): change is DropTable => change instanceof DropTable,
112
+ );
113
+
114
+ if (dropTableChanges.length < 2) {
115
+ return structurallyNormalizedChanges;
116
+ }
117
+
118
+ const droppedSet = new Set(
119
+ dropTableChanges.map((change) => change.table.stableId),
120
+ );
121
+ const droppedFkTargets = new Map<string, Set<string>>();
122
+
123
+ for (const dropTableChange of dropTableChanges) {
124
+ const mainTable =
125
+ mainCatalog.tables[dropTableChange.table.stableId] ??
126
+ dropTableChange.table;
127
+ const targets = new Set<string>();
128
+
129
+ for (const { referencedId } of iterCrossDropFkConstraints(
130
+ mainTable,
131
+ droppedSet,
132
+ )) {
133
+ targets.add(referencedId);
134
+ }
135
+
136
+ droppedFkTargets.set(mainTable.stableId, targets);
137
+ }
138
+
139
+ const explicitConstraintDropIds = collectExplicitConstraintDropIds(
140
+ structurallyNormalizedChanges,
141
+ );
142
+ const injectedConstraintDropsByTableId = new Map<
143
+ string,
144
+ AlterTableDropConstraint[]
145
+ >();
146
+ const externallyDroppedConstraintsByTableId = new Map<
147
+ string,
148
+ ReadonlySet<string>
149
+ >();
150
+ let didMutate = structurallyNormalizedChanges !== changes;
151
+
152
+ for (const dropTableChange of dropTableChanges) {
153
+ const mainTable =
154
+ mainCatalog.tables[dropTableChange.table.stableId] ??
155
+ dropTableChange.table;
156
+ const externallyDroppedConstraints = new Set(
157
+ dropTableChange.externallyDroppedConstraints,
158
+ );
159
+
160
+ for (const { constraint, referencedId } of iterCrossDropFkConstraints(
161
+ mainTable,
162
+ droppedSet,
163
+ )) {
164
+ const isMutual =
165
+ droppedFkTargets.get(referencedId)?.has(mainTable.stableId) === true;
166
+ if (!isMutual) continue;
167
+
168
+ const droppedConstraintStableId = constraintStableId(
169
+ mainTable,
170
+ constraint.name,
171
+ );
172
+ externallyDroppedConstraints.add(constraint.name);
173
+
174
+ if (!explicitConstraintDropIds.has(droppedConstraintStableId)) {
175
+ const injectedDrop = new AlterTableDropConstraint({
176
+ table: mainTable,
177
+ constraint,
178
+ });
179
+ const existingDrops =
180
+ injectedConstraintDropsByTableId.get(mainTable.stableId) ?? [];
181
+ existingDrops.push(injectedDrop);
182
+ injectedConstraintDropsByTableId.set(mainTable.stableId, existingDrops);
183
+ explicitConstraintDropIds.add(droppedConstraintStableId);
184
+ didMutate = true;
185
+ }
186
+ }
187
+
188
+ if (
189
+ !hasSameEntries(
190
+ dropTableChange.externallyDroppedConstraints,
191
+ externallyDroppedConstraints,
192
+ )
193
+ ) {
194
+ externallyDroppedConstraintsByTableId.set(
195
+ mainTable.stableId,
196
+ externallyDroppedConstraints,
197
+ );
198
+ didMutate = true;
199
+ }
200
+ }
201
+
202
+ if (!didMutate) {
203
+ return changes;
204
+ }
205
+
206
+ const normalizedChanges: Change[] = [];
207
+
208
+ for (const change of structurallyNormalizedChanges) {
209
+ if (!(change instanceof DropTable)) {
210
+ normalizedChanges.push(change);
211
+ continue;
212
+ }
213
+
214
+ const injectedConstraintDrops =
215
+ injectedConstraintDropsByTableId.get(change.table.stableId) ?? [];
216
+ if (injectedConstraintDrops.length > 0) {
217
+ normalizedChanges.push(...injectedConstraintDrops);
218
+ }
219
+
220
+ const externallyDroppedConstraints =
221
+ externallyDroppedConstraintsByTableId.get(change.table.stableId);
222
+ if (!externallyDroppedConstraints) {
223
+ normalizedChanges.push(change);
224
+ continue;
225
+ }
226
+
227
+ normalizedChanges.push(
228
+ new DropTable({
229
+ table: change.table,
230
+ externallyDroppedConstraints,
231
+ }),
232
+ );
233
+ }
234
+
235
+ return normalizedChanges;
236
+ }