@intentius/chant-lexicon-sql 0.124.18 → 0.124.21
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/import/live-export.d.ts +8 -0
- package/dist/clickhouse/import/live-export.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/live/undeclarable.d.ts +32 -0
- package/dist/clickhouse/live/undeclarable.d.ts.map +1 -0
- 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/testing/fake-server.d.ts +6 -0
- package/dist/clickhouse/testing/fake-server.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/coverage-page.d.ts +7 -0
- package/dist/codegen/coverage-page.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/core/undeclarable.d.ts +38 -0
- package/dist/core/undeclarable.d.ts.map +1 -0
- package/dist/coverage/clickhouse-commands.json +24 -0
- package/dist/coverage/clickhouse.json +25 -0
- package/dist/coverage/postgres-commands.json +245 -0
- package/dist/coverage/postgres.json +51 -0
- package/dist/coverage.d.ts +68 -0
- package/dist/coverage.d.ts.map +1 -1
- package/dist/files/clickhouse.d.ts +2 -1
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/common.d.ts +12 -0
- package/dist/files/common.d.ts.map +1 -1
- package/dist/files/postgres.d.ts +1 -1
- package/dist/files/postgres.d.ts.map +1 -1
- package/dist/import-parser.d.ts +7 -2
- package/dist/import-parser.d.ts.map +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/import/live-export.d.ts +8 -0
- package/dist/postgres/import/live-export.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 +23 -8
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/live/undeclarable.d.ts +42 -0
- package/dist/postgres/live/undeclarable.d.ts.map +1 -0
- 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/fake-client.d.ts +6 -0
- package/dist/postgres/testing/fake-client.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/import/live-export.ts +17 -2
- 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 +65 -0
- package/src/clickhouse/live/undeclarable.ts +66 -0
- 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/testing/fake-server.ts +5 -0
- package/src/clickhouse/topology.ts +4 -2
- package/src/clickhouse.ts +6 -0
- package/src/codegen/classifier-page.test.ts +6 -0
- package/src/codegen/coverage-page.ts +77 -0
- package/src/codegen/docs.ts +5 -0
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +8 -0
- package/src/core/undeclarable.test.ts +141 -0
- package/src/core/undeclarable.ts +66 -0
- package/src/coverage/clickhouse-commands.json +24 -0
- package/src/coverage/clickhouse.json +25 -0
- package/src/coverage/postgres-commands.json +245 -0
- package/src/coverage/postgres.json +51 -0
- package/src/coverage.test.ts +78 -0
- package/src/coverage.ts +142 -0
- package/src/files/clickhouse.ts +20 -7
- package/src/files/common.ts +18 -0
- package/src/files/postgres.ts +25 -6
- package/src/generated/lexicon-sql.json +40 -0
- package/src/import-parser.ts +12 -4
- 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 +13 -2
- 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/import/live-export.ts +25 -2
- package/src/postgres/kinds.test.ts +140 -0
- package/src/postgres/live/catalog-kinds.ts +312 -0
- package/src/postgres/live/catalog.ts +33 -11
- 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 +83 -0
- package/src/postgres/live/undeclarable.ts +141 -0
- 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/fake-client.ts +5 -0
- 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
|
@@ -10,6 +10,8 @@ import type { ClickHouseTarget } from "./bind";
|
|
|
10
10
|
import { CLICKHOUSE_ENTITY_TYPES, type ClickHouseEntityType } from "../entities";
|
|
11
11
|
import { isChantWorkingObject, stripMarkerFromStatement } from "../ownership";
|
|
12
12
|
import { foreignTool } from "../../core/foreign-tables";
|
|
13
|
+
import { parseCreate, unquote } from "../parser";
|
|
14
|
+
import { tokenizeText } from "../tokens";
|
|
13
15
|
|
|
14
16
|
/** The server's own databases, never part of a declared schema. */
|
|
15
17
|
export const SYSTEM_DATABASES = new Set(["system", "information_schema", "INFORMATION_SCHEMA"]);
|
|
@@ -69,8 +71,10 @@ export async function readLiveSchema(target: ClickHouseTarget, opts: { withState
|
|
|
69
71
|
const databases = await q<{ name: string; engine: string; uuid: string; comment: string }>(
|
|
70
72
|
`SELECT name, engine, toString(uuid) AS uuid, comment FROM system.databases WHERE name ${scope} ORDER BY name`,
|
|
71
73
|
);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
// `system.tables` shows no comment for a table over a table function, whatever it was created with: its statement has it (#3911).
|
|
75
|
+
const tables = await q<{ database: string; name: string; engine: string; uuid: string; comment: string; dictionary: number; proxy: string }>(
|
|
76
|
+
`SELECT database, name, engine, toString(uuid) AS uuid, comment, startsWith(create_table_query, 'CREATE DICTIONARY') AS dictionary, ` +
|
|
77
|
+
`if(comment = '' AND position(create_table_query, ' COMMENT ') > 0, create_table_query, '') AS proxy FROM system.tables ` +
|
|
74
78
|
`WHERE database ${scope} AND NOT is_temporary AND name NOT LIKE '.inner%' ` +
|
|
75
79
|
`ORDER BY database, name`,
|
|
76
80
|
);
|
|
@@ -94,6 +98,7 @@ export async function readLiveSchema(target: ClickHouseTarget, opts: { withState
|
|
|
94
98
|
});
|
|
95
99
|
}
|
|
96
100
|
for (const t of tables) {
|
|
101
|
+
if (t.proxy) t.comment = commentOf(t.proxy) ?? t.comment;
|
|
97
102
|
if (working.has(t.database) || isChantWorkingObject(t.comment)) continue;
|
|
98
103
|
const type = Number(t.dictionary) === 1 ? CLICKHOUSE_ENTITY_TYPES.dictionary : entityTypeOfEngine(t.engine);
|
|
99
104
|
const foreign = type === CLICKHOUSE_ENTITY_TYPES.table ? foreignTool(t.name) : undefined;
|
|
@@ -119,6 +124,23 @@ export async function readLiveSchema(target: ClickHouseTarget, opts: { withState
|
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
|
|
127
|
+
/** A `CREATE` statement's own `COMMENT`, its value unquoted; undefined when it has none or does not parse. */
|
|
128
|
+
function commentOf(statement: string): string | undefined {
|
|
129
|
+
try {
|
|
130
|
+
const tokens = tokenizeText(statement, 0);
|
|
131
|
+
const node = parseCreate(tokens);
|
|
132
|
+
if (!node.comment) return undefined;
|
|
133
|
+
const literal = tokens
|
|
134
|
+
.slice(node.comment.from, node.comment.to)
|
|
135
|
+
.map((t) => t.text)
|
|
136
|
+
.join("")
|
|
137
|
+
.trim();
|
|
138
|
+
return /^'.*'$/s.test(literal) ? literal.slice(1, -1).replace(/\\(.)/g, "$1").replace(/''/g, "'") : unquote(literal);
|
|
139
|
+
} catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
/** The access objects a build declares, by kind and name: what `readLiveAccess` reads. */
|
|
123
145
|
export interface DeclaredAccess {
|
|
124
146
|
kind: string;
|
|
@@ -128,9 +150,10 @@ export interface DeclaredAccess {
|
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
/**
|
|
131
|
-
* The users, roles, row policies and grantees'
|
|
132
|
-
* as the server holds them (#3682):
|
|
133
|
-
* `SHOW CREATE
|
|
153
|
+
* The users, roles, row policies, quotas, settings profiles and grantees'
|
|
154
|
+
* grants the build declares, as the server holds them (#3682, #3911):
|
|
155
|
+
* `SHOW CREATE USER`, `SHOW CREATE ROLE`, `SHOW CREATE ROW POLICY`,
|
|
156
|
+
* `SHOW CREATE QUOTA`, `SHOW CREATE SETTINGS PROFILE`, and for a grantee every line of
|
|
134
157
|
* `SHOW GRANTS FOR`, one per line. Nothing else is read, so a project that
|
|
135
158
|
* declares none needs no access to `system.users` and the like, and an
|
|
136
159
|
* object made by hand is never seen. One that does not exist is left out.
|
|
@@ -155,6 +178,17 @@ export async function readLiveAccess(target: ClickHouseTarget, declared: readonl
|
|
|
155
178
|
for (const name of roles.filter((r) => existingRoles.has(r))) {
|
|
156
179
|
out.push({ type: CLICKHOUSE_ENTITY_TYPES.role, name, engine: "", statement: await first(`SHOW CREATE ROLE ${ident(name)}`) });
|
|
157
180
|
}
|
|
181
|
+
for (const [kind, type, table, show] of [
|
|
182
|
+
["quota", CLICKHOUSE_ENTITY_TYPES.quota, "system.quotas", "QUOTA"],
|
|
183
|
+
["settingsProfile", CLICKHOUSE_ENTITY_TYPES.settingsProfile, "system.settings_profiles", "SETTINGS PROFILE"],
|
|
184
|
+
] as const) {
|
|
185
|
+
const wanted = names(kind);
|
|
186
|
+
if (wanted.length === 0) continue;
|
|
187
|
+
const existing = new Set((await q<{ name: string }>(`SELECT name FROM ${table} WHERE name IN (${list(wanted)})`)).map((r) => r.name));
|
|
188
|
+
for (const name of wanted.filter((n) => existing.has(n))) {
|
|
189
|
+
out.push({ type, name, engine: "", statement: await first(`SHOW CREATE ${show} ${ident(name)}`) });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
158
192
|
const policies = declared.filter((d) => d.kind === "rowPolicy");
|
|
159
193
|
if (policies.length > 0) {
|
|
160
194
|
const live = await q<{ name: string; database: string; table: string }>(
|
|
@@ -180,13 +214,20 @@ export async function readLiveAccess(target: ClickHouseTarget, declared: readonl
|
|
|
180
214
|
}
|
|
181
215
|
|
|
182
216
|
/** The canonical kinds a profile's `access` switch decides (#3716). */
|
|
183
|
-
export const ACCESS_KINDS: ReadonlySet<string> = new Set(["user", "role", "rowPolicy", "grants"]);
|
|
217
|
+
export const ACCESS_KINDS: ReadonlySet<string> = new Set(["user", "role", "rowPolicy", "quota", "settingsProfile", "grants"]);
|
|
184
218
|
|
|
185
219
|
/** The entity types a profile's `access` switch decides (#3716). */
|
|
186
|
-
export const ACCESS_ENTITY_TYPES: ReadonlySet<string> = new Set([
|
|
220
|
+
export const ACCESS_ENTITY_TYPES: ReadonlySet<string> = new Set([
|
|
221
|
+
CLICKHOUSE_ENTITY_TYPES.user,
|
|
222
|
+
CLICKHOUSE_ENTITY_TYPES.role,
|
|
223
|
+
CLICKHOUSE_ENTITY_TYPES.rowPolicy,
|
|
224
|
+
CLICKHOUSE_ENTITY_TYPES.quota,
|
|
225
|
+
CLICKHOUSE_ENTITY_TYPES.settingsProfile,
|
|
226
|
+
CLICKHOUSE_ENTITY_TYPES.grant,
|
|
227
|
+
]);
|
|
187
228
|
|
|
188
229
|
/** Why an access declaration is not read, planned or applied where the profile does not manage access. */
|
|
189
|
-
export const ACCESS_UNMANAGED_DETAIL = "access is not managed in this environment: set sql.profiles.<env>.access to plan and apply users, roles, row policies and grants";
|
|
230
|
+
export const ACCESS_UNMANAGED_DETAIL = "access is not managed in this environment: set sql.profiles.<env>.access to plan and apply users, roles, row policies, quotas, settings profiles and grants";
|
|
190
231
|
|
|
191
232
|
/** The hint a plan gives for the access declarations it left out. */
|
|
192
233
|
export function accessUnmanagedHint(n: number, environment: string): string {
|
|
@@ -200,9 +241,15 @@ export function declaredAccess(objects: ReadonlyArray<{ kind: string; name: stri
|
|
|
200
241
|
.map((o) => ({ kind: o.kind, name: o.name, ...(o.database !== undefined ? { database: o.database } : {}), ...(o.table !== undefined ? { table: o.table } : {}) }));
|
|
201
242
|
}
|
|
202
243
|
|
|
203
|
-
/** The users, roles
|
|
244
|
+
/** The users, roles, row policies, quotas and settings profiles among declared entities, for `readLiveAccess`. */
|
|
204
245
|
export function accessOf(entities: ReadonlyMap<string, { entityType: string; props: Record<string, unknown> }>, defaultDatabase: string): DeclaredAccess[] {
|
|
205
|
-
const kinds: Record<string, string> = {
|
|
246
|
+
const kinds: Record<string, string> = {
|
|
247
|
+
[CLICKHOUSE_ENTITY_TYPES.user]: "user",
|
|
248
|
+
[CLICKHOUSE_ENTITY_TYPES.role]: "role",
|
|
249
|
+
[CLICKHOUSE_ENTITY_TYPES.rowPolicy]: "rowPolicy",
|
|
250
|
+
[CLICKHOUSE_ENTITY_TYPES.quota]: "quota",
|
|
251
|
+
[CLICKHOUSE_ENTITY_TYPES.settingsProfile]: "settingsProfile",
|
|
252
|
+
};
|
|
206
253
|
const out: DeclaredAccess[] = [];
|
|
207
254
|
for (const e of entities.values()) {
|
|
208
255
|
const kind = kinds[e.entityType];
|
|
@@ -42,7 +42,9 @@ export function declaredAddress(entity: DeclaredEntity, defaultDatabase: string)
|
|
|
42
42
|
entity.type === CLICKHOUSE_ENTITY_TYPES.database ||
|
|
43
43
|
entity.type === CLICKHOUSE_ENTITY_TYPES.function ||
|
|
44
44
|
entity.type === CLICKHOUSE_ENTITY_TYPES.user ||
|
|
45
|
-
entity.type === CLICKHOUSE_ENTITY_TYPES.role
|
|
45
|
+
entity.type === CLICKHOUSE_ENTITY_TYPES.role ||
|
|
46
|
+
entity.type === CLICKHOUSE_ENTITY_TYPES.quota ||
|
|
47
|
+
entity.type === CLICKHOUSE_ENTITY_TYPES.settingsProfile
|
|
46
48
|
) {
|
|
47
49
|
return { name };
|
|
48
50
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ClickHouse kinds #3911 adds, against the pinned server: quotas,
|
|
3
|
+
* settings profiles, a table over a table function, and a refreshable
|
|
4
|
+
* materialized view and a table's skip index and projection as the server
|
|
5
|
+
* prints them back.
|
|
6
|
+
*
|
|
7
|
+
* Each round-trips: declared in a source file, built, applied, and then
|
|
8
|
+
* planned with no change; imported back from the server (`exportResources`
|
|
9
|
+
* for the schema, `SHOW CREATE` through a `.sql` file for access control,
|
|
10
|
+
* which an import never reads on its own), built again, and planned with no
|
|
11
|
+
* change. A change to each takes the statement its rule says: `ALTER QUOTA`
|
|
12
|
+
* and `ALTER SETTINGS PROFILE`, never `OR REPLACE`, so a profile that
|
|
13
|
+
* inherits a changed one keeps it; `CREATE OR REPLACE TABLE` for the table
|
|
14
|
+
* over a table function.
|
|
15
|
+
*
|
|
16
|
+
* Needs Docker; skips cleanly without it. A throwaway server at the pin.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
22
|
+
import { build } from "@intentius/chant/build";
|
|
23
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
24
|
+
import { dockerAvailable, startScratchServer, type ScratchServer } from "../container";
|
|
25
|
+
import { clickhouseQuery, type ClickHouseEndpoint } from "../http";
|
|
26
|
+
import { clickhouseImage } from "../../spec/pin";
|
|
27
|
+
import { exportResources } from "../import/live-export";
|
|
28
|
+
import { ClickHouseGenerator } from "../import/generator";
|
|
29
|
+
import { ClickHouseSqlParser } from "../import/parser";
|
|
30
|
+
import { readLiveAccess } from "./catalog";
|
|
31
|
+
import { planAgainstServer } from "../plan/commands";
|
|
32
|
+
import { observeResourcesDeep, sqlDeepNormalizationHooks } from "../plan/deep";
|
|
33
|
+
import { clickhouseApply } from "../../op/activities/clickhouse-apply";
|
|
34
|
+
import { sqlPlugin } from "../../plugin";
|
|
35
|
+
import { sqlSerializer } from "../../serializer";
|
|
36
|
+
|
|
37
|
+
const DB = "chant_e2e_3911";
|
|
38
|
+
const enabled = await dockerAvailable();
|
|
39
|
+
let server: ScratchServer | undefined;
|
|
40
|
+
let endpoint: ClickHouseEndpoint;
|
|
41
|
+
const root = join(import.meta.dirname, "..", "..", "..", `.kinds-tmp-${process.pid}`);
|
|
42
|
+
const q = <T = Record<string, unknown>>(sql: string) => clickhouseQuery<T>(endpoint, sql);
|
|
43
|
+
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
if (!enabled) return;
|
|
46
|
+
server = await startScratchServer(clickhouseImage(), { namePrefix: "chant-sql-kinds" });
|
|
47
|
+
endpoint = server.endpoint;
|
|
48
|
+
}, 600_000);
|
|
49
|
+
|
|
50
|
+
afterAll(async () => {
|
|
51
|
+
await server?.stop();
|
|
52
|
+
rmSync(root, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const config = () =>
|
|
56
|
+
({ ownership: { stack: "e2e3911", env: "test" }, sql: { profiles: { test: { url: endpoint.url, access: true } } } }) as never;
|
|
57
|
+
|
|
58
|
+
/** A project of these files, built: the build document's path and the entities. */
|
|
59
|
+
async function buildProject(name: string, files: Record<string, string>) {
|
|
60
|
+
const dir = join(root, name);
|
|
61
|
+
mkdirSync(join(dir, "src"), { recursive: true });
|
|
62
|
+
writeFileSync(join(dir, "chant.config.ts"), 'export default { lexicons: ["sql"] };\n');
|
|
63
|
+
for (const [file, content] of Object.entries(files)) writeFileSync(join(dir, "src", file), content);
|
|
64
|
+
const result = await build(join(dir, "src"), [sqlSerializer], undefined, { fold: true, intrinsics: sqlPlugin.intrinsics!(), lexicons: ["sql"] });
|
|
65
|
+
expect(result.errors).toEqual([]);
|
|
66
|
+
const buildPath = join(dir, "build.json");
|
|
67
|
+
writeFileSync(buildPath, (result.outputs.get("sql") as SerializerResult).primary);
|
|
68
|
+
const entities = new Map([...result.entities].map(([k, v]) => [k, { entityType: v.entityType, props: (v as unknown as { props: Record<string, unknown> }).props }]));
|
|
69
|
+
return { buildPath, entities };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function apply(buildPath: string) {
|
|
73
|
+
const sent: string[] = [];
|
|
74
|
+
const outcome = await clickhouseApply({ buildPath, environment: "test" }, undefined, { config: config(), env: {}, log: (l) => void (/^[A-Z]+ /.test(l) && sent.push(l)) });
|
|
75
|
+
expect(outcome.failed).toEqual([]);
|
|
76
|
+
return { outcome, sent };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const plan = async (buildPath: string) => (await planAgainstServer("test", buildPath, { config: config(), env: {} })).changes.map((c) => [c.object, c.field, c.rule]);
|
|
80
|
+
|
|
81
|
+
const compared = (props: object): Record<string, unknown> =>
|
|
82
|
+
Object.fromEntries(Object.entries(JSON.parse(JSON.stringify(props)) as Record<string, unknown>).filter(([k]) => !sqlDeepNormalizationHooks.prune!({ pattern: k } as never)));
|
|
83
|
+
|
|
84
|
+
const source = (v: { limit: number; threads: number; nums: number }) => `import { database, table, view, role, quota, settingsProfile } from "@intentius/chant-lexicon-sql/clickhouse";
|
|
85
|
+
|
|
86
|
+
export const shop = database\`CREATE DATABASE ${DB}\`;
|
|
87
|
+
|
|
88
|
+
export const events = table\`
|
|
89
|
+
CREATE TABLE \${shop}.events (
|
|
90
|
+
id UInt64,
|
|
91
|
+
kind LowCardinality(String),
|
|
92
|
+
INDEX by_kind kind TYPE bloom_filter(0.01) GRANULARITY 4,
|
|
93
|
+
PROJECTION per_kind (SELECT kind, count() GROUP BY kind)
|
|
94
|
+
) ENGINE = MergeTree ORDER BY id\`;
|
|
95
|
+
|
|
96
|
+
export const hourly = view\`
|
|
97
|
+
CREATE MATERIALIZED VIEW \${shop}.hourly REFRESH EVERY 1 HOUR
|
|
98
|
+
ENGINE = MergeTree ORDER BY kind
|
|
99
|
+
AS SELECT kind, count() AS n FROM \${events} GROUP BY kind\`;
|
|
100
|
+
|
|
101
|
+
export const nums = table\`CREATE TABLE \${shop}.nums AS numbers(${v.nums})\`;
|
|
102
|
+
export const typedNums = table\`CREATE TABLE \${shop}.typed_nums (number UInt64) AS numbers(10) COMMENT 'counted'\`;
|
|
103
|
+
|
|
104
|
+
export const reader = role\`CREATE ROLE ${DB}_reader\`;
|
|
105
|
+
|
|
106
|
+
export const base = settingsProfile\`CREATE SETTINGS PROFILE ${DB}_base SETTINGS max_threads = ${v.threads}\`;
|
|
107
|
+
|
|
108
|
+
export const analysts = settingsProfile\`
|
|
109
|
+
CREATE PROFILE ${DB}_analysts
|
|
110
|
+
SETTINGS PROFILE \${base}, max_memory_usage = 10000000000 MIN 1000000 MAX 20000000000, readonly = 1 READONLY
|
|
111
|
+
TO \${reader}\`;
|
|
112
|
+
|
|
113
|
+
export const perUser = quota\`
|
|
114
|
+
CREATE QUOTA ${DB}_per_user KEYED BY 'user_name'
|
|
115
|
+
FOR INTERVAL 60 MINUTE MAX QUERIES ${v.limit}, errors = 50,
|
|
116
|
+
FOR RANDOMIZED INTERVAL 1 DAY MAX read_bytes = 1e12
|
|
117
|
+
TO \${reader}\`;
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
describe.skipIf(!enabled)("the kinds #3911 adds, against the pinned server", () => {
|
|
121
|
+
test("declared, built, applied, planned clean; imported back and planned clean; changed with the statement each rule says", async () => {
|
|
122
|
+
const v1 = await buildProject("v1", { "schema.ts": source({ limit: 1000, threads: 4, nums: 10 }) });
|
|
123
|
+
const first = await apply(v1.buildPath);
|
|
124
|
+
expect(first.outcome.applied.filter((a) => a.action === "created").map((a) => a.name)).toEqual(
|
|
125
|
+
expect.arrayContaining([`${DB}.events`, `${DB}.hourly`, `${DB}.nums`, `${DB}.typed_nums`, `quota ${DB}_per_user`, `settings profile ${DB}_base`, `settings profile ${DB}_analysts`]),
|
|
126
|
+
);
|
|
127
|
+
expect(await plan(v1.buildPath)).toEqual([]);
|
|
128
|
+
|
|
129
|
+
// The deep read: every declared object read back as declared.
|
|
130
|
+
const deep = await observeResourcesDeep({ environment: "test", entityNames: [...v1.entities.keys()], entities: v1.entities, config: config(), env: {} });
|
|
131
|
+
expect(deep.unobserved ?? {}).toEqual({});
|
|
132
|
+
for (const k of ["perUser", "base", "analysts", "nums", "typedNums", "hourly", "events"]) {
|
|
133
|
+
expect(compared(deep.resources[k]!.properties), k).toEqual(compared(v1.entities.get(k)!.props));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Imported back: the schema from the catalog, access control from SHOW CREATE in a .sql file.
|
|
137
|
+
const exported = await exportResources({ environment: "test", config: config(), env: {} });
|
|
138
|
+
const [schemaFile] = new ClickHouseGenerator().generate(exported);
|
|
139
|
+
const access = await readLiveAccess({ endpoint, source: "test", defaultDatabase: "default" }, [
|
|
140
|
+
{ kind: "role", name: `${DB}_reader` },
|
|
141
|
+
{ kind: "settingsProfile", name: `${DB}_base` },
|
|
142
|
+
{ kind: "settingsProfile", name: `${DB}_analysts` },
|
|
143
|
+
{ kind: "quota", name: `${DB}_per_user` },
|
|
144
|
+
]);
|
|
145
|
+
expect(access.map((a) => a.statement)).toEqual([
|
|
146
|
+
`CREATE ROLE ${DB}_reader`,
|
|
147
|
+
`CREATE QUOTA ${DB}_per_user KEYED BY user_name FOR INTERVAL 1 hour MAX queries = 1000, errors = 50, FOR RANDOMIZED INTERVAL 1 day MAX read_bytes = 1000000000000 TO ${DB}_reader`,
|
|
148
|
+
`CREATE SETTINGS PROFILE \`${DB}_base\` SETTINGS max_threads = 4`,
|
|
149
|
+
`CREATE SETTINGS PROFILE \`${DB}_analysts\` SETTINGS INHERIT \`${DB}_base\`, max_memory_usage = 10000000000 MIN 1000000 MAX 20000000000, readonly = 1 CONST TO ${DB}_reader`,
|
|
150
|
+
]);
|
|
151
|
+
const [accessFile] = new ClickHouseGenerator().generate(new ClickHouseSqlParser().parse(access.map((a) => `${a.statement};`).join("\n")));
|
|
152
|
+
expect(schemaFile!.content).toMatch(/CREATE TABLE \$\{\w+\}\.nums\s*\(\s*number UInt64\s*\) AS numbers\(10\)/);
|
|
153
|
+
expect(schemaFile!.content).toContain("REFRESH EVERY 1 HOUR");
|
|
154
|
+
const imported = await buildProject("imported", { "schema.ts": schemaFile!.content, "access.ts": accessFile!.content });
|
|
155
|
+
expect(await plan(imported.buildPath)).toEqual([]);
|
|
156
|
+
expect((await apply(imported.buildPath)).sent).toEqual([]);
|
|
157
|
+
|
|
158
|
+
// Changed: a quota's limit, the inherited profile's settings, the table function's call.
|
|
159
|
+
const v2 = await buildProject("v2", { "schema.ts": source({ limit: 2000, threads: 2, nums: 20 }) });
|
|
160
|
+
expect(await plan(v2.buildPath)).toEqual([
|
|
161
|
+
[`base (settings profile ${DB}_base)`, "settings", "SQLCH276"],
|
|
162
|
+
[`nums (${DB}.nums)`, "asFunction", "SQLCH219"],
|
|
163
|
+
[`perUser (quota ${DB}_per_user)`, "interval.3600", "SQLCH275"],
|
|
164
|
+
]);
|
|
165
|
+
const second = await apply(v2.buildPath);
|
|
166
|
+
expect(second.sent).toEqual(
|
|
167
|
+
expect.arrayContaining([
|
|
168
|
+
expect.stringMatching(new RegExp(`^CREATE OR REPLACE TABLE ${DB}\\.nums AS numbers\\(20\\) COMMENT '`)),
|
|
169
|
+
`ALTER QUOTA \`${DB}_per_user\` FOR INTERVAL 3600 second MAX errors = 50, queries = 2000`,
|
|
170
|
+
`ALTER SETTINGS PROFILE \`${DB}_base\` SETTINGS max_threads = 2`,
|
|
171
|
+
]),
|
|
172
|
+
);
|
|
173
|
+
expect(second.sent).toHaveLength(3);
|
|
174
|
+
expect(await plan(v2.buildPath)).toEqual([]);
|
|
175
|
+
// ALTER, not OR REPLACE: the profile that inherits the changed one still does.
|
|
176
|
+
expect(Object.values((await q<Record<string, string>>(`SHOW CREATE SETTINGS PROFILE ${DB}_analysts`))[0]!)[0]).toContain(`INHERIT \`${DB}_base\``);
|
|
177
|
+
expect(Number((await q<{ n: string }>(`SELECT count() AS n FROM ${DB}.nums`))[0]!.n)).toBe(20);
|
|
178
|
+
}, 600_000);
|
|
179
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import against a real ClickHouse server never leaves an object out without
|
|
3
|
+
* a word (#3911): a server holding a named collection and a workload made
|
|
4
|
+
* with SQL, beside a table, fails the import, which names both; a quota and a
|
|
5
|
+
* settings profile are declared kinds, which an import reads only when a
|
|
6
|
+
* build declares them, so they are not refused; with `skipUndeclarable` the table is imported and
|
|
7
|
+
* they are warnings. What `users.xml` defines (the `default` quota and
|
|
8
|
+
* profile) is the server's, and is not refused.
|
|
9
|
+
*
|
|
10
|
+
* Needs Docker; skips cleanly without it. One throwaway
|
|
11
|
+
* `clickhouse/clickhouse-server` container at the pin, removed afterwards
|
|
12
|
+
* even on failure.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
16
|
+
import { dockerAvailable, startScratchServer, type ScratchServer } from "../container";
|
|
17
|
+
import { clickhouseQuery } from "../http";
|
|
18
|
+
import { clickhouseImage } from "../../spec/pin";
|
|
19
|
+
import { exportResources } from "../import/live-export";
|
|
20
|
+
import { UndeclarableObjectsError } from "../../core/undeclarable";
|
|
21
|
+
|
|
22
|
+
const enabled = await dockerAvailable();
|
|
23
|
+
let server: ScratchServer | undefined;
|
|
24
|
+
|
|
25
|
+
const SEED = [
|
|
26
|
+
"CREATE DATABASE analytics",
|
|
27
|
+
"CREATE TABLE analytics.events (id UInt64) ENGINE = MergeTree ORDER BY id",
|
|
28
|
+
"CREATE QUOTA q_reports FOR INTERVAL 1 hour MAX queries = 100",
|
|
29
|
+
"CREATE SETTINGS PROFILE p_reports SETTINGS max_threads = 2",
|
|
30
|
+
"CREATE NAMED COLLECTION nc_s3 AS url = 'https://example.com/bucket'",
|
|
31
|
+
"CREATE WORKLOAD all",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
if (!enabled) return;
|
|
36
|
+
server = await startScratchServer(clickhouseImage(), { namePrefix: "chant-sql-undeclarable" });
|
|
37
|
+
for (const stmt of SEED) await clickhouseQuery(server.endpoint, stmt);
|
|
38
|
+
}, 600_000);
|
|
39
|
+
|
|
40
|
+
afterAll(async () => {
|
|
41
|
+
await server?.stop();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const run = (extra: Record<string, unknown> = {}) => exportResources({ environment: "x", config: {}, env: { CLICKHOUSE_URL: server!.endpoint.url }, ...extra });
|
|
45
|
+
|
|
46
|
+
const NAMED = ["named collection nc_s3", "workload all"];
|
|
47
|
+
|
|
48
|
+
describe.skipIf(!enabled)("importing a server with objects chant cannot declare", () => {
|
|
49
|
+
test("fails, naming every one of them", async () => {
|
|
50
|
+
const err = await run().then(
|
|
51
|
+
() => undefined,
|
|
52
|
+
(e: unknown) => e,
|
|
53
|
+
);
|
|
54
|
+
expect(err).toBeInstanceOf(UndeclarableObjectsError);
|
|
55
|
+
const objects = (err as UndeclarableObjectsError).objects.map((o) => `${o.kind} ${o.schema ? `${o.schema}.` : ""}${o.name}`);
|
|
56
|
+
expect([...objects].sort()).toEqual(NAMED);
|
|
57
|
+
}, 120_000);
|
|
58
|
+
|
|
59
|
+
test("with skipUndeclarable, imports the rest and lists them as warnings", async () => {
|
|
60
|
+
const ir = await run({ skipUndeclarable: true });
|
|
61
|
+
expect(ir.resources.map((r) => r.properties.name)).toEqual(["analytics", "events"]);
|
|
62
|
+
const left = (ir.warnings ?? []).filter((w) => w.includes("is left out (--skip-undeclarable)"));
|
|
63
|
+
expect(left.map((w) => w.split(" is left out")[0]).sort()).toEqual(NAMED);
|
|
64
|
+
}, 120_000);
|
|
65
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The objects on a live ClickHouse server of the kinds no declaration holds
|
|
3
|
+
* (#3911), found so import can refuse them by name instead of leaving them
|
|
4
|
+
* out (`../../core/undeclarable.ts`).
|
|
5
|
+
*
|
|
6
|
+
* One `system.*` query per kind the coverage table marks refused
|
|
7
|
+
* (`../../coverage/clickhouse.json`), keyed by the row's `kind`. A kind whose
|
|
8
|
+
* row is flipped to declared is no longer asked about.
|
|
9
|
+
*
|
|
10
|
+
* What the server's configuration files define (`storage = 'users_xml'`, a
|
|
11
|
+
* named collection whose source is the config, an executable function) is
|
|
12
|
+
* the server's, not a schema's, and is left out; so is what is in a database
|
|
13
|
+
* out of scope. A server-wide object (a quota, a settings profile, a
|
|
14
|
+
* workload) is always looked for.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { clickhouseQuery } from "../http";
|
|
18
|
+
import type { ClickHouseTarget } from "./bind";
|
|
19
|
+
import { SYSTEM_DATABASES } from "./catalog";
|
|
20
|
+
import { KIND_COVERAGE } from "../../coverage";
|
|
21
|
+
import type { UndeclarableObject } from "../../core/undeclarable";
|
|
22
|
+
|
|
23
|
+
const quote = (s: string) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
24
|
+
|
|
25
|
+
/** A query returning `database` (empty for a server-wide object) and `name`; `scope` is the predicate on a database column. */
|
|
26
|
+
type Probe = (scope: (column: string) => string) => string;
|
|
27
|
+
|
|
28
|
+
const serverWide = (table: string, where = "1") => () => `SELECT '' AS database, name FROM system.${table} WHERE ${where}`;
|
|
29
|
+
const SQL_DEFINED = "storage != 'users_xml'";
|
|
30
|
+
|
|
31
|
+
export const CLICKHOUSE_PROBES: Readonly<Record<string, Probe>> = {
|
|
32
|
+
"non-SQL function": serverWide("functions", "origin NOT IN ('System', 'SQLUserDefined', 'ExecutableUserDefined')"),
|
|
33
|
+
"HTTP handler": serverWide("handlers"),
|
|
34
|
+
"masking policy": (scope) => `SELECT database, short_name || ' ON ' || table AS name FROM system.masking_policies WHERE ${SQL_DEFINED} AND ${scope("database")}`,
|
|
35
|
+
"named collection": serverWide("named_collections", "source = 'SQL'"),
|
|
36
|
+
quota: serverWide("quotas", SQL_DEFINED),
|
|
37
|
+
"workload resource": serverWide("resources"),
|
|
38
|
+
"settings profile": serverWide("settings_profiles", SQL_DEFINED),
|
|
39
|
+
"window view": (scope) => `SELECT database, name FROM system.tables WHERE engine = 'WindowView' AND NOT is_temporary AND ${scope("database")}`,
|
|
40
|
+
workload: serverWide("workloads"),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every object in scope of a kind the coverage table refuses, kind by kind
|
|
45
|
+
* in the table's order, each by database then name. A kind the reader may
|
|
46
|
+
* not read (no grant on its `system` table) is a problem in `unread`, not a
|
|
47
|
+
* pass.
|
|
48
|
+
*/
|
|
49
|
+
export async function readUndeclarable(target: ClickHouseTarget): Promise<{ objects: UndeclarableObject[]; unread: string[] }> {
|
|
50
|
+
const scope = (column: string) =>
|
|
51
|
+
target.databases ? `${column} IN (${target.databases.map(quote).join(", ")})` : `${column} NOT IN (${[...SYSTEM_DATABASES].map(quote).join(", ")})`;
|
|
52
|
+
const objects: UndeclarableObject[] = [];
|
|
53
|
+
const unread: string[] = [];
|
|
54
|
+
for (const row of KIND_COVERAGE.clickhouse) {
|
|
55
|
+
if (row.status !== "refused") continue;
|
|
56
|
+
const probe = CLICKHOUSE_PROBES[row.kind];
|
|
57
|
+
if (!probe) throw new Error(`no system table query finds a ClickHouse ${row.kind} (src/clickhouse/live/undeclarable.ts)`);
|
|
58
|
+
try {
|
|
59
|
+
const rows = await clickhouseQuery<{ database: string; name: string }>(target.endpoint, `/* chant undeclarable: ${row.kind} */ SELECT database, name FROM (${probe(scope)}) ORDER BY database, name`);
|
|
60
|
+
for (const r of rows) objects.push({ kind: row.kind, ...(r.database ? { schema: r.database } : {}), name: r.name });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
unread.push(`${row.kind}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { objects, unread };
|
|
66
|
+
}
|
|
@@ -84,8 +84,42 @@ describe("parsing CREATE TABLE", () => {
|
|
|
84
84
|
expect(t.engine!.name).toBe("MergeTree");
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
test("CREATE TABLE ... AS
|
|
88
|
-
expect(() => parse("CREATE TABLE t AS other")).toThrow(/
|
|
87
|
+
test("CREATE TABLE ... AS a table, AS SELECT, CLONE AS and a table with no columns are refused, each saying why (#3911)", () => {
|
|
88
|
+
expect(() => parse("CREATE TABLE t AS other")).toThrow(/AS <table> is not declared: the server copies the other table's columns/);
|
|
89
|
+
expect(() => parse("CREATE TABLE t AS db.other ENGINE = Log")).toThrow(/AS <table> is not declared/);
|
|
90
|
+
expect(() => parse("CREATE TABLE t ENGINE = Log AS SELECT 1 AS a")).toThrow(/expected the column list \(a table declared without one/);
|
|
91
|
+
expect(() => parse("CREATE TABLE t AS SELECT 1 AS a")).toThrow(/AS SELECT is not declared/);
|
|
92
|
+
expect(() => parse("CREATE TABLE t (a UInt8) ENGINE = Memory AS SELECT 1 AS a")).toThrow(/AS SELECT is not declared/);
|
|
93
|
+
expect(() => parse("CREATE TABLE t CLONE AS other")).toThrow(/CLONE AS is not declared/);
|
|
94
|
+
expect(() => parse("CREATE TABLE t EMPTY AS SELECT 1")).toThrow(/EMPTY AS SELECT is not declared/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a table over a table function, with or without its columns (#3911)", () => {
|
|
98
|
+
const bareSql = "CREATE TABLE t AS numbers(10)";
|
|
99
|
+
const bare = parse(bareSql) as TableNode;
|
|
100
|
+
expect(bare.columns).toEqual([]);
|
|
101
|
+
expect(bare.engine).toBeUndefined();
|
|
102
|
+
expect(text(bareSql, bare.asFunction)).toBe("numbers(10)");
|
|
103
|
+
// As SHOW CREATE prints it: the inferred columns, then the call, then the comment.
|
|
104
|
+
const shownSql = "CREATE TABLE default.tf\n(\n `a` UInt64,\n `b` String\n) AS remote('localhost', 'default.t')\nCOMMENT 'x'";
|
|
105
|
+
const shown = parse(shownSql) as TableNode;
|
|
106
|
+
expect(shown.columns.map((c) => c.name)).toEqual(["a", "b"]);
|
|
107
|
+
expect(text(shownSql, shown.asFunction)).toBe("remote('localhost', 'default.t')");
|
|
108
|
+
expect(shown.comment).toBeDefined();
|
|
109
|
+
expect(() => parse("CREATE TABLE t AS numbers(10) ENGINE = Log")).toThrow(/no engine or storage clauses/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("the CREATE kinds chant does not declare are refused by name, with why (#3911)", () => {
|
|
113
|
+
expect(() => parse("CREATE LIVE VIEW lv AS SELECT 1")).toThrow(/CREATE LIVE VIEW is not declared: the pinned server no longer has live views/);
|
|
114
|
+
expect(() => parse("CREATE WINDOW VIEW wv AS SELECT 1")).toThrow(/CREATE WINDOW VIEW is not declared: a window view is experimental/);
|
|
115
|
+
expect(() => parse("CREATE NAMED COLLECTION c AS user = 'u', password = 'p'")).toThrow(/CREATE NAMED COLLECTION is not declared: a named collection holds credentials/);
|
|
116
|
+
expect(() => parse("CREATE MASKING POLICY m ON t UPDATE a = 'x' TO r")).toThrow(/only in ClickHouse Cloud/);
|
|
117
|
+
expect(() => parse("CREATE WORKLOAD all")).toThrow(/CREATE WORKLOAD is not declared: a workload is server-level/);
|
|
118
|
+
expect(() => parse("CREATE RESOURCE r (WRITE DISK d)")).toThrow(/CREATE RESOURCE is not declared/);
|
|
119
|
+
expect(() => parse("CREATE TEMPORARY TABLE t (a UInt8)")).toThrow(/CREATE TEMPORARY TABLE is not declared/);
|
|
120
|
+
expect(() => parse("CREATE INDEX ix ON t (b) TYPE minmax")).toThrow(/CREATE INDEX is not declared: a skip index belongs to its table/);
|
|
121
|
+
expect(() => parse("CREATE QUOTA q")).toThrow(/CREATE QUOTA is access control: declare it with the quota tag/);
|
|
122
|
+
expect(() => parse("CREATE SETTINGS PROFILE p")).toThrow(/settingsProfile tag/);
|
|
89
123
|
});
|
|
90
124
|
});
|
|
91
125
|
|
|
@@ -103,6 +137,15 @@ describe("parsing views and databases", () => {
|
|
|
103
137
|
expect(text(sql, v.select)).toBe("SELECT a, b FROM src");
|
|
104
138
|
});
|
|
105
139
|
|
|
140
|
+
test("a refreshable materialized view as SHOW CREATE prints it: the column list right after the schedule (#3911)", () => {
|
|
141
|
+
const sql =
|
|
142
|
+
"CREATE MATERIALIZED VIEW default.rmv\nREFRESH EVERY 1 HOUR OFFSET 5 MINUTES\n(\n `a` UInt64\n)\nENGINE = MergeTree\nORDER BY a\nSETTINGS index_granularity = 8192\nDEFINER = default SQL SECURITY DEFINER\nAS SELECT a\nFROM default.t";
|
|
143
|
+
const v = parse(sql) as ViewNode;
|
|
144
|
+
expect(text(sql, v.refresh)).toBe("EVERY 1 HOUR OFFSET 5 MINUTES");
|
|
145
|
+
expect(v.columns.map((c) => c.name)).toEqual(["a"]);
|
|
146
|
+
expect(text(sql, v.orderBy)).toBe("a");
|
|
147
|
+
});
|
|
148
|
+
|
|
106
149
|
test("a view the server printed back, with its UUID", () => {
|
|
107
150
|
const v = parse("CREATE VIEW default.v UUID '6a1b' (`x` UInt8) AS SELECT 1 AS x") as ViewNode;
|
|
108
151
|
expect(v.columns[0]!.name).toBe("x");
|