node-red-contrib-questdb 0.2.0 → 0.3.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/nodes/mapper.html CHANGED
@@ -62,7 +62,7 @@
62
62
  const typeSelect = $('<select/>', {class: "column-type"})
63
63
  .css({width: "20%"})
64
64
  .appendTo(row);
65
- ['auto', 'float', 'integer', 'string', 'boolean', 'timestamp'].forEach(function(t) {
65
+ ['auto', 'float', 'double', 'integer', 'long', 'decimal', 'string', 'boolean', 'timestamp', 'array', 'array_double', 'array_long', 'array_string'].forEach(function(t) {
66
66
  $('<option/>').val(t).text(t).appendTo(typeSelect);
67
67
  });
68
68
  typeSelect.val(data.type || "auto");
@@ -151,6 +151,23 @@
151
151
  <dd>Map msg fields to QuestDB columns with type conversion</dd>
152
152
  </dl>
153
153
 
154
+ <h3>Column Types</h3>
155
+ <ul>
156
+ <li><b>auto</b> - Auto-detect type from value</li>
157
+ <li><b>float</b> - 32-bit floating point</li>
158
+ <li><b>double</b> - 64-bit floating point (higher precision)</li>
159
+ <li><b>integer</b> - 32-bit signed integer</li>
160
+ <li><b>long</b> - 64-bit signed integer</li>
161
+ <li><b>decimal</b> - Arbitrary precision decimal (stored as text)</li>
162
+ <li><b>string</b> - Text value</li>
163
+ <li><b>boolean</b> - true/false</li>
164
+ <li><b>timestamp</b> - Date/time value</li>
165
+ <li><b>array</b> - Auto-detect array element type</li>
166
+ <li><b>array_double</b> - Array of doubles</li>
167
+ <li><b>array_long</b> - Array of longs</li>
168
+ <li><b>array_string</b> - Array of strings</li>
169
+ </ul>
170
+
154
171
  <h3>Field Path Syntax</h3>
155
172
  <p>Use dot notation: <code>payload.sensor.value</code></p>
156
173
  <p>Array access: <code>payload.readings[0]</code></p>
@@ -173,13 +190,15 @@
173
190
  payload: {
174
191
  device: "sensor1",
175
192
  temp: 23.5,
193
+ readings: [1.1, 2.2, 3.3],
176
194
  ts: 1699999999000
177
195
  }
178
196
  }</pre>
179
197
  <p>With mappings:</p>
180
198
  <ul>
181
199
  <li>Symbol: <code>payload.device</code> → <code>device_id</code></li>
182
- <li>Column: <code>payload.temp</code> → <code>temperature</code> (float)</li>
200
+ <li>Column: <code>payload.temp</code> → <code>temperature</code> (double)</li>
201
+ <li>Column: <code>payload.readings</code> → <code>values</code> (array_double)</li>
183
202
  <li>Timestamp: <code>payload.ts</code></li>
184
203
  </ul>
185
204
  <p>Output:</p>
@@ -187,7 +206,10 @@
187
206
  topic: "sensors",
188
207
  payload: {
189
208
  symbols: { device_id: "sensor1" },
190
- columns: { temperature: 23.5 },
209
+ columns: {
210
+ temperature: { value: 23.5, type: "double" },
211
+ values: { value: [1.1, 2.2, 3.3], type: "array", elementType: "double" }
212
+ },
191
213
  timestamp: 1699999999000
192
214
  }
193
215
  }</pre>
package/nodes/mapper.js CHANGED
@@ -48,9 +48,35 @@ module.exports = function(RED) {
48
48
  value = parseFloat(value);
49
49
  if (isNaN(value)) continue;
50
50
  break;
51
+ case 'double':
52
+ value = { value: parseFloat(value), type: 'double' };
53
+ if (isNaN(value.value)) continue;
54
+ break;
51
55
  case 'integer':
52
- value = parseInt(value, 10);
53
- if (isNaN(value)) continue;
56
+ value = { value: parseInt(value, 10), type: 'int' };
57
+ if (isNaN(value.value)) continue;
58
+ break;
59
+ case 'long':
60
+ value = { value: parseInt(value, 10), type: 'long' };
61
+ if (isNaN(value.value)) continue;
62
+ break;
63
+ case 'decimal':
64
+ value = { value: String(value), type: 'decimal' };
65
+ break;
66
+ case 'array':
67
+ // Keep array as-is, auto-detect element type
68
+ if (!Array.isArray(value)) {
69
+ value = [value]; // wrap single value in array
70
+ }
71
+ break;
72
+ case 'array_double':
73
+ value = { value: Array.isArray(value) ? value.map(Number) : [Number(value)], type: 'array', elementType: 'double' };
74
+ break;
75
+ case 'array_long':
76
+ value = { value: Array.isArray(value) ? value.map(v => parseInt(v, 10)) : [parseInt(value, 10)], type: 'array', elementType: 'long' };
77
+ break;
78
+ case 'array_string':
79
+ value = { value: Array.isArray(value) ? value.map(String) : [String(value)], type: 'array', elementType: 'string' };
54
80
  break;
55
81
  case 'boolean':
56
82
  value = Boolean(value);
package/nodes/write.js CHANGED
@@ -301,12 +301,74 @@ module.exports = function(RED) {
301
301
  if (payload.columns && typeof payload.columns === 'object') {
302
302
  for (const [key, value] of Object.entries(payload.columns)) {
303
303
  if (value === null || value === undefined) continue;
304
+
305
+ // Check for explicit type specification: { value: x, type: 'int'|'long'|'float'|'double'|'decimal'|'array' }
306
+ if (typeof value === 'object' && value.type && value.value !== undefined) {
307
+ const colType = value.type.toLowerCase();
308
+ const colValue = value.value;
309
+
310
+ if (colType === 'int' || colType === 'integer') {
311
+ connection.sender.intColumn(key, Math.trunc(colValue));
312
+ } else if (colType === 'long') {
313
+ connection.sender.longColumn(key, BigInt(Math.trunc(colValue)));
314
+ } else if (colType === 'float') {
315
+ connection.sender.floatColumn(key, colValue);
316
+ } else if (colType === 'double') {
317
+ connection.sender.doubleColumn(key, colValue);
318
+ } else if (colType === 'decimal') {
319
+ // Decimal: { value: '123.45', type: 'decimal' } or { mantissa: 12345n, scale: 2, type: 'decimal' }
320
+ if (value.mantissa !== undefined && value.scale !== undefined) {
321
+ connection.sender.decimalColumn(key, BigInt(value.mantissa), value.scale);
322
+ } else {
323
+ // Use text representation
324
+ connection.sender.decimalColumnText(key, String(colValue));
325
+ }
326
+ } else if (colType === 'array') {
327
+ // Array column: { value: [1, 2, 3], type: 'array', elementType: 'double' }
328
+ if (Array.isArray(colValue)) {
329
+ const elementType = (value.elementType || 'double').toLowerCase();
330
+ connection.sender.arrayColumn(key, elementType, colValue);
331
+ } else {
332
+ node.warn(`Array column '${key}' value must be an array`);
333
+ }
334
+ } else if (colType === 'string') {
335
+ connection.sender.stringColumn(key, String(colValue));
336
+ } else if (colType === 'boolean') {
337
+ connection.sender.booleanColumn(key, Boolean(colValue));
338
+ } else if (colType === 'timestamp') {
339
+ const microSeconds = BigInt(colValue) * 1000n;
340
+ connection.sender.timestampColumn(key, microSeconds);
341
+ } else {
342
+ node.warn(`Unknown column type '${colType}' for column '${key}'`);
343
+ }
344
+ continue;
345
+ }
346
+
347
+ // Array detection (auto)
348
+ if (Array.isArray(value)) {
349
+ // Detect element type from first non-null element
350
+ const firstElement = value.find(v => v !== null && v !== undefined);
351
+ let elementType = 'double'; // default
352
+ if (typeof firstElement === 'string') {
353
+ elementType = 'string';
354
+ } else if (typeof firstElement === 'boolean') {
355
+ elementType = 'boolean';
356
+ } else if (typeof firstElement === 'bigint') {
357
+ elementType = 'long';
358
+ }
359
+ connection.sender.arrayColumn(key, elementType, value);
360
+ continue;
361
+ }
362
+
304
363
  if (typeof value === 'number') {
305
364
  if (!isFinite(value)) {
306
365
  node.warn(`Skipping non-finite number for column '${key}'`);
307
366
  continue;
308
367
  }
309
- connection.sender.floatColumn(key, value);
368
+ // Default to double for numbers (most precise)
369
+ connection.sender.doubleColumn(key, value);
370
+ } else if (typeof value === 'bigint') {
371
+ connection.sender.longColumn(key, value);
310
372
  } else if (typeof value === 'boolean') {
311
373
  connection.sender.booleanColumn(key, value);
312
374
  } else if (typeof value === 'string') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-questdb",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Node-RED nodes for QuestDB time-series database",
5
5
  "keywords": [
6
6
  "node-red",