w-orm-lmdb 1.0.18 → 1.0.19

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.
@@ -4,9 +4,13 @@
4
4
 
5
5
  規格與各套件之儲存後端無關:後端可為 key-value store、document database、關聯式資料庫或其他形式。規格只規範**對外行為**——函數名稱、參數、回傳結構、各欄位語義、錯誤處置與原子性要求;**不規範達成手段**,各套件依其後端能力自行實作。
6
6
 
7
- 適用函數共六個:`select`、`selectByPk`、`insert`、`save`、`del`、`delAll`。套件得另有後端專屬函數(如建表、檔案儲存),不在本規格範圍內,惟不得與六函數之語義衝突。
7
+ 適用函數共七個,**皆為必備**:`select`、`selectByPk`、`insert`、`insertBulk`、`save`、`del`、`delAll`。
8
8
 
9
- 專屬函數若與六函數之概念對應(插入/單筆直讀/刪除/條件刪除),則**參數形狀亦須比照**,不只回傳結構:收「帶主鍵之數據物件或陣列」者回等長陣列,收單一主鍵者回單一物件。僅對齊回傳值而參數各行其是,會讓同一概念出現兩種呼叫方式,與本規格「呼叫端不須為個別套件寫特例」之目的相違。若某概念在該後端無法滿足本規格之要求(如無法原子取代內容而不能提供 `save`),則不提供該函數,並於 README 說明原因與替代作法;不得提供一個語義較弱而同名的版本。
9
+ 其中 `insertBulk` 之語義與 `insert` **不同**(衝突政策相異),非同一操作之加速版;其於某些後端不會比 `insert` 快,**仍須提供**——呼叫端之所以能於不同套件間替換,倚賴的正是各套件提供同一組函數。
10
+
11
+ 套件得另有後端專屬函數(如建表、檔案儲存),不在本規格範圍內,惟不得與規格所定函數之語義衝突。
12
+
13
+ 專屬函數若與規格所定函數之概念對應(插入/單筆直讀/刪除/條件刪除),則**參數形狀亦須比照**,不只回傳結構:收「帶主鍵之數據物件或陣列」者回等長陣列,收單一主鍵者回單一物件。僅對齊回傳值而參數各行其是,會讓同一概念出現兩種呼叫方式,與本規格「呼叫端不須為個別套件寫特例」之目的相違。若某概念在該後端無法滿足本規格之要求(如無法原子取代內容而不能提供 `save`),則不提供該函數,並於 README 說明原因與替代作法;不得提供一個語義較弱而同名的版本。
10
14
 
11
15
  ---
12
16
 
@@ -28,7 +32,7 @@
28
32
 
29
33
  - 計數欄位(`n`、`nInserted`、`nModified`、`nDeleted`)在該函數的規格表中一旦列出即**恆出現**,無對應行為時填 `0`,不得省略。
30
34
  - 逐筆函數(`save`、`del`)恆回傳與輸入**等長**之陣列,即使輸入為單一物件亦回傳長度為 `1` 之陣列。
31
- - 整批函數(`insert`、`delAll`)恆回傳單一物件。
35
+ - 整批函數(`insert`、`insertBulk`、`delAll`)恆回傳單一物件。
32
36
  - 唯一的例外是 `err`:僅在該筆 `ok` 為 `0` 時出現,`ok` 為 `1` 時不得出現。
33
37
 
34
38
  ### T3 `n` 之定義
@@ -38,6 +42,7 @@
38
42
  | 函數 | `n` |
39
43
  |---|---|
40
44
  | `insert` | 輸入筆數(陣列化後之長度),代表本次嘗試插入之基準 |
45
+ | `insertBulk` | 輸入筆數,同 `insert` |
41
46
  | `save`(逐筆) | 主鍵命中筆數,`0` 或 `1`。命中(不論內容有無變更)或經插入而產生皆為 `1` |
42
47
  | `del`(逐筆) | 主鍵命中筆數,`0` 或 `1` |
43
48
  | `delAll` | 實際刪除筆數,恆等於 `nDeleted` |
@@ -62,6 +67,7 @@
62
67
  | 函數 | 回傳 |
63
68
  |---|---|
64
69
  | `insert` | `{ n: 0, nInserted: 0, ok: 1 }` |
70
+ | `insertBulk` | `{ n: 0, nInserted: 0, ok: 1 }` |
65
71
  | `save` | `[]` |
66
72
  | `del` | `[]` |
67
73
 
@@ -73,7 +79,7 @@
73
79
 
74
80
  | `autoGenPk` | 行為 |
75
81
  |---|---|
76
- | `true`(預設) | `insert` 與 `save` 於輸入未帶有效主鍵時,由套件自動產生主鍵值後再寫入 |
82
+ | `true`(預設) | `insert`、`insertBulk` 與 `save` 於輸入未帶有效主鍵時,由套件自動產生主鍵值後再寫入 |
77
83
  | `false` | 套件一律不產生主鍵值,主鍵須由呼叫端於寫入前自備 |
78
84
 
79
85
  `autoGenPk: false` 之定位為**依賴注入**:主鍵的產生規則改由呼叫端掌握(如採用外部發號器、以業務欄位組合、沿用上游系統既有識別碼),套件不介入。採用此設定後,**主鍵之唯一性、格式與是否與既有資料衝突,皆由呼叫端自負**;套件不做補救,亦不因主鍵不合預期而額外檢查或修正。
@@ -109,6 +115,16 @@
109
115
 
110
116
  達成手段不限,由各套件依後端能力選擇(唯一索引、`ON CONFLICT`、條件寫入、寫交易、比較並交換等)。
111
117
 
118
+ **亦得以「條件寫入配合衝突偵測與重試」達成**,惟須同時滿足下列三項:
119
+
120
+ 1. **每次寫入本身為單一條件式原子語句**——該次寫入成立與否,由資料庫依當下之既有資料判定(如主鍵之唯一約束、`WHERE 主鍵` 之比對),而非由先前讀到的值決定。
121
+ 2. **預測錯誤須可被偵測**——插入時撞及既有主鍵、更新時未命中任何列,皆須能由回傳值或錯誤類別辨識。
122
+ 3. **偵測到衝突須重試**,重試次數有限且能收斂至正確結果。
123
+
124
+ 此形式與本節所禁止者之分野在於:所禁止者為「讀到的值決定了寫入的內容或成敗」,而本形式之讀取僅用於**選擇發出哪一條語句**;選錯不會產生錯誤結果,只會觸發重試。故縱使讀取值於語句發出前已過期,最終結果仍與該次操作單獨執行時相同。採用本形式者,其讀取結果**不得用於決定寫入內容**,此與 `save` 之預讀規定一致。
125
+
126
+ 本形式係為**後端無法於單一語句內回報「本次係插入或更新」**而設。此類後端若堅持以單一 upsert 語句達成本節要求,即無從提供 `insert` 與 `save` 所要求之 `nInserted`,兩項要求將互斥;容許本形式方能兼顧。採用本形式之套件,須於「各套件符合狀態」載明其後端之限制、衝突之偵測方式與重試上限。
127
+
112
128
  **若某後端須開啟特定設定才能達成本要求,該設定必須預設開啟。** 不得以「預設關閉、要正確性請自行開啟」的形式交付——`insert` 的「已存在則跳過」與 `save` 的「不遺失更新」是本規格定義的核心語義,不是選配。若開啟該設定對既有資料有前提(例如建立唯一索引前須先清除重複資料),須於 README 載明升級步驟。
113
129
 
114
130
  `del` 不在本要求內:刪除為冪等操作,重複刪除不產生額外副作用。惟 `nDeleted` 在併發下可能不精確,套件得選擇以原子操作取得精確值。
@@ -179,7 +195,7 @@ ee.emit('change', mode, data, res)
179
195
 
180
196
  #### T10.3 `error` 事件
181
197
 
182
- 操作發生錯誤時發出。**六函數皆須發出**,含讀取函數。
198
+ 操作發生錯誤時發出。**七個函數皆須發出**,含讀取函數。
183
199
 
184
200
  ```
185
201
  ee.emit('error', mode, data, err)
@@ -208,7 +224,7 @@ ee.emit('error', mode, data, err)
208
224
  change (mode, data, res) 資料實際異動成功後,整批一次
209
225
  error (mode, data, err) 整批性錯誤於 reject 前;逐筆失敗於該筆定案後,每筆一次
210
226
 
211
- mode 六函數之名稱;專屬函數取其自身名稱
227
+ mode 規格所定函數之名稱;專屬函數取其自身名稱
212
228
  data 輸入數據,無者為 null
213
229
  err 字串
214
230
  ```
@@ -254,6 +270,59 @@ err 字串
254
270
  - 須符合 T7 之原子性要求。
255
271
  - 輸入無效見 T5。
256
272
 
273
+ ### insertBulk(data) → `Promise<{ n, nInserted, ok }>`
274
+
275
+ 批次插入,**全批視為一個單位**:全部插入成功,或一筆都不寫入。
276
+
277
+ | 欄位 | 值 |
278
+ |---|---|
279
+ | `n` | 輸入筆數 |
280
+ | `nInserted` | 實際插入筆數,成功時**恆等於 `n`** |
281
+ | `ok` | `1` |
282
+
283
+ - **全有全無**:任一筆之主鍵已存在,即以 `Promise.reject` 拋出整批性錯誤,且**不得寫入任何一筆**。
284
+ - 同批含重複主鍵者亦視為衝突,整批 `reject`。
285
+ - 不提供逐筆結果,故不出現 `ok: 0` 與 `err`;需要逐筆處置者改用 `insert`。
286
+ - 主鍵補值依 T6,與 `insert` 相同;主鍵檢查須於任何寫入之前一次完成。
287
+ - 事件依 T10 發出,`mode` 取 `'insertBulk'`。
288
+ - 輸入無效見 T5。
289
+
290
+ 回傳之鍵集合與 `insert` **完全相同**。`nInserted` 於成功時雖恆等於 `n` 而無額外資訊,仍須保留,令呼叫端得共用同一段結果處理程式碼。
291
+
292
+ #### 與 `insert` 之關係
293
+
294
+ **本函數非 `insert` 之加速版,兩者衝突政策不同:**
295
+
296
+ | 情形 | `insert` | `insertBulk` |
297
+ |---|---|---|
298
+ | 主鍵已存在 | 跳過該筆,整批 `ok: 1` | **整批 `reject`,且不寫入任何一筆** |
299
+ | 同批重複主鍵 | 僅首筆計入 `nInserted` | 視為衝突,整批 `reject` |
300
+ | `nInserted` | 實際插入筆數,`0 ≤ nInserted ≤ n` | 成功時恆等於 `n` |
301
+ | 適用場景 | 一般寫入,資料表可能已有既有資料 | 批次匯入,本即預期無衝突 |
302
+
303
+ **確無衝突時,兩者之可觀察結果完全相同**(皆回 `{ n, nInserted: n, ok: 1 }`);差異僅於有衝突時顯現。故呼叫端得於「已知無衝突」之前提下替換,而該前提若不成立,`insertBulk` 會以 `reject` 明白告知。
304
+
305
+ #### 效能不在本規格之保證範圍
306
+
307
+ 本函數之存在理由為**語義**(全有全無),而非速度。是否較 `insert` 快,取決於該後端之批次寫入能力:
308
+
309
+ - 部分後端之批次語句較逐筆寫入快上數量級,此時本函數兼具語義與效能之利。
310
+ - 部分後端之 `insert` 本即以單次往返完成且計數精確,此時本函數不會更快,仍須提供。
311
+
312
+ **不得因「於本後端不會更快」而不提供本函數。** 呼叫端之所以能於不同套件間替換,倚賴的是各套件提供同一組函數;缺一個即迫使呼叫端為個別套件改寫呼叫,與本規格「呼叫端不須為個別套件寫特例」之目的相違。各套件須於「各套件符合狀態」載明其實作方式與是否具效能優勢,供呼叫端判斷何時值得改用。
313
+
314
+ #### 實作要求
315
+
316
+ **一、不得以別名或轉呼叫 `insert` 之方式提供。** 兩者衝突政策不同——別名之下主鍵衝突時不會 `reject` 而是靜默跳過,同一段呼叫端程式碼於不同套件上行為分歧,且分歧只在衝突發生時顯現。此屬 §14 所禁止之「語義較弱而同名的版本」,且為其中最難察覺者:呼叫端之錯誤前提於該套件上永遠不會浮現。
317
+
318
+ **二、得以逐筆寫入實作**,只要整體滿足全有全無。批次語句非本函數之必要條件;於批次不會更快之後端,包在單一交易內逐筆條件寫入亦屬合格實作。
319
+
320
+ **三、全有全無為獨立於 T7 之額外要求。** T7 只保證每一筆之「檢查主鍵不存在」與「寫入」為原子,不保證整批之全有全無。**若寫入於該後端會被拆為多次送出,須以交易包覆並於失敗時回滾**,確保 `reject` 之後資料庫狀態與呼叫前相同。
321
+
322
+ 此非理論顧慮:部分驅動層因參數數量上限而自動將單次批次拆為多個語句,前段可能已落盤。呼叫端收到 `reject` 卻已有部分資料寫入,將無從判斷該重試或該清理,比直接失敗更難處理。
323
+
324
+ **四、後端不支援交易者,得以補償動作達成**:偵測到衝突後,刪除本次呼叫已寫入之筆數,再 `reject`。此法於正常運作下可使狀態回復如初,惟行程於補償途中中止則可能殘留,**須於 README 與「各套件符合狀態」明白載明此限制**。有交易可用者一律優先採用交易。
325
+
257
326
  ### save(data, option) → `Promise<Array<{ n, nInserted, nModified, ok }>>`
258
327
 
259
328
  以主鍵為準更新既有數據,未給之欄位保留;主鍵不存在且 `option.autoInsert` 為 `true`(預設)時改為插入。
@@ -317,6 +386,7 @@ select(find) → [ {...}, {...} ] 無符合為 []
317
386
  selectByPk(pk) → {...} | null
318
387
 
319
388
  insert(data) → { n, nInserted, ok }
389
+ insertBulk(data) → { n, nInserted, ok } 衝突即整批reject且不寫入任何一筆
320
390
  save(data, option) → [ { n, nInserted, nModified, ok }, ... ]
321
391
  del(data) → [ { n, nDeleted, ok }, ... ]
322
392
  delAll(find) → { n, nDeleted, ok }
@@ -332,6 +402,7 @@ delAll(find) → { n, nDeleted, ok }
332
402
  | 要判斷什麼 | 看什麼 | 不要看什麼 |
333
403
  |---|---|---|
334
404
  | 這批有幾筆是新資料 | `insert` 之 `nInserted` | `n`(那是輸入筆數) |
405
+ | 這批有沒有撞到既有主鍵 | `insertBulk` 是否 `reject` | `nInserted`(成功時恆等於 `n`,不帶額外資訊) |
335
406
  | 這筆是不是新資料 | `save` 之 `nInserted === 1` | `n`(命中即為 1,插入與更新皆是) |
336
407
  | 這筆內容有沒有實際寫入 | `save` 之 `nModified === 1` | `n` |
337
408
  | 這筆主鍵原本存不存在 | `save` 之 `n === 1` 且 `nInserted === 0` | 單看 `n` |
@@ -349,7 +420,7 @@ delAll(find) → { n, nDeleted, ok }
349
420
  2. `select` 恆回陣列且不含資料庫內部欄位,`selectByPk` 未命中回 `null`。
350
421
  3. 每個函數之計數欄位恆出現,無對應行為時填 `0`;`err` 僅隨 `ok: 0` 出現。
351
422
  4. `n` 依 T3 定義,四個函數各自的基準寫進函數註解。
352
- 5. `insert` 與 `save` 符合 T7 原子性要求;若倚賴某項設定達成,該設定預設開啟,且 README 載明升級前提。
423
+ 5. `insert` 與 `save` 符合 T7 原子性要求;若倚賴某項設定達成,該設定預設開啟,且 README 載明升級前提。採「條件寫入配合衝突偵測與重試」達成者,每次寫入皆為單一條件式原子語句、衝突可被偵測且重試能收斂,讀取結果未用於決定寫入內容,並已於「各套件符合狀態」載明後端之限制、衝突之偵測方式與重試上限。
353
424
  6. 成功路徑 `ok` 恆為 `1`,不由驅動層旗標推導;`ok: 0` 必附 `err`;單筆失敗不中斷整批。
354
425
  7. `save` 之「內容相同」採**合併後比對**,基準寫進註解。
355
426
  8. `del` 對未帶有效主鍵者不送查詢,直接回 `ok: 0` + `err`。
@@ -357,7 +428,8 @@ delAll(find) → { n, nDeleted, ok }
357
428
  10. 提供 `opt.autoGenPk` 且預設為 `true`;為 `false` 時不產生主鍵值,未帶有效主鍵者 `reject`;不得於 `option` 逐次覆寫。符合 T6 例外而完全不提供該設定者(主鍵具業務語義,或套件無從得知主鍵型別),已於「各套件符合狀態」載明所依據之情形與理由。
358
429
  11. README 依 T8 宣告併發保證範圍,無法保證者載明後果、實測依據與迴避方式。
359
430
  12. 依 T10 發出 `change` 與 `error` 事件,參數形狀為 `(mode, data, res)` 與 `(mode, data, err)`;所採用之 `EventEmitter` 無「`'error'` 於無監聽者時拋出」之語義;每一處 `emit` 皆以 try/catch 包覆;正常結果不發出 `error`;事件所送出之資訊皆另有正規管道,移除全部事件後呼叫端仍能取得完整資訊。
360
- 13. 測試須涵蓋:同批重複主鍵、主鍵不存在、合併後內容相同、只給部份欄位且值相同、`autoInsert` 兩種取值、`autoGenPk` 兩種取值(含 `false` 且未帶主鍵須 `reject`)、單筆失敗、未帶有效主鍵、`delAll` 帶條件且僅部份命中,以及**同一操作於有註冊與未註冊 `error` 監聽兩種情況下回傳值完全相同**。
431
+ 13. `insertBulk` 語義為「衝突即整批 reject 且不寫入任何一筆」而非 `insert` 之加速版,未以別名或轉呼叫實作;寫入於該後端會被拆為多次送出者已以交易包覆並驗證回滾,無交易可用而採補償動作者已載明其限制;實作方式與是否具效能優勢已於「各套件符合狀態」載明。
432
+ 14. 測試須涵蓋:同批重複主鍵、主鍵不存在、合併後內容相同、只給部份欄位且值相同、`autoInsert` 兩種取值、`autoGenPk` 兩種取值(含 `false` 且未帶主鍵須 `reject`)、單筆失敗、未帶有效主鍵、`delAll` 帶條件且僅部份命中,以及**同一操作於有註冊與未註冊 `error` 監聽兩種情況下回傳值完全相同**。提供 `insertBulk` 者另須涵蓋:無衝突時 `nInserted` 等於 `n`、撞既有主鍵時整批 `reject`、同批重複主鍵時整批 `reject`,以及**失敗後資料表無任何新增**。
361
433
 
362
434
  ---
363
435
 
@@ -379,20 +451,22 @@ T7 之原子性以 LMDB 之條件寫入與寫交易達成:`insert` 為 `ifNoEx
379
451
 
380
452
  T8 已完成:README 已宣告單一行程內保證成立、跨行程因 `lmdb-js` 綁定層限制而不成立,並附實測依據(平台、版本、行程數、回合數、發生率)與迴避方式。
381
453
 
454
+
455
+ **`insertBulk`:已實作。** 全有全無以 `childTransaction` 包覆達成——交易內逐筆先 `get` 判定主鍵是否存在,存在即回傳 `ABORT` 中止該交易,由交易回滾保證未寫入任何一筆,無須補償動作。交易內之 `get` 讀得到同一交易稍早之 `put`,故同批含重複主鍵者亦於此被偵測為衝突。存在與否採鍵層判定,與 `insert` 之 `ifNoExists` 一致。
456
+
457
+ **須用 `childTransaction` 而非 `transaction`。** 實測 `lmdb-js` 3.5.6 之非同步 `transaction()` **於中止時並不回滾**:回調內回傳 `ABORT` 或拋錯,其先前之 `put` 皆已落盤。`childTransaction` 與 `transactionSync` 兩者之 `ABORT` 與拋錯皆能正確回滾,本套件取前者以免阻塞事件迴圈。
458
+
459
+ **效能與 `insert` 無顯著差異**(實測 N=20000:`insert` 650ms、`childTransaction` 640ms、`transactionSync` 645ms),因本套件之 `insert` 本即以 `Promise.all` 一次送出全部條件寫入而非逐筆 await。提供本函數係為與其他套件維持同一組函數,令呼叫端得於各套件間替換而不須改寫呼叫。
460
+
382
461
  ### w-orm-mongodb
383
462
 
384
463
  主鍵欄位為 `id`,為無業務語義之識別碼。主鍵欄位目前**固定為 `id`,尚未支援由呼叫端指定**,已於類別註解、`selectByPk` 註解與 README 載明。
385
464
 
386
- 已符合 T1–T9 與六函數全部規格。T10 之 `change` 已符合,且已集中於共用之 `emitChange(mode, data, res)`(內含 try/catch),`error` 待新增。
387
-
388
- T10 待處理:
465
+ 已符合 T1–T10 與六函數全部規格,無待處理項目。
389
466
 
390
- | 項目 | 現況 | 規格 |
391
- |---|---|---|
392
- | `error` 事件 | 未發出 | 依 T10.3 新增;既有之 `emitChange` 可直接擴充為共用之事件發出函數 |
393
- | `EventEmitter` 實作 | `events.EventEmitter`(Node 內建,具「`error` 於無監聽者時拋出」之語義) | 依 T10.1 第 2 條改用 `wsemi` 之 `evem()`(`eventemitter3`),既有相依已涵蓋 |
467
+ T10 之實作:`EventEmitter` `wsemi` `evem()`(即 `eventemitter3`),其於 `'error'` 無監聽者時僅回傳 `false` 而不拋出,符合 T10.1 第 2 條;Node 內建之 `events.EventEmitter` 具該拋出語義,已停止使用。全部事件收斂於共用之 `emitChange(mode, data, res)` 與 `emitError(mode, data, err)` 兩函數,try/catch 由該二處統一保證,`err` 一律經 `getErrMsg` 轉為字串。`change` 之逐筆函數以整批為單位發出一次,`save` 之逐筆插入另發 `mode` 為 `insert` 之事件;`error` 於六函數與四個 GridFS 專屬函數皆發出,整批性錯誤於 `reject` 之前、逐筆失敗於該筆定案後各一次。內部查找函數 `_findGfs` 不自行發出 `error`,其 reject 由 `delGfs` 與 `delAllGfs` 之 catch 接住並於該處發出,以免重複。`selectByPkGfs` 之「查無檔案」於 catch 內以 `code` 為 `ENOENT` 判定為正常結果而不設 `isErr`,故不會誤發 `error`。
394
468
 
395
- `opt.autoGenPk` 預設為 `true`,以 `genID()` 產生主鍵值——為 62 進位 32 碼之隨機字串(約 2^190 種),碰撞機率遠低於 UUIDv4 之 122 bits;為 `false` 時 `insert`、`save` 與 `insertGfs` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,`insert` 與 `save` 之 `option` 未提供覆寫。`del` 不受此設定影響。
469
+ `opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值——即 UUIDv7,符合 T6「如 UUID 或單調序列」,且其時間有序之特性令寫入主鍵唯一索引時之索引局部性優於純隨機字串;為 `false` 時 `insert`、`save` 與 `insertGfs` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,`insert` 與 `save` 之 `option` 未提供覆寫。`del` 不受此設定影響。
396
470
 
397
471
  `save` 之「內容相同」判定即本規格之基準來源:由 MongoDB 於伺服器端將 `$set` 之待寫入物件合併進現值後與現值比對,未寫入即回 `modifiedCount` 為 `0`,比對與寫入於同一原子操作內完成,故不須預讀。
398
472
 
@@ -406,18 +480,16 @@ GridFS 之寫入係先寫 chunks 再寫 files 文件,故違反唯一索引時
406
480
 
407
481
  `save` 無 GridFS 對應函數:GridFS 無法於單一原子操作內取代既有內容,提供 `saveGfs` 將違反 T7,故不提供,更新以 `delGfs` 後再 `insertGfs` 完成。
408
482
 
483
+
484
+ **`insertBulk`:待實作。** 全有全無之達成方式依部署而異:具 replica set 者以交易包覆 `insertMany({ ordered: true })`;standalone 無交易可用,須以補償動作達成——`insertMany({ ordered: false })` 後若 `insertedCount` 小於 `n`,由 `writeErrors` 取得衝突之索引,刪除本次已寫入之筆數後再 `reject`,並須於 README 載明「行程於補償途中中止則可能殘留」之限制。**不得以別名指向 `insert`**:如此則主鍵衝突時不會 `reject` 而是靜默跳過,呼叫端之錯誤前提於本套件上永遠不會浮現。本函數於本後端不會較 `insert` 快(`insert` 本即一次往返且計數精確),仍須提供以維持跨套件之可替換性。
485
+
409
486
  ### w-orm-postgresql
410
487
 
411
488
  主鍵欄位由建構時之 `opt.pk` 指定,預設為 `time`,**已支援由呼叫端指定**,`select` 以外之五函數皆以該欄位認定主鍵,已於類別註解、`selectByPk` 註解與 README 載明。
412
489
 
413
- 已符合 T1–T9 與六函數全部規格。T10 之 `change` 已符合(4 處 `emit` 皆以 try/catch 包覆),`error` 待新增。
414
-
415
- T10 待處理:
490
+ 已符合 T1–T10 與六函數全部規格,無待處理項目。
416
491
 
417
- | 項目 | 現況 | 規格 |
418
- |---|---|---|
419
- | `error` 事件 | 未發出 | 依 T10.3 新增 |
420
- | `EventEmitter` 實作 | `events.EventEmitter`(Node 內建,具「`error` 於無監聽者時拋出」之語義) | 依 T10.1 第 2 條改用 `wsemi` 之 `evem()`(`eventemitter3`),既有相依已涵蓋 |
492
+ T10 之實作:`EventEmitter` `wsemi` `evem()`(即 `eventemitter3`),其於 `'error'` 無監聽者時僅回傳 `false` 而不拋出,符合 T10.1 第 2 條;Node 內建之 `events.EventEmitter` 具該拋出語義,已停止使用。全部事件收斂於共用之 `emitChange(mode, data, res)` 與 `emitError(mode, data, err)` 兩函數,try/catch 由該二處統一保證,`err` 一律經 `getErrMsg` 轉為字串。`change` 之逐筆函數以整批為單位發出一次,`save` 之逐筆插入另發 `mode` 為 `insert` 之事件;`error` 於六函數與專屬函數 `createTable` 皆發出,整批性錯誤於 `reject` 之前、逐筆失敗於該筆定案後各一次。`selectByPk` 之「主鍵未命中」與「主鍵值型別與欄位不符」皆不設 `isErr` 而回傳 `null`,故不會誤發 `error`;`insert` 全數已存在、`save` 合併後內容相同、`del` 主鍵未命中、`delAll` 條件無命中亦同。
421
493
 
422
494
  **不提供`autoGenPk`**,依 T6 之例外辦理,且兩種情形皆成立:
423
495
 
@@ -432,19 +504,44 @@ T10 待處理:
432
504
 
433
505
  T7 之原子性以 `ON CONFLICT` 達成:`insert` 為單一 `INSERT ... ON CONFLICT (主鍵) DO NOTHING`,取 `rowCount` 為 `nInserted`;`save` 於 `autoInsert` 開啟時為單一 `INSERT ... ON CONFLICT (主鍵) DO UPDATE ... RETURNING (xmax = 0)`,以 `xmax` 區辨本語句插入與衝突後更新,關閉時為單一 `UPDATE ... WHERE 主鍵` 以免無中生有。其前提為主鍵具唯一約束,`createTable` 一律以 `PRIMARY KEY` 建立且無關閉選項;README 之 Upgrading 已載明資料表於他處建立而缺該約束時之補建步驟,以及補建前須先清除重複主鍵值。
434
506
 
435
- T8 無須宣告:單一行程內併發與跨行程併發皆成立,故依 T8 未於 README 宣告。原子性全由 PostgreSQL 伺服器端提供,本套件不保有行程內狀態且每次呼叫各自開啟連線,故兩種範圍於後端為同一情形。已測試依據含 2 個獨立行程對相同 20 個主鍵併發 `insert`、`nInserted` 總和為 20 且資料表僅 20 筆,以及 2 個行程對同一主鍵各寫入 5 個不同欄位、10 欄位全數保留。
507
+ T8 無須宣告:單一行程內併發與跨行程併發皆成立,故依 T8 未於 README 宣告。原子性全由 PostgreSQL 伺服器端提供,**寫入路徑不保有行程內狀態**且每次呼叫各自開啟連線,故兩種範圍於後端為同一情形。已測試依據含 2 個獨立行程對相同 20 個主鍵併發 `insert`、`nInserted` 總和為 20 且資料表僅 20 筆,以及 2 個行程對同一主鍵各寫入 5 個不同欄位、10 欄位全數保留。
508
+
509
+ 惟 `opt.useCache` 開啟時,`select` 與 `selectByPk` 之快取為行程內狀態:本行程之寫入會重設快取,他行程之寫入則不會,故跨行程下得讀到過期數據。此僅影響讀取之新鮮度,不影響上述寫入之原子性——快取不參與寫入路徑,`insert`、`save`、`del` 之判定與寫入一律由資料庫端完成。該選項預設為關閉,且已於類別註解載明適用於單程序操作。
436
510
 
437
511
  本套件另有專屬函數 `createTable`,依 §7 不在本規格範圍內,其 `pk` 參數未給時採 `opt.pk`;因 T7 之原子性倚賴主鍵之唯一約束,該參數若給予與 `opt.pk` 不同之欄位,將建出其餘函數無法正確操作之資料表,已於函數註解與 README 載明。
438
512
 
513
+
514
+ **`insertBulk`:待實作。** 單一多值 `INSERT`(不加 `ON CONFLICT`)天然即為全有全無——任一筆撞主鍵約束則整句失敗且不寫入任何一筆,正合本函數語義。惟參數數量超出上限而須分批送出時,須以交易包覆。效能是否優於 `insert` 待實測。
515
+
439
516
  ### w-orm-reladb
440
517
 
441
- 待確認撰寫
442
- <!--
443
- 主鍵欄位由建構時之 `opt.pk` 指定,預設為 `id`,**已支援由呼叫端指定**。已提供 `opt.autoGenPk` 且預設為 `true`,為本規格 T6 之 `autoGenPk` 條款的來源實作。
518
+ 主鍵欄位由建構時之 `opt.pk` 指定,預設為 `id`,**已支援由呼叫端指定**,`select` 以外之五函數皆以該欄位認定主鍵,已於 `selectByPk` 註解與 README 載明。本套件經 sequelize 操作 mssql、sqlite、mysql、mariadb、postgres 五種後端。
444
519
 
445
- **尚未完成完整合規盤查**,下列為已查證項目,其餘條款待逐項比對後補入:
520
+ 已符合 T1–T10 與六函數全部規格,無待處理項目。
446
521
 
447
- | 項目 | 現況 | 規格 |
448
- |---|---|---|
449
- | 單筆直讀函數 | 未提供,僅有 `select`、`insert`、`save`、`del`、`delAll` 五函數 | T1 新增 `selectByPk` |
450
- | `opt.autoGenPk` | 已提供,預設 `true`;為 `false` 時 `insert` 與 `save` 皆不補值 | 符合 T6,惟未帶有效主鍵時之處置待查證是否為 `reject` | -->
522
+ `opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值——為 UUIDv7 格式之 36 碼字串,具單調遞增性,主鍵接近順序遞增可減少 B-tree 索引之頁面分裂;為 `false` 時 `insert` 與 `save` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,`insert` 與 `save` 之 `option` 未提供覆寫。`del` 不受此設定影響。
523
+
524
+ **不採 T6 之例外**,仍提供 `autoGenPk`。雖然本套件之主鍵欄位得由呼叫端指定而其型別隨欄位而異,但與 w-orm-postgresql 不同者為:**本套件持有 sequelize 之 model 定義,得由 `rawAttributes[opt.pk].type.key` 讀得主鍵之型別**,故 T6 例外第 2 種情形(套件無從得知主鍵之型別或值域)於此不成立。因自動產生之值為字串,`procPk` 於補值前檢查主鍵欄位確為字串類型(`STRING`、`TEXT`、`CHAR`、`UUID`、`CITEXT`),不符者以明確訊息拋出整批性錯誤,而非任由資料庫回報型別錯誤。主鍵欄位之長度須能容納 36 碼,已於 README 載明。
525
+
526
+ `save` 之「內容相同」判定採合併後比對:以待寫入物件之非主鍵欄位淺層覆蓋現值後與現值比對,並先以 model 之欄位名濾除非欄位之鍵。合併取淺層而非深層,係為與後端整欄取代之寫入行為一致——判定基準與實際寫入行為若不一致,`nModified` 即無法忠實反映是否真的寫入。快速路徑之預讀僅用於判斷是否略過寫入,寫入內容一律由該次寫入語句自身決定。
527
+
528
+ T7 之原子性採**條件寫入配合衝突偵測與重試**。所依據之後端限制為:**sqlite 無從於單一語句內回報本次係插入或更新**——SQLite 引擎並無 PostgreSQL `xmax` 或 SQL Server `OUTPUT $action` 之對應物,`RETURNING rowid` 於 `DO UPDATE` 兩種路徑皆回傳 rowid 而無從區分,`changes()` 亦同,官方論壇所認可之唯一解法為於資料表增設輔助欄位,而本套件之 models 由呼叫端提供、無權變更其 schema;sequelize 之 `upsert` 於 sqlite 因此回傳 `created` 恆為 `null`。若堅持以單一 upsert 語句達成 T7,即無從提供 `insert` 與 `save` 所要求之 `nInserted`。
529
+
530
+ 其實作為:`insert` 逐筆 `create`,單一 INSERT 由主鍵之唯一約束原子完成「檢查主鍵不存在」與「寫入」,撞既有主鍵者視為已存在而跳過;`save` 先預讀以判斷內容是否相同,存在者發單一 `UPDATE ... WHERE 主鍵`、不存在且開啟 `autoInsert` 者發單一 `INSERT`,兩者皆為條件式原子語句而非由預讀值決定成敗。**衝突之偵測方式**為:插入撞既有主鍵以 sequelize 之 `UniqueConstraintError` 辨識(mssql、sqlite、mysql、mariadb、postgres 之 `query.js` 皆將唯一鍵衝突轉為此類別,故與 dialect 無關),更新未命中任何列以 `affectedCount` 為 `0` 辨識。**重試上限為 3 次**,重試後改走另一條路徑即收斂。主鍵之唯一約束由 models 之 `primaryKey` 提供,`createStorage` 依 model 建表,無關閉選項。
531
+
532
+ T8 已完成:README 已宣告兩範圍之適用情形。**跨行程併發成立**,且與 `opt.useStable` 無關——`insert` 與 `save` 皆以單一條件式語句寫入,勝負由資料庫之主鍵唯一約束判定,不倚賴任何行程內機制。**單一行程內併發於 `opt.useStable` 為 `true`(預設)時成立**,由套件內建之佇列(同時最大執行數 1)序列化達成;**為 `false` 時不成立**,因每個實例共用單一連線變數,於呼叫開始時開啟、結束時關閉,並行呼叫會關掉他者仍在使用之連線。
533
+
534
+ 失效之具體後果:實測同一行程內 30 個並行呼叫,sqlite 拋 `SQLITE_MISUSE: Database handle is closed` 並使 12 次整批 `reject`,mssql 回報 `ConnectionManager.getConnection was called after the connection manager was closed!` 為逐筆失敗,兩者最終皆僅寫入 29 筆(預期 30),即靜默漏寫。迴避方式為維持 `useStable` 之預設 `true`,或由呼叫端自行序列化。
535
+
536
+ 實測依據:跨行程為 2 個獨立行程對相同 20 個主鍵併發 `insert`,`nInserted` 總和為 20 且資料表僅 20 筆;再由 2 個行程對同一批 20 個主鍵各寫入 1 個不同欄位,40 次操作全數 `ok` 為 `1`、每個主鍵僅一次 `nInserted` 為 `1`,且 20 筆之兩欄位全數保留;sqlite 重複 6 輪以排除時序相依之 `SQLITE_BUSY`(平台 Windows 11、Node 24、sequelize 6、MSSQL 2022、SQLite 經 sqlite3 綁定)。
537
+
538
+ 註:此限制非受後端或其驅動層所限,而係本套件之連線管理所致——w-orm-mongodb 與 w-orm-postgresql 每次操作各自建立連線且不保有行程內狀態,故兩種範圍於後端為同一情形而無此區分。本套件因提供 `option.instance` 與 `option.transaction` 之共用連線擴充而保有行程內狀態,改為每次呼叫各自持有連線須一併調整該擴充,屬架構層級改動,暫以佇列預設開啟迴避。
539
+
540
+ T10 之實作:`EventEmitter` 採 `wsemi` 之 `evem()`(即 `eventemitter3`),其於 `'error'` 無監聽者時僅回傳 `false` 而不拋出,符合 T10.1 第 2 條;Node 內建之 `events.EventEmitter` 具該拋出語義,已停止使用。改用前曾實測其後果:呼叫端未註冊 `'error'` 監聽時,`save` 之逐筆失敗會被升級為整批 `reject`,違反 T4 之「單筆失敗不中斷整批」。全部事件收斂於共用之 `emitChange(mode, data, res)` 與 `emitError(mode, data, err)` 兩函數,try/catch 由該二處統一保證,`err` 一律經 `getErrMsg` 轉為字串。`change` 之逐筆函數以整批為單位發出一次,`save` 之逐筆插入另發 `mode` 為 `insert` 之事件;`error` 於六函數皆發出,整批性錯誤於 `reject` 之前、逐筆失敗於該筆定案後各一次。`insert` 全數已存在、`save` 合併後內容相同、`del` 主鍵未命中、`delAll` 條件無命中、`selectByPk` 查無數據皆為正常結果而不設 `isErr`,故不會誤發 `error`。
541
+
542
+ 本套件之六函數另有專屬之 `option.instance` 與 `option.transaction` 兩參數,供呼叫端共用連線實例與交易,未給時各函數自行初始化與關閉;此為向後相容之選配擴充,未給予時行為與規格所定完全相同,故不與六函數之語義衝突。另有專屬函數 `createStorage`、`genModelsByDB`、`genModelsByTabs`、`init`、`genTransaction`,依 §7 不在本規格範圍內,且皆不與六函數之概念對應。
543
+
544
+ 建構時 `opt.url` 解析失敗一律 `throw`:其屬呼叫端未履行契約,且解析失敗後各函數皆無從運作;建構為同步而無從以 `Promise.reject` 回報,拋出即為 `reject` 之同步對應。原先僅 `console.log` 而回傳未綁定各函數之 `EventEmitter`,錯誤不經任何管道抵達呼叫端,呼叫端僅得到 `w.select is not a function` 之無關訊息,與本規格「錯誤不得靜默」之通則相違。
545
+
546
+
547
+ **`insertBulk`:待實作,且效能效益最高。** 其後端之批次語句無從回報「本批有幾筆真的插入」,致 `insert` 只能逐筆寫入;改用批次語句後實測快上數量級。實作須以交易包覆——實測 mssql 因參數數量上限而由驅動層自動拆為多語句,1000 筆之批次於最後一筆撞主鍵時已有 946 筆落盤,未包交易即違反全有全無;包覆後兩後端於失敗時皆為 0 筆新增。