@tmlmobilidade/databases 20260509.340.15 → 20260511.1451.46

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.
@@ -44,6 +44,12 @@ export class GOClickHouseClient {
44
44
  Logger.info('[GOClickHouseClient] Connecting to database...');
45
45
  const connectionString = await this.getConnectionString();
46
46
  this.client = createClient({
47
+ clickhouse_settings: {
48
+ connect_timeout: 360 * 1000,
49
+ http_receive_timeout: 360 * 1000,
50
+ http_send_timeout: 360 * 1000,
51
+ max_execution_time: 360 * 1000,
52
+ },
47
53
  keep_alive: { enabled: false },
48
54
  log: {
49
55
  level: ClickHouseLogLevel.OFF,
@@ -7,6 +7,13 @@ export declare abstract class ClickHouseInterfaceTemplate<T extends object> {
7
7
  protected readonly abstract schema: ClickHouseSchema<T>;
8
8
  protected readonly abstract tableName: string;
9
9
  protected readonly engine: ClickHouseTableEngine;
10
+ /**
11
+ * When `true` (default), `init()` runs `ensureDatabase()` + `ensureTable()` so
12
+ * the schema is created from this class. Set to `false` for tables whose schema
13
+ * is owned externally (e.g. by a `.sql` DDL file applied at startup); the
14
+ * interface then becomes a typed insert/query helper only.
15
+ */
16
+ protected readonly manageSchema: boolean;
10
17
  protected readonly orderBy: string;
11
18
  protected readonly partitionBy: null | string;
12
19
  private client;
@@ -8,6 +8,13 @@ import { Logger } from '@tmlmobilidade/logger';
8
8
  /* * */
9
9
  export class ClickHouseInterfaceTemplate {
10
10
  engine = 'MergeTree';
11
+ /**
12
+ * When `true` (default), `init()` runs `ensureDatabase()` + `ensureTable()` so
13
+ * the schema is created from this class. Set to `false` for tables whose schema
14
+ * is owned externally (e.g. by a `.sql` DDL file applied at startup); the
15
+ * interface then becomes a typed insert/query helper only.
16
+ */
17
+ manageSchema = true;
11
18
  orderBy = '_id';
12
19
  partitionBy = null;
13
20
  client;
@@ -113,13 +120,17 @@ export class ClickHouseInterfaceTemplate {
113
120
  throw new Error('CLICKHOUSE: databaseName is required.');
114
121
  if (!this.tableName)
115
122
  throw new Error('CLICKHOUSE: tableName is required.');
116
- if (!this.schema || Object.entries(this.schema).length === 0)
123
+ if (this.manageSchema && (!this.schema || Object.entries(this.schema).length === 0)) {
117
124
  throw new Error('CLICKHOUSE: schema is required and cannot be empty.');
125
+ }
118
126
  // Connect to the ClickHouse client
119
127
  this.client = await this.connectToClient();
120
- // Ensure the database and table exist, and perform any additional setup
121
- await this.ensureDatabase();
122
- await this.ensureTable();
128
+ // Only own the schema when this interface is the source of truth.
129
+ // External DDL owners (see `manageSchema`) skip this entirely.
130
+ if (this.manageSchema) {
131
+ await this.ensureDatabase();
132
+ await this.ensureTable();
133
+ }
123
134
  await this.postInit();
124
135
  }
125
136
  /**
@@ -198,8 +209,8 @@ export class ClickHouseInterfaceTemplate {
198
209
  CREATE TABLE IF NOT EXISTS "${this.databaseName}"."${this.tableName}" (
199
210
  ${Object.entries(this.schema).map(([key, column]) => `${key} ${column.type}`).join(', ')}
200
211
  ) ENGINE = ${this.getEngineString()}
201
- ${this.orderBy ? `ORDER BY ${this.orderBy}` : ''}
202
- ${this.partitionBy ? `PARTITION BY ${this.partitionBy}` : ''}
212
+ ${this.orderBy ? `ORDER BY (${this.orderBy})` : ''}
213
+ ${this.partitionBy ? `PARTITION BY (${this.partitionBy})` : ''}
203
214
  `;
204
215
  // Perform the query to create the table
205
216
  try {
@@ -239,6 +250,8 @@ export class ClickHouseInterfaceTemplate {
239
250
  switch (this.engine) {
240
251
  case 'MergeTree':
241
252
  return `MergeTree()`;
253
+ case 'ReplacingMergeTree':
254
+ return `ReplacingMergeTree()`;
242
255
  default:
243
256
  throw new Error(`CLICKHOUSE [${this.databaseName}/${this.tableName}]: Unsupported engine type: ${this.engine}`);
244
257
  }
@@ -3,4 +3,4 @@
3
3
  * Please avoid using other engines before consulting with the team
4
4
  * as ClickHouse has many engines with different features and limitations.
5
5
  */
6
- export type ClickHouseTableEngine = 'MergeTree';
6
+ export type ClickHouseTableEngine = 'MergeTree' | 'ReplacingMergeTree';
@@ -1,4 +1,5 @@
1
1
  /* * */
2
+ import { getClickHouseParamType } from './get-clickhouse-param-type.js';
2
3
  import { validateSqlParam } from './validate-sql-param.js';
3
4
  /**
4
5
  * Prepares a SQL query with named parameters by validating the parameter keys
@@ -27,7 +28,7 @@ export function prepareNamedQueryParams(query, params, context) {
27
28
  usedKeys.add(key);
28
29
  const value = providedParams[key];
29
30
  queryParams[key] = value;
30
- return `{${key}:${this.getClickHouseParamType(value)}}`;
31
+ return `{${key}:${getClickHouseParamType(value)}}`;
31
32
  });
32
33
  // Also include explicitly typed placeholders already present in query (e.g. {id:UInt64}).
33
34
  for (const match of normalizedQuery.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*):[^}]+\}/g)) {
@@ -1,4 +1,9 @@
1
1
  import { type ClickHouseClient } from '@clickhouse/client';
2
+ /**
3
+ * Split script into top-level `;`-terminated statements. Ignores semicolons inside `--` / `block`
4
+ * comments and single-quoted strings (naive `.split(';')` breaks DDL when comments contain `;`).
5
+ */
6
+ export declare function splitClickHouseStatements(sql: string): string[];
2
7
  /**
3
8
  * Executes a query from a .sql file with optional parameter substitutions.
4
9
  * @param client The ClickHouse client to use for executing the query.
@@ -15,3 +20,8 @@ import { type ClickHouseClient } from '@clickhouse/client';
15
20
  * });
16
21
  */
17
22
  export declare function queryFromFile<T>(client: ClickHouseClient, filePath: string, params?: Record<string, number | string>): Promise<T[]>;
23
+ /**
24
+ * Like {@link queryFromFile}, but runs each `;`-terminated statement separately. Use when the file
25
+ * contains multiple statements (ClickHouse rejects multi-statement queries by default).
26
+ */
27
+ export declare function queryEachStatementFromFile<T>(client: ClickHouseClient, filePath: string, params?: Record<string, number | string>): Promise<T[]>;
@@ -2,6 +2,97 @@
2
2
  import { prepareNamedQueryParams } from './prepare-named-query-params.js';
3
3
  import { Logger } from '@tmlmobilidade/logger';
4
4
  import { readFile } from 'node:fs/promises';
5
+ function chunkHasExecutableLine(chunk) {
6
+ return chunk.split('\n').some((line) => {
7
+ const t = line.trim();
8
+ return t !== '' && !t.startsWith('--');
9
+ });
10
+ }
11
+ /**
12
+ * Split script into top-level `;`-terminated statements. Ignores semicolons inside `--` / `block`
13
+ * comments and single-quoted strings (naive `.split(';')` breaks DDL when comments contain `;`).
14
+ */
15
+ export function splitClickHouseStatements(sql) {
16
+ const out = [];
17
+ let buf = '';
18
+ let i = 0;
19
+ let lineComment = false;
20
+ let blockComment = false;
21
+ let inString = false;
22
+ while (i < sql.length) {
23
+ const c = sql[i];
24
+ const next = i + 1 < sql.length ? sql[i + 1] : undefined;
25
+ if (!c) {
26
+ break;
27
+ }
28
+ if (lineComment) {
29
+ buf += c;
30
+ if (c === '\n') {
31
+ lineComment = false;
32
+ }
33
+ i += 1;
34
+ continue;
35
+ }
36
+ if (blockComment) {
37
+ buf += c;
38
+ if (c === '*' && next === '/') {
39
+ buf += '/';
40
+ i += 2;
41
+ blockComment = false;
42
+ continue;
43
+ }
44
+ i += 1;
45
+ continue;
46
+ }
47
+ if (inString) {
48
+ buf += c;
49
+ if (c === '\'' && next === '\'') {
50
+ buf += '\'';
51
+ i += 2;
52
+ continue;
53
+ }
54
+ if (c === '\'') {
55
+ inString = false;
56
+ }
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if (c === '-' && next === '-') {
61
+ lineComment = true;
62
+ buf += '--';
63
+ i += 2;
64
+ continue;
65
+ }
66
+ if (c === '/' && next === '*') {
67
+ blockComment = true;
68
+ buf += '/*';
69
+ i += 2;
70
+ continue;
71
+ }
72
+ if (c === '\'') {
73
+ inString = true;
74
+ buf += c;
75
+ i += 1;
76
+ continue;
77
+ }
78
+ if (c === ';') {
79
+ const chunk = buf.trim();
80
+ buf = '';
81
+ i += 1;
82
+ if (chunk && chunkHasExecutableLine(chunk)) {
83
+ out.push(chunk);
84
+ }
85
+ continue;
86
+ }
87
+ buf += c;
88
+ i += 1;
89
+ }
90
+ const tail = buf.trim();
91
+ if (tail && chunkHasExecutableLine(tail)) {
92
+ out.push(tail);
93
+ }
94
+ return out;
95
+ }
5
96
  /**
6
97
  * Executes a query from a .sql file with optional parameter substitutions.
7
98
  * @param client The ClickHouse client to use for executing the query.
@@ -40,3 +131,35 @@ export async function queryFromFile(client, filePath, params) {
40
131
  throw error;
41
132
  }
42
133
  }
134
+ /**
135
+ * Like {@link queryFromFile}, but runs each `;`-terminated statement separately. Use when the file
136
+ * contains multiple statements (ClickHouse rejects multi-statement queries by default).
137
+ */
138
+ export async function queryEachStatementFromFile(client, filePath, params) {
139
+ let sql;
140
+ try {
141
+ sql = await readFile(filePath, { encoding: 'utf-8' });
142
+ }
143
+ catch (error) {
144
+ Logger.error(`CLICKHOUSE: Error @ queryEachStatementFromFile(): Failed to read SQL file "${filePath}": ${error.message}`);
145
+ throw error;
146
+ }
147
+ const statements = splitClickHouseStatements(sql);
148
+ const merged = [];
149
+ for (const statement of statements) {
150
+ const { query, queryParams } = prepareNamedQueryParams(statement, params, filePath);
151
+ try {
152
+ const result = await client.query({
153
+ format: 'JSONEachRow',
154
+ query,
155
+ query_params: queryParams,
156
+ });
157
+ merged.push(...(await result.json()));
158
+ }
159
+ catch (error) {
160
+ Logger.error(`CLICKHOUSE: Error @ queryEachStatementFromFile(): Failed to execute statement from file "${filePath}": ${error.message}`);
161
+ throw error;
162
+ }
163
+ }
164
+ return merged;
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/databases",
3
- "version": "20260509.340.15",
3
+ "version": "20260511.1451.46",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"