w-orm-postgresql 1.0.12 → 1.0.14

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.
@@ -252,7 +252,7 @@ function WOrmPostgresql(opt = {}) {
252
252
  //兩函數之衝突政策由useConflict區分, 語句各自獨立:
253
253
  // true : 添加ON CONFLICT DO NOTHING, 已存在主鍵者跳過而不中斷整批, 為insert之語義
254
254
  // false: 不添加, 任一筆撞主鍵之唯一約束即整句失敗且不寫入任何一筆, 為insertBulk之全有全無語義
255
- let insertBatches = async (client, data, useConflict) => {
255
+ let insertBatches = async (client, data, useConflict, returnPk = false) => {
256
256
 
257
257
  //nCols, 取各筆欄位數之最大值
258
258
  let nCols = 1
@@ -274,8 +274,9 @@ function WOrmPostgresql(opt = {}) {
274
274
 
275
275
  try {
276
276
 
277
- //nIns
277
+ //nIns, rows
278
278
  let nIns = 0
279
+ let rows = []
279
280
 
280
281
  //each batch
281
282
  for (let dt of ltdata) {
@@ -292,6 +293,10 @@ function WOrmPostgresql(opt = {}) {
292
293
  if (useConflict) {
293
294
  sql = `${sql} ON CONFLICT (${pkName}) DO NOTHING`
294
295
  }
296
+ if (returnPk) {
297
+ //RETURNING僅回傳實際插入之列, 供逐筆結果映回輸入序
298
+ sql = `${sql} RETURNING ${pkName}`
299
+ }
295
300
 
296
301
  //insert
297
302
  let r = await client.query(sql, mr.values)
@@ -299,12 +304,20 @@ function WOrmPostgresql(opt = {}) {
299
304
  //nIns
300
305
  nIns += r.rowCount
301
306
 
307
+ //rows, 分批時逐批收集再串接
308
+ if (returnPk) {
309
+ rows = rows.concat(get(r, 'rows', []))
310
+ }
311
+
302
312
  }
303
313
 
304
314
  //commit
305
315
  await client.query('COMMIT')
306
316
 
307
- return nIns
317
+ return {
318
+ nIns,
319
+ rows,
320
+ }
308
321
  }
309
322
  catch (err) {
310
323
 
@@ -316,6 +329,50 @@ function WOrmPostgresql(opt = {}) {
316
329
 
317
330
  }
318
331
 
332
+ //genInsertList, 依RETURNING所回傳之主鍵值映回輸入序, 產生與輸入等長且保序之逐筆結果
333
+ //後端回傳之型別未必與輸入相同(如TIMESTAMPTZ欄位輸入字串而回傳Date物件), 直接比對必然落空,
334
+ //故依回傳值之實際型別決定正規化方式, 且兩側共用同一函數
335
+ //不採[一律嘗試解析為日期]之通用寫法, 因主鍵為文字欄位而值形如'2025'與'2025-01-01'者
336
+ //將正規化為同值而誤判, 依實際型別分流則無此問題
337
+ //同批含重複主鍵時RETURNING僅回傳一次, 故以首次出現者為已插入, 其餘視為已存在而跳過
338
+ let genInsertList = (data, rows) => {
339
+
340
+ //isDateCol, 以回傳值之實際型別判定
341
+ let isDateCol = size(rows) > 0 && rows[0][pkName] instanceof Date
342
+
343
+ //normPk
344
+ let normPk = (v) => {
345
+ if (isDateCol) {
346
+ return new Date(v).toISOString()
347
+ }
348
+ return String(v)
349
+ }
350
+
351
+ //setIns, 實際插入之主鍵
352
+ let setIns = new Set()
353
+ rows.forEach(function(v) {
354
+ setIns.add(normPk(get(v, pkName)))
355
+ })
356
+
357
+ //ltres
358
+ let setSeen = new Set()
359
+ return map(data, function(v) {
360
+
361
+ //pkv
362
+ let pkv = normPk(get(v, pkName))
363
+
364
+ //inserted
365
+ let inserted = setIns.has(pkv) && !setSeen.has(pkv)
366
+ setSeen.add(pkv)
367
+
368
+ return {
369
+ n: 1,
370
+ nInserted: inserted ? 1 : 0,
371
+ ok: 1,
372
+ }
373
+ })
374
+ }
375
+
319
376
  /**
320
377
  * 創建資料表
321
378
  * 註: pk未給時採建構時之opt.pk。因insert與save倚賴主鍵之唯一約束達成原子性,
@@ -655,20 +712,38 @@ function WOrmPostgresql(opt = {}) {
655
712
  /**
656
713
  * 插入數據,僅於主鍵不存在時寫入,已存在者跳過且不覆寫
657
714
  *
658
- * n為輸入筆數,即本次嘗試插入之基準;nInserted為實際插入筆數,全數已存在而nInserted為0屬正常結果,
659
- * 不視為錯誤。同批含重複主鍵時僅首筆計入nInserted。
715
+ * 預設回傳單一聚合物件,n為輸入筆數即本次嘗試插入之基準;nInserted為實際插入筆數,
716
+ * 全數已存在而nInserted為0屬正常結果,不視為錯誤。同批含重複主鍵時僅首筆計入nInserted
660
717
  * 因主鍵得承載業務語義,未帶有效主鍵值時不自動補值,一律以reject回報。
661
718
  *
719
+ * option.returnList為true時改回傳與輸入等長且保序之逐筆陣列,令呼叫端得知[是哪幾筆]為新數據,
720
+ * 而非僅知[有幾筆]。各元素之n恆為1、ok恆為1,因insert之任何錯誤皆屬整批性錯誤而reject,
721
+ * 故逐筆元素不出現ok:0與err;同批含重複主鍵者以首次出現者之nInserted為1,其餘為0。
722
+ * 兩種取值下之不變式為陣列長度等於輸入筆數,且nInserted為1者之筆數等於聚合模式之nInserted。
723
+ *
662
724
  * @memberOf WOrmPostgresql
663
725
  * @param {Object|Array} data 輸入數據物件或陣列
664
- * @returns {Promise} 回傳Promise,resolve回傳插入結果物件{n,nInserted,ok},reject回傳錯誤訊息
726
+ * @param {Object} [option={}] 輸入設定物件,預設為{}
727
+ * @param {Boolean} [option.returnList=false] 輸入是否改回傳逐筆結果陣列,預設為false
728
+ * @returns {Promise} 回傳Promise,resolve於returnList為false時回傳插入結果物件{n,nInserted,ok},
729
+ * 為true時回傳逐筆結果陣列[{n,nInserted,ok}],reject回傳錯誤訊息
665
730
  */
666
- async function insert(data) {
731
+ async function insert(data, option = {}) {
667
732
  let isErr = false
668
733
  let res = null
669
734
 
735
+ //returnList
736
+ let returnList = get(option, 'returnList', false)
737
+ if (!isbol(returnList)) {
738
+ returnList = false
739
+ }
740
+
670
741
  //check
742
+ //輸入無效時, returnList為true者回傳空陣列以對齊save與del之規定
671
743
  if (!iseobj(data) && !isearr(data)) {
744
+ if (returnList) {
745
+ return []
746
+ }
672
747
  return {
673
748
  n: 0,
674
749
  nInserted: 0,
@@ -726,13 +801,19 @@ function WOrmPostgresql(opt = {}) {
726
801
  //由PostgreSQL於單一語句內原子完成[檢查主鍵未存在]與[寫入], 併發時同一主鍵僅有一次成功,
727
802
  //同批含重複主鍵時亦僅首筆成功, 故不須逐筆插入即可取得實際插入筆數
728
803
  //筆數過多而超出綁定參數上限者由insertBatches分批送出, 主鍵認定與genConflictSQL一致
729
- let nIns = await insertBatches(client, data, true)
804
+ //returnList為true時另附RETURNING, 以實際插入之主鍵映回輸入序而得逐筆結果
805
+ let { nIns, rows } = await insertBatches(client, data, true, returnList)
730
806
 
731
807
  //res
732
- res = {
733
- n: nAll,
734
- nInserted: nIns,
735
- ok: 1,
808
+ if (returnList) {
809
+ res = genInsertList(data, rows)
810
+ }
811
+ else {
812
+ res = {
813
+ n: nAll,
814
+ nInserted: nIns,
815
+ ok: 1,
816
+ }
736
817
  }
737
818
 
738
819
  }
@@ -842,7 +923,8 @@ function WOrmPostgresql(opt = {}) {
842
923
 
843
924
  //insertBulk, 不添加conflict, 令任一筆撞主鍵之唯一約束即整句失敗且不寫入任何一筆,
844
925
  //同批含重複主鍵者亦於此被偵測為衝突; 筆數過多者由insertBatches分批送出並以交易維持全有全無
845
- let nIns = await insertBatches(client, data, false)
926
+ //不附RETURNING, 因成功時逐筆恆為已插入而nInserted恆等於n, 取回主鍵值無資訊量
927
+ let { nIns } = await insertBatches(client, data, false)
846
928
 
847
929
  //res
848
930
  res = {
@@ -4,6 +4,7 @@ import path from 'path'
4
4
  import pg from 'pg'
5
5
  import { execFile, spawn } from 'child_process'
6
6
  import { promisify } from 'util'
7
+ import isarr from 'wsemi/src/isarr.mjs'
7
8
  import isestr from 'wsemi/src/isestr.mjs'
8
9
  import WOrm from '../src/WOrmPostgresql.mjs'
9
10
 
@@ -1934,6 +1935,197 @@ describe('insertBulk', function() {
1934
1935
  })
1935
1936
 
1936
1937
 
1938
+ describe('insert returnList', function() {
1939
+ let vans = {}
1940
+ let vget = {}
1941
+
1942
+ before(async function() {
1943
+ this.timeout(180000)
1944
+
1945
+ //wo
1946
+ let cl = 'retlist'
1947
+ let wo = WOrm(genOpt(cl))
1948
+
1949
+ //createTable
1950
+ await wo.createTable(cl, null, {
1951
+ time: '2000-01-01T00:00:00Z',
1952
+ name: 'abc',
1953
+ value: 0.1,
1954
+ }).catch(() => {})
1955
+ await wo.delAll()
1956
+
1957
+ //genRec
1958
+ let genRec = (k) => {
1959
+ return {
1960
+ time: new Date(Date.UTC(2025, 9, 1, 0, 0, 0) + k * 1000).toISOString(),
1961
+ name: `n${k}`,
1962
+ value: k,
1963
+ }
1964
+ }
1965
+
1966
+ //evChange
1967
+ let evChange = []
1968
+ wo.on('change', (mode, data, res) => {
1969
+ evChange.push(res)
1970
+ })
1971
+
1972
+ //先插入第1與第3筆, 令後續之批次呈現[新,舊,新,舊,新]之交錯
1973
+ await wo.insert([genRec(1), genRec(3)])
1974
+
1975
+ //returnList為true, 須與輸入等長保序且對位正確
1976
+ evChange = []
1977
+ vget[1] = await wo.insert([genRec(0), genRec(1), genRec(2), genRec(3), genRec(4)], { returnList: true })
1978
+ vget[2] = vget[1].map((v) => v.nInserted)
1979
+ vget[3] = vget[1].length
1980
+ vget[4] = Object.keys(vget[1][0]).sort()
1981
+ vget[5] = vget[1].filter((v) => v.nInserted === 1).length
1982
+
1983
+ //change事件之res即本次實際回傳值
1984
+ vget[6] = isarr(evChange[0]) ? evChange[0].length : null
1985
+
1986
+ //聚合模式之nInserted須等於逐筆模式之filter計數
1987
+ await wo.delAll()
1988
+ await wo.insert([genRec(1), genRec(3)])
1989
+ vget[7] = await wo.insert([genRec(0), genRec(1), genRec(2), genRec(3), genRec(4)])
1990
+
1991
+ //同批含重複主鍵, 僅首次出現者之nInserted為1
1992
+ await wo.delAll()
1993
+ vget[8] = (await wo.insert([genRec(10), genRec(10), genRec(11)], { returnList: true })).map((v) => v.nInserted)
1994
+
1995
+ //輸入無效, returnList為true回傳空陣列, 為false回傳聚合物件
1996
+ vget[9] = await wo.insert(null, { returnList: true })
1997
+ vget[10] = await wo.insert(null)
1998
+
1999
+ //單一物件輸入亦回傳長度1之陣列
2000
+ await wo.delAll()
2001
+ vget[11] = await wo.insert(genRec(20), { returnList: true })
2002
+
2003
+ //returnList為false時形狀不變
2004
+ vget[12] = await wo.insert(genRec(21))
2005
+
2006
+ //文字主鍵之型別分流, 主鍵非時間戳者不得誤判
2007
+ let clC = 'retlistcode'
2008
+ let woC = WOrm({
2009
+ ...genOpt(clC),
2010
+ pk: 'code',
2011
+ })
2012
+ await woC.createTable(clC, null, {
2013
+ code: 'abc',
2014
+ name: 'abc',
2015
+ value: 0.1,
2016
+ }).catch(() => {})
2017
+ await woC.delAll()
2018
+ await woC.insert([
2019
+ { code: 'c1', name: 'n1', value: 1 },
2020
+ { code: 'c3', name: 'n3', value: 3 },
2021
+ ])
2022
+ vget[13] = (await woC.insert([
2023
+ { code: 'c0', name: 'n0', value: 0 },
2024
+ { code: 'c1', name: 'n1', value: 1 },
2025
+ { code: 'c2', name: 'n2', value: 2 },
2026
+ { code: 'c3', name: 'n3', value: 3 },
2027
+ { code: 'c4', name: 'n4', value: 4 },
2028
+ ], { returnList: true })).map((v) => v.nInserted)
2029
+
2030
+ //文字主鍵而值形如日期者, 不得因正規化而碰撞
2031
+ let clD = 'retlistdate'
2032
+ let woD = WOrm({
2033
+ ...genOpt(clD),
2034
+ pk: 'code',
2035
+ })
2036
+ await woD.createTable(clD, null, {
2037
+ code: 'abc',
2038
+ name: 'abc',
2039
+ value: 0.1,
2040
+ }).catch(() => {})
2041
+ await woD.delAll()
2042
+ await woD.insert({ code: '2025', name: 'y', value: 1 })
2043
+ vget[14] = (await woD.insert([
2044
+ { code: '2025', name: 'y', value: 1 },
2045
+ { code: '2025-01-01', name: 'd', value: 2 },
2046
+ ], { returnList: true })).map((v) => v.nInserted)
2047
+
2048
+ })
2049
+
2050
+ vans[1] = [
2051
+ { n: 1, nInserted: 1, ok: 1 },
2052
+ { n: 1, nInserted: 0, ok: 1 },
2053
+ { n: 1, nInserted: 1, ok: 1 },
2054
+ { n: 1, nInserted: 0, ok: 1 },
2055
+ { n: 1, nInserted: 1, ok: 1 },
2056
+ ]
2057
+ it(`should get ${JSON.stringify(vans[1])} for insert with returnList`, async function() {
2058
+ assert.strict.deepStrictEqual(vget[1], vans[1])
2059
+ })
2060
+
2061
+ vans[2] = [1, 0, 1, 0, 1]
2062
+ it(`should get ${JSON.stringify(vans[2])} for nInserted in order by insert with returnList`, async function() {
2063
+ assert.strict.deepStrictEqual(vget[2], vans[2])
2064
+ })
2065
+
2066
+ vans[3] = 5
2067
+ it(`should get ${JSON.stringify(vans[3])} for length of insert with returnList`, async function() {
2068
+ assert.strict.deepStrictEqual(vget[3], vans[3])
2069
+ })
2070
+
2071
+ vans[4] = ['n', 'nInserted', 'ok']
2072
+ it(`should get ${JSON.stringify(vans[4])} for keys of each element by insert with returnList`, async function() {
2073
+ assert.strict.deepStrictEqual(vget[4], vans[4])
2074
+ })
2075
+
2076
+ vans[5] = 3
2077
+ it(`should get ${JSON.stringify(vans[5])} for count of nInserted===1 by insert with returnList`, async function() {
2078
+ assert.strict.deepStrictEqual(vget[5], vans[5])
2079
+ })
2080
+
2081
+ vans[6] = 5
2082
+ it(`should get ${JSON.stringify(vans[6])} for length of res in change event by insert with returnList`, async function() {
2083
+ assert.strict.deepStrictEqual(vget[6], vans[6])
2084
+ })
2085
+
2086
+ vans[7] = { n: 5, nInserted: 3, ok: 1 }
2087
+ it(`should get ${JSON.stringify(vans[7])} for insert without returnList on same input`, async function() {
2088
+ assert.strict.deepStrictEqual(vget[7], vans[7])
2089
+ })
2090
+
2091
+ vans[8] = [1, 0, 1]
2092
+ it(`should get ${JSON.stringify(vans[8])} for duplicated pk in same batch by insert with returnList`, async function() {
2093
+ assert.strict.deepStrictEqual(vget[8], vans[8])
2094
+ })
2095
+
2096
+ vans[9] = []
2097
+ it(`should get ${JSON.stringify(vans[9])} for insert with invalid data and returnList`, async function() {
2098
+ assert.strict.deepStrictEqual(vget[9], vans[9])
2099
+ })
2100
+
2101
+ vans[10] = { n: 0, nInserted: 0, ok: 1 }
2102
+ it(`should get ${JSON.stringify(vans[10])} for insert with invalid data and without returnList`, async function() {
2103
+ assert.strict.deepStrictEqual(vget[10], vans[10])
2104
+ })
2105
+
2106
+ vans[11] = [{ n: 1, nInserted: 1, ok: 1 }]
2107
+ it(`should get ${JSON.stringify(vans[11])} for insert with single object and returnList`, async function() {
2108
+ assert.strict.deepStrictEqual(vget[11], vans[11])
2109
+ })
2110
+
2111
+ vans[12] = { n: 1, nInserted: 1, ok: 1 }
2112
+ it(`should get ${JSON.stringify(vans[12])} for insert with single object and without returnList`, async function() {
2113
+ assert.strict.deepStrictEqual(vget[12], vans[12])
2114
+ })
2115
+
2116
+ vans[13] = [1, 0, 1, 0, 1]
2117
+ it(`should get ${JSON.stringify(vans[13])} for insert with returnList by custom text pk`, async function() {
2118
+ assert.strict.deepStrictEqual(vget[13], vans[13])
2119
+ })
2120
+
2121
+ vans[14] = [0, 1]
2122
+ it(`should get ${JSON.stringify(vans[14])} for insert with returnList by text pk looking like date`, async function() {
2123
+ assert.strict.deepStrictEqual(vget[14], vans[14])
2124
+ })
2125
+
2126
+ })
2127
+
2128
+
1937
2129
  //runProc, 另起行程對同一資料表操作, 用於驗證跨行程之原子性
1938
2130
  //子行程以--input-type=module執行, 並以pathToFileURL轉換src路徑, 避免專案路徑含非ASCII字元而無法import
1939
2131
  function runProc(tag, opt, mode, payload) {
@@ -7,7 +7,7 @@ let fdTar = './dist'
7
7
 
8
8
 
9
9
  rollupFiles({
10
- fns: getFiles(fdSrc),
10
+ fns: 'WOrmPostgresql.mjs',
11
11
  fdSrc,
12
12
  fdTar,
13
13
  nameDistType: 'kebabCase',