w-orm-postgresql 1.0.8 → 1.0.10

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,8 +1,10 @@
1
1
  import assert from 'assert'
2
2
  import net from 'net'
3
+ import path from 'path'
3
4
  import pg from 'pg'
4
- import { execFile } from 'child_process'
5
+ import { execFile, spawn } from 'child_process'
5
6
  import { promisify } from 'util'
7
+ import isestr from 'wsemi/src/isestr.mjs'
6
8
  import WOrm from '../src/WOrmPostgresql.mjs'
7
9
 
8
10
 
@@ -371,47 +373,47 @@ describe('basic', function() {
371
373
  })
372
374
  vget[6] = rt
373
375
 
374
- //selectByTime
376
+ //selectByPk
375
377
  rt = null
376
378
  // 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')
379
+ await wo.selectByPk('2025-01-01T00:10:00Z')
378
380
  .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
+ // console.log('selectByPk then', msg)
382
+ // selectByPk { time: 2025-01-01T00:10:00.000Z, name: 'rosemary(modify)', value: 113.98 }
381
383
  rt = msg
382
384
  })
383
385
  .catch(function(msg) {
384
- // console.log('selectByTime catch', msg)
386
+ // console.log('selectByPk catch', msg)
385
387
  rt = msg.toString()
386
388
  })
387
389
  vget[18] = rt
388
390
 
389
- //selectByTime by time not existed
391
+ //selectByPk by pk not existed
390
392
  rt = null
391
393
  // vans[19] = null
392
- await wo.selectByTime('2024-01-01T00:00:00Z')
394
+ await wo.selectByPk('2024-01-01T00:00:00Z')
393
395
  .then(function(msg) {
394
- // console.log('selectByTime by time not existed then', msg)
395
- // selectByTime by time not existed null
396
+ // console.log('selectByPk by pk not existed then', msg)
397
+ // selectByPk by pk not existed null
396
398
  rt = msg
397
399
  })
398
400
  .catch(function(msg) {
399
- // console.log('selectByTime by time not existed catch', msg)
401
+ // console.log('selectByPk by pk not existed catch', msg)
400
402
  rt = msg.toString()
401
403
  })
402
404
  vget[19] = rt
403
405
 
404
- //selectByTime by time invalid, 未給有效主鍵值視為查無數據
406
+ //selectByPk by pk invalid, 未給有效主鍵值視為查無數據
405
407
  rt = null
406
408
  // vans[20] = null
407
- await wo.selectByTime('abc')
409
+ await wo.selectByPk('abc')
408
410
  .then(function(msg) {
409
- // console.log('selectByTime by time invalid then', msg)
410
- // selectByTime by time invalid null
411
+ // console.log('selectByPk by pk invalid then', msg)
412
+ // selectByPk by pk invalid null
411
413
  rt = msg
412
414
  })
413
415
  .catch(function(msg) {
414
- // console.log('selectByTime by time invalid catch', msg)
416
+ // console.log('selectByPk by pk invalid catch', msg)
415
417
  rt = msg.toString()
416
418
  })
417
419
  vget[20] = rt
@@ -672,30 +674,30 @@ describe('basic', function() {
672
674
  })
673
675
  vget[17] = rt
674
676
 
675
- //selectByTime (1st time, 從DB讀取並填入快取)
677
+ //selectByPk (1st time, 從DB讀取並填入快取)
676
678
  rt = null
677
679
  // 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')
680
+ await woc.selectByPk('2025-01-01T00:01:00Z')
679
681
  .then(function(msg) {
680
- // console.log('selectByTime 1st then', msg)
682
+ // console.log('selectByPk 1st then', msg)
681
683
  rt = msg
682
684
  })
683
685
  .catch(function(msg) {
684
- // console.log('selectByTime 1st catch', msg)
686
+ // console.log('selectByPk 1st catch', msg)
685
687
  rt = msg.toString()
686
688
  })
687
689
  vget[21] = rt
688
690
 
689
- //selectByTime (2nd time, 命中快取, 內容須與第一次相同)
691
+ //selectByPk (2nd time, 命中快取, 內容須與第一次相同)
690
692
  rt = null
691
693
  // vans[22] = vans[21]
692
- await woc.selectByTime('2025-01-01T00:01:00Z')
694
+ await woc.selectByPk('2025-01-01T00:01:00Z')
693
695
  .then(function(msg) {
694
- // console.log('selectByTime 2nd then', msg)
696
+ // console.log('selectByPk 2nd then', msg)
695
697
  rt = msg
696
698
  })
697
699
  .catch(function(msg) {
698
- // console.log('selectByTime 2nd catch', msg)
700
+ // console.log('selectByPk 2nd catch', msg)
699
701
  rt = msg.toString()
700
702
  })
701
703
  vget[22] = rt
@@ -713,11 +715,11 @@ describe('basic', function() {
713
715
  })
714
716
 
715
717
  vans[3] = [
716
- { n: 1, nModified: 0, ok: 1 },
717
- { n: 1, nModified: 1, ok: 1 },
718
- { n: 1, nModified: 1, ok: 1 },
719
- { n: 1, nModified: 1, ok: 1 },
720
- { n: 0, nModified: 0, ok: 1 }
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 }
721
723
  ]
722
724
  it(`should get ${JSON.stringify(vans[3])} for save(autoInsert=false)`, async function() {
723
725
  assert.strict.deepStrictEqual(vget[3], vans[3])
@@ -756,19 +758,19 @@ describe('basic', function() {
756
758
  assert.strict.deepStrictEqual(vget[6], vans[6])
757
759
  })
758
760
 
759
- //vans[18]~vans[22]為後續新增之selectByTime, 為不更動既有編號故接續於末號之後
761
+ //vans[18]~vans[22]為後續新增之selectByPk, 為不更動既有編號故接續於末號之後
760
762
  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() {
763
+ it(`should get ${JSON.stringify(vans[18])} for selectByPk`, async function() {
762
764
  assert.strict.deepStrictEqual(vget[18], vans[18])
763
765
  })
764
766
 
765
767
  vans[19] = null
766
- it(`should get ${JSON.stringify(vans[19])} for selectByTime by time not existed`, async function() {
768
+ it(`should get ${JSON.stringify(vans[19])} for selectByPk by pk not existed`, async function() {
767
769
  assert.strict.deepStrictEqual(vget[19], vans[19])
768
770
  })
769
771
 
770
772
  vans[20] = null
771
- it(`should get ${JSON.stringify(vans[20])} for selectByTime by time invalid`, async function() {
773
+ it(`should get ${JSON.stringify(vans[20])} for selectByPk by pk invalid`, async function() {
772
774
  assert.strict.deepStrictEqual(vget[20], vans[20])
773
775
  })
774
776
 
@@ -810,12 +812,12 @@ describe('basic', function() {
810
812
  // assert.strict.deepStrictEqual(vget[14], vans[14])
811
813
  // })
812
814
 
813
- vans[10] = [{ n: 1, nInserted: 1, ok: 1 }]
815
+ vans[10] = [{ n: 1, nInserted: 1, nModified: 0, ok: 1 }]
814
816
  it(`should get ${JSON.stringify(vans[10])} for save(autoInsert=true)`, async function() {
815
817
  assert.strict.deepStrictEqual(vget[10], vans[10])
816
818
  })
817
819
 
818
- vans[11] = [{ n: 1, nDeleted: 0, ok: 1 }]
820
+ vans[11] = [{ n: 0, nDeleted: 0, ok: 1 }]
819
821
  it(`should get ${JSON.stringify(vans[11])} for del`, async function() {
820
822
  assert.strict.deepStrictEqual(vget[11], vans[11])
821
823
  })
@@ -860,7 +862,7 @@ describe('basic', function() {
860
862
  assert.strict.deepStrictEqual(vget[15], vans[15])
861
863
  })
862
864
 
863
- vans[16] = [{ n: 1, nModified: 1, ok: 1 }]
865
+ vans[16] = [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }]
864
866
  it(`should get ${JSON.stringify(vans[16])} for save (invalidate cache)`, async function() {
865
867
  assert.strict.deepStrictEqual(vget[16], vans[16])
866
868
  })
@@ -875,12 +877,12 @@ describe('basic', function() {
875
877
  })
876
878
 
877
879
  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() {
880
+ it(`should get ${JSON.stringify(vans[21])} for selectByPk (1st, fill cache)`, async function() {
879
881
  assert.strict.deepStrictEqual(vget[21], vans[21])
880
882
  })
881
883
 
882
884
  vans[22] = vans[21]
883
- it(`should get ${JSON.stringify(vans[22])} for selectByTime (2nd, cache hit)`, async function() {
885
+ it(`should get ${JSON.stringify(vans[22])} for selectByPk (2nd, cache hit)`, async function() {
884
886
  assert.strict.deepStrictEqual(vget[22], vans[22])
885
887
  })
886
888
 
@@ -952,7 +954,7 @@ describe('insert nInserted', function() {
952
954
  vget[7] = ss.length
953
955
 
954
956
  //同批重複者僅首筆入庫
955
- let vdup = await wo.selectByTime('2025-02-01T00:05:00Z')
957
+ let vdup = await wo.selectByPk('2025-02-01T00:05:00Z')
956
958
  vget[8] = vdup.name
957
959
 
958
960
  })
@@ -1026,7 +1028,7 @@ describe('save concurrency', function() {
1026
1028
  wo.save({ time: tMerge, name: 'merge-name' }),
1027
1029
  wo.save({ time: tMerge, value: 99 }),
1028
1030
  ])
1029
- vget[1] = await wo.selectByTime(tMerge)
1031
+ vget[1] = await wo.selectByPk(tMerge)
1030
1032
 
1031
1033
  //併發save對全新time, autoInsert僅一次且不得報錯
1032
1034
  let tNew = '2025-03-01T00:01:00Z'
@@ -1046,12 +1048,12 @@ describe('save concurrency', function() {
1046
1048
  let tDiff = '2025-03-01T00:03:00Z'
1047
1049
  await wo.insert({ time: tDiff, name: 'diff', value: 5 })
1048
1050
  vget[6] = await wo.save({ time: tDiff, value: 6 })
1049
- vget[7] = await wo.selectByTime(tDiff)
1051
+ vget[7] = await wo.selectByPk(tDiff)
1050
1052
 
1051
1053
  //save(autoInsert=false)對不存在之time不得插入
1052
1054
  let tNone = '2025-03-01T00:04:00Z'
1053
1055
  vget[8] = await wo.save({ time: tNone, name: 'none' }, { autoInsert: false })
1054
- vget[9] = await wo.selectByTime(tNone)
1056
+ vget[9] = await wo.selectByPk(tNone)
1055
1057
 
1056
1058
  //併發save(autoInsert=false)對既有time之不同欄位, 各欄位皆須保留且不得報錯
1057
1059
  let tUpd = '2025-03-01T00:05:00Z'
@@ -1061,7 +1063,7 @@ describe('save concurrency', function() {
1061
1063
  wo.save({ time: tUpd, value: 88 }, { autoInsert: false }),
1062
1064
  ])
1063
1065
  vget[10] = rsu.filter((v) => v[0].ok !== 1).length
1064
- vget[11] = await wo.selectByTime(tUpd)
1066
+ vget[11] = await wo.selectByPk(tUpd)
1065
1067
 
1066
1068
  })
1067
1069
 
@@ -1085,12 +1087,12 @@ describe('save concurrency', function() {
1085
1087
  assert.strict.deepStrictEqual(vget[4], vans[4])
1086
1088
  })
1087
1089
 
1088
- vans[5] = [{ n: 1, nModified: 0, ok: 1 }]
1090
+ vans[5] = [{ n: 1, nInserted: 0, nModified: 0, ok: 1 }]
1089
1091
  it(`should get ${JSON.stringify(vans[5])} for save with same content`, async function() {
1090
1092
  assert.strict.deepStrictEqual(vget[5], vans[5])
1091
1093
  })
1092
1094
 
1093
- vans[6] = [{ n: 1, nModified: 1, ok: 1 }]
1095
+ vans[6] = [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }]
1094
1096
  it(`should get ${JSON.stringify(vans[6])} for save with different content`, async function() {
1095
1097
  assert.strict.deepStrictEqual(vget[6], vans[6])
1096
1098
  })
@@ -1100,7 +1102,7 @@ describe('save concurrency', function() {
1100
1102
  assert.strict.deepStrictEqual(vget[7], vans[7])
1101
1103
  })
1102
1104
 
1103
- vans[8] = [{ n: 0, nModified: 0, ok: 1 }]
1105
+ vans[8] = [{ n: 0, nInserted: 0, nModified: 0, ok: 1 }]
1104
1106
  it(`should get ${JSON.stringify(vans[8])} for save(autoInsert=false) with time not existed`, async function() {
1105
1107
  assert.strict.deepStrictEqual(vget[8], vans[8])
1106
1108
  })
@@ -1121,3 +1123,468 @@ describe('save concurrency', function() {
1121
1123
  })
1122
1124
 
1123
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.selectByPk(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
+ describe('custom pk', function() {
1306
+ let vans = {}
1307
+ let vget = {}
1308
+
1309
+ before(async function() {
1310
+ this.timeout(120000)
1311
+
1312
+ //wo, 以opt.pk指定主鍵欄位為code, 驗證select以外之五函數皆改以該欄位認定主鍵
1313
+ let cl = 'custompk'
1314
+ let wo = WOrm({
1315
+ ...genOpt(cl),
1316
+ pk: 'code',
1317
+ })
1318
+
1319
+ //createTable, pk留空以驗證採用建構時之opt.pk
1320
+ await wo.createTable(cl, null, {
1321
+ code: 'abc',
1322
+ name: 'abc',
1323
+ value: 0.1,
1324
+ }).catch(() => {})
1325
+ await wo.delAll()
1326
+
1327
+ //insert以code認定主鍵
1328
+ vget[1] = await wo.insert([
1329
+ { code: 'c1', name: 'peter', value: 1 },
1330
+ { code: 'c2', name: 'rosemary', value: 2 },
1331
+ ])
1332
+
1333
+ //insert同code者跳過, 證明ON CONFLICT係以code為衝突目標
1334
+ vget[2] = await wo.insert({ code: 'c1', name: 'other', value: 9 })
1335
+
1336
+ //selectByPk以code查找
1337
+ vget[3] = await wo.selectByPk('c1')
1338
+
1339
+ //selectByPk未命中
1340
+ vget[4] = await wo.selectByPk('notexist')
1341
+
1342
+ //save只給部份欄位且值不同, 未給之欄位須保留
1343
+ vget[5] = await wo.save({ code: 'c1', value: 5 })
1344
+ vget[6] = await wo.selectByPk('c1')
1345
+
1346
+ //save合併後內容相同不更新
1347
+ vget[7] = await wo.save({ code: 'c1', value: 5 })
1348
+
1349
+ //save對不存在之code自動插入
1350
+ vget[8] = await wo.save({ code: 'c3', name: 'sandler' })
1351
+
1352
+ //del以code刪除
1353
+ vget[9] = await wo.del({ code: 'c2' })
1354
+
1355
+ //del未帶有效code
1356
+ let rsd = await wo.del({ name: 'no-code' })
1357
+ vget[10] = {
1358
+ n: rsd[0].n,
1359
+ nDeleted: rsd[0].nDeleted,
1360
+ ok: rsd[0].ok,
1361
+ hasErr: isestr(rsd[0].err),
1362
+ }
1363
+
1364
+ //insert未帶有效code須reject
1365
+ vget[11] = await wo.insert({ name: 'no-code' })
1366
+ .then(() => 'resolved')
1367
+ .catch(() => 'rejected')
1368
+
1369
+ //最終筆數, 僅存c1與c3
1370
+ vget[12] = (await wo.select()).length
1371
+
1372
+ })
1373
+
1374
+ vans[1] = { n: 2, nInserted: 2, ok: 1 }
1375
+ it(`should get ${JSON.stringify(vans[1])} for insert with custom pk`, async function() {
1376
+ assert.strict.deepStrictEqual(vget[1], vans[1])
1377
+ })
1378
+
1379
+ vans[2] = { n: 1, nInserted: 0, ok: 1 }
1380
+ it(`should get ${JSON.stringify(vans[2])} for insert with existed custom pk`, async function() {
1381
+ assert.strict.deepStrictEqual(vget[2], vans[2])
1382
+ })
1383
+
1384
+ vans[3] = { code: 'c1', name: 'peter', value: 1 }
1385
+ it(`should get ${JSON.stringify(vans[3])} for selectByPk with custom pk`, async function() {
1386
+ assert.strict.deepStrictEqual(vget[3], vans[3])
1387
+ })
1388
+
1389
+ vans[4] = null
1390
+ it(`should get ${JSON.stringify(vans[4])} for selectByPk by custom pk not existed`, async function() {
1391
+ assert.strict.deepStrictEqual(vget[4], vans[4])
1392
+ })
1393
+
1394
+ vans[5] = [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }]
1395
+ it(`should get ${JSON.stringify(vans[5])} for save with custom pk`, async function() {
1396
+ assert.strict.deepStrictEqual(vget[5], vans[5])
1397
+ })
1398
+
1399
+ vans[6] = { code: 'c1', name: 'peter', value: 5 }
1400
+ it(`should get ${JSON.stringify(vans[6])} for keeping field not given in save with custom pk`, async function() {
1401
+ assert.strict.deepStrictEqual(vget[6], vans[6])
1402
+ })
1403
+
1404
+ vans[7] = [{ n: 1, nInserted: 0, nModified: 0, ok: 1 }]
1405
+ it(`should get ${JSON.stringify(vans[7])} for save with same content by custom pk`, async function() {
1406
+ assert.strict.deepStrictEqual(vget[7], vans[7])
1407
+ })
1408
+
1409
+ vans[8] = [{ n: 1, nInserted: 1, nModified: 0, ok: 1 }]
1410
+ it(`should get ${JSON.stringify(vans[8])} for save(autoInsert) with custom pk not existed`, async function() {
1411
+ assert.strict.deepStrictEqual(vget[8], vans[8])
1412
+ })
1413
+
1414
+ vans[9] = [{ n: 1, nDeleted: 1, ok: 1 }]
1415
+ it(`should get ${JSON.stringify(vans[9])} for del with custom pk`, async function() {
1416
+ assert.strict.deepStrictEqual(vget[9], vans[9])
1417
+ })
1418
+
1419
+ vans[10] = { n: 0, nDeleted: 0, ok: 0, hasErr: true }
1420
+ it(`should get ${JSON.stringify(vans[10])} for del without valid custom pk`, async function() {
1421
+ assert.strict.deepStrictEqual(vget[10], vans[10])
1422
+ })
1423
+
1424
+ vans[11] = 'rejected'
1425
+ it(`should get ${JSON.stringify(vans[11])} for insert without valid custom pk`, async function() {
1426
+ assert.strict.deepStrictEqual(vget[11], vans[11])
1427
+ })
1428
+
1429
+ vans[12] = 2
1430
+ it(`should get ${JSON.stringify(vans[12])} for records after all operations with custom pk`, async function() {
1431
+ assert.strict.deepStrictEqual(vget[12], vans[12])
1432
+ })
1433
+
1434
+ })
1435
+
1436
+
1437
+ //runProc, 另起行程對同一資料表操作, 用於驗證跨行程之原子性
1438
+ //子行程以--input-type=module執行, 並以pathToFileURL轉換src路徑, 避免專案路徑含非ASCII字元而無法import
1439
+ function runProc(tag, opt, mode, payload) {
1440
+ return new Promise(function(resolve) {
1441
+ let code = `
1442
+ import { pathToFileURL } from 'url'
1443
+ let { default: WOrm } = await import(pathToFileURL(process.env.SRC).href)
1444
+ let wo = WOrm({ url: process.env.URL, db: process.env.DB, cl: process.env.CL })
1445
+ let payload = JSON.parse(process.env.PAYLOAD)
1446
+ let tag = process.env.TAG
1447
+ if (process.env.MODE === 'insert') {
1448
+ let n = 0
1449
+ for (let time of payload) {
1450
+ let r = await wo.insert({ time, name: tag, value: 1 })
1451
+ n += r.nInserted
1452
+ }
1453
+ console.log(JSON.stringify({ tag, nInserted: n }))
1454
+ }
1455
+ else {
1456
+ let nInserted = 0
1457
+ let nModified = 0
1458
+ for (let f of payload.fields) {
1459
+ let r = await wo.save({ time: payload.time, [f]: tag })
1460
+ nInserted += r[0].nInserted
1461
+ nModified += r[0].nModified
1462
+ }
1463
+ console.log(JSON.stringify({ tag, nInserted, nModified }))
1464
+ }
1465
+ `
1466
+ let out = ''
1467
+ let p = spawn(process.execPath, ['--input-type=module', '-e', code], {
1468
+ shell: false,
1469
+ env: {
1470
+ ...process.env,
1471
+ SRC: path.resolve('./src/WOrmPostgresql.mjs'),
1472
+ URL: opt.url,
1473
+ DB: opt.db,
1474
+ CL: opt.cl,
1475
+ MODE: mode,
1476
+ PAYLOAD: JSON.stringify(payload),
1477
+ TAG: tag,
1478
+ },
1479
+ })
1480
+ p.stdout.on('data', function(d) {
1481
+ out += d.toString()
1482
+ })
1483
+ p.stderr.on('data', function(d) {
1484
+ out += d.toString()
1485
+ })
1486
+ p.on('close', function() {
1487
+ let r = null
1488
+ try {
1489
+ r = JSON.parse(out.trim())
1490
+ }
1491
+ catch (err) {
1492
+ r = { tag, error: out.trim() }
1493
+ }
1494
+ resolve(r)
1495
+ })
1496
+ })
1497
+ }
1498
+
1499
+
1500
+ describe('cross-process concurrency', function() {
1501
+ let vans = {}
1502
+ let vget = {}
1503
+
1504
+ before(async function() {
1505
+ this.timeout(300000)
1506
+
1507
+ //跨行程insert, 兩行程各自對相同20個time插入, nInserted總和須為20且資料表僅20筆
1508
+ let clIns = 'xprocinsert'
1509
+ let optIns = genOpt(clIns)
1510
+ let woIns = WOrm(optIns)
1511
+ await woIns.createTable(clIns, 'time', {
1512
+ time: '2000-01-01T00:00:00Z',
1513
+ name: 'abc',
1514
+ value: 0.1,
1515
+ }).catch(() => {})
1516
+ await woIns.delAll()
1517
+
1518
+ let times = Array.from({ length: 20 }, (v, k) => {
1519
+ return `2025-06-01T00:${`${k}`.padStart(2, '0')}:00Z`
1520
+ })
1521
+ let rps = await Promise.all([
1522
+ runProc('p1', optIns, 'insert', times),
1523
+ runProc('p2', optIns, 'insert', times),
1524
+ ])
1525
+ vget[1] = rps.filter((v) => v.error !== undefined).length
1526
+ vget[2] = rps.reduce((sum, v) => sum + (v.nInserted || 0), 0)
1527
+ vget[3] = (await woIns.select()).length
1528
+
1529
+ //跨行程save對同一time之不同欄位, 兩行程各寫5欄, 10欄須全數保留
1530
+ let clSav = 'xprocsave'
1531
+ let optSav = genOpt(clSav)
1532
+ let woSav = WOrm(optSav)
1533
+ let vSample = { time: '2000-01-01T00:00:00Z' }
1534
+ Array.from({ length: 10 }, (v, k) => k).forEach((k) => {
1535
+ vSample[`f${k}`] = 'abc'
1536
+ })
1537
+ await woSav.createTable(clSav, 'time', vSample).catch(() => {})
1538
+ await woSav.delAll()
1539
+
1540
+ let tSav = '2025-06-02T00:00:00Z'
1541
+ let rpss = await Promise.all([
1542
+ runProc('p1', optSav, 'save', { time: tSav, fields: ['f0', 'f1', 'f2', 'f3', 'f4'] }),
1543
+ runProc('p2', optSav, 'save', { time: tSav, fields: ['f5', 'f6', 'f7', 'f8', 'f9'] }),
1544
+ ])
1545
+ vget[4] = rpss.filter((v) => v.error !== undefined).length
1546
+
1547
+ //兩行程對同一新time各自save, 恰有一方之nInserted為1
1548
+ vget[5] = rpss.reduce((sum, v) => sum + (v.nInserted || 0), 0)
1549
+
1550
+ //10欄須全數保留且值為各自行程之tag
1551
+ let vSav = await woSav.selectByPk(tSav)
1552
+ vget[6] = Array.from({ length: 10 }, (v, k) => k)
1553
+ .filter((k) => {
1554
+ let tag = k <= 4 ? 'p1' : 'p2'
1555
+ return vSav[`f${k}`] === tag
1556
+ }).length
1557
+
1558
+ })
1559
+
1560
+ vans[1] = 0
1561
+ it(`should get ${JSON.stringify(vans[1])} for count of errored process in cross-process insert`, async function() {
1562
+ assert.strict.deepStrictEqual(vget[1], vans[1])
1563
+ })
1564
+
1565
+ vans[2] = 20
1566
+ it(`should get ${JSON.stringify(vans[2])} for sum of nInserted by 2 processes inserting same 20 times`, async function() {
1567
+ assert.strict.deepStrictEqual(vget[2], vans[2])
1568
+ })
1569
+
1570
+ vans[3] = 20
1571
+ it(`should get ${JSON.stringify(vans[3])} for records after cross-process insert`, async function() {
1572
+ assert.strict.deepStrictEqual(vget[3], vans[3])
1573
+ })
1574
+
1575
+ vans[4] = 0
1576
+ it(`should get ${JSON.stringify(vans[4])} for count of errored process in cross-process save`, async function() {
1577
+ assert.strict.deepStrictEqual(vget[4], vans[4])
1578
+ })
1579
+
1580
+ vans[5] = 1
1581
+ it(`should get ${JSON.stringify(vans[5])} for sum of nInserted by 2 processes saving same new time`, async function() {
1582
+ assert.strict.deepStrictEqual(vget[5], vans[5])
1583
+ })
1584
+
1585
+ vans[6] = 10
1586
+ it(`should get ${JSON.stringify(vans[6])} for count of fields kept after cross-process save with different field`, async function() {
1587
+ assert.strict.deepStrictEqual(vget[6], vans[6])
1588
+ })
1589
+
1590
+ })