@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 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
- async insert(tableName, rows, orgId, deviceId) {
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 await this.client.insert({
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, async (value, topic) => {
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
- try {
261
- await this.client.insert({
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
- this.mqttHistoryTable = tableName;
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
- select = [`${agg}(${extractValue}) as aggValue`,
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.4.2",
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.12.1",
27
+ "@clickhouse/client": "^1.16.0",
28
28
  "@transitive-sdk/datacache": "^0.14.1",
29
29
  "wait-port": "^1.1.0"
30
30
  },
@@ -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
  });