@supabase/pg-delta 1.0.0-alpha.4 → 1.0.0-alpha.6

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 (359) hide show
  1. package/README.md +40 -23
  2. package/dist/cli/app.js +26 -3
  3. package/dist/cli/bin/cli.js +5 -0
  4. package/dist/cli/commands/catalog-export.d.ts +5 -0
  5. package/dist/cli/commands/catalog-export.js +64 -0
  6. package/dist/cli/commands/declarative-apply.d.ts +6 -0
  7. package/dist/cli/commands/declarative-apply.js +288 -0
  8. package/dist/cli/commands/declarative-export.d.ts +5 -0
  9. package/dist/cli/commands/declarative-export.js +245 -0
  10. package/dist/cli/commands/plan.js +19 -6
  11. package/dist/cli/exit-code.d.ts +2 -0
  12. package/dist/cli/exit-code.js +7 -0
  13. package/dist/cli/formatters/tree/tree.js +3 -2
  14. package/dist/cli/utils/apply-display.d.ts +52 -0
  15. package/dist/cli/utils/apply-display.js +183 -0
  16. package/dist/cli/utils/export-display.d.ts +43 -0
  17. package/dist/cli/utils/export-display.js +202 -0
  18. package/dist/cli/utils/resolve-input.d.ts +7 -0
  19. package/dist/cli/utils/resolve-input.js +13 -0
  20. package/dist/core/catalog-export/index.d.ts +11 -0
  21. package/dist/core/catalog-export/index.js +10 -0
  22. package/dist/core/catalog.diff.d.ts +1 -0
  23. package/dist/core/catalog.diff.js +64 -48
  24. package/dist/core/catalog.model.d.ts +14 -1
  25. package/dist/core/catalog.model.js +103 -1
  26. package/dist/core/catalog.snapshot.d.ts +66 -0
  27. package/dist/core/catalog.snapshot.js +206 -0
  28. package/dist/core/declarative-apply/discover-sql.d.ts +18 -0
  29. package/dist/core/declarative-apply/discover-sql.js +86 -0
  30. package/dist/core/declarative-apply/extract-catalog-providers.d.ts +23 -0
  31. package/dist/core/declarative-apply/extract-catalog-providers.js +159 -0
  32. package/dist/core/declarative-apply/index.d.ts +49 -0
  33. package/dist/core/declarative-apply/index.js +134 -0
  34. package/dist/core/declarative-apply/round-apply.d.ts +100 -0
  35. package/dist/core/declarative-apply/round-apply.js +378 -0
  36. package/dist/core/export/file-mapper.d.ts +71 -0
  37. package/dist/core/export/file-mapper.js +474 -0
  38. package/dist/core/export/grouper.d.ts +13 -0
  39. package/dist/core/export/grouper.js +76 -0
  40. package/dist/core/export/index.d.ts +45 -0
  41. package/dist/core/export/index.js +63 -0
  42. package/dist/core/export/types.d.ts +84 -0
  43. package/dist/core/export/types.js +25 -0
  44. package/dist/core/fixtures/empty-catalogs/postgres-15-16-baseline.json +287 -0
  45. package/dist/core/integrations/filter/dsl.d.ts +38 -1
  46. package/dist/core/integrations/filter/dsl.js +20 -2
  47. package/dist/core/integrations/filter/extractors.js +42 -0
  48. package/dist/core/integrations/integration-dsl.d.ts +10 -0
  49. package/dist/core/integrations/supabase.d.ts +8 -0
  50. package/dist/core/integrations/supabase.js +9 -0
  51. package/dist/core/objects/aggregate/aggregate.diff.d.ts +2 -8
  52. package/dist/core/objects/aggregate/aggregate.diff.js +16 -70
  53. package/dist/core/objects/aggregate/aggregate.model.d.ts +8 -8
  54. package/dist/core/objects/aggregate/aggregate.model.js +1 -1
  55. package/dist/core/objects/aggregate/changes/aggregate.create.js +1 -1
  56. package/dist/core/objects/aggregate/changes/aggregate.drop.js +1 -1
  57. package/dist/core/objects/base.privilege-diff.d.ts +38 -13
  58. package/dist/core/objects/base.privilege-diff.js +104 -22
  59. package/dist/core/objects/base.privilege.d.ts +1 -0
  60. package/dist/core/objects/base.privilege.js +9 -2
  61. package/dist/core/objects/collation/collation.diff.d.ts +2 -3
  62. package/dist/core/objects/diff-context.d.ts +15 -0
  63. package/dist/core/objects/diff-context.js +1 -0
  64. package/dist/core/objects/domain/changes/domain.create.js +4 -2
  65. package/dist/core/objects/domain/domain.diff.d.ts +2 -8
  66. package/dist/core/objects/domain/domain.diff.js +16 -77
  67. package/dist/core/objects/domain/domain.model.js +1 -1
  68. package/dist/core/objects/event-trigger/event-trigger.diff.d.ts +2 -3
  69. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.diff.d.ts +2 -8
  70. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.diff.js +13 -77
  71. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.js +2 -2
  72. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.d.ts +2 -8
  73. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.js +16 -77
  74. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +1 -1
  75. package/dist/core/objects/foreign-data-wrapper/server/server.diff.d.ts +2 -8
  76. package/dist/core/objects/foreign-data-wrapper/server/server.diff.js +13 -77
  77. package/dist/core/objects/language/language.diff.d.ts +2 -5
  78. package/dist/core/objects/language/language.diff.js +7 -39
  79. package/dist/core/objects/materialized-view/materialized-view.diff.d.ts +2 -8
  80. package/dist/core/objects/materialized-view/materialized-view.diff.js +16 -158
  81. package/dist/core/objects/materialized-view/materialized-view.model.d.ts +3 -3
  82. package/dist/core/objects/materialized-view/materialized-view.model.js +1 -1
  83. package/dist/core/objects/procedure/changes/procedure.alter.js +12 -12
  84. package/dist/core/objects/procedure/procedure.diff.d.ts +2 -8
  85. package/dist/core/objects/procedure/procedure.diff.js +16 -77
  86. package/dist/core/objects/procedure/procedure.model.d.ts +9 -9
  87. package/dist/core/objects/procedure/procedure.model.js +1 -1
  88. package/dist/core/objects/publication/changes/publication.alter.d.ts +0 -9
  89. package/dist/core/objects/publication/changes/publication.alter.js +0 -14
  90. package/dist/core/objects/publication/changes/publication.types.d.ts +2 -2
  91. package/dist/core/objects/publication/publication.diff.d.ts +2 -3
  92. package/dist/core/objects/publication/publication.diff.js +8 -13
  93. package/dist/core/objects/rls-policy/changes/rls-policy.alter.js +3 -3
  94. package/dist/core/objects/rls-policy/rls-policy.model.d.ts +2 -2
  95. package/dist/core/objects/role/role.diff.js +22 -1
  96. package/dist/core/objects/role/role.model.d.ts +4 -3
  97. package/dist/core/objects/role/role.model.js +118 -12
  98. package/dist/core/objects/rule/rule.model.d.ts +1 -1
  99. package/dist/core/objects/schema/schema.diff.d.ts +2 -8
  100. package/dist/core/objects/schema/schema.diff.js +16 -77
  101. package/dist/core/objects/schema/schema.model.js +1 -1
  102. package/dist/core/objects/sequence/sequence.diff.d.ts +2 -8
  103. package/dist/core/objects/sequence/sequence.diff.js +16 -79
  104. package/dist/core/objects/sequence/sequence.model.js +1 -1
  105. package/dist/core/objects/subscription/subscription.diff.d.ts +2 -3
  106. package/dist/core/objects/table/changes/table.create.js +3 -0
  107. package/dist/core/objects/table/table.diff.d.ts +2 -8
  108. package/dist/core/objects/table/table.diff.js +26 -157
  109. package/dist/core/objects/table/table.model.d.ts +23 -22
  110. package/dist/core/objects/table/table.model.js +1 -1
  111. package/dist/core/objects/trigger/changes/trigger.create.js +2 -4
  112. package/dist/core/objects/trigger/trigger.model.d.ts +8 -0
  113. package/dist/core/objects/trigger/trigger.model.js +11 -0
  114. package/dist/core/objects/type/composite-type/composite-type.diff.d.ts +2 -8
  115. package/dist/core/objects/type/composite-type/composite-type.diff.js +16 -77
  116. package/dist/core/objects/type/composite-type/composite-type.model.d.ts +3 -3
  117. package/dist/core/objects/type/composite-type/composite-type.model.js +2 -1
  118. package/dist/core/objects/type/enum/enum.diff.d.ts +2 -8
  119. package/dist/core/objects/type/enum/enum.diff.js +25 -112
  120. package/dist/core/objects/type/enum/enum.model.js +1 -1
  121. package/dist/core/objects/type/range/changes/range.create.js +6 -3
  122. package/dist/core/objects/type/range/range.diff.d.ts +2 -8
  123. package/dist/core/objects/type/range/range.diff.js +16 -77
  124. package/dist/core/objects/type/range/range.model.js +1 -1
  125. package/dist/core/objects/view/view.diff.d.ts +2 -8
  126. package/dist/core/objects/view/view.diff.js +16 -158
  127. package/dist/core/objects/view/view.model.d.ts +18 -4
  128. package/dist/core/objects/view/view.model.js +3 -13
  129. package/dist/core/plan/apply.js +9 -26
  130. package/dist/core/plan/create.d.ts +19 -6
  131. package/dist/core/plan/create.js +134 -174
  132. package/dist/core/plan/serialize.js +16 -4
  133. package/dist/core/plan/sql-format/fixtures.js +3 -5
  134. package/dist/core/plan/sql-format/keyword-case.js +26 -1
  135. package/dist/core/plan/ssl-config.d.ts +32 -0
  136. package/dist/core/plan/ssl-config.js +115 -0
  137. package/dist/core/plan/types.d.ts +6 -0
  138. package/dist/core/postgres-config.d.ts +14 -0
  139. package/dist/core/postgres-config.js +53 -2
  140. package/dist/core/sort/graph-builder.js +10 -0
  141. package/dist/core/sort/logical-sort.js +31 -23
  142. package/dist/core/test-utils/assert-valid-sql.d.ts +10 -0
  143. package/dist/core/test-utils/assert-valid-sql.js +19 -0
  144. package/dist/index.d.ts +6 -0
  145. package/dist/index.js +6 -1
  146. package/package.json +21 -4
  147. package/src/cli/app.ts +27 -3
  148. package/src/cli/bin/cli.ts +6 -0
  149. package/src/cli/commands/catalog-export.ts +78 -0
  150. package/src/cli/commands/declarative-apply.diagnostics.test.ts +77 -0
  151. package/src/cli/commands/declarative-apply.ts +380 -0
  152. package/src/cli/commands/declarative-export.ts +330 -0
  153. package/src/cli/commands/plan.ts +28 -7
  154. package/src/cli/exit-code.test.ts +19 -0
  155. package/src/cli/exit-code.ts +7 -0
  156. package/src/cli/formatters/tree/tree.ts +3 -2
  157. package/src/cli/utils/apply-display.test.ts +348 -0
  158. package/src/cli/utils/apply-display.ts +238 -0
  159. package/src/cli/utils/export-display.test.ts +103 -0
  160. package/src/cli/utils/export-display.ts +275 -0
  161. package/src/cli/utils/integrations.test.ts +44 -0
  162. package/src/cli/utils/resolve-input.test.ts +38 -0
  163. package/src/cli/utils/resolve-input.ts +17 -0
  164. package/src/core/catalog-export/index.ts +20 -0
  165. package/src/core/catalog.diff.ts +79 -78
  166. package/src/core/catalog.model.test.ts +122 -0
  167. package/src/core/catalog.model.ts +127 -1
  168. package/src/core/catalog.snapshot.test.ts +464 -0
  169. package/src/core/catalog.snapshot.ts +289 -0
  170. package/src/core/declarative-apply/discover-sql.test.ts +103 -0
  171. package/src/core/declarative-apply/discover-sql.ts +107 -0
  172. package/src/core/declarative-apply/extract-catalog-providers.ts +220 -0
  173. package/src/core/declarative-apply/index.test.ts +67 -0
  174. package/src/core/declarative-apply/index.ts +205 -0
  175. package/src/core/declarative-apply/round-apply.test.ts +504 -0
  176. package/src/core/declarative-apply/round-apply.ts +562 -0
  177. package/src/core/expand-replace-dependencies.test.ts +70 -0
  178. package/src/core/export/file-mapper.test.ts +816 -0
  179. package/src/core/export/file-mapper.ts +574 -0
  180. package/src/core/export/grouper.ts +108 -0
  181. package/src/core/export/index.ts +129 -0
  182. package/src/core/export/types.ts +104 -0
  183. package/src/core/fixtures/empty-catalogs/postgres-15-16-baseline.json +287 -0
  184. package/src/core/integrations/filter/dsl.test.ts +211 -0
  185. package/src/core/integrations/filter/dsl.ts +65 -3
  186. package/src/core/integrations/filter/extractors.test.ts +244 -0
  187. package/src/core/integrations/filter/extractors.ts +42 -0
  188. package/src/core/integrations/integration-dsl.ts +10 -0
  189. package/src/core/integrations/serialize/dsl.test.ts +91 -0
  190. package/src/core/integrations/supabase.ts +9 -0
  191. package/src/core/objects/aggregate/aggregate.diff.ts +39 -95
  192. package/src/core/objects/aggregate/aggregate.model.ts +1 -1
  193. package/src/core/objects/aggregate/changes/aggregate.alter.test.ts +3 -1
  194. package/src/core/objects/aggregate/changes/aggregate.comment.test.ts +5 -2
  195. package/src/core/objects/aggregate/changes/aggregate.create.test.ts +6 -3
  196. package/src/core/objects/aggregate/changes/aggregate.create.ts +1 -1
  197. package/src/core/objects/aggregate/changes/aggregate.drop.test.ts +7 -3
  198. package/src/core/objects/aggregate/changes/aggregate.drop.ts +1 -1
  199. package/src/core/objects/aggregate/changes/aggregate.privilege.test.ts +9 -3
  200. package/src/core/objects/base.privilege-diff.ts +178 -30
  201. package/src/core/objects/base.privilege.ts +9 -2
  202. package/src/core/objects/collation/changes/collation.alter.test.ts +7 -2
  203. package/src/core/objects/collation/changes/collation.create.test.ts +7 -2
  204. package/src/core/objects/collation/changes/collation.drop.test.ts +4 -1
  205. package/src/core/objects/collation/collation.diff.test.ts +9 -12
  206. package/src/core/objects/collation/collation.diff.ts +2 -1
  207. package/src/core/objects/diff-context.ts +16 -0
  208. package/src/core/objects/domain/changes/domain.alter.test.ts +28 -9
  209. package/src/core/objects/domain/changes/domain.create.test.ts +32 -2
  210. package/src/core/objects/domain/changes/domain.create.ts +7 -1
  211. package/src/core/objects/domain/changes/domain.drop.test.ts +4 -1
  212. package/src/core/objects/domain/domain.diff.ts +39 -102
  213. package/src/core/objects/domain/domain.model.ts +1 -1
  214. package/src/core/objects/event-trigger/changes/event-trigger.alter.test.ts +10 -3
  215. package/src/core/objects/event-trigger/changes/event-trigger.create.test.ts +4 -1
  216. package/src/core/objects/event-trigger/changes/event-trigger.drop.test.ts +4 -1
  217. package/src/core/objects/event-trigger/event-trigger.diff.test.ts +12 -7
  218. package/src/core/objects/event-trigger/event-trigger.diff.ts +2 -1
  219. package/src/core/objects/extension/changes/extension.alter.test.ts +7 -2
  220. package/src/core/objects/extension/changes/extension.create.test.ts +4 -1
  221. package/src/core/objects/extension/changes/extension.drop.test.ts +4 -1
  222. package/src/core/objects/extension/extension.model.test.ts +98 -0
  223. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.test.ts +16 -5
  224. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.test.ts +51 -16
  225. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.drop.test.ts +4 -1
  226. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.diff.test.ts +111 -4
  227. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.diff.ts +31 -101
  228. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.ts +2 -2
  229. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.test.ts +46 -15
  230. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.test.ts +13 -4
  231. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.drop.test.ts +4 -1
  232. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.ts +39 -102
  233. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +1 -1
  234. package/src/core/objects/foreign-data-wrapper/server/changes/server.alter.test.ts +22 -7
  235. package/src/core/objects/foreign-data-wrapper/server/changes/server.create.test.ts +19 -6
  236. package/src/core/objects/foreign-data-wrapper/server/changes/server.drop.test.ts +4 -1
  237. package/src/core/objects/foreign-data-wrapper/server/server.diff.test.ts +95 -0
  238. package/src/core/objects/foreign-data-wrapper/server/server.diff.ts +31 -101
  239. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.test.ts +13 -4
  240. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.test.ts +16 -5
  241. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.drop.test.ts +10 -3
  242. package/src/core/objects/index/changes/index.alter.test.ts +13 -4
  243. package/src/core/objects/index/changes/index.create.test.ts +4 -1
  244. package/src/core/objects/index/changes/index.drop.test.ts +4 -1
  245. package/src/core/objects/language/changes/language.alter.test.ts +4 -1
  246. package/src/core/objects/language/changes/language.create.test.ts +4 -1
  247. package/src/core/objects/language/changes/language.drop.test.ts +4 -1
  248. package/src/core/objects/language/language.diff.test.ts +86 -4
  249. package/src/core/objects/language/language.diff.ts +17 -49
  250. package/src/core/objects/materialized-view/changes/materialized-view.alter.test.ts +10 -3
  251. package/src/core/objects/materialized-view/changes/materialized-view.create.test.ts +7 -2
  252. package/src/core/objects/materialized-view/changes/materialized-view.drop.test.ts +4 -1
  253. package/src/core/objects/materialized-view/materialized-view.diff.test.ts +162 -0
  254. package/src/core/objects/materialized-view/materialized-view.diff.ts +41 -191
  255. package/src/core/objects/materialized-view/materialized-view.model.ts +1 -1
  256. package/src/core/objects/procedure/changes/procedure.alter.test.ts +121 -49
  257. package/src/core/objects/procedure/changes/procedure.alter.ts +15 -12
  258. package/src/core/objects/procedure/changes/procedure.create.test.ts +4 -1
  259. package/src/core/objects/procedure/changes/procedure.drop.test.ts +7 -2
  260. package/src/core/objects/procedure/procedure.diff.ts +39 -102
  261. package/src/core/objects/procedure/procedure.model.ts +1 -1
  262. package/src/core/objects/publication/changes/publication.alter.test.ts +15 -21
  263. package/src/core/objects/publication/changes/publication.alter.ts +0 -18
  264. package/src/core/objects/publication/changes/publication.comment.test.ts +5 -2
  265. package/src/core/objects/publication/changes/publication.create.test.ts +5 -2
  266. package/src/core/objects/publication/changes/publication.drop.test.ts +3 -1
  267. package/src/core/objects/publication/changes/publication.types.ts +0 -2
  268. package/src/core/objects/publication/publication.diff.test.ts +24 -19
  269. package/src/core/objects/publication/publication.diff.ts +9 -15
  270. package/src/core/objects/rls-policy/changes/rls-policy.alter.test.ts +31 -14
  271. package/src/core/objects/rls-policy/changes/rls-policy.alter.ts +3 -3
  272. package/src/core/objects/rls-policy/changes/rls-policy.create.test.ts +10 -3
  273. package/src/core/objects/rls-policy/changes/rls-policy.drop.test.ts +4 -1
  274. package/src/core/objects/role/changes/role.alter.test.ts +31 -15
  275. package/src/core/objects/role/changes/role.create.test.ts +6 -2
  276. package/src/core/objects/role/changes/role.drop.test.ts +4 -1
  277. package/src/core/objects/role/role.diff.test.ts +235 -0
  278. package/src/core/objects/role/role.diff.ts +21 -1
  279. package/src/core/objects/role/role.model.ts +122 -14
  280. package/src/core/objects/rule/changes/rule.alter.test.ts +7 -3
  281. package/src/core/objects/rule/changes/rule.comment.test.ts +5 -2
  282. package/src/core/objects/rule/changes/rule.create.test.ts +6 -2
  283. package/src/core/objects/rule/changes/rule.drop.test.ts +3 -1
  284. package/src/core/objects/schema/changes/schema.alter.test.ts +4 -1
  285. package/src/core/objects/schema/changes/schema.create.test.ts +4 -1
  286. package/src/core/objects/schema/changes/schema.drop.test.ts +4 -1
  287. package/src/core/objects/schema/schema.diff.ts +39 -102
  288. package/src/core/objects/schema/schema.model.ts +1 -1
  289. package/src/core/objects/sequence/changes/sequence.alter.test.ts +11 -5
  290. package/src/core/objects/sequence/changes/sequence.create.test.ts +8 -3
  291. package/src/core/objects/sequence/changes/sequence.drop.test.ts +4 -1
  292. package/src/core/objects/sequence/sequence.diff.test.ts +114 -0
  293. package/src/core/objects/sequence/sequence.diff.ts +39 -104
  294. package/src/core/objects/sequence/sequence.model.ts +1 -1
  295. package/src/core/objects/subscription/changes/subscription.alter.test.ts +15 -5
  296. package/src/core/objects/subscription/changes/subscription.comment.test.ts +5 -2
  297. package/src/core/objects/subscription/changes/subscription.create.test.ts +5 -2
  298. package/src/core/objects/subscription/changes/subscription.drop.test.ts +3 -1
  299. package/src/core/objects/subscription/subscription.diff.test.ts +16 -11
  300. package/src/core/objects/subscription/subscription.diff.ts +2 -1
  301. package/src/core/objects/table/changes/table.alter.test.ts +38 -15
  302. package/src/core/objects/table/changes/table.create.test.ts +41 -3
  303. package/src/core/objects/table/changes/table.create.ts +4 -0
  304. package/src/core/objects/table/changes/table.drop.test.ts +3 -1
  305. package/src/core/objects/table/table.diff.test.ts +157 -0
  306. package/src/core/objects/table/table.diff.ts +54 -190
  307. package/src/core/objects/table/table.model.ts +1 -1
  308. package/src/core/objects/trigger/changes/trigger.alter.test.ts +8 -4
  309. package/src/core/objects/trigger/changes/trigger.create.test.ts +5 -1
  310. package/src/core/objects/trigger/changes/trigger.create.ts +7 -4
  311. package/src/core/objects/trigger/changes/trigger.drop.test.ts +5 -1
  312. package/src/core/objects/trigger/trigger.diff.test.ts +1 -0
  313. package/src/core/objects/trigger/trigger.model.ts +12 -0
  314. package/src/core/objects/type/composite-type/changes/composite-type.alter.test.ts +10 -4
  315. package/src/core/objects/type/composite-type/changes/composite-type.create.test.ts +7 -2
  316. package/src/core/objects/type/composite-type/changes/composite-type.drop.test.ts +4 -1
  317. package/src/core/objects/type/composite-type/composite-type.diff.test.ts +78 -0
  318. package/src/core/objects/type/composite-type/composite-type.diff.ts +39 -101
  319. package/src/core/objects/type/composite-type/composite-type.model.ts +2 -1
  320. package/src/core/objects/type/enum/changes/enum.alter.test.ts +14 -5
  321. package/src/core/objects/type/enum/changes/enum.create.test.ts +4 -1
  322. package/src/core/objects/type/enum/changes/enum.drop.test.ts +4 -1
  323. package/src/core/objects/type/enum/enum.diff.test.ts +181 -0
  324. package/src/core/objects/type/enum/enum.diff.ts +58 -146
  325. package/src/core/objects/type/enum/enum.model.ts +1 -1
  326. package/src/core/objects/type/range/changes/range.alter.test.ts +3 -1
  327. package/src/core/objects/type/range/changes/range.create.test.ts +5 -2
  328. package/src/core/objects/type/range/changes/range.create.ts +6 -2
  329. package/src/core/objects/type/range/changes/range.drop.test.ts +3 -1
  330. package/src/core/objects/type/range/range.diff.test.ts +77 -0
  331. package/src/core/objects/type/range/range.diff.ts +39 -101
  332. package/src/core/objects/type/range/range.model.ts +1 -1
  333. package/src/core/objects/view/changes/view.alter.test.ts +8 -3
  334. package/src/core/objects/view/changes/view.create.test.ts +7 -2
  335. package/src/core/objects/view/changes/view.drop.test.ts +4 -1
  336. package/src/core/objects/view/view.diff.test.ts +82 -0
  337. package/src/core/objects/view/view.diff.ts +41 -191
  338. package/src/core/objects/view/view.model.ts +3 -17
  339. package/src/core/plan/apply.ts +9 -27
  340. package/src/core/plan/create.ts +173 -237
  341. package/src/core/plan/serialize.test.ts +317 -0
  342. package/src/core/plan/serialize.ts +18 -4
  343. package/src/core/plan/sql-format/fixtures.ts +2 -5
  344. package/src/core/plan/sql-format/format-lowercase-coverage.test.ts +52 -0
  345. package/src/core/plan/sql-format/format-off.test.ts +14 -17
  346. package/src/core/plan/sql-format/format-pretty-lower-leading.test.ts +27 -22
  347. package/src/core/plan/sql-format/format-pretty-narrow.test.ts +17 -21
  348. package/src/core/plan/sql-format/format-pretty-preserve.test.ts +25 -20
  349. package/src/core/plan/sql-format/format-pretty-upper.test.ts +23 -20
  350. package/src/core/plan/sql-format/keyword-case.ts +36 -1
  351. package/src/core/plan/ssl-config.ts +172 -0
  352. package/src/core/plan/types.ts +6 -0
  353. package/src/core/postgres-config.ts +71 -2
  354. package/src/core/sort/graph-builder.ts +12 -0
  355. package/src/core/sort/logical-sort.test.ts +371 -0
  356. package/src/core/sort/logical-sort.ts +32 -25
  357. package/src/core/sort/topological-sort.test.ts +275 -0
  358. package/src/core/test-utils/assert-valid-sql.ts +20 -0
  359. package/src/index.ts +26 -2
@@ -4,6 +4,7 @@ import z from "zod";
4
4
  import {
5
5
  BasePgModel,
6
6
  columnPropsSchema,
7
+ normalizeColumns,
7
8
  type TableLikeObject,
8
9
  } from "../base.model.ts";
9
10
  import {
@@ -115,26 +116,11 @@ export class View extends BasePgModel implements TableLikeObject {
115
116
  }
116
117
 
117
118
  override stableSnapshot() {
118
- const normalizeColumns = () =>
119
- [...this.columns]
120
- .map((col) => {
121
- const { position: _pos, ...rest } = col as unknown as Record<
122
- string,
123
- unknown
124
- >;
125
- return rest;
126
- })
127
- .sort((a, b) => {
128
- const nameA = (a.name as string | undefined) ?? "";
129
- const nameB = (b.name as string | undefined) ?? "";
130
- return nameA.localeCompare(nameB);
131
- });
132
-
133
119
  return {
134
120
  identity: this.identityFields,
135
121
  data: {
136
122
  ...this.dataFields,
137
- columns: normalizeColumns(),
123
+ columns: normalizeColumns(this.columns),
138
124
  },
139
125
  };
140
126
  }
@@ -244,7 +230,7 @@ select
244
230
  from (
245
231
  -- one row for object ACL + one row per column ACL
246
232
  select null::name as attname, v.oid as relacl_oid, (
247
- select c_rel.relacl from pg_class c_rel where c_rel.oid = v.oid
233
+ select COALESCE(c_rel.relacl, acldefault('r', c_rel.relowner)) from pg_class c_rel where c_rel.oid = v.oid
248
234
  ) as acl
249
235
  union all
250
236
  select a2.attname, v.oid as relacl_oid, a2.attacl
@@ -3,13 +3,12 @@
3
3
  */
4
4
 
5
5
  import type { Pool } from "pg";
6
- import { escapeIdentifier } from "pg";
7
6
  import { diffCatalogs } from "../catalog.diff.ts";
8
7
  import { extractCatalog } from "../catalog.model.ts";
9
8
  import type { DiffContext } from "../context.ts";
10
9
  import { buildPlanScopeFingerprint, hashStableIds } from "../fingerprint.ts";
11
10
  import { compileFilterDSL } from "../integrations/filter/dsl.ts";
12
- import { createPool, endPool } from "../postgres-config.ts";
11
+ import { createManagedPool, endPool } from "../postgres-config.ts";
13
12
  import { sortChanges } from "../sort/sort-changes.ts";
14
13
  import type { Plan } from "./types.ts";
15
14
 
@@ -57,40 +56,23 @@ export async function applyPlan(
57
56
  let shouldCloseCurrent = false;
58
57
  let shouldCloseDesired = false;
59
58
 
60
- // Suppress expected shutdown errors from idle pool connections (57P01 = admin_shutdown)
61
- const onError = (err: Error & { code?: string }) => {
62
- if (err.code !== "57P01") {
63
- console.error("Pool error:", err);
64
- }
65
- };
66
-
67
59
  if (typeof source === "string") {
68
- currentPool = createPool(source, {
69
- onError,
70
- onConnect: async (client) => {
71
- // Force fully qualified names in catalog queries for fingerprint verification
72
- await client.query("SET search_path = ''");
73
- if (plan.role) {
74
- await client.query(`SET ROLE ${escapeIdentifier(plan.role)}`);
75
- }
76
- },
60
+ const managed = await createManagedPool(source, {
61
+ role: plan.role,
62
+ label: "source",
77
63
  });
64
+ currentPool = managed.pool;
78
65
  shouldCloseCurrent = true;
79
66
  } else {
80
67
  currentPool = source;
81
68
  }
82
69
 
83
70
  if (typeof target === "string") {
84
- desiredPool = createPool(target, {
85
- onError,
86
- onConnect: async (client) => {
87
- // Force fully qualified names in catalog queries for fingerprint verification
88
- await client.query("SET search_path = ''");
89
- if (plan.role) {
90
- await client.query(`SET ROLE ${escapeIdentifier(plan.role)}`);
91
- }
92
- },
71
+ const managed = await createManagedPool(target, {
72
+ role: plan.role,
73
+ label: "target",
93
74
  });
75
+ desiredPool = managed.pool;
94
76
  shouldCloseDesired = true;
95
77
  } else {
96
78
  desiredPool = target;
@@ -2,12 +2,14 @@
2
2
  * Plan creation - the main entry point for creating migration plans.
3
3
  */
4
4
 
5
- import { readFile } from "node:fs/promises";
6
5
  import type { Pool } from "pg";
7
6
  import { escapeIdentifier } from "pg";
8
7
  import { diffCatalogs } from "../catalog.diff.ts";
9
- import type { Catalog } from "../catalog.model.ts";
10
- import { extractCatalog } from "../catalog.model.ts";
8
+ import {
9
+ Catalog,
10
+ createEmptyCatalog,
11
+ extractCatalog,
12
+ } from "../catalog.model.ts";
11
13
  import type { Change } from "../change.types.ts";
12
14
  import type { DiffContext } from "../context.ts";
13
15
  import { buildPlanScopeFingerprint, hashStableIds } from "../fingerprint.ts";
@@ -20,8 +22,9 @@ import {
20
22
  compileSerializeDSL,
21
23
  type SerializeDSL,
22
24
  } from "../integrations/serialize/dsl.ts";
23
- import { createPool, endPool } from "../postgres-config.ts";
25
+ import { createManagedPool, endPool } from "../postgres-config.ts";
24
26
  import { sortChanges } from "../sort/sort-changes.ts";
27
+ import type { PgDependRow } from "../sort/types.ts";
25
28
  import { classifyChangesRisk } from "./risk.ts";
26
29
  import type { CreatePlanOptions, Plan } from "./types.ts";
27
30
 
@@ -30,254 +33,79 @@ import type { CreatePlanOptions, Plan } from "./types.ts";
30
33
  // ============================================================================
31
34
 
32
35
  /**
33
- * Create a migration plan by comparing two databases.
34
- *
35
- * @param fromUrl - Source database connection URL (current state)
36
- * @param toUrl - Target database connection URL (desired state)
37
- * @param options - Optional configuration
38
- * @returns A Plan if there are changes, null if databases are identical
36
+ * Input for source/target: a postgres connection URL, an existing Pool, or
37
+ * an already-resolved Catalog (e.g. deserialized from a snapshot file).
39
38
  */
40
- type ConnectionInput = string | Pool;
41
-
42
- type SslConfig = {
43
- ssl?:
44
- | boolean
45
- | {
46
- rejectUnauthorized: boolean;
47
- ca?: string;
48
- cert?: string;
49
- key?: string;
50
- /**
51
- * Custom server identity check function.
52
- * Used to skip hostname verification for verify-ca mode.
53
- * Returns undefined to indicate success (no error).
54
- */
55
- checkServerIdentity?: () => undefined;
56
- };
57
- cleanedUrl: string;
58
- };
39
+ export type CatalogInput = string | Pool | Catalog;
59
40
 
60
41
  /**
61
- * Parse SSL configuration from a PostgreSQL connection URL.
62
- * Supports sslmode (require, verify-ca, verify-full, prefer, disable).
63
- * Certificates can be provided via:
64
- * - Query string parameters (file paths): sslrootcert, sslcert, sslkey (preferred)
65
- * - Environment variables (content): PGDELTA_SOURCE_SSLROOTCERT/SSLCERT/SSLKEY or PGDELTA_TARGET_SSLROOTCERT/SSLCERT/SSLKEY
66
- * Returns SSL options for the postgres.js library and a cleaned URL without SSL-related query parameters.
42
+ * Create a migration plan by comparing two catalog states.
43
+ *
44
+ * Each input can be:
45
+ * - A postgres connection URL (string) -- a pool is created and catalog extracted
46
+ * - An existing pg Pool -- catalog is extracted directly
47
+ * - A Catalog instance -- used as-is (e.g. from a deserialized snapshot)
48
+ *
49
+ * When `source` is `null`, a minimal empty catalog (`createEmptyCatalog`) is
50
+ * used as the baseline. For a more accurate baseline, pass a Catalog
51
+ * deserialized from a snapshot of `template1` or another reference database.
52
+ *
53
+ * @param source - Source catalog input (current state), or null for empty baseline
54
+ * @param target - Target catalog input (desired state)
55
+ * @param options - Optional configuration
56
+ * @returns A Plan if there are changes, null if databases are identical
67
57
  */
68
- async function parseSslConfig(
69
- url: string,
70
- connectionType: "source" | "target",
71
- ): Promise<SslConfig> {
72
- const urlObj = new URL(url);
73
- const sslmode = urlObj.searchParams.get("sslmode");
74
- const sslrootcert = urlObj.searchParams.get("sslrootcert");
75
- const sslcert = urlObj.searchParams.get("sslcert");
76
- const sslkey = urlObj.searchParams.get("sslkey");
77
-
78
- // Remove SSL-related query parameters since we parse them ourselves
79
- urlObj.searchParams.delete("sslmode");
80
- urlObj.searchParams.delete("sslrootcert");
81
- urlObj.searchParams.delete("sslcert");
82
- urlObj.searchParams.delete("sslkey");
83
- const cleanedUrl = urlObj.toString();
84
-
85
- // Handle different SSL modes
86
- if (sslmode === "disable") {
87
- return { cleanedUrl, ssl: false };
88
- }
89
-
90
- if (
91
- sslmode === "require" ||
92
- sslmode === "prefer" ||
93
- sslmode === "verify-ca" ||
94
- sslmode === "verify-full"
95
- ) {
96
- // Helper function to get certificate value: query param (file path) takes precedence over env var (content)
97
- const getCertValue = async (
98
- queryParam: string | null,
99
- envVarName: string,
100
- ): Promise<string | undefined> => {
101
- // Prefer query parameter (file path)
102
- if (queryParam) {
103
- try {
104
- return await readFile(queryParam, "utf-8");
105
- } catch (error) {
106
- throw new Error(
107
- `Failed to read certificate file '${queryParam}': ${error instanceof Error ? error.message : String(error)}`,
108
- );
109
- }
110
- }
111
- // Fallback to environment variable (content)
112
- const envValue = process.env[envVarName];
113
- return envValue || undefined;
114
- };
115
-
116
- const hasExplicitVerification =
117
- sslmode === "verify-ca" || sslmode === "verify-full";
118
-
119
- // Get CA certificate value.
120
- // - verify-ca/verify-full: check query param first, then env var
121
- // - require/prefer: only check query param (libpq backward compatibility
122
- // requires an explicit root CA *file*, not a global env var)
123
- const caEnvVar =
124
- connectionType === "source"
125
- ? "PGDELTA_SOURCE_SSLROOTCERT"
126
- : "PGDELTA_TARGET_SSLROOTCERT";
127
- let caValue: string | undefined;
128
- if (sslrootcert) {
129
- // Explicit file path in query param — always honour it
130
- caValue = await getCertValue(sslrootcert, caEnvVar);
131
- } else if (hasExplicitVerification) {
132
- // verify-ca / verify-full without file path — fall back to env var
133
- caValue = await getCertValue(null, caEnvVar);
134
- }
135
- // require/prefer without sslrootcert: no CA cert, no verification
136
-
137
- // Determine if we should verify the CA chain
138
- // From PostgreSQL docs: "if a root CA file exists, the behavior of sslmode=require
139
- // will be the same as that of verify-ca"
140
- const hasLibpqCompatibility =
141
- (sslmode === "require" || sslmode === "prefer") && caValue !== undefined;
142
- const shouldVerifyCa = hasExplicitVerification || hasLibpqCompatibility;
143
-
144
- // Determine if we should verify hostname
145
- // - verify-full: verify both CA and hostname
146
- // - verify-ca: verify CA only (skip hostname)
147
- // - require/prefer with CA (libpq compat): behaves like verify-ca (skip hostname)
148
- const shouldVerifyHostname = sslmode === "verify-full";
149
-
150
- const ssl: {
151
- rejectUnauthorized: boolean;
152
- ca?: string;
153
- cert?: string;
154
- key?: string;
155
- checkServerIdentity?: () => undefined;
156
- } = {
157
- rejectUnauthorized: shouldVerifyCa,
158
- };
159
-
160
- // Add CA certificate if verifying
161
- if (shouldVerifyCa && caValue) {
162
- ssl.ca = caValue;
163
- }
164
-
165
- // For verify-ca and libpq compatibility mode: skip hostname verification
166
- // This matches PostgreSQL semantics where verify-ca only checks the CA chain
167
- if (shouldVerifyCa && !shouldVerifyHostname) {
168
- ssl.checkServerIdentity = () => undefined;
169
- }
170
-
171
- // Get client certificate (optional, for mutual TLS)
172
- const certEnvVar =
173
- connectionType === "source"
174
- ? "PGDELTA_SOURCE_SSLCERT"
175
- : "PGDELTA_TARGET_SSLCERT";
176
- const certValue = await getCertValue(sslcert, certEnvVar);
177
- if (certValue) {
178
- ssl.cert = certValue;
179
- }
180
-
181
- // Get client key (optional, for mutual TLS, required if cert is provided)
182
- const keyEnvVar =
183
- connectionType === "source"
184
- ? "PGDELTA_SOURCE_SSLKEY"
185
- : "PGDELTA_TARGET_SSLKEY";
186
- const keyValue = await getCertValue(sslkey, keyEnvVar);
187
- if (keyValue) {
188
- ssl.key = keyValue;
189
- }
190
-
191
- // Warn if cert is provided without key (or vice versa)
192
- if ((ssl.cert && !ssl.key) || (!ssl.cert && ssl.key)) {
193
- throw new Error(
194
- "Both client certificate and key must be provided together for mutual TLS",
195
- );
196
- }
197
-
198
- return { ssl, cleanedUrl };
199
- }
200
-
201
- // No sslmode specified or invalid value - explicitly disable SSL
202
- return { cleanedUrl, ssl: false };
203
- }
204
-
205
58
  export async function createPlan(
206
- source: ConnectionInput,
207
- target: ConnectionInput,
59
+ source: CatalogInput | null,
60
+ target: CatalogInput,
208
61
  options: CreatePlanOptions = {},
209
62
  ): Promise<{ plan: Plan; sortedChanges: Change[]; ctx: DiffContext } | null> {
210
- let sourcePool: Pool;
211
- let targetPool: Pool;
212
- let shouldCloseSource = false;
213
- let shouldCloseTarget = false;
214
-
215
- // Suppress expected shutdown errors from idle pool connections (57P01 = admin_shutdown)
216
- const onError = (err: Error & { code?: string }) => {
217
- if (err.code !== "57P01") {
218
- console.error("Pool error:", err);
63
+ const resolvePool = async (
64
+ input: string | Pool,
65
+ label: "source" | "target",
66
+ ): Promise<{ pool: Pool; shouldClose: boolean }> => {
67
+ if (typeof input === "string") {
68
+ const managed = await createManagedPool(input, {
69
+ role: options.role,
70
+ label,
71
+ });
72
+ return { pool: managed.pool, shouldClose: true };
219
73
  }
74
+ return { pool: input, shouldClose: false };
220
75
  };
221
76
 
222
- if (typeof source === "string") {
223
- const sslConfig = await parseSslConfig(source, "source");
224
- sourcePool = createPool(sslConfig.cleanedUrl, {
225
- ...(sslConfig.ssl !== undefined ? { ssl: sslConfig.ssl } : {}),
226
- onError,
227
- onConnect: async (client) => {
228
- // Force fully qualified names in catalog queries
229
- await client.query("SET search_path = ''");
230
- if (options.role) {
231
- await client.query(`SET ROLE ${escapeIdentifier(options.role)}`);
232
- }
233
- },
234
- });
235
- shouldCloseSource = true;
236
- } else {
237
- sourcePool = source;
238
- }
239
-
240
- if (typeof target === "string") {
241
- const sslConfig = await parseSslConfig(target, "target");
242
- targetPool = createPool(sslConfig.cleanedUrl, {
243
- ...(sslConfig.ssl !== undefined ? { ssl: sslConfig.ssl } : {}),
244
- onError,
245
- onConnect: async (client) => {
246
- // Force fully qualified names in catalog queries
247
- await client.query("SET search_path = ''");
248
- if (options.role) {
249
- await client.query(`SET ROLE ${escapeIdentifier(options.role)}`);
250
- }
251
- },
252
- });
253
- shouldCloseTarget = true;
254
- } else {
255
- targetPool = target;
256
- }
77
+ /**
78
+ * Resolve a CatalogInput to a Catalog, tracking pools that need cleanup.
79
+ */
80
+ const resolveCatalog = async (
81
+ input: CatalogInput,
82
+ label: "source" | "target",
83
+ pools: Array<{ pool: Pool; shouldClose: boolean }>,
84
+ ): Promise<Catalog> => {
85
+ if (input instanceof Catalog) {
86
+ return input;
87
+ }
88
+ const resolved = await resolvePool(input, label);
89
+ pools.push(resolved);
90
+ return extractCatalog(resolved.pool);
91
+ };
257
92
 
258
- const sourceExtraction = extractCatalog(sourcePool);
259
- const targetExtraction = extractCatalog(targetPool);
93
+ const pools: Array<{ pool: Pool; shouldClose: boolean }> = [];
260
94
 
261
95
  try {
262
- const [fromCatalog, toCatalog] = await Promise.all([
263
- sourceExtraction,
264
- targetExtraction,
265
- ]);
96
+ const toCatalog = await resolveCatalog(target, "target", pools);
97
+
98
+ const fromCatalog =
99
+ source !== null
100
+ ? await resolveCatalog(source, "source", pools)
101
+ : await createEmptyCatalog(toCatalog.version, toCatalog.currentUser);
266
102
 
267
103
  return buildPlanForCatalogs(fromCatalog, toCatalog, options);
268
- } catch (error) {
269
- // When one extraction fails, the other may still have in-flight queries.
270
- // Wait for both to settle before pool cleanup to prevent unhandled
271
- // rejections from connections being terminated mid-flight.
272
- await Promise.allSettled([sourceExtraction, targetExtraction]);
273
- throw error;
274
104
  } finally {
275
- const closers: Promise<unknown>[] = [];
276
- if (shouldCloseSource) closers.push(endPool(sourcePool));
277
- if (shouldCloseTarget) closers.push(endPool(targetPool));
278
- if (closers.length) {
279
- await Promise.all(closers);
280
- }
105
+ const closers = pools
106
+ .filter((p) => p.shouldClose)
107
+ .map((p) => endPool(p.pool));
108
+ if (closers.length) await Promise.all(closers);
281
109
  }
282
110
  }
283
111
 
@@ -291,6 +119,7 @@ function buildPlanForCatalogs(
291
119
  ): { plan: Plan; sortedChanges: Change[]; ctx: DiffContext } | null {
292
120
  const changes = diffCatalogs(fromCatalog, toCatalog, {
293
121
  role: options.role,
122
+ skipDefaultPrivilegeSubtraction: options.skipDefaultPrivilegeSubtraction,
294
123
  });
295
124
 
296
125
  const filterOption = options.filter;
@@ -331,10 +160,24 @@ function buildPlanForCatalogs(
331
160
  // Use filter from final integration
332
161
  const filterFn = finalIntegration?.filter;
333
162
 
334
- const filteredChanges = filterFn
163
+ let filteredChanges = filterFn
335
164
  ? changes.filter((change) => filterFn(change))
336
165
  : changes;
337
166
 
167
+ // Cascade dependency exclusions: when a change is excluded by the filter,
168
+ // also exclude changes that depend on it (via requires or pg_depend).
169
+ // DSL filters: cascade only if explicitly opted in (cascade: true). Function filters: cascade by default.
170
+ const shouldCascade = isFilterDSL
171
+ ? (filterDSL as Record<string, unknown>)?.cascade === true
172
+ : true;
173
+ if (filterFn && filteredChanges.length < changes.length && shouldCascade) {
174
+ filteredChanges = cascadeExclusions(
175
+ filteredChanges,
176
+ changes,
177
+ toCatalog.depends,
178
+ );
179
+ }
180
+
338
181
  if (filteredChanges.length === 0) {
339
182
  return null;
340
183
  }
@@ -352,6 +195,99 @@ function buildPlanForCatalogs(
352
195
  return { plan, sortedChanges, ctx };
353
196
  }
354
197
 
198
+ // ============================================================================
199
+ // Dependency Cascading
200
+ // ============================================================================
201
+
202
+ /**
203
+ * Cascade exclusions through dependency relationships.
204
+ *
205
+ * When a change is excluded by the filter, any change that depends on it
206
+ * (via explicit `requires` or via catalog `pg_depend`) should also be excluded.
207
+ * This runs as a fixpoint loop, bounded by the total number of changes to
208
+ * guarantee deterministic termination.
209
+ *
210
+ * @param filteredChanges - Changes that passed the initial filter
211
+ * @param allChanges - All changes before filtering
212
+ * @param catalogDepends - Dependency rows from the target catalog (pg_depend)
213
+ * @returns The filtered changes with cascading exclusions applied
214
+ */
215
+ function cascadeExclusions(
216
+ filteredChanges: Change[],
217
+ allChanges: Change[],
218
+ catalogDepends: PgDependRow[],
219
+ ): Change[] {
220
+ // Collect stableIds created by initially-excluded changes
221
+ const filteredSet = new Set(filteredChanges);
222
+ const excludedIds = new Set<string>();
223
+ for (const change of allChanges) {
224
+ if (!filteredSet.has(change)) {
225
+ for (const id of change.creates ?? []) {
226
+ excludedIds.add(id);
227
+ }
228
+ }
229
+ }
230
+
231
+ if (excludedIds.size === 0) {
232
+ return filteredChanges;
233
+ }
234
+
235
+ // Build reverse dependency map: referenced_stable_id -> Set(dependent_stable_ids)
236
+ const catalogDependents = new Map<string, Set<string>>();
237
+ for (const dep of catalogDepends) {
238
+ const existing = catalogDependents.get(dep.referenced_stable_id);
239
+ if (existing) {
240
+ existing.add(dep.dependent_stable_id);
241
+ } else {
242
+ catalogDependents.set(
243
+ dep.referenced_stable_id,
244
+ new Set([dep.dependent_stable_id]),
245
+ );
246
+ }
247
+ }
248
+
249
+ // Fixpoint loop: bounded by total changes to guarantee termination.
250
+ // Each iteration must remove at least one change, otherwise we break.
251
+ let result = filteredChanges;
252
+ for (let i = 0; i < allChanges.length; i++) {
253
+ const beforeLength = result.length;
254
+ result = result.filter((change) => {
255
+ // Check explicit requirements: does this change require an excluded id?
256
+ const requires = change.requires ?? [];
257
+ if (requires.some((dep) => excludedIds.has(dep))) {
258
+ for (const id of change.creates ?? []) {
259
+ excludedIds.add(id);
260
+ }
261
+ return false;
262
+ }
263
+
264
+ // Check catalog dependencies: does anything this change creates
265
+ // depend on an excluded id via pg_depend?
266
+ const creates = change.creates ?? [];
267
+ for (const createdId of creates) {
268
+ for (const excludedId of excludedIds) {
269
+ const dependents = catalogDependents.get(excludedId);
270
+ if (dependents?.has(createdId)) {
271
+ for (const id of creates) {
272
+ excludedIds.add(id);
273
+ }
274
+ return false;
275
+ }
276
+ }
277
+ }
278
+
279
+ return true;
280
+ });
281
+
282
+ // No changes removed this iteration — fixpoint reached
283
+ if (result.length === beforeLength) {
284
+ break;
285
+ }
286
+ }
287
+
288
+ return result;
289
+ }
290
+
355
291
  // ============================================================================
356
292
  // Plan Building
357
293
  // ============================================================================