@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
@@ -0,0 +1,574 @@
1
+ /**
2
+ * Map changes to declarative schema file paths.
3
+ */
4
+
5
+ import createDebug from "debug";
6
+ import type { Change } from "../change.types.ts";
7
+ import {
8
+ getObjectName,
9
+ getObjectSchema,
10
+ getParentInfo,
11
+ } from "../plan/serialize.ts";
12
+ import type { FileCategory, FilePath, Grouping } from "./types.ts";
13
+
14
+ const debugExport = createDebug("pg-delta:export");
15
+
16
+ // ============================================================================
17
+ // Helpers
18
+ // ============================================================================
19
+
20
+ type RoleDefaultPrivilegeChange = Change & {
21
+ objectType: "role";
22
+ scope: "default_privilege";
23
+ inSchema: string | null;
24
+ };
25
+
26
+ function isRoleDefaultPrivilegeChange(
27
+ change: Change,
28
+ ): change is RoleDefaultPrivilegeChange {
29
+ return (
30
+ change.objectType === "role" &&
31
+ change.scope === "default_privilege" &&
32
+ "inSchema" in change
33
+ );
34
+ }
35
+
36
+ function requireSchema(change: Change): string {
37
+ const schema = getObjectSchema(change);
38
+ if (!schema) {
39
+ throw new Error(
40
+ `Expected schema for ${change.objectType} change '${getObjectName(change)}' (operation: ${change.operation})`,
41
+ );
42
+ }
43
+ return schema;
44
+ }
45
+
46
+ function schemaPath(schema: string, ...parts: string[]): string {
47
+ return `schemas/${schema}/${parts.join("/")}`;
48
+ }
49
+
50
+ // ============================================================================
51
+ // File Path Mapping
52
+ // ============================================================================
53
+
54
+ export function getFilePath(change: Change): FilePath {
55
+ switch (change.objectType) {
56
+ case "role":
57
+ if (isRoleDefaultPrivilegeChange(change) && change.inSchema) {
58
+ const schemaName = change.inSchema;
59
+ return {
60
+ path: schemaPath(schemaName, "schema.sql"),
61
+ category: "schema",
62
+ metadata: {
63
+ objectType: "default_privilege",
64
+ schemaName,
65
+ objectName: schemaName,
66
+ },
67
+ };
68
+ }
69
+ return {
70
+ path: "cluster/roles.sql",
71
+ category: "cluster",
72
+ metadata: { objectType: "role" },
73
+ };
74
+ case "extension": {
75
+ const extensionName = getObjectName(change);
76
+ return {
77
+ path: `cluster/extensions/${extensionName}.sql`,
78
+ category: "extensions",
79
+ metadata: { objectType: "extension", objectName: extensionName },
80
+ };
81
+ }
82
+ case "foreign_data_wrapper":
83
+ case "server":
84
+ case "user_mapping":
85
+ return {
86
+ path: "cluster/foreign_data_wrappers.sql",
87
+ category: "cluster",
88
+ metadata: { objectType: change.objectType },
89
+ };
90
+ case "publication":
91
+ return {
92
+ path: "cluster/publications.sql",
93
+ category: "cluster",
94
+ metadata: { objectType: "publication" },
95
+ };
96
+ case "subscription":
97
+ return {
98
+ path: "cluster/subscriptions.sql",
99
+ category: "cluster",
100
+ metadata: { objectType: "subscription" },
101
+ };
102
+ case "event_trigger":
103
+ return {
104
+ path: "cluster/event_triggers.sql",
105
+ category: "cluster",
106
+ metadata: { objectType: "event_trigger" },
107
+ };
108
+ case "language":
109
+ return {
110
+ path: "cluster/languages.sql",
111
+ category: "cluster",
112
+ metadata: { objectType: "language" },
113
+ };
114
+ case "schema": {
115
+ const schemaName = change.schema.name;
116
+ return {
117
+ path: schemaPath(schemaName, "schema.sql"),
118
+ category: "schema",
119
+ metadata: {
120
+ objectType: "schema",
121
+ schemaName,
122
+ objectName: schemaName,
123
+ },
124
+ };
125
+ }
126
+ case "enum":
127
+ case "composite_type":
128
+ case "range": {
129
+ const schema = requireSchema(change);
130
+ const objectName = getObjectName(change);
131
+ return {
132
+ path: schemaPath(schema, "types", `${objectName}.sql`),
133
+ category: "types",
134
+ metadata: {
135
+ objectType: change.objectType,
136
+ schemaName: schema,
137
+ objectName,
138
+ },
139
+ };
140
+ }
141
+ case "domain": {
142
+ const schema = requireSchema(change);
143
+ const objectName = getObjectName(change);
144
+ return {
145
+ path: schemaPath(schema, "domains", `${objectName}.sql`),
146
+ category: "domains",
147
+ metadata: {
148
+ objectType: "domain",
149
+ schemaName: schema,
150
+ objectName,
151
+ },
152
+ };
153
+ }
154
+ case "collation": {
155
+ const schema = requireSchema(change);
156
+ const objectName = getObjectName(change);
157
+ return {
158
+ path: schemaPath(schema, "collations", `${objectName}.sql`),
159
+ category: "collations",
160
+ metadata: {
161
+ objectType: "collation",
162
+ schemaName: schema,
163
+ objectName,
164
+ },
165
+ };
166
+ }
167
+ case "sequence": {
168
+ const schema = requireSchema(change);
169
+ const objectName = getObjectName(change);
170
+
171
+ // ALTER SEQUENCE ... OWNED BY must be grouped with the owning table,
172
+ // not the sequence file, to avoid ordering issues: the table must exist
173
+ // before the OWNED BY clause can reference its column.
174
+ if (
175
+ change.operation === "alter" &&
176
+ "ownedBy" in change &&
177
+ change.ownedBy
178
+ ) {
179
+ const ownedBy = change.ownedBy as {
180
+ schema: string;
181
+ table: string;
182
+ column: string;
183
+ };
184
+ return {
185
+ path: schemaPath(ownedBy.schema, "tables", `${ownedBy.table}.sql`),
186
+ category: "tables",
187
+ metadata: {
188
+ objectType: "table",
189
+ schemaName: ownedBy.schema,
190
+ objectName: ownedBy.table,
191
+ },
192
+ };
193
+ }
194
+
195
+ return {
196
+ path: schemaPath(schema, "sequences", `${objectName}.sql`),
197
+ category: "sequences",
198
+ metadata: {
199
+ objectType: "sequence",
200
+ schemaName: schema,
201
+ objectName,
202
+ },
203
+ };
204
+ }
205
+ case "table": {
206
+ const schema = change.table.schema;
207
+ const tableName = change.table.name;
208
+ // Partitions always go in the same file as their parent table.
209
+ if (change.table.is_partition && change.table.parent_name) {
210
+ const parentSchema = change.table.parent_schema ?? change.table.schema;
211
+ return {
212
+ path: schemaPath(
213
+ parentSchema,
214
+ "tables",
215
+ `${change.table.parent_name}.sql`,
216
+ ),
217
+ category: "tables",
218
+ metadata: {
219
+ objectType: "table",
220
+ schemaName: parentSchema,
221
+ objectName: change.table.parent_name,
222
+ },
223
+ };
224
+ }
225
+ return {
226
+ path: schemaPath(schema, "tables", `${tableName}.sql`),
227
+ category: "tables",
228
+ metadata: {
229
+ objectType: "table",
230
+ schemaName: schema,
231
+ objectName: tableName,
232
+ },
233
+ };
234
+ }
235
+ case "foreign_table": {
236
+ const schema = requireSchema(change);
237
+ const objectName = getObjectName(change);
238
+ return {
239
+ path: schemaPath(schema, "foreign_tables", `${objectName}.sql`),
240
+ category: "foreign_tables",
241
+ metadata: {
242
+ objectType: "foreign_table",
243
+ schemaName: schema,
244
+ objectName,
245
+ },
246
+ };
247
+ }
248
+ case "view": {
249
+ const schema = requireSchema(change);
250
+ const objectName = getObjectName(change);
251
+ return {
252
+ path: schemaPath(schema, "views", `${objectName}.sql`),
253
+ category: "views",
254
+ metadata: {
255
+ objectType: "view",
256
+ schemaName: schema,
257
+ objectName,
258
+ },
259
+ };
260
+ }
261
+ case "materialized_view": {
262
+ const schema = requireSchema(change);
263
+ const objectName = getObjectName(change);
264
+ return {
265
+ path: schemaPath(schema, "matviews", `${objectName}.sql`),
266
+ category: "matviews",
267
+ metadata: {
268
+ objectType: "materialized_view",
269
+ schemaName: schema,
270
+ objectName,
271
+ },
272
+ };
273
+ }
274
+ case "procedure": {
275
+ const schema = requireSchema(change);
276
+ const objectName = getObjectName(change);
277
+ const isProcedure = change.procedure.kind === "p";
278
+ return {
279
+ path: schemaPath(
280
+ schema,
281
+ isProcedure ? "procedures" : "functions",
282
+ `${objectName}.sql`,
283
+ ),
284
+ category: isProcedure ? "procedures" : "functions",
285
+ metadata: {
286
+ objectType: isProcedure ? "procedure" : "function",
287
+ schemaName: schema,
288
+ objectName,
289
+ },
290
+ };
291
+ }
292
+ case "aggregate": {
293
+ const schema = requireSchema(change);
294
+ const objectName = getObjectName(change);
295
+ return {
296
+ path: schemaPath(schema, "aggregates", `${objectName}.sql`),
297
+ category: "aggregates",
298
+ metadata: {
299
+ objectType: "aggregate",
300
+ schemaName: schema,
301
+ objectName,
302
+ },
303
+ };
304
+ }
305
+ case "index": {
306
+ const schema = requireSchema(change);
307
+ const parent = getParentInfo(change);
308
+ if (!parent) {
309
+ throw new Error(
310
+ `Expected parent for index '${getObjectName(change)}' in schema '${schema}'`,
311
+ );
312
+ }
313
+ const parentName = parent.name;
314
+ const category =
315
+ parent.type === "materialized_view" ? "matviews" : "tables";
316
+ return {
317
+ path: schemaPath(schema, category, `${parentName}.sql`),
318
+ category,
319
+ metadata: {
320
+ objectType: parent.type,
321
+ schemaName: schema,
322
+ objectName: parentName,
323
+ },
324
+ };
325
+ }
326
+ case "trigger":
327
+ case "rls_policy":
328
+ case "rule": {
329
+ const schema = requireSchema(change);
330
+ const parent = getParentInfo(change);
331
+ if (!parent) {
332
+ throw new Error(
333
+ `Expected parent for ${change.objectType} '${getObjectName(change)}' in schema '${schema}'`,
334
+ );
335
+ }
336
+ const parentName = parent.name;
337
+ const category =
338
+ parent.type === "view"
339
+ ? "views"
340
+ : parent.type === "materialized_view"
341
+ ? "matviews"
342
+ : "tables";
343
+ return {
344
+ path: schemaPath(schema, category, `${parentName}.sql`),
345
+ category,
346
+ metadata: {
347
+ objectType: parent.type,
348
+ schemaName: schema,
349
+ objectName: parentName,
350
+ },
351
+ };
352
+ }
353
+ default: {
354
+ const _exhaustive: never = change;
355
+ return _exhaustive;
356
+ }
357
+ }
358
+ }
359
+
360
+ // ============================================================================
361
+ // Entity Grouping
362
+ // ============================================================================
363
+
364
+ /** A compiled grouping pattern: pre-built RegExp + group name. */
365
+ export interface CompiledPattern {
366
+ regex: RegExp;
367
+ name: string;
368
+ }
369
+
370
+ /** Result of compilePatterns: valid compiled patterns plus warnings for skipped/invalid regexes. */
371
+ interface CompilePatternsResult {
372
+ compiled: CompiledPattern[];
373
+ warnings: string[];
374
+ }
375
+
376
+ /**
377
+ * Compile user-facing `GroupingPattern[]` into `CompiledPattern[]`.
378
+ * Strings are turned into `new RegExp(str)`. Invalid regex strings are skipped
379
+ * (no throw), so the returned `compiled` array may be shorter than the input.
380
+ * Any skipped patterns are reported in `warnings`.
381
+ */
382
+ export function compilePatterns(
383
+ patterns: import("./types.ts").GroupingPattern[],
384
+ ): CompilePatternsResult {
385
+ const compiled: CompiledPattern[] = [];
386
+ const warnings: string[] = [];
387
+ for (const p of patterns) {
388
+ let regex: RegExp;
389
+ if (typeof p.pattern === "string") {
390
+ try {
391
+ regex = new RegExp(p.pattern);
392
+ } catch (e) {
393
+ const msg = `Skipping invalid grouping regex '${p.pattern}': ${e instanceof Error ? e.message : String(e)}`;
394
+ debugExport(msg);
395
+ warnings.push(msg);
396
+ continue;
397
+ }
398
+ } else {
399
+ // Strip /g and /y flags — .test() mutates lastIndex with these flags,
400
+ // causing non-deterministic matching across repeated calls.
401
+ const flags = p.pattern.flags.replace(/[gy]/g, "");
402
+ regex =
403
+ flags !== p.pattern.flags
404
+ ? new RegExp(p.pattern.source, flags)
405
+ : p.pattern;
406
+ }
407
+ compiled.push({ regex, name: p.name });
408
+ }
409
+ return { compiled, warnings };
410
+ }
411
+
412
+ /**
413
+ * Create a file mapper that applies regex-based grouping on top of the
414
+ * default `getFilePath` mapping.
415
+ *
416
+ * When no grouping config is provided (or it is undefined), the plain
417
+ * `getFilePath` function is returned unchanged.
418
+ */
419
+ export function createFileMapper(
420
+ grouping?: Grouping,
421
+ onWarning?: (message: string) => void,
422
+ ): (change: Change) => FilePath {
423
+ if (!grouping) return getFilePath;
424
+
425
+ const { compiled, warnings } = compilePatterns(grouping.groupPatterns ?? []);
426
+ for (const w of warnings) {
427
+ onWarning?.(w);
428
+ }
429
+ const autoPartitions = grouping.autoGroupPartitions !== false; // default true
430
+ const flatSet = new Set(grouping.flatSchemas ?? []);
431
+
432
+ return (change: Change): FilePath => {
433
+ const basePath = getFilePath(change);
434
+
435
+ // Flat schemas: collapse everything into one file per category.
436
+ // Applied first -- skips pattern matching for these schemas.
437
+ if (
438
+ flatSet.size > 0 &&
439
+ basePath.metadata.schemaName &&
440
+ flatSet.has(basePath.metadata.schemaName)
441
+ ) {
442
+ return flattenSchema(basePath);
443
+ }
444
+
445
+ const groupName = resolveGroupName(
446
+ change,
447
+ basePath,
448
+ compiled,
449
+ autoPartitions,
450
+ );
451
+ if (!groupName) return basePath;
452
+ return applyGrouping(basePath, groupName, grouping.mode);
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Flatten a schema-scoped file path into one file per category.
458
+ *
459
+ * e.g. `schemas/partman/tables/template_public_events.sql`
460
+ * → `schemas/partman/tables.sql`
461
+ *
462
+ * `schema.sql` is left unchanged (it is already flat).
463
+ */
464
+ export function flattenSchema(filePath: FilePath): FilePath {
465
+ const schema = filePath.metadata.schemaName ?? "";
466
+ const category = filePath.category;
467
+
468
+ // schema.sql stays as-is
469
+ if (category === "schema") return filePath;
470
+
471
+ return {
472
+ path: schemaPath(schema, `${category}.sql`),
473
+ category,
474
+ metadata: {
475
+ ...filePath.metadata,
476
+ objectName: category,
477
+ },
478
+ };
479
+ }
480
+
481
+ /**
482
+ * Determine the group name for a change, or `null` if it should not be
483
+ * grouped.
484
+ *
485
+ * Resolution order:
486
+ * 1. Automatic partition detection -- resolve the parent table name.
487
+ * 2. Regex patterns -- first match wins (user controls priority by ordering).
488
+ *
489
+ * The resolved name from step 1 is fed through step 2 so that a partition
490
+ * parent name can itself be matched by a broader pattern (e.g. parent
491
+ * "kubernetes_resource_events" matches `/^kubernetes/`).
492
+ *
493
+ * If auto-detect resolved a parent but no pattern matched, the parent name
494
+ * is used as-is.
495
+ */
496
+ export function resolveGroupName(
497
+ change: Change,
498
+ filePath: FilePath,
499
+ patterns: CompiledPattern[],
500
+ autoPartitions: boolean,
501
+ ): string | null {
502
+ // Only schema-scoped objects can be grouped (skip cluster-level).
503
+ if (!filePath.metadata.schemaName) return null;
504
+
505
+ // 1. Auto-detect partitions: table changes where the table is a partition
506
+ // of another table.
507
+ let resolvedName: string | null = null;
508
+ if (
509
+ autoPartitions &&
510
+ change.objectType === "table" &&
511
+ change.table.is_partition &&
512
+ change.table.parent_name
513
+ ) {
514
+ resolvedName = change.table.parent_name;
515
+ }
516
+
517
+ // 2. Regex patterns -- first match wins.
518
+ const nameToMatch = resolvedName ?? filePath.metadata.objectName;
519
+ if (nameToMatch) {
520
+ for (const p of patterns) {
521
+ if (p.regex.test(nameToMatch)) {
522
+ return p.name;
523
+ }
524
+ }
525
+ }
526
+
527
+ // 3. If auto-detect found a parent but no pattern matched, use the parent
528
+ // name directly.
529
+ return resolvedName;
530
+ }
531
+
532
+ /**
533
+ * Rewrite a `FilePath` according to the chosen grouping mode.
534
+ *
535
+ * - **single-file**: the filename becomes `{prefix}.sql` inside the original
536
+ * category directory.
537
+ * e.g. `schemas/public/tables/wal_verification_results_p20260107.sql`
538
+ * → `schemas/public/tables/wal_verification_results.sql`
539
+ *
540
+ * - **subdirectory**: the file is moved to a prefix-named directory under the
541
+ * schema root, with the category as the filename.
542
+ * e.g. `schemas/public/tables/wal_verification_results_p20260107.sql`
543
+ * → `schemas/public/wal_verification_results/tables.sql`
544
+ */
545
+ export function applyGrouping(
546
+ filePath: FilePath,
547
+ prefix: string,
548
+ mode: Grouping["mode"],
549
+ ): FilePath {
550
+ const schema = filePath.metadata.schemaName ?? "";
551
+ const category = filePath.category as FileCategory;
552
+
553
+ if (mode === "single-file") {
554
+ // Replace the filename, keep the category directory.
555
+ return {
556
+ path: schemaPath(schema, category, `${prefix}.sql`),
557
+ category,
558
+ metadata: {
559
+ ...filePath.metadata,
560
+ objectName: prefix,
561
+ },
562
+ };
563
+ }
564
+
565
+ // subdirectory mode: schemas/{schema}/{prefix}/{category}.sql
566
+ return {
567
+ path: schemaPath(schema, prefix, `${category}.sql`),
568
+ category,
569
+ metadata: {
570
+ ...filePath.metadata,
571
+ objectName: prefix,
572
+ },
573
+ };
574
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Group changes into declarative schema files and order them for readability.
3
+ */
4
+
5
+ import type { Change } from "../change.types.ts";
6
+ import { getFilePath } from "./file-mapper.ts";
7
+ import type { FileCategory, FileMetadata, FilePath } from "./types.ts";
8
+ import { CATEGORY_PRIORITY } from "./types.ts";
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ interface FileGroup {
15
+ path: string;
16
+ category: FileCategory;
17
+ metadata: FileMetadata;
18
+ changes: Change[];
19
+ }
20
+
21
+ // ============================================================================
22
+ // Within-file ordering
23
+ // ============================================================================
24
+
25
+ const OPERATION_PRIORITY: Record<string, number> = {
26
+ create: 0,
27
+ alter: 1,
28
+ };
29
+
30
+ const SCOPE_PRIORITY: Record<string, number> = {
31
+ object: 0,
32
+ comment: 1,
33
+ privilege: 2,
34
+ default_privilege: 3,
35
+ membership: 4,
36
+ };
37
+
38
+ /**
39
+ * Sort changes within a file for readability:
40
+ * 1. By operation: create → alter
41
+ * 2. By scope: object → comment → privilege → default_privilege → membership
42
+ * 3. Stable tie-break by original position
43
+ */
44
+ function sortChangesWithinFile(changes: Change[]): Change[] {
45
+ // Tag each change with its original index for stable tie-breaking.
46
+ const tagged = changes.map((change, index) => ({ change, index }));
47
+ tagged.sort((a, b) => {
48
+ const opA = OPERATION_PRIORITY[a.change.operation] ?? 99;
49
+ const opB = OPERATION_PRIORITY[b.change.operation] ?? 99;
50
+ if (opA !== opB) return opA - opB;
51
+
52
+ const scopeA =
53
+ SCOPE_PRIORITY[(a.change as { scope?: string }).scope ?? "object"] ?? 99;
54
+ const scopeB =
55
+ SCOPE_PRIORITY[(b.change as { scope?: string }).scope ?? "object"] ?? 99;
56
+ if (scopeA !== scopeB) return scopeA - scopeB;
57
+
58
+ return a.index - b.index;
59
+ });
60
+ return tagged.map((t) => t.change);
61
+ }
62
+
63
+ // ============================================================================
64
+ // Grouping & Ordering
65
+ // ============================================================================
66
+
67
+ export function groupChangesByFile(
68
+ changes: Change[],
69
+ mapper: (change: Change) => FilePath = getFilePath,
70
+ ): FileGroup[] {
71
+ const groups = new Map<string, FileGroup>();
72
+
73
+ for (const change of changes) {
74
+ const file = mapper(change);
75
+
76
+ const existing = groups.get(file.path);
77
+ if (!existing) {
78
+ groups.set(file.path, {
79
+ path: file.path,
80
+ category: file.category,
81
+ metadata: file.metadata,
82
+ changes: [change],
83
+ });
84
+ continue;
85
+ }
86
+
87
+ existing.changes.push(change);
88
+ }
89
+
90
+ // Sort within each file for readability.
91
+ for (const group of groups.values()) {
92
+ group.changes = sortChangesWithinFile(group.changes);
93
+ }
94
+
95
+ // Sort files by category priority, then alphabetically by path.
96
+ return Array.from(groups.values()).sort(sortByCategory);
97
+ }
98
+
99
+ /**
100
+ * Sort by category priority, then path for determinism.
101
+ */
102
+ function sortByCategory(a: FileGroup, b: FileGroup): number {
103
+ const categoryDiff =
104
+ CATEGORY_PRIORITY[a.category] - CATEGORY_PRIORITY[b.category];
105
+ if (categoryDiff !== 0) return categoryDiff;
106
+
107
+ return a.path.localeCompare(b.path);
108
+ }