clickhouse-migrations 1.5.1 → 1.6.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
@@ -1,6 +1,6 @@
1
1
  # clickhouse-migrations
2
2
 
3
- > ClickHouse Migrations CLI
3
+ > ClickHouse Migrations CLI and library
4
4
 
5
5
  ## Install
6
6
 
@@ -153,6 +153,59 @@ LAYOUT(COMPLEX_KEY_HASHED());
153
153
 
154
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
155
 
156
+ ## Use as a library
157
+
158
+ Import `migration` and pass an options object. Named options let you set a timeout or an engine without filling in unused arguments.
159
+
160
+ ```typescript
161
+ import { migration, type MigrationOptions } from 'clickhouse-migrations';
162
+
163
+ const options: MigrationOptions = {
164
+ migrations_home: './migrations',
165
+ host: 'http://localhost:8123',
166
+ username: 'default',
167
+ password: '',
168
+ db_name: 'analytics',
169
+ timeout: '60000',
170
+ };
171
+
172
+ await migration(options);
173
+ ```
174
+
175
+ For CommonJS, use `const { migration } = require('clickhouse-migrations')` and call it from an async function.
176
+
177
+ Possible function parameters:
178
+
179
+ | Option | Type | Required | Description |
180
+ | --- | --- | --- | --- |
181
+ | `migrations_home` | `string` | Yes | Directory containing the SQL files, relative to the working directory or absolute. |
182
+ | `host` | `string` | Yes | ClickHouse HTTP or HTTPS URL, including the port. |
183
+ | `username` | `string` | Yes | ClickHouse username. |
184
+ | `password` | `string` | Yes | Use `''` for an empty password. |
185
+ | `db_name` | `string` | Yes | Target database. Created if it does not exist, unless `skip_db_creation` is enabled. |
186
+ | `db_engine` | `string` | No | Optional `ON CLUSTER` and/or `ENGINE` clauses for database creation. If omitted, ClickHouse chooses its default engine. |
187
+ | `table_engine` | `string` | No | Engine for the `_migrations` table. Defaults to `MergeTree`. |
188
+ | `timeout` | `string` | No | Request timeout in milliseconds, for example `'60000'`. Defaults to the client's `30000` ms. |
189
+ | `ca_cert` | `string` | No | Path to a CA certificate file for TLS. |
190
+ | `cert` | `string` | No | Path to a client certificate. Use with `ca_cert` and `key`. |
191
+ | `key` | `string` | No | Path to the client private key. Use with `ca_cert` and `cert`. |
192
+ | `skip_db_creation` | `boolean` | No | Defaults to `false`. When `true`, checks that the database exists without trying to create it. |
193
+
194
+ Connection options come from the object. The CLI's `CH_MIGRATIONS_*` connection variables are not read by `migration()`. [Environment variable substitution](#environment-variable-substitution-optional) in SQL files uses `CH_MIGRATIONS_SUBSTITUTE_ENV` for both the library and the CLI.
195
+
196
+ `migration(options)` logs progress to the console and returns a `Promise<void>` that resolves after migrations finish successfully and connections are closed. If a migration fails, it attempts to close its connections and rejects with an error your application can catch. It does not exit the process. A cleanup failure does not replace the original migration error, and underlying client or SQL parsing errors are available through `error.cause`. The CLI and deprecated positional form also attempt cleanup, but then log the error and exit with status `1`.
197
+
198
+ ```typescript
199
+ try {
200
+ await migration(options);
201
+ } catch (error) {
202
+ console.error('Migration failed:', error);
203
+ // Decide whether to retry, stop startup, or shut down your application.
204
+ }
205
+ ```
206
+
207
+ **Note:** The previously undocumented positional form of `migration()` is deprecated and will be removed in a future major release. If you use it, switch to the options object shown above.
208
+
156
209
  ## SQL execution and settings
157
210
 
158
211
  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.
package/lib/migrate.d.ts CHANGED
@@ -1,3 +1,35 @@
1
- declare const migration: (migrations_home: string, host: string, username: string, password: string, db_name: string, db_engine?: string, table_engine?: string, timeout?: string, ca_cert?: string | undefined, cert?: string | undefined, key?: string | undefined, skip_db_creation?: boolean) => Promise<void>;
1
+ /** Options object for applying migrations. */
2
+ export interface MigrationOptions {
3
+ migrations_home: string;
4
+ host: string;
5
+ username: string;
6
+ password: string;
7
+ db_name: string;
8
+ db_engine?: string;
9
+ table_engine?: string;
10
+ timeout?: string;
11
+ ca_cert?: string;
12
+ cert?: string;
13
+ key?: string;
14
+ skip_db_creation?: boolean;
15
+ }
16
+ type LegacyMigrationArguments = [
17
+ migrations_home: string,
18
+ host: string,
19
+ username: string,
20
+ password: string,
21
+ db_name: string,
22
+ db_engine?: string,
23
+ table_engine?: string,
24
+ timeout?: string,
25
+ ca_cert?: string,
26
+ cert?: string,
27
+ key?: string,
28
+ skip_db_creation?: boolean
29
+ ];
30
+ /** Apply migrations, rejecting on failure without exiting the process. */
31
+ declare function migration(options: MigrationOptions): Promise<void>;
32
+ /** @deprecated Use migration(options). Positional calls log failures and exit with status 1. */
33
+ declare function migration(...args: LegacyMigrationArguments): Promise<void>;
2
34
  declare const migrate: () => void;
3
35
  export { migrate, migration };
package/lib/migrate.js CHANGED
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.migration = exports.migrate = void 0;
6
+ exports.migrate = void 0;
7
+ exports.migration = migration;
7
8
  const client_1 = require("@clickhouse/client");
8
9
  const commander_1 = require("commander");
9
10
  const fs_1 = __importDefault(require("fs"));
@@ -47,8 +48,7 @@ const connect = (url, username, password, db_name, timeout, ca_cert, cert, key)
47
48
  }
48
49
  }
49
50
  catch (e) {
50
- log('error', 'Failed to read CA certificate file for TLS connection.', e instanceof Error ? e.message : String(e));
51
- process.exit(1);
51
+ throw new Error('Failed to read CA certificate file for TLS connection.', { cause: e });
52
52
  }
53
53
  }
54
54
  return (0, client_1.createClient)(db_params);
@@ -57,52 +57,53 @@ const create_db = async (host, username, password, db_name, db_engine, timeout,
57
57
  // Don't specify database name when creating it - connect to default database
58
58
  const client = connect(host, username, password, undefined, timeout, ca_cert, cert, key);
59
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
60
  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;
61
+ await client.ping();
77
62
  }
78
63
  catch (e) {
79
- log('error', `can't verify that the database ${db_name} exists.`, e.message);
80
- await client.close();
81
- process.exit(1);
64
+ throw new Error(`Failed to connect to ClickHouse`, { cause: e });
82
65
  }
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);
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;
77
+ }
78
+ catch (e) {
79
+ throw new Error(`can't verify that the database ${db_name} exists.`, { cause: e });
80
+ }
81
+ if (!exists) {
82
+ throw new Error(`the database ${db_name} doesn't exist and --skip-db-creation is set. Please create the database manually or remove --skip-db-creation.`);
83
+ }
84
+ }
85
+ else {
86
+ // In open source ClickHouse - default DB engine is "Atomic", for Cloud - "Shared". If not set, appropriate default is used.
87
+ const q = db_engine
88
+ ? `CREATE DATABASE IF NOT EXISTS "${db_name}" ${db_engine}`
89
+ : `CREATE DATABASE IF NOT EXISTS "${db_name}"`;
90
+ try {
91
+ await client.command({
92
+ query: q,
93
+ clickhouse_settings: {
94
+ wait_end_of_query: 1,
95
+ },
96
+ });
97
+ }
98
+ catch (e) {
99
+ throw new Error(`can't create the database ${db_name}.`, { cause: e });
100
+ }
87
101
  }
88
- await client.close();
89
- return;
90
- }
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
- },
101
- });
102
102
  }
103
- catch (e) {
104
- log('error', `can't create the database ${db_name}.`, e.message);
105
- process.exit(1);
103
+ catch (error) {
104
+ // Preserve the original failure if closing the connection also fails.
105
+ await client.close().catch(() => undefined);
106
+ throw error;
106
107
  }
107
108
  await client.close();
108
109
  };
@@ -125,8 +126,7 @@ const init_migration_table = async (client, table_engine = 'MergeTree') => {
125
126
  });
126
127
  }
127
128
  catch (e) {
128
- log('error', `can't create the _migrations table.`, e.message);
129
- process.exit(1);
129
+ throw new Error(`can't create the _migrations table.`, { cause: e });
130
130
  }
131
131
  };
132
132
  const get_migrations = (migrations_home) => {
@@ -134,9 +134,8 @@ const get_migrations = (migrations_home) => {
134
134
  try {
135
135
  files = fs_1.default.readdirSync(migrations_home);
136
136
  }
137
- catch (e) {
138
- log('error', `no migration directory ${migrations_home}. Please create it.`);
139
- process.exit(1);
137
+ catch {
138
+ throw new Error(`no migration directory ${migrations_home}. Please create it.`);
140
139
  }
141
140
  const migrations = [];
142
141
  files.forEach((file) => {
@@ -146,8 +145,7 @@ const get_migrations = (migrations_home) => {
146
145
  }
147
146
  const version = Number(file.split('_')[0]);
148
147
  if (!version) {
149
- log('error', `a migration name should start from number, example: 1_init.sql. Please check, if the migration ${file} is named correctly`);
150
- process.exit(1);
148
+ throw new Error(`a migration name should start from number, example: 1_init.sql. Please check, if the migration ${file} is named correctly`);
151
149
  }
152
150
  migrations.push({
153
151
  version,
@@ -162,7 +160,7 @@ const get_migrations = (migrations_home) => {
162
160
  return migrations;
163
161
  };
164
162
  const apply_migrations = async (client, migrations, migrations_home) => {
165
- let migration_query_result = [];
163
+ let migration_query_result;
166
164
  try {
167
165
  const resultSet = await client.query({
168
166
  query: `SELECT version, checksum, migration_name FROM _migrations ORDER BY version`,
@@ -171,8 +169,7 @@ const apply_migrations = async (client, migrations, migrations_home) => {
171
169
  migration_query_result = await resultSet.json();
172
170
  }
173
171
  catch (e) {
174
- log('error', `can't select data from the _migrations table.`, e.message);
175
- process.exit(1);
172
+ throw new Error(`can't select data from the _migrations table.`, { cause: e });
176
173
  }
177
174
  const migrations_applied = [];
178
175
  migration_query_result.forEach((row) => {
@@ -184,8 +181,7 @@ const apply_migrations = async (client, migrations, migrations_home) => {
184
181
  // Check if migration file was not removed after apply.
185
182
  const migration_exist = migrations.find(({ version }) => version === row.version);
186
183
  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);
184
+ throw new Error(`a migration file shouldn't be removed after apply. Please, restore the migration ${row.migration_name}.`);
189
185
  }
190
186
  });
191
187
  let applied_migrations = '';
@@ -195,8 +191,7 @@ const apply_migrations = async (client, migrations, migrations_home) => {
195
191
  if (migrations_applied[migration.version]) {
196
192
  // Check if migration file was not changed after apply.
197
193
  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.`);
199
- process.exit(1);
194
+ throw new Error(`a migration file should't be changed after apply. Please, restore content of the ${migrations_applied[migration.version].migration_name} migrations.`);
200
195
  }
201
196
  // Skip if a migration is already applied.
202
197
  continue;
@@ -209,8 +204,7 @@ const apply_migrations = async (client, migrations, migrations_home) => {
209
204
  ({ queries, settings } = (0, sql_parse_1.parse_migration_sql)((0, sql_parse_1.substitute_env)(content)));
210
205
  }
211
206
  catch (e) {
212
- log('error', `the migration ${migration.file} has an error.`, e instanceof Error ? e.message : String(e));
213
- process.exit(1);
207
+ throw new Error(`the migration ${migration.file} has an error.`, { cause: e });
214
208
  }
215
209
  // Keep the established file-wide SET behavior: the last assignment wins,
216
210
  // including for queries earlier in the file. Send settings on every request
@@ -228,8 +222,7 @@ const apply_migrations = async (client, migrations, migrations_home) => {
228
222
  if (applied_migrations) {
229
223
  log('info', `The migration(s) ${applied_migrations} was successfully applied!`);
230
224
  }
231
- 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);
232
- process.exit(1);
225
+ throw new 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.`, { cause: e });
233
226
  }
234
227
  }
235
228
  try {
@@ -240,8 +233,7 @@ const apply_migrations = async (client, migrations, migrations_home) => {
240
233
  });
241
234
  }
242
235
  catch (e) {
243
- log('error', `can't insert a data into the table _migrations.`, e.message);
244
- process.exit(1);
236
+ throw new Error(`can't insert a data into the table _migrations.`, { cause: e });
245
237
  }
246
238
  applied_migrations = applied_migrations ? applied_migrations + ', ' + migration.file : migration.file;
247
239
  }
@@ -252,15 +244,54 @@ const apply_migrations = async (client, migrations, migrations_home) => {
252
244
  log('info', `No migrations to apply.`);
253
245
  }
254
246
  };
255
- const migration = async (migrations_home, host, username, password, db_name, db_engine, table_engine, timeout, ca_cert, cert, key, skip_db_creation) => {
247
+ const run_migration = async ({ migrations_home, host, username, password, db_name, db_engine, table_engine, timeout, ca_cert, cert, key, skip_db_creation, }) => {
256
248
  const migrations = get_migrations(migrations_home);
257
249
  await create_db(host, username, password, db_name, db_engine, timeout, ca_cert, cert, key, skip_db_creation);
258
250
  const client = connect(host, username, password, db_name, timeout, ca_cert, cert, key);
259
- await init_migration_table(client, table_engine);
260
- await apply_migrations(client, migrations, migrations_home);
251
+ try {
252
+ await init_migration_table(client, table_engine);
253
+ await apply_migrations(client, migrations, migrations_home);
254
+ }
255
+ catch (error) {
256
+ await client.close().catch(() => undefined);
257
+ throw error;
258
+ }
261
259
  await client.close();
262
260
  };
263
- exports.migration = migration;
261
+ async function migration(...args) {
262
+ if (typeof args[0] !== 'string') {
263
+ // If the first argument is not a string, assume it's the new options object.
264
+ return run_migration(args[0]);
265
+ }
266
+ // Keep using legacy positional arguments for backward compatibility.
267
+ const [migrations_home, host, username, password, db_name, db_engine, table_engine, timeout, ca_cert, cert, key, skip_db_creation,] = args;
268
+ try {
269
+ await run_migration({
270
+ migrations_home,
271
+ host,
272
+ username,
273
+ password,
274
+ db_name,
275
+ db_engine,
276
+ table_engine,
277
+ timeout,
278
+ ca_cert,
279
+ cert,
280
+ key,
281
+ skip_db_creation,
282
+ });
283
+ }
284
+ catch (error) {
285
+ const message = error instanceof Error ? error.message : String(error);
286
+ const cause = error instanceof Error ? error.cause : undefined;
287
+ let detail;
288
+ if (cause !== undefined) {
289
+ detail = cause instanceof Error ? cause.message : String(cause);
290
+ }
291
+ log('error', message, detail);
292
+ process.exit(1);
293
+ }
294
+ }
264
295
  const migrate = () => {
265
296
  const program = new commander_1.Command();
266
297
  program.name('clickhouse-migrations').description('ClickHouse migrations.').version(version_1.VERSION);
@@ -280,7 +311,32 @@ const migrate = () => {
280
311
  .option('--key <path>', 'Client key file path', process.env.CH_MIGRATIONS_KEY)
281
312
  .option('--skip-db-creation', 'Skip database creation', process.env.CH_MIGRATIONS_SKIP_DB_CREATION === 'true')
282
313
  .action(async (options) => {
283
- 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);
314
+ try {
315
+ await run_migration({
316
+ migrations_home: options.migrationsHome,
317
+ host: options.host,
318
+ username: options.user,
319
+ password: options.password,
320
+ db_name: options.db,
321
+ db_engine: options.dbEngine,
322
+ table_engine: options.tableEngine,
323
+ timeout: options.timeout,
324
+ ca_cert: options.caCert,
325
+ cert: options.cert,
326
+ key: options.key,
327
+ skip_db_creation: options.skipDbCreation,
328
+ });
329
+ }
330
+ catch (error) {
331
+ const message = error instanceof Error ? error.message : String(error);
332
+ const cause = error instanceof Error ? error.cause : undefined;
333
+ let detail;
334
+ if (cause !== undefined) {
335
+ detail = cause instanceof Error ? cause.message : String(cause);
336
+ }
337
+ log('error', message, detail);
338
+ process.exit(1);
339
+ }
284
340
  });
285
341
  program.parse();
286
342
  };
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.5.1";
1
+ export declare const VERSION = "1.6.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.5.1'; // x-release-please-version
4
+ exports.VERSION = '1.6.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clickhouse-migrations",
3
- "version": "1.5.1",
4
- "description": "ClickHouse Migrations",
3
+ "version": "1.6.0",
4
+ "description": "ClickHouse Migrations CLI and library.",
5
5
  "bin": {
6
6
  "clickhouse-migrations": "lib/cli.js"
7
7
  },