clickhouse-migrations 1.4.0 → 1.5.0
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/README.md +28 -1
- package/lib/migrate.js +11 -8
- package/lib/sql-parse.d.ts +6 -4
- package/lib/sql-parse.js +81 -29
- package/lib/sql-split.d.ts +3 -0
- package/lib/sql-split.js +213 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@ Create a directory, where migrations will be stored. It will be used as the valu
|
|
|
14
14
|
|
|
15
15
|
In the directory, create migration files, which should be named like this: `1_some_text.sql`, `2_other_text.sql`, `10_more_test.sql`. What's important here is that the migration version number should come first, followed by an underscore (`_`), and then any text can follow. The version number should increase for every next migration. Please note that once a migration file has been applied to the database, it cannot be modified or removed.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Migration files contain ClickHouse SQL statements separated by semicolons (`;`). Semicolons and comment markers inside single-quoted strings, quoted identifiers (`"..."`, `` `...` ``), and dollar-quoted strings (`$tag$...$tag$` or `$$...$$`) are preserved. Whitespace inside quoted text is also preserved. In SQL, comments (`--`, `//`, `# `, `#!`, and nested `/* ... */`) are removed, and whitespace outside quoted text is collapsed. Inline formatted data is preserved separately as described below.
|
|
18
|
+
|
|
19
|
+
See [SQL execution and settings](#sql-execution-and-settings) below for details on settings, supported migration content, and error handling.
|
|
18
20
|
|
|
19
21
|
If the database provided in the `--db` option (or in `CH_MIGRATIONS_DB`) doesn't exist, it will be created automatically. To disable this behavior (for example, when the user has no privileges to create databases), use the `--skip-db-creation` option (or set `CH_MIGRATIONS_SKIP_DB_CREATION` to `'true'`); in that case the database must already exist.
|
|
20
22
|
|
|
@@ -150,3 +152,28 @@ LAYOUT(COMPLEX_KEY_HASHED());
|
|
|
150
152
|
```
|
|
151
153
|
|
|
152
154
|
_Please note:_ When substitution is enabled, the checksum is taken from the **raw file**, before substitution. This keeps a migration stable across environments and keeps secrets out of `_migrations`. Two consequences follow: an already-applied migration is not re-run when a variable changes (ship a new migration instead), and the substituted SQL still reaches ClickHouse, so it may appear in `system.query_log` or server logs.
|
|
155
|
+
|
|
156
|
+
## SQL execution and settings
|
|
157
|
+
|
|
158
|
+
Queries execute in file order. Assignment-style `SET name = value` statements are collected before executing the file: the last assignment to each setting applies to **every query in that file**, including queries written before the `SET`. This preserves the package's existing behavior. Settings do not carry over to the next migration file.
|
|
159
|
+
|
|
160
|
+
Settings are sent with each HTTP request, overriding defaults supplied in the host URL. This works without shared session state when a load balancer routes requests to different servers, and also supports HTTP parameters such as `wait_end_of_query`.
|
|
161
|
+
|
|
162
|
+
Use assignment-style `SET` commands for settings and query parameters. Quoted string values, multiline assignments and compound parameter values are supported; the available setting names and values depend on your ClickHouse version:
|
|
163
|
+
|
|
164
|
+
```sql
|
|
165
|
+
SET max_threads = 2,
|
|
166
|
+
log_comment = 'migration; keep this text';
|
|
167
|
+
SET wait_end_of_query = 1;
|
|
168
|
+
SET session_timezone = 'UTC';
|
|
169
|
+
SET param_d = {'10': [11, 12], '13': [14, 15]};
|
|
170
|
+
SELECT {d:Map(String, Array(UInt8))};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
For a setting specific to one query, use that query's `SETTINGS` clause where ClickHouse supports it. Commands requiring shared session state, such as `SET ROLE` followed by another query, are not supported. Use `SET session_timezone = 'UTC'` to configure the timezone for the file.
|
|
174
|
+
|
|
175
|
+
Inline `INSERT ... FORMAT` data is accepted, including `INSERT ... SELECT ... FROM input(...) FORMAT ...`. `FORMAT Values` uses the same quote handling as `INSERT ... VALUES`. For other formats, everything after the format name is preserved verbatim up to the next semicolon or the end of the file: tabs, newlines, quotes and comment markers remain data.
|
|
176
|
+
|
|
177
|
+
Raw formats retain the legacy semicolon delimiter, even inside quoted data. Payloads containing semicolons should use `INSERT ... VALUES` or be loaded separately. ClickHouse validates the data format; this package does not parse CSV, JSON or other raw formats.
|
|
178
|
+
|
|
179
|
+
Unterminated SQL quotes/comments and malformed setting assignments cause an error identifying the migration file before any query in that file executes. ClickHouse validates SQL syntax, setting names, setting values and inline data when each query runs. Database setup and earlier files may already have run, and server-side errors can still leave a file partially applied. Where appropriate, use idempotent statements such as `CREATE TABLE IF NOT EXISTS ...`.
|
package/lib/migrate.js
CHANGED
|
@@ -201,24 +201,27 @@ const apply_migrations = async (client, migrations, migrations_home) => {
|
|
|
201
201
|
// Skip if a migration is already applied.
|
|
202
202
|
continue;
|
|
203
203
|
}
|
|
204
|
-
//
|
|
205
|
-
// checksum above
|
|
206
|
-
let
|
|
204
|
+
// Parse the entire file before executing its statements. Substitution is
|
|
205
|
+
// opt-in; the checksum above always uses the original file content.
|
|
206
|
+
let queries;
|
|
207
|
+
let settings;
|
|
207
208
|
try {
|
|
208
|
-
|
|
209
|
+
({ queries, settings } = (0, sql_parse_1.parse_migration_sql)((0, sql_parse_1.substitute_env)(content)));
|
|
209
210
|
}
|
|
210
211
|
catch (e) {
|
|
211
212
|
log('error', `the migration ${migration.file} has an error.`, e instanceof Error ? e.message : String(e));
|
|
212
213
|
process.exit(1);
|
|
213
214
|
}
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
// Keep the established file-wide SET behavior: the last assignment wins,
|
|
216
|
+
// including for queries earlier in the file. Send settings on every request
|
|
217
|
+
// instead of using sessions, whose state is lost if a load balancer routes
|
|
218
|
+
// the next query to another server. Per-query settings also override host
|
|
219
|
+
// URL defaults and support HTTP parameters such as wait_end_of_query.
|
|
217
220
|
for (const query of queries) {
|
|
218
221
|
try {
|
|
219
222
|
await client.command({
|
|
220
223
|
query: query,
|
|
221
|
-
clickhouse_settings:
|
|
224
|
+
clickhouse_settings: settings,
|
|
222
225
|
});
|
|
223
226
|
}
|
|
224
227
|
catch (e) {
|
package/lib/sql-parse.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
declare const substitute_env: (content: string) => string;
|
|
2
|
-
declare const
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
declare const parse_migration_sql: (content: string) => {
|
|
3
|
+
queries: string[];
|
|
4
|
+
settings: Record<string, string>;
|
|
5
5
|
};
|
|
6
|
-
|
|
6
|
+
declare const sql_queries: (content: string) => string[];
|
|
7
|
+
declare const sql_sets: (content: string) => Record<string, string>;
|
|
8
|
+
export { parse_migration_sql, sql_queries, sql_sets, substitute_env };
|
package/lib/sql-parse.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.substitute_env = exports.sql_sets = exports.sql_queries = void 0;
|
|
3
|
+
exports.substitute_env = exports.sql_sets = exports.sql_queries = exports.parse_migration_sql = void 0;
|
|
4
|
+
const sql_split_1 = require("./sql-split");
|
|
4
5
|
// Replace ${NAME} in migration content with env vars. Opt-in via
|
|
5
6
|
// CH_MIGRATIONS_SUBSTITUTE_ENV=true; otherwise returns content unchanged.
|
|
6
7
|
// Use $${NAME} to keep a literal ${NAME}. Throws on an unset variable or a
|
|
@@ -51,35 +52,86 @@ const substitute_env = (content) => {
|
|
|
51
52
|
return result;
|
|
52
53
|
};
|
|
53
54
|
exports.substitute_env = substitute_env;
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
// HTTP settings take values, not SQL string-literal syntax. Decode quoted
|
|
56
|
+
// strings once; leave numbers and compound parameter values as text so large
|
|
57
|
+
// integers never lose precision through JavaScript Number conversion.
|
|
58
|
+
const setting_value = (raw) => {
|
|
59
|
+
const tag = /^\$[A-Za-z0-9_]*\$/.exec(raw)?.[0];
|
|
60
|
+
if (tag && raw.endsWith(tag))
|
|
61
|
+
return raw.slice(tag.length, -tag.length);
|
|
62
|
+
const quote = raw[0];
|
|
63
|
+
if (!["'", '"', '`'].includes(quote) || !raw.endsWith(quote))
|
|
64
|
+
return raw;
|
|
65
|
+
// Hex escapes represent bytes; decode UTF-8 after unescaping so \xC3\xA9 is é.
|
|
66
|
+
const input = Buffer.from(raw.slice(1, -1));
|
|
67
|
+
const bytes = [];
|
|
68
|
+
const escapes = {
|
|
69
|
+
'0': 0,
|
|
70
|
+
a: 7,
|
|
71
|
+
b: 8,
|
|
72
|
+
e: 27,
|
|
73
|
+
f: 12,
|
|
74
|
+
n: 10,
|
|
75
|
+
r: 13,
|
|
76
|
+
t: 9,
|
|
77
|
+
v: 11,
|
|
78
|
+
"'": 39,
|
|
79
|
+
'"': 34,
|
|
80
|
+
'`': 96,
|
|
81
|
+
'\\': 92,
|
|
82
|
+
'/': 47,
|
|
83
|
+
'=': 61,
|
|
84
|
+
};
|
|
85
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
86
|
+
const byte = input[i];
|
|
87
|
+
if (byte === quote.charCodeAt(0) && input[i + 1] === byte) {
|
|
88
|
+
i += 1;
|
|
89
|
+
}
|
|
90
|
+
else if (byte === 92 && i + 1 < input.length) {
|
|
91
|
+
const next = String.fromCharCode(input[i + 1]);
|
|
92
|
+
const hex = input.subarray(i + 2, i + 4).toString();
|
|
93
|
+
if (next === 'x' && /^[0-9a-f]{2}$/i.test(hex)) {
|
|
94
|
+
bytes.push(parseInt(hex, 16));
|
|
95
|
+
i += 3;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (Object.hasOwn(escapes, next)) {
|
|
99
|
+
bytes.push(escapes[next]);
|
|
100
|
+
i += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (next === 'N') {
|
|
104
|
+
i += 1; // ClickHouse's \N escape is an empty string inside a literal.
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
bytes.push(byte);
|
|
109
|
+
}
|
|
110
|
+
return Buffer.from(bytes).toString('utf8');
|
|
65
111
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
const parse_migration_sql = (content) => {
|
|
113
|
+
const queries = [];
|
|
114
|
+
const settings = {};
|
|
115
|
+
for (const statement of (0, sql_split_1.split_statements)(content)) {
|
|
116
|
+
// Only assignment-style SET belongs to file-wide settings. Administrative
|
|
117
|
+
// statements such as SET DEFAULT ROLE remain SQL for ClickHouse to execute.
|
|
118
|
+
const setting = /^SET\s+([A-Za-z_][A-Za-z0-9_]*\s*=[\s\S]*)$/i.exec(statement);
|
|
119
|
+
if (!setting) {
|
|
120
|
+
queries.push(statement);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
for (const assignment of (0, sql_split_1.split_assignments)(setting[1])) {
|
|
124
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]+)$/.exec(assignment);
|
|
125
|
+
if (!match)
|
|
126
|
+
throw new Error(`invalid SET assignment: ${assignment}`);
|
|
127
|
+
settings[match[1]] = setting_value(match[2].trim());
|
|
81
128
|
}
|
|
82
|
-
}
|
|
83
|
-
return
|
|
129
|
+
}
|
|
130
|
+
return { queries, settings };
|
|
84
131
|
};
|
|
132
|
+
exports.parse_migration_sql = parse_migration_sql;
|
|
133
|
+
// Preserve the existing helper exports for callers importing lib/sql-parse.
|
|
134
|
+
const sql_queries = (content) => parse_migration_sql(content).queries;
|
|
135
|
+
exports.sql_queries = sql_queries;
|
|
136
|
+
const sql_sets = (content) => parse_migration_sql(content).settings;
|
|
85
137
|
exports.sql_sets = sql_sets;
|
package/lib/sql-split.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.split_assignments = exports.split_statements = void 0;
|
|
4
|
+
// Return a location for error messages, for example: "line 12, column 7".
|
|
5
|
+
const describe_position = (content, index) => {
|
|
6
|
+
const before = content.slice(0, index);
|
|
7
|
+
const line = before.split(/\r\n|\r|\n/).length;
|
|
8
|
+
const lineStart = Math.max(before.lastIndexOf('\n'), before.lastIndexOf('\r')) + 1;
|
|
9
|
+
// Count code points, not UTF-16 units, so supplementary characters do not shift the column.
|
|
10
|
+
const column = Array.from(before.slice(lineStart)).length + 1;
|
|
11
|
+
return `line ${line}, column ${column}`;
|
|
12
|
+
};
|
|
13
|
+
const isSpace = (ch) => ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f' || ch === '\v';
|
|
14
|
+
// Both statements and SET assignments use the same quoting/comment rules.
|
|
15
|
+
const split_sql = (content, separator) => {
|
|
16
|
+
const statements = [];
|
|
17
|
+
let current = '';
|
|
18
|
+
let pendingSpace = false; // whitespace seen in plain SQL, emitted lazily as one space
|
|
19
|
+
let bracketDepth = 0;
|
|
20
|
+
let insertStatement = false;
|
|
21
|
+
let insertHeader = false;
|
|
22
|
+
let inputSource = false;
|
|
23
|
+
let inputFunctionExpected = false;
|
|
24
|
+
let formatExpected = false;
|
|
25
|
+
let previousToken = '';
|
|
26
|
+
const rememberToken = (token, quoted = false) => {
|
|
27
|
+
inputFunctionExpected = token === 'INPUT' && ['FROM', 'JOIN'].includes(previousToken);
|
|
28
|
+
previousToken = quoted ? 'quoted' : token;
|
|
29
|
+
};
|
|
30
|
+
const emitPlain = (text) => {
|
|
31
|
+
if (pendingSpace && current.length > 0) {
|
|
32
|
+
current += ' ';
|
|
33
|
+
}
|
|
34
|
+
pendingSpace = false;
|
|
35
|
+
current += text;
|
|
36
|
+
};
|
|
37
|
+
const flush = (preserveData = false) => {
|
|
38
|
+
const statement = preserveData ? current : current.trim();
|
|
39
|
+
if (statement || separator === ',')
|
|
40
|
+
statements.push(statement);
|
|
41
|
+
current = '';
|
|
42
|
+
pendingSpace = false;
|
|
43
|
+
bracketDepth = 0;
|
|
44
|
+
insertStatement = false;
|
|
45
|
+
insertHeader = false;
|
|
46
|
+
inputSource = false;
|
|
47
|
+
inputFunctionExpected = false;
|
|
48
|
+
formatExpected = false;
|
|
49
|
+
previousToken = '';
|
|
50
|
+
};
|
|
51
|
+
const finishFormat = (name, start) => {
|
|
52
|
+
formatExpected = false;
|
|
53
|
+
insertHeader = false;
|
|
54
|
+
if (name === 'VALUES')
|
|
55
|
+
return start; // VALUES uses SQL literals, including quoted semicolons.
|
|
56
|
+
// Other formats contain data, not SQL: preserve tabs, newlines, quotes and
|
|
57
|
+
// comment markers verbatim. Keep the legacy boundary: the next ';' or EOF.
|
|
58
|
+
// Semicolons inside raw data require a format-aware loader, outside this runner.
|
|
59
|
+
const semicolon = content.indexOf(';', start);
|
|
60
|
+
const end = semicolon === -1 ? content.length : semicolon;
|
|
61
|
+
current += content.slice(start, end);
|
|
62
|
+
flush(true);
|
|
63
|
+
return semicolon === -1 ? end : end + 1;
|
|
64
|
+
};
|
|
65
|
+
const n = content.length;
|
|
66
|
+
let i = 0;
|
|
67
|
+
while (i < n) {
|
|
68
|
+
const ch = content[i];
|
|
69
|
+
const next = i + 1 < n ? content[i + 1] : '';
|
|
70
|
+
// Line comments end at the next newline; markers inside quotes are copied below.
|
|
71
|
+
if ((ch === '-' && next === '-') ||
|
|
72
|
+
(ch === '/' && next === '/') ||
|
|
73
|
+
(ch === '#' && (next === '!' || next === '' || isSpace(next)))) {
|
|
74
|
+
while (i < n && content[i] !== '\n' && content[i] !== '\r') {
|
|
75
|
+
i += 1;
|
|
76
|
+
}
|
|
77
|
+
pendingSpace = true; // the comment stood between tokens; keep them apart
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// block comments, nested
|
|
81
|
+
if (ch === '/' && next === '*') {
|
|
82
|
+
const start = i;
|
|
83
|
+
let depth = 1;
|
|
84
|
+
i += 2;
|
|
85
|
+
while (i < n && depth > 0) {
|
|
86
|
+
if (content[i] === '/' && content[i + 1] === '*') {
|
|
87
|
+
depth += 1;
|
|
88
|
+
i += 2;
|
|
89
|
+
}
|
|
90
|
+
else if (content[i] === '*' && content[i + 1] === '/') {
|
|
91
|
+
depth -= 1;
|
|
92
|
+
i += 2;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
i += 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (depth > 0) {
|
|
99
|
+
throw new Error(`unterminated block comment starting at ${describe_position(content, start)}`);
|
|
100
|
+
}
|
|
101
|
+
pendingSpace = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// quoted strings and identifiers: copied verbatim, including the quotes
|
|
105
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
106
|
+
const quote = ch;
|
|
107
|
+
let j = i + 1;
|
|
108
|
+
while (j < n) {
|
|
109
|
+
const c = content[j];
|
|
110
|
+
if (c === '\\' && j + 1 < n) {
|
|
111
|
+
j += 2; // backslash escape: skip the escaped character whatever it is
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (c === quote) {
|
|
115
|
+
if (content[j + 1] === quote) {
|
|
116
|
+
j += 2; // doubled quote inside the literal
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
break; // closing quote
|
|
120
|
+
}
|
|
121
|
+
j += 1;
|
|
122
|
+
}
|
|
123
|
+
if (j >= n) {
|
|
124
|
+
const what = quote === "'" ? 'string literal' : 'quoted identifier';
|
|
125
|
+
throw new Error(`unterminated ${what} starting at ${describe_position(content, i)}`);
|
|
126
|
+
}
|
|
127
|
+
const end = j + 1; // include the closing quote
|
|
128
|
+
emitPlain(content.slice(i, end));
|
|
129
|
+
if (formatExpected && quote !== "'") {
|
|
130
|
+
i = finishFormat(content.slice(i + 1, j).toUpperCase(), end);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
rememberToken(quote === "'" ? 'quoted' : content.slice(i + 1, j).toUpperCase(), true);
|
|
134
|
+
formatExpected = false;
|
|
135
|
+
i = end;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// dollar-quoted strings: $tag$ ... $tag$ (tag may be empty)
|
|
139
|
+
if (ch === '$' && (i === 0 || !/[A-Za-z0-9_$]/.test(content[i - 1]))) {
|
|
140
|
+
const tagMatch = /^\$[A-Za-z0-9_]*\$/.exec(content.slice(i));
|
|
141
|
+
if (tagMatch) {
|
|
142
|
+
const opener = tagMatch[0];
|
|
143
|
+
const close = content.indexOf(opener, i + opener.length);
|
|
144
|
+
if (close === -1) {
|
|
145
|
+
throw new Error(`unterminated dollar-quoted string ${opener} starting at ${describe_position(content, i)}`);
|
|
146
|
+
}
|
|
147
|
+
const end = close + opener.length;
|
|
148
|
+
emitPlain(content.slice(i, end));
|
|
149
|
+
rememberToken('quoted');
|
|
150
|
+
formatExpected = false;
|
|
151
|
+
i = end;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Commas inside arrays, maps and tuples belong to the setting value.
|
|
156
|
+
if (ch === separator && (separator === ';' || bracketDepth === 0)) {
|
|
157
|
+
flush();
|
|
158
|
+
i += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// plain SQL
|
|
162
|
+
if (isSpace(ch)) {
|
|
163
|
+
pendingSpace = true;
|
|
164
|
+
i += 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Recognize data only in an INSERT header or after input(). WITH/SELECT
|
|
168
|
+
// expressions and identifiers such as "AS format" must stay ordinary SQL.
|
|
169
|
+
const word = /^[A-Za-z_][A-Za-z0-9_$]*/.exec(content.slice(i));
|
|
170
|
+
if (word) {
|
|
171
|
+
const keyword = word[0].toUpperCase();
|
|
172
|
+
if (formatExpected) {
|
|
173
|
+
emitPlain(word[0]);
|
|
174
|
+
i = finishFormat(keyword, i + word[0].length);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (separator === ';' && bracketDepth === 0) {
|
|
178
|
+
const identifier = ['INTO', 'TABLE', 'FUNCTION', '.', 'AS'].includes(previousToken);
|
|
179
|
+
if (!identifier) {
|
|
180
|
+
if (keyword === 'INSERT' && (!current || /^WITH\b/i.test(current))) {
|
|
181
|
+
insertStatement = true;
|
|
182
|
+
insertHeader = true;
|
|
183
|
+
}
|
|
184
|
+
if (['WITH', 'SELECT', 'VALUES'].includes(keyword))
|
|
185
|
+
insertHeader = false;
|
|
186
|
+
formatExpected = insertStatement && (insertHeader || inputSource) && keyword === 'FORMAT';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
rememberToken(keyword);
|
|
190
|
+
emitPlain(word[0]);
|
|
191
|
+
i += word[0].length;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// input() also accepts raw FORMAT data after an INSERT SELECT, even in a subquery.
|
|
195
|
+
if (ch === '(' && inputFunctionExpected)
|
|
196
|
+
inputSource = true;
|
|
197
|
+
inputFunctionExpected = false;
|
|
198
|
+
if ('([{'.includes(ch))
|
|
199
|
+
bracketDepth += 1;
|
|
200
|
+
if (')]}'.includes(ch))
|
|
201
|
+
bracketDepth -= 1;
|
|
202
|
+
previousToken = ch;
|
|
203
|
+
formatExpected = false;
|
|
204
|
+
emitPlain(ch);
|
|
205
|
+
i += 1;
|
|
206
|
+
}
|
|
207
|
+
flush();
|
|
208
|
+
return statements;
|
|
209
|
+
};
|
|
210
|
+
const split_statements = (content) => split_sql(content, ';');
|
|
211
|
+
exports.split_statements = split_statements;
|
|
212
|
+
const split_assignments = (content) => split_sql(content, ',');
|
|
213
|
+
exports.split_assignments = split_assignments;
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.
|
|
1
|
+
export declare const VERSION = "1.5.0";
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clickhouse-migrations",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "ClickHouse Migrations",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clickhouse-migrations": "lib/cli.js"
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"overrides": {
|
|
50
50
|
"js-yaml": "^4.2.0",
|
|
51
51
|
"@babel/core": "^7.29.7",
|
|
52
|
+
"brace-expansion": "^5.0.12",
|
|
52
53
|
"test-exclude": "^8.0.0",
|
|
53
54
|
"glob": "^13.0.6"
|
|
54
55
|
},
|