w-orm-postgresql 1.0.8 → 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.
- package/README.md +8 -8
- package/dist/w-orm-postgresql.umd.js +2 -2
- package/dist/w-orm-postgresql.umd.js.map +1 -1
- package/docs/WOrmPostgresql.html +34 -15
- package/docs/WOrmPostgresql.mjs.html +70 -22
- package/docs/index.html +1 -1
- package/g-basic.mjs +8 -8
- package/package.json +1 -1
- package/src/WOrmPostgresql.mjs +69 -21
- package/test/api-postgresql.test.mjs +347 -12
package/src/WOrmPostgresql.mjs
CHANGED
|
@@ -370,6 +370,10 @@ function WOrmPostgresql(opt = {}) {
|
|
|
370
370
|
/**
|
|
371
371
|
* 由主鍵time查詢單筆數據,因直接由資料表主鍵索引取值,不需如select提取數據至前端再處理,故數據量大時效能較佳
|
|
372
372
|
*
|
|
373
|
+
* 函數名依主鍵欄位命名為selectByTime而不另掛selectById別名,因本套件之主鍵為具業務語義之time,
|
|
374
|
+
* 與以無語義識別碼為主鍵者性質不同,該差異須由呼叫端知悉而非由別名掩蓋。
|
|
375
|
+
* 主鍵未命中或主鍵值無效皆回傳null而不reject,命中之判定基準與insert、save、del內對既有數據之認定一致。
|
|
376
|
+
*
|
|
373
377
|
* @memberOf WOrmPostgresql
|
|
374
378
|
* @param {String|Date} time 輸入主鍵time值,本套件以time欄位為主鍵
|
|
375
379
|
* @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此time則回傳null,reject回傳錯誤訊息
|
|
@@ -480,11 +484,15 @@ function WOrmPostgresql(opt = {}) {
|
|
|
480
484
|
}
|
|
481
485
|
|
|
482
486
|
/**
|
|
483
|
-
*
|
|
487
|
+
* 插入數據,僅於主鍵time不存在時寫入,已存在者跳過且不覆寫
|
|
488
|
+
*
|
|
489
|
+
* n為輸入筆數,即本次嘗試插入之基準;nInserted為實際插入筆數,全數已存在而nInserted為0屬正常結果,
|
|
490
|
+
* 不視為錯誤。同批含重複time時僅首筆計入nInserted。
|
|
491
|
+
* 因主鍵time承載觀測時間之業務意義,未帶有效time時不自動補值,一律以reject回報。
|
|
484
492
|
*
|
|
485
493
|
* @memberOf WOrmPostgresql
|
|
486
494
|
* @param {Object|Array} data 輸入數據物件或陣列
|
|
487
|
-
* @returns {Promise} 回傳Promise,resolve
|
|
495
|
+
* @returns {Promise} 回傳Promise,resolve回傳插入結果物件{n,nInserted,ok},reject回傳錯誤訊息
|
|
488
496
|
*/
|
|
489
497
|
async function insert(data) {
|
|
490
498
|
let isErr = false
|
|
@@ -607,13 +615,21 @@ function WOrmPostgresql(opt = {}) {
|
|
|
607
615
|
}
|
|
608
616
|
|
|
609
617
|
/**
|
|
610
|
-
*
|
|
618
|
+
* 儲存數據,以主鍵time為準更新既有數據,未給之欄位保留
|
|
619
|
+
*
|
|
620
|
+
* 回傳陣列恆與輸入等長,輸入單一物件亦回傳長度1之陣列。
|
|
621
|
+
* 各筆之n為主鍵命中筆數,值為0或1,命中(不論內容有無變更)或經插入而產生皆為1;
|
|
622
|
+
* nInserted與nModified恆同時出現,無對應行為時填0。
|
|
623
|
+
* [內容相同]之判定基準為待寫入物件合併進現值後與現值相同,非待寫入物件與現值全等,
|
|
624
|
+
* 故只給部份欄位且該些欄位值皆與現值相同時,合併結果等於現值,nModified為0,
|
|
625
|
+
* 令nModified忠實反映資料庫端是否真的寫入。
|
|
626
|
+
* 因主鍵time承載觀測時間之業務意義,未帶有效time時不自動補值,一律以reject回報。
|
|
611
627
|
*
|
|
612
628
|
* @memberOf WOrmPostgresql
|
|
613
629
|
* @param {Object|Array} data 輸入數據物件或陣列
|
|
614
630
|
* @param {Object} [option={}] 輸入設定物件,預設為{}
|
|
615
631
|
* @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
|
|
616
|
-
* @returns {Promise} 回傳Promise,resolve
|
|
632
|
+
* @returns {Promise} 回傳Promise,resolve回傳儲存結果陣列[{n,nInserted,nModified,ok}],reject回傳錯誤訊息
|
|
617
633
|
*/
|
|
618
634
|
async function save(data, option = {}) {
|
|
619
635
|
let isErr = false
|
|
@@ -713,16 +729,22 @@ function WOrmPostgresql(opt = {}) {
|
|
|
713
729
|
let _vt = omit(_v, 'time')
|
|
714
730
|
// console.log('_vt', _vt)
|
|
715
731
|
|
|
716
|
-
//vt
|
|
717
|
-
|
|
732
|
+
//vt, [內容相同]之判定基準為[待寫入物件合併進現值後與現值相同],
|
|
733
|
+
//非[待寫入物件與現值全等], 故只給部份欄位且該些欄位值皆與現值相同時視為相同而不寫入,
|
|
734
|
+
//合併採淺層覆蓋, 與後端以EXCLUDED整欄取代之寫入行為一致
|
|
735
|
+
let vt = {
|
|
736
|
+
..._vt,
|
|
737
|
+
...omit(v, 'time'),
|
|
738
|
+
}
|
|
718
739
|
// console.log('vt', vt)
|
|
719
740
|
|
|
720
|
-
if (isEqual(
|
|
741
|
+
if (isEqual(vt, _vt)) {
|
|
721
742
|
//相同時不更新
|
|
722
743
|
|
|
723
744
|
//rest
|
|
724
745
|
rest = {
|
|
725
746
|
n: 1,
|
|
747
|
+
nInserted: 0,
|
|
726
748
|
nModified: 0,
|
|
727
749
|
ok: 1,
|
|
728
750
|
}
|
|
@@ -741,6 +763,7 @@ function WOrmPostgresql(opt = {}) {
|
|
|
741
763
|
//rest
|
|
742
764
|
rest = {
|
|
743
765
|
n: 0,
|
|
766
|
+
nInserted: 0,
|
|
744
767
|
nModified: 0,
|
|
745
768
|
ok: 1,
|
|
746
769
|
}
|
|
@@ -791,18 +814,20 @@ function WOrmPostgresql(opt = {}) {
|
|
|
791
814
|
//rest
|
|
792
815
|
rest = {
|
|
793
816
|
n: 1,
|
|
817
|
+
nInserted: 0,
|
|
794
818
|
nModified: 0,
|
|
795
819
|
ok: 1,
|
|
796
820
|
}
|
|
797
821
|
|
|
798
822
|
}
|
|
799
823
|
else if (get(rows, '0.inserted') === true) {
|
|
800
|
-
|
|
824
|
+
//原不存在而插入
|
|
801
825
|
|
|
802
826
|
//rest
|
|
803
827
|
rest = {
|
|
804
828
|
n: 1,
|
|
805
829
|
nInserted: 1,
|
|
830
|
+
nModified: 0,
|
|
806
831
|
ok: 1,
|
|
807
832
|
}
|
|
808
833
|
// console.log('之前不存在且可自動插入', rest, v)
|
|
@@ -814,6 +839,7 @@ function WOrmPostgresql(opt = {}) {
|
|
|
814
839
|
//rest
|
|
815
840
|
rest = {
|
|
816
841
|
n: 1,
|
|
842
|
+
nInserted: 0,
|
|
817
843
|
nModified: 1,
|
|
818
844
|
ok: 1,
|
|
819
845
|
}
|
|
@@ -828,6 +854,7 @@ function WOrmPostgresql(opt = {}) {
|
|
|
828
854
|
rest = {
|
|
829
855
|
n: 1,
|
|
830
856
|
nInserted: 0,
|
|
857
|
+
nModified: 0,
|
|
831
858
|
ok: 0,
|
|
832
859
|
err: err.message,
|
|
833
860
|
}
|
|
@@ -863,6 +890,7 @@ function WOrmPostgresql(opt = {}) {
|
|
|
863
890
|
//rest
|
|
864
891
|
rest = {
|
|
865
892
|
n: 1,
|
|
893
|
+
nInserted: 0,
|
|
866
894
|
nModified: 1,
|
|
867
895
|
ok: 1,
|
|
868
896
|
}
|
|
@@ -875,6 +903,7 @@ function WOrmPostgresql(opt = {}) {
|
|
|
875
903
|
//rest
|
|
876
904
|
rest = {
|
|
877
905
|
n: 0,
|
|
906
|
+
nInserted: 0,
|
|
878
907
|
nModified: 0,
|
|
879
908
|
ok: 1,
|
|
880
909
|
}
|
|
@@ -887,6 +916,7 @@ function WOrmPostgresql(opt = {}) {
|
|
|
887
916
|
//rest
|
|
888
917
|
rest = {
|
|
889
918
|
n: 1,
|
|
919
|
+
nInserted: 0,
|
|
890
920
|
nModified: 0,
|
|
891
921
|
ok: 0,
|
|
892
922
|
err: err.message,
|
|
@@ -935,9 +965,14 @@ function WOrmPostgresql(opt = {}) {
|
|
|
935
965
|
/**
|
|
936
966
|
* 刪除數據
|
|
937
967
|
*
|
|
968
|
+
* 回傳陣列恆與輸入等長,各筆之n與nDeleted皆為主鍵命中筆數,值為0或1,未命中為0且屬正常結果。
|
|
969
|
+
* 判斷某筆是否真的被刪除一律以nDeleted為準。
|
|
970
|
+
* 未帶有效time者為該筆之輸入問題,回ok:0與err且不送查詢,不中斷其餘筆數,
|
|
971
|
+
* 以此與[主鍵未命中]之ok:1區辨。
|
|
972
|
+
*
|
|
938
973
|
* @memberOf WOrmPostgresql
|
|
939
974
|
* @param {Object|Array} data 輸入數據物件或陣列
|
|
940
|
-
* @returns {Promise} 回傳Promise,resolve
|
|
975
|
+
* @returns {Promise} 回傳Promise,resolve回傳刪除結果陣列[{n,nDeleted,ok}],reject回傳錯誤訊息
|
|
941
976
|
*/
|
|
942
977
|
async function del(data) {
|
|
943
978
|
let isErr = false
|
|
@@ -983,39 +1018,49 @@ function WOrmPostgresql(opt = {}) {
|
|
|
983
1018
|
data = [data]
|
|
984
1019
|
}
|
|
985
1020
|
|
|
986
|
-
//check time
|
|
987
|
-
data = map(data, function(v, k) {
|
|
988
|
-
if (!isDate(v.time)) {
|
|
989
|
-
throw new Error(`invalid data[${k}].time[${v.time}]`)
|
|
990
|
-
}
|
|
991
|
-
return v
|
|
992
|
-
})
|
|
993
|
-
|
|
994
1021
|
//pmSeries
|
|
995
1022
|
res = await pmSeries(data, async(v) => {
|
|
996
1023
|
|
|
997
1024
|
//rest
|
|
998
1025
|
let rest = null
|
|
999
1026
|
|
|
1027
|
+
//time
|
|
1028
|
+
let time = get(v, 'time')
|
|
1029
|
+
|
|
1030
|
+
//check time, 未帶有效主鍵者不補值亦不送查詢,
|
|
1031
|
+
//因mongo-sql會將無效值轉為null而誤中其他數據, 故直接視為該筆無法處理
|
|
1032
|
+
if (!isDate(time)) {
|
|
1033
|
+
|
|
1034
|
+
//rest
|
|
1035
|
+
rest = {
|
|
1036
|
+
n: 0,
|
|
1037
|
+
nDeleted: 0,
|
|
1038
|
+
ok: 0,
|
|
1039
|
+
err: `invalid time[${time}]`,
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return rest
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1000
1045
|
//mr
|
|
1001
1046
|
let mr = mongoSql.sql({
|
|
1002
1047
|
type: 'delete',
|
|
1003
1048
|
table: cl,
|
|
1004
1049
|
where: {
|
|
1005
|
-
time
|
|
1050
|
+
time,
|
|
1006
1051
|
},
|
|
1007
1052
|
})
|
|
1008
1053
|
// console.log('mr', mr)
|
|
1009
1054
|
// console.log('mr.query', mr.query)
|
|
1010
1055
|
// console.log('mr.values', mr.values)
|
|
1011
1056
|
|
|
1012
|
-
//del
|
|
1057
|
+
//del, n與nDeleted皆為主鍵命中筆數, 未命中為0
|
|
1013
1058
|
await client.query(mr.query, mr.values)
|
|
1014
1059
|
.then((r) => {
|
|
1015
1060
|
|
|
1016
1061
|
//res
|
|
1017
1062
|
rest = {
|
|
1018
|
-
n:
|
|
1063
|
+
n: r.rowCount,
|
|
1019
1064
|
nDeleted: r.rowCount,
|
|
1020
1065
|
ok: 1,
|
|
1021
1066
|
}
|
|
@@ -1072,9 +1117,12 @@ function WOrmPostgresql(opt = {}) {
|
|
|
1072
1117
|
/**
|
|
1073
1118
|
* 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
|
|
1074
1119
|
*
|
|
1120
|
+
* n與nDeleted皆為實際刪除筆數,不取全表筆數。find未給或為空物件時刪除全部數據,
|
|
1121
|
+
* 條件無命中時回{n:0,nDeleted:0,ok:1},不視為錯誤。
|
|
1122
|
+
*
|
|
1075
1123
|
* @memberOf WOrmPostgresql
|
|
1076
1124
|
* @param {Object} [find={}] 輸入刪除條件物件
|
|
1077
|
-
* @returns {Promise} 回傳Promise,resolve
|
|
1125
|
+
* @returns {Promise} 回傳Promise,resolve回傳刪除結果物件{n,nDeleted,ok},reject回傳錯誤訊息
|
|
1078
1126
|
*/
|
|
1079
1127
|
async function delAll(find = {}) {
|
|
1080
1128
|
let isErr = false
|
|
@@ -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
|
|
|
@@ -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])
|
|
@@ -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:
|
|
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
|
})
|
|
@@ -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,336 @@ 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.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
|
+
})
|