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

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,245 @@
1
+ /**
2
+ * Declarative export command - export a declarative SQL schema from a database diff.
3
+ */
4
+ import { mkdir, rm, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { buildCommand } from "@stricli/core";
7
+ import chalk from "chalk";
8
+ import { exportDeclarativeSchema } from "../../core/export/index.js";
9
+ import { createPlan } from "../../core/plan/index.js";
10
+ import { assertSafePath, buildFileTree, computeFileDiff, formatExportSummary, } from "../utils/export-display.js";
11
+ import { loadIntegrationDSL } from "../utils/integrations.js";
12
+ import { isPostgresUrl, loadCatalogFromFile } from "../utils/resolve-input.js";
13
+ function parseJsonFlag(label, value) {
14
+ try {
15
+ return JSON.parse(value);
16
+ }
17
+ catch (error) {
18
+ throw new Error(`Invalid ${label} JSON: ${error instanceof Error ? error.message : String(error)}`);
19
+ }
20
+ }
21
+ export const declarativeExportCommand = buildCommand({
22
+ parameters: {
23
+ flags: {
24
+ source: {
25
+ kind: "parsed",
26
+ brief: "Source (current state): postgres URL or catalog snapshot file path. Omit to export all objects from target.",
27
+ parse: String,
28
+ optional: true,
29
+ },
30
+ target: {
31
+ kind: "parsed",
32
+ brief: "Target (desired state): postgres URL or catalog snapshot file path",
33
+ parse: String,
34
+ },
35
+ output: {
36
+ kind: "parsed",
37
+ brief: "Output directory path for declarative schema files",
38
+ parse: String,
39
+ },
40
+ integration: {
41
+ kind: "parsed",
42
+ brief: "Integration name (e.g., 'supabase') or path to integration JSON file",
43
+ parse: String,
44
+ optional: true,
45
+ },
46
+ filter: {
47
+ kind: "parsed",
48
+ brief: 'Filter DSL as inline JSON (e.g., \'{"schema":"public"}\')',
49
+ parse: (v) => parseJsonFlag("filter", v),
50
+ optional: true,
51
+ },
52
+ serialize: {
53
+ kind: "parsed",
54
+ brief: 'Serialize DSL as inline JSON array (e.g., \'[{"when":{"type":"schema"},"options":{"skipAuthorization":true}}]\')',
55
+ parse: (v) => parseJsonFlag("serialize", v),
56
+ optional: true,
57
+ },
58
+ "grouping-mode": {
59
+ kind: "enum",
60
+ brief: "How grouped entities are organized on disk",
61
+ values: ["single-file", "subdirectory"],
62
+ optional: true,
63
+ },
64
+ "group-patterns": {
65
+ kind: "parsed",
66
+ brief: 'JSON array of {pattern, name} objects (e.g., \'[{"pattern":"^auth","name":"auth"}]\')',
67
+ parse: (v) => {
68
+ const parsed = parseJsonFlag("group-patterns", v);
69
+ if (!Array.isArray(parsed)) {
70
+ throw new Error("group-patterns must be a JSON array");
71
+ }
72
+ return parsed;
73
+ },
74
+ optional: true,
75
+ },
76
+ "flat-schemas": {
77
+ kind: "parsed",
78
+ brief: "Comma-separated list of schemas to flatten (e.g., partman,pgboss,audit)",
79
+ parse: String,
80
+ optional: true,
81
+ },
82
+ "format-options": {
83
+ kind: "parsed",
84
+ brief: 'SQL format options as inline JSON (e.g., \'{"keywordCase":"lower","maxWidth":180}\')',
85
+ parse: (v) => parseJsonFlag("format-options", v),
86
+ optional: true,
87
+ },
88
+ force: {
89
+ kind: "boolean",
90
+ brief: "Remove entire output directory before writing",
91
+ optional: true,
92
+ },
93
+ "dry-run": {
94
+ kind: "boolean",
95
+ brief: "Show tree and summary without writing files",
96
+ optional: true,
97
+ },
98
+ "diff-focus": {
99
+ kind: "boolean",
100
+ brief: "Show only files that changed (created/updated/deleted) in the tree",
101
+ optional: true,
102
+ },
103
+ verbose: {
104
+ kind: "boolean",
105
+ brief: "Show detailed output",
106
+ optional: true,
107
+ },
108
+ },
109
+ aliases: {
110
+ s: "source",
111
+ t: "target",
112
+ o: "output",
113
+ },
114
+ },
115
+ docs: {
116
+ brief: "Export a declarative schema from a database diff",
117
+ fullDescription: `
118
+ Export a declarative SQL schema by comparing two databases (source → target).
119
+ Writes .sql files to the output directory, grouped by object type and optional
120
+ grouping rules.
121
+
122
+ When --source is omitted, all objects from the target database are exported
123
+ (equivalent to diffing from an empty database).
124
+
125
+ Flags:
126
+ source - Source database connection URL (optional; omit for full export)
127
+ target - Target database connection URL (desired state)
128
+ output - Directory path for generated .sql files
129
+ integration - Integration name or path (e.g., supabase) for filter/serialize
130
+ filter - Filter DSL as JSON to include/exclude changes
131
+ serialize - Serialize DSL as JSON array for custom SQL generation
132
+ grouping-mode - single-file or subdirectory for grouped entities
133
+ group-patterns - JSON array of {pattern, name} for name-based grouping
134
+ flat-schemas - Comma-separated schemas to merge into one file per category
135
+ format-options - SQL format options as JSON
136
+ force - Remove output directory before writing (full replace)
137
+ dry-run - Show tree and summary only, do not write files
138
+ diff-focus - Show only changed files (created/updated/deleted) in the tree
139
+ verbose - Show detailed output
140
+
141
+ After export, a tip is printed with the command to apply the schema to an empty database.
142
+ `.trim(),
143
+ },
144
+ async func(flags) {
145
+ const { compileSerializeDSL } = await import("../../core/integrations/serialize/dsl.js");
146
+ let filterOption = flags.filter;
147
+ let serializeOption = flags.serialize;
148
+ let integrationEmptyCatalog;
149
+ if (flags.integration) {
150
+ const integrationDSL = await loadIntegrationDSL(flags.integration);
151
+ filterOption = filterOption ?? integrationDSL.filter;
152
+ serializeOption = serializeOption ?? integrationDSL.serialize;
153
+ integrationEmptyCatalog = integrationDSL.emptyCatalog;
154
+ }
155
+ const resolvedSource = flags.source
156
+ ? isPostgresUrl(flags.source)
157
+ ? flags.source
158
+ : await loadCatalogFromFile(flags.source)
159
+ : integrationEmptyCatalog
160
+ ? (await import("../../core/catalog.snapshot.js")).deserializeCatalog(integrationEmptyCatalog)
161
+ : null;
162
+ const resolvedTarget = isPostgresUrl(flags.target)
163
+ ? flags.target
164
+ : await loadCatalogFromFile(flags.target);
165
+ // Pass raw DSL to createPlan (not pre-compiled functions).
166
+ // createPlan compiles them internally and uses the DSL type to correctly
167
+ // determine cascade behavior: DSL filters disable cascading by default
168
+ // (unless cascade:true is set), preventing unintended exclusion of
169
+ // changes that depend on filtered objects (e.g. RLS policies that
170
+ // reference auth.uid() when the auth schema is filtered out).
171
+ const planResult = await createPlan(resolvedSource, resolvedTarget, {
172
+ filter: filterOption,
173
+ serialize: serializeOption,
174
+ skipDefaultPrivilegeSubtraction: true,
175
+ });
176
+ if (!planResult) {
177
+ this.process.stdout.write("No changes detected.\n");
178
+ return;
179
+ }
180
+ const hasGrouping = flags["grouping-mode"] !== undefined ||
181
+ (flags["group-patterns"] !== undefined &&
182
+ flags["group-patterns"].length > 0) ||
183
+ (flags["flat-schemas"] !== undefined && flags["flat-schemas"].length > 0);
184
+ let grouping;
185
+ if (hasGrouping) {
186
+ grouping = {
187
+ mode: flags["grouping-mode"] ?? "single-file",
188
+ groupPatterns: flags["group-patterns"],
189
+ autoGroupPartitions: true,
190
+ flatSchemas: flags["flat-schemas"] !== undefined
191
+ ? flags["flat-schemas"]
192
+ .split(",")
193
+ .map((s) => s.trim())
194
+ .filter(Boolean)
195
+ : undefined,
196
+ };
197
+ }
198
+ const serializeFn = serializeOption !== undefined
199
+ ? compileSerializeDSL(serializeOption)
200
+ : undefined;
201
+ const output = exportDeclarativeSchema(planResult, {
202
+ integration: serializeFn !== undefined ? { serialize: serializeFn } : undefined,
203
+ formatOptions: flags["format-options"] ?? undefined,
204
+ grouping,
205
+ onWarning: (msg) => {
206
+ this.process.stderr.write(chalk.yellow(`Warning: ${msg}\n`));
207
+ },
208
+ });
209
+ const outputDir = path.resolve(flags.output);
210
+ const applyTip = (dir) => `\nTip: To apply this schema to an empty database, run:\n pgdelta declarative apply --path ${dir} --target <database_url>\n`;
211
+ const diff = await computeFileDiff(outputDir, output.files);
212
+ this.process.stdout.write("\n");
213
+ this.process.stdout.write(`${buildFileTree(output.files.map((f) => f.path), path.basename(outputDir) || outputDir, { diff, diffFocus: !!flags["diff-focus"] })}\n`);
214
+ this.process.stdout.write("\n");
215
+ this.process.stdout.write(`${chalk.green("+")} created ${chalk.yellow("~")} updated ${chalk.red("-")} deleted\n`);
216
+ this.process.stdout.write("\n");
217
+ const summary = formatExportSummary(diff, !!flags["dry-run"]);
218
+ if (summary) {
219
+ this.process.stdout.write(`${summary}\n`);
220
+ }
221
+ const totalChanges = planResult.sortedChanges.length;
222
+ const totalStatements = output.files.reduce((s, f) => s + f.statements, 0);
223
+ this.process.stdout.write(`Changes: ${totalChanges} | Files: ${output.files.length} | Statements: ${totalStatements}\n`);
224
+ if (flags["dry-run"]) {
225
+ this.process.stdout.write(chalk.dim("\n(dry-run: no files written)\n"));
226
+ this.process.stdout.write(chalk.cyan(applyTip(outputDir)));
227
+ return;
228
+ }
229
+ if (flags.force) {
230
+ await rm(outputDir, { recursive: true, force: true });
231
+ await mkdir(outputDir, { recursive: true });
232
+ }
233
+ else if (diff.deleted.length > 0) {
234
+ this.process.stderr.write(chalk.yellow(`Warning: ${diff.deleted.length} existing file(s) will no longer be present. Use --force to replace the output directory.\n`));
235
+ }
236
+ for (const file of output.files) {
237
+ assertSafePath(file.path, outputDir);
238
+ const filePath = path.join(outputDir, file.path);
239
+ await mkdir(path.dirname(filePath), { recursive: true });
240
+ await writeFile(filePath, file.sql);
241
+ }
242
+ this.process.stdout.write(chalk.green(`Wrote ${output.files.length} file(s) to ${outputDir}\n`));
243
+ this.process.stdout.write(chalk.cyan(applyTip(outputDir)));
244
+ },
245
+ });
@@ -4,19 +4,22 @@
4
4
  import { writeFile } from "node:fs/promises";
5
5
  import { buildCommand } from "@stricli/core";
6
6
  import { createPlan } from "../../core/plan/index.js";
7
+ import { setCommandExitCode } from "../exit-code.js";
7
8
  import { loadIntegrationDSL } from "../utils/integrations.js";
9
+ import { isPostgresUrl, loadCatalogFromFile } from "../utils/resolve-input.js";
8
10
  import { formatPlanForDisplay } from "../utils.js";
9
11
  export const planCommand = buildCommand({
10
12
  parameters: {
11
13
  flags: {
12
14
  source: {
13
15
  kind: "parsed",
14
- brief: "Source database connection URL (current state)",
16
+ brief: "Source (current state): postgres URL or catalog snapshot file path. Omit for empty baseline.",
15
17
  parse: String,
18
+ optional: true,
16
19
  },
17
20
  target: {
18
21
  kind: "parsed",
19
- brief: "Target database connection URL (desired state)",
22
+ brief: "Target (desired state): postgres URL or catalog snapshot file path",
20
23
  parse: String,
21
24
  },
22
25
  format: {
@@ -103,16 +106,26 @@ json/sql outputs are available for artifacts or piping.
103
106
  `.trim(),
104
107
  },
105
108
  async func(flags) {
106
- // Load integration if provided and extract filter/serialize DSL
107
109
  let filterOption = flags.filter;
108
110
  let serializeOption = flags.serialize;
111
+ let integrationEmptyCatalog;
109
112
  if (flags.integration) {
110
113
  const integrationDSL = await loadIntegrationDSL(flags.integration);
111
- // Use integration DSL if explicit flags not provided
112
114
  filterOption = filterOption ?? integrationDSL.filter;
113
115
  serializeOption = serializeOption ?? integrationDSL.serialize;
116
+ integrationEmptyCatalog = integrationDSL.emptyCatalog;
114
117
  }
115
- const planResult = await createPlan(flags.source, flags.target, {
118
+ const resolvedSource = flags.source
119
+ ? isPostgresUrl(flags.source)
120
+ ? flags.source
121
+ : await loadCatalogFromFile(flags.source)
122
+ : integrationEmptyCatalog
123
+ ? (await import("../../core/catalog.snapshot.js")).deserializeCatalog(integrationEmptyCatalog)
124
+ : null;
125
+ const resolvedTarget = isPostgresUrl(flags.target)
126
+ ? flags.target
127
+ : await loadCatalogFromFile(flags.target);
128
+ const planResult = await createPlan(resolvedSource, resolvedTarget, {
116
129
  role: flags.role,
117
130
  filter: filterOption,
118
131
  serialize: serializeOption,
@@ -153,6 +166,6 @@ json/sql outputs are available for artifacts or piping.
153
166
  }
154
167
  }
155
168
  // Exit code 2 indicates changes were detected
156
- process.exitCode = 2;
169
+ setCommandExitCode(2);
157
170
  },
158
171
  });
@@ -0,0 +1,2 @@
1
+ export declare function setCommandExitCode(code: number): void;
2
+ export declare function getCommandExitCode(): number | undefined;
@@ -0,0 +1,7 @@
1
+ let _exitCode;
2
+ export function setCommandExitCode(code) {
3
+ _exitCode = code;
4
+ }
5
+ export function getCommandExitCode() {
6
+ return _exitCode;
7
+ }
@@ -12,7 +12,7 @@ export function formatTree(plan) {
12
12
  // Summary
13
13
  const total = countTotalChanges(plan);
14
14
  lines.push(chalk.bold(`📋 Migration Plan: ${total} change${total !== 1 ? "s" : ""}`));
15
- const summary = buildSummaryLine(plan);
15
+ const summary = buildPlanSummaryTable(plan);
16
16
  if (summary) {
17
17
  lines.push("");
18
18
  lines.push(summary);
@@ -38,8 +38,9 @@ function countTotalChanges(plan) {
38
38
  }
39
39
  /**
40
40
  * Build summary as a table showing counts by entity type and operation.
41
+ * Exported for use by declarative-export to show the same summary style.
41
42
  */
42
- function buildSummaryLine(plan) {
43
+ function buildPlanSummaryTable(plan) {
43
44
  // Count by object type
44
45
  const byType = {};
45
46
  countFromHierarchy(plan, byType);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Display utilities for the declarative-apply command.
3
+ *
4
+ * Pure formatting and location-resolution functions — no CLI framework dependency.
5
+ * Used to:
6
+ * - Map pg-topo diagnostics into display items (optionally grouped by message/code).
7
+ * - Resolve statement IDs to file paths and line/column for error output.
8
+ * - Format StatementErrors in a pgAdmin-style multi-line block.
9
+ */
10
+ import type { Diagnostic } from "@supabase/pg-topo";
11
+ import type { StatementError } from "../../core/declarative-apply/round-apply.ts";
12
+ /**
13
+ * Convert a 1-based character offset in a string to 1-based line and column.
14
+ * Used when mapping PostgreSQL error positions (in SQL) to file locations.
15
+ */
16
+ export declare function positionToLineColumn(sql: string, position: number): {
17
+ line: number;
18
+ column: number;
19
+ };
20
+ /** Input to buildDiagnosticDisplayItems: a pg-topo diagnostic plus optional location and object key. */
21
+ export type DiagnosticDisplayEntry = {
22
+ diagnostic: Diagnostic;
23
+ location?: string;
24
+ requiredObjectKey?: string;
25
+ };
26
+ /** One display row for a diagnostic (or a group of same-code diagnostics with multiple locations). */
27
+ type DiagnosticDisplayItem = {
28
+ code: string;
29
+ message: string;
30
+ suggestedFix?: string;
31
+ requiredObjectKey?: string;
32
+ locations: string[];
33
+ };
34
+ /** Extract requiredObjectKey from a pg-topo diagnostic if present and non-empty. */
35
+ export declare const requiredObjectKeyFromDiagnostic: (diagnostic: Diagnostic) => string | undefined;
36
+ /**
37
+ * Turn diagnostic entries into display items. If grouped is true, entries with
38
+ * the same code/message/suggestedFix are merged into one item with multiple locations.
39
+ */
40
+ export declare const buildDiagnosticDisplayItems: (entries: DiagnosticDisplayEntry[], grouped: boolean) => DiagnosticDisplayItem[];
41
+ /**
42
+ * Resolve the full path to a .sql file from the schema path (directory or single file)
43
+ * and a relative file path (e.g. from a statement id). If schemaPath is a file, its
44
+ * directory is used as the base.
45
+ */
46
+ export declare function resolveSqlFilePath(schemaPath: string, relativeFilePath: string): Promise<string>;
47
+ /**
48
+ * Format a StatementError in pgAdmin-style: ERROR, Detail, SQL state, optional
49
+ * Context, Hint, and Location (resolving the .sql file and line/column when possible).
50
+ */
51
+ export declare function formatStatementError(err: StatementError, schemaPath: string): Promise<string>;
52
+ export {};
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Display utilities for the declarative-apply command.
3
+ *
4
+ * Pure formatting and location-resolution functions — no CLI framework dependency.
5
+ * Used to:
6
+ * - Map pg-topo diagnostics into display items (optionally grouped by message/code).
7
+ * - Resolve statement IDs to file paths and line/column for error output.
8
+ * - Format StatementErrors in a pgAdmin-style multi-line block.
9
+ */
10
+ import { readFile, stat } from "node:fs/promises";
11
+ import path from "node:path";
12
+ /**
13
+ * Convert a 1-based character offset in a string to 1-based line and column.
14
+ * Used when mapping PostgreSQL error positions (in SQL) to file locations.
15
+ */
16
+ export function positionToLineColumn(sql, position) {
17
+ const lines = sql.split("\n");
18
+ let offset = 0;
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const lineLen = lines[i].length + (i < lines.length - 1 ? 1 : 0);
21
+ if (position <= offset + lineLen) {
22
+ return { line: i + 1, column: position - offset };
23
+ }
24
+ offset += lineLen;
25
+ }
26
+ const last = lines.length;
27
+ const lastLineLen = lines[last - 1]?.length ?? 0;
28
+ return { line: last, column: lastLineLen + 1 };
29
+ }
30
+ /**
31
+ * Parse a statement id in the form "filePath:statementIndex" into components.
32
+ * The last colon separates path from index (paths may contain colons).
33
+ * Returns null if the format is invalid.
34
+ */
35
+ function parseStatementId(id) {
36
+ const lastColon = id.lastIndexOf(":");
37
+ if (lastColon === -1)
38
+ return null;
39
+ const filePath = id.slice(0, lastColon);
40
+ const n = Number.parseInt(id.slice(lastColon + 1), 10);
41
+ if (!Number.isInteger(n) || n < 0)
42
+ return null;
43
+ return { filePath, statementIndex: n };
44
+ }
45
+ /** Extract requiredObjectKey from a pg-topo diagnostic if present and non-empty. */
46
+ export const requiredObjectKeyFromDiagnostic = (diagnostic) => {
47
+ const value = diagnostic.details?.requiredObjectKey;
48
+ return typeof value === "string" && value.length > 0 ? value : undefined;
49
+ };
50
+ /** Build a stable key for grouping diagnostics with the same code, message, and suggested fix. */
51
+ const diagnosticDisplayGroupKey = (entry) => [
52
+ entry.diagnostic.code,
53
+ entry.diagnostic.message,
54
+ entry.diagnostic.suggestedFix ?? "",
55
+ entry.requiredObjectKey ?? "",
56
+ ].join("\u0000");
57
+ /**
58
+ * Turn diagnostic entries into display items. If grouped is true, entries with
59
+ * the same code/message/suggestedFix are merged into one item with multiple locations.
60
+ */
61
+ export const buildDiagnosticDisplayItems = (entries, grouped) => {
62
+ if (!grouped) {
63
+ return entries.map((entry) => ({
64
+ code: entry.diagnostic.code,
65
+ message: entry.diagnostic.message,
66
+ suggestedFix: entry.diagnostic.suggestedFix,
67
+ requiredObjectKey: entry.requiredObjectKey,
68
+ locations: entry.location ? [entry.location] : [],
69
+ }));
70
+ }
71
+ const groupedItems = new Map();
72
+ for (const entry of entries) {
73
+ const key = diagnosticDisplayGroupKey(entry);
74
+ const existing = groupedItems.get(key);
75
+ if (!existing) {
76
+ groupedItems.set(key, {
77
+ code: entry.diagnostic.code,
78
+ message: entry.diagnostic.message,
79
+ suggestedFix: entry.diagnostic.suggestedFix,
80
+ requiredObjectKey: entry.requiredObjectKey,
81
+ locations: entry.location ? [entry.location] : [],
82
+ });
83
+ continue;
84
+ }
85
+ if (entry.location && !existing.locations.includes(entry.location)) {
86
+ existing.locations.push(entry.location);
87
+ }
88
+ }
89
+ return [...groupedItems.values()];
90
+ };
91
+ /**
92
+ * Resolve the full path to a .sql file from the schema path (directory or single file)
93
+ * and a relative file path (e.g. from a statement id). If schemaPath is a file, its
94
+ * directory is used as the base.
95
+ */
96
+ export async function resolveSqlFilePath(schemaPath, relativeFilePath) {
97
+ try {
98
+ const statResult = await stat(schemaPath);
99
+ const baseDir = statResult.isFile() ? path.dirname(schemaPath) : schemaPath;
100
+ return path.join(baseDir, relativeFilePath);
101
+ }
102
+ catch {
103
+ return path.join(schemaPath, relativeFilePath);
104
+ }
105
+ }
106
+ /**
107
+ * Find the 0-based start offset of statementSql in fileContent.
108
+ * Tries exact match first, then trimmed match. Returns -1 if not found.
109
+ */
110
+ function findStatementStartInFile(fileContent, statementSql) {
111
+ const exact = fileContent.indexOf(statementSql);
112
+ if (exact !== -1)
113
+ return exact;
114
+ const trimmedStmt = statementSql.trim();
115
+ if (!trimmedStmt)
116
+ return -1;
117
+ const trimmed = fileContent.indexOf(trimmedStmt);
118
+ if (trimmed !== -1)
119
+ return trimmed;
120
+ return -1;
121
+ }
122
+ /**
123
+ * Format a StatementError in pgAdmin-style: ERROR, Detail, SQL state, optional
124
+ * Context, Hint, and Location (resolving the .sql file and line/column when possible).
125
+ */
126
+ export async function formatStatementError(err, schemaPath) {
127
+ const lines = [];
128
+ lines.push(`ERROR: ${err.message}`);
129
+ if (err.detail) {
130
+ lines.push(`Detail: ${err.detail}`);
131
+ }
132
+ lines.push(`SQL state: ${err.code}`);
133
+ if (err.position !== undefined && err.statement.sql.length > 0) {
134
+ lines.push(`Character: ${err.position}`);
135
+ const pos = Math.max(0, Math.min(err.position - 1, err.statement.sql.length));
136
+ const contextStart = Math.max(0, pos - 40);
137
+ const contextEnd = Math.min(err.statement.sql.length, pos + 40);
138
+ const snippet = err.statement.sql.slice(contextStart, contextEnd);
139
+ const oneLine = snippet.replace(/\s+/g, " ").trim();
140
+ lines.push(`Context: ${oneLine || "(empty)"}`);
141
+ }
142
+ if (err.hint) {
143
+ lines.push(`Hint: ${err.hint}`);
144
+ }
145
+ const parsed = parseStatementId(err.statement.id);
146
+ if (parsed) {
147
+ let locationLine;
148
+ try {
149
+ const fullPath = await resolveSqlFilePath(schemaPath, parsed.filePath);
150
+ const fileContent = await readFile(fullPath, "utf-8");
151
+ const statementStart = findStatementStartInFile(fileContent, err.statement.sql);
152
+ if (statementStart !== -1) {
153
+ if (err.position !== undefined && err.statement.sql.length > 0) {
154
+ const fileErrorOffset = statementStart + (err.position - 1);
155
+ const fileErrorPosition = Math.min(fileErrorOffset + 1, fileContent.length);
156
+ const { line, column } = positionToLineColumn(fileContent, Math.max(1, fileErrorPosition));
157
+ locationLine = `Location: ${parsed.filePath}:${line}:${column}`;
158
+ }
159
+ else {
160
+ const { line } = positionToLineColumn(fileContent, statementStart + 1);
161
+ locationLine = `Location: ${parsed.filePath}:${line}`;
162
+ }
163
+ }
164
+ else {
165
+ locationLine = `Location: ${parsed.filePath} (statement ${parsed.statementIndex})`;
166
+ }
167
+ }
168
+ catch {
169
+ if (err.position !== undefined && err.statement.sql.length > 0) {
170
+ const { line, column } = positionToLineColumn(err.statement.sql, err.position);
171
+ locationLine = `Location: ${parsed.filePath} (statement ${parsed.statementIndex}, line ${line}, column ${column})`;
172
+ }
173
+ else {
174
+ locationLine = `Location: ${parsed.filePath} (statement ${parsed.statementIndex})`;
175
+ }
176
+ }
177
+ lines.push(locationLine);
178
+ }
179
+ else {
180
+ lines.push(`Location: ${err.statement.id}`);
181
+ }
182
+ return lines.map((l) => ` ${l}`).join("\n");
183
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * CLI helpers for declarative export: file tree, diff, and summary formatting.
3
+ */
4
+ import type { FileEntry } from "../../core/export/types.ts";
5
+ /**
6
+ * Ensure a relative file path does not escape the output directory.
7
+ * Uses Node.js path.resolve + startsWith as the canonical traversal check.
8
+ */
9
+ export declare function assertSafePath(filePath: string, outputDir: string): void;
10
+ interface FileDiffResult {
11
+ created: string[];
12
+ updated: string[];
13
+ deleted: string[];
14
+ unchanged: string[];
15
+ }
16
+ interface BuildFileTreeOptions {
17
+ /** When provided, leaf paths are prefixed with + / ~ / - and colorized (created / updated / deleted). */
18
+ diff?: FileDiffResult;
19
+ /** When true, only paths that are created, updated, or deleted are shown; unchanged are omitted. Includes diff.deleted in the tree. */
20
+ diffFocus?: boolean;
21
+ }
22
+ /**
23
+ * Build a directory tree string from file paths.
24
+ * Groups by directory, shows files as leaves with indentation.
25
+ * When options.diff is provided, leaf names are prefixed with + (created), ~ (updated), - (deleted) and colorized.
26
+ * When options.diffFocus is true, only changed paths (and their ancestors) are shown; unchanged files are omitted.
27
+ *
28
+ * @param files - Array of relative file paths (e.g. ["schemas/public/schema.sql", "schemas/public/tables/users.sql"])
29
+ * @param outputDir - Display name for the root (e.g. "declarative-schemas")
30
+ * @param options - Optional diff and diffFocus for symbols and filtering
31
+ */
32
+ export declare function buildFileTree(files: string[], outputDir: string, options?: BuildFileTreeOptions): string;
33
+ /**
34
+ * Compare existing output directory with new file set.
35
+ * Returns created, updated, deleted, and unchanged paths.
36
+ */
37
+ export declare function computeFileDiff(outputDir: string, newFiles: FileEntry[]): Promise<FileDiffResult>;
38
+ /**
39
+ * Format the created/deleted/updated summary with colors.
40
+ * In dry-run mode, uses "would create/delete/update" phrasing.
41
+ */
42
+ export declare function formatExportSummary(diff: FileDiffResult, dryRun: boolean): string;
43
+ export {};