@transitive-sdk/clickhouse 0.4.2 → 0.5.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/index.js +45 -20
- package/package.json +2 -2
- package/test/clickhouse.test.js +3 -2
package/index.js
CHANGED
|
@@ -47,6 +47,8 @@ class ClickHouse {
|
|
|
47
47
|
|
|
48
48
|
mqttHistoryTable = null; // name of the table used for MQTT history, if used
|
|
49
49
|
topics = {}; // list of topics registered for storage, as object for de-duplication
|
|
50
|
+
rowCache = {}; // cache of rows awaiting insertion, by table
|
|
51
|
+
insertionInterval = null; // the actual interval
|
|
50
52
|
|
|
51
53
|
/** Create the client, connecting to Clickhouse */
|
|
52
54
|
async init({ url, dbName, user, password } = {}) {
|
|
@@ -142,7 +144,7 @@ class ClickHouse {
|
|
|
142
144
|
* @param {string} orgId - organization ID to add to each row
|
|
143
145
|
* @param {string} deviceId - device ID to add to each row
|
|
144
146
|
*/
|
|
145
|
-
|
|
147
|
+
insert(tableName, rows, orgId, deviceId) {
|
|
146
148
|
// assert that orgId and deviceId are provided
|
|
147
149
|
if (!orgId || !deviceId) {
|
|
148
150
|
throw new Error('Both orgId and deviceId must be provided for multi-tenant insert');
|
|
@@ -155,18 +157,15 @@ class ClickHouse {
|
|
|
155
157
|
DeviceId: deviceId
|
|
156
158
|
}));
|
|
157
159
|
|
|
158
|
-
return
|
|
159
|
-
table: tableName,
|
|
160
|
-
values: rowsWithIds,
|
|
161
|
-
format: 'JSONEachRow'
|
|
162
|
-
});
|
|
160
|
+
return this.addToCache(tableName, rowsWithIds);
|
|
163
161
|
}
|
|
164
162
|
|
|
165
163
|
/* Enable history recording. Ensure the mqtt_history table exists with the
|
|
166
164
|
* correct schema, set dataCache, and subscribe to changes.
|
|
167
165
|
* @param {object} options = {dataCache, tableName, ttlDays}
|
|
166
|
+
* @param {number} interval = ms interval between batch insertions
|
|
168
167
|
*/
|
|
169
|
-
async enableHistory(options) {
|
|
168
|
+
async enableHistory(options, interval = 10_000) {
|
|
170
169
|
const { dataCache, tableName = 'mqtt_history' } = options;
|
|
171
170
|
|
|
172
171
|
if (this.mqttHistoryTable != tableName) {
|
|
@@ -241,7 +240,7 @@ class ClickHouse {
|
|
|
241
240
|
// it matches any of the registered topics (this avoid duplicate triggers),
|
|
242
241
|
// then store to ClickHouse with current timestamp.
|
|
243
242
|
dataCache.subscribe((changes) => {
|
|
244
|
-
_.forEach(changes,
|
|
243
|
+
_.forEach(changes, (value, topic) => {
|
|
245
244
|
|
|
246
245
|
const matched =
|
|
247
246
|
_.some(this.topics, (_true, selector) => topicMatch(selector, topic));
|
|
@@ -257,24 +256,46 @@ class ClickHouse {
|
|
|
257
256
|
row.Payload = JSON.stringify(value);
|
|
258
257
|
} // else: omit
|
|
259
258
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
table: this.mqttHistoryTable,
|
|
263
|
-
values: [row],
|
|
264
|
-
format: 'JSONEachRow'
|
|
265
|
-
});
|
|
266
|
-
} catch (error) {
|
|
267
|
-
console.error('Error inserting MQTT message into ClickHouse:', error.message);
|
|
268
|
-
}
|
|
259
|
+
// cache it for batch insertion
|
|
260
|
+
this.addToCache(this.mqttHistoryTable, [row]);
|
|
269
261
|
})
|
|
270
262
|
});
|
|
263
|
+
}
|
|
271
264
|
|
|
265
|
+
this.mqttHistoryTable = tableName;
|
|
272
266
|
|
|
267
|
+
// start interval for batch insertion
|
|
268
|
+
if (!this.insertionInterval) {
|
|
269
|
+
this.insertionInterval =
|
|
270
|
+
setInterval(this.batchInsertCache.bind(this), interval);
|
|
273
271
|
}
|
|
272
|
+
}
|
|
274
273
|
|
|
275
|
-
|
|
274
|
+
/** Add the given rows to the cache for batch-insertion to the given table */
|
|
275
|
+
addToCache(table, rows) {
|
|
276
|
+
this.rowCache[this.mqttHistoryTable] ||= [];
|
|
277
|
+
this.rowCache[this.mqttHistoryTable].push(...rows);
|
|
276
278
|
}
|
|
277
279
|
|
|
280
|
+
/** Function responsible fgor inserting all cached rows */
|
|
281
|
+
batchInsertCache() {
|
|
282
|
+
_.forEach(this.rowCache, (rows, table) => {
|
|
283
|
+
if (rows.length == 0) return;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
this.rowCache[table] = [];
|
|
287
|
+
this.client.insert({
|
|
288
|
+
table,
|
|
289
|
+
values: rows,
|
|
290
|
+
format: 'JSONEachRow'
|
|
291
|
+
});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error(`Error inserting ${rows.length} rows into ${table}`, error.message);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
278
299
|
/* Register an MQTT topic for storage in ClickHouse subscribes to the topic
|
|
279
300
|
* and stores incoming messages JSON.stringify'd in a ClickHouse table.
|
|
280
301
|
* Retrieve using `queryMQTTHistory`, or, when quering directly, e.g., from
|
|
@@ -415,7 +436,12 @@ class ClickHouse {
|
|
|
415
436
|
// SQL sub-string to extract the desired value from the JSON payload
|
|
416
437
|
// const wildParts = wildIndices.map(i => `TopicParts[${i + 1}]`);
|
|
417
438
|
// update SELECT statement with aggregations
|
|
418
|
-
|
|
439
|
+
|
|
440
|
+
select = [
|
|
441
|
+
// Cast `count` result to Float64 to avoid UInt64 which ClickHouse turns
|
|
442
|
+
// into a string in JSON.
|
|
443
|
+
agg == 'count' ? `CAST(${agg}(${extractValue}), 'Float64') as aggValue`
|
|
444
|
+
: `${agg}(${extractValue}) as aggValue`,
|
|
419
445
|
// ...wildParts,
|
|
420
446
|
'TopicParts',
|
|
421
447
|
`toStartOfInterval(Timestamp, INTERVAL ${aggSeconds} SECOND) as time`
|
|
@@ -428,7 +454,6 @@ class ClickHouse {
|
|
|
428
454
|
whereStatement} ${group} ORDER BY ${orderBy} ${limit ? ` LIMIT ${limit}` : ''}`;
|
|
429
455
|
// console.log(query);
|
|
430
456
|
const result = await this.client.query({ query, format: 'JSONEachRow' });
|
|
431
|
-
|
|
432
457
|
const rows = await result.json();
|
|
433
458
|
|
|
434
459
|
// map payloads back from JSON; this is the inverse of what we do in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transitive-sdk/clickhouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A tiny ClickHouse utility class for use in the Transitive framework.",
|
|
5
5
|
"homepage": "https://transitiverobotics.com",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test": "node --test --watch"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@clickhouse/client": "^1.
|
|
27
|
+
"@clickhouse/client": "^1.16.0",
|
|
28
28
|
"@transitive-sdk/datacache": "^0.14.1",
|
|
29
29
|
"wait-port": "^1.1.0"
|
|
30
30
|
},
|
package/test/clickhouse.test.js
CHANGED
|
@@ -63,7 +63,7 @@ describe('ClickHouse', function() {
|
|
|
63
63
|
await clickhouse.enableHistory({
|
|
64
64
|
dataCache,
|
|
65
65
|
tableName: TABLE_NAME
|
|
66
|
-
});
|
|
66
|
+
}, 100);
|
|
67
67
|
|
|
68
68
|
await clickhouse.registerMqttTopicForStorage(STANDARD_TOPIC_PATTERN);
|
|
69
69
|
});
|
|
@@ -130,7 +130,6 @@ describe('ClickHouse', function() {
|
|
|
130
130
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
131
131
|
dataCache.update([org, 'device1', 'data'], null);
|
|
132
132
|
await once(emitter, 'insert');
|
|
133
|
-
await once(emitter, 'insert');
|
|
134
133
|
|
|
135
134
|
const rows = await queryRowsByOrg(org);
|
|
136
135
|
|
|
@@ -443,6 +442,7 @@ describe('ClickHouse', function() {
|
|
|
443
442
|
limit: 2 * ROWS,
|
|
444
443
|
});
|
|
445
444
|
// there can be one-off errors due to rounding down to start of interval:
|
|
445
|
+
assert.strictEqual(typeof rows[0].aggValue, 'number');
|
|
446
446
|
assert(Math.abs(rows.length - 60) < 2);
|
|
447
447
|
assertTimelimit(ROWS / 10000);
|
|
448
448
|
});
|
|
@@ -471,6 +471,7 @@ describe('ClickHouse', function() {
|
|
|
471
471
|
limit: 2 * ROWS,
|
|
472
472
|
});
|
|
473
473
|
console.log(rows[0]);
|
|
474
|
+
assert.strictEqual(typeof rows[0].aggValue, 'number');
|
|
474
475
|
assert.equal(rows.length, 10000);
|
|
475
476
|
assertTimelimit(ROWS / 1000);
|
|
476
477
|
});
|