@intentius/chant-lexicon-sql 0.124.19 → 0.124.22
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.
- package/dist/clickhouse/access.d.ts +36 -7
- package/dist/clickhouse/access.d.ts.map +1 -1
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +60 -0
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/import/generator.d.ts +1 -7
- package/dist/clickhouse/import/generator.d.ts.map +1 -1
- package/dist/clickhouse/live/catalog.d.ts +6 -5
- package/dist/clickhouse/live/catalog.d.ts.map +1 -1
- package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
- package/dist/clickhouse/parser.d.ts +16 -0
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +6 -3
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +3 -0
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse/topology.d.ts.map +1 -1
- package/dist/clickhouse.d.ts +1 -1
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/coverage/clickhouse.json +8 -8
- package/dist/coverage/postgres.json +6 -6
- package/dist/files/clickhouse.d.ts +2 -1
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/postgres.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +10 -10
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
- package/dist/lint/post-synth/sql101.d.ts.map +1 -1
- package/dist/lint/rules/sqlch001.d.ts +1 -1
- package/dist/lint/rules/sqlch001.d.ts.map +1 -1
- package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
- package/dist/lint/rules/templates.d.ts +1 -1
- package/dist/lint/rules/templates.d.ts.map +1 -1
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +40 -0
- package/dist/okf/index.md +9 -1
- package/dist/okf/rules/SQL101.md +1 -0
- package/dist/okf/rules/SQLCH001.md +2 -2
- package/dist/okf/rules/SQLPG113.md +2 -0
- package/dist/okf/types/PostgresAggregate.md +13 -0
- package/dist/okf/types/PostgresCast.md +9 -0
- package/dist/okf/types/PostgresCollation.md +9 -0
- package/dist/okf/types/PostgresCompositeType.md +13 -0
- package/dist/okf/types/PostgresEventTrigger.md +9 -0
- package/dist/okf/types/PostgresRangeType.md +13 -0
- package/dist/okf/types/Quota.md +9 -0
- package/dist/okf/types/SettingsProfile.md +9 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +4 -3
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +129 -6
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +6 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +3 -2
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/live/catalog-kinds.d.ts +28 -0
- package/dist/postgres/live/catalog-kinds.d.ts.map +1 -0
- package/dist/postgres/live/catalog.d.ts +15 -11
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +88 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +17 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +3 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +3 -2
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
- package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/postgres-names.ts +15 -2
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sql101.ts +1 -0
- package/dist/rules/sqlch001.ts +10 -3
- package/dist/rules/sqlpg001.ts +6 -21
- package/dist/rules/sqlpg113.ts +1 -1
- package/dist/rules/templates.ts +2 -2
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/access.ts +197 -22
- package/src/clickhouse/apply/statements.ts +17 -5
- package/src/clickhouse/entities.ts +78 -2
- package/src/clickhouse/import/generator.ts +16 -3
- package/src/clickhouse/live/catalog.ts +57 -10
- package/src/clickhouse/live/describe-resources.ts +3 -1
- package/src/clickhouse/live/kinds.e2e.test.ts +179 -0
- package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -4
- package/src/clickhouse/parser.test.ts +45 -2
- package/src/clickhouse/parser.ts +111 -3
- package/src/clickhouse/plan/deep.ts +25 -6
- package/src/clickhouse/plan/diff.ts +41 -1
- package/src/clickhouse/plan/normalize.ts +12 -4
- package/src/clickhouse/plan/rules.ts +22 -1
- package/src/clickhouse/quota-profile.test.ts +186 -0
- package/src/clickhouse/table-function.test.ts +54 -0
- package/src/clickhouse/testdata/server-ddl-baseline.json +17 -14
- package/src/clickhouse/topology.ts +4 -2
- package/src/clickhouse.ts +6 -0
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +8 -0
- package/src/core/undeclarable.test.ts +22 -19
- package/src/coverage/clickhouse.json +8 -8
- package/src/coverage/postgres.json +6 -6
- package/src/files/clickhouse.ts +14 -6
- package/src/files/postgres.ts +19 -5
- package/src/generated/lexicon-sql.json +40 -0
- package/src/index.ts +2 -2
- package/src/lint/post-synth/postgres-names.ts +15 -2
- package/src/lint/post-synth/sql101.ts +1 -0
- package/src/lint/post-synth/sqlpg113.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlch001.ts +10 -3
- package/src/lint/rules/sqlpg001.ts +6 -21
- package/src/lint/rules/templates.ts +2 -2
- package/src/lsp/postgres-template.ts +14 -0
- package/src/mcp/index.ts +5 -5
- package/src/mcp/postgres.ts +1 -1
- package/src/plugin.ts +7 -1
- package/src/postgres/apply/apply.ts +8 -2
- package/src/postgres/apply/statements.ts +29 -6
- package/src/postgres/entities.test.ts +2 -2
- package/src/postgres/entities.ts +316 -28
- package/src/postgres/entity-types.ts +6 -0
- package/src/postgres/import/generator.ts +31 -4
- package/src/postgres/kinds.test.ts +140 -0
- package/src/postgres/live/catalog-kinds.ts +312 -0
- package/src/postgres/live/catalog.ts +30 -12
- package/src/postgres/live/describe-resources.ts +14 -5
- package/src/postgres/live/kinds.e2e.test.ts +182 -0
- package/src/postgres/live/roundtrip.e2e.test.ts +9 -3
- package/src/postgres/live/undeclarable.e2e.test.ts +15 -13
- package/src/postgres/parser.test.ts +2 -1
- package/src/postgres/parser.ts +307 -17
- package/src/postgres/plan/deep.ts +33 -0
- package/src/postgres/plan/diff.ts +45 -0
- package/src/postgres/plan/normalize.ts +153 -2
- package/src/postgres/plan/rules.ts +21 -0
- package/src/postgres/plan/schema.ts +6 -4
- package/src/postgres/testing/roundtrip-schema.ts +19 -1
- package/src/postgres.ts +19 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres.md +7 -3
- package/src/skills/chant-sql.md +3 -1
- package/src/unknown-clauses.test.ts +2 -1
- package/src/validate.ts +8 -0
package/src/clickhouse/access.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ClickHouse access control as declarations (#3682): users, roles, row
|
|
3
|
-
* policies and grants.
|
|
3
|
+
* policies and grants, and quotas and settings profiles (#3911).
|
|
4
4
|
*
|
|
5
5
|
* Each is a statement the server keeps outside any table: `CREATE USER`,
|
|
6
|
-
* `CREATE ROLE`, `CREATE ROW POLICY
|
|
6
|
+
* `CREATE ROLE`, `CREATE ROW POLICY`, `CREATE QUOTA`,
|
|
7
|
+
* `CREATE SETTINGS PROFILE` and `GRANT`. This module parses them
|
|
7
8
|
* (the tags in `./entities.ts`, the lint rule, the plan), reduces each to
|
|
8
9
|
* the fields a plan compares, and writes the statements that make a change.
|
|
9
10
|
*
|
|
@@ -30,19 +31,24 @@ import { isTrivia, tokenizeText, type Token } from "./tokens";
|
|
|
30
31
|
import { kw, SqlCursor, type Span } from "../core/cursor";
|
|
31
32
|
import { unquote } from "./parser";
|
|
32
33
|
|
|
33
|
-
export type AccessStatement = "user" | "role" | "rowPolicy" | "grant";
|
|
34
|
+
export type AccessStatement = "user" | "role" | "rowPolicy" | "quota" | "settingsProfile" | "grant";
|
|
34
35
|
|
|
35
36
|
/** One parsed access statement. Clause spans are token spans, keywords left out. */
|
|
36
37
|
export interface AccessNode {
|
|
37
38
|
statement: AccessStatement;
|
|
38
39
|
orReplace: boolean;
|
|
39
40
|
ifNotExists: boolean;
|
|
40
|
-
/** A user's, role's
|
|
41
|
+
/** A user's, role's, row policy's, quota's or settings profile's name; none for a grant. */
|
|
41
42
|
name?: Span;
|
|
42
43
|
onCluster?: Span;
|
|
43
44
|
/** A row policy's table, `[db.]table`. */
|
|
44
45
|
table?: Span;
|
|
45
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Each clause after its keywords, by key: `identified`, `host`, `defaultRole`,
|
|
48
|
+
* `defaultDatabase`, `grantees`, `settings`, `validUntil`, `as`, `for`,
|
|
49
|
+
* `using`, `to`; a quota's `keyed`, `notKeyed` and `for` (every interval,
|
|
50
|
+
* `FOR` to `FOR`, as one span).
|
|
51
|
+
*/
|
|
46
52
|
clauses: Partial<Record<string, Span>>;
|
|
47
53
|
/** A grant's privileges or roles, one span each. */
|
|
48
54
|
items: Span[];
|
|
@@ -56,7 +62,9 @@ export interface AccessNode {
|
|
|
56
62
|
revoke?: boolean;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
/** A token that can stand where a name goes: a name may start with a digit (`02294_profile`). */
|
|
66
|
+
const isName = (t: Token | undefined): boolean =>
|
|
67
|
+
t !== undefined && (t.kind === "ident" || t.kind === "qident" || t.kind === "ref" || (t.kind === "number" && /[A-Za-z_]/.test(t.text)));
|
|
60
68
|
|
|
61
69
|
class AccessParser extends SqlCursor {
|
|
62
70
|
private name(what: string): Span {
|
|
@@ -108,18 +116,29 @@ class AccessParser extends SqlCursor {
|
|
|
108
116
|
return undefined;
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Clauses in any order: each starts with one of `starts` (a keyword
|
|
121
|
+
* sequence) and runs to the next clause. A key of `repeats` may follow
|
|
122
|
+
* itself (a quota's `FOR INTERVAL ..., FOR INTERVAL ...`): its span runs
|
|
123
|
+
* from the first to the last.
|
|
124
|
+
*/
|
|
125
|
+
private clauses(starts: Record<string, string[]>, node: AccessNode, repeats: readonly string[] = []): void {
|
|
113
126
|
const stopWords = [...new Set(Object.values(starts).map((w) => w[0]!))];
|
|
127
|
+
let last: string | undefined;
|
|
114
128
|
for (;;) {
|
|
115
129
|
if (this.atEnd()) return;
|
|
116
130
|
const hit = Object.entries(starts).find(([, words]) => words.every((w, i) => kw(this.peek(i), w)));
|
|
117
131
|
if (!hit) this.fail(`unexpected ${this.peek()?.text}: expected ${Object.values(starts).map((w) => w.join(" ")).join(", ")}`);
|
|
118
132
|
const [key, words] = hit;
|
|
133
|
+
const again = node.clauses[key];
|
|
134
|
+
if (again && !(repeats.includes(key) && last === key)) this.fail(`${words.join(" ")} is written twice`);
|
|
135
|
+
// A repeated clause keeps its keywords inside the span: `FOR INTERVAL 1 hour ..., FOR INTERVAL 1 day ...`.
|
|
136
|
+
const from = this.idx();
|
|
119
137
|
this.p += words.length;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
node.clauses[key] = key
|
|
138
|
+
// `NOT IDENTIFIED` and `NOT KEYED` have no value; every other clause has one.
|
|
139
|
+
const value = key === "notIdentified" || key === "notKeyed" ? { from: this.idx(-1), to: this.idx(-1) + 1, refs: [] } : this.expr(stopWords, false);
|
|
140
|
+
node.clauses[key] = repeats.includes(key) ? { from: again?.from ?? from, to: value.to, refs: [...(again?.refs ?? []), ...value.refs] } : value;
|
|
141
|
+
last = key;
|
|
123
142
|
}
|
|
124
143
|
}
|
|
125
144
|
|
|
@@ -160,8 +179,33 @@ class AccessParser extends SqlCursor {
|
|
|
160
179
|
this.clauses({ in: ["IN"], settings: ["SETTINGS"] }, n);
|
|
161
180
|
return n;
|
|
162
181
|
}
|
|
182
|
+
if (this.accept("QUOTA")) {
|
|
183
|
+
this.head();
|
|
184
|
+
const n = node("quota");
|
|
185
|
+
n.name = this.name("a quota name");
|
|
186
|
+
if (this.isPunct(",")) this.fail("declare one quota per template");
|
|
187
|
+
n.onCluster = this.onClusterClause();
|
|
188
|
+
this.clauses({ in: ["IN"], keyed: ["KEYED", "BY"], keyBy: ["KEY", "BY"], notKeyed: ["NOT", "KEYED"], for: ["FOR"], to: ["TO"] }, n, ["for"]);
|
|
189
|
+
// `KEY BY` is the server's other spelling of `KEYED BY`.
|
|
190
|
+
if (n.clauses.keyBy) {
|
|
191
|
+
if (n.clauses.keyed) this.fail("KEYED BY is written twice");
|
|
192
|
+
n.clauses.keyed = n.clauses.keyBy;
|
|
193
|
+
delete n.clauses.keyBy;
|
|
194
|
+
}
|
|
195
|
+
if (n.clauses.keyed && n.clauses.notKeyed) this.fail("KEYED BY and NOT KEYED are both written");
|
|
196
|
+
return n;
|
|
197
|
+
}
|
|
198
|
+
if (this.acceptSeq("SETTINGS", "PROFILE") || this.accept("PROFILE")) {
|
|
199
|
+
this.head();
|
|
200
|
+
const n = node("settingsProfile");
|
|
201
|
+
n.name = this.name("a settings profile name");
|
|
202
|
+
if (this.isPunct(",")) this.fail("declare one settings profile per template");
|
|
203
|
+
n.onCluster = this.onClusterClause();
|
|
204
|
+
this.clauses({ in: ["IN"], settings: ["SETTINGS"], to: ["TO"] }, n);
|
|
205
|
+
return n;
|
|
206
|
+
}
|
|
163
207
|
if (this.accept("ROW")) this.expect("POLICY");
|
|
164
|
-
else if (!this.accept("POLICY")) this.fail("expected USER, ROLE or
|
|
208
|
+
else if (!this.accept("POLICY")) this.fail("expected USER, ROLE, ROW POLICY, QUOTA or SETTINGS PROFILE");
|
|
165
209
|
this.head();
|
|
166
210
|
const n = node("rowPolicy");
|
|
167
211
|
n.name = this.name("a policy name");
|
|
@@ -240,7 +284,7 @@ export function isAccessStatement(ddl: string): boolean {
|
|
|
240
284
|
.map((w) => w.toUpperCase());
|
|
241
285
|
if (words[0] === "GRANT" || words[0] === "REVOKE") return true;
|
|
242
286
|
if (words[0] !== "CREATE") return false;
|
|
243
|
-
return
|
|
287
|
+
return ["USER", "ROLE", "ROW", "POLICY", "QUOTA", "PROFILE"].includes(words[1]!) || (words[1] === "SETTINGS" && words[2] === "PROFILE");
|
|
244
288
|
}
|
|
245
289
|
|
|
246
290
|
// ── canonical forms ───────────────────────────────────────────────────
|
|
@@ -284,9 +328,8 @@ function nameList(tokens: Token[], span: Span, keywords: string[]): string {
|
|
|
284
328
|
return [...keys, ...names].join(" , ");
|
|
285
329
|
}
|
|
286
330
|
|
|
287
|
-
/**
|
|
288
|
-
function
|
|
289
|
-
const sig = sigOf(tokens, span);
|
|
331
|
+
/** A span's significant tokens split at its top-level commas. */
|
|
332
|
+
function topLevelParts(sig: Token[]): Token[][] {
|
|
290
333
|
const parts: Token[][] = [[]];
|
|
291
334
|
let depth = 0;
|
|
292
335
|
for (const t of sig) {
|
|
@@ -295,14 +338,104 @@ function settingsList(tokens: Token[], span: Span): string {
|
|
|
295
338
|
if (depth === 0 && t.kind === "punct" && t.text === ",") parts.push([]);
|
|
296
339
|
else parts[parts.length - 1]!.push(t);
|
|
297
340
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
341
|
+
return parts.filter((p) => p.length > 0);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* `SETTINGS a = 1 READONLY, b MIN 0 MAX 2, PROFILE 'base'`: each setting,
|
|
346
|
+
* sorted by name, `READONLY` as the server prints it (`CONST`), and a
|
|
347
|
+
* profile inherited as the server prints it (`INHERIT base`).
|
|
348
|
+
*/
|
|
349
|
+
function settingsList(tokens: Token[], span: Span): string {
|
|
350
|
+
const words = new Set(["MIN", "MAX", "CONST", "READONLY", "WRITABLE", "CHANGEABLE_IN_READONLY", "PROFILE", "INHERIT", "NONE"]);
|
|
351
|
+
return topLevelParts(sigOf(tokens, span))
|
|
352
|
+
.map((p) => {
|
|
353
|
+
const head = p[0]!.text.toUpperCase();
|
|
354
|
+
if ((head === "PROFILE" || head === "INHERIT") && p.length === 2 && p[0]!.kind === "ident") return `INHERIT ${p[1]!.kind === "string" ? stringText(p[1]!.text) : unquote(p[1]!.text)}`;
|
|
355
|
+
// The first word is the setting's name (`readonly = 1`), never a key word.
|
|
356
|
+
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");
|
|
357
|
+
})
|
|
302
358
|
.sort()
|
|
303
359
|
.join(" , ");
|
|
304
360
|
}
|
|
305
361
|
|
|
362
|
+
/** A `'quoted'` string's value. */
|
|
363
|
+
const stringText = (s: string): string => s.slice(1, -1).replace(/''/g, "'").replace(/\\(.)/g, "$1");
|
|
364
|
+
|
|
365
|
+
/** The length of each interval unit a quota takes, in seconds, as the server counts it (a month is a twelfth of a year). */
|
|
366
|
+
export const QUOTA_INTERVAL_SECONDS: Readonly<Record<string, number>> = {
|
|
367
|
+
SECOND: 1,
|
|
368
|
+
MINUTE: 60,
|
|
369
|
+
HOUR: 3600,
|
|
370
|
+
DAY: 86400,
|
|
371
|
+
WEEK: 604800,
|
|
372
|
+
MONTH: 2629746,
|
|
373
|
+
QUARTER: 7889238,
|
|
374
|
+
YEAR: 31556952,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/** A limit's number as the server prints it: `1e9` is `1000000000`, `1000.0` is `1000`. */
|
|
378
|
+
function quotaNumber(text: string): string {
|
|
379
|
+
if (/^\d+$/.test(text)) return text.replace(/^0+(?=\d)/, "");
|
|
380
|
+
const n = Number(text);
|
|
381
|
+
return Number.isFinite(n) ? (Number.isSafeInteger(n) || !Number.isInteger(n) ? String(n) : BigInt(Math.round(n)).toString()) : text;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* A quota's intervals (`FOR [RANDOMIZED] INTERVAL n unit {MAX k = v, ... |
|
|
386
|
+
* NO LIMITS | TRACKING ONLY}`, each `FOR` to the next), keyed by the
|
|
387
|
+
* interval's length in seconds, each as the server keeps it: limits sorted
|
|
388
|
+
* by name, lower case, `MAX queries 5` as `queries = 5`. An interval with
|
|
389
|
+
* `NO LIMITS` is no interval; a second one of the same length replaces the
|
|
390
|
+
* first, as on the server.
|
|
391
|
+
*/
|
|
392
|
+
export function quotaIntervals(tokens: Token[], span: Span): Record<string, string> {
|
|
393
|
+
const sig = sigOf(tokens, span);
|
|
394
|
+
const groups: Token[][] = [];
|
|
395
|
+
for (const t of sig) {
|
|
396
|
+
if (t.kind === "ident" && t.text.toUpperCase() === "FOR") groups.push([]);
|
|
397
|
+
else if (groups.length > 0) groups[groups.length - 1]!.push(t);
|
|
398
|
+
}
|
|
399
|
+
const out: Record<string, string> = {};
|
|
400
|
+
for (const g of groups) {
|
|
401
|
+
const words = g.filter((t) => !(t.kind === "punct" && t.text === ","));
|
|
402
|
+
let i = 0;
|
|
403
|
+
const randomized = words[i]?.text.toUpperCase() === "RANDOMIZED";
|
|
404
|
+
if (randomized) i++;
|
|
405
|
+
if (words[i]?.text.toUpperCase() !== "INTERVAL") throw new Error(`a quota's FOR takes INTERVAL n unit: FOR ${g.map((t) => t.text).join(" ")}`);
|
|
406
|
+
const count = Number(words[i + 1]?.text);
|
|
407
|
+
const unit = QUOTA_INTERVAL_SECONDS[(words[i + 2]?.text ?? "").toUpperCase().replace(/S$/, "")];
|
|
408
|
+
if (!Number.isInteger(count) || unit === undefined) throw new Error(`a quota's interval is a whole number and a unit (second to year): FOR ${g.map((t) => t.text).join(" ")}`);
|
|
409
|
+
const rest = words.slice(i + 3);
|
|
410
|
+
const head = rest.map((t) => t.text.toUpperCase()).slice(0, 2).join(" ");
|
|
411
|
+
const seconds = String(count * unit);
|
|
412
|
+
if (head === "NO LIMITS") {
|
|
413
|
+
delete out[seconds];
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
let limits: string;
|
|
417
|
+
if (head === "TRACKING ONLY") limits = "TRACKING ONLY";
|
|
418
|
+
else {
|
|
419
|
+
if (rest[0]?.text.toUpperCase() !== "MAX") throw new Error(`a quota's interval takes MAX, NO LIMITS or TRACKING ONLY: FOR ${g.map((t) => t.text).join(" ")}`);
|
|
420
|
+
// After MAX: `name = value` or `name value`, separated by commas (already left out).
|
|
421
|
+
const items: string[] = [];
|
|
422
|
+
const after = rest.slice(1);
|
|
423
|
+
for (let k = 0; k < after.length; ) {
|
|
424
|
+
const name = after[k]!.text.toLowerCase();
|
|
425
|
+
k++;
|
|
426
|
+
if (after[k]?.text === "=" && after[k]!.kind !== "string") k++;
|
|
427
|
+
const value = after[k];
|
|
428
|
+
if (!value) throw new Error(`a quota's limit ${name} has no value`);
|
|
429
|
+
items.push(`${name} = ${quotaNumber(value.text)}`);
|
|
430
|
+
k++;
|
|
431
|
+
}
|
|
432
|
+
limits = `MAX ${items.sort().join(" , ")}`;
|
|
433
|
+
}
|
|
434
|
+
out[seconds] = `${randomized ? "RANDOMIZED " : ""}${limits}`;
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
|
|
306
439
|
/** `HOST LOCAL, IP '10.0.0.0/8'`: each host, sorted, the kind of each upper case. */
|
|
307
440
|
function hostList(tokens: Token[], span: Span): string {
|
|
308
441
|
const words = new Set(["LOCAL", "ANY", "NONE", "IP", "NAME", "REGEXP", "LIKE"]);
|
|
@@ -326,9 +459,9 @@ function qualifiedName(tokens: Token[], span: Span, defaultDatabase: string): {
|
|
|
326
459
|
return parts.length >= 2 ? { database: parts[parts.length - 2]!, name: parts[parts.length - 1]! } : { database: defaultDatabase, name: parts[0] ?? "" };
|
|
327
460
|
}
|
|
328
461
|
|
|
329
|
-
/** What a plan compares of a user, role or
|
|
462
|
+
/** What a plan compares of a user, role, row policy, quota or settings profile: one string per field, absent at its default. */
|
|
330
463
|
export interface AccessCanonical {
|
|
331
|
-
kind: "user" | "role" | "rowPolicy";
|
|
464
|
+
kind: "user" | "role" | "rowPolicy" | "quota" | "settingsProfile";
|
|
332
465
|
name: string;
|
|
333
466
|
/** A row policy's table's database. */
|
|
334
467
|
database?: string;
|
|
@@ -373,6 +506,21 @@ export function accessCanonical(ddl: string, defaultDatabase = "default"): Acces
|
|
|
373
506
|
put("settings", c.settings && settingsList(tokens, c.settings), "NONE");
|
|
374
507
|
return { kind: "role", name, fields };
|
|
375
508
|
}
|
|
509
|
+
if (node.statement === "settingsProfile") {
|
|
510
|
+
put("settings", c.settings && settingsList(tokens, c.settings), "NONE");
|
|
511
|
+
put("to", c.to && nameList(tokens, c.to, ["ALL", "NONE", "EXCEPT"]), "NONE");
|
|
512
|
+
return { kind: "settingsProfile", name, fields };
|
|
513
|
+
}
|
|
514
|
+
if (node.statement === "quota") {
|
|
515
|
+
// `KEYED BY 'user_name'`, `KEYED BY client_key, user_name`, `KEYED BY ip_address IPV6_PREFIX_BITS 64`: unquoted, lower case.
|
|
516
|
+
if (c.keyed) {
|
|
517
|
+
const keys = topLevelParts(sigOf(tokens, c.keyed)).map((p) => p.map((t) => (t.kind === "string" ? stringText(t.text) : unquote(t.text)).toLowerCase()).join(" "));
|
|
518
|
+
put("keyed", keys.join(" , "));
|
|
519
|
+
}
|
|
520
|
+
if (c.for) for (const [seconds, limits] of Object.entries(quotaIntervals(tokens, c.for))) fields[`interval.${seconds}`] = limits;
|
|
521
|
+
put("to", c.to && nameList(tokens, c.to, ["ALL", "NONE", "EXCEPT"]), "NONE");
|
|
522
|
+
return { kind: "quota", name, fields };
|
|
523
|
+
}
|
|
376
524
|
const table = qualifiedName(tokens, node.table!, defaultDatabase);
|
|
377
525
|
put("as", c.as && text(tokens, c.as)!.toUpperCase(), "PERMISSIVE");
|
|
378
526
|
put("for", c.for && join(sigOf(tokens, c.for), (w) => w.toUpperCase()), "SELECT");
|
|
@@ -556,6 +704,33 @@ export function alterAccessField(kind: "user" | "role", name: string, field: str
|
|
|
556
704
|
}
|
|
557
705
|
}
|
|
558
706
|
|
|
707
|
+
/**
|
|
708
|
+
* The `ALTER` that sets one field of a quota or settings profile to its
|
|
709
|
+
* declared value, or back to the server's default when the declaration
|
|
710
|
+
* leaves it out. Never `CREATE ... OR REPLACE`: that makes a new object
|
|
711
|
+
* under the name, and a profile that inherits the old one, or a quota's
|
|
712
|
+
* usage, is cut off from it.
|
|
713
|
+
*
|
|
714
|
+
* A quota's interval is set whole (`ALTER QUOTA q FOR INTERVAL 3600 second
|
|
715
|
+
* MAX queries = 5` replaces that interval's limits), and taken away with
|
|
716
|
+
* `NO LIMITS`.
|
|
717
|
+
*/
|
|
718
|
+
export function alterQuotaOrProfileField(kind: "quota" | "settingsProfile", name: string, field: string, declared: AccessCanonical["fields"], declaredText: Partial<Record<string, string>>): string {
|
|
719
|
+
const head = `ALTER ${kind === "quota" ? "QUOTA" : "SETTINGS PROFILE"} ${ident(name)}`;
|
|
720
|
+
const interval = /^interval\.(\d+)$/.exec(field);
|
|
721
|
+
if (kind === "quota" && interval) {
|
|
722
|
+
const value = declared[field];
|
|
723
|
+
if (value === undefined) return `${head} FOR INTERVAL ${interval[1]} second NO LIMITS`;
|
|
724
|
+
const randomized = value.startsWith("RANDOMIZED ");
|
|
725
|
+
const limits = (randomized ? value.slice("RANDOMIZED ".length) : value).replace(/ , /g, ", ");
|
|
726
|
+
return `${head} FOR ${randomized ? "RANDOMIZED " : ""}INTERVAL ${interval[1]} second ${limits}`;
|
|
727
|
+
}
|
|
728
|
+
if (kind === "quota" && field === "keyed") return declaredText.keyed === undefined ? `${head} NOT KEYED` : `${head} KEYED BY ${declaredText.keyed}`;
|
|
729
|
+
if (field === "to") return `${head} TO ${declaredText.to ?? "NONE"}`;
|
|
730
|
+
if (kind === "settingsProfile" && field === "settings") return `${head} SETTINGS ${declaredText.settings ?? "NONE"}`;
|
|
731
|
+
throw new Error(`no ALTER for ${kind} field ${field}`);
|
|
732
|
+
}
|
|
733
|
+
|
|
559
734
|
/** A user's or role's clauses as written, by field, for the `ALTER` that sets one. */
|
|
560
735
|
export function accessClauseText(ddl: string): Partial<Record<string, string>> {
|
|
561
736
|
const tokens = tokenizeText(ddl, 0);
|
|
@@ -24,7 +24,7 @@ import { CLASSIFIER_RULES, type ChangeClass, type ClassifierRuleId } from "../pl
|
|
|
24
24
|
import { stampedComment } from "../ownership";
|
|
25
25
|
import { CLICKHOUSE_ENTITY_TYPES, type ClickHouseEntityType } from "../entities";
|
|
26
26
|
import { renderFor, type Topology } from "../topology";
|
|
27
|
-
import { accessClauseText, alterAccessField, grantStatement, revokeStatement, SECRETLESS, type GrantAtom } from "../access";
|
|
27
|
+
import { accessClauseText, alterAccessField, alterQuotaOrProfileField, grantStatement, revokeStatement, SECRETLESS, type GrantAtom } from "../access";
|
|
28
28
|
|
|
29
29
|
/** A backquoted identifier. */
|
|
30
30
|
export const ident = (name: string): string => `\`${name.replace(/`/g, "``")}\``;
|
|
@@ -143,7 +143,7 @@ export function createStatement(
|
|
|
143
143
|
} else {
|
|
144
144
|
sql = `${statementBody(tokens)} COMMENT ${literal}`;
|
|
145
145
|
}
|
|
146
|
-
if (opts.orReplace && (node.statement === "view" || node.statement === "dictionary") && !node.orReplace) {
|
|
146
|
+
if (opts.orReplace && (node.statement === "view" || node.statement === "dictionary" || node.statement === "table") && !node.orReplace) {
|
|
147
147
|
sql = sql.replace(/^(\s*(?:--[^\n]*\n\s*)*)CREATE\s+/i, "$1CREATE OR REPLACE ");
|
|
148
148
|
}
|
|
149
149
|
return sql;
|
|
@@ -407,7 +407,9 @@ export function alterSteps(obj: DeclaredObject, changes: readonly Change[], opts
|
|
|
407
407
|
if (obj.type === CLICKHOUSE_ENTITY_TYPES.dictionary && changes.some((c) => c.rule === "SQLCH245")) {
|
|
408
408
|
steps.push(stepFor(createStatement(obj, opts.marker, { orReplace: true }), "SQLCH245"));
|
|
409
409
|
}
|
|
410
|
-
|
|
410
|
+
// A table over a table function stores nothing: it is replaced whole (#3911), its comment and marker with it.
|
|
411
|
+
if (obj.type === CLICKHOUSE_ENTITY_TYPES.table && changes.some((c) => c.rule === "SQLCH219")) steps.push(stepFor(createStatement(obj, opts.marker, { orReplace: true }), "SQLCH219"));
|
|
412
|
+
else if (obj.type === CLICKHOUSE_ENTITY_TYPES.table) steps.push(...tableSteps(obj, changes, opts.live));
|
|
411
413
|
else if (obj.type === CLICKHOUSE_ENTITY_TYPES.view || obj.type === CLICKHOUSE_ENTITY_TYPES.materializedView) steps.push(...viewSteps(obj, changes, opts.marker));
|
|
412
414
|
return steps;
|
|
413
415
|
}
|
|
@@ -416,6 +418,7 @@ export function alterSteps(obj: DeclaredObject, changes: readonly Change[], opts
|
|
|
416
418
|
export function stepsSetComment(obj: DeclaredObject, changes: readonly Change[]): boolean {
|
|
417
419
|
return (
|
|
418
420
|
(obj.type === CLICKHOUSE_ENTITY_TYPES.view && changes.some((c) => c.rule === "SQLCH240")) ||
|
|
421
|
+
(obj.type === CLICKHOUSE_ENTITY_TYPES.table && changes.some((c) => c.rule === "SQLCH219")) ||
|
|
419
422
|
(obj.type === CLICKHOUSE_ENTITY_TYPES.dictionary && changes.some((c) => c.rule === "SQLCH245"))
|
|
420
423
|
);
|
|
421
424
|
}
|
|
@@ -509,17 +512,19 @@ export function planStatements(input: StatementPlanInput): StatementPlan {
|
|
|
509
512
|
};
|
|
510
513
|
}
|
|
511
514
|
|
|
512
|
-
const NEVER_DROPPED: ReadonlySet<string> = new Set(["function", "user", "role", "rowPolicy", "grants"]);
|
|
515
|
+
const NEVER_DROPPED: ReadonlySet<string> = new Set(["function", "user", "role", "rowPolicy", "quota", "settingsProfile", "grants"]);
|
|
513
516
|
|
|
514
517
|
/** The entity types of access control (`../access.ts`): no comment, no marker, never dropped. */
|
|
515
518
|
export const ACCESS_TYPES: ReadonlySet<string> = new Set([
|
|
516
519
|
CLICKHOUSE_ENTITY_TYPES.user,
|
|
517
520
|
CLICKHOUSE_ENTITY_TYPES.role,
|
|
518
521
|
CLICKHOUSE_ENTITY_TYPES.rowPolicy,
|
|
522
|
+
CLICKHOUSE_ENTITY_TYPES.quota,
|
|
523
|
+
CLICKHOUSE_ENTITY_TYPES.settingsProfile,
|
|
519
524
|
CLICKHOUSE_ENTITY_TYPES.grant,
|
|
520
525
|
]);
|
|
521
526
|
|
|
522
|
-
/** What a user, role, row policy or grantee's grants take (`../access.ts`). */
|
|
527
|
+
/** What a user, role, row policy, quota, settings profile or grantee's grants take (`../access.ts`). */
|
|
523
528
|
function accessStatements(obj: DeclaredObject, mine: Change[], live: CanonicalObject | undefined, deferred: DeferredDefaultRoles, environmentCreatesUsers: boolean): ObjectStatements {
|
|
524
529
|
const kind = obj.canonical.kind;
|
|
525
530
|
const name = obj.canonical.name;
|
|
@@ -567,6 +572,13 @@ function accessStatements(obj: DeclaredObject, mine: Change[], live: CanonicalOb
|
|
|
567
572
|
: body.replace(/^(\s*(?:--[^\n]*\n\s*)*CREATE\s+(?:ROW\s+)?POLICY)\s+(?:IF\s+NOT\s+EXISTS\s+)?/i, "$1 OR REPLACE ");
|
|
568
573
|
return { verdict: "alter", obj, changes: mine, steps: mine.some((c) => c.rule === "SQLCH272") ? [stepFor(replace, "SQLCH272")] : [] };
|
|
569
574
|
}
|
|
575
|
+
if (kind === "quota" || kind === "settingsProfile") {
|
|
576
|
+
// ALTER, never OR REPLACE: a replaced profile is a new one, and what inherited the old one loses it.
|
|
577
|
+
const clauses = accessClauseText(obj.ddl);
|
|
578
|
+
const rule = kind === "quota" ? "SQLCH275" : "SQLCH276";
|
|
579
|
+
const steps = mine.filter((c) => c.rule === rule).map((c) => stepFor(alterQuotaOrProfileField(kind, name, c.field, obj.canonical.access ?? {}, clauses), rule));
|
|
580
|
+
return { verdict: "alter", obj, changes: mine, steps };
|
|
581
|
+
}
|
|
570
582
|
const clauses = accessClauseText(obj.ddl);
|
|
571
583
|
const rule = kind === "user" ? "SQLCH271" : "SQLCH270";
|
|
572
584
|
const steps = mine
|
|
@@ -65,6 +65,8 @@ export const CLICKHOUSE_ENTITY_TYPES = {
|
|
|
65
65
|
role: "ClickHouse::Role",
|
|
66
66
|
rowPolicy: "ClickHouse::RowPolicy",
|
|
67
67
|
grant: "ClickHouse::Grant",
|
|
68
|
+
quota: "ClickHouse::Quota",
|
|
69
|
+
settingsProfile: "ClickHouse::SettingsProfile",
|
|
68
70
|
} as const;
|
|
69
71
|
|
|
70
72
|
export type ClickHouseEntityType = (typeof CLICKHOUSE_ENTITY_TYPES)[keyof typeof CLICKHOUSE_ENTITY_TYPES];
|
|
@@ -128,6 +130,12 @@ export interface TableProps extends CommonProps, StorageProps {
|
|
|
128
130
|
indexes: Array<{ name: string; expr: string; type: string; granularity?: string }>;
|
|
129
131
|
projections: Array<{ name: string; definition: string }>;
|
|
130
132
|
constraints: Array<{ name: string; kind: "CHECK" | "ASSUME"; expr: string }>;
|
|
133
|
+
/**
|
|
134
|
+
* For a table over a table function (#3911), the call after `AS`:
|
|
135
|
+
* `numbers(10)`, `remote('host', db, t)`. Such a table has no engine and
|
|
136
|
+
* stores nothing; its columns may be left to the server to infer.
|
|
137
|
+
*/
|
|
138
|
+
asFunction?: string;
|
|
131
139
|
orReplace?: boolean;
|
|
132
140
|
ifNotExists?: boolean;
|
|
133
141
|
}
|
|
@@ -315,6 +323,45 @@ export interface GrantProps {
|
|
|
315
323
|
source: { strings: string[] };
|
|
316
324
|
}
|
|
317
325
|
|
|
326
|
+
/** A quota (#3911): limits on what a user's queries use, per interval. */
|
|
327
|
+
export interface QuotaProps {
|
|
328
|
+
name: string;
|
|
329
|
+
onCluster?: string;
|
|
330
|
+
/** `KEYED BY ...` after the keywords: `user_name`, `client_key, user_name`. Absent when not keyed. */
|
|
331
|
+
keyed?: string;
|
|
332
|
+
/** Every `FOR [RANDOMIZED] INTERVAL ...` clause, as written. */
|
|
333
|
+
intervals?: string;
|
|
334
|
+
/** `TO ...` after the keyword: the users and roles it applies to. */
|
|
335
|
+
to?: string;
|
|
336
|
+
orReplace?: boolean;
|
|
337
|
+
ifNotExists?: boolean;
|
|
338
|
+
ddl: string;
|
|
339
|
+
source: { strings: string[] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** A settings profile (#3911): settings and their constraints, for the users and roles it is given to. */
|
|
343
|
+
export interface SettingsProfileProps {
|
|
344
|
+
name: string;
|
|
345
|
+
onCluster?: string;
|
|
346
|
+
/** `SETTINGS ...` after the keyword, as written. */
|
|
347
|
+
settings?: string;
|
|
348
|
+
/** `TO ...` after the keyword. */
|
|
349
|
+
to?: string;
|
|
350
|
+
orReplace?: boolean;
|
|
351
|
+
ifNotExists?: boolean;
|
|
352
|
+
ddl: string;
|
|
353
|
+
source: { strings: string[] };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface ClickHouseQuota extends ClickHouseObject {
|
|
357
|
+
readonly entityType: "ClickHouse::Quota";
|
|
358
|
+
readonly props: QuotaProps;
|
|
359
|
+
}
|
|
360
|
+
export interface ClickHouseSettingsProfile extends ClickHouseObject {
|
|
361
|
+
readonly entityType: "ClickHouse::SettingsProfile";
|
|
362
|
+
readonly props: SettingsProfileProps;
|
|
363
|
+
}
|
|
364
|
+
|
|
318
365
|
export interface ClickHouseUser extends ClickHouseObject {
|
|
319
366
|
readonly entityType: "ClickHouse::User";
|
|
320
367
|
readonly props: UserProps;
|
|
@@ -585,6 +632,7 @@ function build(tag: Expect, strings: TemplateStringsArray | readonly string[], v
|
|
|
585
632
|
),
|
|
586
633
|
projections: node.projections.map((p) => ({ name: p.name, definition: req(text(ctx, p.body, "projections")) })),
|
|
587
634
|
constraints: node.constraints.map((c) => ({ name: c.name, kind: c.kind, expr: req(text(ctx, c.expr, "constraints")) })),
|
|
635
|
+
asFunction: text(ctx, node.asFunction, "asFunction"),
|
|
588
636
|
...storageProps(ctx, node),
|
|
589
637
|
ddl,
|
|
590
638
|
source,
|
|
@@ -729,12 +777,14 @@ export function func(strings: TemplateStringsArray, ...values: unknown[]): Click
|
|
|
729
777
|
return build("func", strings, values) as ClickHouseFunction;
|
|
730
778
|
}
|
|
731
779
|
|
|
732
|
-
type AccessTag = "user" | "role" | "policy" | "grant";
|
|
733
|
-
const ACCESS_STATEMENT: Record<AccessTag, AccessNode["statement"]> = { user: "user", role: "role", policy: "rowPolicy", grant: "grant" };
|
|
780
|
+
type AccessTag = "user" | "role" | "policy" | "quota" | "settingsProfile" | "grant";
|
|
781
|
+
const ACCESS_STATEMENT: Record<AccessTag, AccessNode["statement"]> = { user: "user", role: "role", policy: "rowPolicy", quota: "quota", settingsProfile: "settingsProfile", grant: "grant" };
|
|
734
782
|
const ACCESS_HOLDS: Record<AccessNode["statement"], [string, AccessTag]> = {
|
|
735
783
|
user: ["CREATE USER", "user"],
|
|
736
784
|
role: ["CREATE ROLE", "role"],
|
|
737
785
|
rowPolicy: ["CREATE ROW POLICY", "policy"],
|
|
786
|
+
quota: ["CREATE QUOTA", "quota"],
|
|
787
|
+
settingsProfile: ["CREATE SETTINGS PROFILE", "settingsProfile"],
|
|
738
788
|
grant: ["GRANT", "grant"],
|
|
739
789
|
};
|
|
740
790
|
|
|
@@ -786,6 +836,14 @@ function buildAccess(tag: AccessTag, strings: TemplateStringsArray | readonly st
|
|
|
786
836
|
const props: RoleProps = strip({ ...common, settings: rendered(c.settings), ddl, source });
|
|
787
837
|
return done(makeEntity(CLICKHOUSE_ENTITY_TYPES.role, quoteIdentifier(name), props, undefined, dependsOn));
|
|
788
838
|
}
|
|
839
|
+
if (node.statement === "quota") {
|
|
840
|
+
const props: QuotaProps = strip({ ...common, keyed: rendered(c.keyed), intervals: rendered(c.for), to: rendered(c.to), ddl, source });
|
|
841
|
+
return done(makeEntity(CLICKHOUSE_ENTITY_TYPES.quota, quoteIdentifier(name), props, undefined, dependsOn));
|
|
842
|
+
}
|
|
843
|
+
if (node.statement === "settingsProfile") {
|
|
844
|
+
const props: SettingsProfileProps = strip({ ...common, settings: rendered(c.settings), to: rendered(c.to), ddl, source });
|
|
845
|
+
return done(makeEntity(CLICKHOUSE_ENTITY_TYPES.settingsProfile, quoteIdentifier(name), props, undefined, dependsOn));
|
|
846
|
+
}
|
|
789
847
|
if (node.statement === "rowPolicy") {
|
|
790
848
|
const { database, name: tableName } = qualified(ctx, node.table!);
|
|
791
849
|
const props: RowPolicyProps = strip({
|
|
@@ -837,6 +895,24 @@ export function policy(strings: TemplateStringsArray, ...values: unknown[]): Cli
|
|
|
837
895
|
return buildAccess("policy", strings, values) as ClickHouseRowPolicy;
|
|
838
896
|
}
|
|
839
897
|
|
|
898
|
+
/**
|
|
899
|
+
* `` quota`CREATE QUOTA ...` ``: one quota (#3911), its key, its intervals'
|
|
900
|
+
* limits and whom it applies to. A plan changes it with `ALTER QUOTA`, one
|
|
901
|
+
* clause or interval at a time, and never drops it.
|
|
902
|
+
*/
|
|
903
|
+
export function quota(strings: TemplateStringsArray, ...values: unknown[]): ClickHouseQuota {
|
|
904
|
+
return buildAccess("quota", strings, values) as ClickHouseQuota;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* `` settingsProfile`CREATE SETTINGS PROFILE ...` ``: one settings profile
|
|
909
|
+
* (#3911), its settings, their constraints and the profiles it inherits.
|
|
910
|
+
* Interpolate it where a user, a role or another profile names it.
|
|
911
|
+
*/
|
|
912
|
+
export function settingsProfile(strings: TemplateStringsArray, ...values: unknown[]): ClickHouseSettingsProfile {
|
|
913
|
+
return buildAccess("settingsProfile", strings, values) as ClickHouseSettingsProfile;
|
|
914
|
+
}
|
|
915
|
+
|
|
840
916
|
/**
|
|
841
917
|
* `` grant`GRANT ...` ``: privileges or roles granted to users or roles
|
|
842
918
|
* (#3682). Every grant declaration naming a grantee adds up to its complete
|
|
@@ -29,7 +29,7 @@ export interface TemplateBody {
|
|
|
29
29
|
refs: string[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export const CLICKHOUSE_TAG_OF: Readonly<Record<string, "database" | "table" | "view" | "dictionary" | "func" | "user" | "role" | "policy" | "grant">> = {
|
|
32
|
+
export const CLICKHOUSE_TAG_OF: Readonly<Record<string, "database" | "table" | "view" | "dictionary" | "func" | "user" | "role" | "policy" | "quota" | "settingsProfile" | "grant">> = {
|
|
33
33
|
[CLICKHOUSE_ENTITY_TYPES.database]: "database",
|
|
34
34
|
[CLICKHOUSE_ENTITY_TYPES.table]: "table",
|
|
35
35
|
[CLICKHOUSE_ENTITY_TYPES.view]: "view",
|
|
@@ -40,6 +40,8 @@ export const CLICKHOUSE_TAG_OF: Readonly<Record<string, "database" | "table" | "
|
|
|
40
40
|
[CLICKHOUSE_ENTITY_TYPES.user]: "user",
|
|
41
41
|
[CLICKHOUSE_ENTITY_TYPES.role]: "role",
|
|
42
42
|
[CLICKHOUSE_ENTITY_TYPES.rowPolicy]: "policy",
|
|
43
|
+
[CLICKHOUSE_ENTITY_TYPES.quota]: "quota",
|
|
44
|
+
[CLICKHOUSE_ENTITY_TYPES.settingsProfile]: "settingsProfile",
|
|
43
45
|
[CLICKHOUSE_ENTITY_TYPES.grant]: "grant",
|
|
44
46
|
};
|
|
45
47
|
|
|
@@ -129,13 +131,24 @@ export const bodyText = (body: TemplateBody): string => body.parts.map((p, i) =>
|
|
|
129
131
|
* name) interpolated, and the items in the order a file declares them: each
|
|
130
132
|
* after what it references.
|
|
131
133
|
*/
|
|
134
|
+
/** The kinds a `FROM`, `JOIN` or `TO` never names. */
|
|
135
|
+
const NOT_RELATIONS = new Set<string>([
|
|
136
|
+
CLICKHOUSE_ENTITY_TYPES.function,
|
|
137
|
+
CLICKHOUSE_ENTITY_TYPES.user,
|
|
138
|
+
CLICKHOUSE_ENTITY_TYPES.role,
|
|
139
|
+
CLICKHOUSE_ENTITY_TYPES.rowPolicy,
|
|
140
|
+
CLICKHOUSE_ENTITY_TYPES.quota,
|
|
141
|
+
CLICKHOUSE_ENTITY_TYPES.settingsProfile,
|
|
142
|
+
CLICKHOUSE_ENTITY_TYPES.grant,
|
|
143
|
+
]);
|
|
144
|
+
|
|
132
145
|
export function clickhouseTemplates(items: readonly Item[], targets: readonly Item[] = items): { order: Item[]; bodies: Map<string, TemplateBody> } {
|
|
133
146
|
const byQualified = new Map<string, Item>();
|
|
134
147
|
const dbExport = new Map<string, string>();
|
|
135
148
|
for (const it of targets) {
|
|
136
149
|
if (it.type === CLICKHOUSE_ENTITY_TYPES.database) dbExport.set(it.name, it.exportName);
|
|
137
|
-
// A function is called, never named after FROM, JOIN or TO;
|
|
138
|
-
else if (
|
|
150
|
+
// A function is called, never named after FROM, JOIN or TO; access objects are never named so at all.
|
|
151
|
+
else if (!NOT_RELATIONS.has(it.type)) byQualified.set(it.database ? `${it.database}.${it.name}` : it.name, it);
|
|
139
152
|
}
|
|
140
153
|
const bodies = new Map(items.map((it) => [it.exportName, templateBody(it, byQualified, dbExport)]));
|
|
141
154
|
// Declarations reference each other in one file, so each comes after what it references.
|