clickhouse-migrations 0.1.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 +35 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +340 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
# clickhouse-migrations
|
|
3
|
+
|
|
4
|
+
> CLI NodeJS ClickHouse Migrations.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install --global clickhouse-migrations
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
Create a directory, where migrations will be stored. It will be used as the value for the `--migrations-home` option.
|
|
15
|
+
|
|
16
|
+
Create migrations in the directory, which should look like `1_`some_text`.sql`, `2_`other_text`.sql`, ... `10_`more_test`.sql`. Here, is important: migration versions followed by underscore - `1_`, `2_`, ...`10_`..., and file extension `.sql`. Any text can follow by version number. The version number should increase for every next migration.
|
|
17
|
+
|
|
18
|
+
For migrations' content should be used correct SQL ClickHouse queries. A few queries can be used in one migration file. Every query should end with `;`. The queries could be idempotent - for example: `CREATE TABLE IF NOT EXISTS table ...;` Comments should start with `--- `.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Usage
|
|
22
|
+
$ clickhouse-migrations migrate <options>
|
|
23
|
+
|
|
24
|
+
Required options
|
|
25
|
+
--host=<name> Clickhouse hostname in format https://clichouse
|
|
26
|
+
--port=<number> Port
|
|
27
|
+
--user=<name> Username
|
|
28
|
+
--pass=<password> Password
|
|
29
|
+
--db-name=<database> Database name
|
|
30
|
+
--migrations-home=<dir> Migrations' directory
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
clickhouse-migrations migrate --host=http://localhost --port=8123 --user=default --pass='' --db-name=analytics --migrations-home=/app/clickhouse/migrations
|
|
34
|
+
|
|
35
|
+
```
|
package/lib/cli.d.ts
ADDED
package/lib/cli.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
4
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
5
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
6
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
7
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
8
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
9
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
13
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
14
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
15
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
16
|
+
function step(op) {
|
|
17
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
18
|
+
while (_) try {
|
|
19
|
+
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;
|
|
20
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
21
|
+
switch (op[0]) {
|
|
22
|
+
case 0: case 1: t = op; break;
|
|
23
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
24
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
25
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
26
|
+
default:
|
|
27
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
28
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
29
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
30
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
31
|
+
if (t[2]) _.ops.pop();
|
|
32
|
+
_.trys.pop(); continue;
|
|
33
|
+
}
|
|
34
|
+
op = body.call(thisArg, _);
|
|
35
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
36
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
var createClient = require('@clickhouse/client').createClient;
|
|
41
|
+
var Command = require('commander').Command;
|
|
42
|
+
var fs = require('fs');
|
|
43
|
+
var crypto = require('crypto');
|
|
44
|
+
// Extract SQL queries from migrations.
|
|
45
|
+
function sql_queries(content) {
|
|
46
|
+
var queries = content
|
|
47
|
+
.replace(/\s+/g, ' ')
|
|
48
|
+
.replace(/(\n|\r\n|\r)/gm, ' ')
|
|
49
|
+
.split(';')
|
|
50
|
+
.map(Function.prototype.call, String.prototype.trim)
|
|
51
|
+
.filter(function (el) {
|
|
52
|
+
// remove empty and comments
|
|
53
|
+
return el.length != 0 && !el.startsWith('-- ');
|
|
54
|
+
});
|
|
55
|
+
return queries;
|
|
56
|
+
}
|
|
57
|
+
function log(type, message, error) {
|
|
58
|
+
if (type === void 0) { type = 'info'; }
|
|
59
|
+
if (type === 'info') {
|
|
60
|
+
console.log('\x1b[36m', "clickhouse-migrations :", '\x1b[0m', message);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.error('\x1b[36m', "clickhouse-migrations :", '\x1b[31m', "Error: ".concat(message), error ? "\n\n ".concat(error) : '');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function connect(host, port, username, password, db_name) {
|
|
67
|
+
var db_params = {
|
|
68
|
+
url: host,
|
|
69
|
+
port: parseInt(port),
|
|
70
|
+
debug: false,
|
|
71
|
+
basicAuth: {
|
|
72
|
+
username: username,
|
|
73
|
+
password: password,
|
|
74
|
+
},
|
|
75
|
+
isUseGzip: false,
|
|
76
|
+
format: 'json',
|
|
77
|
+
raw: false,
|
|
78
|
+
config: {
|
|
79
|
+
session_timeout: 60,
|
|
80
|
+
output_format_json_quote_64bit_integers: 0,
|
|
81
|
+
enable_http_compression: 0,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
if (db_name) {
|
|
85
|
+
db_params.database = db_name;
|
|
86
|
+
}
|
|
87
|
+
return new createClient(db_params);
|
|
88
|
+
}
|
|
89
|
+
function create_db(host, port, username, password, db_name) {
|
|
90
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
91
|
+
var client, q, e_1;
|
|
92
|
+
return __generator(this, function (_a) {
|
|
93
|
+
switch (_a.label) {
|
|
94
|
+
case 0:
|
|
95
|
+
client = connect(host, port, username, password);
|
|
96
|
+
q = "CREATE DATABASE IF NOT EXISTS ".concat(db_name, " ENGINE = Atomic");
|
|
97
|
+
_a.label = 1;
|
|
98
|
+
case 1:
|
|
99
|
+
_a.trys.push([1, 3, , 4]);
|
|
100
|
+
return [4 /*yield*/, client.exec({
|
|
101
|
+
query: q,
|
|
102
|
+
})];
|
|
103
|
+
case 2:
|
|
104
|
+
_a.sent();
|
|
105
|
+
return [3 /*break*/, 4];
|
|
106
|
+
case 3:
|
|
107
|
+
e_1 = _a.sent();
|
|
108
|
+
log('error', "can't create the database ".concat(db_name, "."), e_1.message);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
return [3 /*break*/, 4];
|
|
111
|
+
case 4: return [4 /*yield*/, client.close()];
|
|
112
|
+
case 5:
|
|
113
|
+
_a.sent();
|
|
114
|
+
return [2 /*return*/];
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function init_migration_table(client) {
|
|
120
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
121
|
+
var q, e_2;
|
|
122
|
+
return __generator(this, function (_a) {
|
|
123
|
+
switch (_a.label) {
|
|
124
|
+
case 0:
|
|
125
|
+
q = "CREATE TABLE IF NOT EXISTS _migrations (\n uid UUID DEFAULT generateUUIDv4(), \n version UInt32,\n checksum String, \n migration_name String, \n applied_at DateTime DEFAULT now()\n ) \n ENGINE = MergeTree \n ORDER BY tuple(applied_at)";
|
|
126
|
+
_a.label = 1;
|
|
127
|
+
case 1:
|
|
128
|
+
_a.trys.push([1, 3, , 4]);
|
|
129
|
+
return [4 /*yield*/, client.exec({
|
|
130
|
+
query: q,
|
|
131
|
+
})];
|
|
132
|
+
case 2:
|
|
133
|
+
_a.sent();
|
|
134
|
+
return [3 /*break*/, 4];
|
|
135
|
+
case 3:
|
|
136
|
+
e_2 = _a.sent();
|
|
137
|
+
log('error', "can't create the _migrations table.", e_2.message);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
return [3 /*break*/, 4];
|
|
140
|
+
case 4: return [2 /*return*/];
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function get_migrations(migrations_home) {
|
|
146
|
+
var files;
|
|
147
|
+
try {
|
|
148
|
+
files = fs.readdirSync(migrations_home);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
log('error', "no migration directory ".concat(migrations_home, ". Please create it."));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
var migrations = [];
|
|
155
|
+
files.forEach(function (file) {
|
|
156
|
+
var version = Number(file.split('_')[0]);
|
|
157
|
+
// Manage only .sql files.
|
|
158
|
+
if (!file.endsWith('.sql'))
|
|
159
|
+
return;
|
|
160
|
+
migrations.push({
|
|
161
|
+
version: version,
|
|
162
|
+
file: file,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
if (!migrations) {
|
|
166
|
+
log('error', "no migrations in the ".concat(migrations_home, " migrations directory"));
|
|
167
|
+
}
|
|
168
|
+
// Order by version.
|
|
169
|
+
migrations.sort(function (m1, m2) { return m1.version - m2.version; });
|
|
170
|
+
return migrations;
|
|
171
|
+
}
|
|
172
|
+
function apply_migrations(client, migrations, migrations_home) {
|
|
173
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
174
|
+
var migration_query_result, resultSet, e_3, migrations_applied, applied_migrations, _i, migrations_1, migration_1, content, checksum, queries, _a, queries_1, query, e_4, e_5;
|
|
175
|
+
return __generator(this, function (_b) {
|
|
176
|
+
switch (_b.label) {
|
|
177
|
+
case 0:
|
|
178
|
+
migration_query_result = [];
|
|
179
|
+
_b.label = 1;
|
|
180
|
+
case 1:
|
|
181
|
+
_b.trys.push([1, 4, , 5]);
|
|
182
|
+
return [4 /*yield*/, client.query({
|
|
183
|
+
query: "SELECT version, checksum, migration_name FROM _migrations ORDER BY version",
|
|
184
|
+
format: 'JSONEachRow',
|
|
185
|
+
})];
|
|
186
|
+
case 2:
|
|
187
|
+
resultSet = _b.sent();
|
|
188
|
+
return [4 /*yield*/, resultSet.json()];
|
|
189
|
+
case 3:
|
|
190
|
+
migration_query_result = _b.sent();
|
|
191
|
+
return [3 /*break*/, 5];
|
|
192
|
+
case 4:
|
|
193
|
+
e_3 = _b.sent();
|
|
194
|
+
log('error', "can't select data from the _migrations table.", e_3.message);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
return [3 /*break*/, 5];
|
|
197
|
+
case 5:
|
|
198
|
+
migrations_applied = {};
|
|
199
|
+
migration_query_result.forEach(function (row) {
|
|
200
|
+
migrations_applied[row.version] = {
|
|
201
|
+
checksum: row.checksum,
|
|
202
|
+
migration_name: row.migration_name,
|
|
203
|
+
};
|
|
204
|
+
// Check if migration file was not removed after apply.
|
|
205
|
+
var migration_exist = migrations.find(function (_a) {
|
|
206
|
+
var version = _a.version;
|
|
207
|
+
return version === row.version;
|
|
208
|
+
});
|
|
209
|
+
if (!migration_exist) {
|
|
210
|
+
log('error', "a migration file shouldn't be removed after apply. Please, restore the migration ".concat(migrations_applied.version, " version."));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
applied_migrations = '';
|
|
215
|
+
_i = 0, migrations_1 = migrations;
|
|
216
|
+
_b.label = 6;
|
|
217
|
+
case 6:
|
|
218
|
+
if (!(_i < migrations_1.length)) return [3 /*break*/, 17];
|
|
219
|
+
migration_1 = migrations_1[_i];
|
|
220
|
+
content = fs.readFileSync(migrations_home + '/' + migration_1.file).toString();
|
|
221
|
+
checksum = crypto.createHash('md5').update(content).digest('hex');
|
|
222
|
+
if (migrations_applied[migration_1.version]) {
|
|
223
|
+
// Check if migration file was not changed after apply.
|
|
224
|
+
if (migrations_applied[migration_1.version].checksum !== checksum) {
|
|
225
|
+
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."));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
// Skip if a migration is already applied.
|
|
229
|
+
return [3 /*break*/, 16];
|
|
230
|
+
}
|
|
231
|
+
queries = sql_queries(content);
|
|
232
|
+
_a = 0, queries_1 = queries;
|
|
233
|
+
_b.label = 7;
|
|
234
|
+
case 7:
|
|
235
|
+
if (!(_a < queries_1.length)) return [3 /*break*/, 12];
|
|
236
|
+
query = queries_1[_a];
|
|
237
|
+
_b.label = 8;
|
|
238
|
+
case 8:
|
|
239
|
+
_b.trys.push([8, 10, , 11]);
|
|
240
|
+
return [4 /*yield*/, client.exec({
|
|
241
|
+
query: query,
|
|
242
|
+
})];
|
|
243
|
+
case 9:
|
|
244
|
+
_b.sent();
|
|
245
|
+
return [3 /*break*/, 11];
|
|
246
|
+
case 10:
|
|
247
|
+
e_4 = _b.sent();
|
|
248
|
+
if (applied_migrations) {
|
|
249
|
+
log('info', "The migration(s) ".concat(applied_migrations, " was successfully applied!"));
|
|
250
|
+
}
|
|
251
|
+
log('error', "the migrations ".concat(migration_1.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_4.message);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
return [3 /*break*/, 11];
|
|
254
|
+
case 11:
|
|
255
|
+
_a++;
|
|
256
|
+
return [3 /*break*/, 7];
|
|
257
|
+
case 12:
|
|
258
|
+
_b.trys.push([12, 14, , 15]);
|
|
259
|
+
return [4 /*yield*/, client.insert({
|
|
260
|
+
table: '_migrations',
|
|
261
|
+
values: [{ version: migration_1.version, checksum: checksum, migration_name: migration_1.file }],
|
|
262
|
+
format: 'JSONEachRow',
|
|
263
|
+
})];
|
|
264
|
+
case 13:
|
|
265
|
+
_b.sent();
|
|
266
|
+
return [3 /*break*/, 15];
|
|
267
|
+
case 14:
|
|
268
|
+
e_5 = _b.sent();
|
|
269
|
+
log('error', "can't insert a data into the table _migrations.", e_5.message);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
return [3 /*break*/, 15];
|
|
272
|
+
case 15:
|
|
273
|
+
applied_migrations = applied_migrations ? applied_migrations + ', ' + migration_1.file : migration_1.file;
|
|
274
|
+
_b.label = 16;
|
|
275
|
+
case 16:
|
|
276
|
+
_i++;
|
|
277
|
+
return [3 /*break*/, 6];
|
|
278
|
+
case 17:
|
|
279
|
+
if (applied_migrations) {
|
|
280
|
+
log('info', "The migration(s) ".concat(applied_migrations, " was successfully applied!"));
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
log('info', "No migrations to apply.");
|
|
284
|
+
}
|
|
285
|
+
return [2 /*return*/];
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function migration(migrations_home, host, port, username, password, db_name) {
|
|
291
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
292
|
+
var migrations, client;
|
|
293
|
+
return __generator(this, function (_a) {
|
|
294
|
+
switch (_a.label) {
|
|
295
|
+
case 0:
|
|
296
|
+
migrations = get_migrations(migrations_home);
|
|
297
|
+
return [4 /*yield*/, create_db(host, port, username, password, db_name)];
|
|
298
|
+
case 1:
|
|
299
|
+
_a.sent();
|
|
300
|
+
client = connect(host, port, username, password, db_name);
|
|
301
|
+
return [4 /*yield*/, init_migration_table(client)];
|
|
302
|
+
case 2:
|
|
303
|
+
_a.sent();
|
|
304
|
+
return [4 /*yield*/, apply_migrations(client, migrations, migrations_home)];
|
|
305
|
+
case 3:
|
|
306
|
+
_a.sent();
|
|
307
|
+
return [4 /*yield*/, client.close()];
|
|
308
|
+
case 4:
|
|
309
|
+
_a.sent();
|
|
310
|
+
return [2 /*return*/];
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function migrate() {
|
|
316
|
+
var _this = this;
|
|
317
|
+
var program = new Command();
|
|
318
|
+
program.name('clickhouse-migrations').description('ClickHouse migrations.').version('1.0.0');
|
|
319
|
+
program
|
|
320
|
+
.command('migrate')
|
|
321
|
+
.description('apply migrations.')
|
|
322
|
+
.requiredOption('--host <name>', 'clickhouse hostname in format http://clickhouse')
|
|
323
|
+
.requiredOption('--port <number>', 'port')
|
|
324
|
+
.requiredOption('--user <name>', 'username')
|
|
325
|
+
.requiredOption('--pass <password>', 'password')
|
|
326
|
+
.requiredOption('--db-name <dbname>', 'database name')
|
|
327
|
+
.requiredOption('--migrations-home <dir>', 'migrations directory')
|
|
328
|
+
.action(function (options) { return __awaiter(_this, void 0, void 0, function () {
|
|
329
|
+
return __generator(this, function (_a) {
|
|
330
|
+
switch (_a.label) {
|
|
331
|
+
case 0: return [4 /*yield*/, migration(options.migrationsHome, options.host, options.port, options.user, options.pass, options.dbName)];
|
|
332
|
+
case 1:
|
|
333
|
+
_a.sent();
|
|
334
|
+
return [2 /*return*/];
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}); });
|
|
338
|
+
program.parse();
|
|
339
|
+
}
|
|
340
|
+
migrate();
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clickhouse-migrations",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NodeJS ClickHouse Migrations",
|
|
5
|
+
"bin": {
|
|
6
|
+
"clickhouse-migrations": "lib/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"test": "jest --config jestconfig.json",
|
|
11
|
+
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
|
|
12
|
+
"lint": "eslint ./src/*.ts",
|
|
13
|
+
"prepare": "npm run build",
|
|
14
|
+
"prepublishOnly": "npm test && npm run lint",
|
|
15
|
+
"preversion": "npm run lint",
|
|
16
|
+
"version": "npm run format && git add -A src",
|
|
17
|
+
"postversion": "git push && git push --tags"
|
|
18
|
+
},
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@clickhouse/client": "^0.0.9",
|
|
23
|
+
"commander": "^9.4.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/jest": "^29.2.2",
|
|
27
|
+
"@types/node": "^18.11.9",
|
|
28
|
+
"eslint": "^8.26.0",
|
|
29
|
+
"eslint-config-prettier": "^8.5.0",
|
|
30
|
+
"jest": "^29.2.2",
|
|
31
|
+
"prettier": "^2.7.1",
|
|
32
|
+
"ts-jest": "^29.0.3",
|
|
33
|
+
"typescript": "^4.8.4"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"lib/**/*"
|
|
37
|
+
]
|
|
38
|
+
}
|