@intentius/chant-lexicon-sql 0.124.31 → 0.124.33

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,159 @@
1
+ /**
2
+ * `chant sql diff` and `chant sql plan`: the classified change between two
3
+ * builds, or between a build and a live server. A Postgres build (its output
4
+ * says `"dialect": "postgres"`) or a Postgres binding goes to
5
+ * `../../postgres/plan/commands.ts`.
6
+ *
7
+ * chant sql diff <before.json> <after.json> [--json] [--statements] [--topology <topology>]
8
+ * chant sql plan <env> <build.json> [--json]
9
+ *
10
+ * `diff` is offline: it compares two `chant build` outputs, say a pull
11
+ * request's base and head, and needs no server. `plan` compares a build with
12
+ * the server `sql.profiles.<env>` binds and asks that server's formatter about
13
+ * expressions the rules leave different. Both exit 2 when a change needs a
14
+ * rebuild, which a plan refuses to make in place, and `plan` also when the
15
+ * declared databases hold an object whose definition it cannot read (#3653),
16
+ * which it names; 0 otherwise. For each
17
+ * refused table they name the `ClickHouseRebuildOp` to run instead
18
+ * (`rebuildOps` in `--json`, `./rebuild-handoff.ts`).
19
+ *
20
+ * `diff --statements` prints the statements instead, for a migration file:
21
+ * each one the applier would send, with its change's rule and class, and a
22
+ * refused change as the Op that makes it (`../../migration-statements.ts`).
23
+ * The marker in each `CREATE` is the project's `ownership` config's.
24
+ *
25
+ * `--topology` (`single`, `cluster:<name>`, `replicated`, `replicated:<cluster>`,
26
+ * `cloud`; `../topology.ts`) renders a ClickHouse diff's declarations and
27
+ * statements for that topology: `ON CLUSTER` and the engine. `--statements`
28
+ * renders for `single` without it; the plain diff compares the declarations as
29
+ * written. `plan` takes the topology from `sql.profiles.<env>.topology`.
30
+ */
31
+ import { diffSchemas } from "./diff.js";
32
+ import { renderDiff } from "./report.js";
33
+ import { keyedByQualifiedName, schemaFromBuildFile, schemaFromServer } from "./schema.js";
34
+ import { dropFormattingOnly } from "./server-format.js";
35
+ import { scopeOf } from "./normalize.js";
36
+ import { ACCESS_KINDS, accessUnmanagedHint, declaredAccess } from "../live/catalog.js";
37
+ import { bindClickHouse } from "../live/bind.js";
38
+ import { rebuildOpSuggestions } from "./rebuild-handoff.js";
39
+ import { readFileSync } from "node:fs";
40
+ import { parseTopology } from "../topology.js";
41
+ /** The dialect a build output names. */
42
+ function outputDialect(path) {
43
+ try {
44
+ return JSON.parse(readFileSync(path, "utf-8")).dialect;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ function split(args) {
51
+ const positional = [];
52
+ let topology;
53
+ for (let i = 0; i < args.length; i++) {
54
+ const a = args[i];
55
+ if (a === "--topology")
56
+ topology = args[++i] ?? "";
57
+ else if (a.startsWith("--topology="))
58
+ topology = a.slice("--topology=".length);
59
+ else if (!a.startsWith("--"))
60
+ positional.push(a);
61
+ }
62
+ return { positional, json: args.includes("--json"), statements: args.includes("--statements"), ...(topology !== undefined ? { topology } : {}) };
63
+ }
64
+ /** `chant sql diff --statements`: the statements between two builds, as SQL with comments or as JSON. Exits 2 when a step is an Op or manual. */
65
+ async function runDiffStatements(before, after, json, topology) {
66
+ const { diffStatements, renderStatements } = await import("../../migration-statements.js");
67
+ const { resolveOwnershipMarker } = await import("../../core/apply.js");
68
+ let marker;
69
+ try {
70
+ const { loadChantConfig } = await import("@intentius/chant/config");
71
+ const config = await loadChantConfig(process.cwd()).then((r) => r.config).catch(() => undefined);
72
+ marker = resolveOwnershipMarker({}, config, "chant sql diff --statements");
73
+ }
74
+ catch (err) {
75
+ console.error(err instanceof Error ? err.message : String(err));
76
+ return 1;
77
+ }
78
+ const doc = diffStatements(readFileSync(before, "utf-8"), readFileSync(after, "utf-8"), { ...(marker ? { marker } : {}), ...(topology ? { topology } : {}) });
79
+ console.log(json ? JSON.stringify(doc, null, 2) : renderStatements(doc));
80
+ return doc.refused ? 2 : 0;
81
+ }
82
+ function emit(diff, json, title) {
83
+ console.log(json ? JSON.stringify(diff, null, 2) : renderDiff(diff, { title }));
84
+ return diff.rebuilds.length > 0 || (diff.unreadable?.length ?? 0) > 0 ? 2 : 0;
85
+ }
86
+ export async function runDiff(ctx) {
87
+ const { positional, json, statements, topology: topologyArg } = split(ctx.rawArgs);
88
+ if (positional.length !== 2) {
89
+ console.error("usage: chant sql diff <before.json> <after.json> [--json] [--statements] [--topology <topology>]");
90
+ return 1;
91
+ }
92
+ let topology;
93
+ try {
94
+ topology = topologyArg !== undefined ? parseTopology(topologyArg) : undefined;
95
+ }
96
+ catch (err) {
97
+ console.error(`--topology: ${err.message}`);
98
+ return 1;
99
+ }
100
+ if (statements)
101
+ return runDiffStatements(positional[0], positional[1], json, topology);
102
+ if (outputDialect(positional[1]) === "postgres") {
103
+ const { diffPgBuildFiles, emitPg, projectMajor } = await import("../../postgres/plan/commands.js");
104
+ return emitPg(diffPgBuildFiles(positional[0], positional[1], await projectMajor()), json, `${positional[0]} -> ${positional[1]}`);
105
+ }
106
+ const after = schemaFromBuildFile(positional[1], "default", topology);
107
+ const diff = diffSchemas(schemaFromBuildFile(positional[0], "default", topology), after);
108
+ const rebuildOps = rebuildOpSuggestions(diff, new Map(after.map((o) => [o.key, o.canonical])), "<env>");
109
+ return emit(rebuildOps.length > 0 ? { ...diff, rebuildOps } : diff, json, `${positional[0]} -> ${positional[1]}`);
110
+ }
111
+ /** The declared objects against the server, keys and labels by `database.name`. */
112
+ export async function planAgainstServer(environment, buildFile, options = {}) {
113
+ const target = await bindClickHouse({ ...options, environment });
114
+ const all = keyedByQualifiedName(schemaFromBuildFile(buildFile, target.defaultDatabase, target.topology));
115
+ // Access declarations a profile does not manage are not planned, and the server's access is not read (#3716).
116
+ const declared = target.access === true ? all : all.filter((o) => !ACCESS_KINDS.has(o.canonical.kind));
117
+ const unmanaged = all.length - declared.length;
118
+ const databases = new Set(declared.map((o) => scopeOf(o.canonical)));
119
+ const unreadable = [];
120
+ const live = await schemaFromServer(target, databases, unreadable, declaredAccess(declared.map((o) => o.canonical)));
121
+ const diff = diffSchemas(live, declared);
122
+ const changes = await dropFormattingOnly(target.endpoint, diff.changes);
123
+ const rebuildOps = rebuildOpSuggestions({ ...diff, changes, rebuilds: changes.filter((c) => c.class === "rebuild") }, new Map(declared.map((o) => [o.key, o.canonical])), environment);
124
+ const label = new Map(declared.map((o) => [o.key, o.exportName === o.key ? o.key : `${o.exportName} (${o.key})`]));
125
+ const relabel = changes.map((c) => ({ ...c, object: label.get(c.object) ?? c.object }));
126
+ return {
127
+ changes: relabel,
128
+ rebuilds: relabel.filter((c) => c.class === "rebuild"),
129
+ hints: unmanaged > 0 ? [...diff.hints, accessUnmanagedHint(unmanaged, environment)] : diff.hints,
130
+ ...(rebuildOps.length > 0 ? { rebuildOps } : {}),
131
+ ...(unreadable.length > 0 ? { unreadable } : {}),
132
+ };
133
+ }
134
+ export async function runPlan(ctx) {
135
+ const { positional, json } = split(ctx.rawArgs);
136
+ if (positional.length !== 2) {
137
+ console.error("usage: chant sql plan <env> <build.json> [--json]");
138
+ return 1;
139
+ }
140
+ if (outputDialect(positional[1]) === "postgres") {
141
+ const { planPgAgainstServer, emitPg } = await import("../../postgres/plan/commands.js");
142
+ return emitPg(await planPgAgainstServer(positional[0], positional[1]), json, `${positional[1]} against ${positional[0]}`);
143
+ }
144
+ const diff = await planAgainstServer(positional[0], positional[1]);
145
+ return emit(diff, json, `${positional[1]} against ${positional[0]}`);
146
+ }
147
+ export const sqlCommands = {
148
+ name: "sql",
149
+ description: "Schema changes, classified: between two builds, or a build and a live server",
150
+ commands: [
151
+ { name: "diff", description: "Classify the change between two chant build outputs (offline); --statements prints the statements it takes, --topology renders them for single, cluster:<name>, replicated or cloud", handler: runDiff },
152
+ { name: "plan", description: "Classify the change from an environment's server to a chant build output", handler: runPlan },
153
+ {
154
+ name: "types",
155
+ description: "Write sql-types.d.ts: typed columns for each table, view and dictionary the build declares, for tags given their name (table<\"app.users\">) and RowOf; --output <path> writes it elsewhere; --check compares instead of writing and exits 1 when the file differs",
156
+ handler: async (ctx) => (await import("../../sql-types-command.js")).runTypes(ctx),
157
+ },
158
+ ],
159
+ };
@@ -0,0 +1,345 @@
1
+ /**
2
+ * `observeResourcesDeep()` for the ClickHouse dialect: each declared object's
3
+ * live definition as a property tree in the declaration's own shape, so
4
+ * `chant lifecycle diff --live` and `plan` report property drift with no false
5
+ * drift.
6
+ *
7
+ * The reader parses the server's `SHOW CREATE` with the same tag the
8
+ * declaration used, which gives the same props shape. A field whose canonical
9
+ * form (`./normalize.ts`) equals the declaration's is written with the
10
+ * declaration's own value, so `INTERVAL 180 DAY` and `toIntervalDay(180)` do
11
+ * not read as drift; a field that differs keeps the server's value.
12
+ * `deepNormalizationHooks` prunes what is not a property of the object (the
13
+ * DDL text, the template source, lineage, a view's reads and `TO` target).
14
+ * `chant sql plan` covers the target, and classifies every change.
15
+ *
16
+ * On a `cluster:<name>` topology every replica of every shard is read
17
+ * (#3664). When a server's copy of a table differs from the profile
18
+ * server's, that copy is what is reported, with the server in `observedOn`,
19
+ * which the drift report prints as `seen on`.
20
+ *
21
+ * Where the profile manages access, the grants an apply would give or take
22
+ * from each declared grantee are reported as `pending` (#3733), the same
23
+ * statements `chant sql plan` plans (SQLCH273, SQLCH274), so a privilege
24
+ * granted or revoked by hand after an approval moves the plan digest.
25
+ */
26
+ import { deepObservation, } from "@intentius/chant/deep-observation";
27
+ import { bindClickHouse, classifyClickHouseFailure } from "../live/bind.js";
28
+ import { ACCESS_ENTITY_TYPES, ACCESS_UNMANAGED_DETAIL, accessOf, readLiveAccess, readLiveSchema, SYSTEM_DATABASES } from "../live/catalog.js";
29
+ import { clickhouseQuery } from "../http.js";
30
+ import { sqlString } from "../apply/statements.js";
31
+ import { stripMarkerFromStatement } from "../ownership.js";
32
+ import { database, dictionary, func, policy, quota, role, settingsProfile, table, user, view, CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
33
+ import { canonicalObject, grantsObject, objectKey } from "./normalize.js";
34
+ import { grantStatement, grantsByGrantee, revokeStatement } from "../access.js";
35
+ import { renderFor } from "../topology.js";
36
+ // `concurrently` is how a Postgres index is created, not a property of it.
37
+ const PRUNED = new Set(["ddl", "source", "lineage", "reads", "to", "orReplace", "ifNotExists", "concurrently"]);
38
+ export const sqlDeepNormalizationHooks = {
39
+ prune: (node) => PRUNED.has(node.pattern.split(/[.[]/)[0]),
40
+ };
41
+ function liveProps(o) {
42
+ const tag = o.type === CLICKHOUSE_ENTITY_TYPES.database
43
+ ? database
44
+ : o.type === CLICKHOUSE_ENTITY_TYPES.table
45
+ ? table
46
+ : o.type === CLICKHOUSE_ENTITY_TYPES.dictionary
47
+ ? dictionary
48
+ : o.type === CLICKHOUSE_ENTITY_TYPES.function
49
+ ? func
50
+ : o.type === CLICKHOUSE_ENTITY_TYPES.user
51
+ ? user
52
+ : o.type === CLICKHOUSE_ENTITY_TYPES.role
53
+ ? role
54
+ : o.type === CLICKHOUSE_ENTITY_TYPES.rowPolicy
55
+ ? policy
56
+ : o.type === CLICKHOUSE_ENTITY_TYPES.quota
57
+ ? quota
58
+ : o.type === CLICKHOUSE_ENTITY_TYPES.settingsProfile
59
+ ? settingsProfile
60
+ : view;
61
+ const strings = Object.assign([o.statement], { raw: [o.statement] });
62
+ return { ...tag(strings).props };
63
+ }
64
+ const sameColumn = (a, b) => {
65
+ const strip = ({ text: _t, position: _p, previously: _v, ...rest }) => rest;
66
+ return JSON.stringify(strip(a)) === JSON.stringify(strip(b));
67
+ };
68
+ /** The live props, with each field the declaration means the same as written the declaration's way. */
69
+ export function inDeclaredVocabulary(declared, live, d, l) {
70
+ const out = { ...live };
71
+ const same = (k) => JSON.stringify(d[k]) === JSON.stringify(l[k]);
72
+ const adopt = (key, equal) => {
73
+ if (!equal)
74
+ return;
75
+ if (declared[key] === undefined)
76
+ delete out[key];
77
+ else
78
+ out[key] = declared[key];
79
+ };
80
+ adopt("name", d.name === l.name);
81
+ adopt("database", d.database === l.database);
82
+ adopt("engine", same("engine"));
83
+ for (const k of ["orderBy", "primaryKey", "partitionBy", "sampleBy", "ttl", "comment", "select", "refresh", "dataSource", "layout", "lifetime", "range", "asFunction"])
84
+ adopt(k, same(k));
85
+ adopt("settings", same("settings"));
86
+ // A view's DEFINER and SQL SECURITY are not compared by a plan; the server adds its own to a refreshable materialized view (#3911).
87
+ adopt("security", d.kind === "view" || d.kind === "materializedView");
88
+ // A user's, role's or row policy's fields, compared as `../access.ts` reads them.
89
+ if (d.kind === "user" || d.kind === "role" || d.kind === "rowPolicy") {
90
+ for (const k of ["identified", "host", "validUntil", "defaultRole", "defaultDatabase", "grantees", "settings", "as", "for", "using", "to"]) {
91
+ adopt(k, d.access?.[k] === l.access?.[k]);
92
+ }
93
+ adopt("table", d.table === l.table);
94
+ // A user's authentication is the environment's unless the declaration says it.
95
+ if (d.kind === "user" && declared.identified === undefined)
96
+ delete out.identified;
97
+ }
98
+ // A quota's or settings profile's fields; a quota's intervals are one prop, the same when every interval is.
99
+ if (d.kind === "quota" || d.kind === "settingsProfile") {
100
+ const fields = (o, pick) => JSON.stringify(Object.entries(o.access ?? {}).filter(([k]) => pick(k)));
101
+ for (const k of ["keyed", "settings", "to"])
102
+ adopt(k, d.access?.[k] === l.access?.[k]);
103
+ adopt("intervals", fields(d, (k) => k.startsWith("interval.")) === fields(l, (k) => k.startsWith("interval.")));
104
+ }
105
+ // A function's parameters and expression are one lambda: the same lambda is the declaration's words.
106
+ adopt("params", d.kind === "function" && same("lambda"));
107
+ adopt("body", d.kind === "function" && same("lambda"));
108
+ adopt("indexes", same("indexes"));
109
+ adopt("projections", same("projections"));
110
+ adopt("constraints", same("constraints"));
111
+ const declaredColumns = declared.columns ?? [];
112
+ if (d.kind === "view" || d.kind === "materializedView" || (d.kind === "table" && d.asFunction !== undefined && declaredColumns.length === 0)) {
113
+ // The server infers a view's columns, and a table function's (#3911); a declaration that lists none is not drift.
114
+ if (declaredColumns.length === 0)
115
+ out.columns = [];
116
+ }
117
+ else if (d.kind === "table" || d.kind === "dictionary") {
118
+ const liveColumns = live.columns ?? [];
119
+ out.columns = liveColumns.map((c, i) => {
120
+ const lc = l.columns[i];
121
+ const dc = d.columns.find((x) => x.name === c.name);
122
+ const di = dc ? declaredColumns[dc.position] : undefined;
123
+ return lc && dc && di && sameColumn(lc, dc) ? di : c;
124
+ });
125
+ }
126
+ return out;
127
+ }
128
+ /** The entity types that belong to no database. */
129
+ const GLOBAL_TYPES = new Set([
130
+ CLICKHOUSE_ENTITY_TYPES.database,
131
+ CLICKHOUSE_ENTITY_TYPES.function,
132
+ CLICKHOUSE_ENTITY_TYPES.user,
133
+ CLICKHOUSE_ENTITY_TYPES.role,
134
+ CLICKHOUSE_ENTITY_TYPES.quota,
135
+ CLICKHOUSE_ENTITY_TYPES.settingsProfile,
136
+ ]);
137
+ /** Whether the server's formatter prints two expressions the same. A formatter that cannot parse them says no. */
138
+ async function sameExpression(target, a, b) {
139
+ try {
140
+ const [row] = await clickhouseQuery(target.endpoint, `SELECT formatQuerySingleLine(${sqlString(`SELECT ${a}`)}) AS a, formatQuerySingleLine(${sqlString(`SELECT ${b}`)}) AS b`);
141
+ return row !== undefined && row.a === row.b;
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
147
+ /**
148
+ * On a `cluster:<name>` topology, read `system.tables` on every replica of
149
+ * every shard through the profile's server (`clusterAllReplicas`). A change
150
+ * made on one server without `ON CLUSTER` (a TTL altered on shard 2 alone)
151
+ * is then seen, which the profile's server alone would not show. Undefined
152
+ * for any other topology: there is one copy.
153
+ */
154
+ async function clusterCopies(target) {
155
+ if (target.topology?.kind !== "cluster")
156
+ return undefined;
157
+ const scope = target.databases
158
+ ? `IN (${target.databases.map(sqlString).join(", ")})`
159
+ : `NOT IN (${[...SYSTEM_DATABASES].map(sqlString).join(", ")})`;
160
+ const [self] = await clickhouseQuery(target.endpoint, "SELECT hostName() AS host");
161
+ const rows = await clickhouseQuery(target.endpoint, `SELECT hostName() AS host, database, name, create_table_query AS statement ` +
162
+ `FROM clusterAllReplicas(${sqlString(target.topology.cluster)}, system.tables) ` +
163
+ `WHERE database ${scope} AND NOT is_temporary AND name NOT LIKE '.inner%' AND engine != 'Dictionary' ORDER BY host, database, name`);
164
+ const byKey = new Map();
165
+ for (const r of rows) {
166
+ const key = `${r.database}.${r.name}`;
167
+ if (!byKey.has(key))
168
+ byKey.set(key, new Map());
169
+ byKey.get(key).set(r.host, stripMarkerFromStatement(r.statement));
170
+ }
171
+ return { self: self?.host ?? "", byKey };
172
+ }
173
+ /**
174
+ * When the servers' copies of `key` do not all read the same, the copy to
175
+ * report and its server: whichever departs furthest from the declaration
176
+ * `d`, the profile server's own winning a tie. A copy that differs from the
177
+ * declaration on any server is then the one the diff sees. Undefined when
178
+ * every copy reads the same.
179
+ */
180
+ function copyToReport(copies, key, d, defaultDatabase) {
181
+ const byHost = copies.byKey.get(key);
182
+ const mine = byHost?.get(copies.self);
183
+ if (!byHost || mine === undefined)
184
+ return undefined;
185
+ const shape = (c) => {
186
+ const { columns, ...rest } = c;
187
+ return JSON.stringify({ ...rest, columns: columns.map(({ text: _t, position: _p, ...col }) => col) });
188
+ };
189
+ const reference = shape(canonicalObject(mine, defaultDatabase));
190
+ const declared = JSON.parse(shape(d));
191
+ /** How many of the declaration's fields this copy holds differently. */
192
+ const departures = (statement) => {
193
+ const live = JSON.parse(shape(canonicalObject(statement, defaultDatabase)));
194
+ return Object.keys({ ...declared, ...live }).filter((k) => k !== "name" && k !== "database" && JSON.stringify(declared[k]) !== JSON.stringify(live[k])).length;
195
+ };
196
+ let pick;
197
+ for (const [host, statement] of byHost) {
198
+ if (host === copies.self || shape(canonicalObject(statement, defaultDatabase)) === reference)
199
+ continue;
200
+ const score = departures(statement);
201
+ if (!pick || score > pick.score)
202
+ pick = { host, statement, score };
203
+ }
204
+ if (!pick)
205
+ return undefined;
206
+ return pick.score > departures(mine) ? { host: pick.host, statement: pick.statement } : { host: copies.self, statement: mine };
207
+ }
208
+ export async function observeResourcesDeep(options) {
209
+ const resources = {};
210
+ const unobserved = {};
211
+ let target;
212
+ let live;
213
+ try {
214
+ target = await bindClickHouse(options);
215
+ live = [...(await readLiveSchema(target)), ...(target.access === true ? await readLiveAccess(target, accessOf(options.entities, target.defaultDatabase)) : [])];
216
+ }
217
+ catch (err) {
218
+ const why = classifyClickHouseFailure(err);
219
+ for (const name of options.entityNames) {
220
+ unobserved[name] = { type: options.entities.get(name)?.entityType ?? "", reason: why.reason, detail: why.detail };
221
+ }
222
+ return deepObservation({}, unobserved);
223
+ }
224
+ const byKey = new Map(live.map((o) => [objectKey(o), o]));
225
+ let copies;
226
+ try {
227
+ copies = await clusterCopies(target);
228
+ }
229
+ catch (err) {
230
+ // A cluster whose servers cannot all be read is not reported as matching.
231
+ const why = classifyClickHouseFailure(err);
232
+ for (const name of options.entityNames) {
233
+ unobserved[name] = { type: options.entities.get(name)?.entityType ?? "", reason: why.reason, detail: `reading every server of the cluster: ${why.detail}` };
234
+ }
235
+ return deepObservation({}, unobserved);
236
+ }
237
+ const pending = [];
238
+ if (target.access === true) {
239
+ try {
240
+ pending.push(...(await pendingGrants(target, options.entities)));
241
+ }
242
+ catch (err) {
243
+ const why = classifyClickHouseFailure(err);
244
+ for (const name of options.entityNames) {
245
+ if (options.entities.get(name)?.entityType !== CLICKHOUSE_ENTITY_TYPES.grant)
246
+ continue;
247
+ unobserved[name] = { type: CLICKHOUSE_ENTITY_TYPES.grant, reason: "read-failed", detail: `the server's grants could not be compared: ${why.detail}` };
248
+ }
249
+ }
250
+ }
251
+ for (const name of options.entityNames) {
252
+ const entity = options.entities.get(name);
253
+ if (!entity || !entity.entityType.startsWith("ClickHouse::")) {
254
+ unobserved[name] = { type: entity?.entityType ?? "", reason: "unsupported-kind" };
255
+ continue;
256
+ }
257
+ if (target.access !== true && ACCESS_ENTITY_TYPES.has(entity.entityType)) {
258
+ unobserved[name] = { type: entity.entityType, reason: "filtered", detail: ACCESS_UNMANAGED_DETAIL };
259
+ continue;
260
+ }
261
+ if (entity.entityType === CLICKHOUSE_ENTITY_TYPES.grant) {
262
+ if (unobserved[name])
263
+ continue;
264
+ unobserved[name] = {
265
+ type: entity.entityType,
266
+ reason: "unsupported-kind",
267
+ detail: "a grant is compared as part of its grantees' grants: what an apply would grant or revoke is listed as pending (SQLCH273, SQLCH274)",
268
+ };
269
+ continue;
270
+ }
271
+ const props = entity.props;
272
+ const isDb = GLOBAL_TYPES.has(entity.entityType);
273
+ const db = isDb ? undefined : typeof props.database === "string" ? props.database : target.defaultDatabase;
274
+ const o = byKey.get(objectKey({ type: entity.entityType, ...(db !== undefined ? { database: db } : {}), name: String(props.name), ...(typeof props.table === "string" ? { table: props.table } : {}) }));
275
+ if (!o)
276
+ continue;
277
+ try {
278
+ const d = canonicalObject(renderFor(String(props.ddl), target.topology), target.defaultDatabase);
279
+ const physicalId = o.uuid ?? `${db ? `${db}.` : ""}${o.name}`;
280
+ const other = copies && !isDb ? copyToReport(copies, `${db ?? ""}.${o.name}`, d, target.defaultDatabase) : undefined;
281
+ if (other) {
282
+ // The servers disagree: report the copy that departs from the declaration, and where it was seen.
283
+ const copy = { ...o, statement: other.host === copies.self ? o.statement : other.statement };
284
+ resources[name] = {
285
+ type: o.type,
286
+ physicalId,
287
+ properties: inDeclaredVocabulary(props, liveProps(copy), d, canonicalObject(copy.statement, target.defaultDatabase)),
288
+ observedOn: other.host,
289
+ };
290
+ continue;
291
+ }
292
+ let l = canonicalObject(o.statement, target.defaultDatabase);
293
+ // The server prints a function's expression its own way (`k*x + b` as `((k * x) + b)`): its formatter says whether the two are one.
294
+ if (d.kind === "function" && d.lambda !== l.lambda && (await sameExpression(target, d.lambda ?? "", l.lambda ?? "")))
295
+ l = { ...l, lambda: d.lambda };
296
+ // A row policy's condition, the same way.
297
+ const using = d.access?.using;
298
+ if (d.kind === "rowPolicy" && using !== undefined && using !== l.access?.using && (await sameExpression(target, using, l.access?.using ?? ""))) {
299
+ l = { ...l, access: { ...l.access, using } };
300
+ }
301
+ resources[name] = {
302
+ type: o.type,
303
+ physicalId,
304
+ properties: inDeclaredVocabulary(props, liveProps(o), d, l),
305
+ };
306
+ }
307
+ catch (err) {
308
+ unobserved[name] = { type: entity.entityType, reason: "read-failed", detail: `the server's definition does not parse: ${err.message}` };
309
+ }
310
+ }
311
+ return deepObservation(resources, unobserved, pending);
312
+ }
313
+ /**
314
+ * The grants an apply would give or take from each declared grantee (#3733),
315
+ * as `chant sql plan` compares them: the declared grants against what
316
+ * `SHOW GRANTS FOR` prints, atom by atom. One change per grantee, its
317
+ * REVOKEs before its GRANTs as the applier sends them, with the grant
318
+ * declarations behind it. A grantee the server does not hold yet is given
319
+ * every declared grant.
320
+ */
321
+ export async function pendingGrants(target, entities) {
322
+ const declared = grantsByGrantee([...entities].map(([name, e]) => ({ export: name, type: e.entityType, ddl: String(e.props.ddl ?? "") })));
323
+ if (declared.length === 0)
324
+ return [];
325
+ const live = await readLiveAccess(target, declared.map((g) => ({ kind: "grants", name: g.grantee })));
326
+ const held = new Map(live.filter((o) => o.type === CLICKHOUSE_ENTITY_TYPES.grant).map((o) => [o.name, grantsObject(o.statement, o.name, target.defaultDatabase).access ?? {}]));
327
+ const atom = (json) => JSON.parse(json);
328
+ const out = [];
329
+ for (const g of declared) {
330
+ const want = grantsObject(g.ddl, g.grantee, target.defaultDatabase).access ?? {};
331
+ const have = held.get(g.grantee) ?? {};
332
+ const statements = [
333
+ ...Object.keys(have)
334
+ .filter((k) => !(k in want))
335
+ .map((k) => revokeStatement(atom(have[k]), g.grantee)),
336
+ ...Object.keys(want)
337
+ .filter((k) => !(k in have))
338
+ .map((k) => grantStatement(atom(want[k]), g.grantee)),
339
+ ];
340
+ if (statements.length === 0)
341
+ continue;
342
+ out.push({ subject: `grants ${g.grantee}`, change: statements.join("; "), entities: [...g.exports].sort() });
343
+ }
344
+ return out;
345
+ }