jsql-neo 6.4.1 → 6.4.3

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,199 @@
1
+ /*
2
+ * 差分模糊测试:_compileMatcher(快路径)必须与 _matchRow(原实现)逐行一致。
3
+ *
4
+ * 背景:_matchRow 每行都做 Object.entries(query) / Object.keys(condition) /
5
+ * 一串 startswith 判断。实测这部分占单次全表扫描耗时的 **98%** ——
6
+ * 10 万行点查 13.9ms/次,其中 13.6ms 是反射开销;同样语义的手写循环只要 0.28ms。
7
+ * $like / $regex 更糟:正则每行每字段重编一次。
8
+ *
9
+ * _compileMatcher 把这些一次性挪到循环外。语义必须一字不差 ——
10
+ * 这个测试用随机数据 × 随机查询做交叉验证来保证。
11
+ *
12
+ * 它抓到过真 bug:$nin 的 target 非数组时,原实现返回 false(不匹配),
13
+ * 而编译版一度漏判、当成通过。
14
+ */
15
+ const path = require('path');
16
+
17
+ const ROOT = path.join(__dirname, '..');
18
+ const Database = require(path.join(ROOT, 'lib/database'));
19
+
20
+ let pass = 0, fail = 0;
21
+ const failures = [];
22
+ function ok(name, cond, extra) {
23
+ if (cond) { pass++; } else { fail++; failures.push(name + (extra !== undefined ? ' -> ' + JSON.stringify(extra).slice(0, 170) : '')); }
24
+ }
25
+
26
+ (async () => {
27
+ const db = new Database(':memory:', { autoSave: false });
28
+ await db.createTable('f', {
29
+ id: { type: 'integer', primaryKey: true },
30
+ num: { type: 'integer' },
31
+ str: { type: 'string' },
32
+ flt: { type: 'float' },
33
+ arr: { type: 'array' },
34
+ obj: { type: 'object' },
35
+ });
36
+ const STRS = ['a', 'ab', 'Ab', 'abc', 'b', '', 'a%b', 'a_b', 'A', 'zzz', 'aXb'];
37
+ const NUMS = [0, 1, 2, 3, 5, 10, -1, -5, 100, 25, 50]; // num 是 integer 列
38
+ const rows = [];
39
+ for (let i = 1; i <= 400; i++) {
40
+ rows.push({
41
+ id: i,
42
+ num: NUMS[i % NUMS.length],
43
+ str: STRS[i % STRS.length],
44
+ flt: (i % 17) / 2,
45
+ arr: i % 3 === 0 ? [1, 2, 3] : (i % 3 === 1 ? ['x', 'y'] : []),
46
+ obj: i % 4 === 0 ? { city: 'SH', n: i } : (i % 4 === 1 ? { city: 'BJ' } : { n: 1 }),
47
+ });
48
+ }
49
+ await db.insert('f', rows);
50
+ const t = db._tables.f;
51
+ const all = t._rows;
52
+
53
+ // ---- 可复现的伪随机(固定种子,失败可重放)----
54
+ let seed = 12345;
55
+ const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
56
+ const pick = (a) => a[Math.floor(rnd() * a.length)];
57
+
58
+ const FIELDS = ['num', 'str', 'flt', 'arr', 'obj', 'obj.city', 'id'];
59
+ const OPS = ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$between',
60
+ '$exists', '$isNull', '$startsWith', '$endsWith', '$contains', '$type', '$size',
61
+ '$like', '$regex'];
62
+
63
+ const genValue = (op) => {
64
+ switch (op) {
65
+ case '$in': case '$nin':
66
+ return [pick(NUMS), pick(STRS), pick(NUMS)].slice(0, 1 + Math.floor(rnd() * 3));
67
+ case '$between': return [pick(NUMS), pick(NUMS)].sort((a, b) => a - b);
68
+ case '$exists': case '$isNull': return rnd() < 0.5;
69
+ case '$type': return pick(['string', 'number', 'object', 'undefined', 'boolean']);
70
+ case '$size': return pick([0, 1, 2, 3]);
71
+ case '$startsWith': case '$endsWith': return pick(['a', 'A', 'b', 'ab', '']);
72
+ case '$like': return pick(['a%', '%b', 'a_b', '%', 'a%b', 'A%', '_', 'abc']);
73
+ case '$regex': return pick(['^a', 'b$', '[0-9]+', 'a.*b', '.']);
74
+ case '$gt': case '$gte': case '$lt': case '$lte': case '$eq': case '$ne':
75
+ return rnd() < 0.25 ? null : pick(rnd() < 0.5 ? NUMS : STRS);
76
+ default: return pick(NUMS);
77
+ }
78
+ };
79
+ const genFieldCond = () => {
80
+ const f = pick(FIELDS);
81
+ if (rnd() < 0.35) return { [f]: genValue(pick(OPS)) };
82
+ const ops = {};
83
+ const n = 1 + Math.floor(rnd() * 2);
84
+ for (let i = 0; i < n; i++) ops[pick(OPS)] = genValue(pick(OPS));
85
+ return { [f]: ops };
86
+ };
87
+ const genQuery = () => {
88
+ const r = rnd();
89
+ if (r < 0.12) return {}; // 空查询
90
+ if (r < 0.30) { // $or
91
+ const n = 1 + Math.floor(rnd() * 3);
92
+ return { $or: Array.from({ length: n }, () => genFieldCond()) };
93
+ }
94
+ if (r < 0.42) { // $and
95
+ const n = 1 + Math.floor(rnd() * 3);
96
+ return { $and: Array.from({ length: n }, () => genFieldCond()) };
97
+ }
98
+ if (r < 0.50) return { $not: genFieldCond() }; // $not
99
+ const q = {}; // 普通多字段
100
+ const n = 1 + Math.floor(rnd() * 3);
101
+ for (let i = 0; i < n; i++) Object.assign(q, genFieldCond());
102
+ return q;
103
+ };
104
+
105
+ // ---- 差分比对 ----
106
+ const N = 4000;
107
+ let mismatches = 0, empty = 0;
108
+ for (let i = 0; i < N; i++) {
109
+ const q = genQuery();
110
+ if (Object.keys(q).length === 0) empty++;
111
+ let fast, slow, e1 = null, e2 = null;
112
+ try { fast = all.filter(r => t._compileMatcher(q)(r)); } catch (e) { e1 = e.message; }
113
+ try { slow = all.filter(r => t._matchRow(r, q)); } catch (e) { e2 = e.message; }
114
+ if (e1 || e2) {
115
+ // 抛错行为也必须一致
116
+ if (!!e1 !== !!e2) { mismatches++; if (failures.length < 10) failures.push('抛错不一致 ' + JSON.stringify(q)); }
117
+ continue;
118
+ }
119
+ if (fast.length !== slow.length) {
120
+ mismatches++;
121
+ if (failures.length < 10) failures.push(`行数不一致 ${fast.length} vs ${slow.length} : ${JSON.stringify(q)}`);
122
+ continue;
123
+ }
124
+ for (let j = 0; j < fast.length; j++) {
125
+ if (fast[j] !== slow[j]) { mismatches++; if (failures.length < 10) failures.push('行内容不一致 ' + JSON.stringify(q)); break; }
126
+ }
127
+ }
128
+ ok(`差分模糊测试 ${N} 个随机查询零偏差(含 ${empty} 个空查询)`, mismatches === 0, failures.slice(0, 4));
129
+ console.log(` 随机查询 ${N} 个,偏差 ${mismatches}`);
130
+
131
+ // ---- 定向验证:逐个操作符 + 边界 ----
132
+ const dir = [
133
+ [{ num: 25 }, '等值'],
134
+ [{ num: { $eq: 25 } }, '$eq'],
135
+ [{ num: { $ne: 25 } }, '$ne'],
136
+ [{ num: { $gt: 50 } }, '$gt'],
137
+ [{ num: { $gte: 100 } }, '$gte'],
138
+ [{ num: { $lt: 0 } }, '$lt'],
139
+ [{ num: { $lte: 0 } }, '$lte'],
140
+ [{ num: { $in: [1, 25] } }, '$in'],
141
+ [{ num: { $nin: [1, 25] } }, '$nin'],
142
+ [{ num: { $nin: true } }, '$nin 非数组 target(曾在此分歧)'],
143
+ [{ num: { $nin: 'Ab' } }, '$nin 字符串 target'],
144
+ [{ num: { $in: true } }, '$in 非数组 target'],
145
+ [{ num: { $between: [1, 50] } }, '$between'],
146
+ [{ num: { $between: true } }, '$between 非数组'],
147
+ [{ num: { $exists: true } }, '$exists'],
148
+ [{ num: { $isNull: true } }, '$isNull'],
149
+ [{ str: { $startsWith: 'a' } }, '$startsWith'],
150
+ [{ str: { $endsWith: 'b' } }, '$endsWith'],
151
+ [{ arr: { $contains: 1 } }, '$contains'],
152
+ [{ arr: { $size: 3 } }, '$size'],
153
+ [{ obj: { $type: 'object' } }, '$type'],
154
+ [{ str: { $like: 'a%' } }, '$like'],
155
+ [{ str: { $like: 'a_b' } }, '$like _'],
156
+ [{ str: { $like: 'a%b' } }, '$like 转义 %'],
157
+ [{ str: { $regex: '^a' } }, '$regex'],
158
+ [{ 'obj.city': 'SH' }, '点号路径'],
159
+ [{ obj: { city: 'SH' } }, '嵌套对象'],
160
+ [{ obj: { city: 'SH', n: 1 } }, '嵌套对象两字段'],
161
+ [{ $or: [{ num: 1 }, { str: 'b' }] }, '$or'],
162
+ [{ $and: [{ num: 1 }, { str: 'a' }] }, '$and'],
163
+ [{ $not: { num: 1 } }, '$not'],
164
+ [{ num: { $gt: 0 }, str: { $startsWith: 'a' } }, '多字段'],
165
+ [{}, '空查询'],
166
+ [{ $or: [{ num: { $nin: true } }] }, '$or 内 $nin 非数组'],
167
+ ];
168
+ for (const [q, label] of dir) {
169
+ const fast = all.filter(r => t._compileMatcher(q)(r));
170
+ const slow = all.filter(r => t._matchRow(r, q));
171
+ ok('定向: ' + label, fast.length === slow.length && fast.every((r, i) => r === slow[i]),
172
+ { fast: fast.length, slow: slow.length });
173
+ }
174
+
175
+ // ---- 软删除模式下也必须一致 ----
176
+ {
177
+ await db.query('DELETE FROM f WHERE id <= 40', { safety: false });
178
+ const q = { num: 25 };
179
+ const fast = t._applyFilterOptimized(t._getVisibleRows(), q);
180
+ const slow = t._rows.filter(r => !r._deleted).filter(r => t._matchRow(r, q));
181
+ ok('软删除模式下一致', fast.length === slow.length && fast.every((r, i) => r === slow[i]),
182
+ { fast: fast.length, slow: slow.length });
183
+ }
184
+
185
+ // ---- LIKE / REGEX 的正则只编译一次(性能回归护栏)----
186
+ {
187
+ const q = { str: { $like: 'a%' } };
188
+ const m = t._compileMatcher(q);
189
+ const t0 = Date.now();
190
+ for (let i = 0; i < 300; i++) all.filter(r => m(r));
191
+ const ms = Date.now() - t0;
192
+ ok(`编译版 LIKE 300 次全表扫描在 ${ms}ms 内(原实现每行重编正则)`, ms < 1500, ms);
193
+ console.log(` 编译版 LIKE ×300 全表扫描: ${ms}ms`);
194
+ }
195
+
196
+ console.log(`\n${pass} passed, ${fail} failed`);
197
+ if (failures.length) { console.log('\n失败项:'); failures.slice(0, 15).forEach(f => console.log(' - ' + f)); }
198
+ process.exit(fail === 0 ? 0 : 1);
199
+ })().catch(e => { console.error('💥', e); process.exit(1); });
@@ -0,0 +1,131 @@
1
+ /*
2
+ * SQL 语义修正测试(IN 的三值逻辑 / 窗口默认帧 / JSON 路径操作符 / 补齐的标量函数)。
3
+ *
4
+ * 这一批全是「不报错、结果错」——静默失败最难发现,靠拿标准答案逐条对照才挖出来。
5
+ *
6
+ * 1. IN 列表里含 NULL 时整体失效:`v IN (10, NULL)` 连 v=10 的行都匹配不上。
7
+ * 正确语义是 `v=10 OR v=NULL` —— 有一个元素匹配就是 TRUE,
8
+ * 列表里别的位置有没有 NULL 都不影响;只有**没匹配上**时才变 UNKNOWN。
9
+ * 2. 窗口函数默认帧算错:`SUM(x) OVER (ORDER BY id)` 返回分区总和而不是累计值。
10
+ * SQL 标准:有 ORDER BY 且未指定帧 → RANGE UNBOUNDED PRECEDING TO CURRENT ROW。
11
+ * 3. `d->>'$.name'`(文档里给的示例)永远返回 null —— 求值层把 '$' 开头的
12
+ * JSONPath 当成字面键名去取了。纯键名写法 `d->>'name'` 一直是对的。
13
+ * 4. ASCII / CHAR / SPACE / FIELD / ELT / JSON_ARRAY_LENGTH 根本没实现,
14
+ * 调用时静默返回 null。
15
+ */
16
+ const path = require('path');
17
+
18
+ const ROOT = path.join(__dirname, '..');
19
+ const { Database } = require(path.join(ROOT, 'index.js'));
20
+ const { executeSQL } = require(path.join(ROOT, 'lib/sql.js'));
21
+
22
+ const OPTS = { safety: false };
23
+ let pass = 0, fail = 0;
24
+
25
+ function ok(name, cond, extra) {
26
+ if (cond) { pass++; console.log('[OK] ', name); }
27
+ else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 150) : ''); }
28
+ }
29
+ const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
30
+
31
+ (async () => {
32
+ const db = new Database(':memory:');
33
+ const q = (sql) => executeSQL(db, sql, OPTS);
34
+ const rows = async (sql) => (await q(sql)).rows;
35
+ const one = async (sql) => (await q(sql)).rows[0][0];
36
+
37
+ await q('CREATE TABLE t (id INT PRIMARY KEY, g TEXT, v INT, s TEXT)');
38
+ await q(`INSERT INTO t VALUES
39
+ (1,'a',10,'x'),(2,'a',20,'y'),(3,'b',10,NULL),(4,'b',30,'x'),(5,NULL,20,'y'),(6,NULL,NULL,'z')`);
40
+
41
+ console.log('\n--- IN 的三值逻辑 ---');
42
+ ok('v IN (10) 正常匹配', eq(await rows('SELECT COUNT(*) FROM t WHERE v IN (10)'), [[2]]));
43
+ ok('v IN (10, NULL) 仍能匹配 v=10(关键用例)',
44
+ eq(await rows('SELECT COUNT(*) FROM t WHERE v IN (10, NULL)'), [[2]]),
45
+ await rows('SELECT COUNT(*) FROM t WHERE v IN (10, NULL)'));
46
+ ok('NULL 在列表开头也一样', eq(await rows('SELECT COUNT(*) FROM t WHERE v IN (NULL, 10)'), [[2]]));
47
+ ok('多个非 NULL 元素', eq(await rows('SELECT COUNT(*) FROM t WHERE v IN (10,20,30)'), [[5]]));
48
+ ok('v IN (NULL) 恒为空集', eq(await rows('SELECT COUNT(*) FROM t WHERE v IN (NULL)'), [[0]]));
49
+ ok('v NOT IN (10)', eq(await rows('SELECT COUNT(*) FROM t WHERE v NOT IN (10)'), [[3]]));
50
+ ok('v NOT IN (10, NULL) 恒为空集(UNKNOWN 取反仍是 UNKNOWN)',
51
+ eq(await rows('SELECT COUNT(*) FROM t WHERE v NOT IN (10, NULL)'), [[0]]));
52
+ ok('与等价的 OR 写法结果一致',
53
+ eq(await rows('SELECT COUNT(*) FROM t WHERE v = 10 OR v = NULL'),
54
+ await rows('SELECT COUNT(*) FROM t WHERE v IN (10, NULL)')));
55
+ ok('字符串 IN 含 NULL', eq(await rows("SELECT COUNT(*) FROM t WHERE s IN ('x', NULL)"), [[2]]));
56
+ ok('字面量 "null" 不会被 NULL 误匹配',
57
+ eq(await rows("SELECT COUNT(*) FROM t WHERE s IN ('null')"), [[0]]));
58
+ ok('子查询 IN(结果含 NULL)',
59
+ eq(await rows('SELECT COUNT(*) FROM t WHERE v IN (SELECT v FROM t WHERE v = 10 OR v IS NULL)'), [[2]]));
60
+ ok('NOT IN 子查询含 NULL → 空集',
61
+ eq(await rows('SELECT COUNT(*) FROM t WHERE v NOT IN (SELECT v FROM t WHERE v = 10 OR v IS NULL)'), [[0]]));
62
+
63
+ console.log('\n--- 窗口函数默认帧 ---');
64
+ await q('CREATE TABLE e (id INT PRIMARY KEY, dept TEXT, sal INT)');
65
+ await q(`INSERT INTO e VALUES (1,'a',100),(2,'a',200),(3,'a',200),(4,'b',50),(5,'b',300)`);
66
+ ok('SUM OVER (ORDER BY) 是累计值(关键用例)',
67
+ eq(await rows('SELECT id, SUM(sal) OVER (PARTITION BY dept ORDER BY id) s FROM e ORDER BY id'),
68
+ [[1,100],[2,300],[3,500],[4,50],[5,350]]),
69
+ await rows('SELECT id, SUM(sal) OVER (PARTITION BY dept ORDER BY id) s FROM e ORDER BY id'));
70
+ ok('无 ORDER BY 时是整分区总和',
71
+ eq(await rows('SELECT id, SUM(sal) OVER (PARTITION BY dept) s FROM e ORDER BY id'),
72
+ [[1,500],[2,500],[3,500],[4,350],[5,350]]));
73
+ ok('无分区无排序 = 全表总和',
74
+ eq(await rows('SELECT id, SUM(sal) OVER () s FROM e ORDER BY id'),
75
+ [[1,850],[2,850],[3,850],[4,850],[5,850]]));
76
+ ok('COUNT OVER (ORDER BY) 累计',
77
+ eq(await rows('SELECT id, COUNT(*) OVER (ORDER BY id) c FROM e ORDER BY id'),
78
+ [[1,1],[2,2],[3,3],[4,4],[5,5]]));
79
+ ok('AVG OVER (ORDER BY) 累计',
80
+ eq(await rows("SELECT id, AVG(sal) OVER (PARTITION BY dept ORDER BY id) a FROM e WHERE dept='a' ORDER BY id"),
81
+ [[1,100],[2,150],[3,166.66666666666666]]));
82
+ ok('默认帧按 RANGE 语义:同值 peer 一并纳入',
83
+ eq(await rows("SELECT id, SUM(sal) OVER (PARTITION BY dept ORDER BY sal) s FROM e WHERE dept='a' ORDER BY id"),
84
+ [[1,100],[2,500],[3,500]]));
85
+ ok('显式 ROWS 帧不受 peer 影响',
86
+ eq(await rows("SELECT id, SUM(sal) OVER (PARTITION BY dept ORDER BY sal ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) s FROM e WHERE dept='a' ORDER BY id"),
87
+ [[1,100],[2,300],[3,500]]));
88
+ ok('ROW_NUMBER 不受默认帧影响',
89
+ eq(await rows('SELECT id, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY sal DESC, id) r FROM e ORDER BY id'),
90
+ [[1,3],[2,1],[3,2],[4,2],[5,1]]));
91
+
92
+ console.log('\n--- JSON 路径操作符 ---');
93
+ await q('CREATE TABLE j (id INT PRIMARY KEY, d TEXT)');
94
+ await q(`INSERT INTO j VALUES (1,'{"name":"ann","n":5,"addr":{"city":"SH"},"arr":[10,20]}')`);
95
+ ok("d->>'$.name'(文档里的写法)", await one("SELECT d->>'$.name' FROM j") === 'ann');
96
+ ok("d->'$.name'", await one("SELECT d->'$.name' FROM j") === 'ann');
97
+ ok("d->>'$.n' 数字转文本", await one("SELECT d->>'$.n' FROM j") === '5');
98
+ ok("d->'$.n' 保留数字类型", await one("SELECT d->'$.n' FROM j") === 5);
99
+ ok("d->>'$.addr.city' 嵌套", await one("SELECT d->>'$.addr.city' FROM j") === 'SH');
100
+ ok("d->'$.addr' 返回对象", eq(await one("SELECT d->'$.addr' FROM j"), { city: 'SH' }));
101
+ ok("d->>'$.arr[1]' 数组下标", await one("SELECT d->>'$.arr[1]' FROM j") === '20');
102
+ ok("d->>'$.nope' 不存在 → null", await one("SELECT d->>'$.nope' FROM j") === null);
103
+ ok("纯键名写法不受影响 d->>'name'", await one("SELECT d->>'name' FROM j") === 'ann');
104
+ ok("链式 d->'addr'->>'city'", await one("SELECT d->'addr'->>'city' FROM j") === 'SH');
105
+ ok('JSON_EXTRACT 未受影响',
106
+ await one("SELECT JSON_EXTRACT(d,'$.addr.city') FROM j") === 'SH');
107
+ ok("JSON_EXTRACT '$' 取根", eq(await one("SELECT JSON_EXTRACT('{\"a\":1}','$')"), { a: 1 }));
108
+
109
+ console.log('\n--- 补齐的标量函数 ---');
110
+ ok("ASCII('A')", await one("SELECT ASCII('A')") === 65);
111
+ ok("ASCII('abc')", await one("SELECT ASCII('abc')") === 97);
112
+ ok("ASCII('') 为 0", await one("SELECT ASCII('')") === 0);
113
+ ok('ASCII(NULL) 为 NULL', await one('SELECT ASCII(NULL)') === null);
114
+ ok('CHAR(65)', await one('SELECT CHAR(65)') === 'A');
115
+ ok('CHAR 多参数', await one('SELECT CHAR(72,105)') === 'Hi');
116
+ ok('SPACE(3)', await one('SELECT LENGTH(SPACE(3))') === 3);
117
+ ok('SPACE(0)', await one('SELECT LENGTH(SPACE(0))') === 0);
118
+ ok("FIELD 命中", await one("SELECT FIELD('b','a','b','c')") === 2);
119
+ ok('FIELD 未命中为 0', await one("SELECT FIELD('z','a','b')") === 0);
120
+ ok('FIELD(NULL) 为 NULL', await one("SELECT FIELD(NULL,'a')") === null);
121
+ ok('ELT 命中', await one("SELECT ELT(2,'a','b','c')") === 'b');
122
+ ok('ELT 越界为 NULL', await one("SELECT ELT(9,'a','b')") === null);
123
+ ok('ELT(0) 为 NULL', await one("SELECT ELT(0,'a','b')") === null);
124
+ ok('JSON_ARRAY_LENGTH', await one("SELECT JSON_ARRAY_LENGTH('[1,2,3]')") === 3);
125
+ ok('JSON_ARRAY_LENGTH 非数组 → NULL',
126
+ await one("SELECT JSON_ARRAY_LENGTH('{\"a\":1}')") === null);
127
+ ok('JSON_ARRAY_LENGTH 空数组', await one("SELECT JSON_ARRAY_LENGTH('[]')") === 0);
128
+
129
+ console.log(`\n${pass} passed, ${fail} failed`);
130
+ process.exit(fail === 0 ? 0 : 1);
131
+ })().catch(e => { console.error('💥', e); process.exit(1); });