jsql-neo 6.4.2 → 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.
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.2** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.4.3** — 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
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.4.2 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.3 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
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;
@@ -2986,6 +3020,14 @@ function resolveOperand(operand, row, ctx) {
2986
3020
  if (typeof base === 'string') {
2987
3021
  try { base = JSON.parse(base); } catch (e) { return null; }
2988
3022
  }
3023
+ // '$' 开头是 JSONPath(MySQL 5.7+ 的写法,`d->>'$.name'`、`d->>'$.a.b'`)。
3024
+ // 此前把它当字面键名去取(v['$.name']),于是文档里那条示例永远返回 null。
3025
+ if (!operand.isPath && typeof operand.key === 'string' && operand.key.trim().charAt(0) === '$') {
3026
+ const jv = jsonPathGet(base, operand.key.trim());
3027
+ if (jv === null || jv === undefined) return null;
3028
+ if (operand.asText) return typeof jv === 'object' ? JSON.stringify(jv) : String(jv);
3029
+ return jv;
3030
+ }
2989
3031
  const keys = operand.isPath ? parsePgPath(operand.key) : [operand.key];
2990
3032
  let v = base;
2991
3033
  for (const key of keys) {
@@ -3607,6 +3649,55 @@ function applyScalarFunction(fnNode, row, ctx) {
3607
3649
  return idx + 1;
3608
3650
  }
3609
3651
  case 'REVERSE': return args[0] == null ? null : String(args[0]).split('').reverse().join('');
3652
+
3653
+ // ---- 以下这批此前没有实现:调用时静默返回 null(不报错),
3654
+ // 只有拿标准答案对照才发现。语义按 MySQL 8.0。
3655
+ case 'ASCII': {
3656
+ // 最左字符的数值;空串返回 0,NULL 返回 NULL
3657
+ if (args[0] == null) return null;
3658
+ const s = String(args[0]);
3659
+ return s.length ? s.charCodeAt(0) : 0;
3660
+ }
3661
+ case 'ORD': {
3662
+ if (args[0] == null) return null;
3663
+ const s = String(args[0]);
3664
+ return s.length ? s.codePointAt(0) : 0;
3665
+ }
3666
+ case 'CHAR': {
3667
+ // CHAR(65,66) → 'AB';跳过 NULL 参数
3668
+ const codes = args.filter(a => a != null).map(a => Number(a));
3669
+ if (!codes.length) return '';
3670
+ return codes.map(n => String.fromCharCode(n)).join('');
3671
+ }
3672
+ case 'SPACE': {
3673
+ if (args[0] == null) return null;
3674
+ const n = Math.max(0, Math.floor(Number(args[0])) || 0);
3675
+ return ' '.repeat(n);
3676
+ }
3677
+ case 'FIELD': {
3678
+ // FIELD(x, a, b, ...) → x 在列表中的 1 基下标;找不到返回 0;x 为 NULL 返回 NULL
3679
+ if (args[0] == null) return null;
3680
+ const needle = String(args[0]);
3681
+ for (let i = 1; i < args.length; i++) {
3682
+ if (args[i] != null && String(args[i]) === needle) return i;
3683
+ }
3684
+ return 0;
3685
+ }
3686
+ case 'ELT': {
3687
+ // ELT(n, a, b, ...) → 第 n 个;越界或 n 为 NULL 返回 NULL
3688
+ if (args[0] == null) return null;
3689
+ const idx = Math.floor(Number(args[0]));
3690
+ if (!Number.isFinite(idx) || idx < 1 || idx >= args.length) return null;
3691
+ return args[idx] == null ? null : String(args[idx]);
3692
+ }
3693
+ case 'JSON_ARRAY_LENGTH': {
3694
+ if (args[0] == null) return null;
3695
+ let v = args[0];
3696
+ if (typeof v === 'string') {
3697
+ try { v = JSON.parse(v); } catch (e) { return null; }
3698
+ }
3699
+ return Array.isArray(v) ? v.length : null;
3700
+ }
3610
3701
  case 'LPAD': {
3611
3702
  if (args[0] == null) return null;
3612
3703
  let s = String(args[0]);
@@ -3685,34 +3776,7 @@ function applyScalarFunction(fnNode, row, ctx) {
3685
3776
  if (typeof json === 'string') {
3686
3777
  try { data = JSON.parse(json); } catch (e) { return null; }
3687
3778
  }
3688
- // 解析 JSONPath: $.key 或 $.a.b 或 $.a[0].b
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;
3779
+ return jsonPathGet(data, path);
3716
3780
  }
3717
3781
  case 'JSON_KEYS': {
3718
3782
  // JSON_KEYS(json) → [key1, key2, ...]
@@ -4255,11 +4319,22 @@ function evaluateExpr(expr, row, ctx) {
4255
4319
  if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
4256
4320
  if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
4257
4321
  if (!expr.list) return expr.not ? true : false;
4258
- // 列表中出现 NULL 时:三值逻辑 → NULL IN (1,2,NULL) 为 NULL(UNKNOWN)
4322
+
4323
+ // 先找匹配:只要有一个非 NULL 元素等于 v,整个 IN 就是 TRUE ——
4324
+ // 列表里别的位置有没有 NULL 都不影响。
4325
+ //
4326
+ // 此前是先判「列表里有 NULL 就返回 NULL」,于是 `v IN (10, NULL)`
4327
+ // 连 v=10 的行都匹配不上(实测返回 0 行,应为 2 行)。
4328
+ // 三值逻辑里 NULL 只在**没匹配上**的时候才让结果变 UNKNOWN。
4329
+ const matched = expr.list.some(x =>
4330
+ x !== null && x !== undefined && (x === v || String(x) === String(v))
4331
+ );
4332
+ if (matched) return expr.not ? false : true;
4333
+
4334
+ // 没匹配上:列表里有 NULL → UNKNOWN,否则是确定的 FALSE
4259
4335
  const hasNull = expr.list.some(x => x === null || x === undefined);
4260
4336
  if (hasNull) return null;
4261
- const r = expr.list.some(x => x === v || String(x) === String(v));
4262
- return expr.not ? !r : r;
4337
+ return expr.not ? true : false;
4263
4338
  }
4264
4339
  case 'like': {
4265
4340
  const v = resolveOperand(expr.operand, row, ctx);
@@ -6485,7 +6560,26 @@ class SQLExecutor {
6485
6560
  // 帧范围(ROWS 帧;RANGE 在当前实现下退化为整个分区,保守处理)
6486
6561
  const frameIdx = (i) => {
6487
6562
  const f = w.over.frame;
6488
- if (!f) return { start: 0, end: ordered.length - 1 };
6563
+ if (!f) {
6564
+ // 没显式写帧时的默认帧,按 SQL 标准分两种情况:
6565
+ // 有 ORDER BY → RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
6566
+ // 无 ORDER BY → 整个分区
6567
+ //
6568
+ // 此前一律返回整个分区,于是
6569
+ // SUM(sal) OVER (PARTITION BY dept ORDER BY id)
6570
+ // 得到的是分区总和(500)而不是累计值(100/300/500)。
6571
+ //
6572
+ // 用 RANGE 而非 ROWS 语义:ORDER BY 值相同的行互为 peer,
6573
+ // 要一并纳入当前行(这正是 RANGE 与 ROWS 的区别)。
6574
+ if (!orderBy.length) return { start: 0, end: ordered.length - 1 };
6575
+ const curKey = this._windowPeerKey(rows[ordered[i]], orderBy, this.ctx);
6576
+ let end = i;
6577
+ while (end + 1 < ordered.length &&
6578
+ this._windowPeerKey(rows[ordered[end + 1]], orderBy, this.ctx) === curKey) {
6579
+ end++;
6580
+ }
6581
+ return { start: 0, end };
6582
+ }
6489
6583
  let start = 0, end = ordered.length - 1;
6490
6584
  const apply = (b, isStart) => {
6491
6585
  if (!b) return isStart ? 0 : ordered.length - 1;
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 (this._softDelete && rows[i]._deleted) continue;
1028
- if (this._matchRow(rows[i], query)) {
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.2",
3
+ "version": "6.4.3",
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
- * 运行:npx vitest run test/extra-coverage.test.js
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) 返回 NULL(列表含 NULL → UNKNOWN)', async () => {
82
+ test('1 IN (1,NULL) 返回 TRUE(有一个元素匹配就成立)', async () => {
72
83
  const { q } = makeDb();
73
- assert.strictEqual(await one(q, 'SELECT 1 IN (1,NULL)'), null);
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 导致 UNKNOWN(返回 0 行)', async () => {
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); });