@intentius/chant-lexicon-sql 0.124.46 → 0.124.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/dist/change-set.d.ts +1 -1
  2. package/dist/change-set.d.ts.map +1 -1
  3. package/dist/change-set.js +2 -1
  4. package/dist/clickhouse/apply/statements.d.ts +9 -0
  5. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  6. package/dist/clickhouse/apply/statements.js +24 -0
  7. package/dist/clickhouse/import/live-export.d.ts +7 -3
  8. package/dist/clickhouse/import/live-export.d.ts.map +1 -1
  9. package/dist/clickhouse/import/live-export.js +21 -5
  10. package/dist/clickhouse/live/named-collections.d.ts +27 -0
  11. package/dist/clickhouse/live/named-collections.d.ts.map +1 -0
  12. package/dist/clickhouse/live/named-collections.js +53 -0
  13. package/dist/clickhouse/live/undeclarable.d.ts +15 -3
  14. package/dist/clickhouse/live/undeclarable.d.ts.map +1 -1
  15. package/dist/clickhouse/live/undeclarable.js +18 -4
  16. package/dist/clickhouse/named-collections.d.ts +83 -0
  17. package/dist/clickhouse/named-collections.d.ts.map +1 -0
  18. package/dist/clickhouse/named-collections.js +203 -0
  19. package/dist/clickhouse/parser.d.ts.map +1 -1
  20. package/dist/clickhouse/parser.js +1 -1
  21. package/dist/clickhouse/plan/commands.d.ts +2 -1
  22. package/dist/clickhouse/plan/commands.d.ts.map +1 -1
  23. package/dist/clickhouse/plan/commands.js +17 -2
  24. package/dist/clickhouse/plan/diff.d.ts +15 -0
  25. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  26. package/dist/clickhouse/plan/diff.js +43 -1
  27. package/dist/clickhouse/plan/disruption.d.ts.map +1 -1
  28. package/dist/clickhouse/plan/disruption.js +5 -0
  29. package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
  30. package/dist/clickhouse/plan/rebuild-handoff.js +4 -0
  31. package/dist/clickhouse/plan/report.d.ts.map +1 -1
  32. package/dist/clickhouse/plan/report.js +6 -1
  33. package/dist/clickhouse/plan/rules.d.ts +5 -1
  34. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  35. package/dist/clickhouse/plan/rules.js +7 -3
  36. package/dist/clickhouse/plan/statements.d.ts +29 -0
  37. package/dist/clickhouse/plan/statements.d.ts.map +1 -0
  38. package/dist/clickhouse/plan/statements.js +334 -0
  39. package/dist/clickhouse/plan/stream.d.ts +40 -0
  40. package/dist/clickhouse/plan/stream.d.ts.map +1 -0
  41. package/dist/clickhouse/plan/stream.js +107 -0
  42. package/dist/clickhouse/rebuild/observe.d.ts +3 -1
  43. package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
  44. package/dist/clickhouse/rebuild/observe.js +14 -1
  45. package/dist/clickhouse/review-catalog.d.ts +213 -0
  46. package/dist/clickhouse/review-catalog.d.ts.map +1 -0
  47. package/dist/clickhouse/review-catalog.js +330 -0
  48. package/dist/codegen/classifier-page.d.ts.map +1 -1
  49. package/dist/codegen/classifier-page.js +44 -4
  50. package/dist/codegen/clickhouse-module.d.ts.map +1 -1
  51. package/dist/codegen/clickhouse-module.js +3 -0
  52. package/dist/codegen/coverage-page.d.ts.map +1 -1
  53. package/dist/codegen/coverage-page.js +5 -3
  54. package/dist/codegen/docs.js +1 -1
  55. package/dist/codegen/postgres-classifier-page.js +2 -2
  56. package/dist/codegen/registry.d.ts.map +1 -1
  57. package/dist/codegen/registry.js +10 -0
  58. package/dist/composites/catalog.d.ts.map +1 -1
  59. package/dist/composites/catalog.js +100 -0
  60. package/dist/composites/clickhouse.d.ts +2 -0
  61. package/dist/composites/clickhouse.d.ts.map +1 -1
  62. package/dist/composites/clickhouse.js +2 -0
  63. package/dist/composites/otel-tables.d.ts +100 -0
  64. package/dist/composites/otel-tables.d.ts.map +1 -0
  65. package/dist/composites/otel-tables.js +282 -0
  66. package/dist/composites/stream-ingest.d.ts +63 -0
  67. package/dist/composites/stream-ingest.d.ts.map +1 -0
  68. package/dist/composites/stream-ingest.js +39 -0
  69. package/dist/core/statement-class.d.ts +55 -0
  70. package/dist/core/statement-class.d.ts.map +1 -0
  71. package/dist/core/statement-class.js +43 -0
  72. package/dist/coverage/clickhouse.json +1 -1
  73. package/dist/coverage/postgres.json +12 -12
  74. package/dist/coverage.d.ts +16 -6
  75. package/dist/coverage.d.ts.map +1 -1
  76. package/dist/coverage.js +10 -5
  77. package/dist/drizzle-command.d.ts +17 -0
  78. package/dist/drizzle-command.d.ts.map +1 -0
  79. package/dist/drizzle-command.js +91 -0
  80. package/dist/drizzle.d.ts +70 -0
  81. package/dist/drizzle.d.ts.map +1 -0
  82. package/dist/drizzle.js +689 -0
  83. package/dist/files/clickhouse.d.ts.map +1 -1
  84. package/dist/files/clickhouse.js +8 -1
  85. package/dist/files/common.d.ts +13 -0
  86. package/dist/files/common.d.ts.map +1 -1
  87. package/dist/files/common.js +5 -0
  88. package/dist/files/postgres.d.ts.map +1 -1
  89. package/dist/files/postgres.js +35 -3
  90. package/dist/generated/clickhouse.d.ts +2 -0
  91. package/dist/generated/clickhouse.d.ts.map +1 -1
  92. package/dist/generated/clickhouse.js +2 -0
  93. package/dist/import-parser.d.ts +21 -1
  94. package/dist/import-parser.d.ts.map +1 -1
  95. package/dist/import-parser.js +79 -5
  96. package/dist/index.d.ts +1 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +1 -1
  99. package/dist/integrity.json +7 -6
  100. package/dist/lint/post-synth/audit-entries.d.ts.map +1 -1
  101. package/dist/lint/post-synth/audit-entries.js +1 -0
  102. package/dist/lint/post-synth/index.d.ts.map +1 -1
  103. package/dist/lint/post-synth/index.js +2 -0
  104. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  105. package/dist/lint/post-synth/postgres-names.js +3 -0
  106. package/dist/lint/post-synth/sqlch128.d.ts +18 -0
  107. package/dist/lint/post-synth/sqlch128.d.ts.map +1 -0
  108. package/dist/lint/post-synth/sqlch128.js +222 -0
  109. package/dist/lint/rules/postgres-templates.js +2 -2
  110. package/dist/lsp/postgres-template.d.ts.map +1 -1
  111. package/dist/lsp/postgres-template.js +21 -0
  112. package/dist/manifest.json +1 -1
  113. package/dist/mcp/postgres.js +1 -1
  114. package/dist/meta.json +50 -0
  115. package/dist/migrate/from-postgres/detect.d.ts +4 -0
  116. package/dist/migrate/from-postgres/detect.d.ts.map +1 -0
  117. package/dist/migrate/from-postgres/detect.js +7 -0
  118. package/dist/migrate/from-postgres/emit.d.ts +26 -0
  119. package/dist/migrate/from-postgres/emit.d.ts.map +1 -0
  120. package/dist/migrate/from-postgres/emit.js +86 -0
  121. package/dist/migrate/from-postgres/index.d.ts +51 -0
  122. package/dist/migrate/from-postgres/index.d.ts.map +1 -0
  123. package/dist/migrate/from-postgres/index.js +115 -0
  124. package/dist/migrate/from-postgres/translate.d.ts +73 -0
  125. package/dist/migrate/from-postgres/translate.d.ts.map +1 -0
  126. package/dist/migrate/from-postgres/translate.js +261 -0
  127. package/dist/migrate/from-postgres/types.d.ts +51 -0
  128. package/dist/migrate/from-postgres/types.d.ts.map +1 -0
  129. package/dist/migrate/from-postgres/types.js +211 -0
  130. package/dist/migration-classify.d.ts +23 -0
  131. package/dist/migration-classify.d.ts.map +1 -0
  132. package/dist/migration-classify.js +44 -0
  133. package/dist/migration-replay.d.ts +73 -0
  134. package/dist/migration-replay.d.ts.map +1 -0
  135. package/dist/migration-replay.js +294 -0
  136. package/dist/okf/index.md +11 -0
  137. package/dist/okf/rules/SQLCH128.md +11 -0
  138. package/dist/okf/types/PostgresForeignDataWrapper.md +9 -0
  139. package/dist/okf/types/PostgresForeignServer.md +9 -0
  140. package/dist/okf/types/PostgresForeignTable.md +9 -0
  141. package/dist/okf/types/PostgresOperator.md +9 -0
  142. package/dist/okf/types/PostgresOperatorClass.md +9 -0
  143. package/dist/okf/types/PostgresOperatorFamily.md +9 -0
  144. package/dist/okf/types/PostgresPublication.md +9 -0
  145. package/dist/okf/types/PostgresStatistics.md +9 -0
  146. package/dist/okf/types/PostgresTextSearchConfiguration.md +9 -0
  147. package/dist/okf/types/PostgresTextSearchDictionary.md +9 -0
  148. package/dist/op/activities/postgres-migration.d.ts.map +1 -1
  149. package/dist/op/activities/postgres-migration.js +4 -0
  150. package/dist/op/activity-contracts.d.ts +20 -0
  151. package/dist/op/activity-contracts.d.ts.map +1 -1
  152. package/dist/op/activity-contracts.js +2 -0
  153. package/dist/plugin.d.ts.map +1 -1
  154. package/dist/plugin.js +49 -2
  155. package/dist/postgres/access/acl.js +1 -1
  156. package/dist/postgres/access/import.js +1 -1
  157. package/dist/postgres/apply/apply.js +13 -1
  158. package/dist/postgres/apply/statements.d.ts.map +1 -1
  159. package/dist/postgres/apply/statements.js +243 -4
  160. package/dist/postgres/constraint-names.d.ts +30 -0
  161. package/dist/postgres/constraint-names.d.ts.map +1 -0
  162. package/dist/postgres/constraint-names.js +72 -0
  163. package/dist/postgres/entities.d.ts +229 -2
  164. package/dist/postgres/entities.d.ts.map +1 -1
  165. package/dist/postgres/entities.js +326 -5
  166. package/dist/postgres/entity-types.d.ts +10 -0
  167. package/dist/postgres/entity-types.d.ts.map +1 -1
  168. package/dist/postgres/entity-types.js +10 -0
  169. package/dist/postgres/import/generator.d.ts.map +1 -1
  170. package/dist/postgres/import/generator.js +65 -2
  171. package/dist/postgres/import/live-export.d.ts.map +1 -1
  172. package/dist/postgres/import/live-export.js +2 -0
  173. package/dist/postgres/live/catalog-kinds.d.ts +20 -0
  174. package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
  175. package/dist/postgres/live/catalog-kinds.js +452 -0
  176. package/dist/postgres/live/catalog.d.ts +2 -0
  177. package/dist/postgres/live/catalog.d.ts.map +1 -1
  178. package/dist/postgres/live/catalog.js +3 -1
  179. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  180. package/dist/postgres/live/describe-resources.js +12 -2
  181. package/dist/postgres/migrate/backfill.d.ts +2 -0
  182. package/dist/postgres/migrate/backfill.d.ts.map +1 -1
  183. package/dist/postgres/migrate/backfill.js +7 -1
  184. package/dist/postgres/migrate/carry.d.ts +45 -1
  185. package/dist/postgres/migrate/carry.d.ts.map +1 -1
  186. package/dist/postgres/migrate/carry.js +72 -8
  187. package/dist/postgres/migrate/observe.d.ts +28 -4
  188. package/dist/postgres/migrate/observe.d.ts.map +1 -1
  189. package/dist/postgres/migrate/observe.js +45 -13
  190. package/dist/postgres/migrate/op.d.ts +26 -1
  191. package/dist/postgres/migrate/op.d.ts.map +1 -1
  192. package/dist/postgres/migrate/op.js +3 -1
  193. package/dist/postgres/migrate/replication.d.ts +42 -0
  194. package/dist/postgres/migrate/replication.d.ts.map +1 -1
  195. package/dist/postgres/migrate/replication.js +53 -0
  196. package/dist/postgres/migrate/steps.d.ts +35 -2
  197. package/dist/postgres/migrate/steps.d.ts.map +1 -1
  198. package/dist/postgres/migrate/steps.js +158 -12
  199. package/dist/postgres/parser.d.ts +144 -1
  200. package/dist/postgres/parser.d.ts.map +1 -1
  201. package/dist/postgres/parser.js +370 -4
  202. package/dist/postgres/plan/deep.d.ts.map +1 -1
  203. package/dist/postgres/plan/deep.js +43 -4
  204. package/dist/postgres/plan/diff.d.ts.map +1 -1
  205. package/dist/postgres/plan/diff.js +154 -1
  206. package/dist/postgres/plan/normalize.d.ts +28 -2
  207. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  208. package/dist/postgres/plan/normalize.js +260 -1
  209. package/dist/postgres/plan/rules.d.ts +4 -0
  210. package/dist/postgres/plan/rules.d.ts.map +1 -1
  211. package/dist/postgres/plan/rules.js +6 -2
  212. package/dist/postgres/plan/schema.js +1 -1
  213. package/dist/postgres/plan/statements.d.ts +26 -0
  214. package/dist/postgres/plan/statements.d.ts.map +1 -0
  215. package/dist/postgres/plan/statements.js +364 -0
  216. package/dist/postgres/secret-options.d.ts +10 -0
  217. package/dist/postgres/secret-options.d.ts.map +1 -0
  218. package/dist/postgres/secret-options.js +9 -0
  219. package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
  220. package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
  221. package/dist/postgres/testing/roundtrip-schema.js +24 -1
  222. package/dist/postgres.d.ts +1 -1
  223. package/dist/postgres.d.ts.map +1 -1
  224. package/dist/postgres.js +1 -1
  225. package/dist/rules/audit-entries.ts +1 -0
  226. package/dist/rules/postgres-names.ts +2 -0
  227. package/dist/rules/postgres-templates.ts +2 -2
  228. package/dist/rules/sqlch128.ts +230 -0
  229. package/dist/serializer.d.ts +5 -0
  230. package/dist/serializer.d.ts.map +1 -1
  231. package/dist/serializer.js +13 -2
  232. package/dist/sql-types-command.d.ts +13 -0
  233. package/dist/sql-types-command.d.ts.map +1 -1
  234. package/dist/sql-types-command.js +31 -16
  235. package/dist/sql-types.d.ts +22 -0
  236. package/dist/sql-types.d.ts.map +1 -1
  237. package/dist/sql-types.js +10 -6
  238. package/dist/sqlserver/lexer.d.ts +37 -0
  239. package/dist/sqlserver/lexer.d.ts.map +1 -0
  240. package/dist/sqlserver/lexer.js +254 -0
  241. package/dist/sqlserver/parse.d.ts +185 -0
  242. package/dist/sqlserver/parse.d.ts.map +1 -0
  243. package/dist/sqlserver/parse.js +767 -0
  244. package/dist/sqlserver/translate.d.ts +34 -0
  245. package/dist/sqlserver/translate.d.ts.map +1 -0
  246. package/dist/sqlserver/translate.js +495 -0
  247. package/dist/sqlserver/types.d.ts +44 -0
  248. package/dist/sqlserver/types.d.ts.map +1 -0
  249. package/dist/sqlserver/types.js +133 -0
  250. package/dist/sqlserver/validate.d.ts +31 -0
  251. package/dist/sqlserver/validate.d.ts.map +1 -0
  252. package/dist/sqlserver/validate.js +67 -0
  253. package/dist/validate.d.ts.map +1 -1
  254. package/dist/validate.js +10 -0
  255. package/package.json +2 -2
  256. package/src/change-set.ts +2 -1
  257. package/src/clickhouse/apply/statements.ts +25 -0
  258. package/src/clickhouse/import/live-export.ts +20 -5
  259. package/src/clickhouse/live/named-collections.e2e.test.ts +88 -0
  260. package/src/clickhouse/live/named-collections.ts +61 -0
  261. package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -3
  262. package/src/clickhouse/live/undeclarable.ts +20 -4
  263. package/src/clickhouse/named-collections.test.ts +187 -0
  264. package/src/clickhouse/named-collections.ts +224 -0
  265. package/src/clickhouse/parser.ts +2 -1
  266. package/src/clickhouse/plan/commands.ts +17 -2
  267. package/src/clickhouse/plan/diff.ts +57 -1
  268. package/src/clickhouse/plan/disruption.ts +5 -0
  269. package/src/clickhouse/plan/rebuild-handoff.ts +3 -0
  270. package/src/clickhouse/plan/report.ts +6 -1
  271. package/src/clickhouse/plan/rules.ts +14 -4
  272. package/src/clickhouse/plan/statements.test.ts +61 -0
  273. package/src/clickhouse/plan/statements.ts +306 -0
  274. package/src/clickhouse/plan/stream-s3queue.e2e.test.ts +148 -0
  275. package/src/clickhouse/plan/stream.test.ts +237 -0
  276. package/src/clickhouse/plan/stream.ts +111 -0
  277. package/src/clickhouse/rebuild/observe.ts +15 -1
  278. package/src/clickhouse/review-catalog.test.ts +181 -0
  279. package/src/clickhouse/review-catalog.ts +489 -0
  280. package/src/codegen/classifier-page.ts +44 -4
  281. package/src/codegen/clickhouse-module.ts +3 -0
  282. package/src/codegen/coverage-page.ts +5 -3
  283. package/src/codegen/docs.ts +1 -1
  284. package/src/codegen/postgres-classifier-page.ts +2 -2
  285. package/src/codegen/registry.ts +10 -0
  286. package/src/composites/catalog.test.ts +6 -1
  287. package/src/composites/catalog.ts +100 -0
  288. package/src/composites/clickhouse.ts +2 -0
  289. package/src/composites/composites.test.ts +1 -1
  290. package/src/composites/otel-tables.test.ts +118 -0
  291. package/src/composites/otel-tables.ts +339 -0
  292. package/src/composites/stream-ingest.test.ts +114 -0
  293. package/src/composites/stream-ingest.ts +82 -0
  294. package/src/core/statement-class.ts +88 -0
  295. package/src/core/undeclarable.test.ts +6 -5
  296. package/src/coverage/clickhouse.json +1 -1
  297. package/src/coverage/postgres.json +12 -12
  298. package/src/coverage.test.ts +27 -2
  299. package/src/coverage.ts +18 -7
  300. package/src/drizzle-command.ts +94 -0
  301. package/src/drizzle-kit.e2e.test.ts +89 -0
  302. package/src/drizzle-types.e2e.test.ts +67 -0
  303. package/src/drizzle.test.ts +358 -0
  304. package/src/drizzle.ts +742 -0
  305. package/src/files/clickhouse.ts +8 -1
  306. package/src/files/common.ts +14 -1
  307. package/src/files/postgres.ts +34 -3
  308. package/src/generated/clickhouse.ts +3 -0
  309. package/src/generated/lexicon-sql.json +50 -0
  310. package/src/import-parser.sqlserver.test.ts +92 -0
  311. package/src/import-parser.ts +79 -5
  312. package/src/index.ts +1 -1
  313. package/src/lint/post-synth/audit-entries.ts +1 -0
  314. package/src/lint/post-synth/index.ts +2 -0
  315. package/src/lint/post-synth/postgres-checks.test.ts +8 -1
  316. package/src/lint/post-synth/postgres-names.ts +2 -0
  317. package/src/lint/post-synth/sqlch128.test.ts +107 -0
  318. package/src/lint/post-synth/sqlch128.ts +230 -0
  319. package/src/lint/rules/postgres-templates.ts +2 -2
  320. package/src/lsp/postgres-template.ts +20 -0
  321. package/src/mcp/postgres.ts +1 -1
  322. package/src/migrate/from-postgres/cli.test.ts +165 -0
  323. package/src/migrate/from-postgres/detect.ts +7 -0
  324. package/src/migrate/from-postgres/emit.ts +96 -0
  325. package/src/migrate/from-postgres/example.test.ts +205 -0
  326. package/src/migrate/from-postgres/index.ts +122 -0
  327. package/src/migrate/from-postgres/translate.test.ts +242 -0
  328. package/src/migrate/from-postgres/translate.ts +359 -0
  329. package/src/migrate/from-postgres/types.test.ts +116 -0
  330. package/src/migrate/from-postgres/types.ts +236 -0
  331. package/src/migrate/from-postgres/validate.e2e.test.ts +44 -0
  332. package/src/migrate/from-postgres/workspace.e2e.test.ts +114 -0
  333. package/src/migration-classify.test.ts +150 -0
  334. package/src/migration-classify.ts +45 -0
  335. package/src/migration-replay.e2e.test.ts +155 -0
  336. package/src/migration-replay.test.ts +173 -0
  337. package/src/migration-replay.ts +289 -0
  338. package/src/op/__snapshots__/watch-pipeline.test.ts.snap +2 -2
  339. package/src/op/activities/postgres-migration.ts +4 -0
  340. package/src/op/activity-contracts.ts +2 -0
  341. package/src/op/watch-pipeline.test.ts +8 -1
  342. package/src/plugin.ts +51 -2
  343. package/src/postgres/access/acl.ts +1 -1
  344. package/src/postgres/access/import.ts +1 -1
  345. package/src/postgres/apply/apply.ts +14 -1
  346. package/src/postgres/apply/statements.ts +225 -4
  347. package/src/postgres/constraint-names.ts +71 -0
  348. package/src/postgres/database-kinds.test.ts +865 -0
  349. package/src/postgres/entities.ts +541 -5
  350. package/src/postgres/entity-types.ts +10 -0
  351. package/src/postgres/import/generator.ts +54 -2
  352. package/src/postgres/import/live-export.ts +1 -0
  353. package/src/postgres/live/catalog-kinds.ts +494 -0
  354. package/src/postgres/live/catalog.ts +6 -1
  355. package/src/postgres/live/database-kinds.e2e.test.ts +164 -0
  356. package/src/postgres/live/describe-resources.ts +13 -2
  357. package/src/postgres/live/undeclarable.e2e.test.ts +13 -6
  358. package/src/postgres/migrate/backfill.ts +13 -1
  359. package/src/postgres/migrate/carry.ts +103 -8
  360. package/src/postgres/migrate/followups.e2e.test.ts +218 -0
  361. package/src/postgres/migrate/followups.test.ts +199 -0
  362. package/src/postgres/migrate/kinds.e2e.test.ts +5 -5
  363. package/src/postgres/migrate/observe.ts +56 -15
  364. package/src/postgres/migrate/op.ts +28 -1
  365. package/src/postgres/migrate/partition.e2e.test.ts +129 -0
  366. package/src/postgres/migrate/partition.test.ts +206 -0
  367. package/src/postgres/migrate/replication.ts +66 -0
  368. package/src/postgres/migrate/steps.ts +157 -12
  369. package/src/postgres/migrate/subscriber.e2e.test.ts +141 -0
  370. package/src/postgres/migrate/subscriber.test.ts +59 -0
  371. package/src/postgres/parser.ts +455 -4
  372. package/src/postgres/plan/deep.ts +42 -4
  373. package/src/postgres/plan/diff.ts +137 -1
  374. package/src/postgres/plan/normalize.ts +277 -1
  375. package/src/postgres/plan/rules.ts +32 -4
  376. package/src/postgres/plan/schema.ts +1 -1
  377. package/src/postgres/plan/statements.test.ts +78 -0
  378. package/src/postgres/plan/statements.ts +299 -0
  379. package/src/postgres/secret-options.ts +10 -0
  380. package/src/postgres/testing/roundtrip-schema.ts +24 -1
  381. package/src/postgres.ts +33 -0
  382. package/src/serializer.ts +13 -2
  383. package/src/skills/chant-sql-postgres-migration.md +4 -3
  384. package/src/skills/chant-sql-postgres.md +11 -1
  385. package/src/skills/chant-sql.md +2 -1
  386. package/src/sql-files.test.ts +33 -0
  387. package/src/sql-types-command.ts +38 -16
  388. package/src/sql-types.ts +11 -6
  389. package/src/sqlserver/fixture.test.ts +186 -0
  390. package/src/sqlserver/import-cli.test.ts +61 -0
  391. package/src/sqlserver/lexer.ts +260 -0
  392. package/src/sqlserver/parse.ts +865 -0
  393. package/src/sqlserver/translate.test.ts +396 -0
  394. package/src/sqlserver/translate.ts +558 -0
  395. package/src/sqlserver/types.test.ts +102 -0
  396. package/src/sqlserver/types.ts +154 -0
  397. package/src/sqlserver/validate.e2e.test.ts +32 -0
  398. package/src/sqlserver/validate.test.ts +49 -0
  399. package/src/sqlserver/validate.ts +76 -0
  400. package/src/validate.ts +10 -0
  401. package/src/workspace-kinds.test.ts +11 -0
  402. package/workspace-kinds.json +100 -2
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Named collections referenced by name (#4395): the build records each one a
3
+ * declaration names, a plan checks each exists by name and never reads the
4
+ * values, and import lists the server's as prerequisites instead of refusing
5
+ * the server.
6
+ */
7
+
8
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
9
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
10
+ import type { SerializerResult } from "@intentius/chant/serializer";
11
+ import { sqlSerializer } from "../serializer";
12
+ import { database, dictionary, table, view } from "./entities";
13
+ import { postSynthChecks } from "../lint/post-synth/index";
14
+ import { COLLECTION_ENGINES, COLLECTION_FUNCTIONS, createdCollectionName, functionCollections, namedCollectionReferences, sourceCollections, statementCollections } from "./named-collections";
15
+ import { checkNamedCollections, referencedCollections, renderNamedCollections } from "./live/named-collections";
16
+ import { renderDiff } from "./plan/report";
17
+ import { fakeClickHouse, type FakeServer } from "./testing/fake-server";
18
+ import { exportResources } from "./import/live-export";
19
+ import { SqlFileParser } from "../import-parser";
20
+ import { KIND_COVERAGE, kindCoverageProblems } from "../coverage";
21
+ import { CLICKHOUSE_PROBES } from "./live/undeclarable";
22
+
23
+ type Doc = { namedCollections?: string[]; objects: Array<{ export: string; namedCollections?: string[] }> };
24
+
25
+ const serialize = (entities: Record<string, unknown>) => sqlSerializer.serialize(new Map(Object.entries(entities)) as never) as SerializerResult;
26
+
27
+ const lake = table`CREATE TABLE lake (a Int32) ENGINE = S3(lake_creds, url = 'http://minio:9000/bucket/x.parquet', format = 'Parquet')`;
28
+
29
+ describe("a declaration names a collection", () => {
30
+ test("ENGINE = S3(lake_creds, url = '...', format = 'Parquet') builds and records lake_creds", () => {
31
+ const out = serialize({ lake });
32
+ const doc = JSON.parse(out.primary) as Doc;
33
+ expect(doc.namedCollections).toEqual(["lake_creds"]);
34
+ expect(doc.objects[0]).toMatchObject({ export: "lake", namedCollections: ["lake_creds"] });
35
+ // No post-synth check objects to it.
36
+ const ctx = makePostSynthCtx("sql", out.primary);
37
+ expect(postSynthChecks.flatMap((c) => c.check(ctx)).filter((d) => d.severity === "error")).toEqual([]);
38
+ });
39
+
40
+ test("an engine, a database engine, a table function, a view's SELECT and a dictionary SOURCE each name one", () => {
41
+ const pg = database`CREATE DATABASE pg ENGINE = PostgreSQL(pg_creds)`;
42
+ const kafka = table`CREATE TABLE k (a String) ENGINE = Kafka(kafka_creds, kafka_format = 'JSONEachRow')`;
43
+ const over = table`CREATE TABLE over AS s3(\`lake creds\`, filename = 'x.parquet')`;
44
+ const v = view`CREATE VIEW v AS SELECT * FROM mysql(mysql_creds, table = 'orders')`;
45
+ const d = dictionary`CREATE DICTIONARY d (id UInt64, name String) PRIMARY KEY id SOURCE(MYSQL(NAME mysql_creds TABLE 'names')) LAYOUT(HASHED()) LIFETIME(300)`;
46
+ const doc = JSON.parse(serialize({ pg, kafka, over, v, d }).primary) as Doc;
47
+ expect(doc.namedCollections).toEqual(["kafka_creds", "lake creds", "mysql_creds", "pg_creds"]);
48
+ expect(Object.fromEntries(doc.objects.map((o) => [o.export, o.namedCollections]))).toEqual({
49
+ pg: ["pg_creds"],
50
+ kafka: ["kafka_creds"],
51
+ over: ["lake creds"],
52
+ v: ["mysql_creds"],
53
+ d: ["mysql_creds"],
54
+ });
55
+ });
56
+
57
+ test("a build that names none records nothing", () => {
58
+ const t = table`CREATE TABLE t (a Int32) ENGINE = S3('http://minio:9000/bucket/x.parquet', 'Parquet')`;
59
+ const plain = table`CREATE TABLE p (a Int32) ENGINE = MergeTree ORDER BY a`;
60
+ const doc = JSON.parse(serialize({ t, plain }).primary) as Doc;
61
+ expect(doc.namedCollections).toBeUndefined();
62
+ expect(doc.objects.map((o) => o.namedCollections)).toEqual([undefined, undefined]);
63
+ });
64
+
65
+ test("a first argument that is a string, NOSIGN, a cluster or a column is not a collection", () => {
66
+ expect(namedCollectionReferences({ engine: { name: "S3", args: ["NOSIGN"] } })).toEqual([]);
67
+ expect(namedCollectionReferences({ engine: { name: "Distributed", args: ["main", "db", "t"] } })).toEqual([]);
68
+ expect(namedCollectionReferences({ engine: { name: "ReplacingMergeTree", args: ["ver"] } })).toEqual([]);
69
+ expect(functionCollections("SELECT * FROM s3Cluster(main, 'http://x/y', 'CSV')")).toEqual([]);
70
+ expect(functionCollections("SELECT * FROM remote('h:9000', db, t)")).toEqual([]);
71
+ expect(functionCollections("SELECT x.s3(a) FROM t")).toEqual([]);
72
+ });
73
+
74
+ test("the engines and table functions listed are ones the pinned server has", () => {
75
+ for (const e of ["S3", "Kafka", "MySQL", "PostgreSQL", "MongoDB", "AzureBlobStorage", "RabbitMQ", "NATS"]) expect(COLLECTION_ENGINES, e).toContain(e);
76
+ for (const f of ["s3", "mysql", "postgresql", "url", "remote", "azureblobstorage"]) expect(COLLECTION_FUNCTIONS, f).toContain(f);
77
+ expect([...COLLECTION_FUNCTIONS].some((f) => f.endsWith("cluster"))).toBe(false);
78
+ });
79
+
80
+ test("a statement as the server prints it", () => {
81
+ expect(statementCollections("CREATE TABLE analytics.lake\n(\n `a` Int32\n)\nENGINE = S3(lake_creds, url = 'http://x/y', format = 'Parquet')")).toEqual(["lake_creds"]);
82
+ expect(statementCollections("CREATE DICTIONARY d (`id` UInt64, `name` String) PRIMARY KEY id SOURCE(MYSQL(NAME 'mysql_creds')) LAYOUT(HASHED()) LIFETIME(300)")).toEqual(["mysql_creds"]);
83
+ // A column called name is not a source's NAME.
84
+ expect(statementCollections("CREATE TABLE t (`name` String) ENGINE = MergeTree ORDER BY name")).toEqual([]);
85
+ expect(sourceCollections("CLICKHOUSE(TABLE 'rates')")).toEqual([]);
86
+ expect(createdCollectionName("CREATE NAMED COLLECTION IF NOT EXISTS `lake creds` AS url = 'x'")).toBe("lake creds");
87
+ });
88
+ });
89
+
90
+ describe("a plan checks each exists, by name", () => {
91
+ let server: FakeServer;
92
+ beforeAll(async () => {
93
+ server = await fakeClickHouse([], {});
94
+ });
95
+ afterAll(() => server.close());
96
+
97
+ test("the build output's list is what is checked", () => {
98
+ expect(referencedCollections(serialize({ lake }).primary)).toEqual(["lake_creds"]);
99
+ });
100
+
101
+ test("it reads names only, never a collection's values", async () => {
102
+ await checkNamedCollections({ url: server.url }, ["lake_creds", "kafka_creds"]);
103
+ const sent = server.queries.filter((q) => q.includes("named_collections"));
104
+ expect(sent).toEqual(["/* chant plan: named collections */ SELECT name FROM system.named_collections WHERE name IN ('kafka_creds', 'lake_creds')"]);
105
+ });
106
+
107
+ test("one the server does not have fails the plan, naming it", () => {
108
+ const check = { referenced: ["kafka_creds", "lake_creds"], missing: ["lake_creds"] };
109
+ const text = renderDiff({ changes: [], rebuilds: [], hints: [], namedCollections: check });
110
+ expect(text).toContain("No changes.");
111
+ expect(text).toMatch(/Refused: the build references a named collection the server does not have: lake_creds\./);
112
+ expect(renderNamedCollections({ referenced: ["lake_creds"], missing: [] })).toEqual([]);
113
+ });
114
+
115
+ test("a server whose collections cannot be read fails the plan too", async () => {
116
+ const failing = await fakeClickHouse([], { fail: { status: 500, body: "Code: 497. DB::Exception: Not enough privileges. (ACCESS_DENIED)" } });
117
+ try {
118
+ const check = await checkNamedCollections({ url: failing.url }, ["lake_creds"]);
119
+ expect(check).toMatchObject({ referenced: ["lake_creds"], missing: ["lake_creds"], unread: expect.stringMatching(/ACCESS_DENIED/) });
120
+ expect(renderNamedCollections(check).join("\n")).toMatch(/could not be read to check them/);
121
+ } finally {
122
+ await failing.close();
123
+ }
124
+ });
125
+ });
126
+
127
+ describe("import lists the server's as prerequisites", () => {
128
+ let server: FakeServer;
129
+ beforeAll(async () => {
130
+ server = await fakeClickHouse(
131
+ [
132
+ { name: "analytics", engine: "Atomic", statement: "CREATE DATABASE analytics\nENGINE = Atomic" },
133
+ {
134
+ database: "analytics",
135
+ name: "lake",
136
+ engine: "S3",
137
+ statement: "CREATE TABLE analytics.lake\n(\n `a` Int32\n)\nENGINE = S3(lake_creds, url = 'http://minio:9000/bucket/x.parquet', format = 'Parquet')",
138
+ },
139
+ ],
140
+ { undeclarable: [{ kind: "named collection", name: "lake_creds" }, { kind: "named collection", name: "spare" }] },
141
+ );
142
+ });
143
+ afterAll(() => server.close());
144
+
145
+ test("a server with SQL-defined named collections imports, and lists each with what names it", async () => {
146
+ const ir = await exportResources({ environment: "prod", config: {}, env: { CLICKHOUSE_URL: server.url } });
147
+ expect(ir.resources.map((r) => r.properties.name)).toEqual(["analytics", "lake"]);
148
+ expect(ir.metadata?.prerequisites).toEqual([
149
+ { kind: "named collection", name: "lake_creds", referencedBy: ["analytics.lake"] },
150
+ { kind: "named collection", name: "spare", referencedBy: [] },
151
+ ]);
152
+ const warnings = (ir.warnings ?? []).filter((w) => w.startsWith("named collection"));
153
+ expect(warnings).toEqual([
154
+ expect.stringMatching(/^named collection lake_creds is a prerequisite of prod, named by analytics\.lake: the server holds it, and chant does not declare it or read its values/),
155
+ expect.stringMatching(/^named collection spare is a prerequisite of prod: the server holds it/),
156
+ ]);
157
+ // Names only: no query reads a collection's keys, values or create query.
158
+ const reads = server.queries.filter((q) => q.includes("named_collections"));
159
+ expect(reads).toHaveLength(1);
160
+ expect(reads[0]).toMatch(/SELECT database, name FROM \(SELECT '' AS database, name FROM system\.named_collections WHERE source = 'SQL'\)/);
161
+ });
162
+
163
+ test("a .sql file's CREATE NAMED COLLECTION is a prerequisite, not a refusal", () => {
164
+ const ir = new SqlFileParser("clickhouse").parse(
165
+ "CREATE DATABASE a;\nCREATE NAMED COLLECTION lake_creds AS url = 'x', secret_access_key = 'y';\nCREATE TABLE a.t (x Int32) ENGINE = S3(lake_creds, format = 'Parquet')",
166
+ );
167
+ expect(ir.resources.map((r) => r.type)).toEqual(["ClickHouse::Database", "ClickHouse::Table"]);
168
+ expect(ir.metadata?.prerequisites).toEqual([{ kind: "named collection", name: "lake_creds", referencedBy: ["a.t"] }]);
169
+ expect(ir.warnings).toEqual([expect.stringMatching(/^named collection lake_creds is a prerequisite, named by a\.t: the DDL creates it, and chant does not declare it/)]);
170
+ // Its values are not carried anywhere.
171
+ expect(JSON.stringify(ir)).not.toContain("secret_access_key");
172
+ });
173
+ });
174
+
175
+ describe("the coverage table's referenced status", () => {
176
+ test("a named collection is referenced, with a reason and a live query", () => {
177
+ const row = KIND_COVERAGE.clickhouse.find((r) => r.kind === "named collection");
178
+ expect(row).toMatchObject({ status: "referenced" });
179
+ expect(row?.reason).toMatch(/never declares one or reads its values/);
180
+ expect(Object.keys(CLICKHOUSE_PROBES)).toContain("named collection");
181
+ });
182
+
183
+ test("a referenced row needs a reason, as a refused one does", () => {
184
+ const rows = [{ command: "CREATE NAMED COLLECTION", kind: "named collection", status: "referenced" as const }];
185
+ expect(kindCoverageProblems("clickhouse", { pins: [{ version: "x", commands: ["CREATE NAMED COLLECTION"] }], rows })).toEqual(["named collection is referenced, and gives no reason"]);
186
+ });
187
+ });
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Named collections, referenced by name (#4395).
3
+ *
4
+ * A named collection holds an integration's connection settings and its
5
+ * credentials. Those are the environment's, so chant never declares one and
6
+ * never reads its values (the coverage table's `referenced` status,
7
+ * `../coverage/clickhouse.json`). A declaration may name one where the server
8
+ * takes a collection in place of inline arguments:
9
+ *
10
+ * - an integration engine's first argument: `ENGINE = S3(lake_creds, format = 'Parquet')`,
11
+ * `ENGINE = Kafka(kafka_creds)`, a database's `ENGINE = PostgreSQL(pg_creds)`;
12
+ * - a table function's first argument: `AS s3(lake_creds)`, `SELECT ... FROM mysql(mysql_creds, table = 't')`;
13
+ * - a dictionary source's `NAME`: `SOURCE(MYSQL(NAME mysql_creds))`.
14
+ *
15
+ * The build records each reference (`namedCollections` on the object and on
16
+ * the document, `../serializer.ts`), and `chant sql plan` checks that every
17
+ * one exists on the server by name (`./plan/commands.ts`).
18
+ */
19
+
20
+ import { DATABASE_ENGINES, TABLE_ENGINES, TABLE_FUNCTIONS } from "../generated/clickhouse";
21
+ import { unquoteWith } from "../core/cursor";
22
+ import { isTrivia, tokenizeText, type Token } from "./tokens";
23
+
24
+ /**
25
+ * The table and database engines whose first argument may be a named
26
+ * collection, as the 26.8 reference documents them for each engine.
27
+ * Filtered to the engines the pinned server has.
28
+ */
29
+ const COLLECTION_ENGINE_NAMES = [
30
+ "S3",
31
+ "S3Queue",
32
+ "AzureBlobStorage",
33
+ "AzureQueue",
34
+ "Iceberg",
35
+ "IcebergS3",
36
+ "IcebergAzure",
37
+ "DeltaLake",
38
+ "DeltaLakeS3",
39
+ "DeltaLakeAzure",
40
+ "Hudi",
41
+ "Paimon",
42
+ "PaimonS3",
43
+ "HDFS",
44
+ "URL",
45
+ "Kafka",
46
+ "RabbitMQ",
47
+ "NATS",
48
+ "MySQL",
49
+ "PostgreSQL",
50
+ "MaterializedPostgreSQL",
51
+ "MongoDB",
52
+ "Redis",
53
+ "ArrowFlight",
54
+ "YTsaurus",
55
+ "DataLakeCatalog",
56
+ ];
57
+
58
+ const ENGINES = { ...TABLE_ENGINES, ...DATABASE_ENGINES } as Readonly<Record<string, unknown>>;
59
+
60
+ /** The engines a named collection may be the first argument of. */
61
+ export const COLLECTION_ENGINES: ReadonlySet<string> = new Set(COLLECTION_ENGINE_NAMES.filter((n) => n in ENGINES));
62
+
63
+ /**
64
+ * The table functions a named collection may be the first argument of, lower
65
+ * case. A `...Cluster` function's first argument is the cluster, so those are
66
+ * left out.
67
+ */
68
+ export const COLLECTION_FUNCTIONS: ReadonlySet<string> = new Set(
69
+ TABLE_FUNCTIONS.filter(
70
+ (f) => !/Cluster$/.test(f) && /^(s3|gcs|oss|cosn|url|hdfs|mysql|postgresql|mongodb|redis|remote|azureblobstorage|iceberg|deltalake|hudi|paimon|arrowflight|ytsaurus)/i.test(f),
71
+ ).map((f) => f.toLowerCase()),
72
+ );
73
+
74
+ /** `lake_creds` or `` `lake creds` ``: an identifier, not a string, a call or `key = value`. */
75
+ function identifierName(text: string): string | undefined {
76
+ const t = text.trim();
77
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(t)) return t.toUpperCase() === "NOSIGN" ? undefined : t;
78
+ if (/^`(?:[^`\\]|\\.)+`$/.test(t)) return unquoteWith(t, "`");
79
+ return undefined;
80
+ }
81
+
82
+ /** The collection an engine names in its first argument, when it is one of the engines that take one. */
83
+ export function engineCollection(engine: { name: string; args?: readonly string[] } | undefined): string | undefined {
84
+ if (!engine || !COLLECTION_ENGINES.has(engine.name) || !engine.args || engine.args.length === 0) return undefined;
85
+ return identifierName(engine.args[0]!);
86
+ }
87
+
88
+ const significant = (text: string): Token[] => {
89
+ try {
90
+ return tokenizeText(text, 0).filter((t) => !isTrivia(t));
91
+ } catch {
92
+ return [];
93
+ }
94
+ };
95
+
96
+ /** The collections a SQL text's table function calls name: `s3(lake_creds, ...)`, `mysql(mysql_creds)`. */
97
+ export function functionCollections(text: string): string[] {
98
+ const tokens = significant(text);
99
+ const out: string[] = [];
100
+ for (let i = 0; i + 3 < tokens.length; i++) {
101
+ const [fn, open, arg, after] = [tokens[i]!, tokens[i + 1]!, tokens[i + 2]!, tokens[i + 3]!];
102
+ if (fn.kind !== "ident" || !COLLECTION_FUNCTIONS.has(fn.text.toLowerCase())) continue;
103
+ if (open.kind !== "punct" || open.text !== "(") continue;
104
+ if (tokens[i - 1]?.kind === "punct" && tokens[i - 1]!.text === ".") continue;
105
+ if (arg.kind !== "ident" && arg.kind !== "qident") continue;
106
+ if (after.kind !== "punct" || (after.text !== "," && after.text !== ")")) continue;
107
+ const name = identifierName(arg.text);
108
+ if (name) out.push(name);
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /** The collection a dictionary source names: `MYSQL(NAME mysql_creds)`. */
114
+ export function sourceCollections(dataSource: string): string[] {
115
+ const tokens = significant(dataSource);
116
+ const out: string[] = [];
117
+ for (let i = 0; i + 1 < tokens.length; i++) {
118
+ if (tokens[i]!.kind !== "ident" || tokens[i]!.text.toUpperCase() !== "NAME") continue;
119
+ const v = tokens[i + 1]!;
120
+ if (v.kind === "ident" || v.kind === "qident") out.push(v.kind === "qident" ? unquoteWith(v.text, '`"') : v.text);
121
+ else if (v.kind === "string") out.push(unquoteWith(v.text, "'"));
122
+ }
123
+ return out;
124
+ }
125
+
126
+ /** What a declaration's props hold that can name a collection. */
127
+ export interface CollectionReferrer {
128
+ engine?: { name: string; args?: readonly string[] };
129
+ asFunction?: string;
130
+ select?: string;
131
+ dataSource?: string;
132
+ }
133
+
134
+ /** Every named collection an object names, once each, in the order written. */
135
+ export function namedCollectionReferences(o: CollectionReferrer): string[] {
136
+ const names = [
137
+ ...(engineCollection(o.engine) !== undefined ? [engineCollection(o.engine)!] : []),
138
+ ...(o.asFunction ? functionCollections(o.asFunction) : []),
139
+ ...(o.select ? functionCollections(o.select) : []),
140
+ ...(o.dataSource ? sourceCollections(o.dataSource) : []),
141
+ ];
142
+ return [...new Set(names)];
143
+ }
144
+
145
+ /** `CREATE NAMED COLLECTION [IF NOT EXISTS] name ...`: the name, or undefined for another statement. */
146
+ export function createdCollectionName(sql: string): string | undefined {
147
+ const m = /^\s*CREATE\s+NAMED\s+COLLECTION\s+(?:IF\s+NOT\s+EXISTS\s+)?(`(?:[^`\\]|\\.)+`|"(?:[^"\\]|\\.)+"|[A-Za-z_][A-Za-z0-9_]*)/i.exec(sql);
148
+ return m ? unquoteWith(m[1]!, '`"') : undefined;
149
+ }
150
+
151
+ /**
152
+ * The collections a whole statement names, read off its tokens: what
153
+ * {@link namedCollectionReferences} reads from a parsed declaration, for a
154
+ * statement import has only as text (`SHOW CREATE`).
155
+ */
156
+ export function statementCollections(ddl: string): string[] {
157
+ const tokens = significant(ddl);
158
+ const out: string[] = [];
159
+ for (let i = 0; i < tokens.length; i++) {
160
+ const t = tokens[i]!;
161
+ if (t.kind !== "ident") continue;
162
+ const word = t.text.toUpperCase();
163
+ if (word === "ENGINE" && tokens[i + 1]?.text === "=" && tokens[i + 3]?.text === "(") {
164
+ const engine = tokens[i + 2]!;
165
+ const arg = tokens[i + 4];
166
+ const after = tokens[i + 5];
167
+ if (COLLECTION_ENGINES.has(engine.text) && arg && (arg.kind === "ident" || arg.kind === "qident") && (after?.text === "," || after?.text === ")")) {
168
+ const name = identifierName(arg.text);
169
+ if (name) out.push(name);
170
+ }
171
+ } else if (word === "SOURCE" && tokens[i + 1]?.text === "(") {
172
+ let depth = 0;
173
+ let j = i + 1;
174
+ for (; j < tokens.length; j++) {
175
+ if (tokens[j]!.text === "(") depth++;
176
+ else if (tokens[j]!.text === ")" && --depth === 0) break;
177
+ }
178
+ out.push(...sourceCollections(tokens.slice(i + 2, j).map((x) => x.text).join(" ")));
179
+ i = j;
180
+ }
181
+ }
182
+ out.push(...functionCollections(ddl));
183
+ return [...new Set(out)];
184
+ }
185
+
186
+ /** One environment prerequisite an import found: a named collection (#4395). */
187
+ export interface Prerequisite {
188
+ kind: "named collection";
189
+ name: string;
190
+ /** The imported objects that name it, `database.name`. */
191
+ referencedBy: string[];
192
+ }
193
+
194
+ /**
195
+ * The named collections an import found as prerequisites (#4395), each with
196
+ * the imported objects that name it, and a warning for each. A collection an
197
+ * imported object names that was not found is listed too: a live reader
198
+ * without `SHOW NAMED COLLECTIONS` sees none.
199
+ */
200
+ export function namedCollectionPrerequisites(
201
+ found: ReadonlyArray<{ name: string }>,
202
+ objects: ReadonlyArray<{ database?: string; name: string; ddl: string }>,
203
+ wording: { of?: string; found: string; unseen: string },
204
+ ): { prerequisites: Prerequisite[]; warnings: string[] } {
205
+ const by = new Map<string, string[]>(found.map((o) => [o.name, []]));
206
+ for (const o of objects) {
207
+ for (const c of statementCollections(o.ddl)) {
208
+ if (!by.has(c)) by.set(c, []);
209
+ by.get(c)!.push(o.database ? `${o.database}.${o.name}` : o.name);
210
+ }
211
+ }
212
+ const prerequisites: Prerequisite[] = [...by]
213
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
214
+ .map(([name, referencedBy]) => ({ kind: "named collection", name, referencedBy }));
215
+ const seen = new Set(found.map((o) => o.name));
216
+ const warnings = prerequisites.map((p) => {
217
+ const used = p.referencedBy.length > 0 ? `, named by ${p.referencedBy.join(", ")}` : "";
218
+ return (
219
+ `named collection ${p.name} is a prerequisite${wording.of ? ` of ${wording.of}` : ""}${used}: ${seen.has(p.name) ? wording.found : wording.unseen}, ` +
220
+ "and chant does not declare it or read its values. Create it on every server the project applies to; chant sql plan checks that it exists"
221
+ );
222
+ });
223
+ return { prerequisites, warnings };
224
+ }
@@ -227,7 +227,8 @@ export const REFUSED_CREATE_KINDS: ReadonlyArray<{ words: readonly string[]; kin
227
227
  {
228
228
  words: ["NAMED", "COLLECTION"],
229
229
  kind: "NAMED COLLECTION",
230
- reason: "a named collection holds credentials (a password, a secret key), which are the environment's and never in a build; the server hides its values, so a plan could not compare them either. Create it from the server's configuration or by hand",
230
+ reason:
231
+ "a named collection holds credentials (a password, a secret key), which are the environment's and never in a build. Create it on the server, and name it where it is used: ENGINE = S3(lake_creds, format = 'Parquet'), s3(lake_creds), SOURCE(MYSQL(NAME mysql_creds)). A plan checks that it exists",
231
232
  },
232
233
  { words: ["MASKING", "POLICY"], kind: "MASKING POLICY", reason: "masking policies are available only in ClickHouse Cloud, not on the pinned server" },
233
234
  { words: ["WORKLOAD"], kind: "WORKLOAD", reason: "a workload is server-level scheduling, configured with the server, not a schema object" },
@@ -13,7 +13,8 @@
13
13
  * expressions the rules leave different. Both exit 2 when a change needs a
14
14
  * rebuild, which a plan refuses to make in place, and `plan` also when the
15
15
  * declared databases hold an object whose definition it cannot read (#3653),
16
- * which it names; 0 otherwise. For each
16
+ * which it names, or when the build references a named collection the
17
+ * server does not have (#4395), which it names; 0 otherwise. For each
17
18
  * refused table they name the `ClickHouseRebuildOp` to run instead
18
19
  * (`rebuildOps` in `--json`, `./rebuild-handoff.ts`).
19
20
  *
@@ -40,6 +41,7 @@ import { bindClickHouse } from "../live/bind";
40
41
  import { rebuildOpSuggestions } from "./rebuild-handoff";
41
42
  import { readFileSync } from "node:fs";
42
43
  import { parseTopology, type Topology } from "../topology";
44
+ import { checkNamedCollections, namedCollectionsFail, referencedCollectionsOf } from "../live/named-collections";
43
45
 
44
46
  /** The dialect a build output names. */
45
47
  function outputDialect(path: string): string | undefined {
@@ -90,7 +92,7 @@ async function runDiffStatements(before: string, after: string, json: boolean, t
90
92
 
91
93
  function emit(diff: SchemaDiff, json: boolean, title: string): number {
92
94
  console.log(json ? JSON.stringify(diff, null, 2) : renderDiff(diff, { title }));
93
- return diff.rebuilds.length > 0 || (diff.unreadable?.length ?? 0) > 0 ? 2 : 0;
95
+ return diff.rebuilds.length > 0 || (diff.unreadable?.length ?? 0) > 0 || namedCollectionsFail(diff.namedCollections) ? 2 : 0;
94
96
  }
95
97
 
96
98
  export async function runDiff(ctx: CommandGroupContext): Promise<number> {
@@ -134,6 +136,8 @@ export async function planAgainstServer(environment: string, buildFile: string,
134
136
  new Map(declared.map((o) => [o.key, o.canonical])),
135
137
  environment,
136
138
  );
139
+ // Each named collection the build references must exist on the server, by name; its values are never read (#4395).
140
+ const namedCollections = await checkNamedCollections(target.endpoint, referencedCollectionsOf(buildFile));
137
141
  const label = new Map(declared.map((o) => [o.key, o.exportName === o.key ? o.key : `${o.exportName} (${o.key})`]));
138
142
  const relabel = changes.map((c) => ({ ...c, object: label.get(c.object) ?? c.object }));
139
143
  return {
@@ -142,6 +146,7 @@ export async function planAgainstServer(environment: string, buildFile: string,
142
146
  hints: unmanaged > 0 ? [...diff.hints, accessUnmanagedHint(unmanaged, environment)] : diff.hints,
143
147
  ...(rebuildOps.length > 0 ? { rebuildOps } : {}),
144
148
  ...(unreadable.length > 0 ? { unreadable } : {}),
149
+ ...(namedCollections ? { namedCollections } : {}),
145
150
  };
146
151
  }
147
152
 
@@ -170,5 +175,15 @@ export const sqlCommands: CommandGroup = {
170
175
  description: "Write sql-types.d.ts: typed columns for each table, view and dictionary the build declares, for tags given their name (table<\"app.users\">) and RowOf; --output <path> writes it elsewhere; --check compares instead of writing and exits 1 when the file differs",
171
176
  handler: async (ctx) => (await import("../../sql-types-command")).runTypes(ctx),
172
177
  },
178
+ {
179
+ name: "replay",
180
+ description: "Replay a goose, golang-migrate, clickhouse-migrations or Flyway directory into the empty server --env binds, and record the tables and columns it makes as chant.replay.json; --check compares instead of writing",
181
+ handler: async (ctx) => (await import("../../migration-replay")).runReplay(ctx),
182
+ },
183
+ {
184
+ name: "drizzle",
185
+ description: "Write a Drizzle schema module (drizzle-schema.ts) from a Postgres build: pgTable per table with columns, keys, indexes, enums, sequences and views, so an application queries with Drizzle's row types while chant owns the schema; --output <path> writes it elsewhere; --check compares instead of writing and exits 1 when the file differs; --bigint bigint reads bigint columns as bigint, not number",
186
+ handler: async (ctx) => (await import("../../drizzle-command")).runDrizzle(ctx),
187
+ },
173
188
  ],
174
189
  };
@@ -20,6 +20,7 @@ import { CLASSIFIER_RULES, type ChangeClass, type ClassifierRuleId } from "./rul
20
20
  import type { CanonicalColumn, CanonicalObject } from "./normalize";
21
21
  import type { RebuildOpSuggestion } from "./rebuild-handoff";
22
22
  import { MERGE_TREE_SETTINGS } from "../../generated/clickhouse";
23
+ import { isStreamEngine, isStreamTable, positionWarnings, readsTable } from "./stream";
23
24
  import { classifiedChange, type ChangeSet, type ClassifiedChange } from "../../core/classifier";
24
25
  import { changedEntries, matchByIdentity, type Keyed } from "../../core/diff";
25
26
 
@@ -38,6 +39,22 @@ export interface SchemaDiff extends ChangeSet<Change> {
38
39
  * it out.
39
40
  */
40
41
  unreadable?: UnreadableEntry[];
42
+ /**
43
+ * The named collections the build references, checked against the server
44
+ * by name (#4395). A plan with one `missing`, or whose check could not run
45
+ * (`unread`), fails naming it. Their values are never read.
46
+ */
47
+ namedCollections?: NamedCollectionCheck;
48
+ }
49
+
50
+ /** What a plan found of the named collections a build references: each by name, never its values. */
51
+ export interface NamedCollectionCheck {
52
+ /** Every collection the build names, sorted. */
53
+ referenced: string[];
54
+ /** Those the server has no collection of, or that the plan's user may not see. */
55
+ missing: string[];
56
+ /** Why the server's collections could not be read, when they could not. */
57
+ unread?: string;
41
58
  }
42
59
 
43
60
  /** An object a plan could not read: `database.name`, its type and why. */
@@ -228,8 +245,14 @@ function diffObject(key: string, before: CanonicalObject, after: CanonicalObject
228
245
  return;
229
246
  }
230
247
 
248
+ // A stream table (#4396): replaced, not altered, and not rebuilt, since it holds no rows.
249
+ if (isStreamTable(before) && isStreamTable(after)) {
250
+ diffStreamTable(key, before, after, out);
251
+ return;
252
+ }
253
+
231
254
  // A table.
232
- if (before.engine !== after.engine) out.push(change(key, "engine", "SQLCH223", before.engine, after.engine));
255
+ if (before.engine !== after.engine) out.push(change(key, "engine", "SQLCH223", before.engine, after.engine, isStreamEngine(before.engineName) || isStreamEngine(after.engineName) ? { note: "one side is a stream engine and the other is not, which is a different kind of table: neither the replace nor the rebuild applies" } : {}));
233
256
  const added = diffColumns(key, before, after, out, hints);
234
257
 
235
258
  const beforePk = before.primaryKey ?? before.orderBy;
@@ -260,6 +283,28 @@ function diffObject(key: string, before: CanonicalObject, after: CanonicalObject
260
283
  diffMap(key, "constraints", before.constraints, after.constraints, "SQLCH215", out);
261
284
  }
262
285
 
286
+ /**
287
+ * A table on a stream engine, on both sides (#4396). Its engine, its settings
288
+ * and its columns are all replaced the same way (SQLCH225); the comment is
289
+ * compared before this. A change that loses the table's position says so in
290
+ * its note.
291
+ */
292
+ function diffStreamTable(key: string, before: CanonicalObject, after: CanonicalObject, out: Change[]): void {
293
+ const start = out.length;
294
+ if (before.engine !== after.engine) out.push(change(key, "engine", "SQLCH225", before.engine, after.engine));
295
+ for (const name of changedEntries(before.settings, after.settings)) out.push(change(key, `settings.${name}`, "SQLCH225", before.settings[name], after.settings[name]));
296
+ const column = (c: CanonicalColumn) => [c.type, c.nullable ? "NULL" : "", c.defaultKind ?? "", c.defaultExpr ?? "", c.codec ? `CODEC(${c.codec})` : ""].filter(Boolean).join(" ");
297
+ const b = new Map(before.columns.map((c) => [c.name, column(c)]));
298
+ const a = new Map(after.columns.map((c) => [c.name, column(c)]));
299
+ for (const name of new Set([...b.keys(), ...a.keys()])) {
300
+ if (b.get(name) !== a.get(name)) out.push(change(key, `columns.${name}`, "SQLCH225", b.get(name), a.get(name)));
301
+ }
302
+ const order = (o: CanonicalObject) => o.columns.map((c) => c.name).join(", ");
303
+ if (order(before) !== order(after) && a.size === b.size && [...a.keys()].every((n) => b.has(n))) out.push(change(key, "columns.order", "SQLCH225", order(before), order(after)));
304
+ const warnings = positionWarnings(before, after);
305
+ if (warnings.length > 0 && out.length > start) out[start] = { ...out[start]!, note: `loses its position: ${warnings.join("; ")}` };
306
+ }
307
+
263
308
  /**
264
309
  * A table over a table function (#3911), on either side. It stores nothing,
265
310
  * so every change but its comment replaces it (SQLCH219); a table with data
@@ -326,5 +371,16 @@ export function diffSchemas(before: readonly SchemaObject[], after: readonly Sch
326
371
  changes.push(change(b.key, "object", "SQLCH250", qualified(b.canonical), undefined, { destructive: b.canonical.kind === "table" || b.canonical.kind === "materializedView" }));
327
372
  }
328
373
  }
374
+ // A replaced stream table is replaced around the views that read it (#4396): say which.
375
+ const replaced = new Set(changes.filter((c) => c.rule === "SQLCH225").map((c) => c.object));
376
+ for (const m of matches) {
377
+ if (m.kind !== "matched" || !replaced.has(m.after.key)) continue;
378
+ const readers = before.filter((o) => readsTable(o.canonical, m.before.canonical)).map((o) => qualified(o.canonical));
379
+ hints.push(
380
+ readers.length > 0
381
+ ? `${qualified(m.before.canonical)} is replaced: ${readers.join(", ")} ${readers.length === 1 ? "reads it and is" : "read it and are"} detached first and attached after`
382
+ : `${qualified(m.before.canonical)} is replaced; no materialized view reads it`,
383
+ );
384
+ }
329
385
  return { changes, rebuilds: changes.filter((c) => c.class === "rebuild"), hints };
330
386
  }
@@ -15,6 +15,9 @@ import { MERGE_TREE_SETTINGS } from "../../generated/clickhouse";
15
15
  import { classifyDisruptionWith } from "../../core/classifier";
16
16
  import { CHANGE_CLASSES, CLASSIFIER_RULES, type ClassifierRuleId } from "./rules";
17
17
 
18
+ /** Settings only a stream engine takes; the path alone cannot name the engine. */
19
+ const STREAM_SETTING = /^(?:kafka_|rabbitmq_|nats_|s3queue_|azure_queue_|keeper_path$)/;
20
+
18
21
  function ruleFor(path: string): ClassifierRuleId | "ambiguous" | undefined {
19
22
  const root = path.split(/[.[]/)[0]!;
20
23
  if (root === "name" || root === "database") return "SQLCH230";
@@ -32,6 +35,8 @@ function ruleFor(path: string): ClassifierRuleId | "ambiguous" | undefined {
32
35
  if (root === "refresh") return "SQLCH244";
33
36
  if (root === "settings") {
34
37
  const key = path.split(".")[1];
38
+ // A stream engine's own settings (Kafka, RabbitMQ, NATS, S3Queue's keeper_path) have no MODIFY SETTING: the table is replaced (#4396).
39
+ if (key !== undefined && STREAM_SETTING.test(key)) return "SQLCH225";
35
40
  const spec = key ? (MERGE_TREE_SETTINGS as Record<string, { readonly: boolean } | undefined>)[key] : undefined;
36
41
  return spec?.readonly ? "SQLCH218" : "SQLCH206";
37
42
  }
@@ -19,6 +19,7 @@
19
19
  import type { CanonicalObject } from "./normalize";
20
20
  import type { SchemaDiff } from "./diff";
21
21
  import { migrationOpName, renderMigrationOps, type MigrationOpSuggestion } from "../../core/handoff";
22
+ import { isStreamEngine } from "./stream";
22
23
  import { collapsesRows } from "../rebuild/engines";
23
24
 
24
25
  export interface RebuildOpSuggestion extends MigrationOpSuggestion {
@@ -41,6 +42,8 @@ export function rebuildOpSuggestions(diff: SchemaDiff, declared: ReadonlyMap<str
41
42
  for (const key of [...new Set(diff.rebuilds.filter((c) => c.rule !== "SQLCH290").map((c) => c.object))]) {
42
43
  const c = declared.get(key);
43
44
  if (!c || c.kind !== "table") continue;
45
+ // The Op refuses a stream-engine table (#4396); a change to one that reaches here is a move to or from a stream engine, for review.
46
+ if (isStreamEngine(c.engineName)) continue;
44
47
  const table = `${c.database ?? "default"}.${c.name}`;
45
48
  const time = c.columns.find((col) => !col.nullable && TIME_TYPE.test(col.type) && (!col.defaultKind || col.defaultKind === "DEFAULT"));
46
49
  const dualWrite: RebuildOpSuggestion["dualWrite"] = time ? { mode: "materialized-view", cutoverColumn: time.name } : { mode: "app" };
@@ -4,6 +4,7 @@ import { renderChangeSet } from "../../core/classifier";
4
4
  import { CHANGE_CLASSES, CLASSIFIER_RULES } from "./rules";
5
5
  import type { SchemaDiff } from "./diff";
6
6
  import { renderRebuildOps } from "./rebuild-handoff";
7
+ import { renderNamedCollections } from "../live/named-collections";
7
8
 
8
9
  export function renderDiff(diff: SchemaDiff, opts: { title?: string } = {}): string {
9
10
  const unreadable = diff.unreadable ?? [];
@@ -17,6 +18,7 @@ export function renderDiff(diff: SchemaDiff, opts: { title?: string } = {}): str
17
18
  : [];
18
19
  const trailer = [
19
20
  ...refusedRead,
21
+ ...renderNamedCollections(diff.namedCollections),
20
22
  ...(diff.rebuilds.length > 0
21
23
  ? [
22
24
  "",
@@ -26,5 +28,8 @@ export function renderDiff(diff: SchemaDiff, opts: { title?: string } = {}): str
26
28
  ]
27
29
  : []),
28
30
  ];
29
- return renderChangeSet(diff, { ...(opts.title ? { title: opts.title } : {}), rules: CLASSIFIER_RULES, classes: CHANGE_CLASSES, trailer });
31
+ const text = renderChangeSet(diff, { ...(opts.title ? { title: opts.title } : {}), rules: CLASSIFIER_RULES, classes: CHANGE_CLASSES, trailer });
32
+ // The shared report prints no trailer under "No changes."; a missing named collection fails the plan all the same (#4395).
33
+ const missing = renderNamedCollections(diff.namedCollections);
34
+ return diff.changes.length === 0 && missing.length > 0 ? [text, ...missing].join("\n") : text;
30
35
  }