@intentius/chant-lexicon-sql 0.124.46 → 0.124.49

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 (402) hide show
  1. package/dist/change-set.d.ts +1 -1
  2. package/dist/change-set.d.ts.map +1 -1
  3. package/dist/change-set.js +2 -1
  4. package/dist/clickhouse/apply/statements.d.ts +9 -0
  5. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  6. package/dist/clickhouse/apply/statements.js +24 -0
  7. package/dist/clickhouse/import/live-export.d.ts +7 -3
  8. package/dist/clickhouse/import/live-export.d.ts.map +1 -1
  9. package/dist/clickhouse/import/live-export.js +21 -5
  10. package/dist/clickhouse/live/named-collections.d.ts +27 -0
  11. package/dist/clickhouse/live/named-collections.d.ts.map +1 -0
  12. package/dist/clickhouse/live/named-collections.js +53 -0
  13. package/dist/clickhouse/live/undeclarable.d.ts +15 -3
  14. package/dist/clickhouse/live/undeclarable.d.ts.map +1 -1
  15. package/dist/clickhouse/live/undeclarable.js +18 -4
  16. package/dist/clickhouse/named-collections.d.ts +83 -0
  17. package/dist/clickhouse/named-collections.d.ts.map +1 -0
  18. package/dist/clickhouse/named-collections.js +203 -0
  19. package/dist/clickhouse/parser.d.ts.map +1 -1
  20. package/dist/clickhouse/parser.js +1 -1
  21. package/dist/clickhouse/plan/commands.d.ts +2 -1
  22. package/dist/clickhouse/plan/commands.d.ts.map +1 -1
  23. package/dist/clickhouse/plan/commands.js +17 -2
  24. package/dist/clickhouse/plan/diff.d.ts +15 -0
  25. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  26. package/dist/clickhouse/plan/diff.js +43 -1
  27. package/dist/clickhouse/plan/disruption.d.ts.map +1 -1
  28. package/dist/clickhouse/plan/disruption.js +5 -0
  29. package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
  30. package/dist/clickhouse/plan/rebuild-handoff.js +4 -0
  31. package/dist/clickhouse/plan/report.d.ts.map +1 -1
  32. package/dist/clickhouse/plan/report.js +6 -1
  33. package/dist/clickhouse/plan/rules.d.ts +5 -1
  34. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  35. package/dist/clickhouse/plan/rules.js +7 -3
  36. package/dist/clickhouse/plan/statements.d.ts +29 -0
  37. package/dist/clickhouse/plan/statements.d.ts.map +1 -0
  38. package/dist/clickhouse/plan/statements.js +334 -0
  39. package/dist/clickhouse/plan/stream.d.ts +40 -0
  40. package/dist/clickhouse/plan/stream.d.ts.map +1 -0
  41. package/dist/clickhouse/plan/stream.js +107 -0
  42. package/dist/clickhouse/rebuild/observe.d.ts +3 -1
  43. package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
  44. package/dist/clickhouse/rebuild/observe.js +14 -1
  45. package/dist/clickhouse/review-catalog.d.ts +213 -0
  46. package/dist/clickhouse/review-catalog.d.ts.map +1 -0
  47. package/dist/clickhouse/review-catalog.js +330 -0
  48. package/dist/codegen/classifier-page.d.ts.map +1 -1
  49. package/dist/codegen/classifier-page.js +44 -4
  50. package/dist/codegen/clickhouse-module.d.ts.map +1 -1
  51. package/dist/codegen/clickhouse-module.js +3 -0
  52. package/dist/codegen/coverage-page.d.ts.map +1 -1
  53. package/dist/codegen/coverage-page.js +5 -3
  54. package/dist/codegen/docs.js +1 -1
  55. package/dist/codegen/postgres-classifier-page.js +2 -2
  56. package/dist/codegen/registry.d.ts.map +1 -1
  57. package/dist/codegen/registry.js +10 -0
  58. package/dist/composites/catalog.d.ts.map +1 -1
  59. package/dist/composites/catalog.js +100 -0
  60. package/dist/composites/clickhouse.d.ts +2 -0
  61. package/dist/composites/clickhouse.d.ts.map +1 -1
  62. package/dist/composites/clickhouse.js +2 -0
  63. package/dist/composites/otel-tables.d.ts +100 -0
  64. package/dist/composites/otel-tables.d.ts.map +1 -0
  65. package/dist/composites/otel-tables.js +282 -0
  66. package/dist/composites/stream-ingest.d.ts +63 -0
  67. package/dist/composites/stream-ingest.d.ts.map +1 -0
  68. package/dist/composites/stream-ingest.js +39 -0
  69. package/dist/core/statement-class.d.ts +55 -0
  70. package/dist/core/statement-class.d.ts.map +1 -0
  71. package/dist/core/statement-class.js +43 -0
  72. package/dist/coverage/clickhouse.json +1 -1
  73. package/dist/coverage/postgres.json +12 -12
  74. package/dist/coverage.d.ts +16 -6
  75. package/dist/coverage.d.ts.map +1 -1
  76. package/dist/coverage.js +10 -5
  77. package/dist/drizzle-command.d.ts +17 -0
  78. package/dist/drizzle-command.d.ts.map +1 -0
  79. package/dist/drizzle-command.js +91 -0
  80. package/dist/drizzle.d.ts +70 -0
  81. package/dist/drizzle.d.ts.map +1 -0
  82. package/dist/drizzle.js +689 -0
  83. package/dist/files/clickhouse.d.ts.map +1 -1
  84. package/dist/files/clickhouse.js +8 -1
  85. package/dist/files/common.d.ts +13 -0
  86. package/dist/files/common.d.ts.map +1 -1
  87. package/dist/files/common.js +5 -0
  88. package/dist/files/postgres.d.ts.map +1 -1
  89. package/dist/files/postgres.js +35 -3
  90. package/dist/generated/clickhouse.d.ts +2 -0
  91. package/dist/generated/clickhouse.d.ts.map +1 -1
  92. package/dist/generated/clickhouse.js +2 -0
  93. package/dist/import-parser.d.ts +21 -1
  94. package/dist/import-parser.d.ts.map +1 -1
  95. package/dist/import-parser.js +79 -5
  96. package/dist/index.d.ts +1 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +1 -1
  99. package/dist/integrity.json +7 -6
  100. package/dist/lint/post-synth/audit-entries.d.ts.map +1 -1
  101. package/dist/lint/post-synth/audit-entries.js +1 -0
  102. package/dist/lint/post-synth/index.d.ts.map +1 -1
  103. package/dist/lint/post-synth/index.js +2 -0
  104. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  105. package/dist/lint/post-synth/postgres-names.js +3 -0
  106. package/dist/lint/post-synth/sqlch128.d.ts +18 -0
  107. package/dist/lint/post-synth/sqlch128.d.ts.map +1 -0
  108. package/dist/lint/post-synth/sqlch128.js +222 -0
  109. package/dist/lint/rules/postgres-templates.js +2 -2
  110. package/dist/lsp/postgres-template.d.ts.map +1 -1
  111. package/dist/lsp/postgres-template.js +21 -0
  112. package/dist/manifest.json +1 -1
  113. package/dist/mcp/postgres.js +1 -1
  114. package/dist/meta.json +50 -0
  115. package/dist/migrate/from-postgres/detect.d.ts +4 -0
  116. package/dist/migrate/from-postgres/detect.d.ts.map +1 -0
  117. package/dist/migrate/from-postgres/detect.js +7 -0
  118. package/dist/migrate/from-postgres/emit.d.ts +26 -0
  119. package/dist/migrate/from-postgres/emit.d.ts.map +1 -0
  120. package/dist/migrate/from-postgres/emit.js +86 -0
  121. package/dist/migrate/from-postgres/index.d.ts +51 -0
  122. package/dist/migrate/from-postgres/index.d.ts.map +1 -0
  123. package/dist/migrate/from-postgres/index.js +115 -0
  124. package/dist/migrate/from-postgres/translate.d.ts +73 -0
  125. package/dist/migrate/from-postgres/translate.d.ts.map +1 -0
  126. package/dist/migrate/from-postgres/translate.js +261 -0
  127. package/dist/migrate/from-postgres/types.d.ts +51 -0
  128. package/dist/migrate/from-postgres/types.d.ts.map +1 -0
  129. package/dist/migrate/from-postgres/types.js +211 -0
  130. package/dist/migration-classify.d.ts +23 -0
  131. package/dist/migration-classify.d.ts.map +1 -0
  132. package/dist/migration-classify.js +44 -0
  133. package/dist/migration-replay.d.ts +73 -0
  134. package/dist/migration-replay.d.ts.map +1 -0
  135. package/dist/migration-replay.js +294 -0
  136. package/dist/okf/index.md +11 -0
  137. package/dist/okf/rules/SQLCH128.md +11 -0
  138. package/dist/okf/types/PostgresForeignDataWrapper.md +9 -0
  139. package/dist/okf/types/PostgresForeignServer.md +9 -0
  140. package/dist/okf/types/PostgresForeignTable.md +9 -0
  141. package/dist/okf/types/PostgresOperator.md +9 -0
  142. package/dist/okf/types/PostgresOperatorClass.md +9 -0
  143. package/dist/okf/types/PostgresOperatorFamily.md +9 -0
  144. package/dist/okf/types/PostgresPublication.md +9 -0
  145. package/dist/okf/types/PostgresStatistics.md +9 -0
  146. package/dist/okf/types/PostgresTextSearchConfiguration.md +9 -0
  147. package/dist/okf/types/PostgresTextSearchDictionary.md +9 -0
  148. package/dist/op/activities/postgres-migration.d.ts.map +1 -1
  149. package/dist/op/activities/postgres-migration.js +4 -0
  150. package/dist/op/activity-contracts.d.ts +20 -0
  151. package/dist/op/activity-contracts.d.ts.map +1 -1
  152. package/dist/op/activity-contracts.js +2 -0
  153. package/dist/plugin.d.ts.map +1 -1
  154. package/dist/plugin.js +49 -2
  155. package/dist/postgres/access/acl.js +1 -1
  156. package/dist/postgres/access/import.js +1 -1
  157. package/dist/postgres/apply/apply.js +13 -1
  158. package/dist/postgres/apply/statements.d.ts.map +1 -1
  159. package/dist/postgres/apply/statements.js +243 -4
  160. package/dist/postgres/constraint-names.d.ts +30 -0
  161. package/dist/postgres/constraint-names.d.ts.map +1 -0
  162. package/dist/postgres/constraint-names.js +72 -0
  163. package/dist/postgres/entities.d.ts +229 -2
  164. package/dist/postgres/entities.d.ts.map +1 -1
  165. package/dist/postgres/entities.js +326 -5
  166. package/dist/postgres/entity-types.d.ts +10 -0
  167. package/dist/postgres/entity-types.d.ts.map +1 -1
  168. package/dist/postgres/entity-types.js +10 -0
  169. package/dist/postgres/import/generator.d.ts.map +1 -1
  170. package/dist/postgres/import/generator.js +65 -2
  171. package/dist/postgres/import/live-export.d.ts.map +1 -1
  172. package/dist/postgres/import/live-export.js +2 -0
  173. package/dist/postgres/live/catalog-kinds.d.ts +20 -0
  174. package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
  175. package/dist/postgres/live/catalog-kinds.js +452 -0
  176. package/dist/postgres/live/catalog.d.ts +2 -0
  177. package/dist/postgres/live/catalog.d.ts.map +1 -1
  178. package/dist/postgres/live/catalog.js +3 -1
  179. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  180. package/dist/postgres/live/describe-resources.js +12 -2
  181. package/dist/postgres/migrate/backfill.d.ts +2 -0
  182. package/dist/postgres/migrate/backfill.d.ts.map +1 -1
  183. package/dist/postgres/migrate/backfill.js +7 -1
  184. package/dist/postgres/migrate/carry.d.ts +45 -1
  185. package/dist/postgres/migrate/carry.d.ts.map +1 -1
  186. package/dist/postgres/migrate/carry.js +72 -8
  187. package/dist/postgres/migrate/observe.d.ts +28 -4
  188. package/dist/postgres/migrate/observe.d.ts.map +1 -1
  189. package/dist/postgres/migrate/observe.js +45 -13
  190. package/dist/postgres/migrate/op.d.ts +26 -1
  191. package/dist/postgres/migrate/op.d.ts.map +1 -1
  192. package/dist/postgres/migrate/op.js +3 -1
  193. package/dist/postgres/migrate/replication.d.ts +42 -0
  194. package/dist/postgres/migrate/replication.d.ts.map +1 -1
  195. package/dist/postgres/migrate/replication.js +53 -0
  196. package/dist/postgres/migrate/steps.d.ts +35 -2
  197. package/dist/postgres/migrate/steps.d.ts.map +1 -1
  198. package/dist/postgres/migrate/steps.js +158 -12
  199. package/dist/postgres/parser.d.ts +144 -1
  200. package/dist/postgres/parser.d.ts.map +1 -1
  201. package/dist/postgres/parser.js +370 -4
  202. package/dist/postgres/plan/deep.d.ts.map +1 -1
  203. package/dist/postgres/plan/deep.js +43 -4
  204. package/dist/postgres/plan/diff.d.ts.map +1 -1
  205. package/dist/postgres/plan/diff.js +154 -1
  206. package/dist/postgres/plan/normalize.d.ts +28 -2
  207. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  208. package/dist/postgres/plan/normalize.js +260 -1
  209. package/dist/postgres/plan/rules.d.ts +4 -0
  210. package/dist/postgres/plan/rules.d.ts.map +1 -1
  211. package/dist/postgres/plan/rules.js +6 -2
  212. package/dist/postgres/plan/schema.js +1 -1
  213. package/dist/postgres/plan/statements.d.ts +26 -0
  214. package/dist/postgres/plan/statements.d.ts.map +1 -0
  215. package/dist/postgres/plan/statements.js +364 -0
  216. package/dist/postgres/secret-options.d.ts +10 -0
  217. package/dist/postgres/secret-options.d.ts.map +1 -0
  218. package/dist/postgres/secret-options.js +9 -0
  219. package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
  220. package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
  221. package/dist/postgres/testing/roundtrip-schema.js +24 -1
  222. package/dist/postgres.d.ts +1 -1
  223. package/dist/postgres.d.ts.map +1 -1
  224. package/dist/postgres.js +1 -1
  225. package/dist/rules/audit-entries.ts +1 -0
  226. package/dist/rules/postgres-names.ts +2 -0
  227. package/dist/rules/postgres-templates.ts +2 -2
  228. package/dist/rules/sqlch128.ts +230 -0
  229. package/dist/serializer.d.ts +5 -0
  230. package/dist/serializer.d.ts.map +1 -1
  231. package/dist/serializer.js +13 -2
  232. package/dist/sql-types-command.d.ts +13 -0
  233. package/dist/sql-types-command.d.ts.map +1 -1
  234. package/dist/sql-types-command.js +31 -16
  235. package/dist/sql-types.d.ts +22 -0
  236. package/dist/sql-types.d.ts.map +1 -1
  237. package/dist/sql-types.js +10 -6
  238. package/dist/sqlserver/lexer.d.ts +37 -0
  239. package/dist/sqlserver/lexer.d.ts.map +1 -0
  240. package/dist/sqlserver/lexer.js +254 -0
  241. package/dist/sqlserver/parse.d.ts +185 -0
  242. package/dist/sqlserver/parse.d.ts.map +1 -0
  243. package/dist/sqlserver/parse.js +767 -0
  244. package/dist/sqlserver/translate.d.ts +34 -0
  245. package/dist/sqlserver/translate.d.ts.map +1 -0
  246. package/dist/sqlserver/translate.js +495 -0
  247. package/dist/sqlserver/types.d.ts +44 -0
  248. package/dist/sqlserver/types.d.ts.map +1 -0
  249. package/dist/sqlserver/types.js +133 -0
  250. package/dist/sqlserver/validate.d.ts +31 -0
  251. package/dist/sqlserver/validate.d.ts.map +1 -0
  252. package/dist/sqlserver/validate.js +67 -0
  253. package/dist/validate.d.ts.map +1 -1
  254. package/dist/validate.js +10 -0
  255. package/package.json +2 -2
  256. package/src/change-set.ts +2 -1
  257. package/src/clickhouse/apply/statements.ts +25 -0
  258. package/src/clickhouse/import/live-export.ts +20 -5
  259. package/src/clickhouse/live/named-collections.e2e.test.ts +88 -0
  260. package/src/clickhouse/live/named-collections.ts +61 -0
  261. package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -3
  262. package/src/clickhouse/live/undeclarable.ts +20 -4
  263. package/src/clickhouse/named-collections.test.ts +187 -0
  264. package/src/clickhouse/named-collections.ts +224 -0
  265. package/src/clickhouse/parser.ts +2 -1
  266. package/src/clickhouse/plan/commands.ts +17 -2
  267. package/src/clickhouse/plan/diff.ts +57 -1
  268. package/src/clickhouse/plan/disruption.ts +5 -0
  269. package/src/clickhouse/plan/rebuild-handoff.ts +3 -0
  270. package/src/clickhouse/plan/report.ts +6 -1
  271. package/src/clickhouse/plan/rules.ts +14 -4
  272. package/src/clickhouse/plan/statements.test.ts +61 -0
  273. package/src/clickhouse/plan/statements.ts +306 -0
  274. package/src/clickhouse/plan/stream-s3queue.e2e.test.ts +148 -0
  275. package/src/clickhouse/plan/stream.test.ts +237 -0
  276. package/src/clickhouse/plan/stream.ts +111 -0
  277. package/src/clickhouse/rebuild/observe.ts +15 -1
  278. package/src/clickhouse/review-catalog.test.ts +181 -0
  279. package/src/clickhouse/review-catalog.ts +489 -0
  280. package/src/codegen/classifier-page.ts +44 -4
  281. package/src/codegen/clickhouse-module.ts +3 -0
  282. package/src/codegen/coverage-page.ts +5 -3
  283. package/src/codegen/docs.ts +1 -1
  284. package/src/codegen/postgres-classifier-page.ts +2 -2
  285. package/src/codegen/registry.ts +10 -0
  286. package/src/composites/catalog.test.ts +6 -1
  287. package/src/composites/catalog.ts +100 -0
  288. package/src/composites/clickhouse.ts +2 -0
  289. package/src/composites/composites.test.ts +1 -1
  290. package/src/composites/otel-tables.test.ts +118 -0
  291. package/src/composites/otel-tables.ts +339 -0
  292. package/src/composites/stream-ingest.test.ts +114 -0
  293. package/src/composites/stream-ingest.ts +82 -0
  294. package/src/core/statement-class.ts +88 -0
  295. package/src/core/undeclarable.test.ts +6 -5
  296. package/src/coverage/clickhouse.json +1 -1
  297. package/src/coverage/postgres.json +12 -12
  298. package/src/coverage.test.ts +27 -2
  299. package/src/coverage.ts +18 -7
  300. package/src/drizzle-command.ts +94 -0
  301. package/src/drizzle-kit.e2e.test.ts +89 -0
  302. package/src/drizzle-types.e2e.test.ts +67 -0
  303. package/src/drizzle.test.ts +358 -0
  304. package/src/drizzle.ts +742 -0
  305. package/src/files/clickhouse.ts +8 -1
  306. package/src/files/common.ts +14 -1
  307. package/src/files/postgres.ts +34 -3
  308. package/src/generated/clickhouse.ts +3 -0
  309. package/src/generated/lexicon-sql.json +50 -0
  310. package/src/import-parser.sqlserver.test.ts +92 -0
  311. package/src/import-parser.ts +79 -5
  312. package/src/index.ts +1 -1
  313. package/src/lint/post-synth/audit-entries.ts +1 -0
  314. package/src/lint/post-synth/index.ts +2 -0
  315. package/src/lint/post-synth/postgres-checks.test.ts +8 -1
  316. package/src/lint/post-synth/postgres-names.ts +2 -0
  317. package/src/lint/post-synth/sqlch128.test.ts +107 -0
  318. package/src/lint/post-synth/sqlch128.ts +230 -0
  319. package/src/lint/rules/postgres-templates.ts +2 -2
  320. package/src/lsp/postgres-template.ts +20 -0
  321. package/src/mcp/postgres.ts +1 -1
  322. package/src/migrate/from-postgres/cli.test.ts +165 -0
  323. package/src/migrate/from-postgres/detect.ts +7 -0
  324. package/src/migrate/from-postgres/emit.ts +96 -0
  325. package/src/migrate/from-postgres/example.test.ts +205 -0
  326. package/src/migrate/from-postgres/index.ts +122 -0
  327. package/src/migrate/from-postgres/translate.test.ts +242 -0
  328. package/src/migrate/from-postgres/translate.ts +359 -0
  329. package/src/migrate/from-postgres/types.test.ts +116 -0
  330. package/src/migrate/from-postgres/types.ts +236 -0
  331. package/src/migrate/from-postgres/validate.e2e.test.ts +44 -0
  332. package/src/migrate/from-postgres/workspace.e2e.test.ts +114 -0
  333. package/src/migration-classify.test.ts +150 -0
  334. package/src/migration-classify.ts +45 -0
  335. package/src/migration-replay.e2e.test.ts +155 -0
  336. package/src/migration-replay.test.ts +173 -0
  337. package/src/migration-replay.ts +289 -0
  338. package/src/op/__snapshots__/watch-pipeline.test.ts.snap +2 -2
  339. package/src/op/activities/postgres-migration.ts +4 -0
  340. package/src/op/activity-contracts.ts +2 -0
  341. package/src/op/watch-pipeline.test.ts +8 -1
  342. package/src/plugin.ts +51 -2
  343. package/src/postgres/access/acl.ts +1 -1
  344. package/src/postgres/access/import.ts +1 -1
  345. package/src/postgres/apply/apply.ts +14 -1
  346. package/src/postgres/apply/statements.ts +225 -4
  347. package/src/postgres/constraint-names.ts +71 -0
  348. package/src/postgres/database-kinds.test.ts +865 -0
  349. package/src/postgres/entities.ts +541 -5
  350. package/src/postgres/entity-types.ts +10 -0
  351. package/src/postgres/import/generator.ts +54 -2
  352. package/src/postgres/import/live-export.ts +1 -0
  353. package/src/postgres/live/catalog-kinds.ts +494 -0
  354. package/src/postgres/live/catalog.ts +6 -1
  355. package/src/postgres/live/database-kinds.e2e.test.ts +164 -0
  356. package/src/postgres/live/describe-resources.ts +13 -2
  357. package/src/postgres/live/undeclarable.e2e.test.ts +13 -6
  358. package/src/postgres/migrate/backfill.ts +13 -1
  359. package/src/postgres/migrate/carry.ts +103 -8
  360. package/src/postgres/migrate/followups.e2e.test.ts +218 -0
  361. package/src/postgres/migrate/followups.test.ts +199 -0
  362. package/src/postgres/migrate/kinds.e2e.test.ts +5 -5
  363. package/src/postgres/migrate/observe.ts +56 -15
  364. package/src/postgres/migrate/op.ts +28 -1
  365. package/src/postgres/migrate/partition.e2e.test.ts +129 -0
  366. package/src/postgres/migrate/partition.test.ts +206 -0
  367. package/src/postgres/migrate/replication.ts +66 -0
  368. package/src/postgres/migrate/steps.ts +157 -12
  369. package/src/postgres/migrate/subscriber.e2e.test.ts +141 -0
  370. package/src/postgres/migrate/subscriber.test.ts +59 -0
  371. package/src/postgres/parser.ts +455 -4
  372. package/src/postgres/plan/deep.ts +42 -4
  373. package/src/postgres/plan/diff.ts +137 -1
  374. package/src/postgres/plan/normalize.ts +277 -1
  375. package/src/postgres/plan/rules.ts +32 -4
  376. package/src/postgres/plan/schema.ts +1 -1
  377. package/src/postgres/plan/statements.test.ts +78 -0
  378. package/src/postgres/plan/statements.ts +299 -0
  379. package/src/postgres/secret-options.ts +10 -0
  380. package/src/postgres/testing/roundtrip-schema.ts +24 -1
  381. package/src/postgres.ts +33 -0
  382. package/src/serializer.ts +13 -2
  383. package/src/skills/chant-sql-postgres-migration.md +4 -3
  384. package/src/skills/chant-sql-postgres.md +11 -1
  385. package/src/skills/chant-sql.md +2 -1
  386. package/src/sql-files.test.ts +33 -0
  387. package/src/sql-types-command.ts +38 -16
  388. package/src/sql-types.ts +11 -6
  389. package/src/sqlserver/fixture.test.ts +186 -0
  390. package/src/sqlserver/import-cli.test.ts +61 -0
  391. package/src/sqlserver/lexer.ts +260 -0
  392. package/src/sqlserver/parse.ts +865 -0
  393. package/src/sqlserver/translate.test.ts +396 -0
  394. package/src/sqlserver/translate.ts +558 -0
  395. package/src/sqlserver/types.test.ts +102 -0
  396. package/src/sqlserver/types.ts +154 -0
  397. package/src/sqlserver/validate.e2e.test.ts +32 -0
  398. package/src/sqlserver/validate.test.ts +49 -0
  399. package/src/sqlserver/validate.ts +76 -0
  400. package/src/validate.ts +10 -0
  401. package/src/workspace-kinds.test.ts +11 -0
  402. package/workspace-kinds.json +100 -2
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Renames the Op refused in #3322 and makes since (#3331), against the
3
+ * pinned server:
4
+ *
5
+ * - A rename of a column with a default (`defaultAtContract`): writers of
6
+ * either name during the migration, the new column without the default
7
+ * until the contract, and the default there once it ends.
8
+ * - A rename of a `serial` column: the sequence stays on the old column
9
+ * until the contract and is handed to the new one there.
10
+ * - A rename of an identity column: the identity moves to the new column at
11
+ * the contract and carries on where the old one stood.
12
+ * - A rename with a type change (`reverse`): writers of either name, and a
13
+ * `reverse` that does not give the old value back fails the verification.
14
+ *
15
+ * Needs Docker; skips cleanly without it.
16
+ */
17
+
18
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
19
+ import { tmpdir } from "node:os";
20
+ import { join } from "node:path";
21
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
22
+ import { OpRunFailure } from "@intentius/chant/op";
23
+ import { dockerAvailable, startTestPostgres, type TestPostgres } from "../testing/server";
24
+ import { ApprovingLedger, runMigrationOp } from "../testing/migration";
25
+ import type { PostgresClient } from "../live/client";
26
+ import { planPgAgainstServer } from "../plan/commands";
27
+ import { postgresApply } from "../../op/activities/postgres-apply";
28
+ import { postgresMigrationPlan, type PostgresMigrationDeps } from "../../op/activities/postgres-migration";
29
+ import type { PostgresMigrationOpConfig } from "./op";
30
+
31
+ const enabled = await dockerAvailable();
32
+ let server: TestPostgres | undefined;
33
+ let admin: PostgresClient | undefined;
34
+ const dir = mkdtempSync(join(tmpdir(), "chant-pg-followups-"));
35
+
36
+ beforeAll(async () => {
37
+ if (!enabled) return;
38
+ server = await startTestPostgres();
39
+ admin = await server.connect();
40
+ }, 600_000);
41
+
42
+ afterAll(async () => {
43
+ await admin?.end();
44
+ await server?.stop();
45
+ rmSync(dir, { recursive: true, force: true });
46
+ });
47
+
48
+ interface Obj {
49
+ export: string;
50
+ type: string;
51
+ ddl: string;
52
+ dependsOn?: string[];
53
+ }
54
+
55
+ function writeBuild(file: string, objects: Obj[]): string {
56
+ writeFileSync(join(dir, file), JSON.stringify({ dialect: "postgres", postgresMajor: 18, applyOrder: objects.map((o) => o.export), objects: objects.map((o) => ({ dependsOn: [], ...o })) }));
57
+ return file;
58
+ }
59
+
60
+ const MARKER = { stack: "e2e", env: "test" };
61
+ const profile = () => ({
62
+ config: { ownership: MARKER, sql: { profiles: { e2e: { url: server!.endpoint().url, password: { env: "PG_E2E_PASSWORD" } } } } },
63
+ env: { PG_E2E_PASSWORD: server!.endpoint().password },
64
+ });
65
+ const deps = (): PostgresMigrationDeps => ({ ...profile(), log: () => undefined });
66
+ const apply = (file: string) => postgresApply({ buildPath: join(dir, file), environment: "e2e" }, undefined, { ...profile(), log: () => undefined });
67
+ const plan = (file: string) => planPgAgainstServer("e2e", join(dir, file), profile());
68
+ const one = async <T>(sql: string): Promise<T> => (await admin!.query<T>(sql))[0]!;
69
+ const schemaObj = (name: string): Obj => ({ export: name, type: "Postgres::Schema", ddl: `CREATE SCHEMA ${name}` });
70
+ const table = (exportName: string, schema: string, ddl: string): Obj => ({ export: exportName, type: "Postgres::Table", dependsOn: [schema], ddl });
71
+ /** A table of a `bigint` key and one more column, last, with the `-- previously:` hint a rename is declared by. */
72
+ const keyed = (exportName: string, schema: string, column: string, previously?: string): Obj =>
73
+ table(exportName, schema, `CREATE TABLE ${schema}.${exportName} (\n id bigint PRIMARY KEY,\n ${column}${previously ? ` -- previously: ${previously}` : ""}\n)`);
74
+ const config = (over: Pick<PostgresMigrationOpConfig, "name" | "table" | "column" | "output"> & Partial<PostgresMigrationOpConfig>): PostgresMigrationOpConfig => ({
75
+ env: "e2e",
76
+ build: false,
77
+ path: dir,
78
+ retain: "0s",
79
+ stack: MARKER.stack,
80
+ ownershipEnv: MARKER.env,
81
+ ...over,
82
+ });
83
+ const refusal = (tableName: string, column: string, output: string, extra: Partial<PostgresMigrationOpConfig> = {}) =>
84
+ postgresMigrationPlan({ table: tableName, column, buildPath: output, environment: "e2e", stack: MARKER.stack, ownershipEnv: MARKER.env, cwd: dir, ...extra } as never, undefined, deps()).then(
85
+ () => "",
86
+ (e: unknown) => String((e as Error).message),
87
+ );
88
+ const defaultOf = async (t: string, c: string) =>
89
+ (await one<{ d: string | null }>(`SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) AS d FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum WHERE a.attrelid = '${t}'::regclass AND a.attname = '${c}'`)).d;
90
+
91
+ /** Run the Op until it is done, approving each gate; `atGate` runs after the first run, with both columns written and nothing switched. */
92
+ async function runToEnd(c: PostgresMigrationOpConfig, atGate?: () => Promise<void>) {
93
+ const ledger = new ApprovingLedger();
94
+ const runs = [];
95
+ for (let i = 0; i < 4; i++) {
96
+ const r = await runMigrationOp(c, ledger.port, deps);
97
+ runs.push(r);
98
+ if (i === 0) await atGate?.();
99
+ if (r.status !== "gated") break;
100
+ ledger.approveLast();
101
+ }
102
+ return runs;
103
+ }
104
+
105
+ describe.skipIf(!enabled)("a rename of a column with a default", () => {
106
+ const DF = schemaObj("df");
107
+ const users = (c: string, previously?: string) => keyed("users", "df", `${c} text NOT NULL DEFAULT 'none'`, previously);
108
+
109
+ test("refused without defaultAtContract; with it, writers of either name agree and the default lands at the contract", async () => {
110
+ await apply(writeBuild("df-v1.json", [DF, users("email")]));
111
+ await admin!.query("INSERT INTO df.users (id, email) SELECT g, 'u' || g FROM generate_series(1, 500) g");
112
+ writeBuild("df-v2.json", [DF, users("login", "email")]);
113
+ expect(await refusal("df.users", "login", "df-v2.json")).toMatch(/Set defaultAtContract: true/);
114
+
115
+ const runs = await runToEnd(config({ name: "migrate-df-login", table: "df.users", column: "login", output: "df-v2.json", defaultAtContract: true, batchSize: 200 }), async () => {
116
+ // Nothing is switched: the new column has no default, the old one keeps its own.
117
+ expect(await defaultOf("df.users", "login")).toBeNull();
118
+ expect(await defaultOf("df.users", "email")).toBe("'none'::text");
119
+ await admin!.query("INSERT INTO df.users (id) VALUES (1001)"); // neither name
120
+ await admin!.query("INSERT INTO df.users (id, email) VALUES (1002, 'old')"); // the old name
121
+ await admin!.query("INSERT INTO df.users (id, login) VALUES (1003, 'new')"); // the new name
122
+ await admin!.query("UPDATE df.users SET login = 'new2' WHERE id = 1002");
123
+ await admin!.query("UPDATE df.users SET email = 'old2' WHERE id = 1003");
124
+ });
125
+ expect(runs.map((r) => r.status)).toEqual(["gated", "gated", "ok"]);
126
+ expect(await admin!.query("SELECT id::int AS id, login FROM df.users WHERE id > 1000 ORDER BY id")).toEqual([
127
+ { id: 1001, login: "none" },
128
+ { id: 1002, login: "new2" },
129
+ { id: 1003, login: "old2" },
130
+ ]);
131
+ expect(await defaultOf("df.users", "login")).toBe("'none'::text");
132
+ expect(await admin!.query("SELECT attname FROM pg_attribute WHERE attrelid = 'df.users'::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum")).toEqual([{ attname: "id" }, { attname: "login" }]);
133
+ expect((await plan("df-v2.json")).changes).toEqual([]);
134
+ }, 300_000);
135
+ });
136
+
137
+ describe.skipIf(!enabled)("a rename of a serial column", () => {
138
+ const SR = schemaObj("sq");
139
+ const items = (c: string, previously?: string) => keyed("items", "sq", `${c} serial`, previously);
140
+
141
+ test("the sequence stays with the old column until the contract, then belongs to the new one and stays its default", async () => {
142
+ await apply(writeBuild("sq-v1.json", [SR, items("n")]));
143
+ await admin!.query("INSERT INTO sq.items (id) SELECT g FROM generate_series(1, 300) g");
144
+ writeBuild("sq-v2.json", [SR, items("num", "n")]);
145
+
146
+ const runs = await runToEnd(config({ name: "migrate-items-num", table: "sq.items", column: "num", output: "sq-v2.json", defaultAtContract: true }), async () => {
147
+ await admin!.query("INSERT INTO sq.items (id) VALUES (1001)"); // takes the old default; the trigger copies it
148
+ await admin!.query("INSERT INTO sq.items (id, num) VALUES (1002, 7000)"); // the new name
149
+ expect(await one("SELECT n = num AS same FROM sq.items WHERE id = 1001")).toEqual({ same: true });
150
+ expect(await one("SELECT n AS n FROM sq.items WHERE id = 1002")).toEqual({ n: 7000 });
151
+ });
152
+ expect(runs.map((r) => r.status)).toEqual(["gated", "gated", "ok"]);
153
+ expect(await one("SELECT pg_catalog.pg_get_serial_sequence('sq.items', 'num') AS seq")).toEqual({ seq: "sq.items_n_seq" });
154
+ const next = await one<{ id: number; num: number }>("INSERT INTO sq.items (id) VALUES (2000) RETURNING id::int AS id, num");
155
+ expect(next.num).toBeGreaterThan(300);
156
+ expect((await plan("sq-v2.json")).changes).toEqual([]);
157
+ }, 300_000);
158
+ });
159
+
160
+ describe.skipIf(!enabled)("a rename of an identity column", () => {
161
+ const ID = schemaObj("ix");
162
+ const things = (c: string, previously?: string) => keyed("things", "ix", `${c} integer GENERATED ALWAYS AS IDENTITY`, previously);
163
+
164
+ test("the identity moves to the new column at the contract and carries on where the old one stood", async () => {
165
+ await apply(writeBuild("ix-v1.json", [ID, things("n")]));
166
+ await admin!.query("INSERT INTO ix.things (id) SELECT g FROM generate_series(1, 120) g");
167
+ writeBuild("ix-v2.json", [ID, things("num", "n")]);
168
+
169
+ const runs = await runToEnd(config({ name: "migrate-things-num", table: "ix.things", column: "num", output: "ix-v2.json", defaultAtContract: true }), async () => {
170
+ await admin!.query("INSERT INTO ix.things (id) VALUES (1001)"); // the old identity gives 121; the trigger copies it
171
+ expect(await one("SELECT n = num AS same FROM ix.things WHERE id = 1001")).toEqual({ same: true });
172
+ // A writer of the new name gives a value; the trigger sets the old column's ALWAYS identity from it.
173
+ await admin!.query("INSERT INTO ix.things (id, num) VALUES (1002, 5000)");
174
+ expect(await one("SELECT n AS n FROM ix.things WHERE id = 1002")).toEqual({ n: 5000 });
175
+ });
176
+ expect(runs.map((r) => r.status)).toEqual(["gated", "gated", "ok"]);
177
+ expect(await one("SELECT attidentity::text AS identity FROM pg_attribute WHERE attrelid = 'ix.things'::regclass AND attname = 'num'")).toEqual({ identity: "a" });
178
+ const next = await one<{ num: number }>("INSERT INTO ix.things (id) VALUES (2000) RETURNING num");
179
+ expect(next.num).toBeGreaterThan(120);
180
+ expect((await plan("ix-v2.json")).changes).toEqual([]);
181
+ }, 300_000);
182
+ });
183
+
184
+ describe.skipIf(!enabled)("a rename with a type change", () => {
185
+ const RT = schemaObj("rt");
186
+ const people = (column: string, previously?: string) => keyed("people", "rt", column, previously);
187
+
188
+ test("refused without reverse; with it, writers of either name agree and a wrong reverse fails the verification", async () => {
189
+ await apply(writeBuild("rt-v1.json", [RT, people("age_text text NOT NULL")]));
190
+ await admin!.query("INSERT INTO rt.people SELECT g, (g % 90)::text FROM generate_series(1, 400) g");
191
+ writeBuild("rt-v2.json", [RT, people("age integer NOT NULL", "age_text")]);
192
+ expect(await refusal("rt.people", "age", "rt-v2.json")).toMatch(/give that expression as reverse/);
193
+
194
+ // A reverse that does not give the old value back: nothing is switched and onFailure drops what the expand added.
195
+ const bad = await runMigrationOp(config({ name: "migrate-people-age", table: "rt.people", column: "age", output: "rt-v2.json", using: "CAST(age_text AS integer)", reverse: "CAST(age AS text) || 'x'" }), new ApprovingLedger().port, deps).then(
196
+ () => undefined,
197
+ (e: unknown) => e,
198
+ );
199
+ expect(bad).toBeInstanceOf(OpRunFailure);
200
+ expect((bad as OpRunFailure).result.records.find((x) => x.fn === "postgresMigrationVerify")?.error).toMatch(/must give the old value back/);
201
+ expect(await one("SELECT count(*)::int AS n FROM pg_attribute WHERE attrelid = 'rt.people'::regclass AND attname = 'age' AND NOT attisdropped")).toEqual({ n: 0 });
202
+
203
+ const runs = await runToEnd(config({ name: "migrate-people-age", table: "rt.people", column: "age", output: "rt-v2.json", using: "CAST(age_text AS integer)", reverse: "CAST(age AS text)" }), async () => {
204
+ await admin!.query("INSERT INTO rt.people (id, age_text) VALUES (1001, '41')"); // the old name
205
+ await admin!.query("INSERT INTO rt.people (id, age) VALUES (1002, 42)"); // the new name
206
+ await admin!.query("UPDATE rt.people SET age = 43 WHERE id = 1001");
207
+ await admin!.query("UPDATE rt.people SET age_text = '44' WHERE id = 1002");
208
+ expect(await admin!.query("SELECT id::int AS id, age, age_text FROM rt.people WHERE id > 1000 ORDER BY id")).toEqual([
209
+ { id: 1001, age: 43, age_text: "43" },
210
+ { id: 1002, age: 44, age_text: "44" },
211
+ ]);
212
+ });
213
+ expect(runs.map((r) => r.status)).toEqual(["gated", "gated", "ok"]);
214
+ expect(await one("SELECT pg_catalog.format_type(atttypid, atttypmod) AS t FROM pg_attribute WHERE attrelid = 'rt.people'::regclass AND attname = 'age'")).toEqual({ t: "integer" });
215
+ expect(await admin!.query("SELECT attname FROM pg_attribute WHERE attrelid = 'rt.people'::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum")).toEqual([{ attname: "id" }, { attname: "age" }]);
216
+ expect((await plan("rt-v2.json")).changes).toEqual([]);
217
+ }, 300_000);
218
+ });
@@ -0,0 +1,199 @@
1
+ /**
2
+ * The migration Op's follow-ups to #3322 that need no server (#3331): a
3
+ * rename combined with a type change (`reverse`), and a rename of a column
4
+ * with a default, a serial or an identity (`defaultAtContract`). The dual
5
+ * write's body, the contract's statements and the refusals; the e2e is
6
+ * `followups.e2e.test.ts`.
7
+ */
8
+
9
+ import { describe, expect, test } from "vitest";
10
+ import { validateActivitySteps, collectActivityContracts, loadActivityContracts, mergeActivityContracts, type ActivityContract, type OpConfig } from "@intentius/chant/op";
11
+ import * as contractsModule from "../../op/activity-contracts";
12
+ import type { PostgresClient } from "../live/client";
13
+ import type { PgChange } from "../plan/diff";
14
+ import { PostgresMigrationOp, type PostgresMigrationOpConfig } from "./op";
15
+ import { migrationNames } from "./names";
16
+ import { dualWriteBody, contractStatements } from "./steps";
17
+ import { migrationPlanSubject, refuseUnsupported, type MigrationObservation, type RelationRow, type ServerColumn } from "./observe";
18
+
19
+ const BASE: PostgresMigrationOpConfig = { name: "migrate-users-login", env: "prod", table: "app.users", column: "login", build: false };
20
+ const propsOf = (config: PostgresMigrationOpConfig) => (PostgresMigrationOp(config).op as unknown as { props: OpConfig }).props;
21
+
22
+ const names = migrationNames("app", "users", "login", "rename", "email");
23
+
24
+ const observation = (over: Partial<MigrationObservation> = {}): MigrationObservation =>
25
+ ({
26
+ state: "switched",
27
+ names,
28
+ declared: {} as MigrationObservation["declared"],
29
+ column: { name: "login", type: "text" },
30
+ oid: "16384",
31
+ batchKey: "id",
32
+ keyColumns: [{ name: "id", type: "bigint" }],
33
+ carried: [],
34
+ carriedStates: new Map(),
35
+ views: [],
36
+ partitioned: false,
37
+ columns: new Map(),
38
+ changes: [],
39
+ expression: '"email"',
40
+ publications: [],
41
+ major: 18,
42
+ old: { attnum: 2, name: "email", type: "text", notNull: true, generated: false, identity: false },
43
+ trigger: { name: names.trigger },
44
+ fn: { name: names.fn },
45
+ ...over,
46
+ }) as MigrationObservation;
47
+
48
+ describe("a rename with a type change", () => {
49
+ const retyped = migrationNames("app", "users", "age", "rename", "age_text");
50
+ const o = { names: retyped, expression: "CAST(age_text AS integer)", reverse: "CAST(age AS text)" };
51
+
52
+ test("a writer of the old name has the expression applied, a writer of the new name the reverse", () => {
53
+ const body = dualWriteBody(o);
54
+ expect(body).toContain("IF NEW.age IS NULL THEN NEW.age := (SELECT CAST(age_text AS integer) FROM (SELECT NEW.*) AS users); ELSE NEW.age_text := (SELECT CAST(age AS text) FROM (SELECT NEW.*) AS users); END IF;");
55
+ expect(body).toContain("ELSIF NEW.age IS DISTINCT FROM OLD.age THEN\n NEW.age_text := (SELECT CAST(age AS text) FROM (SELECT NEW.*) AS users);");
56
+ expect(body).toContain("ELSE\n NEW.age := (SELECT CAST(age_text AS integer) FROM (SELECT NEW.*) AS users);");
57
+ });
58
+
59
+ test("a plain rename's body is unchanged", () => {
60
+ expect(dualWriteBody({ names, expression: '"email"' })).toContain("IF NEW.login IS NULL THEN NEW.login := NEW.email; ELSE NEW.email := NEW.login; END IF;");
61
+ });
62
+
63
+ test("the reverse is part of what the gate binds", () => {
64
+ const subject = (reverse?: string) =>
65
+ migrationPlanSubject(observation({ state: "migrate", source: { attnum: 2, name: "age_text", type: "text", notNull: false, generated: false, identity: false }, ...(reverse ? { reverse } : {}) }));
66
+ expect(subject("CAST(age AS text)")).toMatchObject({ reverse: "CAST(age AS text)" });
67
+ expect(subject()).not.toHaveProperty("reverse");
68
+ });
69
+ });
70
+
71
+ describe("the contract of a rename that kept its default", () => {
72
+ const sequence = { kind: "serial", schema: "app", name: "users_seq_seq", ident: "app.users_seq_seq" } as const;
73
+
74
+ test("a plain default is set on the new column in the contract's transaction, after the old column goes", () => {
75
+ const o = observation({ column: { name: "login", type: "text", default: "'none'" } });
76
+ expect(contractStatements(o)).toEqual([
77
+ "DROP TRIGGER login__chant_sync ON app.users",
78
+ "DROP FUNCTION app.users__login__chant_sync()",
79
+ "ALTER TABLE app.users DROP COLUMN email",
80
+ "ALTER TABLE app.users ALTER COLUMN login SET DEFAULT 'none'",
81
+ ]);
82
+ });
83
+
84
+ test("a serial's sequence is handed to the new column before the old one is dropped, and stays its default", () => {
85
+ const o = observation({
86
+ column: { name: "login", type: "serial" },
87
+ old: { attnum: 2, name: "email", type: "integer", notNull: true, default: "nextval('app.users_seq_seq'::regclass)", generated: false, identity: false },
88
+ sequence,
89
+ });
90
+ expect(contractStatements(o)).toEqual([
91
+ "DROP TRIGGER login__chant_sync ON app.users",
92
+ "DROP FUNCTION app.users__login__chant_sync()",
93
+ "ALTER SEQUENCE app.users_seq_seq OWNED BY app.users.login",
94
+ "ALTER TABLE app.users DROP COLUMN email",
95
+ "ALTER TABLE app.users ALTER COLUMN login SET DEFAULT nextval('app.users_seq_seq'::regclass)",
96
+ ]);
97
+ });
98
+
99
+ test("an identity moves to the new column under its sequence's name and carries on where the old one stood", () => {
100
+ const o = observation({
101
+ column: { name: "login", type: "integer", generated: { kind: "identity", always: true, options: "(CACHE 5)" } },
102
+ old: { attnum: 2, name: "email", type: "integer", notNull: true, generated: false, identity: true },
103
+ sequence: { ...sequence, kind: "identity" },
104
+ });
105
+ expect(contractStatements(o, { lastValue: "41", isCalled: true })).toEqual([
106
+ "DROP TRIGGER login__chant_sync ON app.users",
107
+ "DROP FUNCTION app.users__login__chant_sync()",
108
+ "ALTER TABLE app.users ALTER COLUMN email DROP IDENTITY",
109
+ "ALTER TABLE app.users DROP COLUMN email",
110
+ "ALTER TABLE app.users ALTER COLUMN login ADD GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME app.users_seq_seq CACHE 5)",
111
+ "SELECT pg_catalog.setval('app.users_seq_seq', 41, true)",
112
+ ]);
113
+ expect(() => contractStatements(o)).toThrow(/needs its sequence's state/);
114
+ });
115
+
116
+ test("a rename without a default drops the old column and nothing else, as before", () => {
117
+ expect(contractStatements(observation())).toEqual([
118
+ "DROP TRIGGER login__chant_sync ON app.users",
119
+ "DROP FUNCTION app.users__login__chant_sync()",
120
+ "ALTER TABLE app.users DROP COLUMN email",
121
+ ]);
122
+ });
123
+
124
+ test("a type change's contract is the plain drop: its trigger went at the switch", () => {
125
+ const t = migrationNames("app", "orders", "amount", "type", "amount");
126
+ const o = observation({
127
+ names: t,
128
+ old: { attnum: 3, name: t.oldColumn, type: "text", notNull: false, generated: false, identity: false },
129
+ column: { name: "amount", type: "numeric", default: "0" },
130
+ });
131
+ delete (o as { trigger?: unknown }).trigger;
132
+ delete (o as { fn?: unknown }).fn;
133
+ expect(contractStatements(o)).toEqual(["ALTER TABLE app.orders DROP COLUMN amount__chant_old"]);
134
+ });
135
+ });
136
+
137
+ describe("what the Op still refuses", () => {
138
+ const rel: RelationRow = { oid: "16384", kind: "r", partition: false, parents: null, children: null, partkey: null, partattrs: null };
139
+ const source = (over: Partial<ServerColumn> = {}): ServerColumn => ({ attnum: 2, name: "email", type: "text", notNull: false, generated: false, identity: false, ...over });
140
+ const rename = { rule: "SQLPG205", object: "app.users", field: "columns.login", before: "email", after: "login" } as PgChange;
141
+ const retype = { rule: "SQLPG208", object: "app.users", field: "columns.login.type", before: "text", after: "integer" } as PgChange;
142
+ // Every query answers nothing, except the primary key's.
143
+ const client = { query: async (sql: string) => (sql.includes("contype = 'p'") ? [{ name: "id", type: "bigint" }] : []) } as unknown as PostgresClient;
144
+ const run = (extra: { reverse?: string; defaultAtContract?: boolean }, src: ServerColumn, column: { name: string; type: string; default?: string }, changes: PgChange[], isRename = true) =>
145
+ refuseUnsupported(
146
+ { client, target: { defaultSchema: "app" }, declared: { props: {}, canonical: { schema: "app", name: "users" } }, column: "login", ...extra } as never,
147
+ names,
148
+ rel,
149
+ src,
150
+ column as never,
151
+ changes,
152
+ isRename,
153
+ changes.find((c) => c.rule !== "SQLPG205"),
154
+ );
155
+
156
+ test("a rename with a type change names reverse, and goes on with it", async () => {
157
+ await expect(run({}, source(), { name: "login", type: "integer" }, [rename, retype])).rejects.toThrow(/give that expression as reverse/);
158
+ await expect(run({ reverse: "CAST(login AS text)" }, source(), { name: "login", type: "integer" }, [rename, retype])).resolves.toMatchObject({ batchKey: "id" });
159
+ });
160
+
161
+ test("reverse is refused where nothing needs it, and may not carry the function's quote", async () => {
162
+ await expect(run({ reverse: "x" }, source(), { name: "login", type: "text" }, [rename])).rejects.toThrow(/reverse is for a rename combined with a type change, and this migration is a rename only/);
163
+ await expect(run({ reverse: "x" }, source(), { name: "login", type: "integer" }, [retype], false)).rejects.toThrow(/a type change without a rename/);
164
+ await expect(run({ reverse: "$chant$" }, source(), { name: "login", type: "integer" }, [rename, retype])).rejects.toThrow(/may not contain \$chant\$/);
165
+ });
166
+
167
+ test("a rename with a type change and a default or identity is refused whatever is set", async () => {
168
+ const msg = /also changes its type[\s\S]*Rename first/;
169
+ await expect(run({ reverse: "x", defaultAtContract: true }, source({ default: "'a'::text" }), { name: "login", type: "integer" }, [rename, retype])).rejects.toThrow(msg);
170
+ await expect(run({ reverse: "x", defaultAtContract: true }, source({ identity: true }), { name: "login", type: "integer" }, [rename, retype])).rejects.toThrow(msg);
171
+ });
172
+
173
+ test("a rename of a column with a default needs defaultAtContract, and says what it gives up", async () => {
174
+ const withDefault = source({ default: "'none'::text" });
175
+ const refused = run({}, withDefault, { name: "login", type: "text", default: "'none'" }, [rename]);
176
+ await expect(refused).rejects.toThrow(/Set defaultAtContract: true/);
177
+ await expect(run({}, withDefault, { name: "login", type: "text", default: "'none'" }, [rename])).rejects.toThrow(/explicit NULL gets the default, and UPDATE \.\.\. SET <new> = DEFAULT sets NULL/);
178
+ await expect(run({}, source({ identity: true }), { name: "login", type: "integer" }, [rename])).rejects.toThrow(/Set defaultAtContract: true/);
179
+ await expect(run({ defaultAtContract: true }, withDefault, { name: "login", type: "text", default: "'none'" }, [rename])).resolves.toMatchObject({ batchKey: "id" });
180
+ });
181
+
182
+ test("defaultAtContract is refused on a rename that has no default to wait for", async () => {
183
+ await expect(run({ defaultAtContract: true }, source(), { name: "login", type: "text" }, [rename])).rejects.toThrow(/defaultAtContract is for a rename of a column with a default/);
184
+ });
185
+ });
186
+
187
+ describe("the Op's configuration", () => {
188
+ test("reverse and defaultAtContract reach every step, and the steps still pass the activity contracts", async () => {
189
+ const config: PostgresMigrationOpConfig = { ...BASE, using: "CAST(age_text AS integer)", reverse: "CAST(age AS text)", defaultAtContract: true };
190
+ const props = propsOf(config);
191
+ const steps = props.phases.flatMap((p) => p.steps).filter((s) => s.kind === "activity" && (s as { fn: string }).fn.startsWith("postgresMigration")) as Array<{ args: Record<string, unknown> }>;
192
+ expect(steps).toHaveLength(9);
193
+ for (const s of steps) expect(s.args).toMatchObject({ reverse: "CAST(age AS text)", defaultAtContract: true });
194
+ const contracts = new Map<string, ActivityContract>();
195
+ collectActivityContracts(contractsModule as unknown as Record<string, unknown>, contracts);
196
+ expect(validateActivitySteps(props, mergeActivityContracts(await loadActivityContracts([]), contracts))).toEqual([]);
197
+ for (const s of propsOf(BASE).phases.flatMap((p) => p.steps)) if (s.kind === "activity") expect((s as { args?: Record<string, unknown> }).args ?? {}).not.toHaveProperty("defaultAtContract");
198
+ });
199
+ });
@@ -10,7 +10,7 @@
10
10
  * - A column of a partitioned table: every partition follows the
11
11
  * partitioned table.
12
12
  * - Refused, each naming why: a partition, an inheritance tree, a column in
13
- * the partition key, an index on a partitioned table, a generated column,
13
+ * the partition key, a key on a partitioned table, a generated column,
14
14
  * a rename of an identity column.
15
15
  *
16
16
  * Needs Docker; skips cleanly without it.
@@ -177,7 +177,7 @@ describe.skipIf(!enabled)("what the Op refuses among the rest", () => {
177
177
  (e: unknown) => String((e as Error).message),
178
178
  );
179
179
 
180
- test("a partition, an inheritance tree, the partition key, an index on a partitioned table, a generated column, a renamed identity", async () => {
180
+ test("a partition, an inheritance tree, the partition key, a key on a partitioned table, a generated column, a renamed identity", async () => {
181
181
  const parted = (v: string, key = "at") => table("parted", "rf", `CREATE TABLE rf.parted (id bigint, at date, v ${v}, PRIMARY KEY (id, at)) PARTITION BY RANGE (${key})`);
182
182
  const p1 = table("parted1", "rf", "CREATE TABLE rf.parted1 PARTITION OF rf.parted FOR VALUES FROM ('2025-01-01') TO ('2026-01-01')", ["parted"]);
183
183
  const parent = (v: string) => table("parent", "rf", `CREATE TABLE rf.parent (id bigint PRIMARY KEY, v ${v})`);
@@ -185,16 +185,16 @@ describe.skipIf(!enabled)("what the Op refuses among the rest", () => {
185
185
  const ident = (c: string, hint = "") => table("ident", "rf", `CREATE TABLE rf.ident (\n ${c} integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,${hint}\n v text\n)`);
186
186
  await apply(writeBuild("rf-v1.json", [RF, parted("integer"), p1, parent("integer"), gen("integer"), ident("id")]));
187
187
  await admin!.query("CREATE TABLE rf.child (extra text) INHERITS (rf.parent)");
188
- await admin!.query("CREATE INDEX parted_v_idx ON rf.parted (v)");
188
+ await admin!.query("ALTER TABLE rf.parted ADD CONSTRAINT parted_v_key UNIQUE (id, at, v)");
189
189
  writeBuild("rf-v2.json", [RF, parted("bigint"), p1, parent("bigint"), gen("bigint"), ident("key", " -- previously: id")]);
190
190
 
191
191
  expect(await refusal("rf.parted1", "v", "rf-v2.json")).toMatch(/^rf\.parted1 is a partition of rf\.parted\. A partition's columns are its partitioned table's: run the Op on rf\.parted/);
192
192
  expect(await refusal("rf.parent", "v", "rf-v2.json")).toMatch(/^rf\.parent has inheritance children \(rf\.child\)\. The Op does not migrate inheritance trees: a write to a child table does not fire the parent's dual-write trigger/);
193
- expect(await refusal("rf.parted", "v", "rf-v2.json")).toMatch(/index rf\.parted_v_idx is a partitioned index, which cannot be built CONCURRENTLY/);
193
+ expect(await refusal("rf.parted", "v", "rf-v2.json")).toMatch(/unique constraint parted_v_key on rf\.parted: on a partitioned table a key cannot be attached from an index built CONCURRENTLY/);
194
194
  expect(await refusal("rf.gen", "g", "rf-v2.json")).toMatch(/^rf\.gen\.g is a generated column\. There is no expand and contract for one/);
195
195
  expect(await refusal("rf.ident", "key", "rf-v2.json")).toMatch(/sequence rf\.ident_id_seq gives the column its values \(an identity column\); during a rename/);
196
196
 
197
- await admin!.query("DROP INDEX rf.parted_v_idx");
197
+ await admin!.query("ALTER TABLE rf.parted DROP CONSTRAINT parted_v_key");
198
198
  writeBuild("rf-v3.json", [RF, parted("integer", "v"), p1]);
199
199
  await admin!.query("DROP TABLE rf.parted");
200
200
  await apply(writeBuild("rf-v3.json", [RF, table("parted", "rf", "CREATE TABLE rf.parted (id bigint, at date, v integer, PRIMARY KEY (id, v)) PARTITION BY RANGE (v)")]));
@@ -24,12 +24,13 @@
24
24
  * - `done`: the server holds the declared column and nothing of the
25
25
  * migration is left.
26
26
  *
27
+ * A partitioned table's indexes are carried over per partition (`./steps.ts`).
27
28
  * Anything else is refused with a {@link MigrationRefusal}, naming what to
28
29
  * do: a table that is not there, a partition (run it on the partitioned
29
30
  * table) or an inheritance tree; no primary key to batch by; a column in the
30
31
  * partition key; a generated column; a column change the Op does not make
31
- * (a rename and a type change at once, a rename of a column with a default
32
- * or a sequence); something that uses the column and is
32
+ * (a rename and a type change without `reverse`, a rename of a column with a
33
+ * default or a sequence without `defaultAtContract`); something that uses the column and is
33
34
  * not carried over to the new one (`./carry.ts`: indexes, key, check and
34
35
  * foreign key constraints and declared views are); a publication that sends
35
36
  * it; a working object chant did not make.
@@ -48,9 +49,9 @@ import { POSTGRES_ENTITY_TYPES } from "../entity-types";
48
49
  import type { ColumnDef } from "../entities";
49
50
  import type { DeclaredPgObject } from "../apply/statements";
50
51
  import { col, migrationNames, MIGRATION_TRAILER_KEY, pgName, type MigrationChange, type MigrationNames } from "./names";
51
- import { publicationsOf, type PublicationHit } from "./replication";
52
+ import { publicationsOf, subscriptionRefusal, subscriptionsOf, type PublicationHit } from "./replication";
52
53
  import { keyText, type KeyColumn } from "./batches";
53
- import { carriedStates, carriedSubject, discoverDependents, replaceColumn, storedType, type CarriedObject, type CarriedState, type CarriedView, type ColumnSequence, type Dependents } from "./carry";
54
+ import { carriedStates, carriedSubject, columnSequence, discoverDependents, replaceColumn, storedType, type CarriedObject, type CarriedState, type CarriedView, type ColumnSequence, type Dependents } from "./carry";
54
55
 
55
56
  /** A refusal: the migration cannot start, or cannot go on, for a reason a person has to act on. */
56
57
  export class MigrationRefusal extends Error {
@@ -102,7 +103,7 @@ export interface MigrationObservation {
102
103
  carriedStates: Map<string, CarriedState>;
103
104
  /** The views that read the old column, made again at the switch. */
104
105
  views: CarriedView[];
105
- /** The sequence that gives the old column its values (identity or serial), moved to the new column at the switch. */
106
+ /** The sequence that gives the old column its values (identity or serial): moved to the new column at the switch for a type change, at the contract for a rename. */
106
107
  sequence?: ColumnSequence;
107
108
  /** The table is partitioned: every step runs on it, and its partitions follow. */
108
109
  partitioned: boolean;
@@ -118,6 +119,10 @@ export interface MigrationObservation {
118
119
  changes: PgChange[];
119
120
  /** The expression the new column's value is computed with, over the old row's columns. */
120
121
  expression: string;
122
+ /** A rename with a type change: the expression over the new row's columns that gives the old column's value. */
123
+ reverse?: string;
124
+ /** A rename keeps its default, serial or identity on the old column until the contract (`defaultAtContract`). */
125
+ defaultAtContract?: boolean;
121
126
  publications: PublicationHit[];
122
127
  major: number;
123
128
  }
@@ -132,6 +137,10 @@ export interface ObserveInput {
132
137
  objects?: readonly DeclaredPgObject[];
133
138
  /** The type change's expression, over the old row's columns. Default: `CAST(<column> AS <declared type>)`. */
134
139
  using?: string;
140
+ /** A rename with a type change: the expression over the new row's columns that gives the old column's value. */
141
+ reverse?: string;
142
+ /** A rename of a column with a default, serial or identity: the declared default lands on the new column at the contract, not the switch. */
143
+ defaultAtContract?: boolean;
135
144
  /** The major the build targets; the server's own wins. */
136
145
  major?: number;
137
146
  }
@@ -216,7 +225,7 @@ async function columnChanges(input: ObserveInput): Promise<{ changes: PgChange[]
216
225
 
217
226
  const ruleText = (c: PgChange) => `${c.rule} ${PG_CLASSIFIER_RULES[c.rule].title} (${c.field}${c.before !== undefined || c.after !== undefined ? `: ${c.before ?? "-"} -> ${c.after ?? "-"}` : ""})`;
218
227
 
219
- interface RelationRow {
228
+ export interface RelationRow {
220
229
  oid: string;
221
230
  kind: string;
222
231
  partition: boolean;
@@ -371,7 +380,8 @@ export async function observeMigration(input: ObserveInput): Promise<MigrationOb
371
380
  );
372
381
  },
373
382
  );
374
- if (change === "type") {
383
+ const retyped = change === "rename" && input.reverse !== undefined;
384
+ if (change === "type" || retyped) {
375
385
  expression = input.using ?? `CAST(${col(sourceName)} AS ${storedType(column.type)})`;
376
386
  if (expression.includes("$chant$")) throw new MigrationRefusal(`${where}: the using expression may not contain $chant$, which quotes the dual-write function's body`);
377
387
  } else if (input.using !== undefined) {
@@ -379,7 +389,7 @@ export async function observeMigration(input: ObserveInput): Promise<MigrationOb
379
389
  }
380
390
  // The declared type as the classifier compared it: the server's spelling (character varying(10), not varchar(10)).
381
391
  const serial = storedType(column.type) !== column.type;
382
- const expectedType = change === "type" ? (serial ? storedType(column.type)! : String(typeChange?.after ?? column.type)) : source.type;
392
+ const expectedType = change === "type" || retyped ? (serial ? storedType(column.type)! : String(typeChange?.after ?? column.type)) : source.type;
383
393
  if (newColumn && newColumn.type !== expectedType) {
384
394
  throw new MigrationRefusal(
385
395
  `${where}: ${newColumn.name} was made as ${newColumn.type}, from another declaration of ${input.column} (now ${column.type}). The run fails so onFailure drops it, and the next run starts again from the current declaration.`,
@@ -395,6 +405,14 @@ export async function observeMigration(input: ObserveInput): Promise<MigrationOb
395
405
  `A migration of the publisher and the subscriber together is chant #3332.`,
396
406
  );
397
407
  }
408
+ const subscriptions = await subscriptionsOf(client, schema, table, major);
409
+ if (subscriptions.length > 0) throw new MigrationRefusal(subscriptionRefusal(where, input.column, subscriptions));
410
+ }
411
+
412
+ // After a rename's switch the old column still holds the default or the identity the contract moves, and its sequence is read for it.
413
+ if (state === "switched" && old && (old.identity || old.default?.includes("nextval("))) {
414
+ const seq = await columnSequence(client, rel.oid, old.attnum);
415
+ if (seq) dependents = { ...dependents, sequence: seq };
398
416
  }
399
417
 
400
418
  return {
@@ -419,13 +437,15 @@ export async function observeMigration(input: ObserveInput): Promise<MigrationOb
419
437
  ...(check ? { check } : {}),
420
438
  changes,
421
439
  expression,
440
+ ...(input.reverse !== undefined && state === "migrate" ? { reverse: input.reverse } : {}),
441
+ ...(input.defaultAtContract && state === "migrate" ? { defaultAtContract: true } : {}),
422
442
  publications,
423
443
  major,
424
444
  };
425
445
  }
426
446
 
427
447
  /** The checks that hold only while there is something to migrate. Returns the batch key and what the Op carries over. */
428
- async function refuseUnsupported(
448
+ export async function refuseUnsupported(
429
449
  input: ObserveInput,
430
450
  names: MigrationNames,
431
451
  rel: RelationRow,
@@ -437,20 +457,38 @@ async function refuseUnsupported(
437
457
  ): Promise<{ batchKey: string; keyColumns: KeyColumn[]; dependents: Pick<Dependents, "carried" | "views" | "sequence"> }> {
438
458
  const where = `${names.schema}.${names.table}`;
439
459
  const oid = rel.oid;
440
- if (rename && typeChange) {
460
+ if (rename && typeChange && input.reverse === undefined) {
441
461
  throw new MigrationRefusal(
442
462
  `${where}: ${source.name} is renamed to ${names.column} and its type changes (${typeChange.before} -> ${typeChange.after}) in one declaration. ` +
443
- `Migrate one at a time: declare the rename with the old type, run the Op to its end, then change the type. ` +
444
- `(In one run both names would stay written until the contract, which needs a conversion back to the old type for writers of the new name; chant #3331.)`,
463
+ `Both names stay written until the contract, so a writer of the new name needs its value converted back to the old type: give that expression as reverse ` +
464
+ `(over the row's columns, the new column by its declared name; using gives the new value from the old one). The verification fails the run unless reverse over every new value gives the old one back. ` +
465
+ `Or migrate one at a time: declare the rename with the old type, run the Op to its end, then change the type.`,
466
+ );
467
+ }
468
+ if (input.reverse !== undefined && !(rename && typeChange)) {
469
+ throw new MigrationRefusal(`${where}.${names.column}: reverse is for a rename combined with a type change, and this migration is ${rename ? "a rename only" : "a type change without a rename"}`);
470
+ }
471
+ if (input.reverse !== undefined && input.reverse.includes("$chant$")) {
472
+ throw new MigrationRefusal(`${where}: the reverse expression may not contain $chant$, which quotes the dual-write function's body`);
473
+ }
474
+ const hasDefault = source.default !== undefined || column.default !== undefined;
475
+ if (rename && typeChange && (hasDefault || source.identity || column.generated?.kind === "identity")) {
476
+ throw new MigrationRefusal(
477
+ `${where}: ${source.name} has a default, serial or identity, and its rename also changes its type. The default is an expression of the old type; converting it, and the sequence's type, ` +
478
+ `is a third conversion on top of using and reverse that nothing verifies. Rename first (defaultAtContract: true), run the Op to its end, then change the type.`,
445
479
  );
446
480
  }
447
- if (rename && (source.default !== undefined || column.default !== undefined)) {
481
+ if (rename && (hasDefault || source.identity) && !input.defaultAtContract) {
448
482
  throw new MigrationRefusal(
449
- `${where}: ${source.name} has a default (${source.default ?? column.default}). During a rename both columns are written, each from the other, ` +
483
+ `${where}: ${source.name} has a default (${source.default ?? column.default ?? "identity"}). During a rename both columns are written, each from the other, ` +
450
484
  `and a row inserted by a writer that names one column would get the other's default, which the trigger cannot tell from a value it was given. ` +
451
- `Drop the default first (the applier does), rename, then declare it again. A rename that keeps its default needs versioned views (chant #3331).`,
485
+ `Set defaultAtContract: true to leave the new column without the default until the contract, which gives the default to the new column in the contract's transaction; ` +
486
+ `until then an INSERT that names the new column with an explicit NULL gets the default, and UPDATE ... SET <new> = DEFAULT sets NULL. Or drop the default first (the applier does), rename, then declare it again.`,
452
487
  );
453
488
  }
489
+ if (input.defaultAtContract && !(rename && (hasDefault || source.identity))) {
490
+ throw new MigrationRefusal(`${where}.${names.column}: defaultAtContract is for a rename of a column with a default, serial or identity, and this migration is not one`);
491
+ }
454
492
  if (source.generated || (column.generated && column.generated.kind !== "identity")) {
455
493
  throw new MigrationRefusal(
456
494
  `${where}.${source.name} is a generated column. There is no expand and contract for one: Postgres cannot make an existing column generated, so the new column could only be added generated, ` +
@@ -475,6 +513,7 @@ async function refuseUnsupported(
475
513
  declared: input.declared,
476
514
  defaultSchema: input.target.defaultSchema,
477
515
  partitioned: rel.kind === "p",
516
+ ...(input.defaultAtContract ? { keepSequence: true } : {}),
478
517
  });
479
518
  if (dependents.refused.length > 0) {
480
519
  throw new MigrationRefusal(
@@ -510,6 +549,8 @@ export function migrationPlanSubject(o: MigrationObservation): Record<string, un
510
549
  from: o.source ? { name: o.source.name, type: o.source.type, notNull: o.source.notNull, default: o.source.default ?? null } : null,
511
550
  to: { name: o.column.name, type: o.column.type ?? null, notNull: o.column.notNull === true, default: o.column.default ?? null, comment: o.column.comment ?? null },
512
551
  expression: o.expression,
552
+ ...(o.reverse !== undefined ? { reverse: o.reverse } : {}),
553
+ ...(o.defaultAtContract ? { defaultAtContract: true } : {}),
513
554
  batchKey: o.batchKey,
514
555
  rules: o.changes.map((c) => c.rule),
515
556
  ...(o.carried.length > 0 || o.views.length > 0 || o.sequence ? { dependents: carriedSubject(o) } : {}),