w-orm-reladb 1.1.6 → 1.1.8

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,9 +1,10 @@
1
1
  import assert from 'assert'
2
+ import Sequelize from 'sequelize'
2
3
  import wo from '../src/WOrmReladb.mjs'
3
4
 
4
5
 
5
6
  //本檔為mssql與sqlite共用之測試套組, 令兩種後端以同一組斷言驗證, 避免各自漂移
6
- //斷言皆為規格(資料庫函數回傳定義計算.md)之可執行翻譯, 非現況指紋
7
+ //斷言皆為規格(w-orm系列套件統一規格.md)之可執行翻譯, 非現況指紋
7
8
 
8
9
 
9
10
  //genRs, 初始插入數據
@@ -120,8 +121,9 @@ export function defineSuites(label, getOpt) {
120
121
  assert.deepStrictEqual(sp[1], { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 })
121
122
  })
122
123
 
123
- it('select $regex不分大小寫', async function() {
124
- let sr = await w.select({ name: { $regex: 'PeT', $options: '$i' } })
124
+ it('select $regex給i時不分大小寫', async function() {
125
+ //rulebook R6: $options為i時兩側LOWER, 不分大小寫不倚賴定序
126
+ let sr = await w.select({ name: { $regex: 'PeT', $options: 'i' } })
125
127
  assert.deepStrictEqual(sr, [{ id: 'id-peter', name: 'peter(modify)', value: 123 }])
126
128
  })
127
129
 
@@ -427,6 +429,17 @@ export function defineSuites(label, getOpt) {
427
429
  await chk('delAll', () => w.delAll({}, cs))
428
430
  })
429
431
 
432
+ it('close後select亦由入口守門reject, 訊息同其餘六函數', async function() {
433
+ //rulebook R7: 七函數皆經procClosed, 不倚賴驅動層訊息恰含closed
434
+ let instance = await w.init()
435
+ await instance.close()
436
+ let r = await w.select({ id: 'k1' }, { instance })
437
+ .then((v) => ({ RESOLVE: v }))
438
+ .catch((err) => ({ REJECT: String(err.message || err) }))
439
+ assert.ok(r.REJECT !== undefined, `未reject, 實得 ${JSON.stringify(r.RESOLVE)}`)
440
+ assert.ok(r.REJECT.indexOf('instance is closed') >= 0, `訊息非入口守門之訊息: ${r.REJECT}`)
441
+ })
442
+
430
443
  it('close後selectByPk給無效主鍵亦須reject而非回null', async function() {
431
444
  let instance = await w.init()
432
445
  await instance.close()
@@ -871,4 +884,480 @@ export function defineSuites(label, getOpt) {
871
884
  })
872
885
 
873
886
 
887
+ describe(`${label} find`, function() {
888
+
889
+ //條件(find)之轉譯, 對應CLAUDE_rulebook.md之R1–R7; 預期值皆由帳本規則推得, 非現況指紋
890
+ //資料: r1–r5各欄有值, r6除主鍵外皆為null; r2之d為本地今日00:00(用以驗DATE條件不得以今日00:00比對而誤刪)
891
+
892
+ let Op = Sequelize.Op
893
+ let w = null
894
+
895
+ let today0 = () => {
896
+ let n = new Date()
897
+ return new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0, 0)
898
+ }
899
+
900
+ let genRc = () => {
901
+ return [
902
+ { 'id': 'r1', 's': 's1', 't': 'peter', 'i': 1, 'f': 1.5, 'p': 1.5, 'dec': 1.25, 'b': true, 'd': new Date('2019-01-01T00:00:00Z'), 'length': 0, 'p$x': 'q' },
903
+ { 'id': 'r2', 's': 's2', 't': 'Peter', 'i': 2, 'f': 2.5, 'p': 2.5, 'dec': 2.5, 'b': false, 'd': new Date('2026-06-15T00:00:00Z'), 'length': 3, 'p$x': 'w' },
904
+ { 'id': 'r3', 's': 's3', 't': 'rose_mary', 'i': 3, 'f': 3.5, 'p': 3.5, 'dec': 3.75, 'b': true, 'd': new Date('2026-06-01T00:00:00Z'), 'length': 5, 'p$x': 'q' },
905
+ { 'id': 'r4', 's': 's4', 't': '100%', 'i': 4, 'f': 4.5, 'p': 4.5, 'dec': 4.5, 'b': false, 'd': new Date('2026-06-02T00:00:00Z'), 'length': 7, 'p$x': 'e' },
906
+ { 'id': 'r5', 's': 's5', 't': 'x[a-c]y', 'i': 5, 'f': 5.5, 'p': 5.5, 'dec': 5.5, 'b': true, 'd': new Date('2026-06-03T00:00:00Z'), 'length': 9, 'p$x': 'r' },
907
+ { id: 'r6' },
908
+ ]
909
+ }
910
+ let ALL = ['r1', 'r2', 'r3', 'r4', 'r5', 'r6']
911
+ let NN = ['r1', 'r2', 'r3', 'r4', 'r5']
912
+
913
+ let ids = (rs) => rs.map((v) => v.id).sort()
914
+
915
+ let reset = async () => {
916
+ await w.delAll()
917
+ let r = await w.insert(genRc())
918
+ assert.strictEqual(r.nInserted, 6)
919
+ }
920
+
921
+ before(async function() {
922
+ w = wo(getOpt({ cl: 'tcond' }))
923
+ await w.createStorage()
924
+ })
925
+
926
+ //[說明, find, 預期命中之id]
927
+ let cases = [
928
+ //R4 七欄型之等值
929
+ ['STRING 等值', { s: 's1' }, ['r1']],
930
+ ['TEXT 等值', { t: 'rose_mary' }, ['r3']],
931
+ ['INTEGER 等值', { i: 1 }, ['r1']],
932
+ ['FLOAT 等值', { f: 1.5 }, ['r1']],
933
+ ['DECIMAL 等值', { dec: 1.25 }, ['r1']],
934
+ ['BOOLEAN 等值', { b: true }, ['r1', 'r3', 'r5']],
935
+ ['DATE 等值', { d: new Date('2019-01-01T00:00:00Z') }, ['r1']],
936
+ //R4 七欄型 × 運算子
937
+ ['STRING $gt', { s: { $gt: 's3' } }, ['r4', 'r5']],
938
+ ['INTEGER $gt', { i: { $gt: 3 } }, ['r4', 'r5']],
939
+ ['FLOAT $gt', { f: { $gt: 3.5 } }, ['r4', 'r5']],
940
+ ['DECIMAL $gt', { dec: { $gt: 3.75 } }, ['r4', 'r5']],
941
+ ['DATE $gt', { d: { $gt: new Date('2026-06-01T12:00:00Z') } }, ['r2', 'r4', 'r5']],
942
+ ['INTEGER $ne(SQL三值不含null)', { i: { $ne: 1 } }, ['r2', 'r3', 'r4', 'r5']],
943
+ ['BOOLEAN $ne', { b: { $ne: true } }, ['r2', 'r4']],
944
+ ['INTEGER $in', { i: { $in: [1, 3] } }, ['r1', 'r3']],
945
+ ['FLOAT $in', { f: { $in: [2.5] } }, ['r2']],
946
+ ['DECIMAL $in', { dec: { $in: [2.5] } }, ['r2']],
947
+ ['DATE $in', { d: { $in: [new Date('2019-01-01T00:00:00Z')] } }, ['r1']],
948
+ ['INTEGER $nin', { i: { $nin: [1, 3] } }, ['r2', 'r4', 'r5']],
949
+ ['INTEGER $not $gt', { i: { $not: { $gt: 2 } } }, ['r1', 'r2']],
950
+ ['FLOAT $not $lte', { f: { $not: { $lte: 2.5 } } }, ['r3', 'r4', 'r5']],
951
+ ['DATE $not $gt', { d: { $not: { $gt: new Date('2020-01-01T00:00:00Z') } } }, ['r1']],
952
+ ['同欄多運算子', { i: { $gt: 1, $lt: 5 } }, ['r2', 'r3', 'r4']],
953
+ ['DATE 以帶時區之ISO字串比較', { d: { $lt: '2020-01-01T00:00:00Z' } }, ['r1']],
954
+ ['DATE 以帶偏移之ISO字串比較', { d: { $lt: '2020-01-01T08:00:00+08:00' } }, ['r1']],
955
+ ['DOUBLE $gt', { p: { $gt: 3.5 } }, ['r4', 'r5']],
956
+ ['DOUBLE $not $lte', { p: { $not: { $lte: 2.5 } } }, ['r3', 'r4', 'r5']],
957
+ ['INTEGER 以數值字串比較', { i: '2' }, ['r2']],
958
+ //R3 邏輯
959
+ ['$and', { $and: [{ i: { $gte: 2 } }, { b: false }] }, ['r2', 'r4']],
960
+ ['$or', { $or: [{ s: 's1' }, { i: 5 }] }, ['r1', 'r5']],
961
+ ['$nor(SQL三值)', { $nor: [{ s: 's1' }, { i: 5 }] }, ['r2', 'r3', 'r4']],
962
+ //R5 NULL
963
+ ['{t:null}', { t: null }, ['r6']],
964
+ ['$eq null', { t: { $eq: null } }, ['r6']],
965
+ ['$in [null] 與{t:null}一致', { t: { $in: [null] } }, ['r6']],
966
+ ['$in [null,值]', { t: { $in: [null, 'rose_mary'] } }, ['r3', 'r6']],
967
+ ['$ne null', { t: { $ne: null } }, NN],
968
+ ['$nin [null] 與$ne null一致', { t: { $nin: [null] } }, NN],
969
+ ['$nin [null,值]', { t: { $nin: [null, 'rose_mary'] } }, ['r1', 'r2', 'r4', 'r5']],
970
+ ['$exists true', { i: { $exists: true } }, NN],
971
+ ['$exists false', { i: { $exists: false } }, ['r6']],
972
+ ['$in 空陣列為無', { i: { $in: [] } }, []],
973
+ ['$nin 空陣列為全部', { i: { $nin: [] } }, ALL],
974
+ ['$not $in 空陣列為全部', { i: { $not: { $in: [] } } }, ALL],
975
+ //R6 $regex
976
+ ['$regex 字面底線', { t: { $regex: '_' } }, ['r3']],
977
+ ['$regex 字面百分比', { t: { $regex: '%' } }, ['r4']],
978
+ ['$regex 跳脫之方括號', { t: { $regex: '\\[a-c\\]' } }, ['r5']],
979
+ ['$regex $options i', { t: { $regex: 'PET', $options: 'i' } }, ['r1', 'r2']],
980
+ ['$regex RegExp i', { t: { $regex: /PET/i } }, ['r1', 'r2']],
981
+ ['$regex $options $i(過渡期視同i)', { t: { $regex: 'PeT', $options: '$i' } }, ['r1', 'r2']],
982
+ ['RegExp 作為值', { t: /PET/i }, ['r1', 'r2']],
983
+ ['$not $regex', { t: { $not: { $regex: '_' } } }, ['r1', 'r2', 'r4', 'r5']],
984
+ //R2 特殊鍵
985
+ ['欄名length不得使條件失效', { s: 's1', length: 0 }, ['r1']],
986
+ ['欄名length帶運算子', { length: { $gte: 5 } }, ['r3', 'r4', 'r5']],
987
+ ['欄名含$', { 'p$x': 'q' }, ['r1', 'r3']],
988
+ ['Symbol Op.or', { [Op.or]: [{ s: 's1' }, { s: 's2' }] }, ['r1', 'r2']],
989
+ ['Symbol Op.in', { i: { [Op.in]: [1, 2] } }, ['r1', 'r2']],
990
+ ['陣列值為IN', { i: [1, 2] }, ['r1', 'r2']],
991
+ ['陣列值含null同$in', { t: [null, 'rose_mary'] }, ['r3', 'r6']],
992
+ //空子式為恆真/恆假, 不得被濾除而改變語意
993
+ ['$or 含空條件為全部', { $or: [{}, { s: 's1' }] }, ALL],
994
+ ['$nor 含空條件為無', { $nor: [{}] }, []],
995
+ ['$nor 含空條件與他式為無', { $nor: [{}, { s: 's1' }] }, []],
996
+ ['$not $regex i', { t: { $not: { $regex: 'PET', $options: 'i' } } }, ['r3', 'r4', 'r5']],
997
+ ['$regex 空字串為非null', { t: { $regex: '' } }, NN],
998
+ ['$regex 空RegExp', { t: { $regex: new RegExp('') } }, NN],
999
+ ['$exists 1', { i: { $exists: 1 } }, NN],
1000
+ ['$exists 0', { i: { $exists: 0 } }, ['r6']],
1001
+ ['$options null視同未給(大小寫依定序)', { t: { $regex: 'rose', $options: null } }, ['r3']],
1002
+ ['DECIMAL 以不超過scale之字串', { dec: '1.25' }, ['r1']],
1003
+ ['DATE 以epoch數值比較', { d: { $lt: Date.parse('2020-01-01T00:00:00Z') } }, ['r1']],
1004
+ ['同義之$lt與Op.lt', { i: { $lt: 3, [Op.lt]: 100 } }, ['r1', 'r2']],
1005
+ ['$not $in [null]為非null', { i: { $not: { $in: [null] } } }, NN],
1006
+ //BOOLEAN之比較依1/0
1007
+ ['BOOLEAN $gt false', { b: { $gt: false } }, ['r1', 'r3', 'r5']],
1008
+ ['BOOLEAN $not $gt true', { b: { $not: { $gt: true } } }, NN],
1009
+ //同義之鍵各自成立, Symbol之$not同$not
1010
+ ['同義之鍵不得互相覆寫', { i: { $gt: 3, [Op.gt]: 1 } }, ['r4', 'r5']],
1011
+ ['Symbol Op.not', { i: { [Op.not]: { [Op.gt]: 2 } } }, ['r1', 'r2']],
1012
+ ['$and與Op.and並存', { $and: [{ i: { $gte: 2 } }], [Op.and]: [{ i: { $lte: 3 } }] }, ['r2', 'r3']],
1013
+ ]
1014
+
1015
+ for (let [t, find, exp] of cases) {
1016
+ it(`一致性: ${t}`, async function() {
1017
+ //rulebook R1: select命中 = delAll刪除 = 規則推得之常數, 且n與nDeleted為刪除數
1018
+ await reset()
1019
+ assert.deepStrictEqual(ids(await w.select(find)), exp, 'select之命中')
1020
+ let r = await w.delAll(find)
1021
+ let left = ids(await w.select())
1022
+ let gone = ALL.filter((v) => left.indexOf(v) < 0)
1023
+ assert.deepStrictEqual(gone, exp, 'delAll之刪除')
1024
+ assert.deepStrictEqual(r, { n: exp.length, nDeleted: exp.length, ok: 1 })
1025
+ })
1026
+ }
1027
+
1028
+ it('DATE 清除舊資料只刪舊列, 不得刪除本地今日00:00之列', async function() {
1029
+ //rulebook R4: 修正前DATE欄之運算子物件被stringify為本地今日00:00而刪錯列並回報成功
1030
+ await reset()
1031
+ await w.insert({ id: 'rt', d: today0() })
1032
+ let r = await w.delAll({ d: { $lt: new Date('2020-01-01T00:00:00Z') } })
1033
+ assert.deepStrictEqual(r, { n: 1, nDeleted: 1, ok: 1 })
1034
+ let left = ids(await w.select())
1035
+ assert.deepStrictEqual(left, ['r2', 'r3', 'r4', 'r5', 'r6', 'rt'])
1036
+ })
1037
+
1038
+ //[說明, find, reject訊息須含之字串]
1039
+ let bads = [
1040
+ ['未知運算子', { i: { $foo: 1 } }, 'unsupported operator [$foo]'],
1041
+ ['頂層未知運算子', { $foo: 1 }, 'unsupported operator [$foo]'],
1042
+ ['sequelize專屬名稱', { s: { $like: 's%' } }, 'unsupported operator [$like]'],
1043
+ ['頂層$not', { $not: { s: 's1' } }, 'unsupported operator [$not]'],
1044
+ ['MongoDB之$type', { i: { $type: 'number' } }, 'unsupported operator [$type]'],
1045
+ ['比較運算子之值為物件', { d: { $gt: { x: 1 } } }, 'value must be'],
1046
+ ['DATE欄收非日期字串', { d: { $lt: 'garbage' } }, 'does not match column type'],
1047
+ ['數值欄收非數值字串', { i: { $lt: 'abc' } }, 'does not match column type'],
1048
+ ['字串欄收數值', { s: 1 }, 'does not match column type'],
1049
+ ['$regex含未跳脫元字元', { t: { $regex: '^pe' } }, 'unescaped metacharacter'],
1050
+ ['$regex用於非字串欄', { i: { $regex: '1' } }, '$regex requires a string column'],
1051
+ ['$options不支援之旗標', { t: { $regex: 'p', $options: 'm' } }, 'supports [i] only'],
1052
+ ['undefined值', { s: undefined }, 'value is undefined'],
1053
+ ['NaN', { i: NaN }, 'number must be finite'],
1054
+ ['空運算子物件', { i: {} }, 'empty operator object'],
1055
+ ['巢狀欄位物件', { s: { a: 1 } }, 'nested object is not supported'],
1056
+ ['空字串欄名', { '': 1 }, 'invalid column name'],
1057
+ ['非sequelize運算子之Symbol', { [Symbol('or')]: [{ s: 's1' }] }, 'unsupported symbol operator'],
1058
+ ['欄位層非運算子之Symbol', { i: { [Symbol('gt')]: 1 } }, 'unsupported symbol operator'],
1059
+ ['欄位層Op.or', { i: { [Sequelize.Op.or]: [{ [Sequelize.Op.eq]: 1 }], [Sequelize.Op.gt]: 10 } }, 'unsupported symbol operator'],
1060
+ ['稀疏陣列', { $and: new Array(2) }, 'holes or undefined'],
1061
+ ['$regex含NUL', { t: { $regex: 'a' + String.fromCharCode(0) + 'x' } }, 'NUL'],
1062
+ ['整數欄超出安全整數', { i: Number('9007199254740993') }, 'does not match column type [int]'],
1063
+ ['$and空陣列', { $and: [] }, 'nonempty array'],
1064
+ ['$or空陣列', { $or: [] }, 'nonempty array'],
1065
+ ['$nor空陣列', { $nor: [] }, 'nonempty array'],
1066
+ ['DATE欄收無時區之日期字串', { d: { $lt: '2020-01-01' } }, 'does not match column type [date]'],
1067
+ ['DATE欄收不存在之日期', { d: { $lt: '2026-02-30T00:00:00Z' } }, 'does not match column type [date]'],
1068
+ ['DOUBLE欄收非數值字串', { p: { $lt: 'abc' } }, 'does not match column type [num]'],
1069
+ ['$regex用於DOUBLE欄', { p: { $regex: '1' } }, '$regex requires a string column'],
1070
+ ['非十進位之數值字串', { i: { $lt: '0x10' } }, 'does not match column type [int]'],
1071
+ ['前後空白之數值字串', { i: ' 5' }, 'does not match column type [int]'],
1072
+ ['整數欄收指數或小數寫法之字串', { i: '1e3' }, 'does not match column type [int]'],
1073
+ ['DECIMAL欄收指數寫法之字串', { dec: '1e3' }, 'does not match column type [num]'],
1074
+ ['DECIMAL欄收超過scale之字串', { dec: '1.254' }, 'with scale [2]'],
1075
+ ['DATE欄收超出Date範圍之epoch', { d: { $lt: 1e16 } }, 'does not match column type [date]'],
1076
+ ['原型上之條件', Object.create({ s: 's1' }), 'find must be a plain object'],
1077
+ ['帶toStringTag之物件', { s: 's1', [Symbol.toStringTag]: 'Q' }, 'unsupported symbol operator'],
1078
+ ['欄位層Op.or空陣列', { i: { [Sequelize.Op.or]: [] } }, 'unsupported symbol operator'],
1079
+ ['讀值拋錯之getter', Object.defineProperty({}, 's', {
1080
+ enumerable: true,
1081
+ get: () => {
1082
+ throw new Error('getter boom')
1083
+ }
1084
+ }), 'getter boom'],
1085
+ ]
1086
+
1087
+ for (let [t, find, msg] of bads) {
1088
+ it(`不合法條件兩函數皆reject且資料不變: ${t}`, async function() {
1089
+ //rulebook R3/R4/R6/R7: 不得靜默忽略或改變語意; 兩函數訊息相同且各發一次error事件
1090
+ await reset()
1091
+ let evs = []
1092
+ let fn = (mode, data, err) => evs.push({ mode, err })
1093
+ w.on('error', fn)
1094
+ let rs = await w.select(find).then(() => null).catch((err) => String(err.message || err))
1095
+ let rd = await w.delAll(find).then(() => null).catch((err) => String(err.message || err))
1096
+ w.off('error', fn)
1097
+ assert.ok(rs !== null && rs.indexOf(msg) >= 0, `select: ${rs}`)
1098
+ assert.strictEqual(rd, rs)
1099
+ assert.deepStrictEqual(evs.map((v) => v.mode), ['select', 'delAll'])
1100
+ assert.strictEqual(evs[0].err, rs)
1101
+ assert.strictEqual((await w.select()).length, 6)
1102
+ })
1103
+ }
1104
+
1105
+ })
1106
+
1107
+
1108
+ describe(`${label} pk type`, function() {
1109
+
1110
+ //rulebook R8(run04): 主鍵值依model主鍵欄型別正規化, 與w-orm-mdb之normPk同一規則(主控複審run03第二節之表);
1111
+ //型別不符或無效者: selectByPk回null、del該筆ok為0、save/insert/insertBulk整批reject、select/delAll reject
1112
+ //users之id為STRING, tpkint之id為INTEGER
1113
+
1114
+ let w = null
1115
+ let wi = null
1116
+ let wd = null
1117
+
1118
+ let errOf = (p) => p.then(() => null).catch((err) => String(err.message || err))
1119
+
1120
+ before(async function() {
1121
+ w = wo(getOpt())
1122
+ w.on('error', () => {})
1123
+ await w.createStorage()
1124
+ wi = wo(getOpt({ cl: 'tpkint', autoGenPk: false }))
1125
+ wi.on('error', () => {})
1126
+ await wi.createStorage()
1127
+ wd = wo(getOpt({ cl: 'tpkdec', autoGenPk: false }))
1128
+ wd.on('error', () => {})
1129
+ await wd.createStorage()
1130
+ })
1131
+
1132
+ describe('字串主鍵欄', function() {
1133
+
1134
+ let ids = async () => (await w.select()).map((v) => v.id).sort()
1135
+
1136
+ beforeEach(async function() {
1137
+ await w.delAll()
1138
+ await w.insert([{ id: '123', name: 'n123' }, { id: '45', name: 'n45' }, { id: 'abc', name: 'nabc' }])
1139
+ })
1140
+
1141
+ it('selectByPk 收有限數字視為String(v)而命中', async function() {
1142
+ //R8: 字串類主鍵欄之有限數字以String(v)處置
1143
+ assert.deepStrictEqual(await w.selectByPk(123), { id: '123', name: 'n123', value: null })
1144
+ assert.deepStrictEqual(await w.selectByPk('123'), { id: '123', name: 'n123', value: null })
1145
+ assert.strictEqual(await w.selectByPk(1.5), null) //即'1.5', 不存在
1146
+ })
1147
+
1148
+ it('selectByPk 收無效值回null且不reject', async function() {
1149
+ //R8: NaN、±Infinity、空字串、布林、物件、陣列、null、undefined為無效
1150
+ for (let v of [NaN, Infinity, -Infinity, '', true, {}, [123], null, undefined]) {
1151
+ assert.strictEqual(await w.selectByPk(v), null, String(v))
1152
+ }
1153
+ })
1154
+
1155
+ it('insert與insertBulk 收有限數字存為字串, 其後各函數視為同一主鍵', async function() {
1156
+ //R8: 正規化值就地寫回數據後寫入, 令insert之寫入值與selectByPk、insert去重之比對值一致(規格:265)
1157
+ let evs = []
1158
+ let fn = (mode, data) => evs.push(data)
1159
+ w.on('change', fn)
1160
+ let r1 = await w.insert({ id: 456, name: 'n456' })
1161
+ w.off('change', fn)
1162
+ assert.deepStrictEqual(r1, { n: 1, nInserted: 1, ok: 1 })
1163
+ assert.strictEqual(evs[0][0].id, '456') //事件之數據即實際寫入者
1164
+ assert.strictEqual((await w.select({ id: '456' }))[0].id, '456')
1165
+ assert.deepStrictEqual(await w.insert({ id: '456' }), { n: 1, nInserted: 0, ok: 1 })
1166
+ assert.deepStrictEqual(await w.insert({ id: 123 }), { n: 1, nInserted: 0, ok: 1 })
1167
+ assert.strictEqual((await w.selectByPk(456)).name, 'n456')
1168
+ assert.deepStrictEqual(await w.insertBulk([{ id: 789, name: 'n789' }]), { n: 1, nInserted: 1, ok: 1 })
1169
+ assert.ok(await errOf(w.insertBulk([{ id: '789' }])) !== null, 'insertBulk未把789與\'789\'視為同一主鍵')
1170
+ assert.deepStrictEqual(await ids(), ['123', '45', '456', '789', 'abc'])
1171
+ })
1172
+
1173
+ it('save 收有限數字視為字串而更新既有數據', async function() {
1174
+ //R8: 有效主鍵之正規化值供save之預讀與更新條件
1175
+ assert.deepStrictEqual(await w.save({ id: 123, name: 'x' }), [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }])
1176
+ assert.strictEqual((await w.selectByPk('123')).name, 'x')
1177
+ assert.deepStrictEqual(await ids(), ['123', '45', 'abc'])
1178
+ })
1179
+
1180
+ it('del 收有限數字視為字串而刪除, 收無效值該筆ok為0', async function() {
1181
+ //R8: del之無效 → 該筆{n:0,nDeleted:0,ok:0,err}, 不中斷他筆
1182
+ let msg = await w.del([{ id: 123 }, { id: true }, { id: 45 }])
1183
+ assert.deepStrictEqual(msg.map((v) => [v.n, v.nDeleted, v.ok]), [[1, 1, 1], [0, 0, 0], [1, 1, 1]])
1184
+ assert.strictEqual(typeof msg[1].err, 'string')
1185
+ assert.deepStrictEqual(await ids(), ['abc'])
1186
+ })
1187
+
1188
+ //[說明, find, 預期命中之id]
1189
+ let cases = [
1190
+ ['等值', { id: 123 }, ['123']],
1191
+ ['$eq', { id: { $eq: 45 } }, ['45']],
1192
+ ['$in', { id: { $in: [123, 45] } }, ['123', '45']],
1193
+ ['陣列值即$in', { id: [123, 'abc'] }, ['123', 'abc']],
1194
+ ['$nin', { id: { $nin: [123] } }, ['45', 'abc']],
1195
+ ['$ne', { id: { $ne: 45 } }, ['123', 'abc']],
1196
+ ['$not', { id: { $not: { $eq: 123 } } }, ['45', 'abc']],
1197
+ ['$gt以字串比較', { id: { $gt: 2 } }, ['45', 'abc']],
1198
+ ['$or', { $or: [{ id: 123 }, { name: 'nabc' }] }, ['123', 'abc']],
1199
+ ['Op.in', { id: { [Sequelize.Op.in]: [45] } }, ['45']],
1200
+ ]
1201
+ for (let [t, find, exp] of cases) {
1202
+ it(`select與delAll 主鍵欄之數字條件值視為字串且兩者一致: ${t}`, async function() {
1203
+ //R8: 主鍵欄之條件值依normPk正規化($gt:2即> '2'之字串比較); R1: select命中 = delAll刪除 = 本表常數
1204
+ assert.deepStrictEqual((await w.select(find)).map((v) => v.id).sort(), exp)
1205
+ assert.deepStrictEqual(await w.delAll(find), { n: exp.length, nDeleted: exp.length, ok: 1 })
1206
+ assert.deepStrictEqual(await ids(), ['123', '45', 'abc'].filter((v) => exp.indexOf(v) < 0))
1207
+ })
1208
+ }
1209
+
1210
+ let bads = [
1211
+ ['布林', { id: true }],
1212
+ ['空字串', { id: '' }],
1213
+ ['$ne空字串', { id: { $ne: '' } }],
1214
+ ['$in含布林', { id: { $in: [123, false] } }],
1215
+ ]
1216
+ for (let [t, find] of bads) {
1217
+ it(`select與delAll 主鍵欄之無效條件值皆reject且不刪除: ${t}`, async function() {
1218
+ //R8: 無效之主鍵條件值reject(同R4之訊息), 不誤刪
1219
+ let rs = await errOf(w.select(find))
1220
+ let rd = await errOf(w.delAll(find))
1221
+ assert.ok(rs !== null && rs.indexOf('does not match column type [str]') >= 0, `select: ${rs}`)
1222
+ assert.strictEqual(rd, rs)
1223
+ assert.deepStrictEqual(await ids(), ['123', '45', 'abc'])
1224
+ })
1225
+ }
1226
+
1227
+ })
1228
+
1229
+ describe('整數主鍵欄', function() {
1230
+
1231
+ let ids = async () => (await wi.select()).map((v) => v.id).sort((a, b) => a - b)
1232
+
1233
+ beforeEach(async function() {
1234
+ await wi.delAll()
1235
+ await wi.insert([{ id: 12, name: 'n12' }, { id: 5, name: 'n5' }])
1236
+ })
1237
+
1238
+ it('selectByPk 收標準整數寫法之字串視為整數而命中', async function() {
1239
+ //R8: 整數類主鍵欄之標準整數寫法字串(String(Number(s))===s)轉為Number(s)
1240
+ assert.strictEqual((await wi.selectByPk('12')).name, 'n12')
1241
+ assert.strictEqual((await wi.selectByPk(12)).name, 'n12')
1242
+ assert.strictEqual((await wi.selectByPk('12')).id, 12)
1243
+ })
1244
+
1245
+ it('selectByPk 收非標準寫法之字串與非整數為型別不符而回null', async function() {
1246
+ //R8: '012'、'+12'、'12.0'、'1e1'、' 12'、'0x0c'、'abc'、12.4為型別不符, 不送查詢
1247
+ for (let v of ['012', '+12', '12.0', '1e1', ' 12', '0x0c', 'abc', 12.4]) {
1248
+ assert.strictEqual(await wi.selectByPk(v), null, String(v))
1249
+ }
1250
+ })
1251
+
1252
+ it('insert收標準整數寫法之字串存為整數; 收型別不符者insert、insertBulk、save皆整批reject且不寫入', async function() {
1253
+ //R8: 有效者正規化後寫入; 型別不符者寫入前整批reject, 不以自動產生之主鍵取代
1254
+ assert.deepStrictEqual(await wi.insert({ id: '13', name: 'n13' }), { n: 1, nInserted: 1, ok: 1 })
1255
+ assert.strictEqual((await wi.selectByPk(13)).id, 13)
1256
+ assert.deepStrictEqual(await wi.insert({ id: 13 }), { n: 1, nInserted: 0, ok: 1 })
1257
+ for (let bad of ['012', 12.4, 'abc']) {
1258
+ let r = await errOf(wi.insert([{ id: 14 }, { id: bad }]))
1259
+ assert.ok(r !== null && r.indexOf('type does not match pk column') >= 0, `insert ${bad}: ${r}`)
1260
+ let rb = await errOf(wi.insertBulk([{ id: 14 }, { id: bad }]))
1261
+ assert.ok(rb !== null && rb.indexOf('type does not match pk column') >= 0, `insertBulk ${bad}: ${rb}`)
1262
+ let rv = await errOf(wi.save([{ id: 14 }, { id: bad }]))
1263
+ assert.ok(rv !== null && rv.indexOf('type does not match pk column') >= 0, `save ${bad}: ${rv}`)
1264
+ }
1265
+ assert.deepStrictEqual(await ids(), [5, 12, 13])
1266
+ })
1267
+
1268
+ it('save收標準整數寫法之字串視為整數而更新', async function() {
1269
+ //R8: 有效主鍵之正規化值供save之預讀與更新條件
1270
+ assert.deepStrictEqual(await wi.save({ id: '12', name: 'x' }), [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }])
1271
+ assert.strictEqual((await wi.selectByPk(12)).name, 'x')
1272
+ assert.deepStrictEqual(await ids(), [5, 12])
1273
+ })
1274
+
1275
+ it('del收標準整數寫法之字串而刪除; 型別不符者該筆ok為0且不送查詢', async function() {
1276
+ //R8: del之型別不符 → 該筆{n:0,nDeleted:0,ok:0,err}, 不中斷他筆
1277
+ let msg = await wi.del([{ id: '012' }, { id: 12.4 }, { id: 'abc' }, { id: '12' }])
1278
+ assert.deepStrictEqual(msg.map((v) => [v.n, v.nDeleted, v.ok]), [[0, 0, 0], [0, 0, 0], [0, 0, 0], [1, 1, 1]])
1279
+ for (let k = 0; k < 3; k++) {
1280
+ assert.ok(msg[k].err.indexOf('type does not match pk column') >= 0, msg[k].err)
1281
+ }
1282
+ assert.deepStrictEqual(await ids(), [5])
1283
+ })
1284
+
1285
+ //[說明, find, 預期命中之id]
1286
+ let cases = [
1287
+ ['等值字串', { id: '12' }, [12]],
1288
+ ['$in字串', { id: { $in: ['12', '5'] } }, [5, 12]],
1289
+ ['$gt字串', { id: { $gt: '6' } }, [12]],
1290
+ ['$nin字串', { id: { $nin: ['12'] } }, [5]],
1291
+ ]
1292
+ for (let [t, find, exp] of cases) {
1293
+ it(`select與delAll 主鍵欄之整數寫法字串視為整數且兩者一致: ${t}`, async function() {
1294
+ //R8: 主鍵欄之條件值依normPk正規化; R1: select命中 = delAll刪除 = 本表常數
1295
+ assert.deepStrictEqual((await wi.select(find)).map((v) => v.id).sort((a, b) => a - b), exp)
1296
+ assert.deepStrictEqual(await wi.delAll(find), { n: exp.length, nDeleted: exp.length, ok: 1 })
1297
+ assert.deepStrictEqual(await ids(), [5, 12].filter((v) => exp.indexOf(v) < 0))
1298
+ })
1299
+ }
1300
+
1301
+ let bads = [
1302
+ ['前導零', { id: '012' }],
1303
+ ['非整數', { id: 12.4 }],
1304
+ ['非數值字串', { id: { $in: ['abc'] } }],
1305
+ ['$ne前導零', { id: { $ne: '012' } }],
1306
+ ]
1307
+ for (let [t, find] of bads) {
1308
+ it(`select與delAll 主鍵欄之型別不符條件值皆reject且不刪除: ${t}`, async function() {
1309
+ //R8: 型別不符之主鍵條件值reject(同R4之訊息), 不誤刪
1310
+ let rs = await errOf(wi.select(find))
1311
+ let rd = await errOf(wi.delAll(find))
1312
+ assert.ok(rs !== null && rs.indexOf('does not match column type [int]') >= 0, `select: ${rs}`)
1313
+ assert.strictEqual(rd, rs)
1314
+ assert.deepStrictEqual(await ids(), [5, 12])
1315
+ })
1316
+ }
1317
+
1318
+ })
1319
+
1320
+ describe('DECIMAL主鍵欄', function() {
1321
+
1322
+ //tpkdec之id為DECIMAL(10,2)
1323
+
1324
+ let ids = async () => (await wd.select()).map((v) => Number(v.id)).sort((a, b) => a - b)
1325
+
1326
+ beforeEach(async function() {
1327
+ await wd.delAll()
1328
+ await wd.insert([{ id: 1.25, name: 'a' }, { id: 0.5, name: 'b' }])
1329
+ })
1330
+
1331
+ it('標準數字字串視為數值, selectByPk、select、del之命中一致', async function() {
1332
+ //R8: 其他數值類主鍵欄之標準數字字串(String(Number(s))===s)轉為數值
1333
+ assert.strictEqual((await wd.selectByPk('1.25')).name, 'a')
1334
+ assert.deepStrictEqual((await wd.select({ id: '0.5' })).map((v) => v.name), ['b'])
1335
+ assert.deepStrictEqual((await wd.del({ id: '1.25' })).map((v) => [v.n, v.nDeleted, v.ok]), [[1, 1, 1]])
1336
+ assert.deepStrictEqual(await ids(), [0.5])
1337
+ })
1338
+
1339
+ for (let bad of [1.254, '1.254']) {
1340
+ it(`小數位多於scale者為型別不符, 各函數皆不寫入、不命中、不刪除: ${typeof bad} ${bad}`, async function() {
1341
+ //R8(表外細化): 寫入時會被四捨五入而改變鍵(mssql存為1.25而insert回報已存在、selectByPk卻回null), 故視為型別不符
1342
+ assert.strictEqual(await wd.selectByPk(bad), null)
1343
+ for (let [k, fn] of [['insert', () => wd.insert({ id: bad })], ['insertBulk', () => wd.insertBulk({ id: bad })], ['save', () => wd.save({ id: bad, name: 'x' })]]) {
1344
+ let r = await errOf(fn())
1345
+ assert.ok(r !== null && r.indexOf('type does not match pk column') >= 0, `${k}: ${r}`)
1346
+ }
1347
+ let md = await wd.del({ id: bad })
1348
+ assert.deepStrictEqual([md[0].n, md[0].nDeleted, md[0].ok], [0, 0, 0])
1349
+ let rs = await errOf(wd.select({ id: bad }))
1350
+ let rd = await errOf(wd.delAll({ id: bad }))
1351
+ assert.ok(rs !== null && rs.indexOf('does not match column type [num]') >= 0, `select: ${rs}`)
1352
+ assert.strictEqual(rd, rs)
1353
+ assert.deepStrictEqual(await ids(), [0.5, 1.25])
1354
+ assert.strictEqual((await wd.selectByPk(1.25)).name, 'a')
1355
+ })
1356
+ }
1357
+
1358
+ })
1359
+
1360
+ })
1361
+
1362
+
874
1363
  }
@@ -0,0 +1,35 @@
1
+ //api-models.test.mjs之併發子行程: 以同一models資料夾反覆操作, 結束時輸出一行JSON
2
+ //用法: node test/tools/modelsWorker.mjs <fdModels> <storage> <次數>
3
+ import wo from '../../src/WOrmReladb.mjs'
4
+
5
+
6
+ let fdModels = process.argv[2]
7
+ let storage = process.argv[3]
8
+ let n = Number(process.argv[4] || 50)
9
+
10
+
11
+ let main = async () => {
12
+ let w = wo({ url: 'sqlite://:', storage, db: 'worm', cl: 'users', fdModels })
13
+ let nOk = 0
14
+ let errs = []
15
+ await w.createStorage()
16
+ .then(() => {
17
+ nOk++
18
+ })
19
+ .catch((err) => {
20
+ errs.push(String(err && err.message ? err.message : err))
21
+ })
22
+ for (let i = 0; i < n; i++) {
23
+ await w.select()
24
+ .then(() => {
25
+ nOk++
26
+ })
27
+ .catch((err) => {
28
+ errs.push(String(err && err.message ? err.message : err))
29
+ })
30
+ }
31
+ console.log(JSON.stringify({ nOk, errs: errs.slice(0, 3), nErr: errs.length }))
32
+ }
33
+
34
+
35
+ main()