@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.
- package/README.md +7 -1
- package/dist/core/catalog.diff.js +7 -1
- package/dist/core/connection-url.d.ts +32 -0
- package/dist/core/connection-url.js +77 -0
- package/dist/core/expand-replace-dependencies.d.ts +8 -2
- package/dist/core/expand-replace-dependencies.js +24 -10
- package/dist/core/integrations/supabase.js +1 -0
- package/dist/core/objects/procedure/procedure.diff.js +8 -0
- package/dist/core/objects/sequence/sequence.diff.js +14 -6
- package/dist/core/objects/table/changes/table.alter.js +4 -1
- 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/objects/table/table.diff.js +7 -2
- 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 +27 -0
- package/dist/core/postgres-config.js +99 -7
- package/package.json +2 -1
- package/src/core/catalog.diff.ts +7 -1
- package/src/core/connection-url.test.ts +142 -0
- package/src/core/connection-url.ts +82 -0
- package/src/core/expand-replace-dependencies.test.ts +247 -8
- package/src/core/expand-replace-dependencies.ts +33 -5
- package/src/core/integrations/supabase.ts +1 -0
- package/src/core/objects/procedure/procedure.diff.test.ts +25 -0
- package/src/core/objects/procedure/procedure.diff.ts +12 -0
- 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.alter.test.ts +14 -0
- package/src/core/objects/table/changes/table.alter.ts +4 -1
- package/src/core/objects/table/changes/table.drop.ts +27 -4
- package/src/core/objects/table/table.diff.test.ts +55 -0
- package/src/core/objects/table/table.diff.ts +10 -2
- 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 +241 -0
- 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: {
|
|
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,
|
|
@@ -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 (
|
|
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
|
+
}
|