clickhouse-migrations 1.3.1 → 1.4.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 +36 -1
- package/lib/cli.js +1 -1
- package/lib/migrate.js +191 -332
- package/lib/sql-parse.d.ts +2 -1
- package/lib/sql-parse.js +60 -10
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +19 -13
package/README.md
CHANGED
|
@@ -63,6 +63,10 @@ For TLS/HTTPS connections, you can provide a custom CA certificate and optional
|
|
|
63
63
|
(optional) Skip database creation,
|
|
64
64
|
set to 'true' to enable
|
|
65
65
|
(--skip-db-creation)
|
|
66
|
+
CH_MIGRATIONS_SUBSTITUTE_ENV
|
|
67
|
+
(optional) Substitute ${VAR} in migration
|
|
68
|
+
files from the environment at apply time,
|
|
69
|
+
set to 'true' to enable
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
CLI executions examples
|
|
@@ -99,7 +103,7 @@ For TLS/HTTPS connections, you can provide a custom CA certificate and optional
|
|
|
99
103
|
Migration file example:
|
|
100
104
|
(e.g., located at /app/clickhouse/migrations/1_init.sql)
|
|
101
105
|
|
|
102
|
-
```
|
|
106
|
+
```sql
|
|
103
107
|
-- an example of migration file 1_init.sql
|
|
104
108
|
|
|
105
109
|
SET allow_experimental_json_type = 1;
|
|
@@ -115,3 +119,34 @@ SAMPLE BY session_id
|
|
|
115
119
|
ORDER BY (session_id)
|
|
116
120
|
SETTINGS index_granularity = 8192;
|
|
117
121
|
```
|
|
122
|
+
|
|
123
|
+
### Environment variable substitution (optional)
|
|
124
|
+
|
|
125
|
+
Some migrations need a value that differs per environment, for example: the host of a Postgres instance behind a `DICTIONARY`, a cluster name, a bucket, and so on.
|
|
126
|
+
|
|
127
|
+
Enable `CH_MIGRATIONS_SUBSTITUTE_ENV=true` and `${VAR}` placeholders in migration files are replaced with the matching environment variables when the migration is applied. It's off by default, so existing migrations behave exactly as before.
|
|
128
|
+
|
|
129
|
+
Rules:
|
|
130
|
+
|
|
131
|
+
- `${NAME}` is replaced with the value of the `NAME` environment variable.
|
|
132
|
+
- If `NAME` is unset, the migration fails - the placeholder is never left behind in the SQL.
|
|
133
|
+
- To keep a literal `${NAME}` (for example in a comment or a string), escape it as `$${NAME}`.
|
|
134
|
+
- A malformed (`${PG-HOST}`, `${}`) or unterminated (`${PG_HOST`) placeholder also fails, so nothing is ever silently passed through.
|
|
135
|
+
|
|
136
|
+
Substitution is textual (like `envsubst`): it doesn't parse SQL and doesn't escape values, so anything placed inside quotes must be quoting-safe.
|
|
137
|
+
|
|
138
|
+
A typical use is an idempotent dictionary pointing at a per-environment source:
|
|
139
|
+
|
|
140
|
+
```sql
|
|
141
|
+
CREATE OR REPLACE DICTIONARY my_db.dict_offers
|
|
142
|
+
(
|
|
143
|
+
`id` UUID,
|
|
144
|
+
`name` String DEFAULT ''
|
|
145
|
+
)
|
|
146
|
+
PRIMARY KEY id
|
|
147
|
+
SOURCE(POSTGRESQL(HOST '${PG_HOST}' PORT ${PG_PORT} USER '${PG_USER}' PASSWORD '${PG_PASSWORD}' DB '${PG_DB}' TABLE 'offers'))
|
|
148
|
+
LIFETIME(MIN 0 MAX 300)
|
|
149
|
+
LAYOUT(COMPLEX_KEY_HASHED());
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
_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.
|
package/lib/cli.js
CHANGED
package/lib/migrate.js
CHANGED
|
@@ -1,74 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
-
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
-
function step(op) {
|
|
16
|
-
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
-
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
-
switch (op[0]) {
|
|
21
|
-
case 0: case 1: t = op; break;
|
|
22
|
-
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
-
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
-
default:
|
|
26
|
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
-
if (t[2]) _.ops.pop();
|
|
31
|
-
_.trys.pop(); continue;
|
|
32
|
-
}
|
|
33
|
-
op = body.call(thisArg, _);
|
|
34
|
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
39
|
-
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
40
|
-
if (ar || !(i in from)) {
|
|
41
|
-
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
42
|
-
ar[i] = from[i];
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return to.concat(ar || Array.prototype.slice.call(from));
|
|
46
|
-
};
|
|
47
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
48
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
4
|
};
|
|
50
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
6
|
exports.migration = exports.migrate = void 0;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (type === void 0) { type = 'info'; }
|
|
7
|
+
const client_1 = require("@clickhouse/client");
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const sql_parse_1 = require("./sql-parse");
|
|
12
|
+
const version_1 = require("./version");
|
|
13
|
+
const log = (type = 'info', message, error) => {
|
|
60
14
|
if (type === 'info') {
|
|
61
|
-
console.log('\x1b[36m',
|
|
15
|
+
console.log('\x1b[36m', `clickhouse-migrations :`, '\x1b[0m', message);
|
|
62
16
|
}
|
|
63
17
|
else {
|
|
64
|
-
console.error('\x1b[36m',
|
|
18
|
+
console.error('\x1b[36m', `clickhouse-migrations :`, '\x1b[31m', `Error: ${message}`, error ? `\n\n ${error}` : '');
|
|
65
19
|
}
|
|
66
20
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
url
|
|
70
|
-
username
|
|
71
|
-
password
|
|
21
|
+
const connect = (url, username, password, db_name, timeout, ca_cert, cert, key) => {
|
|
22
|
+
const db_params = {
|
|
23
|
+
url,
|
|
24
|
+
username,
|
|
25
|
+
password,
|
|
72
26
|
application: 'clickhouse-migrations',
|
|
73
27
|
};
|
|
74
28
|
if (db_name) {
|
|
@@ -99,301 +53,213 @@ var connect = function (url, username, password, db_name, timeout, ca_cert, cert
|
|
|
99
53
|
}
|
|
100
54
|
return (0, client_1.createClient)(db_params);
|
|
101
55
|
};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
_a.label = 5;
|
|
124
|
-
case 5:
|
|
125
|
-
_a.trys.push([5, 8, , 10]);
|
|
126
|
-
return [4 /*yield*/, client.query({
|
|
127
|
-
query: "SELECT 1 AS ok FROM system.databases WHERE name = {db_name:String}",
|
|
128
|
-
query_params: { db_name: db_name },
|
|
129
|
-
format: 'JSONEachRow',
|
|
130
|
-
})];
|
|
131
|
-
case 6:
|
|
132
|
-
resultSet = _a.sent();
|
|
133
|
-
return [4 /*yield*/, resultSet.json()];
|
|
134
|
-
case 7:
|
|
135
|
-
rows = (_a.sent());
|
|
136
|
-
exists = rows.length > 0;
|
|
137
|
-
return [3 /*break*/, 10];
|
|
138
|
-
case 8:
|
|
139
|
-
e_2 = _a.sent();
|
|
140
|
-
log('error', "can't verify that the database ".concat(db_name, " exists."), e_2.message);
|
|
141
|
-
return [4 /*yield*/, client.close()];
|
|
142
|
-
case 9:
|
|
143
|
-
_a.sent();
|
|
144
|
-
process.exit(1);
|
|
145
|
-
return [3 /*break*/, 10];
|
|
146
|
-
case 10:
|
|
147
|
-
if (!!exists) return [3 /*break*/, 12];
|
|
148
|
-
log('error', "the database ".concat(db_name, " doesn't exist and --skip-db-creation is set. Please create the database manually or remove --skip-db-creation."));
|
|
149
|
-
return [4 /*yield*/, client.close()];
|
|
150
|
-
case 11:
|
|
151
|
-
_a.sent();
|
|
152
|
-
process.exit(1);
|
|
153
|
-
_a.label = 12;
|
|
154
|
-
case 12: return [4 /*yield*/, client.close()];
|
|
155
|
-
case 13:
|
|
156
|
-
_a.sent();
|
|
157
|
-
return [2 /*return*/];
|
|
158
|
-
case 14:
|
|
159
|
-
q = db_engine
|
|
160
|
-
? "CREATE DATABASE IF NOT EXISTS \"".concat(db_name, "\" ").concat(db_engine)
|
|
161
|
-
: "CREATE DATABASE IF NOT EXISTS \"".concat(db_name, "\"");
|
|
162
|
-
_a.label = 15;
|
|
163
|
-
case 15:
|
|
164
|
-
_a.trys.push([15, 17, , 18]);
|
|
165
|
-
return [4 /*yield*/, client.command({
|
|
166
|
-
query: q,
|
|
167
|
-
clickhouse_settings: {
|
|
168
|
-
wait_end_of_query: 1,
|
|
169
|
-
},
|
|
170
|
-
})];
|
|
171
|
-
case 16:
|
|
172
|
-
_a.sent();
|
|
173
|
-
return [3 /*break*/, 18];
|
|
174
|
-
case 17:
|
|
175
|
-
e_3 = _a.sent();
|
|
176
|
-
log('error', "can't create the database ".concat(db_name, "."), e_3.message);
|
|
177
|
-
process.exit(1);
|
|
178
|
-
return [3 /*break*/, 18];
|
|
179
|
-
case 18: return [4 /*yield*/, client.close()];
|
|
180
|
-
case 19:
|
|
181
|
-
_a.sent();
|
|
182
|
-
return [2 /*return*/];
|
|
56
|
+
const create_db = async (host, username, password, db_name, db_engine, timeout, ca_cert, cert, key, skip_db_creation) => {
|
|
57
|
+
// Don't specify database name when creating it - connect to default database
|
|
58
|
+
const client = connect(host, username, password, undefined, timeout, ca_cert, cert, key);
|
|
59
|
+
try {
|
|
60
|
+
await client.ping();
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
log('error', `Failed to connect to ClickHouse`, e.message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
if (skip_db_creation) {
|
|
67
|
+
// Database creation is disabled - verify the database already exists.
|
|
68
|
+
let exists = false;
|
|
69
|
+
try {
|
|
70
|
+
const resultSet = await client.query({
|
|
71
|
+
query: `SELECT 1 AS ok FROM system.databases WHERE name = {db_name:String}`,
|
|
72
|
+
query_params: { db_name },
|
|
73
|
+
format: 'JSONEachRow',
|
|
74
|
+
});
|
|
75
|
+
const rows = (await resultSet.json());
|
|
76
|
+
exists = rows.length > 0;
|
|
183
77
|
}
|
|
184
|
-
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
78
|
+
catch (e) {
|
|
79
|
+
log('error', `can't verify that the database ${db_name} exists.`, e.message);
|
|
80
|
+
await client.close();
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
if (!exists) {
|
|
84
|
+
log('error', `the database ${db_name} doesn't exist and --skip-db-creation is set. Please create the database manually or remove --skip-db-creation.`);
|
|
85
|
+
await client.close();
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
await client.close();
|
|
89
|
+
return;
|
|
190
90
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return [4 /*yield*/, client.command({
|
|
202
|
-
query: q,
|
|
203
|
-
clickhouse_settings: {
|
|
204
|
-
wait_end_of_query: 1,
|
|
205
|
-
},
|
|
206
|
-
})];
|
|
207
|
-
case 2:
|
|
208
|
-
_a.sent();
|
|
209
|
-
return [3 /*break*/, 4];
|
|
210
|
-
case 3:
|
|
211
|
-
e_4 = _a.sent();
|
|
212
|
-
log('error', "can't create the _migrations table.", e_4.message);
|
|
213
|
-
process.exit(1);
|
|
214
|
-
return [3 /*break*/, 4];
|
|
215
|
-
case 4: return [2 /*return*/];
|
|
216
|
-
}
|
|
91
|
+
// In open source ClickHouse - default DB engine is "Atomic", for Cloud - "Shared". If not set, appropriate default is used.
|
|
92
|
+
const q = db_engine
|
|
93
|
+
? `CREATE DATABASE IF NOT EXISTS "${db_name}" ${db_engine}`
|
|
94
|
+
: `CREATE DATABASE IF NOT EXISTS "${db_name}"`;
|
|
95
|
+
try {
|
|
96
|
+
await client.command({
|
|
97
|
+
query: q,
|
|
98
|
+
clickhouse_settings: {
|
|
99
|
+
wait_end_of_query: 1,
|
|
100
|
+
},
|
|
217
101
|
});
|
|
218
|
-
}
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
log('error', `can't create the database ${db_name}.`, e.message);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
await client.close();
|
|
108
|
+
};
|
|
109
|
+
const init_migration_table = async (client, table_engine = 'MergeTree') => {
|
|
110
|
+
const q = `CREATE TABLE IF NOT EXISTS _migrations (
|
|
111
|
+
uid UUID DEFAULT generateUUIDv4(),
|
|
112
|
+
version UInt32,
|
|
113
|
+
checksum String,
|
|
114
|
+
migration_name String,
|
|
115
|
+
applied_at DateTime DEFAULT now()
|
|
116
|
+
)
|
|
117
|
+
ENGINE = ${table_engine}
|
|
118
|
+
ORDER BY tuple(applied_at)`;
|
|
119
|
+
try {
|
|
120
|
+
await client.command({
|
|
121
|
+
query: q,
|
|
122
|
+
clickhouse_settings: {
|
|
123
|
+
wait_end_of_query: 1,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
log('error', `can't create the _migrations table.`, e.message);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
219
131
|
};
|
|
220
|
-
|
|
221
|
-
|
|
132
|
+
const get_migrations = (migrations_home) => {
|
|
133
|
+
let files;
|
|
222
134
|
try {
|
|
223
135
|
files = fs_1.default.readdirSync(migrations_home);
|
|
224
136
|
}
|
|
225
137
|
catch (e) {
|
|
226
|
-
log('error',
|
|
138
|
+
log('error', `no migration directory ${migrations_home}. Please create it.`);
|
|
227
139
|
process.exit(1);
|
|
228
140
|
}
|
|
229
|
-
|
|
230
|
-
files.forEach(
|
|
141
|
+
const migrations = [];
|
|
142
|
+
files.forEach((file) => {
|
|
231
143
|
// Manage only .sql files.
|
|
232
144
|
if (!file.endsWith('.sql')) {
|
|
233
145
|
return;
|
|
234
146
|
}
|
|
235
|
-
|
|
147
|
+
const version = Number(file.split('_')[0]);
|
|
236
148
|
if (!version) {
|
|
237
|
-
log('error',
|
|
149
|
+
log('error', `a migration name should start from number, example: 1_init.sql. Please check, if the migration ${file} is named correctly`);
|
|
238
150
|
process.exit(1);
|
|
239
151
|
}
|
|
240
152
|
migrations.push({
|
|
241
|
-
version
|
|
242
|
-
file
|
|
153
|
+
version,
|
|
154
|
+
file,
|
|
243
155
|
});
|
|
244
156
|
});
|
|
245
157
|
if (!migrations) {
|
|
246
|
-
log('error',
|
|
158
|
+
log('error', `no migrations in the ${migrations_home} migrations directory`);
|
|
247
159
|
}
|
|
248
160
|
// Order by version.
|
|
249
|
-
migrations.sort(
|
|
161
|
+
migrations.sort((m1, m2) => m1.version - m2.version);
|
|
250
162
|
return migrations;
|
|
251
163
|
};
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
164
|
+
const apply_migrations = async (client, migrations, migrations_home) => {
|
|
165
|
+
let migration_query_result = [];
|
|
166
|
+
try {
|
|
167
|
+
const resultSet = await client.query({
|
|
168
|
+
query: `SELECT version, checksum, migration_name FROM _migrations ORDER BY version`,
|
|
169
|
+
format: 'JSONEachRow',
|
|
170
|
+
});
|
|
171
|
+
migration_query_result = await resultSet.json();
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
log('error', `can't select data from the _migrations table.`, e.message);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
const migrations_applied = [];
|
|
178
|
+
migration_query_result.forEach((row) => {
|
|
179
|
+
migrations_applied[row.version] = {
|
|
180
|
+
version: row.version,
|
|
181
|
+
checksum: row.checksum,
|
|
182
|
+
migration_name: row.migration_name,
|
|
183
|
+
};
|
|
184
|
+
// Check if migration file was not removed after apply.
|
|
185
|
+
const migration_exist = migrations.find(({ version }) => version === row.version);
|
|
186
|
+
if (!migration_exist) {
|
|
187
|
+
log('error', `a migration file shouldn't be removed after apply. Please, restore the migration ${row.migration_name}.`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
let applied_migrations = '';
|
|
192
|
+
for (const migration of migrations) {
|
|
193
|
+
const content = fs_1.default.readFileSync(migrations_home + '/' + migration.file).toString();
|
|
194
|
+
const checksum = crypto_1.default.createHash('md5').update(content).digest('hex');
|
|
195
|
+
if (migrations_applied[migration.version]) {
|
|
196
|
+
// Check if migration file was not changed after apply.
|
|
197
|
+
if (migrations_applied[migration.version].checksum !== checksum) {
|
|
198
|
+
log('error', `a migration file should't be changed after apply. Please, restore content of the ${migrations_applied[migration.version].migration_name} migrations.`);
|
|
274
199
|
process.exit(1);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
200
|
+
}
|
|
201
|
+
// Skip if a migration is already applied.
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Substitute ${VAR} from the environment (opt-in) at apply time only. The
|
|
205
|
+
// checksum above is over the raw file, so it stays stable across environments.
|
|
206
|
+
let sql;
|
|
207
|
+
try {
|
|
208
|
+
sql = (0, sql_parse_1.substitute_env)(content);
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
log('error', `the migration ${migration.file} has an error.`, e instanceof Error ? e.message : String(e));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
// Extract sql from the migration.
|
|
215
|
+
const queries = (0, sql_parse_1.sql_queries)(sql);
|
|
216
|
+
const sets = (0, sql_parse_1.sql_sets)(sql);
|
|
217
|
+
for (const query of queries) {
|
|
218
|
+
try {
|
|
219
|
+
await client.command({
|
|
220
|
+
query: query,
|
|
221
|
+
clickhouse_settings: sets,
|
|
293
222
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
_b.label = 6;
|
|
297
|
-
case 6:
|
|
298
|
-
if (!(_i < migrations_1.length)) return [3 /*break*/, 17];
|
|
299
|
-
migration_1 = migrations_1[_i];
|
|
300
|
-
content = fs_1.default.readFileSync(migrations_home + '/' + migration_1.file).toString();
|
|
301
|
-
checksum = crypto_1.default.createHash('md5').update(content).digest('hex');
|
|
302
|
-
if (migrations_applied[migration_1.version]) {
|
|
303
|
-
// Check if migration file was not changed after apply.
|
|
304
|
-
if (migrations_applied[migration_1.version].checksum !== checksum) {
|
|
305
|
-
log('error', "a migration file should't be changed after apply. Please, restore content of the ".concat(migrations_applied[migration_1.version].migration_name, " migrations."));
|
|
306
|
-
process.exit(1);
|
|
307
|
-
}
|
|
308
|
-
// Skip if a migration is already applied.
|
|
309
|
-
return [3 /*break*/, 16];
|
|
310
|
-
}
|
|
311
|
-
queries = (0, sql_parse_1.sql_queries)(content);
|
|
312
|
-
sets = (0, sql_parse_1.sql_sets)(content);
|
|
313
|
-
_a = 0, queries_1 = queries;
|
|
314
|
-
_b.label = 7;
|
|
315
|
-
case 7:
|
|
316
|
-
if (!(_a < queries_1.length)) return [3 /*break*/, 12];
|
|
317
|
-
query = queries_1[_a];
|
|
318
|
-
_b.label = 8;
|
|
319
|
-
case 8:
|
|
320
|
-
_b.trys.push([8, 10, , 11]);
|
|
321
|
-
return [4 /*yield*/, client.command({
|
|
322
|
-
query: query,
|
|
323
|
-
clickhouse_settings: sets,
|
|
324
|
-
})];
|
|
325
|
-
case 9:
|
|
326
|
-
_b.sent();
|
|
327
|
-
return [3 /*break*/, 11];
|
|
328
|
-
case 10:
|
|
329
|
-
e_6 = _b.sent();
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
330
225
|
if (applied_migrations) {
|
|
331
|
-
log('info',
|
|
226
|
+
log('info', `The migration(s) ${applied_migrations} was successfully applied!`);
|
|
332
227
|
}
|
|
333
|
-
log('error',
|
|
334
|
-
process.exit(1);
|
|
335
|
-
return [3 /*break*/, 11];
|
|
336
|
-
case 11:
|
|
337
|
-
_a++;
|
|
338
|
-
return [3 /*break*/, 7];
|
|
339
|
-
case 12:
|
|
340
|
-
_b.trys.push([12, 14, , 15]);
|
|
341
|
-
return [4 /*yield*/, client.insert({
|
|
342
|
-
table: '_migrations',
|
|
343
|
-
values: [{ version: migration_1.version, checksum: checksum, migration_name: migration_1.file }],
|
|
344
|
-
format: 'JSONEachRow',
|
|
345
|
-
})];
|
|
346
|
-
case 13:
|
|
347
|
-
_b.sent();
|
|
348
|
-
return [3 /*break*/, 15];
|
|
349
|
-
case 14:
|
|
350
|
-
e_7 = _b.sent();
|
|
351
|
-
log('error', "can't insert a data into the table _migrations.", e_7.message);
|
|
228
|
+
log('error', `the migrations ${migration.file} has an error. Please, fix it (be sure that already executed parts of the migration would not be run second time) and re-run migration script.`, e.message);
|
|
352
229
|
process.exit(1);
|
|
353
|
-
|
|
354
|
-
case 15:
|
|
355
|
-
applied_migrations = applied_migrations ? applied_migrations + ', ' + migration_1.file : migration_1.file;
|
|
356
|
-
_b.label = 16;
|
|
357
|
-
case 16:
|
|
358
|
-
_i++;
|
|
359
|
-
return [3 /*break*/, 6];
|
|
360
|
-
case 17:
|
|
361
|
-
if (applied_migrations) {
|
|
362
|
-
log('info', "The migration(s) ".concat(applied_migrations, " was successfully applied!"));
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
log('info', "No migrations to apply.");
|
|
366
|
-
}
|
|
367
|
-
return [2 /*return*/];
|
|
230
|
+
}
|
|
368
231
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
case 0:
|
|
376
|
-
migrations = get_migrations(migrations_home);
|
|
377
|
-
return [4 /*yield*/, create_db(host, username, password, db_name, db_engine, timeout, ca_cert, cert, key, skip_db_creation)];
|
|
378
|
-
case 1:
|
|
379
|
-
_a.sent();
|
|
380
|
-
client = connect(host, username, password, db_name, timeout, ca_cert, cert, key);
|
|
381
|
-
return [4 /*yield*/, init_migration_table(client, table_engine)];
|
|
382
|
-
case 2:
|
|
383
|
-
_a.sent();
|
|
384
|
-
return [4 /*yield*/, apply_migrations(client, migrations, migrations_home)];
|
|
385
|
-
case 3:
|
|
386
|
-
_a.sent();
|
|
387
|
-
return [4 /*yield*/, client.close()];
|
|
388
|
-
case 4:
|
|
389
|
-
_a.sent();
|
|
390
|
-
return [2 /*return*/];
|
|
232
|
+
try {
|
|
233
|
+
await client.insert({
|
|
234
|
+
table: '_migrations',
|
|
235
|
+
values: [{ version: migration.version, checksum: checksum, migration_name: migration.file }],
|
|
236
|
+
format: 'JSONEachRow',
|
|
237
|
+
});
|
|
391
238
|
}
|
|
392
|
-
|
|
393
|
-
|
|
239
|
+
catch (e) {
|
|
240
|
+
log('error', `can't insert a data into the table _migrations.`, e.message);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
applied_migrations = applied_migrations ? applied_migrations + ', ' + migration.file : migration.file;
|
|
244
|
+
}
|
|
245
|
+
if (applied_migrations) {
|
|
246
|
+
log('info', `The migration(s) ${applied_migrations} was successfully applied!`);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
log('info', `No migrations to apply.`);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const migration = async (migrations_home, host, username, password, db_name, db_engine, table_engine, timeout, ca_cert, cert, key, skip_db_creation) => {
|
|
253
|
+
const migrations = get_migrations(migrations_home);
|
|
254
|
+
await create_db(host, username, password, db_name, db_engine, timeout, ca_cert, cert, key, skip_db_creation);
|
|
255
|
+
const client = connect(host, username, password, db_name, timeout, ca_cert, cert, key);
|
|
256
|
+
await init_migration_table(client, table_engine);
|
|
257
|
+
await apply_migrations(client, migrations, migrations_home);
|
|
258
|
+
await client.close();
|
|
259
|
+
};
|
|
394
260
|
exports.migration = migration;
|
|
395
|
-
|
|
396
|
-
|
|
261
|
+
const migrate = () => {
|
|
262
|
+
const program = new commander_1.Command();
|
|
397
263
|
program.name('clickhouse-migrations').description('ClickHouse migrations.').version(version_1.VERSION);
|
|
398
264
|
program
|
|
399
265
|
.command('migrate')
|
|
@@ -410,16 +276,9 @@ var migrate = function () {
|
|
|
410
276
|
.option('--cert <path>', 'Client certificate file path', process.env.CH_MIGRATIONS_CERT)
|
|
411
277
|
.option('--key <path>', 'Client key file path', process.env.CH_MIGRATIONS_KEY)
|
|
412
278
|
.option('--skip-db-creation', 'Skip database creation', process.env.CH_MIGRATIONS_SKIP_DB_CREATION === 'true')
|
|
413
|
-
.action(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
case 0: return [4 /*yield*/, migration(options.migrationsHome, options.host, options.user, options.password, options.db, options.dbEngine, options.tableEngine, options.timeout, options.caCert, options.cert, options.key, options.skipDbCreation)];
|
|
417
|
-
case 1:
|
|
418
|
-
_a.sent();
|
|
419
|
-
return [2 /*return*/];
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
}); });
|
|
279
|
+
.action(async (options) => {
|
|
280
|
+
await migration(options.migrationsHome, options.host, options.user, options.password, options.db, options.dbEngine, options.tableEngine, options.timeout, options.caCert, options.cert, options.key, options.skipDbCreation);
|
|
281
|
+
});
|
|
423
282
|
program.parse();
|
|
424
283
|
};
|
|
425
284
|
exports.migrate = migrate;
|
package/lib/sql-parse.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
declare const substitute_env: (content: string) => string;
|
|
1
2
|
declare const sql_queries: (content: string) => string[];
|
|
2
3
|
declare const sql_sets: (content: string) => {
|
|
3
4
|
[key: string]: string;
|
|
4
5
|
};
|
|
5
|
-
export { sql_queries, sql_sets };
|
|
6
|
+
export { sql_queries, sql_sets, substitute_env };
|
package/lib/sql-parse.js
CHANGED
|
@@ -1,31 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sql_sets = exports.sql_queries = void 0;
|
|
3
|
+
exports.substitute_env = exports.sql_sets = exports.sql_queries = void 0;
|
|
4
|
+
// Replace ${NAME} in migration content with env vars. Opt-in via
|
|
5
|
+
// CH_MIGRATIONS_SUBSTITUTE_ENV=true; otherwise returns content unchanged.
|
|
6
|
+
// Use $${NAME} to keep a literal ${NAME}. Throws on an unset variable or a
|
|
7
|
+
// malformed/unterminated placeholder, so nothing is ever silently left as-is.
|
|
8
|
+
// Note: the caller checksums the raw file, so substitution only affects the
|
|
9
|
+
// SQL sent to ClickHouse, not what's stored in _migrations.
|
|
10
|
+
const substitute_env = (content) => {
|
|
11
|
+
if (process.env.CH_MIGRATIONS_SUBSTITUTE_ENV !== 'true') {
|
|
12
|
+
return content;
|
|
13
|
+
}
|
|
14
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
|
+
let result = '';
|
|
16
|
+
let position = 0;
|
|
17
|
+
while (position < content.length) {
|
|
18
|
+
// Escaped placeholder: $${NAME} -> ${NAME} (kept literal, not substituted).
|
|
19
|
+
if (content.startsWith('$${', position)) {
|
|
20
|
+
const end = content.indexOf('}', position + 3);
|
|
21
|
+
if (end === -1) {
|
|
22
|
+
throw new Error(`unterminated escaped placeholder at position ${position}`);
|
|
23
|
+
}
|
|
24
|
+
// Drop only the leading "$" and keep the rest as a literal.
|
|
25
|
+
result += content.slice(position + 1, end + 1);
|
|
26
|
+
position = end + 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// Environment placeholder: ${NAME}.
|
|
30
|
+
if (content.startsWith('${', position)) {
|
|
31
|
+
const end = content.indexOf('}', position + 2);
|
|
32
|
+
if (end === -1) {
|
|
33
|
+
throw new Error(`unterminated environment placeholder at position ${position}`);
|
|
34
|
+
}
|
|
35
|
+
const placeholder = content.slice(position, end + 1);
|
|
36
|
+
const name = content.slice(position + 2, end);
|
|
37
|
+
if (!ENV_NAME.test(name)) {
|
|
38
|
+
throw new Error(`invalid environment placeholder "${placeholder}"; expected \${NAME} or escape it as $${placeholder}`);
|
|
39
|
+
}
|
|
40
|
+
const value = process.env[name];
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
throw new Error(`migration references ${placeholder}, but environment variable ${name} is not set`);
|
|
43
|
+
}
|
|
44
|
+
result += value;
|
|
45
|
+
position = end + 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
result += content[position];
|
|
49
|
+
position += 1;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
exports.substitute_env = substitute_env;
|
|
4
54
|
// Extract sql queries from migrations.
|
|
5
|
-
|
|
6
|
-
|
|
55
|
+
const sql_queries = (content) => {
|
|
56
|
+
const queries = content
|
|
7
57
|
.replace(/(--|#!|#\s).*(\n|\r\n|\r|$)/gm, '\n')
|
|
8
58
|
.replace(/^\s*(SET\s).*(\n|\r\n|\r|$)/gm, '')
|
|
9
59
|
.replace(/(\n|\r\n|\r)/gm, ' ')
|
|
10
60
|
.replace(/\s+/g, ' ')
|
|
11
61
|
.split(';')
|
|
12
|
-
.map(
|
|
13
|
-
.filter(
|
|
62
|
+
.map((el) => el.trim())
|
|
63
|
+
.filter((el) => el.length != 0);
|
|
14
64
|
return queries;
|
|
15
65
|
};
|
|
16
66
|
exports.sql_queries = sql_queries;
|
|
17
67
|
// Extract query settings from migrations.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
68
|
+
const sql_sets = (content) => {
|
|
69
|
+
const sets = {};
|
|
70
|
+
const sets_arr = content
|
|
21
71
|
.replace(/(--|#!|#\s).*(\n|\r\n|\r|$)/gm, '\n')
|
|
22
72
|
.replace(/^\s*(?!SET\s).*(\n|\r\n|\r|$)/gm, '')
|
|
23
73
|
.replace(/^\s*(SET\s)/gm, '')
|
|
24
74
|
.replace(/(\n|\r\n|\r)/gm, ' ')
|
|
25
75
|
.replace(/\s+/g, '')
|
|
26
76
|
.split(';');
|
|
27
|
-
sets_arr.forEach(
|
|
28
|
-
|
|
77
|
+
sets_arr.forEach((set_full) => {
|
|
78
|
+
const set = set_full.split('=');
|
|
29
79
|
if (set[0]) {
|
|
30
80
|
sets[set[0]] = set[1];
|
|
31
81
|
}
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.
|
|
1
|
+
export declare const VERSION = "1.4.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.4.0",
|
|
4
4
|
"description": "ClickHouse Migrations",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clickhouse-migrations": "lib/cli.js"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"author": "VVVi",
|
|
16
16
|
"repository": "VVVi/clickhouse-migrations",
|
|
17
17
|
"scripts": {
|
|
18
|
-
"build": "rm lib/*; tsc",
|
|
18
|
+
"build": "rm -rf lib/*; tsc",
|
|
19
19
|
"test": "jest --config jestconfig.json",
|
|
20
20
|
"test:unit": "jest --config jestconfig.json --testPathPatterns=\"\\.(unit|integration)\\.test\\.ts$\"",
|
|
21
21
|
"test:e2e": "jest --config jestconfig.json --testPathPatterns=\"\\.e2e\\.test\\.ts$\"",
|
|
@@ -28,23 +28,29 @@
|
|
|
28
28
|
"postversion": "git push && git push --tags"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@clickhouse/client": "^1.
|
|
31
|
+
"@clickhouse/client": "^1.23.1",
|
|
32
32
|
"commander": "^14.0.3"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@eslint/js": "^10.0.1",
|
|
36
36
|
"@types/jest": "^30.0.0",
|
|
37
|
-
"@types/node": "^20.19.
|
|
38
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
39
|
-
"@typescript-eslint/parser": "^8.
|
|
40
|
-
"eslint": "^10.
|
|
37
|
+
"@types/node": "^20.19.43",
|
|
38
|
+
"@typescript-eslint/eslint-plugin": "^8.61.1",
|
|
39
|
+
"@typescript-eslint/parser": "^8.61.1",
|
|
40
|
+
"eslint": "^10.5.0",
|
|
41
41
|
"eslint-config-prettier": "^10.1.8",
|
|
42
|
-
"eslint-plugin-prettier": "^5.5.
|
|
43
|
-
"jest": "^30.
|
|
44
|
-
"prettier": "^3.8.
|
|
45
|
-
"ts-jest": "^29.4.
|
|
46
|
-
"typescript": "^
|
|
47
|
-
"typescript-eslint": "^8.
|
|
42
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
43
|
+
"jest": "^30.4.2",
|
|
44
|
+
"prettier": "^3.8.4",
|
|
45
|
+
"ts-jest": "^29.4.11",
|
|
46
|
+
"typescript": "^6.0.3",
|
|
47
|
+
"typescript-eslint": "^8.61.1"
|
|
48
|
+
},
|
|
49
|
+
"overrides": {
|
|
50
|
+
"js-yaml": "^4.2.0",
|
|
51
|
+
"@babel/core": "^7.29.7",
|
|
52
|
+
"test-exclude": "^8.0.0",
|
|
53
|
+
"glob": "^13.0.6"
|
|
48
54
|
},
|
|
49
55
|
"files": [
|
|
50
56
|
"lib/**/*"
|