w-orm-lmdb 1.0.18 → 1.0.20
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 +44 -2
- package/dist/w-orm-lmdb.umd.js +2 -2
- package/dist/w-orm-lmdb.umd.js.map +1 -1
- package/docs/WOrmLmdb.html +303 -12
- package/docs/WOrmLmdb.mjs.html +184 -15
- package/docs/index.html +2 -2
- package/g-basic.mjs +43 -1
- package/package.json +1 -1
- package/src/WOrmLmdb.mjs +182 -13
- package/test/unit-closed.test.mjs +108 -0
- package/test/unit-insertbulk.test.mjs +205 -0
- package/test/unit-returnlist.test.mjs +195 -0
- package/toolg/gDistRollup.mjs +1 -1
- package//350/263/207/346/226/231/345/272/253/345/207/275/346/225/270/345/233/236/345/202/263/345/256/232/347/276/251/350/250/210/347/256/227.md +228 -42
- package/dist/req-mingo.umd.js +0 -7
- package/dist/req-mingo.umd.js.map +0 -1
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
規格與各套件之儲存後端無關:後端可為 key-value store、document database、關聯式資料庫或其他形式。規格只規範**對外行為**——函數名稱、參數、回傳結構、各欄位語義、錯誤處置與原子性要求;**不規範達成手段**,各套件依其後端能力自行實作。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
適用函數共七個,**皆為必備**:`select`、`selectByPk`、`insert`、`insertBulk`、`save`、`del`、`delAll`。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
其中 `insertBulk` 之語義與 `insert` **不同**(衝突政策相異),非同一操作之加速版;其於某些後端不會比 `insert` 快,**仍須提供**——呼叫端之所以能於不同套件間替換,倚賴的正是各套件提供同一組函數。
|
|
10
|
+
|
|
11
|
+
套件得另有後端專屬函數(如建表、檔案儲存),不在本規格範圍內,惟不得與規格所定函數之語義衝突。
|
|
12
|
+
|
|
13
|
+
專屬函數若與規格所定函數之概念對應(插入/單筆直讀/刪除/條件刪除),則**參數形狀亦須比照**,不只回傳結構:收「帶主鍵之數據物件或陣列」者回等長陣列,收單一主鍵者回單一物件。僅對齊回傳值而參數各行其是,會讓同一概念出現兩種呼叫方式,與本規格「呼叫端不須為個別套件寫特例」之目的相違。若某概念在該後端無法滿足本規格之要求(如無法原子取代內容而不能提供 `save`),則不提供該函數,並於 README 說明原因與替代作法;不得提供一個語義較弱而同名的版本。
|
|
10
14
|
|
|
11
15
|
---
|
|
12
16
|
|
|
@@ -24,13 +28,15 @@
|
|
|
24
28
|
|
|
25
29
|
### T2 回傳型別與鍵集合固定
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
回傳之**型別與鍵集合**由「函數」與「呼叫端明確給定之 option 取值」唯一決定;同一組合下不論走哪條內部路徑、不論數據內容為何,型別與鍵集合完全相同。呼叫端不得需要先判斷某個鍵是否存在。
|
|
28
32
|
|
|
29
33
|
- 計數欄位(`n`、`nInserted`、`nModified`、`nDeleted`)在該函數的規格表中一旦列出即**恆出現**,無對應行為時填 `0`,不得省略。
|
|
30
34
|
- 逐筆函數(`save`、`del`)恆回傳與輸入**等長**之陣列,即使輸入為單一物件亦回傳長度為 `1` 之陣列。
|
|
31
|
-
- 整批函數(`insert`、`delAll
|
|
35
|
+
- 整批函數(`insert`、`insertBulk`、`delAll`)恆回傳單一物件;惟 `insert` 於 `option.returnList` 開啟時改回逐筆陣列,見該函數規格。
|
|
32
36
|
- 唯一的例外是 `err`:僅在該筆 `ok` 為 `0` 時出現,`ok` 為 `1` 時不得出現。
|
|
33
37
|
|
|
38
|
+
option 對回傳形式之切換須為**靜態**:呼叫點寫死取值即知回傳形狀,不得設計成依執行結果或數據內容而變。共用之結果處理程式碼不得跨「不同 option 取值之呼叫點」混用——兩種取值即兩份契約。
|
|
39
|
+
|
|
34
40
|
### T3 `n` 之定義
|
|
35
41
|
|
|
36
42
|
`n` 為**本次操作於資料庫端所命中或涉及之筆數**。逐函數定義如下,跨套件不得有第二種解讀:
|
|
@@ -38,12 +44,15 @@
|
|
|
38
44
|
| 函數 | `n` |
|
|
39
45
|
|---|---|
|
|
40
46
|
| `insert` | 輸入筆數(陣列化後之長度),代表本次嘗試插入之基準 |
|
|
47
|
+
| `insertBulk` | 輸入筆數,同 `insert` |
|
|
41
48
|
| `save`(逐筆) | 主鍵命中筆數,`0` 或 `1`。命中(不論內容有無變更)或經插入而產生皆為 `1` |
|
|
42
49
|
| `del`(逐筆) | 主鍵命中筆數,`0` 或 `1` |
|
|
43
50
|
| `delAll` | 實際刪除筆數,恆等於 `nDeleted` |
|
|
44
51
|
|
|
45
52
|
`n` 不得取全表筆數,不得為與結果無關之常數。
|
|
46
53
|
|
|
54
|
+
`insert` 於 `option.returnList` 開啟時之逐筆元素,其 `n` 恆為 `1`——該筆主鍵或為命中既有、或經插入而產生,對齊本表 `save`(逐筆)之定義;資訊由 `nInserted` 承載。
|
|
55
|
+
|
|
47
56
|
### T4 `ok` 與錯誤處置
|
|
48
57
|
|
|
49
58
|
`ok` 僅有兩值:`1` 成功、`0` 該筆失敗。
|
|
@@ -51,10 +60,12 @@
|
|
|
51
60
|
- 成功路徑一律 `ok: 1`。**不得**由驅動層之確認旗標(如 MongoDB 之 `acknowledged`、SQL driver 之連線狀態)直接推導——該類旗標會產生沒有錯誤訊息的 `ok: 0`,呼叫端無從處理。若確實需要反映驅動層的未確認狀態,須將其視為該筆失敗,回 `ok: 0` 並附 `err`。
|
|
52
61
|
- `ok: 0` 僅出現於逐筆函數(`save`、`del`),且**必附 `err` 字串**說明原因。
|
|
53
62
|
- 單筆失敗**不中斷整批**:其餘筆數照常處理,整批仍 resolve,該筆以 `ok: 0` 回報。
|
|
54
|
-
-
|
|
63
|
+
- 整批性錯誤(連線失敗、實例已關閉、參數型別錯誤、資料表不存在、權限不足等)以 `Promise.reject` 拋出,不進入逐筆結果。
|
|
55
64
|
|
|
56
65
|
判別「該筆失敗」與「整批性錯誤」的原則:錯誤只影響該筆資料者為前者,影響後續所有筆數者為後者。
|
|
57
66
|
|
|
67
|
+
**實例已關閉屬整批性錯誤,且須於各函數入口快速失敗。** 關閉為終態,後續操作必然失敗,故不得空轉等待至逾時,亦不得以正常空結果(`null`、未命中、空陣列)回應——後者使「已關閉」與「查無資料」同形,去重類呼叫端將把每筆判為未存在而靜默 fail-open(整批重送下游昂貴動作),此為該類機制最不能發生的失效方向。
|
|
68
|
+
|
|
58
69
|
### T5 輸入無效之處置
|
|
59
70
|
|
|
60
71
|
「輸入無效」指傳入之 `data` 既非有效物件亦非有效陣列。此時不視為錯誤,回傳空結果:
|
|
@@ -62,6 +73,8 @@
|
|
|
62
73
|
| 函數 | 回傳 |
|
|
63
74
|
|---|---|
|
|
64
75
|
| `insert` | `{ n: 0, nInserted: 0, ok: 1 }` |
|
|
76
|
+
| `insert`(`returnList` 開啟) | `[]` |
|
|
77
|
+
| `insertBulk` | `{ n: 0, nInserted: 0, ok: 1 }` |
|
|
65
78
|
| `save` | `[]` |
|
|
66
79
|
| `del` | `[]` |
|
|
67
80
|
|
|
@@ -73,12 +86,12 @@
|
|
|
73
86
|
|
|
74
87
|
| `autoGenPk` | 行為 |
|
|
75
88
|
|---|---|
|
|
76
|
-
| `true`(預設) | `insert` 與 `save` 於輸入未帶有效主鍵時,由套件自動產生主鍵值後再寫入 |
|
|
89
|
+
| `true`(預設) | `insert`、`insertBulk` 與 `save` 於輸入未帶有效主鍵時,由套件自動產生主鍵值後再寫入 |
|
|
77
90
|
| `false` | 套件一律不產生主鍵值,主鍵須由呼叫端於寫入前自備 |
|
|
78
91
|
|
|
79
92
|
`autoGenPk: false` 之定位為**依賴注入**:主鍵的產生規則改由呼叫端掌握(如採用外部發號器、以業務欄位組合、沿用上游系統既有識別碼),套件不介入。採用此設定後,**主鍵之唯一性、格式與是否與既有資料衝突,皆由呼叫端自負**;套件不做補救,亦不因主鍵不合預期而額外檢查或修正。
|
|
80
93
|
|
|
81
|
-
`autoGenPk`
|
|
94
|
+
`autoGenPk` 為建構層設定,**不得於各寫入函數之 `option` 逐次覆寫**。主鍵由誰產生是資料所有權的歸屬,屬整個資料表的政策;若逐次可改,同一資料表將混入兩種來源之主鍵而難以追溯。
|
|
82
95
|
|
|
83
96
|
`autoGenPk` 為 `true` 時,自動產生之主鍵值須具足夠唯一性(如 UUID 或單調序列),不得採用可預期碰撞之來源。
|
|
84
97
|
|
|
@@ -92,7 +105,7 @@
|
|
|
92
105
|
|
|
93
106
|
採用本例外者為**完全不提供該設定**,而非提供一個恆為 `false` 之設定——後者會讓呼叫端誤以為可切換,且徒增一個讀了文件仍不能用的選項。
|
|
94
107
|
|
|
95
|
-
此類套件之 `insert` 與 `save` 於輸入未帶有效主鍵時,一律以 `Promise.reject` 拋出整批性錯誤,行為等同 `autoGenPk` 為 `false`;主鍵之唯一性與格式同樣由呼叫端自負。
|
|
108
|
+
此類套件之 `insert`、`insertBulk` 與 `save` 於輸入未帶有效主鍵時,一律以 `Promise.reject` 拋出整批性錯誤,行為等同 `autoGenPk` 為 `false`;主鍵之唯一性與格式同樣由呼叫端自負。
|
|
96
109
|
|
|
97
110
|
採用本例外之套件,須於下方「各套件符合狀態」載明其主鍵欄位、所依據之情形與理由;未載明者一律適用 `autoGenPk` 預設為 `true` 之規定。
|
|
98
111
|
|
|
@@ -109,6 +122,16 @@
|
|
|
109
122
|
|
|
110
123
|
達成手段不限,由各套件依後端能力選擇(唯一索引、`ON CONFLICT`、條件寫入、寫交易、比較並交換等)。
|
|
111
124
|
|
|
125
|
+
**亦得以「條件寫入配合衝突偵測與重試」達成**,惟須同時滿足下列三項:
|
|
126
|
+
|
|
127
|
+
1. **每次寫入本身為單一條件式原子語句**——該次寫入成立與否,由資料庫依當下之既有資料判定(如主鍵之唯一約束、`WHERE 主鍵` 之比對),而非由先前讀到的值決定。
|
|
128
|
+
2. **預測錯誤須可被偵測**——插入時撞及既有主鍵、更新時未命中任何列,皆須能由回傳值或錯誤類別辨識。
|
|
129
|
+
3. **偵測到衝突須重試**,重試次數有限且能收斂至正確結果。
|
|
130
|
+
|
|
131
|
+
此形式與本節所禁止者之分野在於:所禁止者為「讀到的值決定了寫入的內容或成敗」,而本形式之讀取僅用於**選擇發出哪一條語句**;選錯不會產生錯誤結果,只會觸發重試。故縱使讀取值於語句發出前已過期,最終結果仍與該次操作單獨執行時相同。採用本形式者,其讀取結果**不得用於決定寫入內容**,此與 `save` 之預讀規定一致。
|
|
132
|
+
|
|
133
|
+
本形式係為**後端無法於單一語句內回報「本次係插入或更新」**而設。此類後端若堅持以單一 upsert 語句達成本節要求,即無從提供 `insert` 與 `save` 所要求之 `nInserted`,兩項要求將互斥;容許本形式方能兼顧。採用本形式之套件,須於「各套件符合狀態」載明其後端之限制、衝突之偵測方式與重試上限。
|
|
134
|
+
|
|
112
135
|
**若某後端須開啟特定設定才能達成本要求,該設定必須預設開啟。** 不得以「預設關閉、要正確性請自行開啟」的形式交付——`insert` 的「已存在則跳過」與 `save` 的「不遺失更新」是本規格定義的核心語義,不是選配。若開啟該設定對既有資料有前提(例如建立唯一索引前須先清除重複資料),須於 README 載明升級步驟。
|
|
113
136
|
|
|
114
137
|
`del` 不在本要求內:刪除為冪等操作,重複刪除不產生額外副作用。惟 `nDeleted` 在併發下可能不精確,套件得選擇以原子操作取得精確值。
|
|
@@ -179,7 +202,7 @@ ee.emit('change', mode, data, res)
|
|
|
179
202
|
|
|
180
203
|
#### T10.3 `error` 事件
|
|
181
204
|
|
|
182
|
-
|
|
205
|
+
操作發生錯誤時發出。**七個函數皆須發出**,含讀取函數。
|
|
183
206
|
|
|
184
207
|
```
|
|
185
208
|
ee.emit('error', mode, data, err)
|
|
@@ -208,7 +231,7 @@ ee.emit('error', mode, data, err)
|
|
|
208
231
|
change (mode, data, res) 資料實際異動成功後,整批一次
|
|
209
232
|
error (mode, data, err) 整批性錯誤於 reject 前;逐筆失敗於該筆定案後,每筆一次
|
|
210
233
|
|
|
211
|
-
mode
|
|
234
|
+
mode 規格所定函數之名稱;專屬函數取其自身名稱
|
|
212
235
|
data 輸入數據,無者為 null
|
|
213
236
|
err 字串
|
|
214
237
|
```
|
|
@@ -239,10 +262,12 @@ err 字串
|
|
|
239
262
|
|
|
240
263
|
「命中」之判定基準須與 `insert`、`save`、`del` 內對既有數據之認定一致,不得出現 `selectByPk` 回傳物件而 `insert` 仍視為不存在之矛盾。
|
|
241
264
|
|
|
242
|
-
### insert(data) → `Promise<{ n, nInserted, ok }
|
|
265
|
+
### insert(data, option) → `Promise<{ n, nInserted, ok } | Array<{ n, nInserted, ok }>>`
|
|
243
266
|
|
|
244
267
|
**僅於主鍵不存在時寫入,已存在者跳過且不覆寫。**
|
|
245
268
|
|
|
269
|
+
預設(`option.returnList` 未給或為 `false`)回傳單一聚合物件:
|
|
270
|
+
|
|
246
271
|
| 欄位 | 值 |
|
|
247
272
|
|---|---|
|
|
248
273
|
| `n` | 輸入筆數 |
|
|
@@ -254,6 +279,78 @@ err 字串
|
|
|
254
279
|
- 須符合 T7 之原子性要求。
|
|
255
280
|
- 輸入無效見 T5。
|
|
256
281
|
|
|
282
|
+
#### `option.returnList`(預設 `false`):改回逐筆結果
|
|
283
|
+
|
|
284
|
+
聚合計數回答「有幾筆是新的」,回答不了「**是哪幾筆**」——而後者正是去重之產出物(下游僅對新資料執行昂貴動作)。缺少逐筆結果時,呼叫端唯一出路是把批次退化為單筆呼叫、以 `nInserted === 1` 反推,令套件之批次寫入優化完全失效。本選項將函數內部本就算出之逐筆判定交出來。
|
|
285
|
+
|
|
286
|
+
`returnList` 為 `true` 時,回傳**與輸入等長、保序**之逐筆陣列,元素沿用本規格逐筆結果之家族形狀:
|
|
287
|
+
|
|
288
|
+
| 情形 | `n` | `nInserted` | `ok` |
|
|
289
|
+
|---|---|---|---|
|
|
290
|
+
| 該筆已插入 | `1` | `1` | `1` |
|
|
291
|
+
| 該筆主鍵已存在而跳過(含同批重複之非首筆) | `1` | `0` | `1` |
|
|
292
|
+
|
|
293
|
+
- 元素之 `n` 恆為 `1`(見 T3),`ok` 恆為 `1`——`insert` 之任何錯誤皆屬整批性錯誤而 `reject`(T4 不變),故逐筆元素不出現 `ok: 0` 與 `err`。
|
|
294
|
+
- 不變式:陣列長度等於輸入筆數;`filter(v => v.nInserted === 1).length` 等於預設模式之 `nInserted`。
|
|
295
|
+
- 輸入無效時回傳 `[]`(對齊 `save`/`del` 之 T5 規定)。
|
|
296
|
+
- 事件依 T10:`change` 之 `res` 即本次實際回傳值。
|
|
297
|
+
- 選擇此形狀而非另創布林陣列欄位,係因「逐筆結果」於本規格已有既定形狀(`save`/`del` 之元素);沿用同一形狀,呼叫端得以同一段程式碼處理各函數之逐筆結果,且自單筆迴圈遷移至批次時判準(`nInserted === 1`)不變。
|
|
298
|
+
|
|
299
|
+
**其餘函數不提供本選項**:`save` 與 `del` 之回傳本即與輸入等長、保序之逐筆陣列,資訊已在(`rs[i].nInserted === 1` 即第 i 筆是否插入、`rs[i].nModified === 1` 即第 i 筆是否實際寫入),再加即為冗餘且無處可掛;`insertBulk` 成功時逐筆恆為已插入(零資訊量)、失敗時整批 `reject` 而無部分結果,兩種情況皆無資訊可回。
|
|
300
|
+
|
|
301
|
+
### insertBulk(data) → `Promise<{ n, nInserted, ok }>`
|
|
302
|
+
|
|
303
|
+
批次插入,**全批視為一個單位**:全部插入成功,或一筆都不寫入。
|
|
304
|
+
|
|
305
|
+
| 欄位 | 值 |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `n` | 輸入筆數 |
|
|
308
|
+
| `nInserted` | 實際插入筆數,成功時**恆等於 `n`** |
|
|
309
|
+
| `ok` | `1` |
|
|
310
|
+
|
|
311
|
+
- **全有全無**:任一筆之主鍵已存在,即以 `Promise.reject` 拋出整批性錯誤,且**不得寫入任何一筆**。
|
|
312
|
+
- 同批含重複主鍵者亦視為衝突,整批 `reject`。
|
|
313
|
+
- 不提供逐筆結果,故不出現 `ok: 0` 與 `err`;需要逐筆處置者改用 `insert`。
|
|
314
|
+
- 主鍵補值依 T6,與 `insert` 相同;主鍵檢查須於任何寫入之前一次完成。
|
|
315
|
+
- 事件依 T10 發出,`mode` 取 `'insertBulk'`。
|
|
316
|
+
- 輸入無效見 T5。
|
|
317
|
+
|
|
318
|
+
回傳之鍵集合與 `insert` **完全相同**。`nInserted` 於成功時雖恆等於 `n` 而無額外資訊,仍須保留,令呼叫端得共用同一段結果處理程式碼。
|
|
319
|
+
|
|
320
|
+
#### 與 `insert` 之關係
|
|
321
|
+
|
|
322
|
+
**本函數非 `insert` 之加速版,兩者衝突政策不同:**
|
|
323
|
+
|
|
324
|
+
| 情形 | `insert` | `insertBulk` |
|
|
325
|
+
|---|---|---|
|
|
326
|
+
| 主鍵已存在 | 跳過該筆,整批 `ok: 1` | **整批 `reject`,且不寫入任何一筆** |
|
|
327
|
+
| 同批重複主鍵 | 僅首筆計入 `nInserted` | 視為衝突,整批 `reject` |
|
|
328
|
+
| `nInserted` | 實際插入筆數,`0 ≤ nInserted ≤ n` | 成功時恆等於 `n` |
|
|
329
|
+
| 適用場景 | 一般寫入,資料表可能已有既有資料 | 批次匯入,本即預期無衝突 |
|
|
330
|
+
|
|
331
|
+
**確無衝突時,兩者之可觀察結果完全相同**(皆回 `{ n, nInserted: n, ok: 1 }`);差異僅於有衝突時顯現。故呼叫端得於「已知無衝突」之前提下替換,而該前提若不成立,`insertBulk` 會以 `reject` 明白告知。
|
|
332
|
+
|
|
333
|
+
#### 效能不在本規格之保證範圍
|
|
334
|
+
|
|
335
|
+
本函數之存在理由為**語義**(全有全無),而非速度。是否較 `insert` 快,取決於該後端之批次寫入能力:
|
|
336
|
+
|
|
337
|
+
- 部分後端之批次語句較逐筆寫入快上數量級,此時本函數兼具語義與效能之利。
|
|
338
|
+
- 部分後端之 `insert` 本即以單次往返完成且計數精確,此時本函數不會更快,仍須提供。
|
|
339
|
+
|
|
340
|
+
**不得因「於本後端不會更快」而不提供本函數。** 呼叫端之所以能於不同套件間替換,倚賴的是各套件提供同一組函數;缺一個即迫使呼叫端為個別套件改寫呼叫,與本規格「呼叫端不須為個別套件寫特例」之目的相違。各套件須於「各套件符合狀態」載明其實作方式與是否具效能優勢,供呼叫端判斷何時值得改用。
|
|
341
|
+
|
|
342
|
+
#### 實作要求
|
|
343
|
+
|
|
344
|
+
**一、不得以別名或轉呼叫 `insert` 之方式提供。** 兩者衝突政策不同——別名之下主鍵衝突時不會 `reject` 而是靜默跳過,同一段呼叫端程式碼於不同套件上行為分歧,且分歧只在衝突發生時顯現。此屬 §14 所禁止之「語義較弱而同名的版本」,且為其中最難察覺者:呼叫端之錯誤前提於該套件上永遠不會浮現。
|
|
345
|
+
|
|
346
|
+
**二、得以逐筆寫入實作**,只要整體滿足全有全無。批次語句非本函數之必要條件;於批次不會更快之後端,包在單一交易內逐筆條件寫入亦屬合格實作。
|
|
347
|
+
|
|
348
|
+
**三、全有全無為獨立於 T7 之額外要求。** T7 只保證每一筆之「檢查主鍵不存在」與「寫入」為原子,不保證整批之全有全無。**若寫入於該後端會被拆為多次送出,須以交易包覆並於失敗時回滾**,確保 `reject` 之後資料庫狀態與呼叫前相同。
|
|
349
|
+
|
|
350
|
+
此非理論顧慮:部分驅動層因參數數量上限而自動將單次批次拆為多個語句,前段可能已落盤。呼叫端收到 `reject` 卻已有部分資料寫入,將無從判斷該重試或該清理,比直接失敗更難處理。
|
|
351
|
+
|
|
352
|
+
**四、後端不支援交易者,得以補償動作達成**:偵測到衝突後,刪除本次呼叫已寫入之筆數,再 `reject`。此法於正常運作下可使狀態回復如初,惟行程於補償途中中止則可能殘留,**須於 README 與「各套件符合狀態」明白載明此限制**。有交易可用者一律優先採用交易。
|
|
353
|
+
|
|
257
354
|
### save(data, option) → `Promise<Array<{ n, nInserted, nModified, ok }>>`
|
|
258
355
|
|
|
259
356
|
以主鍵為準更新既有數據,未給之欄位保留;主鍵不存在且 `option.autoInsert` 為 `true`(預設)時改為插入。
|
|
@@ -317,6 +414,9 @@ select(find) → [ {...}, {...} ] 無符合為 []
|
|
|
317
414
|
selectByPk(pk) → {...} | null
|
|
318
415
|
|
|
319
416
|
insert(data) → { n, nInserted, ok }
|
|
417
|
+
insert(data, { returnList: true })
|
|
418
|
+
→ [ { n, nInserted, ok }, ... ] 與輸入等長保序, 逐筆ok恆1
|
|
419
|
+
insertBulk(data) → { n, nInserted, ok } 衝突即整批reject且不寫入任何一筆
|
|
320
420
|
save(data, option) → [ { n, nInserted, nModified, ok }, ... ]
|
|
321
421
|
del(data) → [ { n, nDeleted, ok }, ... ]
|
|
322
422
|
delAll(find) → { n, nDeleted, ok }
|
|
@@ -332,6 +432,8 @@ delAll(find) → { n, nDeleted, ok }
|
|
|
332
432
|
| 要判斷什麼 | 看什麼 | 不要看什麼 |
|
|
333
433
|
|---|---|---|
|
|
334
434
|
| 這批有幾筆是新資料 | `insert` 之 `nInserted` | `n`(那是輸入筆數) |
|
|
435
|
+
| 這批裡**哪幾筆**是新資料 | `insert` 開啟 `returnList` 後逐筆之 `nInserted === 1` | 聚合之 `nInserted`(那只有數量沒有身分) |
|
|
436
|
+
| 這批有沒有撞到既有主鍵 | `insertBulk` 是否 `reject` | `nInserted`(成功時恆等於 `n`,不帶額外資訊) |
|
|
335
437
|
| 這筆是不是新資料 | `save` 之 `nInserted === 1` | `n`(命中即為 1,插入與更新皆是) |
|
|
336
438
|
| 這筆內容有沒有實際寫入 | `save` 之 `nModified === 1` | `n` |
|
|
337
439
|
| 這筆主鍵原本存不存在 | `save` 之 `n === 1` 且 `nInserted === 0` | 單看 `n` |
|
|
@@ -345,11 +447,11 @@ delAll(find) → { n, nDeleted, ok }
|
|
|
345
447
|
|
|
346
448
|
新套件納入 `w-orm-*` 系列前,逐項確認:
|
|
347
449
|
|
|
348
|
-
1.
|
|
450
|
+
1. 七個函數皆存在,單筆直讀函數依 T1 名為 `selectByPk`,主鍵欄位名已於函數註解與「各套件符合狀態」載明。
|
|
349
451
|
2. `select` 恆回陣列且不含資料庫內部欄位,`selectByPk` 未命中回 `null`。
|
|
350
|
-
3. 每個函數之計數欄位恆出現,無對應行為時填 `0`;`err` 僅隨 `ok: 0`
|
|
351
|
-
4. `n` 依 T3
|
|
352
|
-
5. `insert` 與 `save` 符合 T7 原子性要求;若倚賴某項設定達成,該設定預設開啟,且 README
|
|
452
|
+
3. 每個函數之計數欄位恆出現,無對應行為時填 `0`;`err` 僅隨 `ok: 0` 出現。以 option 靜態切換回傳形式者(如 `insert` 之 `returnList`),同一取值下形狀恆定。
|
|
453
|
+
4. `n` 依 T3 定義,五個函數各自的基準寫進函數註解。
|
|
454
|
+
5. `insert` 與 `save` 符合 T7 原子性要求;若倚賴某項設定達成,該設定預設開啟,且 README 載明升級前提。採「條件寫入配合衝突偵測與重試」達成者,每次寫入皆為單一條件式原子語句、衝突可被偵測且重試能收斂,讀取結果未用於決定寫入內容,並已於「各套件符合狀態」載明後端之限制、衝突之偵測方式與重試上限。
|
|
353
455
|
6. 成功路徑 `ok` 恆為 `1`,不由驅動層旗標推導;`ok: 0` 必附 `err`;單筆失敗不中斷整批。
|
|
354
456
|
7. `save` 之「內容相同」採**合併後比對**,基準寫進註解。
|
|
355
457
|
8. `del` 對未帶有效主鍵者不送查詢,直接回 `ok: 0` + `err`。
|
|
@@ -357,7 +459,8 @@ delAll(find) → { n, nDeleted, ok }
|
|
|
357
459
|
10. 提供 `opt.autoGenPk` 且預設為 `true`;為 `false` 時不產生主鍵值,未帶有效主鍵者 `reject`;不得於 `option` 逐次覆寫。符合 T6 例外而完全不提供該設定者(主鍵具業務語義,或套件無從得知主鍵型別),已於「各套件符合狀態」載明所依據之情形與理由。
|
|
358
460
|
11. README 依 T8 宣告併發保證範圍,無法保證者載明後果、實測依據與迴避方式。
|
|
359
461
|
12. 依 T10 發出 `change` 與 `error` 事件,參數形狀為 `(mode, data, res)` 與 `(mode, data, err)`;所採用之 `EventEmitter` 無「`'error'` 於無監聽者時拋出」之語義;每一處 `emit` 皆以 try/catch 包覆;正常結果不發出 `error`;事件所送出之資訊皆另有正規管道,移除全部事件後呼叫端仍能取得完整資訊。
|
|
360
|
-
13.
|
|
462
|
+
13. `insertBulk` 語義為「衝突即整批 reject 且不寫入任何一筆」而非 `insert` 之加速版,未以別名或轉呼叫實作;寫入於該後端會被拆為多次送出者已以交易包覆並驗證回滾,無交易可用而採補償動作者已載明其限制;實作方式與是否具效能優勢已於「各套件符合狀態」載明。
|
|
463
|
+
14. 測試須涵蓋:同批重複主鍵、主鍵不存在、合併後內容相同、只給部份欄位且值相同、`autoInsert` 兩種取值、`autoGenPk` 兩種取值(含 `false` 且未帶主鍵須 `reject`)、單筆失敗、未帶有效主鍵、`delAll` 帶條件且僅部份命中,以及**同一操作於有註冊與未註冊 `error` 監聽兩種情況下回傳值完全相同**。提供 `insertBulk` 者另須涵蓋:無衝突時 `nInserted` 等於 `n`、撞既有主鍵時整批 `reject`、同批重複主鍵時整批 `reject`,以及**失敗後資料表無任何新增**。`insert` 之 `returnList` 另須涵蓋:兩種取值下之形狀、開啟時與輸入等長保序且對位正確、同批重複主鍵僅首筆 `nInserted` 為 `1`、`filter` 計數等於聚合模式之 `nInserted`、輸入無效回 `[]`、逐筆元素鍵集合恰為 `{n, nInserted, ok}`。
|
|
361
464
|
|
|
362
465
|
---
|
|
363
466
|
|
|
@@ -367,9 +470,9 @@ delAll(find) → { n, nDeleted, ok }
|
|
|
367
470
|
|
|
368
471
|
主鍵欄位為 `id`,為無業務語義之識別碼。主鍵欄位目前**固定為 `id`,尚未支援由呼叫端指定**。
|
|
369
472
|
|
|
370
|
-
已符合 T1–T10
|
|
473
|
+
已符合 T1–T10 與七函數全部規格,無待處理項目。
|
|
371
474
|
|
|
372
|
-
T10 之實作:`EventEmitter` 採 `wsemi` 之 `evem()`(即 `eventemitter3`),本即無「`'error'` 於無監聽者時拋出」之語義,符合 T10.1 第 2 條且無須改動。全部事件收斂於共用之 `emitChange(mode, data, res)` 與 `emitError(mode, data, err)` 兩函數,try/catch 由該二處統一保證,`err` 一律經 `getErrMsg` 轉為字串。`change` 之逐筆函數以整批為單位發出一次,`save` 之逐筆插入另發 `mode` 為 `insert` 之事件;`error`
|
|
475
|
+
T10 之實作:`EventEmitter` 採 `wsemi` 之 `evem()`(即 `eventemitter3`),本即無「`'error'` 於無監聽者時拋出」之語義,符合 T10.1 第 2 條且無須改動。全部事件收斂於共用之 `emitChange(mode, data, res)` 與 `emitError(mode, data, err)` 兩函數,try/catch 由該二處統一保證,`err` 一律經 `getErrMsg` 轉為字串。`change` 之逐筆函數以整批為單位發出一次,`save` 之逐筆插入另發 `mode` 為 `insert` 之事件;`error` 於七函數皆發出,整批性錯誤於 `reject` 之前、逐筆失敗於該筆定案後各一次。
|
|
373
476
|
|
|
374
477
|
`opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值;為 `false` 時 `insert` 與 `save` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`del` 不受此設定影響。
|
|
375
478
|
|
|
@@ -379,20 +482,28 @@ T7 之原子性以 LMDB 之條件寫入與寫交易達成:`insert` 為 `ifNoEx
|
|
|
379
482
|
|
|
380
483
|
T8 已完成:README 已宣告單一行程內保證成立、跨行程因 `lmdb-js` 綁定層限制而不成立,並附實測依據(平台、版本、行程數、回合數、發生率)與迴避方式。
|
|
381
484
|
|
|
485
|
+
|
|
486
|
+
**`insertBulk`:已實作。** 全有全無以 `childTransaction` 包覆達成——交易內逐筆先 `get` 判定主鍵是否存在,存在即回傳 `ABORT` 中止該交易,由交易回滾保證未寫入任何一筆,無須補償動作。交易內之 `get` 讀得到同一交易稍早之 `put`,故同批含重複主鍵者亦於此被偵測為衝突。存在與否採鍵層判定,與 `insert` 之 `ifNoExists` 一致。
|
|
487
|
+
|
|
488
|
+
**須用 `childTransaction` 而非 `transaction`。** 實測 `lmdb-js` 3.5.6 之非同步 `transaction()` **於中止時並不回滾**:回調內回傳 `ABORT` 或拋錯,其先前之 `put` 皆已落盤。`childTransaction` 與 `transactionSync` 兩者之 `ABORT` 與拋錯皆能正確回滾,本套件取前者以免阻塞事件迴圈。
|
|
489
|
+
|
|
490
|
+
**效能與 `insert` 無顯著差異**(實測 N=20000:`insert` 650ms、`childTransaction` 640ms、`transactionSync` 645ms),因本套件之 `insert` 本即以 `Promise.all` 一次送出全部條件寫入而非逐筆 await。提供本函數係為與其他套件維持同一組函數,令呼叫端得於各套件間替換而不須改寫呼叫。
|
|
491
|
+
|
|
492
|
+
**`insert` 之 `option.returnList`:已實作。** 逐筆判定即 `Promise.all` 各 `ifNoExists` 之回傳值(與輸入等長、保序),開啟時僅改包裝為逐筆結果陣列而不摺成計數,零推導成本。同批重複主鍵者僅首筆 `nInserted` 為 `1`,與聚合模式之計數一致。
|
|
493
|
+
|
|
494
|
+
**close() 後之快速失敗:已實作**(T4「實例已關閉」條)。七函數入口共用 `procClosed` 守門,`close()` 後再操作一律於入口立即以整批性錯誤 `reject`(訊息明示 closed,並依 T10.3 發出 `error` 事件),不與「查無資料」同形;`waitOpen` 另有終態前置判斷作為縱深防禦(攔操作進行中被 close 之競態),並移除輪詢中之逐秒 stdout 輸出、等待參數收斂為 50×200ms(waitFun 預設 200×1s)。修正前之實測症狀:`select`/`selectByPk`/`delAll` 於 close 後空轉約 200 秒方 reject;`del` 靜默回「主鍵未命中」之正常結果(`{n:0, nDeleted:0, ok:1}`,fail-open);`save` 誤降為逐筆 `ok: 0` 之 resolve;`insertBulk` 之 `childTransaction` 於已關閉 env 上自 `setImmediate` 拋出未捕捉例外而使**行程崩潰**——皆由入口守門一併攔下。
|
|
495
|
+
|
|
382
496
|
### w-orm-mongodb
|
|
383
497
|
|
|
384
498
|
主鍵欄位為 `id`,為無業務語義之識別碼。主鍵欄位目前**固定為 `id`,尚未支援由呼叫端指定**,已於類別註解、`selectByPk` 註解與 README 載明。
|
|
385
499
|
|
|
386
|
-
已符合 T1–
|
|
500
|
+
已符合 T1–T10 與七函數之全部規格;惟 insert 之 returnList 選項尚未實作,見下方待處理。
|
|
387
501
|
|
|
388
|
-
T10
|
|
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`。
|
|
389
503
|
|
|
390
|
-
|
|
391
|
-
|---|---|---|
|
|
392
|
-
| `error` 事件 | 未發出 | 依 T10.3 新增;既有之 `emitChange` 可直接擴充為共用之事件發出函數 |
|
|
393
|
-
| `EventEmitter` 實作 | `events.EventEmitter`(Node 內建,具「`error` 於無監聽者時拋出」之語義) | 依 T10.1 第 2 條改用 `wsemi` 之 `evem()`(`eventemitter3`),既有相依已涵蓋 |
|
|
504
|
+
`opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值——即 UUIDv7,符合 T6「如 UUID 或單調序列」,且其時間有序之特性令寫入主鍵唯一索引時之索引局部性優於純隨機字串;為 `false` 時 `insert`、`insertBulk`、`save` 與 `insertGfs` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,`insert`、`insertBulk` 與 `save` 之 `option` 未提供覆寫。`del` 不受此設定影響。
|
|
394
505
|
|
|
395
|
-
`
|
|
506
|
+
`save` 之重試上限為 3 次:其主體為單一 `updateOne` 配合 `upsert`,MongoDB 於單一語句內即可回報係插入或更新(`upsertedCount` 與 `matchedCount`),故不屬 T7 所稱「條件寫入配合衝突偵測與重試」之形式;該重試僅為處理「併發 upsert 於唯一索引上可能拋重複鍵錯誤」之既知競態,重試時該主鍵已存在故必走更新路徑而收斂。
|
|
396
507
|
|
|
397
508
|
`save` 之「內容相同」判定即本規格之基準來源:由 MongoDB 於伺服器端將 `$set` 之待寫入物件合併進現值後與現值比對,未寫入即回 `modifiedCount` 為 `0`,比對與寫入於同一原子操作內完成,故不須預讀。
|
|
398
509
|
|
|
@@ -406,45 +517,120 @@ GridFS 之寫入係先寫 chunks 再寫 files 文件,故違反唯一索引時
|
|
|
406
517
|
|
|
407
518
|
`save` 無 GridFS 對應函數:GridFS 無法於單一原子操作內取代既有內容,提供 `saveGfs` 將違反 T7,故不提供,更新以 `delGfs` 後再 `insertGfs` 完成。
|
|
408
519
|
|
|
409
|
-
### w-orm-postgresql
|
|
410
520
|
|
|
411
|
-
|
|
521
|
+
**`insertBulk`:已實作。** 全有全無之達成方式依部署而異,由套件於執行期以 `hello` 回應判定拓樸(`setName` 存在或 `msg` 為 `isdbgrid` 即表交易可用),每個實例判定一次並快取:
|
|
522
|
+
|
|
523
|
+
- **具 replica set 或分片叢集者以交易達成**——`session.withTransaction` 包覆 `insertMany({ ordered: true })`,任一筆衝突即整批回滾,無須補償動作。
|
|
524
|
+
- **standalone 無交易可用,以補償動作達成**——`insertMany({ ordered: false })` 失敗後刪除本次已寫入者再 `reject`。刪除之依據為**本次由驅動於送出前在用戶端所產生之 `_id`**,而非由 `writeErrors` 之索引反推輸入之主鍵值:後者於錯誤不帶 `writeErrors` 時(如網路中斷)會把全部輸入主鍵當成本次寫入而刪除,其中已存在者屬呼叫前既有資料,將造成資料損毀。已實測確認衝突筆所獲配之 `_id` 與庫內既有同主鍵者之 `_id` 不同,故按 `_id` 刪除不會誤及既有資料;刪除未寫入者為無操作,故不須精確得知哪幾筆已寫入。**限制**:行程若於補償途中中止則已寫入之部份可能殘留,已於 README 明白載明。
|
|
525
|
+
|
|
526
|
+
**未以別名或轉呼叫 `insert` 實作**:如此則主鍵衝突時不會 `reject` 而是靜默跳過,呼叫端之錯誤前提於本套件上永遠不會浮現。
|
|
412
527
|
|
|
413
|
-
|
|
528
|
+
**本函數於本後端不會較 `insert` 快**(`insert` 本即一次往返且計數精確),提供之理由為語義與跨套件之可替換性。
|
|
414
529
|
|
|
415
|
-
|
|
530
|
+
兩條路徑皆已測試:standalone 之補償路徑見 `test/api-basic.test.mjs` 之 `insertBulk`,交易路徑見 `test/api-insertbulk-rs.test.mjs`。後者另起 `--replSet rs0` 之容器,並先斷言 `hello.setName` 存在以證明確走交易路徑,涵蓋撞既有主鍵、同批重複主鍵、200 筆批次末筆衝突之回滾,皆斷言失敗後筆數增量為 0 且既有數據未被改動。該容器不開啟認證,因 `--replSet` 配合 root 帳號須另備 keyFile 以供成員間內部認證,於測試無必要;連線採 `directConnection=true`,因容器內之成員位址為 `127.0.0.1:27017`,由宿主機依該位址無法連線,不可令驅動走成員探索。
|
|
531
|
+
|
|
532
|
+
**GridFS 不提供 `insertBulkGfs`**:依 §7 專屬函數不在本規格範圍內,§9 亦僅要求「若與六函數概念對應則參數形狀比照」而未要求補齊所有概念。GridFS 每筆寫入為 chunks 與 files 兩階段,全有全無所須清理之對象較一般資料表複雜,且無對應之使用場景。
|
|
533
|
+
|
|
534
|
+
待處理:
|
|
416
535
|
|
|
417
536
|
| 項目 | 現況 | 規格 |
|
|
418
537
|
|---|---|---|
|
|
419
|
-
| `
|
|
420
|
-
|
|
538
|
+
| `insert` 之 `option.returnList` | 未提供 | 依 insert 規格新增;逐筆判定可由 `insertMany({ ordered: false })` 之 `writeErrors` 索引反推(有索引者 `nInserted: 0`,其餘 `1`),與輸入等長保序 |
|
|
539
|
+
|
|
540
|
+
### w-orm-postgresql
|
|
541
|
+
|
|
542
|
+
主鍵欄位由建構時之 `opt.pk` 指定,預設為 `time`,**已支援由呼叫端指定**,`select` 以外之六函數皆以該欄位認定主鍵,已於類別註解與 `selectByPk` 註解載明。
|
|
543
|
+
|
|
544
|
+
已符合 T1–T10 與七函數之全部規格;惟 insert 之 returnList 選項尚未實作,見下方待處理。
|
|
545
|
+
|
|
546
|
+
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
547
|
|
|
422
548
|
**不提供`autoGenPk`**,依 T6 之例外辦理,且兩種情形皆成立:
|
|
423
549
|
|
|
424
550
|
1. 預設主鍵 `time` 承載觀測時間之業務意義。此類欄位若自動補值,將使「呼叫端漏給主鍵」靜默變成「以當下之值多寫入一筆」,且該筆無從與正常資料區辨。
|
|
425
551
|
2. **主鍵欄位可由呼叫端指定,其型別則由資料表 schema 決定,套件並不知情**,故無從產生型別相容且符合 T6「須具足夠唯一性」之值:主鍵為 `TIMESTAMPTZ` 時可產生者僅有當下時間,同一毫秒內併發即碰撞,違反 T6「不得採用可預期碰撞之來源」;為 `TEXT` 時方適用隨機字串;為 `INTEGER` 時須倚賴資料庫端之 sequence。三者所需之產生策略互斥,而套件於寫入前無從得知係何者。
|
|
426
552
|
|
|
427
|
-
故縱使呼叫端將主鍵指定為無業務語義之欄位,本套件仍不提供補值,主鍵一律由呼叫端自備。`insert` 與 `save` 於輸入未帶有效主鍵值時,以 `Promise.reject` 拋出整批性錯誤;主鍵檢查於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`del` 不受此設定影響,未帶有效主鍵值者仍依 T6 回該筆 `ok: 0` + `err`。
|
|
553
|
+
故縱使呼叫端將主鍵指定為無業務語義之欄位,本套件仍不提供補值,主鍵一律由呼叫端自備。`insert`、`insertBulk` 與 `save` 於輸入未帶有效主鍵值時,以 `Promise.reject` 拋出整批性錯誤;主鍵檢查於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`del` 不受此設定影響,未帶有效主鍵值者仍依 T6 回該筆 `ok: 0` + `err`。
|
|
428
554
|
|
|
429
|
-
主鍵值之有效性僅要求有給值而不限定型別,理由同上——主鍵欄位得由呼叫端指定而其型別隨欄位而異。型別與欄位不符者由 PostgreSQL 回報 `22P02` 或 `22007`,各函數依其規格分別處置:`selectByPk` 視為主鍵值無效而回傳 `null`,`insert` 與 `save` 為整批性錯誤而 `reject`,`del` 為該筆失敗而回 `ok: 0` + `err`。
|
|
555
|
+
主鍵值之有效性僅要求有給值而不限定型別,理由同上——主鍵欄位得由呼叫端指定而其型別隨欄位而異。型別與欄位不符者由 PostgreSQL 回報 `22P02` 或 `22007`,各函數依其規格分別處置:`selectByPk` 視為主鍵值無效而回傳 `null`,`insert`、`insertBulk` 與 `save` 為整批性錯誤而 `reject`,`del` 為該筆失敗而回 `ok: 0` + `err`。
|
|
430
556
|
|
|
431
557
|
`save` 之「內容相同」判定採合併後比對:以待寫入物件之非主鍵欄位淺層覆蓋現值後與現值比對。合併取淺層而非深層,係為與後端以 `EXCLUDED` 整欄取代之寫入行為一致——判定基準與實際寫入行為若不一致,`nModified` 即無法忠實反映是否真的寫入。快速路徑之預讀僅用於判斷是否略過寫入,寫入內容一律由原子語句自身決定。
|
|
432
558
|
|
|
433
|
-
T7 之原子性以 `ON CONFLICT` 達成:`insert`
|
|
559
|
+
T7 之原子性以 `ON CONFLICT` 達成:`insert` 為 `INSERT ... ON CONFLICT (主鍵) DO NOTHING`,取 `rowCount` 為 `nInserted`,筆數超出綁定參數上限者分批送出(見下方 `insertBulk` 之說明),因各筆之檢查與寫入本即逐筆獨立,分批不影響本項要求;`save` 於 `autoInsert` 開啟時為單一 `INSERT ... ON CONFLICT (主鍵) DO UPDATE ... RETURNING (xmax = 0)`,以 `xmax` 區辨本語句插入與衝突後更新,關閉時為單一 `UPDATE ... WHERE 主鍵` 以免無中生有。其前提為主鍵具唯一約束,`createTable` 一律以 `PRIMARY KEY` 建立且無關閉選項;資料表若於他處建立而缺該約束,`insert` 與 `save` 將以 `there is no unique or exclusion constraint matching the ON CONFLICT specification` 拒絕,須先清除重複主鍵值後補建約束。
|
|
560
|
+
|
|
561
|
+
T8 無須宣告:單一行程內併發與跨行程併發皆成立,故依 T8 未於 README 宣告。原子性全由 PostgreSQL 伺服器端提供,**寫入路徑不保有行程內狀態**且每次呼叫各自開啟連線,故兩種範圍於後端為同一情形。已測試依據含 2 個獨立行程對相同 20 個主鍵併發 `insert`、`nInserted` 總和為 20 且資料表僅 20 筆,以及 2 個行程對同一主鍵各寫入 5 個不同欄位、10 欄位全數保留。
|
|
562
|
+
|
|
563
|
+
惟 `opt.useCache` 開啟時,`select` 與 `selectByPk` 之快取為行程內狀態:本行程之寫入會重設快取,他行程之寫入則不會,故跨行程下得讀到過期數據。此僅影響讀取之新鮮度,不影響上述寫入之原子性——快取不參與寫入路徑,`insert`、`save`、`del` 之判定與寫入一律由資料庫端完成。該選項預設為關閉,且已於類別註解載明適用於單程序操作。
|
|
564
|
+
|
|
565
|
+
本套件另有專屬函數 `createTable`,依 §7 不在本規格範圍內,其 `pk` 參數未給時採 `opt.pk`;因 T7 之原子性倚賴主鍵之唯一約束,該參數若給予與 `opt.pk` 不同之欄位,將建出其餘函數無法正確操作之資料表,已於函數註解載明。
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
**`insertBulk`:已實作。** 全有全無以「單一多值 `INSERT`(不加 `ON CONFLICT`)配合交易包覆」達成:不加 `ON CONFLICT` 時,任一筆撞主鍵之唯一約束即整句失敗且不寫入任何一筆,同批含重複主鍵者亦於此被偵測為衝突,無須另行比對。未以 `insert` 之別名或轉呼叫實作,兩者之語句與衝突政策各自獨立。
|
|
434
569
|
|
|
435
|
-
|
|
570
|
+
PostgreSQL 之協定以 int16 記綁定參數個數,上限為 65535,故單一語句可送之筆數受欄位數所限(筆數 × 欄位數 ≤ 65535),超出者依 `floor(65535 / 欄位數)` 分批送出。分批時單語句之原子性已不足以維持「失敗即不留下部份寫入」,故各批一律以 `BEGIN`/`COMMIT` 包覆,任一批失敗即 `ROLLBACK`;不因批數而異,令單批與分批之保證來源一致。
|
|
436
571
|
|
|
437
|
-
|
|
572
|
+
**分批與交易包覆為 `insert` 與 `insertBulk` 共用**,收斂於內部之 `insertBatches`;衝突政策由參數區分而語句各自獨立——`insert` 添加 `ON CONFLICT DO NOTHING`,`insertBulk` 不添加。故 `insertBulk` 非 `insert` 之別名或轉呼叫,兩者之語義各自成立:已測試 21 欄之資料表送 3200 筆(分 2 批)而其中既有 1 筆主鍵者,`insert` 回 `nInserted` 為 3199 且資料表為 3200 筆,`insertBulk` 則整批 `reject` 且資料表之筆數增量為 0。
|
|
573
|
+
|
|
574
|
+
**效能優於 `insert`**,且筆數愈多差距愈大。同一資料表無衝突插入之實測(Windows 11、PostgreSQL 17.11、Node v24.19.0,各 3 輪取平均):1000 筆 41ms→40ms、5000 筆 100ms→57ms、20000 筆 350ms→148ms。原因為 `insert` 之 `ON CONFLICT DO NOTHING` 須對每列進行推測性插入,`insertBulk` 無此開銷;筆數少時該開銷不顯著,故兩者相當。
|
|
575
|
+
|
|
576
|
+
`insert` 於採用共用之分批機制前未分批,3 欄之資料表送 25000 筆即以 `bind message has 9464 parameter formats but 0 parameters` 拒絕且 0 筆寫入;改用後兩函數於同一輸入下皆分批完成 25000 筆。
|
|
577
|
+
|
|
578
|
+
待處理:
|
|
579
|
+
|
|
580
|
+
| 項目 | 現況 | 規格 |
|
|
581
|
+
|---|---|---|
|
|
582
|
+
| `insert` 之 `option.returnList` | 未提供 | 依 insert 規格新增;逐筆判定可於 `INSERT ... ON CONFLICT DO NOTHING` 附加 `RETURNING 主鍵` 後映回輸入序(同批重複主鍵以首次出現者為插入),分批送出時逐批映回再串接 |
|
|
438
583
|
|
|
439
584
|
### w-orm-reladb
|
|
440
585
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
586
|
+
主鍵欄位由建構時之 `opt.pk` 指定,預設為 `id`,**已支援由呼叫端指定**,`select` 以外之六函數皆以該欄位認定主鍵,已於 `selectByPk` 註解與 README 載明。本套件經 sequelize 操作 mssql、sqlite、mysql、mariadb、postgres 五種後端。
|
|
587
|
+
|
|
588
|
+
已符合 T1–T10 與七函數之全部規格;惟 insert 之 returnList 選項尚未實作,見下方待處理。
|
|
589
|
+
|
|
590
|
+
`opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值——為 UUIDv7 格式之 36 碼字串,具單調遞增性,主鍵接近順序遞增可減少 B-tree 索引之頁面分裂;為 `false` 時 `insert` 與 `save` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,`insert` 與 `save` 之 `option` 未提供覆寫。`del` 不受此設定影響。
|
|
591
|
+
|
|
592
|
+
**不採 T6 之例外**,仍提供 `autoGenPk`。雖然本套件之主鍵欄位得由呼叫端指定而其型別隨欄位而異,但與 w-orm-postgresql 不同者為:**本套件持有 sequelize 之 model 定義,得由 `rawAttributes[opt.pk].type.key` 讀得主鍵之型別**,故 T6 例外第 2 種情形(套件無從得知主鍵之型別或值域)於此不成立。因自動產生之值為字串,`procPk` 於補值前檢查主鍵欄位確為字串類型(`STRING`、`TEXT`、`CHAR`、`UUID`、`CITEXT`),不符者以明確訊息拋出整批性錯誤,而非任由資料庫回報型別錯誤。主鍵欄位之長度須能容納 36 碼,已於 README 載明。
|
|
593
|
+
|
|
594
|
+
`save` 之「內容相同」判定採合併後比對:以待寫入物件之非主鍵欄位淺層覆蓋現值後與現值比對,並先以 model 之欄位名濾除非欄位之鍵。合併取淺層而非深層,係為與後端整欄取代之寫入行為一致——判定基準與實際寫入行為若不一致,`nModified` 即無法忠實反映是否真的寫入。快速路徑之預讀僅用於判斷是否略過寫入,寫入內容一律由該次寫入語句自身決定。
|
|
595
|
+
|
|
596
|
+
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`。
|
|
597
|
+
|
|
598
|
+
其實作為:`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 建表,無關閉選項。
|
|
599
|
+
|
|
600
|
+
T8 已完成:README 已宣告兩範圍之適用情形。**跨行程併發成立**,且與 `opt.useStable` 無關——`insert` 與 `save` 皆以單一條件式語句寫入,勝負由資料庫之主鍵唯一約束判定,不倚賴任何行程內機制。**單一行程內併發於 `opt.useStable` 為 `true`(預設)時成立**,由套件內建之佇列(同時最大執行數 1)序列化達成;**為 `false` 時不成立**,因每個實例共用單一連線變數,於呼叫開始時開啟、結束時關閉,並行呼叫會關掉他者仍在使用之連線。
|
|
601
|
+
|
|
602
|
+
失效之具體後果:實測同一行程內 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`,或由呼叫端自行序列化。
|
|
603
|
+
|
|
604
|
+
實測依據:跨行程為 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 綁定)。
|
|
605
|
+
|
|
606
|
+
註:此限制非受後端或其驅動層所限,而係本套件之連線管理所致——w-orm-mongodb 與 w-orm-postgresql 每次操作各自建立連線且不保有行程內狀態,故兩種範圍於後端為同一情形而無此區分。本套件因提供 `option.instance` 與 `option.transaction` 之共用連線擴充而保有行程內狀態,改為每次呼叫各自持有連線須一併調整該擴充,屬架構層級改動,暫以佇列預設開啟迴避。
|
|
607
|
+
|
|
608
|
+
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`。
|
|
609
|
+
|
|
610
|
+
本套件之七函數另有專屬之 `option.instance` 與 `option.transaction` 兩參數,供呼叫端共用連線實例與交易,未給時各函數自行初始化與關閉;此為向後相容之選配擴充,未給予時行為與規格所定完全相同,故不與規格所定函數之語義衝突。另有專屬函數 `createStorage`、`genModelsByDB`、`genModelsByTabs`、`init`、`genTransaction`,依 §7 不在本規格範圍內,且皆不與規格所定函數之概念對應。
|
|
611
|
+
|
|
612
|
+
建構時 `opt.url` 解析失敗一律 `throw`:其屬呼叫端未履行契約,且解析失敗後各函數皆無從運作;建構為同步而無從以 `Promise.reject` 回報,拋出即為 `reject` 之同步對應。原先僅 `console.log` 而回傳未綁定各函數之 `EventEmitter`,錯誤不經任何管道抵達呼叫端,呼叫端僅得到 `w.select is not a function` 之無關訊息,與本規格「錯誤不得靜默」之通則相違。
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
**`insertBulk`:已實作。** 全有全無以「單次 `bulkCreate`(不加任何跳過選項)配合交易包覆」達成:不加跳過選項時,任一筆撞主鍵之唯一約束即整句失敗,同批含重複主鍵者亦於此被偵測為衝突,無須另行比對。未以 `insert` 之別名或轉呼叫實作——`insert` 為逐筆 `create` 並攔截 `UniqueConstraintError` 以跳過既有主鍵,`insertBulk` 為單次 `bulkCreate` 且不攔截,兩者之語句與衝突政策各自獨立。
|
|
616
|
+
|
|
617
|
+
**交易包覆為必要而非優化。** 實測 mssql 因綁定參數數量上限(2100)而由驅動層自動將批次拆為多語句送出,1000 筆之批次於最後一筆撞主鍵時已有 **946 筆落盤**;sqlite 之批次為單一語句故天然原子。包覆後兩後端於失敗時皆為 0 筆新增,已以測試斷言。
|
|
618
|
+
|
|
619
|
+
**呼叫端已給 `option.transaction` 時改以巢狀交易(SAVEPOINT)包覆**,令回滾範圍限於本次呼叫。若逕自於呼叫端之交易內分批寫入而中途失敗,前段將留在該交易內未回滾,全有全無即不成立;亦不得回滾呼叫端之交易,那會連帶撤銷其先前之寫入。已實測 sqlite 與 mssql 之 SAVEPOINT 回滾範圍皆正確——外層交易先前之寫入保留、內層失敗之批次全數不存在,並以測試斷言。
|
|
620
|
+
|
|
621
|
+
**效能優於 `insert`,且筆數愈多差距愈大**(Windows 11、Node 24、sequelize 6、MSSQL 2022、SQLite 經 sqlite3 綁定):
|
|
622
|
+
|
|
623
|
+
| 筆數 | sqlite `insert` → `insertBulk` | mssql `insert` → `insertBulk` |
|
|
624
|
+
|---|---|---|
|
|
625
|
+
| 1000 | 5549 ms → 25 ms(226x) | 6802 ms → 190 ms(36x) |
|
|
626
|
+
| 5000 | 29825 ms → 50 ms(592x) | 44116 ms → 489 ms(90x) |
|
|
627
|
+
|
|
628
|
+
差距之來源為 `insert` 須逐筆寫入方能回報精確之 `nInserted`(見上方 T7 之說明——本後端之批次語句無從回報「本批有幾筆真的插入」),`insertBulk` 因採全有全無而無此需求。
|
|
629
|
+
|
|
630
|
+
`opt.useEncryption` 開啟且後端為 sqlite 時不自開交易,因 `@journeyapps/sqlcipher` 不支援交易;該情形下全有全無倚賴 sqlite 單一批次語句之原子性。
|
|
444
631
|
|
|
445
|
-
|
|
632
|
+
待處理:
|
|
446
633
|
|
|
447
634
|
| 項目 | 現況 | 規格 |
|
|
448
635
|
|---|---|---|
|
|
449
|
-
|
|
|
450
|
-
| `opt.autoGenPk` | 已提供,預設 `true`;為 `false` 時 `insert` 與 `save` 皆不補值 | 符合 T6,惟未帶有效主鍵時之處置待查證是否為 `reject` | -->
|
|
636
|
+
| `insert` 之 `option.returnList` | 未提供 | 依 insert 規格新增;本套件之 `insert` 本即逐筆 `create` 並以 `UniqueConstraintError` 辨識既有主鍵,逐筆判定現成,僅須改包裝為逐筆結果陣列 |
|
package/dist/req-mingo.umd.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* req-mingo v1.0.18
|
|
3
|
-
* (c) 2018-2021 yuda-lyu(semisphere)
|
|
4
|
-
* Released under the MIT License.
|
|
5
|
-
*/
|
|
6
|
-
!function(e,o){"object"==typeof exports&&"undefined"!=typeof module?module.exports=o(require("mingo")):"function"==typeof define&&define.amd?define(["mingo"],o):(e="undefined"!=typeof globalThis?globalThis:e||self)["req-mingo"]=o(e.mingo)}(this,function(e){"use strict";function o(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}return o(e)});
|
|
7
|
-
//# sourceMappingURL=req-mingo.umd.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"req-mingo.umd.js","sources":["../src/reqMingo.js"],"sourcesContent":null,"names":["require$$0"],"mappings":";;;;;2XAAYA"}
|