w-orm-reladb 1.1.13 → 1.1.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.
@@ -0,0 +1,262 @@
1
+ import assert from 'assert'
2
+ import wo from '../src/WOrmReladb.mjs'
3
+ import cvFind, { toFloating } from '../src/cvFind.mjs'
4
+ import { withTz } from './tools/withTz.mjs'
5
+ import { TZS, TZS_DST_EAST, OPS, WALL, CONDS, pad, condVal, condWall, wantIdx, tzSelfCheck } from './tools/floatTools.mjs'
6
+
7
+
8
+ //本檔為mssql與sqlite共用之浮動時間測試套組(CLAUDE_rulebook.md之R14), 由api-sqlite.test.mjs與api-mssql.test.mjs引入
9
+ //模擬w-orm-mdb之queryMemory: 資料表之DATE欄存「牆上時間分量作為UTC瞬間」(數據列經toFloating), 條件走ctx.dateNoTz:'floating'經cvFind取得where, 由model執行findAll/destroy
10
+ //(WOrmReladb之getFindCtx不設定dateNoTz, 公開select/delAll不走此路徑; 數據由公開insert寫入, 取得where後由實例之model執行)
11
+ //預言機: 牆上時間字串以UTC分量數值比較(Jet語意: 無時區、無夏令), 與主機時區無關
12
+ //時區矩陣: 12個時區(含負偏移、半小時與45分偏移、+14、夏令跳過之UTC+時區), 以執行期賦值process.env.TZ切換, 每個時區先自證偏移
13
+
14
+
15
+ let rid = (i) => `r${pad(i)}`
16
+
17
+ let hostZones = TZS.concat(TZS_DST_EAST)
18
+
19
+
20
+ /**
21
+ * 定義共用測試套組
22
+ *
23
+ * @param {String} label 輸入後端名稱字串('sqlite'或'mssql'), 供describe標題使用
24
+ * @param {Function} getOpt 輸入取得建構設定之函數, 於hook內呼叫以取得當下有效之連線資訊
25
+ */
26
+ export function defineFloatSuites(label, getOpt) {
27
+
28
+ describe(`${label} 浮動時間: dateNoTz:'floating'與toFloating`, function() {
29
+
30
+ let w = null
31
+ let ins = null
32
+ let md = null
33
+ let cs = null
34
+
35
+ //load, 清表後以公開insert寫入[id, d]
36
+ let load = async (pairs) => {
37
+ await md.destroy({ where: {} })
38
+ let r = await w.insert(pairs.map(([id, d]) => ({ id, d })), cs)
39
+ assert.strictEqual(r.nInserted, pairs.length)
40
+ }
41
+
42
+ //whereOf, 取經cvFind轉譯之where(attrs取實際執行where之model之rawAttributes, 同mdb之queryMemory)
43
+ let whereOf = (find, ex = {}) => cvFind(find, { attrs: md.rawAttributes, pk: 'id', ...ex })
44
+
45
+ let idsOf = async (find, ex = {}) => {
46
+ let rs = await md.findAll({ where: whereOf(find, ex), attributes: ['id'], raw: true })
47
+ return rs.map((r) => r.id).sort()
48
+ }
49
+
50
+ let allIds = async () => (await md.findAll({ attributes: ['id'], raw: true })).map((r) => r.id).sort()
51
+
52
+ let msgOf = (e) => String(e && e.message !== undefined ? e.message : e)
53
+
54
+ before(async function() {
55
+ w = wo(getOpt({ cl: 'tfloat' }))
56
+ await w.createStorage()
57
+ ins = await w.init()
58
+ md = ins.mds.tfloat
59
+ cs = { instance: ins }
60
+ })
61
+
62
+ after(async function() {
63
+ await ins.close()
64
+ })
65
+
66
+ //運算矩陣: 條件18個牆上時間(字串、本地分量建構之Date、其epoch) x 6運算子 = 108組, 數據20列(含哨兵9999、年0100、夏令跳過/重疊之牆上時間、NULL)
67
+ for (let tz of hostZones) {
68
+
69
+ it(`${tz}: 108組條件(18個牆上時間 x 6運算子)命中集合 = 牆上時間數值比較之預言機, 無錯誤無誤拒; delAll($lt 今日)不誤刪9999哨兵`, async function() {
70
+ this.timeout(120000)
71
+ await withTz(tz, async () => {
72
+ tzSelfCheck(tz)
73
+ //數據列: 牆上時間字串經toFloating(同mdb之資料列轉換; 轉換失敗須整批拒收, 此處所有值皆須可換算)
74
+ let pairs = WALL.map((x, i) => {
75
+ let v = x === null ? null : toFloating(x)
76
+ assert.ok(x === null || v !== null, `數據列無法換算: ${x}`)
77
+ return [rid(i), v]
78
+ })
79
+ await load(pairs)
80
+ let n = 0
81
+ let bads = []
82
+ for (let [wl, kind] of CONDS) {
83
+ for (let op of OPS) {
84
+ n += 1
85
+ let cw = condWall(wl, kind)
86
+ let want = wantIdx(op, cw).map(rid)
87
+ let got
88
+ try {
89
+ got = await idsOf({ d: { [op]: condVal(wl, kind) } }, { dateNoTz: 'floating' })
90
+ }
91
+ catch (e) {
92
+ got = `REJ:${msgOf(e).slice(0, 80)}`
93
+ }
94
+ if (JSON.stringify(got) !== JSON.stringify(want)) {
95
+ bads.push({ wl, kind, op, got, want })
96
+ }
97
+ }
98
+ }
99
+ assert.strictEqual(n, 108)
100
+ assert.deepStrictEqual(bads, [], `${tz}: 錯誤或誤拒 ${bads.length}/108`)
101
+
102
+ //delAll($lt 今日): 三列expired、never(9999-12-31 23:59:59)、future, 只刪expired
103
+ await load([['e', toFloating('2020-01-01 00:00:00')], ['n', toFloating('9999-12-31 23:59:59')], ['f', toFloating('2099-01-01 00:00:00')]])
104
+ await md.destroy({ where: whereOf({ d: { $lt: '2026-10-20T00:00:00' } }, { dateNoTz: 'floating' }) })
105
+ assert.deepStrictEqual(await allIds(), ['f', 'n'], `${tz}: delAll($lt 今日)誤刪哨兵`)
106
+
107
+ //哨兵本身作條件值: 可用, 且只命中never
108
+ assert.deepStrictEqual(await idsOf({ d: { $gte: '9999-12-31T23:59:59' } }, { dateNoTz: 'floating' }), ['n'])
109
+ assert.deepStrictEqual(await idsOf({ d: '9999-12-31T23:59:59' }, { dateNoTz: 'floating' }), ['n'])
110
+ assert.deepStrictEqual(await idsOf({ d: { $gt: '9999-12-31T23:59:58', $lte: '9999-12-31T23:59:59' } }, { dateNoTz: 'floating' }), ['n'])
111
+ //$gt哨兵無列; $lt哨兵命中其餘兩列
112
+ assert.deepStrictEqual(await idsOf({ d: { $gt: '9999-12-31T23:59:59' } }, { dateNoTz: 'floating' }), [])
113
+ assert.deepStrictEqual(await idsOf({ d: { $lt: '9999-12-31T23:59:59' } }, { dateNoTz: 'floating' }), ['f'])
114
+ })
115
+ })
116
+
117
+ }
118
+
119
+ it('$in/$nin: 浮動值逐元素比對(2個時區)', async function() {
120
+ for (let tz of ['America/Los_Angeles', 'Asia/Taipei']) {
121
+ await withTz(tz, async () => {
122
+ await load([['a', toFloating('2026-01-01 00:00:00')], ['b', toFloating('2026-06-01 12:00:00')], ['c', toFloating('9999-12-31 23:59:59')], ['z', null]])
123
+ assert.deepStrictEqual(await idsOf({ d: { $in: ['2026-01-01T00:00:00', '9999-12-31T23:59:59'] } }, { dateNoTz: 'floating' }), ['a', 'c'])
124
+ assert.deepStrictEqual(await idsOf({ d: { $nin: ['2026-01-01T00:00:00', '9999-12-31T23:59:59'] } }, { dateNoTz: 'floating' }), ['b'])
125
+ assert.deepStrictEqual(await idsOf({ d: { $in: [] } }, { dateNoTz: 'floating' }), [])
126
+ assert.deepStrictEqual(await idsOf({ d: null }, { dateNoTz: 'floating' }), ['z'])
127
+ assert.deepStrictEqual(await idsOf({ d: { $ne: null } }, { dateNoTz: 'floating' }), ['a', 'b', 'c'])
128
+ })
129
+ }
130
+ })
131
+
132
+ it('不合法條件於轉譯階段拒收(不送SQL、資料表不變): 帶時區字串之曆法不存在、年0、5位年、亂字串、純日期(外審minimax F7)', async function() {
133
+ for (let tz of ['America/Los_Angeles', 'Asia/Taipei']) {
134
+ await withTz(tz, async () => {
135
+ await load([['a', toFloating('2026-01-01 00:00:00')], ['b', toFloating('2026-06-01 00:00:00')]])
136
+ for (let s of ['2026-02-30T00:00:00Z', '2025-02-29T00:00:00+08:00', '2026-04-31T00:00:00-08:00', '0000-06-15T00:00:00Z', '0000-06-15T00:00:00', '10000-01-01T00:00:00Z', '10000-01-01T00:00:00', 'garbage', '2026-01-01']) {
137
+ let e = null
138
+ try {
139
+ await idsOf({ d: { $lt: s } }, { dateNoTz: 'floating' })
140
+ }
141
+ catch (err) {
142
+ e = msgOf(err)
143
+ }
144
+ assert.ok(e !== null && e.indexOf('does not match column type [date]') >= 0, `${tz} [${s}] 應拒收: ${e}`)
145
+ }
146
+ assert.deepStrictEqual(await allIds(), ['a', 'b'])
147
+ })
148
+ }
149
+ })
150
+
151
+ //判別力自證(僅sqlite: 年10000之文字可存入sqlite而mssql不可): 同一矩陣對'local'臂(數據列以sequelize預設new Date(字串)依主機時區入表)於負偏移時區須出現錯誤或誤拒,
152
+ //證明上列矩陣與預言機能偵測此缺陷(現有mdb全套測試於TZ=PST8PDT亦全綠, 對此缺陷零偵測力)
153
+ if (label === 'sqlite') {
154
+ for (let tz of ['America/Los_Angeles', 'America/New_York', 'America/Sao_Paulo', 'Etc/GMT+12']) {
155
+ it(`${tz}: 判別力自證——'local'臂(數據列依主機時區入表)出現錯誤或誤拒, 且delAll($lt 今日)誤刪9999哨兵`, async function() {
156
+ await withTz(tz, async () => {
157
+ await md.destroy({ where: {} })
158
+ await md.bulkCreate(WALL.map((x, i) => ({ id: rid(i), d: x }))) //x為字串, sequelize以new Date(字串)依主機時區解析
159
+ let nBad = 0
160
+ for (let [wl, kind] of CONDS) {
161
+ for (let op of OPS) {
162
+ let want = wantIdx(op, condWall(wl, kind)).map(rid)
163
+ let got
164
+ try {
165
+ got = await idsOf({ d: { [op]: condVal(wl, kind) } }, { dateNoTz: 'local' })
166
+ }
167
+ catch (e) {
168
+ got = `REJ`
169
+ }
170
+ if (JSON.stringify(got) !== JSON.stringify(want)) {
171
+ nBad += 1
172
+ }
173
+ }
174
+ }
175
+ assert.ok(nBad >= 20, `${tz}: 'local'臂僅 ${nBad}/108 組出錯, 矩陣的判別力不足`)
176
+ //delAll($lt 今日)
177
+ await md.destroy({ where: {} })
178
+ await md.bulkCreate([['e', '2020-01-01 00:00:00'], ['n', '9999-12-31 23:59:59'], ['f', '2099-01-01 00:00:00']].map(([id, d]) => ({ id, d })))
179
+ await md.destroy({ where: whereOf({ d: { $lt: '2026-10-20T00:00:00' } }, { dateNoTz: 'local' }) })
180
+ assert.deepStrictEqual(await allIds(), ['f'], `${tz}: 'local'臂應誤刪哨兵n(此為修前缺陷, 'local'不變)`)
181
+ })
182
+ })
183
+ }
184
+ }
185
+
186
+ //契約①: 編碼後之浮動Date(經資料庫讀回之Date)不可再當'floating'條件輸入
187
+ it('契約①: 以toFloating入表之列, 經model讀回之Date再作條件輸入即二次位移(LA: $eq不命中該列、$ne命中; UTC: 恆等)', async function() {
188
+ for (let tz of ['America/Los_Angeles', 'Asia/Taipei', 'UTC']) {
189
+ await withTz(tz, async () => {
190
+ await load([['a', toFloating('2026-10-20 01:00:00')], ['b', toFloating('2026-10-21 01:00:00')]])
191
+ let row = (await md.findAll({ where: { id: 'a' } }))[0]
192
+ let back = row.get('d')
193
+ assert.ok(back instanceof Date)
194
+ assert.strictEqual(back.toISOString(), '2026-10-20T01:00:00.000Z', '讀回之Date即浮動瞬間')
195
+ //原條件(牆上時間字串)命中
196
+ assert.deepStrictEqual(await idsOf({ d: '2026-10-20T01:00:00' }, { dateNoTz: 'floating' }), ['a'])
197
+ //讀回之Date當條件
198
+ let eq = await idsOf({ d: { $eq: back } }, { dateNoTz: 'floating' })
199
+ let ne = await idsOf({ d: { $ne: back } }, { dateNoTz: 'floating' })
200
+ if (tz === 'UTC') {
201
+ assert.deepStrictEqual(eq, ['a'])
202
+ assert.deepStrictEqual(ne, ['b'])
203
+ }
204
+ else {
205
+ assert.deepStrictEqual(eq, [], `${tz}: 二次位移後不應命中該列`)
206
+ assert.deepStrictEqual(ne, ['a', 'b'], `${tz}: 二次位移後$ne命中該列(可被delAll刪除)`)
207
+ }
208
+ })
209
+ }
210
+ })
211
+
212
+ //契約②: toFloating回null者呼叫端須整批拒收, 不得以原值或NULL代入
213
+ it('契約②: 參考之呼叫端轉換遇toFloating為null即整批拒收(資料表不變); 以NULL代入則該列被範圍條件漏掉、被IS NULL命中', async function() {
214
+ let encode = (rows) => rows.map((r) => {
215
+ let v = r.d === null || r.d === undefined ? r.d : toFloating(r.d)
216
+ if (v === null) {
217
+ throw new Error(`invalid wall time at [${r.id}]`)
218
+ }
219
+ return { id: r.id, d: v }
220
+ })
221
+ await load([['k', toFloating('2020-01-01 00:00:00')]])
222
+ let rows = [{ id: 'a', d: '2026-01-01 00:00:00' }, { id: 'b', d: '2026-02-30 00:00:00' }, { id: 'c', d: '2026-03-01 00:00:00' }]
223
+ let r = null
224
+ try {
225
+ encode(rows)
226
+ }
227
+ catch (e) {
228
+ r = msgOf(e)
229
+ }
230
+ assert.strictEqual(r, 'invalid wall time at [b]')
231
+ assert.deepStrictEqual(await allIds(), ['k'], '整批拒收時不得有任何列寫入')
232
+
233
+ //反例: 以NULL代入
234
+ let filled = rows.map((x) => ({ id: x.id, d: toFloating(x.d) })) //b為null: 即以NULL代入
235
+ assert.strictEqual(filled[1].d, null)
236
+ let ins2 = await w.insert(filled, cs)
237
+ assert.strictEqual(ins2.nInserted, 3)
238
+ assert.ok((await idsOf({ d: { $lt: '2099-01-01T00:00:00' } }, { dateNoTz: 'floating' })).indexOf('b') < 0, 'NULL列不被範圍條件命中')
239
+ assert.deepStrictEqual(await idsOf({ d: null }, { dateNoTz: 'floating' }), ['b'], 'NULL列被IS NULL命中')
240
+ })
241
+
242
+ //契約④: 條件走'floating'與資料側toFloating同進同退
243
+ it('契約④: 同進同退——數據列經toFloating + floating條件得正確結果; 混用(數據列未經toFloating)於LA靜默誤命中', async function() {
244
+ await withTz('America/Los_Angeles', async () => {
245
+ let cond = { d: { $lt: '2026-10-20T04:00:00' } }
246
+
247
+ //同進: 牆上01:00 < 04:00 命中, 牆上05:00不命中
248
+ await load([['a', toFloating('2026-10-20 01:00:00')], ['b', toFloating('2026-10-20 05:00:00')]])
249
+ assert.deepStrictEqual(await idsOf(cond, { dateNoTz: 'floating' }), ['a'])
250
+
251
+ //混用: 數據列以本地分量建構之Date入表(PDT, UTC-7: 牆上01:00 = 08:00Z), 條件為浮動值(04:00Z)
252
+ await load([['a', new Date(2026, 9, 20, 1, 0, 0)], ['b', new Date(2026, 9, 20, 5, 0, 0)]])
253
+ assert.deepStrictEqual(await idsOf(cond, { dateNoTz: 'floating' }), [], '混用: 牆上01:00之列本應命中而被漏掉(靜默誤命中)')
254
+ //另一方向: 浮動數據列配'local'條件亦錯位(牆上01:00 = 01:00Z, 'local'條件04:00本地 = 11:00Z)
255
+ await load([['a', toFloating('2026-10-20 01:00:00')], ['b', toFloating('2026-10-20 05:00:00')], ['c', toFloating('2026-10-20 15:00:00')]])
256
+ assert.deepStrictEqual(await idsOf(cond, { dateNoTz: 'local' }), ['a', 'b'], '混用: 浮動數據列配local條件, 牆上05:00之列被誤命中')
257
+ })
258
+ })
259
+
260
+ })
261
+
262
+ }
@@ -0,0 +1,129 @@
1
+ import assert from 'assert'
2
+ import wo from '../src/WOrmReladb.mjs'
3
+ import lowerCases from 'wsemi/src/_lowerFindCases.mjs'
4
+
5
+
6
+ //本檔為mssql與sqlite共用之wsemi契約案例表_lowerFindCases(方言中立條件IR之契約表, 85案)測試套組(CLAUDE_rulebook.md之R15), 由api-sqlite.test.mjs與api-mssql.test.mjs引入
7
+ //本套件不採IR, 但此表之hits(手寫, 依SQL三值邏輯手推)是SQL族對公開select與delAll之共同期望, 作為日後任何翻譯層變更之守門
8
+ //規則: 依表之columns建model(models/tlower.js), seed()6列; 逐案對公開select與delAll驗——
9
+ // 非schema案兩函數須皆接受, select命中 = delAll實際刪除之id集合 = hits; hits為[]者恆假;
10
+ // hits為null(字串順序比較依collation而異)只驗兩函數一致; schema為true者(型別化後端可因欄位型別拒收)可reject, 但兩函數須一致reject、資料不變, 未reject時須 = hits
11
+
12
+
13
+ let { seed, semantics } = lowerCases
14
+
15
+ let ALL = [1, 2, 3, 4, 5, 6]
16
+
17
+ //案例表規模下限(wsemi 1.9.25為85案)與schema案之上限, 防止案例表被掏空後仍全綠
18
+ let nMin = 85
19
+
20
+ let ids = (rs) => rs.map((v) => v.id).sort((a, b) => a - b)
21
+
22
+ let msgOf = (err) => String(err && err.message !== undefined ? err.message : err)
23
+
24
+ //settle, 執行promise並回傳{ok,v}或{err}
25
+ let settle = (pm) => pm.then((v) => ({ ok: true, v })).catch((err) => ({ err }))
26
+
27
+
28
+ /**
29
+ * 定義共用測試套組
30
+ *
31
+ * @param {String} label 輸入後端名稱字串('sqlite'或'mssql'), 供describe標題使用
32
+ * @param {Function} getOpt 輸入取得建構設定之函數, 於hook內呼叫以取得當下有效之連線資訊
33
+ */
34
+ export function defineLowerCaseSuites(label, getOpt) {
35
+
36
+ describe(`${label} lowerFind契約表: 案例逐案對select與delAll`, function() {
37
+
38
+ let w = null
39
+ let ins = null
40
+ let cs = null
41
+
42
+ //stats, 各案測試時累計, 供最末之防空轉斷言
43
+ let stats = { accepted: 0, acceptedHit: 0, acceptedNull: 0, rejected: 0 }
44
+
45
+ let reset = async () => {
46
+ await w.delAll({}, cs)
47
+ let r = await w.insert(seed(), cs)
48
+ assert.strictEqual(r.nInserted, 6)
49
+ }
50
+
51
+ before(async function() {
52
+ w = wo(getOpt({ cl: 'tlower' }))
53
+ await w.createStorage()
54
+ ins = await w.init()
55
+ cs = { instance: ins }
56
+ })
57
+
58
+ after(async function() {
59
+ await ins.close()
60
+ })
61
+
62
+ it(`案例表規模: 至少${nMin}案, 代號不重複, 含hits為[]、非空、null三類與schema案`, function() {
63
+ assert.ok(semantics.length >= nMin, `案數 ${semantics.length}`)
64
+ assert.strictEqual(new Set(semantics.map((c) => c.id)).size, semantics.length)
65
+ assert.ok(semantics.some((c) => Array.isArray(c.hits) && c.hits.length === 0))
66
+ assert.ok(semantics.some((c) => Array.isArray(c.hits) && c.hits.length > 0))
67
+ assert.ok(semantics.some((c) => c.hits === null))
68
+ assert.ok(semantics.some((c) => c.schema === true))
69
+ })
70
+
71
+ it('種子: 6列, id 1~6', async function() {
72
+ let sd = seed()
73
+ assert.deepStrictEqual(sd.map((v) => v.id), ALL)
74
+ await reset()
75
+ assert.deepStrictEqual(ids(await w.select({}, cs)), ALL)
76
+ })
77
+
78
+ for (let c of semantics) {
79
+
80
+ it(`${c.id} (${c.node}): select命中 = delAll刪除 = hits${c.hits === null ? '(null: 只驗兩函數一致)' : ''}${c.schema ? '(schema案可reject)' : ''}`, async function() {
81
+ await reset()
82
+ let rs = await settle(w.select(c.find(), cs))
83
+ if (rs.err !== undefined) {
84
+ //schema案: 型別化後端可因欄位型別檢核而reject, 此時兩函數須一致reject且資料不變
85
+ assert.ok(c.schema === true, `非schema案不得reject: ${msgOf(rs.err)}`)
86
+ assert.strictEqual(msgOf(rs.err).indexOf('invalid find: '), 0, msgOf(rs.err))
87
+ let before = ids(await w.select({}, cs))
88
+ let rd = await settle(w.delAll(c.find(), cs))
89
+ assert.ok(rd.err !== undefined, 'select reject而delAll未reject')
90
+ assert.strictEqual(msgOf(rd.err), msgOf(rs.err))
91
+ assert.deepStrictEqual(ids(await w.select({}, cs)), before)
92
+ stats.rejected += 1
93
+ return
94
+ }
95
+ let sel = ids(rs.v)
96
+ if (c.hits !== null) {
97
+ assert.deepStrictEqual(sel, c.hits, 'select之命中')
98
+ }
99
+ let r = await w.delAll(c.find(), cs)
100
+ let left = ids(await w.select({}, cs))
101
+ let gone = ALL.filter((v) => left.indexOf(v) < 0)
102
+ assert.deepStrictEqual(gone, sel, 'delAll之刪除 = select之命中')
103
+ if (c.hits !== null) {
104
+ assert.deepStrictEqual(gone, c.hits, 'delAll之刪除')
105
+ }
106
+ assert.deepStrictEqual(r, { n: gone.length, nDeleted: gone.length, ok: 1 })
107
+ stats.accepted += 1
108
+ if (c.hits === null) {
109
+ stats.acceptedNull += 1
110
+ }
111
+ else if (c.hits.length > 0) {
112
+ stats.acceptedHit += 1
113
+ }
114
+ })
115
+
116
+ }
117
+
118
+ it('防空轉: 全部非schema案皆被接受(任一正常案全部reject即紅), 且命中非空之案數達標', function() {
119
+ let nSchema = semantics.filter((c) => c.schema === true).length
120
+ let nHit = semantics.filter((c) => Array.isArray(c.hits) && c.hits.length > 0).length
121
+ assert.ok(stats.rejected <= nSchema, `reject ${stats.rejected} 案, 超過schema案數 ${nSchema}`)
122
+ assert.ok(stats.accepted >= semantics.length - nSchema, `接受 ${stats.accepted} 案, 少於 ${semantics.length - nSchema}`)
123
+ assert.ok(stats.acceptedHit >= nHit - nSchema, `命中非空之接受案 ${stats.acceptedHit} 少於 ${nHit - nSchema}`)
124
+ assert.ok(stats.acceptedHit >= 50, `命中非空之接受案 ${stats.acceptedHit} 不足50`)
125
+ })
126
+
127
+ })
128
+
129
+ }
@@ -5,6 +5,9 @@ import { defineSuites } from './api-suite.mjs'
5
5
  import { defineWhitelistSuites } from './api-find-whitelist.mjs'
6
6
  import { defineDateWriteSuites } from './api-date-write.mjs'
7
7
  import { defineWellformedSuites } from './api-wellformed.mjs'
8
+ import { defineLowerCaseSuites } from './api-lower-cases.mjs'
9
+ import { defineScopeSuites } from './api-scope.mjs'
10
+ import { defineFloatSuites } from './api-float.mjs'
8
11
 
9
12
 
10
13
  //測試自行創建docker容器供MSSQL服務, 測試結束即銷毀, 故僅需環境已安裝docker
@@ -151,3 +154,6 @@ defineSuites('mssql', getOpt)
151
154
  defineWhitelistSuites('mssql', getOpt)
152
155
  defineDateWriteSuites('mssql', getOpt)
153
156
  defineWellformedSuites('mssql', getOpt)
157
+ defineLowerCaseSuites('mssql', getOpt)
158
+ defineScopeSuites('mssql', getOpt)
159
+ defineFloatSuites('mssql', getOpt)
@@ -0,0 +1,208 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import assert from 'assert'
4
+ import wo from '../src/WOrmReladb.mjs'
5
+
6
+
7
+ //本檔為mssql與sqlite共用之defaultScope測試套組(CLAUDE_rulebook.md之R16), 由api-sqlite.test.mjs與api-mssql.test.mjs引入
8
+ //sequelize預設以Object.assign合併scope.where與傳入where(同名頂層鍵後者覆寫), scope.where為Sequelize.literal或Sequelize.where(非一般物件)時scope直接被丟棄;
9
+ //WOrmReladb於Sequelize實例之define層級設定whereMergeStrategy:'and', 令兩者一律以AND結合
10
+ //5種defaultScope(純鍵、Op.or、Op.and、Sequelize.literal、Sequelize.where) x 5種find, 對公開select與delAll:
11
+ // select之命中 = delAll實際刪除之主鍵集合 = 手推期望(scope範圍內且符合find之列); delAll實際刪除之主鍵以不套scope之讀取(unscoped)取得, 不以select代替
12
+ //Op.and之scope: chkScopeForDelAll維持拒收delAll(R1), select則須套用該scope
13
+
14
+
15
+ let ROWS = [
16
+ { id: '1', g: 'A', name: 'x' },
17
+ { id: '2', g: 'B', name: 'x' },
18
+ { id: '3', g: 'A', name: 'y' },
19
+ { id: '4', g: 'C', name: 'x' },
20
+ ]
21
+
22
+ //SCOPES, where為寫入model檔之defaultScope.where原始碼, inScope為scope範圍之手推判定
23
+ let SCOPES = [
24
+ { key: 'pk', label: '純鍵', where: '{ g: \'A\' }', inScope: (r) => r.g === 'A', delAllRejects: false },
25
+ { key: 'or', label: 'Op.or', where: '{ [Op.or]: [{ g: \'A\' }, { g: \'C\' }] }', inScope: (r) => r.g === 'A' || r.g === 'C', delAllRejects: false },
26
+ { key: 'and', label: 'Op.and', where: '{ [Op.and]: [{ g: \'A\' }] }', inScope: (r) => r.g === 'A', delAllRejects: true },
27
+ { key: 'lit', label: 'Sequelize.literal', where: 'sequelize.literal(\'g = \\\'A\\\'\')', inScope: (r) => r.g === 'A', delAllRejects: false },
28
+ { key: 'whr', label: 'Sequelize.where', where: 'sequelize.where(sequelize.col(\'g\'), \'A\')', inScope: (r) => r.g === 'A', delAllRejects: false },
29
+ //model自身指定whereMergeStrategy:'overwrite'(蓋過Sequelize實例之define預設): 匯入後仍強制為'and'(外審sol F2)
30
+ { key: 'ovw', label: 'Sequelize.literal + model自身指定whereMergeStrategy overwrite', where: 'sequelize.literal(\'g = \\\'A\\\'\')', modelOpt: ', whereMergeStrategy: \'overwrite\'', inScope: (r) => r.g === 'A', delAllRejects: false },
31
+ ]
32
+
33
+ //FINDS, p為find之手推判定
34
+ let FINDS = [
35
+ { label: '{g:"B"}(與scope同欄之單葉)', find: () => ({ g: 'B' }), p: (r) => r.g === 'B' },
36
+ { label: '{name:"x"}(他欄單葉)', find: () => ({ name: 'x' }), p: (r) => r.name === 'x' },
37
+ { label: '$or根', find: () => ({ $or: [{ g: 'B' }, { name: 'y' }] }), p: (r) => r.g === 'B' || r.name === 'y' },
38
+ { label: 'and根(多欄)', find: () => ({ name: 'x', g: { $ne: 'Z' } }), p: (r) => r.name === 'x' && r.g !== 'Z' },
39
+ { label: '{}(全部)', find: () => ({}), p: () => true },
40
+ ]
41
+
42
+ let ids = (rs) => rs.map((v) => v.id).sort()
43
+
44
+ let msgOf = (err) => String(err && err.message !== undefined ? err.message : err)
45
+
46
+ let genModel = (cl, where, modelOpt = '') => {
47
+ return [
48
+ 'module.exports = function(sequelize, DataTypes) {',
49
+ ' let Op = require(\'sequelize\').Op',
50
+ ` return sequelize.define('${cl}', {`,
51
+ ' id: { type: DataTypes.STRING, allowNull: false, primaryKey: true },',
52
+ ' g: { type: DataTypes.STRING, allowNull: true },',
53
+ ' name: { type: DataTypes.STRING, allowNull: true },',
54
+ ` }, { tableName: '${cl}', defaultScope: { where: ${where} }${modelOpt} })`,
55
+ '}',
56
+ '',
57
+ ].join('\n')
58
+ }
59
+
60
+
61
+ /**
62
+ * 定義共用測試套組
63
+ *
64
+ * @param {String} label 輸入後端名稱字串('sqlite'或'mssql'), 供describe標題與暫存夾使用
65
+ * @param {Function} getOpt 輸入取得建構設定之函數, 於hook內呼叫以取得當下有效之連線資訊
66
+ */
67
+ export function defineScopeSuites(label, getOpt) {
68
+
69
+ let fd = `./test/_tmp/api-scope-${label}`
70
+
71
+ describe(`${label} defaultScope: 6種scope(含model自身指定overwrite) x 5種find, select與delAll不越出scope`, function() {
72
+
73
+ before(function() {
74
+ fs.mkdirSync(fd, { recursive: true })
75
+ for (let sc of SCOPES) {
76
+ fs.writeFileSync(path.join(fd, `tsc${sc.key}.js`), genModel(`tsc${sc.key}`, sc.where, sc.modelOpt), 'utf8')
77
+ }
78
+ })
79
+
80
+ after(function() {
81
+ fs.rmSync(fd, { recursive: true, force: true })
82
+ })
83
+
84
+ for (let sc of SCOPES) {
85
+
86
+ describe(`scope ${sc.label}`, function() {
87
+
88
+ let cl = `tsc${sc.key}`
89
+ let w = null
90
+ let ins = null
91
+ let cs = null
92
+
93
+ //unscoped, 不套scope之全表讀取(取delAll實際刪除之主鍵), 不經select
94
+ let unscoped = async () => {
95
+ return ids(await ins.mds[cl].unscoped().findAll({ raw: true }))
96
+ }
97
+
98
+ before(async function() {
99
+ w = wo(getOpt({ cl, fdModels: fd }))
100
+ await w.createStorage()
101
+ ins = await w.init()
102
+ cs = { instance: ins }
103
+ })
104
+
105
+ after(async function() {
106
+ await ins.close()
107
+ })
108
+
109
+ beforeEach(async function() {
110
+ await ins.mds[cl].unscoped().destroy({ where: {} })
111
+ let r = await w.insert(ROWS, cs)
112
+ assert.strictEqual(r.nInserted, 4)
113
+ assert.deepStrictEqual(await unscoped(), ['1', '2', '3', '4'])
114
+ })
115
+
116
+ it('合併策略: Sequelize實例之define預設與匯入後之model皆為whereMergeStrategy:and(model自身指定overwrite者亦被改為and)', function() {
117
+ let md = ins.mds[cl]
118
+ assert.strictEqual(md.sequelize.options.define.whereMergeStrategy, 'and')
119
+ assert.strictEqual(md.options.whereMergeStrategy, 'and')
120
+ })
121
+
122
+ it('前提: select({})僅回scope範圍內之列(scope確實套用)', async function() {
123
+ let exp = ids(ROWS.filter(sc.inScope))
124
+ assert.ok(exp.length > 0 && exp.length < ROWS.length, '夾具之scope須同時含範圍內外之列')
125
+ assert.deepStrictEqual(ids(await w.select({}, cs)), exp)
126
+ })
127
+
128
+ for (let f of FINDS) {
129
+
130
+ it(`${f.label}: select命中 = 手推期望(scope AND find)`, async function() {
131
+ let exp = ids(ROWS.filter((r) => sc.inScope(r) && f.p(r)))
132
+ assert.deepStrictEqual(ids(await w.select(f.find(), cs)), exp)
133
+ })
134
+
135
+ if (sc.delAllRejects) {
136
+ it(`${f.label}: delAll拒收(chkScopeForDelAll維持, 含Op.and之scope.where)且不刪除`, async function() {
137
+ let r = await w.delAll(f.find(), cs).then(() => null).catch((err) => msgOf(err))
138
+ assert.ok(r !== null && r.indexOf('defaultScope') >= 0, `未reject: ${r}`)
139
+ assert.deepStrictEqual(await unscoped(), ['1', '2', '3', '4'])
140
+ })
141
+ }
142
+ else {
143
+ it(`${f.label}: delAll實際刪除之主鍵集合 = 手推期望, 不越出scope`, async function() {
144
+ let exp = ids(ROWS.filter((r) => sc.inScope(r) && f.p(r)))
145
+ let r = await w.delAll(f.find(), cs)
146
+ let left = await unscoped()
147
+ let gone = ['1', '2', '3', '4'].filter((v) => left.indexOf(v) < 0)
148
+ assert.deepStrictEqual(gone, exp, `delAll刪除之主鍵(scope範圍: ${ids(ROWS.filter(sc.inScope))})`)
149
+ assert.ok(gone.every((v) => sc.inScope(ROWS.find((x) => x.id === v))), 'delAll刪到scope之外')
150
+ assert.deepStrictEqual(r, { n: exp.length, nDeleted: exp.length, ok: 1 })
151
+ })
152
+ }
153
+
154
+ }
155
+
156
+ it('scope內之主鍵: selectByPk命中、del實際刪除該列(其餘列不動)、save更新該列完整欄位(外審sol F5)', async function() {
157
+ let inRows = ROWS.filter(sc.inScope)
158
+ let inId = inRows[0].id
159
+ let other = ['1', '2', '3', '4'].filter((v) => v !== inId)
160
+ assert.deepStrictEqual(await w.selectByPk(inId, cs), ROWS.find((r) => r.id === inId))
161
+ //save: 更新scope內之列
162
+ let rs = await w.save({ id: inId, g: inRows[0].g, name: 'zz' }, cs)
163
+ assert.deepStrictEqual(rs, [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }])
164
+ assert.deepStrictEqual(await w.selectByPk(inId, cs), { id: inId, g: inRows[0].g, name: 'zz' })
165
+ //del: 刪除scope內之列
166
+ let rd = await w.del({ id: inId }, cs)
167
+ assert.deepStrictEqual(rd, [{ n: 1, nDeleted: 1, ok: 1 }])
168
+ assert.deepStrictEqual(await unscoped(), other, 'del只刪該列')
169
+ })
170
+
171
+ it('scope外已存在之主鍵: save(預設autoInsert:true)本筆失敗(ok:0, 預讀不見該列而改插入, 撞主鍵重試耗盡), 該列不改動(外審minimax F1; 修前literal/where scope下該列會被更新)', async function() {
172
+ let outRow = ROWS.find((r) => !sc.inScope(r))
173
+ let rs = await w.save({ id: outRow.id, g: outRow.g, name: 'zz' }, cs)
174
+ assert.strictEqual(rs.length, 1)
175
+ assert.strictEqual(rs[0].ok, 0, `scope外之既有主鍵save應失敗: ${JSON.stringify(rs)}`)
176
+ assert.strictEqual(rs[0].nModified, 0)
177
+ assert.strictEqual(rs[0].nInserted, 0)
178
+ assert.ok(String(rs[0].err).indexOf('can not save') >= 0, String(rs[0].err))
179
+ let row = (await ins.mds[cl].unscoped().findAll({ where: { id: outRow.id }, raw: true }))[0]
180
+ assert.strictEqual(row.name, outRow.name, 'scope外之列被改動')
181
+ assert.deepStrictEqual(await unscoped(), ['1', '2', '3', '4'])
182
+ })
183
+
184
+ it('scope外之主鍵: save(autoInsert:false)不改動該列(外審sol F5)', async function() {
185
+ let outRow = ROWS.find((r) => !sc.inScope(r))
186
+ let rs = await w.save({ id: outRow.id, g: outRow.g, name: 'zz' }, { ...cs, autoInsert: false })
187
+ assert.strictEqual(rs[0].nModified, 0, `save更新了scope外之列: ${JSON.stringify(rs)}`)
188
+ let row = (await ins.mds[cl].unscoped().findAll({ where: { id: outRow.id }, raw: true }))[0]
189
+ assert.strictEqual(row.name, outRow.name, 'scope外之列被改動')
190
+ })
191
+
192
+ it('selectByPk、del亦不越出scope(scope外之主鍵: selectByPk回null、del不刪)', async function() {
193
+ let outId = ROWS.find((r) => !sc.inScope(r)).id
194
+ let inId = ROWS.find((r) => sc.inScope(r)).id
195
+ assert.strictEqual(await w.selectByPk(outId, cs), null)
196
+ assert.ok((await w.selectByPk(inId, cs)) !== null)
197
+ let r = await w.del({ id: outId }, cs)
198
+ assert.deepStrictEqual(r, [{ n: 0, nDeleted: 0, ok: 1 }], 'del刪到scope外之列')
199
+ assert.deepStrictEqual(await unscoped(), ['1', '2', '3', '4'])
200
+ })
201
+
202
+ })
203
+
204
+ }
205
+
206
+ })
207
+
208
+ }
@@ -3,6 +3,9 @@ import { defineSuites } from './api-suite.mjs'
3
3
  import { defineWhitelistSuites } from './api-find-whitelist.mjs'
4
4
  import { defineDateWriteSuites } from './api-date-write.mjs'
5
5
  import { defineWellformedSuites } from './api-wellformed.mjs'
6
+ import { defineLowerCaseSuites } from './api-lower-cases.mjs'
7
+ import { defineScopeSuites } from './api-scope.mjs'
8
+ import { defineFloatSuites } from './api-float.mjs'
6
9
 
7
10
 
8
11
  //sqlite使用獨立暫存storage檔測試, 不使用專案根目錄之worm.sqlite
@@ -46,3 +49,6 @@ defineSuites('sqlite', getOpt)
46
49
  defineWhitelistSuites('sqlite', getOpt)
47
50
  defineDateWriteSuites('sqlite', getOpt)
48
51
  defineWellformedSuites('sqlite', getOpt)
52
+ defineLowerCaseSuites('sqlite', getOpt)
53
+ defineScopeSuites('sqlite', getOpt)
54
+ defineFloatSuites('sqlite', getOpt)