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.
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.1** — 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.1 — 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/database.js CHANGED
@@ -666,7 +666,12 @@ class Database {
666
666
  // 重命名也要能回放:否则崩溃恢复后表名会退回去,
667
667
  // 而后续的 insert/update 记录全都打在新名字上,全部丢失。
668
668
  if (op.from && op.to && this._tables[op.from] && !this._tables[op.to]) {
669
- this.renameTable(op.from, op.to);
669
+ // renameTable 是 async 的(内部 await _runHooks),而本回放路径
670
+ // 是同步的,没法 await。**注意不能把 promise 直接 return** ——
671
+ // `if (promise)` 恒为真,失败会被当成成功。
672
+ // 这里同步发起并接住 rejection,避免未处理的 Promise 拒绝
673
+ // 把进程打崩;真失败了会体现在后续按新表名的记录上。
674
+ Promise.resolve(this.renameTable(op.from, op.to)).catch(() => {});
670
675
  }
671
676
  return true;
672
677
 
@@ -490,7 +490,10 @@ class JSQL {
490
490
  }
491
491
 
492
492
  async _insertBatch(table, rows) {
493
+ // 直接传 JSON 字符串,让 Rust 处理 schema 映射和插入
494
+ // 消除 JS encodeBatch 二进制序列化和 Rust 解码的开销
493
495
  const schema = this._schemas[table];
496
+ // 对 JSON/array/object 列保留 safeStringify 以给出可读错误(环形对象等)
494
497
  if (schema) {
495
498
  rows = rows.map(row => {
496
499
  const out = {};
@@ -510,8 +513,17 @@ class JSQL {
510
513
  return out;
511
514
  });
512
515
  }
513
- const bin = encodeBatch(rows);
514
- const r = safeParse(this._n('jsqlInsertBuf', table, bin));
516
+ const rawJson = JSON.stringify(rows);
517
+ // 优先用 jsqlInsertRaw(Rust 直接 JSON 解析,消除二进制序列化)
518
+ // 旧环境未编译 jsqlInsertRaw 时 fallback 到 jsqlInsertBuf
519
+ let r;
520
+ if (typeof native.jsqlInsertRaw === 'function') {
521
+ r = safeParse(this._n('jsqlInsertRaw', table, rawJson));
522
+ } else {
523
+ // fallback: 用 encodeBatch 二进制格式(向后兼容)
524
+ const bin = encodeBatch(rows);
525
+ r = safeParse(this._n('jsqlInsertBuf', table, bin));
526
+ }
515
527
  if (r && r.error) throw new Error(r.error);
516
528
  if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
517
529
  const pk = this._pkOf(table);
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);
@@ -4579,7 +4654,10 @@ class SQLExecutor {
4579
4654
  if (typeof this.engine.renameTable !== 'function') {
4580
4655
  throw new Error(`ALTER TABLE ... RENAME is not supported by this engine (no renameTable method)`);
4581
4656
  }
4582
- this.engine.renameTable(statement.name, op.newName);
4657
+ // renameTable 是 async 的(它要 await _runHooks),漏了 await 会让
4658
+ // 它抛出的错变成未处理的 Promise 拒绝:SQL 这边照常返回 ok:true,
4659
+ // 调用方以为改名成功,而进程随后被 rejection 打崩。
4660
+ await this.engine.renameTable(statement.name, op.newName);
4583
4661
  table = this.engine._tables[op.newName] || table;
4584
4662
  statement.name = op.newName;
4585
4663
  break;
@@ -6482,7 +6560,26 @@ class SQLExecutor {
6482
6560
  // 帧范围(ROWS 帧;RANGE 在当前实现下退化为整个分区,保守处理)
6483
6561
  const frameIdx = (i) => {
6484
6562
  const f = w.over.frame;
6485
- 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
+ }
6486
6583
  let start = 0, end = ordered.length - 1;
6487
6584
  const apply = (b, isStart) => {
6488
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
  }
@@ -481,6 +481,7 @@ class JSQL {
481
481
  }
482
482
 
483
483
  async _insertBatch(table, rows) {
484
+ // schema-aware 转换:JSON 列用 safeStringify 给出可读错误(环形对象等)
484
485
  const schema = this._schemas[table];
485
486
  if (schema) {
486
487
  rows = rows.map(row => {
@@ -501,7 +502,8 @@ class JSQL {
501
502
  return out;
502
503
  });
503
504
  }
504
- const r = safeJsonParse(wasmBindings.jsql_insert_json(table, safeStringify(rows, table, '<rows>')));
505
+ const rawJson = JSON.stringify(rows);
506
+ const r = safeJsonParse(wasmBindings.jsql_insert_json(table, rawJson));
505
507
  if (r && r.error) throw new Error(r.error);
506
508
  if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
507
509
  const pk = this._pkOf(table);
Binary file
@@ -41,6 +41,15 @@ pub trait Engine {
41
41
  fn list_tables(&self) -> Vec<String>;
42
42
 
43
43
  fn insert(&mut self, table: &str, fields: Vec<HashMap<String, serde_json::Value>>) -> Result<Vec<u64>, String>;
44
+ /// 接收原始 JSON 数组字符串,直接解析并存入(消除 JS encodeBatch 二进制序列化开销)
45
+ fn insert_raw(&mut self, table: &str, raw_json: &str) -> Result<Vec<u64>, String> {
46
+ let rows: Vec<serde_json::Value> = serde_json::from_str(raw_json)
47
+ .map_err(|e| format!("invalid JSON array: {}", e))?;
48
+ let batch: Vec<HashMap<String, serde_json::Value>> = rows.into_iter()
49
+ .map(|v| v.as_object().map(|m| m.clone().into_iter().collect()).unwrap_or_else(HashMap::new))
50
+ .collect();
51
+ self.insert(table, batch)
52
+ }
44
53
  fn find_by_id(&self, table: &str, id: u64) -> Result<Option<Row>, String>;
45
54
  fn find_by_id_json(&self, table: &str, id: u64) -> Result<Option<String>, String>;
46
55
  fn find_by_ids_json(&self, table: &str, ids: &[u64]) -> Result<String, String>;
@@ -274,6 +274,23 @@ fn do_insert_buf(eng: &mut HybridEngine, table: &str, buf_val: napi::JsBufferVal
274
274
  safe_json_result(res)
275
275
  }
276
276
 
277
+ #[napi]
278
+ pub fn jsql_insert_raw(table: String, raw_json: String) -> String {
279
+ with_engine_mut(|eng| do_insert_raw(eng, &table, &raw_json))
280
+ }
281
+
282
+ #[napi]
283
+ pub fn jsql_insert_raw_h(handle: u32, table: String, raw_json: String) -> String {
284
+ with_engine_handle_mut(handle, |eng| do_insert_raw(eng, &table, &raw_json))
285
+ }
286
+
287
+ fn do_insert_raw(eng: &mut HybridEngine, table: &str, raw_json: &str) -> String {
288
+ match eng.insert_raw(table, raw_json) {
289
+ Ok(ids) => serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string()),
290
+ Err(e) => format!(r#"{{"error":"{}"}}"#, e.replace('"', r#"\""#)),
291
+ }
292
+ }
293
+
277
294
  napi_engine_fn!(jsql_find, jsql_find_h,
278
295
  (table: String, filter_json: String, limit: i32, offset: i32) => |eng| {
279
296
  let filter: Option<HashMap<String, serde_json::Value>> =
@@ -76,96 +76,18 @@ pub fn jsql_insert(table: &str, data_json: &str) -> String {
76
76
  #[wasm_bindgen]
77
77
  pub fn jsql_insert_json(table: &str, json: &str) -> String {
78
78
  ENGINE.with(|eng| {
79
- let res = (|| -> Result<Vec<u64>, String> {
80
- let mut eng = eng.borrow_mut();
81
- let t = eng.tables.get_mut(table).ok_or_else(|| format!("table '{}' not found", table))?;
82
-
83
- let data: Vec<serde_json::Value> = serde_json::from_str(json)
84
- .map_err(|e| format!("invalid JSON: {}", e))?;
85
- let n = data.len();
86
- if n == 0 { return Ok(Vec::new()); }
87
-
88
- let first = data[0].as_object()
89
- .ok_or_else(|| "expected array of objects".to_string())?;
90
- let field_names: Vec<String> = first.keys().cloned().collect();
91
-
92
- let remap: Vec<Option<usize>> = t.field_order.iter()
93
- .map(|name| field_names.iter().position(|n| n == name))
94
- .collect();
95
-
96
- let pk_idx = t.pk_field.as_ref().and_then(|pk| {
97
- t.schema.get(pk.as_str()).and_then(|fs| {
98
- if fs.auto_increment { t.field_index.get(pk.as_str()).copied() } else { None }
99
- })
100
- });
101
-
102
- let now = js_sys::Date::now() as u64;
103
- let start_id = t.next_id;
104
- t.next_id += n as u64;
105
- t.pk_index.reserve(n);
106
- t.rows.reserve(n);
107
- t.values.reserve(t.field_order.len());
108
- let start_ri = t.rows.len();
109
-
110
- for (ri, row_val) in data.iter().enumerate() {
111
- let auto_id = start_id + ri as u64;
112
- let mut id = auto_id;
113
- let obj = row_val.as_object()
114
- .ok_or_else(|| "non-object in array".to_string())?;
115
-
116
- let mut values = Vec::with_capacity(t.field_order.len());
117
- for (i, rp) in remap.iter().enumerate() {
118
- if let Some(fi) = rp {
119
- let key = &field_names[*fi];
120
- if let Some(jv) = obj.get(key) {
121
- values.push(FieldValue::from_json(jv.clone()));
122
- continue;
123
- }
124
- } else if let Some(pi) = pk_idx {
125
- if pi == i {
126
- values.push(FieldValue::Int(id as i64));
127
- continue;
128
- }
129
- }
130
- if let Some(name) = t.field_order.get(i) {
131
- if let Some(fs) = t.schema.get(name) {
132
- if let Some(ref default) = fs.default {
133
- values.push(FieldValue::from_json(default.clone()));
134
- continue;
135
- }
136
- if !fs.nullable {
137
- values.push(default_value(&fs.field_type));
138
- continue;
139
- }
140
- }
141
- }
142
- values.push(FieldValue::Null);
143
- }
144
-
145
- // auto-increment PK: keep explicit positive value, else fill with auto id
146
- if let Some(pi) = pk_idx {
147
- if let Some(v) = values.get_mut(pi) {
148
- match v {
149
- FieldValue::Null => *v = FieldValue::Int(id as i64),
150
- FieldValue::Int(e) if *e > 0 => {
151
- id = *e as u64;
152
- t.next_id = t.next_id.max(id + 1);
153
- }
154
- _ => {}
155
- }
156
- }
157
- }
158
-
159
- t.pk_index.insert(id, start_ri + ri);
160
- let vs = t.values.len();
161
- t.values.extend(values);
162
- t.rows.push(RowStore { id, values_start: vs, num_values: t.field_order.len(), created_at: now, updated_at: now });
163
- }
79
+ match eng.borrow_mut().insert_raw(table, json) {
80
+ Ok(ids) => serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string()),
81
+ Err(e) => format!(r#"{{"error":"{}"}}"#, e),
82
+ }
83
+ })
84
+ }
164
85
 
165
- let ids: Vec<u64> = (start_id..start_id + n as u64).collect();
166
- Ok(ids)
167
- })();
168
- match res {
86
+ /// JSON 字符串直接插入(绕过 JS encodeBatch 二进制序列化)
87
+ #[wasm_bindgen]
88
+ pub fn jsql_insert_raw(table: &str, raw_json: &str) -> String {
89
+ ENGINE.with(|eng| {
90
+ match eng.borrow_mut().insert_raw(table, raw_json) {
169
91
  Ok(ids) => serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string()),
170
92
  Err(e) => format!(r#"{{"error":"{}"}}"#, e),
171
93
  }