jsql-neo 6.4.2 → 6.4.4
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 +1 -1
- package/index.js +1 -1
- package/lib/sql.js +248 -53
- package/lib/table.js +182 -4
- package/package.json +5 -3
- package/test/extra-coverage.test.js +54 -5
- package/test/filter-compile.test.js +199 -0
- package/test/sql-semantics.test.js +131 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.4.
|
|
6
|
+
> **v6.4.4** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
package/index.js
CHANGED
package/lib/sql.js
CHANGED
|
@@ -131,6 +131,40 @@ function nameOfToken(t) {
|
|
|
131
131
|
return t && t.text !== undefined ? t.text : (t ? t.value : undefined);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* 按 JSONPath 取值($.a、$.a.b、$.a[0].b、$)。
|
|
136
|
+
*
|
|
137
|
+
* 原先是 JSON_EXTRACT 里的一段内联逻辑。`->` / `->>` 同样接受 JSONPath
|
|
138
|
+
* (MySQL 5.7+ 的写法,也是 doc/sql-reference.md 里给出的示例),
|
|
139
|
+
* 但此前把 '$' 开头的路径当成字面键名去取(v['$.name']),永远取不到 ——
|
|
140
|
+
* 所以抽出来共用,避免两处实现漂移。
|
|
141
|
+
*/
|
|
142
|
+
function jsonPathGet(data, path) {
|
|
143
|
+
const p = path == null ? '' : String(path);
|
|
144
|
+
if (p === '$' || p === '') return data;
|
|
145
|
+
const m = p.match(/^\$\.?([^.\[]+)/);
|
|
146
|
+
if (!m) return null;
|
|
147
|
+
let current = data;
|
|
148
|
+
for (const seg of p.replace(/^\$\.?/, '').split('.')) {
|
|
149
|
+
if (current === null || current === undefined) return null;
|
|
150
|
+
const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
|
|
151
|
+
if (arrMatch) {
|
|
152
|
+
const key = arrMatch[1];
|
|
153
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
154
|
+
if (key) {
|
|
155
|
+
if (typeof current !== 'object' || current === null) return null;
|
|
156
|
+
current = current[key];
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(current)) return null;
|
|
159
|
+
current = current[idx];
|
|
160
|
+
} else {
|
|
161
|
+
if (typeof current !== 'object' || current === null) return null;
|
|
162
|
+
current = current[seg];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return current === undefined ? null : current;
|
|
166
|
+
}
|
|
167
|
+
|
|
134
168
|
function tokenize(sql) {
|
|
135
169
|
const tokens = [];
|
|
136
170
|
let i = 0;
|
|
@@ -2779,6 +2813,40 @@ function rowHasPrefixedKeys(row) {
|
|
|
2779
2813
|
return false;
|
|
2780
2814
|
}
|
|
2781
2815
|
|
|
2816
|
+
/**
|
|
2817
|
+
* 整棵语句树里有没有出现过「表.列」形式的限定引用。
|
|
2818
|
+
*
|
|
2819
|
+
* executeSelect 会给每一行调用 _prefixRow,把键复制成「表.列」形式。
|
|
2820
|
+
* 单表查询里这些键根本没被引用,10 万行就是 10 万个白造对象
|
|
2821
|
+
* (node --prof 显示占整个查询 30% 的 GC)。
|
|
2822
|
+
*
|
|
2823
|
+
* 但限定引用不一定长在 {type:/column/} 节点里 —— ORDER BY 的 o.column
|
|
2824
|
+
* 就是纯字符串 "t.age",HAVING / pushdown 的键也是。只扫对象节点会漏,
|
|
2825
|
+
* 漏了就少加前缀,相关子查询和限定列全部解析错。两种形态都要查。
|
|
2826
|
+
*
|
|
2827
|
+
* 消费方清单(摸清这些才敢跳过前缀):
|
|
2828
|
+
* 1. resolveOperand 的 column 分支 —— 限定引用解析
|
|
2829
|
+
* 2. rowHasPrefixedKeys —— 未知限定符的防自匹配守卫
|
|
2830
|
+
* 3. SELECT * 的 starKeys —— 单表时取 Object.keys(schema),与行前缀无关
|
|
2831
|
+
* 4. JOIN 右表 —— 多表同名列消歧,本来就强制前缀
|
|
2832
|
+
*/
|
|
2833
|
+
const QUALIFIED_REF_RE = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2834
|
+
function hasQualifiedRef(node, depth) {
|
|
2835
|
+
if (!node || typeof node !== "object" || depth > 200) return false;
|
|
2836
|
+
if (Array.isArray(node)) {
|
|
2837
|
+
for (const n of node) if (hasQualifiedRef(n, depth + 1)) return true;
|
|
2838
|
+
return false;
|
|
2839
|
+
}
|
|
2840
|
+
if (node.type === "column" && typeof node.name === "string" && node.name.indexOf(".") !== -1) return true;
|
|
2841
|
+
for (const k in node) {
|
|
2842
|
+
if (k === "ctx" || k === "engine" || k === "session") continue;
|
|
2843
|
+
const v = node[k];
|
|
2844
|
+
if (typeof v === "string") { if (QUALIFIED_REF_RE.test(v)) return true; }
|
|
2845
|
+
else if (v && typeof v === "object" && hasQualifiedRef(v, depth + 1)) return true;
|
|
2846
|
+
}
|
|
2847
|
+
return false;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2782
2850
|
/**
|
|
2783
2851
|
* 字符串能否当作数值参与算术运算。
|
|
2784
2852
|
* MySQL 里 '5' + 5 = 10、'5' + '5' = 10、'abc' + 1 = 1(非数字按 0)。
|
|
@@ -2986,6 +3054,14 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2986
3054
|
if (typeof base === 'string') {
|
|
2987
3055
|
try { base = JSON.parse(base); } catch (e) { return null; }
|
|
2988
3056
|
}
|
|
3057
|
+
// '$' 开头是 JSONPath(MySQL 5.7+ 的写法,`d->>'$.name'`、`d->>'$.a.b'`)。
|
|
3058
|
+
// 此前把它当字面键名去取(v['$.name']),于是文档里那条示例永远返回 null。
|
|
3059
|
+
if (!operand.isPath && typeof operand.key === 'string' && operand.key.trim().charAt(0) === '$') {
|
|
3060
|
+
const jv = jsonPathGet(base, operand.key.trim());
|
|
3061
|
+
if (jv === null || jv === undefined) return null;
|
|
3062
|
+
if (operand.asText) return typeof jv === 'object' ? JSON.stringify(jv) : String(jv);
|
|
3063
|
+
return jv;
|
|
3064
|
+
}
|
|
2989
3065
|
const keys = operand.isPath ? parsePgPath(operand.key) : [operand.key];
|
|
2990
3066
|
let v = base;
|
|
2991
3067
|
for (const key of keys) {
|
|
@@ -3607,6 +3683,55 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
3607
3683
|
return idx + 1;
|
|
3608
3684
|
}
|
|
3609
3685
|
case 'REVERSE': return args[0] == null ? null : String(args[0]).split('').reverse().join('');
|
|
3686
|
+
|
|
3687
|
+
// ---- 以下这批此前没有实现:调用时静默返回 null(不报错),
|
|
3688
|
+
// 只有拿标准答案对照才发现。语义按 MySQL 8.0。
|
|
3689
|
+
case 'ASCII': {
|
|
3690
|
+
// 最左字符的数值;空串返回 0,NULL 返回 NULL
|
|
3691
|
+
if (args[0] == null) return null;
|
|
3692
|
+
const s = String(args[0]);
|
|
3693
|
+
return s.length ? s.charCodeAt(0) : 0;
|
|
3694
|
+
}
|
|
3695
|
+
case 'ORD': {
|
|
3696
|
+
if (args[0] == null) return null;
|
|
3697
|
+
const s = String(args[0]);
|
|
3698
|
+
return s.length ? s.codePointAt(0) : 0;
|
|
3699
|
+
}
|
|
3700
|
+
case 'CHAR': {
|
|
3701
|
+
// CHAR(65,66) → 'AB';跳过 NULL 参数
|
|
3702
|
+
const codes = args.filter(a => a != null).map(a => Number(a));
|
|
3703
|
+
if (!codes.length) return '';
|
|
3704
|
+
return codes.map(n => String.fromCharCode(n)).join('');
|
|
3705
|
+
}
|
|
3706
|
+
case 'SPACE': {
|
|
3707
|
+
if (args[0] == null) return null;
|
|
3708
|
+
const n = Math.max(0, Math.floor(Number(args[0])) || 0);
|
|
3709
|
+
return ' '.repeat(n);
|
|
3710
|
+
}
|
|
3711
|
+
case 'FIELD': {
|
|
3712
|
+
// FIELD(x, a, b, ...) → x 在列表中的 1 基下标;找不到返回 0;x 为 NULL 返回 NULL
|
|
3713
|
+
if (args[0] == null) return null;
|
|
3714
|
+
const needle = String(args[0]);
|
|
3715
|
+
for (let i = 1; i < args.length; i++) {
|
|
3716
|
+
if (args[i] != null && String(args[i]) === needle) return i;
|
|
3717
|
+
}
|
|
3718
|
+
return 0;
|
|
3719
|
+
}
|
|
3720
|
+
case 'ELT': {
|
|
3721
|
+
// ELT(n, a, b, ...) → 第 n 个;越界或 n 为 NULL 返回 NULL
|
|
3722
|
+
if (args[0] == null) return null;
|
|
3723
|
+
const idx = Math.floor(Number(args[0]));
|
|
3724
|
+
if (!Number.isFinite(idx) || idx < 1 || idx >= args.length) return null;
|
|
3725
|
+
return args[idx] == null ? null : String(args[idx]);
|
|
3726
|
+
}
|
|
3727
|
+
case 'JSON_ARRAY_LENGTH': {
|
|
3728
|
+
if (args[0] == null) return null;
|
|
3729
|
+
let v = args[0];
|
|
3730
|
+
if (typeof v === 'string') {
|
|
3731
|
+
try { v = JSON.parse(v); } catch (e) { return null; }
|
|
3732
|
+
}
|
|
3733
|
+
return Array.isArray(v) ? v.length : null;
|
|
3734
|
+
}
|
|
3610
3735
|
case 'LPAD': {
|
|
3611
3736
|
if (args[0] == null) return null;
|
|
3612
3737
|
let s = String(args[0]);
|
|
@@ -3685,34 +3810,7 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
3685
3810
|
if (typeof json === 'string') {
|
|
3686
3811
|
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3687
3812
|
}
|
|
3688
|
-
|
|
3689
|
-
const m = path.match(/^\$\.?([^.\[]+)/);
|
|
3690
|
-
if (!m) {
|
|
3691
|
-
// 尝试 $ 开头后直接跟 key
|
|
3692
|
-
if (path === '$') return data;
|
|
3693
|
-
return null;
|
|
3694
|
-
}
|
|
3695
|
-
const firstKey = m[1];
|
|
3696
|
-
if (firstKey === '') return data;
|
|
3697
|
-
let current = data;
|
|
3698
|
-
// 逐段解析路径
|
|
3699
|
-
const segments = path.replace(/^\$\.?/, '').split('.');
|
|
3700
|
-
for (const seg of segments) {
|
|
3701
|
-
if (current == null) return null;
|
|
3702
|
-
// 处理数组索引: key[0] 或 [0]
|
|
3703
|
-
const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3704
|
-
if (arrMatch) {
|
|
3705
|
-
const key = arrMatch[1];
|
|
3706
|
-
const idx = parseInt(arrMatch[2], 10);
|
|
3707
|
-
if (key) current = current[key];
|
|
3708
|
-
if (Array.isArray(current)) current = current[idx];
|
|
3709
|
-
else return null;
|
|
3710
|
-
} else {
|
|
3711
|
-
if (typeof current !== 'object' || current === null) return null;
|
|
3712
|
-
current = current[seg];
|
|
3713
|
-
}
|
|
3714
|
-
}
|
|
3715
|
-
return current === undefined ? null : current;
|
|
3813
|
+
return jsonPathGet(data, path);
|
|
3716
3814
|
}
|
|
3717
3815
|
case 'JSON_KEYS': {
|
|
3718
3816
|
// JSON_KEYS(json) → [key1, key2, ...]
|
|
@@ -4255,11 +4353,22 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4255
4353
|
if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
|
|
4256
4354
|
if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
|
|
4257
4355
|
if (!expr.list) return expr.not ? true : false;
|
|
4258
|
-
|
|
4356
|
+
|
|
4357
|
+
// 先找匹配:只要有一个非 NULL 元素等于 v,整个 IN 就是 TRUE ——
|
|
4358
|
+
// 列表里别的位置有没有 NULL 都不影响。
|
|
4359
|
+
//
|
|
4360
|
+
// 此前是先判「列表里有 NULL 就返回 NULL」,于是 `v IN (10, NULL)`
|
|
4361
|
+
// 连 v=10 的行都匹配不上(实测返回 0 行,应为 2 行)。
|
|
4362
|
+
// 三值逻辑里 NULL 只在**没匹配上**的时候才让结果变 UNKNOWN。
|
|
4363
|
+
const matched = expr.list.some(x =>
|
|
4364
|
+
x !== null && x !== undefined && (x === v || String(x) === String(v))
|
|
4365
|
+
);
|
|
4366
|
+
if (matched) return expr.not ? false : true;
|
|
4367
|
+
|
|
4368
|
+
// 没匹配上:列表里有 NULL → UNKNOWN,否则是确定的 FALSE
|
|
4259
4369
|
const hasNull = expr.list.some(x => x === null || x === undefined);
|
|
4260
4370
|
if (hasNull) return null;
|
|
4261
|
-
|
|
4262
|
-
return expr.not ? !r : r;
|
|
4371
|
+
return expr.not ? true : false;
|
|
4263
4372
|
}
|
|
4264
4373
|
case 'like': {
|
|
4265
4374
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
@@ -6485,7 +6594,26 @@ class SQLExecutor {
|
|
|
6485
6594
|
// 帧范围(ROWS 帧;RANGE 在当前实现下退化为整个分区,保守处理)
|
|
6486
6595
|
const frameIdx = (i) => {
|
|
6487
6596
|
const f = w.over.frame;
|
|
6488
|
-
if (!f)
|
|
6597
|
+
if (!f) {
|
|
6598
|
+
// 没显式写帧时的默认帧,按 SQL 标准分两种情况:
|
|
6599
|
+
// 有 ORDER BY → RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
|
6600
|
+
// 无 ORDER BY → 整个分区
|
|
6601
|
+
//
|
|
6602
|
+
// 此前一律返回整个分区,于是
|
|
6603
|
+
// SUM(sal) OVER (PARTITION BY dept ORDER BY id)
|
|
6604
|
+
// 得到的是分区总和(500)而不是累计值(100/300/500)。
|
|
6605
|
+
//
|
|
6606
|
+
// 用 RANGE 而非 ROWS 语义:ORDER BY 值相同的行互为 peer,
|
|
6607
|
+
// 要一并纳入当前行(这正是 RANGE 与 ROWS 的区别)。
|
|
6608
|
+
if (!orderBy.length) return { start: 0, end: ordered.length - 1 };
|
|
6609
|
+
const curKey = this._windowPeerKey(rows[ordered[i]], orderBy, this.ctx);
|
|
6610
|
+
let end = i;
|
|
6611
|
+
while (end + 1 < ordered.length &&
|
|
6612
|
+
this._windowPeerKey(rows[ordered[end + 1]], orderBy, this.ctx) === curKey) {
|
|
6613
|
+
end++;
|
|
6614
|
+
}
|
|
6615
|
+
return { start: 0, end };
|
|
6616
|
+
}
|
|
6489
6617
|
let start = 0, end = ordered.length - 1;
|
|
6490
6618
|
const apply = (b, isStart) => {
|
|
6491
6619
|
if (!b) return isStart ? 0 : ordered.length - 1;
|
|
@@ -6749,10 +6877,25 @@ class SQLExecutor {
|
|
|
6749
6877
|
return (a && String(a).includes('.') ? String(a) : `${firstPrefix}.${a}`);
|
|
6750
6878
|
}).filter(Boolean)
|
|
6751
6879
|
: null;
|
|
6880
|
+
/*
|
|
6881
|
+
* 曾经试着在「单表 + 无 JOIN」时跳过 _prefixRow 以省掉 10 万次对象分配
|
|
6882
|
+
* (node --prof 显示它占 30% GC)。实测确实快 2~3 倍,但相关子查询
|
|
6883
|
+
* (EXISTS / 标量 / IN)靠 `外层表.列` 引用外层行,没前缀就解析不到,
|
|
6884
|
+
* 而 _collectSubqueries 在这个位置抓不到它们(前面已被改写)。
|
|
6885
|
+
* 收益真实但边界没摸清,先回退,留待摸清后再上。
|
|
6886
|
+
*/
|
|
6887
|
+
/*
|
|
6888
|
+
* 单表 + 无 JOIN + 无派生表 + 整棵树没有限定引用时,跳过前缀。
|
|
6889
|
+
* 不缓存到 statement 上:语句对象可能被 prepare() 复用,挂属性会串味。
|
|
6890
|
+
*/
|
|
6891
|
+
const needPrefix = isVirtualSource
|
|
6892
|
+
|| !!(statement.from.joins && statement.from.joins.length)
|
|
6893
|
+
|| hasQualifiedRef(statement, 0);
|
|
6752
6894
|
let rows = isVirtualSource
|
|
6753
6895
|
? rowsAll.rows.map(r => this._prefixRow(r, firstPrefix))
|
|
6754
|
-
: (firstItem._virtualRows
|
|
6755
|
-
|
|
6896
|
+
: (needPrefix || firstItem._virtualRows
|
|
6897
|
+
? rowsAll.rows.map(r => this._prefixRow(r, firstPrefix))
|
|
6898
|
+
: rowsAll.rows);
|
|
6756
6899
|
for (const j of statement.from.joins) {
|
|
6757
6900
|
// 虚拟行(递归 CTE 物化的派生表)要直接取行,不能当普通子查询执行
|
|
6758
6901
|
const jVirtual = j.item._virtualRows ||
|
|
@@ -7038,28 +7181,54 @@ class SQLExecutor {
|
|
|
7038
7181
|
if (statement.orderBy) {
|
|
7039
7182
|
// ORDER BY 项可能是列名字符串、表达式节点,或列的位置序号(ORDER BY 1,1-based)
|
|
7040
7183
|
const ordNames = statement.columns.map(c => scalarColumnName(c));
|
|
7041
|
-
|
|
7184
|
+
/*
|
|
7185
|
+
* 把排序键的解析**预先编译**,比较器里只做数组下标访问。
|
|
7186
|
+
*
|
|
7187
|
+
* 此前每次比较都调 `resolveOperand({type:'column', name}, row, ctx)` ——
|
|
7188
|
+
* 那个对象字面量是每次新建的。6.8 万行排序约 230 万次比较,
|
|
7189
|
+
* 就是 230 万次白白分配,实测占掉整个 ORDER BY 的大半时间
|
|
7190
|
+
* (10 万行 WHERE+ORDER+LIMIT 从 133ms 量级大幅下降)。
|
|
7191
|
+
*
|
|
7192
|
+
* 同时按「投影列名 → 下标」解析;命中就直接 `row[idx]`,
|
|
7193
|
+
* 不再走 resolveOperand 的通用路径。
|
|
7194
|
+
*/
|
|
7195
|
+
const ordKeys = statement.orderBy.map(o => {
|
|
7196
|
+
const dir = o.dir === 'asc' ? 1 : -1;
|
|
7197
|
+
// 位置序号:ORDER BY 1
|
|
7042
7198
|
const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
|
|
7043
7199
|
? o.column.value
|
|
7044
7200
|
: (typeof o.column === 'number' ? o.column : null);
|
|
7045
7201
|
if (pos !== null && pos !== undefined) {
|
|
7046
7202
|
const name = ordNames[pos - 1];
|
|
7047
|
-
|
|
7203
|
+
return { dir, at: (name === undefined || name === null) ? () => null : (r) => (r == null ? null : r[name]) };
|
|
7048
7204
|
}
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
:
|
|
7052
|
-
|
|
7205
|
+
if (typeof o.column === 'string') {
|
|
7206
|
+
const f = o.column;
|
|
7207
|
+
// 节点对象提到循环外复用 —— 此前每次比较都新建一个 {type:'column',name}
|
|
7208
|
+
const node = { type: 'column', name: f };
|
|
7209
|
+
return {
|
|
7210
|
+
dir,
|
|
7211
|
+
at: (r) => {
|
|
7212
|
+
// 先按字段名直取;取不到再走 resolveOperand(点号路径、表达式等)
|
|
7213
|
+
const v = (r == null) ? undefined : r[f];
|
|
7214
|
+
return v !== undefined ? v : resolveOperand(node, r, this.ctx);
|
|
7215
|
+
},
|
|
7216
|
+
};
|
|
7217
|
+
}
|
|
7218
|
+
return { dir, at: (r) => resolveOperand(o.column, r, this.ctx) };
|
|
7219
|
+
});
|
|
7220
|
+
|
|
7053
7221
|
const cmp = (a, b) => {
|
|
7054
|
-
for (
|
|
7055
|
-
const
|
|
7056
|
-
const
|
|
7222
|
+
for (let i = 0; i < ordKeys.length; i++) {
|
|
7223
|
+
const k = ordKeys[i];
|
|
7224
|
+
const av = k.at(a);
|
|
7225
|
+
const bv = k.at(b);
|
|
7057
7226
|
if (av === bv || (av === undefined && bv === undefined)) continue;
|
|
7058
|
-
if (av === undefined || av === null) return
|
|
7059
|
-
if (bv === undefined || bv === null) return
|
|
7227
|
+
if (av === undefined || av === null) return -1 * k.dir;
|
|
7228
|
+
if (bv === undefined || bv === null) return 1 * k.dir;
|
|
7060
7229
|
// P2-1: localeCompare 对字符串排序受 locale 影响,应使用简单字符串比较
|
|
7061
7230
|
const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : (String(av) < String(bv) ? -1 : String(av) > String(bv) ? 1 : 0);
|
|
7062
|
-
if (r !== 0) return
|
|
7231
|
+
if (r !== 0) return r * k.dir;
|
|
7063
7232
|
}
|
|
7064
7233
|
return 0;
|
|
7065
7234
|
};
|
|
@@ -7158,16 +7327,42 @@ class SQLExecutor {
|
|
|
7158
7327
|
if (c.expr === '*') { for (const k of starKeys) cols.push(k.includes('.') ? k.slice(k.indexOf('.') + 1) : k); }
|
|
7159
7328
|
else cols.push(scalarColumnName(c));
|
|
7160
7329
|
}
|
|
7330
|
+
/*
|
|
7331
|
+
* 预先编译每列的取值函数。
|
|
7332
|
+
*
|
|
7333
|
+
* 此前循环里每次调用都新建 `{type:'column', name}` 对象字面量,
|
|
7334
|
+
* 10 万行 × N 列就是 10 万 × N 次白分配 —— 实测光 `SELECT id FROM t`
|
|
7335
|
+
* 就要 94ms,其中绝大部分是这个。
|
|
7336
|
+
*
|
|
7337
|
+
* 最常见的「普通列名」直接走属性访问,不再过 resolveOperand 的通用路径;
|
|
7338
|
+
* 取不到值才退回(兼容点号路径、限定引用等)。
|
|
7339
|
+
*/
|
|
7340
|
+
const starIdx = [];
|
|
7341
|
+
const colGetters = statement.columns.map((c, ci) => {
|
|
7342
|
+
if (c.expr === '*') { starIdx.push(ci); return null; }
|
|
7343
|
+
if (c.over || (c.scalar && c.scalar.over)) return (r) => windowColumnValue(c, r);
|
|
7344
|
+
if (c.scalar) return (r) => projectScalar(resolveOperand(c.scalar, r, this.ctx));
|
|
7345
|
+
if (c.literal !== undefined) return () => c.literal;
|
|
7346
|
+
if (c.caseExpr) return (r) => projectScalar(evaluateCaseVal(c.caseExpr, r));
|
|
7347
|
+
if (typeof c.expr === 'string') {
|
|
7348
|
+
const f = c.expr;
|
|
7349
|
+
const node = { type: 'column', name: f };
|
|
7350
|
+
return (r) => {
|
|
7351
|
+
const v = (r == null) ? undefined : r[f];
|
|
7352
|
+
return v === undefined ? projectScalar(resolveOperand(node, r, this.ctx)) : projectScalar(v);
|
|
7353
|
+
};
|
|
7354
|
+
}
|
|
7355
|
+
const node2 = { type: 'column', name: c.expr };
|
|
7356
|
+
return (r) => projectScalar(resolveOperand(node2, r, this.ctx));
|
|
7357
|
+
});
|
|
7358
|
+
|
|
7161
7359
|
const mapped = await this._projectRows(statement, rows, (r) => {
|
|
7162
7360
|
const out = [];
|
|
7163
|
-
for (
|
|
7164
|
-
|
|
7361
|
+
for (let ci = 0; ci < colGetters.length; ci++) {
|
|
7362
|
+
const g = colGetters[ci];
|
|
7363
|
+
if (g === null) {
|
|
7165
7364
|
for (const k of starKeys) out.push(r[k] !== undefined ? r[k] : r[String(k).slice(String(k).indexOf('.') + 1)]);
|
|
7166
|
-
} else
|
|
7167
|
-
else if (c.scalar) out.push(projectScalar(resolveOperand(c.scalar, r, this.ctx)));
|
|
7168
|
-
else if (c.literal !== undefined) out.push(c.literal);
|
|
7169
|
-
else if (c.caseExpr) out.push(projectScalar(evaluateCaseVal(c.caseExpr, r)));
|
|
7170
|
-
else out.push(projectScalar(resolveOperand({ type: 'column', name: c.expr }, r, this.ctx)));
|
|
7365
|
+
} else out.push(g(r));
|
|
7171
7366
|
}
|
|
7172
7367
|
return out;
|
|
7173
7368
|
});
|
package/lib/table.js
CHANGED
|
@@ -9,6 +9,19 @@ const BTree = require('./btree');
|
|
|
9
9
|
const { validateDateType, now } = require('./date-types');
|
|
10
10
|
const { createError } = require('./errors');
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 会被 _compileMatcher 编译成快路径的操作符。
|
|
14
|
+
*
|
|
15
|
+
* 收录标准:语义简单、求值廉价,且能在编译期确定目标值。
|
|
16
|
+
* 不在表内的操作符($inSub / $elemMatch / $textSearch / $expr / 作为值级
|
|
17
|
+
* 逻辑的 $and $or $not …)走退回路径,逐字段交给 _matchOperator,语义不变。
|
|
18
|
+
*/
|
|
19
|
+
const FAST_MATCH_OPS = new Set([
|
|
20
|
+
'$eq', '$ne', '$gt', '$gte', '$lt', '$lte',
|
|
21
|
+
'$in', '$nin', '$between', '$exists', '$isNull',
|
|
22
|
+
'$startsWith', '$endsWith', '$contains', '$type', '$size',
|
|
23
|
+
]);
|
|
24
|
+
|
|
12
25
|
function isSafeRegex(pattern) {
|
|
13
26
|
if (typeof pattern !== 'string') return false;
|
|
14
27
|
if (pattern.length > 256) return false;
|
|
@@ -1020,14 +1033,179 @@ class Table {
|
|
|
1020
1033
|
return this._applyFilter(rows, query);
|
|
1021
1034
|
}
|
|
1022
1035
|
|
|
1036
|
+
/**
|
|
1037
|
+
* 把 filter 编译成一个谓词函数。
|
|
1038
|
+
*
|
|
1039
|
+
* 动机(实测数据,10 万行):
|
|
1040
|
+
* _matchRow 每行都要做 Object.entries(query)、Object.keys(condition)、
|
|
1041
|
+
* 以及一串 startswith 判断。这部分占单次全表扫描耗时的 **98%** ——
|
|
1042
|
+
* 点查 13.9ms/次,其中 13.6ms 是反射;同样语义的手写循环只要 0.28ms。
|
|
1043
|
+
* $like / $regex 更糟:正则每行每字段重编一次。
|
|
1044
|
+
*
|
|
1045
|
+
* 做法:编译一次,把字段、路径、操作符、正则全部预先解析好,
|
|
1046
|
+
* 循环里只剩取值和比较。语义与 _matchRow 逐一对应。
|
|
1047
|
+
*
|
|
1048
|
+
* 遇到冷门操作符($inSub / $elemMatch / $textSearch / $expr …)
|
|
1049
|
+
* 时**只把那一个字段**退回 _matchRow,其余字段仍走快路径 ——
|
|
1050
|
+
* 既保住语义,也不让混合查询整体退回。
|
|
1051
|
+
*/
|
|
1052
|
+
_compileMatcher(query) {
|
|
1053
|
+
const self = this;
|
|
1054
|
+
const parts = [];
|
|
1055
|
+
for (const [field, condition] of Object.entries(query)) {
|
|
1056
|
+
// --- 顶层逻辑操作符 ---
|
|
1057
|
+
if (field === '$or' || field === '$and') {
|
|
1058
|
+
if (!Array.isArray(condition)) { parts.push({ k: 0 }); continue; } // 原实现直接 return false
|
|
1059
|
+
parts.push({ k: field === '$or' ? 3 : 4, subs: condition.map(c => self._compileMatcher(c)) });
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (field === '$not') {
|
|
1063
|
+
// $not 的条件形态太多(对象/标量都合法),整体退回,保证逐字一致
|
|
1064
|
+
parts.push({ k: 7, q: { $not: condition } });
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// --- 字段路径:点号路径预先切好,循环里不再 split/includes ---
|
|
1069
|
+
const path = (field.indexOf('.') !== -1 && field[0] !== '$') ? field.split('.') : null;
|
|
1070
|
+
|
|
1071
|
+
const plainObj = condition !== null && typeof condition === 'object'
|
|
1072
|
+
&& !Array.isArray(condition) && !(condition instanceof RegExp);
|
|
1073
|
+
|
|
1074
|
+
if (!plainObj) {
|
|
1075
|
+
parts.push({ k: 1, path, field, target: condition }); // 等值
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const keys = Object.keys(condition);
|
|
1080
|
+
const isOp = keys.length > 0 && keys.every(k => k[0] === '$');
|
|
1081
|
+
if (!isOp) {
|
|
1082
|
+
// 嵌套对象匹配
|
|
1083
|
+
parts.push({ k: 5, path, field, sub: self._compileMatcher(condition) });
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// --- 操作符:能编译的编译,不能的整体退回这一字段 ---
|
|
1088
|
+
const ops = [];
|
|
1089
|
+
let ok = true;
|
|
1090
|
+
for (const [op, target] of Object.entries(condition)) {
|
|
1091
|
+
if (op === '$like' && typeof target === 'string') {
|
|
1092
|
+
const escaped = target
|
|
1093
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1094
|
+
.replace(/%/g, '.*')
|
|
1095
|
+
.replace(/_/g, '.');
|
|
1096
|
+
ops.push({ op, re: new RegExp('^' + escaped + '$', 'i') });
|
|
1097
|
+
} else if (op === '$regex' && !(target instanceof RegExp) && typeof target === 'string') {
|
|
1098
|
+
if (!isSafeRegex(target)) throw createError('ER_BAD_REGEX', String(target));
|
|
1099
|
+
ops.push({ op, re: new RegExp(target) });
|
|
1100
|
+
} else if (FAST_MATCH_OPS.has(op)) {
|
|
1101
|
+
ops.push({ op, target });
|
|
1102
|
+
} else {
|
|
1103
|
+
ok = false;
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (!ok) { parts.push({ k: 7, q: { [field]: condition } }); continue; }
|
|
1108
|
+
parts.push({ k: 2, path, field, ops });
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return function match(row) {
|
|
1112
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1113
|
+
const p = parts[i];
|
|
1114
|
+
switch (p.k) {
|
|
1115
|
+
case 1: { // 等值
|
|
1116
|
+
const v = p.path ? self._walk(row, p.path) : row[p.field];
|
|
1117
|
+
if (v !== p.target) return false;
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
case 2: { // 操作符
|
|
1121
|
+
const v = p.path ? self._walk(row, p.path) : row[p.field];
|
|
1122
|
+
const ops = p.ops;
|
|
1123
|
+
for (let j = 0; j < ops.length; j++) {
|
|
1124
|
+
const o = ops[j];
|
|
1125
|
+
const op = o.op;
|
|
1126
|
+
if (op === '$eq') { if (v !== o.target) return false; }
|
|
1127
|
+
else if (op === '$ne') { if (v === o.target) return false; }
|
|
1128
|
+
else if (op === '$gt') { if (!(v > o.target)) return false; }
|
|
1129
|
+
else if (op === '$gte') { if (!(v >= o.target)) return false; }
|
|
1130
|
+
else if (op === '$lt') { if (!(v < o.target)) return false; }
|
|
1131
|
+
else if (op === '$lte') { if (!(v <= o.target)) return false; }
|
|
1132
|
+
else if (op === '$in') { if (!Array.isArray(o.target) || o.target.indexOf(v) === -1) return false; }
|
|
1133
|
+
else if (op === '$nin') {
|
|
1134
|
+
// 非数组 target 时原实现返回 false(不匹配),不能当成通过
|
|
1135
|
+
if (!Array.isArray(o.target) || o.target.indexOf(v) !== -1) return false;
|
|
1136
|
+
}
|
|
1137
|
+
else if (op === '$between') {
|
|
1138
|
+
if (!Array.isArray(o.target) || o.target.length !== 2
|
|
1139
|
+
|| !(v >= o.target[0]) || !(v <= o.target[1])) return false;
|
|
1140
|
+
}
|
|
1141
|
+
else if (op === '$exists') { if (o.target ? v === undefined : v !== undefined) return false; }
|
|
1142
|
+
else if (op === '$isNull') {
|
|
1143
|
+
const isn = v === null || v === undefined;
|
|
1144
|
+
if (o.target ? !isn : isn) return false;
|
|
1145
|
+
}
|
|
1146
|
+
else if (op === '$startsWith') { if (typeof v !== 'string' || !v.startsWith(o.target)) return false; }
|
|
1147
|
+
else if (op === '$endsWith') { if (typeof v !== 'string' || !v.endsWith(o.target)) return false; }
|
|
1148
|
+
else if (op === '$contains') { if (!Array.isArray(v) || v.indexOf(o.target) === -1) return false; }
|
|
1149
|
+
else if (op === '$type') { if (typeof v !== o.target) return false; }
|
|
1150
|
+
else if (op === '$size') { if (!Array.isArray(v) || v.length !== o.target) return false; }
|
|
1151
|
+
else if (op === '$like') {
|
|
1152
|
+
if (typeof v !== 'string') return false;
|
|
1153
|
+
if (!o.re.test(v)) return false;
|
|
1154
|
+
}
|
|
1155
|
+
else if (op === '$regex') {
|
|
1156
|
+
if (o.re instanceof RegExp) { if (!o.re.test(String(v))) return false; }
|
|
1157
|
+
else if (!o.re.test(v)) return false;
|
|
1158
|
+
}
|
|
1159
|
+
else { if (!self._matchOperator(v, op, o.target)) return false; }
|
|
1160
|
+
}
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
case 3: { // $or
|
|
1164
|
+
let hit = false;
|
|
1165
|
+
for (let j = 0; j < p.subs.length; j++) { if (p.subs[j](row)) { hit = true; break; } }
|
|
1166
|
+
if (!hit) return false;
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
case 4: { // $and
|
|
1170
|
+
for (let j = 0; j < p.subs.length; j++) if (!p.subs[j](row)) return false;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
case 5: { // 嵌套对象
|
|
1174
|
+
const v = p.path ? self._walk(row, p.path) : row[p.field];
|
|
1175
|
+
if (v === null || typeof v !== 'object') return false;
|
|
1176
|
+
if (!p.sub(v)) return false;
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
case 7: { // 退回原有逐字段求值
|
|
1180
|
+
if (!self._matchRow(row, p.q)) return false;
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
default: return false; // k===0:原实现直接 return false
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return true;
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** 点号路径取值,与 _resolvePath 的分支语义一致 */
|
|
1191
|
+
_walk(row, parts) {
|
|
1192
|
+
let v = row;
|
|
1193
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1194
|
+
if (v === null || v === undefined) return undefined;
|
|
1195
|
+
v = v[parts[i]];
|
|
1196
|
+
}
|
|
1197
|
+
return v;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1023
1200
|
_applyFilter(rows, query) {
|
|
1201
|
+
const match = this._compileMatcher(query);
|
|
1202
|
+
const soft = this._softDelete;
|
|
1024
1203
|
const result = [];
|
|
1025
1204
|
for (let i = 0; i < rows.length; i++) {
|
|
1205
|
+
const r = rows[i];
|
|
1026
1206
|
// 软删除行不应出现在任何查询结果中
|
|
1027
|
-
if (
|
|
1028
|
-
if (
|
|
1029
|
-
result.push(rows[i]);
|
|
1030
|
-
}
|
|
1207
|
+
if (soft && r._deleted) continue;
|
|
1208
|
+
if (match(r)) result.push(r);
|
|
1031
1209
|
}
|
|
1032
1210
|
return result;
|
|
1033
1211
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.4",
|
|
4
4
|
"description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"test:btree": "node test/btree.test.js",
|
|
52
52
|
"test:native": "node test/native.test.js",
|
|
53
53
|
"test:regress": "node test/regress-5.1.0.js",
|
|
54
|
-
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js",
|
|
54
|
+
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js",
|
|
55
55
|
"test:coverage": "c8 --all --include='lib/**' --exclude='lib/web_ui.js' --reporter=text --reporter=lcov node test/coverage.js",
|
|
56
56
|
"test:orms": "node examples/orms/run-all.js",
|
|
57
57
|
"test:dml": "node test/dml-constraints.test.js",
|
|
@@ -61,7 +61,9 @@
|
|
|
61
61
|
"test:savepoint": "node test/savepoint.test.js",
|
|
62
62
|
"test:alter": "node test/alter-table.test.js",
|
|
63
63
|
"test:ident": "node test/identifier-keywords.test.js",
|
|
64
|
-
"test:null": "node test/null-semantics.test.js"
|
|
64
|
+
"test:null": "node test/null-semantics.test.js",
|
|
65
|
+
"test:semantics": "node test/sql-semantics.test.js",
|
|
66
|
+
"test:filter": "node test/filter-compile.test.js"
|
|
65
67
|
},
|
|
66
68
|
"keywords": [
|
|
67
69
|
"database",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* extra-coverage.test.js — JSQL-neo 边界情况全覆盖测试
|
|
3
3
|
*
|
|
4
|
-
* 运行:
|
|
4
|
+
* 运行:node test/extra-coverage.test.js
|
|
5
5
|
*
|
|
6
6
|
* 覆盖五大类边界情况:
|
|
7
7
|
* 1. SQL 语义边界(NULL、除零、精度、Unicode、日期、BETWEEN)
|
|
@@ -12,10 +12,21 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
|
-
import { test } from 'vitest';
|
|
16
15
|
import assert from 'assert';
|
|
17
16
|
import { createRequire } from 'module';
|
|
18
17
|
|
|
18
|
+
/*
|
|
19
|
+
* 原先这里是 `import { test } from 'vitest'`。但 vitest 既不在 package.json 的
|
|
20
|
+
* 依赖里、也没安装,而这个文件又不在 test:all 里 —— 结果是这 68 个用例
|
|
21
|
+
* **从来没跑过一次**,纯摆设。死测试比没有测试更糟:它给人虚假的信心。
|
|
22
|
+
*
|
|
23
|
+
* 好在断言本来就全用 Node 内置的 assert,需要替换的只有 test 这一层。
|
|
24
|
+
* 下面这个极简 runner 行为对齐 vitest:顺序执行、逐个报结果、
|
|
25
|
+
* 有失败就以非零码退出。
|
|
26
|
+
*/
|
|
27
|
+
const _tests = [];
|
|
28
|
+
const test = (name, fn) => { _tests.push({ name, fn }); };
|
|
29
|
+
|
|
19
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
31
|
const __dirname = path.dirname(__filename);
|
|
21
32
|
const require = createRequire(import.meta.url);
|
|
@@ -68,9 +79,15 @@ test('NULL NOT IN (1,2) 返回 NULL(UNKNOWN)', async () => {
|
|
|
68
79
|
assert.strictEqual(await one(q, 'SELECT NULL NOT IN (1,2)'), null);
|
|
69
80
|
});
|
|
70
81
|
|
|
71
|
-
test('1 IN (1,NULL) 返回
|
|
82
|
+
test('1 IN (1,NULL) 返回 TRUE(有一个元素匹配就成立)', async () => {
|
|
72
83
|
const { q } = makeDb();
|
|
73
|
-
|
|
84
|
+
// 等价于 `1=1 OR 1=NULL` —— TRUE OR UNKNOWN 还是 TRUE。
|
|
85
|
+
// 列表里别的位置有没有 NULL 不影响;只有**没匹配上**时才是 UNKNOWN。
|
|
86
|
+
// (原先这里断言的是 null,即「列表含 NULL 就 UNKNOWN」——那是个常见误解,
|
|
87
|
+
// MySQL / PostgreSQL 实测都返回 TRUE。)
|
|
88
|
+
assert.strictEqual(await one(q, 'SELECT 1 IN (1,NULL)'), 1);
|
|
89
|
+
assert.strictEqual(await one(q, 'SELECT 1 IN (NULL,1)'), 1);
|
|
90
|
+
assert.strictEqual(await one(q, 'SELECT 99 IN (1,NULL)'), null);
|
|
74
91
|
});
|
|
75
92
|
|
|
76
93
|
test('3 NOT IN (1,2) 返回 1(正常匹配)', async () => {
|
|
@@ -363,12 +380,20 @@ test('IN 子查询', async () => {
|
|
|
363
380
|
);
|
|
364
381
|
});
|
|
365
382
|
|
|
366
|
-
test('IN 子查询含 NULL
|
|
383
|
+
test('IN 子查询含 NULL:匹配上的行仍应返回', async () => {
|
|
367
384
|
const { q } = makeDb();
|
|
368
385
|
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
369
386
|
await q('INSERT INTO t VALUES (1,1),(2,NULL)');
|
|
387
|
+
// 子查询结果是 {1, NULL}。g=1 那行匹配得上(1 IN (1,NULL) 为 TRUE),
|
|
388
|
+
// 只有 g=NULL 那行是 UNKNOWN 被过滤 —— 所以是 1 行,不是 0 行。
|
|
389
|
+
// (原先断言 0 行,把 bug 当成了预期。)
|
|
370
390
|
assert.strictEqual(
|
|
371
391
|
await one(q, 'SELECT COUNT(*) FROM t WHERE g IN (SELECT g FROM t)'),
|
|
392
|
+
1
|
|
393
|
+
);
|
|
394
|
+
// 反过来:NOT IN 的列表含 NULL 时恒为 UNKNOWN,一行都不返回
|
|
395
|
+
assert.strictEqual(
|
|
396
|
+
await one(q, 'SELECT COUNT(*) FROM t WHERE g NOT IN (SELECT g FROM t)'),
|
|
372
397
|
0
|
|
373
398
|
);
|
|
374
399
|
});
|
|
@@ -567,3 +592,27 @@ test('IF 函数三值行为(NULL / 0 / 空串均为假)', async () => {
|
|
|
567
592
|
assert.strictEqual(await one(q, "SELECT IF('', 'a', 'b')"), 'b');
|
|
568
593
|
assert.strictEqual(await one(q, "SELECT IF(1, 'a', 'b')"), 'a');
|
|
569
594
|
});
|
|
595
|
+
|
|
596
|
+
// ---- runner:顺序执行全部用例,按 vitest 的习惯输出与退出码 ----
|
|
597
|
+
;(async () => {
|
|
598
|
+
let pass = 0, fail = 0;
|
|
599
|
+
const failures = [];
|
|
600
|
+
for (const t of _tests) {
|
|
601
|
+
try {
|
|
602
|
+
await t.fn();
|
|
603
|
+
pass++;
|
|
604
|
+
console.log('[OK] ', t.name);
|
|
605
|
+
} catch (e) {
|
|
606
|
+
fail++;
|
|
607
|
+
failures.push(t.name);
|
|
608
|
+
const msg = e && e.message ? e.message : String(e);
|
|
609
|
+
console.log('[FAIL]', t.name, '->', msg.slice(0, 130));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
613
|
+
if (failures.length) {
|
|
614
|
+
console.log('\n失败用例:');
|
|
615
|
+
for (const n of failures) console.log(' - ' + n);
|
|
616
|
+
}
|
|
617
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
618
|
+
})();
|
|
@@ -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); });
|