w-orm-postgresql 1.0.7 → 1.0.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.
@@ -1,7 +1,134 @@
1
1
  import assert from 'assert'
2
+ import net from 'net'
3
+ import pg from 'pg'
4
+ import { execFile } from 'child_process'
5
+ import { promisify } from 'util'
2
6
  import WOrm from '../src/WOrmPostgresql.mjs'
3
7
 
4
8
 
9
+ //測試自行創建docker容器供PostgreSQL服務, 測試結束即銷毀, 故僅需環境已安裝docker
10
+
11
+
12
+ let execFileAsync = promisify(execFile)
13
+
14
+ let ctName = 'worm-test-postgresql'
15
+ let ctImage = 'postgres:17-alpine'
16
+ let ctUser = 'username'
17
+ let ctPassword = 'password'
18
+ let ctDb = 'worm'
19
+ let ctPort = null //容器對外埠, 由getFreePort動態取得, 避免與既有PostgreSQL服務搶5432
20
+
21
+
22
+ let delay = (ms) => {
23
+ return new Promise((resolve) => {
24
+ setTimeout(resolve, ms)
25
+ })
26
+ }
27
+
28
+ //getFreePort, 由系統配發空閒埠
29
+ let getFreePort = () => {
30
+ return new Promise((resolve, reject) => {
31
+ let srv = net.createServer()
32
+ srv.on('error', reject)
33
+ srv.listen(0, () => {
34
+ let port = srv.address().port
35
+ srv.close(() => {
36
+ resolve(port)
37
+ })
38
+ })
39
+ })
40
+ }
41
+
42
+ //docker, 以陣列傳參不經shell, 避免路徑與引號轉譯問題
43
+ let docker = (args) => {
44
+ return execFileAsync('docker', args)
45
+ }
46
+
47
+ //genUrl
48
+ let genUrl = () => {
49
+ return `postgresql://${ctUser}:${ctPassword}@127.0.0.1:${ctPort}`
50
+ }
51
+
52
+ //genOpt
53
+ let genOpt = (cl = 'users', useCache = false) => {
54
+ return {
55
+ url: genUrl(),
56
+ db: ctDb,
57
+ cl,
58
+ useCache,
59
+ }
60
+ }
61
+
62
+ //probeReady, 以pg直連容器對外埠試跑SELECT 1
63
+ //不使用容器內pg_isready, 因postgres映像初始化期間會先啟動僅監聽unix socket之臨時服務,
64
+ //pg_isready預設亦走unix socket故會提早回報就緒, 直連TCP方為測試實際所走路徑
65
+ let probeReady = async () => {
66
+ let client = new pg.Client({ connectionString: `${genUrl()}/${ctDb}` })
67
+ let ok = false
68
+ try {
69
+ await client.connect()
70
+ await client.query('SELECT 1')
71
+ ok = true
72
+ }
73
+ catch (err) {
74
+ ok = false
75
+ }
76
+ finally {
77
+ await client.end().catch(() => {})
78
+ }
79
+ return ok
80
+ }
81
+
82
+ //startContainer, 創建容器並等待服務就緒
83
+ //postgres映像以POSTGRES_USER、POSTGRES_PASSWORD、POSTGRES_DB即建妥帳號與資料庫, 無須另跑建置語句
84
+ let startContainer = async () => {
85
+
86
+ //check docker
87
+ await docker(['version', '--format', '{{.Server.Version}}'])
88
+ .catch(() => {
89
+ throw new Error('需先安裝並啟動docker才能執行postgresql測試')
90
+ })
91
+
92
+ //清除前次殘留容器
93
+ await docker(['rm', '-f', ctName]).catch(() => {})
94
+
95
+ //run
96
+ ctPort = await getFreePort()
97
+ await docker(['run', '-d', '--rm', '--name', ctName, '-e', `POSTGRES_USER=${ctUser}`, '-e', `POSTGRES_PASSWORD=${ctPassword}`, '-e', `POSTGRES_DB=${ctDb}`, '-p', `${ctPort}:5432`, ctImage])
98
+
99
+ //等待服務就緒, 首次需拉取映像故給予較長時間
100
+ let ready = false
101
+ for (let i = 0; i < 90; i++) {
102
+ let ok = await probeReady()
103
+ if (ok) {
104
+ ready = true
105
+ break
106
+ }
107
+ await delay(2000)
108
+ }
109
+ if (!ready) {
110
+ throw new Error('postgresql容器啟動逾時')
111
+ }
112
+
113
+ }
114
+
115
+ //stopContainer, 銷毀容器
116
+ let stopContainer = async () => {
117
+ await docker(['rm', '-f', ctName]).catch(() => {})
118
+ }
119
+
120
+
121
+ before(async function() {
122
+ this.timeout(600000) //含拉取映像與服務啟動
123
+ await startContainer()
124
+ })
125
+
126
+ after(async function() {
127
+ this.timeout(120000)
128
+ await stopContainer()
129
+ })
130
+
131
+
5
132
  describe('basic', function() {
6
133
  let rt = null
7
134
  let vans = {}
@@ -9,27 +136,7 @@ describe('basic', function() {
9
136
 
10
137
  before(async function () {
11
138
 
12
- let opt = {
13
- url: 'postgresql://username:password@127.0.0.1:5432',
14
- db: 'worm',
15
- cl: 'users',
16
- }
17
-
18
- //probe, 偵測postgres是否可用, 不可用則skip全部測試
19
- //pg預設無connect timeout, 連不到時會等到OS socket timeout (~75s), 故用Promise.race包3s timeout
20
- try {
21
- let probe = WOrm(opt)
22
- await Promise.race([
23
- probe.select(),
24
- new Promise((_resolve, reject) => setTimeout(() => reject(new Error('probe timeout')), 3000)),
25
- ])
26
- }
27
- catch (err) {
28
- let msg = err && err.message ? err.message : err
29
- console.log(`PostgreSQL unavailable, skip tests: ${msg}`)
30
- this.skip()
31
- return
32
- }
139
+ let opt = genOpt('users')
33
140
 
34
141
  let rs = [
35
142
  {
@@ -264,6 +371,51 @@ describe('basic', function() {
264
371
  })
265
372
  vget[6] = rt
266
373
 
374
+ //selectByTime
375
+ rt = null
376
+ // vans[18] = { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 }
377
+ await wo.selectByTime('2025-01-01T00:10:00Z')
378
+ .then(function(msg) {
379
+ // console.log('selectByTime then', msg)
380
+ // selectByTime { time: 2025-01-01T00:10:00.000Z, name: 'rosemary(modify)', value: 113.98 }
381
+ rt = msg
382
+ })
383
+ .catch(function(msg) {
384
+ // console.log('selectByTime catch', msg)
385
+ rt = msg.toString()
386
+ })
387
+ vget[18] = rt
388
+
389
+ //selectByTime by time not existed
390
+ rt = null
391
+ // vans[19] = null
392
+ await wo.selectByTime('2024-01-01T00:00:00Z')
393
+ .then(function(msg) {
394
+ // console.log('selectByTime by time not existed then', msg)
395
+ // selectByTime by time not existed null
396
+ rt = msg
397
+ })
398
+ .catch(function(msg) {
399
+ // console.log('selectByTime by time not existed catch', msg)
400
+ rt = msg.toString()
401
+ })
402
+ vget[19] = rt
403
+
404
+ //selectByTime by time invalid, 未給有效主鍵值視為查無數據
405
+ rt = null
406
+ // vans[20] = null
407
+ await wo.selectByTime('abc')
408
+ .then(function(msg) {
409
+ // console.log('selectByTime by time invalid then', msg)
410
+ // selectByTime by time invalid null
411
+ rt = msg
412
+ })
413
+ .catch(function(msg) {
414
+ // console.log('selectByTime by time invalid catch', msg)
415
+ rt = msg.toString()
416
+ })
417
+ vget[20] = rt
418
+
267
419
  //select by $and, $gt, $lt
268
420
  rt = null
269
421
  // vans[7] = [
@@ -520,6 +672,34 @@ describe('basic', function() {
520
672
  })
521
673
  vget[17] = rt
522
674
 
675
+ //selectByTime (1st time, 從DB讀取並填入快取)
676
+ rt = null
677
+ // vans[21] = { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary(modify)', value: 654.321 }
678
+ await woc.selectByTime('2025-01-01T00:01:00Z')
679
+ .then(function(msg) {
680
+ // console.log('selectByTime 1st then', msg)
681
+ rt = msg
682
+ })
683
+ .catch(function(msg) {
684
+ // console.log('selectByTime 1st catch', msg)
685
+ rt = msg.toString()
686
+ })
687
+ vget[21] = rt
688
+
689
+ //selectByTime (2nd time, 命中快取, 內容須與第一次相同)
690
+ rt = null
691
+ // vans[22] = vans[21]
692
+ await woc.selectByTime('2025-01-01T00:01:00Z')
693
+ .then(function(msg) {
694
+ // console.log('selectByTime 2nd then', msg)
695
+ rt = msg
696
+ })
697
+ .catch(function(msg) {
698
+ // console.log('selectByTime 2nd catch', msg)
699
+ rt = msg.toString()
700
+ })
701
+ vget[22] = rt
702
+
523
703
  })
524
704
 
525
705
  vans[1] = { ok: 1 }
@@ -576,6 +756,22 @@ describe('basic', function() {
576
756
  assert.strict.deepStrictEqual(vget[6], vans[6])
577
757
  })
578
758
 
759
+ //vans[18]~vans[22]為後續新增之selectByTime, 為不更動既有編號故接續於末號之後
760
+ vans[18] = { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 }
761
+ it(`should get ${JSON.stringify(vans[18])} for selectByTime`, async function() {
762
+ assert.strict.deepStrictEqual(vget[18], vans[18])
763
+ })
764
+
765
+ vans[19] = null
766
+ it(`should get ${JSON.stringify(vans[19])} for selectByTime by time not existed`, async function() {
767
+ assert.strict.deepStrictEqual(vget[19], vans[19])
768
+ })
769
+
770
+ vans[20] = null
771
+ it(`should get ${JSON.stringify(vans[20])} for selectByTime by time invalid`, async function() {
772
+ assert.strict.deepStrictEqual(vget[20], vans[20])
773
+ })
774
+
579
775
  vans[7] = [
580
776
  { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary', value: 123.456 },
581
777
  { time: new Date('2025-01-01T00:04:00.000Z'), name: 'rosemary', value: 123.1236 },
@@ -678,4 +874,250 @@ describe('basic', function() {
678
874
  assert.strict.deepStrictEqual(vget[17], vans[17])
679
875
  })
680
876
 
877
+ vans[21] = { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary(modify)', value: 654.321 }
878
+ it(`should get ${JSON.stringify(vans[21])} for selectByTime (1st, fill cache)`, async function() {
879
+ assert.strict.deepStrictEqual(vget[21], vans[21])
880
+ })
881
+
882
+ vans[22] = vans[21]
883
+ it(`should get ${JSON.stringify(vans[22])} for selectByTime (2nd, cache hit)`, async function() {
884
+ assert.strict.deepStrictEqual(vget[22], vans[22])
885
+ })
886
+
887
+ })
888
+
889
+
890
+ //insert與save之併發測試, 因各操作皆各自new PgClient另開連線, 原子性全由PostgreSQL提供,
891
+ //故同行程以Promise.all併發即等價於跨行程併發, 不須另起行程
892
+
893
+
894
+ describe('insert nInserted', function() {
895
+ let vans = {}
896
+ let vget = {}
897
+
898
+ before(async function() {
899
+ this.timeout(120000)
900
+
901
+ //wo
902
+ let cl = 'insertcnt'
903
+ let wo = WOrm(genOpt(cl))
904
+
905
+ //createTable
906
+ await wo.createTable(cl, 'time', {
907
+ time: '2000-01-01T00:00:00Z',
908
+ name: 'abc',
909
+ value: 0.1,
910
+ }).catch(() => {})
911
+ await wo.delAll()
912
+
913
+ //全新3筆
914
+ vget[1] = await wo.insert([
915
+ { time: '2025-02-01T00:00:00Z', name: 'peter', value: 1 },
916
+ { time: '2025-02-01T00:01:00Z', name: 'rosemary', value: 2 },
917
+ { time: '2025-02-01T00:02:00Z', name: 'kettle', value: 3 },
918
+ ])
919
+
920
+ //重複插入同3筆, 全數已存在故皆不插入
921
+ vget[2] = await wo.insert([
922
+ { time: '2025-02-01T00:00:00Z', name: 'peter', value: 1 },
923
+ { time: '2025-02-01T00:01:00Z', name: 'rosemary', value: 2 },
924
+ { time: '2025-02-01T00:02:00Z', name: 'kettle', value: 3 },
925
+ ])
926
+
927
+ //部份重複, 1筆已存在2筆為新
928
+ vget[3] = await wo.insert([
929
+ { time: '2025-02-01T00:01:00Z', name: 'rosemary', value: 2 },
930
+ { time: '2025-02-01T00:03:00Z', name: 'sandler', value: 4 },
931
+ { time: '2025-02-01T00:04:00Z', name: 'joyce', value: 5 },
932
+ ])
933
+
934
+ //同批含重複time, 僅首筆成功
935
+ vget[4] = await wo.insert([
936
+ { time: '2025-02-01T00:05:00Z', name: 'dup-1', value: 6 },
937
+ { time: '2025-02-01T00:05:00Z', name: 'dup-2', value: 7 },
938
+ { time: '2025-02-01T00:06:00Z', name: 'uniq', value: 8 },
939
+ ])
940
+
941
+ //併發對同一time插入10次, nInserted總和須為1
942
+ let rs = await Promise.all(Array.from({ length: 10 }, (v, k) => {
943
+ return wo.insert({ time: '2025-02-01T00:07:00Z', name: `race-${k}`, value: k })
944
+ }))
945
+ vget[5] = rs.reduce((sum, v) => sum + v.nInserted, 0)
946
+
947
+ //併發皆不得報錯
948
+ vget[6] = rs.filter((v) => v.ok !== 1).length
949
+
950
+ //最終筆數, 3+0+2+2+1=8
951
+ let ss = await wo.select()
952
+ vget[7] = ss.length
953
+
954
+ //同批重複者僅首筆入庫
955
+ let vdup = await wo.selectByTime('2025-02-01T00:05:00Z')
956
+ vget[8] = vdup.name
957
+
958
+ })
959
+
960
+ vans[1] = { n: 3, nInserted: 3, ok: 1 }
961
+ it(`should get ${JSON.stringify(vans[1])} for insert 3 new records`, async function() {
962
+ assert.strict.deepStrictEqual(vget[1], vans[1])
963
+ })
964
+
965
+ vans[2] = { n: 3, nInserted: 0, ok: 1 }
966
+ it(`should get ${JSON.stringify(vans[2])} for insert 3 existed records`, async function() {
967
+ assert.strict.deepStrictEqual(vget[2], vans[2])
968
+ })
969
+
970
+ vans[3] = { n: 3, nInserted: 2, ok: 1 }
971
+ it(`should get ${JSON.stringify(vans[3])} for insert 1 existed and 2 new records`, async function() {
972
+ assert.strict.deepStrictEqual(vget[3], vans[3])
973
+ })
974
+
975
+ vans[4] = { n: 3, nInserted: 2, ok: 1 }
976
+ it(`should get ${JSON.stringify(vans[4])} for insert with duplicated time in same batch`, async function() {
977
+ assert.strict.deepStrictEqual(vget[4], vans[4])
978
+ })
979
+
980
+ vans[5] = 1
981
+ it(`should get ${JSON.stringify(vans[5])} for sum of nInserted by 10 concurrent insert with same time`, async function() {
982
+ assert.strict.deepStrictEqual(vget[5], vans[5])
983
+ })
984
+
985
+ vans[6] = 0
986
+ it(`should get ${JSON.stringify(vans[6])} for count of ok!==1 in 10 concurrent insert`, async function() {
987
+ assert.strict.deepStrictEqual(vget[6], vans[6])
988
+ })
989
+
990
+ vans[7] = 8
991
+ it(`should get ${JSON.stringify(vans[7])} for records after all insert`, async function() {
992
+ assert.strict.deepStrictEqual(vget[7], vans[7])
993
+ })
994
+
995
+ vans[8] = 'dup-1'
996
+ it(`should get ${JSON.stringify(vans[8])} for keeping first one of duplicated time in same batch`, async function() {
997
+ assert.strict.deepStrictEqual(vget[8], vans[8])
998
+ })
999
+
1000
+ })
1001
+
1002
+
1003
+ describe('save concurrency', function() {
1004
+ let vans = {}
1005
+ let vget = {}
1006
+
1007
+ before(async function() {
1008
+ this.timeout(120000)
1009
+
1010
+ //wo
1011
+ let cl = 'saverace'
1012
+ let wo = WOrm(genOpt(cl))
1013
+
1014
+ //createTable
1015
+ await wo.createTable(cl, 'time', {
1016
+ time: '2000-01-01T00:00:00Z',
1017
+ name: 'abc',
1018
+ value: 0.1,
1019
+ }).catch(() => {})
1020
+ await wo.delAll()
1021
+
1022
+ //併發save對既有time之不同欄位, 各欄位皆須保留
1023
+ let tMerge = '2025-03-01T00:00:00Z'
1024
+ await wo.insert({ time: tMerge, name: 'origin', value: 1 })
1025
+ await Promise.all([
1026
+ wo.save({ time: tMerge, name: 'merge-name' }),
1027
+ wo.save({ time: tMerge, value: 99 }),
1028
+ ])
1029
+ vget[1] = await wo.selectByTime(tMerge)
1030
+
1031
+ //併發save對全新time, autoInsert僅一次且不得報錯
1032
+ let tNew = '2025-03-01T00:01:00Z'
1033
+ let rsn = await Promise.all(Array.from({ length: 5 }, (v, k) => {
1034
+ return wo.save({ time: tNew, name: `new-${k}` })
1035
+ }))
1036
+ vget[2] = rsn.filter((v) => v[0].nInserted === 1).length
1037
+ vget[3] = rsn.filter((v) => v[0].ok !== 1).length
1038
+ vget[4] = (await wo.select({ time: tNew })).length
1039
+
1040
+ //save內容相同不更新
1041
+ let tSame = '2025-03-01T00:02:00Z'
1042
+ await wo.insert({ time: tSame, name: 'same', value: 5 })
1043
+ vget[5] = await wo.save({ time: tSame, name: 'same', value: 5 })
1044
+
1045
+ //save內容不同須更新, 未給之欄位須保留
1046
+ let tDiff = '2025-03-01T00:03:00Z'
1047
+ await wo.insert({ time: tDiff, name: 'diff', value: 5 })
1048
+ vget[6] = await wo.save({ time: tDiff, value: 6 })
1049
+ vget[7] = await wo.selectByTime(tDiff)
1050
+
1051
+ //save(autoInsert=false)對不存在之time不得插入
1052
+ let tNone = '2025-03-01T00:04:00Z'
1053
+ vget[8] = await wo.save({ time: tNone, name: 'none' }, { autoInsert: false })
1054
+ vget[9] = await wo.selectByTime(tNone)
1055
+
1056
+ //併發save(autoInsert=false)對既有time之不同欄位, 各欄位皆須保留且不得報錯
1057
+ let tUpd = '2025-03-01T00:05:00Z'
1058
+ await wo.insert({ time: tUpd, name: 'origin', value: 1 })
1059
+ let rsu = await Promise.all([
1060
+ wo.save({ time: tUpd, name: 'upd-name' }, { autoInsert: false }),
1061
+ wo.save({ time: tUpd, value: 88 }, { autoInsert: false }),
1062
+ ])
1063
+ vget[10] = rsu.filter((v) => v[0].ok !== 1).length
1064
+ vget[11] = await wo.selectByTime(tUpd)
1065
+
1066
+ })
1067
+
1068
+ vans[1] = { time: new Date('2025-03-01T00:00:00.000Z'), name: 'merge-name', value: 99 }
1069
+ it(`should get ${JSON.stringify(vans[1])} for fields after 2 concurrent save with different field`, async function() {
1070
+ assert.strict.deepStrictEqual(vget[1], vans[1])
1071
+ })
1072
+
1073
+ vans[2] = 1
1074
+ it(`should get ${JSON.stringify(vans[2])} for count of nInserted===1 in 5 concurrent save with time not existed`, async function() {
1075
+ assert.strict.deepStrictEqual(vget[2], vans[2])
1076
+ })
1077
+
1078
+ vans[3] = 0
1079
+ it(`should get ${JSON.stringify(vans[3])} for count of ok!==1 in 5 concurrent save with time not existed`, async function() {
1080
+ assert.strict.deepStrictEqual(vget[3], vans[3])
1081
+ })
1082
+
1083
+ vans[4] = 1
1084
+ it(`should get ${JSON.stringify(vans[4])} for records after 5 concurrent save with time not existed`, async function() {
1085
+ assert.strict.deepStrictEqual(vget[4], vans[4])
1086
+ })
1087
+
1088
+ vans[5] = [{ n: 1, nModified: 0, ok: 1 }]
1089
+ it(`should get ${JSON.stringify(vans[5])} for save with same content`, async function() {
1090
+ assert.strict.deepStrictEqual(vget[5], vans[5])
1091
+ })
1092
+
1093
+ vans[6] = [{ n: 1, nModified: 1, ok: 1 }]
1094
+ it(`should get ${JSON.stringify(vans[6])} for save with different content`, async function() {
1095
+ assert.strict.deepStrictEqual(vget[6], vans[6])
1096
+ })
1097
+
1098
+ vans[7] = { time: new Date('2025-03-01T00:03:00.000Z'), name: 'diff', value: 6 }
1099
+ it(`should get ${JSON.stringify(vans[7])} for keeping field not given in save`, async function() {
1100
+ assert.strict.deepStrictEqual(vget[7], vans[7])
1101
+ })
1102
+
1103
+ vans[8] = [{ n: 0, nModified: 0, ok: 1 }]
1104
+ it(`should get ${JSON.stringify(vans[8])} for save(autoInsert=false) with time not existed`, async function() {
1105
+ assert.strict.deepStrictEqual(vget[8], vans[8])
1106
+ })
1107
+
1108
+ vans[9] = null
1109
+ it(`should get ${JSON.stringify(vans[9])} for record after save(autoInsert=false) with time not existed`, async function() {
1110
+ assert.strict.deepStrictEqual(vget[9], vans[9])
1111
+ })
1112
+
1113
+ vans[10] = 0
1114
+ it(`should get ${JSON.stringify(vans[10])} for count of ok!==1 in 2 concurrent save(autoInsert=false)`, async function() {
1115
+ assert.strict.deepStrictEqual(vget[10], vans[10])
1116
+ })
1117
+
1118
+ vans[11] = { time: new Date('2025-03-01T00:05:00.000Z'), name: 'upd-name', value: 88 }
1119
+ it(`should get ${JSON.stringify(vans[11])} for fields after 2 concurrent save(autoInsert=false) with different field`, async function() {
1120
+ assert.strict.deepStrictEqual(vget[11], vans[11])
1121
+ })
1122
+
681
1123
  })