@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
@@ -10,6 +10,9 @@
10
10
  * parts until `MATERIALIZE INDEX`).
11
11
  * - `rewrite`: the change is a mutation that rewrites existing parts in the
12
12
  * background (`system.mutations`), with no rollback.
13
+ * - `replace`: ClickHouse cannot alter the object, but it holds no rows to
14
+ * keep, so the applier drops and creates it, around the views that read it
15
+ * (a stream-engine table, #4396). Nothing is copied.
13
16
  * - `rebuild`: ClickHouse cannot make the change to the table it has; the data
14
17
  * has to be copied into a new table. A plan refuses to make one in place.
15
18
  *
@@ -18,16 +21,16 @@
18
21
 
19
22
  import { classifierRule, type ChangeClasses, type ClassifierRule as SqlClassifierRule } from "../../core/classifier";
20
23
 
21
- export type ChangeClass = "metadata" | "rewrite" | "rebuild" | "create" | "drop";
24
+ export type ChangeClass = "metadata" | "rewrite" | "replace" | "rebuild" | "create" | "drop";
22
25
 
23
26
  /** A ClickHouse rule: the shared core's rule shape over these classes, citing a clickhouse.com page. */
24
27
  export type ClassifierRule = SqlClassifierRule<ChangeClass>;
25
28
 
26
29
  /** The classes as data: report order and labels, and what each costs for `classifyDisruption()`. */
27
30
  export const CHANGE_CLASSES: ChangeClasses<ChangeClass> = {
28
- order: ["create", "metadata", "rewrite", "rebuild", "drop"],
29
- label: { create: "create", drop: "drop", metadata: "metadata only", rewrite: "background rewrite", rebuild: "REBUILD" },
30
- disruption: { metadata: "in-place", rewrite: "rolling", rebuild: "replace", create: "in-place", drop: "destroy" },
31
+ order: ["create", "metadata", "rewrite", "replace", "rebuild", "drop"],
32
+ label: { create: "create", drop: "drop", metadata: "metadata only", rewrite: "background rewrite", replace: "replace table", rebuild: "REBUILD" },
33
+ disruption: { metadata: "in-place", rewrite: "rolling", replace: "replace", rebuild: "replace", create: "in-place", drop: "destroy" },
31
34
  };
32
35
 
33
36
  const DOCS = "https://clickhouse.com/docs/sql-reference/statements";
@@ -192,6 +195,13 @@ export const CLASSIFIER_RULES = {
192
195
  "A table cannot become a view or a view a table; the old object is dropped and the new one created.",
193
196
  `${DOCS}/create`,
194
197
  ),
198
+ SQLCH225: rule(
199
+ "SQLCH225",
200
+ "replace",
201
+ "Change a stream table",
202
+ "A Kafka, S3Queue, AzureQueue, RabbitMQ, NATS or FileLog table stores no rows and has no ALTER for its engine arguments, settings or columns; the views that read it are detached, the table is dropped and created, and the views are attached again. Its position (a Kafka group's offsets, an S3Queue's record of processed files) is kept by the broker or Keeper, not by the table.",
203
+ `${DOCS}/create/table`,
204
+ ),
195
205
  SQLCH230: rule(
196
206
  "SQLCH230",
197
207
  "metadata",
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { classifyClickHouseStatements } from "./statements";
3
+
4
+ const one = (sql: string) => classifyClickHouseStatements([sql])[0]!;
5
+ const rules = (sql: string) => one(sql).changes.map((c) => c.rule);
6
+
7
+ describe("ClickHouse statements", () => {
8
+ test("a sort-key change is SQLCH220, a rebuild", () => {
9
+ const v = one("ALTER TABLE events MODIFY ORDER BY (user_id, ts)");
10
+ expect(v.outcome).toBe("classified");
11
+ expect(v.changes).toMatchObject([{ rule: "SQLCH220", class: "rebuild", label: "REBUILD" }]);
12
+ });
13
+
14
+ test("appending the column the same ALTER adds is undecided between SQLCH216 and SQLCH220", () => {
15
+ const v = one("ALTER TABLE events ADD COLUMN region String, MODIFY ORDER BY (user_id, region)");
16
+ expect(v.outcome).toBe("undecided");
17
+ expect(v.candidates!.map((c) => c.rule)).toEqual(["SQLCH216", "SQLCH220"]);
18
+ expect(v.changes.map((c) => c.rule)).toEqual(["SQLCH201"]);
19
+ });
20
+
21
+ test("a table created in the same file decides the key questions", () => {
22
+ const create = "CREATE TABLE events (user_id UInt64, ts DateTime, region String) ENGINE = MergeTree ORDER BY (user_id, ts)";
23
+ const [, appended] = classifyClickHouseStatements([create, "ALTER TABLE events ADD COLUMN extra String, MODIFY ORDER BY (user_id, ts, extra)"]);
24
+ expect(appended!.outcome).toBe("classified");
25
+ expect(appended!.changes.map((c) => c.rule)).toEqual(["SQLCH201", "SQLCH216"]);
26
+ expect(classifyClickHouseStatements([create, "ALTER TABLE events MODIFY COLUMN user_id UInt32"])[1]!.changes[0]!.rule).toBe("SQLCH211");
27
+ expect(classifyClickHouseStatements([create, "ALTER TABLE events MODIFY COLUMN region LowCardinality(String)"])[1]!.changes[0]!.rule).toBe("SQLCH210");
28
+ });
29
+
30
+ test("a column type change on a table the file does not create is undecided", () => {
31
+ const v = one("ALTER TABLE events MODIFY COLUMN user_id UInt32");
32
+ expect(v.outcome).toBe("undecided");
33
+ expect(v.candidates!.map((c) => c.rule)).toEqual(["SQLCH210", "SQLCH211"]);
34
+ expect(one("ALTER TABLE events DROP COLUMN a").candidates!.map((c) => c.rule)).toEqual(["SQLCH202", "SQLCH213"]);
35
+ expect(one("ALTER TABLE events RENAME COLUMN a TO b").candidates!.map((c) => c.rule)).toEqual(["SQLCH212", "SQLCH213"]);
36
+ });
37
+
38
+ test("metadata and rewrite changes", () => {
39
+ expect(rules("ALTER TABLE events ADD COLUMN IF NOT EXISTS a String DEFAULT ''")).toEqual(["SQLCH201"]);
40
+ expect(rules("ALTER TABLE events ON CLUSTER c MODIFY COLUMN a DEFAULT 'x'")).toEqual(["SQLCH207"]);
41
+ expect(rules("ALTER TABLE events MODIFY TTL ts + INTERVAL 30 DAY")).toEqual(["SQLCH205"]);
42
+ expect(rules("ALTER TABLE events ADD INDEX i a TYPE minmax GRANULARITY 1")).toEqual(["SQLCH204"]);
43
+ expect(rules("ALTER TABLE events MODIFY SETTING ttl_only_drop_parts = 1")).toEqual(["SQLCH206"]);
44
+ expect(rules("ALTER TABLE events MODIFY COMMENT 'x'")).toEqual(["SQLCH203"]);
45
+ });
46
+
47
+ test("create, drop, rename", () => {
48
+ expect(rules("CREATE TABLE t (a UInt8) ENGINE = MergeTree ORDER BY a")).toEqual(["SQLCH200"]);
49
+ expect(rules("CREATE MATERIALIZED VIEW mv TO t AS SELECT a FROM s")).toEqual(["SQLCH200"]);
50
+ expect(rules("CREATE USER u IDENTIFIED WITH no_password")).toEqual(["SQLCH200"]);
51
+ expect(one("DROP TABLE t").changes[0]).toMatchObject({ rule: "SQLCH250", destructive: true });
52
+ expect(rules("RENAME TABLE a TO b")).toEqual(["SQLCH230"]);
53
+ expect(rules("GRANT SELECT ON db.* TO r")).toEqual(["SQLCH273"]);
54
+ });
55
+
56
+ test("mutations are data; partition operations have no rule", () => {
57
+ expect(one("ALTER TABLE t UPDATE a = 1 WHERE b = 2").outcome).toBe("data");
58
+ expect(one("ALTER TABLE t DROP PARTITION 2024").outcome).toBe("unclassified");
59
+ expect(one("INSERT INTO t VALUES (1)").outcome).toBe("data");
60
+ });
61
+ });
@@ -0,0 +1,306 @@
1
+ /**
2
+ * The ClickHouse statement classifier (#4407): a migration file's statements,
3
+ * each given the class and rule the schema-diff classifier (`./rules.ts`)
4
+ * gives the same change. It reads text and contacts no server.
5
+ *
6
+ * `CREATE` goes through the declaration parser (`../parser.ts`), which also
7
+ * tells the classifier a table's keys when the file creates the table. The
8
+ * parser has no `ALTER` grammar, so `ALTER`, `DROP` and `RENAME` are read word
9
+ * by word from the same tokens.
10
+ *
11
+ * A statement alone cannot decide some changes, and those answer `undecided`
12
+ * naming both rules, the way `./disruption.ts` answers `unknown`:
13
+ *
14
+ * - `MODIFY COLUMN c <type>` is SQLCH210 (a rewrite), or SQLCH211 (a rebuild)
15
+ * when `c` is in a key. The file states the keys only when it creates the
16
+ * table.
17
+ * - `DROP COLUMN` is SQLCH202 or SQLCH213, and `RENAME COLUMN` SQLCH212 or
18
+ * SQLCH213, on the same condition.
19
+ * - `MODIFY ORDER BY` that the same `ALTER` pairs with `ADD COLUMN` is SQLCH216
20
+ * when it only appends those columns and SQLCH220 (a rebuild) otherwise.
21
+ * Without such a column it is SQLCH220.
22
+ */
23
+
24
+ import { MERGE_TREE_SETTINGS } from "../../generated/clickhouse";
25
+ import { isTrivia, tokenizeText, type Token } from "../tokens";
26
+ import { parseCreate, type CreateNode } from "../parser";
27
+ import type { Span } from "../../core/cursor";
28
+ import { classifiedOf, splitTopLevel, sqlHead, type StatementClassified, type StatementVerdict } from "../../core/statement-class";
29
+ import { CHANGE_CLASSES, CLASSIFIER_RULES, type ClassifierRuleId } from "./rules";
30
+
31
+ const classified = (id: ClassifierRuleId, extra: Partial<StatementClassified> = {}): StatementClassified => classifiedOf(CLASSIFIER_RULES, CHANGE_CLASSES.label, id, extra);
32
+
33
+ type Reading = { rules: Array<{ id: ClassifierRuleId; extra?: Partial<StatementClassified> }> } | { undecided: ClassifierRuleId[]; note: string } | { none: string } | { data: string };
34
+
35
+ const rule = (id: ClassifierRuleId, extra?: Partial<StatementClassified>): Reading => ({ rules: [{ id, ...(extra ? { extra } : {}) }] });
36
+
37
+ /** Settings only a stream engine takes (the same list `./disruption.ts` reads). */
38
+ const STREAM_SETTING = /^(?:kafka_|rabbitmq_|nats_|s3queue_|azure_queue_|keeper_path$)/;
39
+
40
+ /** What a table the file creates tells later statements in it. */
41
+ interface Created {
42
+ /** The columns in the sorting, primary, partition or sampling key. */
43
+ keys: Set<string>;
44
+ orderBy?: string;
45
+ }
46
+
47
+ const bare = (name: string): string => (name.split(".").pop() ?? name).replace(/^[`"]|[`"]$/g, "").toLowerCase();
48
+
49
+ const text = (tokens: readonly Token[], s: Span | undefined): string => (s ? tokens.slice(s.from, s.to).filter((t) => !isTrivia(t)).map((t) => t.text).join(" ") : "");
50
+
51
+ /** The identifier tokens of a key expression that are not function names. */
52
+ function columnsOf(tokens: readonly Token[], s: Span | undefined): string[] {
53
+ if (!s) return [];
54
+ const toks = tokens.slice(s.from, s.to).filter((t) => !isTrivia(t));
55
+ return toks.flatMap((t, i) => ((t.kind === "ident" || t.kind === "qident") && toks[i + 1]?.text !== "(" ? [bare(t.text)] : []));
56
+ }
57
+
58
+ function nameAt(words: readonly string[], i: number): { name: string; next: number } {
59
+ let name = words[i] ?? "";
60
+ let j = i + 1;
61
+ while (words[j] === "." && words[j + 1] !== undefined) {
62
+ name += `.${words[j + 1]}`;
63
+ j += 2;
64
+ }
65
+ return { name, next: j };
66
+ }
67
+
68
+ const KNOWN_VERBS = new Set(["ADD", "DROP", "RENAME", "MODIFY", "COMMENT", "CLEAR", "MATERIALIZE", "UPDATE", "DELETE", "ATTACH", "DETACH", "FREEZE", "FETCH", "MOVE", "REPLACE", "RESET", "REMOVE", "APPLY", "UNFREEZE", "EXECUTE"]);
69
+
70
+ /** Normalise a key expression for comparing an old and a new one: no spaces, no outer parentheses. */
71
+ const keyText = (words: readonly string[]): string => {
72
+ let t = words.join("").toLowerCase();
73
+ while (t.startsWith("(") && t.endsWith(")")) t = t.slice(1, -1);
74
+ return t;
75
+ };
76
+
77
+ interface TableContext {
78
+ created?: Created;
79
+ /** Columns the same ALTER adds. */
80
+ added: Set<string>;
81
+ }
82
+
83
+ function columnVerdict(kind: "drop" | "rename", col: string, ctx: TableContext): Reading {
84
+ const plain: ClassifierRuleId = kind === "drop" ? "SQLCH202" : "SQLCH212";
85
+ const extra = kind === "drop" ? { destructive: true } : undefined;
86
+ if (ctx.created) {
87
+ return ctx.created.keys.has(bare(col)) ? rule("SQLCH213", extra) : rule(plain, extra);
88
+ }
89
+ return { undecided: [plain, "SQLCH213"], note: `${col} is a column change or, in a key, a rebuild; the file does not create the table, so its keys are not known offline` };
90
+ }
91
+
92
+ function tableAction(a: readonly string[], ctx: TableContext): Reading {
93
+ const u = a.map((w) => w.toUpperCase());
94
+ const first = u[0];
95
+ const second = u[1];
96
+ if (first === "ADD") {
97
+ if (second === "COLUMN") return rule("SQLCH201");
98
+ if (second === "INDEX") return rule("SQLCH204");
99
+ if (second === "PROJECTION") return rule("SQLCH214");
100
+ if (second === "CONSTRAINT") return rule("SQLCH215");
101
+ return { none: `ALTER TABLE ${u.slice(0, 2).join(" ")}` };
102
+ }
103
+ if (first === "DROP") {
104
+ if (second === "COLUMN") {
105
+ const k = u[2] === "IF" ? 4 : 2;
106
+ return columnVerdict("drop", a[k] ?? "column", ctx);
107
+ }
108
+ if (second === "INDEX") return rule("SQLCH204");
109
+ if (second === "PROJECTION") return rule("SQLCH214");
110
+ if (second === "CONSTRAINT") return rule("SQLCH215");
111
+ return { none: `ALTER TABLE ${u.slice(0, 2).join(" ")} (removes data parts, no rule covers it)` };
112
+ }
113
+ if (first === "CLEAR") {
114
+ if (second === "INDEX") return rule("SQLCH204");
115
+ if (second === "PROJECTION") return rule("SQLCH214");
116
+ return { none: `ALTER TABLE CLEAR ${second ?? ""}`.trim() };
117
+ }
118
+ if (first === "RENAME" && second === "COLUMN") {
119
+ const k = u[2] === "IF" ? 4 : 2;
120
+ return columnVerdict("rename", a[k] ?? "column", ctx);
121
+ }
122
+ if (first === "COMMENT" && second === "COLUMN") return rule("SQLCH203");
123
+ if (first === "MODIFY" || first === "RESET") {
124
+ if (second === "COLUMN") {
125
+ let k = 2;
126
+ if (u[k] === "IF" && u[k + 1] === "EXISTS") k += 2;
127
+ const col = a[k] ?? "column";
128
+ const after = u[k + 1];
129
+ if (after === "REMOVE") {
130
+ const what = u[k + 2];
131
+ return what === "CODEC" ? rule("SQLCH208") : what === "COMMENT" ? rule("SQLCH203") : what === "TTL" ? rule("SQLCH205") : rule("SQLCH207");
132
+ }
133
+ if (after === "DEFAULT" || after === "MATERIALIZED" || after === "ALIAS" || after === "EPHEMERAL") return rule("SQLCH207");
134
+ if (after === "CODEC") return rule("SQLCH208");
135
+ if (after === "COMMENT") return rule("SQLCH203");
136
+ if (after === "TTL") return rule("SQLCH205");
137
+ if (after === "MODIFY" || after === "RESET") return { none: "MODIFY COLUMN ... SETTING" };
138
+ if (after === undefined) return { none: "MODIFY COLUMN with no change" };
139
+ if (ctx.created) return ctx.created.keys.has(bare(col)) ? rule("SQLCH211") : rule("SQLCH210");
140
+ return { undecided: ["SQLCH210", "SQLCH211"], note: `a type change of ${col} is a background rewrite or, when ${col} is in a key, a rebuild; the file does not create the table, so its keys are not known offline` };
141
+ }
142
+ if (second === "ORDER" && u[2] === "BY") {
143
+ const next = a.slice(3);
144
+ if (ctx.created?.orderBy !== undefined) {
145
+ const was = ctx.created.orderBy;
146
+ const now = keyText(next);
147
+ const tail = now.startsWith(was) ? now.slice(was.length) : undefined;
148
+ const extra = tail === undefined ? [] : tail.split(/[^a-z0-9_]+/).filter(Boolean);
149
+ if (tail !== undefined && extra.length > 0 && extra.every((c) => ctx.added.has(c))) return rule("SQLCH216");
150
+ return rule("SQLCH220");
151
+ }
152
+ const mentions = next.some((w) => ctx.added.has(bare(w)));
153
+ if (mentions) return { undecided: ["SQLCH216", "SQLCH220"], note: "the new key appends the columns this ALTER adds only if its other elements are the old key's, which the file does not state" };
154
+ return rule("SQLCH220");
155
+ }
156
+ if (second === "SAMPLE" && u[2] === "BY") return rule("SQLCH217");
157
+ if (second === "TTL") return rule("SQLCH205");
158
+ if (second === "COMMENT") return rule("SQLCH203");
159
+ if (second === "QUERY") return rule("SQLCH241");
160
+ if (second === "REFRESH") return rule("SQLCH244");
161
+ if (second === "SETTING" || second === "SETTINGS") {
162
+ const key = (a[2] ?? "").toLowerCase();
163
+ return settingRule(key);
164
+ }
165
+ return { none: `ALTER TABLE ${u.slice(0, 2).join(" ")}` };
166
+ }
167
+ if (first === "REMOVE") {
168
+ if (second === "TTL") return rule("SQLCH205");
169
+ if (second === "SAMPLE" && u[2] === "BY") return rule("SQLCH217");
170
+ return { none: `ALTER TABLE REMOVE ${second ?? ""}`.trim() };
171
+ }
172
+ if (first === "UPDATE" || first === "DELETE") return { data: "a mutation: it rewrites the parts it touches in the background (system.mutations) and has no rollback" };
173
+ return { none: `ALTER TABLE ${u.slice(0, 2).join(" ")}` };
174
+ }
175
+
176
+ function settingRule(key: string): Reading {
177
+ if (STREAM_SETTING.test(key)) return rule("SQLCH225", { note: `${key} belongs to a stream engine, which has no MODIFY SETTING` });
178
+ const spec = (MERGE_TREE_SETTINGS as Record<string, { readonly: boolean } | undefined>)[key];
179
+ return spec?.readonly ? rule("SQLCH218", { note: `${key} is fixed at creation` }) : rule("SQLCH206");
180
+ }
181
+
182
+ /** The `ALTER TABLE` actions of one statement, in order, a `MODIFY SETTING` list kept together. */
183
+ function alterTable(words: readonly string[], u: readonly string[], created: Map<string, Created>): Reading[] {
184
+ let i = 2;
185
+ if (u[i] === "IF" && u[i + 1] === "EXISTS") i += 2;
186
+ const { name, next } = nameAt(words, i);
187
+ i = next;
188
+ if (u[i] === "ON" && u[i + 1] === "CLUSTER") i += 3;
189
+ const actions = splitTopLevel(words.slice(i));
190
+ const added = new Set<string>();
191
+ for (const a of actions) {
192
+ const au = a.map((w) => w.toUpperCase());
193
+ if (au[0] === "ADD" && au[1] === "COLUMN") {
194
+ const k = au[2] === "IF" ? 5 : 2;
195
+ if (a[k]) added.add(bare(a[k]!));
196
+ }
197
+ }
198
+ const ctx: TableContext = { added, ...(created.has(bare(name)) ? { created: created.get(bare(name))! } : {}) };
199
+ const out: Reading[] = [];
200
+ let inSettings = false;
201
+ for (const a of actions) {
202
+ const au = a.map((w) => w.toUpperCase());
203
+ if (inSettings && au[0] !== undefined && !KNOWN_VERBS.has(au[0])) {
204
+ out.push(settingRule((a[0] ?? "").toLowerCase()));
205
+ continue;
206
+ }
207
+ inSettings = (au[0] === "MODIFY" || au[0] === "RESET") && (au[1] === "SETTING" || au[1] === "SETTINGS");
208
+ out.push(tableAction(a, ctx));
209
+ }
210
+ return out;
211
+ }
212
+
213
+ const OBJECTS: Record<string, ClassifierRuleId> = { USER: "SQLCH271", ROLE: "SQLCH270", QUOTA: "SQLCH275" };
214
+
215
+ function alterOther(u: readonly string[]): Reading {
216
+ const obj = u[1];
217
+ if (obj && OBJECTS[obj]) return rule(OBJECTS[obj]!);
218
+ if (obj === "ROW" && u[2] === "POLICY") return rule("SQLCH272");
219
+ if (obj === "POLICY") return rule("SQLCH272");
220
+ if (obj === "SETTINGS" && u[2] === "PROFILE") return rule("SQLCH276");
221
+ if (obj === "DATABASE" && u.includes("COMMENT")) return rule("SQLCH203");
222
+ return { none: `ALTER ${obj ?? ""}`.trim() };
223
+ }
224
+
225
+ const DATA_WORDS = new Set(["INSERT", "SELECT", "WITH", "SYSTEM", "OPTIMIZE", "TRUNCATE", "SET", "USE", "EXPLAIN", "EXISTS", "DESCRIBE", "DESC", "SHOW", "CHECK", "KILL", "WATCH"]);
226
+
227
+
228
+ /** A `CREATE` or `ATTACH`: the parser reads tables, views, dictionaries, databases and functions; the rest go by key words. */
229
+ function create(tokens: Token[], u: readonly string[], created: Map<string, Created>): Reading {
230
+ let i = 1;
231
+ const orReplace = u[i] === "OR" && u[i + 1] === "REPLACE";
232
+ if (orReplace) i += 2;
233
+ let node: CreateNode | undefined;
234
+ try {
235
+ node = parseCreate(tokens);
236
+ } catch {
237
+ node = undefined;
238
+ }
239
+ if (node?.statement === "table") {
240
+ const name = text(tokens, node.name).replace(/\s*\.\s*/g, ".");
241
+ const keys = new Set<string>([...columnsOf(tokens, node.orderBy), ...columnsOf(tokens, node.primaryKey), ...columnsOf(tokens, node.partitionBy), ...columnsOf(tokens, node.sampleBy)]);
242
+ for (const c of node.columns) if (c.primaryKey) keys.add(bare(c.name));
243
+ created.set(bare(name), { keys, ...(node.orderBy ? { orderBy: keyText(text(tokens, node.orderBy).split(" ")) } : {}) });
244
+ }
245
+ if (orReplace && u[i] === "FUNCTION") return rule("SQLCH260");
246
+ if (orReplace && (u[i] === "TABLE" || node?.statement === "table")) return rule("SQLCH200", { note: "OR REPLACE drops an existing table of that name, and its rows, before it creates this one" });
247
+ return rule("SQLCH200");
248
+ }
249
+
250
+ function read(sql: string, created: Map<string, Created>): Reading[] {
251
+ let tokens: Token[];
252
+ try {
253
+ tokens = tokenizeText(sql, 0);
254
+ } catch {
255
+ return [{ none: "text the tokenizer could not read" }];
256
+ }
257
+ const words = tokens.filter((t) => !isTrivia(t) && t.text !== ";").map((t) => t.text);
258
+ const u = words.map((w) => w.toUpperCase());
259
+ const head = u[0];
260
+ if (head === undefined) return [{ data: "an empty statement" }];
261
+ if (head === "CREATE") return [create(tokens, u, created)];
262
+ if (head === "ALTER" && u[1] === "TABLE") return alterTable(words, u, created);
263
+ if (head === "ALTER") return [alterOther(u)];
264
+ if (head === "DROP") return [rule("SQLCH250", { destructive: true })];
265
+ if (head === "RENAME") {
266
+ if (u[1] === "DATABASE") return [rule("SQLCH231")];
267
+ if (u[1] === "TABLE" || u[1] === "DICTIONARY") return [rule("SQLCH230")];
268
+ return [{ none: `RENAME ${u[1] ?? ""}`.trim() }];
269
+ }
270
+ if (head === "GRANT") return [rule("SQLCH273")];
271
+ if (head === "REVOKE") return [rule("SQLCH274")];
272
+ if (head === "EXCHANGE") return [{ none: "EXCHANGE TABLES swaps two tables; no rule covers it" }];
273
+ if (head === "TRUNCATE") return [{ data: "TRUNCATE removes every row of the table" }];
274
+ if (DATA_WORDS.has(head)) return [{ data: `${head} reads or writes rows or the session, not the schema` }];
275
+ return [{ none: `${head} is not a statement the classifier reads` }];
276
+ }
277
+
278
+ /**
279
+ * Classify a migration's up statements, in order. The statements are the
280
+ * file's own text, one per entry.
281
+ */
282
+ export function classifyClickHouseStatements(statements: readonly string[]): StatementVerdict[] {
283
+ const created = new Map<string, Created>();
284
+ return statements.map((sql, k) => {
285
+ const readings = read(sql, created);
286
+ const out: StatementVerdict = { index: k + 1, sql: sqlHead(sql), outcome: "classified", changes: [] };
287
+ const none: string[] = [];
288
+ const notes: string[] = [];
289
+ for (const r of readings) {
290
+ if ("rules" in r) out.changes.push(...r.rules.map((x) => classified(x.id, x.extra)));
291
+ else if ("undecided" in r) {
292
+ out.candidates = [...(out.candidates ?? []), ...r.undecided.map((id) => classified(id))];
293
+ notes.push(r.note);
294
+ } else if ("none" in r) none.push(r.none);
295
+ else notes.push(r.data);
296
+ }
297
+ if (out.candidates) out.outcome = "undecided";
298
+ else if (out.changes.length === 0 && none.length === 0) out.outcome = "data";
299
+ else if (none.length > 0) {
300
+ out.outcome = out.changes.length > 0 ? "classified" : "unclassified";
301
+ notes.push(`no rule covers ${none.join("; ")}`);
302
+ }
303
+ if (notes.length > 0) out.note = notes.join("; ");
304
+ return out;
305
+ });
306
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Replacing an S3Queue table against the pinned server (#4396): a change to
3
+ * the table's settings is planned as SQLCH225, the apply detaches the view,
4
+ * drops and creates the table and attaches the view, and the view keeps
5
+ * feeding the target with no file processed twice. A different keeper_path
6
+ * plans with the reprocessing warning.
7
+ *
8
+ * Needs Docker; skips cleanly without it. A one-server cluster (its embedded
9
+ * Keeper is what S3Queue records processed files in) and a MinIO container on
10
+ * its network as the object store, both removed afterwards even on failure.
11
+ * The credentials are MinIO's published test values, held in a named
12
+ * collection on the server (#4395) and never in the declaration.
13
+ */
14
+
15
+ import { execFile } from "node:child_process";
16
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { promisify } from "node:util";
20
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
21
+ import { expandComposite } from "@intentius/chant/composite";
22
+ import type { Declarable } from "@intentius/chant/declarable";
23
+ import type { SerializerResult } from "@intentius/chant/serializer";
24
+ import { dockerAvailable, startScratchCluster, type ScratchCluster } from "../container";
25
+ import { clickhouseQuery } from "../http";
26
+ import { clickhouseImage } from "../../spec/pin";
27
+ import { sqlSerializer } from "../../serializer";
28
+ import { StreamIngest } from "../../composites";
29
+ import { bindClickHouse } from "../live/bind";
30
+ import { applyClickHouse, declaredObjects } from "../apply/apply";
31
+ import { planAgainstServer } from "./commands";
32
+ import { renderDiff } from "./report";
33
+
34
+ const exec = promisify(execFile);
35
+ const enabled = await dockerAvailable();
36
+ let cluster: ScratchCluster | undefined;
37
+ let minio: string | undefined;
38
+ const dir = mkdtempSync(join(tmpdir(), "chant-sql-s3queue-"));
39
+ const buildFile = join(dir, "files.json");
40
+
41
+ const MINIO_IMAGE = "minio/minio:RELEASE.2025-04-22T22-12-26Z";
42
+ const q = <T = unknown>(sql: string) => clickhouseQuery<T>(cluster!.replicas[0]!, sql);
43
+ const env = () => ({ config: {}, env: { CLICKHOUSE_URL: cluster!.replicas[0]!.url } });
44
+
45
+ beforeAll(async () => {
46
+ if (!enabled) return;
47
+ cluster = await startScratchCluster(clickhouseImage(), { replicas: 1, namePrefix: "chant-sql-s3queue" });
48
+ minio = `${cluster.name}-minio`;
49
+ await exec("docker", [
50
+ "run", "-d", "--name", minio, "--network", `${cluster.name}-net`,
51
+ "-e", "MINIO_ROOT_USER=minioadmin", "-e", "MINIO_ROOT_PASSWORD=minioadmin",
52
+ MINIO_IMAGE, "server", "/data",
53
+ ], { timeout: 600_000 });
54
+ await exec("docker", ["exec", minio, "mkdir", "-p", "/data/lake"]);
55
+ await q(`CREATE NAMED COLLECTION lake_creds AS url = 'http://${minio}:9000/lake/in/*.csv', access_key_id = 'minioadmin', secret_access_key = 'minioadmin'`);
56
+ await q("CREATE DATABASE shop");
57
+ }, 900_000);
58
+
59
+ afterAll(async () => {
60
+ if (minio) await exec("docker", ["rm", "-f", minio]).catch(() => undefined);
61
+ await cluster?.stop();
62
+ rmSync(dir, { recursive: true, force: true });
63
+ });
64
+
65
+ /** The ingest chain with the S3Queue table's settings as given. */
66
+ function buildOf(settings: string): string {
67
+ const entities = new Map<string, Declarable>();
68
+ for (const [k, v] of expandComposite(
69
+ "files",
70
+ StreamIngest({
71
+ name: "shop.files",
72
+ columns: "id UInt64, v String",
73
+ engine: "S3Queue(lake_creds, format = 'CSV')",
74
+ settings,
75
+ orderBy: "id",
76
+ }),
77
+ )) {
78
+ entities.set(k, v);
79
+ }
80
+ return (sqlSerializer.serialize(entities) as SerializerResult).primary;
81
+ }
82
+
83
+ const SETTINGS = (maxMs: number, path = "/chant/e2e/files") =>
84
+ `mode = 'unordered', keeper_path = '${path}', s3queue_polling_min_timeout_ms = 100, s3queue_polling_max_timeout_ms = ${maxMs}`;
85
+
86
+ async function putFile(name: string, rows: string): Promise<void> {
87
+ await q(`INSERT INTO FUNCTION s3('http://${minio}:9000/lake/in/${name}', 'minioadmin', 'minioadmin', 'CSV', 'id UInt64, v String') VALUES ${rows}`);
88
+ }
89
+
90
+ async function waitForRows(n: number): Promise<void> {
91
+ const deadline = Date.now() + 90_000;
92
+ for (;;) {
93
+ const [row] = await q<{ n: string }>("SELECT count() AS n FROM shop.files");
94
+ if (Number(row?.n) >= n) return;
95
+ if (Date.now() > deadline) throw new Error(`shop.files held ${row?.n} rows, wanted ${n}`);
96
+ await new Promise((r) => setTimeout(r, 500));
97
+ }
98
+ }
99
+
100
+ async function apply(json: string) {
101
+ const target = await bindClickHouse({ environment: "e2e", ...env() });
102
+ return applyClickHouse(target, declaredObjects(json, "default", target.topology));
103
+ }
104
+
105
+ describe.skipIf(!enabled)("replacing an S3Queue table", () => {
106
+ test("the view keeps feeding the target, and no file is processed twice", async () => {
107
+ await apply(buildOf(SETTINGS(500)));
108
+ await putFile("f1.csv", "(1, 'a'), (2, 'b')");
109
+ await putFile("f2.csv", "(3, 'c')");
110
+ await waitForRows(3);
111
+ const [before] = await q<{ uuid: string }>("SELECT toString(uuid) AS uuid FROM system.tables WHERE database = 'shop' AND name = 'files_stream'");
112
+
113
+ // The settings change: planned as a stream-table replacement, not a rebuild, with nothing refused.
114
+ const next = buildOf(SETTINGS(1000));
115
+ writeFileSync(buildFile, next);
116
+ const diff = await planAgainstServer("e2e", buildFile, env());
117
+ expect(diff.changes.map((c) => [c.rule, c.field])).toEqual([["SQLCH225", "settings.s3queue_polling_max_timeout_ms"]]);
118
+ expect(diff.rebuilds).toEqual([]);
119
+ expect(diff.changes[0]!.note).toBeUndefined();
120
+ expect(diff.hints.join("\n")).toMatch(/shop\.files_stream is replaced: shop\.files_mv reads it/);
121
+
122
+ const outcome = await apply(next);
123
+ expect(outcome.failed).toEqual([]);
124
+ const replaced = outcome.applied.find((a) => a.name === "shop.files_stream");
125
+ expect(replaced?.statements.map((s) => s.split(" ").slice(0, 2).join(" "))).toEqual(["DETACH TABLE", "DROP TABLE", "CREATE TABLE", "ATTACH TABLE"]);
126
+
127
+ const [after] = await q<{ uuid: string }>("SELECT toString(uuid) AS uuid FROM system.tables WHERE database = 'shop' AND name = 'files_stream'");
128
+ expect(after!.uuid).not.toBe(before!.uuid);
129
+ const [view] = await q<{ n: string }>("SELECT count() AS n FROM system.tables WHERE database = 'shop' AND name = 'files_mv'");
130
+ expect(Number(view!.n)).toBe(1);
131
+
132
+ // A file that lands after the replacement is read by the new table through the same view.
133
+ await putFile("f3.csv", "(4, 'd'), (5, 'e')");
134
+ await waitForRows(5);
135
+ // Give the new table the chance to read f1 and f2 again, which it must not.
136
+ await new Promise((r) => setTimeout(r, 5000));
137
+ const [rows] = await q<{ n: string; distinct: string }>("SELECT count() AS n, uniqExact(id) AS distinct FROM shop.files");
138
+ expect([Number(rows!.n), Number(rows!.distinct)]).toEqual([5, 5]);
139
+ expect((await planAgainstServer("e2e", buildFile, env())).changes).toEqual([]);
140
+ }, 300_000);
141
+
142
+ test("a different keeper_path plans with the reprocessing warning", async () => {
143
+ writeFileSync(buildFile, buildOf(SETTINGS(1000, "/chant/e2e/files-v2")));
144
+ const diff = await planAgainstServer("e2e", buildFile, env());
145
+ expect(diff.changes.map((c) => [c.rule, c.field])).toEqual([["SQLCH225", "settings.keeper_path"]]);
146
+ expect(renderDiff(diff)).toMatch(/every file already seen is processed again/);
147
+ }, 120_000);
148
+ });