@intentius/chant-lexicon-sql 0.124.30 → 0.124.32

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 (322) hide show
  1. package/dist/change-set.js +250 -0
  2. package/dist/clickhouse/access.js +722 -0
  3. package/dist/clickhouse/apply/apply.js +243 -0
  4. package/dist/clickhouse/apply/mutations.js +66 -0
  5. package/dist/clickhouse/apply/statements.js +545 -0
  6. package/dist/clickhouse/catalog-types.js +7 -0
  7. package/dist/clickhouse/container.js +152 -0
  8. package/dist/clickhouse/engine-syntax.js +159 -0
  9. package/dist/clickhouse/entities.js +593 -0
  10. package/dist/clickhouse/expressions.js +107 -0
  11. package/dist/clickhouse/http.js +55 -0
  12. package/dist/clickhouse/import/generator.js +192 -0
  13. package/dist/clickhouse/import/inlined.js +207 -0
  14. package/dist/clickhouse/import/ir.js +160 -0
  15. package/dist/clickhouse/import/live-export.js +131 -0
  16. package/dist/clickhouse/import/parser.js +17 -0
  17. package/dist/clickhouse/live/bind.js +116 -0
  18. package/dist/clickhouse/live/catalog.js +223 -0
  19. package/dist/clickhouse/live/describe-resources.js +91 -0
  20. package/dist/clickhouse/live/undeclarable.js +59 -0
  21. package/dist/clickhouse/overlays/codecs.js +57 -0
  22. package/dist/clickhouse/overlays/engines.js +101 -0
  23. package/dist/clickhouse/overlays/kinds.js +1 -0
  24. package/dist/clickhouse/overlays/projections.js +26 -0
  25. package/dist/clickhouse/overlays/skip-indexes.js +40 -0
  26. package/dist/clickhouse/overlays/ttl.js +34 -0
  27. package/dist/clickhouse/overlays/types.js +59 -0
  28. package/dist/clickhouse/ownership.js +83 -0
  29. package/dist/clickhouse/parser.js +674 -0
  30. package/dist/clickhouse/plan/commands.js +159 -0
  31. package/dist/clickhouse/plan/deep.js +345 -0
  32. package/dist/clickhouse/plan/diff.js +312 -0
  33. package/dist/clickhouse/plan/disruption.js +73 -0
  34. package/dist/clickhouse/plan/normalize.js +545 -0
  35. package/dist/clickhouse/plan/rebuild-handoff.js +57 -0
  36. package/dist/clickhouse/plan/report.js +26 -0
  37. package/dist/clickhouse/plan/rules.js +72 -0
  38. package/dist/clickhouse/plan/schema.js +67 -0
  39. package/dist/clickhouse/plan/server-format.js +35 -0
  40. package/dist/clickhouse/rebuild/backfill.js +265 -0
  41. package/dist/clickhouse/rebuild/engines.js +146 -0
  42. package/dist/clickhouse/rebuild/observe.js +260 -0
  43. package/dist/clickhouse/rebuild/op.js +152 -0
  44. package/dist/clickhouse/rebuild/partitions.js +65 -0
  45. package/dist/clickhouse/rebuild/receipts.js +120 -0
  46. package/dist/clickhouse/rebuild/replicas.js +121 -0
  47. package/dist/clickhouse/rebuild/shards.js +76 -0
  48. package/dist/clickhouse/rebuild/steps.js +383 -0
  49. package/dist/clickhouse/rebuild/verify.js +279 -0
  50. package/dist/clickhouse/row-types.js +151 -0
  51. package/dist/clickhouse/testing/fake-server.js +47 -0
  52. package/dist/clickhouse/testing/roundtrip-schema.js +25 -0
  53. package/dist/clickhouse/testing/writable-server.js +123 -0
  54. package/dist/clickhouse/tokens.js +23 -0
  55. package/dist/clickhouse/topology.js +463 -0
  56. package/dist/clickhouse/type-syntax.js +73 -0
  57. package/dist/clickhouse.js +25 -0
  58. package/dist/codegen/classifier-page.js +93 -0
  59. package/dist/codegen/clickhouse-module.js +249 -0
  60. package/dist/codegen/coverage-page.js +70 -0
  61. package/dist/codegen/docs-cli.js +3 -0
  62. package/dist/codegen/docs.js +222 -0
  63. package/dist/codegen/generate-cli.js +13 -0
  64. package/dist/codegen/generate.js +97 -0
  65. package/dist/codegen/package.js +28 -0
  66. package/dist/codegen/postgres-classifier-page.js +95 -0
  67. package/dist/codegen/postgres-module.js +418 -0
  68. package/dist/codegen/registry.js +52 -0
  69. package/dist/composites/audit-log-table.js +35 -0
  70. package/dist/composites/catalog.js +524 -0
  71. package/dist/composites/cdc-mirror.js +32 -0
  72. package/dist/composites/clickhouse.js +6 -0
  73. package/dist/composites/events-table.js +26 -0
  74. package/dist/composites/index.js +8 -0
  75. package/dist/composites/join-table.js +25 -0
  76. package/dist/composites/postgres.js +6 -0
  77. package/dist/composites/refreshed-view.js +24 -0
  78. package/dist/composites/replacing-table.js +28 -0
  79. package/dist/composites/rollup-view.js +29 -0
  80. package/dist/composites/sharded-table.js +27 -0
  81. package/dist/composites/soft-delete-table.js +34 -0
  82. package/dist/composites/tenant-table.js +26 -0
  83. package/dist/composites/typed.js +8 -0
  84. package/dist/config.js +206 -0
  85. package/dist/core/apply.js +93 -0
  86. package/dist/core/classifier.js +115 -0
  87. package/dist/core/container.js +69 -0
  88. package/dist/core/cursor.js +261 -0
  89. package/dist/core/diff.js +48 -0
  90. package/dist/core/entity.js +58 -0
  91. package/dist/core/find-templates.js +118 -0
  92. package/dist/core/foreign-tables.js +27 -0
  93. package/dist/core/handoff.js +32 -0
  94. package/dist/core/index.js +64 -0
  95. package/dist/core/interpolation.js +191 -0
  96. package/dist/core/normalize.js +43 -0
  97. package/dist/core/ownership.js +119 -0
  98. package/dist/core/receipts.js +22 -0
  99. package/dist/core/references.js +61 -0
  100. package/dist/core/statements.js +42 -0
  101. package/dist/core/template.js +79 -0
  102. package/dist/core/testing/http-stub.js +21 -0
  103. package/dist/core/tokens.js +202 -0
  104. package/dist/core/typed.js +40 -0
  105. package/dist/core/undeclarable.js +46 -0
  106. package/dist/coverage.js +161 -0
  107. package/dist/detect.js +32 -0
  108. package/dist/dialects.js +18 -0
  109. package/dist/files/build-root.js +72 -0
  110. package/dist/files/clickhouse.js +138 -0
  111. package/dist/files/common.js +58 -0
  112. package/dist/files/postgres.js +389 -0
  113. package/dist/generated/clickhouse-types.js +5 -0
  114. package/dist/generated/clickhouse.js +4361 -0
  115. package/dist/generated/postgres-types.js +227 -0
  116. package/dist/generated/postgres.js +3667 -0
  117. package/dist/import-generator.js +14 -0
  118. package/dist/import-parser.js +71 -0
  119. package/dist/index.js +24 -0
  120. package/dist/init-templates.js +290 -0
  121. package/dist/integrity.json +2 -2
  122. package/dist/lint/audit-catalog.js +31 -0
  123. package/dist/lint/audit-lineage.js +22 -0
  124. package/dist/lint/post-synth/audit-entries.js +34 -0
  125. package/dist/lint/post-synth/clickhouse-helpers.js +79 -0
  126. package/dist/lint/post-synth/clickhouse-type-check.js +162 -0
  127. package/dist/lint/post-synth/index.js +112 -0
  128. package/dist/lint/post-synth/postgres-audit-entries.js +35 -0
  129. package/dist/lint/post-synth/postgres-helpers.js +65 -0
  130. package/dist/lint/post-synth/postgres-names.js +507 -0
  131. package/dist/lint/post-synth/sql-helpers.js +23 -0
  132. package/dist/lint/post-synth/sql101.js +59 -0
  133. package/dist/lint/post-synth/sql102.js +34 -0
  134. package/dist/lint/post-synth/sqlch101.js +34 -0
  135. package/dist/lint/post-synth/sqlch102.js +23 -0
  136. package/dist/lint/post-synth/sqlch103.js +45 -0
  137. package/dist/lint/post-synth/sqlch104.js +41 -0
  138. package/dist/lint/post-synth/sqlch105.js +36 -0
  139. package/dist/lint/post-synth/sqlch106.js +27 -0
  140. package/dist/lint/post-synth/sqlch107.js +39 -0
  141. package/dist/lint/post-synth/sqlch108.js +28 -0
  142. package/dist/lint/post-synth/sqlch109.js +21 -0
  143. package/dist/lint/post-synth/sqlch110.js +31 -0
  144. package/dist/lint/post-synth/sqlch111.js +41 -0
  145. package/dist/lint/post-synth/sqlch112.js +28 -0
  146. package/dist/lint/post-synth/sqlch113.js +19 -0
  147. package/dist/lint/post-synth/sqlch114.js +26 -0
  148. package/dist/lint/post-synth/sqlch115.js +30 -0
  149. package/dist/lint/post-synth/sqlch116.js +22 -0
  150. package/dist/lint/post-synth/sqlch117.js +26 -0
  151. package/dist/lint/post-synth/sqlch118.js +27 -0
  152. package/dist/lint/post-synth/sqlch119.js +32 -0
  153. package/dist/lint/post-synth/sqlch120.js +20 -0
  154. package/dist/lint/post-synth/sqlch121.js +11 -0
  155. package/dist/lint/post-synth/sqlch122.js +11 -0
  156. package/dist/lint/post-synth/sqlch123.js +10 -0
  157. package/dist/lint/post-synth/sqlch124.js +21 -0
  158. package/dist/lint/post-synth/sqlch125.js +55 -0
  159. package/dist/lint/post-synth/sqlch126.js +62 -0
  160. package/dist/lint/post-synth/sqlch127.js +132 -0
  161. package/dist/lint/post-synth/sqlpg101.js +25 -0
  162. package/dist/lint/post-synth/sqlpg102.js +35 -0
  163. package/dist/lint/post-synth/sqlpg103.js +23 -0
  164. package/dist/lint/post-synth/sqlpg104.js +23 -0
  165. package/dist/lint/post-synth/sqlpg105.js +22 -0
  166. package/dist/lint/post-synth/sqlpg106.js +22 -0
  167. package/dist/lint/post-synth/sqlpg107.js +22 -0
  168. package/dist/lint/post-synth/sqlpg108.js +28 -0
  169. package/dist/lint/post-synth/sqlpg109.js +39 -0
  170. package/dist/lint/post-synth/sqlpg110.js +38 -0
  171. package/dist/lint/post-synth/sqlpg111.js +23 -0
  172. package/dist/lint/post-synth/sqlpg112.js +25 -0
  173. package/dist/lint/post-synth/sqlpg113.js +29 -0
  174. package/dist/lint/post-synth/sqlpg114.js +39 -0
  175. package/dist/lint/post-synth/sqlpg115.js +48 -0
  176. package/dist/lint/post-synth/sqlpg116.js +26 -0
  177. package/dist/lint/post-synth/sqlpg117.js +24 -0
  178. package/dist/lint/post-synth/sqlpg118.js +22 -0
  179. package/dist/lint/post-synth/sqlpg119.js +41 -0
  180. package/dist/lint/post-synth/sqlpg120.js +83 -0
  181. package/dist/lint/post-synth/sqlpg121.js +33 -0
  182. package/dist/lint/post-synth/sqlpg122.js +27 -0
  183. package/dist/lint/post-synth/sqlpg123.js +69 -0
  184. package/dist/lint/post-synth/sqlpg124.js +50 -0
  185. package/dist/lint/post-synth/sqlpg125.js +41 -0
  186. package/dist/lint/post-synth/sqlpg126.js +38 -0
  187. package/dist/lint/rules/index.js +12 -0
  188. package/dist/lint/rules/postgres-templates.js +27 -0
  189. package/dist/lint/rules/sqlch001.js +79 -0
  190. package/dist/lint/rules/sqlch002.js +124 -0
  191. package/dist/lint/rules/sqlch003.js +69 -0
  192. package/dist/lint/rules/sqlch004.js +83 -0
  193. package/dist/lint/rules/sqlpg001.js +68 -0
  194. package/dist/lint/rules/sqlpg002.js +69 -0
  195. package/dist/lint/rules/sqlpg003.js +58 -0
  196. package/dist/lint/rules/sqlpg004.js +77 -0
  197. package/dist/lint/rules/sqlpg005.js +86 -0
  198. package/dist/lint/rules/templates.js +25 -0
  199. package/dist/lint/rules/typed-names.js +132 -0
  200. package/dist/live-dialect.js +32 -0
  201. package/dist/lsp/catalog.js +44 -0
  202. package/dist/lsp/completions.js +136 -0
  203. package/dist/lsp/hover.js +115 -0
  204. package/dist/lsp/postgres-catalog.js +176 -0
  205. package/dist/lsp/postgres-template.js +171 -0
  206. package/dist/lsp/postgres.js +266 -0
  207. package/dist/lsp/refs.js +60 -0
  208. package/dist/lsp/registry.d.ts.map +1 -1
  209. package/dist/lsp/registry.js +19 -0
  210. package/dist/lsp/template.js +143 -0
  211. package/dist/manifest.json +1 -1
  212. package/dist/mcp/index.d.ts.map +1 -1
  213. package/dist/mcp/index.js +293 -0
  214. package/dist/mcp/postgres.js +117 -0
  215. package/dist/migration-statements.js +243 -0
  216. package/dist/op/activities/clickhouse-apply.js +83 -0
  217. package/dist/op/activities/clickhouse-emulator.js +53 -0
  218. package/dist/op/activities/clickhouse-rebuild.js +119 -0
  219. package/dist/op/activities/index.js +26 -0
  220. package/dist/op/activities/postgres-apply.js +126 -0
  221. package/dist/op/activities/postgres-emulator.js +70 -0
  222. package/dist/op/activities/postgres-migration.js +156 -0
  223. package/dist/op/activities/sql-exec.js +57 -0
  224. package/dist/op/activity-contracts.js +103 -0
  225. package/dist/package-cli.js +14 -0
  226. package/dist/plugin.js +259 -0
  227. package/dist/postgres/access/acl.js +344 -0
  228. package/dist/postgres/access/changes.js +30 -0
  229. package/dist/postgres/access/import.js +61 -0
  230. package/dist/postgres/access/live.js +96 -0
  231. package/dist/postgres/apply/apply.js +557 -0
  232. package/dist/postgres/apply/statements.js +882 -0
  233. package/dist/postgres/catalog-types.js +18 -0
  234. package/dist/postgres/container.js +58 -0
  235. package/dist/postgres/entities.js +1390 -0
  236. package/dist/postgres/entity-types.js +28 -0
  237. package/dist/postgres/import/generator.js +283 -0
  238. package/dist/postgres/import/ir.js +53 -0
  239. package/dist/postgres/import/live-export.js +83 -0
  240. package/dist/postgres/keywords.js +32 -0
  241. package/dist/postgres/live/bind.js +130 -0
  242. package/dist/postgres/live/catalog-kinds.js +301 -0
  243. package/dist/postgres/live/catalog.js +644 -0
  244. package/dist/postgres/live/client.js +151 -0
  245. package/dist/postgres/live/describe-resources.js +135 -0
  246. package/dist/postgres/live/undeclarable.js +111 -0
  247. package/dist/postgres/migrate/backfill.js +170 -0
  248. package/dist/postgres/migrate/batches.js +122 -0
  249. package/dist/postgres/migrate/carry.js +518 -0
  250. package/dist/postgres/migrate/handoff.js +59 -0
  251. package/dist/postgres/migrate/names.js +61 -0
  252. package/dist/postgres/migrate/observe.js +360 -0
  253. package/dist/postgres/migrate/op.js +178 -0
  254. package/dist/postgres/migrate/receipts.js +117 -0
  255. package/dist/postgres/migrate/replication.js +106 -0
  256. package/dist/postgres/migrate/steps.js +514 -0
  257. package/dist/postgres/overlays/columns.js +31 -0
  258. package/dist/postgres/overlays/constraints.js +52 -0
  259. package/dist/postgres/overlays/extension-objects.js +88 -0
  260. package/dist/postgres/overlays/indexes.js +25 -0
  261. package/dist/postgres/overlays/kinds.js +2 -0
  262. package/dist/postgres/overlays/partitioning.js +23 -0
  263. package/dist/postgres/overlays/relations.js +25 -0
  264. package/dist/postgres/overlays/storage.js +65 -0
  265. package/dist/postgres/overlays/type-ddl.js +36 -0
  266. package/dist/postgres/overlays/types.js +74 -0
  267. package/dist/postgres/parser.js +1824 -0
  268. package/dist/postgres/plan/commands.js +137 -0
  269. package/dist/postgres/plan/deep.js +324 -0
  270. package/dist/postgres/plan/diff.js +499 -0
  271. package/dist/postgres/plan/disruption.js +64 -0
  272. package/dist/postgres/plan/normalize.js +889 -0
  273. package/dist/postgres/plan/report.js +21 -0
  274. package/dist/postgres/plan/rules.js +115 -0
  275. package/dist/postgres/plan/schema.js +105 -0
  276. package/dist/postgres/plan/server-normalize.js +166 -0
  277. package/dist/postgres/providers/aurora.js +44 -0
  278. package/dist/postgres/providers/azure.js +53 -0
  279. package/dist/postgres/providers/cloud-sql.js +41 -0
  280. package/dist/postgres/providers/index.js +73 -0
  281. package/dist/postgres/providers/neon.js +44 -0
  282. package/dist/postgres/providers/rds.js +47 -0
  283. package/dist/postgres/providers/supabase.js +45 -0
  284. package/dist/postgres/providers/types.js +21 -0
  285. package/dist/postgres/row-types.js +107 -0
  286. package/dist/postgres/testing/fake-client.js +34 -0
  287. package/dist/postgres/testing/migration.js +48 -0
  288. package/dist/postgres/testing/roundtrip-schema.js +64 -0
  289. package/dist/postgres/testing/server.js +47 -0
  290. package/dist/postgres/testing/writable-server.js +160 -0
  291. package/dist/postgres/tokens.js +41 -0
  292. package/dist/postgres.js +15 -0
  293. package/dist/read/clickhouse.js +82 -0
  294. package/dist/read/objects.js +44 -0
  295. package/dist/read/postgres.js +104 -0
  296. package/dist/read.js +52 -0
  297. package/dist/receipts.js +207 -0
  298. package/dist/serializer.js +172 -0
  299. package/dist/skill-defs.js +93 -0
  300. package/dist/spec/catalog.js +191 -0
  301. package/dist/spec/fetch.d.ts.map +1 -1
  302. package/dist/spec/fetch.js +90 -0
  303. package/dist/spec/pin.js +42 -0
  304. package/dist/spec/postgres-catalog.js +171 -0
  305. package/dist/spec/postgres-fetch.js +91 -0
  306. package/dist/spec/postgres-pin.js +82 -0
  307. package/dist/sql-files.js +206 -0
  308. package/dist/sql-types-command.js +97 -0
  309. package/dist/sql-types.js +323 -0
  310. package/dist/token-source.js +156 -0
  311. package/dist/validate-cli.js +5 -0
  312. package/dist/validate.js +151 -0
  313. package/dist/workspace-outputs.js +191 -0
  314. package/package.json +7 -7
  315. package/src/codegen/package.ts +2 -2
  316. package/src/lint/rules/typescript-7.test.ts +2 -1
  317. package/src/lsp/catalog.ts +2 -2
  318. package/src/lsp/postgres-catalog.ts +2 -2
  319. package/src/lsp/registry.ts +2 -1
  320. package/src/mcp/index.ts +4 -2
  321. package/src/spec/fetch.ts +2 -2
  322. package/src/spec/postgres-fetch.ts +2 -2
@@ -0,0 +1,463 @@
1
+ /**
2
+ * One ClickHouse statement rendered for the topology it runs on (#3645):
3
+ * a single node, a cluster (`ON CLUSTER`), a `Replicated` database, or
4
+ * ClickHouse Cloud.
5
+ *
6
+ * A migration source is written once. What differs between topologies is
7
+ * two things, and this module rewrites only those:
8
+ *
9
+ * - `ON CLUSTER`. A cluster's DDL carries it so every host runs the
10
+ * statement. A single node has no cluster to name. A `Replicated`
11
+ * database replicates its own DDL and refuses `ON CLUSTER` naming any
12
+ * other cluster ("ON CLUSTER is not allowed for Replicated database",
13
+ * code 80 on 26.8) unless `ignore_on_cluster_for_replicated_database` is
14
+ * set (default 0). Cloud's databases are Replicated, so the same holds
15
+ * there.
16
+ * - The table engine. A MergeTree-family table replicates its rows only as
17
+ * `Replicated*MergeTree`. On a cluster that engine names its Keeper path
18
+ * and replica; inside a `Replicated` database it takes no path arguments,
19
+ * since `database_replicated_allow_replicated_engine_arguments` defaults to
20
+ * 0 (refuse them) and the database supplies its own. A single node has no
21
+ * Keeper to replicate through, so it gets the plain family. Cloud converts
22
+ * every MergeTree and `Replicated*` engine to its `Shared*` equivalent
23
+ * itself ("You don't add a `Shared` or `Replicated` prefix yourself",
24
+ * https://clickhouse.com/docs/whats-new/cloud-compatibility), so it gets
25
+ * the plain family as well, with no path arguments to clash.
26
+ *
27
+ * Every function here is pure: SQL in, SQL out, nothing sent. Statements
28
+ * this module does not recognize (`INSERT`, `SELECT`, `SYSTEM`) come back
29
+ * unchanged, so a caller can pass every statement it sends.
30
+ */
31
+ import { isTrivia, tokenizeText } from "./tokens.js";
32
+ import { parseCreate, unquote } from "./parser.js";
33
+ /**
34
+ * The Keeper path a cluster's `Replicated*MergeTree` gets: the server's own
35
+ * `default_replica_path`. `{uuid}` is the same on every host because the
36
+ * `CREATE` runs `ON CLUSTER`, and it does not change when the table is
37
+ * renamed or exchanged, so a rebuild's new table never shares the old one's
38
+ * path.
39
+ */
40
+ export const DEFAULT_REPLICA_PATH = "/clickhouse/tables/{uuid}/{shard}";
41
+ /** The replica name a cluster's `Replicated*MergeTree` gets: the server's `default_replica_name`. */
42
+ export const DEFAULT_REPLICA_NAME = "{replica}";
43
+ /** The Keeper path a `Replicated` database is created with in the `replicated` topology. */
44
+ export const replicatedDatabasePath = (database) => `/clickhouse/databases/${database}`;
45
+ /**
46
+ * Read a topology from its string form, as a command-line flag or an
47
+ * environment variable gives it: `single`, `cluster:<name>`, `replicated`,
48
+ * `replicated:<cluster>`, `cloud`.
49
+ */
50
+ export function parseTopology(value) {
51
+ const at = value.indexOf(":");
52
+ const kind = (at < 0 ? value : value.slice(0, at)).trim();
53
+ const arg = at < 0 ? undefined : value.slice(at + 1).trim();
54
+ switch (kind) {
55
+ case "single":
56
+ case "cloud":
57
+ if (arg !== undefined)
58
+ throw new Error(`topology ${kind} takes no argument: ${JSON.stringify(value)}`);
59
+ return { kind };
60
+ case "cluster":
61
+ if (!arg)
62
+ throw new Error(`topology cluster needs the cluster's name: cluster:<name>, got ${JSON.stringify(value)}`);
63
+ return { kind, cluster: arg };
64
+ case "replicated":
65
+ return arg ? { kind, cluster: arg } : { kind };
66
+ default:
67
+ throw new Error(`unknown topology ${JSON.stringify(value)}: expected single, cluster:<name>, replicated or cloud`);
68
+ }
69
+ }
70
+ const FAMILY = /^(Replicated)?(\w*MergeTree)$/;
71
+ const isStringLiteral = (arg) => arg !== undefined && /^'(?:[^'\\]|\\.)*'$/s.test(arg.trim());
72
+ const sqlString = (value) => `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
73
+ /**
74
+ * A MergeTree-family engine split into its family and its own arguments,
75
+ * with the Keeper path and replica name a `Replicated*` engine names taken
76
+ * apart. Undefined for any other engine (`Log`, `Distributed`) and for a
77
+ * `Shared*` one, which only Cloud writes.
78
+ *
79
+ * As ClickHouse reads it, a `Replicated*MergeTree` names a path and replica
80
+ * when its first argument is a string literal; otherwise every argument is
81
+ * the family's own (`ReplicatedReplacingMergeTree(ver)`).
82
+ */
83
+ function mergeTreeFamily(engine) {
84
+ const m = FAMILY.exec(engine.name);
85
+ if (!m || m[2].startsWith("Shared"))
86
+ return undefined;
87
+ const args = (engine.args ?? []).map((a) => a.trim());
88
+ if (m[1] && isStringLiteral(args[0]) && isStringLiteral(args[1])) {
89
+ return { family: m[2], replicated: true, replica: [args[0], args[1]], args: args.slice(2) };
90
+ }
91
+ return { family: m[2], replicated: m[1] !== undefined, args };
92
+ }
93
+ /**
94
+ * The engine a table gets on a topology. A MergeTree-family engine becomes
95
+ * `Replicated*` on a cluster (with the path and replica it declared, or the
96
+ * topology's) and in a Replicated database (with none); the plain family on
97
+ * a single node and on Cloud. Any other engine is returned as declared.
98
+ */
99
+ export function renderEngine(engine, topology) {
100
+ const f = mergeTreeFamily(engine);
101
+ if (!f)
102
+ return engine;
103
+ const parens = engine.args !== undefined;
104
+ const withArgs = (name, args) => (args.length > 0 || parens ? { name, args } : { name });
105
+ switch (topology.kind) {
106
+ case "single":
107
+ case "cloud":
108
+ return withArgs(f.family, f.args);
109
+ case "replicated":
110
+ return withArgs(`Replicated${f.family}`, f.args);
111
+ case "cluster": {
112
+ const replica = f.replica ?? [sqlString(topology.replicaPath ?? DEFAULT_REPLICA_PATH), sqlString(topology.replicaName ?? DEFAULT_REPLICA_NAME)];
113
+ return { name: `Replicated${f.family}`, args: [...replica, ...f.args] };
114
+ }
115
+ }
116
+ }
117
+ const engineText = (e) => (e.args === undefined ? e.name : `${e.name}(${e.args.join(", ")})`);
118
+ // ── statements ────────────────────────────────────────────────────────
119
+ /** `ON CLUSTER` and the name after it: a backquoted identifier, or a string literal for a macro (`'{cluster}'`). */
120
+ export function onClusterClause(cluster) {
121
+ return `ON CLUSTER ${/[{}]/.test(cluster) ? sqlString(cluster) : `\`${cluster.replace(/`/g, "``")}\``}`;
122
+ }
123
+ function applyEdits(tokens, edits) {
124
+ const sorted = [...edits].sort((a, b) => a.from - b.from);
125
+ let out = "";
126
+ let at = 0;
127
+ for (const e of sorted) {
128
+ out += tokens.slice(at, e.from).map((t) => t.text).join("") + e.text;
129
+ at = e.to;
130
+ }
131
+ return out + tokens.slice(at).map((t) => t.text).join("");
132
+ }
133
+ const word = (t, ...words) => t !== undefined && t.kind === "ident" && words.includes(t.text.toUpperCase());
134
+ const isName = (t) => t !== undefined && (t.kind === "ident" || t.kind === "qident" || t.kind === "number" || t.kind === "ref");
135
+ /** A walker over the significant tokens, which knows each one's index in the full list. */
136
+ class Walk {
137
+ constructor(tokens) {
138
+ this.tokens = tokens;
139
+ this.p = 0;
140
+ this.sig = tokens.flatMap((t, i) => (isTrivia(t) ? [] : [i]));
141
+ }
142
+ peek(n = 0) {
143
+ const i = this.sig[this.p + n];
144
+ return i === undefined ? undefined : this.tokens[i];
145
+ }
146
+ /** Index in `tokens` of the significant token `n` ahead, or the end. */
147
+ index(n = 0) {
148
+ return this.sig[this.p + n] ?? this.tokens.length;
149
+ }
150
+ /** Index in `tokens` just past the last significant token consumed. */
151
+ end() {
152
+ return this.p === 0 ? 0 : this.sig[this.p - 1] + 1;
153
+ }
154
+ accept(...words) {
155
+ if (!word(this.peek(), ...words))
156
+ return false;
157
+ this.p++;
158
+ return true;
159
+ }
160
+ acceptSeq(...words) {
161
+ if (!words.every((w, i) => word(this.peek(i), w)))
162
+ return false;
163
+ this.p += words.length;
164
+ return true;
165
+ }
166
+ /** `name` or `db.name`. False when no name is here. */
167
+ name() {
168
+ if (!isName(this.peek()))
169
+ return false;
170
+ this.p++;
171
+ while (this.peek()?.text === "." && isName(this.peek(1)))
172
+ this.p += 2;
173
+ return true;
174
+ }
175
+ /** The significant token index of the last top-level `SETTINGS`, outside any parentheses, or undefined. */
176
+ topLevelSettings() {
177
+ let depth = 0;
178
+ let found;
179
+ for (let k = this.p; k < this.sig.length; k++) {
180
+ const t = this.tokens[this.sig[k]];
181
+ if (t.text === "(")
182
+ depth++;
183
+ else if (t.text === ")")
184
+ depth--;
185
+ else if (depth === 0 && word(t, "SETTINGS"))
186
+ found = k;
187
+ }
188
+ return found;
189
+ }
190
+ /** Index in `tokens` of the significant token `k` (absolute), or the end of the statement. */
191
+ at(k) {
192
+ return this.sig[k] ?? this.tokens.length;
193
+ }
194
+ /** Index in `tokens` past the last significant token, ignoring a trailing `;`. */
195
+ bodyEnd() {
196
+ let k = this.sig.length - 1;
197
+ if (k >= 0 && this.tokens[this.sig[k]].text === ";")
198
+ k--;
199
+ return k < 0 ? 0 : this.sig[k] + 1;
200
+ }
201
+ }
202
+ /**
203
+ * The edit that makes the `ON CLUSTER` at the walker's position what the
204
+ * topology wants: replaced with the topology's cluster, removed, or (when
205
+ * there is none and `cluster` is given) inserted at `insertAt`.
206
+ */
207
+ function onClusterEdit(w, cluster, insertAt) {
208
+ if (word(w.peek(), "ON") && word(w.peek(1), "CLUSTER")) {
209
+ // Back to the end of the token before ON, so removing it takes its leading space too.
210
+ const from = w.end();
211
+ w.p += 2;
212
+ if (!w.name() && w.peek()?.kind === "string")
213
+ w.p++;
214
+ const to = w.end();
215
+ return { from, to, text: cluster === undefined ? "" : ` ${onClusterClause(cluster)}` };
216
+ }
217
+ return cluster === undefined ? undefined : { from: insertAt, to: insertAt, text: ` ${onClusterClause(cluster)}` };
218
+ }
219
+ /** The cluster a statement on a table or view carries on this topology, if any. */
220
+ const objectCluster = (topology) => (topology.kind === "cluster" ? topology.cluster : undefined);
221
+ /**
222
+ * The cluster a statement on a database itself carries on this topology, if
223
+ * any. In the replicated topology only creating and dropping the database
224
+ * reach every replica this way; an `ALTER` or `RENAME` of a Replicated
225
+ * database is replicated by the database.
226
+ */
227
+ const databaseCluster = (topology, createOrDrop = false) => topology.kind === "cluster" ? topology.cluster : topology.kind === "replicated" && createOrDrop ? topology.cluster : undefined;
228
+ const text = (tokens, span) => tokens
229
+ .slice(span.from, span.to)
230
+ .map((t) => t.text)
231
+ .join("")
232
+ .trim();
233
+ /** The `ON CLUSTER` edit for a parsed `CREATE`: after the name and any `UUID '...'`. */
234
+ function createClusterEdit(tokens, node, cluster) {
235
+ if (node.onCluster) {
236
+ let on = node.onCluster.from - 1;
237
+ // Back over the trivia and the CLUSTER and ON keywords, to the end of what came before.
238
+ let seen = 0;
239
+ while (on >= 0 && seen < 2) {
240
+ if (!isTrivia(tokens[on]))
241
+ seen++;
242
+ on--;
243
+ }
244
+ while (on >= 0 && isTrivia(tokens[on]))
245
+ on--;
246
+ return { from: on + 1, to: node.onCluster.to, text: cluster === undefined ? "" : ` ${onClusterClause(cluster)}` };
247
+ }
248
+ if (cluster === undefined)
249
+ return undefined;
250
+ const at = afterName(tokens, node);
251
+ return { from: at, to: at, text: ` ${onClusterClause(cluster)}` };
252
+ }
253
+ /** Index in `tokens` past a `CREATE`'s name and any `UUID '...'` after it: where `ON CLUSTER` goes. */
254
+ function afterName(tokens, node) {
255
+ const w = new Walk(tokens.slice(node.name.to));
256
+ if (w.accept("UUID") && w.peek()?.kind === "string") {
257
+ w.p++;
258
+ return node.name.to + w.end();
259
+ }
260
+ return node.name.to;
261
+ }
262
+ function renderCreate(tokens, node, topology) {
263
+ const edits = [];
264
+ if (node.statement === "database") {
265
+ const cluster = createClusterEdit(tokens, node, databaseCluster(topology, true));
266
+ if (cluster)
267
+ edits.push(cluster);
268
+ const declared = node.engine?.name;
269
+ if (topology.kind === "replicated") {
270
+ if (declared !== "Replicated") {
271
+ const name = unquote(text(tokens, node.name));
272
+ const clause = `ENGINE = Replicated(${sqlString(replicatedDatabasePath(name))}, '{shard}', '{replica}')`;
273
+ if (node.engine)
274
+ edits.push({ from: node.engine.span.from, to: node.engine.span.to, text: clause });
275
+ else {
276
+ // After ON CLUSTER as declared, or where an inserted one goes; edits at one index apply in the order pushed.
277
+ const at = node.onCluster ? node.onCluster.to : afterName(tokens, node);
278
+ edits.push({ from: at, to: at, text: ` ${clause}` });
279
+ }
280
+ }
281
+ }
282
+ else if (declared === "Replicated" && node.engine) {
283
+ // Only the replicated topology runs a Replicated database; elsewhere the server's default (Atomic, or Cloud's own).
284
+ let from = node.engine.span.from;
285
+ while (from > 0 && isTrivia(tokens[from - 1]))
286
+ from--;
287
+ edits.push({ from, to: node.engine.span.to, text: "" });
288
+ }
289
+ return applyEdits(tokens, edits);
290
+ }
291
+ // A function belongs to no database, so in the replicated topology it reaches every replica only through the cluster.
292
+ const cluster = createClusterEdit(tokens, node, node.statement === "function" ? databaseCluster(topology, true) : objectCluster(topology));
293
+ if (cluster)
294
+ edits.push(cluster);
295
+ if ((node.statement === "table" || node.statement === "view") && node.engine) {
296
+ const declared = { name: node.engine.name, ...(node.engine.args ? { args: node.engine.args.map((a) => text(tokens, a)) } : {}) };
297
+ const rendered = renderEngine(declared, topology);
298
+ if (engineText(rendered) !== engineText(declared)) {
299
+ // From the engine's name to the end of its argument list; `ENGINE =` stays as written.
300
+ edits.push({ from: node.engine.nameSpan.from, to: node.engine.span.to, text: engineText(rendered) });
301
+ }
302
+ }
303
+ return applyEdits(tokens, edits);
304
+ }
305
+ /** A `CREATE` the parser does not take (`CREATE TABLE ... AS`, a dictionary): `ON CLUSTER` after the name, the rest as written. */
306
+ function renderOtherCreate(tokens, topology) {
307
+ const w = new Walk(tokens);
308
+ w.accept("CREATE");
309
+ if (w.accept("OR"))
310
+ w.accept("REPLACE");
311
+ w.accept("TEMPORARY");
312
+ // Access control (`./access.ts`) belongs to no database: like a database's own DDL, it reaches every replica through the cluster.
313
+ if (w.accept("USER", "ROLE", "POLICY", "QUOTA", "PROFILE") || w.acceptSeq("ROW", "POLICY") || w.acceptSeq("SETTINGS", "PROFILE")) {
314
+ w.acceptSeq("IF", "NOT", "EXISTS") || w.acceptSeq("OR", "REPLACE");
315
+ if (!w.name())
316
+ return applyEdits(tokens, []);
317
+ const edit = onClusterEdit(w, databaseCluster(topology, true), w.end());
318
+ return applyEdits(tokens, edit ? [edit] : []);
319
+ }
320
+ let database = false;
321
+ if (w.accept("DATABASE"))
322
+ database = true;
323
+ else if (w.accept("MATERIALIZED", "LIVE", "WINDOW"))
324
+ w.accept("VIEW");
325
+ else if (!w.accept("TABLE", "VIEW", "DICTIONARY"))
326
+ return applyEdits(tokens, []);
327
+ w.acceptSeq("IF", "NOT", "EXISTS");
328
+ if (!w.name())
329
+ return applyEdits(tokens, []);
330
+ let at = w.end();
331
+ if (w.accept("UUID") && w.peek()?.kind === "string") {
332
+ w.p++;
333
+ at = w.end();
334
+ }
335
+ const edit = onClusterEdit(w, database ? databaseCluster(topology) : objectCluster(topology), at);
336
+ return applyEdits(tokens, edit ? [edit] : []);
337
+ }
338
+ /**
339
+ * The `ON CLUSTER` for a statement whose clause follows the object's name:
340
+ * `ALTER`, `DROP`, `TRUNCATE`, `OPTIMIZE`, `DETACH`, `ATTACH`.
341
+ */
342
+ function renderAfterName(tokens, w, cluster) {
343
+ w.acceptSeq("IF", "NOT", "EXISTS") || w.acceptSeq("IF", "EXISTS");
344
+ if (!w.name())
345
+ return applyEdits(tokens, []);
346
+ const edit = onClusterEdit(w, cluster, w.end());
347
+ return applyEdits(tokens, edit ? [edit] : []);
348
+ }
349
+ /** `RENAME ... TO ...` and `EXCHANGE ... AND ...`: `ON CLUSTER` at the end, before any `SETTINGS`. */
350
+ function renderAtEnd(tokens, w, cluster) {
351
+ // An `ON CLUSTER` already written, after the renames.
352
+ for (let n = 0; w.peek(n) !== undefined; n++) {
353
+ if (word(w.peek(n), "ON") && word(w.peek(n + 1), "CLUSTER")) {
354
+ w.p += n;
355
+ return applyEdits(tokens, [onClusterEdit(w, cluster, 0)]);
356
+ }
357
+ }
358
+ if (cluster === undefined)
359
+ return applyEdits(tokens, []);
360
+ const settings = w.topLevelSettings();
361
+ let at;
362
+ if (settings !== undefined) {
363
+ at = w.at(settings);
364
+ while (at > 0 && isTrivia(tokens[at - 1]))
365
+ at--;
366
+ }
367
+ else
368
+ at = w.bodyEnd();
369
+ return applyEdits(tokens, [{ from: at, to: at, text: ` ${onClusterClause(cluster)}` }]);
370
+ }
371
+ /**
372
+ * One statement as it runs on `topology`: `ON CLUSTER` added, replaced or
373
+ * removed, and in a `CREATE`, the engine the topology needs. Comments,
374
+ * spacing and everything else in the statement are kept as written.
375
+ * A statement that is not DDL on a database, table or view is returned
376
+ * unchanged, as is `KILL QUERY`/`KILL MUTATION` everywhere but a cluster,
377
+ * where it gains `ON CLUSTER` (inside a Replicated database
378
+ * `KILL QUERY ON CLUSTER '<db>'` names the database's own cluster and is kept).
379
+ */
380
+ export function renderStatement(sql, topology) {
381
+ const tokens = tokenizeText(sql, 0);
382
+ const w = new Walk(tokens);
383
+ const head = w.peek();
384
+ if (word(head, "CREATE")) {
385
+ let node;
386
+ try {
387
+ node = parseCreate(tokens);
388
+ }
389
+ catch {
390
+ node = undefined;
391
+ }
392
+ return node ? renderCreate(tokens, node, topology) : renderOtherCreate(tokens, topology);
393
+ }
394
+ if (word(head, "ALTER", "DROP", "DETACH", "ATTACH", "TRUNCATE", "OPTIMIZE", "UNDROP")) {
395
+ w.p++;
396
+ if (word(head, "DROP", "DETACH", "ATTACH"))
397
+ w.accept("TEMPORARY");
398
+ if (w.accept("DATABASE"))
399
+ return renderAfterName(tokens, w, databaseCluster(topology, word(head, "DROP")));
400
+ // A function and access control belong to no database: like the database's own DDL, they reach every replica through the cluster.
401
+ if (w.accept("FUNCTION", "USER", "ROLE", "POLICY", "QUOTA", "PROFILE") || w.acceptSeq("ROW", "POLICY") || w.acceptSeq("SETTINGS", "PROFILE")) {
402
+ return renderAfterName(tokens, w, databaseCluster(topology, true));
403
+ }
404
+ // `TRUNCATE t` may leave out TABLE.
405
+ if (w.accept("TABLE", "VIEW", "DICTIONARY") || word(head, "TRUNCATE"))
406
+ return renderAfterName(tokens, w, objectCluster(topology));
407
+ return sql;
408
+ }
409
+ if (word(head, "GRANT", "REVOKE")) {
410
+ w.p++;
411
+ const edit = onClusterEdit(w, databaseCluster(topology, true), w.end());
412
+ return applyEdits(tokens, edit ? [edit] : []);
413
+ }
414
+ if (word(head, "RENAME")) {
415
+ w.p++;
416
+ if (w.accept("DATABASE"))
417
+ return renderAtEnd(tokens, w, databaseCluster(topology));
418
+ if (w.accept("TABLE", "DICTIONARY"))
419
+ return renderAtEnd(tokens, w, objectCluster(topology));
420
+ return sql;
421
+ }
422
+ if (word(head, "EXCHANGE")) {
423
+ w.p++;
424
+ if (w.accept("TABLES", "DICTIONARIES"))
425
+ return renderAtEnd(tokens, w, objectCluster(topology));
426
+ return sql;
427
+ }
428
+ if (word(head, "KILL") && topology.kind === "cluster") {
429
+ w.p++;
430
+ if (!w.accept("QUERY", "MUTATION"))
431
+ return sql;
432
+ const edit = onClusterEdit(w, topology.cluster, w.end());
433
+ return applyEdits(tokens, edit ? [edit] : []);
434
+ }
435
+ return sql;
436
+ }
437
+ /** Each step's statement rendered for `topology`; every other field of a step is kept. */
438
+ export function renderSteps(steps, topology) {
439
+ return steps.map((s) => ({ ...s, sql: renderStatement(s.sql, topology) }));
440
+ }
441
+ /**
442
+ * A statement for a topology that may not be configured: with none, the
443
+ * statement as the source wrote it. The applier, the plan and the rebuild
444
+ * migration render only when an environment names its topology
445
+ * (`sql.profiles.<env>.topology`, `CLICKHOUSE_TOPOLOGY`), so a project that
446
+ * names none sends what it declared, as before.
447
+ */
448
+ export const renderFor = (sql, topology) => (topology ? renderStatement(sql, topology) : sql);
449
+ /** A topology from its string form or its object form (`sql.profiles.<env>.topology`). */
450
+ export function toTopology(value) {
451
+ return typeof value === "string" ? parseTopology(value) : value;
452
+ }
453
+ /** The topology's string form, as `parseTopology` reads it. Replica path options are not part of it. */
454
+ export function topologyLabel(topology) {
455
+ switch (topology.kind) {
456
+ case "cluster":
457
+ return `cluster:${topology.cluster}`;
458
+ case "replicated":
459
+ return topology.cluster ? `replicated:${topology.cluster}` : "replicated";
460
+ default:
461
+ return topology.kind;
462
+ }
463
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A column type, read into its family and parameters: `Nullable(String)` is
3
+ * the family `Nullable` with the one parameter `String`;
4
+ * `DateTime64(3, 'UTC')` is `DateTime64` with `3` and `'UTC'`.
5
+ *
6
+ * The reading is lexical, over the same tokenizer the parser uses, so a
7
+ * string literal holding a parenthesis (`Enum8('a(' = 1)`) or a comma does not
8
+ * split a parameter. What a parameter means (a type, a number, a name) is the
9
+ * caller's question, answered from `TYPE_PARAMETERS`.
10
+ */
11
+ import { isTrivia, tokenizeText } from "./tokens.js";
12
+ const textOf = (tokens) => tokens.map((t) => t.text).join("").trim();
13
+ /** The significant tokens of a type, or undefined when it does not tokenize. */
14
+ export function typeTokens(text) {
15
+ try {
16
+ return tokenizeText(text, 0);
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ /**
23
+ * Read a type into its leading words and its parameters. Undefined when the
24
+ * text is not a type this can read: no leading name, or unbalanced parentheses.
25
+ * Anything after the closing parenthesis (`COLLATE utf8`) is ignored.
26
+ */
27
+ export function readType(tokens) {
28
+ const sig = tokens.filter((t) => !isTrivia(t));
29
+ const words = [];
30
+ let i = 0;
31
+ // `String COLLATE utf8`: COLLATE ends the type's words.
32
+ while (i < sig.length && (sig[i].kind === "ident" || sig[i].kind === "qident") && sig[i].text.toUpperCase() !== "COLLATE")
33
+ words.push(sig[i++]);
34
+ if (words.length === 0)
35
+ return undefined;
36
+ if (i >= sig.length || !(sig[i].kind === "punct" && sig[i].text === "("))
37
+ return { words };
38
+ // Parameters: the tokens up to the matching parenthesis, split at top-level commas.
39
+ const start = tokens.indexOf(sig[i]);
40
+ const params = [];
41
+ let depth = 0;
42
+ let current = [];
43
+ for (let j = start; j < tokens.length; j++) {
44
+ const t = tokens[j];
45
+ if (t.kind === "punct" && t.text === "(") {
46
+ if (depth++ === 0)
47
+ continue;
48
+ }
49
+ else if (t.kind === "punct" && t.text === ")") {
50
+ if (--depth === 0) {
51
+ if (params.length > 0 || current.some((x) => !isTrivia(x)))
52
+ params.push({ text: textOf(current), tokens: current });
53
+ return { words, params };
54
+ }
55
+ }
56
+ else if (depth === 1 && t.kind === "punct" && t.text === ",") {
57
+ params.push({ text: textOf(current), tokens: current });
58
+ current = [];
59
+ continue;
60
+ }
61
+ current.push(t);
62
+ }
63
+ return undefined;
64
+ }
65
+ /** A parameter that is a numeric literal, possibly negative: its value. */
66
+ export function numericParam(param) {
67
+ const sig = param.tokens.filter((t) => !isTrivia(t));
68
+ const negative = sig.length === 2 && sig[0].kind === "op" && sig[0].text === "-";
69
+ const n = negative ? sig[1] : sig.length === 1 ? sig[0] : undefined;
70
+ if (!n || n.kind !== "number" || !/^\d+(\.\d+)?([eE][+-]?\d+)?$/.test(n.text))
71
+ return undefined;
72
+ return negative ? -Number(n.text) : Number(n.text);
73
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@intentius/chant-lexicon-sql/clickhouse`: the ClickHouse dialect.
3
+ *
4
+ * Everything here is spec-true to ClickHouse at the pinned server release
5
+ * (`CLICKHOUSE_VERSION`), not a database-agnostic model: the engine names,
6
+ * column type families, codecs, skip index types and settings are the pinned
7
+ * server's own, read from its `system.*` tables, and the argument grammar the
8
+ * catalog does not carry comes from the overlays beside them.
9
+ */
10
+ export { database, table, view, dictionary, func, user, role, policy, grant, quota, settingsProfile, literal, SqlLiteral, SqlTemplateError, ClickHouseObject, CLICKHOUSE_ENTITY_TYPES, isClickHouseObject, isColumnRef, quoteIdentifier, } from "./clickhouse/entities.js";
11
+ export * from "./generated/clickhouse.js";
12
+ export * from "./composites/clickhouse.js";
13
+ export { ENGINE_ARGUMENTS } from "./clickhouse/overlays/engines.js";
14
+ export { SKIP_INDEX_PARAMETERS } from "./clickhouse/overlays/skip-indexes.js";
15
+ export { CODEC_OVERLAY } from "./clickhouse/overlays/codecs.js";
16
+ export { TYPE_PARAMETERS, WRAPPER_TYPES } from "./clickhouse/overlays/types.js";
17
+ export { TTL_ACTIONS, COLUMN_TTL_ACTIONS, TTL_RESULT_TYPES, TTL_SETTINGS, } from "./clickhouse/overlays/ttl.js";
18
+ export { PROJECTION_FORMS, PROJECTION_SETTINGS, PROJECTION_FORBIDDEN_CLAUSES, } from "./clickhouse/overlays/projections.js";
19
+ export { CLICKHOUSE_IMAGE_DIGEST, CLICKHOUSE_IMAGE_REPOSITORY, clickhouseImage } from "./spec/pin.js";
20
+ export { ClickHouseRebuildOp, } from "./clickhouse/rebuild/op.js";
21
+ export { RECEIPTS_DATABASE, RECEIPTS_TABLE } from "./clickhouse/rebuild/receipts.js";
22
+ export { DEFAULT_REPLICA_NAME, DEFAULT_REPLICA_PATH, onClusterClause, parseTopology, renderEngine, renderFor, renderStatement, renderSteps, replicatedDatabasePath, topologyLabel, toTopology, } from "./clickhouse/topology.js";
23
+ // The build's ClickHouse objects as typed values, for a project's own
24
+ // `lint.policies` (#3870); also at `@intentius/chant-lexicon-sql/read`.
25
+ export { readClickHouse, } from "./read/clickhouse.js";
@@ -0,0 +1,93 @@
1
+ /**
2
+ * `docs/pages/change-classifier.mdx`, written from the classifier's own rule
3
+ * table by `npm run docs`, so the page cannot disagree with the rules.
4
+ */
5
+ import { CLASSIFIER_RULES } from "../clickhouse/plan/rules.js";
6
+ const LABEL = {
7
+ create: "create",
8
+ drop: "drop",
9
+ metadata: "metadata only",
10
+ rewrite: "background rewrite",
11
+ rebuild: "rebuild",
12
+ };
13
+ const cell = (s) => s.replace(/\|/g, "\\|");
14
+ export function renderClassifierPage() {
15
+ const rows = Object.values(CLASSIFIER_RULES).map((r) => `| ${r.id} | ${LABEL[r.class]} | ${cell(r.title)} | ${cell(r.restriction)} [ClickHouse docs](${r.cite}) |`);
16
+ return `---
17
+ title: "Planning ClickHouse Changes"
18
+ label: "Planning and the Change Classifier"
19
+ description: "How a ClickHouse schema change is classified as metadata only, a background rewrite or a rebuild, each with the ALTER restriction behind it"
20
+ diataxis: reference
21
+ group: "ClickHouse"
22
+ order: 1
23
+ ---
24
+
25
+ {/* Written by src/codegen/classifier-page.ts from src/clickhouse/plan/rules.ts. Edit the rules, then run npm run docs. */}
26
+
27
+ Every change between two ClickHouse schemas is classified before anything runs:
28
+
29
+ | Class | Means |
30
+ |---|---|
31
+ | metadata only | the server records the change and touches no existing data |
32
+ | background rewrite | a mutation rewrites existing parts in the background, with no rollback |
33
+ | rebuild | ClickHouse cannot make the change to the existing table, and the data has to be copied into a new one; a plan refuses to make it in place |
34
+
35
+ Two commands report the classification, and \`chant lifecycle plan\` reports it as each update's disruption (metadata only is \`in-place\`, a rewrite \`rolling\`, a rebuild \`replace\`):
36
+
37
+ \`\`\`bash
38
+ chant sql diff base.json head.json # two chant build outputs, offline
39
+ chant sql plan prod schema.json # a build output against the prod server
40
+ \`\`\`
41
+
42
+ Both exit 2 when a change needs a rebuild. \`chant sql diff\` compares a pull request's base and head builds with no server. \`chant sql plan\` reads the server \`sql.profiles.<env>\` binds, and asks that server's formatter about any expression the normalization rules leave different, so the server's own rewriting (\`INTERVAL 1 DAY\` as \`toIntervalDay(1)\`, \`a+b*2\` as \`a + (b * 2)\`) is not reported as a change.
43
+
44
+ \`chant sql plan\` also exits 2 when a declared database holds an object whose definition it cannot read. It names each one under \`Refused\` (\`unreadable\` in \`--json\`) instead of leaving it out. Dictionaries are read and planned like the other objects: any change but a comment replaces one (SQLCH245).
45
+
46
+ A clause chant's parser does not know ([Plain .sql Files](../sql-files/#clauses-chant-does-not-read)) is compared as text, and the rest of its object is planned as usual. A change to one is SQLCH290, refused for review with no rebuild Op suggested, since chant cannot tell what the change takes. A clause only the server prints is a hint, not a change: a newer server printing a clause with its default looks the same as a clause removed from the declaration.
47
+
48
+ ## After the plan
49
+
50
+ The plan only reports. [\`clickhouseApply\`](../applying/) makes the metadata-only and background-rewrite changes, and refuses a rebuild the same way the plan does, sending nothing for that object. A rebuild runs as \`ClickHouseRebuildOp\`, a gated migration Op that creates the new table, backfills it, verifies it and swaps it in: see [Rebuilding a Table](../rebuild/). Both commands print the Op declaration to start from for each refused table.
51
+
52
+ \`chant migrate\` plays no part in any of this. It translates a file from one lexicon's format into another's, such as a GitHub Actions workflow into GitLab CI, and does not run schema migrations.
53
+
54
+ ## Statements for a migration file
55
+
56
+ \`chant sql diff base.json head.json --statements\` prints the statements that take the base schema to the head one, offline: each one the applier would send for the same change, after a comment naming its object, rule and class. \`--json\` prints them as one document, which \`diffStatements(before, after)\` from \`@intentius/chant-lexicon-sql\` also returns:
57
+
58
+ \`\`\`sql
59
+ -- events (analytics.events): SQLCH201 metadata
60
+ ALTER TABLE \`analytics\`.\`events\` ADD COLUMN region String DEFAULT 'eu' AFTER \`user_id\`;
61
+
62
+ -- events (analytics.events): SQLCH210 rewrite, waits for its mutation
63
+ ALTER TABLE \`analytics\`.\`events\` MODIFY COLUMN \`kind\` LowCardinality(String);
64
+
65
+ -- sessions (analytics.sessions): SQLCH220 made by ClickHouseRebuildOp, not a statement:
66
+ -- export const { op } = ClickHouseRebuildOp({ name: "rebuild-analytics-sessions", env: "<env>", table: "analytics.sessions", dualWrite: { mode: "materialized-view", cutoverColumn: "ts" } });
67
+ \`\`\`
68
+
69
+ A rebuild is never DDL: it is a step naming \`ClickHouseRebuildOp\`, or for a view or a database, which the Op does not rebuild, a manual step. A column or object drop is a statement marked destructive. Each \`CREATE\` carries the project's ownership marker, as the applier stamps it. The command exits 2 when a step is not a statement. A migration runner's history table (\`schema_migrations\`, \`goose_db_version\`; see [Importing a Live Server](../importing/)) is never dropped: the report names it in a hint. Two things the applier asks the server are left out: whether a difference is formatting only, and whether a database is empty before it is dropped.
70
+
71
+ \`--topology\` renders the statements for where they will run: \`single\` (the default), \`cluster:<name>\`, \`replicated\` or \`cloud\`. On a cluster every statement carries \`ON CLUSTER\` and a MergeTree-family table becomes \`Replicated*MergeTree\` with its Keeper path; in a \`Replicated\` database the tables are \`Replicated*MergeTree\` with no path and no \`ON CLUSTER\`. \`diffStatements(before, after, { topology })\` takes the same choice; see [Applying to a Server](../applying/#topology).
72
+
73
+ ## Identity and renames
74
+
75
+ An object is identified by its export name between two builds, so a changed name in the SQL under the same export is a rename. Against a server, which has no export names, an object is identified by \`database.name\`; a declaration renamed since the server last saw it says so with \`-- previously: <old name>\` before its CREATE.
76
+
77
+ A column is identified by its name. A renamed column says so on its own line:
78
+
79
+ \`\`\`sql
80
+ CREATE TABLE events (
81
+ event_kind LowCardinality(String), -- previously: kind
82
+ ...
83
+ \`\`\`
84
+
85
+ Without the hint the change is a drop and an add, and the report points out a drop and an add with the same type in the same place.
86
+
87
+ ## The rules
88
+
89
+ | Id | Class | Change | Restriction |
90
+ |---|---|---|---|
91
+ ${rows.join("\n")}
92
+ `;
93
+ }