node-red-contrib-prib-functions 0.23.2 → 0.26.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.
Files changed (58) hide show
  1. package/.github/copilot-instructions.md +36 -0
  2. package/README.md +153 -140
  3. package/columnar/columnar.html +258 -0
  4. package/columnar/columnar.js +1055 -0
  5. package/columnar/icons/columnar.svg +38 -0
  6. package/fileSystem/filesystem.html +299 -0
  7. package/fileSystem/filesystem.js +170 -0
  8. package/gitlab/gitlab.html +191 -0
  9. package/gitlab/gitlab.js +248 -0
  10. package/gitlab/icons/gitlab.svg +17 -0
  11. package/lib/AlphaBeta.js +32 -0
  12. package/lib/GraphDB.js +40 -9
  13. package/lib/MinMax.js +17 -0
  14. package/lib/Tree.js +64 -0
  15. package/lib/objectExtensions.js +28 -5
  16. package/lib/timeDimension.js +36 -0
  17. package/lib/typedInput.js +18 -2
  18. package/logisticRegression/icons/logisticregression.svg +22 -0
  19. package/logisticRegression/logisticRegression.html +136 -0
  20. package/logisticRegression/logisticRegression.js +83 -0
  21. package/package.json +21 -9
  22. package/test/02-graphdb.js +46 -0
  23. package/test/columnar.js +509 -0
  24. package/test/data/.config.nodes.json +114 -70
  25. package/test/data/.config.nodes.json.backup +104 -71
  26. package/test/data/.config.runtime.json +2 -1
  27. package/test/data/.config.runtime.json.backup +2 -1
  28. package/test/data/.config.users.json +3 -2
  29. package/test/data/.config.users.json.backup +3 -2
  30. package/test/data/.flow.json.backup +1545 -369
  31. package/test/data/flow.json +1457 -270
  32. package/test/data/package-lock.json +11 -11
  33. package/test/data/shares/.config.nodes.json +611 -0
  34. package/test/data/shares/.config.nodes.json.backup +589 -0
  35. package/test/data/shares/.config.runtime.json +5 -0
  36. package/test/data/shares/.config.runtime.json.backup +4 -0
  37. package/test/data/shares/.config.users.json +33 -0
  38. package/test/data/shares/.config.users.json.backup +33 -0
  39. package/test/data/shares/.flow.json.backup +230 -0
  40. package/test/data/shares/.flow_cred.json.backup +3 -0
  41. package/test/data/shares/flow.json +267 -0
  42. package/test/data/shares/flow_cred.json +3 -0
  43. package/test/data/shares/package.json +6 -0
  44. package/test/data/shares/settings.js +544 -0
  45. package/test/dataAnalysisExtensions.js +93 -93
  46. package/test/logisticRegression.js +379 -0
  47. package/test/transform.js +11 -11
  48. package/test/transformConfluence.js +4 -2
  49. package/test/transformNumPy.js +3 -1
  50. package/test/transformXLSX.js +4 -2
  51. package/test/transformXML.js +4 -2
  52. package/test-runner.js +400 -0
  53. package/test.parq +0 -0
  54. package/test_select.js +37 -0
  55. package/testing/test.js +8 -7
  56. package/transform/transform.html +23 -2
  57. package/transform/transform.js +239 -283
  58. package/transform/xlsx2.js +74 -0
@@ -0,0 +1,509 @@
1
+ const should = require("should");
2
+ const { SimpleColumnarStore } = require("../columnar/columnar.js");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ describe("Columnar Store", function () {
8
+ it("writes, reads and queries using columnar rid maps or filters", async function () {
9
+ // methods are static; we call them directly on the class
10
+ const tmp = path.join(os.tmpdir(), "prib-test.columnar");
11
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
12
+
13
+ const records = [
14
+ { id: 1, name: "a", age: 20, dept: "eng" },
15
+ { id: 2, name: "b", age: 30, dept: "hr" },
16
+ { id: 3, name: "c", age: 40, dept: "eng" },
17
+ { id: 4, name: "d", age: 35, dept: "hr" },
18
+ ];
19
+
20
+ // write and then read the full set back
21
+ await SimpleColumnarStore.writeRecords(records, tmp);
22
+ const all = await SimpleColumnarStore.readRecords(tmp);
23
+ all.should.have.property("records");
24
+ all.records.length.should.equal(4);
25
+ // schema is an object mapping field names to types
26
+ Object.keys(all.schema).should.containEql("age");
27
+
28
+ // filter using a javascript predicate
29
+ const filtered = await SimpleColumnarStore.queryRecords(tmp, {
30
+ filter: (r) => r.age > 25,
31
+ });
32
+ filtered.records.length.should.equal(3);
33
+
34
+ // filter using a ridMap on the 'age' column
35
+ const ridMap = { age: [1, 2, 3] };
36
+ const filtered2 = await SimpleColumnarStore.queryRecords(tmp, { ridMap });
37
+ filtered2.records.length.should.equal(3);
38
+ // ensure the rows returned correspond to the expected indices
39
+ filtered2.records[0].should.deepEqual(records[1]);
40
+
41
+ // Basic SQL query
42
+ const sqlResult = await SimpleColumnarStore.sqlQuery(
43
+ tmp,
44
+ "SELECT * FROM ? WHERE age > 25",
45
+ );
46
+ sqlResult.length.should.equal(3);
47
+
48
+ // SQL with column projection
49
+ const projected = await SimpleColumnarStore.sqlQuery(
50
+ tmp,
51
+ "SELECT id, name FROM ? WHERE age > 30",
52
+ );
53
+ projected.length.should.equal(2);
54
+ projected[0].should.have.property("id");
55
+ projected[0].should.have.property("name");
56
+ projected[0].should.not.have.property("age");
57
+
58
+ // SQL with ORDER BY
59
+ const ordered = await SimpleColumnarStore.sqlQuery(
60
+ tmp,
61
+ "SELECT * FROM ? ORDER BY age DESC",
62
+ );
63
+ ordered.length.should.equal(4);
64
+ ordered[0].age.should.equal(40);
65
+ ordered[3].age.should.equal(20);
66
+
67
+ // SQL with LIMIT
68
+ const limited = await SimpleColumnarStore.sqlQuery(
69
+ tmp,
70
+ "SELECT * FROM ? ORDER BY age DESC LIMIT 2",
71
+ );
72
+ limited.length.should.equal(2);
73
+
74
+ // SQL with GROUP BY and COUNT
75
+ const grouped = await SimpleColumnarStore.sqlQuery(
76
+ tmp,
77
+ "SELECT dept, COUNT(*) AS cnt FROM ? GROUP BY dept",
78
+ );
79
+ grouped.length.should.equal(2);
80
+ const engGroup = grouped.find((g) => g.dept === "eng");
81
+ engGroup.should.have.property("cnt", 2);
82
+
83
+ // SQL with aggregates
84
+ const agg = await SimpleColumnarStore.sqlQuery(
85
+ tmp,
86
+ "SELECT AVG(age) AS avg_age, MAX(age) AS max_age, MIN(age) AS min_age FROM ?",
87
+ );
88
+ agg.length.should.equal(1);
89
+ agg[0].avg_age.should.be.approximately(31.25, 0.01);
90
+ agg[0].max_age.should.equal(40);
91
+ agg[0].min_age.should.equal(20);
92
+
93
+ // SQL with GROUP BY and SUM
94
+ const sumAgg = await SimpleColumnarStore.sqlQuery(
95
+ tmp,
96
+ "SELECT dept, SUM(age) AS total_age FROM ? GROUP BY dept",
97
+ );
98
+ sumAgg.length.should.equal(2);
99
+ });
100
+
101
+ it("supports INNER, LEFT, RIGHT, and FULL OUTER JOINs", async function () {
102
+ const tmpDir = os.tmpdir();
103
+ const file1 = path.join(tmpDir, "join-test-1.columnar");
104
+ const file2 = path.join(tmpDir, "join-test-2.columnar");
105
+ [file1, file2].forEach((f) => {
106
+ if (fs.existsSync(f)) fs.unlinkSync(f);
107
+ });
108
+
109
+ // Table 1: users
110
+ const users = [
111
+ { id: 1, name: "Alice" },
112
+ { id: 2, name: "Bob" },
113
+ { id: 3, name: "Charlie" },
114
+ ];
115
+
116
+ // Table 2: orders (only users 1 and 2 have orders)
117
+ const orders = [
118
+ { user_id: 1, amount: 100 },
119
+ { user_id: 1, amount: 200 },
120
+ { user_id: 2, amount: 150 },
121
+ ];
122
+
123
+ await SimpleColumnarStore.writeRecords(users, file1);
124
+ await SimpleColumnarStore.writeRecords(orders, file2);
125
+
126
+ // INNER JOIN: only matching rows
127
+ const innerJoin = await SimpleColumnarStore.sqlQuery(
128
+ file1,
129
+ `SELECT u.id, u.name, o.amount FROM ? u INNER JOIN '${file2}' o ON u.id = o.user_id`,
130
+ );
131
+ innerJoin.length.should.equal(3); // 2 orders for user 1, 1 order for user 2
132
+ innerJoin[0].should.have.property("u.name", "Alice");
133
+ innerJoin[0].should.have.property("o.amount");
134
+
135
+ // LEFT JOIN: all from left, matching from right
136
+ const leftJoin = await SimpleColumnarStore.sqlQuery(
137
+ file1,
138
+ `SELECT u.id, u.name, o.amount FROM ? u LEFT JOIN '${file2}' o ON u.id = o.user_id`,
139
+ );
140
+ leftJoin.length.should.equal(4); // 3 orders for users 1&2 + 1 unmatched user 3
141
+ leftJoin[3].should.have.property("u.name", "Charlie");
142
+ should.not.exist(leftJoin[3]["o.amount"]);
143
+
144
+ // RIGHT JOIN: all from right, matching from left
145
+ const rightJoin = await SimpleColumnarStore.sqlQuery(
146
+ file1,
147
+ `SELECT u.id, u.name, o.amount FROM ? u RIGHT JOIN '${file2}' o ON u.id = o.user_id`,
148
+ );
149
+ rightJoin.length.should.equal(3); // all 3 orders have matching users
150
+
151
+ // FULL OUTER JOIN: all rows from both tables
152
+ const fullJoin = await SimpleColumnarStore.sqlQuery(
153
+ file1,
154
+ `SELECT u.id, u.name, o.amount FROM ? u FULL OUTER JOIN '${file2}' o ON u.id = o.user_id`,
155
+ );
156
+ fullJoin.length.should.equal(4); // 3 matched + 1 unmatched from left
157
+
158
+ // Clean up
159
+ [file1, file2].forEach((f) => {
160
+ if (fs.existsSync(f)) fs.unlinkSync(f);
161
+ });
162
+ });
163
+
164
+ it("supports appending records to existing files", async function () {
165
+ const tmp = path.join(os.tmpdir(), "append-test.columnar");
166
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
167
+
168
+ // Initial records
169
+ const initialRecords = [
170
+ { id: 1, name: "Alice", age: 25 },
171
+ { id: 2, name: "Bob", age: 30 },
172
+ ];
173
+
174
+ // Write initial records
175
+ const writeResult = await SimpleColumnarStore.writeRecords(
176
+ initialRecords,
177
+ tmp,
178
+ );
179
+ writeResult.recordsWritten.should.equal(2);
180
+ writeResult.totalRecords.should.equal(2);
181
+ writeResult.appended.should.be.false();
182
+
183
+ // Verify initial records
184
+ const read1 = await SimpleColumnarStore.readRecords(tmp);
185
+ read1.records.length.should.equal(2);
186
+
187
+ // Append more records
188
+ const appendRecords = [
189
+ { id: 3, name: "Charlie", age: 35 },
190
+ { id: 4, name: "Diana", age: 28 },
191
+ ];
192
+
193
+ const appendResult = await SimpleColumnarStore.appendRecords(
194
+ appendRecords,
195
+ tmp,
196
+ );
197
+ appendResult.recordsWritten.should.equal(2);
198
+ appendResult.totalRecords.should.equal(4);
199
+ appendResult.appended.should.be.true();
200
+
201
+ // Verify all records are present
202
+ const read2 = await SimpleColumnarStore.readRecords(tmp);
203
+ read2.records.length.should.equal(4);
204
+ read2.records[0].name.should.equal("Alice");
205
+ read2.records[2].name.should.equal("Charlie");
206
+ read2.records[3].name.should.equal("Diana");
207
+
208
+ // Test appending to non-existent file (should create it)
209
+ const newFile = path.join(os.tmpdir(), "new-append-test.columnar");
210
+ if (fs.existsSync(newFile)) fs.unlinkSync(newFile);
211
+
212
+ const newAppendResult = await SimpleColumnarStore.appendRecords(
213
+ [{ id: 5, name: "Eve" }],
214
+ newFile,
215
+ );
216
+ newAppendResult.recordsWritten.should.equal(1);
217
+ newAppendResult.totalRecords.should.equal(1);
218
+ newAppendResult.appended.should.be.false(); // File didn't exist, so not appended
219
+
220
+ // Clean up
221
+ [tmp, newFile].forEach((f) => {
222
+ if (fs.existsSync(f)) fs.unlinkSync(f);
223
+ });
224
+ });
225
+
226
+ it("supports hardcoded SQL queries in node configuration", async function () {
227
+ const tmp = path.join(os.tmpdir(), "hardcoded-sql-test.columnar");
228
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
229
+
230
+ const records = [
231
+ { id: 1, name: "Alice", age: 25, dept: "eng" },
232
+ { id: 2, name: "Bob", age: 30, dept: "hr" },
233
+ { id: 3, name: "Charlie", age: 35, dept: "eng" },
234
+ ];
235
+
236
+ await SimpleColumnarStore.writeRecords(records, tmp);
237
+
238
+ // Simulate Node-RED node with hardcoded SQL
239
+ const mockNode = {
240
+ filePath: tmp,
241
+ sqlQuery: "SELECT name, age FROM ? WHERE age > 28 ORDER BY age DESC",
242
+ };
243
+
244
+ // Simulate message without SQL (should use hardcoded query)
245
+ const mockMsg = {
246
+ payload: {
247
+ filePath: tmp,
248
+ // No sql property - should use node.sqlQuery
249
+ },
250
+ };
251
+
252
+ // Simulate the sqlQuery action logic
253
+ const filePath = mockMsg.payload.filePath || mockNode.filePath;
254
+ const sql = mockMsg.payload.sql || mockNode.sqlQuery;
255
+
256
+ const result = await SimpleColumnarStore.sqlQuery(filePath, sql);
257
+ result.length.should.equal(2);
258
+ result[0].name.should.equal("Charlie");
259
+ result[0].age.should.equal(35);
260
+ result[1].name.should.equal("Bob");
261
+ result[1].age.should.equal(30);
262
+
263
+ // Test that msg.payload.sql takes precedence over hardcoded query
264
+ mockMsg.payload.sql = "SELECT COUNT(*) as total FROM ?";
265
+ const result2 = await SimpleColumnarStore.sqlQuery(
266
+ filePath,
267
+ mockMsg.payload.sql,
268
+ );
269
+ result2.length.should.equal(1);
270
+ result2[0].total.should.equal(3);
271
+
272
+ // Clean up
273
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
274
+ });
275
+
276
+ it("caches parsed SQL tokens for hardcoded queries", async function () {
277
+ const tmp = path.join(os.tmpdir(), "cached-sql-test.columnar");
278
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
279
+
280
+ const records = [
281
+ { id: 1, name: "Alice", age: 25 },
282
+ { id: 2, name: "Bob", age: 30 },
283
+ { id: 3, name: "Charlie", age: 35 },
284
+ ];
285
+
286
+ await SimpleColumnarStore.writeRecords(records, tmp);
287
+
288
+ // Test that parsing works normally
289
+ const sql = "SELECT name FROM ? WHERE age > 28 ORDER BY age DESC";
290
+ const result1 = await SimpleColumnarStore.sqlQuery(tmp, sql);
291
+ result1.length.should.equal(2);
292
+ result1[0].name.should.equal("Charlie");
293
+
294
+ // Test with pre-parsed tokens (simulate node caching)
295
+ const parsedTokens = SimpleColumnarStore._parseSql(sql);
296
+ const result2 = await SimpleColumnarStore.sqlQuery(tmp, sql, parsedTokens);
297
+ result2.length.should.equal(2);
298
+ result2[0].name.should.equal("Charlie");
299
+
300
+ // Results should be identical
301
+ result1.should.deepEqual(result2);
302
+
303
+ // Clean up
304
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
305
+ });
306
+
307
+ it("demonstrates SQL parsing performance improvement with caching", async function () {
308
+ const tmp = path.join(os.tmpdir(), "perf-sql-test.columnar");
309
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
310
+
311
+ // Create a larger dataset for performance testing
312
+ const records = [];
313
+ for (let i = 0; i < 1000; i++) {
314
+ records.push({
315
+ id: i,
316
+ name: `User${i}`,
317
+ age: 20 + (i % 50),
318
+ dept: ["eng", "hr", "sales", "marketing"][i % 4],
319
+ salary: 50000 + i * 100,
320
+ });
321
+ }
322
+
323
+ await SimpleColumnarStore.writeRecords(records, tmp);
324
+
325
+ const sql =
326
+ "SELECT dept, COUNT(*) as cnt, AVG(salary) as avg_salary FROM ? GROUP BY dept ORDER BY cnt DESC";
327
+
328
+ // Time parsing + execution without cache
329
+ const start1 = Date.now();
330
+ for (let i = 0; i < 10; i++) {
331
+ await SimpleColumnarStore.sqlQuery(tmp, sql);
332
+ }
333
+ const time1 = Date.now() - start1;
334
+
335
+ // Time execution with pre-parsed tokens
336
+ const parsedTokens = SimpleColumnarStore._parseSql(sql);
337
+ const start2 = Date.now();
338
+ for (let i = 0; i < 10; i++) {
339
+ await SimpleColumnarStore.sqlQuery(tmp, sql, parsedTokens);
340
+ }
341
+ const time2 = Date.now() - start2;
342
+
343
+ // Cached version should be faster (parsing is skipped)
344
+ console.log(`Without cache: ${time1}ms, With cache: ${time2}ms`);
345
+ time2.should.be.below(time1); // Cached should be faster
346
+
347
+ // Results should be identical
348
+ const result1 = await SimpleColumnarStore.sqlQuery(tmp, sql);
349
+ const result2 = await SimpleColumnarStore.sqlQuery(tmp, sql, parsedTokens);
350
+ result1.should.deepEqual(result2);
351
+
352
+ // Clean up
353
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
354
+ });
355
+
356
+ it("supports configurable output property for results", async function () {
357
+ const tmp = path.join(os.tmpdir(), "output-prop-test.columnar");
358
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
359
+
360
+ const records = [
361
+ { id: 1, name: "Alice", age: 25 },
362
+ { id: 2, name: "Bob", age: 30 },
363
+ ];
364
+
365
+ await SimpleColumnarStore.writeRecords(records, tmp);
366
+
367
+ // Test that sqlQuery returns results correctly
368
+ const sqlResult = await SimpleColumnarStore.sqlQuery(
369
+ tmp,
370
+ "SELECT name FROM ? WHERE age > 26",
371
+ );
372
+ sqlResult.length.should.equal(1);
373
+ sqlResult[0].name.should.equal("Bob");
374
+
375
+ // Simulate node behavior with different output properties
376
+ const mockMsg1 = { payload: {} };
377
+ const outputProperty1 = "result";
378
+ mockMsg1[outputProperty1] = sqlResult;
379
+ mockMsg1.should.have.property("result");
380
+ mockMsg1.result[0].name.should.equal("Bob");
381
+
382
+ const mockMsg2 = { payload: {} };
383
+ const outputProperty2 = "myData";
384
+ mockMsg2[outputProperty2] = sqlResult;
385
+ mockMsg2.should.have.property("myData");
386
+ mockMsg2.should.not.have.property("result");
387
+ mockMsg2.myData[0].name.should.equal("Bob");
388
+
389
+ // Test default behavior (empty outputProperty should default to 'result')
390
+ const mockMsg3 = { payload: {} };
391
+ const outputProperty3 = null; // should default to 'result'
392
+ const defaultProp = outputProperty3 || "result";
393
+ mockMsg3[defaultProp] = sqlResult;
394
+ mockMsg3.should.have.property("result");
395
+ mockMsg3.result[0].name.should.equal("Bob");
396
+
397
+ // Clean up
398
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
399
+ });
400
+
401
+ it("supports parameter substitution in SQL queries", async function () {
402
+ const tmp = path.join(os.tmpdir(), "prib-test-params.columnar");
403
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
404
+
405
+ const records = [
406
+ { id: 1, name: "Alice", age: 25, dept: "eng", salary: 50000 },
407
+ { id: 2, name: "Bob", age: 30, dept: "hr", salary: 60000 },
408
+ { id: 3, name: "Charlie", age: 35, dept: "eng", salary: 70000 },
409
+ { id: 4, name: "Diana", age: 28, dept: "hr", salary: 55000 },
410
+ ];
411
+
412
+ await SimpleColumnarStore.writeRecords(records, tmp);
413
+
414
+ // Test explicit :msg., :flow., :global. parameter markers
415
+ const msg = { payload: { minAge: 28, personName: "Bob" } };
416
+ const flow = { minSalary: 60000 };
417
+ const global = { dept: "eng" };
418
+ const context = { msg, flow, global };
419
+
420
+ // :msg. marker
421
+ const result1 = await SimpleColumnarStore.sqlQuery(
422
+ tmp,
423
+ "SELECT * FROM ? WHERE age > :msg.payload.minAge",
424
+ null,
425
+ context,
426
+ );
427
+ result1.length.should.equal(2);
428
+ result1[0].name.should.equal("Bob");
429
+ result1[1].name.should.equal("Charlie");
430
+
431
+ // :flow. marker
432
+ const result2 = await SimpleColumnarStore.sqlQuery(
433
+ tmp,
434
+ "SELECT * FROM ? WHERE salary > :flow.minSalary",
435
+ null,
436
+ context,
437
+ );
438
+ result2.length.should.equal(1);
439
+ result2[0].name.should.equal("Charlie");
440
+
441
+ // :global. marker
442
+ const result3 = await SimpleColumnarStore.sqlQuery(
443
+ tmp,
444
+ "SELECT * FROM ? WHERE dept = :global.dept",
445
+ null,
446
+ context,
447
+ );
448
+ result3.length.should.equal(2);
449
+ result3[0].name.should.equal("Alice");
450
+ result3[1].name.should.equal("Charlie");
451
+
452
+ // Multiple explicit markers
453
+ const result4 = await SimpleColumnarStore.sqlQuery(
454
+ tmp,
455
+ "SELECT * FROM ? WHERE age >= :msg.payload.minAge AND dept = :global.dept",
456
+ null,
457
+ context,
458
+ );
459
+ result4.length.should.equal(1);
460
+ result4[0].name.should.equal("Charlie");
461
+
462
+ // Test missing parameter should throw error
463
+ try {
464
+ await SimpleColumnarStore.sqlQuery(
465
+ tmp,
466
+ "SELECT * FROM ? WHERE age > :msg.payload.missingParam",
467
+ null,
468
+ context,
469
+ );
470
+ should.fail("Should have thrown error for missing parameter");
471
+ } catch (error) {
472
+ error.message.should.match(
473
+ /Parameter ':msg.payload.missingParam' not provided/,
474
+ );
475
+ }
476
+ // SQL injection attempt should not succeed
477
+ {
478
+ const injectionContext = {
479
+ msg: { payload: { personName: "Bob' OR 1=1 --" } },
480
+ flow: {},
481
+ global: {},
482
+ };
483
+ // This should only match a name exactly equal to the injected string, not all rows
484
+ const injResult = await SimpleColumnarStore.sqlQuery(
485
+ tmp,
486
+ "SELECT * FROM ? WHERE name = :msg.payload.personName",
487
+ null,
488
+ injectionContext,
489
+ );
490
+ injResult.length.should.equal(0);
491
+ }
492
+
493
+ // Test parameter substitution in SELECT clause
494
+ {
495
+ const result = await SimpleColumnarStore.sqlQuery(
496
+ tmp,
497
+ "SELECT :msg.payload.minAge as age_threshold, name FROM ? LIMIT 5",
498
+ null,
499
+ { msg: { payload: { minAge: 28 } }, flow: {}, global: {} },
500
+ );
501
+ result.length.should.equal(4);
502
+ result[0].should.have.property("age_threshold", 28);
503
+ result[0].should.have.property("name");
504
+ }
505
+
506
+ // Clean up
507
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
508
+ });
509
+ });