@intentius/chant-lexicon-sql 0.124.46 → 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,230 @@
1
+ import { DATABASE_ENGINES, FORMATS, TABLE_ENGINES } from "../../generated/clickhouse";
2
+ import { parseEngineSyntax } from "../../clickhouse/engine-syntax";
3
+ import { COLLECTION_ENGINES, engineCollection } from "../../clickhouse/named-collections";
4
+ import { isTrivia, tokenizeText, type Token } from "../../clickhouse/tokens";
5
+ import { checkOf, splitTop } from "./clickhouse-helpers";
6
+ import { clickhouseObjects, type OutputObject } from "./sql-helpers";
7
+
8
+ /** A syntax-line argument that holds a secret: `password`, `secret_access_key`, `account_key`, `session_token`, `oauth_token`. */
9
+ const SECRET_ARGUMENT = /password|secret|account_key|session_token|oauth_token/i;
10
+
11
+ /** A `key = value` argument or a setting that holds a secret: the above, and `kafka_sasl_password`, `nats_token`. */
12
+ const SECRET_KEY = /password|secret|account_key|session_token|oauth_token|(?:^|_)token$|private_key/i;
13
+
14
+ /** Credentials written inside another value: a URL's `user:password@`, an Azure connection string's key or signature. */
15
+ const EMBEDDED = [
16
+ { re: /^[a-z][a-z0-9+.-]*:\/\/[^/@:\s]+:[^/@\s]+@/i, what: "a URL with a password in it" },
17
+ { re: /(?:^|;)\s*(?:AccountKey|SharedAccessSignature)\s*=/i, what: "a connection string with an account key or signature in it" },
18
+ ];
19
+
20
+ /** Compression methods the integration engines take after the format. */
21
+ const COMPRESSION = new Set(["none", "auto", "gzip", "gz", "deflate", "br", "brotli", "xz", "lzma", "zstd", "zst", "lz4", "bz2", "snappy"]);
22
+ const FORMAT_NAMES = new Set(FORMATS.map((f) => f.toLowerCase()));
23
+
24
+ /** The table functions whose arguments follow an engine's syntax line, lower case, by engine. */
25
+ const FUNCTION_ENGINE: Readonly<Record<string, string>> = {
26
+ s3: "S3",
27
+ gcs: "S3",
28
+ oss: "S3",
29
+ cosn: "S3",
30
+ url: "URL",
31
+ mysql: "MySQL",
32
+ postgresql: "PostgreSQL",
33
+ mongodb: "MongoDB",
34
+ azureblobstorage: "AzureBlobStorage",
35
+ iceberg: "Iceberg",
36
+ icebergs3: "IcebergS3",
37
+ icebergazure: "IcebergAzure",
38
+ deltalake: "DeltaLake",
39
+ deltalakes3: "DeltaLakeS3",
40
+ deltalakeazure: "DeltaLakeAzure",
41
+ hudi: "Hudi",
42
+ paimon: "Paimon",
43
+ paimons3: "PaimonS3",
44
+ arrowflight: "ArrowFlight",
45
+ ytsaurus: "YTsaurus",
46
+ };
47
+
48
+ /** `remote('addresses', db, table, 'user', 'password')`: no syntax line says it, so its password's position is written here. */
49
+ const FUNCTION_SECRETS: Readonly<Record<string, Array<{ name: string; position: number }>>> = {
50
+ remote: [{ name: "password", position: 4 }],
51
+ remotesecure: [{ name: "password", position: 4 }],
52
+ };
53
+
54
+ const ENGINE_SYNTAX = { ...TABLE_ENGINES, ...DATABASE_ENGINES } as Readonly<Record<string, { syntax: string } | undefined>>;
55
+
56
+ /** The secret arguments an engine's syntax line names, with their positions. */
57
+ function secretPositions(engine: string): Array<{ name: string; position: number }> {
58
+ const syntax = ENGINE_SYNTAX[engine]?.syntax;
59
+ const parsed = syntax ? parseEngineSyntax(syntax) : undefined;
60
+ if (!parsed) return [];
61
+ return parsed.args
62
+ .filter((a) => !a.named && a.name.split("|").some((n) => SECRET_ARGUMENT.test(n)))
63
+ .map((a) => ({ name: a.name.split("|").find((n) => SECRET_ARGUMENT.test(n))!, position: a.position }));
64
+ }
65
+
66
+ /** The text of a string literal, or undefined for anything else. */
67
+ function stringValue(text: string): string | undefined {
68
+ const t = text.trim();
69
+ if (!/^'(?:[^'\\]|\\.|'')*'$/.test(t)) return undefined;
70
+ return t.slice(1, -1).replace(/\\(.)/g, "$1").replace(/''/g, "'");
71
+ }
72
+
73
+ /**
74
+ * Whether the arguments before `position` leave it where the syntax line puts
75
+ * it. An optional group (`[, access_key_id, secret_access_key]`) may be left
76
+ * out, and the format and compression move up into its place:
77
+ * `S3('url', 'Parquet', 'gzip')`. When a format, a compression method or
78
+ * `NOSIGN` comes earlier, the position says nothing.
79
+ */
80
+ function aligned(args: readonly string[], position: number): boolean {
81
+ for (const a of args.slice(1, position)) {
82
+ const t = a.trim();
83
+ if (t.toUpperCase() === "NOSIGN") return false;
84
+ const v = stringValue(t)?.toLowerCase();
85
+ if (v !== undefined && (FORMAT_NAMES.has(v) || COMPRESSION.has(v))) return false;
86
+ }
87
+ return true;
88
+ }
89
+
90
+ interface Finding {
91
+ where: string;
92
+ what: string;
93
+ masked: boolean;
94
+ }
95
+
96
+ /** The literal credentials in one engine's or table function's arguments. */
97
+ function argumentFindings(label: string, args: readonly string[], secrets: ReadonlyArray<{ name: string; position: number }>, collection: boolean): Finding[] {
98
+ const out: Finding[] = [];
99
+ const add = (where: string, what: string, value: string) => out.push({ where, what, masked: value === "[HIDDEN]" });
100
+ // After a named collection, the arguments are `key = value` overrides, not positions.
101
+ if (!collection) {
102
+ for (const s of secrets) {
103
+ const arg = args[s.position];
104
+ const value = arg !== undefined ? stringValue(arg) : undefined;
105
+ if (value && aligned(args, s.position)) add(`${label} argument ${s.name}`, "a literal credential", value);
106
+ }
107
+ }
108
+ args.forEach((arg, i) => {
109
+ const kv = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]+)$/.exec(arg);
110
+ if (kv) {
111
+ const value = stringValue(kv[2]!);
112
+ if (value && SECRET_KEY.test(kv[1]!)) add(`${label} argument ${kv[1]}`, "a literal credential", value);
113
+ return;
114
+ }
115
+ const value = stringValue(arg);
116
+ if (!value) return;
117
+ for (const e of EMBEDDED) if (e.re.test(value)) add(`${label} argument ${i + 1}`, e.what, value);
118
+ });
119
+ return out;
120
+ }
121
+
122
+ /** Each table function call in a SQL text, with its arguments as written. */
123
+ function functionCalls(text: string): Array<{ name: string; args: string[] }> {
124
+ let tokens: Token[];
125
+ try {
126
+ tokens = tokenizeText(text, 0);
127
+ } catch {
128
+ return [];
129
+ }
130
+ const sig = tokens.map((t, i) => ({ t, i })).filter(({ t }) => !isTrivia(t));
131
+ const out: Array<{ name: string; args: string[] }> = [];
132
+ for (let k = 0; k + 1 < sig.length; k++) {
133
+ const fn = sig[k]!.t;
134
+ const open = sig[k + 1]!;
135
+ if (fn.kind !== "ident" || open.t.kind !== "punct" || open.t.text !== "(") continue;
136
+ const lower = fn.text.toLowerCase();
137
+ const base = lower.endsWith("cluster") ? lower.slice(0, -"cluster".length) : lower;
138
+ if (!(base in FUNCTION_ENGINE) && !(base in FUNCTION_SECRETS)) continue;
139
+ let depth = 0;
140
+ let end = open.i;
141
+ for (let j = open.i; j < tokens.length; j++) {
142
+ const t = tokens[j]!;
143
+ if (t.kind === "punct" && t.text === "(") depth++;
144
+ else if (t.kind === "punct" && t.text === ")" && --depth === 0) {
145
+ end = j;
146
+ break;
147
+ }
148
+ }
149
+ const inner = tokens
150
+ .slice(open.i + 1, end)
151
+ .map((t) => t.text)
152
+ .join("");
153
+ out.push({ name: fn.text, args: inner.trim() === "" ? [] : splitTop(inner) });
154
+ }
155
+ return out;
156
+ }
157
+
158
+ /** The literal credentials in each table function call a SQL text makes. */
159
+ function callFindings(text: string): Finding[] {
160
+ return functionCalls(text).flatMap((call) => {
161
+ const lower = call.name.toLowerCase();
162
+ const clustered = lower.endsWith("cluster") && !(lower in FUNCTION_ENGINE);
163
+ const base = clustered ? lower.slice(0, -"cluster".length) : lower;
164
+ // `s3Cluster(cluster, url, ...)`: the cluster comes first.
165
+ const args = clustered ? call.args.slice(1) : call.args;
166
+ const engine = FUNCTION_ENGINE[base];
167
+ const secrets = engine ? secretPositions(engine) : (FUNCTION_SECRETS[base] ?? []);
168
+ const collection = args[0] !== undefined && /^\s*(?:[A-Za-z_][A-Za-z0-9_]*|`[^`]+`)\s*$/.test(args[0]) && args[0].trim().toUpperCase() !== "NOSIGN";
169
+ return argumentFindings(`${call.name}()`, args, secrets, collection);
170
+ });
171
+ }
172
+
173
+ /** The literal credentials in a table's or database's engine and its settings. */
174
+ function engineFindings(o: OutputObject & { settings?: Record<string, string> }): Finding[] {
175
+ const engine = o.engine;
176
+ if (!engine) return [];
177
+ const out = argumentFindings(`ENGINE = ${engine.name}`, engine.args ?? [], secretPositions(engine.name), engineCollection(engine) !== undefined);
178
+ if (COLLECTION_ENGINES.has(engine.name)) {
179
+ for (const [key, raw] of Object.entries(o.settings ?? {})) {
180
+ const value = stringValue(raw);
181
+ if (value && SECRET_KEY.test(key)) out.push({ where: `${engine.name} setting ${key}`, what: "a literal credential", masked: value === "[HIDDEN]" });
182
+ }
183
+ }
184
+ return out;
185
+ }
186
+
187
+ /** `PASSWORD 'secret'` in a dictionary's source. */
188
+ function sourceFindings(dataSource: string): Finding[] {
189
+ const m = /\bPASSWORD\s+('(?:[^'\\]|\\.|'')*')/i.exec(dataSource);
190
+ const value = m ? stringValue(m[1]!) : undefined;
191
+ return value ? [{ where: "SOURCE PASSWORD", what: "a literal credential", masked: value === "[HIDDEN]" }] : [];
192
+ }
193
+
194
+ /**
195
+ * SQLCH128: an integration engine's or table function's argument holds a
196
+ * literal credential (#4395): `ENGINE = S3('https://...', 'AKIA...', 'secret')`,
197
+ * `mysql('db:3306', 'shop', 'orders', 'app', 'secret')`, a Kafka table's
198
+ * `kafka_sasl_password` setting, a dictionary source's `PASSWORD`.
199
+ *
200
+ * A credential is the environment's, so it does not belong in a declaration,
201
+ * the build output or `clickhouse.sql`, as `IDENTIFIED BY` does not on a
202
+ * user (`../../clickhouse/access.ts`). The secret arguments are found by name
203
+ * from each engine's syntax line in the pinned catalog (`password`,
204
+ * `secret_access_key`, `account_key`, `session_token`, `oauth_token`), as
205
+ * `key = value` overrides and settings by the same names, and inside a URL
206
+ * (`user:password@`) or an Azure connection string (`AccountKey=`). The fix
207
+ * is a named collection the environment creates, named in place of the
208
+ * arguments: `ENGINE = S3(lake_creds, format = 'Parquet')`.
209
+ */
210
+ export const sqlch128 = checkOf({ id: "SQLCH128", description: "An integration engine or table function argument holds a literal credential" }, (ctx, report) => {
211
+ for (const o of clickhouseObjects(ctx)) {
212
+ const props = o as OutputObject & { settings?: Record<string, string>; asFunction?: string; select?: string; dataSource?: string };
213
+ const findings = [
214
+ ...engineFindings(props),
215
+ ...(props.asFunction ? callFindings(props.asFunction) : []),
216
+ ...(props.select ? callFindings(props.select) : []),
217
+ ...(props.dataSource ? sourceFindings(props.dataSource) : []),
218
+ ];
219
+ for (const f of findings) {
220
+ const held = f.masked ? "holds '[HIDDEN]', the server's mask for a credential, in place of one" : `holds ${f.what}, which is the environment's`;
221
+ report({
222
+ severity: "error",
223
+ message:
224
+ `${o.export} (${o.name}): ${f.where} ${held}. ` +
225
+ "Create a named collection with the credentials on the server and name it in place of the arguments (ENGINE = S3(lake_creds, format = 'Parquet'), s3(lake_creds), SOURCE(MYSQL(NAME mysql_creds))); chant sql plan checks that it exists",
226
+ entity: o.export,
227
+ });
228
+ }
229
+ }
230
+ });
@@ -17,12 +17,12 @@ export const POSTGRES_TEMPLATE_SOURCES: readonly TemplateSource<PostgresTag>[] =
17
17
  {
18
18
  dialect: "postgres",
19
19
  modules: ["@intentius/chant-lexicon-sql/postgres"],
20
- tags: ["schema", "table", "index", "view", "sequence", "type", "domain", "extension", "func", "procedure", "aggregate", "trigger", "eventTrigger", "collation", "cast", "policy", "role", "grant"],
20
+ tags: ["schema", "table", "index", "view", "sequence", "type", "domain", "extension", "func", "procedure", "aggregate", "trigger", "eventTrigger", "operator", "statistics", "foreignDataWrapper", "foreignServer", "publication", "collation", "cast", "policy", "role", "grant"],
21
21
  },
22
22
  {
23
23
  dialect: "postgres",
24
24
  modules: ["@intentius/chant-lexicon-sql"],
25
- tags: ["schema", "index", "sequence", "type", "domain", "extension", "func", "procedure", "aggregate", "trigger", "eventTrigger", "collation", "cast", "policy", "role", "grant"],
25
+ tags: ["schema", "index", "sequence", "type", "domain", "extension", "func", "procedure", "aggregate", "trigger", "eventTrigger", "operator", "statistics", "foreignDataWrapper", "foreignServer", "publication", "collation", "cast", "policy", "role", "grant"],
26
26
  },
27
27
  ];
28
28
 
@@ -152,6 +152,17 @@ export function postgresDeclarations(content: string, fileName = "file.ts"): Pos
152
152
  sqlName = text(node.name.span);
153
153
  kind = "event trigger";
154
154
  break;
155
+ case "operator":
156
+ sqlName = text(node.name.span);
157
+ kind = "operator";
158
+ break;
159
+ case "statistics":
160
+ case "foreignDataWrapper":
161
+ case "foreignServer":
162
+ case "publication":
163
+ sqlName = text(node.name.span);
164
+ kind = { statistics: "statistics", foreignDataWrapper: "foreign data wrapper", foreignServer: "foreign server", publication: "publication" }[node.statement];
165
+ break;
155
166
  case "index":
156
167
  sqlName = node.name ? text(node.name.span) : undefined;
157
168
  break;
@@ -81,7 +81,7 @@ export function postgresSearch(params: Record<string, unknown>): unknown {
81
81
  };
82
82
  }
83
83
 
84
- const TAGS = { schema: pg.schema, table: pg.table, index: pg.index, view: pg.view, sequence: pg.sequence, type: pg.type, domain: pg.domain, extension: pg.extension, func: pg.func, procedure: pg.procedure, aggregate: pg.aggregate, trigger: pg.trigger, eventTrigger: pg.eventTrigger, collation: pg.collation, cast: pg.cast, policy: pg.policy, role: pg.role, grant: pg.grant } as const;
84
+ const TAGS = { schema: pg.schema, table: pg.table, index: pg.index, view: pg.view, sequence: pg.sequence, type: pg.type, domain: pg.domain, extension: pg.extension, func: pg.func, procedure: pg.procedure, aggregate: pg.aggregate, trigger: pg.trigger, eventTrigger: pg.eventTrigger, operator: pg.operator, statistics: pg.statistics, foreignDataWrapper: pg.foreignDataWrapper, foreignServer: pg.foreignServer, publication: pg.publication, collation: pg.collation, cast: pg.cast, policy: pg.policy, role: pg.role, grant: pg.grant } as const;
85
85
  export const POSTGRES_TAGS = Object.keys(TAGS);
86
86
 
87
87
  /** Parse one statement with the Postgres tag; an error comes back as SQLPG001 with its line and column. */
@@ -0,0 +1,165 @@
1
+ /**
2
+ * `chant migrate --from sql/postgres --to sql/clickhouse` in-process, for the
3
+ * second Done-when box of #4399: every decision is a SARIF diagnostic, and
4
+ * `--strict` exits 1 while one is open. Also the naming of the two sides, the
5
+ * member directory as a source, and the workspace links written beside the
6
+ * output. `--validate` needs the emulator: `validate.e2e.test.ts`.
7
+ */
8
+
9
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ import { afterAll, afterEach, describe, expect, test, vi } from "vitest";
13
+ import { parseArgs } from "@intentius/chant/cli/parse-args";
14
+ import { runMigrate } from "@intentius/chant/cli/handlers/migrate";
15
+
16
+ const dir = mkdtempSync(join(tmpdir(), "migrate-pg-ch-"));
17
+ afterAll(() => rmSync(dir, { recursive: true, force: true }));
18
+ afterEach(() => vi.restoreAllMocks());
19
+
20
+ const SCHEMA = "CREATE TABLE users (id bigint PRIMARY KEY, email text NOT NULL, status text NOT NULL, bio text);\n";
21
+ let n = 0;
22
+
23
+ function write(path: string, text: string): string {
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ writeFileSync(path, text);
26
+ return path;
27
+ }
28
+
29
+ async function migrate(argv: string[]) {
30
+ const args = parseArgs(["migrate", ...argv]);
31
+ const err = vi.spyOn(console, "error").mockImplementation(() => {});
32
+ vi.spyOn(console, "log").mockImplementation(() => {});
33
+ const out = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
34
+ const code = await runMigrate({ args, plugins: [], serializers: [] });
35
+ return { code, stderr: err.mock.calls.map((c) => String(c[0])).join("\n"), stdout: out.mock.calls.map((c) => String(c[0])).join("") };
36
+ }
37
+
38
+ const input = write(join(dir, "schema.sql"), SCHEMA);
39
+ const sides = ["--from", "sql/postgres", "--to", "sql/clickhouse"];
40
+
41
+ describe("chant migrate --from sql/postgres --to sql/clickhouse", () => {
42
+ test("writes declarations, exits 0 and prints the review summary", async () => {
43
+ const out = join(dir, `plain-${++n}.ts`);
44
+ const r = await migrate([input, ...sides, "--output", out]);
45
+ expect(r.code).toBe(0);
46
+ expect(readFileSync(out, "utf8")).toContain("export const users = table`");
47
+ expect(r.stderr).toContain("## Migration report");
48
+ expect(r.stderr).toContain("SQLRV001");
49
+ // The GitLab pair's advice does not appear.
50
+ expect(r.stderr).not.toContain("GitLab");
51
+ });
52
+
53
+ test("--use-composites writes a CdcMirror call; --emit sql writes DDL; the default for this pair is ts", async () => {
54
+ const ts = join(dir, `c-${++n}.ts`);
55
+ expect((await migrate([input, ...sides, "--use-composites", "--output", ts])).code).toBe(0);
56
+ expect(readFileSync(ts, "utf8")).toContain('columns: "id Int64, email String, status String, bio Nullable(String)"');
57
+ const sql = join(dir, `c-${++n}.sql`);
58
+ expect((await migrate([input, ...sides, "--emit", "sql", "--output", sql])).code).toBe(0);
59
+ expect(readFileSync(sql, "utf8")).toMatch(/^CREATE TABLE users/);
60
+ });
61
+
62
+ test("--report writes every finding as SARIF with its rule id, entity and the source file", async () => {
63
+ const report = join(dir, `r-${++n}.sarif`);
64
+ const r = await migrate([input, ...sides, "--output", join(dir, `r-${n}.ts`), "--report", report]);
65
+ expect(r.code).toBe(0);
66
+ const sarif = JSON.parse(readFileSync(report, "utf8")) as { runs: Array<{ results: Array<{ ruleId: string; level: string; message: { text: string }; locations: Array<{ physicalLocation: { artifactLocation: { uri: string } } }> }> }> };
67
+ const results = sarif.runs[0]!.results;
68
+ expect(results.map((x) => x.ruleId).sort()).toEqual(["SQLRV001", "SQLRV002", "SQLRV003", "SQLRV004"]);
69
+ expect(results.every((x) => x.level === "warning")).toBe(true);
70
+ expect(results.every((x) => x.locations[0]!.physicalLocation.artifactLocation.uri.endsWith("schema.sql"))).toBe(true);
71
+ expect(results.find((x) => x.ruleId === "SQLRV003")!.message.text).toContain("users.bio");
72
+ });
73
+
74
+ test("the report describes each rule from the catalog, with the documentation it rests on", async () => {
75
+ const report = join(dir, `m-${++n}.sarif`);
76
+ await migrate([input, ...sides, "--output", join(dir, `m-${n}.ts`), "--report", report]);
77
+ const sarif = JSON.parse(readFileSync(report, "utf8")) as { runs: Array<{ tool: { driver: { rules: Array<{ id: string; shortDescription: { text: string }; helpUri: string }> } } }> };
78
+ const rule = sarif.runs[0]!.tool.driver.rules.find((r) => r.id === "SQLRV001")!;
79
+ expect(rule.shortDescription.text).toContain("sort-key");
80
+ expect(rule.helpUri).toContain("clickhouse.com/docs");
81
+ });
82
+
83
+ test("--strict exits 1 while a warning is open, and the report then marks them errors", async () => {
84
+ const report = join(dir, `s-${++n}.sarif`);
85
+ const r = await migrate([input, ...sides, "--strict", "--output", join(dir, `s-${n}.ts`), "--report", report]);
86
+ expect(r.code).toBe(1);
87
+ const sarif = JSON.parse(readFileSync(report, "utf8")) as { runs: Array<{ results: Array<{ level: string }> }> };
88
+ expect(sarif.runs[0]!.results.every((x) => x.level === "error")).toBe(true);
89
+ });
90
+
91
+ test("a side with a slash that the lexicon does not know is refused by name", async () => {
92
+ const r = await migrate([input, "--from", "sql/mysql", "--to", "sql/clickhouse"]);
93
+ expect(r.code).toBe(1);
94
+ expect(r.stderr).toContain('does not support migration from "sql/mysql"');
95
+ const back = await migrate([input, "--from", "sql/postgres", "--to", "sql/postgres"]);
96
+ expect(back.code).toBe(1);
97
+ expect(back.stderr).toContain('does not support migration from "sql/postgres"');
98
+ });
99
+
100
+ test("--emit sql is refused for a target that is not a sql lexicon", async () => {
101
+ const r = await migrate([input, "--emit", "sql"]);
102
+ expect(r.code).toBe(1);
103
+ expect(r.stderr).toContain("not a sql target");
104
+ });
105
+
106
+ test("a file that is not Postgres DDL is refused by name", async () => {
107
+ const ch = write(join(dir, "ch.sql"), "CREATE TABLE t (id UInt64) ENGINE = MergeTree ORDER BY id;\n");
108
+ const r = await migrate([ch, ...sides]);
109
+ expect(r.code).toBe(1);
110
+ expect(r.stderr).toContain("does not look like sql/postgres source");
111
+ });
112
+ });
113
+
114
+ describe("in a workspace", () => {
115
+ function workspace() {
116
+ const root = join(dir, `ws-${++n}`);
117
+ write(join(root, "chant.workspace.json"), JSON.stringify({ name: "shop", schema: 1, members: [{ name: "db", dir: "db", kind: "chant" }, { name: "mirror", dir: "mirror", kind: "chant" }] }, null, 2) + "\n");
118
+ write(join(root, "db/chant.config.ts"), "");
119
+ write(join(root, "mirror/chant.config.ts"), "");
120
+ return root;
121
+ }
122
+ const links = (root: string) => (JSON.parse(readFileSync(join(root, "chant.workspace.json"), "utf8")) as { members: Array<{ name: string; links?: Array<{ member: string; output: string }> }> }).members.find((m) => m.name === "mirror")!.links;
123
+
124
+ test("the mirror member links each mirrored column to the Postgres member's output, once", async () => {
125
+ const root = workspace();
126
+ const source = write(join(root, "db/schema.sql"), SCHEMA);
127
+ const out = join(root, "mirror/src/mirrors.ts");
128
+ const first = await migrate([source, ...sides, "--use-composites", "--output", out]);
129
+ expect(first.code).toBe(0);
130
+ expect(first.stderr).toContain("Workspace links: added 4 links from member mirror to db");
131
+ expect(links(root)).toEqual(["id", "email", "status", "bio"].map((c) => ({ member: "db", output: `public.users.${c}` })));
132
+ const again = await migrate([source, ...sides, "--use-composites", "--output", out]);
133
+ expect(again.stderr).toContain("already links every output");
134
+ expect(links(root)).toHaveLength(4);
135
+ });
136
+
137
+ test("a table added to the source adds only its links; stdout output writes none and says so", async () => {
138
+ const root = workspace();
139
+ const source = write(join(root, "db/schema.sql"), SCHEMA);
140
+ const out = join(root, "mirror/mirrors.ts");
141
+ await migrate([source, ...sides, "--output", out]);
142
+ write(source, `${SCHEMA}CREATE TABLE tags (id bigint PRIMARY KEY, label text NOT NULL);\n`);
143
+ const r = await migrate([source, ...sides, "--output", out]);
144
+ expect(r.stderr).toContain("added 2 links");
145
+ expect(links(root)).toHaveLength(6);
146
+ const toStdout = await migrate([source, ...sides]);
147
+ expect(toStdout.stderr).toContain("pass --output inside the ClickHouse member");
148
+ expect(toStdout.stdout).toContain("export const users");
149
+ });
150
+
151
+ test("a member directory or a TypeScript file is a source: its project is built and its tables translated", async () => {
152
+ const example = join(import.meta.dirname, "../../../examples/postgres-cdc-source");
153
+ for (const path of [example, join(example, "src/shop.ts")]) {
154
+ const out = join(dir, `ts-${++n}.ts`);
155
+ const r = await migrate([path, ...sides, "--use-composites", "--output", out]);
156
+ expect(r.code, r.stderr).toBe(0);
157
+ const text = readFileSync(out, "utf8");
158
+ expect(text).toContain("export const customers = CdcMirror({");
159
+ expect(text).toContain("export const orders = CdcMirror({");
160
+ expect(text).toContain("// FOREIGN KEY (customer_id) REFERENCES shop.customers (id)");
161
+ // The output sits in a temporary directory, outside any member of a workspace above the example: no link is written.
162
+ expect(r.stderr).not.toContain("added");
163
+ }
164
+ });
165
+ });
@@ -0,0 +1,7 @@
1
+ /** Kept apart from the translator so the plugin can ask it without loading the translator. */
2
+
3
+ /** Text that looks like Postgres DDL or like a Postgres declaration, and not like ClickHouse or T-SQL. */
4
+ export function looksLikePostgres(content: string): boolean {
5
+ if (/\bENGINE\s*=/i.test(content) || /^\s*GO\s*$/im.test(content)) return false;
6
+ return /\bCREATE\s+(?:UNLOGGED\s+|TEMP(?:ORARY)?\s+)?TABLE\b/i.test(content) || /@intentius\/chant-lexicon-sql\/postgres/.test(content);
7
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * A {@link Mirror} as text: ClickHouse DDL, the TypeScript declarations that
3
+ * write it, or the statements a server runs (#4399).
4
+ *
5
+ * With `composite`, a mirror is a `CdcMirror` call (`../../composites/cdc-mirror.ts`)
6
+ * that adds the version and deleted columns and the view of live rows
7
+ * itself. Without it, a mirror is a `table` declaration with the engine and
8
+ * the two columns written out, and no view.
9
+ */
10
+
11
+ import { CdcMirror } from "../../composites/cdc-mirror";
12
+ import { CLICKHOUSE_ENTITY_TYPES } from "../../clickhouse/entities";
13
+ import type { ImportedObject } from "../../clickhouse/import/ir";
14
+ import { engineReview } from "../../clickhouse/review-catalog";
15
+ import { MIRROR_COLUMNS, type Mirror } from "./translate";
16
+
17
+ const ident = (name: string): string => (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `\`${name.replace(/\\/g, "\\\\").replace(/`/g, "\\`")}\``);
18
+ const chString = (s: string): string => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
19
+ const oneLine = (s: string): string => s.replace(/\s+/g, " ").trim();
20
+
21
+ /** `id Int64`, `email String COMMENT '...'`. */
22
+ export const columnSql = (c: Mirror["columns"][number]): string => `${ident(c.name)} ${c.type}${c.comment ? ` COMMENT ${chString(oneLine(c.comment))}` : ""}`;
23
+
24
+ /** The sort key as `CdcMirror`'s `primaryKey` and `ORDER BY` write it. */
25
+ export const orderBySql = (m: Mirror): string => (m.primaryKey.length === 0 ? "tuple()" : m.primaryKey.length === 1 ? ident(m.primaryKey[0]!) : `(${m.primaryKey.map(ident).join(", ")})`);
26
+
27
+ /** The `columns` string of a `CdcMirror` call. */
28
+ export const mirrorColumns = (m: Mirror): string => m.columns.map(columnSql).join(", ");
29
+
30
+ /** The table as a bare `CREATE TABLE`, constraint comments first. */
31
+ export function tableSql(m: Mirror): string {
32
+ const engine = engineReview({ object: m.name, mode: "cdc", cdcColumns: { version: MIRROR_COLUMNS.version, deleted: MIRROR_COLUMNS.deleted } });
33
+ const settings = Object.entries(m.settings).map(([k, v]) => `${k} = ${v}`);
34
+ return [
35
+ ...m.constraintComments,
36
+ `CREATE TABLE ${ident(m.name)}`,
37
+ "(",
38
+ [...m.columns.map(columnSql), ...engine.extraColumns.map((c) => `${c.name} ${c.type}`)].map((l) => ` ${l}`).join(",\n"),
39
+ ")",
40
+ `ENGINE = ${engine.engine}`,
41
+ `ORDER BY ${orderBySql(m)}`,
42
+ ...(settings.length > 0 ? [`SETTINGS ${settings.join(", ")}`] : []),
43
+ ...(m.comment ? [`COMMENT ${chString(oneLine(m.comment))}`] : []),
44
+ ].join("\n");
45
+ }
46
+
47
+ /** The call's props, for `CdcMirror` and for the TypeScript that writes it. */
48
+ function callProps(m: Mirror) {
49
+ return { name: m.name, columns: mirrorColumns(m), primaryKey: orderBySql(m) };
50
+ }
51
+
52
+ /** The statements a server runs for the mirrors, in creation order. */
53
+ export function mirrorObjects(mirrors: readonly Mirror[], composite: boolean): ImportedObject[] {
54
+ const out: ImportedObject[] = [];
55
+ for (const m of mirrors) {
56
+ if (!composite) {
57
+ out.push({ type: CLICKHOUSE_ENTITY_TYPES.table, name: m.name, ddl: tableSql(m) });
58
+ continue;
59
+ }
60
+ // The composite writes the DDL: what is validated is what `chant build` makes of the call.
61
+ const { table, current } = CdcMirror(callProps(m));
62
+ out.push({ type: CLICKHOUSE_ENTITY_TYPES.table, name: m.name, ddl: (table.props as { ddl: string }).ddl });
63
+ out.push({ type: CLICKHOUSE_ENTITY_TYPES.view, name: `${m.name}_current`, ddl: (current.props as { ddl: string }).ddl });
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /** `--emit sql`: the statements, each ended with a semicolon. */
69
+ export function emitSql(mirrors: readonly Mirror[], composite: boolean): string {
70
+ const objects = mirrorObjects(mirrors, composite);
71
+ // A constraint comment sits in the table's own text, so a statement is the object's ddl as it is.
72
+ const comments = composite ? new Map(mirrors.map((m) => [m.name, m.constraintComments])) : new Map<string, string[]>();
73
+ return `${objects.map((o) => `${(comments.get(o.name) ?? []).map((c) => `${c}\n`).join("")}${o.ddl.replace(/\s+$/, "")};`).join("\n\n")}\n`;
74
+ }
75
+
76
+ const str = (s: string): string => JSON.stringify(s);
77
+ const templateSafe = (s: string): string => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
78
+
79
+ /** `--emit ts`: the declarations, as a file of `CdcMirror` calls or of `table` templates. */
80
+ export function emitTs(mirrors: readonly Mirror[], composite: boolean): string {
81
+ const lines: string[] = [];
82
+ if (composite) {
83
+ lines.push(`import { CdcMirror } from "@intentius/chant-lexicon-sql/clickhouse";`, "");
84
+ for (const m of mirrors) {
85
+ for (const c of m.constraintComments) lines.push(`//${c.slice(2)}`);
86
+ const p = callProps(m);
87
+ lines.push(`export const ${m.export} = CdcMirror({`, ` name: ${str(p.name)},`, ` columns: ${str(p.columns)},`, ` primaryKey: ${str(p.primaryKey)},`, `});`, "");
88
+ }
89
+ } else {
90
+ lines.push(`import { table } from "@intentius/chant-lexicon-sql/clickhouse";`, "");
91
+ for (const m of mirrors) {
92
+ lines.push(`export const ${m.export} = table\``, ...tableSql(m).split("\n").map((l) => ` ${templateSafe(l)}`), "`;", "");
93
+ }
94
+ }
95
+ return `${lines.join("\n").replace(/\n+$/, "")}\n`;
96
+ }