w-orm-lmdb 1.0.12 → 1.0.13

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.
@@ -0,0 +1,283 @@
1
+ # w-orm 系列套件統一規格
2
+
3
+ 本文為 `w-orm-*` 系列套件之**規格**,非現況描述。凡新增或修改此系列之任一套件,皆須以本文為準。
4
+
5
+ 規格與各套件之儲存後端無關:後端可為 key-value store、document database、關聯式資料庫或其他形式。規格只規範**對外行為**——函數名稱、參數、回傳結構、各欄位語義、錯誤處置與原子性要求;**不規範達成手段**,各套件依其後端能力自行實作。
6
+
7
+ 適用函數共六個:`select`、`selectById`、`insert`、`save`、`del`、`delAll`。套件得另有後端專屬函數(如建表、檔案儲存),不在本規格範圍內,惟不得與六函數之語義衝突。
8
+
9
+ ---
10
+
11
+ ## 通則
12
+
13
+ ### T1 主鍵稱謂與單筆直讀函數
14
+
15
+ 各套件之主鍵欄位名不同,本文以「主鍵」統稱,實作時代換為各套件之欄位名。
16
+
17
+ 單筆直讀函數之名稱一律為 `selectById`。主鍵欄位名非 `id` 者,得另掛語義相同之別名(如主鍵為 `time` 者掛 `selectByTime`),但 `selectById` 必須存在,以利呼叫端跨套件替換。
18
+
19
+ ### T2 回傳型別與鍵集合固定
20
+
21
+ 同一函數不論走哪條路徑,回傳之**型別與鍵集合完全相同**。呼叫端不得需要先判斷某個鍵是否存在。
22
+
23
+ - 計數欄位(`n`、`nInserted`、`nModified`、`nDeleted`)在該函數的規格表中一旦列出即**恆出現**,無對應行為時填 `0`,不得省略。
24
+ - 逐筆函數(`save`、`del`)恆回傳與輸入**等長**之陣列,即使輸入為單一物件亦回傳長度為 `1` 之陣列。
25
+ - 整批函數(`insert`、`delAll`)恆回傳單一物件。
26
+ - 唯一的例外是 `err`:僅在該筆 `ok` 為 `0` 時出現,`ok` 為 `1` 時不得出現。
27
+
28
+ ### T3 `n` 之定義
29
+
30
+ `n` 為**本次操作於資料庫端所命中或涉及之筆數**。逐函數定義如下,跨套件不得有第二種解讀:
31
+
32
+ | 函數 | `n` |
33
+ |---|---|
34
+ | `insert` | 輸入筆數(陣列化後之長度),代表本次嘗試插入之基準 |
35
+ | `save`(逐筆) | 主鍵命中筆數,`0` 或 `1`。命中(不論內容有無變更)或經插入而產生皆為 `1` |
36
+ | `del`(逐筆) | 主鍵命中筆數,`0` 或 `1` |
37
+ | `delAll` | 實際刪除筆數,恆等於 `nDeleted` |
38
+
39
+ `n` 不得取全表筆數,不得為與結果無關之常數。
40
+
41
+ ### T4 `ok` 與錯誤處置
42
+
43
+ `ok` 僅有兩值:`1` 成功、`0` 該筆失敗。
44
+
45
+ - 成功路徑一律 `ok: 1`。**不得**由驅動層之確認旗標(如 MongoDB 之 `acknowledged`、SQL driver 之連線狀態)直接推導——該類旗標會產生沒有錯誤訊息的 `ok: 0`,呼叫端無從處理。若確實需要反映驅動層的未確認狀態,須將其視為該筆失敗,回 `ok: 0` 並附 `err`。
46
+ - `ok: 0` 僅出現於逐筆函數(`save`、`del`),且**必附 `err` 字串**說明原因。
47
+ - 單筆失敗**不中斷整批**:其餘筆數照常處理,整批仍 resolve,該筆以 `ok: 0` 回報。
48
+ - 整批性錯誤(連線失敗、參數型別錯誤、資料表不存在、權限不足等)以 `Promise.reject` 拋出,不進入逐筆結果。
49
+
50
+ 判別「該筆失敗」與「整批性錯誤」的原則:錯誤只影響該筆資料者為前者,影響後續所有筆數者為後者。
51
+
52
+ ### T5 輸入無效之處置
53
+
54
+ 「輸入無效」指傳入之 `data` 既非有效物件亦非有效陣列。此時不視為錯誤,回傳空結果:
55
+
56
+ | 函數 | 回傳 |
57
+ |---|---|
58
+ | `insert` | `{ n: 0, nInserted: 0, ok: 1 }` |
59
+ | `save` | `[]` |
60
+ | `del` | `[]` |
61
+
62
+ 主鍵值無效(未給、型別不符)之處置見各函數說明。
63
+
64
+ ### T6 主鍵補值
65
+
66
+ `insert` 與 `save` 於輸入未帶有效主鍵時,自動產生主鍵值後再寫入。
67
+
68
+ `del` **不補值**——未帶有效主鍵者視為該筆無法處理,回 `ok: 0` 並附 `err`,且不得將無效主鍵送進查詢條件(部分後端會把 `undefined` 轉為 `null` 而誤中其他資料)。
69
+
70
+ ### T7 原子性要求
71
+
72
+ 以下兩項判斷與寫入,必須由資料庫在**單一原子操作**內完成,不得以「先讀出、再依讀到的結果決定寫入」的方式實作:
73
+
74
+ | 函數 | 須原子完成之內容 |
75
+ |---|---|
76
+ | `insert` | 「檢查主鍵不存在」與「寫入」 |
77
+ | `save` | 「查找主鍵」與「更新或插入」 |
78
+
79
+ 達成手段不限,由各套件依後端能力選擇(唯一索引、`ON CONFLICT`、條件寫入、寫交易、比較並交換等)。
80
+
81
+ **若某後端須開啟特定設定才能達成本要求,該設定必須預設開啟。** 不得以「預設關閉、要正確性請自行開啟」的形式交付——`insert` 的「已存在則跳過」與 `save` 的「不遺失更新」是本規格定義的核心語義,不是選配。若開啟該設定對既有資料有前提(例如建立唯一索引前須先清除重複資料),須於 README 載明升級步驟。
82
+
83
+ `del` 不在本要求內:刪除為冪等操作,重複刪除不產生額外副作用。惟 `nDeleted` 在併發下可能不精確,套件得選擇以原子操作取得精確值。
84
+
85
+ ### T8 併發保證之宣告義務
86
+
87
+ 各套件須於 README 明確宣告其原子性保證的**適用範圍**,至少區分:
88
+
89
+ - **單一行程內併發**:同一行程內多個並行呼叫。
90
+ - **跨行程併發**:多個行程操作同一資料庫。
91
+
92
+ 若某一範圍無法保證(受限於後端或其驅動層),須明確標示,並說明:失效時的**具體後果**(哪些欄位會失準、資料是否會損毀)、**實測依據**(平台、版本、條件、發生率),以及**建議的迴避方式**(單一寫入者、跨行程鎖等)。
93
+
94
+ 不得只寫「支援併發」而不界定範圍。呼叫端據以決定是否需要自行加鎖,此資訊缺漏會直接導致誤用。
95
+
96
+ ### T9 讀取函數不得有副作用
97
+
98
+ `select` 與 `selectById` 不得寫入資料、不得建立索引或資料表、不得改變任何可被觀察的狀態。需要初始化的動作應於建構時或寫入函數內完成。
99
+
100
+ ---
101
+
102
+ ## 各函數規格
103
+
104
+ ### select(find) → `Promise<Array<Object>>`
105
+
106
+ 依查詢條件取回多筆數據。
107
+
108
+ - 恆回傳陣列。無符合數據回 `[]`,不得回 `null` 或 `undefined`。
109
+ - `find` 未給或為空物件時回傳全部數據。
110
+ - 陣列元素為數據物件,**不得含資料庫內部欄位**(如 MongoDB 之 `_id`、SQL 之自增序號),呼叫端看到的欄位須與寫入時給的一致。
111
+ - 錯誤時 `reject`。
112
+
113
+ ### selectById(id) → `Promise<Object | null>`
114
+
115
+ 依主鍵直讀單筆數據,不經 `select` 之全表提取與過濾。
116
+
117
+ | 情境 | 回傳 |
118
+ |---|---|
119
+ | 主鍵命中 | 該筆數據物件,內容與 `select({主鍵})[0]` 相同 |
120
+ | 主鍵未命中 | `null` |
121
+ | 主鍵值無效(未給、型別不符) | `null`,不 `reject` |
122
+ | 錯誤 | `reject` |
123
+
124
+ 「命中」之判定基準須與 `insert`、`save`、`del` 內對既有數據之認定一致,不得出現 `selectById` 回傳物件而 `insert` 仍視為不存在之矛盾。
125
+
126
+ ### insert(data) → `Promise<{ n, nInserted, ok }>`
127
+
128
+ **僅於主鍵不存在時寫入,已存在者跳過且不覆寫。**
129
+
130
+ | 欄位 | 值 |
131
+ |---|---|
132
+ | `n` | 輸入筆數 |
133
+ | `nInserted` | 實際插入筆數,`0 ≤ nInserted ≤ n` |
134
+ | `ok` | `1` |
135
+
136
+ - 全數已存在而 `nInserted` 為 `0` 屬**正常結果**,不視為錯誤,不得 `reject`。
137
+ - 同批含重複主鍵時僅首筆計入 `nInserted`,其餘視為已存在。
138
+ - 須符合 T7 之原子性要求。
139
+ - 輸入無效見 T5。
140
+
141
+ ### save(data, option) → `Promise<Array<{ n, nInserted, nModified, ok }>>`
142
+
143
+ 以主鍵為準更新既有數據,未給之欄位保留;主鍵不存在且 `option.autoInsert` 為 `true`(預設)時改為插入。
144
+
145
+ 逐筆結果:
146
+
147
+ | 情境 | `n` | `nInserted` | `nModified` | `ok` |
148
+ |---|---|---|---|---|
149
+ | 主鍵存在,合併後內容有變更 | `1` | `0` | `1` | `1` |
150
+ | 主鍵存在,合併後內容與現值相同而未寫入 | `1` | `0` | `0` | `1` |
151
+ | 主鍵不存在,`autoInsert: true` | `1` | `1` | `0` | `1` |
152
+ | 主鍵不存在,`autoInsert: false` | `0` | `0` | `0` | `1` |
153
+ | 該筆執行失敗 | `1` | `0` | `0` | `0` + `err` |
154
+
155
+ **「內容相同」之判定基準為:把待寫入物件合併進現值之後,結果與現值相同。** 相同則不寫入,`nModified` 為 `0`。
156
+
157
+ 此基準的用意是讓 `nModified` 忠實反映「資料庫端是否真的寫入」。只給部份欄位且該些欄位值皆與現值相同時,合併結果等於現值,故 `nModified` 為 `0`——不得因為「傳入物件與現值不全等」就回報已修改,那會讓呼叫端把沒發生的變更當成發生了。
158
+
159
+ - `nInserted` 與 `nModified` **同時存在**,依 T2。
160
+ - 得於原子操作前先行預讀以判斷內容相同而略過寫入(快速路徑)。預讀值過期不影響正確性,因內容相同時該筆等價於無操作,可視為於預讀當下即已完成。**預讀結果不得用於決定寫入內容**——寫入內容一律由原子操作內讀到的現值決定。
161
+ - 須符合 T7 之原子性要求。
162
+ - 輸入無效見 T5。
163
+
164
+ ### del(data) → `Promise<Array<{ n, nDeleted, ok }>>`
165
+
166
+ 依主鍵刪除數據。
167
+
168
+ 逐筆結果:
169
+
170
+ | 情境 | `n` | `nDeleted` | `ok` |
171
+ |---|---|---|---|
172
+ | 主鍵命中並刪除 | `1` | `1` | `1` |
173
+ | 主鍵未命中 | `0` | `0` | `1` |
174
+ | 該筆未帶有效主鍵 | `0` | `0` | `0` + `err` |
175
+ | 該筆執行失敗 | `1` | `0` | `0` + `err` |
176
+
177
+ - 「未帶有效主鍵」與「主鍵未命中」須以 `ok` 分辨:前者為輸入問題(`ok: 0`),後者為正常結果(`ok: 1`)。
178
+ - 判斷某筆是否真的被刪除,一律以 `nDeleted` 為準。
179
+ - 輸入無效見 T5。
180
+
181
+ ### delAll(find) → `Promise<{ n, nDeleted, ok }>`
182
+
183
+ 依條件刪除多筆數據。與 `del` 分開,避免未傳數據而誤刪全表。
184
+
185
+ | 欄位 | 值 |
186
+ |---|---|
187
+ | `n` | 實際刪除筆數 |
188
+ | `nDeleted` | 實際刪除筆數,恆等於 `n` |
189
+ | `ok` | `1` |
190
+
191
+ - `find` 未給或為空物件時刪除全部數據。
192
+ - 條件無命中時回 `{ n: 0, nDeleted: 0, ok: 1 }`,不視為錯誤。
193
+ - `n` **不得**取全表筆數。
194
+
195
+ ---
196
+
197
+ ## 回傳形狀速查
198
+
199
+ ```
200
+ select(find) → [ {...}, {...} ] 無符合為 []
201
+ selectById(id) → {...} | null
202
+
203
+ insert(data) → { n, nInserted, ok }
204
+ save(data, option) → [ { n, nInserted, nModified, ok }, ... ]
205
+ del(data) → [ { n, nDeleted, ok }, ... ]
206
+ delAll(find) → { n, nDeleted, ok }
207
+
208
+ 單筆失敗 → { ..., ok: 0, err: '...' } 僅 save、del
209
+ 整批失敗 → Promise.reject(err)
210
+ ```
211
+
212
+ ---
213
+
214
+ ## 呼叫端判讀準則
215
+
216
+ | 要判斷什麼 | 看什麼 | 不要看什麼 |
217
+ |---|---|---|
218
+ | 這批有幾筆是新資料 | `insert` 之 `nInserted` | `n`(那是輸入筆數) |
219
+ | 這筆是不是新資料 | `save` 之 `nInserted === 1` | `n`(命中即為 1,插入與更新皆是) |
220
+ | 這筆內容有沒有實際寫入 | `save` 之 `nModified === 1` | `n` |
221
+ | 這筆主鍵原本存不存在 | `save` 之 `n === 1` 且 `nInserted === 0` | 單看 `n` |
222
+ | 這筆有沒有真的被刪 | `nDeleted` | `n` |
223
+ | 整批有沒有失敗 | Promise 是否 `reject` | — |
224
+ | 個別筆有沒有失敗 | 逐筆之 `ok === 0`,訊息取 `err` | — |
225
+
226
+ ---
227
+
228
+ ## 新增套件之檢查表
229
+
230
+ 新套件納入 `w-orm-*` 系列前,逐項確認:
231
+
232
+ 1. 六個函數皆存在,`selectById` 名稱到位(主鍵非 `id` 者另掛別名)。
233
+ 2. `select` 恆回陣列且不含資料庫內部欄位,`selectById` 未命中回 `null`。
234
+ 3. 每個函數之計數欄位恆出現,無對應行為時填 `0`;`err` 僅隨 `ok: 0` 出現。
235
+ 4. `n` 依 T3 定義,四個函數各自的基準寫進函數註解。
236
+ 5. `insert` 與 `save` 符合 T7 原子性要求;若倚賴某項設定達成,該設定預設開啟,且 README 載明升級前提。
237
+ 6. 成功路徑 `ok` 恆為 `1`,不由驅動層旗標推導;`ok: 0` 必附 `err`;單筆失敗不中斷整批。
238
+ 7. `save` 之「內容相同」採**合併後比對**,基準寫進註解。
239
+ 8. `del` 對未帶有效主鍵者不送查詢,直接回 `ok: 0` + `err`。
240
+ 9. 輸入無效之回傳依 T5;讀取函數無副作用(T9)。
241
+ 10. README 依 T8 宣告併發保證範圍,無法保證者載明後果、實測依據與迴避方式。
242
+ 11. 測試須涵蓋:同批重複主鍵、主鍵不存在、合併後內容相同、只給部份欄位且值相同、`autoInsert` 兩種取值、單筆失敗、未帶有效主鍵、`delAll` 帶條件且僅部份命中。
243
+
244
+ ---
245
+
246
+ ## 各套件符合狀態
247
+
248
+ ### w-orm-lmdb
249
+
250
+ 已符合 T1–T6、T9 與 `select`/`selectById`/`insert`/`del`/`delAll` 全部規格。
251
+
252
+ 待處理:
253
+
254
+ | 項目 | 現況 | 規格 |
255
+ |---|---|---|
256
+ | `save` 之「內容相同」判定 | 以「待寫入物件與現值全等」為基準,只給部份欄位且值相同時仍寫入並回 `nModified: 1` | 改為合併後比對,該情境應回 `nModified: 0` |
257
+
258
+ T8 已完成:README 已宣告單一行程內保證成立、跨行程因 `lmdb-js` 綁定層限制而不成立,並附實測依據與迴避方式。
259
+
260
+ ### w-orm-mongodb
261
+
262
+ | 項目 | 現況 | 規格 |
263
+ |---|---|---|
264
+ | `insert` 之「已存在則跳過」 | 倚賴 `opt.uniqueId`,而其**預設為 `false`**,預設情境下同一主鍵會插出多筆 | 依 T7,該設定須預設開啟 |
265
+ | `save` 之 `nInserted`/`nModified` | 二選一出現 | 兩者恆存在 |
266
+ | `insert`(`uniqueId: false`)、`del`、`delAll` 之 `ok` | 取自驅動層 `acknowledged` | 常數 `1`;確需反映未確認狀態則回 `ok: 0` + `err` |
267
+ | `insert`(`uniqueId: false`)插入筆數為 `0` | 判為錯誤並 `reject` | 屬正常結果 |
268
+ | `del` 未帶有效主鍵 | 無此分支,無效主鍵仍被送進查詢條件 | 直接回 `ok: 0` + `err`,不送查詢 |
269
+ | `save`、`del` 單筆失敗 | 整批 `reject` | 該筆 `ok: 0` + `err`,整批 resolve |
270
+ | T8 併發宣告 | 未宣告 | 須於 README 宣告 |
271
+
272
+ `save` 之「內容相同」判定已符合本規格(由 MongoDB 於 `$set` 內逐欄位比對,未寫入即回 `modifiedCount` 為 `0`)——本規格之判定基準即以此為準。
273
+
274
+ ### w-orm-postgresql
275
+
276
+ | 項目 | 現況 | 規格 |
277
+ |---|---|---|
278
+ | `save` 之「內容相同」判定 | 以「非主鍵欄位全等」為基準,只給部份欄位且值相同時仍寫入並回 `nModified: 1` | 改為合併後比對 |
279
+ | `save` 之 `nInserted`/`nModified` | 二選一出現 | 兩者恆存在 |
280
+ | `del` 之 `n` | 常數 `1` | 主鍵命中筆數(`0` 或 `1`) |
281
+ | `del` 未帶有效主鍵 | 於前置檢查 `throw`,整批 `reject` | 該筆 `ok: 0` + `err` |
282
+ | `selectById` | 名為 `selectByTime` | 須另掛 `selectById` 別名 |
283
+ | T8 併發宣告 | 未宣告 | 須於 README 宣告 |