w-orm-reladb 1.1.9 → 1.1.10
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 +59 -28
- package/dist/w-orm-reladb.umd.js +2 -2
- package/dist/w-orm-reladb.umd.js.map +1 -1
- package/docs/WOrmReladb.html +52 -30
- package/docs/WOrmReladb.mjs.html +59 -28
- package/docs/cvFind.mjs.html +127 -405
- package/docs/global.html +14 -11
- package/docs/importModels.mjs.html +1 -1
- package/docs/index.html +1 -1
- package/g-mssql-transaction-commit.mjs +1 -1
- package/g-mssql-transaction-rollback.mjs +1 -1
- package/g-mssql.mjs +4 -4
- package/g-sqlite-encryption-transaction-commit.mjs +1 -1
- package/g-sqlite-encryption.mjs +4 -4
- package/g-sqlite-transaction-commit.mjs +1 -1
- package/g-sqlite-transaction-rollback.mjs +1 -1
- package/g-sqlite.mjs +4 -4
- package/models/tfind.js +50 -0
- package/package.json +2 -2
- package/src/WOrmReladb.mjs +58 -27
- package/src/cvFind.mjs +126 -404
- package/test/api-find-whitelist.mjs +390 -0
- package/test/api-mssql.test.mjs +2 -0
- package/test/api-sqlite.test.mjs +2 -0
- package/test/api-suite.mjs +95 -136
- package/test/unit-cvfind.test.mjs +241 -218
package/test/api-suite.mjs
CHANGED
|
@@ -122,10 +122,10 @@ export function defineSuites(label, getOpt) {
|
|
|
122
122
|
assert.deepStrictEqual(sp[1], { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 })
|
|
123
123
|
})
|
|
124
124
|
|
|
125
|
-
it('select $regex
|
|
126
|
-
//rulebook R6: $options
|
|
127
|
-
let
|
|
128
|
-
assert.
|
|
125
|
+
it('select $regex(白名單外)整批reject', async function() {
|
|
126
|
+
//rulebook R6/R11: $regex、$options已移除, 於形狀階段reject; 需子字串/不分大小寫者請select後於記憶體過濾
|
|
127
|
+
let r = await w.select({ name: { $regex: 'PeT', $options: 'i' } }).then(() => null).catch((err) => String(err.message || err))
|
|
128
|
+
assert.ok(r !== null && r.indexOf('invalid find: unsupported operator [$regex]') === 0, String(r))
|
|
129
129
|
})
|
|
130
130
|
|
|
131
131
|
it('del 刪除3筆並保留kettle', async function() {
|
|
@@ -421,7 +421,7 @@ export function defineSuites(label, getOpt) {
|
|
|
421
421
|
assert.ok(r.REJECT.indexOf('closed') >= 0, `${t} 之錯誤訊息未指明已關閉: ${r.REJECT}`)
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
await chk('select', () => w.select(
|
|
424
|
+
await chk('select', () => w.select({}, cs))
|
|
425
425
|
await chk('selectByPk', () => w.selectByPk('k1', cs))
|
|
426
426
|
await chk('insert', () => w.insert({ id: 'k9' }, cs))
|
|
427
427
|
await chk('insertBulk', () => w.insertBulk({ id: 'k8' }, cs))
|
|
@@ -594,7 +594,7 @@ export function defineSuites(label, getOpt) {
|
|
|
594
594
|
assert.ok(r !== null, '未reject')
|
|
595
595
|
|
|
596
596
|
//交易內先前之寫入須保留, 本次之tx-2須不存在
|
|
597
|
-
let ssIn = await w.select(
|
|
597
|
+
let ssIn = await w.select({}, connState)
|
|
598
598
|
let ids = ssIn.map((v) => v.id).sort()
|
|
599
599
|
assert.deepStrictEqual(ids, ['tx-1'])
|
|
600
600
|
|
|
@@ -821,7 +821,7 @@ export function defineSuites(label, getOpt) {
|
|
|
821
821
|
assert.deepStrictEqual(md, [{ n: 1, nDeleted: 1, ok: 1 }])
|
|
822
822
|
|
|
823
823
|
//交易內select可查到暫時有效的數據
|
|
824
|
-
let ssIn = sortByName(await w.select(
|
|
824
|
+
let ssIn = sortByName(await w.select({}, connState))
|
|
825
825
|
assert.strictEqual(ssIn.length, 2)
|
|
826
826
|
assert.strictEqual(ssIn[0].name, 'kettle')
|
|
827
827
|
assert.strictEqual(ssIn[1].name, 'peter(modify)')
|
|
@@ -925,6 +925,7 @@ export function defineSuites(label, getOpt) {
|
|
|
925
925
|
})
|
|
926
926
|
|
|
927
927
|
//[說明, find, 預期命中之id]
|
|
928
|
+
//(第33項批1: 白名單外之寫法〔$not $nor $regex $exists、RegExp、Symbol Op、陣列值、$in/$nin含null、空$nin、空子式〕已移至下方removed, 一律reject)
|
|
928
929
|
let cases = [
|
|
929
930
|
//R4 七欄型之等值
|
|
930
931
|
['STRING 等值', { s: 's1' }, ['r1']],
|
|
@@ -947,91 +948,41 @@ export function defineSuites(label, getOpt) {
|
|
|
947
948
|
['DECIMAL $in', { dec: { $in: [2.5] } }, ['r2']],
|
|
948
949
|
['DATE $in', { d: { $in: [new Date('2019-01-01T00:00:00Z')] } }, ['r1']],
|
|
949
950
|
['INTEGER $nin', { i: { $nin: [1, 3] } }, ['r2', 'r4', 'r5']],
|
|
950
|
-
['INTEGER $not $gt', { i: { $not: { $gt: 2 } } }, ['r1', 'r2']],
|
|
951
|
-
['FLOAT $not $lte', { f: { $not: { $lte: 2.5 } } }, ['r3', 'r4', 'r5']],
|
|
952
|
-
['DATE $not $gt', { d: { $not: { $gt: new Date('2020-01-01T00:00:00Z') } } }, ['r1']],
|
|
953
951
|
['同欄多運算子', { i: { $gt: 1, $lt: 5 } }, ['r2', 'r3', 'r4']],
|
|
952
|
+
['同欄三運算子併入同一欄位物件', { i: { $gt: 1, $lt: 5, $ne: 3 } }, ['r2', 'r4']],
|
|
954
953
|
['DATE 以帶時區之ISO字串比較', { d: { $lt: '2020-01-01T00:00:00Z' } }, ['r1']],
|
|
955
954
|
['DATE 以帶偏移之ISO字串比較', { d: { $lt: '2020-01-01T08:00:00+08:00' } }, ['r1']],
|
|
956
955
|
['DOUBLE $gt', { p: { $gt: 3.5 } }, ['r4', 'r5']],
|
|
957
|
-
['DOUBLE $not $lte', { p: { $not: { $lte: 2.5 } } }, ['r3', 'r4', 'r5']],
|
|
958
956
|
['INTEGER 以數值字串比較', { i: '2' }, ['r2']],
|
|
959
957
|
//R3 邏輯
|
|
960
958
|
['$and', { $and: [{ i: { $gte: 2 } }, { b: false }] }, ['r2', 'r4']],
|
|
961
959
|
['$or', { $or: [{ s: 's1' }, { i: 5 }] }, ['r1', 'r5']],
|
|
962
|
-
['$
|
|
960
|
+
['$and 內含$or', { $and: [{ $or: [{ s: 's1' }, { i: 5 }] }, { b: true }] }, ['r1', 'r5']],
|
|
963
961
|
//R5 NULL
|
|
964
962
|
['{t:null}', { t: null }, ['r6']],
|
|
965
963
|
['$eq null', { t: { $eq: null } }, ['r6']],
|
|
966
|
-
['$in [null] 與{t:null}一致', { t: { $in: [null] } }, ['r6']],
|
|
967
|
-
['$in [null,值]', { t: { $in: [null, 'rose_mary'] } }, ['r3', 'r6']],
|
|
968
964
|
['$ne null', { t: { $ne: null } }, NN],
|
|
969
|
-
['$
|
|
970
|
-
['$nin
|
|
971
|
-
['
|
|
972
|
-
['
|
|
965
|
+
['$ne不含NULL(SQL三值)', { t: { $ne: 'rose_mary' } }, ['r1', 'r2', 'r4', 'r5']],
|
|
966
|
+
['$nin不含NULL(SQL三值)', { t: { $nin: ['rose_mary'] } }, ['r1', 'r2', 'r4', 'r5']],
|
|
967
|
+
['可攜寫法: 排除NULL', { $and: [{ t: { $ne: null } }, { t: { $ne: 'rose_mary' } }] }, ['r1', 'r2', 'r4', 'r5']],
|
|
968
|
+
['可攜寫法: 含NULL', { $or: [{ t: null }, { t: { $ne: 'rose_mary' } }] }, ['r1', 'r2', 'r4', 'r5', 'r6']],
|
|
973
969
|
['$in 空陣列為無', { i: { $in: [] } }, []],
|
|
974
|
-
['$nin 空陣列為全部', { i: { $nin: [] } }, ALL],
|
|
975
|
-
['$not $in 空陣列為全部', { i: { $not: { $in: [] } } }, ALL],
|
|
976
|
-
//R6 $regex
|
|
977
|
-
['$regex 字面底線', { t: { $regex: '_' } }, ['r3']],
|
|
978
|
-
['$regex 字面百分比', { t: { $regex: '%' } }, ['r4']],
|
|
979
|
-
['$regex 跳脫之方括號', { t: { $regex: '\\[a-c\\]' } }, ['r5']],
|
|
980
|
-
['$regex $options i', { t: { $regex: 'PET', $options: 'i' } }, ['r1', 'r2']],
|
|
981
|
-
['$regex RegExp i', { t: { $regex: /PET/i } }, ['r1', 'r2']],
|
|
982
|
-
['$regex $options $i(過渡期視同i)', { t: { $regex: 'PeT', $options: '$i' } }, ['r1', 'r2']],
|
|
983
|
-
['RegExp 作為值', { t: /PET/i }, ['r1', 'r2']],
|
|
984
|
-
['$not $regex', { t: { $not: { $regex: '_' } } }, ['r1', 'r2', 'r4', 'r5']],
|
|
985
970
|
//R2 特殊鍵
|
|
986
971
|
['欄名length不得使條件失效', { s: 's1', length: 0 }, ['r1']],
|
|
987
972
|
['欄名length帶運算子', { length: { $gte: 5 } }, ['r3', 'r4', 'r5']],
|
|
988
|
-
['欄名含$', { 'p$x': 'q' }, ['r1', 'r3']],
|
|
989
|
-
['Symbol Op.or', { [Op.or]: [{ s: 's1' }, { s: 's2' }] }, ['r1', 'r2']],
|
|
990
|
-
['Symbol Op.in', { i: { [Op.in]: [1, 2] } }, ['r1', 'r2']],
|
|
991
|
-
['陣列值為IN', { i: [1, 2] }, ['r1', 'r2']],
|
|
992
|
-
['陣列值含null同$in', { t: [null, 'rose_mary'] }, ['r3', 'r6']],
|
|
993
|
-
//空子式為恆真/恆假, 不得被濾除而改變語意
|
|
994
|
-
['$or 含空條件為全部', { $or: [{}, { s: 's1' }] }, ALL],
|
|
995
|
-
['$nor 含空條件為無', { $nor: [{}] }, []],
|
|
996
|
-
['$nor 含空條件與他式為無', { $nor: [{}, { s: 's1' }] }, []],
|
|
997
|
-
['$not $regex i', { t: { $not: { $regex: 'PET', $options: 'i' } } }, ['r3', 'r4', 'r5']],
|
|
998
|
-
['$regex 空字串為非null', { t: { $regex: '' } }, NN],
|
|
999
|
-
['$regex 空RegExp', { t: { $regex: new RegExp('') } }, NN],
|
|
1000
|
-
['$exists 1', { i: { $exists: 1 } }, NN],
|
|
1001
|
-
['$exists 0', { i: { $exists: 0 } }, ['r6']],
|
|
1002
|
-
['$options null視同未給(大小寫依定序)', { t: { $regex: 'rose', $options: null } }, ['r3']],
|
|
1003
973
|
['DECIMAL 以不超過scale之字串', { dec: '1.25' }, ['r1']],
|
|
1004
974
|
['DATE 以epoch數值比較', { d: { $lt: Date.parse('2020-01-01T00:00:00Z') } }, ['r1']],
|
|
1005
|
-
['同義之$lt與Op.lt', { i: { $lt: 3, [Op.lt]: 100 } }, ['r1', 'r2']],
|
|
1006
|
-
['$not $in [null]為非null', { i: { $not: { $in: [null] } } }, NN],
|
|
1007
975
|
|
|
1008
|
-
//審計02(規格句 R5): 欄名正確時,
|
|
1009
|
-
//空集合改為引用其欄位(f IS NULL AND 1 = 0 / f IS NULL OR 1 = 1)後, 命中集合須與原常數形式相同; select命中 = delAll刪除(R1)
|
|
976
|
+
//審計02(規格句 R5): 欄名正確時, 空$in引用其欄位(f IS NULL AND 1 = 0)後, 命中集合須為無且不使其他分支失效; select命中 = delAll刪除(R1)
|
|
1010
977
|
['TEXT $in 空陣列為無', { t: { $in: [] } }, []],
|
|
1011
|
-
['TEXT $nin 空陣列為全部(含NULL列)', { t: { $nin: [] } }, ALL],
|
|
1012
|
-
['TEXT 陣列值[]為無', { t: [] }, []],
|
|
1013
|
-
['Op.in 空陣列為無', { i: { [Op.in]: [] } }, []],
|
|
1014
|
-
['Op.notIn 空陣列為全部', { i: { [Op.notIn]: [] } }, ALL],
|
|
1015
|
-
['$not $nin 空陣列為無', { t: { $not: { $nin: [] } } }, []],
|
|
1016
|
-
['Symbol Op.not 包 Op.notIn 空陣列為無', { i: { [Op.not]: { [Op.notIn]: [] } } }, []],
|
|
1017
|
-
['$in空與$nin空並存為無', { t: { $in: [], $nin: [] } }, []],
|
|
1018
|
-
['$or 含空$nin 為全部', { $or: [{ t: { $nin: [] } }, { s: 'zz' }] }, ALL],
|
|
1019
|
-
['$and 含空$in 為無', { $and: [{ t: { $in: [] } }, { s: 's1' }] }, []],
|
|
1020
|
-
['$nor 含空$in 為全部', { $nor: [{ t: { $in: [] } }] }, ALL],
|
|
1021
|
-
['$nor 含空$nin 為無', { $nor: [{ t: { $nin: [] } }] }, []],
|
|
1022
|
-
['文件層Op.not 包空$in 為全部', { [Op.not]: { t: { $in: [] } } }, ALL],
|
|
1023
|
-
['主鍵欄 $nin 空陣列為全部', { id: { $nin: [] } }, ALL],
|
|
1024
978
|
['主鍵欄 $in 空陣列為無', { id: { $in: [] } }, []],
|
|
1025
|
-
['DATE 欄 $
|
|
979
|
+
['DATE 欄 $in 空陣列為無', { d: { $in: [] } }, []],
|
|
1026
980
|
['BOOLEAN 欄 $in 空陣列為無', { b: { $in: [] } }, []],
|
|
1027
|
-
['
|
|
981
|
+
['$and 含空$in 為無', { $and: [{ t: { $in: [] } }, { s: 's1' }] }, []],
|
|
982
|
+
['$or 含空$in 不使其他分支失效', { $or: [{ t: { $in: [] } }, { s: 's1' }] }, ['r1']],
|
|
983
|
+
['$in空與$nin並存為無', { t: { $in: [], $nin: ['x'] } }, []],
|
|
1028
984
|
//BOOLEAN之比較依1/0
|
|
1029
985
|
['BOOLEAN $gt false', { b: { $gt: false } }, ['r1', 'r3', 'r5']],
|
|
1030
|
-
['BOOLEAN $not $gt true', { b: { $not: { $gt: true } } }, NN],
|
|
1031
|
-
//同義之鍵各自成立, Symbol之$not同$not
|
|
1032
|
-
['同義之鍵不得互相覆寫', { i: { $gt: 3, [Op.gt]: 1 } }, ['r4', 'r5']],
|
|
1033
|
-
['Symbol Op.not', { i: { [Op.not]: { [Op.gt]: 2 } } }, ['r1', 'r2']],
|
|
1034
|
-
['$and與Op.and並存', { $and: [{ i: { $gte: 2 } }], [Op.and]: [{ i: { $lte: 3 } }] }, ['r2', 'r3']],
|
|
1035
986
|
]
|
|
1036
987
|
|
|
1037
988
|
for (let [t, find, exp] of cases) {
|
|
@@ -1058,55 +1009,90 @@ export function defineSuites(label, getOpt) {
|
|
|
1058
1009
|
})
|
|
1059
1010
|
|
|
1060
1011
|
//[說明, find, reject訊息須含之字串]
|
|
1012
|
+
//第33項批1: 形狀不合法者由wsemi之cvOrmFind於連線前reject(訊息格式invalid find: 原因 at [路徑]), 翻譯層(欄名嚴格鍵、欄位型別、主鍵)之reject維持
|
|
1061
1013
|
let bads = [
|
|
1062
1014
|
['未知運算子', { i: { $foo: 1 } }, 'unsupported operator [$foo]'],
|
|
1063
1015
|
['頂層未知運算子', { $foo: 1 }, 'unsupported operator [$foo]'],
|
|
1064
1016
|
['sequelize專屬名稱', { s: { $like: 's%' } }, 'unsupported operator [$like]'],
|
|
1065
1017
|
['頂層$not', { $not: { s: 's1' } }, 'unsupported operator [$not]'],
|
|
1066
1018
|
['MongoDB之$type', { i: { $type: 'number' } }, 'unsupported operator [$type]'],
|
|
1067
|
-
['比較運算子之值為物件', { d: { $gt: { x: 1 } } }, 'value
|
|
1019
|
+
['比較運算子之值為物件', { d: { $gt: { x: 1 } } }, 'unsupported value type'],
|
|
1068
1020
|
['DATE欄收非日期字串', { d: { $lt: 'garbage' } }, 'does not match column type'],
|
|
1069
1021
|
['數值欄收非數值字串', { i: { $lt: 'abc' } }, 'does not match column type'],
|
|
1070
1022
|
['字串欄收數值', { s: 1 }, 'does not match column type'],
|
|
1071
|
-
['
|
|
1072
|
-
['
|
|
1073
|
-
['$options不支援之旗標', { t: { $regex: 'p', $options: 'm' } }, 'supports [i] only'],
|
|
1074
|
-
['undefined值', { s: undefined }, 'value is undefined'],
|
|
1075
|
-
['NaN', { i: NaN }, 'number must be finite'],
|
|
1023
|
+
['undefined值', { s: undefined }, 'undefined is not allowed'],
|
|
1024
|
+
['NaN', { i: NaN }, 'number is not finite'],
|
|
1076
1025
|
['空運算子物件', { i: {} }, 'empty operator object'],
|
|
1077
|
-
['巢狀欄位物件', { s: { a: 1 } }, '
|
|
1078
|
-
['空字串欄名', { '': 1 }, '
|
|
1079
|
-
['非sequelize運算子之Symbol', { [Symbol('or')]: [{ s: 's1' }] }, '
|
|
1080
|
-
['欄位層非運算子之Symbol', { i: { [Symbol('gt')]: 1 } }, '
|
|
1081
|
-
['欄位層Op.or', { i: { [Sequelize.Op.or]: [{ [Sequelize.Op.eq]: 1 }], [Sequelize.Op.gt]: 10 } }, '
|
|
1082
|
-
['稀疏陣列', { $and: new Array(2) }, '
|
|
1083
|
-
['$regex含NUL', { t: { $regex: 'a' + String.fromCharCode(0) + 'x' } }, 'NUL'],
|
|
1026
|
+
['巢狀欄位物件', { s: { a: 1 } }, 'unexpected key'],
|
|
1027
|
+
['空字串欄名', { '': 1 }, 'unsupported column name'],
|
|
1028
|
+
['非sequelize運算子之Symbol', { [Symbol('or')]: [{ s: 's1' }] }, 'symbol key is not allowed'],
|
|
1029
|
+
['欄位層非運算子之Symbol', { i: { [Symbol('gt')]: 1 } }, 'symbol key is not allowed'],
|
|
1030
|
+
['欄位層Op.or', { i: { [Sequelize.Op.or]: [{ [Sequelize.Op.eq]: 1 }], [Sequelize.Op.gt]: 10 } }, 'symbol key is not allowed'],
|
|
1031
|
+
['稀疏陣列', { $and: new Array(2) }, 'sparse array hole'],
|
|
1084
1032
|
['整數欄超出安全整數', { i: Number('9007199254740993') }, 'does not match column type [int]'],
|
|
1085
|
-
['$and空陣列', { $and: [] }, '
|
|
1086
|
-
['$or空陣列', { $or: [] }, '
|
|
1087
|
-
['$nor空陣列', { $nor: [] }, '
|
|
1033
|
+
['$and空陣列', { $and: [] }, '$and with empty array'],
|
|
1034
|
+
['$or空陣列', { $or: [] }, '$or with empty array'],
|
|
1035
|
+
['$nor空陣列', { $nor: [] }, 'unsupported operator [$nor]'],
|
|
1088
1036
|
['DATE欄收無時區之日期字串', { d: { $lt: '2020-01-01' } }, 'does not match column type [date]'],
|
|
1089
1037
|
['DATE欄收不存在之日期', { d: { $lt: '2026-02-30T00:00:00Z' } }, 'does not match column type [date]'],
|
|
1090
1038
|
['DOUBLE欄收非數值字串', { p: { $lt: 'abc' } }, 'does not match column type [num]'],
|
|
1091
|
-
['$regex用於DOUBLE欄', { p: { $regex: '1' } }, '$regex requires a string column'],
|
|
1092
1039
|
['非十進位之數值字串', { i: { $lt: '0x10' } }, 'does not match column type [int]'],
|
|
1093
1040
|
['前後空白之數值字串', { i: ' 5' }, 'does not match column type [int]'],
|
|
1094
1041
|
['整數欄收指數或小數寫法之字串', { i: '1e3' }, 'does not match column type [int]'],
|
|
1095
1042
|
['DECIMAL欄收指數寫法之字串', { dec: '1e3' }, 'does not match column type [num]'],
|
|
1096
1043
|
['DECIMAL欄收超過scale之字串', { dec: '1.254' }, 'with scale [2]'],
|
|
1097
1044
|
['DATE欄收超出Date範圍之epoch', { d: { $lt: 1e16 } }, 'does not match column type [date]'],
|
|
1098
|
-
['原型上之條件', Object.create({ s: 's1' }), '
|
|
1099
|
-
['帶toStringTag之物件', { s: 's1', [Symbol.toStringTag]: 'Q' }, '
|
|
1100
|
-
['欄位層Op.or空陣列', { i: { [Sequelize.Op.or]: [] } }, '
|
|
1045
|
+
['原型上之條件', Object.create({ s: 's1' }), 'condition must be a plain object'],
|
|
1046
|
+
['帶toStringTag之物件', { s: 's1', [Symbol.toStringTag]: 'Q' }, 'condition must be a plain object'],
|
|
1047
|
+
['欄位層Op.or空陣列', { i: { [Sequelize.Op.or]: [] } }, 'symbol key is not allowed'],
|
|
1048
|
+
|
|
1049
|
+
//第33項批1(R11): 白名單外之寫法一律reject(原為支援或部分支援, 見帳本〈破壞性變更紀錄(第33項批1)〉)
|
|
1050
|
+
['欄位層$not', { i: { $not: { $gt: 2 } } }, 'unsupported operator [$not]'],
|
|
1051
|
+
['$not包空$in', { i: { $not: { $in: [] } } }, 'unsupported operator [$not]'],
|
|
1052
|
+
['文件層$nor', { $nor: [{ s: 's1' }, { i: 5 }] }, 'unsupported operator [$nor]'],
|
|
1053
|
+
['$nor含空條件', { $nor: [{}] }, 'unsupported operator [$nor]'],
|
|
1054
|
+
['$exists true', { i: { $exists: true } }, 'unsupported operator [$exists]'],
|
|
1055
|
+
['$exists false', { i: { $exists: false } }, 'unsupported operator [$exists]'],
|
|
1056
|
+
['$regex', { t: { $regex: '_' } }, 'unsupported operator [$regex]'],
|
|
1057
|
+
['$regex + $options', { t: { $regex: 'PET', $options: 'i' } }, 'unsupported operator [$regex]'],
|
|
1058
|
+
['$regex RegExp值', { t: { $regex: /PET/i } }, 'unsupported operator [$regex]'],
|
|
1059
|
+
['單獨$options', { t: { $options: 'i' } }, 'unsupported operator [$options]'],
|
|
1060
|
+
['$regex用於非字串欄', { i: { $regex: '1' } }, 'unsupported operator [$regex]'],
|
|
1061
|
+
['RegExp作為值', { t: /PET/i }, 'unsupported value type [RegExp]'],
|
|
1062
|
+
['陣列值即$in(已移除)', { i: [1, 2] }, 'array value is not supported, use $in'],
|
|
1063
|
+
['空陣列值', { t: [] }, 'array value is not supported, use $in'],
|
|
1064
|
+
['陣列值含null', { t: [null, 'rose_mary'] }, 'array value is not supported, use $in'],
|
|
1065
|
+
['Symbol Op.or', { [Op.or]: [{ s: 's1' }, { s: 's2' }] }, 'symbol key is not allowed'],
|
|
1066
|
+
['Symbol Op.in', { i: { [Op.in]: [1, 2] } }, 'symbol key is not allowed'],
|
|
1067
|
+
['Symbol Op.notIn空陣列', { i: { [Op.notIn]: [] } }, 'symbol key is not allowed'],
|
|
1068
|
+
['Symbol Op.not', { i: { [Op.not]: { [Op.gt]: 2 } } }, 'symbol key is not allowed'],
|
|
1069
|
+
['Symbol Op.between', { i: { [Op.between]: [1, 2] } }, 'symbol key is not allowed'],
|
|
1070
|
+
['Symbol Op.like', { t: { [Op.like]: 'p%' } }, 'symbol key is not allowed'],
|
|
1071
|
+
['同義之$lt與Op.lt', { i: { $lt: 3, [Op.lt]: 100 } }, 'symbol key is not allowed'],
|
|
1072
|
+
['$and與Op.and並存', { $and: [{ i: { $gte: 2 } }], [Op.and]: [{ i: { $lte: 3 } }] }, 'symbol key is not allowed'],
|
|
1073
|
+
['$in含null', { t: { $in: [null, 'rose_mary'] } }, 'null is not allowed here'],
|
|
1074
|
+
['$in:[null]', { t: { $in: [null] } }, 'null is not allowed here'],
|
|
1075
|
+
['$nin含null', { t: { $nin: [null, 'rose_mary'] } }, 'null is not allowed here'],
|
|
1076
|
+
['$nin:[null]', { t: { $nin: [null] } }, 'null is not allowed here'],
|
|
1077
|
+
['比較運算子之值為null', { i: { $gt: null } }, 'null is not allowed here'],
|
|
1078
|
+
['空$nin', { i: { $nin: [] } }, '$nin with empty array is always true'],
|
|
1079
|
+
['$or含空$nin', { $or: [{ t: { $nin: [] } }, { s: 'zz' }] }, '$nin with empty array is always true'],
|
|
1080
|
+
['$or含空條件', { $or: [{}, { s: 's1' }] }, 'empty condition'],
|
|
1081
|
+
['$and含空條件', { $and: [{}] }, 'empty condition'],
|
|
1082
|
+
['欄名含$', { 'p$x': 'q' }, 'unsupported column name [p$x]'],
|
|
1083
|
+
['頂層null(原為全部)', null, 'condition must be a plain object'],
|
|
1084
|
+
['頂層陣列(原為全部)', [], 'condition must be a plain object'],
|
|
1085
|
+
['頂層字串(原為全部)', 's1', 'condition must be a plain object'],
|
|
1101
1086
|
|
|
1102
1087
|
//run04(規格句 R4欄名解析 嚴格鍵): 鍵須與屬性名或資料庫欄名完全相符, 其餘一律於轉譯階段reject, 不倚賴資料庫之識別字解析(兩方言同結果、兩函數訊息相同);
|
|
1103
1088
|
//原sqlite下型別不符之變體鍵使delAll刪除全部非NULL列, 並有sqlite隱含欄rowid、引號被sequelize刪除而成既有欄、mssql之全形/尾端空白/可忽略字元等價之繞過
|
|
1089
|
+
//(第33項批1: 尾端空白、全形空白、可忽略字元等含非字母數字底線者於形狀階段即reject為unsupported column name; 全形字母、等價字母等仍屬欄名文法, 由嚴格鍵reject為unknown column name)
|
|
1104
1090
|
['欄名大小寫變體 × 非數值字串', { I: { $lt: 'abc' } }, 'unknown column name [I]'],
|
|
1105
1091
|
['欄名大小寫變體 × 非日期字串', { D: { $lt: 'garbage' } }, 'unknown column name [D]'],
|
|
1106
1092
|
['欄名全形變體(mssql預設定序視為同一識別字)', { 'i': { $lt: 'abc' } }, 'unknown column name'],
|
|
1107
|
-
['欄名尾端空白(mssql忽略識別字尾端空白)', { 'i ': { $lt: 'abc' } }, '
|
|
1108
|
-
['欄名尾端全形空白', { ['i' + String.fromCharCode(0x3000)]: { $lt: 'abc' } }, '
|
|
1109
|
-
['欄名附加可忽略字元(mssql預設定序視為同一識別字)', { ['i' + String.fromCharCode(0xFEFF)]: { $lt: 'abc' } }, '
|
|
1093
|
+
['欄名尾端空白(mssql忽略識別字尾端空白)', { 'i ': { $lt: 'abc' } }, 'unsupported column name'],
|
|
1094
|
+
['欄名尾端全形空白', { ['i' + String.fromCharCode(0x3000)]: { $lt: 'abc' } }, 'unsupported column name'],
|
|
1095
|
+
['欄名附加可忽略字元(mssql預設定序視為同一識別字)', { ['i' + String.fromCharCode(0xFEFF)]: { $lt: 'abc' } }, 'unsupported column name'],
|
|
1110
1096
|
['欄名以mssql預設定序之等價字元(U+026A)取代i', { [String.fromCharCode(0x26A)]: { $lt: 'abc' } }, 'unknown column name'],
|
|
1111
1097
|
['sqlite隱含欄rowid(attrs未列)', { rowid: { $lt: 'abc' } }, 'unknown column name [rowid]'],
|
|
1112
1098
|
['欄名含反引號(sqlite之quoteIdentifier刪除之而成i)', { 'i`': { $lt: 'abc' } }, 'unsupported column name'],
|
|
@@ -1115,38 +1101,29 @@ export function defineSuites(label, getOpt) {
|
|
|
1115
1101
|
['欄名含雙引號', { 'i"': { $lt: 'abc' } }, 'unsupported column name'],
|
|
1116
1102
|
['欄名含點(JSON路徑/表.欄)', { 'i.x': { $lt: 'abc' } }, 'unsupported column name'],
|
|
1117
1103
|
['Object.prototype成員名', { constructor: { $lt: 'abc' } }, 'unsupported column name [constructor]'],
|
|
1118
|
-
//run04(sol H1):
|
|
1119
|
-
['僅$options為undefined', { s: { $options: undefined } }, 'operator
|
|
1104
|
+
//run04(sol H1)於第33項批1: $options不再受理, 僅有$options者為白名單外之運算子
|
|
1105
|
+
['僅$options為undefined', { s: { $options: undefined } }, 'unsupported operator [$options]'],
|
|
1120
1106
|
//審計02 外審 opus [2](規格句 R4 DATE: 換算UTC後年份須在1–9999): 年10000起 sequelize 以5位數格式化, sqlite 以文字比較致 $gt 命中並刪除全部日期列
|
|
1121
1107
|
['DATE欄收UTC年10000之Date', { d: { $gt: new Date(253402300800000) } }, 'does not match column type [date]'],
|
|
1122
1108
|
['DATE欄收UTC年10000之epoch', { d: { $gt: 253402300800000 } }, 'does not match column type [date]'],
|
|
1123
1109
|
['DATE欄收UTC年10000之ISO字串', { d: { $gt: '9999-12-31T23:30:00-05:00' } }, 'does not match column type [date]'],
|
|
1124
1110
|
['DATE欄收UTC年0000之epoch', { d: { $lt: -62135596800001 } }, 'does not match column type [date]'],
|
|
1125
|
-
['
|
|
1111
|
+
['讀值之getter不被執行而reject(存取器屬性)', Object.defineProperty({}, 's', {
|
|
1126
1112
|
enumerable: true,
|
|
1127
1113
|
get: () => {
|
|
1128
1114
|
throw new Error('getter boom')
|
|
1129
1115
|
}
|
|
1130
|
-
}), '
|
|
1116
|
+
}), 'accessor property is not allowed'],
|
|
1131
1117
|
]
|
|
1132
1118
|
|
|
1133
1119
|
//run04(規格句 R3欄位引用不變式、R4嚴格鍵、系列最低要求②③): attrs已給時不存在之欄 × 空集合各入口於轉譯階段reject(原常數形式使delAll清空全表;
|
|
1134
1120
|
//HEAD改為引用欄位後仰賴資料庫回報, 現由嚴格鍵直接reject, 兩函數訊息相同, mssql同一語句多次引用不存在之欄而err.message為空之情形不再可達)
|
|
1135
1121
|
for (let [t, col, find] of [
|
|
1136
1122
|
['空$in', 'nocol', { nocol: { $in: [] } }],
|
|
1137
|
-
['
|
|
1138
|
-
['陣列值[]', 'nocol', { nocol: [] }],
|
|
1139
|
-
['Op.in空陣列', 'nocol', { nocol: { [Sequelize.Op.in]: [] } }],
|
|
1140
|
-
['Op.notIn空陣列', 'nocol', { nocol: { [Sequelize.Op.notIn]: [] } }],
|
|
1141
|
-
['$not包空$in', 'nocol', { nocol: { $not: { $in: [] } } }],
|
|
1142
|
-
['$not包空$nin', 'nocol', { nocol: { $not: { $nin: [] } } }],
|
|
1143
|
-
['Symbol Op.not包Op.notIn空陣列', 'nocol', { nocol: { [Sequelize.Op.not]: { [Sequelize.Op.notIn]: [] } } }],
|
|
1144
|
-
['$in空與$nin空並存', 'nocol', { nocol: { $in: [], $nin: [] } }],
|
|
1145
|
-
['$or含空$nin', 'nocol', { $or: [{ nocol: { $nin: [] } }, { id: 'zz' }] }],
|
|
1123
|
+
['$or含空$in', 'nocol', { $or: [{ nocol: { $in: [] } }, { id: 'zz' }] }],
|
|
1146
1124
|
['$and含空$in', 'nocol', { $and: [{ nocol: { $in: [] } }, { id: 'r1' }] }],
|
|
1147
|
-
['
|
|
1148
|
-
['
|
|
1149
|
-
['欄名打錯且排除清單恰為空', 'sttus', { sttus: { $nin: [] } }],
|
|
1125
|
+
['欄名打錯且空$in', 'sttus', { sttus: { $in: [] } }],
|
|
1126
|
+
['欄名打錯且$in有值', 'sttus', { sttus: { $in: ['x'] } }],
|
|
1150
1127
|
]) {
|
|
1151
1128
|
bads.push([`不存在之欄 × ${t}`, find, `unknown column name [${col}]`])
|
|
1152
1129
|
}
|
|
@@ -1261,13 +1238,10 @@ export function defineSuites(label, getOpt) {
|
|
|
1261
1238
|
['等值', { id: 123 }, ['123']],
|
|
1262
1239
|
['$eq', { id: { $eq: 45 } }, ['45']],
|
|
1263
1240
|
['$in', { id: { $in: [123, 45] } }, ['123', '45']],
|
|
1264
|
-
['陣列值即$in', { id: [123, 'abc'] }, ['123', 'abc']],
|
|
1265
1241
|
['$nin', { id: { $nin: [123] } }, ['45', 'abc']],
|
|
1266
1242
|
['$ne', { id: { $ne: 45 } }, ['123', 'abc']],
|
|
1267
|
-
['$not', { id: { $not: { $eq: 123 } } }, ['45', 'abc']],
|
|
1268
1243
|
['$gt以字串比較', { id: { $gt: 2 } }, ['45', 'abc']],
|
|
1269
1244
|
['$or', { $or: [{ id: 123 }, { name: 'nabc' }] }, ['123', 'abc']],
|
|
1270
|
-
['Op.in', { id: { [Sequelize.Op.in]: [45] } }, ['45']],
|
|
1271
1245
|
]
|
|
1272
1246
|
for (let [t, find, exp] of cases) {
|
|
1273
1247
|
it(`select與delAll 主鍵欄之數字條件值視為字串且兩者一致: ${t}`, async function() {
|
|
@@ -1398,10 +1372,10 @@ export function defineSuites(label, getOpt) {
|
|
|
1398
1372
|
['欄名大小寫變體+$in字串', { Id: { $in: ['12', '5'] } }, 'unknown column name [Id]'],
|
|
1399
1373
|
['欄名大小寫變體+非整數', { Id: 12.4 }, 'unknown column name [Id]'],
|
|
1400
1374
|
['欄名全形變體+前導零', { 'ID': '012' }, 'unknown column name'],
|
|
1401
|
-
['欄名尾端空白+前導零', { 'id ': '012' }, '
|
|
1402
|
-
['欄名大寫+尾端空白+前導零', { 'ID ': '012' }, '
|
|
1403
|
-
['欄名尾端全形空白+前導零', { ['id' + ws]: '012' }, '
|
|
1404
|
-
['欄名附加可忽略字元+前導零', { ['id' + fe]: '012' }, '
|
|
1375
|
+
['欄名尾端空白+前導零', { 'id ': '012' }, 'unsupported column name'],
|
|
1376
|
+
['欄名大寫+尾端空白+前導零', { 'ID ': '012' }, 'unsupported column name'],
|
|
1377
|
+
['欄名尾端全形空白+前導零', { ['id' + ws]: '012' }, 'unsupported column name'],
|
|
1378
|
+
['欄名附加可忽略字元+前導零', { ['id' + fe]: '012' }, 'unsupported column name'],
|
|
1405
1379
|
['欄名以U+026A取代i+前導零', { [ip + 'd']: '012' }, 'unknown column name'],
|
|
1406
1380
|
['欄名含反引號+前導零', { 'i`d': '012' }, 'unsupported column name'],
|
|
1407
1381
|
['欄名含方括號+前導零', { 'i[d]': '012' }, 'unsupported column name'],
|
|
@@ -1552,11 +1526,8 @@ export function defineSuites(label, getOpt) {
|
|
|
1552
1526
|
['$ne(SQL三值不含NULL列)', { d: { $ne: '2026-06-15T08:30:15' } }, (t) => t !== T(S0)],
|
|
1553
1527
|
['$in', { d: { $in: ['2026-01-01T00:00:00', '2026-12-31T23:59:59.999'] } }, (t) => t === T([2026, 1, 1]) || t === T([2026, 12, 31, 23, 59, 59, 999])],
|
|
1554
1528
|
['$nin(SQL三值不含NULL列)', { d: { $nin: ['2026-01-01T00:00:00', '2026-12-31T23:59:59.999'] } }, (t) => t !== T([2026, 1, 1]) && t !== T([2026, 12, 31, 23, 59, 59, 999])],
|
|
1555
|
-
['陣列值即$in', { d: ['2026-01-01T00:00:00', '2026-06-15T08:30:15'] }, (t) => t === T([2026, 1, 1]) || t === T(S0)],
|
|
1556
|
-
['$not.$gt(SQL三值不含NULL列)', { d: { $not: { $gt: '2026-06-15T08:30:15' } } }, (t) => !(t > T(S0))],
|
|
1557
1529
|
['範圍$gte+$lt', { d: { $gte: '2026-06-15T00:00:00', $lt: '2026-06-16T00:00:00' } }, (t) => t >= T([2026, 6, 15]) && t < T([2026, 6, 16])],
|
|
1558
|
-
['Op.between', { d: {
|
|
1559
|
-
['Op.in', { d: { [Sequelize.Op.in]: ['2026-01-01T00:00:00'] } }, (t) => t === T([2026, 1, 1])],
|
|
1530
|
+
['$gte+$lte同欄(取代原Op.between)', { d: { $gte: '2026-06-15T08:30:15', $lte: '2026-12-31T23:59:59.999' } }, (t) => t >= T(S0) && t <= T([2026, 12, 31, 23, 59, 59, 999])],
|
|
1560
1531
|
['$in混有帶時區字串', { d: { $in: ['2026-01-01T00:00:00', '2026-06-15T00:30:15.123Z'] } }, (t) => t === T([2026, 1, 1]) || t === Date.parse('2026-06-15T00:30:15.123Z')],
|
|
1561
1532
|
['null照舊', { d: null }, ['r5']],
|
|
1562
1533
|
]
|
|
@@ -1656,11 +1627,8 @@ export function defineSuites(label, getOpt) {
|
|
|
1656
1627
|
['INTEGER 等值(另一列)', 'ageNum', 'age_num', 6, ['r2']],
|
|
1657
1628
|
['INTEGER $gt', 'ageNum', 'age_num', { $gt: 5 }, ['r2', 'r3']],
|
|
1658
1629
|
['INTEGER 數值字串', 'ageNum', 'age_num', '7', ['r3']],
|
|
1659
|
-
['INTEGER 空$nin', 'ageNum', 'age_num', { $nin: [] }, ['r1', 'r2', 'r3', 'r4']], //A2: 空集合$nin為恆真(含NULL列)
|
|
1660
1630
|
['INTEGER 空$in', 'ageNum', 'age_num', { $in: [] }, []],
|
|
1661
1631
|
['TEXT 等值', 'title', 'physical_title', 'Peter', ['r2']],
|
|
1662
|
-
['TEXT $regex', 'title', 'physical_title', { $regex: '_' }, ['r1']], //底線為字面
|
|
1663
|
-
['TEXT $regex i', 'title', 'physical_title', { $regex: 'E', $options: 'i' }, ['r1', 'r2']],
|
|
1664
1632
|
['DATE $lt ISO字串', 'at', 'at_col', { $lt: '2026-06-01T00:00:00Z' }, ['r1']],
|
|
1665
1633
|
['DATE $gt epoch', 'at', 'at_col', { $gt: Date.parse('2020-01-01T00:00:00Z') }, ['r2', 'r3']],
|
|
1666
1634
|
['DATE $in 混合', 'at', 'at_col', { $in: [new Date('2019-01-01T00:00:00Z'), '2026-06-15T00:00:00Z', Date.parse('2026-06-02T00:00:00Z')] }, ['r1', 'r2']],
|
|
@@ -1684,21 +1652,15 @@ export function defineSuites(label, getOpt) {
|
|
|
1684
1652
|
//屬性名鍵仍由sequelize映射為資料庫欄名; 不存在之欄須由資料庫回報而不得清空或刪除
|
|
1685
1653
|
let ctx0 = () => ({ escape: (v) => sq.escape(v), dialect: label })
|
|
1686
1654
|
let hit0 = async (find) => (await md.findAll({ where: cvFind(find, ctx0()), raw: true })).map((v) => v.id).sort()
|
|
1687
|
-
it('未給attrs: 空集合$in
|
|
1655
|
+
it('未給attrs: 空集合$in於資料庫實跑之命中(A2), 欄名經sequelize映射', async function() {
|
|
1688
1656
|
await seed()
|
|
1689
1657
|
assert.deepStrictEqual(await hit0({ ageNum: { $in: [] } }), [])
|
|
1690
|
-
assert.deepStrictEqual(await hit0({
|
|
1691
|
-
assert.deepStrictEqual(await hit0({ ageNum: { $nin: [] } }), ALL)
|
|
1692
|
-
assert.deepStrictEqual(await hit0({ ageNum: { $not: { $in: [] } } }), ALL)
|
|
1693
|
-
assert.deepStrictEqual(await hit0({ age_num: { $nin: [] } }), ALL)
|
|
1658
|
+
assert.deepStrictEqual(await hit0({ age_num: { $in: [] } }), [])
|
|
1694
1659
|
assert.deepStrictEqual(await hit0({ ageNum: 5 }), ['r1'])
|
|
1695
1660
|
})
|
|
1696
1661
|
for (let [t, find] of [
|
|
1697
|
-
['$nin:[]', { nocol: { $nin: [] } }],
|
|
1698
1662
|
['$in:[]', { nocol: { $in: [] } }],
|
|
1699
|
-
['
|
|
1700
|
-
['$not:{$in:[]}', { nocol: { $not: { $in: [] } } }],
|
|
1701
|
-
['$or含空$nin', { $or: [{ nocol: { $nin: [] } }, { id: 'zz' }] }],
|
|
1663
|
+
['$or含空$in', { $or: [{ nocol: { $in: [] } }, { id: 'zz' }] }],
|
|
1702
1664
|
]) {
|
|
1703
1665
|
it(`未給attrs: 不存在之欄 × 空集合由資料庫回報而reject, 資料不變: ${t}`, async function() {
|
|
1704
1666
|
await seed()
|
|
@@ -1715,7 +1677,7 @@ export function defineSuites(label, getOpt) {
|
|
|
1715
1677
|
['整數欄之欄名 × 非數值字串', { age_num: { $lt: 'abc' } }, '[int]'],
|
|
1716
1678
|
['整數欄之欄名大小寫變體(不解析)', { AGE_NUM: 'abc' }, 'unknown column name [AGE_NUM]'],
|
|
1717
1679
|
['DATE欄之欄名 × 非日期字串', { at_col: { $lt: 'garbage' } }, '[date]'],
|
|
1718
|
-
['整數欄之欄名 × $regex', { age_num: { $regex: '1' } }, '
|
|
1680
|
+
['整數欄之欄名 × $regex(白名單外)', { age_num: { $regex: '1' } }, 'unsupported operator [$regex]'],
|
|
1719
1681
|
['DATE欄之欄名大小寫變體 × 合法epoch', { AT_COL: { $gt: Date.parse('2030-01-01T00:00:00Z') } }, 'unknown column name [AT_COL]'],
|
|
1720
1682
|
['屬性rb(欄rc) × 非數值字串', { rb: { $lt: 'abc' } }, '[int]'],
|
|
1721
1683
|
['欄名rc × 非數值字串', { rc: { $lt: 'abc' } }, '[int]'],
|
|
@@ -1843,8 +1805,7 @@ export function defineSuites(label, getOpt) {
|
|
|
1843
1805
|
['等值', { a: 5 }],
|
|
1844
1806
|
['$gt', { a: { $gt: 100 } }],
|
|
1845
1807
|
['$in', { a: { $in: [5] } }],
|
|
1846
|
-
['空$
|
|
1847
|
-
['$exists', { a: { $exists: true } }],
|
|
1808
|
+
['空$in', { a: { $in: [] } }],
|
|
1848
1809
|
]) {
|
|
1849
1810
|
it(`映射後之欄名為空字串: 兩函數皆reject且資料不變: ${t}`, async function() {
|
|
1850
1811
|
//mssql之sync會把rawAttributes.a.field由''正規化為'a'(2026-10-16實測), 該model於mssql無此形態; sqlite不正規化(實測 WHERE (5) 刪全表), 故僅sqlite實跑, 兩方言之轉譯階段拒收由單元測試涵蓋
|
|
@@ -1858,8 +1819,6 @@ export function defineSuites(label, getOpt) {
|
|
|
1858
1819
|
for (let [t, find] of [
|
|
1859
1820
|
['等值', { w: 'needle' }],
|
|
1860
1821
|
['$in', { w: { $in: ['needle'] } }],
|
|
1861
|
-
['$regex', { w: { $regex: 'needle' } }],
|
|
1862
|
-
['$regex + i(原LOWER引用完整欄名而命中另一列)', { w: { $regex: 'needle', $options: 'i' } }],
|
|
1863
1822
|
]) {
|
|
1864
1823
|
it(`首尾皆$且中間有換行之欄名: 兩函數皆reject且資料不變: ${t}`, async function() {
|
|
1865
1824
|
await rej(mdV, [{ id: 'r1', t: 'needle', w: 'x' }, { id: 'r2', t: 'x', w: 'needle' }, { id: 'r3' }], find, 'unsupported column name [$\nname$]')
|
|
@@ -1870,7 +1829,7 @@ export function defineSuites(label, getOpt) {
|
|
|
1870
1829
|
['先定義者 × 日期字串', { sa: { $lt: '2030-01-01T00:00:00Z' } }],
|
|
1871
1830
|
['先定義者 × epoch', { sa: { $gt: 1893456000000 } }],
|
|
1872
1831
|
['先定義者 × Date', { sa: new Date(0) }],
|
|
1873
|
-
['先定義者 × 空$
|
|
1832
|
+
['先定義者 × 空$in', { sa: { $in: [] } }],
|
|
1874
1833
|
]) {
|
|
1875
1834
|
it(`共用一欄且欄名恰為先定義者之名稱: 兩函數皆reject且資料不變: ${t}`, async function() {
|
|
1876
1835
|
await rej(mdS, [{ id: 'r1', sb: 1 }, { id: 'r2', sb: 2 }, { id: 'r3' }], find, 'ambiguous column name [sa]')
|