w-orm-postgresql 1.0.11 → 1.0.12
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 +56 -0
- package/dist/w-orm-postgresql.umd.js +2 -2
- package/dist/w-orm-postgresql.umd.js.map +1 -1
- package/docs/WOrmPostgresql.html +177 -10
- package/docs/WOrmPostgresql.mjs.html +200 -31
- package/docs/index.html +2 -2
- package/g-basic.mjs +56 -0
- package/package.json +1 -1
- package/src/WOrmPostgresql.mjs +198 -29
- package/test/api-postgresql.test.mjs +260 -0
package/g-basic.mjs
CHANGED
|
@@ -110,6 +110,19 @@ let rsa = [
|
|
|
110
110
|
},
|
|
111
111
|
]
|
|
112
112
|
|
|
113
|
+
let rsb = [
|
|
114
|
+
{
|
|
115
|
+
time: '2025-01-01T00:20:00Z',
|
|
116
|
+
name: 'bulk-1',
|
|
117
|
+
value: 201,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
time: '2025-01-01T00:21:00Z',
|
|
121
|
+
name: 'bulk-2',
|
|
122
|
+
value: 202,
|
|
123
|
+
},
|
|
124
|
+
]
|
|
125
|
+
|
|
113
126
|
async function test() {
|
|
114
127
|
|
|
115
128
|
//wo
|
|
@@ -132,6 +145,9 @@ async function test() {
|
|
|
132
145
|
wo.on('change', function(mode, data, res) {
|
|
133
146
|
console.log('change', mode)
|
|
134
147
|
})
|
|
148
|
+
wo.on('error', function(mode, data, err) {
|
|
149
|
+
console.log('error', mode, err)
|
|
150
|
+
})
|
|
135
151
|
|
|
136
152
|
//delAll
|
|
137
153
|
await wo.delAll()
|
|
@@ -151,6 +167,24 @@ async function test() {
|
|
|
151
167
|
console.log('insert catch', msg)
|
|
152
168
|
})
|
|
153
169
|
|
|
170
|
+
//insertBulk, 全批視為一個單位, 全部插入成功或一筆都不寫入
|
|
171
|
+
await wo.insertBulk(rsb)
|
|
172
|
+
.then(function(msg) {
|
|
173
|
+
console.log('insertBulk then', msg)
|
|
174
|
+
})
|
|
175
|
+
.catch(function(msg) {
|
|
176
|
+
console.log('insertBulk catch', msg)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
//insertBulk by pk existed, 任一筆之主鍵已存在即整批reject且不寫入任何一筆
|
|
180
|
+
await wo.insertBulk(rsb)
|
|
181
|
+
.then(function(msg) {
|
|
182
|
+
console.log('insertBulk by pk existed then', msg)
|
|
183
|
+
})
|
|
184
|
+
.catch(function(msg) {
|
|
185
|
+
console.log('insertBulk by pk existed catch', `${msg}`) //reject回傳Error物件, 以樣板字串取其訊息以免印出堆疊
|
|
186
|
+
})
|
|
187
|
+
|
|
154
188
|
//save
|
|
155
189
|
await wo.save(rsm, { autoInsert: false })
|
|
156
190
|
.then(function(msg) {
|
|
@@ -213,6 +247,15 @@ async function test() {
|
|
|
213
247
|
console.log('del catch', msg)
|
|
214
248
|
})
|
|
215
249
|
|
|
250
|
+
//del by data without pk, 該筆無法處理故ok為0並附err, 整批仍resolve且另發出error事件
|
|
251
|
+
await wo.del({ name: 'no-pk' })
|
|
252
|
+
.then(function(msg) {
|
|
253
|
+
console.log('del by data without pk then', msg)
|
|
254
|
+
})
|
|
255
|
+
.catch(function(msg) {
|
|
256
|
+
console.log('del by data without pk catch', msg)
|
|
257
|
+
})
|
|
258
|
+
|
|
216
259
|
//del
|
|
217
260
|
let ssDel = await wo.select()
|
|
218
261
|
let ds = ssDel.filter(function(v) {
|
|
@@ -298,6 +341,10 @@ test()
|
|
|
298
341
|
// delAll then { n: 0, nDeleted: 0, ok: 1 }
|
|
299
342
|
// change insert
|
|
300
343
|
// insert then { n: 13, nInserted: 13, ok: 1 }
|
|
344
|
+
// change insertBulk
|
|
345
|
+
// insertBulk then { n: 2, nInserted: 2, ok: 1 }
|
|
346
|
+
// error insertBulk duplicate key value violates unique constraint "users_pkey"
|
|
347
|
+
// insertBulk by pk existed catch error: duplicate key value violates unique constraint "users_pkey"
|
|
301
348
|
// change save
|
|
302
349
|
// save then [
|
|
303
350
|
// { n: 1, nInserted: 0, nModified: 0, ok: 1 },
|
|
@@ -317,6 +364,8 @@ test()
|
|
|
317
364
|
// { time: 2025-01-01T00:07:00.000Z, name: 'rosemary', value: 124.76 },
|
|
318
365
|
// { time: 2025-01-01T00:08:00.000Z, name: 'kettle', value: 524 },
|
|
319
366
|
// { time: 2025-01-01T00:09:00.000Z, name: 'peter', value: 127 },
|
|
367
|
+
// { time: 2025-01-01T00:20:00.000Z, name: 'bulk-1', value: 201 },
|
|
368
|
+
// { time: 2025-01-01T00:21:00.000Z, name: 'bulk-2', value: 202 },
|
|
320
369
|
// {
|
|
321
370
|
// time: 2025-01-01T00:10:00.000Z,
|
|
322
371
|
// name: 'rosemary(modify)',
|
|
@@ -352,6 +401,8 @@ test()
|
|
|
352
401
|
// { time: 2025-01-01T00:03:00.000Z, name: 'peter', value: 200 },
|
|
353
402
|
// { time: 2025-01-01T00:05:00.000Z, name: 'kettle', value: 488 },
|
|
354
403
|
// { time: 2025-01-01T00:08:00.000Z, name: 'kettle', value: 524 },
|
|
404
|
+
// { time: 2025-01-01T00:20:00.000Z, name: 'bulk-1', value: 201 },
|
|
405
|
+
// { time: 2025-01-01T00:21:00.000Z, name: 'bulk-2', value: 202 },
|
|
355
406
|
// {
|
|
356
407
|
// time: 2025-01-01T00:11:00.000Z,
|
|
357
408
|
// name: 'kettle(modify)',
|
|
@@ -374,6 +425,9 @@ test()
|
|
|
374
425
|
// save then [ { n: 1, nInserted: 1, nModified: 0, ok: 1 } ]
|
|
375
426
|
// change del
|
|
376
427
|
// del then [ { n: 0, nDeleted: 0, ok: 1 } ]
|
|
428
|
+
// error del invalid time[undefined]
|
|
429
|
+
// change del
|
|
430
|
+
// del by data without pk then [ { n: 0, nDeleted: 0, ok: 0, err: 'invalid time[undefined]' } ]
|
|
377
431
|
// change del
|
|
378
432
|
// del then [
|
|
379
433
|
// { n: 1, nDeleted: 1, ok: 1 },
|
|
@@ -391,6 +445,8 @@ test()
|
|
|
391
445
|
// { time: 2025-01-01T00:01:00.000Z, name: 'rosemary', value: 123.456 },
|
|
392
446
|
// { time: 2025-01-01T00:04:00.000Z, name: 'rosemary', value: 123.1236 },
|
|
393
447
|
// { time: 2025-01-01T00:07:00.000Z, name: 'rosemary', value: 124.76 },
|
|
448
|
+
// { time: 2025-01-01T00:20:00.000Z, name: 'bulk-1', value: 201 },
|
|
449
|
+
// { time: 2025-01-01T00:21:00.000Z, name: 'bulk-2', value: 202 },
|
|
394
450
|
// {
|
|
395
451
|
// time: 2025-01-01T00:10:00.000Z,
|
|
396
452
|
// name: 'rosemary(modify)',
|
package/package.json
CHANGED
package/src/WOrmPostgresql.mjs
CHANGED
|
@@ -245,6 +245,77 @@ function WOrmPostgresql(opt = {}) {
|
|
|
245
245
|
_cache[key] = cloneDeep(data) //與外部使用數據脫勾
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
//insertBatches, 分批送出插入語句並以交易包覆, 回傳實際插入筆數, 供insert與insertBulk共用
|
|
249
|
+
//PostgreSQL之協定以int16記綁定參數個數故上限為65535, 單一語句可送之筆數受欄位數所限,
|
|
250
|
+
//超出者須分批送出; 分批後單語句之原子性已不足以維持[失敗即不留下部份寫入], 故一律以交易包覆,
|
|
251
|
+
//令單批與分批之保證來源一致而不因批數而異
|
|
252
|
+
//兩函數之衝突政策由useConflict區分, 語句各自獨立:
|
|
253
|
+
// true : 添加ON CONFLICT DO NOTHING, 已存在主鍵者跳過而不中斷整批, 為insert之語義
|
|
254
|
+
// false: 不添加, 任一筆撞主鍵之唯一約束即整句失敗且不寫入任何一筆, 為insertBulk之全有全無語義
|
|
255
|
+
let insertBatches = async (client, data, useConflict) => {
|
|
256
|
+
|
|
257
|
+
//nCols, 取各筆欄位數之最大值
|
|
258
|
+
let nCols = 1
|
|
259
|
+
data.forEach(function(v) {
|
|
260
|
+
nCols = Math.max(nCols, size(keys(v)))
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
//nPerBatch
|
|
264
|
+
let nPerBatch = Math.max(1, Math.floor(65535 / nCols))
|
|
265
|
+
|
|
266
|
+
//ltdata, 分批
|
|
267
|
+
let ltdata = []
|
|
268
|
+
for (let i = 0; i < size(data); i += nPerBatch) {
|
|
269
|
+
ltdata.push(data.slice(i, i + nPerBatch))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
//transaction
|
|
273
|
+
await client.query('BEGIN')
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
|
|
277
|
+
//nIns
|
|
278
|
+
let nIns = 0
|
|
279
|
+
|
|
280
|
+
//each batch
|
|
281
|
+
for (let dt of ltdata) {
|
|
282
|
+
|
|
283
|
+
//mr
|
|
284
|
+
let mr = mongoSql.sql({
|
|
285
|
+
type: 'insert',
|
|
286
|
+
table: cl,
|
|
287
|
+
values: dt,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
//sql
|
|
291
|
+
let sql = mr.query
|
|
292
|
+
if (useConflict) {
|
|
293
|
+
sql = `${sql} ON CONFLICT (${pkName}) DO NOTHING`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
//insert
|
|
297
|
+
let r = await client.query(sql, mr.values)
|
|
298
|
+
|
|
299
|
+
//nIns
|
|
300
|
+
nIns += r.rowCount
|
|
301
|
+
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
//commit
|
|
305
|
+
await client.query('COMMIT')
|
|
306
|
+
|
|
307
|
+
return nIns
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
|
|
311
|
+
//rollback, 令已送出之批次一併回滾
|
|
312
|
+
await client.query('ROLLBACK').catch(() => {})
|
|
313
|
+
|
|
314
|
+
throw err
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
}
|
|
318
|
+
|
|
248
319
|
/**
|
|
249
320
|
* 創建資料表
|
|
250
321
|
* 註: pk未給時採建構時之opt.pk。因insert與save倚賴主鍵之唯一約束達成原子性,
|
|
@@ -648,40 +719,137 @@ function WOrmPostgresql(opt = {}) {
|
|
|
648
719
|
return v
|
|
649
720
|
})
|
|
650
721
|
|
|
651
|
-
//
|
|
652
|
-
let
|
|
653
|
-
type: 'insert',
|
|
654
|
-
table: cl,
|
|
655
|
-
values: data,
|
|
656
|
-
})
|
|
657
|
-
// console.log('mr', mr)
|
|
658
|
-
// console.log('mr.query', mr.query)
|
|
659
|
-
// console.log('mr.values', mr.values)
|
|
722
|
+
//nAll
|
|
723
|
+
let nAll = size(data)
|
|
660
724
|
|
|
661
|
-
|
|
725
|
+
//insert, 以ON CONFLICT DO NOTHING令已存在主鍵者跳過而不中斷整批插入,
|
|
662
726
|
//由PostgreSQL於單一語句內原子完成[檢查主鍵未存在]與[寫入], 併發時同一主鍵僅有一次成功,
|
|
663
727
|
//同批含重複主鍵時亦僅首筆成功, 故不須逐筆插入即可取得實際插入筆數
|
|
664
|
-
|
|
728
|
+
//筆數過多而超出綁定參數上限者由insertBatches分批送出, 主鍵認定與genConflictSQL一致
|
|
729
|
+
let nIns = await insertBatches(client, data, true)
|
|
730
|
+
|
|
731
|
+
//res
|
|
732
|
+
res = {
|
|
733
|
+
n: nAll,
|
|
734
|
+
nInserted: nIns,
|
|
735
|
+
ok: 1,
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
isErr = true
|
|
741
|
+
res = err
|
|
742
|
+
}
|
|
743
|
+
finally {
|
|
744
|
+
await client.end()
|
|
745
|
+
client = null
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
//update
|
|
749
|
+
if (useCache) {
|
|
750
|
+
clearCache()
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
//emit
|
|
754
|
+
if (!isErr) {
|
|
755
|
+
emitChange('insert', data, res)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
//check
|
|
759
|
+
if (isErr) {
|
|
760
|
+
|
|
761
|
+
//emit, 整批性錯誤於reject之前發出
|
|
762
|
+
emitError('insert', data, res)
|
|
763
|
+
|
|
764
|
+
return Promise.reject(res)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return res
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* 批次插入數據,全批視為一個單位,全部插入成功或一筆都不寫入
|
|
772
|
+
*
|
|
773
|
+
* 本函數非insert之加速版,兩者衝突政策不同:insert於主鍵已存在時跳過該筆且整批ok為1,
|
|
774
|
+
* 本函數則於任一筆之主鍵已存在時整批reject且不寫入任何一筆,同批含重複主鍵者亦視為衝突。
|
|
775
|
+
* n為輸入筆數,nInserted為實際插入筆數且成功時恆等於n。
|
|
776
|
+
* 不提供逐筆結果,故不出現ok:0與err,需要逐筆處置者改用insert。
|
|
777
|
+
* 因主鍵得承載業務語義,未帶有效主鍵值時不自動補值,一律以reject回報,
|
|
778
|
+
* 且主鍵檢查於任何寫入之前一次完成,故整批reject時同批之有效筆數亦不會被寫入。
|
|
779
|
+
*
|
|
780
|
+
* @memberOf WOrmPostgresql
|
|
781
|
+
* @param {Object|Array} data 輸入數據物件或陣列
|
|
782
|
+
* @returns {Promise} 回傳Promise,resolve回傳插入結果物件{n,nInserted,ok},reject回傳錯誤訊息
|
|
783
|
+
*/
|
|
784
|
+
async function insertBulk(data) {
|
|
785
|
+
let isErr = false
|
|
786
|
+
let res = null
|
|
787
|
+
|
|
788
|
+
//check
|
|
789
|
+
if (!iseobj(data) && !isearr(data)) {
|
|
790
|
+
return {
|
|
791
|
+
n: 0,
|
|
792
|
+
nInserted: 0,
|
|
793
|
+
ok: 1,
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
//cloneDeep
|
|
798
|
+
data = cloneDeep(data)
|
|
799
|
+
|
|
800
|
+
//client
|
|
801
|
+
let client = new PgClient({ connectionString })
|
|
802
|
+
|
|
803
|
+
//connect
|
|
804
|
+
try {
|
|
805
|
+
|
|
806
|
+
//connect
|
|
807
|
+
await client.connect()
|
|
808
|
+
|
|
809
|
+
}
|
|
810
|
+
catch (err) {
|
|
811
|
+
isErr = true
|
|
812
|
+
res = err
|
|
813
|
+
client = null
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
//check
|
|
817
|
+
if (isErr) {
|
|
818
|
+
|
|
819
|
+
//emit, 整批性錯誤於reject之前發出
|
|
820
|
+
emitError('insertBulk', data, res)
|
|
821
|
+
|
|
822
|
+
return Promise.reject(res)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
try {
|
|
826
|
+
|
|
827
|
+
//check
|
|
828
|
+
if (!isarr(data)) {
|
|
829
|
+
data = [data]
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
//check pk, 因主鍵得承載業務語義故不自動補值, 未帶有效主鍵值者屬整批性錯誤
|
|
833
|
+
data = map(data, function(v, k) {
|
|
834
|
+
if (!checkPk(get(v, pkName))) {
|
|
835
|
+
throw new Error(`invalid data[${k}].${pkName}[${get(v, pkName)}]`)
|
|
836
|
+
}
|
|
837
|
+
return v
|
|
838
|
+
})
|
|
665
839
|
|
|
666
840
|
//nAll
|
|
667
841
|
let nAll = size(data)
|
|
668
842
|
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
//res
|
|
674
|
-
res = {
|
|
675
|
-
n: nAll,
|
|
676
|
-
nInserted: r.rowCount,
|
|
677
|
-
ok: 1,
|
|
678
|
-
}
|
|
843
|
+
//insertBulk, 不添加conflict, 令任一筆撞主鍵之唯一約束即整句失敗且不寫入任何一筆,
|
|
844
|
+
//同批含重複主鍵者亦於此被偵測為衝突; 筆數過多者由insertBatches分批送出並以交易維持全有全無
|
|
845
|
+
let nIns = await insertBatches(client, data, false)
|
|
679
846
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
847
|
+
//res
|
|
848
|
+
res = {
|
|
849
|
+
n: nAll,
|
|
850
|
+
nInserted: nIns,
|
|
851
|
+
ok: 1,
|
|
852
|
+
}
|
|
685
853
|
|
|
686
854
|
}
|
|
687
855
|
catch (err) {
|
|
@@ -700,14 +868,14 @@ function WOrmPostgresql(opt = {}) {
|
|
|
700
868
|
|
|
701
869
|
//emit
|
|
702
870
|
if (!isErr) {
|
|
703
|
-
emitChange('
|
|
871
|
+
emitChange('insertBulk', data, res)
|
|
704
872
|
}
|
|
705
873
|
|
|
706
874
|
//check
|
|
707
875
|
if (isErr) {
|
|
708
876
|
|
|
709
877
|
//emit, 整批性錯誤於reject之前發出
|
|
710
|
-
emitError('
|
|
878
|
+
emitError('insertBulk', data, res)
|
|
711
879
|
|
|
712
880
|
return Promise.reject(res)
|
|
713
881
|
}
|
|
@@ -716,7 +884,7 @@ function WOrmPostgresql(opt = {}) {
|
|
|
716
884
|
}
|
|
717
885
|
|
|
718
886
|
/**
|
|
719
|
-
*
|
|
887
|
+
* 儲存數據,以主鍵為準更新既有數據,未給之欄位保留
|
|
720
888
|
*
|
|
721
889
|
* 回傳陣列恆與輸入等長,輸入單一物件亦回傳長度1之陣列。
|
|
722
890
|
* 各筆之n為主鍵命中筆數,值為0或1,命中(不論內容有無變更)或經插入而產生皆為1;
|
|
@@ -1361,6 +1529,7 @@ function WOrmPostgresql(opt = {}) {
|
|
|
1361
1529
|
ee.select = select
|
|
1362
1530
|
ee.selectByPk = selectByPk
|
|
1363
1531
|
ee.insert = insert
|
|
1532
|
+
ee.insertBulk = insertBulk
|
|
1364
1533
|
ee.save = save
|
|
1365
1534
|
ee.del = del
|
|
1366
1535
|
ee.delAll = delAll
|
|
@@ -957,6 +957,32 @@ describe('insert nInserted', function() {
|
|
|
957
957
|
let vdup = await wo.selectByPk('2025-02-01T00:05:00Z')
|
|
958
958
|
vget[8] = vdup.name
|
|
959
959
|
|
|
960
|
+
//跨批次之插入
|
|
961
|
+
//欄位數愈多則單一語句可送之筆數愈少, 以21欄之資料表令每批上限為3120筆, 送3200筆即須分為2批
|
|
962
|
+
//其中既有主鍵者跳過而其餘照插, 與insertBulk之全有全無不同
|
|
963
|
+
let clBat = 'insertbatch'
|
|
964
|
+
let woBat = WOrm(genOpt(clBat))
|
|
965
|
+
let vSample = { time: '2000-01-01T00:00:00Z' }
|
|
966
|
+
Array.from({ length: 20 }, (v, k) => k).forEach((k) => {
|
|
967
|
+
vSample[`f${k}`] = 'abc'
|
|
968
|
+
})
|
|
969
|
+
await woBat.createTable(clBat, null, vSample).catch(() => {})
|
|
970
|
+
await woBat.delAll()
|
|
971
|
+
|
|
972
|
+
//genRec
|
|
973
|
+
let genRec = (k) => {
|
|
974
|
+
let v = { time: new Date(Date.UTC(2025, 8, 1, 0, 0, 0) + k * 1000).toISOString() }
|
|
975
|
+
Array.from({ length: 20 }, (v2, k2) => k2).forEach((k2) => {
|
|
976
|
+
v[`f${k2}`] = `v${k}-${k2}`
|
|
977
|
+
})
|
|
978
|
+
return v
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
//先插入末筆之主鍵, 令後續之大批次於第2批撞上該筆
|
|
982
|
+
await woBat.insert(genRec(3199))
|
|
983
|
+
vget[9] = await woBat.insert(Array.from({ length: 3200 }, (v, k) => genRec(k)))
|
|
984
|
+
vget[10] = (await woBat.select()).length
|
|
985
|
+
|
|
960
986
|
})
|
|
961
987
|
|
|
962
988
|
vans[1] = { n: 3, nInserted: 3, ok: 1 }
|
|
@@ -999,6 +1025,16 @@ describe('insert nInserted', function() {
|
|
|
999
1025
|
assert.strict.deepStrictEqual(vget[8], vans[8])
|
|
1000
1026
|
})
|
|
1001
1027
|
|
|
1028
|
+
vans[9] = { n: 3200, nInserted: 3199, ok: 1 }
|
|
1029
|
+
it(`should get ${JSON.stringify(vans[9])} for insert across batches with 1 pk existed`, async function() {
|
|
1030
|
+
assert.strict.deepStrictEqual(vget[9], vans[9])
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
vans[10] = 3200
|
|
1034
|
+
it(`should get ${JSON.stringify(vans[10])} for records after insert across batches`, async function() {
|
|
1035
|
+
assert.strict.deepStrictEqual(vget[10], vans[10])
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1002
1038
|
})
|
|
1003
1039
|
|
|
1004
1040
|
|
|
@@ -1674,6 +1710,230 @@ describe('events', function() {
|
|
|
1674
1710
|
})
|
|
1675
1711
|
|
|
1676
1712
|
|
|
1713
|
+
describe('insertBulk', function() {
|
|
1714
|
+
let vans = {}
|
|
1715
|
+
let vget = {}
|
|
1716
|
+
|
|
1717
|
+
before(async function() {
|
|
1718
|
+
this.timeout(300000)
|
|
1719
|
+
|
|
1720
|
+
//wo
|
|
1721
|
+
let cl = 'bulk'
|
|
1722
|
+
let wo = WOrm(genOpt(cl))
|
|
1723
|
+
|
|
1724
|
+
//createTable
|
|
1725
|
+
await wo.createTable(cl, null, {
|
|
1726
|
+
time: '2000-01-01T00:00:00Z',
|
|
1727
|
+
name: 'abc',
|
|
1728
|
+
value: 0.1,
|
|
1729
|
+
}).catch(() => {})
|
|
1730
|
+
await wo.delAll()
|
|
1731
|
+
|
|
1732
|
+
//genTime, 依序產生不重複之主鍵值
|
|
1733
|
+
let genTime = (k) => {
|
|
1734
|
+
return new Date(Date.UTC(2025, 7, 1, 0, 0, 0) + k * 1000).toISOString()
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
//evChange, evError
|
|
1738
|
+
let evChange = []
|
|
1739
|
+
let evError = []
|
|
1740
|
+
wo.on('change', (mode) => {
|
|
1741
|
+
evChange.push(mode)
|
|
1742
|
+
})
|
|
1743
|
+
wo.on('error', (mode) => {
|
|
1744
|
+
evError.push(mode)
|
|
1745
|
+
})
|
|
1746
|
+
|
|
1747
|
+
//nRec, 取得目前筆數
|
|
1748
|
+
let nRec = async () => {
|
|
1749
|
+
return (await wo.select()).length
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
//無衝突, nInserted須等於n
|
|
1753
|
+
vget[1] = await wo.insertBulk([
|
|
1754
|
+
{ time: genTime(0), name: 'peter', value: 1 },
|
|
1755
|
+
{ time: genTime(1), name: 'rosemary', value: 2 },
|
|
1756
|
+
{ time: genTime(2), name: 'kettle', value: 3 },
|
|
1757
|
+
])
|
|
1758
|
+
vget[2] = await nRec()
|
|
1759
|
+
vget[3] = evChange.slice()
|
|
1760
|
+
|
|
1761
|
+
//撞既有主鍵, 整批reject且不寫入任何一筆
|
|
1762
|
+
evChange = []
|
|
1763
|
+
evError = []
|
|
1764
|
+
vget[4] = await wo.insertBulk([
|
|
1765
|
+
{ time: genTime(10), name: 'new-1', value: 11 },
|
|
1766
|
+
{ time: genTime(0), name: 'conflict', value: 12 },
|
|
1767
|
+
{ time: genTime(11), name: 'new-2', value: 13 },
|
|
1768
|
+
])
|
|
1769
|
+
.then(() => 'resolved')
|
|
1770
|
+
.catch(() => 'rejected')
|
|
1771
|
+
vget[5] = await nRec()
|
|
1772
|
+
vget[6] = evError.slice()
|
|
1773
|
+
vget[7] = evChange.slice()
|
|
1774
|
+
|
|
1775
|
+
//同批含重複主鍵, 亦視為衝突而整批reject
|
|
1776
|
+
evChange = []
|
|
1777
|
+
evError = []
|
|
1778
|
+
vget[8] = await wo.insertBulk([
|
|
1779
|
+
{ time: genTime(20), name: 'dup-1', value: 21 },
|
|
1780
|
+
{ time: genTime(20), name: 'dup-2', value: 22 },
|
|
1781
|
+
{ time: genTime(21), name: 'uniq', value: 23 },
|
|
1782
|
+
])
|
|
1783
|
+
.then(() => 'resolved')
|
|
1784
|
+
.catch(() => 'rejected')
|
|
1785
|
+
vget[9] = await nRec()
|
|
1786
|
+
|
|
1787
|
+
//未帶有效主鍵, 不補值而整批reject
|
|
1788
|
+
vget[10] = await wo.insertBulk([
|
|
1789
|
+
{ time: genTime(30), name: 'ok', value: 31 },
|
|
1790
|
+
{ name: 'no-pk', value: 32 },
|
|
1791
|
+
])
|
|
1792
|
+
.then(() => 'resolved')
|
|
1793
|
+
.catch(() => 'rejected')
|
|
1794
|
+
vget[11] = await nRec()
|
|
1795
|
+
|
|
1796
|
+
//輸入無效
|
|
1797
|
+
vget[12] = await wo.insertBulk(null)
|
|
1798
|
+
|
|
1799
|
+
//單一物件亦回傳單一物件
|
|
1800
|
+
vget[13] = await wo.insertBulk({ time: genTime(40), name: 'single', value: 41 })
|
|
1801
|
+
vget[14] = await nRec()
|
|
1802
|
+
|
|
1803
|
+
//跨批次之回滾
|
|
1804
|
+
//欄位數愈多則單一語句可送之筆數愈少, 以21欄之資料表令每批上限為3120筆,
|
|
1805
|
+
//送3200筆即須分為2批, 末筆撞既有主鍵時第1批已送出, 須由交易回滾方能維持全有全無
|
|
1806
|
+
let clBat = 'bulkbatch'
|
|
1807
|
+
let woBat = WOrm(genOpt(clBat))
|
|
1808
|
+
let vSample = { time: '2000-01-01T00:00:00Z' }
|
|
1809
|
+
Array.from({ length: 20 }, (v, k) => k).forEach((k) => {
|
|
1810
|
+
vSample[`f${k}`] = 'abc'
|
|
1811
|
+
})
|
|
1812
|
+
await woBat.createTable(clBat, null, vSample).catch(() => {})
|
|
1813
|
+
await woBat.delAll()
|
|
1814
|
+
|
|
1815
|
+
//genRec
|
|
1816
|
+
let genRec = (k) => {
|
|
1817
|
+
let v = { time: genTime(k) }
|
|
1818
|
+
Array.from({ length: 20 }, (v2, k2) => k2).forEach((k2) => {
|
|
1819
|
+
v[`f${k2}`] = `v${k}-${k2}`
|
|
1820
|
+
})
|
|
1821
|
+
return v
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
//先插入末筆之主鍵, 令後續之大批次於第2批撞上
|
|
1825
|
+
let kLast = 3199
|
|
1826
|
+
await woBat.insert(genRec(kLast))
|
|
1827
|
+
let nBefore = (await woBat.select()).length
|
|
1828
|
+
|
|
1829
|
+
//3200筆之批次, 末筆與既有衝突
|
|
1830
|
+
vget[15] = await woBat.insertBulk(Array.from({ length: 3200 }, (v, k) => genRec(k)))
|
|
1831
|
+
.then(() => 'resolved')
|
|
1832
|
+
.catch(() => 'rejected')
|
|
1833
|
+
|
|
1834
|
+
//失敗後資料表不得有任何新增
|
|
1835
|
+
vget[16] = (await woBat.select()).length - nBefore
|
|
1836
|
+
|
|
1837
|
+
//跨批次且無衝突者須全數寫入
|
|
1838
|
+
await woBat.delAll()
|
|
1839
|
+
vget[17] = await woBat.insertBulk(Array.from({ length: 3200 }, (v, k) => genRec(k)))
|
|
1840
|
+
vget[18] = (await woBat.select()).length
|
|
1841
|
+
|
|
1842
|
+
})
|
|
1843
|
+
|
|
1844
|
+
vans[1] = { n: 3, nInserted: 3, ok: 1 }
|
|
1845
|
+
it(`should get ${JSON.stringify(vans[1])} for insertBulk without conflict`, async function() {
|
|
1846
|
+
assert.strict.deepStrictEqual(vget[1], vans[1])
|
|
1847
|
+
})
|
|
1848
|
+
|
|
1849
|
+
vans[2] = 3
|
|
1850
|
+
it(`should get ${JSON.stringify(vans[2])} for records after insertBulk without conflict`, async function() {
|
|
1851
|
+
assert.strict.deepStrictEqual(vget[2], vans[2])
|
|
1852
|
+
})
|
|
1853
|
+
|
|
1854
|
+
vans[3] = ['insertBulk']
|
|
1855
|
+
it(`should get ${JSON.stringify(vans[3])} for change modes by insertBulk`, async function() {
|
|
1856
|
+
assert.strict.deepStrictEqual(vget[3], vans[3])
|
|
1857
|
+
})
|
|
1858
|
+
|
|
1859
|
+
vans[4] = 'rejected'
|
|
1860
|
+
it(`should get ${JSON.stringify(vans[4])} for insertBulk with pk existed`, async function() {
|
|
1861
|
+
assert.strict.deepStrictEqual(vget[4], vans[4])
|
|
1862
|
+
})
|
|
1863
|
+
|
|
1864
|
+
vans[5] = 3
|
|
1865
|
+
it(`should get ${JSON.stringify(vans[5])} for records after insertBulk with pk existed`, async function() {
|
|
1866
|
+
assert.strict.deepStrictEqual(vget[5], vans[5])
|
|
1867
|
+
})
|
|
1868
|
+
|
|
1869
|
+
vans[6] = ['insertBulk']
|
|
1870
|
+
it(`should get ${JSON.stringify(vans[6])} for error modes by insertBulk with pk existed`, async function() {
|
|
1871
|
+
assert.strict.deepStrictEqual(vget[6], vans[6])
|
|
1872
|
+
})
|
|
1873
|
+
|
|
1874
|
+
vans[7] = []
|
|
1875
|
+
it(`should get ${JSON.stringify(vans[7])} for change modes by insertBulk rejected`, async function() {
|
|
1876
|
+
assert.strict.deepStrictEqual(vget[7], vans[7])
|
|
1877
|
+
})
|
|
1878
|
+
|
|
1879
|
+
vans[8] = 'rejected'
|
|
1880
|
+
it(`should get ${JSON.stringify(vans[8])} for insertBulk with duplicated pk in same batch`, async function() {
|
|
1881
|
+
assert.strict.deepStrictEqual(vget[8], vans[8])
|
|
1882
|
+
})
|
|
1883
|
+
|
|
1884
|
+
vans[9] = 3
|
|
1885
|
+
it(`should get ${JSON.stringify(vans[9])} for records after insertBulk with duplicated pk`, async function() {
|
|
1886
|
+
assert.strict.deepStrictEqual(vget[9], vans[9])
|
|
1887
|
+
})
|
|
1888
|
+
|
|
1889
|
+
vans[10] = 'rejected'
|
|
1890
|
+
it(`should get ${JSON.stringify(vans[10])} for insertBulk without valid pk`, async function() {
|
|
1891
|
+
assert.strict.deepStrictEqual(vget[10], vans[10])
|
|
1892
|
+
})
|
|
1893
|
+
|
|
1894
|
+
vans[11] = 3
|
|
1895
|
+
it(`should get ${JSON.stringify(vans[11])} for records after insertBulk without valid pk`, async function() {
|
|
1896
|
+
assert.strict.deepStrictEqual(vget[11], vans[11])
|
|
1897
|
+
})
|
|
1898
|
+
|
|
1899
|
+
vans[12] = { n: 0, nInserted: 0, ok: 1 }
|
|
1900
|
+
it(`should get ${JSON.stringify(vans[12])} for insertBulk with invalid data`, async function() {
|
|
1901
|
+
assert.strict.deepStrictEqual(vget[12], vans[12])
|
|
1902
|
+
})
|
|
1903
|
+
|
|
1904
|
+
vans[13] = { n: 1, nInserted: 1, ok: 1 }
|
|
1905
|
+
it(`should get ${JSON.stringify(vans[13])} for insertBulk with single object`, async function() {
|
|
1906
|
+
assert.strict.deepStrictEqual(vget[13], vans[13])
|
|
1907
|
+
})
|
|
1908
|
+
|
|
1909
|
+
vans[14] = 4
|
|
1910
|
+
it(`should get ${JSON.stringify(vans[14])} for records after insertBulk with single object`, async function() {
|
|
1911
|
+
assert.strict.deepStrictEqual(vget[14], vans[14])
|
|
1912
|
+
})
|
|
1913
|
+
|
|
1914
|
+
vans[15] = 'rejected'
|
|
1915
|
+
it(`should get ${JSON.stringify(vans[15])} for insertBulk across batches with pk existed`, async function() {
|
|
1916
|
+
assert.strict.deepStrictEqual(vget[15], vans[15])
|
|
1917
|
+
})
|
|
1918
|
+
|
|
1919
|
+
vans[16] = 0
|
|
1920
|
+
it(`should get ${JSON.stringify(vans[16])} for increment of records after insertBulk across batches failed`, async function() {
|
|
1921
|
+
assert.strict.deepStrictEqual(vget[16], vans[16])
|
|
1922
|
+
})
|
|
1923
|
+
|
|
1924
|
+
vans[17] = { n: 3200, nInserted: 3200, ok: 1 }
|
|
1925
|
+
it(`should get ${JSON.stringify(vans[17])} for insertBulk across batches without conflict`, async function() {
|
|
1926
|
+
assert.strict.deepStrictEqual(vget[17], vans[17])
|
|
1927
|
+
})
|
|
1928
|
+
|
|
1929
|
+
vans[18] = 3200
|
|
1930
|
+
it(`should get ${JSON.stringify(vans[18])} for records after insertBulk across batches`, async function() {
|
|
1931
|
+
assert.strict.deepStrictEqual(vget[18], vans[18])
|
|
1932
|
+
})
|
|
1933
|
+
|
|
1934
|
+
})
|
|
1935
|
+
|
|
1936
|
+
|
|
1677
1937
|
//runProc, 另起行程對同一資料表操作, 用於驗證跨行程之原子性
|
|
1678
1938
|
//子行程以--input-type=module執行, 並以pathToFileURL轉換src路徑, 避免專案路徑含非ASCII字元而無法import
|
|
1679
1939
|
function runProc(tag, opt, mode, payload) {
|