@intentius/chant-lexicon-sql 0.124.12 → 0.124.13
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/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +7 -1
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/expressions.d.ts +27 -0
- package/dist/clickhouse/expressions.d.ts.map +1 -0
- package/dist/clickhouse/parser.d.ts +12 -2
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +11 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +1 -0
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse.d.ts +1 -0
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/postgres-classifier-page.d.ts.map +1 -1
- package/dist/core/cursor.d.ts +41 -0
- package/dist/core/cursor.d.ts.map +1 -1
- package/dist/core/interpolation.d.ts +18 -1
- package/dist/core/interpolation.d.ts.map +1 -1
- package/dist/files/clickhouse.d.ts +2 -2
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/common.d.ts +16 -0
- package/dist/files/common.d.ts.map +1 -1
- package/dist/files/postgres.d.ts +2 -2
- package/dist/files/postgres.d.ts.map +1 -1
- package/dist/import-parser.d.ts +2 -1
- package/dist/import-parser.d.ts.map +1 -1
- package/dist/integrity.json +19 -19
- package/dist/lint/post-synth/clickhouse-helpers.d.ts +6 -19
- package/dist/lint/post-synth/clickhouse-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-helpers.d.ts +15 -55
- package/dist/lint/post-synth/postgres-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-names.d.ts +2 -2
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
- package/dist/lint/post-synth/sql-helpers.d.ts +5 -6
- package/dist/lint/post-synth/sql-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch102.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch103.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch104.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch105.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch106.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch107.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch111.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch112.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch113.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch114.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch115.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch120.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch125.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +7 -1
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +10 -2
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +1 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -0
- package/dist/postgres.d.ts.map +1 -1
- package/dist/read/clickhouse.d.ts +66 -0
- package/dist/read/clickhouse.d.ts.map +1 -0
- package/dist/read/objects.d.ts +99 -0
- package/dist/read/objects.d.ts.map +1 -0
- package/dist/read/postgres.d.ts +83 -0
- package/dist/read/postgres.d.ts.map +1 -0
- package/dist/read.d.ts +49 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/rules/clickhouse-helpers.ts +20 -97
- package/dist/rules/postgres-helpers.ts +18 -53
- package/dist/rules/postgres-names.ts +6 -6
- package/dist/rules/sql-helpers.ts +8 -28
- package/dist/rules/sqlch102.ts +2 -3
- package/dist/rules/sqlch103.ts +2 -3
- package/dist/rules/sqlch104.ts +2 -3
- package/dist/rules/sqlch105.ts +2 -3
- package/dist/rules/sqlch106.ts +2 -3
- package/dist/rules/sqlch107.ts +2 -3
- package/dist/rules/sqlch111.ts +2 -3
- package/dist/rules/sqlch112.ts +2 -3
- package/dist/rules/sqlch113.ts +2 -3
- package/dist/rules/sqlch114.ts +2 -3
- package/dist/rules/sqlch115.ts +2 -3
- package/dist/rules/sqlch120.ts +2 -3
- package/dist/rules/sqlch125.ts +2 -3
- package/dist/serializer.d.ts +11 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/sql-files.d.ts +17 -1
- package/dist/sql-files.d.ts.map +1 -1
- package/dist/sql-types.d.ts +3 -0
- package/dist/sql-types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/apply/statements.ts +2 -0
- package/src/clickhouse/entities.ts +8 -1
- package/src/clickhouse/expressions.ts +108 -0
- package/src/clickhouse/parser.ts +113 -31
- package/src/clickhouse/plan/diff.ts +19 -0
- package/src/clickhouse/plan/normalize.ts +27 -2
- package/src/clickhouse/plan/rebuild-handoff.ts +2 -1
- package/src/clickhouse/plan/rules.ts +7 -0
- package/src/clickhouse/testdata/server-ddl-baseline.json +67 -0
- package/src/clickhouse.ts +10 -0
- package/src/codegen/classifier-page.ts +2 -0
- package/src/codegen/postgres-classifier-page.ts +2 -0
- package/src/core/cursor.ts +96 -0
- package/src/core/interpolation.ts +32 -1
- package/src/files/clickhouse.ts +7 -5
- package/src/files/common.ts +23 -0
- package/src/files/postgres.ts +12 -8
- package/src/import-parser.ts +2 -1
- package/src/lint/post-synth/clickhouse-helpers.ts +20 -97
- package/src/lint/post-synth/postgres-helpers.ts +18 -53
- package/src/lint/post-synth/postgres-names.ts +6 -6
- package/src/lint/post-synth/sql-helpers.ts +8 -28
- package/src/lint/post-synth/sqlch102.ts +2 -3
- package/src/lint/post-synth/sqlch103.ts +2 -3
- package/src/lint/post-synth/sqlch104.ts +2 -3
- package/src/lint/post-synth/sqlch105.ts +2 -3
- package/src/lint/post-synth/sqlch106.ts +2 -3
- package/src/lint/post-synth/sqlch107.ts +2 -3
- package/src/lint/post-synth/sqlch111.ts +2 -3
- package/src/lint/post-synth/sqlch112.ts +2 -3
- package/src/lint/post-synth/sqlch113.ts +2 -3
- package/src/lint/post-synth/sqlch114.ts +2 -3
- package/src/lint/post-synth/sqlch115.ts +2 -3
- package/src/lint/post-synth/sqlch120.ts +2 -3
- package/src/lint/post-synth/sqlch125.ts +2 -3
- package/src/postgres/apply/statements.ts +2 -0
- package/src/postgres/entities.ts +9 -1
- package/src/postgres/parser.ts +31 -8
- package/src/postgres/plan/diff.ts +18 -0
- package/src/postgres/plan/normalize.ts +12 -0
- package/src/postgres/plan/rules.ts +7 -0
- package/src/postgres/testdata/server-ddl-baseline.json +38 -0
- package/src/postgres.ts +12 -0
- package/src/read/clickhouse.ts +137 -0
- package/src/read/objects.ts +137 -0
- package/src/read/postgres.ts +180 -0
- package/src/read-policies.e2e.test.ts +144 -0
- package/src/read.test.ts +143 -0
- package/src/read.ts +69 -0
- package/src/serializer.ts +37 -1
- package/src/sql-files.ts +27 -6
- package/src/sql-types.ts +19 -2
- package/src/unknown-clauses.test.ts +227 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small scanners over ClickHouse expression text as the parser keeps it
|
|
3
|
+
* (sort keys, engine arguments, column types), shared by the post-synth
|
|
4
|
+
* checks (`../lint/post-synth/clickhouse-helpers.ts`) and the public read API
|
|
5
|
+
* (`../read/clickhouse.ts`). They answer narrow questions ("is this a bare
|
|
6
|
+
* column name", "what family is this type"), not what an expression
|
|
7
|
+
* evaluates to.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TABLE_ENGINES, TYPE_FAMILIES } from "../generated/clickhouse";
|
|
11
|
+
import type { EngineDef } from "./entities";
|
|
12
|
+
|
|
13
|
+
/** The engine's catalog entry, when the pinned server has the engine. */
|
|
14
|
+
export function engineSpec(engine: EngineDef | undefined) {
|
|
15
|
+
return engine ? (TABLE_ENGINES as Record<string, (typeof TABLE_ENGINES)[keyof typeof TABLE_ENGINES]>)[engine.name] : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A MergeTree family engine, replicated or not. */
|
|
19
|
+
export function isMergeTree(engine: EngineDef | undefined): boolean {
|
|
20
|
+
return engineSpec(engine)?.mergeTree === true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The engine without its `Replicated` prefix: `ReplicatedReplacingMergeTree` is `ReplacingMergeTree`. */
|
|
24
|
+
export function baseEngineName(engine: EngineDef): string {
|
|
25
|
+
const spec = engineSpec(engine) as { replicates?: string } | undefined;
|
|
26
|
+
return spec?.replicates ?? engine.name;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/** Split on commas that are not inside parentheses, brackets or quotes. */
|
|
32
|
+
export function splitTop(text: string, separator = ","): string[] {
|
|
33
|
+
const parts: string[] = [];
|
|
34
|
+
let depth = 0;
|
|
35
|
+
let quote: string | undefined;
|
|
36
|
+
let start = 0;
|
|
37
|
+
for (let i = 0; i < text.length; i++) {
|
|
38
|
+
const c = text[i]!;
|
|
39
|
+
if (quote) {
|
|
40
|
+
if (c === "\\") i++;
|
|
41
|
+
else if (c === quote) quote = undefined;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (c === "'" || c === '"' || c === "`") quote = c;
|
|
45
|
+
else if (c === "(" || c === "[") depth++;
|
|
46
|
+
else if (c === ")" || c === "]") depth--;
|
|
47
|
+
else if (c === separator && depth === 0) {
|
|
48
|
+
parts.push(text.slice(start, i).trim());
|
|
49
|
+
start = i + 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const last = text.slice(start).trim();
|
|
53
|
+
if (last !== "" || parts.length > 0) parts.push(last);
|
|
54
|
+
return parts.filter((p) => p !== "");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `(a, b)` is the list `a`, `b`; `a, b` is the same; `tuple()` and `()` are empty. */
|
|
58
|
+
export function keyElements(expr: string): string[] {
|
|
59
|
+
let text = expr.trim();
|
|
60
|
+
if (/^tuple\s*\(\s*\)$/i.test(text) || text === "()") return [];
|
|
61
|
+
if (text.startsWith("(") && closingParen(text, 0) === text.length - 1) text = text.slice(1, -1);
|
|
62
|
+
return splitTop(text).map((e) => e.replace(/\s+(ASC|DESC)$/i, "").replace(/\s+/g, " ").trim());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function closingParen(text: string, open: number): number {
|
|
66
|
+
let depth = 0;
|
|
67
|
+
let quote: string | undefined;
|
|
68
|
+
for (let i = open; i < text.length; i++) {
|
|
69
|
+
const c = text[i]!;
|
|
70
|
+
if (quote) {
|
|
71
|
+
if (c === "\\") i++;
|
|
72
|
+
else if (c === quote) quote = undefined;
|
|
73
|
+
} else if (c === "'" || c === '"' || c === "`") quote = c;
|
|
74
|
+
else if (c === "(") depth++;
|
|
75
|
+
else if (c === ")" && --depth === 0) return i;
|
|
76
|
+
}
|
|
77
|
+
return -1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The column a bare name refers to: `a` or `` `a b` ``, else undefined. */
|
|
81
|
+
export function bareName(element: string): string | undefined {
|
|
82
|
+
const text = element.trim();
|
|
83
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(text)) return text;
|
|
84
|
+
const quoted = /^`((?:[^`]|``)*)`$/.exec(text) ?? /^"((?:[^"]|"")*)"$/.exec(text);
|
|
85
|
+
return quoted ? quoted[1]!.replace(/``|""/g, (m) => m[0]!) : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/** A type with its `Nullable(...)` and `LowCardinality(...)` wrappers removed. */
|
|
91
|
+
export function unwrapType(type: string | undefined): string {
|
|
92
|
+
let t = (type ?? "").trim();
|
|
93
|
+
for (;;) {
|
|
94
|
+
const m = /^(Nullable|LowCardinality)\s*\(([\s\S]*)\)$/i.exec(t);
|
|
95
|
+
if (!m) return t;
|
|
96
|
+
t = m[2]!.trim();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The canonical family of a column's type (`BIGINT UNSIGNED` is `UInt64`), or its leading word when unknown. */
|
|
101
|
+
export function typeFamily(type: string | undefined): string {
|
|
102
|
+
const head = /^[A-Za-z_][A-Za-z0-9_ ]*?(?=\s*\(|$)/.exec(unwrapType(type))?.[0]?.trim() ?? "";
|
|
103
|
+
const table = TYPE_FAMILIES as Record<string, { canonical: string } | undefined>;
|
|
104
|
+
return (table[head] ?? table[head.toUpperCase()])?.canonical ?? head;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const isNullableType = (type: string | undefined): boolean => /^Nullable\s*\(/i.test((type ?? "").trim());
|
|
108
|
+
|
package/src/clickhouse/parser.ts
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import type { Token } from "./tokens";
|
|
23
|
-
import { kw, SqlCursor, unquoteWith, type Span } from "../core/cursor";
|
|
23
|
+
import { kw, SqlCursor, unquoteWith, type Span, type UnknownClause } from "../core/cursor";
|
|
24
24
|
|
|
25
|
-
export type { Span } from "../core/cursor";
|
|
25
|
+
export type { Span, UnknownClause } from "../core/cursor";
|
|
26
26
|
|
|
27
27
|
export interface ColumnNode {
|
|
28
28
|
/** The name as written, unquoted. Empty when the name is an interpolation. */
|
|
@@ -73,6 +73,8 @@ export interface StorageNode {
|
|
|
73
73
|
|
|
74
74
|
export interface DatabaseNode {
|
|
75
75
|
statement: "database";
|
|
76
|
+
/** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
|
|
77
|
+
unknown?: UnknownClause[];
|
|
76
78
|
ifNotExists: boolean;
|
|
77
79
|
name: Span;
|
|
78
80
|
onCluster?: Span;
|
|
@@ -83,6 +85,8 @@ export interface DatabaseNode {
|
|
|
83
85
|
|
|
84
86
|
export interface TableNode extends StorageNode {
|
|
85
87
|
statement: "table";
|
|
88
|
+
/** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
|
|
89
|
+
unknown?: UnknownClause[];
|
|
86
90
|
orReplace: boolean;
|
|
87
91
|
ifNotExists: boolean;
|
|
88
92
|
name: Span;
|
|
@@ -96,6 +100,8 @@ export interface TableNode extends StorageNode {
|
|
|
96
100
|
|
|
97
101
|
export interface ViewNode extends StorageNode {
|
|
98
102
|
statement: "view";
|
|
103
|
+
/** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
|
|
104
|
+
unknown?: UnknownClause[];
|
|
99
105
|
materialized: boolean;
|
|
100
106
|
orReplace: boolean;
|
|
101
107
|
ifNotExists: boolean;
|
|
@@ -128,6 +134,8 @@ export interface DictionaryAttributeNode {
|
|
|
128
134
|
|
|
129
135
|
export interface DictionaryNode {
|
|
130
136
|
statement: "dictionary";
|
|
137
|
+
/** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
|
|
138
|
+
unknown?: UnknownClause[];
|
|
131
139
|
orReplace: boolean;
|
|
132
140
|
ifNotExists: boolean;
|
|
133
141
|
name: Span;
|
|
@@ -151,6 +159,8 @@ export interface DictionaryNode {
|
|
|
151
159
|
/** A SQL user-defined function: `CREATE FUNCTION name AS (x, y) -> expr`. It lives outside any database. */
|
|
152
160
|
export interface FunctionNode {
|
|
153
161
|
statement: "function";
|
|
162
|
+
/** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
|
|
163
|
+
unknown?: UnknownClause[];
|
|
154
164
|
orReplace: boolean;
|
|
155
165
|
ifNotExists: boolean;
|
|
156
166
|
name: Span;
|
|
@@ -177,18 +187,75 @@ export const unquote = (text: string): string => unquoteWith(text, '`"');
|
|
|
177
187
|
|
|
178
188
|
const countOf = (text: string, c: string): number => text.split(c).length - 1;
|
|
179
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Words an expression in a DDL clause holds between two operands, or right
|
|
192
|
+
* after one: operators, `CASE`, `INTERVAL` and its units, a TTL's actions.
|
|
193
|
+
* Any other key word right after an operand starts a new clause (#3872).
|
|
194
|
+
*/
|
|
195
|
+
const EXPRESSION_WORDS = new Set(
|
|
196
|
+
(
|
|
197
|
+
"AND OR NOT IN IS NULL LIKE ILIKE BETWEEN ASC DESC NULLS FIRST LAST COLLATE CASE WHEN THEN ELSE END AS INTERVAL GLOBAL ANY ALL " +
|
|
198
|
+
"DISTINCT FROM TO IF EXISTS DISK VOLUME DELETE RECOMPRESS CODEC WHERE GROUP BY SET WITH FILL STEP TRUE FALSE " +
|
|
199
|
+
"NANOSECOND MICROSECOND MILLISECOND SECOND MINUTE HOUR DAY WEEK MONTH QUARTER YEAR"
|
|
200
|
+
).split(" "),
|
|
201
|
+
);
|
|
202
|
+
|
|
180
203
|
const STORAGE_KEYWORDS = ["ENGINE", "ORDER", "PRIMARY", "PARTITION", "SAMPLE", "TTL", "SETTINGS", "COMMENT"];
|
|
181
204
|
|
|
205
|
+
/** A column's own clause words: an unknown one stops at the next of these (#3872). */
|
|
206
|
+
const COLUMN_WORDS = ["DEFAULT", "MATERIALIZED", "ALIAS", "EPHEMERAL", "NULL", "NOT", "COMMENT", "CODEC", "TTL", "STATISTICS", "SETTINGS", "PRIMARY"];
|
|
207
|
+
|
|
182
208
|
class Parser extends SqlCursor {
|
|
183
|
-
/**
|
|
209
|
+
/** Where the clause-aware expression being read started (#3872); undefined outside one. */
|
|
210
|
+
private clauseFrom: number | undefined;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* `ORDER`, `PARTITION`, `SAMPLE` and `GROUP` stop only before `BY`, `PRIMARY`
|
|
214
|
+
* only before `KEY`. In a clause-aware expression, a clause the parser does
|
|
215
|
+
* not know stops it too.
|
|
216
|
+
*/
|
|
184
217
|
protected override stopsAt(stop: readonly string[]): boolean {
|
|
185
|
-
if (!super.stopsAt(stop)) return
|
|
218
|
+
if (!super.stopsAt(stop)) return this.startsUnknownClause();
|
|
186
219
|
const word = this.peek()!.text.toUpperCase();
|
|
187
220
|
const two = ["ORDER", "PARTITION", "SAMPLE", "GROUP"].includes(word) ? kw(this.peek(1), "BY") : true;
|
|
188
221
|
const key = word === "PRIMARY" ? kw(this.peek(1), "KEY") : true;
|
|
189
222
|
return two && key;
|
|
190
223
|
}
|
|
191
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Whether a clause the parser does not know starts here, inside a
|
|
227
|
+
* clause-aware expression (#3872): a bare key word right after a complete
|
|
228
|
+
* operand (a name, a literal, a closing parenthesis), which no expression
|
|
229
|
+
* puts side by side unless the word is one of an expression's own.
|
|
230
|
+
*/
|
|
231
|
+
private startsUnknownClause(): boolean {
|
|
232
|
+
if (this.clauseFrom === undefined || this.p <= this.clauseFrom) return false;
|
|
233
|
+
const t = this.peek();
|
|
234
|
+
const prev = this.peek(-1);
|
|
235
|
+
if (t?.kind !== "ident" || !/^[A-Za-z_]{3,}$/.test(t.text) || EXPRESSION_WORDS.has(t.text.toUpperCase())) return false;
|
|
236
|
+
if (prev === undefined) return false;
|
|
237
|
+
// A query read into the expression (`ORDER BY id AS SELECT ...`, which the server takes as CREATE TABLE ... AS) is the query's, as it always was.
|
|
238
|
+
for (let n = this.clauseFrom; n < this.p; n++) if (kw(this.tokens[this.sig[n]!], "SELECT")) return false;
|
|
239
|
+
return (
|
|
240
|
+
prev.kind === "number" ||
|
|
241
|
+
prev.kind === "string" ||
|
|
242
|
+
prev.kind === "qident" ||
|
|
243
|
+
(prev.kind === "punct" && prev.text === ")") ||
|
|
244
|
+
(prev.kind === "ident" && !EXPRESSION_WORDS.has(prev.text.toUpperCase()))
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** An expression a clause the parser does not know can follow (#3872): it ends where that clause starts. */
|
|
249
|
+
private clauseExpr(stop: readonly string[] = [], stopAtComma = true): Span {
|
|
250
|
+
const saved = this.clauseFrom;
|
|
251
|
+
this.clauseFrom = this.p;
|
|
252
|
+
try {
|
|
253
|
+
return this.expr(stop, stopAtComma);
|
|
254
|
+
} finally {
|
|
255
|
+
this.clauseFrom = saved;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
192
259
|
/** Array and map literals: `[` and `]` arrive inside operator runs. */
|
|
193
260
|
protected override depthChange(t: Token): number {
|
|
194
261
|
if (t.kind === "op") return countOf(t.text, "[") + countOf(t.text, "{") - countOf(t.text, "]") - countOf(t.text, "}");
|
|
@@ -255,20 +322,20 @@ class Parser extends SqlCursor {
|
|
|
255
322
|
col.nullable = false;
|
|
256
323
|
} else if (kw(this.peek(), "DEFAULT", "MATERIALIZED", "ALIAS")) {
|
|
257
324
|
const kind = this.next().text.toUpperCase() as "DEFAULT" | "MATERIALIZED" | "ALIAS";
|
|
258
|
-
col.default = { kind, expr: this.
|
|
325
|
+
col.default = { kind, expr: this.clauseExpr(STOP) };
|
|
259
326
|
} else if (this.accept("EPHEMERAL")) {
|
|
260
327
|
const t = this.peek();
|
|
261
328
|
const hasExpr = t && !(t.kind === "punct" && (t.text === "," || t.text === ")")) && !kw(t, ...STOP);
|
|
262
|
-
col.default = { kind: "EPHEMERAL", expr: hasExpr ? this.
|
|
263
|
-
} else if (this.accept("COMMENT")) col.comment = this.
|
|
329
|
+
col.default = { kind: "EPHEMERAL", expr: hasExpr ? this.clauseExpr(STOP) : undefined };
|
|
330
|
+
} else if (this.accept("COMMENT")) col.comment = this.clauseExpr(STOP);
|
|
264
331
|
else if (this.accept("CODEC")) col.codec = this.parenthesized();
|
|
265
332
|
else if (this.accept("STATISTICS")) col.statistics = this.parenthesized();
|
|
266
|
-
else if (this.accept("TTL")) col.ttl = this.
|
|
333
|
+
else if (this.accept("TTL")) col.ttl = this.clauseExpr(STOP);
|
|
267
334
|
else if (this.accept("SETTINGS")) col.settings = this.parenthesized();
|
|
268
335
|
else if (kw(this.peek(), "PRIMARY") && kw(this.peek(1), "KEY")) {
|
|
269
336
|
this.p += 2;
|
|
270
337
|
col.primaryKey = true;
|
|
271
|
-
} else break;
|
|
338
|
+
} else if (!this.unknownClause(COLUMN_WORDS, { inList: true, column: text || undefined })) break;
|
|
272
339
|
}
|
|
273
340
|
return col;
|
|
274
341
|
}
|
|
@@ -360,7 +427,7 @@ class Parser extends SqlCursor {
|
|
|
360
427
|
for (;;) {
|
|
361
428
|
const key = this.name("a setting name").text;
|
|
362
429
|
this.expectPunct("=");
|
|
363
|
-
out.push({ key, value: this.
|
|
430
|
+
out.push({ key, value: this.clauseExpr(stop) });
|
|
364
431
|
if (!this.acceptPunct(",")) return out;
|
|
365
432
|
}
|
|
366
433
|
}
|
|
@@ -372,27 +439,33 @@ class Parser extends SqlCursor {
|
|
|
372
439
|
if (this.accept("ENGINE")) into.engine = this.engine();
|
|
373
440
|
else if (kw(this.peek(), "ORDER") && kw(this.peek(1), "BY")) {
|
|
374
441
|
this.p += 2;
|
|
375
|
-
into.orderBy = this.
|
|
442
|
+
into.orderBy = this.clauseExpr(STOP, false);
|
|
376
443
|
} else if (kw(this.peek(), "PRIMARY") && kw(this.peek(1), "KEY")) {
|
|
377
444
|
this.p += 2;
|
|
378
|
-
into.primaryKey = this.
|
|
445
|
+
into.primaryKey = this.clauseExpr(STOP, false);
|
|
379
446
|
} else if (kw(this.peek(), "PARTITION") && kw(this.peek(1), "BY")) {
|
|
380
447
|
this.p += 2;
|
|
381
|
-
into.partitionBy = this.
|
|
448
|
+
into.partitionBy = this.clauseExpr(STOP, false);
|
|
382
449
|
} else if (kw(this.peek(), "SAMPLE") && kw(this.peek(1), "BY")) {
|
|
383
450
|
this.p += 2;
|
|
384
|
-
into.sampleBy = this.
|
|
385
|
-
} else if (this.accept("TTL")) into.ttl = this.
|
|
451
|
+
into.sampleBy = this.clauseExpr(STOP, false);
|
|
452
|
+
} else if (this.accept("TTL")) into.ttl = this.clauseExpr(STOP, false);
|
|
386
453
|
else if (kw(this.peek(), "SETTINGS")) {
|
|
387
454
|
const from = this.idx();
|
|
388
455
|
this.p++;
|
|
389
456
|
into.settings = this.settingsList(STOP);
|
|
390
457
|
into.settingsClause = this.span(from, into.settings.flatMap((s) => s.value.refs));
|
|
391
|
-
} else return;
|
|
458
|
+
} else if (!this.unknownClause(STOP)) return;
|
|
392
459
|
}
|
|
393
460
|
}
|
|
394
461
|
|
|
395
462
|
parse(): CreateNode {
|
|
463
|
+
const node = this.statement();
|
|
464
|
+
if (this.unknownClauses.length > 0) node.unknown = [...this.unknownClauses];
|
|
465
|
+
return node;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private statement(): CreateNode {
|
|
396
469
|
this.expect("CREATE");
|
|
397
470
|
let orReplace = false;
|
|
398
471
|
if (this.accept("OR")) {
|
|
@@ -429,7 +502,9 @@ class Parser extends SqlCursor {
|
|
|
429
502
|
if (kw(this.peek(), "UUID") && this.peek(1)?.kind === "string") this.p += 2;
|
|
430
503
|
}
|
|
431
504
|
|
|
432
|
-
|
|
505
|
+
/** The statement's end, after any trailing clauses the parser does not know (#3872); `known` are the statement's own clause words. */
|
|
506
|
+
private finish(what: string, known: readonly string[] = []): void {
|
|
507
|
+
while (this.unknownClause(known));
|
|
433
508
|
this.acceptPunct(";");
|
|
434
509
|
if (this.peek() !== undefined) this.fail(`unexpected token after the ${what}`);
|
|
435
510
|
}
|
|
@@ -438,10 +513,15 @@ class Parser extends SqlCursor {
|
|
|
438
513
|
const node: DatabaseNode = { statement: "database", ifNotExists: this.ifNotExists(), name: this.qualifiedName() };
|
|
439
514
|
this.uuid();
|
|
440
515
|
node.onCluster = this.onCluster();
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
516
|
+
const CLAUSES = ["ENGINE", "SETTINGS", "COMMENT"];
|
|
517
|
+
for (;;) {
|
|
518
|
+
// In the server's order: ENGINE, SETTINGS, COMMENT, with clauses chant does not know between them.
|
|
519
|
+
if (!node.engine && !node.settings && !node.comment && this.accept("ENGINE")) node.engine = this.engine();
|
|
520
|
+
else if (!node.settings && !node.comment && this.accept("SETTINGS")) node.settings = this.settingsList(CLAUSES);
|
|
521
|
+
else if (!node.comment && this.accept("COMMENT")) node.comment = this.clauseExpr();
|
|
522
|
+
else if (!this.unknownClause(CLAUSES)) break;
|
|
523
|
+
}
|
|
524
|
+
this.finish("database definition", CLAUSES);
|
|
445
525
|
return node;
|
|
446
526
|
}
|
|
447
527
|
|
|
@@ -463,8 +543,8 @@ class Parser extends SqlCursor {
|
|
|
463
543
|
if (!this.isPunct("(")) this.fail("expected the column list (CREATE TABLE ... AS is not declared here)");
|
|
464
544
|
this.tableElements(node, true);
|
|
465
545
|
this.storage(node, []);
|
|
466
|
-
if (this.accept("COMMENT")) node.comment = this.
|
|
467
|
-
this.finish("table definition");
|
|
546
|
+
if (this.accept("COMMENT")) node.comment = this.clauseExpr();
|
|
547
|
+
this.finish("table definition", STORAGE_KEYWORDS);
|
|
468
548
|
return node;
|
|
469
549
|
}
|
|
470
550
|
|
|
@@ -473,10 +553,10 @@ class Parser extends SqlCursor {
|
|
|
473
553
|
const STOP = ["DEFAULT", "EXPRESSION", "HIERARCHICAL", "BIDIRECTIONAL", "INJECTIVE", "IS_OBJECT_ID"];
|
|
474
554
|
const attr: DictionaryAttributeNode = { name: text, nameSpan: span, type: this.typeSpan(), flags: [] };
|
|
475
555
|
for (;;) {
|
|
476
|
-
if (this.accept("DEFAULT")) attr.default = this.
|
|
477
|
-
else if (this.accept("EXPRESSION")) attr.expression = this.
|
|
556
|
+
if (this.accept("DEFAULT")) attr.default = this.clauseExpr(STOP);
|
|
557
|
+
else if (this.accept("EXPRESSION")) attr.expression = this.clauseExpr(STOP);
|
|
478
558
|
else if (kw(this.peek(), "HIERARCHICAL", "BIDIRECTIONAL", "INJECTIVE", "IS_OBJECT_ID")) attr.flags.push(this.next().text.toUpperCase());
|
|
479
|
-
else return attr;
|
|
559
|
+
else if (!this.unknownClause(STOP, { inList: true, column: text || undefined })) return attr;
|
|
480
560
|
}
|
|
481
561
|
}
|
|
482
562
|
|
|
@@ -499,19 +579,19 @@ class Parser extends SqlCursor {
|
|
|
499
579
|
for (;;) {
|
|
500
580
|
if (kw(this.peek(), "PRIMARY") && kw(this.peek(1), "KEY")) {
|
|
501
581
|
this.p += 2;
|
|
502
|
-
node.primaryKey = this.
|
|
582
|
+
node.primaryKey = this.clauseExpr(CLAUSES, false);
|
|
503
583
|
} else if (this.accept("SOURCE")) node.source = this.parenthesized();
|
|
504
584
|
else if (this.accept("LAYOUT")) node.layout = this.parenthesized();
|
|
505
585
|
else if (this.accept("LIFETIME")) node.lifetime = this.parenthesized();
|
|
506
586
|
else if (this.accept("RANGE")) node.range = this.parenthesized();
|
|
507
587
|
else if (this.accept("SETTINGS")) node.settings = this.parenthesized();
|
|
508
|
-
else if (this.accept("COMMENT")) node.comment = this.
|
|
509
|
-
else break;
|
|
588
|
+
else if (this.accept("COMMENT")) node.comment = this.clauseExpr();
|
|
589
|
+
else if (!this.unknownClause(CLAUSES)) break;
|
|
510
590
|
}
|
|
511
591
|
if (!node.primaryKey) this.fail("expected PRIMARY KEY: a dictionary needs its key columns");
|
|
512
592
|
if (!node.source) this.fail("expected SOURCE(...): a dictionary needs a source");
|
|
513
593
|
if (!node.layout) this.fail("expected LAYOUT(...): a dictionary needs a layout");
|
|
514
|
-
this.finish("dictionary definition");
|
|
594
|
+
this.finish("dictionary definition", CLAUSES);
|
|
515
595
|
return node;
|
|
516
596
|
}
|
|
517
597
|
|
|
@@ -573,10 +653,12 @@ class Parser extends SqlCursor {
|
|
|
573
653
|
this.tableElements(holder, false);
|
|
574
654
|
node.columnsSpan = this.span(from, []);
|
|
575
655
|
}
|
|
576
|
-
|
|
656
|
+
const BEFORE_AS = ["AS", "POPULATE", "EMPTY", "DEFINER", "SQL"];
|
|
657
|
+
this.storage(node, BEFORE_AS);
|
|
577
658
|
if (this.accept("POPULATE")) node.populate = true;
|
|
578
659
|
if (this.accept("EMPTY")) node.empty = true;
|
|
579
660
|
if (kw(this.peek(), "DEFINER", "SQL")) node.security = this.expr(["AS"], false);
|
|
661
|
+
while (!kw(this.peek(), "AS") && this.unknownClause([...STORAGE_KEYWORDS, ...BEFORE_AS]));
|
|
580
662
|
this.expect("AS");
|
|
581
663
|
node.select = this.expr(["COMMENT"], false);
|
|
582
664
|
if (this.accept("COMMENT")) node.comment = this.expr();
|
|
@@ -129,7 +129,26 @@ function diffMap(key: string, field: string, before: Record<string, string>, aft
|
|
|
129
129
|
for (const name of changedEntries(before, after)) out.push(change(key, `${field}.${name}`, rule, before[name], after[name]));
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The clauses chant does not read (#3872), compared as text. One the
|
|
134
|
+
* declaration has and the server does not, or has differently, is a change
|
|
135
|
+
* to review (SQLCH290). One only the server prints is a hint, not a change: a
|
|
136
|
+
* clause a new server prints with its default cannot be told from one
|
|
137
|
+
* removed from the declaration.
|
|
138
|
+
*/
|
|
139
|
+
function diffUnparsed(key: string, before: CanonicalObject, after: CanonicalObject, out: Change[], hints: string[]): void {
|
|
140
|
+
const b = before.unparsed ?? {};
|
|
141
|
+
const a = after.unparsed ?? {};
|
|
142
|
+
for (const clause of Object.keys(a)) {
|
|
143
|
+
if (b[clause] !== a[clause]) out.push(change(key, `unparsed.${clause}`, "SQLCH290", b[clause], a[clause], { note: "chant does not read this clause; review the change" }));
|
|
144
|
+
}
|
|
145
|
+
for (const clause of Object.keys(b)) {
|
|
146
|
+
if (!(clause in a)) hints.push(`${key}: the current definition has ${clause} (${b[clause]}), a clause chant does not read and the declaration does not have; it is not compared`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
132
150
|
function diffObject(key: string, before: CanonicalObject, after: CanonicalObject, out: Change[], hints: string[]): void {
|
|
151
|
+
diffUnparsed(key, before, after, out, hints);
|
|
133
152
|
if (before.kind !== after.kind) {
|
|
134
153
|
out.push(change(key, "kind", "SQLCH224", before.kind, after.kind, { destructive: before.kind === "table" }));
|
|
135
154
|
return;
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* - a codec's default level goes (`ZSTD(1)` is `ZSTD`, `Delta(4)` is `Delta`);
|
|
20
20
|
* - a TTL rule's trailing `DELETE` goes;
|
|
21
21
|
* - a setting at the pinned server's default goes;
|
|
22
|
-
* - a view's column list is not compared: the server infers it
|
|
22
|
+
* - a view's column list is not compared: the server infers it;
|
|
23
|
+
* - a clause the parser does not know (#3872) is kept as an expression, by
|
|
24
|
+
* its name, and compared as one.
|
|
23
25
|
*
|
|
24
26
|
* What the rules cannot see is the server's own expression formatting
|
|
25
27
|
* (`a+b*2` prints as `a + (b * 2)`). When a plan reads a live server it asks
|
|
@@ -32,7 +34,7 @@
|
|
|
32
34
|
*/
|
|
33
35
|
|
|
34
36
|
import { isTrivia, tokenizeText, type Token } from "../tokens";
|
|
35
|
-
import { parseCreate, unquote, type ColumnNode, type CreateNode, type DictionaryAttributeNode, type DictionaryNode, type Span } from "../parser";
|
|
37
|
+
import { parseCreate, unquote, type ColumnNode, type CreateNode, type DictionaryAttributeNode, type DictionaryNode, type Span, type UnknownClause } from "../parser";
|
|
36
38
|
import { MERGE_TREE_SETTINGS, TYPE_FAMILIES } from "../../generated/clickhouse";
|
|
37
39
|
import { previouslyIn, splitTopLevel as splitTop } from "../../core/normalize";
|
|
38
40
|
import { accessCanonical, grantsCanonical, isAccessStatement } from "../access";
|
|
@@ -100,6 +102,14 @@ export interface CanonicalObject {
|
|
|
100
102
|
previously?: string;
|
|
101
103
|
/** The tool that keeps a live table (a migration runner's history table): a plan never drops it. */
|
|
102
104
|
foreign?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Clauses the parser does not know (#3872), each canonical as an
|
|
107
|
+
* expression, keyed by the clause (`columns.<name>.<clause>` for a
|
|
108
|
+
* column's). Absent when there are none. The rest of the object is read
|
|
109
|
+
* as usual; these are compared as text, and a plan classes a change in
|
|
110
|
+
* one as a change to review.
|
|
111
|
+
*/
|
|
112
|
+
unparsed?: Record<string, string>;
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
const KEYWORDS = new Set(
|
|
@@ -305,6 +315,8 @@ export function canonicalObject(ddl: string, defaultDatabase = "default"): Canon
|
|
|
305
315
|
};
|
|
306
316
|
const previously = previouslyIn(leading);
|
|
307
317
|
if (previously) obj.previously = previously;
|
|
318
|
+
const unparsed = unparsedOf(tokens, node.unknown, database);
|
|
319
|
+
if (unparsed) obj.unparsed = unparsed;
|
|
308
320
|
|
|
309
321
|
const comment0 = stringValue(spanText(tokens, node.comment));
|
|
310
322
|
if (node.statement === "dictionary") {
|
|
@@ -379,6 +391,19 @@ export function canonicalObject(ddl: string, defaultDatabase = "default"): Canon
|
|
|
379
391
|
return obj;
|
|
380
392
|
}
|
|
381
393
|
|
|
394
|
+
/** A statement's unknown clauses (#3872), by clause, each canonical as an expression; a second of the same name is `CLAUSE#2`. */
|
|
395
|
+
function unparsedOf(tokens: Token[], unknown: readonly UnknownClause[] | undefined, database: string | undefined): Record<string, string> | undefined {
|
|
396
|
+
if (!unknown || unknown.length === 0) return undefined;
|
|
397
|
+
const out: Record<string, string> = {};
|
|
398
|
+
for (const u of unknown) {
|
|
399
|
+
const base = u.column !== undefined ? `columns.${u.column}.${u.clause}` : u.clause;
|
|
400
|
+
let key = base;
|
|
401
|
+
for (let n = 2; key in out; n++) key = `${base}#${n}`;
|
|
402
|
+
out[key] = canonicalExpression(spanText(tokens, u.span) ?? "", database);
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
382
407
|
// ── dictionaries ──────────────────────────────────────────────────────
|
|
383
408
|
|
|
384
409
|
/**
|
|
@@ -37,7 +37,8 @@ const TIME_TYPE = /^(?:Nullable\s*\(\s*)?(?:Date|Date32|DateTime|DateTime64)\b/;
|
|
|
37
37
|
*/
|
|
38
38
|
export function rebuildOpSuggestions(diff: SchemaDiff, declared: ReadonlyMap<string, CanonicalObject>, env: string): RebuildOpSuggestion[] {
|
|
39
39
|
const out: RebuildOpSuggestion[] = [];
|
|
40
|
-
|
|
40
|
+
// A clause chant does not read (SQLCH290) is refused for review, not because a rebuild is known to be what it takes.
|
|
41
|
+
for (const key of [...new Set(diff.rebuilds.filter((c) => c.rule !== "SQLCH290").map((c) => c.object))]) {
|
|
41
42
|
const c = declared.get(key);
|
|
42
43
|
if (!c || c.kind !== "table") continue;
|
|
43
44
|
const table = `${c.database ?? "default"}.${c.name}`;
|
|
@@ -278,6 +278,13 @@ export const CLASSIFIER_RULES = {
|
|
|
278
278
|
"REVOKE takes a privilege or a role away, including one granted by hand to a grantee the build declares grants for; queries that relied on it fail from then on.",
|
|
279
279
|
`${DOCS}/revoke`,
|
|
280
280
|
),
|
|
281
|
+
SQLCH290: rule(
|
|
282
|
+
"SQLCH290",
|
|
283
|
+
"rebuild",
|
|
284
|
+
"Change a clause chant does not read",
|
|
285
|
+
"chant's parser does not know this clause, so it cannot tell whether ALTER can make the change in place; the plan refuses it rather than guess. Review it: make it by hand with the ALTER the server documents for the clause, or drop and create the object.",
|
|
286
|
+
`${DOCS}/create/table`,
|
|
287
|
+
),
|
|
281
288
|
SQLCH250: rule("SQLCH250", "drop", "Drop an object", "DROP removes the object and, for a table, its data; it is not undone.", `${DOCS}/drop`),
|
|
282
289
|
} as const satisfies Record<string, ClassifierRule>;
|
|
283
290
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"release": "v26.8.15.10-lts",
|
|
3
|
+
"unknownClauses": [
|
|
4
|
+
"APPEND TO",
|
|
5
|
+
"AUTO_INCREMENT",
|
|
6
|
+
"CHAR",
|
|
7
|
+
"CHAR VARYING",
|
|
8
|
+
"CHARACTER",
|
|
9
|
+
"CHARACTER LARGE OBJECT",
|
|
10
|
+
"CHARACTER VARYING",
|
|
11
|
+
"DEFINER",
|
|
12
|
+
"DEPENDS ON",
|
|
13
|
+
"FORMAT",
|
|
14
|
+
"FORMAT NULL",
|
|
15
|
+
"HAVE",
|
|
16
|
+
"REFERENCES",
|
|
17
|
+
"SQL SECURITY INVOKER",
|
|
18
|
+
"SQL SECURITY NONE",
|
|
19
|
+
"STRING",
|
|
20
|
+
"UNIQUE KEY",
|
|
21
|
+
"UUID",
|
|
22
|
+
"YEARS"
|
|
23
|
+
],
|
|
24
|
+
"failures": [
|
|
25
|
+
"expected ')' at 'WITH'",
|
|
26
|
+
"expected '='",
|
|
27
|
+
"expected '=' at '('",
|
|
28
|
+
"expected '=' at ','",
|
|
29
|
+
"expected -> and the function's expression at '('",
|
|
30
|
+
"expected AS at 'COMMENT'",
|
|
31
|
+
"expected AS at 'LANGUAGE'",
|
|
32
|
+
"expected AS at 'WASM'",
|
|
33
|
+
"expected PRIMARY KEY: a dictionary needs its key columns",
|
|
34
|
+
"expected TYPE at ')'",
|
|
35
|
+
"expected a clause (CREATE starts a query or a statement, which is not declared here) at 'CREATE'",
|
|
36
|
+
"expected a column name at '“'",
|
|
37
|
+
"expected a name at '{'",
|
|
38
|
+
"expected a setting name at ''John''",
|
|
39
|
+
"expected a setting name at ''val-''",
|
|
40
|
+
"expected a setting name at '*+'",
|
|
41
|
+
"expected a setting name at '1'",
|
|
42
|
+
"expected a setting name at '3628718300'",
|
|
43
|
+
"expected a type at '123'",
|
|
44
|
+
"expected a type at '`Int64`'",
|
|
45
|
+
"expected a type at '`Null`'",
|
|
46
|
+
"expected a type at '`a.b`'",
|
|
47
|
+
"expected a type at '`ab`'",
|
|
48
|
+
"expected an expression",
|
|
49
|
+
"expected an expression at ')'",
|
|
50
|
+
"expected an expression at 'TYPE'",
|
|
51
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'AS'",
|
|
52
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'CLONE'",
|
|
53
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'COMMENT'",
|
|
54
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'ENGINE'",
|
|
55
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'Engine'",
|
|
56
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'ORDER'",
|
|
57
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'SQL'",
|
|
58
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'TO'",
|
|
59
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'as'",
|
|
60
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'empty'",
|
|
61
|
+
"expected the column list (CREATE TABLE ... AS is not declared here) at 'engine'",
|
|
62
|
+
"unexpected end of statement",
|
|
63
|
+
"unexpected token after the table definition at 'AS'",
|
|
64
|
+
"unexpected token after the table definition at 'SETTINGS'",
|
|
65
|
+
"unexpected token after the table definition at 'as'"
|
|
66
|
+
]
|
|
67
|
+
}
|
package/src/clickhouse.ts
CHANGED
|
@@ -106,3 +106,13 @@ export {
|
|
|
106
106
|
type Topology,
|
|
107
107
|
type TopologyKind,
|
|
108
108
|
} from "./clickhouse/topology";
|
|
109
|
+
|
|
110
|
+
// The build's ClickHouse objects as typed values, for a project's own
|
|
111
|
+
// `lint.policies` (#3870); also at `@intentius/chant-lexicon-sql/read`.
|
|
112
|
+
export {
|
|
113
|
+
readClickHouse,
|
|
114
|
+
type ClickHouseRead,
|
|
115
|
+
type ClickHouseTableInfo,
|
|
116
|
+
type ClickHouseColumnInfo,
|
|
117
|
+
type ClickHouseViewInfo,
|
|
118
|
+
} from "./read/clickhouse";
|
|
@@ -49,6 +49,8 @@ Both exit 2 when a change needs a rebuild. \`chant sql diff\` compares a pull re
|
|
|
49
49
|
|
|
50
50
|
\`chant sql plan\` also exits 2 when a declared database holds an object whose definition it cannot read. It names each one under \`Refused\` (\`unreadable\` in \`--json\`) instead of leaving it out. Dictionaries are read and planned like the other objects: any change but a comment replaces one (SQLCH245).
|
|
51
51
|
|
|
52
|
+
A clause chant's parser does not know ([Plain .sql Files](../sql-files/#clauses-chant-does-not-read)) is compared as text, and the rest of its object is planned as usual. A change to one is SQLCH290, refused for review with no rebuild Op suggested, since chant cannot tell what the change takes. A clause only the server prints is a hint, not a change: a newer server printing a clause with its default looks the same as a clause removed from the declaration.
|
|
53
|
+
|
|
52
54
|
## After the plan
|
|
53
55
|
|
|
54
56
|
The plan only reports. [\`clickhouseApply\`](../applying/) makes the metadata-only and background-rewrite changes, and refuses a rebuild the same way the plan does, sending nothing for that object. A rebuild runs as \`ClickHouseRebuildOp\`, a gated migration Op that creates the new table, backfills it, verifies it and swaps it in: see [Rebuilding a Table](../rebuild/). Both commands print the Op declaration to start from for each refused table.
|
|
@@ -44,6 +44,8 @@ chant sql plan prod schema.json # a build output against the prod server
|
|
|
44
44
|
|
|
45
45
|
Both read the build's \`dialect\` and exit 2 when a change can only be made as expand and contract. \`--json\` prints the changes, the hints, the refused changes and the migration Op suggestions as one document. \`chant sql diff\` compares a pull request's base and head builds with no server. \`chant sql plan\` reads the server \`sql.profiles.<env>\` binds (else \`POSTGRES_URL\`), in the profile's \`schemas\` when it lists them, otherwise in the default schema and every schema the build declares an object in. Where the normalization rules leave an expression different, it asks that server: the declared view, or the table's column types, defaults, generated expressions and checks, are created as temporary objects in a transaction that is always rolled back, and read back with the same printers, so a view's \`SELECT *\` and the server's added parentheses are not reported as changes.
|
|
46
46
|
|
|
47
|
+
A clause chant's parser does not know ([Plain .sql Files](../sql-files/#clauses-chant-does-not-read)) is compared as text, and the rest of its object is planned as usual. A change to one is SQLPG299, refused for review with no migration Op suggested, since chant cannot tell which lock the change takes. A clause only the server prints is a hint, not a change.
|
|
48
|
+
|
|
47
49
|
## Statements for a migration file
|
|
48
50
|
|
|
49
51
|
\`chant sql diff base.json head.json --statements\` prints the statements that take the base schema to the head one, offline: each one the applier would send for the same change, after a comment naming its object, rule and class, and whether it has to run outside a transaction block. \`--json\` prints them as one document, which \`diffStatements(before, after)\` from \`@intentius/chant-lexicon-sql\` also returns, each statement with \`transactional\` set:
|