clickhouse-migrations 1.3.2 → 1.4.1

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/migrate.js CHANGED
@@ -201,9 +201,19 @@ const apply_migrations = async (client, migrations, migrations_home) => {
201
201
  // Skip if a migration is already applied.
202
202
  continue;
203
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
+ }
204
214
  // Extract sql from the migration.
205
- const queries = (0, sql_parse_1.sql_queries)(content);
206
- const sets = (0, sql_parse_1.sql_sets)(content);
215
+ const queries = (0, sql_parse_1.sql_queries)(sql);
216
+ const sets = (0, sql_parse_1.sql_sets)(sql);
207
217
  for (const query of queries) {
208
218
  try {
209
219
  await client.command({
@@ -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,6 +1,56 @@
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
55
  const sql_queries = (content) => {
6
56
  const queries = content
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.3.2";
1
+ export declare const VERSION = "1.4.1";
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.2'; // x-release-please-version
4
+ exports.VERSION = '1.4.1'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clickhouse-migrations",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
4
4
  "description": "ClickHouse Migrations",
5
5
  "bin": {
6
6
  "clickhouse-migrations": "lib/cli.js"
@@ -28,7 +28,7 @@
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": {
@@ -49,6 +49,7 @@
49
49
  "overrides": {
50
50
  "js-yaml": "^4.2.0",
51
51
  "@babel/core": "^7.29.7",
52
+ "brace-expansion": "^5.0.12",
52
53
  "test-exclude": "^8.0.0",
53
54
  "glob": "^13.0.6"
54
55
  },