@transitive-sdk/clickhouse 0.3.6 → 0.3.8

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
@@ -8,7 +8,7 @@ const { topicToPath, topicMatch } = require('@transitive-sdk/datacache');
8
8
  // Default TTL in days for mqtt_history table
9
9
  const DEFAULT_TTL_DAYS = 30;
10
10
 
11
- // Shared multi-tenant schema components used by createTable and ensureMqttHistoryTable
11
+ // Shared multi-tenant schema components used by createTable and enableHistory
12
12
  const MULTI_TENANT_SCHEMA = {
13
13
  // Column definitions for OrgId and DeviceId
14
14
  columns: [
@@ -109,13 +109,15 @@ class ClickHouse {
109
109
  * @param {Array<string>} columns - array of column definitions and indexes, e.g. ['Timestamp DateTime CODEC(ZSTD(1))', 'Value Float32 CODEC(ZSTD(1))']
110
110
  * @param {Array<string>} settings - array of table settings, e.g. ['ENGINE = MergeTree()', 'ORDER BY (Timestamp)']
111
111
  */
112
- async createTable(tableName, columns, settings = []) {
112
+ async createTable(tableName, columns, settings = ['ORDER BY (OrgId, DeviceId)']) {
113
113
  const fullSchema = [
114
114
  ...columns,
115
115
  ...MULTI_TENANT_SCHEMA.columns,
116
116
  ...MULTI_TENANT_SCHEMA.indexes
117
117
  ];
118
- const query = `CREATE TABLE IF NOT EXISTS ${tableName} (${fullSchema.join(', ')}) ${settings.join(' ')}`;
118
+ const query = `CREATE TABLE IF NOT EXISTS ${tableName}
119
+ (${fullSchema.join(', ')})
120
+ ${settings.join(' ')}`;
119
121
 
120
122
  try {
121
123
  return await this.client.exec({
@@ -346,29 +348,17 @@ class ClickHouse {
346
348
  topicSelector,
347
349
  since = undefined,
348
350
  until = undefined,
349
- orderBy = 'Timestamp ASC',
351
+ orderBy = 'Timestamp DESC',
350
352
  limit = 1000
351
353
  } = options;
352
354
 
353
355
  const path = topicToPath(topicSelector);
354
356
 
355
357
  // interpret wildcards
356
- // const where = [];
357
- // _.forEach(path, (value, i) => {
358
- // if (!['+','#'].includes(value[0])) {
359
- // // it's a constant, filter by it
360
- // where.push(`TopicParts[${i + 1}] = '${value}'`);
361
- // // Note that ClickHouse/SQL index starting at 1, not 0
362
- // }
363
- // });
364
358
  const where = path2where(path);
365
-
366
359
  since && where.push(`Timestamp >= fromUnixTimestamp64Milli(${since.getTime()})`);
367
360
  until && where.push(`Timestamp <= fromUnixTimestamp64Milli(${until.getTime()})`);
368
-
369
- const whereStatement = where.length > 0
370
- ? `WHERE ${where.join(' AND ')}`
371
- : '';
361
+ const whereStatement = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
372
362
 
373
363
  const result = await this.client.query({
374
364
  query: `SELECT Payload,TopicParts,Timestamp FROM default.${this.mqttHistoryTable} ${
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitive-sdk/clickhouse",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "A tiny ClickHouse utility class for use in the Transitive framework.",
5
5
  "homepage": "https://transitiverobotics.com",
6
6
  "repository": {
@@ -33,6 +33,7 @@ const interceptInserts = () => {
33
33
  const queryRowsByOrg = async (org, options = {}) =>
34
34
  await clickhouse.queryMQTTHistory({
35
35
  topicSelector: `/${org}/+/+/+/+/+`,
36
+ orderBy: 'Timestamp ASC',
36
37
  ...options
37
38
  });
38
39
 
@@ -67,6 +68,19 @@ describe('ClickHouse', function() {
67
68
  await clickhouse.registerMqttTopicForStorage(STANDARD_TOPIC_PATTERN);
68
69
  });
69
70
 
71
+ describe('basics', () => {
72
+ it('creates tables without crashing', async () => {
73
+ const result = await clickhouse.createTable('test_tmp',
74
+ ['text String']);
75
+
76
+ // clean up
77
+ await clickhouse.client.command({
78
+ query: `DROP TABLE IF EXISTS test_tmp`,
79
+ clickhouse_settings: { wait_end_of_query: 1 }
80
+ })
81
+ });
82
+ });
83
+
70
84
  describe('enableHistory', () => {
71
85
  it('should create the mqtt_history table', async () => {
72
86
  const result = await clickhouse.client.query({
@@ -232,12 +246,14 @@ describe('ClickHouse', function() {
232
246
 
233
247
  it('queries with wild cards', async () => {
234
248
  const rows = await clickhouse.queryMQTTHistory({
249
+ orderBy: 'Timestamp ASC',
235
250
  topicSelector: `/${org}/+/+/+/+/+` });
236
251
  assert(rows.length > 0);
237
252
  });
238
253
 
239
254
  it('queries with multiple selectors', async () => {
240
255
  const [row] = await clickhouse.queryMQTTHistory({
256
+ orderBy: 'Timestamp ASC',
241
257
  topicSelector: `/${org}/+/+/capdata/+/+` });
242
258
  assert.strictEqual(row.DeviceId, 'device1');
243
259
  assert.deepEqual(row.SubTopic, ['data']);
@@ -247,6 +263,7 @@ describe('ClickHouse', function() {
247
263
 
248
264
  it('queries based on sub-topic selectors', async () => {
249
265
  const [row] = await clickhouse.queryMQTTHistory({
266
+ orderBy: 'Timestamp ASC',
250
267
  topicSelector: `/${org}/+/+/+/+/data2` });
251
268
  assert.strictEqual(row.DeviceId, 'device1');
252
269
  assert.deepStrictEqual(row.Payload, { y: 2 });
@@ -254,12 +271,14 @@ describe('ClickHouse', function() {
254
271
 
255
272
  it('queries based on sub-topic selectors with wildcards', async () => {
256
273
  const [row] = await clickhouse.queryMQTTHistory({
274
+ orderBy: 'Timestamp ASC',
257
275
  topicSelector: `/${org}/+/+/+/+/+/sub2/+` });
258
276
  assert.deepStrictEqual(row.SubTopic[2], 'sub3.1');
259
277
  });
260
278
 
261
279
  it('queries based on multiple sub-topic selectors with wildcards', async () => {
262
280
  const rows = await clickhouse.queryMQTTHistory({
281
+ orderBy: 'Timestamp ASC',
263
282
  topicSelector: `/${org}/+/+/+/+/sub1/+/+` });
264
283
  assert.strictEqual(rows[0].SubTopic.length, 3);
265
284
  assert.strictEqual(rows[0].SubTopic[2], 'sub3.1');
@@ -268,6 +287,7 @@ describe('ClickHouse', function() {
268
287
 
269
288
  it('returns the history', async () => {
270
289
  const rows = await clickhouse.queryMQTTHistory({
290
+ orderBy: 'Timestamp ASC',
271
291
  topicSelector: `/${org}/+/+/+/+/data/+/+` });
272
292
  assert.deepStrictEqual(rows.length, 2);
273
293
  assert.deepStrictEqual(rows[0].Payload, {x: 1});
@@ -277,6 +297,7 @@ describe('ClickHouse', function() {
277
297
 
278
298
  it('handles null values', async () => {
279
299
  const rows = await clickhouse.queryMQTTHistory({
300
+ orderBy: 'Timestamp ASC',
280
301
  topicSelector: `/${org}/+/+/+/+/willBeNull` });
281
302
  assert.strictEqual(rows.at(-1).Payload, null);
282
303
  });
@@ -301,7 +322,8 @@ describe('ClickHouse', function() {
301
322
  for (let i = 0; i < ROWS; i++) {
302
323
  rows.push({
303
324
  Timestamp: new Date(now + i * GAP), // use current date to avoid immediate TTL cleanup
304
- TopicParts: [`org${i % 50}`, `device${i % 1000}`, '@myscope', `cap${i % 100}`, `1.${i % 100}.0`, 'data', i],
325
+ TopicParts: [`org${i % 50}`, `device${i % 1000}`, '@myscope',
326
+ `cap${i % 100}`, `1.${i % 100}.0`, `data_${i % 1000}`, i],
305
327
  Payload: { i },
306
328
  })
307
329
  }
@@ -332,7 +354,7 @@ describe('ClickHouse', function() {
332
354
  limit: 2 * ROWS,
333
355
  });
334
356
  assert.equal(rows.length, ROWS);
335
- assert(rows[0].Timestamp < rows[1].Timestamp);
357
+ assert(rows[0].Timestamp > rows[1].Timestamp);
336
358
  assertTimelimit(ROWS / 100);
337
359
  });
338
360
 
@@ -347,7 +369,7 @@ describe('ClickHouse', function() {
347
369
 
348
370
  it('quickly filters by DeviceId', async () => {
349
371
  const rows = await clickhouse.queryMQTTHistory({
350
- topicSelector: `/+/device123/+/+/+/data`,
372
+ topicSelector: `/+/device123/+/+/+/+`,
351
373
  limit: 2 * ROWS,
352
374
  });
353
375
  assert.equal(rows.length, ROWS / 1000);
@@ -356,16 +378,25 @@ describe('ClickHouse', function() {
356
378
 
357
379
  it('quickly filters by CapabilityName', async () => {
358
380
  const rows = await clickhouse.queryMQTTHistory({
359
- topicSelector: `/+/+/+/cap34/+/data`,
381
+ topicSelector: `/+/+/+/cap34/+/+`,
360
382
  limit: 2 * ROWS,
361
383
  });
362
384
  assert.equal(rows.length, ROWS / 100);
363
385
  assertTimelimit(ROWS / 1000);
364
386
  });
365
387
 
388
+ it('quickly filters by SubTopic', async () => {
389
+ const rows = await clickhouse.queryMQTTHistory({
390
+ topicSelector: `/+/+/+/+/+/data_123`,
391
+ limit: 2 * ROWS,
392
+ });
393
+ assert.equal(rows.length, ROWS / 1000);
394
+ assertTimelimit(ROWS / 1000);
395
+ });
396
+
366
397
  it('quickly filters by time: since', async () => {
367
398
  const rows = await clickhouse.queryMQTTHistory({
368
- topicSelector: `/+/+/+/+/+/data`,
399
+ topicSelector: `/+/+/+/+/+/+`,
369
400
  since: new Date(now + (ROWS - 400) * GAP),
370
401
  limit: 2 * ROWS,
371
402
  });
@@ -375,7 +406,7 @@ describe('ClickHouse', function() {
375
406
 
376
407
  it('quickly filters by time: until', async () => {
377
408
  const rows = await clickhouse.queryMQTTHistory({
378
- topicSelector: `/+/+/+/+/+/data`,
409
+ topicSelector: `/+/+/+/+/+/+`,
379
410
  until: new Date(now + 400 * GAP),
380
411
  limit: 2 * ROWS,
381
412
  });
@@ -385,7 +416,7 @@ describe('ClickHouse', function() {
385
416
 
386
417
  it('quickly filters by org and time: since', async () => {
387
418
  const rows = await clickhouse.queryMQTTHistory({
388
- topicSelector: `/org23/+/+/+/+/data`,
419
+ topicSelector: `/org23/+/+/+/+/+`,
389
420
  since: new Date(now + (ROWS - 400) * GAP),
390
421
  limit: 2 * ROWS,
391
422
  });