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

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 (81) hide show
  1. package/dist/core/catalog.diff.js +4 -4
  2. package/dist/core/catalog.model.d.ts +8 -1
  3. package/dist/core/catalog.model.js +9 -8
  4. package/dist/core/expand-replace-dependencies.js +23 -0
  5. package/dist/core/objects/extract-with-retry.d.ts +36 -0
  6. package/dist/core/objects/extract-with-retry.js +51 -0
  7. package/dist/core/objects/index/index.diff.js +0 -1
  8. package/dist/core/objects/index/index.model.d.ts +2 -3
  9. package/dist/core/objects/index/index.model.js +17 -6
  10. package/dist/core/objects/materialized-view/materialized-view.model.d.ts +2 -1
  11. package/dist/core/objects/materialized-view/materialized-view.model.js +20 -4
  12. package/dist/core/objects/procedure/procedure.model.d.ts +2 -1
  13. package/dist/core/objects/procedure/procedure.model.js +20 -4
  14. package/dist/core/objects/publication/changes/publication.alter.d.ts +1 -1
  15. package/dist/core/objects/rls-policy/rls-policy.diff.js +13 -1
  16. package/dist/core/objects/rule/rule.model.d.ts +2 -1
  17. package/dist/core/objects/rule/rule.model.js +20 -3
  18. package/dist/core/objects/sequence/sequence.diff.d.ts +2 -1
  19. package/dist/core/objects/sequence/sequence.diff.js +41 -9
  20. package/dist/core/objects/table/changes/table.alter.d.ts +16 -1
  21. package/dist/core/objects/table/changes/table.alter.js +39 -6
  22. package/dist/core/objects/table/table.diff.js +40 -17
  23. package/dist/core/objects/table/table.model.d.ts +6 -1
  24. package/dist/core/objects/table/table.model.js +50 -12
  25. package/dist/core/objects/trigger/trigger.model.d.ts +2 -1
  26. package/dist/core/objects/trigger/trigger.model.js +20 -4
  27. package/dist/core/objects/utils.d.ts +1 -0
  28. package/dist/core/objects/utils.js +3 -0
  29. package/dist/core/objects/view/view.model.d.ts +2 -1
  30. package/dist/core/objects/view/view.model.js +20 -4
  31. package/dist/core/plan/create.js +3 -1
  32. package/dist/core/plan/types.d.ts +8 -0
  33. package/dist/core/post-diff-normalization.d.ts +36 -0
  34. package/dist/core/post-diff-normalization.js +202 -0
  35. package/dist/core/sort/cycle-breakers.d.ts +15 -0
  36. package/dist/core/sort/cycle-breakers.js +269 -0
  37. package/dist/core/sort/sort-changes.js +97 -43
  38. package/dist/core/sort/utils.d.ts +10 -0
  39. package/dist/core/sort/utils.js +28 -0
  40. package/package.json +1 -1
  41. package/src/core/catalog.diff.ts +4 -3
  42. package/src/core/catalog.model.ts +20 -8
  43. package/src/core/expand-replace-dependencies.test.ts +139 -5
  44. package/src/core/expand-replace-dependencies.ts +24 -0
  45. package/src/core/objects/extract-with-retry.test.ts +143 -0
  46. package/src/core/objects/extract-with-retry.ts +87 -0
  47. package/src/core/objects/index/index.diff.ts +0 -1
  48. package/src/core/objects/index/index.model.test.ts +37 -1
  49. package/src/core/objects/index/index.model.ts +25 -6
  50. package/src/core/objects/materialized-view/materialized-view.model.test.ts +93 -0
  51. package/src/core/objects/materialized-view/materialized-view.model.ts +27 -4
  52. package/src/core/objects/procedure/procedure.model.test.ts +117 -0
  53. package/src/core/objects/procedure/procedure.model.ts +28 -5
  54. package/src/core/objects/publication/changes/publication.alter.ts +1 -1
  55. package/src/core/objects/rls-policy/rls-policy.diff.ts +19 -1
  56. package/src/core/objects/rule/rule.model.test.ts +99 -0
  57. package/src/core/objects/rule/rule.model.ts +28 -4
  58. package/src/core/objects/sequence/sequence.diff.test.ts +93 -1
  59. package/src/core/objects/sequence/sequence.diff.ts +43 -10
  60. package/src/core/objects/table/changes/table.alter.test.ts +26 -23
  61. package/src/core/objects/table/changes/table.alter.ts +66 -10
  62. package/src/core/objects/table/table.diff.test.ts +43 -0
  63. package/src/core/objects/table/table.diff.ts +52 -23
  64. package/src/core/objects/table/table.model.test.ts +209 -0
  65. package/src/core/objects/table/table.model.ts +62 -14
  66. package/src/core/objects/trigger/trigger.model.test.ts +113 -0
  67. package/src/core/objects/trigger/trigger.model.ts +28 -5
  68. package/src/core/objects/utils.ts +3 -0
  69. package/src/core/objects/view/view.model.test.ts +90 -0
  70. package/src/core/objects/view/view.model.ts +28 -5
  71. package/src/core/plan/create.ts +3 -1
  72. package/src/core/plan/types.ts +8 -0
  73. package/src/core/{post-diff-cycle-breaking.test.ts → post-diff-normalization.test.ts} +168 -160
  74. package/src/core/post-diff-normalization.ts +260 -0
  75. package/src/core/sort/cycle-breakers.test.ts +476 -0
  76. package/src/core/sort/cycle-breakers.ts +311 -0
  77. package/src/core/sort/sort-changes.ts +135 -50
  78. package/src/core/sort/utils.ts +38 -0
  79. package/dist/core/post-diff-cycle-breaking.d.ts +0 -29
  80. package/dist/core/post-diff-cycle-breaking.js +0 -209
  81. package/src/core/post-diff-cycle-breaking.ts +0 -317
@@ -0,0 +1,260 @@
1
+ import type { Change } from "./change.types.ts";
2
+ import { CreateIndex } from "./objects/index/changes/index.create.ts";
3
+ import { DropIndex } from "./objects/index/changes/index.drop.ts";
4
+ import {
5
+ AlterTableAddConstraint,
6
+ AlterTableDropColumn,
7
+ AlterTableDropConstraint,
8
+ AlterTableSetReplicaIdentity,
9
+ AlterTableValidateConstraint,
10
+ } from "./objects/table/changes/table.alter.ts";
11
+ import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.ts";
12
+ import type { Table } from "./objects/table/table.model.ts";
13
+ import { stableId } from "./objects/utils.ts";
14
+
15
+ function constraintStableId(
16
+ table: { schema: string; name: string },
17
+ constraintName: string,
18
+ ) {
19
+ return stableId.constraint(table.schema, table.name, constraintName);
20
+ }
21
+
22
+ function isSupersededByTableReplacement(
23
+ change: Change,
24
+ replacedTableIds: ReadonlySet<string>,
25
+ ): boolean {
26
+ if (
27
+ !(change instanceof AlterTableDropColumn) &&
28
+ !(change instanceof AlterTableDropConstraint)
29
+ ) {
30
+ return false;
31
+ }
32
+ return replacedTableIds.has(change.table.stableId);
33
+ }
34
+
35
+ /**
36
+ * Drop earlier duplicates of `AlterTableAddConstraint` /
37
+ * `AlterTableValidateConstraint` / `CreateCommentOnConstraint` targeting
38
+ * replaced tables, keeping only the last occurrence of each
39
+ * `(changeType, table.stableId, constraint.name)`.
40
+ *
41
+ * When `expandReplaceDependencies()` promotes a table to a full
42
+ * `DropTable + CreateTable` pair, it also emits one
43
+ * `AlterTableAddConstraint` (plus optional `VALIDATE CONSTRAINT` /
44
+ * `COMMENT ON CONSTRAINT`) per branch constraint. If `diffTables()` already
45
+ * emitted the same change for a shape flip or a new constraint on that
46
+ * table, the plan ends up with two identical `ALTER TABLE ... ADD
47
+ * CONSTRAINT ...` statements and PostgreSQL fails at apply time with
48
+ * `constraint "..." for relation "..." already exists`. Because
49
+ * `expandReplaceDependencies()` appends its additions after the original
50
+ * `diffTables()` output, the last occurrence is the expansion's emission —
51
+ * keeping it preserves correctness while removing the duplicate.
52
+ */
53
+ function dropReplacedTableDuplicateConstraintChanges(
54
+ changes: Change[],
55
+ replacedTableIds: ReadonlySet<string>,
56
+ ): Change[] {
57
+ if (replacedTableIds.size === 0) return changes;
58
+
59
+ const keyFor = (change: Change): string | null => {
60
+ if (
61
+ !(change instanceof AlterTableAddConstraint) &&
62
+ !(change instanceof AlterTableValidateConstraint) &&
63
+ !(change instanceof CreateCommentOnConstraint)
64
+ ) {
65
+ return null;
66
+ }
67
+ if (!replacedTableIds.has(change.table.stableId)) return null;
68
+ const tag =
69
+ change instanceof AlterTableAddConstraint
70
+ ? "add"
71
+ : change instanceof AlterTableValidateConstraint
72
+ ? "validate"
73
+ : "comment";
74
+ return `${tag}:${constraintStableId(change.table, change.constraint.name)}`;
75
+ };
76
+
77
+ const seen = new Set<string>();
78
+ const reversedKept: Change[] = [];
79
+ let mutated = false;
80
+
81
+ // Walk backwards: the first encounter of each key corresponds to its LAST
82
+ // occurrence in the original order. `expandReplaceDependencies()` appends
83
+ // additions after the original changes, so "last wins" keeps the
84
+ // expansion's emission and drops the earlier diffTables duplicate.
85
+ for (let i = changes.length - 1; i >= 0; i--) {
86
+ const change = changes[i] as Change;
87
+ const key = keyFor(change);
88
+ if (key !== null) {
89
+ if (seen.has(key)) {
90
+ mutated = true;
91
+ continue;
92
+ }
93
+ seen.add(key);
94
+ }
95
+ reversedKept.push(change);
96
+ }
97
+
98
+ return mutated ? reversedKept.reverse() : changes;
99
+ }
100
+
101
+ /**
102
+ * Re-emit `ALTER TABLE ... REPLICA IDENTITY USING INDEX <idx>` after any
103
+ * `DropIndex(idx) + CreateIndex(idx)` pair where `idx` is the replica-identity
104
+ * index of a branch table.
105
+ *
106
+ * Background: PostgreSQL silently flips a table's `relreplident` to `'d'`
107
+ * (DEFAULT) when the index it points to is dropped. `CREATE INDEX` cannot
108
+ * restore the marker — only `ALTER TABLE ... REPLICA IDENTITY USING INDEX`
109
+ * can. When both main and branch carry `replica_identity = 'i'` pointing at
110
+ * the same index name, `diffTables()` emits no replica-identity change of its
111
+ * own, so the marker would be lost on apply.
112
+ *
113
+ * This is a whole-plan interaction: `diffTables()` cannot detect it without
114
+ * also looking at index changes. Per the "whole-plan interactions belong in
115
+ * post-diff normalization" rule in the package CLAUDE.md, the restoration
116
+ * lives here.
117
+ *
118
+ * Insertion is idempotent: if `diffTables()` already emitted the same
119
+ * `AlterTableSetReplicaIdentity` for this table (e.g. when the user is also
120
+ * switching the replica-identity index name in the same migration), no
121
+ * duplicate is added.
122
+ */
123
+ function restoreReplicaIdentityAfterIndexReplace(
124
+ changes: Change[],
125
+ branchTables: Record<string, Table>,
126
+ ): Change[] {
127
+ // Build the index-stable-id → owning-table map from branch state. Only
128
+ // tables in 'i' mode contribute, and only those whose configured index name
129
+ // is non-null (the extractor returns null for any other mode).
130
+ const replicaIdentityIndexToTable = new Map<string, Table>();
131
+ for (const table of Object.values(branchTables)) {
132
+ if (table.replica_identity !== "i" || !table.replica_identity_index) {
133
+ continue;
134
+ }
135
+ const indexId = stableId.index(
136
+ table.schema,
137
+ table.name,
138
+ table.replica_identity_index,
139
+ );
140
+ replicaIdentityIndexToTable.set(indexId, table);
141
+ }
142
+ if (replicaIdentityIndexToTable.size === 0) return changes;
143
+
144
+ // Find the indexes that are both dropped AND created in this plan. A pure
145
+ // drop or a pure create is handled by `diffTables()` directly (the table's
146
+ // replica_identity / replica_identity_index fields will have changed). The
147
+ // hole is specifically the drop+create pair that recreates the same name.
148
+ const droppedIndexIds = new Set<string>();
149
+ const createdIndexIds = new Set<string>();
150
+ for (const change of changes) {
151
+ if (change instanceof DropIndex) {
152
+ droppedIndexIds.add(change.index.stableId);
153
+ } else if (change instanceof CreateIndex) {
154
+ createdIndexIds.add(change.index.stableId);
155
+ }
156
+ }
157
+ const replacedIndexIds = new Set<string>();
158
+ for (const id of droppedIndexIds) {
159
+ if (createdIndexIds.has(id) && replicaIdentityIndexToTable.has(id)) {
160
+ replacedIndexIds.add(id);
161
+ }
162
+ }
163
+ if (replacedIndexIds.size === 0) return changes;
164
+
165
+ // Skip tables for which `diffTables()` already emitted a replica-identity
166
+ // setter — re-emitting would produce a redundant ALTER TABLE (harmless on
167
+ // apply, but noisy in plan output).
168
+ const tablesWithExistingReplicaIdentitySetter = new Set<string>();
169
+ for (const change of changes) {
170
+ if (change instanceof AlterTableSetReplicaIdentity) {
171
+ tablesWithExistingReplicaIdentitySetter.add(change.table.stableId);
172
+ }
173
+ }
174
+
175
+ // Insert one `AlterTableSetReplicaIdentity` per replaced index, immediately
176
+ // after the matching `CreateIndex`. The change's `requires` already names
177
+ // both the table and the recreated index, so the topo sort orders it
178
+ // correctly relative to the surrounding DDL.
179
+ const result: Change[] = [];
180
+ for (const change of changes) {
181
+ result.push(change);
182
+ if (
183
+ !(change instanceof CreateIndex) ||
184
+ !replacedIndexIds.has(change.index.stableId)
185
+ ) {
186
+ continue;
187
+ }
188
+ const table = replicaIdentityIndexToTable.get(change.index.stableId);
189
+ if (!table) continue;
190
+ if (tablesWithExistingReplicaIdentitySetter.has(table.stableId)) continue;
191
+
192
+ result.push(
193
+ new AlterTableSetReplicaIdentity({
194
+ table,
195
+ mode: "i",
196
+ indexName: table.replica_identity_index,
197
+ }),
198
+ );
199
+ // Mark as emitted so a second replaced index on the same table — if that
200
+ // ever arises — doesn't double-emit.
201
+ tablesWithExistingReplicaIdentitySetter.add(table.stableId);
202
+ }
203
+
204
+ return result;
205
+ }
206
+
207
+ /**
208
+ * Apply structural rewrites to the change list that are only obvious once
209
+ * every object diff has been collected. This pass does NOT prevent dependency
210
+ * cycles — that responsibility now lives in the sort phase, where
211
+ * `sortPhaseChanges` invokes `tryBreakCycleByChangeInjection` lazily on cycles
212
+ * that edge filtering can't break (FK SCC of dropped tables,
213
+ * AlterPublicationDropTables ↔ AlterTableDropColumn, …).
214
+ *
215
+ * Concretely, this pass:
216
+ *
217
+ * - Prunes `AlterTableDropColumn(T.*)` / `AlterTableDropConstraint(T.*)`
218
+ * changes that are made redundant by an expansion-emitted
219
+ * `DropTable(T) + CreateTable(T)` pair. Without this, the apply phase
220
+ * would try to drop a column that no longer exists in the freshly
221
+ * recreated table.
222
+ * - Dedupes duplicate `AlterTableAddConstraint` /
223
+ * `AlterTableValidateConstraint` / `CreateCommentOnConstraint` changes
224
+ * produced when `diffTables()` and `expandReplaceDependencies()` both
225
+ * emit the same constraint operation for a replaced table. Last write
226
+ * wins so the expansion's emission survives.
227
+ * - Re-emits `ALTER TABLE ... REPLICA IDENTITY USING INDEX <idx>` after any
228
+ * `DropIndex(idx) + CreateIndex(idx)` pair where `idx` is the replica
229
+ * identity index of a branch table — Postgres silently clears the marker
230
+ * when the underlying index is dropped, and `CREATE INDEX` cannot restore
231
+ * it.
232
+ *
233
+ * Object-local PostgreSQL semantics (for example owned-sequence cascades)
234
+ * stay in the corresponding `diff*` function instead of this pass.
235
+ */
236
+ export function normalizePostDiffChanges({
237
+ changes,
238
+ replacedTableIds = new Set<string>(),
239
+ branchTables = {},
240
+ }: {
241
+ changes: Change[];
242
+ replacedTableIds?: ReadonlySet<string>;
243
+ branchTables?: Record<string, Table>;
244
+ }): Change[] {
245
+ const restoredChanges = restoreReplicaIdentityAfterIndexReplace(
246
+ changes,
247
+ branchTables,
248
+ );
249
+
250
+ const dedupedChanges = dropReplacedTableDuplicateConstraintChanges(
251
+ restoredChanges,
252
+ replacedTableIds,
253
+ );
254
+
255
+ if (replacedTableIds.size === 0) return dedupedChanges;
256
+
257
+ return dedupedChanges.filter(
258
+ (change) => !isSupersededByTableReplacement(change, replacedTableIds),
259
+ );
260
+ }