@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,722 @@
1
+ /**
2
+ * ClickHouse access control as declarations (#3682): users, roles, row
3
+ * policies and grants, and quotas and settings profiles (#3911).
4
+ *
5
+ * Each is a statement the server keeps outside any table: `CREATE USER`,
6
+ * `CREATE ROLE`, `CREATE ROW POLICY`, `CREATE QUOTA`,
7
+ * `CREATE SETTINGS PROFILE` and `GRANT`. This module parses them
8
+ * (the tags in `./entities.ts`, the lint rule, the plan), reduces each to
9
+ * the fields a plan compares, and writes the statements that make a change.
10
+ *
11
+ * What a declaration owns and what the environment owns:
12
+ *
13
+ * - A password is the environment's. `IDENTIFIED BY` is refused in a
14
+ * declaration (only `ssh_key BY KEY`, a public key, is taken), so no
15
+ * secret is ever in a build. A user declared with no `IDENTIFIED` clause
16
+ * is compared without it, and is not created by chant: create it with its
17
+ * password, and the next apply manages the rest of it. A user declared
18
+ * with a method that holds no secret (`ssl_certificate`, `ldap`,
19
+ * `kerberos`, `http`, `ssh_key`, `no_password`) is created and kept so.
20
+ * - Grants are compared per grantee, for each grantee the build declares a
21
+ * grant to: every grant declaration naming it adds up to its complete
22
+ * list, and a grant made by hand to it is revoked. A grant another one
23
+ * covers (`SELECT ON db.t` beside `SELECT ON db.*`) is folded away, as the
24
+ * server folds it.
25
+ * - None of these objects has a comment, so none carries chant's ownership
26
+ * marker. A plan reads only the ones the build declares; chant creates and
27
+ * changes them, and never drops one.
28
+ */
29
+ import { isTrivia, tokenizeText } from "./tokens.js";
30
+ import { kw, SqlCursor } from "../core/cursor.js";
31
+ import { unquote } from "./parser.js";
32
+ /** A token that can stand where a name goes: a name may start with a digit (`02294_profile`). */
33
+ const isName = (t) => t !== undefined && (t.kind === "ident" || t.kind === "qident" || t.kind === "ref" || (t.kind === "number" && /[A-Za-z_]/.test(t.text)));
34
+ class AccessParser extends SqlCursor {
35
+ constructor() {
36
+ super(...arguments);
37
+ this.orReplace = false;
38
+ this.ifNotExists = false;
39
+ }
40
+ name(what) {
41
+ const t = this.next();
42
+ if (!isName(t))
43
+ this.fail(`expected ${what}`, t);
44
+ const i = this.idx(-1);
45
+ return { from: i, to: i + 1, refs: t.kind === "ref" ? [t.part] : [] };
46
+ }
47
+ /** `name` or `db.name`, a `*` in either place. */
48
+ qualified() {
49
+ const from = this.idx();
50
+ const refs = [];
51
+ const one = () => {
52
+ const t = this.next();
53
+ if (!(isName(t) || (t.kind === "op" && t.text === "*")))
54
+ this.fail("expected a name", t);
55
+ if (t.kind === "ref")
56
+ refs.push(t.part);
57
+ };
58
+ one();
59
+ while (this.isPunct(".")) {
60
+ this.p++;
61
+ one();
62
+ }
63
+ return this.span(from, refs);
64
+ }
65
+ /** `IF NOT EXISTS` or `OR REPLACE`, which ClickHouse takes after USER, ROLE and ROW POLICY. */
66
+ head() {
67
+ if (kw(this.peek(), "IF") && kw(this.peek(1), "NOT") && kw(this.peek(2), "EXISTS")) {
68
+ this.p += 3;
69
+ this.ifNotExists = true;
70
+ }
71
+ else if (kw(this.peek(), "OR") && kw(this.peek(1), "REPLACE")) {
72
+ this.p += 2;
73
+ this.orReplace = true;
74
+ }
75
+ }
76
+ onClusterClause() {
77
+ if (kw(this.peek(), "ON") && kw(this.peek(1), "CLUSTER")) {
78
+ this.p += 2;
79
+ const from = this.idx();
80
+ const t = this.next();
81
+ if (!isName(t) && t.kind !== "string")
82
+ this.fail("expected a cluster name", t);
83
+ return this.span(from, t.kind === "ref" ? [t.part] : []);
84
+ }
85
+ return undefined;
86
+ }
87
+ /**
88
+ * Clauses in any order: each starts with one of `starts` (a keyword
89
+ * sequence) and runs to the next clause. A key of `repeats` may follow
90
+ * itself (a quota's `FOR INTERVAL ..., FOR INTERVAL ...`): its span runs
91
+ * from the first to the last.
92
+ */
93
+ clauses(starts, node, repeats = []) {
94
+ const stopWords = [...new Set(Object.values(starts).map((w) => w[0]))];
95
+ let last;
96
+ for (;;) {
97
+ if (this.atEnd())
98
+ return;
99
+ const hit = Object.entries(starts).find(([, words]) => words.every((w, i) => kw(this.peek(i), w)));
100
+ if (!hit)
101
+ this.fail(`unexpected ${this.peek()?.text}: expected ${Object.values(starts).map((w) => w.join(" ")).join(", ")}`);
102
+ const [key, words] = hit;
103
+ const again = node.clauses[key];
104
+ if (again && !(repeats.includes(key) && last === key))
105
+ this.fail(`${words.join(" ")} is written twice`);
106
+ // A repeated clause keeps its keywords inside the span: `FOR INTERVAL 1 hour ..., FOR INTERVAL 1 day ...`.
107
+ const from = this.idx();
108
+ this.p += words.length;
109
+ // `NOT IDENTIFIED` and `NOT KEYED` have no value; every other clause has one.
110
+ const value = key === "notIdentified" || key === "notKeyed" ? { from: this.idx(-1), to: this.idx(-1) + 1, refs: [] } : this.expr(stopWords, false);
111
+ node.clauses[key] = repeats.includes(key) ? { from: again?.from ?? from, to: value.to, refs: [...(again?.refs ?? []), ...value.refs] } : value;
112
+ last = key;
113
+ }
114
+ }
115
+ parse() {
116
+ if (this.accept("GRANT"))
117
+ return this.grant(false);
118
+ if (this.accept("REVOKE"))
119
+ return this.grant(true);
120
+ this.expect("CREATE");
121
+ const node = (statement) => ({ statement, orReplace: this.orReplace, ifNotExists: this.ifNotExists, clauses: {}, items: [], grantees: [] });
122
+ if (this.accept("USER")) {
123
+ this.head();
124
+ const n = node("user");
125
+ n.name = this.name("a user name");
126
+ if (this.isPunct(","))
127
+ this.fail("declare one user per template");
128
+ n.onCluster = this.onClusterClause();
129
+ this.clauses({
130
+ notIdentified: ["NOT", "IDENTIFIED"],
131
+ identified: ["IDENTIFIED"],
132
+ host: ["HOST"],
133
+ validUntil: ["VALID", "UNTIL"],
134
+ in: ["IN"],
135
+ defaultRole: ["DEFAULT", "ROLE"],
136
+ defaultDatabase: ["DEFAULT", "DATABASE"],
137
+ grantees: ["GRANTEES"],
138
+ settings: ["SETTINGS"],
139
+ }, n);
140
+ refusePassword(this, this.tokens, n);
141
+ return n;
142
+ }
143
+ if (this.accept("ROLE")) {
144
+ this.head();
145
+ const n = node("role");
146
+ n.name = this.name("a role name");
147
+ if (this.isPunct(","))
148
+ this.fail("declare one role per template");
149
+ n.onCluster = this.onClusterClause();
150
+ this.clauses({ in: ["IN"], settings: ["SETTINGS"] }, n);
151
+ return n;
152
+ }
153
+ if (this.accept("QUOTA")) {
154
+ this.head();
155
+ const n = node("quota");
156
+ n.name = this.name("a quota name");
157
+ if (this.isPunct(","))
158
+ this.fail("declare one quota per template");
159
+ n.onCluster = this.onClusterClause();
160
+ this.clauses({ in: ["IN"], keyed: ["KEYED", "BY"], keyBy: ["KEY", "BY"], notKeyed: ["NOT", "KEYED"], for: ["FOR"], to: ["TO"] }, n, ["for"]);
161
+ // `KEY BY` is the server's other spelling of `KEYED BY`.
162
+ if (n.clauses.keyBy) {
163
+ if (n.clauses.keyed)
164
+ this.fail("KEYED BY is written twice");
165
+ n.clauses.keyed = n.clauses.keyBy;
166
+ delete n.clauses.keyBy;
167
+ }
168
+ if (n.clauses.keyed && n.clauses.notKeyed)
169
+ this.fail("KEYED BY and NOT KEYED are both written");
170
+ return n;
171
+ }
172
+ if (this.acceptSeq("SETTINGS", "PROFILE") || this.accept("PROFILE")) {
173
+ this.head();
174
+ const n = node("settingsProfile");
175
+ n.name = this.name("a settings profile name");
176
+ if (this.isPunct(","))
177
+ this.fail("declare one settings profile per template");
178
+ n.onCluster = this.onClusterClause();
179
+ this.clauses({ in: ["IN"], settings: ["SETTINGS"], to: ["TO"] }, n);
180
+ return n;
181
+ }
182
+ if (this.accept("ROW"))
183
+ this.expect("POLICY");
184
+ else if (!this.accept("POLICY"))
185
+ this.fail("expected USER, ROLE, ROW POLICY, QUOTA or SETTINGS PROFILE");
186
+ this.head();
187
+ const n = node("rowPolicy");
188
+ n.name = this.name("a policy name");
189
+ n.onCluster = this.onClusterClause();
190
+ this.expect("ON");
191
+ n.table = this.qualified();
192
+ if (this.isPunct(","))
193
+ this.fail("declare one row policy, on one table, per template");
194
+ this.clauses({ as: ["AS"], for: ["FOR"], using: ["USING"], to: ["TO"] }, n);
195
+ if (!n.clauses.using)
196
+ this.fail("expected USING and the policy's condition");
197
+ return n;
198
+ }
199
+ grant(revoke) {
200
+ const n = { statement: "grant", orReplace: false, ifNotExists: false, clauses: {}, items: [], grantees: [], ...(revoke ? { revoke: true } : {}) };
201
+ n.onCluster = this.onClusterClause();
202
+ if (revoke && (this.acceptSeq("GRANT", "OPTION", "FOR") || this.acceptSeq("ADMIN", "OPTION", "FOR")))
203
+ this.fail("REVOKE ... OPTION FOR is not read");
204
+ if (kw(this.peek(), "CURRENT") && kw(this.peek(1), "GRANTS"))
205
+ this.fail("GRANT CURRENT GRANTS grants whatever the applying user holds; name the privileges");
206
+ const to = revoke ? "FROM" : "TO";
207
+ for (;;) {
208
+ n.items.push(this.expr(["ON", to]));
209
+ if (this.acceptPunct(","))
210
+ continue;
211
+ break;
212
+ }
213
+ if (this.accept("ON"))
214
+ n.target = this.qualified();
215
+ this.expect(to);
216
+ for (;;) {
217
+ n.grantees.push(this.expr(["WITH"]));
218
+ if (this.acceptPunct(","))
219
+ continue;
220
+ break;
221
+ }
222
+ if (this.accept("WITH")) {
223
+ if (this.acceptSeq("GRANT", "OPTION"))
224
+ n.withOption = "GRANT";
225
+ else if (this.acceptSeq("ADMIN", "OPTION"))
226
+ n.withOption = "ADMIN";
227
+ else if (kw(this.peek(), "REPLACE"))
228
+ this.fail("WITH REPLACE OPTION revokes whatever else the grantee holds; declare the complete list instead");
229
+ else
230
+ this.fail("expected GRANT OPTION or ADMIN OPTION");
231
+ }
232
+ if (!n.target && n.withOption === "GRANT")
233
+ this.fail("WITH GRANT OPTION is for privileges; a role takes WITH ADMIN OPTION");
234
+ this.acceptPunct(";");
235
+ if (this.peek() !== undefined)
236
+ this.fail("unexpected token after the grant");
237
+ return n;
238
+ }
239
+ }
240
+ /** The authentication methods that hold no secret: a user declared with one is created by chant. */
241
+ export const SECRETLESS = new Set(["no_password", "ssl_certificate", "ldap", "kerberos", "http", "ssh_key"]);
242
+ function refusePassword(p, tokens, node) {
243
+ const span = node.clauses.identified;
244
+ if (!span)
245
+ return;
246
+ const sig = tokens.slice(span.from, span.to).filter((t) => !isTrivia(t));
247
+ const words = sig.map((t) => t.text.toLowerCase());
248
+ const method = words[0] === "with" ? words[1] : undefined;
249
+ // `SHOW CREATE USER` prints a password's method without the password (`IDENTIFIED WITH sha256_password`): that much is read.
250
+ const secret = words.includes("by") || words.includes("hash");
251
+ if (secret && method !== "ssh_key") {
252
+ p.fail(method === undefined
253
+ ? "IDENTIFIED BY holds a password, which is the environment's: declare the user without IDENTIFIED, create it with its password, and chant manages the rest"
254
+ : `IDENTIFIED WITH ${method} holds a password or its hash, which is the environment's: declare the user without IDENTIFIED, create it with its password, and chant manages the rest`, sig[0]);
255
+ }
256
+ }
257
+ /** Parse one access statement. Throws `SqlSyntaxError` located in the statement. */
258
+ export function parseAccess(tokens) {
259
+ return new AccessParser(tokens).parse();
260
+ }
261
+ /** Whether a statement's text is an access statement this module reads. */
262
+ export function isAccessStatement(ddl) {
263
+ const words = ddl
264
+ .replace(/--[^\n]*\n/g, " ")
265
+ .trim()
266
+ .split(/\s+/, 4)
267
+ .map((w) => w.toUpperCase());
268
+ if (words[0] === "GRANT" || words[0] === "REVOKE")
269
+ return true;
270
+ if (words[0] !== "CREATE")
271
+ return false;
272
+ return ["USER", "ROLE", "ROW", "POLICY", "QUOTA", "PROFILE"].includes(words[1]) || (words[1] === "SETTINGS" && words[2] === "PROFILE");
273
+ }
274
+ // ── canonical forms ───────────────────────────────────────────────────
275
+ const text = (tokens, span) => span && span.to > span.from
276
+ ? tokens
277
+ .slice(span.from, span.to)
278
+ .map((t) => t.text)
279
+ .join("")
280
+ .trim()
281
+ : undefined;
282
+ const sigOf = (tokens, span) => tokens.slice(span.from, span.to).filter((t) => !isTrivia(t));
283
+ /** Tokens joined by single spaces: identifiers unquoted, bare words as `word` says. */
284
+ function join(sig, word) {
285
+ return sig.map((t) => (t.kind === "qident" ? unquote(t.text) : t.kind === "ident" ? word(t.text) : t.text)).join(" ");
286
+ }
287
+ /** A comma-separated list of names, sorted, with the words of `keywords` upper case: `DEFAULT ROLE b, a` and `TO ALL EXCEPT r`. */
288
+ function nameList(tokens, span, keywords) {
289
+ const sig = sigOf(tokens, span);
290
+ const out = [];
291
+ let cur = [];
292
+ const flush = () => {
293
+ if (cur.length)
294
+ out.push(join(cur, (w) => (keywords.includes(w.toUpperCase()) ? w.toUpperCase() : w)));
295
+ cur = [];
296
+ };
297
+ for (const t of sig) {
298
+ if (t.kind === "punct" && t.text === ",")
299
+ flush();
300
+ else if (t.kind === "ident" && keywords.includes(t.text.toUpperCase()) && cur.length > 0 && !keywords.includes(cur[cur.length - 1].text.toUpperCase())) {
301
+ // `ALL EXCEPT a`: the keyword starts a new part.
302
+ flush();
303
+ cur.push(t);
304
+ }
305
+ else
306
+ cur.push(t);
307
+ }
308
+ flush();
309
+ const keys = out.filter((x) => keywords.includes(x.split(" ")[0]));
310
+ const names = out.filter((x) => !keywords.includes(x.split(" ")[0])).sort();
311
+ return [...keys, ...names].join(" , ");
312
+ }
313
+ /** A span's significant tokens split at its top-level commas. */
314
+ function topLevelParts(sig) {
315
+ const parts = [[]];
316
+ let depth = 0;
317
+ for (const t of sig) {
318
+ if (t.text === "(")
319
+ depth++;
320
+ if (t.text === ")")
321
+ depth--;
322
+ if (depth === 0 && t.kind === "punct" && t.text === ",")
323
+ parts.push([]);
324
+ else
325
+ parts[parts.length - 1].push(t);
326
+ }
327
+ return parts.filter((p) => p.length > 0);
328
+ }
329
+ /**
330
+ * `SETTINGS a = 1 READONLY, b MIN 0 MAX 2, PROFILE 'base'`: each setting,
331
+ * sorted by name, `READONLY` as the server prints it (`CONST`), and a
332
+ * profile inherited as the server prints it (`INHERIT base`).
333
+ */
334
+ function settingsList(tokens, span) {
335
+ const words = new Set(["MIN", "MAX", "CONST", "READONLY", "WRITABLE", "CHANGEABLE_IN_READONLY", "PROFILE", "INHERIT", "NONE"]);
336
+ return topLevelParts(sigOf(tokens, span))
337
+ .map((p) => {
338
+ const head = p[0].text.toUpperCase();
339
+ if ((head === "PROFILE" || head === "INHERIT") && p.length === 2 && p[0].kind === "ident")
340
+ return `INHERIT ${p[1].kind === "string" ? stringText(p[1].text) : unquote(p[1].text)}`;
341
+ // The first word is the setting's name (`readonly = 1`), never a key word.
342
+ return [join(p.slice(0, 1), (w) => w), join(p.slice(1), (w) => (words.has(w.toUpperCase()) ? w.toUpperCase() : w))].filter(Boolean).join(" ").replace(/\bREADONLY$/, "CONST");
343
+ })
344
+ .sort()
345
+ .join(" , ");
346
+ }
347
+ /** A `'quoted'` string's value. */
348
+ const stringText = (s) => s.slice(1, -1).replace(/''/g, "'").replace(/\\(.)/g, "$1");
349
+ /** The length of each interval unit a quota takes, in seconds, as the server counts it (a month is a twelfth of a year). */
350
+ export const QUOTA_INTERVAL_SECONDS = {
351
+ SECOND: 1,
352
+ MINUTE: 60,
353
+ HOUR: 3600,
354
+ DAY: 86400,
355
+ WEEK: 604800,
356
+ MONTH: 2629746,
357
+ QUARTER: 7889238,
358
+ YEAR: 31556952,
359
+ };
360
+ /** A limit's number as the server prints it: `1e9` is `1000000000`, `1000.0` is `1000`. */
361
+ function quotaNumber(text) {
362
+ if (/^\d+$/.test(text))
363
+ return text.replace(/^0+(?=\d)/, "");
364
+ const n = Number(text);
365
+ return Number.isFinite(n) ? (Number.isSafeInteger(n) || !Number.isInteger(n) ? String(n) : BigInt(Math.round(n)).toString()) : text;
366
+ }
367
+ /**
368
+ * A quota's intervals (`FOR [RANDOMIZED] INTERVAL n unit {MAX k = v, ... |
369
+ * NO LIMITS | TRACKING ONLY}`, each `FOR` to the next), keyed by the
370
+ * interval's length in seconds, each as the server keeps it: limits sorted
371
+ * by name, lower case, `MAX queries 5` as `queries = 5`. An interval with
372
+ * `NO LIMITS` is no interval; a second one of the same length replaces the
373
+ * first, as on the server.
374
+ */
375
+ export function quotaIntervals(tokens, span) {
376
+ const sig = sigOf(tokens, span);
377
+ const groups = [];
378
+ for (const t of sig) {
379
+ if (t.kind === "ident" && t.text.toUpperCase() === "FOR")
380
+ groups.push([]);
381
+ else if (groups.length > 0)
382
+ groups[groups.length - 1].push(t);
383
+ }
384
+ const out = {};
385
+ for (const g of groups) {
386
+ const words = g.filter((t) => !(t.kind === "punct" && t.text === ","));
387
+ let i = 0;
388
+ const randomized = words[i]?.text.toUpperCase() === "RANDOMIZED";
389
+ if (randomized)
390
+ i++;
391
+ if (words[i]?.text.toUpperCase() !== "INTERVAL")
392
+ throw new Error(`a quota's FOR takes INTERVAL n unit: FOR ${g.map((t) => t.text).join(" ")}`);
393
+ const count = Number(words[i + 1]?.text);
394
+ const unit = QUOTA_INTERVAL_SECONDS[(words[i + 2]?.text ?? "").toUpperCase().replace(/S$/, "")];
395
+ if (!Number.isInteger(count) || unit === undefined)
396
+ throw new Error(`a quota's interval is a whole number and a unit (second to year): FOR ${g.map((t) => t.text).join(" ")}`);
397
+ const rest = words.slice(i + 3);
398
+ const head = rest.map((t) => t.text.toUpperCase()).slice(0, 2).join(" ");
399
+ const seconds = String(count * unit);
400
+ if (head === "NO LIMITS") {
401
+ delete out[seconds];
402
+ continue;
403
+ }
404
+ let limits;
405
+ if (head === "TRACKING ONLY")
406
+ limits = "TRACKING ONLY";
407
+ else {
408
+ if (rest[0]?.text.toUpperCase() !== "MAX")
409
+ throw new Error(`a quota's interval takes MAX, NO LIMITS or TRACKING ONLY: FOR ${g.map((t) => t.text).join(" ")}`);
410
+ // After MAX: `name = value` or `name value`, separated by commas (already left out).
411
+ const items = [];
412
+ const after = rest.slice(1);
413
+ for (let k = 0; k < after.length;) {
414
+ const name = after[k].text.toLowerCase();
415
+ k++;
416
+ if (after[k]?.text === "=" && after[k].kind !== "string")
417
+ k++;
418
+ const value = after[k];
419
+ if (!value)
420
+ throw new Error(`a quota's limit ${name} has no value`);
421
+ items.push(`${name} = ${quotaNumber(value.text)}`);
422
+ k++;
423
+ }
424
+ limits = `MAX ${items.sort().join(" , ")}`;
425
+ }
426
+ out[seconds] = `${randomized ? "RANDOMIZED " : ""}${limits}`;
427
+ }
428
+ return out;
429
+ }
430
+ /** `HOST LOCAL, IP '10.0.0.0/8'`: each host, sorted, the kind of each upper case. */
431
+ function hostList(tokens, span) {
432
+ const words = new Set(["LOCAL", "ANY", "NONE", "IP", "NAME", "REGEXP", "LIKE"]);
433
+ const sig = sigOf(tokens, span);
434
+ const parts = [];
435
+ let cur = [];
436
+ for (const t of [...sig, undefined]) {
437
+ if (!t || (t.kind === "punct" && t.text === ",")) {
438
+ if (cur.length)
439
+ parts.push(join(cur, (w) => (words.has(w.toUpperCase()) ? w.toUpperCase() : w)));
440
+ cur = [];
441
+ }
442
+ else
443
+ cur.push(t);
444
+ }
445
+ return parts.sort().join(" , ");
446
+ }
447
+ /** `[db.]name` with a table's database filled in. */
448
+ function qualifiedName(tokens, span, defaultDatabase) {
449
+ const parts = sigOf(tokens, span)
450
+ .filter((t) => !(t.kind === "punct" && t.text === "."))
451
+ .map((t) => unquote(t.text));
452
+ return parts.length >= 2 ? { database: parts[parts.length - 2], name: parts[parts.length - 1] } : { database: defaultDatabase, name: parts[0] ?? "" };
453
+ }
454
+ /**
455
+ * A user, role or row policy as a plan compares it. A field at the server's
456
+ * default is left out, so a declaration that leaves out `HOST ANY` and the
457
+ * server that does not print it agree. A user's `IDENTIFIED` is kept only
458
+ * when written; a plan compares it only when the declaration has one.
459
+ */
460
+ export function accessCanonical(ddl, defaultDatabase = "default") {
461
+ const tokens = tokenizeText(ddl, 0);
462
+ const node = parseAccess(tokens);
463
+ if (node.statement === "grant")
464
+ throw new Error("a grant is compared as its grantee's grants (grantsCanonical)");
465
+ const name = unquote(text(tokens, node.name) ?? "");
466
+ const fields = {};
467
+ const put = (key, value, def) => {
468
+ if (value !== undefined && value !== "" && value !== def)
469
+ fields[key] = value;
470
+ };
471
+ const c = node.clauses;
472
+ if (node.statement === "user") {
473
+ if (c.notIdentified)
474
+ fields.identified = "no_password";
475
+ else if (c.identified) {
476
+ // `WITH method` lower case, as the server prints it; key words upper case; strings as written.
477
+ const sig = sigOf(tokens, c.identified);
478
+ const out = sig.map((t, i) => (t.kind === "ident" ? (i === 1 && sig[0].text.toUpperCase() === "WITH" ? t.text.toLowerCase() : t.text.toUpperCase()) : t.text));
479
+ fields.identified = out.join(" ").replace(/^WITH no_password$/, "no_password").replace(/^WITH /, "");
480
+ }
481
+ put("host", c.host && hostList(tokens, c.host), "ANY");
482
+ put("validUntil", text(tokens, c.validUntil));
483
+ put("defaultRole", c.defaultRole && nameList(tokens, c.defaultRole, ["ALL", "NONE", "EXCEPT"]), "ALL");
484
+ put("defaultDatabase", c.defaultDatabase && unquote(text(tokens, c.defaultDatabase) ?? ""), "NONE");
485
+ put("grantees", c.grantees && nameList(tokens, c.grantees, ["ANY", "NONE", "EXCEPT"]), "ANY");
486
+ put("settings", c.settings && settingsList(tokens, c.settings), "NONE");
487
+ return { kind: "user", name, fields };
488
+ }
489
+ if (node.statement === "role") {
490
+ put("settings", c.settings && settingsList(tokens, c.settings), "NONE");
491
+ return { kind: "role", name, fields };
492
+ }
493
+ if (node.statement === "settingsProfile") {
494
+ put("settings", c.settings && settingsList(tokens, c.settings), "NONE");
495
+ put("to", c.to && nameList(tokens, c.to, ["ALL", "NONE", "EXCEPT"]), "NONE");
496
+ return { kind: "settingsProfile", name, fields };
497
+ }
498
+ if (node.statement === "quota") {
499
+ // `KEYED BY 'user_name'`, `KEYED BY client_key, user_name`, `KEYED BY ip_address IPV6_PREFIX_BITS 64`: unquoted, lower case.
500
+ if (c.keyed) {
501
+ const keys = topLevelParts(sigOf(tokens, c.keyed)).map((p) => p.map((t) => (t.kind === "string" ? stringText(t.text) : unquote(t.text)).toLowerCase()).join(" "));
502
+ put("keyed", keys.join(" , "));
503
+ }
504
+ if (c.for)
505
+ for (const [seconds, limits] of Object.entries(quotaIntervals(tokens, c.for)))
506
+ fields[`interval.${seconds}`] = limits;
507
+ put("to", c.to && nameList(tokens, c.to, ["ALL", "NONE", "EXCEPT"]), "NONE");
508
+ return { kind: "quota", name, fields };
509
+ }
510
+ const table = qualifiedName(tokens, node.table, defaultDatabase);
511
+ put("as", c.as && text(tokens, c.as).toUpperCase(), "PERMISSIVE");
512
+ put("for", c.for && join(sigOf(tokens, c.for), (w) => w.toUpperCase()), "SELECT");
513
+ put("using", text(tokens, c.using));
514
+ put("to", c.to && nameList(tokens, c.to, ["ALL", "NONE", "EXCEPT"]), "NONE");
515
+ return { kind: "rowPolicy", name, database: table.database, table: table.name, fields };
516
+ }
517
+ /** An atom's key, which is also how a plan prints it: `SELECT(a) ON shop.t`, `ROLE reader WITH ADMIN OPTION`. */
518
+ export function atomKey(a) {
519
+ if (a.role !== undefined)
520
+ return `ROLE ${a.role}${a.option ? " WITH ADMIN OPTION" : ""}`;
521
+ return `${a.revoke ? "REVOKE " : ""}${a.privilege}${a.column !== undefined ? `(${a.column})` : ""} ON ${a.target}${a.option ? " WITH GRANT OPTION" : ""}`;
522
+ }
523
+ function grantAtoms(ddl, defaultDatabase) {
524
+ const out = [];
525
+ for (const statement of splitLines(ddl)) {
526
+ const tokens = tokenizeText(statement, 0);
527
+ const node = parseAccess(tokens);
528
+ if (node.statement !== "grant")
529
+ throw new Error(`not a grant: ${statement}`);
530
+ const grantees = node.grantees.map((g) => unquote(text(tokens, g) ?? ""));
531
+ const atoms = [];
532
+ let target;
533
+ if (node.target) {
534
+ const parts = sigOf(tokens, node.target)
535
+ .filter((t) => !(t.kind === "punct" && t.text === "."))
536
+ .map((t) => unquote(t.text));
537
+ target = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0] === "*" ? "*" : `${defaultDatabase}.${parts[0]}`;
538
+ }
539
+ for (const item of node.items) {
540
+ const sig = sigOf(tokens, item);
541
+ if (!target) {
542
+ atoms.push({ role: unquote(text(tokens, item) ?? ""), ...(node.withOption === "ADMIN" ? { option: true } : {}) });
543
+ continue;
544
+ }
545
+ const open = sig.findIndex((t) => t.text === "(");
546
+ const words = (open < 0 ? sig : sig.slice(0, open)).map((t) => t.text.toUpperCase()).join(" ");
547
+ const columns = open < 0
548
+ ? [undefined]
549
+ : sig
550
+ .slice(open + 1, -1)
551
+ .filter((t) => !(t.kind === "punct" && t.text === ","))
552
+ .map((t) => unquote(t.text));
553
+ for (const column of columns) {
554
+ atoms.push({ privilege: words, ...(column !== undefined ? { column } : {}), target, ...(node.withOption === "GRANT" ? { option: true } : {}), ...(node.revoke ? { revoke: true } : {}) });
555
+ }
556
+ }
557
+ out.push({ grantees, atoms });
558
+ }
559
+ return out;
560
+ }
561
+ /** Statements on their own lines, or separated by `;`, as `SHOW GRANTS` prints them and a group of declarations is joined. */
562
+ function splitLines(ddl) {
563
+ return ddl
564
+ .split(/;\s*\n|\n(?=\s*(?:GRANT|REVOKE)\b)/i)
565
+ .map((s) => s.trim().replace(/;$/, ""))
566
+ .filter((s) => s.length > 0 && !/^--/.test(s));
567
+ }
568
+ /** Whether `outer` covers `inner`'s target: the same table, its database's `*`, or `*.*`. */
569
+ function covers(outer, inner) {
570
+ if (outer === inner)
571
+ return true;
572
+ if (outer === "*.*")
573
+ return true;
574
+ const [db] = inner.split(".");
575
+ return outer === `${db}.*` && inner !== outer;
576
+ }
577
+ /**
578
+ * The atoms a grantee holds, with each atom another covers left out, as the
579
+ * server folds them: a column's privilege under the table's, a table's under
580
+ * its database's, any under `*.*`, the same privilege, with a grant option at
581
+ * least as wide.
582
+ */
583
+ export function foldAtoms(atoms) {
584
+ const keyed = new Map(atoms.map((a) => [atomKey(a), a]));
585
+ const all = [...keyed.values()];
586
+ return all.filter((a) => a.role !== undefined ||
587
+ a.revoke ||
588
+ !all.some((b) => b !== a &&
589
+ !b.revoke &&
590
+ b.role === undefined &&
591
+ b.privilege === a.privilege &&
592
+ b.column === undefined &&
593
+ (b.option ?? false) >= (a.option ?? false) &&
594
+ covers(b.target, a.target) &&
595
+ (a.column !== undefined || b.target !== a.target)));
596
+ }
597
+ /** A grantee's grants as a plan compares them: each atom's key, mapped to the atom. */
598
+ export function grantsCanonical(ddl, grantee, defaultDatabase = "default") {
599
+ const atoms = grantAtoms(ddl, defaultDatabase)
600
+ .filter((g) => g.grantees.includes(grantee))
601
+ .flatMap((g) => g.atoms);
602
+ return Object.fromEntries(foldAtoms(atoms)
603
+ .map((a) => [atomKey(a), JSON.stringify(a)])
604
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
605
+ }
606
+ /** Every grantee a grant statement names. */
607
+ export function granteesOf(ddl) {
608
+ return [...new Set(grantAtoms(ddl, "default").flatMap((g) => g.grantees))];
609
+ }
610
+ // ── statements ────────────────────────────────────────────────────────
611
+ const ident = (name) => `\`${name.replace(/`/g, "``")}\``;
612
+ const targetSql = (target) => target === "*" || target === "*.*"
613
+ ? target
614
+ : target
615
+ .split(".")
616
+ .map((p) => (p === "*" ? p : ident(p)))
617
+ .join(".");
618
+ /** The statement that gives `grantee` one atom. */
619
+ export function grantStatement(atom, grantee) {
620
+ if (atom.role !== undefined)
621
+ return `GRANT ${ident(atom.role)} TO ${ident(grantee)}${atom.option ? " WITH ADMIN OPTION" : ""}`;
622
+ const priv = `${atom.privilege}${atom.column !== undefined ? `(${ident(atom.column)})` : ""}`;
623
+ // Undoing a partial revoke grants the privilege back.
624
+ return `GRANT ${priv} ON ${targetSql(atom.target)} TO ${ident(grantee)}${atom.option && !atom.revoke ? " WITH GRANT OPTION" : ""}`;
625
+ }
626
+ /** The statement that takes one atom from `grantee`. */
627
+ export function revokeStatement(atom, grantee) {
628
+ if (atom.role !== undefined)
629
+ return `REVOKE ${ident(atom.role)} FROM ${ident(grantee)}`;
630
+ const priv = `${atom.privilege}${atom.column !== undefined ? `(${ident(atom.column)})` : ""}`;
631
+ return `REVOKE ${priv} ON ${targetSql(atom.target)} FROM ${ident(grantee)}`;
632
+ }
633
+ /**
634
+ * The `ALTER` that sets one field of a user or role to its declared value,
635
+ * or back to the server's default when the declaration leaves it out.
636
+ */
637
+ export function alterAccessField(kind, name, field, declared, declaredText) {
638
+ const head = `ALTER ${kind === "user" ? "USER" : "ROLE"} ${ident(name)}`;
639
+ const value = declaredText[field];
640
+ switch (field) {
641
+ case "identified":
642
+ return value === undefined || value === "NOT IDENTIFIED" ? `${head} NOT IDENTIFIED` : `${head} IDENTIFIED ${value}`;
643
+ case "host":
644
+ return `${head} HOST ${value ?? "ANY"}`;
645
+ case "validUntil":
646
+ return `${head} VALID UNTIL ${value ?? "'infinity'"}`;
647
+ case "defaultRole":
648
+ return `${head} DEFAULT ROLE ${value ?? "ALL"}`;
649
+ case "defaultDatabase":
650
+ return `${head} DEFAULT DATABASE ${value ?? "NONE"}`;
651
+ case "grantees":
652
+ return `${head} GRANTEES ${value ?? "ANY"}`;
653
+ case "settings":
654
+ return `${head} SETTINGS ${value ?? "NONE"}`;
655
+ default:
656
+ throw new Error(`no ALTER for ${kind} field ${field}`);
657
+ }
658
+ }
659
+ /**
660
+ * The `ALTER` that sets one field of a quota or settings profile to its
661
+ * declared value, or back to the server's default when the declaration
662
+ * leaves it out. Never `CREATE ... OR REPLACE`: that makes a new object
663
+ * under the name, and a profile that inherits the old one, or a quota's
664
+ * usage, is cut off from it.
665
+ *
666
+ * A quota's interval is set whole (`ALTER QUOTA q FOR INTERVAL 3600 second
667
+ * MAX queries = 5` replaces that interval's limits), and taken away with
668
+ * `NO LIMITS`.
669
+ */
670
+ export function alterQuotaOrProfileField(kind, name, field, declared, declaredText) {
671
+ const head = `ALTER ${kind === "quota" ? "QUOTA" : "SETTINGS PROFILE"} ${ident(name)}`;
672
+ const interval = /^interval\.(\d+)$/.exec(field);
673
+ if (kind === "quota" && interval) {
674
+ const value = declared[field];
675
+ if (value === undefined)
676
+ return `${head} FOR INTERVAL ${interval[1]} second NO LIMITS`;
677
+ const randomized = value.startsWith("RANDOMIZED ");
678
+ const limits = (randomized ? value.slice("RANDOMIZED ".length) : value).replace(/ , /g, ", ");
679
+ return `${head} FOR ${randomized ? "RANDOMIZED " : ""}INTERVAL ${interval[1]} second ${limits}`;
680
+ }
681
+ if (kind === "quota" && field === "keyed")
682
+ return declaredText.keyed === undefined ? `${head} NOT KEYED` : `${head} KEYED BY ${declaredText.keyed}`;
683
+ if (field === "to")
684
+ return `${head} TO ${declaredText.to ?? "NONE"}`;
685
+ if (kind === "settingsProfile" && field === "settings")
686
+ return `${head} SETTINGS ${declaredText.settings ?? "NONE"}`;
687
+ throw new Error(`no ALTER for ${kind} field ${field}`);
688
+ }
689
+ /** A user's or role's clauses as written, by field, for the `ALTER` that sets one. */
690
+ export function accessClauseText(ddl) {
691
+ const tokens = tokenizeText(ddl, 0);
692
+ const node = parseAccess(tokens);
693
+ const out = {};
694
+ for (const [key, span] of Object.entries(node.clauses)) {
695
+ if (!span)
696
+ continue;
697
+ out[key === "notIdentified" ? "identified" : key] = key === "notIdentified" ? "NOT IDENTIFIED" : text(tokens, span);
698
+ }
699
+ return out;
700
+ }
701
+ /**
702
+ * The grant declarations of a build, one entry per grantee they name: the
703
+ * export names that grant to it, their statements on their own lines, and
704
+ * what they depend on. A plan compares each entry with what the server
705
+ * holds for that grantee.
706
+ */
707
+ export function grantsByGrantee(objects) {
708
+ const by = new Map();
709
+ for (const o of objects) {
710
+ if (o.type !== "ClickHouse::Grant")
711
+ continue;
712
+ for (const grantee of granteesOf(o.ddl)) {
713
+ const entry = by.get(grantee) ?? { exports: [], statements: [], dependsOn: new Set() };
714
+ entry.exports.push(o.export);
715
+ entry.statements.push(o.ddl.trim().replace(/;$/, ""));
716
+ for (const d of o.dependsOn ?? [])
717
+ entry.dependsOn.add(d);
718
+ by.set(grantee, entry);
719
+ }
720
+ }
721
+ return [...by].map(([grantee, e]) => ({ grantee, exports: e.exports, ddl: e.statements.join(";\n"), dependsOn: [...e.dependsOn] }));
722
+ }