w-orm-postgresql 1.0.9 → 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.
@@ -26,11 +26,16 @@ import pmSeries from 'wsemi/src/pmSeries.mjs'
26
26
  /**
27
27
  * 操作資料庫(PostgreSQL)
28
28
  *
29
+ * 本套件之主鍵欄位由opt.pk指定,預設為time,select以外之五函數皆以此欄位認定主鍵。
30
+ * 因主鍵得為承載業務語義之欄位(如時序資料之觀測時間),insert與save於輸入未帶有效主鍵值時不自動補值,
31
+ * 一律以reject回報;del亦不補值,該筆回ok:0與err。
32
+ *
29
33
  * @class
30
34
  * @param {Object} [opt={}] 輸入設定物件,預設{}
31
35
  * @param {String} [opt.url='postgresql://127.0.0.1:5432'] 輸入連接資料庫字串,預設'postgresql://127.0.0.1:5432'
32
36
  * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
33
37
  * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
38
+ * @param {String} [opt.pk='time'] 輸入主鍵欄位名字串,預設'time'
34
39
  * @param {Boolean} [opt.useCache=false] 輸入是否使用select快取,適用於單程序操作,預設false
35
40
  * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
36
41
  */
@@ -63,6 +68,26 @@ function WOrmPostgresql(opt = {}) {
63
68
  useCache = false
64
69
  }
65
70
 
71
+ //pkName, 主鍵欄位名, select以外之五函數皆以此欄位認定主鍵, 預設為time
72
+ let pkName = get(opt, 'pk')
73
+ if (!isestr(pkName)) {
74
+ pkName = 'time'
75
+ }
76
+
77
+ //checkPk, 判定主鍵值是否有效
78
+ //因主鍵欄位得由呼叫端指定, 其型別隨欄位而異, 故此處僅要求有給值而不限定型別,
79
+ //型別與欄位不符者由PostgreSQL回報, 各函數再依其規格處置
80
+ function checkPk(pkv) {
81
+ return isestr(pkv) || isnum(pkv) || isbol(pkv) || isDate(pkv)
82
+ }
83
+
84
+ //isErrPkType, 判定錯誤是否為[主鍵值型別與欄位不符]
85
+ //22P02為invalid_text_representation, 22007為invalid_datetime_format
86
+ function isErrPkType(err) {
87
+ let code = get(err, 'code')
88
+ return code === '22P02' || code === '22007'
89
+ }
90
+
66
91
  //getValueType
67
92
  function getValueType(value) {
68
93
  if (isstr(value)) {
@@ -115,7 +140,7 @@ function WOrmPostgresql(opt = {}) {
115
140
  let allKeys = keys(obj)
116
141
 
117
142
  //conflictKeys
118
- let conflictKeys = ['time']
143
+ let conflictKeys = [pkName]
119
144
 
120
145
  //updateKeys
121
146
  let updateKeys = filter(allKeys, (k) => {
@@ -190,14 +215,22 @@ function WOrmPostgresql(opt = {}) {
190
215
 
191
216
  /**
192
217
  * 創建資料表
218
+ * 註: pk未給時採建構時之opt.pk。因insert與save倚賴主鍵之唯一約束達成原子性,
219
+ * 若給予與opt.pk不同之欄位,將建出其餘函數無法正確操作之資料表,故一般應留空或給予相同值
193
220
  *
194
221
  * @memberOf WOrmPostgresql
195
222
  * @param {String} cl 輸入資料表名字串
196
- * @param {String} pk 輸入主鍵字串
223
+ * @param {String} [pk=opt.pk] 輸入主鍵欄位名字串,預設為建構時之opt.pk
197
224
  * @param {Array|Object} arr 輸入數據物件陣列或數據物件
198
225
  * @returns {Promise} 回傳Promise,resolve回傳成功訊息,reject回傳錯誤訊息
199
226
  */
200
227
  async function createTable(cl, pk, arr) {
228
+
229
+ //pk, 未給時採建構時設定之主鍵欄位
230
+ if (!isestr(pk)) {
231
+ pk = pkName
232
+ }
233
+
201
234
  let isErr = false
202
235
  let res = null
203
236
 
@@ -368,29 +401,29 @@ function WOrmPostgresql(opt = {}) {
368
401
  }
369
402
 
370
403
  /**
371
- * 由主鍵time查詢單筆數據,因直接由資料表主鍵索引取值,不需如select提取數據至前端再處理,故數據量大時效能較佳
404
+ * 由主鍵查詢單筆數據,因直接由資料表主鍵索引取值,不需如select提取數據至前端再處理,故數據量大時效能較佳
405
+ * 註: 本套件之主鍵欄位由建構時之opt.pk指定,預設為time
372
406
  *
373
- * 函數名依主鍵欄位命名為selectByTime而不另掛selectById別名,因本套件之主鍵為具業務語義之time,
374
- * 與以無語義識別碼為主鍵者性質不同,該差異須由呼叫端知悉而非由別名掩蓋。
375
407
  * 主鍵未命中或主鍵值無效皆回傳null而不reject,命中之判定基準與insert、save、del內對既有數據之認定一致。
408
+ * 主鍵值之型別與欄位不符時PostgreSQL回報22P02或22007,亦屬主鍵值無效而回傳null。
376
409
  *
377
410
  * @memberOf WOrmPostgresql
378
- * @param {String|Date} time 輸入主鍵time值,本套件以time欄位為主鍵
379
- * @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此time則回傳null,reject回傳錯誤訊息
411
+ * @param {String|Number|Date} pk 輸入主鍵值,即數據之主鍵欄位值
412
+ * @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此主鍵則回傳null,reject回傳錯誤訊息
380
413
  */
381
- async function selectByTime(time) {
414
+ async function selectByPk(pk) {
382
415
  let isErr = false
383
416
  let res = null
384
417
 
385
418
  //check
386
- if (!isDate(time)) {
387
- //未給有效主鍵值視為查無數據, 判定基準與insert、save、del內對time之認定一致
419
+ if (!checkPk(pk)) {
420
+ //未給有效主鍵值視為查無數據, 判定基準與insert、save、del內對主鍵之認定一致
388
421
  return null
389
422
  }
390
423
 
391
- //find, 本套件以time欄位為主鍵, 與save、del、genConflictSQL之認定一致
424
+ //find, 以建構時設定之主鍵欄位查找,insert、save、del、genConflictSQL之認定一致
392
425
  let find = {
393
- time,
426
+ [pkName]: pk,
394
427
  }
395
428
 
396
429
  //cache, 與select共用快取, 因快取鍵值由find與order組成, 故此處等同select(find)之結果
@@ -444,7 +477,7 @@ function WOrmPostgresql(opt = {}) {
444
477
 
445
478
  //check
446
479
  if (!isarr(rows)) {
447
- throw new Error(`can not select by time[${time}]`)
480
+ throw new Error(`can not select by pk[${pk}]`)
448
481
  }
449
482
 
450
483
  //cache
@@ -460,14 +493,22 @@ function WOrmPostgresql(opt = {}) {
460
493
  res = v
461
494
  }
462
495
  else {
463
- //不存在time, 回傳null
496
+ //不存在主鍵, 回傳null
464
497
  res = null
465
498
  }
466
499
 
467
500
  }
468
501
  catch (err) {
469
- isErr = true
470
- res = err
502
+
503
+ //check, 主鍵值之型別與欄位不符者亦屬主鍵值無效, 依規格回傳null而不reject
504
+ if (isErrPkType(err)) {
505
+ res = null
506
+ }
507
+ else {
508
+ isErr = true
509
+ res = err
510
+ }
511
+
471
512
  }
472
513
  finally {
473
514
  await client.end()
@@ -484,11 +525,11 @@ function WOrmPostgresql(opt = {}) {
484
525
  }
485
526
 
486
527
  /**
487
- * 插入數據,僅於主鍵time不存在時寫入,已存在者跳過且不覆寫
528
+ * 插入數據,僅於主鍵不存在時寫入,已存在者跳過且不覆寫
488
529
  *
489
530
  * n為輸入筆數,即本次嘗試插入之基準;nInserted為實際插入筆數,全數已存在而nInserted為0屬正常結果,
490
- * 不視為錯誤。同批含重複time時僅首筆計入nInserted。
491
- * 因主鍵time承載觀測時間之業務意義,未帶有效time時不自動補值,一律以reject回報。
531
+ * 不視為錯誤。同批含重複主鍵時僅首筆計入nInserted。
532
+ * 因主鍵得承載業務語義,未帶有效主鍵值時不自動補值,一律以reject回報。
492
533
  *
493
534
  * @memberOf WOrmPostgresql
494
535
  * @param {Object|Array} data 輸入數據物件或陣列
@@ -538,10 +579,10 @@ function WOrmPostgresql(opt = {}) {
538
579
  data = [data]
539
580
  }
540
581
 
541
- //check time
582
+ //check pk, 因主鍵得承載業務語義故不自動補值, 未帶有效主鍵值者屬整批性錯誤
542
583
  data = map(data, function(v, k) {
543
- if (!isDate(v.time)) {
544
- throw new Error(`invalid data[${k}].time[${v.time}]`)
584
+ if (!checkPk(get(v, pkName))) {
585
+ throw new Error(`invalid data[${k}].${pkName}[${get(v, pkName)}]`)
545
586
  }
546
587
  return v
547
588
  })
@@ -556,10 +597,10 @@ function WOrmPostgresql(opt = {}) {
556
597
  // console.log('mr.query', mr.query)
557
598
  // console.log('mr.values', mr.values)
558
599
 
559
- //添加conflict, 令已存在time者跳過而不中斷整批插入, 主鍵認定與genConflictSQL一致
560
- //由PostgreSQL於單一語句內原子完成[檢查time未存在]與[寫入], 併發時同一time僅有一次成功,
561
- //同批含重複time時亦僅首筆成功, 故不須逐筆插入即可取得實際插入筆數
562
- let sql = `${mr.query} ON CONFLICT (time) DO NOTHING`
600
+ //添加conflict, 令已存在主鍵者跳過而不中斷整批插入, 主鍵認定與genConflictSQL一致
601
+ //由PostgreSQL於單一語句內原子完成[檢查主鍵未存在]與[寫入], 併發時同一主鍵僅有一次成功,
602
+ //同批含重複主鍵時亦僅首筆成功, 故不須逐筆插入即可取得實際插入筆數
603
+ let sql = `${mr.query} ON CONFLICT (${pkName}) DO NOTHING`
563
604
 
564
605
  //nAll
565
606
  let nAll = size(data)
@@ -623,7 +664,7 @@ function WOrmPostgresql(opt = {}) {
623
664
  * [內容相同]之判定基準為待寫入物件合併進現值後與現值相同,非待寫入物件與現值全等,
624
665
  * 故只給部份欄位且該些欄位值皆與現值相同時,合併結果等於現值,nModified為0,
625
666
  * 令nModified忠實反映資料庫端是否真的寫入。
626
- * 因主鍵time承載觀測時間之業務意義,未帶有效time時不自動補值,一律以reject回報。
667
+ * 因主鍵得承載業務語義,未帶有效主鍵值時不自動補值,一律以reject回報。
627
668
  *
628
669
  * @memberOf WOrmPostgresql
629
670
  * @param {Object|Array} data 輸入數據物件或陣列
@@ -674,10 +715,10 @@ function WOrmPostgresql(opt = {}) {
674
715
  data = [data]
675
716
  }
676
717
 
677
- //check time
718
+ //check pk, 因主鍵得承載業務語義故不自動補值, 未帶有效主鍵值者屬整批性錯誤
678
719
  data = map(data, function(v, k) {
679
- if (!isDate(v.time)) {
680
- throw new Error(`invalid data[${k}].time[${v.time}]`)
720
+ if (!checkPk(get(v, pkName))) {
721
+ throw new Error(`invalid data[${k}].${pkName}[${get(v, pkName)}]`)
681
722
  }
682
723
  return v
683
724
  })
@@ -688,6 +729,9 @@ function WOrmPostgresql(opt = {}) {
688
729
  //rest
689
730
  let rest = null
690
731
 
732
+ //pkv
733
+ let pkv = get(v, pkName)
734
+
691
735
  //_v
692
736
  let _v = null
693
737
  if (true) {
@@ -697,7 +741,7 @@ function WOrmPostgresql(opt = {}) {
697
741
  type: 'select',
698
742
  table: cl,
699
743
  where: {
700
- time: v.time,
744
+ [pkName]: pkv,
701
745
  },
702
746
  // limit: 10,
703
747
  // order,
@@ -726,7 +770,7 @@ function WOrmPostgresql(opt = {}) {
726
770
  if (iseobj(_v)) {
727
771
 
728
772
  //_vt
729
- let _vt = omit(_v, 'time')
773
+ let _vt = omit(_v, pkName)
730
774
  // console.log('_vt', _vt)
731
775
 
732
776
  //vt, [內容相同]之判定基準為[待寫入物件合併進現值後與現值相同],
@@ -734,7 +778,7 @@ function WOrmPostgresql(opt = {}) {
734
778
  //合併採淺層覆蓋, 與後端以EXCLUDED整欄取代之寫入行為一致
735
779
  let vt = {
736
780
  ..._vt,
737
- ...omit(v, 'time'),
781
+ ...omit(v, pkName),
738
782
  }
739
783
  // console.log('vt', vt)
740
784
 
@@ -789,11 +833,11 @@ function WOrmPostgresql(opt = {}) {
789
833
  // console.log('mr.query', mr.query)
790
834
  // console.log('mr.values', mr.values)
791
835
 
792
- //添加conflict, 僅有time欄位時genConflictSQL無可更新欄位而回傳空字串,
793
- //此時退回DO NOTHING, 以免併發插入同一time者報錯
836
+ //添加conflict, 僅有主鍵欄位時genConflictSQL無可更新欄位而回傳空字串,
837
+ //此時退回DO NOTHING, 以免併發插入同一主鍵者報錯
794
838
  let conflict = genConflictSQL(v)
795
839
  if (!isestr(conflict)) {
796
- conflict = 'ON CONFLICT (time) DO NOTHING'
840
+ conflict = `ON CONFLICT (${pkName}) DO NOTHING`
797
841
  }
798
842
 
799
843
  //sql, xmax為0表該列由本語句插入, 非0表係由DO UPDATE更新而來,
@@ -864,15 +908,15 @@ function WOrmPostgresql(opt = {}) {
864
908
  }
865
909
  else {
866
910
  //未開啟autoInsert則僅能更新既有數據, 不可用INSERT以免無中生有,
867
- //由UPDATE...WHERE time原子完成[查找time]與[更新], 併發時不會遺失更新
911
+ //由UPDATE...WHERE主鍵原子完成[查找主鍵]與[更新], 併發時不會遺失更新
868
912
 
869
913
  //mr
870
914
  let mr = mongoSql.sql({
871
915
  type: 'update',
872
916
  table: cl,
873
- updates: omit(v, 'time'),
917
+ updates: omit(v, pkName),
874
918
  where: {
875
- time: v.time,
919
+ [pkName]: pkv,
876
920
  },
877
921
  })
878
922
  // console.log('mr', mr)
@@ -967,7 +1011,7 @@ function WOrmPostgresql(opt = {}) {
967
1011
  *
968
1012
  * 回傳陣列恆與輸入等長,各筆之n與nDeleted皆為主鍵命中筆數,值為0或1,未命中為0且屬正常結果。
969
1013
  * 判斷某筆是否真的被刪除一律以nDeleted為準。
970
- * 未帶有效time者為該筆之輸入問題,回ok:0與err且不送查詢,不中斷其餘筆數,
1014
+ * 未帶有效主鍵值者為該筆之輸入問題,回ok:0與err且不送查詢,不中斷其餘筆數,
971
1015
  * 以此與[主鍵未命中]之ok:1區辨。
972
1016
  *
973
1017
  * @memberOf WOrmPostgresql
@@ -1024,19 +1068,19 @@ function WOrmPostgresql(opt = {}) {
1024
1068
  //rest
1025
1069
  let rest = null
1026
1070
 
1027
- //time
1028
- let time = get(v, 'time')
1071
+ //pkv
1072
+ let pkv = get(v, pkName)
1029
1073
 
1030
- //check time, 未帶有效主鍵者不補值亦不送查詢,
1074
+ //check pk, 未帶有效主鍵者不補值亦不送查詢,
1031
1075
  //因mongo-sql會將無效值轉為null而誤中其他數據, 故直接視為該筆無法處理
1032
- if (!isDate(time)) {
1076
+ if (!checkPk(pkv)) {
1033
1077
 
1034
1078
  //rest
1035
1079
  rest = {
1036
1080
  n: 0,
1037
1081
  nDeleted: 0,
1038
1082
  ok: 0,
1039
- err: `invalid time[${time}]`,
1083
+ err: `invalid ${pkName}[${pkv}]`,
1040
1084
  }
1041
1085
 
1042
1086
  return rest
@@ -1047,7 +1091,7 @@ function WOrmPostgresql(opt = {}) {
1047
1091
  type: 'delete',
1048
1092
  table: cl,
1049
1093
  where: {
1050
- time,
1094
+ [pkName]: pkv,
1051
1095
  },
1052
1096
  })
1053
1097
  // console.log('mr', mr)
@@ -1228,7 +1272,7 @@ function WOrmPostgresql(opt = {}) {
1228
1272
  //save
1229
1273
  ee.createTable = createTable
1230
1274
  ee.select = select
1231
- ee.selectByTime = selectByTime
1275
+ ee.selectByPk = selectByPk
1232
1276
  ee.insert = insert
1233
1277
  ee.save = save
1234
1278
  ee.del = del
@@ -373,47 +373,47 @@ describe('basic', function() {
373
373
  })
374
374
  vget[6] = rt
375
375
 
376
- //selectByTime
376
+ //selectByPk
377
377
  rt = null
378
378
  // vans[18] = { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 }
379
- await wo.selectByTime('2025-01-01T00:10:00Z')
379
+ await wo.selectByPk('2025-01-01T00:10:00Z')
380
380
  .then(function(msg) {
381
- // console.log('selectByTime then', msg)
382
- // selectByTime { time: 2025-01-01T00:10:00.000Z, name: 'rosemary(modify)', value: 113.98 }
381
+ // console.log('selectByPk then', msg)
382
+ // selectByPk { time: 2025-01-01T00:10:00.000Z, name: 'rosemary(modify)', value: 113.98 }
383
383
  rt = msg
384
384
  })
385
385
  .catch(function(msg) {
386
- // console.log('selectByTime catch', msg)
386
+ // console.log('selectByPk catch', msg)
387
387
  rt = msg.toString()
388
388
  })
389
389
  vget[18] = rt
390
390
 
391
- //selectByTime by time not existed
391
+ //selectByPk by pk not existed
392
392
  rt = null
393
393
  // vans[19] = null
394
- await wo.selectByTime('2024-01-01T00:00:00Z')
394
+ await wo.selectByPk('2024-01-01T00:00:00Z')
395
395
  .then(function(msg) {
396
- // console.log('selectByTime by time not existed then', msg)
397
- // selectByTime by time not existed null
396
+ // console.log('selectByPk by pk not existed then', msg)
397
+ // selectByPk by pk not existed null
398
398
  rt = msg
399
399
  })
400
400
  .catch(function(msg) {
401
- // console.log('selectByTime by time not existed catch', msg)
401
+ // console.log('selectByPk by pk not existed catch', msg)
402
402
  rt = msg.toString()
403
403
  })
404
404
  vget[19] = rt
405
405
 
406
- //selectByTime by time invalid, 未給有效主鍵值視為查無數據
406
+ //selectByPk by pk invalid, 未給有效主鍵值視為查無數據
407
407
  rt = null
408
408
  // vans[20] = null
409
- await wo.selectByTime('abc')
409
+ await wo.selectByPk('abc')
410
410
  .then(function(msg) {
411
- // console.log('selectByTime by time invalid then', msg)
412
- // selectByTime by time invalid null
411
+ // console.log('selectByPk by pk invalid then', msg)
412
+ // selectByPk by pk invalid null
413
413
  rt = msg
414
414
  })
415
415
  .catch(function(msg) {
416
- // console.log('selectByTime by time invalid catch', msg)
416
+ // console.log('selectByPk by pk invalid catch', msg)
417
417
  rt = msg.toString()
418
418
  })
419
419
  vget[20] = rt
@@ -674,30 +674,30 @@ describe('basic', function() {
674
674
  })
675
675
  vget[17] = rt
676
676
 
677
- //selectByTime (1st time, 從DB讀取並填入快取)
677
+ //selectByPk (1st time, 從DB讀取並填入快取)
678
678
  rt = null
679
679
  // vans[21] = { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary(modify)', value: 654.321 }
680
- await woc.selectByTime('2025-01-01T00:01:00Z')
680
+ await woc.selectByPk('2025-01-01T00:01:00Z')
681
681
  .then(function(msg) {
682
- // console.log('selectByTime 1st then', msg)
682
+ // console.log('selectByPk 1st then', msg)
683
683
  rt = msg
684
684
  })
685
685
  .catch(function(msg) {
686
- // console.log('selectByTime 1st catch', msg)
686
+ // console.log('selectByPk 1st catch', msg)
687
687
  rt = msg.toString()
688
688
  })
689
689
  vget[21] = rt
690
690
 
691
- //selectByTime (2nd time, 命中快取, 內容須與第一次相同)
691
+ //selectByPk (2nd time, 命中快取, 內容須與第一次相同)
692
692
  rt = null
693
693
  // vans[22] = vans[21]
694
- await woc.selectByTime('2025-01-01T00:01:00Z')
694
+ await woc.selectByPk('2025-01-01T00:01:00Z')
695
695
  .then(function(msg) {
696
- // console.log('selectByTime 2nd then', msg)
696
+ // console.log('selectByPk 2nd then', msg)
697
697
  rt = msg
698
698
  })
699
699
  .catch(function(msg) {
700
- // console.log('selectByTime 2nd catch', msg)
700
+ // console.log('selectByPk 2nd catch', msg)
701
701
  rt = msg.toString()
702
702
  })
703
703
  vget[22] = rt
@@ -758,19 +758,19 @@ describe('basic', function() {
758
758
  assert.strict.deepStrictEqual(vget[6], vans[6])
759
759
  })
760
760
 
761
- //vans[18]~vans[22]為後續新增之selectByTime, 為不更動既有編號故接續於末號之後
761
+ //vans[18]~vans[22]為後續新增之selectByPk, 為不更動既有編號故接續於末號之後
762
762
  vans[18] = { time: new Date('2025-01-01T00:10:00.000Z'), name: 'rosemary(modify)', value: 113.98 }
763
- it(`should get ${JSON.stringify(vans[18])} for selectByTime`, async function() {
763
+ it(`should get ${JSON.stringify(vans[18])} for selectByPk`, async function() {
764
764
  assert.strict.deepStrictEqual(vget[18], vans[18])
765
765
  })
766
766
 
767
767
  vans[19] = null
768
- 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() {
769
769
  assert.strict.deepStrictEqual(vget[19], vans[19])
770
770
  })
771
771
 
772
772
  vans[20] = null
773
- 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() {
774
774
  assert.strict.deepStrictEqual(vget[20], vans[20])
775
775
  })
776
776
 
@@ -877,12 +877,12 @@ describe('basic', function() {
877
877
  })
878
878
 
879
879
  vans[21] = { time: new Date('2025-01-01T00:01:00.000Z'), name: 'rosemary(modify)', value: 654.321 }
880
- it(`should get ${JSON.stringify(vans[21])} for selectByTime (1st, fill cache)`, async function() {
880
+ it(`should get ${JSON.stringify(vans[21])} for selectByPk (1st, fill cache)`, async function() {
881
881
  assert.strict.deepStrictEqual(vget[21], vans[21])
882
882
  })
883
883
 
884
884
  vans[22] = vans[21]
885
- 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() {
886
886
  assert.strict.deepStrictEqual(vget[22], vans[22])
887
887
  })
888
888
 
@@ -954,7 +954,7 @@ describe('insert nInserted', function() {
954
954
  vget[7] = ss.length
955
955
 
956
956
  //同批重複者僅首筆入庫
957
- let vdup = await wo.selectByTime('2025-02-01T00:05:00Z')
957
+ let vdup = await wo.selectByPk('2025-02-01T00:05:00Z')
958
958
  vget[8] = vdup.name
959
959
 
960
960
  })
@@ -1028,7 +1028,7 @@ describe('save concurrency', function() {
1028
1028
  wo.save({ time: tMerge, name: 'merge-name' }),
1029
1029
  wo.save({ time: tMerge, value: 99 }),
1030
1030
  ])
1031
- vget[1] = await wo.selectByTime(tMerge)
1031
+ vget[1] = await wo.selectByPk(tMerge)
1032
1032
 
1033
1033
  //併發save對全新time, autoInsert僅一次且不得報錯
1034
1034
  let tNew = '2025-03-01T00:01:00Z'
@@ -1048,12 +1048,12 @@ describe('save concurrency', function() {
1048
1048
  let tDiff = '2025-03-01T00:03:00Z'
1049
1049
  await wo.insert({ time: tDiff, name: 'diff', value: 5 })
1050
1050
  vget[6] = await wo.save({ time: tDiff, value: 6 })
1051
- vget[7] = await wo.selectByTime(tDiff)
1051
+ vget[7] = await wo.selectByPk(tDiff)
1052
1052
 
1053
1053
  //save(autoInsert=false)對不存在之time不得插入
1054
1054
  let tNone = '2025-03-01T00:04:00Z'
1055
1055
  vget[8] = await wo.save({ time: tNone, name: 'none' }, { autoInsert: false })
1056
- vget[9] = await wo.selectByTime(tNone)
1056
+ vget[9] = await wo.selectByPk(tNone)
1057
1057
 
1058
1058
  //併發save(autoInsert=false)對既有time之不同欄位, 各欄位皆須保留且不得報錯
1059
1059
  let tUpd = '2025-03-01T00:05:00Z'
@@ -1063,7 +1063,7 @@ describe('save concurrency', function() {
1063
1063
  wo.save({ time: tUpd, value: 88 }, { autoInsert: false }),
1064
1064
  ])
1065
1065
  vget[10] = rsu.filter((v) => v[0].ok !== 1).length
1066
- vget[11] = await wo.selectByTime(tUpd)
1066
+ vget[11] = await wo.selectByPk(tUpd)
1067
1067
 
1068
1068
  })
1069
1069
 
@@ -1148,7 +1148,7 @@ describe('spec compliance', function() {
1148
1148
  let tPart = '2025-04-01T00:00:00Z'
1149
1149
  await wo.insert({ time: tPart, name: 'peter', value: 5 })
1150
1150
  vget[1] = await wo.save({ time: tPart, value: 5 })
1151
- vget[2] = await wo.selectByTime(tPart)
1151
+ vget[2] = await wo.selectByPk(tPart)
1152
1152
 
1153
1153
  //save單筆失敗不中斷整批, 失敗筆須帶err
1154
1154
  let tOk = '2025-04-01T00:01:00Z'
@@ -1302,6 +1302,138 @@ describe('spec compliance', function() {
1302
1302
  })
1303
1303
 
1304
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
+
1305
1437
  //runProc, 另起行程對同一資料表操作, 用於驗證跨行程之原子性
1306
1438
  //子行程以--input-type=module執行, 並以pathToFileURL轉換src路徑, 避免專案路徑含非ASCII字元而無法import
1307
1439
  function runProc(tag, opt, mode, payload) {
@@ -1416,7 +1548,7 @@ describe('cross-process concurrency', function() {
1416
1548
  vget[5] = rpss.reduce((sum, v) => sum + (v.nInserted || 0), 0)
1417
1549
 
1418
1550
  //10欄須全數保留且值為各自行程之tag
1419
- let vSav = await woSav.selectByTime(tSav)
1551
+ let vSav = await woSav.selectByPk(tSav)
1420
1552
  vget[6] = Array.from({ length: 10 }, (v, k) => k)
1421
1553
  .filter((k) => {
1422
1554
  let tag = k <= 4 ? 'p1' : 'p2'