@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,236 @@
1
+ /**
2
+ * The Postgres to ClickHouse type map (#4399).
3
+ *
4
+ * A type either maps with no judgment (`note` absent) or carries a `note`
5
+ * naming what the map could not keep. Only the first kind is mechanical: the
6
+ * translator writes no diagnostic for it. A type this file does not know (an
7
+ * enum, a domain, an extension type) maps to `String` with a note, because a
8
+ * mirror still has to hold the value.
9
+ *
10
+ * Source types and defaults are from the Postgres data types chapter
11
+ * (https://www.postgresql.org/docs/current/datatype.html): `timestamp` and
12
+ * `time` take a precision of 0 to 6 and default to microseconds, `numeric`
13
+ * with no precision holds any value, `money` is a fixed-point type with a
14
+ * locale-set scale, `float(n)` is `real` for n up to 24 and `double
15
+ * precision` above.
16
+ *
17
+ * Target types are ClickHouse types of the pinned server (`../../spec/pin.ts`);
18
+ * `types.test.ts` checks every family this file writes against the generated
19
+ * catalog.
20
+ */
21
+
22
+ export interface MappedType {
23
+ ok: true;
24
+ /** The ClickHouse type, before `Nullable`. */
25
+ type: string;
26
+ /** The declared maximum length of a character type, when it has one. */
27
+ maxLength?: number;
28
+ /** The column is `serial`, `bigserial` or `smallserial`: an integer with a sequence behind it. */
29
+ serial?: boolean;
30
+ /** The column is an array. ClickHouse cannot make an `Array` `Nullable`. */
31
+ array?: boolean;
32
+ /** Present when the map is a judgment: what was lost or chosen. */
33
+ note?: { message: string; evidence: string[] };
34
+ }
35
+
36
+ export type TypeMapping = MappedType;
37
+
38
+ interface ParsedType {
39
+ /** Lower case words of the type, schema prefix and quotes removed: `timestamp with time zone`. */
40
+ base: string;
41
+ args: number[];
42
+ /** Array dimensions. */
43
+ dims: number;
44
+ raw: string;
45
+ }
46
+
47
+ /** Split a column type as written into its words, its parenthesised arguments and its array dimensions. */
48
+ export function parseType(text: string): ParsedType {
49
+ let rest = text.trim();
50
+ let dims = 0;
51
+ for (;;) {
52
+ const m = /\s*\[\s*\d*\s*\]\s*$/.exec(rest);
53
+ if (!m) break;
54
+ rest = rest.slice(0, m.index);
55
+ dims++;
56
+ }
57
+ const arrayWord = /\s+array(\s*\[\s*\d*\s*\])?$/i.exec(rest);
58
+ if (arrayWord) {
59
+ rest = rest.slice(0, arrayWord.index);
60
+ dims++;
61
+ }
62
+ const args: number[] = [];
63
+ rest = rest.replace(/\(([^)]*)\)/g, (_all, inner: string) => {
64
+ for (const part of inner.split(",")) {
65
+ const n = Number(part.trim());
66
+ if (Number.isFinite(n)) args.push(n);
67
+ }
68
+ return " ";
69
+ });
70
+ const base = rest
71
+ .replace(/"/g, "")
72
+ .replace(/^pg_catalog\./i, "")
73
+ .replace(/\s+/g, " ")
74
+ .trim()
75
+ .toLowerCase();
76
+ return { base, args, dims, raw: text.trim() };
77
+ }
78
+
79
+ const STRING_NOTE = (what: string, evidence: string[]) => ({ message: `${what} is read as String.`, evidence });
80
+
81
+ function mapScalar(t: ParsedType): MappedType {
82
+ const { base, args } = t;
83
+ switch (base) {
84
+ case "smallint":
85
+ case "int2":
86
+ return { ok: true, type: "Int16" };
87
+ case "integer":
88
+ case "int":
89
+ case "int4":
90
+ return { ok: true, type: "Int32" };
91
+ case "bigint":
92
+ case "int8":
93
+ return { ok: true, type: "Int64" };
94
+ case "smallserial":
95
+ case "serial2":
96
+ return { ok: true, type: "Int16", serial: true };
97
+ case "serial":
98
+ case "serial4":
99
+ return { ok: true, type: "Int32", serial: true };
100
+ case "bigserial":
101
+ case "serial8":
102
+ return { ok: true, type: "Int64", serial: true };
103
+ case "real":
104
+ case "float4":
105
+ return { ok: true, type: "Float32" };
106
+ case "double precision":
107
+ case "float8":
108
+ return { ok: true, type: "Float64" };
109
+ case "float":
110
+ return { ok: true, type: (args[0] ?? 53) <= 24 ? "Float32" : "Float64" };
111
+ case "numeric":
112
+ case "decimal": {
113
+ if (args.length === 0) {
114
+ return {
115
+ ok: true,
116
+ type: "Decimal(38, 18)",
117
+ note: {
118
+ message: "numeric with no precision is read as Decimal(38, 18).",
119
+ evidence: ["Postgres stores up to 131072 digits before the point and 16383 after; a ClickHouse Decimal has a fixed precision and scale", "a value that does not fit fails the insert"],
120
+ },
121
+ };
122
+ }
123
+ const p = args[0]!;
124
+ const s = args[1] ?? 0;
125
+ if (p > 76 || s < 0 || s > p) {
126
+ return {
127
+ ok: true,
128
+ type: "Decimal(76, 38)",
129
+ note: { message: `numeric(${String(p)}, ${String(s)}) is read as Decimal(76, 38).`, evidence: ["ClickHouse Decimal holds at most 76 digits"] },
130
+ };
131
+ }
132
+ return { ok: true, type: `Decimal(${String(p)}, ${String(s)})` };
133
+ }
134
+ case "money":
135
+ return {
136
+ ok: true,
137
+ type: "Decimal(19, 2)",
138
+ note: { message: "money is read as Decimal(19, 2).", evidence: ["money takes its scale from the lc_monetary setting of the source server, and two decimals is the common one"] },
139
+ };
140
+ case "boolean":
141
+ case "bool":
142
+ return { ok: true, type: "Bool" };
143
+ case "text":
144
+ case "varchar":
145
+ case "character varying":
146
+ case "name":
147
+ case "citext":
148
+ return { ok: true, type: "String", ...(args[0] !== undefined ? { maxLength: args[0] } : {}) };
149
+ case "char":
150
+ case "character":
151
+ case "bpchar":
152
+ return {
153
+ ok: true,
154
+ type: "String",
155
+ ...(args[0] !== undefined ? { maxLength: args[0] } : {}),
156
+ ...(args[0] !== undefined && args[0] > 1 ? { note: STRING_NOTE("char(n)", ["Postgres pads a char(n) value with spaces to n characters; a String keeps the value as it is loaded"]) } : {}),
157
+ };
158
+ case "bytea":
159
+ return { ok: true, type: "String" };
160
+ case "uuid":
161
+ return { ok: true, type: "UUID" };
162
+ case "date":
163
+ return { ok: true, type: "Date32" };
164
+ case "time":
165
+ case "time without time zone":
166
+ return { ok: true, type: `Time64(${String(args[0] ?? 6)})` };
167
+ case "timetz":
168
+ case "time with time zone":
169
+ return { ok: true, type: "String", note: STRING_NOTE("timetz", ["a time of day with an offset has no ClickHouse type; the offset is kept in the text"]) };
170
+ case "timestamp":
171
+ case "timestamp without time zone":
172
+ return { ok: true, type: `DateTime64(${String(args[0] ?? 6)})` };
173
+ case "timestamptz":
174
+ case "timestamp with time zone":
175
+ return { ok: true, type: `DateTime64(${String(args[0] ?? 6)}, 'UTC')` };
176
+ case "interval":
177
+ return { ok: true, type: "String", note: STRING_NOTE("interval", ["a ClickHouse Interval type cannot be stored in a column", "the value is kept as Postgres prints it"]) };
178
+ case "json":
179
+ case "jsonb":
180
+ return { ok: true, type: "JSON" };
181
+ case "oid":
182
+ return { ok: true, type: "UInt32" };
183
+ case "inet":
184
+ case "cidr":
185
+ return {
186
+ ok: true,
187
+ type: "IPv6",
188
+ note: { message: `${base} is read as IPv6.`, evidence: ["an IPv6 column holds an IPv4 address in its mapped form", "the prefix length of a network is not kept"] },
189
+ };
190
+ case "xml":
191
+ case "macaddr":
192
+ case "macaddr8":
193
+ case "tsvector":
194
+ case "tsquery":
195
+ case "bit":
196
+ case "bit varying":
197
+ case "varbit":
198
+ case "point":
199
+ case "line":
200
+ case "lseg":
201
+ case "box":
202
+ case "path":
203
+ case "polygon":
204
+ case "circle":
205
+ return { ok: true, type: "String", note: STRING_NOTE(base, [`ClickHouse has no ${base} type`, "the value is kept as Postgres prints it"]) };
206
+ default:
207
+ return {
208
+ ok: true,
209
+ type: "String",
210
+ note: STRING_NOTE(t.raw, [`${t.raw} is not a built-in type chant maps (an enum, a domain, a composite or an extension type)`, "the value is kept as Postgres prints it; an enum could be an Enum8 or Enum16 once its labels are settled"]),
211
+ };
212
+ }
213
+ }
214
+
215
+ /** Map a column type as the declaration wrote it. */
216
+ export function mapPostgresType(text: string): TypeMapping {
217
+ const t = parseType(text);
218
+ const inner = mapScalar(t);
219
+ if (t.dims === 0) return inner;
220
+ let type = inner.type;
221
+ for (let i = 0; i < t.dims; i++) type = `Array(${type})`;
222
+ return {
223
+ ok: true,
224
+ type,
225
+ array: true,
226
+ ...(inner.serial ? { serial: true } : {}),
227
+ note: {
228
+ message: `${t.raw} is read as ${type}.`,
229
+ evidence: [
230
+ ...(inner.note ? [inner.note.message] : []),
231
+ "ClickHouse cannot make an Array Nullable, so a NULL array is stored as an empty one",
232
+ "Postgres does not enforce the declared dimensions or lengths of an array",
233
+ ],
234
+ },
235
+ };
236
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Fourth Done-when box of #4399: `--validate` creates the translated example
3
+ * on the pinned ClickHouse server, as a bare table and as a `CdcMirror` call
4
+ * with its view, and reports a statement the server refuses by name.
5
+ *
6
+ * Needs Docker; skips cleanly without it. One throwaway container per call,
7
+ * removed afterwards even on failure. Written, not run, in the session that
8
+ * added it (no e2e and no Docker, by the brief).
9
+ */
10
+
11
+ import { join } from "node:path";
12
+ import { describe, expect, test } from "vitest";
13
+ import { dockerAvailable } from "../../clickhouse/container";
14
+ import type { Declarable } from "@intentius/chant/declarable";
15
+ import { transform } from "./index";
16
+ import * as source from "../../../examples/postgres-cdc-source/src/shop";
17
+ import { translatePostgres } from "./translate";
18
+
19
+ const enabled = await dockerAvailable();
20
+ const sourceFile = join(import.meta.dirname, "../../../examples/postgres-cdc-source");
21
+
22
+ describe.skipIf(!enabled)("the emulator accepts the translated example", () => {
23
+ test.each([
24
+ ["bare tables", false],
25
+ ["CdcMirror calls and their views", true],
26
+ ])("every statement of %s is created", async (_name, useComposites) => {
27
+ const r = await transform("", { emit: "ts", useComposites, sourceFile });
28
+ const found = await r.validate!();
29
+ expect(found, found.map((d) => String(d.message)).join("\n")).toEqual([]);
30
+ }, 300_000);
31
+
32
+ test("a statement the server refuses is an error naming the table", async () => {
33
+ // A key column the table does not have is caught by the server, not by the translator.
34
+ const entities = new Map(Object.entries(source) as Array<[string, Declarable]>);
35
+ const t = translatePostgres(entities, { composite: false });
36
+ t.mirrors[0]!.primaryKey = ["no_such_column"];
37
+ const { validateOnEmulator } = await import("../../sqlserver/validate");
38
+ const { objectsToIR } = await import("../../clickhouse/import/ir");
39
+ const { mirrorObjects } = await import("./emit");
40
+ const found = await validateOnEmulator(objectsToIR(mirrorObjects(t.mirrors, false)));
41
+ expect(found).toHaveLength(1);
42
+ expect(found[0]).toMatchObject({ ruleId: "SQLRV012", severity: "error", entity: "customers" });
43
+ }, 300_000);
44
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Third Done-when box of #4399, through `chant workspace check`: a workspace
3
+ * with the Postgres member `db` (the DDL of `examples/postgres-cdc-source`)
4
+ * and the ClickHouse member `mirror`. `chant migrate` writes the mirror and its
5
+ * links; dropping a mirrored column from the Postgres side then fails the
6
+ * check, naming the mirror, until the mirror follows.
7
+ *
8
+ * The unit version of the same facts, on the example's own declarations, is
9
+ * `example.test.ts`. This one reads a real declaration file and folds the
10
+ * member's `.sql` file the way `chant workspace check` does, so it is e2e
11
+ * under the 5 s unit budget (#2817). Written, not run, in the session that
12
+ * added it.
13
+ */
14
+
15
+ import { execFileSync } from "node:child_process";
16
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { dirname, join } from "node:path";
19
+ import { afterAll, afterEach, describe, expect, test, vi } from "vitest";
20
+ import { runDeclarationChecks } from "@intentius/chant/workspace/checks";
21
+ import { parseArgs } from "@intentius/chant/cli/parse-args";
22
+ import { runMigrate } from "@intentius/chant/cli/handlers/migrate";
23
+
24
+ const scratch: string[] = [];
25
+ afterAll(() => {
26
+ for (const d of scratch) rmSync(d, { recursive: true, force: true });
27
+ });
28
+ afterEach(() => vi.restoreAllMocks());
29
+
30
+ function repo(files: Record<string, string>): string {
31
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-4399-")));
32
+ scratch.push(root);
33
+ execFileSync("git", ["init", "-q"], { cwd: root });
34
+ for (const [path, text] of Object.entries(files)) {
35
+ mkdirSync(dirname(join(root, path)), { recursive: true });
36
+ writeFileSync(join(root, path), text);
37
+ }
38
+ return root;
39
+ }
40
+
41
+ const schema = (email: string) => `CREATE SCHEMA shop;
42
+
43
+ CREATE TABLE shop.customers (
44
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
45
+ ${email} country text NOT NULL,
46
+ created_at timestamptz NOT NULL DEFAULT now()
47
+ );
48
+
49
+ CREATE TABLE shop.orders (
50
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
51
+ customer_id bigint NOT NULL REFERENCES shop.customers (id),
52
+ total numeric(12, 2) NOT NULL,
53
+ status text NOT NULL,
54
+ placed_at timestamptz NOT NULL DEFAULT now()
55
+ );
56
+ `;
57
+
58
+ const EMAIL = " email text NOT NULL,\n";
59
+ const config = (dialect: string) => `import type { ChantConfig } from "@intentius/chant";\nexport default { lexicons: ["sql"], sql: { dialect: "${dialect}" } } satisfies ChantConfig;\n`;
60
+
61
+ function shop(): string {
62
+ return repo({
63
+ "chant.workspace.json": `${JSON.stringify({ name: "shop", schema: 1, members: [{ name: "db", dir: "db", kind: "chant" }, { name: "mirror", dir: "mirror", kind: "chant" }] }, null, 2)}\n`,
64
+ "db/chant.config.ts": config("postgres"),
65
+ "db/schema.sql": schema(EMAIL),
66
+ "mirror/chant.config.ts": config("clickhouse"),
67
+ });
68
+ }
69
+
70
+ async function migrate(root: string): Promise<number> {
71
+ vi.spyOn(console, "error").mockImplementation(() => {});
72
+ return runMigrate({
73
+ args: parseArgs(["migrate", join(root, "db/schema.sql"), "--from", "sql/postgres", "--to", "sql/clickhouse", "--use-composites", "--output", join(root, "mirror/src/mirrors.ts")]),
74
+ plugins: [],
75
+ serializers: [],
76
+ } as never);
77
+ }
78
+
79
+ describe("chant migrate links the mirror to the Postgres member, and chant workspace check holds them", () => {
80
+ test("the links resolve; dropping a mirrored column fails the check naming the mirror; unlinking it and migrating again passes", async () => {
81
+ const root = shop();
82
+ expect(await migrate(root)).toBe(0);
83
+ const declared = JSON.parse(readFileSync(join(root, "chant.workspace.json"), "utf8")) as { members: Array<{ name: string; links?: Array<{ member: string; output: string }> }> };
84
+ const links = declared.members.find((m) => m.name === "mirror")!.links!;
85
+ expect(links).toHaveLength(9);
86
+ expect(links).toContainEqual({ member: "db", output: "shop.customers.email" });
87
+
88
+ const ok = await runDeclarationChecks(root);
89
+ expect(ok.diagnostics.filter((d) => d.ruleId.startsWith("WSP09"))).toEqual([]);
90
+ expect(ok.links.filter((r) => r.origin === "declared" && r.status === "resolved")).toHaveLength(9);
91
+
92
+ // The Postgres side drops email.
93
+ writeFileSync(join(root, "db/schema.sql"), schema(""));
94
+ const failed = await runDeclarationChecks(root);
95
+ expect(failed.ok).toBe(false);
96
+ expect(failed.diagnostics.map((d) => `${d.ruleId}:${d.entity}`)).toEqual(["WSP093:mirror"]);
97
+ expect(failed.diagnostics[0]!.message).toContain("db has no output shop.customers.email");
98
+
99
+ // The mirror follows: the link goes with the column, and migrating again adds nothing it should not.
100
+ const edited = { ...declared, members: declared.members.map((m) => (m.name === "mirror" ? { ...m, links: m.links!.filter((l) => l.output !== "shop.customers.email") } : m)) };
101
+ writeFileSync(join(root, "chant.workspace.json"), `${JSON.stringify(edited, null, 2)}\n`);
102
+ expect(await migrate(root)).toBe(0);
103
+ const after = await runDeclarationChecks(root);
104
+ expect(after.diagnostics.filter((d) => d.ruleId.startsWith("WSP09"))).toEqual([]);
105
+ });
106
+
107
+ test("a renamed column with a previously hint tells the mirror where it went", async () => {
108
+ const root = shop();
109
+ await migrate(root);
110
+ writeFileSync(join(root, "db/schema.sql"), schema(" email_hash text NOT NULL, -- previously: email\n"));
111
+ const report = await runDeclarationChecks(root);
112
+ expect(report.diagnostics[0]!.message).toContain("it was renamed to shop.customers.email_hash, so link to that instead");
113
+ });
114
+ });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Classifying the migration files a pull request adds (#4407), for goose,
3
+ * golang-migrate, clickhouse-migrations and Flyway. Each fixture directory is
4
+ * an in-memory tree at the base commit and another at the head, with one
5
+ * file added; the answer is what the plan note lists. Offline: nothing here
6
+ * opens a server.
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { describe, expect, test, vi } from "vitest";
12
+ import { LINK_READERS, type KindLinkRead } from "@intentius/chant/workspace/kind-link-readers";
13
+ import { classifyNewMigrations, newMigrationGroups } from "@intentius/chant/workspace/new-migrations";
14
+ import { renderMigrationLines } from "@intentius/chant/pr-migrations";
15
+ import type { WorkspaceTree } from "@intentius/chant/workspace/tree";
16
+ import { classifyMigrationFiles } from "./migration-classify";
17
+
18
+ function memoryTree(files: Record<string, string>): WorkspaceTree {
19
+ const dirs = new Set([""]);
20
+ for (const f of Object.keys(files)) {
21
+ const parts = f.split("/");
22
+ for (let i = 1; i < parts.length; i++) dirs.add(parts.slice(0, i).join("/"));
23
+ }
24
+ return {
25
+ label: "",
26
+ stat: (p) => (p in files ? "file" : dirs.has(p) ? "dir" : undefined),
27
+ list(p) {
28
+ if (!dirs.has(p)) return undefined;
29
+ const prefix = p === "" ? "" : `${p}/`;
30
+ const seen = new Map<string, "file" | "dir">();
31
+ for (const f of Object.keys(files)) {
32
+ if (!f.startsWith(prefix)) continue;
33
+ const rest = f.slice(prefix.length);
34
+ seen.set(rest.split("/")[0]!, rest.includes("/") ? "dir" : "file");
35
+ }
36
+ return [...seen].map(([name, type]) => ({ name, type }));
37
+ },
38
+ read(p) {
39
+ if (!(p in files)) throw new Error(`no such file ${p}`);
40
+ return files[p]!;
41
+ },
42
+ };
43
+ }
44
+
45
+ const sql = { name: "sql", classifyMigrations: classifyMigrationFiles };
46
+
47
+ /** The files the pull request adds, classified, as the plan note lists them. */
48
+ async function classify(reader: keyof typeof LINK_READERS, fields: Record<string, string>, base: Record<string, string>, added: Record<string, string>) {
49
+ const head = memoryTree({ ...base, ...added });
50
+ const read = new Map<string, KindLinkRead>([["m", LINK_READERS[reader]({ member: { name: "m" } as never, dir: "db", fields: fields as never, tree: head })]]);
51
+ const groups = newMigrationGroups([{ name: "m", dir: "db" }], read, memoryTree(base));
52
+ return classifyNewMigrations(groups, head, [sql]);
53
+ }
54
+
55
+ const rules = (f: Awaited<ReturnType<typeof classify>>[number]) => f.statements.flatMap((s) => s.changes.map((c) => c.rule));
56
+
57
+ describe("a new file in each migration directory kind appears with classes", () => {
58
+ test("goose: the Up section only", async () => {
59
+ const [f, ...rest] = await classify(
60
+ "goose-dir",
61
+ { dialect: "postgres" },
62
+ { "db/00001_init.sql": "-- +goose Up\nCREATE TABLE orders (id bigint PRIMARY KEY, status text);\n-- +goose Down\nDROP TABLE orders;\n" },
63
+ {
64
+ "db/00002_idx.sql":
65
+ "-- +goose Up\nCREATE INDEX orders_status_idx ON orders (status);\nALTER TABLE orders ADD COLUMN note text;\n-- +goose Down\nDROP INDEX orders_status_idx;\n",
66
+ },
67
+ );
68
+ expect(rest).toEqual([]);
69
+ expect(f).toMatchObject({ member: "m", reader: "goose-dir", file: "db/00002_idx.sql", dialect: "postgres" });
70
+ expect(rules(f!)).toEqual(["SQLPG241", "SQLPG201"]);
71
+ expect(f!.statements[0]!.changes[0]).toMatchObject({ class: "concurrently", lock: "SHARE" });
72
+ });
73
+
74
+ test("golang-migrate: the .up file, not the .down file", async () => {
75
+ const out = await classify(
76
+ "golang-migrate-dir",
77
+ { dialect: "postgres" },
78
+ { "db/000001_init.up.sql": "CREATE TABLE t (id bigint);", "db/000001_init.down.sql": "DROP TABLE t;" },
79
+ { "db/000002_fk.up.sql": "ALTER TABLE t ADD CONSTRAINT fk FOREIGN KEY (id) REFERENCES u (id) NOT VALID;", "db/000002_fk.down.sql": "ALTER TABLE t DROP CONSTRAINT fk;" },
80
+ );
81
+ expect(out.map((f) => f.file)).toEqual(["db/000002_fk.up.sql"]);
82
+ expect(rules(out[0]!)).toEqual(["SQLPG217"]);
83
+ });
84
+
85
+ test("clickhouse-migrations: a sort-key change is a rebuild", async () => {
86
+ const [f] = await classify(
87
+ "clickhouse-migrations-dir",
88
+ {},
89
+ { "db/1_events.sql": "CREATE TABLE events (user_id UInt64, ts DateTime) ENGINE = MergeTree ORDER BY ts;" },
90
+ { "db/2_key.sql": "ALTER TABLE events MODIFY ORDER BY (user_id, ts);" },
91
+ );
92
+ expect(f).toMatchObject({ dialect: "clickhouse" });
93
+ expect(f!.statements[0]).toMatchObject({ outcome: "classified", changes: [{ rule: "SQLCH220", class: "rebuild", label: "REBUILD" }] });
94
+ });
95
+
96
+ test("flyway: placeholders are substituted before the statements are read", async () => {
97
+ const [f] = await classify(
98
+ "flyway-dir",
99
+ { dialect: "postgres" },
100
+ { "db/V1__init.sql": "CREATE TABLE ${schema}.t (id bigint);", "db/flyway.conf": "flyway.locations=filesystem:.\nflyway.placeholders.schema=app\n" },
101
+ { "db/V2__idx.sql": "CREATE INDEX CONCURRENTLY i ON ${schema}.t (id);" },
102
+ );
103
+ expect(f).toMatchObject({ file: "db/V2__idx.sql", type: "versioned" });
104
+ expect(f!.statements[0]!.sql).toBe("CREATE INDEX CONCURRENTLY i ON app.t (id)");
105
+ expect(rules(f!)).toEqual(["SQLPG240"]);
106
+ });
107
+
108
+ test("flyway: a placeholder with no value is reported on the file, not thrown", async () => {
109
+ const [f] = await classify("flyway-dir", { dialect: "postgres" }, { "db/V1__init.sql": "SELECT 1;" }, { "db/V2__x.sql": "CREATE TABLE ${missing}.t (id bigint);" });
110
+ expect(f!.statements).toEqual([]);
111
+ expect(f!.error).toContain("missing");
112
+ });
113
+ });
114
+
115
+ describe("the plan note lines", () => {
116
+ test("a Postgres CREATE INDEX without CONCURRENTLY is listed with its lock", async () => {
117
+ const files = await classify("goose-dir", { dialect: "postgres" }, { "db/1_a.sql": "-- +goose Up\nSELECT 1;\n" }, { "db/2_b.sql": "-- +goose Up\nCREATE INDEX i ON orders (status);\n" });
118
+ const lines = renderMigrationLines(files).join("\n");
119
+ expect(lines).toContain("`db/2_b.sql` (member `m`, postgres)");
120
+ expect(lines).toContain("`CREATE INDEX i ON orders (status)`: needs CONCURRENTLY (SQLPG241, SHARE)");
121
+ });
122
+
123
+ test("a ClickHouse sort-key change is listed as a rebuild", async () => {
124
+ const files = await classify("clickhouse-migrations-dir", {}, { "db/1_a.sql": "SELECT 1;" }, { "db/2_b.sql": "ALTER TABLE events MODIFY ORDER BY (a, b);" });
125
+ expect(renderMigrationLines(files).join("\n")).toContain("REBUILD (SQLCH220)");
126
+ });
127
+
128
+ test("an undecided statement names both rules", async () => {
129
+ const files = await classify("clickhouse-migrations-dir", {}, { "db/1_a.sql": "SELECT 1;" }, { "db/2_b.sql": "ALTER TABLE events MODIFY COLUMN user_id UInt32;" });
130
+ expect(renderMigrationLines(files).join("\n")).toContain("undecided between SQLCH210 background rewrite and SQLCH211 REBUILD");
131
+ });
132
+ });
133
+
134
+ describe("offline", () => {
135
+ test("classifying contacts no server and starts no process", async () => {
136
+ const fetched = vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("network used"));
137
+ await classify("goose-dir", { dialect: "postgres" }, { "db/1_a.sql": "-- +goose Up\nSELECT 1;\n" }, { "db/2_b.sql": "-- +goose Up\nDROP TABLE t;\n" });
138
+ await classify("clickhouse-migrations-dir", {}, { "db/1_a.sql": "SELECT 1;" }, { "db/2_b.sql": "DROP TABLE t;" });
139
+ expect(fetched).not.toHaveBeenCalled();
140
+ fetched.mockRestore();
141
+ });
142
+
143
+ test("the classifier modules import no live binding, client or process API", () => {
144
+ for (const f of ["migration-classify.ts", "postgres/plan/statements.ts", "clickhouse/plan/statements.ts", "core/statement-class.ts"]) {
145
+ const src = readFileSync(join(import.meta.dirname, f), "utf8");
146
+ const imports = [...src.matchAll(/(?:from|import\()\s*["']([^"']+)["']/g)].map((m) => m[1]!);
147
+ for (const spec of imports) expect(spec, `${f} imports ${spec}`).not.toMatch(/\/live\/|\/apply\/|\/http$|child_process|node:net|node:http|node:https|\/bind$/);
148
+ }
149
+ });
150
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Classify the migration files a pull request adds (#4407), the lexicon's
3
+ * answer to core's `classifyMigrations` hook. The "check" adoption level:
4
+ * each new file's up statements are read and given the class the schema-diff
5
+ * classifier gives the same change, and nothing else happens. No server is
6
+ * contacted, no migration tool runs and nothing is applied, so it needs no
7
+ * environment binding (compare `chant sql replay`, which does).
8
+ *
9
+ * The up statements are the ones replay would run: goose's `-- +goose Up`
10
+ * section, the whole file for golang-migrate and clickhouse-migrations, and a
11
+ * Flyway file after placeholder substitution with the configuration's values.
12
+ */
13
+
14
+ import type { MigrationFileClassification } from "@intentius/chant/pr-migrations";
15
+ import { findFlywayConfig, readFlywayConfig, substitute } from "@intentius/chant/workspace/flyway-layout";
16
+ import type { WorkspaceTree } from "@intentius/chant/workspace/tree";
17
+ import { classifyClickHouseStatements } from "./clickhouse/plan/statements";
18
+ import { gooseUpStatements, splitStatements } from "./migration-replay";
19
+ import { classifyPgStatements } from "./postgres/plan/statements";
20
+
21
+ export const MIGRATION_DIALECTS = ["postgres", "clickhouse"] as const;
22
+
23
+ /** The reader names whose files this lexicon reads. */
24
+ const READERS = new Set(["goose-dir", "golang-migrate-dir", "clickhouse-migrations-dir", "flyway-dir"]);
25
+
26
+ export function classifyMigrationFiles(options: { reader: string; dialect: string; tree: WorkspaceTree; dir: string; files: string[] }): MigrationFileClassification[] {
27
+ const { reader, dialect, tree, dir, files } = options;
28
+ if (!READERS.has(reader) || !(MIGRATION_DIALECTS as readonly string[]).includes(dialect)) return [];
29
+ const classify = dialect === "postgres" ? classifyPgStatements : classifyClickHouseStatements;
30
+ let flyway: ReturnType<typeof readFlywayConfig> | undefined;
31
+ return files.map((file) => {
32
+ try {
33
+ const text = tree.read(file);
34
+ let statements: string[];
35
+ if (reader === "goose-dir") statements = gooseUpStatements(text);
36
+ else if (reader === "flyway-dir") {
37
+ flyway ??= readFlywayConfig(tree, findFlywayConfig(tree, dir), dir);
38
+ statements = splitStatements(substitute(text, file, flyway));
39
+ } else statements = splitStatements(text);
40
+ return { file, statements: classify(statements) };
41
+ } catch (err) {
42
+ return { file, statements: [], error: err instanceof Error ? err.message : String(err) };
43
+ }
44
+ });
45
+ }