@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,205 @@
1
+ /**
2
+ * The first and third Done-when boxes of #4399, on the example pair.
3
+ *
4
+ * `examples/postgres-cdc-source` translated with `--use-composites` gives the
5
+ * `CdcMirror` calls of `examples/cdc-mirror`, apart from the choices the
6
+ * review diagnostics ask for. And the two projects' column names no longer
7
+ * need a test that compares them: the mirror links each column to the
8
+ * Postgres member's output, so dropping a Postgres column fails the link.
9
+ * The link check here is core's `resolveLinks`, the function behind WSP093;
10
+ * `workspace.e2e.test.ts` runs `chant workspace check` itself.
11
+ */
12
+
13
+ import { describe, expect, test } from "vitest";
14
+ import { parseDeclaration } from "@intentius/chant/workspace/declaration";
15
+ import { resolveLinks, type MemberHandles } from "@intentius/chant/workspace/links";
16
+ import type { Declarable } from "@intentius/chant/declarable";
17
+ import { CdcMirror } from "../../composites/cdc-mirror";
18
+ import { schema, table } from "../../postgres/entities";
19
+ import { sqlWorkspaceOutputs } from "../../workspace-outputs";
20
+ import * as source from "../../../examples/postgres-cdc-source/src/shop";
21
+ import * as mirrors from "../../../examples/cdc-mirror/src/mirrors";
22
+ import { mirrorColumns, orderBySql } from "./emit";
23
+ import { translatePostgres, type Mirror } from "./translate";
24
+
25
+ const sourceEntities = (): Map<string, Declarable> => new Map(Object.entries(source) as Array<[string, Declarable]>);
26
+ const ddl = (e: unknown): string => (e as { props: { ddl: string } }).props.ddl;
27
+
28
+ /**
29
+ * The call for a mirror once its review is done. `types` replaces a column's
30
+ * type, `call` adds the call's own props.
31
+ */
32
+ function reviewed(m: Mirror, review: { types?: Record<string, string>; call?: Record<string, string> } = {}) {
33
+ const columns = m.columns.map((c) => ({ ...c, type: review.types?.[c.name] ?? c.type }));
34
+ return { name: m.name, columns: mirrorColumns({ ...m, columns }), primaryKey: orderBySql(m), ...review.call };
35
+ }
36
+
37
+ describe("postgres-cdc-source translated with --use-composites", () => {
38
+ const t = translatePostgres(sourceEntities(), { composite: true });
39
+ const byName = Object.fromEntries(t.mirrors.map((m) => [m.name, m]));
40
+
41
+ test("is the two tables, keyed on their primary keys", () => {
42
+ expect(t.mirrors.map((m) => m.name)).toEqual(["customers", "orders"]);
43
+ expect(t.mirrors.map((m) => m.primaryKey)).toEqual([["id"], ["id"]]);
44
+ });
45
+
46
+ test("the call's columns are the mechanical type map, with the column comment kept", () => {
47
+ expect(mirrorColumns(byName.customers!)).toBe("id Int64, email String COMMENT 'PII, hashed downstream', country String, created_at DateTime64(6, 'UTC')");
48
+ expect(mirrorColumns(byName.orders!)).toBe("id Int64, customer_id Int64, total Decimal(12, 2), status String, placed_at DateTime64(6, 'UTC')");
49
+ });
50
+
51
+ test("customers is examples/cdc-mirror's declaration once country is LowCardinality (SQLRV004)", () => {
52
+ const call = CdcMirror(reviewed(byName.customers!, { types: { country: "LowCardinality(String)" } }));
53
+ expect(ddl(call.table)).toBe(ddl(mirrors.customers.table));
54
+ expect(ddl(call.current)).toBe(ddl(mirrors.customers.current));
55
+ });
56
+
57
+ test("orders is examples/cdc-mirror's declaration once status is LowCardinality and the pipeline's names and partition are chosen (SQLRV004, SQLRV002)", () => {
58
+ const call = CdcMirror(
59
+ reviewed(byName.orders!, {
60
+ types: { status: "LowCardinality(String)" },
61
+ call: { version: "_peerdb_version", deleted: "_peerdb_is_deleted", partitionBy: "toYYYYMM(placed_at)" },
62
+ }),
63
+ );
64
+ expect(ddl(call.table)).toBe(ddl(mirrors.orders.table));
65
+ expect(ddl(call.current)).toBe(ddl(mirrors.orders.current));
66
+ });
67
+
68
+ test("each of those edits is asked for by an open diagnostic", () => {
69
+ const at = (rule: string, entity: string, text: string) => t.diagnostics.some((d) => d.ruleId === rule && d.entity === entity && d.message.includes(text));
70
+ expect(at("SQLRV004", "shop.customers", "shop.customers.country")).toBe(true);
71
+ expect(at("SQLRV004", "shop.orders", "shop.orders.status")).toBe(true);
72
+ expect(at("SQLRV002", "shop.orders", "ReplacingMergeTree(_version, _is_deleted)")).toBe(true);
73
+ });
74
+
75
+ test("the diagnostics are the decisions: no mechanical map writes one", () => {
76
+ const rules = new Set(t.diagnostics.map((d) => d.ruleId));
77
+ expect([...rules].sort()).toEqual(["SQLRV001", "SQLRV002", "SQLRV004", "SQLRV005", "SQLRV006", "SQLRV008", "SQLRV009", "SQLRV011"]);
78
+ // No type diagnostic (SQLRV007): bigint, text, numeric(12, 2) and timestamptz are mechanical.
79
+ expect(t.diagnostics.filter((d) => d.ruleId === "SQLRV007")).toEqual([]);
80
+ // No Nullable finding: every column is NOT NULL or a key.
81
+ expect(t.diagnostics.filter((d) => d.ruleId === "SQLRV003")).toEqual([]);
82
+ });
83
+
84
+ test("the foreign key from orders to customers and the index on orders are said not to be carried", () => {
85
+ const fk = t.diagnostics.find((d) => d.ruleId === "SQLRV005");
86
+ expect(fk).toMatchObject({ entity: "shop.orders", severity: "warning" });
87
+ expect(fk!.message).toContain("REFERENCES shop.customers (id)");
88
+ const ix = t.diagnostics.find((d) => d.ruleId === "SQLRV009");
89
+ expect(ix!.message).toContain("orders_by_customer_idx");
90
+ });
91
+
92
+ test("the links are the Postgres outputs of every mirrored column", () => {
93
+ expect(t.outputs).toEqual([
94
+ "shop.customers.id",
95
+ "shop.customers.email",
96
+ "shop.customers.country",
97
+ "shop.customers.created_at",
98
+ "shop.orders.id",
99
+ "shop.orders.customer_id",
100
+ "shop.orders.total",
101
+ "shop.orders.status",
102
+ "shop.orders.placed_at",
103
+ ]);
104
+ });
105
+ });
106
+
107
+ /** The two projects as a workspace: `db` is the Postgres member, `mirror` links to each column. */
108
+ function workspace(entities: ReadonlyMap<string, Declarable>, links: readonly string[]) {
109
+ const declaration = parseDeclaration(
110
+ JSON.stringify({
111
+ name: "shop",
112
+ schema: 1,
113
+ members: [
114
+ { name: "db", dir: "postgres-cdc-source", kind: "chant" },
115
+ { name: "mirror", dir: "cdc-mirror", kind: "chant", links: links.map((output) => ({ member: "db", output })) },
116
+ ],
117
+ }),
118
+ "chant.workspace.json",
119
+ );
120
+ const report = sqlWorkspaceOutputs(entities as never)!;
121
+ const handles = new Map<string, MemberHandles>([
122
+ [
123
+ "db",
124
+ {
125
+ member: "db",
126
+ outputs: report.outputs.map((o) => ({ name: o.name, ...(o.previously?.length ? { previously: o.previously } : {}) })),
127
+ inputs: [],
128
+ complete: (report.gaps ?? []).length === 0,
129
+ why: null,
130
+ exposesNone: false,
131
+ },
132
+ ],
133
+ ]);
134
+ return resolveLinks(declaration, handles);
135
+ }
136
+
137
+ describe("the workspace link replaces the test that held the column names together", () => {
138
+ const links = translatePostgres(sourceEntities(), { composite: true }).outputs;
139
+
140
+ test("every link of the translated example resolves against the Postgres member", () => {
141
+ const rows = workspace(sourceEntities(), links);
142
+ expect(rows).toHaveLength(9);
143
+ expect(rows.map((r) => r.status)).toEqual(Array(9).fill("resolved"));
144
+ });
145
+
146
+ test("the mirror's hand-written column names are among the outputs, so a drift would show", () => {
147
+ // examples/cdc-mirror declares these columns; each is a Postgres output.
148
+ const declared = [...Object.values(mirrors)].flatMap((m) => ((m.table as unknown as { props: { columns: Array<{ name: string }> } }).props.columns.map((c) => c.name)));
149
+ const outputs = new Set(sqlWorkspaceOutputs(sourceEntities() as never)!.outputs.map((o) => o.name));
150
+ const named = declared.filter((n) => !n.startsWith("_"));
151
+ expect(named.length).toBe(9);
152
+ for (const n of named) expect([...outputs].some((o) => o.endsWith(`.${n}`)), n).toBe(true);
153
+ });
154
+
155
+ /** The customers declaration without email, as an author who drops the column would leave it. */
156
+ function withoutEmail(): Map<string, Declarable> {
157
+ const entities = sourceEntities();
158
+ const shop = schema`CREATE SCHEMA shop`;
159
+ entities.set("shop", shop as unknown as Declarable);
160
+ entities.set(
161
+ "customers",
162
+ table`
163
+ CREATE TABLE ${shop}.customers (
164
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
165
+ country text NOT NULL,
166
+ created_at timestamptz NOT NULL DEFAULT now()
167
+ )` as unknown as Declarable,
168
+ );
169
+ return entities;
170
+ }
171
+
172
+ test("dropping a mirrored column from the Postgres declaration fails the mirror's link (WSP093)", () => {
173
+ const rows = workspace(withoutEmail(), links);
174
+ const missing = rows.filter((r) => r.status === "missing");
175
+ expect(missing).toHaveLength(1);
176
+ expect(missing[0]).toMatchObject({ consumer: "mirror", producer: "db", output: "shop.customers.email" });
177
+ expect(missing[0]!.reason).toContain("db has no output shop.customers.email");
178
+ });
179
+
180
+ test("the link passes again once the mirror follows: migrate again, and the dropped column is gone from the links", () => {
181
+ const entities = withoutEmail();
182
+ const again = translatePostgres(entities, { composite: true });
183
+ expect(again.outputs).not.toContain("shop.customers.email");
184
+ expect(workspace(entities, again.outputs).every((r) => r.status === "resolved")).toBe(true);
185
+ });
186
+
187
+ test("renaming a column fails the link, and a previously hint says where it went", () => {
188
+ const entities = sourceEntities();
189
+ const shop = schema`CREATE SCHEMA shop`;
190
+ entities.set("shop", shop as unknown as Declarable);
191
+ entities.set(
192
+ "customers",
193
+ table`
194
+ CREATE TABLE ${shop}.customers (
195
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
196
+ email_hash text NOT NULL, -- previously: email
197
+ country text NOT NULL,
198
+ created_at timestamptz NOT NULL DEFAULT now()
199
+ )` as unknown as Declarable,
200
+ );
201
+ const missing = workspace(entities, links).filter((r) => r.status === "missing");
202
+ expect(missing).toHaveLength(1);
203
+ expect(missing[0]!.reason).toContain("renamed to shop.customers.email_hash");
204
+ });
205
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `chant migrate --from sql/postgres --to sql/clickhouse <file or member>`
3
+ * (#4399): the Postgres tables a CDC pipeline mirrors, as the ClickHouse
4
+ * tables that receive them.
5
+ *
6
+ * ## Naming
7
+ *
8
+ * A `chant migrate` side names a lexicon, and the sql lexicon holds two
9
+ * dialects whose builds never mix, so a side is `<lexicon>/<dialect>`:
10
+ * `sql/postgres` and `sql/clickhouse`. `--to sql/clickhouse` loads the sql
11
+ * lexicon and asks it for `migrationSource("sql/postgres", "sql/clickhouse")`.
12
+ * The same shape would name a later pair (`sql/clickhouse` to `sql/postgres`)
13
+ * without a new flag. A side with no slash is a lexicon alone, as for
14
+ * `--from github --to gitlab`.
15
+ *
16
+ * ## The source
17
+ *
18
+ * - a `.sql` file of Postgres DDL;
19
+ * - a TypeScript file or a member directory of `table` declarations, read by
20
+ * a folded build of the project it belongs to (`chant sql types` reads the
21
+ * same way), so `${shop}.customers` and other interpolations resolve.
22
+ *
23
+ * ## What it writes
24
+ *
25
+ * `--emit ts` (the default here) writes declarations: `table` templates, or
26
+ * `CdcMirror` calls with `--use-composites`. `--emit sql` writes the DDL.
27
+ * Every decision that is not the type map is a finding from the shared
28
+ * catalog (`../../clickhouse/review-catalog.ts`), reported as SARIF with
29
+ * `--report`, and a `warning` fails the run under `--strict`.
30
+ *
31
+ * `links` names the Postgres output each mirrored column reads
32
+ * (`shop.customers.email`). In a workspace, `chant migrate` writes them as
33
+ * `links` of the ClickHouse member (`../../../../packages/core/src/workspace/migrate-links.ts`),
34
+ * so `chant workspace check` fails when the Postgres declaration drops or
35
+ * renames one.
36
+ *
37
+ * `--validate` creates the emitted DDL on a throwaway server of the pinned
38
+ * ClickHouse image (`../../sqlserver/validate.ts` runs it).
39
+ */
40
+
41
+ import { existsSync, statSync } from "node:fs";
42
+ import { dirname } from "node:path";
43
+ import type { Declarable } from "@intentius/chant/declarable";
44
+ import type { MigrateOptions, MigrationResult, MigrationSource } from "@intentius/chant/lexicon";
45
+ import { looksLikePostgres } from "./detect";
46
+ import { emitSql, emitTs, mirrorObjects } from "./emit";
47
+ import { translatePostgres, type Translation } from "./translate";
48
+
49
+ export const FROM_POSTGRES = "sql/postgres";
50
+ export const TO_CLICKHOUSE = "sql/clickhouse";
51
+
52
+ /** Whether `from` and `to` name this pair. */
53
+ export const isPostgresToClickHouse = (from: string, to?: string): boolean => from === FROM_POSTGRES && (to === undefined || to === TO_CLICKHOUSE);
54
+
55
+ /** The Postgres declarations at `sourceFile`: a `.sql` file read as DDL, else a folded build of its project. */
56
+ export async function loadPostgresEntities(content: string, sourceFile: string | undefined): Promise<Map<string, Declarable>> {
57
+ const isDir = sourceFile !== undefined && existsSync(sourceFile) && statSync(sourceFile).isDirectory();
58
+ if (!isDir && (sourceFile === undefined || sourceFile.endsWith(".sql"))) {
59
+ const { sqlFileEntities } = await import("../../sql-files");
60
+ return sqlFileEntities("postgres", [{ ddl: content, origin: sourceFile ?? "the DDL" }], { known: new Map() });
61
+ }
62
+ const { buildFolded, loadProject } = await import("../../sql-types-command");
63
+ const project = await loadProject(isDir ? sourceFile! : dirname(sourceFile!));
64
+ const result = await buildFolded(project, "chant migrate", "nothing was translated");
65
+ if (!result) throw new Error(`the build of ${sourceFile} failed`);
66
+ return new Map(result.entities as ReadonlyMap<string, Declarable>);
67
+ }
68
+
69
+ /** A finding as the lint diagnostic `chant migrate` writes as SARIF. */
70
+ function toDiagnostic(file: string, d: Translation["diagnostics"][number], strict: boolean): Record<string, unknown> {
71
+ return {
72
+ file,
73
+ line: d.line ?? 1,
74
+ column: d.column ?? 1,
75
+ ruleId: d.ruleId,
76
+ // Under --strict a warning is an error, which is what fails the run (core reads the severity).
77
+ severity: strict && d.severity === "warning" ? "error" : d.severity,
78
+ message: d.message,
79
+ ...(d.entity ? { entity: d.entity } : {}),
80
+ };
81
+ }
82
+
83
+ export async function transform(content: string, opts: MigrateOptions): Promise<MigrationResult> {
84
+ const emit = opts.emit ?? "ts";
85
+ if (emit === "yaml") throw new Error("--emit yaml has no meaning here; use --emit ts or --emit sql");
86
+ const composite = opts.useComposites === true;
87
+ const file = opts.sourceFile ?? "schema.sql";
88
+ const entities = await loadPostgresEntities(content, opts.sourceFile);
89
+ const translation = translatePostgres(entities, { composite });
90
+ if (translation.mirrors.length === 0) {
91
+ const refused = translation.diagnostics.filter((d) => d.severity === "error").map((d) => d.message);
92
+ throw new Error(`no Postgres table to mirror in ${file}${refused.length > 0 ? `: ${refused.join(" ")}` : ""}`);
93
+ }
94
+ const output = emit === "sql" ? emitSql(translation.mirrors, composite) : emitTs(translation.mirrors, composite);
95
+ const diagnostics = translation.diagnostics.map((d) => toDiagnostic(file, d, opts.strict === true));
96
+ const provenance = translation.mirrors.map((m) => ({
97
+ sourceKey: `${m.source.schema}.${m.source.table}`,
98
+ rule: composite ? "SQLMIG-CDC-MIRROR" : "SQLMIG-TABLE",
99
+ category: "translated",
100
+ note: `${String(m.columns.length)} columns; ORDER BY ${m.primaryKey.length === 0 ? "tuple()" : `(${m.primaryKey.join(", ")})`}`,
101
+ }));
102
+ return {
103
+ output,
104
+ provenance,
105
+ diagnostics,
106
+ links: translation.outputs,
107
+ validate: async () => {
108
+ const [{ objectsToIR }, { validateOnEmulator }] = await Promise.all([import("../../clickhouse/import/ir"), import("../../sqlserver/validate")]);
109
+ const found = await validateOnEmulator(objectsToIR(mirrorObjects(translation.mirrors, composite)));
110
+ return found.map((d) => ({ file, line: 1, column: 1, ruleId: d.ruleId, severity: d.severity, message: d.message, ...(d.entity ? { entity: d.entity } : {}) }));
111
+ },
112
+ };
113
+ }
114
+
115
+ /** The `migrationSource` of the sql lexicon for this pair. */
116
+ export function postgresToClickHouseSource(): MigrationSource {
117
+ return {
118
+ detect: looksLikePostgres,
119
+ directory: true,
120
+ transform,
121
+ };
122
+ }
@@ -0,0 +1,242 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { sqlFileEntities } from "../../sql-files";
3
+ import { table as clickhouseTable } from "../../clickhouse/entities";
4
+ import { emitSql, emitTs, mirrorObjects, tableSql } from "./emit";
5
+ import { looksLikePostgres } from "./detect";
6
+ import { transform } from "./index";
7
+ import { translatePostgres, type Translation } from "./translate";
8
+
9
+ const entities = (ddl: string) => sqlFileEntities("postgres", [{ ddl, origin: "schema.sql" }], { known: new Map() });
10
+ const run = (ddl: string, composite = false): Translation => translatePostgres(entities(ddl), { composite });
11
+ const rules = (t: Translation, entity?: string) => t.diagnostics.filter((d) => entity === undefined || d.entity === entity).map((d) => d.ruleId);
12
+ const find = (t: Translation, ruleId: string, text = "") => t.diagnostics.find((d) => d.ruleId === ruleId && d.message.includes(text));
13
+
14
+ describe("sort key and engine", () => {
15
+ test("the primary key is the sort key; a composite key keeps its order", () => {
16
+ const t = run("CREATE TABLE docs (tenant_id bigint NOT NULL, id bigint NOT NULL, body text NOT NULL, PRIMARY KEY (tenant_id, id))");
17
+ expect(t.mirrors[0]!.primaryKey).toEqual(["tenant_id", "id"]);
18
+ expect(find(t, "SQLRV001")!.message).toContain("ORDER BY (tenant_id, id) suggested from the primary key");
19
+ expect(tableSql(t.mirrors[0]!)).toContain("ORDER BY (tenant_id, id)");
20
+ });
21
+
22
+ test("with no primary key the first unique key is the sort key, and the finding says it passed over nothing else", () => {
23
+ const t = run("CREATE TABLE docs (slug text NOT NULL UNIQUE, body text NOT NULL)");
24
+ expect(t.mirrors[0]!.primaryKey).toEqual(["slug"]);
25
+ expect(find(t, "SQLRV001")!.message).toContain("unique key");
26
+ });
27
+
28
+ test("with no key at all the sort key is tuple(), and the engine finding still warns", () => {
29
+ const t = run("CREATE TABLE log (line text NOT NULL)");
30
+ expect(t.mirrors[0]!.primaryKey).toEqual([]);
31
+ expect(find(t, "SQLRV001")!.message).toContain("tuple()");
32
+ expect(tableSql(t.mirrors[0]!)).toContain("ORDER BY tuple()");
33
+ expect(find(t, "SQLRV002")!.severity).toBe("warning");
34
+ });
35
+
36
+ test("every table gets the ReplacingMergeTree engine with the two columns the composite names", () => {
37
+ const sql = tableSql(run("CREATE TABLE t (id bigint PRIMARY KEY)").mirrors[0]!);
38
+ expect(sql).toContain("ENGINE = ReplacingMergeTree(_version, _is_deleted)");
39
+ expect(sql).toContain("_version UInt64");
40
+ expect(sql).toContain("_is_deleted UInt8");
41
+ });
42
+
43
+ test("a source column named like one the mirror adds refuses the table with an error", () => {
44
+ const t = run("CREATE TABLE t (id bigint PRIMARY KEY, _version int)");
45
+ expect(t.mirrors).toEqual([]);
46
+ expect(find(t, "SQLRV011", "_version")).toMatchObject({ severity: "error" });
47
+ });
48
+
49
+ test("a nullable unique key needs allow_nullable_key, which a CdcMirror call cannot carry", () => {
50
+ const ddl = "CREATE TABLE t (code text UNIQUE, n int NOT NULL)";
51
+ const bare = run(ddl);
52
+ expect(tableSql(bare.mirrors[0]!)).toContain("SETTINGS allow_nullable_key = 1");
53
+ const composite = run(ddl, true);
54
+ expect(find(composite, "SQLRV001", "no SETTINGS")).toMatchObject({ severity: "warning" });
55
+ });
56
+ });
57
+
58
+ describe("Nullable, defaults and column kinds", () => {
59
+ test("a nullable column is Nullable with a finding, and a default is named as the way out", () => {
60
+ const t = run("CREATE TABLE t (id bigint PRIMARY KEY, note text, qty integer DEFAULT 0, tag text NOT NULL)");
61
+ const types = Object.fromEntries(t.mirrors[0]!.columns.map((c) => [c.name, c.type]));
62
+ expect(types).toEqual({ id: "Int64", note: "Nullable(String)", qty: "Nullable(Int32)", tag: "String" });
63
+ expect(find(t, "SQLRV003", "t.qty")!.message).toContain("DEFAULT 0");
64
+ expect(find(t, "SQLRV003", "t.note")!.message).toContain("no default");
65
+ expect(t.diagnostics.filter((d) => d.ruleId === "SQLRV003")).toHaveLength(2);
66
+ });
67
+
68
+ test("a primary key, identity and serial column are not Nullable whatever the DDL says", () => {
69
+ const t = run("CREATE TABLE t (a bigint PRIMARY KEY, b bigint GENERATED BY DEFAULT AS IDENTITY, c serial, d bigint NOT NULL)");
70
+ expect(t.mirrors[0]!.columns.map((c) => c.type)).toEqual(["Int64", "Int64", "Int32", "Int64"]);
71
+ expect(rules(t).filter((r) => r === "SQLRV003")).toEqual([]);
72
+ expect(t.diagnostics.filter((d) => d.ruleId === "SQLRV006").map((d) => d.severity)).toEqual(["info", "info"]);
73
+ });
74
+
75
+ test("an array is not Nullable and says why", () => {
76
+ const t = run("CREATE TABLE t (id bigint PRIMARY KEY, tags text[])");
77
+ expect(t.mirrors[0]!.columns[1]!.type).toBe("Array(String)");
78
+ expect(find(t, "SQLRV007", "t.tags")!.message).toContain("Array(String)");
79
+ expect(rules(t)).not.toContain("SQLRV003");
80
+ });
81
+
82
+ test("a stored generated column is a warning; the type judgments are warnings; a default is an info", () => {
83
+ const t = run("CREATE TABLE t (id bigint PRIMARY KEY, a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) STORED, m money NOT NULL, ts timestamptz NOT NULL DEFAULT now())");
84
+ expect(find(t, "SQLRV006", "t.b")).toMatchObject({ severity: "warning" });
85
+ expect(find(t, "SQLRV007", "money")).toMatchObject({ severity: "warning" });
86
+ expect(find(t, "SQLRV008", "t.ts")).toMatchObject({ severity: "info" });
87
+ expect(t.mirrors[0]!.columns.find((c) => c.name === "ts")!.type).toBe("DateTime64(6, 'UTC')");
88
+ expect(tableSql(t.mirrors[0]!)).not.toContain("DEFAULT");
89
+ });
90
+
91
+ test("the type of an enum or extension column is a judgment, never silent", () => {
92
+ const t = run("CREATE TABLE t (id bigint PRIMARY KEY, mood citext NOT NULL, geo geometry NOT NULL)");
93
+ expect(find(t, "SQLRV007", "t.geo")).toBeDefined();
94
+ expect(find(t, "SQLRV007", "t.mood")).toBeUndefined();
95
+ });
96
+
97
+ test("a column comment is kept as COMMENT; a table comment is kept bare and noted under a composite", () => {
98
+ const ddl = "CREATE TABLE t (id bigint PRIMARY KEY, email text NOT NULL); COMMENT ON COLUMN t.email IS 'PII'; COMMENT ON TABLE t IS 'The accounts'";
99
+ const bare = run(ddl);
100
+ expect(tableSql(bare.mirrors[0]!)).toContain("email String COMMENT 'PII'");
101
+ expect(tableSql(bare.mirrors[0]!)).toContain("COMMENT 'The accounts'");
102
+ const composite = run(ddl, true);
103
+ expect(find(composite, "SQLRV011", "description")).toMatchObject({ severity: "info" });
104
+ expect(emitTs(composite.mirrors, true)).toContain("email String COMMENT 'PII'");
105
+ });
106
+ });
107
+
108
+ describe("constraints, indexes and storage", () => {
109
+ const ddl = `
110
+ CREATE TABLE a (id bigint PRIMARY KEY);
111
+ CREATE TABLE b (
112
+ id bigint PRIMARY KEY,
113
+ a_id bigint NOT NULL REFERENCES a (id),
114
+ code text NOT NULL UNIQUE,
115
+ qty int NOT NULL CHECK (qty > 0),
116
+ during tstzrange,
117
+ EXCLUDE USING gist (during WITH &&)
118
+ ) WITH (fillfactor = 70);
119
+ CREATE UNIQUE INDEX b_code_lower ON b (lower(code));
120
+ CREATE INDEX b_qty ON b (qty);`;
121
+
122
+ test("foreign keys, uniques, checks and exclusions are findings and comment lines", () => {
123
+ const t = run(ddl);
124
+ const b = t.mirrors.find((m) => m.name === "b")!;
125
+ expect(b.constraintComments).toEqual([
126
+ "-- FOREIGN KEY (a_id) REFERENCES a (id)",
127
+ "-- UNIQUE (code)",
128
+ "-- CHECK qty > 0",
129
+ "-- EXCLUDE USING gist (during WITH &&)",
130
+ ]);
131
+ expect(t.diagnostics.filter((d) => d.ruleId === "SQLRV005" && d.entity === "b")).toHaveLength(4);
132
+ expect(tableSql(b).split("\n").slice(0, 4)).toEqual(b.constraintComments);
133
+ });
134
+
135
+ test("an index that is not a plain unique key, and the storage options, are warnings", () => {
136
+ const t = run(ddl);
137
+ expect(find(t, "SQLRV009", "b_qty")).toMatchObject({ severity: "warning", entity: "b" });
138
+ expect(find(t, "SQLRV009", "b_code_lower")).toBeDefined();
139
+ expect(find(t, "SQLRV009", "Storage options")!.message).toContain("WITH (fillfactor = 70)");
140
+ });
141
+
142
+ test("a view, a sequence and a partition are named as not carried", () => {
143
+ const t = run(`
144
+ CREATE TABLE events (id bigint, at timestamptz NOT NULL) PARTITION BY RANGE (at);
145
+ CREATE TABLE events_2026 PARTITION OF events FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
146
+ CREATE VIEW recent AS SELECT * FROM events;
147
+ CREATE SEQUENCE ticket_seq;`);
148
+ expect(t.mirrors.map((m) => m.name)).toEqual(["events"]);
149
+ expect(t.diagnostics.filter((d) => d.ruleId === "SQLRV011").map((d) => d.entity).sort()).toEqual(["events_2026", "sequence ticket_seq", "view recent"]);
150
+ expect(find(t, "SQLRV009", "PARTITION BY RANGE")).toBeDefined();
151
+ });
152
+
153
+ test("two schemas with one table name keep the schema as a database qualifier", () => {
154
+ const t = run("CREATE SCHEMA a; CREATE SCHEMA b; CREATE TABLE a.items (id bigint PRIMARY KEY); CREATE TABLE b.items (id bigint PRIMARY KEY); CREATE TABLE b.other (id bigint PRIMARY KEY)");
155
+ expect(t.mirrors.map((m) => m.name)).toEqual(["a.items", "b.items", "other"]);
156
+ expect(t.outputs).toContain("b.items.id");
157
+ expect(find(t, "SQLRV011", "keeps its schema")).toBeDefined();
158
+ });
159
+ });
160
+
161
+ describe("what is written", () => {
162
+ const t = run("CREATE TABLE accounts (tenant_id bigint NOT NULL, id bigint NOT NULL, name text NOT NULL, PRIMARY KEY (tenant_id, id)); CREATE TABLE \"Odd Name\" (id bigint PRIMARY KEY)");
163
+ const plain = run("CREATE TABLE accounts (tenant_id bigint NOT NULL, id bigint NOT NULL, name text NOT NULL, PRIMARY KEY (tenant_id, id))", true);
164
+
165
+ test("--emit ts with composites is CdcMirror calls with the sort key as primaryKey", () => {
166
+ const ts = emitTs(plain.mirrors, true);
167
+ expect(ts).toContain('import { CdcMirror } from "@intentius/chant-lexicon-sql/clickhouse";');
168
+ expect(ts).toContain('primaryKey: "(tenant_id, id)"');
169
+ expect(ts).toContain('columns: "tenant_id Int64, id Int64, name String"');
170
+ });
171
+
172
+ test("--emit ts without composites is table declarations whose DDL the ClickHouse tag reads", () => {
173
+ const ts = emitTs(t.mirrors, false);
174
+ expect(ts).toContain('import { table } from "@intentius/chant-lexicon-sql/clickhouse";');
175
+ for (const m of t.mirrors) {
176
+ const sql = tableSql(m);
177
+ const entity = clickhouseTable(Object.assign([sql], { raw: [sql] }) as unknown as TemplateStringsArray);
178
+ const props = (entity as unknown as { props: { engine: { name: string; args: string[] }; columns: Array<{ name: string }> } }).props;
179
+ expect(props.engine).toMatchObject({ name: "ReplacingMergeTree", args: ["_version", "_is_deleted"] });
180
+ expect(props.columns.map((c) => c.name).slice(-2)).toEqual(["_version", "_is_deleted"]);
181
+ }
182
+ });
183
+
184
+ test("--emit sql ends each statement; a composite adds the view of live rows", () => {
185
+ const sql = emitSql(plain.mirrors, true);
186
+ expect(sql.match(/^CREATE (TABLE|VIEW)/gm)).toHaveLength(2);
187
+ expect(sql).toContain("CREATE VIEW accounts_current AS");
188
+ expect(sql.trimEnd().endsWith(";")).toBe(true);
189
+ expect(mirrorObjects(t.mirrors, false).map((o) => o.name)).toEqual(["accounts", "Odd Name"]);
190
+ });
191
+
192
+ test("a name that needs quoting is quoted in DDL, and refused for a CdcMirror call, which writes it bare", () => {
193
+ expect(tableSql(t.mirrors[1]!)).toContain("CREATE TABLE `Odd Name`");
194
+ const composite = run('CREATE TABLE "Odd Name" (id bigint PRIMARY KEY)', true);
195
+ expect(composite.mirrors).toEqual([]);
196
+ expect(find(composite, "SQLRV011", "needs quoting")).toMatchObject({ severity: "error" });
197
+ });
198
+ });
199
+
200
+ describe("transform: the MigrationSource's result", () => {
201
+ const ddl = "CREATE TABLE t (id bigint PRIMARY KEY, status text NOT NULL, note text);";
202
+
203
+ test("findings are SARIF-shaped diagnostics against the source file; links name the Postgres outputs", async () => {
204
+ const r = await transform(ddl, { emit: "ts", useComposites: true, sourceFile: "db/schema.sql" });
205
+ expect(r.diagnostics.every((d) => d.file === "db/schema.sql" && d.line === 1 && d.column === 1)).toBe(true);
206
+ expect(new Set(r.diagnostics.map((d) => d.ruleId))).toEqual(new Set(["SQLRV001", "SQLRV002", "SQLRV003", "SQLRV004"]));
207
+ expect(r.links).toEqual(["public.t.id", "public.t.status", "public.t.note"]);
208
+ expect(r.output).toContain("CdcMirror({");
209
+ expect(r.provenance).toHaveLength(1);
210
+ expect(typeof r.validate).toBe("function");
211
+ });
212
+
213
+ test("--strict makes every open warning an error and leaves info alone", async () => {
214
+ const lax = await transform(ddl, { emit: "ts" });
215
+ const strict = await transform(ddl, { emit: "ts", strict: true });
216
+ expect(lax.diagnostics.some((d) => d.severity === "error")).toBe(false);
217
+ const warnings = lax.diagnostics.filter((d) => d.severity === "warning").length;
218
+ expect(warnings).toBeGreaterThan(0);
219
+ expect(strict.diagnostics.filter((d) => d.severity === "error")).toHaveLength(warnings);
220
+ expect(strict.diagnostics.filter((d) => d.severity === "warning")).toEqual([]);
221
+ });
222
+
223
+ test("a table with nothing left to decide still has the sort key and engine open, so strict has something to fail on", async () => {
224
+ const r = await transform("CREATE TABLE t (id bigint PRIMARY KEY);", { emit: "sql", strict: true });
225
+ expect(r.diagnostics.filter((d) => d.severity === "error").map((d) => d.ruleId)).toEqual(["SQLRV001", "SQLRV002"]);
226
+ });
227
+
228
+ test("--emit yaml is refused, and a file with no table to mirror fails by name", async () => {
229
+ await expect(transform(ddl, { emit: "yaml" })).rejects.toThrow("--emit yaml");
230
+ await expect(transform("CREATE VIEW v AS SELECT 1;", { emit: "ts", sourceFile: "x.sql" })).rejects.toThrow("no Postgres table to mirror in x.sql");
231
+ });
232
+ });
233
+
234
+ describe("looksLikePostgres", () => {
235
+ test("recognises Postgres DDL and declarations, and not ClickHouse or T-SQL", () => {
236
+ expect(looksLikePostgres("CREATE TABLE t (id int);")).toBe(true);
237
+ expect(looksLikePostgres('import { table } from "@intentius/chant-lexicon-sql/postgres";')).toBe(true);
238
+ expect(looksLikePostgres("CREATE TABLE t (id UInt64) ENGINE = MergeTree ORDER BY id")).toBe(false);
239
+ expect(looksLikePostgres("CREATE TABLE t (id int)\nGO\n")).toBe(false);
240
+ expect(looksLikePostgres("SELECT 1")).toBe(false);
241
+ });
242
+ });