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 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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- var migrate_1 = require("./migrate");
4
+ const migrate_1 = require("./migrate");
5
5
  (0, migrate_1.migrate)();
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
- var client_1 = require("@clickhouse/client");
53
- var commander_1 = require("commander");
54
- var fs_1 = __importDefault(require("fs"));
55
- var crypto_1 = __importDefault(require("crypto"));
56
- var sql_parse_1 = require("./sql-parse");
57
- var version_1 = require("./version");
58
- var log = function (type, message, error) {
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', "clickhouse-migrations :", '\x1b[0m', message);
15
+ console.log('\x1b[36m', `clickhouse-migrations :`, '\x1b[0m', message);
62
16
  }
63
17
  else {
64
- console.error('\x1b[36m', "clickhouse-migrations :", '\x1b[31m', "Error: ".concat(message), error ? "\n\n ".concat(error) : '');
18
+ console.error('\x1b[36m', `clickhouse-migrations :`, '\x1b[31m', `Error: ${message}`, error ? `\n\n ${error}` : '');
65
19
  }
66
20
  };
67
- var connect = function (url, username, password, db_name, timeout, ca_cert, cert, key) {
68
- var db_params = {
69
- url: url,
70
- username: username,
71
- password: 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
- var create_db = function (host, username, password, db_name, db_engine, timeout, ca_cert, cert, key, skip_db_creation) { return __awaiter(void 0, void 0, void 0, function () {
103
- var client, e_1, exists, resultSet, rows, e_2, q, e_3;
104
- return __generator(this, function (_a) {
105
- switch (_a.label) {
106
- case 0:
107
- client = connect(host, username, password, undefined, timeout, ca_cert, cert, key);
108
- _a.label = 1;
109
- case 1:
110
- _a.trys.push([1, 3, , 4]);
111
- return [4 /*yield*/, client.ping()];
112
- case 2:
113
- _a.sent();
114
- return [3 /*break*/, 4];
115
- case 3:
116
- e_1 = _a.sent();
117
- log('error', "Failed to connect to ClickHouse", e_1.message);
118
- process.exit(1);
119
- return [3 /*break*/, 4];
120
- case 4:
121
- if (!skip_db_creation) return [3 /*break*/, 14];
122
- exists = false;
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
- var init_migration_table = function (client_2) {
187
- var args_1 = [];
188
- for (var _i = 1; _i < arguments.length; _i++) {
189
- args_1[_i - 1] = arguments[_i];
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
- return __awaiter(void 0, __spreadArray([client_2], args_1, true), void 0, function (client, table_engine) {
192
- var q, e_4;
193
- if (table_engine === void 0) { table_engine = 'MergeTree'; }
194
- return __generator(this, function (_a) {
195
- switch (_a.label) {
196
- case 0:
197
- 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 = ".concat(table_engine, "\n ORDER BY tuple(applied_at)");
198
- _a.label = 1;
199
- case 1:
200
- _a.trys.push([1, 3, , 4]);
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
- var get_migrations = function (migrations_home) {
221
- var files;
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', "no migration directory ".concat(migrations_home, ". Please create it."));
138
+ log('error', `no migration directory ${migrations_home}. Please create it.`);
227
139
  process.exit(1);
228
140
  }
229
- var migrations = [];
230
- files.forEach(function (file) {
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
- var version = Number(file.split('_')[0]);
147
+ const version = Number(file.split('_')[0]);
236
148
  if (!version) {
237
- log('error', "a migration name should start from number, example: 1_init.sql. Please check, if the migration ".concat(file, " is named correctly"));
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: version,
242
- file: file,
153
+ version,
154
+ file,
243
155
  });
244
156
  });
245
157
  if (!migrations) {
246
- log('error', "no migrations in the ".concat(migrations_home, " migrations directory"));
158
+ log('error', `no migrations in the ${migrations_home} migrations directory`);
247
159
  }
248
160
  // Order by version.
249
- migrations.sort(function (m1, m2) { return m1.version - m2.version; });
161
+ migrations.sort((m1, m2) => m1.version - m2.version);
250
162
  return migrations;
251
163
  };
252
- var apply_migrations = function (client, migrations, migrations_home) { return __awaiter(void 0, void 0, void 0, function () {
253
- var migration_query_result, resultSet, e_5, migrations_applied, applied_migrations, _i, migrations_1, migration_1, content, checksum, queries, sets, _a, queries_1, query, e_6, e_7;
254
- return __generator(this, function (_b) {
255
- switch (_b.label) {
256
- case 0:
257
- migration_query_result = [];
258
- _b.label = 1;
259
- case 1:
260
- _b.trys.push([1, 4, , 5]);
261
- return [4 /*yield*/, client.query({
262
- query: "SELECT version, checksum, migration_name FROM _migrations ORDER BY version",
263
- format: 'JSONEachRow',
264
- })];
265
- case 2:
266
- resultSet = _b.sent();
267
- return [4 /*yield*/, resultSet.json()];
268
- case 3:
269
- migration_query_result = _b.sent();
270
- return [3 /*break*/, 5];
271
- case 4:
272
- e_5 = _b.sent();
273
- log('error', "can't select data from the _migrations table.", e_5.message);
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
- return [3 /*break*/, 5];
276
- case 5:
277
- migrations_applied = [];
278
- migration_query_result.forEach(function (row) {
279
- migrations_applied[row.version] = {
280
- version: row.version,
281
- checksum: row.checksum,
282
- migration_name: row.migration_name,
283
- };
284
- // Check if migration file was not removed after apply.
285
- var migration_exist = migrations.find(function (_a) {
286
- var version = _a.version;
287
- return version === row.version;
288
- });
289
- if (!migration_exist) {
290
- log('error', "a migration file shouldn't be removed after apply. Please, restore the migration ".concat(row.migration_name, "."));
291
- process.exit(1);
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
- applied_migrations = '';
295
- _i = 0, migrations_1 = migrations;
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', "The migration(s) ".concat(applied_migrations, " was successfully applied!"));
226
+ log('info', `The migration(s) ${applied_migrations} was successfully applied!`);
332
227
  }
333
- 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_6.message);
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
- return [3 /*break*/, 15];
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
- var migration = function (migrations_home, host, username, password, db_name, db_engine, table_engine, timeout, ca_cert, cert, key, skip_db_creation) { return __awaiter(void 0, void 0, void 0, function () {
372
- var migrations, client;
373
- return __generator(this, function (_a) {
374
- switch (_a.label) {
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
- var migrate = function () {
396
- var program = new commander_1.Command();
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(function (options) { return __awaiter(void 0, void 0, void 0, function () {
414
- return __generator(this, function (_a) {
415
- switch (_a.label) {
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;
@@ -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
- var sql_queries = function (content) {
6
- var queries = content
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(function (el) { return el.trim(); })
13
- .filter(function (el) { return el.length != 0; });
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
- var sql_sets = function (content) {
19
- var sets = {};
20
- var sets_arr = content
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(function (set_full) {
28
- var set = set_full.split('=');
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.3.1";
1
+ export declare const VERSION = "1.4.0";
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '1.3.1'; // x-release-please-version
4
+ exports.VERSION = '1.4.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clickhouse-migrations",
3
- "version": "1.3.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.20.0",
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.37",
38
- "@typescript-eslint/eslint-plugin": "^8.57.2",
39
- "@typescript-eslint/parser": "^8.57.2",
40
- "eslint": "^10.1.0",
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.5",
43
- "jest": "^30.3.0",
44
- "prettier": "^3.8.1",
45
- "ts-jest": "^29.4.6",
46
- "typescript": "^5.9.3",
47
- "typescript-eslint": "^8.57.2"
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/**/*"