w-orm-lmdb 1.0.20 → 1.0.21

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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * w-orm-lmdb v1.0.20
2
+ * w-orm-lmdb v1.0.21
3
3
  * (c) 2018-2021 yuda-lyu(semisphere)
4
4
  * Released under the MIT License.
5
5
  */
@@ -1937,7 +1937,7 @@ err為錯誤訊息字串,內容與正規管道所送出者一致。
1937
1937
  <br class="clear">
1938
1938
 
1939
1939
  <footer>
1940
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 17 2026 17:58:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1940
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Aug 18 2026 12:13:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1941
1941
  </footer>
1942
1942
 
1943
1943
  <script>prettyPrint();</script>
@@ -1102,7 +1102,7 @@ export default WOrmLmdb
1102
1102
  <br class="clear">
1103
1103
 
1104
1104
  <footer>
1105
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 17 2026 17:58:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1105
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Aug 18 2026 12:13:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1106
1106
  </footer>
1107
1107
 
1108
1108
  <script>prettyPrint();</script>
package/docs/index.html CHANGED
@@ -71,7 +71,7 @@
71
71
  <br class="clear">
72
72
 
73
73
  <footer>
74
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 17 2026 17:58:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Aug 18 2026 12:13:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
75
75
  </footer>
76
76
 
77
77
  <script>prettyPrint();</script>
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "w-orm-lmdb",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "main": "dist/w-orm-lmdb.umd.js",
5
5
  "dependencies": {
6
6
  "lmdb": "^3.5.6",
7
- "mingo": "^7.2.2",
8
- "wsemi": "^1.8.70"
7
+ "mingo": "^7.2.4",
8
+ "wsemi": "^1.8.73"
9
9
  },
10
10
  "devDependencies": {
11
11
  "w-package-tools": "^1.1.12"
@@ -497,9 +497,13 @@ T8 已完成:README 已宣告單一行程內保證成立、跨行程因 `lmdb-
497
497
 
498
498
  主鍵欄位為 `id`,為無業務語義之識別碼。主鍵欄位目前**固定為 `id`,尚未支援由呼叫端指定**,已於類別註解、`selectByPk` 註解與 README 載明。
499
499
 
500
- 已符合 T1–T10 與七函數之全部規格;惟 insert 之 returnList 選項尚未實作,見下方待處理。
500
+ 已符合 T1–T10 與七函數之全部規格,無待處理項目。
501
501
 
502
- 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`。
502
+ `insert` 之 `option.returnList` 已實作。逐筆判定不須額外查詢:`insertMany({ ordered: false })` `writeErrors` `index` 即為未插入者於輸入內之位置,故由同一往返即可得——有索引者該筆 `nInserted` `0`,其餘為 `1`;`n` `ok` 恆為 `1`。回傳形式之切換為靜態,僅由 `option.returnList` 之取值決定,不因數據內容或執行結果而變。測試涵蓋兩種取值之形狀、等長保序與對位正確、同批重複主鍵僅首筆為 `1`、`filter` 計數等於聚合模式之 `nInserted`、輸入無效回 `[]`、逐筆元素鍵集合恰為 `{n, nInserted, ok}`、`change` 事件之 `res` 即實際回傳值,以及 `autoGenPk` 為 `false` 時仍為整批 `reject` 而不因本選項降為逐筆。
503
+
504
+ **T4「實例已關閉」條不適用本套件**:本套件不提供 `close()`,每次操作各自建立 MongoClient 並於 `finally` 關閉,不保有跨呼叫之行程內狀態,故無「實例已關閉」之狀態存在,亦無該條所欲防範之空轉逾時與靜默 fail-open。
505
+
506
+ 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`。
503
507
 
504
508
  `opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值——即 UUIDv7,符合 T6「如 UUID 或單調序列」,且其時間有序之特性令寫入主鍵唯一索引時之索引局部性優於純隨機字串;為 `false` 時 `insert`、`insertBulk`、`save` 與 `insertGfs` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,`insert`、`insertBulk` 與 `save` 之 `option` 未提供覆寫。`del` 不受此設定影響。
505
509
 
@@ -531,17 +535,11 @@ GridFS 之寫入係先寫 chunks 再寫 files 文件,故違反唯一索引時
531
535
 
532
536
  **GridFS 不提供 `insertBulkGfs`**:依 §7 專屬函數不在本規格範圍內,§9 亦僅要求「若與六函數概念對應則參數形狀比照」而未要求補齊所有概念。GridFS 每筆寫入為 chunks 與 files 兩階段,全有全無所須清理之對象較一般資料表複雜,且無對應之使用場景。
533
537
 
534
- 待處理:
535
-
536
- | 項目 | 現況 | 規格 |
537
- |---|---|---|
538
- | `insert` 之 `option.returnList` | 未提供 | 依 insert 規格新增;逐筆判定可由 `insertMany({ ordered: false })` 之 `writeErrors` 索引反推(有索引者 `nInserted: 0`,其餘 `1`),與輸入等長保序 |
539
-
540
538
  ### w-orm-postgresql
541
539
 
542
540
  主鍵欄位由建構時之 `opt.pk` 指定,預設為 `time`,**已支援由呼叫端指定**,`select` 以外之六函數皆以該欄位認定主鍵,已於類別註解與 `selectByPk` 註解載明。
543
541
 
544
- 已符合 T1–T10 與七函數之全部規格;惟 insert 之 returnList 選項尚未實作,見下方待處理。
542
+ 已符合 T1–T10 與七函數之全部規格,無待處理項目。
545
543
 
546
544
  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` 條件無命中亦同。
547
545
 
@@ -575,17 +573,17 @@ PostgreSQL 之協定以 int16 記綁定參數個數,上限為 65535,故單
575
573
 
576
574
  `insert` 於採用共用之分批機制前未分批,3 欄之資料表送 25000 筆即以 `bind message has 9464 parameter formats but 0 parameters` 拒絕且 0 筆寫入;改用後兩函數於同一輸入下皆分批完成 25000 筆。
577
575
 
578
- 待處理:
576
+ **`insert` 之 `option.returnList`:已實作。** 於 `INSERT ... ON CONFLICT DO NOTHING` 附加 `RETURNING 主鍵`——該子句僅回傳實際插入之列,故以其主鍵值映回輸入序即得逐筆判定;分批送出時逐批收集再串接。同批含重複主鍵者 `RETURNING` 僅回一次,故映回時以首次出現者之 `nInserted` 為 `1`,其餘為 `0`。`insertBulk` 不附此子句,因其成功時逐筆恆為已插入而無資訊量。
579
577
 
580
- | 項目 | 現況 | 規格 |
581
- |---|---|---|
582
- | `insert` `option.returnList` | 未提供 | 依 insert 規格新增;逐筆判定可於 `INSERT ... ON CONFLICT DO NOTHING` 附加 `RETURNING 主鍵` 後映回輸入序(同批重複主鍵以首次出現者為插入),分批送出時逐批映回再串接 |
578
+ 映回時之比對**依主鍵欄位之實際型別分流**:PostgreSQL 所回傳之型別未必與輸入相同,主鍵為 `TIMESTAMPTZ` 時輸入為字串而回傳為 `Date` 物件,直接比對必然落空而使每一筆皆誤判為跳過,且該錯誤於僅驗聚合計數之測試下不會顯現。故先以 `rows[0][主鍵] instanceof Date` 判定欄位型別,再令兩側共用同一正規化函數。
579
+
580
+ 不採「一律嘗試解析為日期」之通用寫法:主鍵為文字欄位而值形如 `2025` `2025-01-01` 者,將正規化為同一時刻而碰撞,令已存在者誤判為新插入。已就此加測——文字主鍵之資料表已存在 `2025` 時,送 `2025` `2025-01-01` `[0, 1]`。
583
581
 
584
582
  ### w-orm-reladb
585
583
 
586
584
  主鍵欄位由建構時之 `opt.pk` 指定,預設為 `id`,**已支援由呼叫端指定**,`select` 以外之六函數皆以該欄位認定主鍵,已於 `selectByPk` 註解與 README 載明。本套件經 sequelize 操作 mssql、sqlite、mysql、mariadb、postgres 五種後端。
587
585
 
588
- 已符合 T1–T10 與七函數之全部規格;惟 insert 之 returnList 選項尚未實作,見下方待處理。
586
+ 已符合 T1–T10 與七函數之全部規格,含 `insert``option.returnList`,無待處理項目。
589
587
 
590
588
  `opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值——為 UUIDv7 格式之 36 碼字串,具單調遞增性,主鍵接近順序遞增可減少 B-tree 索引之頁面分裂;為 `false` 時 `insert` 與 `save` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,`insert` 與 `save` 之 `option` 未提供覆寫。`del` 不受此設定影響。
591
589
 
@@ -629,8 +627,141 @@ T10 之實作:`EventEmitter` 採 `wsemi` 之 `evem()`(即 `eventemitter3`)
629
627
 
630
628
  `opt.useEncryption` 開啟且後端為 sqlite 時不自開交易,因 `@journeyapps/sqlcipher` 不支援交易;該情形下全有全無倚賴 sqlite 單一批次語句之原子性。
631
629
 
632
- 待處理:
630
+ **`insert` 之 `option.returnList`:已實作。** 逐筆判定即 `insert` 逐筆 `create` 之成敗(撞既有主鍵者以 `UniqueConstraintError` 辨識為跳過),與輸入等長、保序,開啟時僅改包裝為逐筆結果陣列而不摺成計數,零推導成本。同批重複主鍵者僅首筆 `nInserted` 為 `1`(逐筆循序送出,其後同鍵者撞唯一約束而跳過),與聚合模式之計數一致。回傳形式之切換為靜態,僅由該選項之取值決定。`insertBulk` 不提供本選項,因其成功時逐筆恆為已插入而無資訊量。測試涵蓋兩種取值之形狀、等長保序與對位正確、同批重複主鍵僅首筆為 `1`、`filter` 計數等於聚合模式之 `nInserted`、輸入無效回 `[]`、逐筆元素鍵集合恰為 `{n, nInserted, ok}`、`change` 事件之 `res` 即實際回傳值,以及 `autoGenPk` 為 `false` 時仍為整批 `reject` 而不因本選項降為逐筆。
633
631
 
634
- | 項目 | 現況 | 規格 |
635
- |---|---|---|
636
- | `insert` `option.returnList` | 未提供 | insert 規格新增;本套件之 `insert` 本即逐筆 `create` 並以 `UniqueConstraintError` 辨識既有主鍵,逐筆判定現成,僅須改包裝為逐筆結果陣列 |
632
+ **close() 後之快速失敗:已實作**(T4「實例已關閉」條)。本套件提供 `init()` 取得實例、`instance.close()` 關閉,並以 `option.instance` 供呼叫端共用,故「實例已關閉」之狀態確實存在,與 w-orm-mongodb、w-orm-mdb 之「不保有行程內狀態故不適用」不同。
633
+
634
+ 七函數入口共用 `procClosed(instance)` 守門:**判定依據為套件自身之閉包變數 `sequelize` 是否為 `null`**——`closeSequelize()` 於關閉後將其設為 `null`,故為精確訊號,不須倚賴驅動層之錯誤訊息比對。此點有其必要:sequelize `ConnectionManager.close()` 係將 `getConnection` 換成拋出**原生 `Error`** 之函數(訊息為 `ConnectionManager.getConnection was called after the connection manager was closed!`),該錯誤並非 `ConnectionError` 之實例,無從以錯誤類別辨識。
635
+
636
+ `selectByPk` 之守門**置於「主鍵值無效回 `null`」之判定之前**,否則兩者同形而使「已關閉」被誤讀為「查無資料」。
637
+
638
+ 另於 `save` 與 `del` 之逐筆錯誤處置加入 `isBatchLevelError` 判定:連線層錯誤(`Sequelize.ConnectionError` 家族,及訊息含 `connection manager was closed` 或 `SQLITE_MISUSE` 之原生錯誤)影響後續所有筆數,依 T4 之判別原則往外拋為整批性錯誤,而不降為該筆 `ok: 0`。
639
+
640
+ 修正前之實測症狀(close 後再以該實例操作):`select`、`selectByPk`、`insert`、`insertBulk`、`delAll` 已為 `reject` 且皆於 20ms 內完成而無空轉問題;惟 **`save` 與 `del` 將整批性錯誤降級為逐筆 `ok: 0` 而整批 `resolve`**——呼叫端若只看 Promise 是否 `reject` 即誤判整批成功,縱使逐筆檢查亦僅見「這幾筆失敗」而不知實例已死、後續每一筆皆不可能成功。修正後七函數於 close 後一律整批 `reject`,訊息明示 closed,並依 T10.3 發出 `error` 事件,已以測試斷言。
641
+
642
+ ### w-orm-mdb
643
+
644
+ 主鍵欄位由建構時之 `opt.pk` 指定,預設為 `id`,**已支援由呼叫端指定**,`select` 以外之六函數皆以該欄位認定主鍵,已於類別註解與 `selectByPk` 註解載明。本套件經自帶之 `connMDB.exe`(C# 源碼於 `srcCs/MdbBridge.cs`,以 Windows 內建 csc 編譯)操作 Windows 內建之 Jet 4.0 引擎,僅支援 `.mdb`(Jet4)不支援 `.accdb`。資料不經主控台管線:輸入寫暫存 JSON 檔,查詢結果由 exe 逐列串流寫 jsonl 檔。
645
+
646
+ 已符合 T1–T10 與七函數之全部規格,含 `insert` 之 `option.returnList`,無待處理項目。
647
+
648
+ **exe 為 one-shot:** 每次呼叫跑一次 exe,各自開啟與關閉連線,故本套件**不保有任何行程內狀態**。因此不提供 `option.transaction` 與 `genTransaction`——跨呼叫之交易於此架構下無從維持;交易僅用於**單次呼叫內部**(`insertBulk` 之全有全無、`delAll` 之分批刪除)。亦因無持久實例,T4 所稱「實例已關閉」之情形於本套件不存在。
649
+
650
+ `opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值(UUIDv7 格式之 36 碼字串);為 `false` 時 `insert`、`insertBulk` 與 `save` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,各寫入函數之 `option` 未提供覆寫。`del` 不受此設定影響。**不採 T6 之例外**,理由同 w-orm-reladb:本套件持有 sequelize 之 model 定義,得由 `rawAttributes[opt.pk].type.key` 讀得主鍵型別,故例外第 2 種情形不成立;`procPk` 於補值前檢查主鍵欄位確為字串類型,不符者以明確訊息拋出整批性錯誤。
651
+
652
+ **Jet 之字串比對不分大小寫,本套件據此統一各函數之命中判定。** 已實測:`WHERE [id]='ID-PETER'` 命中既存之 `id-peter`,且主鍵唯一約束亦視兩者為同一鍵而拒絕插入。`selectByPk`、`insert`、`save`、`del` 皆走 Jet 故天然一致;惟 `select` 之複雜條件係由 Jet 取回全部數據後於**記憶體 sqlite** 內過濾(因 Access 之 SQL 方言不支援 `$in`、`$nin`、`$regex` 等),而 sqlite 預設之 BINARY 定序**分**大小寫,若不處理即出現「`selectByPk('ID-X')` 回傳物件而 `select({id:'ID-X'})` 查無」之矛盾,與 `selectByPk` 規格所禁止者同型。故過濾表之字串欄位一律以 `CITEXT`(於 sqlite 即 `TEXT COLLATE NOCASE`)承載,令兩層一致。**限制**:sqlite 之 `NOCASE` 僅折疊 ASCII 之 `A-Z`,而 Jet 之定序另折疊部分非 ASCII 字元,故僅大小寫相異之非 ASCII 主鍵於兩層仍可能不一致,已於類別註解載明。
653
+
654
+ `save` 之「內容相同」判定採合併後比對:以待寫入物件之非主鍵欄位淺層覆蓋現值後與現值比對,並先以 model 之欄位名濾除非欄位之鍵。合併取淺層以與 Jet 整欄取代之寫入行為一致。**不以 UPDATE 之影響列數決定 `nModified`**:已實測 Jet 之影響列數為「符合 WHERE 之列數」而非「內容真的有變之列數」(將欄位設為與現值相同之值仍回報影響 1 列),若逕取之,`nModified` 將無法忠實反映是否真的寫入。快速路徑之預讀僅用於判斷是否略過寫入,寫入內容一律由該次寫入語句自身決定。
655
+
656
+ T7 之原子性採**條件寫入配合衝突偵測與重試**。所依據之後端限制為:**Jet 無 upsert 語句,亦無從於單一語句內回報本次係插入或更新**——其 SQL 方言無 `ON CONFLICT`/`MERGE ... OUTPUT $action` 之對應物,`ExecuteNonQuery` 僅回影響列數而不帶路徑資訊。若堅持以單一語句達成 T7,即無從提供 `insert` 與 `save` 所要求之 `nInserted`。
657
+
658
+ 其實作為:`insert` 逐筆送單一 `INSERT`,由 Jet 之主鍵唯一約束原子完成「檢查主鍵不存在」與「寫入」,撞既有主鍵者視為已存在而跳過;`save` 先預讀以判斷內容是否相同,存在者發單一 `UPDATE ... WHERE 主鍵`、不存在且開啟 `autoInsert` 者發單一 `INSERT`,兩者皆為條件式原子語句而非由預讀值決定成敗。**衝突之偵測方式**為:插入撞既有鍵以 Jet 錯誤編號 **3022** 辨識,更新未命中任何列以影響列數為 `0` 辨識。該錯誤編號取自 `OleDbError.SQLState`(Jet provider 於此欄位承載有文件之引擎錯誤編號),由 exe 以 `errorCode` 回傳;**不採 `NativeError`**(為無文件之衍生值),亦**不比對錯誤訊息文字**(隨系統語系而異)。**重試上限為 3 次**,重試後改走另一條路徑即收斂。主鍵之唯一約束須由資料表自身提供(`PRIMARY KEY`),已於類別註解載明其為 `insert` 與 `save` 之前提;測試資產之建表語句(`toolg/genTestMdbAssets.mjs`)即含之。
659
+
660
+ **3022 涵蓋主鍵與其他唯一索引之衝突,兩者無從由錯誤碼區辨**,故 `insert` 於攔得 3022 後另以 `SELECT 主鍵 WHERE 主鍵 IN (...)` 核對該些主鍵是否確實存在(分批送出,比對依上述不分大小寫之基準):存在者方視為「已存在則跳過」,不存在者即係撞及其他唯一索引,屬影響該筆以外之錯誤而以整批性錯誤 `reject`。若逕將全部 3022 視為主鍵已存在,該類寫入會被靜默吞掉——`insert` 之聚合模式無逐筆結果,呼叫端只會看到 `nInserted` 短少而無從得知原因。此核對僅於確有衝突時才發生,無衝突之常態不增加往返。
661
+
662
+ **批次一次往返:** exe 之協定支援 per-cmd 之 `stopOnError`(預設 `true`)與 per-run 之 `useTransaction`。`insert` 以 `stopOnError: false` 將全部插入語句於單次 exe 呼叫內循序送出且不因單筆衝突而中止,由逐筆結果直接得出精確之 `nInserted`;`save` 每回合以兩次呼叫完成(一次批次預讀、一次批次寫入),N 筆於無衝突時僅需 2 次呼叫而非 2N 次(exe 冷啟約 0.2 秒,逐筆各跑一次即差一個量級)。
663
+
664
+ `insert` 之 `option.returnList` **已實作**。逐筆判定即 exe 之 per-cmd results(與輸入等長、保序),開啟時僅改包裝為逐筆結果陣列而不摺成計數,零推導成本。同批重複主鍵者僅首筆 `nInserted` 為 `1`(同一連線內循序執行,其後同鍵者撞 3022 而跳過),與聚合模式之計數一致。回傳形式之切換為靜態,僅由該選項之取值決定。測試涵蓋兩種取值之形狀、等長保序與對位正確、同批重複主鍵僅首筆為 `1`、`filter` 計數等於聚合模式之 `nInserted`、輸入無效回 `[]`、逐筆元素鍵集合恰為 `{n, nInserted, ok}`、`change` 事件之 `res` 即實際回傳值,以及 `autoGenPk` 為 `false` 與整批性錯誤時仍為整批 `reject` 而不降為逐筆。
665
+
666
+ `delAll` 帶條件時**先以與 `select` 相同之記憶體過濾取得命中數據之主鍵清單,再依主鍵清單分批 `DELETE ... WHERE 主鍵 IN (...)`**(每批 200 筆,以交易包覆令任一批失敗即整體回滾)。不逕將條件送 Jet,因 Access 不支援 `$in`、`$nin`、`$regex` 等運算子,那會出現「`select` 查得到而 `delAll` 刪不到」之不一致。`find` 未給或為空物件時改送單一 `DELETE FROM`(不需主鍵清單即可清空),兩路徑之 `n` 皆取自 Jet 回報之影響列數,故為實際刪除筆數。命中數據若有主鍵值無效者,因無從精確定位而以整批性錯誤 `reject`,不靜默少刪。
667
+
668
+ T8 無須宣告:**單一行程內併發與跨行程併發皆成立**,故依 T8 未於 README 宣告。原子性全由 Jet 引擎提供,本套件每次操作各自開啟連線且不保有行程內狀態,故兩種範圍於後端為同一情形;`opt.useStable` 為 `true`(預設)時另以佇列序列化同一行程內之呼叫,為 `false` 時亦成立。
669
+
670
+ **惟達成上述保證須處理一項後端特性:** Jet 以 `.ldb` 鎖檔協調多連線存取,而本套件為 one-shot 故高頻開關連線,該鎖檔之競爭會間歇令連線開啟失敗(Jet 3734)。**未處理前已實機重現其後果**:跨行程 `save` 之逐筆結果出現 `ok: 0` 而使該行程之寫入靜默遺失(20 筆之 `value` 欄位全失),單一行程內 `useStable` 為 `false` 時亦間歇出現逐筆失敗。處理方式為:exe 於連線開啟失敗時**一律中止其後 cmds**(不受 `stopOnError` 影響——若續跑而其後 cmd 開啟成功並執行,該標記將不再代表「零語句已執行」,重試整批即會重複執行),並於「本次尚未執行任何指令」時回傳 `openFailed` 標記;`jet.mjs` 據以重試整批(線性退避,上限 8 次),且**僅限可重試之鎖競爭類錯誤碼**(3734 已實測、3050 鎖檔無法建立)——密碼錯誤(3031)、檔案不存在、格式不符(3343)等永久性開啟失敗不重試而直接回報。因 `openFailed` 保證尚未執行任何語句,重試不會重複套用寫入。重試耗盡者,`save` 與 `del` 亦以整批性錯誤 `reject` 而不降為逐筆 `ok: 0`(連線失敗影響全部數據,屬 T4 之整批性錯誤)。
671
+
672
+ 實測依據(平台 Windows 11、Node v24.19.0、Jet 4.0 32-bit,測試檔 `test/unit-concurrency.test.mjs`):2 個獨立行程對相同 20 個主鍵併發 `insert`,`nInserted` 總和為 20 且資料表僅 20 筆;2 個行程對同一批 20 個主鍵各寫入 1 個不同欄位,40 個欄位值全數保留;單一行程內 30 個並行 `insert`(`useStable` 兩種取值)之 `nInserted` 總和為 30 且資料表 30 筆;30 個並行 `insert` 於同一主鍵之 `nInserted` 總和為 1;20 個並行 `save` 於同一列(`useStable` 為 `false`)之逐筆結果全為 `ok: 1`。連續 20 輪無失敗(重試機制加入前約每 3 輪出現一次失敗)。
673
+
674
+ 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` 於七個函數與專屬函數 `createStorage` 皆發出,整批性錯誤於 `reject` 之前、逐筆失敗於該筆定案後各一次且依輸入順序,故逐筆 `error` 恆早於整批 `change`。`insert` 全數已存在、`save` 合併後內容相同、`del` 主鍵未命中、`delAll` 條件無命中、`selectByPk` 查無數據皆為正常結果而不發 `error`。
675
+
676
+ 建構時 `opt.url` 解析失敗一律 `throw`,理由同 w-orm-reladb。
677
+
678
+ 本套件另有專屬函數 `createStorage`、`genModelsByTabs`、`init`,依 §7 不在本規格範圍內,且皆不與規格所定函數之概念對應。
679
+
680
+ **修正一項會損毀使用者檔案之既有缺陷:** 原先各操作皆經 `w-orm-reladb` 之 `importModels`,其於**每一次**匯入時無條件以 `fs.writeFileSync` 重寫使用者之 model 原始檔(用意為將 `id` 補為主鍵),而本套件每次操作皆匯入一次,等同每次操作都重寫該檔。已實機重現:2 個行程併發操作時,一方正截斷檔案而另一方讀取,`models/users.js` 被截為 0 位元組,其後**所有行程**之操作全數失敗且該檔永久損毀。已改以本套件自有之 `src/importModels.mjs`,其(一)已具主鍵設定者原樣返回而完全不寫檔,(二)保留原檔之行尾字元,否則 Windows 之 CRLF 檔會因被正規化為 LF 而每次皆判定為有變更,(三)確需變更者改以「寫暫存檔後 rename」之原子寫入。修正後全部測試與併發實測後 model 檔皆維持未改動。
681
+
682
+
683
+ **`insertBulk`:已實作。** 全有全無以 **Jet 之交易(`OleDbTransaction`)包覆全部插入語句**達成:exe 於 `useTransaction` 為 `true` 時開啟交易並於任一 cmd 失敗時 `Rollback`,故 `reject` 之後資料庫狀態與呼叫前相同,**無須補償動作**。同批含重複主鍵者亦於交易內被偵測為衝突(同一連線內循序執行,其後同鍵者撞 3022)。未以 `insert` 之別名或轉呼叫實作——`insert` 為 `stopOnError: false` 且不開交易並攔截 3022 以跳過,`insertBulk` 為首錯即停且開交易而不攔截,兩者之送出方式與衝突政策各自獨立。
684
+
685
+ **交易包覆為必要而非優化**:本後端之批次本即被拆為多個 `INSERT` 語句循序送出(Access 不支援一次插入多組 `VALUES`),無交易時前段必然落盤。已實測 200 筆之批次於末筆撞主鍵時,包覆後筆數增量為 0,並以測試斷言。
686
+
687
+ **效能略優於 `insert`**(平台同上,各 3 輪取平均):
688
+
689
+ | 筆數 | `insert` → `insertBulk` |
690
+ |---|---|
691
+ | 100 | 249 ms → 238 ms |
692
+ | 1000 | 493 ms → 416 ms |
693
+ | 5000 | 1648 ms → 1225 ms |
694
+
695
+ 兩者皆為單次 exe 往返,差距來自交易內之寫入得由 Jet 緩衝後一次提交,而 `insert` 之各語句為自動提交。差距不大,提供本函數主要係為語義(全有全無)與跨套件之可替換性。
696
+
697
+ ### w-orm-lowdb
698
+
699
+ 主鍵欄位為 `id`,為無業務語義之識別碼。主鍵欄位目前**固定為 `id`,尚未支援由呼叫端指定**,已於類別註解與 `selectByPk` 註解載明。
700
+
701
+ 已符合 T1–T10 與七函數之全部規格,含 `insert` 之 `option.returnList`,無待處理項目。
702
+
703
+ T10 之實作:`EventEmitter` 採 `wsemi` 之 `evem()`(即 `eventemitter3`),其於 `'error'` 無監聽者時僅回傳 `false` 而不拋出,符合 T10.1 第 2 條;Node 內建之 `events.EventEmitter` 具該拋出語義,已停止使用(改用前本套件僅發出 `change` 而未發 `error`,故該語義尚未致災,惟依規格補齊 `error` 事件後即會使未註冊監聽之呼叫端行為分歧)。全部事件收斂於共用之 `emitChange(mode, data, res)` 與 `emitError(mode, data, err)` 兩函數,try/catch 由該二處統一保證,`err` 一律經 `getErrMsg` 轉為字串。`change` 之逐筆函數以整批為單位發出一次,`save` 之逐筆插入另發 `mode` 為 `insert` 之事件;`error` 於七函數皆發出,整批性錯誤於 `reject` 之前、逐筆失敗於該筆定案後各一次。`insert` 全數已存在、`save` 合併後內容相同、`del` 主鍵未命中、`delAll` 條件無命中、`selectByPk` 查無數據皆為正常結果而不發 `error`。
704
+
705
+ 逐筆之 `error` 事件係於**整批寫檔成功之後**依輸入順序發出,而非於逐筆處理當下:本套件之逐筆結果須待整檔寫回成功方為定案,寫檔失敗者整批 `reject` 而不回傳逐筆結果,故此順序方符合 T10.1 第 4 項「須於結果定案之後發出」,且逐筆 `error` 仍恆早於整批 `change`。
706
+
707
+ `opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值(UUIDv7 格式之 36 碼字串);為 `false` 時 `insert`、`insertBulk` 與 `save` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,各寫入函數之 `option` 未提供覆寫。`del` 不受此設定影響。**不採 T6 之例外**:主鍵欄位固定為 `id`、無業務語義、型別固定為字串,例外之兩種情形皆不成立。
708
+
709
+ `save` 之「內容相同」判定採合併後比對:以 `merge({}, 現值, 待寫入物件)` 深層合併後與現值比對,相同則不寫入。合併取深層而非淺層,係為與本後端之寫入行為一致——本套件實際寫入之內容即為該深層合併之結果,判定基準與寫入行為一致方能令 `nModified` 忠實反映是否真的寫入。
710
+
711
+ **T7 之原子性由行程內之序列化佇列達成,非條件寫入。** lowdb 之寫入為「整檔讀出、記憶體修改、整檔寫回」,其 API 無條件寫入、無比較並交換、無交易,亦無檔案鎖,故不適用 T7 所稱「條件寫入配合衝突偵測與重試」之形式;達成手段為套件自行序列化:同一資料庫檔案之全部呼叫(以 `path.resolve(opt.url)` 為鍵,含多個實例與不同 `db`/`cl` 者)一律排入同一條 Promise 鏈,依呼叫順序逐一執行,故 `insert` 之「檢查主鍵不存在與寫入」與 `save` 之「查找主鍵與更新或插入」不會與他者交錯。佇列無關閉選項,符合 T7「若須開啟特定設定才能達成,該設定必須預設開啟」。**此保證僅及於單一行程內**,跨行程之情形見下方 T8。
712
+
713
+ 佇列採 Promise 鏈而非「布林旗標配合輪詢等待」(本套件原先之作法):後者之「檢查旗標」與「設定旗標」之間隔有 `await` 而非原子之 test-and-set,多個等候者之輪詢若落在同一批 timer 觸發即會同時進入臨界區;且 `waitFun` 逾時後為 resolve 而非 reject,等滿 200 秒即照樣放行。Promise 鏈為嚴格 FIFO 且無逾時放行,由結構保證互斥。
714
+
715
+ T8 已完成:README 已宣告兩範圍之適用情形。**單一行程內併發成立**,實測依據(Windows 11、Node v24.19.0、lowdb 7,測試檔 `test/unit-concurrency.test.mjs`)含 30 個並行 `insert` 於不同主鍵之 `nInserted` 總和為 30 且資料表 30 筆、30 個並行 `insert` 於同一主鍵之總和為 1、20 個並行 `save` 於同一列各寫入不同欄位之 20 個欄位全數保留、5 個實例對同一檔案各 10 筆並行 `insert` 之總和為 50。
716
+
717
+ **跨行程併發不成立**,因佇列為 module 層變數僅同一行程內共享,lowdb 亦未使用檔案鎖。失效之具體後果有二,皆已實機重現:(一)**整批遺失**——兩行程各持寫入前之整檔快照,後寫者整檔覆蓋,前寫者之數據整筆消失;(二)**寫檔拋錯而整批 `reject`**——lowdb 之寫檔委由 `steno`,其暫存檔名由資料庫檔名推導(`db.json` 對應 `.db.json.tmp`),兩行程共用同一暫存檔,一方完成 `rename` 後另一方即因來源已不存在而拋 `ENOENT`。實測依據:兩個獨立行程對同一檔案各循序 `insert` 200 筆(共 400 筆)並以同一時間點起跑,連續 3 輪皆有一方以 `ENOENT` 中止,最終資料表分別為 200、200、281 筆,發生率 3/3。迴避方式為單一寫入者、呼叫端自備跨行程鎖,或改用具跨行程原子性之後端。
718
+
719
+ 順帶更正本套件 README 原有之一項推測(「更名回 db.json 時似乎非 rename 而是串流寫入」):`steno@4.0.2` 之寫入為 `writeFile(暫存檔)` 後 `rename`,屬原子替換,故不會讀到半截 JSON;跨行程之問題不在讀到破碎檔,而在上述兩項。
720
+
721
+ `opt.useCache` 開啟時,`select` 與 `selectByPk` 之快取為行程內狀態:本行程之寫入會重設快取,他行程之寫入則不會,故跨行程下得讀到過期數據。此僅影響讀取之新鮮度,不影響寫入路徑之判定——快取不參與寫入,各寫入函數一律重新讀檔。該選項預設關閉,且已於類別註解與 README 載明適用於單程序操作。
722
+
723
+ **T4「實例已關閉」條不適用本套件**:本套件不提供 `close()`,lowdb 無連線且無須釋放之資源,故無「實例已關閉」之狀態存在,亦無該條所欲防範之空轉逾時與靜默 fail-open。
724
+
725
+ **`save` 之逐筆失敗路徑(`ok: 0`)於本套件不可達**:逐筆處理皆為記憶體內之比對與合併而不拋錯,讀檔與寫檔之失敗影響全部筆數而屬 T4 之整批性錯誤,依規格以 `reject` 回報而不降為逐筆。該路徑仍依規格保留於實作內。`del` 之逐筆 `ok: 0` 則確實可達(未帶有效主鍵),「單筆失敗不中斷整批」已由該情形測試涵蓋。
726
+
727
+ **`insertBulk`:已實作。** 全有全無之達成無須交易亦無須補償動作:主鍵檢查(含同批重複,以隨檢查同步更新之對照表偵測)於**任何狀態修改之前**一次完成,衝突即於此拋出而尚未修改任何內容;通過檢查者以**單次** `lowdb.write()` 寫回,而該次寫檔為 `steno` 之「寫暫存檔後 rename」原子替換,不會被拆為多次送出,故不存在「前段已落盤」之情形。寫檔本身失敗者,套件於 `writeData` 內還原記憶體狀態後再拋出,令 `reject` 之後記憶體與檔案一致。未以 `insert` 之別名或轉呼叫實作——`insert` 於撞既有主鍵時跳過該筆並續行,`insertBulk` 則於首次撞及即中止整批且不寫入,兩者之流程與衝突政策各自獨立。已測試涵蓋撞既有主鍵、同批重複主鍵、201 筆之批次末筆衝突,皆斷言筆數增量為 0 且既有數據未被改動。
728
+
729
+ **效能與 `insert` 無顯著差異**(Windows 11、Node v24.19.0,各 3 輪取平均):1000 筆 7ms→6ms、5000 筆 11ms→9ms、20000 筆 30ms→26ms。因兩者皆為一次讀檔、記憶體處理、一次寫檔,差距僅在 `insert` 須逐筆查對照表。提供本函數係為語義(全有全無)與跨套件之可替換性。
730
+
731
+ **`insert` 之 `option.returnList`:已實作。** 逐筆判定即逐筆查對照表之結果(與輸入等長、保序),開啟時僅改包裝為逐筆結果陣列而不摺成計數,零推導成本。同批重複主鍵者僅首筆 `nInserted` 為 `1`(對照表隨插入同步更新),與聚合模式之計數一致。回傳形式之切換為靜態,僅由該選項之取值決定。測試涵蓋兩種取值之形狀、等長保序與對位正確、同批重複主鍵僅首筆為 `1`、`filter` 計數等於聚合模式之 `nInserted`、輸入無效回 `[]`、逐筆元素鍵集合恰為 `{n, nInserted, ok}`、`change` 事件之 `res` 即實際回傳值,以及 `autoGenPk` 為 `false` 時仍為整批 `reject` 而不因本選項降為逐筆。
732
+
733
+ **修正之既有缺陷**(皆已由測試斷言):(一)`insert` 之同批重複主鍵原會全數寫入而使資料表出現重複主鍵,因其對照表僅由既有數據建立而未隨插入更新;(二)`delAll` 之 `n` 原取全表筆數而非實際刪除筆數,違反 T3;(三)`save` 之「內容相同」原以「待寫入物件與現值全等」判定,只給部份欄位且值皆相同者會被誤報 `nModified: 1` 並實際寫檔;(四)`save` 之回傳原依路徑而異(更新路徑無 `nInserted`、插入路徑無 `nModified`),違反 T2;(五)`del` 之主鍵未命中原回 `n: 1`、未帶有效主鍵原回 `n: 1` 且未附 `err`。
734
+
735
+ ### w-orm-level
736
+
737
+ 主鍵欄位為 `id`,為無業務語義之識別碼。主鍵欄位目前**固定為 `id`,尚未支援由呼叫端指定**,已於類別註解與 `selectByPk` 註解載明。
738
+
739
+ 已符合 T1–T10 與七函數之全部規格,含 `insert` 之 `option.returnList`,無待處理項目。
740
+
741
+ T10 之實作:`EventEmitter` 採 `wsemi` 之 `evem()`(即 `eventemitter3`),其於 `'error'` 無監聽者時僅回傳 `false` 而不拋出,符合 T10.1 第 2 條。全部事件收斂於共用之 `emitChange(mode, data, res)` 與 `emitError(mode, data, err)` 兩函數,try/catch 由該二處統一保證,`err` 一律經 `getErrMsg` 轉為字串。`change` 之逐筆函數以整批為單位發出一次,`save` 之逐筆插入另發 `mode` 為 `insert` 之事件;`error` 於七函數皆發出,整批性錯誤於 `reject` 之前、逐筆失敗於該筆定案後各一次。`insert` 全數已存在、`save` 合併後內容相同、`del` 主鍵未命中、`delAll` 條件無命中、`selectByPk` 查無數據皆為正常結果而不發 `error`。改版前本套件僅發出 `change` 而完全未發 `error`。
742
+
743
+ `opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值(UUIDv7 格式之 36 碼字串,其單調遞增之特性令主鍵接近順序遞增,於 LevelDB 之有序鍵空間下寫入局部性較佳);為 `false` 時 `insert`、`insertBulk` 與 `save` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,各寫入函數之 `option` 未提供覆寫。`del` 不受此設定影響。**不採 T6 之例外**:主鍵欄位固定為 `id`、無業務語義、型別固定為字串,例外之兩種情形皆不成立。改版前係以 `genID()` 補值且無此設定。
744
+
745
+ `save` 之「內容相同」判定採合併後比對:以 `merge({}, 現值, 待寫入物件)` 深層合併後與現值比對,相同則不寫入。合併取深層而非淺層,係為與本後端之寫入行為一致——本套件實際寫入之內容即為該深層合併之結果。
746
+
747
+ **T7 之原子性由行程內之序列化佇列達成,非條件寫入。** level(abstract-level 3)之 API 無條件寫入、無比較並交換、無交易,其 `db.batch()` 雖為原子之 WriteBatch 但不具條件語義,故不適用 T7 所稱「條件寫入配合衝突偵測與重試」之形式;達成手段為套件自行序列化:同一實例之七函數(含 `select`)一律排入同一條 Promise 鏈依序執行,故 `insert` 之「檢查主鍵不存在與寫入」與 `save` 之「查找主鍵與更新或插入」不會與他者交錯。佇列無關閉選項,符合 T7「若須開啟特定設定才能達成,該設定必須預設開啟」。各函數之實作為:於臨界區內先以單次 `getMany` 取回全部相關主鍵之現值,逐筆判定後以單次 `batch` 寫入。
748
+
749
+ **佇列掛於實例層而非如 w-orm-lowdb 以資料庫路徑為鍵之 module 層對照表**,因 LevelDB 對資料庫目錄持有 OS 層之獨佔鎖(`LOCK` 檔),同一目錄無從由第二個實例或第二個行程開啟(實測後開者一律以 `LEVEL_DATABASE_NOT_OPEN` 失敗),故單一實例之佇列所涵蓋者即為該目錄之全部操作。
750
+
751
+ T8 已完成:README 已宣告兩範圍之適用情形。**單一行程內併發成立**,實測依據(Windows 11、Node v24.19.0、level 10.0.0、classic-level 3,測試檔 `test/unit-concurrency.test.mjs`)含 30 個並行 `insert` 於不同主鍵之 `nInserted` 總和為 30 且資料表 30 筆、30 個並行 `insert` 於同一主鍵之總和為 1、20 個並行 `save` 於同一列各寫入不同欄位之 20 個欄位全數保留、5 個實例對不同資料表各 10 筆並行 `insert` 之總和為 50。
752
+
753
+ **跨行程併發則為「無從發生」而非「保證失效」**:受上述獨佔鎖所限,本行程持有期間他行程一律於開啟時失敗,故不存在兩個寫入者同時操作同一目錄之情形,亦無計數失準或資料損毀之風險,後開者係明確失敗而非靜默錯亂。實測依據:本行程持有某目錄期間,子行程對同一目錄之 5 筆 `insert` 全數失敗且資料表筆數未變;本行程 `close()` 釋放鎖後同一子行程即正常寫入 5 筆。迴避方式為由單一行程負責全部存取,或改用具跨行程原子性之後端。
754
+
755
+ `opt.useCache` 開啟時,`select` 之快取為行程內狀態,本實例之寫入會重設快取;`selectByPk` 與 `delAll` 一律直讀資料庫而不經快取——前者本即為單鍵直讀,後者不得依過期數據決定刪除對象。該選項預設關閉,且已於類別註解與 README 載明適用於單程序操作。
756
+
757
+ **close() 後之快速失敗:已實作**(T4「實例已關閉」條)。本套件於建構時開啟 level 實例並保有跨呼叫之行程內狀態,且因獨佔鎖之故用畢須 `close()` 方能令該目錄再被開啟,故「實例已關閉」之狀態確實存在。七函數入口共用 `procClosed` 守門,`close()` 後再操作一律於入口立即以整批性錯誤 `reject`(訊息明示 closed,並依 T10.3 發出 `error` 事件),不與「查無資料」同形;`selectByPk` 之守門置於「主鍵值無效回 `null`」之判定之前。`waitOpen` 另作為縱深防禦:其等待條件納入終態(`closed`)並於等待後複檢實際狀態,故**開啟失敗者亦快速失敗**——此點於本後端尤其必要,目錄被他者持有而開啟失敗時 `status` 直接為終態 `closed`,改版前之 `waitFun(() => status === 'open')` 無參數等待將空轉約 200 秒並逐輪 `console.log`,且其後 `getValue` 之 catch 吞下 `LEVEL_DATABASE_NOT_OPEN` 而使 `del` 靜默回「主鍵未命中」之正常結果(fail-open)。
758
+
759
+ **`insertBulk`:已實作。** 全有全無無須交易亦無須補償動作:主鍵檢查(含同批重複,以隨檢查同步更新之對照表偵測)於**任何寫入之前**一次完成,衝突即於此拋出而尚未寫入任何一筆;通過檢查者以**單次** `client.batch()` 寫入,而 LevelDB 之 WriteBatch 為原子且不會被拆為多次送出,故不存在「前段已落盤」之情形。未以 `insert` 之別名或轉呼叫實作——`insert` 於撞既有主鍵時跳過該筆並續行,`insertBulk` 則於首次撞及即中止整批且不寫入,兩者之流程與衝突政策各自獨立。已測試涵蓋撞既有主鍵、同批重複主鍵、201 筆之批次末筆衝突,皆斷言筆數增量為 0 且既有數據未被改動。
760
+
761
+ **效能與 `insert` 無顯著差異**(Windows 11、Node v24.19.0,各 3 輪取平均,已排除資料庫開啟成本):1000 筆 6ms→5ms、5000 筆 15ms→14ms、20000 筆 52ms→55ms。因兩者皆為一次 `getMany` 與一次 `batch`,差距僅在 `insert` 須逐筆查對照表。提供本函數係為語義(全有全無)與跨套件之可替換性。
762
+
763
+ **`insert` 之 `option.returnList`:已實作。** 逐筆判定即逐筆查對照表之結果(與輸入等長、保序),開啟時僅改包裝為逐筆結果陣列而不摺成計數,零推導成本。同批重複主鍵者僅首筆 `nInserted` 為 `1`(對照表隨插入同步更新),與聚合模式之計數一致。回傳形式之切換為靜態,僅由該選項之取值決定。測試涵蓋兩種取值之形狀、等長保序與對位正確、同批重複主鍵僅首筆為 `1`、`filter` 計數等於聚合模式之 `nInserted`、輸入無效回 `[]`、逐筆元素鍵集合恰為 `{n, nInserted, ok}`、`change` 事件之 `res` 即實際回傳值,以及 `autoGenPk` 為 `false` 時仍為整批 `reject` 而不因本選項降為逐筆。
764
+
765
+ **`save` 之逐筆失敗路徑(`ok: 0`)於本套件不可達**:逐筆處理皆為記憶體內之比對與合併而不拋錯,批次取值與批次寫入之失敗影響全部筆數而屬 T4 之整批性錯誤,依規格以 `reject` 回報而不降為逐筆。該路徑仍依規格保留於實作內。`del` 之逐筆 `ok: 0` 則確實可達(未帶有效主鍵),「單筆失敗不中斷整批」已由該情形測試涵蓋。
766
+
767
+ **修正之既有缺陷**(皆已由測試斷言):(一)缺 `selectByPk`、`insertBulk`、`close` 三個函數;(二)完全未發出 `error` 事件;(三)無 `autoGenPk`;(四)`insert` 與 `save` 為「先讀出、再依讀到的結果決定寫入」而違反 T7;(五)`delAll` 之 `n` 原取全表筆數而非實際刪除筆數,違反 T3;(六)`save` 之「內容相同」原以「待寫入物件與現值全等」判定,只給部份欄位且值皆相同者會被誤報 `nModified: 1` 並實際寫入;(七)`save` 之回傳原依路徑而異(更新路徑無 `nInserted`、自動插入路徑轉呼叫 `insert` 而無 `nModified`),違反 T2,且命中而內容相同者原回 `n: 0`;(八)`del` 之主鍵未命中原回 `n: 1`、未帶有效主鍵原回 `n: 1` 且未附 `err`;(九)close 或開啟失敗後之空轉約 200 秒與 `del` 之靜默 fail-open(見上方 T4 條)。