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

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 (228) hide show
  1. package/dist/core/catalog.model.js +1 -0
  2. package/dist/core/integrations/filter/flatten.js +13 -0
  3. package/dist/core/objects/aggregate/aggregate.diff.js +16 -0
  4. package/dist/core/objects/aggregate/aggregate.model.d.ts +10 -0
  5. package/dist/core/objects/aggregate/aggregate.model.js +19 -1
  6. package/dist/core/objects/aggregate/changes/aggregate.base.d.ts +1 -1
  7. package/dist/core/objects/aggregate/changes/aggregate.security-label.d.ts +28 -0
  8. package/dist/core/objects/aggregate/changes/aggregate.security-label.js +64 -0
  9. package/dist/core/objects/aggregate/changes/aggregate.types.d.ts +2 -1
  10. package/dist/core/objects/base.model.d.ts +8 -0
  11. package/dist/core/objects/base.model.js +2 -0
  12. package/dist/core/objects/domain/changes/domain.base.d.ts +1 -1
  13. package/dist/core/objects/domain/changes/domain.security-label.d.ts +28 -0
  14. package/dist/core/objects/domain/changes/domain.security-label.js +61 -0
  15. package/dist/core/objects/domain/changes/domain.types.d.ts +2 -1
  16. package/dist/core/objects/domain/domain.diff.js +16 -0
  17. package/dist/core/objects/domain/domain.model.d.ts +10 -0
  18. package/dist/core/objects/domain/domain.model.js +19 -1
  19. package/dist/core/objects/event-trigger/changes/event-trigger.base.d.ts +1 -1
  20. package/dist/core/objects/event-trigger/changes/event-trigger.security-label.d.ts +28 -0
  21. package/dist/core/objects/event-trigger/changes/event-trigger.security-label.js +61 -0
  22. package/dist/core/objects/event-trigger/changes/event-trigger.types.d.ts +2 -1
  23. package/dist/core/objects/event-trigger/event-trigger.diff.js +16 -0
  24. package/dist/core/objects/event-trigger/event-trigger.model.d.ts +10 -0
  25. package/dist/core/objects/event-trigger/event-trigger.model.js +19 -1
  26. package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.base.d.ts +1 -1
  27. package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.security-label.d.ts +28 -0
  28. package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.security-label.js +61 -0
  29. package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.types.d.ts +2 -1
  30. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.js +16 -0
  31. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +22 -0
  32. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +20 -1
  33. package/dist/core/objects/materialized-view/changes/materialized-view.base.d.ts +1 -1
  34. package/dist/core/objects/materialized-view/changes/materialized-view.security-label.d.ts +28 -0
  35. package/dist/core/objects/materialized-view/changes/materialized-view.security-label.js +61 -0
  36. package/dist/core/objects/materialized-view/changes/materialized-view.types.d.ts +2 -1
  37. package/dist/core/objects/materialized-view/materialized-view.diff.js +18 -0
  38. package/dist/core/objects/materialized-view/materialized-view.model.d.ts +22 -0
  39. package/dist/core/objects/materialized-view/materialized-view.model.js +20 -1
  40. package/dist/core/objects/procedure/changes/procedure.base.d.ts +1 -1
  41. package/dist/core/objects/procedure/changes/procedure.security-label.d.ts +28 -0
  42. package/dist/core/objects/procedure/changes/procedure.security-label.js +69 -0
  43. package/dist/core/objects/procedure/changes/procedure.types.d.ts +2 -1
  44. package/dist/core/objects/procedure/procedure.diff.js +16 -0
  45. package/dist/core/objects/procedure/procedure.model.d.ts +10 -0
  46. package/dist/core/objects/procedure/procedure.model.js +19 -1
  47. package/dist/core/objects/publication/changes/publication.base.d.ts +1 -1
  48. package/dist/core/objects/publication/changes/publication.security-label.d.ts +28 -0
  49. package/dist/core/objects/publication/changes/publication.security-label.js +61 -0
  50. package/dist/core/objects/publication/changes/publication.types.d.ts +2 -1
  51. package/dist/core/objects/publication/publication.diff.js +16 -0
  52. package/dist/core/objects/publication/publication.model.d.ts +14 -0
  53. package/dist/core/objects/publication/publication.model.js +20 -1
  54. package/dist/core/objects/role/changes/role.base.d.ts +1 -1
  55. package/dist/core/objects/role/changes/role.security-label.d.ts +28 -0
  56. package/dist/core/objects/role/changes/role.security-label.js +61 -0
  57. package/dist/core/objects/role/changes/role.types.d.ts +2 -1
  58. package/dist/core/objects/role/role.diff.js +16 -0
  59. package/dist/core/objects/role/role.model.d.ts +10 -0
  60. package/dist/core/objects/role/role.model.js +29 -0
  61. package/dist/core/objects/schema/changes/schema.base.d.ts +1 -1
  62. package/dist/core/objects/schema/changes/schema.security-label.d.ts +28 -0
  63. package/dist/core/objects/schema/changes/schema.security-label.js +61 -0
  64. package/dist/core/objects/schema/changes/schema.types.d.ts +2 -1
  65. package/dist/core/objects/schema/schema.diff.js +24 -1
  66. package/dist/core/objects/schema/schema.model.d.ts +10 -0
  67. package/dist/core/objects/schema/schema.model.js +18 -1
  68. package/dist/core/objects/security-label.types.d.ts +20 -0
  69. package/dist/core/objects/security-label.types.js +46 -0
  70. package/dist/core/objects/sequence/changes/sequence.base.d.ts +1 -1
  71. package/dist/core/objects/sequence/changes/sequence.security-label.d.ts +28 -0
  72. package/dist/core/objects/sequence/changes/sequence.security-label.js +61 -0
  73. package/dist/core/objects/sequence/changes/sequence.types.d.ts +2 -1
  74. package/dist/core/objects/sequence/sequence.diff.js +16 -0
  75. package/dist/core/objects/sequence/sequence.model.d.ts +10 -0
  76. package/dist/core/objects/sequence/sequence.model.js +19 -1
  77. package/dist/core/objects/subscription/changes/subscription.base.d.ts +1 -1
  78. package/dist/core/objects/subscription/changes/subscription.security-label.d.ts +28 -0
  79. package/dist/core/objects/subscription/changes/subscription.security-label.js +61 -0
  80. package/dist/core/objects/subscription/changes/subscription.types.d.ts +2 -1
  81. package/dist/core/objects/subscription/subscription.diff.js +16 -0
  82. package/dist/core/objects/subscription/subscription.model.d.ts +10 -0
  83. package/dist/core/objects/subscription/subscription.model.js +19 -1
  84. package/dist/core/objects/table/changes/table.base.d.ts +1 -1
  85. package/dist/core/objects/table/changes/table.security-label.d.ts +63 -0
  86. package/dist/core/objects/table/changes/table.security-label.js +134 -0
  87. package/dist/core/objects/table/changes/table.types.d.ts +2 -1
  88. package/dist/core/objects/table/table.diff.js +49 -0
  89. package/dist/core/objects/table/table.model.d.ts +30 -0
  90. package/dist/core/objects/table/table.model.js +34 -2
  91. package/dist/core/objects/type/composite-type/changes/composite-type.base.d.ts +1 -1
  92. package/dist/core/objects/type/composite-type/changes/composite-type.security-label.d.ts +28 -0
  93. package/dist/core/objects/type/composite-type/changes/composite-type.security-label.js +61 -0
  94. package/dist/core/objects/type/composite-type/changes/composite-type.types.d.ts +2 -1
  95. package/dist/core/objects/type/composite-type/composite-type.diff.js +16 -0
  96. package/dist/core/objects/type/composite-type/composite-type.model.d.ts +22 -0
  97. package/dist/core/objects/type/composite-type/composite-type.model.js +22 -2
  98. package/dist/core/objects/type/enum/changes/enum.base.d.ts +1 -1
  99. package/dist/core/objects/type/enum/changes/enum.security-label.d.ts +28 -0
  100. package/dist/core/objects/type/enum/changes/enum.security-label.js +61 -0
  101. package/dist/core/objects/type/enum/changes/enum.types.d.ts +2 -1
  102. package/dist/core/objects/type/enum/enum.diff.js +16 -0
  103. package/dist/core/objects/type/enum/enum.model.d.ts +10 -0
  104. package/dist/core/objects/type/enum/enum.model.js +20 -1
  105. package/dist/core/objects/type/range/changes/range.base.d.ts +1 -1
  106. package/dist/core/objects/type/range/changes/range.security-label.d.ts +28 -0
  107. package/dist/core/objects/type/range/changes/range.security-label.js +61 -0
  108. package/dist/core/objects/type/range/changes/range.types.d.ts +2 -1
  109. package/dist/core/objects/type/range/range.diff.js +16 -0
  110. package/dist/core/objects/type/range/range.model.d.ts +10 -0
  111. package/dist/core/objects/type/range/range.model.js +19 -1
  112. package/dist/core/objects/utils.d.ts +1 -0
  113. package/dist/core/objects/utils.js +3 -0
  114. package/dist/core/objects/view/changes/view.base.d.ts +1 -1
  115. package/dist/core/objects/view/changes/view.security-label.d.ts +28 -0
  116. package/dist/core/objects/view/changes/view.security-label.js +61 -0
  117. package/dist/core/objects/view/changes/view.types.d.ts +2 -1
  118. package/dist/core/objects/view/view.diff.js +13 -0
  119. package/dist/core/objects/view/view.model.d.ts +26 -0
  120. package/dist/core/objects/view/view.model.js +20 -1
  121. package/dist/core/plan/sql-format/fixtures.js +1 -0
  122. package/dist/core/post-diff-normalization.d.ts +7 -0
  123. package/dist/core/post-diff-normalization.js +33 -4
  124. package/dist/core/sort/cycle-breakers.js +139 -17
  125. package/package.json +1 -1
  126. package/src/core/catalog.model.ts +1 -0
  127. package/src/core/integrations/filter/dsl.test.ts +27 -0
  128. package/src/core/integrations/filter/flatten.ts +16 -0
  129. package/src/core/objects/aggregate/aggregate.diff.ts +33 -0
  130. package/src/core/objects/aggregate/aggregate.model.ts +22 -1
  131. package/src/core/objects/aggregate/changes/aggregate.base.ts +5 -1
  132. package/src/core/objects/aggregate/changes/aggregate.security-label.ts +99 -0
  133. package/src/core/objects/aggregate/changes/aggregate.types.ts +3 -1
  134. package/src/core/objects/base.model.ts +2 -0
  135. package/src/core/objects/domain/changes/domain.base.ts +5 -1
  136. package/src/core/objects/domain/changes/domain.security-label.test.ts +56 -0
  137. package/src/core/objects/domain/changes/domain.security-label.ts +77 -0
  138. package/src/core/objects/domain/changes/domain.types.ts +3 -1
  139. package/src/core/objects/domain/domain.diff.ts +33 -0
  140. package/src/core/objects/domain/domain.model.ts +22 -1
  141. package/src/core/objects/event-trigger/changes/event-trigger.base.ts +1 -1
  142. package/src/core/objects/event-trigger/changes/event-trigger.security-label.ts +95 -0
  143. package/src/core/objects/event-trigger/changes/event-trigger.types.ts +3 -1
  144. package/src/core/objects/event-trigger/event-trigger.diff.ts +33 -0
  145. package/src/core/objects/event-trigger/event-trigger.model.ts +22 -1
  146. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.base.ts +5 -1
  147. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.security-label.ts +95 -0
  148. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.types.ts +3 -1
  149. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.ts +33 -0
  150. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +24 -1
  151. package/src/core/objects/materialized-view/changes/materialized-view.base.ts +5 -1
  152. package/src/core/objects/materialized-view/changes/materialized-view.security-label.test.ts +63 -0
  153. package/src/core/objects/materialized-view/changes/materialized-view.security-label.ts +95 -0
  154. package/src/core/objects/materialized-view/changes/materialized-view.types.ts +3 -1
  155. package/src/core/objects/materialized-view/materialized-view.diff.ts +37 -0
  156. package/src/core/objects/materialized-view/materialized-view.model.ts +25 -4
  157. package/src/core/objects/procedure/changes/procedure.base.ts +5 -1
  158. package/src/core/objects/procedure/changes/procedure.security-label.ts +105 -0
  159. package/src/core/objects/procedure/changes/procedure.types.ts +3 -1
  160. package/src/core/objects/procedure/procedure.diff.ts +33 -0
  161. package/src/core/objects/procedure/procedure.model.ts +23 -2
  162. package/src/core/objects/publication/changes/publication.base.ts +1 -1
  163. package/src/core/objects/publication/changes/publication.security-label.ts +95 -0
  164. package/src/core/objects/publication/changes/publication.types.ts +3 -1
  165. package/src/core/objects/publication/publication.diff.ts +33 -0
  166. package/src/core/objects/publication/publication.model.ts +24 -1
  167. package/src/core/objects/role/changes/role.base.ts +2 -1
  168. package/src/core/objects/role/changes/role.security-label.ts +77 -0
  169. package/src/core/objects/role/changes/role.types.ts +3 -1
  170. package/src/core/objects/role/role.diff.ts +33 -0
  171. package/src/core/objects/role/role.model.ts +32 -0
  172. package/src/core/objects/schema/changes/schema.alter.test.ts +1 -0
  173. package/src/core/objects/schema/changes/schema.base.ts +5 -1
  174. package/src/core/objects/schema/changes/schema.create.test.ts +1 -0
  175. package/src/core/objects/schema/changes/schema.drop.test.ts +1 -0
  176. package/src/core/objects/schema/changes/schema.security-label.test.ts +76 -0
  177. package/src/core/objects/schema/changes/schema.security-label.ts +77 -0
  178. package/src/core/objects/schema/changes/schema.types.ts +3 -1
  179. package/src/core/objects/schema/schema.diff.test.ts +1 -0
  180. package/src/core/objects/schema/schema.diff.ts +43 -1
  181. package/src/core/objects/schema/schema.model.ts +21 -1
  182. package/src/core/objects/security-label.types.test.ts +106 -0
  183. package/src/core/objects/security-label.types.ts +61 -0
  184. package/src/core/objects/sequence/changes/sequence.base.ts +5 -1
  185. package/src/core/objects/sequence/changes/sequence.security-label.test.ts +58 -0
  186. package/src/core/objects/sequence/changes/sequence.security-label.ts +92 -0
  187. package/src/core/objects/sequence/changes/sequence.types.ts +3 -1
  188. package/src/core/objects/sequence/sequence.diff.ts +33 -0
  189. package/src/core/objects/sequence/sequence.model.ts +22 -1
  190. package/src/core/objects/subscription/changes/subscription.base.ts +1 -1
  191. package/src/core/objects/subscription/changes/subscription.security-label.ts +95 -0
  192. package/src/core/objects/subscription/changes/subscription.types.ts +3 -1
  193. package/src/core/objects/subscription/subscription.diff.ts +33 -0
  194. package/src/core/objects/subscription/subscription.model.ts +22 -1
  195. package/src/core/objects/table/changes/table.base.ts +5 -1
  196. package/src/core/objects/table/changes/table.security-label.test.ts +140 -0
  197. package/src/core/objects/table/changes/table.security-label.ts +183 -0
  198. package/src/core/objects/table/changes/table.types.ts +3 -1
  199. package/src/core/objects/table/table.diff.ts +87 -0
  200. package/src/core/objects/table/table.model.ts +42 -2
  201. package/src/core/objects/type/composite-type/changes/composite-type.base.ts +5 -1
  202. package/src/core/objects/type/composite-type/changes/composite-type.security-label.ts +95 -0
  203. package/src/core/objects/type/composite-type/changes/composite-type.types.ts +3 -1
  204. package/src/core/objects/type/composite-type/composite-type.diff.ts +33 -0
  205. package/src/core/objects/type/composite-type/composite-type.model.ts +26 -2
  206. package/src/core/objects/type/enum/changes/enum.base.ts +5 -1
  207. package/src/core/objects/type/enum/changes/enum.security-label.ts +77 -0
  208. package/src/core/objects/type/enum/changes/enum.types.ts +3 -1
  209. package/src/core/objects/type/enum/enum.diff.ts +33 -0
  210. package/src/core/objects/type/enum/enum.model.ts +25 -1
  211. package/src/core/objects/type/range/changes/range.base.ts +5 -1
  212. package/src/core/objects/type/range/changes/range.security-label.ts +77 -0
  213. package/src/core/objects/type/range/changes/range.types.ts +3 -1
  214. package/src/core/objects/type/range/range.diff.ts +33 -0
  215. package/src/core/objects/type/range/range.model.ts +22 -1
  216. package/src/core/objects/utils.ts +3 -0
  217. package/src/core/objects/view/changes/view.base.ts +5 -1
  218. package/src/core/objects/view/changes/view.security-label.test.ts +64 -0
  219. package/src/core/objects/view/changes/view.security-label.ts +77 -0
  220. package/src/core/objects/view/changes/view.types.ts +3 -1
  221. package/src/core/objects/view/view.diff.ts +31 -0
  222. package/src/core/objects/view/view.model.ts +25 -2
  223. package/src/core/plan/sql-format/fixtures.ts +1 -0
  224. package/src/core/post-diff-normalization.test.ts +123 -0
  225. package/src/core/post-diff-normalization.ts +40 -4
  226. package/src/core/sort/cycle-breakers.test.ts +236 -2
  227. package/src/core/sort/cycle-breakers.ts +184 -24
  228. package/src/core/sort/sort-changes.test.ts +317 -0
@@ -78,6 +78,35 @@ export function tryBreakCycleByChangeInjection(
78
78
  );
79
79
  if (pubColBroken) return pubColBroken;
80
80
 
81
+ // ─── Branch C: Publication ↔ dropped FK chain ↔ constraint drop ──────
82
+ // Triggered when publication membership is being removed for tables in
83
+ // the same drop phase as a FK chain, and the chain ends at a separately
84
+ // emitted `AlterTableDropConstraint` on a table that is also being
85
+ // removed from the publication.
86
+ //
87
+ // Example (4-change cycle):
88
+ // AlterPublicationDropTables(p, [labs, posts, post_attachments])
89
+ // DropTable(post_attachments)
90
+ // DropTable(posts)
91
+ // AlterTableDropConstraint(labs.unique_lab_id)
92
+ //
93
+ // Cycle:
94
+ // publication:p → table:post_attachments
95
+ // post_attachments.post_id_fkey → column:posts.id
96
+ // posts.lab_id_fkey → constraint:labs.unique_lab_id
97
+ // constraint:labs.unique_lab_id → table:labs
98
+ //
99
+ // Fix: inject explicit FK drops for the FK constraints claimed by the
100
+ // DropTables in the cycle, including FKs that point at the terminal
101
+ // dropped constraint. The publication and terminal constraint changes
102
+ // stay unchanged; only the intermediate FK ownership is reassigned from
103
+ // DropTable to dedicated AlterTableDropConstraint changes.
104
+ const pubFkConstraintBroken = tryBreakPublicationFkConstraintDropCycle(
105
+ cycleNodeIndexes,
106
+ phaseChanges,
107
+ );
108
+ if (pubFkConstraintBroken) return pubFkConstraintBroken;
109
+
81
110
  // No known pattern. Returning null lets sortPhaseChanges throw the
82
111
  // formatted CycleError with full diagnostic — better a clear bug
83
112
  // report than silently shipping a broken plan.
@@ -109,6 +138,34 @@ function tryBreakFkCycle(
109
138
  cycleDropTables.map((change) => change.table.stableId),
110
139
  );
111
140
 
141
+ return injectFkConstraintDropsForDropTables({
142
+ phaseChanges,
143
+ dropTables: cycleDropTables,
144
+ shouldInject: (fk, tableId) =>
145
+ isCrossCycleFkConstraint(fk, tableId, cycleTableIds),
146
+ });
147
+ }
148
+
149
+ type FkConstraintPredicate = (
150
+ fk: TableConstraintProps,
151
+ tableId: string,
152
+ ) => boolean;
153
+
154
+ /**
155
+ * Shared FK-drop injection used by Branch A and Branch C. The caller owns
156
+ * the cycle-specific matcher; this helper only handles the mechanical
157
+ * rewrite: add dedicated `AlterTableDropConstraint` changes and rebuild
158
+ * affected `DropTable`s with updated `externallyDroppedConstraints`.
159
+ */
160
+ function injectFkConstraintDropsForDropTables({
161
+ phaseChanges,
162
+ dropTables,
163
+ shouldInject,
164
+ }: {
165
+ phaseChanges: readonly Change[];
166
+ dropTables: readonly DropTable[];
167
+ shouldInject: FkConstraintPredicate;
168
+ }): Change[] | null {
112
169
  // For each DropTable in the cycle, find every FK whose referenced table
113
170
  // is also in the cycle. Each such FK becomes one injected
114
171
  // `AlterTableDropConstraint` and one entry on the source table's
@@ -120,16 +177,14 @@ function tryBreakFkCycle(
120
177
  const updatedExternalsByTableId = new Map<string, Set<string>>();
121
178
  let didMutate = false;
122
179
 
123
- for (const dropTable of cycleDropTables) {
180
+ for (const dropTable of dropTables) {
124
181
  const tableId = dropTable.table.stableId;
125
182
  const existingExternals = new Set(dropTable.externallyDroppedConstraints);
126
183
  let tableMutated = false;
127
184
 
128
- for (const fk of iterCrossCycleFkConstraints(
129
- dropTable.table.constraints,
130
- tableId,
131
- cycleTableIds,
132
- )) {
185
+ for (const fk of iterFkConstraints(dropTable.table.constraints)) {
186
+ if (!shouldInject(fk, tableId)) continue;
187
+
133
188
  // Skip if a same-table `AlterTableDropConstraint` is already in the
134
189
  // change list — could happen if a previous breaker iteration
135
190
  // injected one, or the diff layer emitted one explicitly.
@@ -187,35 +242,45 @@ function tryBreakFkCycle(
187
242
  }
188
243
 
189
244
  /**
190
- * Yield FK constraints on `constraints` whose referenced table is also a
191
- * member of the cycle (i.e. an FK strictly between two cycle DropTables).
245
+ * Yield FK constraints on `constraints`.
192
246
  *
193
- * Self-referencing FKs are skipped they create a self-loop in the
194
- * dependency graph which the existing sort-phase handler resolves on its
195
- * own; injecting an `AlterTableDropConstraint` for a self-FK would just
196
- * add noise.
247
+ * Partition clones are skipped because PostgreSQL drops them when the
248
+ * parent constraint is dropped.
197
249
  */
198
- function* iterCrossCycleFkConstraints(
250
+ function* iterFkConstraints(
199
251
  constraints: readonly TableConstraintProps[],
200
- ownTableId: string,
201
- cycleTableIds: ReadonlySet<string>,
202
252
  ): Iterable<TableConstraintProps> {
203
253
  for (const constraint of constraints) {
204
254
  if (constraint.constraint_type !== "f") continue;
205
255
  if (constraint.is_partition_clone) continue;
206
- if (!constraint.foreign_key_schema || !constraint.foreign_key_table) {
207
- continue;
208
- }
209
- const referencedId = stableId.table(
210
- constraint.foreign_key_schema,
211
- constraint.foreign_key_table,
212
- );
213
- if (referencedId === ownTableId) continue;
214
- if (!cycleTableIds.has(referencedId)) continue;
215
256
  yield constraint;
216
257
  }
217
258
  }
218
259
 
260
+ /**
261
+ * True when `constraint` references another DropTable in the cycle.
262
+ *
263
+ * Self-referencing FKs are skipped — they create a self-loop in the
264
+ * dependency graph which the existing sort-phase handler resolves on its
265
+ * own; injecting an `AlterTableDropConstraint` for a self-FK would just
266
+ * add noise.
267
+ */
268
+ function isCrossCycleFkConstraint(
269
+ constraint: TableConstraintProps,
270
+ ownTableId: string,
271
+ cycleTableIds: ReadonlySet<string>,
272
+ ): boolean {
273
+ if (!constraint.foreign_key_schema || !constraint.foreign_key_table) {
274
+ return false;
275
+ }
276
+ const referencedId = stableId.table(
277
+ constraint.foreign_key_schema,
278
+ constraint.foreign_key_table,
279
+ );
280
+ if (referencedId === ownTableId) return false;
281
+ return cycleTableIds.has(referencedId);
282
+ }
283
+
219
284
  /**
220
285
  * True iff `phaseChanges` already contains an explicit
221
286
  * `AlterTableDropConstraint(table, constraint)` for the given pair —
@@ -309,3 +374,98 @@ function tryBreakPublicationColumnCycle(
309
374
  });
310
375
  return rewritten;
311
376
  }
377
+
378
+ /**
379
+ * Branch C worker — break a publication membership removal cycle where
380
+ * dropped tables form a FK chain ending at a separately dropped referenced
381
+ * constraint.
382
+ */
383
+ function tryBreakPublicationFkConstraintDropCycle(
384
+ cycleNodeIndexes: readonly number[],
385
+ phaseChanges: readonly Change[],
386
+ ): Change[] | null {
387
+ let pubChange: AlterPublicationDropTables | null = null;
388
+ let terminalConstraintDrop: AlterTableDropConstraint | null = null;
389
+ const dropTables: DropTable[] = [];
390
+
391
+ for (const nodeIndex of cycleNodeIndexes) {
392
+ const change = phaseChanges[nodeIndex];
393
+ if (change instanceof AlterPublicationDropTables) {
394
+ if (pubChange !== null) return null;
395
+ pubChange = change;
396
+ } else if (change instanceof AlterTableDropConstraint) {
397
+ if (terminalConstraintDrop !== null) return null;
398
+ terminalConstraintDrop = change;
399
+ } else if (change instanceof DropTable) {
400
+ dropTables.push(change);
401
+ } else {
402
+ return null;
403
+ }
404
+ }
405
+
406
+ if (
407
+ pubChange === null ||
408
+ terminalConstraintDrop === null ||
409
+ dropTables.length === 0
410
+ ) {
411
+ return null;
412
+ }
413
+
414
+ const publicationTableIds = new Set<string>(
415
+ pubChange.tables.map((table) => stableId.table(table.schema, table.name)),
416
+ );
417
+ if (!publicationTableIds.has(terminalConstraintDrop.table.stableId)) {
418
+ return null;
419
+ }
420
+
421
+ for (const dropTable of dropTables) {
422
+ if (!publicationTableIds.has(dropTable.table.stableId)) return null;
423
+ }
424
+
425
+ const cycleDropTableIds = new Set(
426
+ dropTables.map((change) => change.table.stableId),
427
+ );
428
+ let hasFkToTerminalConstraint = false;
429
+
430
+ for (const dropTable of dropTables) {
431
+ for (const fk of iterFkConstraints(dropTable.table.constraints)) {
432
+ if (fkReferencesConstraint(fk, terminalConstraintDrop)) {
433
+ hasFkToTerminalConstraint = true;
434
+ break;
435
+ }
436
+ }
437
+ if (hasFkToTerminalConstraint) break;
438
+ }
439
+ if (!hasFkToTerminalConstraint) return null;
440
+
441
+ return injectFkConstraintDropsForDropTables({
442
+ phaseChanges,
443
+ dropTables,
444
+ shouldInject: (fk, tableId) =>
445
+ isCrossCycleFkConstraint(fk, tableId, cycleDropTableIds) ||
446
+ fkReferencesConstraint(fk, terminalConstraintDrop),
447
+ });
448
+ }
449
+
450
+ function fkReferencesConstraint(
451
+ fk: TableConstraintProps,
452
+ constraintDrop: AlterTableDropConstraint,
453
+ ): boolean {
454
+ if (
455
+ fk.foreign_key_schema !== constraintDrop.table.schema ||
456
+ fk.foreign_key_table !== constraintDrop.table.name ||
457
+ fk.foreign_key_columns === null
458
+ ) {
459
+ return false;
460
+ }
461
+
462
+ return sameOrderedStrings(
463
+ fk.foreign_key_columns,
464
+ constraintDrop.constraint.key_columns,
465
+ );
466
+ }
467
+
468
+ function sameOrderedStrings(left: readonly string[], right: readonly string[]) {
469
+ if (left.length !== right.length) return false;
470
+ return left.every((value, index) => value === right[index]);
471
+ }
@@ -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 type { PgDepend } from "../depend.ts";
5
+ import { AlterPublicationDropTables } from "../objects/publication/changes/publication.alter.ts";
6
+ import { Publication } from "../objects/publication/publication.model.ts";
7
+ import { AlterTableDropConstraint } from "../objects/table/changes/table.alter.ts";
8
+ import { DropTable } from "../objects/table/changes/table.drop.ts";
9
+ import { Table } from "../objects/table/table.model.ts";
10
+ import { sortChanges } from "./sort-changes.ts";
11
+
12
+ const baseTableProps = {
13
+ schema: "public",
14
+ persistence: "p" as const,
15
+ row_security: false,
16
+ force_row_security: false,
17
+ has_indexes: false,
18
+ has_rules: false,
19
+ has_triggers: false,
20
+ has_subclasses: false,
21
+ is_populated: true,
22
+ replica_identity: "d" as const,
23
+ is_partition: false,
24
+ options: null,
25
+ partition_bound: null,
26
+ partition_by: null,
27
+ owner: "postgres",
28
+ comment: null,
29
+ parent_schema: null,
30
+ parent_name: null,
31
+ privileges: [],
32
+ };
33
+
34
+ function integerColumn(name: string, position: number) {
35
+ return {
36
+ name,
37
+ position,
38
+ data_type: "integer",
39
+ data_type_str: "integer",
40
+ is_custom_type: false,
41
+ custom_type_type: null,
42
+ custom_type_category: null,
43
+ custom_type_schema: null,
44
+ custom_type_name: null,
45
+ not_null: false,
46
+ is_identity: false,
47
+ is_identity_always: false,
48
+ is_generated: false,
49
+ collation: null,
50
+ default: null,
51
+ comment: null,
52
+ };
53
+ }
54
+
55
+ function fkConstraint(props: {
56
+ name: string;
57
+ fkColumn: string;
58
+ targetTable: string;
59
+ targetColumn?: string;
60
+ }) {
61
+ const targetColumn = props.targetColumn ?? "id";
62
+ return {
63
+ name: props.name,
64
+ constraint_type: "f" as const,
65
+ deferrable: false,
66
+ initially_deferred: false,
67
+ validated: true,
68
+ is_local: true,
69
+ no_inherit: false,
70
+ is_temporal: false,
71
+ is_partition_clone: false,
72
+ parent_constraint_schema: null,
73
+ parent_constraint_name: null,
74
+ parent_table_schema: null,
75
+ parent_table_name: null,
76
+ key_columns: [props.fkColumn],
77
+ foreign_key_columns: [targetColumn],
78
+ foreign_key_table: props.targetTable,
79
+ foreign_key_schema: "public",
80
+ foreign_key_table_is_partition: false,
81
+ foreign_key_parent_schema: null,
82
+ foreign_key_parent_table: null,
83
+ foreign_key_effective_schema: "public",
84
+ foreign_key_effective_table: props.targetTable,
85
+ on_update: "a" as const,
86
+ on_delete: "a" as const,
87
+ match_type: "s" as const,
88
+ check_expression: null,
89
+ owner: "postgres",
90
+ definition: `FOREIGN KEY (${props.fkColumn}) REFERENCES public.${props.targetTable}(${targetColumn})`,
91
+ comment: null,
92
+ };
93
+ }
94
+
95
+ function uniqueConstraint(name: string, column: string) {
96
+ return {
97
+ name,
98
+ constraint_type: "u" as const,
99
+ deferrable: false,
100
+ initially_deferred: false,
101
+ validated: true,
102
+ is_local: true,
103
+ no_inherit: false,
104
+ is_temporal: false,
105
+ is_partition_clone: false,
106
+ parent_constraint_schema: null,
107
+ parent_constraint_name: null,
108
+ parent_table_schema: null,
109
+ parent_table_name: null,
110
+ key_columns: [column],
111
+ foreign_key_columns: null,
112
+ foreign_key_table: null,
113
+ foreign_key_schema: null,
114
+ foreign_key_table_is_partition: null,
115
+ foreign_key_parent_schema: null,
116
+ foreign_key_parent_table: null,
117
+ foreign_key_effective_schema: null,
118
+ foreign_key_effective_table: null,
119
+ on_update: null,
120
+ on_delete: null,
121
+ match_type: null,
122
+ check_expression: null,
123
+ owner: "postgres",
124
+ definition: `UNIQUE (${column})`,
125
+ comment: null,
126
+ };
127
+ }
128
+
129
+ function table(
130
+ name: string,
131
+ constraints: ConstructorParameters<typeof Table>[0]["constraints"] = [],
132
+ ) {
133
+ return new Table({
134
+ ...baseTableProps,
135
+ name,
136
+ columns: [
137
+ { ...integerColumn("id", 1), not_null: true },
138
+ integerColumn("post_id", 2),
139
+ integerColumn("lab_id", 3),
140
+ ],
141
+ constraints,
142
+ });
143
+ }
144
+
145
+ async function catalogWithDepends(depends: PgDepend[]) {
146
+ const base = await createEmptyCatalog(170000, "postgres");
147
+ return new Catalog({ ...base, depends });
148
+ }
149
+
150
+ function changeLabel(change: Change) {
151
+ if (change instanceof AlterTableDropConstraint) {
152
+ return `${change.constructor.name}:${change.table.name}.${change.constraint.name}`;
153
+ }
154
+ if (change instanceof DropTable) {
155
+ return `${change.constructor.name}:${change.table.name}`;
156
+ }
157
+ return change.constructor.name;
158
+ }
159
+
160
+ describe("sortChanges", () => {
161
+ test("breaks publication FK-chain constraint-drop cycle with one dropped table", async () => {
162
+ const labs = table("labs", [uniqueConstraint("unique_lab_id", "id")]);
163
+ const posts = table("posts", [
164
+ fkConstraint({
165
+ name: "posts_lab_id_fkey",
166
+ fkColumn: "lab_id",
167
+ targetTable: "labs",
168
+ }),
169
+ ]);
170
+ const publication = new Publication({
171
+ name: "supabase_realtime",
172
+ owner: "postgres",
173
+ comment: null,
174
+ all_tables: false,
175
+ publish_insert: true,
176
+ publish_update: true,
177
+ publish_delete: true,
178
+ publish_truncate: true,
179
+ publish_via_partition_root: false,
180
+ tables: [
181
+ {
182
+ schema: "public",
183
+ name: "labs",
184
+ columns: null,
185
+ row_filter: null,
186
+ },
187
+ {
188
+ schema: "public",
189
+ name: "posts",
190
+ columns: null,
191
+ row_filter: null,
192
+ },
193
+ ],
194
+ schemas: [],
195
+ });
196
+ const changes: Change[] = [
197
+ new AlterPublicationDropTables({
198
+ publication,
199
+ tables: publication.tables,
200
+ }),
201
+ new DropTable({ table: posts }),
202
+ new AlterTableDropConstraint({
203
+ table: labs,
204
+ constraint: labs.constraints[0],
205
+ }),
206
+ ];
207
+ const mainCatalog = await catalogWithDepends([
208
+ {
209
+ dependent_stable_id: "publication:supabase_realtime",
210
+ referenced_stable_id: "table:public.posts",
211
+ deptype: "n",
212
+ },
213
+ {
214
+ dependent_stable_id: "constraint:public.posts.posts_lab_id_fkey",
215
+ referenced_stable_id: "constraint:public.labs.unique_lab_id",
216
+ deptype: "n",
217
+ },
218
+ ]);
219
+ const branchCatalog = await catalogWithDepends([]);
220
+
221
+ const sorted = sortChanges({ mainCatalog, branchCatalog }, changes);
222
+
223
+ expect(sorted.map(changeLabel)).toContain(
224
+ "AlterTableDropConstraint:posts.posts_lab_id_fkey",
225
+ );
226
+ });
227
+
228
+ test("breaks publication FK-chain constraint-drop cycle in the drop phase", async () => {
229
+ const labs = table("labs", [uniqueConstraint("unique_lab_id", "id")]);
230
+ const posts = table("posts", [
231
+ fkConstraint({
232
+ name: "posts_lab_id_fkey",
233
+ fkColumn: "lab_id",
234
+ targetTable: "labs",
235
+ }),
236
+ ]);
237
+ const postAttachments = table("post_attachments", [
238
+ fkConstraint({
239
+ name: "post_attachments_post_id_fkey",
240
+ fkColumn: "post_id",
241
+ targetTable: "posts",
242
+ }),
243
+ ]);
244
+ const publication = new Publication({
245
+ name: "supabase_realtime",
246
+ owner: "postgres",
247
+ comment: null,
248
+ all_tables: false,
249
+ publish_insert: true,
250
+ publish_update: true,
251
+ publish_delete: true,
252
+ publish_truncate: true,
253
+ publish_via_partition_root: false,
254
+ tables: [
255
+ {
256
+ schema: "public",
257
+ name: "labs",
258
+ columns: null,
259
+ row_filter: null,
260
+ },
261
+ {
262
+ schema: "public",
263
+ name: "post_attachments",
264
+ columns: null,
265
+ row_filter: null,
266
+ },
267
+ {
268
+ schema: "public",
269
+ name: "posts",
270
+ columns: null,
271
+ row_filter: null,
272
+ },
273
+ ],
274
+ schemas: [],
275
+ });
276
+ const changes: Change[] = [
277
+ new AlterPublicationDropTables({
278
+ publication,
279
+ tables: publication.tables,
280
+ }),
281
+ new DropTable({ table: postAttachments }),
282
+ new DropTable({ table: posts }),
283
+ new AlterTableDropConstraint({
284
+ table: labs,
285
+ constraint: labs.constraints[0],
286
+ }),
287
+ ];
288
+ const mainCatalog = await catalogWithDepends([
289
+ {
290
+ dependent_stable_id: "publication:supabase_realtime",
291
+ referenced_stable_id: "table:public.post_attachments",
292
+ deptype: "n",
293
+ },
294
+ {
295
+ dependent_stable_id:
296
+ "constraint:public.post_attachments.post_attachments_post_id_fkey",
297
+ referenced_stable_id: "column:public.posts.id",
298
+ deptype: "n",
299
+ },
300
+ {
301
+ dependent_stable_id: "constraint:public.posts.posts_lab_id_fkey",
302
+ referenced_stable_id: "constraint:public.labs.unique_lab_id",
303
+ deptype: "n",
304
+ },
305
+ ]);
306
+ const branchCatalog = await catalogWithDepends([]);
307
+
308
+ const sorted = sortChanges({ mainCatalog, branchCatalog }, changes);
309
+
310
+ expect(sorted.map(changeLabel)).toContain(
311
+ "AlterTableDropConstraint:post_attachments.post_attachments_post_id_fkey",
312
+ );
313
+ expect(sorted.map(changeLabel)).toContain(
314
+ "AlterTableDropConstraint:posts.posts_lab_id_fkey",
315
+ );
316
+ });
317
+ });