@intentius/chant-lexicon-sql 0.124.45 → 0.124.48

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 (360) 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/stream.d.ts +40 -0
  37. package/dist/clickhouse/plan/stream.d.ts.map +1 -0
  38. package/dist/clickhouse/plan/stream.js +107 -0
  39. package/dist/clickhouse/rebuild/observe.d.ts +3 -1
  40. package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
  41. package/dist/clickhouse/rebuild/observe.js +14 -1
  42. package/dist/clickhouse/review-catalog.d.ts +213 -0
  43. package/dist/clickhouse/review-catalog.d.ts.map +1 -0
  44. package/dist/clickhouse/review-catalog.js +330 -0
  45. package/dist/codegen/classifier-page.d.ts.map +1 -1
  46. package/dist/codegen/classifier-page.js +21 -4
  47. package/dist/codegen/clickhouse-module.d.ts.map +1 -1
  48. package/dist/codegen/clickhouse-module.js +3 -0
  49. package/dist/codegen/coverage-page.d.ts.map +1 -1
  50. package/dist/codegen/coverage-page.js +5 -3
  51. package/dist/codegen/docs.js +1 -1
  52. package/dist/codegen/postgres-classifier-page.js +1 -1
  53. package/dist/codegen/registry.d.ts.map +1 -1
  54. package/dist/codegen/registry.js +5 -0
  55. package/dist/composites/catalog.d.ts.map +1 -1
  56. package/dist/composites/catalog.js +100 -0
  57. package/dist/composites/clickhouse.d.ts +2 -0
  58. package/dist/composites/clickhouse.d.ts.map +1 -1
  59. package/dist/composites/clickhouse.js +2 -0
  60. package/dist/composites/otel-tables.d.ts +100 -0
  61. package/dist/composites/otel-tables.d.ts.map +1 -0
  62. package/dist/composites/otel-tables.js +282 -0
  63. package/dist/composites/stream-ingest.d.ts +63 -0
  64. package/dist/composites/stream-ingest.d.ts.map +1 -0
  65. package/dist/composites/stream-ingest.js +39 -0
  66. package/dist/coverage/clickhouse.json +1 -1
  67. package/dist/coverage/postgres.json +7 -7
  68. package/dist/coverage.d.ts +16 -6
  69. package/dist/coverage.d.ts.map +1 -1
  70. package/dist/coverage.js +10 -5
  71. package/dist/drizzle-command.d.ts +17 -0
  72. package/dist/drizzle-command.d.ts.map +1 -0
  73. package/dist/drizzle-command.js +91 -0
  74. package/dist/drizzle.d.ts +70 -0
  75. package/dist/drizzle.d.ts.map +1 -0
  76. package/dist/drizzle.js +684 -0
  77. package/dist/files/clickhouse.d.ts.map +1 -1
  78. package/dist/files/clickhouse.js +8 -1
  79. package/dist/files/common.d.ts +13 -0
  80. package/dist/files/common.d.ts.map +1 -1
  81. package/dist/files/common.js +5 -0
  82. package/dist/files/postgres.js +12 -2
  83. package/dist/generated/clickhouse.d.ts +2 -0
  84. package/dist/generated/clickhouse.d.ts.map +1 -1
  85. package/dist/generated/clickhouse.js +2 -0
  86. package/dist/import-parser.d.ts +21 -1
  87. package/dist/import-parser.d.ts.map +1 -1
  88. package/dist/import-parser.js +79 -5
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +1 -1
  92. package/dist/integrity.json +6 -5
  93. package/dist/lint/post-synth/audit-entries.d.ts.map +1 -1
  94. package/dist/lint/post-synth/audit-entries.js +1 -0
  95. package/dist/lint/post-synth/index.d.ts.map +1 -1
  96. package/dist/lint/post-synth/index.js +2 -0
  97. package/dist/lint/post-synth/sqlch128.d.ts +18 -0
  98. package/dist/lint/post-synth/sqlch128.d.ts.map +1 -0
  99. package/dist/lint/post-synth/sqlch128.js +222 -0
  100. package/dist/lint/rules/postgres-templates.js +2 -2
  101. package/dist/lsp/postgres-template.d.ts.map +1 -1
  102. package/dist/lsp/postgres-template.js +11 -0
  103. package/dist/manifest.json +1 -1
  104. package/dist/mcp/postgres.js +1 -1
  105. package/dist/meta.json +25 -0
  106. package/dist/migrate/from-postgres/detect.d.ts +4 -0
  107. package/dist/migrate/from-postgres/detect.d.ts.map +1 -0
  108. package/dist/migrate/from-postgres/detect.js +7 -0
  109. package/dist/migrate/from-postgres/emit.d.ts +26 -0
  110. package/dist/migrate/from-postgres/emit.d.ts.map +1 -0
  111. package/dist/migrate/from-postgres/emit.js +86 -0
  112. package/dist/migrate/from-postgres/index.d.ts +51 -0
  113. package/dist/migrate/from-postgres/index.d.ts.map +1 -0
  114. package/dist/migrate/from-postgres/index.js +115 -0
  115. package/dist/migrate/from-postgres/translate.d.ts +73 -0
  116. package/dist/migrate/from-postgres/translate.d.ts.map +1 -0
  117. package/dist/migrate/from-postgres/translate.js +261 -0
  118. package/dist/migrate/from-postgres/types.d.ts +51 -0
  119. package/dist/migrate/from-postgres/types.d.ts.map +1 -0
  120. package/dist/migrate/from-postgres/types.js +211 -0
  121. package/dist/migration-replay.d.ts +73 -0
  122. package/dist/migration-replay.d.ts.map +1 -0
  123. package/dist/migration-replay.js +294 -0
  124. package/dist/okf/index.md +6 -0
  125. package/dist/okf/rules/SQLCH128.md +11 -0
  126. package/dist/okf/types/PostgresForeignDataWrapper.md +9 -0
  127. package/dist/okf/types/PostgresForeignServer.md +9 -0
  128. package/dist/okf/types/PostgresOperator.md +9 -0
  129. package/dist/okf/types/PostgresPublication.md +9 -0
  130. package/dist/okf/types/PostgresStatistics.md +9 -0
  131. package/dist/op/activities/postgres-migration.d.ts.map +1 -1
  132. package/dist/op/activities/postgres-migration.js +4 -0
  133. package/dist/op/activity-contracts.d.ts +20 -0
  134. package/dist/op/activity-contracts.d.ts.map +1 -1
  135. package/dist/op/activity-contracts.js +2 -0
  136. package/dist/plugin.d.ts.map +1 -1
  137. package/dist/plugin.js +36 -2
  138. package/dist/postgres/apply/apply.js +8 -1
  139. package/dist/postgres/apply/statements.d.ts.map +1 -1
  140. package/dist/postgres/apply/statements.js +87 -2
  141. package/dist/postgres/constraint-names.d.ts +30 -0
  142. package/dist/postgres/constraint-names.d.ts.map +1 -0
  143. package/dist/postgres/constraint-names.js +72 -0
  144. package/dist/postgres/entities.d.ts +110 -1
  145. package/dist/postgres/entities.d.ts.map +1 -1
  146. package/dist/postgres/entities.js +146 -0
  147. package/dist/postgres/entity-types.d.ts +5 -0
  148. package/dist/postgres/entity-types.d.ts.map +1 -1
  149. package/dist/postgres/entity-types.js +5 -0
  150. package/dist/postgres/import/generator.d.ts.map +1 -1
  151. package/dist/postgres/import/generator.js +50 -2
  152. package/dist/postgres/import/live-export.d.ts.map +1 -1
  153. package/dist/postgres/import/live-export.js +2 -0
  154. package/dist/postgres/live/catalog-kinds.d.ts +14 -0
  155. package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
  156. package/dist/postgres/live/catalog-kinds.js +206 -0
  157. package/dist/postgres/live/catalog.d.ts +2 -0
  158. package/dist/postgres/live/catalog.d.ts.map +1 -1
  159. package/dist/postgres/live/catalog.js +3 -1
  160. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  161. package/dist/postgres/live/describe-resources.js +10 -2
  162. package/dist/postgres/migrate/backfill.d.ts +2 -0
  163. package/dist/postgres/migrate/backfill.d.ts.map +1 -1
  164. package/dist/postgres/migrate/backfill.js +7 -1
  165. package/dist/postgres/migrate/carry.d.ts +45 -1
  166. package/dist/postgres/migrate/carry.d.ts.map +1 -1
  167. package/dist/postgres/migrate/carry.js +72 -8
  168. package/dist/postgres/migrate/observe.d.ts +28 -4
  169. package/dist/postgres/migrate/observe.d.ts.map +1 -1
  170. package/dist/postgres/migrate/observe.js +41 -12
  171. package/dist/postgres/migrate/op.d.ts +26 -1
  172. package/dist/postgres/migrate/op.d.ts.map +1 -1
  173. package/dist/postgres/migrate/op.js +3 -1
  174. package/dist/postgres/migrate/steps.d.ts +35 -2
  175. package/dist/postgres/migrate/steps.d.ts.map +1 -1
  176. package/dist/postgres/migrate/steps.js +158 -12
  177. package/dist/postgres/parser.d.ts +72 -1
  178. package/dist/postgres/parser.d.ts.map +1 -1
  179. package/dist/postgres/parser.js +194 -2
  180. package/dist/postgres/plan/deep.d.ts.map +1 -1
  181. package/dist/postgres/plan/deep.js +29 -1
  182. package/dist/postgres/plan/diff.d.ts.map +1 -1
  183. package/dist/postgres/plan/diff.js +39 -1
  184. package/dist/postgres/plan/normalize.d.ts +17 -2
  185. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  186. package/dist/postgres/plan/normalize.js +109 -1
  187. package/dist/postgres/plan/rules.d.ts +1 -0
  188. package/dist/postgres/plan/rules.d.ts.map +1 -1
  189. package/dist/postgres/plan/rules.js +3 -2
  190. package/dist/postgres/plan/schema.js +1 -1
  191. package/dist/postgres/secret-options.d.ts +10 -0
  192. package/dist/postgres/secret-options.d.ts.map +1 -0
  193. package/dist/postgres/secret-options.js +9 -0
  194. package/dist/postgres/testing/roundtrip-schema.d.ts +3 -2
  195. package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
  196. package/dist/postgres/testing/roundtrip-schema.js +13 -1
  197. package/dist/postgres.d.ts +1 -1
  198. package/dist/postgres.d.ts.map +1 -1
  199. package/dist/postgres.js +1 -1
  200. package/dist/rules/audit-entries.ts +1 -0
  201. package/dist/rules/postgres-templates.ts +2 -2
  202. package/dist/rules/sqlch128.ts +230 -0
  203. package/dist/serializer.d.ts +5 -0
  204. package/dist/serializer.d.ts.map +1 -1
  205. package/dist/serializer.js +13 -2
  206. package/dist/sql-types-command.d.ts +13 -0
  207. package/dist/sql-types-command.d.ts.map +1 -1
  208. package/dist/sql-types-command.js +31 -16
  209. package/dist/sql-types.d.ts +22 -0
  210. package/dist/sql-types.d.ts.map +1 -1
  211. package/dist/sql-types.js +10 -6
  212. package/dist/sqlserver/lexer.d.ts +37 -0
  213. package/dist/sqlserver/lexer.d.ts.map +1 -0
  214. package/dist/sqlserver/lexer.js +254 -0
  215. package/dist/sqlserver/parse.d.ts +185 -0
  216. package/dist/sqlserver/parse.d.ts.map +1 -0
  217. package/dist/sqlserver/parse.js +767 -0
  218. package/dist/sqlserver/translate.d.ts +34 -0
  219. package/dist/sqlserver/translate.d.ts.map +1 -0
  220. package/dist/sqlserver/translate.js +495 -0
  221. package/dist/sqlserver/types.d.ts +44 -0
  222. package/dist/sqlserver/types.d.ts.map +1 -0
  223. package/dist/sqlserver/types.js +133 -0
  224. package/dist/sqlserver/validate.d.ts +31 -0
  225. package/dist/sqlserver/validate.d.ts.map +1 -0
  226. package/dist/sqlserver/validate.js +67 -0
  227. package/dist/validate.d.ts.map +1 -1
  228. package/dist/validate.js +5 -0
  229. package/package.json +2 -2
  230. package/src/change-set.ts +2 -1
  231. package/src/clickhouse/apply/statements.ts +25 -0
  232. package/src/clickhouse/import/live-export.ts +20 -5
  233. package/src/clickhouse/live/named-collections.e2e.test.ts +88 -0
  234. package/src/clickhouse/live/named-collections.ts +61 -0
  235. package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -3
  236. package/src/clickhouse/live/undeclarable.ts +20 -4
  237. package/src/clickhouse/named-collections.test.ts +187 -0
  238. package/src/clickhouse/named-collections.ts +224 -0
  239. package/src/clickhouse/parser.ts +2 -1
  240. package/src/clickhouse/plan/commands.ts +17 -2
  241. package/src/clickhouse/plan/diff.ts +57 -1
  242. package/src/clickhouse/plan/disruption.ts +5 -0
  243. package/src/clickhouse/plan/rebuild-handoff.ts +3 -0
  244. package/src/clickhouse/plan/report.ts +6 -1
  245. package/src/clickhouse/plan/rules.ts +14 -4
  246. package/src/clickhouse/plan/stream-s3queue.e2e.test.ts +148 -0
  247. package/src/clickhouse/plan/stream.test.ts +237 -0
  248. package/src/clickhouse/plan/stream.ts +111 -0
  249. package/src/clickhouse/rebuild/observe.ts +15 -1
  250. package/src/clickhouse/review-catalog.test.ts +181 -0
  251. package/src/clickhouse/review-catalog.ts +489 -0
  252. package/src/codegen/classifier-page.ts +21 -4
  253. package/src/codegen/clickhouse-module.ts +3 -0
  254. package/src/codegen/coverage-page.ts +5 -3
  255. package/src/codegen/docs.ts +1 -1
  256. package/src/codegen/postgres-classifier-page.ts +1 -1
  257. package/src/codegen/registry.ts +5 -0
  258. package/src/composites/catalog.test.ts +6 -1
  259. package/src/composites/catalog.ts +100 -0
  260. package/src/composites/clickhouse.ts +2 -0
  261. package/src/composites/composites.test.ts +1 -1
  262. package/src/composites/otel-tables.test.ts +118 -0
  263. package/src/composites/otel-tables.ts +339 -0
  264. package/src/composites/stream-ingest.test.ts +114 -0
  265. package/src/composites/stream-ingest.ts +82 -0
  266. package/src/core/undeclarable.test.ts +6 -5
  267. package/src/coverage/clickhouse.json +1 -1
  268. package/src/coverage/postgres.json +7 -7
  269. package/src/coverage.test.ts +27 -2
  270. package/src/coverage.ts +18 -7
  271. package/src/drizzle-command.ts +94 -0
  272. package/src/drizzle-kit.e2e.test.ts +89 -0
  273. package/src/drizzle-types.e2e.test.ts +67 -0
  274. package/src/drizzle.test.ts +358 -0
  275. package/src/drizzle.ts +737 -0
  276. package/src/files/clickhouse.ts +8 -1
  277. package/src/files/common.ts +14 -1
  278. package/src/files/postgres.ts +12 -2
  279. package/src/generated/clickhouse.ts +3 -0
  280. package/src/generated/lexicon-sql.json +25 -0
  281. package/src/import-parser.sqlserver.test.ts +92 -0
  282. package/src/import-parser.ts +79 -5
  283. package/src/index.ts +1 -1
  284. package/src/lint/post-synth/audit-entries.ts +1 -0
  285. package/src/lint/post-synth/index.ts +2 -0
  286. package/src/lint/post-synth/sqlch128.test.ts +107 -0
  287. package/src/lint/post-synth/sqlch128.ts +230 -0
  288. package/src/lint/rules/postgres-templates.ts +2 -2
  289. package/src/lsp/postgres-template.ts +11 -0
  290. package/src/mcp/postgres.ts +1 -1
  291. package/src/migrate/from-postgres/cli.test.ts +165 -0
  292. package/src/migrate/from-postgres/detect.ts +7 -0
  293. package/src/migrate/from-postgres/emit.ts +96 -0
  294. package/src/migrate/from-postgres/example.test.ts +205 -0
  295. package/src/migrate/from-postgres/index.ts +122 -0
  296. package/src/migrate/from-postgres/translate.test.ts +242 -0
  297. package/src/migrate/from-postgres/translate.ts +359 -0
  298. package/src/migrate/from-postgres/types.test.ts +116 -0
  299. package/src/migrate/from-postgres/types.ts +236 -0
  300. package/src/migrate/from-postgres/validate.e2e.test.ts +44 -0
  301. package/src/migrate/from-postgres/workspace.e2e.test.ts +114 -0
  302. package/src/migration-replay.e2e.test.ts +155 -0
  303. package/src/migration-replay.test.ts +173 -0
  304. package/src/migration-replay.ts +289 -0
  305. package/src/op/__snapshots__/watch-pipeline.test.ts.snap +2 -2
  306. package/src/op/activities/postgres-migration.ts +4 -0
  307. package/src/op/activity-contracts.ts +2 -0
  308. package/src/plugin.ts +37 -2
  309. package/src/postgres/apply/apply.ts +9 -1
  310. package/src/postgres/apply/statements.ts +74 -1
  311. package/src/postgres/constraint-names.ts +71 -0
  312. package/src/postgres/database-kinds.test.ts +384 -0
  313. package/src/postgres/entities.ts +266 -1
  314. package/src/postgres/entity-types.ts +5 -0
  315. package/src/postgres/import/generator.ts +42 -2
  316. package/src/postgres/import/live-export.ts +1 -0
  317. package/src/postgres/live/catalog-kinds.ts +223 -0
  318. package/src/postgres/live/catalog.ts +6 -1
  319. package/src/postgres/live/database-kinds.e2e.test.ts +164 -0
  320. package/src/postgres/live/describe-resources.ts +11 -2
  321. package/src/postgres/live/undeclarable.e2e.test.ts +13 -6
  322. package/src/postgres/migrate/backfill.ts +13 -1
  323. package/src/postgres/migrate/carry.ts +103 -8
  324. package/src/postgres/migrate/followups.e2e.test.ts +218 -0
  325. package/src/postgres/migrate/followups.test.ts +199 -0
  326. package/src/postgres/migrate/kinds.e2e.test.ts +5 -5
  327. package/src/postgres/migrate/observe.ts +53 -14
  328. package/src/postgres/migrate/op.ts +28 -1
  329. package/src/postgres/migrate/partition.e2e.test.ts +129 -0
  330. package/src/postgres/migrate/partition.test.ts +206 -0
  331. package/src/postgres/migrate/steps.ts +157 -12
  332. package/src/postgres/parser.ts +234 -2
  333. package/src/postgres/plan/deep.ts +28 -1
  334. package/src/postgres/plan/diff.ts +36 -1
  335. package/src/postgres/plan/normalize.ts +122 -1
  336. package/src/postgres/plan/rules.ts +10 -3
  337. package/src/postgres/plan/schema.ts +1 -1
  338. package/src/postgres/secret-options.ts +10 -0
  339. package/src/postgres/testing/roundtrip-schema.ts +13 -1
  340. package/src/postgres.ts +16 -0
  341. package/src/serializer.ts +13 -2
  342. package/src/skills/chant-sql-postgres-migration.md +3 -3
  343. package/src/skills/chant-sql-postgres.md +6 -1
  344. package/src/skills/chant-sql.md +2 -1
  345. package/src/sql-types-command.ts +38 -16
  346. package/src/sql-types.ts +11 -6
  347. package/src/sqlserver/fixture.test.ts +186 -0
  348. package/src/sqlserver/import-cli.test.ts +61 -0
  349. package/src/sqlserver/lexer.ts +260 -0
  350. package/src/sqlserver/parse.ts +865 -0
  351. package/src/sqlserver/translate.test.ts +396 -0
  352. package/src/sqlserver/translate.ts +558 -0
  353. package/src/sqlserver/types.test.ts +102 -0
  354. package/src/sqlserver/types.ts +154 -0
  355. package/src/sqlserver/validate.e2e.test.ts +32 -0
  356. package/src/sqlserver/validate.test.ts +49 -0
  357. package/src/sqlserver/validate.ts +76 -0
  358. package/src/validate.ts +5 -0
  359. package/src/workspace-kinds.test.ts +11 -0
  360. package/workspace-kinds.json +100 -2
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Postgres tables into the ClickHouse tables a CDC pipeline fills (#4399).
3
+ *
4
+ * The input is the Postgres declarations a build reads (or a `.sql` file read
5
+ * into the same declarations), the output a {@link Mirror} per table: its
6
+ * ClickHouse columns, its sort key and the facts the writers need. Writing
7
+ * the mirror as DDL or as a `CdcMirror` call is `./emit.ts`.
8
+ *
9
+ * What is mechanical makes no finding: the type map in `./types.ts`, names,
10
+ * column order, a `NOT NULL` column keeping its type. Everything else is
11
+ * decided by the shared catalog (`../../clickhouse/review-catalog.ts`): the
12
+ * sort key from the primary key, the `ReplacingMergeTree` engine for a
13
+ * mirror, `Nullable` against a default, `LowCardinality` candidates, and the
14
+ * constraints ClickHouse does not enforce. This file adds what only Postgres
15
+ * has: identity, serial and generated columns, defaults, storage options,
16
+ * indexes that are not carried, and the objects of the file that are not
17
+ * tables.
18
+ *
19
+ * A mirror's columns carry no `DEFAULT`: the pipeline writes every column of
20
+ * a row, so the source's default has already run. That is an `info` finding
21
+ * (SQLRV008), not a `warning`, since it changes nothing a reader of the
22
+ * mirror sees.
23
+ *
24
+ * The Postgres schema is not part of the mirror's name. A pipeline chooses
25
+ * the ClickHouse database it writes to, so `shop.customers` becomes
26
+ * `customers` in that database. Two tables with one name in different
27
+ * schemas keep their schema as a database qualifier (`shop.customers`).
28
+ */
29
+
30
+ import type { Declarable } from "@intentius/chant/declarable";
31
+ import type { ImportDiagnostic } from "@intentius/chant/import/parser";
32
+ import {
33
+ constraintReview,
34
+ engineReview,
35
+ findingText,
36
+ lowCardinalityReview,
37
+ nullableReview,
38
+ sortKeyReview,
39
+ type ConstraintFact,
40
+ type ReviewFinding,
41
+ type ReviewRuleId,
42
+ type SortKeyCandidate,
43
+ } from "../../clickhouse/review-catalog";
44
+ import { isPostgresObject, POSTGRES_ENTITY_TYPES, type ColumnDef, type IndexProps, type TableProps } from "../../postgres/entities";
45
+ import { mapPostgresType } from "./types";
46
+
47
+ /** The names of the columns the pipeline adds, and what `CdcMirror` calls them by default. */
48
+ export const MIRROR_COLUMNS = { version: "_version", deleted: "_is_deleted" } as const;
49
+
50
+ export interface MirrorColumn {
51
+ name: string;
52
+ /** The ClickHouse type, `Nullable` and `LowCardinality` decisions included. */
53
+ type: string;
54
+ comment?: string;
55
+ }
56
+
57
+ export interface Mirror {
58
+ /** The export the Postgres table is declared as, or its name when the source has none. */
59
+ export: string;
60
+ /** The ClickHouse table name, qualified with a database only when two tables would collide. */
61
+ name: string;
62
+ /** The Postgres schema and table, for the workspace link. */
63
+ source: { schema: string; table: string };
64
+ columns: MirrorColumn[];
65
+ /** The sort key; empty means `tuple()`. */
66
+ primaryKey: string[];
67
+ /** MergeTree settings the key needs. */
68
+ settings: Record<string, string>;
69
+ /** `-- ` lines for constraints ClickHouse does not enforce. */
70
+ constraintComments: string[];
71
+ /** The table's description, which `CdcMirror` has no place for. */
72
+ comment?: string;
73
+ }
74
+
75
+ export interface Translation {
76
+ mirrors: Mirror[];
77
+ diagnostics: ImportDiagnostic[];
78
+ /** The Postgres outputs each mirrored column reads, as a workspace link names them (`shop.customers.email`). */
79
+ outputs: string[];
80
+ }
81
+
82
+ export interface TranslateOptions {
83
+ /** The mirror is a `CdcMirror` call: a table description has no place in it. */
84
+ composite: boolean;
85
+ }
86
+
87
+ const DEFAULT_SCHEMA = "public";
88
+
89
+ const ident = (name: string): string => (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `\`${name.replace(/\\/g, "\\\\").replace(/`/g, "\\`")}\``);
90
+ const oneLine = (s: string): string => s.replace(/\s+/g, " ").trim();
91
+
92
+ class Report {
93
+ readonly diagnostics: ImportDiagnostic[] = [];
94
+ finding(f: ReviewFinding): void {
95
+ this.diagnostics.push({ ruleId: f.ruleId, severity: f.severity, message: findingText(f), entity: f.object });
96
+ }
97
+ /** A finding of this file's own. `error` marks a table the translation refuses; the catalog's findings never are. */
98
+ note(ruleId: ReviewRuleId, severity: "warning" | "info" | "error", object: string, message: string, evidence: string[], column?: string, suggestion?: string): void {
99
+ const text = findingText({ ruleId, severity: severity === "error" ? "warning" : severity, object, ...(column ? { column } : {}), message, evidence, ...(suggestion ? { suggestion } : {}) });
100
+ this.diagnostics.push({ ruleId, severity, message: text, entity: object });
101
+ }
102
+ }
103
+
104
+ const propsOf = <P>(e: Declarable): P => (e as unknown as { props: P }).props;
105
+ const display = (schema: string | undefined, name: string): string => (schema ? `${schema}.${name}` : name);
106
+
107
+ /** The Postgres tables among `entities`, in the order given, with the export each is declared as. */
108
+ function tablesOf(entities: ReadonlyMap<string, Declarable>): Array<{ key: string; props: TableProps }> {
109
+ const out: Array<{ key: string; props: TableProps }> = [];
110
+ for (const [key, e] of entities) if (isPostgresObject(e) && e.entityType === POSTGRES_ENTITY_TYPES.table) out.push({ key, props: propsOf<TableProps>(e) });
111
+ return out;
112
+ }
113
+
114
+ /** Whether an index is on this table: its table is named bare, or with the schema, as the DDL wrote it. */
115
+ const tableMatches = (ix: IndexProps, t: TableProps): boolean => {
116
+ const target = ix.tableName.replace(/"/g, "");
117
+ return target === t.name || target === `${t.schema ?? DEFAULT_SCHEMA}.${t.name}`;
118
+ };
119
+
120
+ function translateTable(
121
+ key: string,
122
+ t: TableProps,
123
+ indexes: readonly IndexProps[],
124
+ mirrorName: string,
125
+ opts: TranslateOptions,
126
+ report: Report,
127
+ ): Mirror | undefined {
128
+ const schema = t.schema ?? DEFAULT_SCHEMA;
129
+ const object = display(t.schema, t.name);
130
+ const pk = new Set(t.primaryKey?.columns ?? []);
131
+
132
+ for (const reserved of Object.values(MIRROR_COLUMNS)) {
133
+ if (t.columns.some((c) => c.name === reserved)) {
134
+ report.note("SQLRV011", "error", object, `The table is not carried: it already has a column named ${reserved}, which the mirror adds.`, [`column ${reserved} in the source`], reserved, "Rename the source column, or name the pipeline's columns otherwise in the CdcMirror call.");
135
+ return undefined;
136
+ }
137
+ }
138
+ if (opts.composite && !/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(mirrorName)) {
139
+ report.note("SQLRV011", "error", object, `The table is not carried: CdcMirror writes the name ${mirrorName} unquoted, and it needs quoting.`, [`the name ${mirrorName} is not a plain identifier`], undefined, "Emit without --use-composites to keep the quoted name, or rename the table.");
140
+ return undefined;
141
+ }
142
+ if (t.partitionOf !== undefined) {
143
+ report.note("SQLRV011", "info", object, "A partition is not carried; its rows reach the mirror through the parent table.", [`PARTITION OF ${typeof t.partitionOf === "string" ? t.partitionOf : "the parent"}`]);
144
+ return undefined;
145
+ }
146
+ const unread: string[] = [];
147
+ if (t.like.length > 0) unread.push(...t.like.map((l) => `LIKE ${l}`));
148
+ if (t.inherits.length > 0) unread.push(`INHERITS (${t.inherits.map((i) => (typeof i === "string" ? i : "a declared table")).join(", ")})`);
149
+ if (t.ofType !== undefined) unread.push(`OF ${typeof t.ofType === "string" ? t.ofType : "a declared type"}`);
150
+ if (unread.length > 0) {
151
+ report.note("SQLRV011", "warning", object, "Columns that come from LIKE, INHERITS or OF are not read.", unread, undefined, "Add the columns to the CdcMirror call by hand, or declare them in the table.");
152
+ }
153
+
154
+ const columnTypes: Record<string, string> = {};
155
+ const columns: MirrorColumn[] = [];
156
+ const maxLengths = new Map<string, number>();
157
+ const droppedStorage: string[] = [];
158
+
159
+ for (const col of t.columns) {
160
+ const m = mapPostgresType(col.type ?? "");
161
+ if (m.note) report.note("SQLRV007", "warning", object, m.note.message, m.note.evidence, col.name);
162
+ if (m.maxLength !== undefined) maxLengths.set(col.name, m.maxLength);
163
+ describeGenerated(col, m.serial === true, object, report);
164
+ if (col.default !== undefined && col.generated === undefined && !(m.serial && /nextval\(/i.test(col.default))) {
165
+ report.note(
166
+ "SQLRV008",
167
+ "info",
168
+ object,
169
+ `The default of ${col.name} is not carried.`,
170
+ [`DEFAULT ${oneLine(col.default)}`, "the pipeline writes every column of a row, so the source has already run the default"],
171
+ col.name,
172
+ "Add a ClickHouse DEFAULT if the table is also written by something other than the pipeline.",
173
+ );
174
+ }
175
+ if (col.collate) {
176
+ report.note("SQLRV007", "warning", object, `Collation ${col.collate} on ${col.name} is not carried.`, ["ClickHouse compares String values as bytes; a Postgres collation orders them by locale"], col.name);
177
+ }
178
+ if (col.compression) droppedStorage.push(`column ${col.name} COMPRESSION ${col.compression}`);
179
+ if (col.storage) droppedStorage.push(`column ${col.name} STORAGE ${col.storage}`);
180
+
181
+ // A primary key column, an identity column and a serial column are NOT NULL whatever the DDL says.
182
+ const forcedNotNull = pk.has(col.name) || col.generated?.kind === "identity" || m.serial === true;
183
+ const sourceNullable = m.array === true ? false : !(col.notNull === true || forcedNotNull);
184
+ const n = nullableReview({ object, column: col.name, type: m.type, sourceNullable, ...(col.default !== undefined && sourceNullable ? { defaultExpr: oneLine(col.default) } : {}) });
185
+ if (n.finding) report.finding(n.finding);
186
+ columnTypes[col.name] = n.type;
187
+ const lc = lowCardinalityReview({ object, column: col.name, type: n.type, ...(m.maxLength !== undefined ? { maxLength: m.maxLength } : {}) });
188
+ if (lc) report.finding(lc);
189
+ columns.push({ name: col.name, type: n.type, ...(col.comment ? { comment: col.comment } : {}) });
190
+ }
191
+ if (columns.length === 0) {
192
+ report.note("SQLRV011", "error", object, "The table is not carried: it declares no column.", ["the CREATE TABLE lists no column"]);
193
+ return undefined;
194
+ }
195
+
196
+ // Sort key: the primary key, else the first unique key.
197
+ const uniqueIndex = indexes.find((i) => i.unique === true && i.elements.every((e) => e.column !== undefined));
198
+ const candidates: SortKeyCandidate[] = [];
199
+ if (t.primaryKey && t.primaryKey.columns.length > 0) {
200
+ candidates.push({ kind: "primary-key", ...(t.primaryKey.name ? { name: t.primaryKey.name } : {}), columns: t.primaryKey.columns, source: `PRIMARY KEY${t.primaryKey.name ? ` ${t.primaryKey.name}` : ""}` });
201
+ }
202
+ const firstUnique = t.uniques[0];
203
+ if (firstUnique) candidates.push({ kind: "unique-key", ...(firstUnique.name ? { name: firstUnique.name } : {}), columns: firstUnique.columns, source: `UNIQUE${firstUnique.name ? ` ${firstUnique.name}` : ""}` });
204
+ else if (uniqueIndex) candidates.push({ kind: "unique-key", name: uniqueIndex.name, columns: uniqueIndex.elements.map((e) => e.column!), source: `CREATE UNIQUE INDEX ${uniqueIndex.name}` });
205
+ const sortKey = sortKeyReview({ object, candidates: candidates.filter((c) => c.columns.every((c2) => c2 in columnTypes)), columnTypes });
206
+ report.finding(sortKey.finding);
207
+ if (opts.composite && Object.keys(sortKey.settings).length > 0) {
208
+ report.note(
209
+ "SQLRV001",
210
+ "warning",
211
+ object,
212
+ `The CdcMirror call has no SETTINGS, so ${Object.keys(sortKey.settings).join(", ")} is not set.`,
213
+ [`the sort key has a Nullable column and needs ${Object.entries(sortKey.settings).map(([k, v]) => `${k} = ${v}`).join(", ")}`],
214
+ undefined,
215
+ "Emit without --use-composites to keep the setting, or make the key columns NOT NULL in Postgres.",
216
+ );
217
+ }
218
+
219
+ const engine = engineReview({
220
+ object,
221
+ mode: "cdc",
222
+ cdcColumns: { version: MIRROR_COLUMNS.version, deleted: MIRROR_COLUMNS.deleted },
223
+ ...(sortKey.orderBy.length > 0 ? { uniqueKey: sortKey.orderBy } : {}),
224
+ facts: [`the table is declared in Postgres${t.persistence ? ` as ${t.persistence}` : ""}`],
225
+ });
226
+ report.finding(engine.finding);
227
+
228
+ // Constraints ClickHouse does not enforce.
229
+ const facts: ConstraintFact[] = [];
230
+ for (const fk of t.foreignKeys) {
231
+ facts.push({ kind: "foreign-key", ...(fk.name ? { name: fk.name } : {}), columns: fk.columns, references: { table: fk.refTable, columns: fk.refColumns } });
232
+ }
233
+ for (const u of t.uniques) facts.push({ kind: "unique", ...(u.name ? { name: u.name } : {}), columns: u.columns });
234
+ for (const ix of indexes) {
235
+ if (ix.unique === true && ix.elements.every((e) => e.column !== undefined)) facts.push({ kind: "unique", name: ix.name, columns: ix.elements.map((e) => e.column!) });
236
+ }
237
+ for (const c of t.checks) facts.push({ kind: "check", ...(c.name ? { name: c.name } : {}), expression: c.expr });
238
+ for (const x of t.exclusions) facts.push({ kind: "exclusion", ...(x.name ? { name: x.name } : {}), expression: `${x.using ? `USING ${x.using} ` : ""}(${x.elements})` });
239
+ const constraints = constraintReview({ object, constraints: facts });
240
+ for (const f of constraints.findings) report.finding(f);
241
+
242
+ // Indexes that are not carried.
243
+ for (const ix of indexes) {
244
+ if (ix.unique === true && ix.elements.every((e) => e.column !== undefined)) continue;
245
+ const on = ix.elements.map((e) => e.column ?? oneLine(e.expr)).join(", ");
246
+ report.note(
247
+ "SQLRV009",
248
+ "warning",
249
+ object,
250
+ `${ix.name}${ix.unique ? ", a unique index," : ", an index,"} on (${on}) is not carried.`,
251
+ [`CREATE ${ix.unique ? "UNIQUE " : ""}INDEX ${ix.name}${ix.method ? ` USING ${ix.method}` : ""}${ix.where ? ` WHERE ${oneLine(ix.where)}` : ""}`],
252
+ undefined,
253
+ "The sort key is the only index a MergeTree table has by default. A skip index (minmax, set, bloom_filter) on the column can prune granules.",
254
+ );
255
+ }
256
+
257
+ // Storage the source chose.
258
+ const dropped = [...droppedStorage];
259
+ if (t.partitionBy) dropped.push(`PARTITION BY ${oneLine(t.partitionBy)}`);
260
+ if (t.with) dropped.push(`WITH ${oneLine(t.with)}`);
261
+ if (t.tablespace) dropped.push(`TABLESPACE ${t.tablespace}`);
262
+ if (t.using) dropped.push(`USING ${t.using}`);
263
+ if (t.persistence) dropped.push(t.persistence.toUpperCase());
264
+ if (t.rowSecurity === true) dropped.push("ROW LEVEL SECURITY");
265
+ if (dropped.length > 0) {
266
+ report.note("SQLRV009", "warning", object, "Storage options of the source table are not carried.", dropped, undefined, "Choose PARTITION BY for the mirror from how it is queried; a source partition is by the write path, a ClickHouse one by the read path.");
267
+ }
268
+
269
+ if (t.comment && opts.composite) {
270
+ report.note("SQLRV011", "info", object, "The table's description is not carried: the CdcMirror call has no place for it.", [`COMMENT ON TABLE: ${oneLine(t.comment)}`], undefined, "Emit without --use-composites to keep it as a COMMENT clause.");
271
+ }
272
+
273
+ return {
274
+ export: key,
275
+ name: mirrorName,
276
+ source: { schema, table: t.name },
277
+ columns,
278
+ primaryKey: sortKey.orderBy,
279
+ settings: sortKey.settings,
280
+ constraintComments: constraints.comments,
281
+ ...(t.comment && !opts.composite ? { comment: t.comment } : {}),
282
+ };
283
+ }
284
+
285
+ /** Identity, serial and generated columns: plain columns here, said once each. */
286
+ function describeGenerated(col: ColumnDef, serial: boolean, object: string, report: Report): void {
287
+ if (col.generated?.kind === "identity") {
288
+ report.note(
289
+ "SQLRV006",
290
+ "info",
291
+ object,
292
+ `Identity column ${col.name} is a plain column here.`,
293
+ [`GENERATED ${col.generated.always ? "ALWAYS" : "BY DEFAULT"} AS IDENTITY${col.generated.options ? ` (${oneLine(col.generated.options)})` : ""}`, "ClickHouse has no auto-increment column, and the pipeline loads the value the source made"],
294
+ col.name,
295
+ );
296
+ } else if (col.generated !== undefined) {
297
+ report.note(
298
+ "SQLRV006",
299
+ "warning",
300
+ object,
301
+ `Generated column ${col.name} is a plain column here.`,
302
+ [`GENERATED ALWAYS AS (${oneLine(col.generated.expr ?? "")}) ${col.generated.kind.toUpperCase()}`, "the pipeline loads the stored value; a virtual column is not sent by every pipeline"],
303
+ col.name,
304
+ col.generated.kind === "virtual" ? "Add the column as ALIAS or MATERIALIZED with the expression rewritten in ClickHouse SQL, if the pipeline does not send it." : undefined,
305
+ );
306
+ } else if (serial) {
307
+ report.note("SQLRV006", "info", object, `Serial column ${col.name} is a plain integer column here.`, ["serial draws from a sequence, which ClickHouse does not have, and the pipeline loads the value the source made"], col.name);
308
+ }
309
+ }
310
+
311
+ /** What an object of the source that is not a table is called in a finding. */
312
+ function describeObject(e: Declarable): string {
313
+ const props = propsOf<{ name?: string; schema?: string }>(e);
314
+ return `${e.entityType.replace(/^Postgres::/, "").toLowerCase()} ${display(props.schema, props.name ?? "?")}`;
315
+ }
316
+
317
+ export function translatePostgres(entities: ReadonlyMap<string, Declarable>, opts: TranslateOptions): Translation {
318
+ const report = new Report();
319
+ const tables = tablesOf(entities);
320
+ const indexes = [...entities.values()].filter((e) => isPostgresObject(e) && e.entityType === POSTGRES_ENTITY_TYPES.index).map((e) => propsOf<IndexProps>(e));
321
+
322
+ // Names: a table keeps its bare name unless another schema has a table of that name.
323
+ const counts = new Map<string, number>();
324
+ for (const { props } of tables) counts.set(props.name, (counts.get(props.name) ?? 0) + 1);
325
+ const schemas = new Set(tables.map(({ props }) => props.schema).filter((s): s is string => s !== undefined && s !== DEFAULT_SCHEMA));
326
+
327
+ const mirrors: Mirror[] = [];
328
+ for (const { key, props } of tables) {
329
+ const collides = (counts.get(props.name) ?? 0) > 1;
330
+ const name = collides ? display(props.schema ?? DEFAULT_SCHEMA, props.name) : props.name;
331
+ if (collides) {
332
+ report.note("SQLRV011", "info", display(props.schema, props.name), `The table keeps its schema as a database, ${name}, because two schemas declare a table of this name.`, ["the Postgres schema is otherwise not part of the mirror's name"]);
333
+ }
334
+ const mine = indexes.filter((ix) => tableMatches(ix, props));
335
+ const m = translateTable(key, props, mine, name, opts, report);
336
+ if (m) mirrors.push(m);
337
+ }
338
+ for (const s of [...schemas].sort()) {
339
+ report.note(
340
+ "SQLRV011",
341
+ "info",
342
+ s,
343
+ `Schema ${s} is not part of the mirror's name.`,
344
+ ["a pipeline writes to one ClickHouse database, which the profile or the pipeline names"],
345
+ undefined,
346
+ `Create the tables in the database your pipeline targets (${s} is a common choice).`,
347
+ );
348
+ }
349
+
350
+ // Everything else the source declares: named once, with no decision to make.
351
+ for (const e of entities.values()) {
352
+ if (!isPostgresObject(e)) continue;
353
+ if (e.entityType === POSTGRES_ENTITY_TYPES.table || e.entityType === POSTGRES_ENTITY_TYPES.index || e.entityType === POSTGRES_ENTITY_TYPES.schema) continue;
354
+ report.note("SQLRV011", "info", describeObject(e), `Not carried: ${describeObject(e)}.`, ["a mirror is made of tables; this object is not one"]);
355
+ }
356
+
357
+ const outputs = mirrors.flatMap((m) => m.columns.map((c) => `${m.source.schema}.${m.source.table}.${c.name}`));
358
+ return { mirrors, diagnostics: report.diagnostics, outputs };
359
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { TYPE_FAMILIES } from "../../generated/clickhouse";
3
+ import { numericParam, readType, typeTokens } from "../../clickhouse/type-syntax";
4
+ import { mapPostgresType, parseType } from "./types";
5
+
6
+ /** [Postgres type as written, ClickHouse type]: the maps that need no review. */
7
+ const MECHANICAL: Array<[string, string]> = [
8
+ ["bigint", "Int64"],
9
+ ["int8", "Int64"],
10
+ ["integer", "Int32"],
11
+ ["int", "Int32"],
12
+ ["smallint", "Int16"],
13
+ ["text", "String"],
14
+ ["varchar(40)", "String"],
15
+ ["character varying(255)", "String"],
16
+ ["citext", "String"],
17
+ ["bytea", "String"],
18
+ ["numeric(12, 2)", "Decimal(12, 2)"],
19
+ ["decimal(10)", "Decimal(10, 0)"],
20
+ ["uuid", "UUID"],
21
+ ["boolean", "Bool"],
22
+ ["bool", "Bool"],
23
+ ["jsonb", "JSON"],
24
+ ["json", "JSON"],
25
+ ["real", "Float32"],
26
+ ["double precision", "Float64"],
27
+ ["float(24)", "Float32"],
28
+ ["float(53)", "Float64"],
29
+ ["date", "Date32"],
30
+ ["time", "Time64(6)"],
31
+ ["timestamp", "DateTime64(6)"],
32
+ ["timestamp(3) without time zone", "DateTime64(3)"],
33
+ ["timestamptz", "DateTime64(6, 'UTC')"],
34
+ ["timestamp with time zone", "DateTime64(6, 'UTC')"],
35
+ ["timestamptz(3)", "DateTime64(3, 'UTC')"],
36
+ ["oid", "UInt32"],
37
+ ["pg_catalog.int4", "Int32"],
38
+ ['"int8"', "Int64"],
39
+ ];
40
+
41
+ describe("mapPostgresType", () => {
42
+ test.each(MECHANICAL)("%s is %s with no note", (type, expected) => {
43
+ const m = mapPostgresType(type);
44
+ expect(m.type).toBe(expected);
45
+ expect(m.note).toBeUndefined();
46
+ });
47
+
48
+ test("a character type keeps its declared length for the review rules", () => {
49
+ expect(mapPostgresType("varchar(20)").maxLength).toBe(20);
50
+ expect(mapPostgresType("text").maxLength).toBeUndefined();
51
+ });
52
+
53
+ test.each([
54
+ ["numeric", "Decimal(38, 18)", "no precision"],
55
+ ["numeric(100, 2)", "Decimal(76, 38)", "76 digits"],
56
+ ["money", "Decimal(19, 2)", "lc_monetary"],
57
+ ["char(8)", "String", "pads"],
58
+ ["interval", "String", "Interval"],
59
+ ["timetz", "String", "offset"],
60
+ ["inet", "IPv6", "prefix"],
61
+ ["xml", "String", "xml"],
62
+ ["tsvector", "String", "tsvector"],
63
+ ["mood", "String", "not a built-in type"],
64
+ ["shop.status", "String", "not a built-in type"],
65
+ ])("%s is a judgment: %s, with a note naming %s", (type, expected, evidence) => {
66
+ const m = mapPostgresType(type);
67
+ expect(m.type).toBe(expected);
68
+ expect(JSON.stringify(m.note).toLowerCase()).toContain(evidence.toLowerCase());
69
+ });
70
+
71
+ test("serial types are integers with the serial flag", () => {
72
+ expect(mapPostgresType("serial")).toMatchObject({ type: "Int32", serial: true });
73
+ expect(mapPostgresType("bigserial")).toMatchObject({ type: "Int64", serial: true });
74
+ expect(mapPostgresType("smallserial")).toMatchObject({ type: "Int16", serial: true });
75
+ });
76
+
77
+ test("an array is Array(T) with a note that it cannot be Nullable; a nested one nests", () => {
78
+ for (const written of ["text[]", "text ARRAY", "text[3]"]) {
79
+ const m = mapPostgresType(written);
80
+ expect(m, written).toMatchObject({ type: "Array(String)", array: true });
81
+ expect(JSON.stringify(m.note)).toContain("Nullable");
82
+ }
83
+ expect(mapPostgresType("integer[][]").type).toBe("Array(Array(Int32))");
84
+ expect(mapPostgresType("timestamptz[]").type).toBe("Array(DateTime64(6, 'UTC'))");
85
+ });
86
+ });
87
+
88
+ describe("parseType", () => {
89
+ test("splits words, arguments and dimensions", () => {
90
+ expect(parseType("Timestamp(3) With Time Zone")).toMatchObject({ base: "timestamp with time zone", args: [3], dims: 0 });
91
+ expect(parseType("numeric(12, 2)[]")).toMatchObject({ base: "numeric", args: [12, 2], dims: 1 });
92
+ });
93
+ });
94
+
95
+ describe("every target type is in the pinned ClickHouse catalog", () => {
96
+ const written = [...MECHANICAL.map(([t]) => t), "numeric", "money", "inet", "text[]", "integer[][]", "timestamptz(6)", "timestamp(0)"];
97
+
98
+ test("each type's family exists in TYPE_FAMILIES, and its precision is in range", () => {
99
+ for (const w of written) {
100
+ const m = mapPostgresType(w);
101
+ const t = readType(typeTokens(m.type)!)!;
102
+ const family = t.words.map((x) => x.text).join(" ");
103
+ expect(family in TYPE_FAMILIES, `${w} -> ${m.type}: ${family}`).toBe(true);
104
+ const params = (t.params ?? []).map(numericParam);
105
+ if (family === "DateTime64" || family === "Time64") {
106
+ expect(params[0]).toBeGreaterThanOrEqual(0);
107
+ expect(params[0]).toBeLessThanOrEqual(9);
108
+ }
109
+ if (family === "Decimal") {
110
+ expect(params[0]).toBeGreaterThanOrEqual(1);
111
+ expect(params[0]).toBeLessThanOrEqual(76);
112
+ expect(params[1]).toBeLessThanOrEqual(params[0]!);
113
+ }
114
+ }
115
+ });
116
+ });