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/examples/examples.json +924 -0
- package/nodes/mapper.html +25 -3
- package/nodes/mapper.js +28 -2
- package/nodes/write.js +63 -1
- package/package.json +1 -1
- package/examples/comprehensive-test-flows.json +0 -908
- package/examples/test-flows.json +0 -330
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> (
|
|
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: {
|
|
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
|
-
|
|
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') {
|