@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.
- package/dist/clients/go-clickhouse.js +6 -0
- package/dist/templates/clickhouse.d.ts +7 -0
- package/dist/templates/clickhouse.js +19 -6
- package/dist/types/clickhouse/table-engines.d.ts +1 -1
- package/dist/utils/clickhouse/prepare-named-query-params.js +2 -1
- package/dist/utils/clickhouse/query-from-file.d.ts +10 -0
- package/dist/utils/clickhouse/query-from-file.js +123 -0
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
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}:${
|
|
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
|
+
}
|