w-orm-postgresql 1.0.7 → 1.0.9

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.
@@ -0,0 +1,1458 @@
1
+ import assert from 'assert'
2
+ import net from 'net'
3
+ import path from 'path'
4
+ import pg from 'pg'
5
+ import { execFile, spawn } from 'child_process'
6
+ import { promisify } from 'util'
7
+ import isestr from 'wsemi/src/isestr.mjs'
8
+ import WOrm from '../src/WOrmPostgresql.mjs'
9
+
10
+
11
+ //測試自行創建docker容器供PostgreSQL服務, 測試結束即銷毀, 故僅需環境已安裝docker
12
+
13
+
14
+ let execFileAsync = promisify(execFile)
15
+
16
+ let ctName = 'worm-test-postgresql'
17
+ let ctImage = 'postgres:17-alpine'
18
+ let ctUser = 'username'
19
+ let ctPassword = 'password'
20
+ let ctDb = 'worm'
21
+ let ctPort = null //容器對外埠, 由getFreePort動態取得, 避免與既有PostgreSQL服務搶5432
22
+
23
+
24
+ let delay = (ms) => {
25
+ return new Promise((resolve) => {
26
+ setTimeout(resolve, ms)
27
+ })
28
+ }
29
+
30
+ //getFreePort, 由系統配發空閒埠
31
+ let getFreePort = () => {
32
+ return new Promise((resolve, reject) => {
33
+ let srv = net.createServer()
34
+ srv.on('error', reject)
35
+ srv.listen(0, () => {
36
+ let port = srv.address().port
37
+ srv.close(() => {
38
+ resolve(port)
39
+ })
40
+ })
41
+ })
42
+ }
43
+
44
+ //docker, 以陣列傳參不經shell, 避免路徑與引號轉譯問題
45
+ let docker = (args) => {
46
+ return execFileAsync('docker', args)
47
+ }
48
+
49
+ //genUrl
50
+ let genUrl = () => {
51
+ return `postgresql://${ctUser}:${ctPassword}@127.0.0.1:${ctPort}`
52
+ }
53
+
54
+ //genOpt
55
+ let genOpt = (cl = 'users', useCache = false) => {
56
+ return {
57
+ url: genUrl(),
58
+ db: ctDb,
59
+ cl,
60
+ useCache,
61
+ }
62
+ }
63
+
64
+ //probeReady, 以pg直連容器對外埠試跑SELECT 1
65
+ //不使用容器內pg_isready, 因postgres映像初始化期間會先啟動僅監聽unix socket之臨時服務,
66
+ //pg_isready預設亦走unix socket故會提早回報就緒, 直連TCP方為測試實際所走路徑
67
+ let probeReady = async () => {
68
+ let client = new pg.Client({ connectionString: `${genUrl()}/${ctDb}` })
69
+ let ok = false
70
+ try {
71
+ await client.connect()
72
+ await client.query('SELECT 1')
73
+ ok = true
74
+ }
75
+ catch (err) {
76
+ ok = false
77
+ }
78
+ finally {
79
+ await client.end().catch(() => {})
80
+ }
81
+ return ok
82
+ }
83
+
84
+ //startContainer, 創建容器並等待服務就緒
85
+ //postgres映像以POSTGRES_USER、POSTGRES_PASSWORD、POSTGRES_DB即建妥帳號與資料庫, 無須另跑建置語句
86
+ let startContainer = async () => {
87
+
88
+ //check docker
89
+ await docker(['version', '--format', '{{.Server.Version}}'])
90
+ .catch(() => {
91
+ throw new Error('需先安裝並啟動docker才能執行postgresql測試')
92
+ })
93
+
94
+ //清除前次殘留容器
95
+ await docker(['rm', '-f', ctName]).catch(() => {})
96
+
97
+ //run
98
+ ctPort = await getFreePort()
99
+ await docker(['run', '-d', '--rm', '--name', ctName, '-e', `POSTGRES_USER=${ctUser}`, '-e', `POSTGRES_PASSWORD=${ctPassword}`, '-e', `POSTGRES_DB=${ctDb}`, '-p', `${ctPort}:5432`, ctImage])
100
+
101
+ //等待服務就緒, 首次需拉取映像故給予較長時間
102
+ let ready = false
103
+ for (let i = 0; i < 90; i++) {
104
+ let ok = await probeReady()
105
+ if (ok) {
106
+ ready = true
107
+ break
108
+ }
109
+ await delay(2000)
110
+ }
111
+ if (!ready) {
112
+ throw new Error('postgresql容器啟動逾時')
113
+ }
114
+
115
+ }
116
+
117
+ //stopContainer, 銷毀容器
118
+ let stopContainer = async () => {
119
+ await docker(['rm', '-f', ctName]).catch(() => {})
120
+ }
121
+
122
+
123
+ before(async function() {
124
+ this.timeout(600000) //含拉取映像與服務啟動
125
+ await startContainer()
126
+ })
127
+
128
+ after(async function() {
129
+ this.timeout(120000)
130
+ await stopContainer()
131
+ })
132
+
133
+
134
+ describe('basic', function() {
135
+ let rt = null
136
+ let vans = {}
137
+ let vget = {}
138
+
139
+ before(async function () {
140
+
141
+ let opt = genOpt('users')
142
+
143
+ let rs = [
144
+ {
145
+ time: '2025-01-01T00:00:00Z',
146
+ name: 'peter',
147
+ value: 123,
148
+ },
149
+ {
150
+ time: '2025-01-01T00:01:00Z',
151
+ name: 'rosemary',
152
+ value: 123.456,
153
+ },
154
+ {
155
+ time: '2025-01-01T00:02:00Z',
156
+ name: 'kettle',
157
+ value: 456,
158
+ },
159
+ {
160
+ time: '2025-01-01T00:03:00Z',
161
+ name: 'peter',
162
+ value: 200,
163
+ },
164
+ {
165
+ time: '2025-01-01T00:04:00Z',
166
+ name: 'rosemary',
167
+ value: 123.1236,
168
+ },
169
+ {
170
+ time: '2025-01-01T00:05:00Z',
171
+ name: 'kettle',
172
+ value: 488,
173
+ },
174
+ {
175
+ time: '2025-01-01T00:06:00Z',
176
+ name: 'peter',
177
+ value: 125,
178
+ },
179
+ {
180
+ time: '2025-01-01T00:07:00Z',
181
+ name: 'rosemary',
182
+ value: 124.76,
183
+ },
184
+ {
185
+ time: '2025-01-01T00:08:00Z',
186
+ name: 'kettle',
187
+ value: 524,
188
+ },
189
+ {
190
+ time: '2025-01-01T00:09:00Z',
191
+ name: 'peter',
192
+ value: 127,
193
+ },
194
+ {
195
+ time: '2025-01-01T00:10:00Z',
196
+ name: 'rosemary',
197
+ value: 113.98,
198
+ },
199
+ {
200
+ time: '2025-01-01T00:11:00Z',
201
+ name: 'kettle',
202
+ value: 447,
203
+ },
204
+ {
205
+ time: '2025-01-01T00:12:00Z',
206
+ name: 'peter',
207
+ value: 131,
208
+ },
209
+ ]
210
+
211
+ let rsm = [
212
+ { //相同
213
+ time: '2025-01-01T00:09:00Z',
214
+ name: 'peter',
215
+ value: 127,
216
+ },
217
+ { //name變更
218
+ time: '2025-01-01T00:10:00Z',
219
+ name: 'rosemary(modify)',
220
+ value: 113.98,
221
+ },
222
+ { //name變更
223
+ time: '2025-01-01T00:11:00Z',
224
+ name: 'kettle(modify)',
225
+ value: 447,
226
+ },
227
+ { //無name且value變更
228
+ time: '2025-01-01T00:12:00Z',
229
+ // name: 'peter',
230
+ value: 99,
231
+ },
232
+ { //無time須新增且新增數據無value
233
+ time: '2025-01-01T00:13:00Z',
234
+ name: 'sandler',
235
+ },
236
+ ]
237
+
238
+ let rsa = [
239
+ {
240
+ time: '2025-01-01T00:13:00Z',
241
+ name: 'sandler',
242
+ },
243
+ ]
244
+
245
+ //wo
246
+ let wo = WOrm(opt)
247
+
248
+ //on
249
+ wo.on('change', function(mode, data, res) {
250
+ // console.log('change', mode)
251
+ })
252
+
253
+ await wo.createTable(opt.cl, 'time', {
254
+ time: '2000-01-01T00:00:00Z', //time
255
+ name: 'abc', //string
256
+ value: 0.1, //float
257
+ })
258
+ .then(function(msg) {
259
+ // console.log('createTable then', msg)
260
+ })
261
+ .catch(function(msg) {
262
+ // console.log('createTable catch', msg)
263
+ })
264
+
265
+ //delAll
266
+ rt = null
267
+ // vans[1] = { ok: 1 }
268
+ await wo.delAll()
269
+ .then(function(msg) {
270
+ // console.log('delAll then', msg)
271
+ //考慮有不同初始狀態, 僅比對ok欄位
272
+ rt = {
273
+ ok: msg.ok,
274
+ }
275
+ })
276
+ .catch(function(msg) {
277
+ // console.log('delAll catch', msg)
278
+ rt = msg.toString()
279
+ })
280
+ vget[1] = rt
281
+
282
+ //insert
283
+ rt = null
284
+ // vans[2] = { n: 13, nInserted: 13, ok: 1 }
285
+ await wo.insert(rs)
286
+ .then(function(msg) {
287
+ // console.log('insert then', msg)
288
+ rt = msg
289
+ })
290
+ .catch(function(msg) {
291
+ // console.log('insert catch', msg)
292
+ rt = msg.toString()
293
+ })
294
+ vget[2] = rt
295
+
296
+ //save
297
+ rt = null
298
+ // vans[3] = [
299
+ // { n: 1, nModified: 0, ok: 1 },
300
+ // { n: 1, nModified: 1, ok: 1 },
301
+ // { n: 1, nModified: 1, ok: 1 },
302
+ // { n: 1, nModified: 1, ok: 1 },
303
+ // { n: 0, nModified: 0, ok: 1 }
304
+ // ]
305
+ await wo.save(rsm, { autoInsert: false })
306
+ .then(function(msg) {
307
+ // console.log('save then', msg)
308
+ rt = msg
309
+ })
310
+ .catch(function(msg) {
311
+ // console.log('save catch', msg)
312
+ rt = msg.toString()
313
+ })
314
+ vget[3] = rt
315
+
316
+ //select all
317
+ rt = null
318
+ // vans[4] = [
319
+ // { time: new Date('2025-01-01T00:00:00.000Z'), name: 'peter', value: 123 },
320
+ // { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
321
+ // { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
322
+ // { time: new Date('2025-01-01T00:03:00.000Z'), name: 'peter', value: 200 },
323
+ // { time: new Date('2025-01-01T00:04:00.000Z'), name: 'rosemary', value: 123.1236 },
324
+ // { time: new Date('2025-01-01T00:05:00.000Z'), name: 'kettle', value: 488 },
325
+ // { time: new Date('2025-01-01T00:06:00.000Z'), name: 'peter', value: 125 },
326
+ // { time: new Date('2025-01-01T00:07:00.000Z'), name: 'rosemary', value: 124.76 },
327
+ // { time: new Date('2025-01-01T00:08:00.000Z'), name: 'kettle', value: 524 },
328
+ // { time: new Date('2025-01-01T00:09:00.000Z'), name: 'peter', value: 127 },
329
+ // { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 },
330
+ // { time: new Date('2025-01-01T00:11:00.000Z'), name: 'kettle(modify)', value: 447 },
331
+ // { time: new Date('2025-01-01T00:12:00.000Z'), name: 'peter', value: 99 }
332
+ // ]
333
+ await wo.select()
334
+ .then(function(msg) {
335
+ // console.log('select all then', msg)
336
+ rt = msg
337
+ })
338
+ .catch(function(msg) {
339
+ // console.log('select all catch', msg)
340
+ rt = msg.toString()
341
+ })
342
+ vget[4] = rt
343
+
344
+ //select all by cache
345
+ rt = null
346
+ // vans[5] = vans[4]
347
+ await wo.select()
348
+ .then(function(msg) {
349
+ // console.log('select all by cache then', msg)
350
+ rt = msg
351
+ })
352
+ .catch(function(msg) {
353
+ // console.log('select all by cache catch', msg)
354
+ rt = msg.toString()
355
+ })
356
+ vget[5] = rt
357
+
358
+ //select by name
359
+ rt = null
360
+ // vans[6] = [
361
+ // { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
362
+ // { time: new Date('2025-01-01T00:04:00.000Z'), name: 'rosemary', value: 123.1236 },
363
+ // { time: new Date('2025-01-01T00:07:00.000Z'), name: 'rosemary', value: 124.76 }
364
+ // ]
365
+ await wo.select({ name: 'rosemary' })
366
+ .then(function(msg) {
367
+ // console.log('select all then', msg)
368
+ rt = msg
369
+ })
370
+ .catch(function(msg) {
371
+ // console.log('select all catch', msg)
372
+ rt = msg.toString()
373
+ })
374
+ vget[6] = rt
375
+
376
+ //selectByTime
377
+ rt = null
378
+ // vans[18] = { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 }
379
+ await wo.selectByTime('2025-01-01T00:10:00Z')
380
+ .then(function(msg) {
381
+ // console.log('selectByTime then', msg)
382
+ // selectByTime { time: 2025-01-01T00:10:00.000Z, name: 'rosemary(modify)', value: 113.98 }
383
+ rt = msg
384
+ })
385
+ .catch(function(msg) {
386
+ // console.log('selectByTime catch', msg)
387
+ rt = msg.toString()
388
+ })
389
+ vget[18] = rt
390
+
391
+ //selectByTime by time not existed
392
+ rt = null
393
+ // vans[19] = null
394
+ await wo.selectByTime('2024-01-01T00:00:00Z')
395
+ .then(function(msg) {
396
+ // console.log('selectByTime by time not existed then', msg)
397
+ // selectByTime by time not existed null
398
+ rt = msg
399
+ })
400
+ .catch(function(msg) {
401
+ // console.log('selectByTime by time not existed catch', msg)
402
+ rt = msg.toString()
403
+ })
404
+ vget[19] = rt
405
+
406
+ //selectByTime by time invalid, 未給有效主鍵值視為查無數據
407
+ rt = null
408
+ // vans[20] = null
409
+ await wo.selectByTime('abc')
410
+ .then(function(msg) {
411
+ // console.log('selectByTime by time invalid then', msg)
412
+ // selectByTime by time invalid null
413
+ rt = msg
414
+ })
415
+ .catch(function(msg) {
416
+ // console.log('selectByTime by time invalid catch', msg)
417
+ rt = msg.toString()
418
+ })
419
+ vget[20] = rt
420
+
421
+ //select by $and, $gt, $lt
422
+ rt = null
423
+ // vans[7] = [
424
+ // { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
425
+ // { time: new Date('2025-01-01T00:04:00.000Z'), name: 'rosemary', value: 123.1236 },
426
+ // { time: new Date('2025-01-01T00:06:00.000Z'), name: 'peter', value: 125 },
427
+ // { time: new Date('2025-01-01T00:07:00.000Z'), name: 'rosemary', value: 124.76 },
428
+ // { time: new Date('2025-01-01T00:09:00.000Z'), name: 'peter', value: 127 }
429
+ // ]
430
+ await wo.select({ '$and': [{ value: { '$gt': 123 } }, { value: { '$lt': 200 } }] })
431
+ .then(function(msg) {
432
+ // console.log('select all then', msg)
433
+ rt = msg
434
+ })
435
+ .catch(function(msg) {
436
+ // console.log('select all catch', msg)
437
+ rt = msg.toString()
438
+ })
439
+ vget[7] = rt
440
+
441
+ //select by $or, $gte, $lte
442
+ rt = null
443
+ // vans[8] = [
444
+ // { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
445
+ // { time: new Date('2025-01-01T00:03:00.000Z'), name: 'peter', value: 200 },
446
+ // { time: new Date('2025-01-01T00:05:00.000Z'), name: 'kettle', value: 488 },
447
+ // { time: new Date('2025-01-01T00:08:00.000Z'), name: 'kettle', value: 524 },
448
+ // { time: new Date('2025-01-01T00:11:00.000Z'), name: 'kettle(modify)', value: 447 }
449
+ // ]
450
+ await wo.select({ '$or': [{ value: { '$lte': -1 } }, { value: { '$gte': 200 } }] })
451
+ .then(function(msg) {
452
+ // console.log('select all then', msg)
453
+ rt = msg
454
+ })
455
+ .catch(function(msg) {
456
+ // console.log('select all catch', msg)
457
+ rt = msg.toString()
458
+ })
459
+ vget[8] = rt
460
+
461
+ //select by $or, $and, $ne, $in, $nin
462
+ rt = null
463
+ // vans[9] = [
464
+ // { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
465
+ // { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
466
+ // { time: new Date('2025-01-01T00:05:00.000Z'), name: 'kettle', value: 488 },
467
+ // { time: new Date('2025-01-01T00:08:00.000Z'), name: 'kettle', value: 524 },
468
+ // { time: new Date('2025-01-01T00:11:00.000Z'), name: 'kettle(modify)', value: 447 }
469
+ // ]
470
+ await wo.select({ '$or': [{ '$and': [{ value: { '$ne': 123 } }, { value: { '$in': [123, 321, 123.456, 456] } }, { value: { '$nin': [456, 654] } }] }, { '$or': [{ value: { '$lte': -1 } }, { value: { '$gte': 400 } }] }] })
471
+ .then(function(msg) {
472
+ // console.log('select all then', msg)
473
+ rt = msg
474
+ })
475
+ .catch(function(msg) {
476
+ // console.log('select all catch', msg)
477
+ rt = msg.toString()
478
+ })
479
+ vget[9] = rt
480
+
481
+ // //select by regex
482
+ // rt = null
483
+ // vans[14] = []
484
+ // let sr = await wo.select({ name: { $regex: 'PeT', $options: '$i' } })
485
+ // .then(function(msg) {
486
+ // // console.log('select all then', msg)
487
+ // rt = msg
488
+ // })
489
+ // .catch(function(msg) {
490
+ // // console.log('select all catch', msg)
491
+ // rt = msg.toString()
492
+ // })
493
+ // vget[14] = rt
494
+
495
+ //save
496
+ rt = null
497
+ // vans[10] = [{ n: 1, nInserted: 1, ok: 1 }]
498
+ await wo.save(rsa, { autoInsert: true })
499
+ .then(function(msg) {
500
+ // console.log('save then', msg)
501
+ rt = msg
502
+ })
503
+ .catch(function(msg) {
504
+ // console.log('save catch', msg)
505
+ rt = msg.toString()
506
+ })
507
+ vget[10] = rt
508
+
509
+ //del
510
+ rt = null
511
+ // vans[11] = [{ n: 1, nDeleted: 0, ok: 1 }]
512
+ let d = {
513
+ time: '2024-01-01T00:00:00Z',
514
+ }
515
+ await wo.del(d)
516
+ .then(function(msg) {
517
+ // console.log('del then', msg)
518
+ rt = msg
519
+ })
520
+ .catch(function(msg) {
521
+ // console.log('del catch', msg)
522
+ rt = msg.toString()
523
+ })
524
+ vget[11] = rt
525
+
526
+ //del
527
+ rt = null
528
+ // vans[12] = [
529
+ // { n: 1, nDeleted: 1, ok: 1 },
530
+ // { n: 1, nDeleted: 1, ok: 1 },
531
+ // { n: 1, nDeleted: 1, ok: 1 },
532
+ // { n: 1, nDeleted: 1, ok: 1 },
533
+ // { n: 1, nDeleted: 1, ok: 1 },
534
+ // { n: 1, nDeleted: 1, ok: 1 },
535
+ // { n: 1, nDeleted: 1, ok: 1 },
536
+ // { n: 1, nDeleted: 1, ok: 1 },
537
+ // { n: 1, nDeleted: 1, ok: 1 },
538
+ // { n: 1, nDeleted: 1, ok: 1 }
539
+ // ]
540
+ let ss = await wo.select()
541
+ let ds = ss.filter(function(v) {
542
+ return v.name.indexOf('peter') >= 0 || v.name.indexOf('kettle') >= 0 || v.name.indexOf('sandler') >= 0
543
+ })
544
+ await wo.del(ds)
545
+ .then(function(msg) {
546
+ // console.log('del then', msg)
547
+ rt = msg
548
+ })
549
+ .catch(function(msg) {
550
+ // console.log('del catch', msg)
551
+ rt = msg.toString()
552
+ })
553
+ vget[12] = rt
554
+
555
+ //select all final
556
+ rt = null
557
+ // vans[13] = [
558
+ // { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
559
+ // { time: new Date('2025-01-01T00:04:00.000Z'), name: 'rosemary', value: 123.1236 },
560
+ // { time: new Date('2025-01-01T00:07:00.000Z'), name: 'rosemary', value: 124.76 },
561
+ // { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 }
562
+ // ]
563
+ await wo.select()
564
+ .then(function(msg) {
565
+ // console.log('select all then', msg)
566
+ rt = msg
567
+ })
568
+ .catch(function(msg) {
569
+ // console.log('select all catch', msg)
570
+ rt = msg.toString()
571
+ })
572
+ vget[13] = rt
573
+
574
+ //清除數據, 後續cache測試共用users表
575
+ await wo.delAll()
576
+
577
+ //woc, 啟用useCache
578
+ let woc = WOrm({
579
+ ...opt,
580
+ useCache: true,
581
+ })
582
+
583
+ let rsc = [
584
+ {
585
+ time: '2025-01-01T00:00:00Z',
586
+ name: 'peter',
587
+ value: 123,
588
+ },
589
+ {
590
+ time: '2025-01-01T00:01:00Z',
591
+ name: 'rosemary',
592
+ value: 123.456,
593
+ },
594
+ {
595
+ time: '2025-01-01T00:02:00Z',
596
+ name: 'kettle',
597
+ value: 456,
598
+ },
599
+ ]
600
+
601
+ let rscm = [
602
+ {
603
+ time: '2025-01-01T00:01:00Z',
604
+ name: 'rosemary(modify)',
605
+ value: 654.321,
606
+ },
607
+ ]
608
+
609
+ //insert
610
+ await woc.insert(rsc)
611
+
612
+ //select all (1st time, 從DB讀取並填入快取)
613
+ rt = null
614
+ // vans[14] = [
615
+ // { time: new Date('2025-01-01T00:00:00.000Z'), name: 'peter', value: 123 },
616
+ // { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
617
+ // { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
618
+ // ]
619
+ await woc.select()
620
+ .then(function(msg) {
621
+ // console.log('select 1st then', msg)
622
+ //依time排序, postgres UPDATE後物理順序可能改變
623
+ rt = msg.sort((a, b) => a.time - b.time)
624
+ })
625
+ .catch(function(msg) {
626
+ // console.log('select 1st catch', msg)
627
+ rt = msg.toString()
628
+ })
629
+ vget[14] = rt
630
+
631
+ //select all (2nd time, 命中快取, 內容須與第一次相同)
632
+ rt = null
633
+ // vans[15] = vans[14]
634
+ await woc.select()
635
+ .then(function(msg) {
636
+ // console.log('select 2nd then', msg)
637
+ rt = msg.sort((a, b) => a.time - b.time)
638
+ })
639
+ .catch(function(msg) {
640
+ // console.log('select 2nd catch', msg)
641
+ rt = msg.toString()
642
+ })
643
+ vget[15] = rt
644
+
645
+ //save (更新rosemary, 觸發快取重設)
646
+ rt = null
647
+ // vans[16] = [{ n: 1, nModified: 1, ok: 1 }]
648
+ await woc.save(rscm, { autoInsert: false })
649
+ .then(function(msg) {
650
+ // console.log('save then', msg)
651
+ rt = msg
652
+ })
653
+ .catch(function(msg) {
654
+ // console.log('save catch', msg)
655
+ rt = msg.toString()
656
+ })
657
+ vget[16] = rt
658
+
659
+ //select all (3rd time, 快取已重設, 從DB重新讀取, 須反映rosemary更新)
660
+ rt = null
661
+ // vans[17] = [
662
+ // { time: new Date('2025-01-01T00:00:00.000Z'), name: 'peter', value: 123 },
663
+ // { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary(modify)', value: 654.321 },
664
+ // { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
665
+ // ]
666
+ await woc.select()
667
+ .then(function(msg) {
668
+ // console.log('select 3rd then', msg)
669
+ rt = msg.sort((a, b) => a.time - b.time)
670
+ })
671
+ .catch(function(msg) {
672
+ // console.log('select 3rd catch', msg)
673
+ rt = msg.toString()
674
+ })
675
+ vget[17] = rt
676
+
677
+ //selectByTime (1st time, 從DB讀取並填入快取)
678
+ rt = null
679
+ // vans[21] = { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary(modify)', value: 654.321 }
680
+ await woc.selectByTime('2025-01-01T00:01:00Z')
681
+ .then(function(msg) {
682
+ // console.log('selectByTime 1st then', msg)
683
+ rt = msg
684
+ })
685
+ .catch(function(msg) {
686
+ // console.log('selectByTime 1st catch', msg)
687
+ rt = msg.toString()
688
+ })
689
+ vget[21] = rt
690
+
691
+ //selectByTime (2nd time, 命中快取, 內容須與第一次相同)
692
+ rt = null
693
+ // vans[22] = vans[21]
694
+ await woc.selectByTime('2025-01-01T00:01:00Z')
695
+ .then(function(msg) {
696
+ // console.log('selectByTime 2nd then', msg)
697
+ rt = msg
698
+ })
699
+ .catch(function(msg) {
700
+ // console.log('selectByTime 2nd catch', msg)
701
+ rt = msg.toString()
702
+ })
703
+ vget[22] = rt
704
+
705
+ })
706
+
707
+ vans[1] = { ok: 1 }
708
+ it(`should get ${JSON.stringify(vans[1])} for delAll`, async function() {
709
+ assert.strict.deepStrictEqual(vget[1], vans[1])
710
+ })
711
+
712
+ vans[2] = { n: 13, nInserted: 13, ok: 1 }
713
+ it(`should get ${JSON.stringify(vans[2])} for insert`, async function() {
714
+ assert.strict.deepStrictEqual(vget[2], vans[2])
715
+ })
716
+
717
+ vans[3] = [
718
+ { n: 1, nInserted: 0, nModified: 0, ok: 1 },
719
+ { n: 1, nInserted: 0, nModified: 1, ok: 1 },
720
+ { n: 1, nInserted: 0, nModified: 1, ok: 1 },
721
+ { n: 1, nInserted: 0, nModified: 1, ok: 1 },
722
+ { n: 0, nInserted: 0, nModified: 0, ok: 1 }
723
+ ]
724
+ it(`should get ${JSON.stringify(vans[3])} for save(autoInsert=false)`, async function() {
725
+ assert.strict.deepStrictEqual(vget[3], vans[3])
726
+ })
727
+
728
+ vans[4] = [
729
+ { time: new Date('2025-01-01T00:00:00.000Z'), name: 'peter', value: 123 },
730
+ { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
731
+ { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
732
+ { time: new Date('2025-01-01T00:03:00.000Z'), name: 'peter', value: 200 },
733
+ { time: new Date('2025-01-01T00:04:00.000Z'), name: 'rosemary', value: 123.1236 },
734
+ { time: new Date('2025-01-01T00:05:00.000Z'), name: 'kettle', value: 488 },
735
+ { time: new Date('2025-01-01T00:06:00.000Z'), name: 'peter', value: 125 },
736
+ { time: new Date('2025-01-01T00:07:00.000Z'), name: 'rosemary', value: 124.76 },
737
+ { time: new Date('2025-01-01T00:08:00.000Z'), name: 'kettle', value: 524 },
738
+ { time: new Date('2025-01-01T00:09:00.000Z'), name: 'peter', value: 127 },
739
+ { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 },
740
+ { time: new Date('2025-01-01T00:11:00.000Z'), name: 'kettle(modify)', value: 447 },
741
+ { time: new Date('2025-01-01T00:12:00.000Z'), name: 'peter', value: 99 }
742
+ ]
743
+ it(`should get ${JSON.stringify(vans[4])} for select all`, async function() {
744
+ assert.strict.deepStrictEqual(vget[4], vans[4])
745
+ })
746
+
747
+ vans[5] = vans[4]
748
+ it(`should get ${JSON.stringify(vans[5])} for select all by cache`, async function() {
749
+ assert.strict.deepStrictEqual(vget[5], vans[5])
750
+ })
751
+
752
+ vans[6] = [
753
+ { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
754
+ { time: new Date('2025-01-01T00:04:00.000Z'), name: 'rosemary', value: 123.1236 },
755
+ { time: new Date('2025-01-01T00:07:00.000Z'), name: 'rosemary', value: 124.76 }
756
+ ]
757
+ it(`should get ${JSON.stringify(vans[6])} for select by name`, async function() {
758
+ assert.strict.deepStrictEqual(vget[6], vans[6])
759
+ })
760
+
761
+ //vans[18]~vans[22]為後續新增之selectByTime, 為不更動既有編號故接續於末號之後
762
+ vans[18] = { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 }
763
+ it(`should get ${JSON.stringify(vans[18])} for selectByTime`, async function() {
764
+ assert.strict.deepStrictEqual(vget[18], vans[18])
765
+ })
766
+
767
+ vans[19] = null
768
+ it(`should get ${JSON.stringify(vans[19])} for selectByTime by time not existed`, async function() {
769
+ assert.strict.deepStrictEqual(vget[19], vans[19])
770
+ })
771
+
772
+ vans[20] = null
773
+ it(`should get ${JSON.stringify(vans[20])} for selectByTime by time invalid`, async function() {
774
+ assert.strict.deepStrictEqual(vget[20], vans[20])
775
+ })
776
+
777
+ vans[7] = [
778
+ { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
779
+ { time: new Date('2025-01-01T00:04:00.000Z'), name: 'rosemary', value: 123.1236 },
780
+ { time: new Date('2025-01-01T00:06:00.000Z'), name: 'peter', value: 125 },
781
+ { time: new Date('2025-01-01T00:07:00.000Z'), name: 'rosemary', value: 124.76 },
782
+ { time: new Date('2025-01-01T00:09:00.000Z'), name: 'peter', value: 127 }
783
+ ]
784
+ it(`should get ${JSON.stringify(vans[7])} for select by $and, $gt, $lt`, async function() {
785
+ assert.strict.deepStrictEqual(vget[7], vans[7])
786
+ })
787
+
788
+ vans[8] = [
789
+ { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
790
+ { time: new Date('2025-01-01T00:03:00.000Z'), name: 'peter', value: 200 },
791
+ { time: new Date('2025-01-01T00:05:00.000Z'), name: 'kettle', value: 488 },
792
+ { time: new Date('2025-01-01T00:08:00.000Z'), name: 'kettle', value: 524 },
793
+ { time: new Date('2025-01-01T00:11:00.000Z'), name: 'kettle(modify)', value: 447 }
794
+ ]
795
+ it(`should get ${JSON.stringify(vans[8])} for select by $or, $gte, $lte`, async function() {
796
+ assert.strict.deepStrictEqual(vget[8], vans[8])
797
+ })
798
+
799
+ vans[9] = [
800
+ { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
801
+ { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
802
+ { time: new Date('2025-01-01T00:05:00.000Z'), name: 'kettle', value: 488 },
803
+ { time: new Date('2025-01-01T00:08:00.000Z'), name: 'kettle', value: 524 },
804
+ { time: new Date('2025-01-01T00:11:00.000Z'), name: 'kettle(modify)', value: 447 }
805
+ ]
806
+ it(`should get ${JSON.stringify(vans[9])} for select by $or, $and, $ne, $in, $nin`, async function() {
807
+ assert.strict.deepStrictEqual(vget[9], vans[9])
808
+ })
809
+
810
+ // vans[14] = []
811
+ // it(`should get ${JSON.stringify(vans[14])} for select by regex`, async function() {
812
+ // assert.strict.deepStrictEqual(vget[14], vans[14])
813
+ // })
814
+
815
+ vans[10] = [{ n: 1, nInserted: 1, nModified: 0, ok: 1 }]
816
+ it(`should get ${JSON.stringify(vans[10])} for save(autoInsert=true)`, async function() {
817
+ assert.strict.deepStrictEqual(vget[10], vans[10])
818
+ })
819
+
820
+ vans[11] = [{ n: 0, nDeleted: 0, ok: 1 }]
821
+ it(`should get ${JSON.stringify(vans[11])} for del`, async function() {
822
+ assert.strict.deepStrictEqual(vget[11], vans[11])
823
+ })
824
+
825
+ vans[12] = [
826
+ { n: 1, nDeleted: 1, ok: 1 },
827
+ { n: 1, nDeleted: 1, ok: 1 },
828
+ { n: 1, nDeleted: 1, ok: 1 },
829
+ { n: 1, nDeleted: 1, ok: 1 },
830
+ { n: 1, nDeleted: 1, ok: 1 },
831
+ { n: 1, nDeleted: 1, ok: 1 },
832
+ { n: 1, nDeleted: 1, ok: 1 },
833
+ { n: 1, nDeleted: 1, ok: 1 },
834
+ { n: 1, nDeleted: 1, ok: 1 },
835
+ { n: 1, nDeleted: 1, ok: 1 }
836
+ ]
837
+ it(`should get ${JSON.stringify(vans[12])} for del`, async function() {
838
+ assert.strict.deepStrictEqual(vget[12], vans[12])
839
+ })
840
+
841
+ vans[13] = [
842
+ { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
843
+ { time: new Date('2025-01-01T00:04:00.000Z'), name: 'rosemary', value: 123.1236 },
844
+ { time: new Date('2025-01-01T00:07:00.000Z'), name: 'rosemary', value: 124.76 },
845
+ { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 }
846
+ ]
847
+ it(`should get ${JSON.stringify(vans[13])} for select all final`, async function() {
848
+ assert.strict.deepStrictEqual(vget[13], vans[13])
849
+ })
850
+
851
+ vans[14] = [
852
+ { time: new Date('2025-01-01T00:00:00.000Z'), name: 'peter', value: 123 },
853
+ { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
854
+ { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
855
+ ]
856
+ it(`should get ${JSON.stringify(vans[14])} for select all (1st, fill cache)`, async function() {
857
+ assert.strict.deepStrictEqual(vget[14], vans[14])
858
+ })
859
+
860
+ vans[15] = vans[14]
861
+ it(`should get ${JSON.stringify(vans[15])} for select all (2nd, cache hit)`, async function() {
862
+ assert.strict.deepStrictEqual(vget[15], vans[15])
863
+ })
864
+
865
+ vans[16] = [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }]
866
+ it(`should get ${JSON.stringify(vans[16])} for save (invalidate cache)`, async function() {
867
+ assert.strict.deepStrictEqual(vget[16], vans[16])
868
+ })
869
+
870
+ vans[17] = [
871
+ { time: new Date('2025-01-01T00:00:00.000Z'), name: 'peter', value: 123 },
872
+ { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary(modify)', value: 654.321 },
873
+ { time: new Date('2025-01-01T00:02:00.000Z'), name: 'kettle', value: 456 },
874
+ ]
875
+ it(`should get ${JSON.stringify(vans[17])} for select all (3rd, reload after save)`, async function() {
876
+ assert.strict.deepStrictEqual(vget[17], vans[17])
877
+ })
878
+
879
+ vans[21] = { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary(modify)', value: 654.321 }
880
+ it(`should get ${JSON.stringify(vans[21])} for selectByTime (1st, fill cache)`, async function() {
881
+ assert.strict.deepStrictEqual(vget[21], vans[21])
882
+ })
883
+
884
+ vans[22] = vans[21]
885
+ it(`should get ${JSON.stringify(vans[22])} for selectByTime (2nd, cache hit)`, async function() {
886
+ assert.strict.deepStrictEqual(vget[22], vans[22])
887
+ })
888
+
889
+ })
890
+
891
+
892
+ //insert與save之併發測試, 因各操作皆各自new PgClient另開連線, 原子性全由PostgreSQL提供,
893
+ //故同行程以Promise.all併發即等價於跨行程併發, 不須另起行程
894
+
895
+
896
+ describe('insert nInserted', function() {
897
+ let vans = {}
898
+ let vget = {}
899
+
900
+ before(async function() {
901
+ this.timeout(120000)
902
+
903
+ //wo
904
+ let cl = 'insertcnt'
905
+ let wo = WOrm(genOpt(cl))
906
+
907
+ //createTable
908
+ await wo.createTable(cl, 'time', {
909
+ time: '2000-01-01T00:00:00Z',
910
+ name: 'abc',
911
+ value: 0.1,
912
+ }).catch(() => {})
913
+ await wo.delAll()
914
+
915
+ //全新3筆
916
+ vget[1] = await wo.insert([
917
+ { time: '2025-02-01T00:00:00Z', name: 'peter', value: 1 },
918
+ { time: '2025-02-01T00:01:00Z', name: 'rosemary', value: 2 },
919
+ { time: '2025-02-01T00:02:00Z', name: 'kettle', value: 3 },
920
+ ])
921
+
922
+ //重複插入同3筆, 全數已存在故皆不插入
923
+ vget[2] = await wo.insert([
924
+ { time: '2025-02-01T00:00:00Z', name: 'peter', value: 1 },
925
+ { time: '2025-02-01T00:01:00Z', name: 'rosemary', value: 2 },
926
+ { time: '2025-02-01T00:02:00Z', name: 'kettle', value: 3 },
927
+ ])
928
+
929
+ //部份重複, 1筆已存在2筆為新
930
+ vget[3] = await wo.insert([
931
+ { time: '2025-02-01T00:01:00Z', name: 'rosemary', value: 2 },
932
+ { time: '2025-02-01T00:03:00Z', name: 'sandler', value: 4 },
933
+ { time: '2025-02-01T00:04:00Z', name: 'joyce', value: 5 },
934
+ ])
935
+
936
+ //同批含重複time, 僅首筆成功
937
+ vget[4] = await wo.insert([
938
+ { time: '2025-02-01T00:05:00Z', name: 'dup-1', value: 6 },
939
+ { time: '2025-02-01T00:05:00Z', name: 'dup-2', value: 7 },
940
+ { time: '2025-02-01T00:06:00Z', name: 'uniq', value: 8 },
941
+ ])
942
+
943
+ //併發對同一time插入10次, nInserted總和須為1
944
+ let rs = await Promise.all(Array.from({ length: 10 }, (v, k) => {
945
+ return wo.insert({ time: '2025-02-01T00:07:00Z', name: `race-${k}`, value: k })
946
+ }))
947
+ vget[5] = rs.reduce((sum, v) => sum + v.nInserted, 0)
948
+
949
+ //併發皆不得報錯
950
+ vget[6] = rs.filter((v) => v.ok !== 1).length
951
+
952
+ //最終筆數, 3+0+2+2+1=8
953
+ let ss = await wo.select()
954
+ vget[7] = ss.length
955
+
956
+ //同批重複者僅首筆入庫
957
+ let vdup = await wo.selectByTime('2025-02-01T00:05:00Z')
958
+ vget[8] = vdup.name
959
+
960
+ })
961
+
962
+ vans[1] = { n: 3, nInserted: 3, ok: 1 }
963
+ it(`should get ${JSON.stringify(vans[1])} for insert 3 new records`, async function() {
964
+ assert.strict.deepStrictEqual(vget[1], vans[1])
965
+ })
966
+
967
+ vans[2] = { n: 3, nInserted: 0, ok: 1 }
968
+ it(`should get ${JSON.stringify(vans[2])} for insert 3 existed records`, async function() {
969
+ assert.strict.deepStrictEqual(vget[2], vans[2])
970
+ })
971
+
972
+ vans[3] = { n: 3, nInserted: 2, ok: 1 }
973
+ it(`should get ${JSON.stringify(vans[3])} for insert 1 existed and 2 new records`, async function() {
974
+ assert.strict.deepStrictEqual(vget[3], vans[3])
975
+ })
976
+
977
+ vans[4] = { n: 3, nInserted: 2, ok: 1 }
978
+ it(`should get ${JSON.stringify(vans[4])} for insert with duplicated time in same batch`, async function() {
979
+ assert.strict.deepStrictEqual(vget[4], vans[4])
980
+ })
981
+
982
+ vans[5] = 1
983
+ it(`should get ${JSON.stringify(vans[5])} for sum of nInserted by 10 concurrent insert with same time`, async function() {
984
+ assert.strict.deepStrictEqual(vget[5], vans[5])
985
+ })
986
+
987
+ vans[6] = 0
988
+ it(`should get ${JSON.stringify(vans[6])} for count of ok!==1 in 10 concurrent insert`, async function() {
989
+ assert.strict.deepStrictEqual(vget[6], vans[6])
990
+ })
991
+
992
+ vans[7] = 8
993
+ it(`should get ${JSON.stringify(vans[7])} for records after all insert`, async function() {
994
+ assert.strict.deepStrictEqual(vget[7], vans[7])
995
+ })
996
+
997
+ vans[8] = 'dup-1'
998
+ it(`should get ${JSON.stringify(vans[8])} for keeping first one of duplicated time in same batch`, async function() {
999
+ assert.strict.deepStrictEqual(vget[8], vans[8])
1000
+ })
1001
+
1002
+ })
1003
+
1004
+
1005
+ describe('save concurrency', function() {
1006
+ let vans = {}
1007
+ let vget = {}
1008
+
1009
+ before(async function() {
1010
+ this.timeout(120000)
1011
+
1012
+ //wo
1013
+ let cl = 'saverace'
1014
+ let wo = WOrm(genOpt(cl))
1015
+
1016
+ //createTable
1017
+ await wo.createTable(cl, 'time', {
1018
+ time: '2000-01-01T00:00:00Z',
1019
+ name: 'abc',
1020
+ value: 0.1,
1021
+ }).catch(() => {})
1022
+ await wo.delAll()
1023
+
1024
+ //併發save對既有time之不同欄位, 各欄位皆須保留
1025
+ let tMerge = '2025-03-01T00:00:00Z'
1026
+ await wo.insert({ time: tMerge, name: 'origin', value: 1 })
1027
+ await Promise.all([
1028
+ wo.save({ time: tMerge, name: 'merge-name' }),
1029
+ wo.save({ time: tMerge, value: 99 }),
1030
+ ])
1031
+ vget[1] = await wo.selectByTime(tMerge)
1032
+
1033
+ //併發save對全新time, autoInsert僅一次且不得報錯
1034
+ let tNew = '2025-03-01T00:01:00Z'
1035
+ let rsn = await Promise.all(Array.from({ length: 5 }, (v, k) => {
1036
+ return wo.save({ time: tNew, name: `new-${k}` })
1037
+ }))
1038
+ vget[2] = rsn.filter((v) => v[0].nInserted === 1).length
1039
+ vget[3] = rsn.filter((v) => v[0].ok !== 1).length
1040
+ vget[4] = (await wo.select({ time: tNew })).length
1041
+
1042
+ //save內容相同不更新
1043
+ let tSame = '2025-03-01T00:02:00Z'
1044
+ await wo.insert({ time: tSame, name: 'same', value: 5 })
1045
+ vget[5] = await wo.save({ time: tSame, name: 'same', value: 5 })
1046
+
1047
+ //save內容不同須更新, 未給之欄位須保留
1048
+ let tDiff = '2025-03-01T00:03:00Z'
1049
+ await wo.insert({ time: tDiff, name: 'diff', value: 5 })
1050
+ vget[6] = await wo.save({ time: tDiff, value: 6 })
1051
+ vget[7] = await wo.selectByTime(tDiff)
1052
+
1053
+ //save(autoInsert=false)對不存在之time不得插入
1054
+ let tNone = '2025-03-01T00:04:00Z'
1055
+ vget[8] = await wo.save({ time: tNone, name: 'none' }, { autoInsert: false })
1056
+ vget[9] = await wo.selectByTime(tNone)
1057
+
1058
+ //併發save(autoInsert=false)對既有time之不同欄位, 各欄位皆須保留且不得報錯
1059
+ let tUpd = '2025-03-01T00:05:00Z'
1060
+ await wo.insert({ time: tUpd, name: 'origin', value: 1 })
1061
+ let rsu = await Promise.all([
1062
+ wo.save({ time: tUpd, name: 'upd-name' }, { autoInsert: false }),
1063
+ wo.save({ time: tUpd, value: 88 }, { autoInsert: false }),
1064
+ ])
1065
+ vget[10] = rsu.filter((v) => v[0].ok !== 1).length
1066
+ vget[11] = await wo.selectByTime(tUpd)
1067
+
1068
+ })
1069
+
1070
+ vans[1] = { time: new Date('2025-03-01T00:00:00.000Z'), name: 'merge-name', value: 99 }
1071
+ it(`should get ${JSON.stringify(vans[1])} for fields after 2 concurrent save with different field`, async function() {
1072
+ assert.strict.deepStrictEqual(vget[1], vans[1])
1073
+ })
1074
+
1075
+ vans[2] = 1
1076
+ it(`should get ${JSON.stringify(vans[2])} for count of nInserted===1 in 5 concurrent save with time not existed`, async function() {
1077
+ assert.strict.deepStrictEqual(vget[2], vans[2])
1078
+ })
1079
+
1080
+ vans[3] = 0
1081
+ it(`should get ${JSON.stringify(vans[3])} for count of ok!==1 in 5 concurrent save with time not existed`, async function() {
1082
+ assert.strict.deepStrictEqual(vget[3], vans[3])
1083
+ })
1084
+
1085
+ vans[4] = 1
1086
+ it(`should get ${JSON.stringify(vans[4])} for records after 5 concurrent save with time not existed`, async function() {
1087
+ assert.strict.deepStrictEqual(vget[4], vans[4])
1088
+ })
1089
+
1090
+ vans[5] = [{ n: 1, nInserted: 0, nModified: 0, ok: 1 }]
1091
+ it(`should get ${JSON.stringify(vans[5])} for save with same content`, async function() {
1092
+ assert.strict.deepStrictEqual(vget[5], vans[5])
1093
+ })
1094
+
1095
+ vans[6] = [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }]
1096
+ it(`should get ${JSON.stringify(vans[6])} for save with different content`, async function() {
1097
+ assert.strict.deepStrictEqual(vget[6], vans[6])
1098
+ })
1099
+
1100
+ vans[7] = { time: new Date('2025-03-01T00:03:00.000Z'), name: 'diff', value: 6 }
1101
+ it(`should get ${JSON.stringify(vans[7])} for keeping field not given in save`, async function() {
1102
+ assert.strict.deepStrictEqual(vget[7], vans[7])
1103
+ })
1104
+
1105
+ vans[8] = [{ n: 0, nInserted: 0, nModified: 0, ok: 1 }]
1106
+ it(`should get ${JSON.stringify(vans[8])} for save(autoInsert=false) with time not existed`, async function() {
1107
+ assert.strict.deepStrictEqual(vget[8], vans[8])
1108
+ })
1109
+
1110
+ vans[9] = null
1111
+ it(`should get ${JSON.stringify(vans[9])} for record after save(autoInsert=false) with time not existed`, async function() {
1112
+ assert.strict.deepStrictEqual(vget[9], vans[9])
1113
+ })
1114
+
1115
+ vans[10] = 0
1116
+ it(`should get ${JSON.stringify(vans[10])} for count of ok!==1 in 2 concurrent save(autoInsert=false)`, async function() {
1117
+ assert.strict.deepStrictEqual(vget[10], vans[10])
1118
+ })
1119
+
1120
+ vans[11] = { time: new Date('2025-03-01T00:05:00.000Z'), name: 'upd-name', value: 88 }
1121
+ it(`should get ${JSON.stringify(vans[11])} for fields after 2 concurrent save(autoInsert=false) with different field`, async function() {
1122
+ assert.strict.deepStrictEqual(vget[11], vans[11])
1123
+ })
1124
+
1125
+ })
1126
+
1127
+
1128
+ describe('spec compliance', function() {
1129
+ let vans = {}
1130
+ let vget = {}
1131
+
1132
+ before(async function() {
1133
+ this.timeout(120000)
1134
+
1135
+ //wo
1136
+ let cl = 'specok'
1137
+ let wo = WOrm(genOpt(cl))
1138
+
1139
+ //createTable
1140
+ await wo.createTable(cl, 'time', {
1141
+ time: '2000-01-01T00:00:00Z',
1142
+ name: 'abc',
1143
+ value: 0.1,
1144
+ }).catch(() => {})
1145
+ await wo.delAll()
1146
+
1147
+ //save只給部份欄位且值皆與現值相同, 合併後等於現值故不寫入
1148
+ let tPart = '2025-04-01T00:00:00Z'
1149
+ await wo.insert({ time: tPart, name: 'peter', value: 5 })
1150
+ vget[1] = await wo.save({ time: tPart, value: 5 })
1151
+ vget[2] = await wo.selectByTime(tPart)
1152
+
1153
+ //save單筆失敗不中斷整批, 失敗筆須帶err
1154
+ let tOk = '2025-04-01T00:01:00Z'
1155
+ let tBad = '2025-04-01T00:02:00Z'
1156
+ let rsf = await wo.save([
1157
+ { time: tOk, name: 'ok' },
1158
+ { time: tBad, notexistcol: 1 },
1159
+ ])
1160
+ vget[3] = rsf.length
1161
+ vget[4] = rsf[0]
1162
+ vget[5] = {
1163
+ n: rsf[1].n,
1164
+ nInserted: rsf[1].nInserted,
1165
+ nModified: rsf[1].nModified,
1166
+ ok: rsf[1].ok,
1167
+ hasErr: isestr(rsf[1].err),
1168
+ }
1169
+
1170
+ //del未帶有效主鍵者回ok:0且不中斷整批
1171
+ let tDel = '2025-04-01T00:03:00Z'
1172
+ await wo.insert({ time: tDel, name: 'todel' })
1173
+ let rsd = await wo.del([
1174
+ { time: tDel },
1175
+ { name: 'no-time' },
1176
+ ])
1177
+ vget[6] = rsd.length
1178
+ vget[7] = rsd[0]
1179
+ vget[8] = {
1180
+ n: rsd[1].n,
1181
+ nDeleted: rsd[1].nDeleted,
1182
+ ok: rsd[1].ok,
1183
+ hasErr: isestr(rsd[1].err),
1184
+ }
1185
+
1186
+ //T6例外, insert與save未帶有效time時不補值而reject
1187
+ vget[9] = await wo.insert({ name: 'no-time' })
1188
+ .then(() => 'resolved')
1189
+ .catch(() => 'rejected')
1190
+ vget[10] = await wo.save({ name: 'no-time' })
1191
+ .then(() => 'resolved')
1192
+ .catch(() => 'rejected')
1193
+
1194
+ //T5輸入無效
1195
+ vget[11] = await wo.insert(null)
1196
+ vget[12] = await wo.save(null)
1197
+ vget[13] = await wo.del(null)
1198
+
1199
+ //delAll帶條件且僅部份命中
1200
+ let clPart = 'specdelall'
1201
+ let woPart = WOrm(genOpt(clPart))
1202
+ await woPart.createTable(clPart, 'time', {
1203
+ time: '2000-01-01T00:00:00Z',
1204
+ name: 'abc',
1205
+ value: 0.1,
1206
+ }).catch(() => {})
1207
+ await woPart.delAll()
1208
+ await woPart.insert([
1209
+ { time: '2025-05-01T00:00:00Z', name: 'aa', value: 1 },
1210
+ { time: '2025-05-01T00:01:00Z', name: 'aa', value: 2 },
1211
+ { time: '2025-05-01T00:02:00Z', name: 'bb', value: 3 },
1212
+ { time: '2025-05-01T00:03:00Z', name: 'bb', value: 4 },
1213
+ ])
1214
+ vget[16] = await woPart.delAll({ name: 'aa' })
1215
+ vget[17] = (await woPart.select()).length
1216
+
1217
+ //delAll條件無命中
1218
+ vget[18] = await woPart.delAll({ name: 'notexist' })
1219
+
1220
+ })
1221
+
1222
+ vans[1] = [{ n: 1, nInserted: 0, nModified: 0, ok: 1 }]
1223
+ it(`should get ${JSON.stringify(vans[1])} for save with partial fields all equal to current`, async function() {
1224
+ assert.strict.deepStrictEqual(vget[1], vans[1])
1225
+ })
1226
+
1227
+ vans[2] = { time: new Date('2025-04-01T00:00:00.000Z'), name: 'peter', value: 5 }
1228
+ it(`should get ${JSON.stringify(vans[2])} for record unchanged after save with partial fields`, async function() {
1229
+ assert.strict.deepStrictEqual(vget[2], vans[2])
1230
+ })
1231
+
1232
+ vans[3] = 2
1233
+ it(`should get ${JSON.stringify(vans[3])} for length of save result with 1 failed record`, async function() {
1234
+ assert.strict.deepStrictEqual(vget[3], vans[3])
1235
+ })
1236
+
1237
+ vans[4] = { n: 1, nInserted: 1, nModified: 0, ok: 1 }
1238
+ it(`should get ${JSON.stringify(vans[4])} for succeeded record in save with 1 failed record`, async function() {
1239
+ assert.strict.deepStrictEqual(vget[4], vans[4])
1240
+ })
1241
+
1242
+ vans[5] = { n: 1, nInserted: 0, nModified: 0, ok: 0, hasErr: true }
1243
+ it(`should get ${JSON.stringify(vans[5])} for failed record in save`, async function() {
1244
+ assert.strict.deepStrictEqual(vget[5], vans[5])
1245
+ })
1246
+
1247
+ vans[6] = 2
1248
+ it(`should get ${JSON.stringify(vans[6])} for length of del result with 1 invalid time`, async function() {
1249
+ assert.strict.deepStrictEqual(vget[6], vans[6])
1250
+ })
1251
+
1252
+ vans[7] = { n: 1, nDeleted: 1, ok: 1 }
1253
+ it(`should get ${JSON.stringify(vans[7])} for succeeded record in del with 1 invalid time`, async function() {
1254
+ assert.strict.deepStrictEqual(vget[7], vans[7])
1255
+ })
1256
+
1257
+ vans[8] = { n: 0, nDeleted: 0, ok: 0, hasErr: true }
1258
+ it(`should get ${JSON.stringify(vans[8])} for record without valid time in del`, async function() {
1259
+ assert.strict.deepStrictEqual(vget[8], vans[8])
1260
+ })
1261
+
1262
+ vans[9] = 'rejected'
1263
+ it(`should get ${JSON.stringify(vans[9])} for insert without valid time`, async function() {
1264
+ assert.strict.deepStrictEqual(vget[9], vans[9])
1265
+ })
1266
+
1267
+ vans[10] = 'rejected'
1268
+ it(`should get ${JSON.stringify(vans[10])} for save without valid time`, async function() {
1269
+ assert.strict.deepStrictEqual(vget[10], vans[10])
1270
+ })
1271
+
1272
+ vans[11] = { n: 0, nInserted: 0, ok: 1 }
1273
+ it(`should get ${JSON.stringify(vans[11])} for insert with invalid data`, async function() {
1274
+ assert.strict.deepStrictEqual(vget[11], vans[11])
1275
+ })
1276
+
1277
+ vans[12] = []
1278
+ it(`should get ${JSON.stringify(vans[12])} for save with invalid data`, async function() {
1279
+ assert.strict.deepStrictEqual(vget[12], vans[12])
1280
+ })
1281
+
1282
+ vans[13] = []
1283
+ it(`should get ${JSON.stringify(vans[13])} for del with invalid data`, async function() {
1284
+ assert.strict.deepStrictEqual(vget[13], vans[13])
1285
+ })
1286
+
1287
+ vans[16] = { n: 2, nDeleted: 2, ok: 1 }
1288
+ it(`should get ${JSON.stringify(vans[16])} for delAll with find matched partially`, async function() {
1289
+ assert.strict.deepStrictEqual(vget[16], vans[16])
1290
+ })
1291
+
1292
+ vans[17] = 2
1293
+ it(`should get ${JSON.stringify(vans[17])} for records after delAll with find matched partially`, async function() {
1294
+ assert.strict.deepStrictEqual(vget[17], vans[17])
1295
+ })
1296
+
1297
+ vans[18] = { n: 0, nDeleted: 0, ok: 1 }
1298
+ it(`should get ${JSON.stringify(vans[18])} for delAll with find matched nothing`, async function() {
1299
+ assert.strict.deepStrictEqual(vget[18], vans[18])
1300
+ })
1301
+
1302
+ })
1303
+
1304
+
1305
+ //runProc, 另起行程對同一資料表操作, 用於驗證跨行程之原子性
1306
+ //子行程以--input-type=module執行, 並以pathToFileURL轉換src路徑, 避免專案路徑含非ASCII字元而無法import
1307
+ function runProc(tag, opt, mode, payload) {
1308
+ return new Promise(function(resolve) {
1309
+ let code = `
1310
+ import { pathToFileURL } from 'url'
1311
+ let { default: WOrm } = await import(pathToFileURL(process.env.SRC).href)
1312
+ let wo = WOrm({ url: process.env.URL, db: process.env.DB, cl: process.env.CL })
1313
+ let payload = JSON.parse(process.env.PAYLOAD)
1314
+ let tag = process.env.TAG
1315
+ if (process.env.MODE === 'insert') {
1316
+ let n = 0
1317
+ for (let time of payload) {
1318
+ let r = await wo.insert({ time, name: tag, value: 1 })
1319
+ n += r.nInserted
1320
+ }
1321
+ console.log(JSON.stringify({ tag, nInserted: n }))
1322
+ }
1323
+ else {
1324
+ let nInserted = 0
1325
+ let nModified = 0
1326
+ for (let f of payload.fields) {
1327
+ let r = await wo.save({ time: payload.time, [f]: tag })
1328
+ nInserted += r[0].nInserted
1329
+ nModified += r[0].nModified
1330
+ }
1331
+ console.log(JSON.stringify({ tag, nInserted, nModified }))
1332
+ }
1333
+ `
1334
+ let out = ''
1335
+ let p = spawn(process.execPath, ['--input-type=module', '-e', code], {
1336
+ shell: false,
1337
+ env: {
1338
+ ...process.env,
1339
+ SRC: path.resolve('./src/WOrmPostgresql.mjs'),
1340
+ URL: opt.url,
1341
+ DB: opt.db,
1342
+ CL: opt.cl,
1343
+ MODE: mode,
1344
+ PAYLOAD: JSON.stringify(payload),
1345
+ TAG: tag,
1346
+ },
1347
+ })
1348
+ p.stdout.on('data', function(d) {
1349
+ out += d.toString()
1350
+ })
1351
+ p.stderr.on('data', function(d) {
1352
+ out += d.toString()
1353
+ })
1354
+ p.on('close', function() {
1355
+ let r = null
1356
+ try {
1357
+ r = JSON.parse(out.trim())
1358
+ }
1359
+ catch (err) {
1360
+ r = { tag, error: out.trim() }
1361
+ }
1362
+ resolve(r)
1363
+ })
1364
+ })
1365
+ }
1366
+
1367
+
1368
+ describe('cross-process concurrency', function() {
1369
+ let vans = {}
1370
+ let vget = {}
1371
+
1372
+ before(async function() {
1373
+ this.timeout(300000)
1374
+
1375
+ //跨行程insert, 兩行程各自對相同20個time插入, nInserted總和須為20且資料表僅20筆
1376
+ let clIns = 'xprocinsert'
1377
+ let optIns = genOpt(clIns)
1378
+ let woIns = WOrm(optIns)
1379
+ await woIns.createTable(clIns, 'time', {
1380
+ time: '2000-01-01T00:00:00Z',
1381
+ name: 'abc',
1382
+ value: 0.1,
1383
+ }).catch(() => {})
1384
+ await woIns.delAll()
1385
+
1386
+ let times = Array.from({ length: 20 }, (v, k) => {
1387
+ return `2025-06-01T00:${`${k}`.padStart(2, '0')}:00Z`
1388
+ })
1389
+ let rps = await Promise.all([
1390
+ runProc('p1', optIns, 'insert', times),
1391
+ runProc('p2', optIns, 'insert', times),
1392
+ ])
1393
+ vget[1] = rps.filter((v) => v.error !== undefined).length
1394
+ vget[2] = rps.reduce((sum, v) => sum + (v.nInserted || 0), 0)
1395
+ vget[3] = (await woIns.select()).length
1396
+
1397
+ //跨行程save對同一time之不同欄位, 兩行程各寫5欄, 10欄須全數保留
1398
+ let clSav = 'xprocsave'
1399
+ let optSav = genOpt(clSav)
1400
+ let woSav = WOrm(optSav)
1401
+ let vSample = { time: '2000-01-01T00:00:00Z' }
1402
+ Array.from({ length: 10 }, (v, k) => k).forEach((k) => {
1403
+ vSample[`f${k}`] = 'abc'
1404
+ })
1405
+ await woSav.createTable(clSav, 'time', vSample).catch(() => {})
1406
+ await woSav.delAll()
1407
+
1408
+ let tSav = '2025-06-02T00:00:00Z'
1409
+ let rpss = await Promise.all([
1410
+ runProc('p1', optSav, 'save', { time: tSav, fields: ['f0', 'f1', 'f2', 'f3', 'f4'] }),
1411
+ runProc('p2', optSav, 'save', { time: tSav, fields: ['f5', 'f6', 'f7', 'f8', 'f9'] }),
1412
+ ])
1413
+ vget[4] = rpss.filter((v) => v.error !== undefined).length
1414
+
1415
+ //兩行程對同一新time各自save, 恰有一方之nInserted為1
1416
+ vget[5] = rpss.reduce((sum, v) => sum + (v.nInserted || 0), 0)
1417
+
1418
+ //10欄須全數保留且值為各自行程之tag
1419
+ let vSav = await woSav.selectByTime(tSav)
1420
+ vget[6] = Array.from({ length: 10 }, (v, k) => k)
1421
+ .filter((k) => {
1422
+ let tag = k <= 4 ? 'p1' : 'p2'
1423
+ return vSav[`f${k}`] === tag
1424
+ }).length
1425
+
1426
+ })
1427
+
1428
+ vans[1] = 0
1429
+ it(`should get ${JSON.stringify(vans[1])} for count of errored process in cross-process insert`, async function() {
1430
+ assert.strict.deepStrictEqual(vget[1], vans[1])
1431
+ })
1432
+
1433
+ vans[2] = 20
1434
+ it(`should get ${JSON.stringify(vans[2])} for sum of nInserted by 2 processes inserting same 20 times`, async function() {
1435
+ assert.strict.deepStrictEqual(vget[2], vans[2])
1436
+ })
1437
+
1438
+ vans[3] = 20
1439
+ it(`should get ${JSON.stringify(vans[3])} for records after cross-process insert`, async function() {
1440
+ assert.strict.deepStrictEqual(vget[3], vans[3])
1441
+ })
1442
+
1443
+ vans[4] = 0
1444
+ it(`should get ${JSON.stringify(vans[4])} for count of errored process in cross-process save`, async function() {
1445
+ assert.strict.deepStrictEqual(vget[4], vans[4])
1446
+ })
1447
+
1448
+ vans[5] = 1
1449
+ it(`should get ${JSON.stringify(vans[5])} for sum of nInserted by 2 processes saving same new time`, async function() {
1450
+ assert.strict.deepStrictEqual(vget[5], vans[5])
1451
+ })
1452
+
1453
+ vans[6] = 10
1454
+ it(`should get ${JSON.stringify(vans[6])} for count of fields kept after cross-process save with different field`, async function() {
1455
+ assert.strict.deepStrictEqual(vget[6], vans[6])
1456
+ })
1457
+
1458
+ })