jsql-neo 6.5.8 → 6.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.5.8** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.5.10** — stable release · [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)
@@ -188,3 +188,6 @@ Apache-2.0 License — contributions welcome!
188
188
  - **GitHub:** [vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
189
189
  - **Issues:** [Open an issue](https://github.com/vexify-org/JSQL-neo/issues)
190
190
  - **PRs:** Fork → branch → PR — thank you!
191
+ - **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
192
+ - **Code of Conduct:** [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
193
+ - **Security:** [SECURITY.md](SECURITY.md) — please report vulnerabilities privately
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.8 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.10 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/btree.js CHANGED
@@ -173,7 +173,7 @@ class BTree {
173
173
  const s = k === undefined || k === null ? '' : String(k);
174
174
  // 不能假设键按字典序排序(数字键按数值序),必须检查所有键
175
175
  if (s.startsWith(p)) {
176
- result.push(...node.values[i]);
176
+ for (const __e of node.values[i]) result.push(__e);
177
177
  }
178
178
  }
179
179
  node = node.next;
@@ -193,7 +193,7 @@ class BTree {
193
193
  const k = node.keys[i];
194
194
  if (k > max) return result;
195
195
  if (k >= min) {
196
- result.push(...node.values[i]);
196
+ for (const __e of node.values[i]) result.push(__e);
197
197
  }
198
198
  }
199
199
  node = node.next;
@@ -209,7 +209,7 @@ class BTree {
209
209
  let node = this._findLeaf(min);
210
210
  while (node) {
211
211
  for (let i = 0; i < node.keys.length; i++) {
212
- if (node.keys[i] > min) result.push(...node.values[i]);
212
+ if (node.keys[i] > min) for (const __e of node.values[i]) result.push(__e);
213
213
  }
214
214
  node = node.next;
215
215
  }
@@ -221,7 +221,7 @@ class BTree {
221
221
  let node = this._findLeaf(min);
222
222
  while (node) {
223
223
  for (let i = 0; i < node.keys.length; i++) {
224
- if (node.keys[i] >= min) result.push(...node.values[i]);
224
+ if (node.keys[i] >= min) for (const __e of node.values[i]) result.push(__e);
225
225
  }
226
226
  node = node.next;
227
227
  }
@@ -237,7 +237,7 @@ class BTree {
237
237
  while (!node.leaf) node = node.children[0];
238
238
  while (node) {
239
239
  for (let i = 0; i < node.keys.length; i++) {
240
- if (node.keys[i] < max) result.push(...node.values[i]);
240
+ if (node.keys[i] < max) for (const __e of node.values[i]) result.push(__e);
241
241
  else return result;
242
242
  }
243
243
  node = node.next;
@@ -251,7 +251,7 @@ class BTree {
251
251
  while (!node.leaf) node = node.children[0];
252
252
  while (node) {
253
253
  for (let i = 0; i < node.keys.length; i++) {
254
- if (node.keys[i] <= max) result.push(...node.values[i]);
254
+ if (node.keys[i] <= max) for (const __e of node.values[i]) result.push(__e);
255
255
  else return result;
256
256
  }
257
257
  node = node.next;
@@ -408,9 +408,9 @@ class BTree {
408
408
  left.keys.push(midKey);
409
409
  left.values.push(midVal);
410
410
  }
411
- left.keys.push(...right.keys);
412
- left.values.push(...right.values);
413
- if (!left.leaf) left.children.push(...right.children);
411
+ for (const __e of right.keys) left.keys.push(__e);
412
+ for (const __e of right.values) left.values.push(__e);
413
+ if (!left.leaf) for (const __e of right.children) left.children.push(__e);
414
414
  if (left.leaf) left.next = right.next;
415
415
 
416
416
  parent.keys.splice(index, 1);
package/lib/cache.js CHANGED
@@ -152,7 +152,7 @@ class Cache {
152
152
  throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
153
153
  }
154
154
  if (!list) { list = []; this._getData().set(k, list); }
155
- list.push(...values.map(String));
155
+ for (const __e of values.map(String)) list.push(__e);
156
156
  this._dirty = true;
157
157
  return list.length;
158
158
  }
package/lib/database.js CHANGED
@@ -566,7 +566,7 @@ class Database {
566
566
  case 'insert': {
567
567
  const t = op.table && this._tables[op.table];
568
568
  if (!t || !Array.isArray(op.rows) || op.rows.length === 0) { failed++; break; }
569
- t._rows.push(...op.rows);
569
+ for (const __e of op.rows) t._rows.push(__e);
570
570
  touched.add(t);
571
571
  replayed++;
572
572
  break;
@@ -510,7 +510,7 @@ class JSQLFormat {
510
510
  t.schema = schema;
511
511
 
512
512
  const { rows: oldRows } = this.readTable(name);
513
- oldRows.push(...rows);
513
+ for (const __e of rows) oldRows.push(__e);
514
514
  this.writeTable(name, schema, oldRows, pkField);
515
515
  }
516
516
  }
@@ -647,10 +647,14 @@ class MongoServer {
647
647
  doc[k] = vals.reduce((a, b) => a + b, 0) / vals.length;
648
648
  } else if (v.$min !== undefined) {
649
649
  const vals = grow.map(r => r[field]).filter(v => v != null);
650
- doc[k] = vals.length ? Math.min(...vals) : null;
650
+ let best = null;
651
+ for (const x of vals) if (best === null || x < best) best = x;
652
+ doc[k] = best;
651
653
  } else if (v.$max !== undefined) {
652
654
  const vals = grow.map(r => r[field]).filter(v => v != null);
653
- doc[k] = vals.length ? Math.max(...vals) : null;
655
+ let best = null;
656
+ for (const x of vals) if (best === null || x > best) best = x;
657
+ doc[k] = best;
654
658
  } else if (v.$first !== undefined) {
655
659
  doc[k] = grow[0] ? grow[0][field] : null;
656
660
  } else if (v.$last !== undefined) {
@@ -851,7 +851,7 @@ class JSQL {
851
851
  }
852
852
  if (!(await this._runHooks('beforeUpdate', [table, pairs]))) return;
853
853
  if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
854
- this._opBuffer.update[table].push(...pairs);
854
+ for (const __e of pairs) this._opBuffer.update[table].push(__e);
855
855
  this._scheduleOpFlush();
856
856
  }
857
857
 
@@ -170,7 +170,7 @@ function applyModifier(doc, update) {
170
170
  for (const [k, v] of Object.entries(payload)) {
171
171
  const cur = getPath(doc, k);
172
172
  const arr = Array.isArray(cur) ? cur : [];
173
- if (v && typeof v === 'object' && v.$each) arr.push(...v.$each);
173
+ if (v && typeof v === 'object' && v.$each) for (const __e of v.$each) arr.push(__e);
174
174
  else arr.push(v);
175
175
  setPath(doc, k, arr);
176
176
  }
package/lib/query.js CHANGED
@@ -706,7 +706,7 @@ class Query {
706
706
  const result = [];
707
707
  for (const group of Object.values(groups)) {
708
708
  this._applyWindowToGroup(group, fn, field, dir);
709
- result.push(...group);
709
+ for (const __e of group) result.push(__e);
710
710
  }
711
711
  return result;
712
712
  }
@@ -335,7 +335,7 @@ class RedisServer {
335
335
  if (cmd === 'LPUSH') {
336
336
  for (const val of args.slice(1)) list.unshift(val);
337
337
  } else {
338
- list.push(...args.slice(1));
338
+ for (const __e of args.slice(1)) list.push(__e);
339
339
  }
340
340
  this.db.set(args[0], { type: 'list', val: list, ttl: v ? v.ttl : null });
341
341
  this._scheduleSnapshot();
package/lib/sql.js CHANGED
@@ -1079,6 +1079,22 @@ class Parser {
1079
1079
  }
1080
1080
 
1081
1081
  parseValueAtom() {
1082
+ // MySQL 前缀运算符:BINARY expr —— 强制大小写敏感比较。
1083
+ // 仅当后面确实是值起始 token 才当作运算符,避免误吞裸列名 `binary`
1084
+ // (`SELECT binary FROM t` / `WHERE binary = 1` 仍按列名解析)。
1085
+ if (this.isWord('BINARY')) {
1086
+ const nt = this.peek(1);
1087
+ const startVal = nt && (nt.type === 'number' || nt.type === 'string' || nt.type === 'ident' || nt.type === 'param' ||
1088
+ (nt.type === 'op' && (nt.value === '(' || nt.value === '-' || nt.value === '+')) ||
1089
+ (nt.type === 'keyword' && (nt.value === 'NULL' || nt.value === 'TRUE' || nt.value === 'FALSE' || nt.value === 'DEFAULT')));
1090
+ if (startVal) {
1091
+ this.next(); // 消费 BINARY
1092
+ const lt = this.peek();
1093
+ if (lt.type === 'string' || lt.type === 'number') { const lit = this.next(); return { type: 'binary', operand: { type: 'literal', value: lit.value } }; }
1094
+ const operand = this.parseValue();
1095
+ return { type: 'binary', operand };
1096
+ }
1097
+ }
1082
1098
  const t = this.next();
1083
1099
  if (t.type === 'number' || t.type === 'string') return t.value;
1084
1100
  // MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
@@ -1583,9 +1599,30 @@ class Parser {
1583
1599
  let where = null;
1584
1600
  if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
1585
1601
  else console.warn(`⚠️ UPDATE without WHERE clause — this will modify ALL rows in '${table}'`);
1602
+ // UPDATE ... ORDER BY ... LIMIT n(与 DELETE 对称;按排序后的前 n 行更新)
1603
+ let orderBy = null, limit = null;
1604
+ if (this.isKeyword('ORDER')) {
1605
+ this.next(); this.expectKeyword('BY');
1606
+ orderBy = [];
1607
+ for (;;) {
1608
+ const col = this.parseColumnRef();
1609
+ let dir = 'asc';
1610
+ if (this.isKeyword('ASC')) this.next();
1611
+ else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
1612
+ orderBy.push({ column: col, dir });
1613
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1614
+ break;
1615
+ }
1616
+ }
1617
+ if (this.isKeyword('LIMIT')) {
1618
+ this.next();
1619
+ limit = this.parseValue();
1620
+ if (typeof limit === 'bigint') limit = Number(limit <= BigInt(Number.MAX_SAFE_INTEGER) ? limit : BigInt(Number.MAX_SAFE_INTEGER));
1621
+ if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
1622
+ }
1586
1623
  const returning = this.parseOptionalReturning();
1587
1624
  this.optionalTailSemicolon();
1588
- return { type: 'update', table, assignments, where, returning };
1625
+ return { type: 'update', table, assignments, where, orderBy, limit, returning };
1589
1626
  }
1590
1627
 
1591
1628
  parseDelete() {
@@ -2191,6 +2228,37 @@ class Parser {
2191
2228
  }
2192
2229
 
2193
2230
  parseOperand() {
2231
+ // MySQL 前缀运算符:BINARY expr —— 强制大小写敏感比较。
2232
+ if (this.isWord('BINARY')) {
2233
+ const nt = this.peek(1);
2234
+ const startVal = nt && (nt.type === 'number' || nt.type === 'string' || nt.type === 'ident' || nt.type === 'param' ||
2235
+ (nt.type === 'op' && (nt.value === '(' || nt.value === '-' || nt.value === '+')) ||
2236
+ (nt.type === 'keyword' && (nt.value === 'NULL' || nt.value === 'TRUE' || nt.value === 'FALSE' || nt.value === 'DEFAULT')));
2237
+ if (startVal) {
2238
+ this.next(); // 消费 BINARY
2239
+ // 字面量要包成 literal,否则 resolveOperand 把 'ABC' 当列名
2240
+ const lt = this.peek();
2241
+ if (lt.type === 'string' || lt.type === 'number') { const lit = this.next(); return { type: 'binary', operand: { type: 'literal', value: lit.value } }; }
2242
+ const operand = this.parseValue();
2243
+ return { type: 'binary', operand };
2244
+ }
2245
+ }
2246
+ // MySQL 区间字面量:INTERVAL n UNIT(date + INTERVAL 3 DAY / date - INTERVAL 1 MONTH)
2247
+ if (this.isWord('INTERVAL')) {
2248
+ this.next();
2249
+ const nTok = this.peek();
2250
+ let nVal;
2251
+ if (nTok.type === 'op' && (nTok.value === '-' || nTok.value === '+')) {
2252
+ this.next();
2253
+ const numTok = this.next();
2254
+ nVal = (nTok.value === '-' ? -1 : 1) * Number(numTok.value);
2255
+ } else {
2256
+ this.next();
2257
+ nVal = Number(nTok.value);
2258
+ }
2259
+ const unitTok = this.next();
2260
+ return { type: 'interval', value: nVal, unit: String(unitTok.value).toUpperCase() };
2261
+ }
2194
2262
  const t = this.next();
2195
2263
  // P1-20: SELECT -col 报错——parseOperand 不处理负号
2196
2264
  if (t.type === 'op' && t.value === '-') {
@@ -2871,6 +2939,8 @@ function resolveOperand(operand, row, ctx) {
2871
2939
  case 'value':
2872
2940
  case 'literal':
2873
2941
  return operand.value;
2942
+ case 'binary':
2943
+ return resolveOperand(operand.operand, row, ctx);
2874
2944
  case 'sysvar': {
2875
2945
  const s = (ctx && ctx.session && ctx.session.sysvars) || {};
2876
2946
  const name = String(operand.name).toLowerCase();
@@ -2909,6 +2979,32 @@ function resolveOperand(operand, row, ctx) {
2909
2979
  return undefined;
2910
2980
  }
2911
2981
  case 'arith': {
2982
+ // date + INTERVAL n UNIT / date - INTERVAL n UNIT(MySQL 区间算术)
2983
+ const lIv = operand.left && operand.left.type === 'interval';
2984
+ const rIv = operand.right && operand.right.type === 'interval';
2985
+ if ((operand.op === '+' || operand.op === '-') && (lIv || rIv)) {
2986
+ if (lIv && operand.op === '-') return null; // INTERVAL - date 无意义
2987
+ const ivNode = lIv ? operand.left : operand.right;
2988
+ const dateSide = lIv ? resolveOperand(operand.right, row, ctx) : resolveOperand(operand.left, row, ctx);
2989
+ const base = parseSqlDate(dateSide);
2990
+ if (!base) return null;
2991
+ const sign = operand.op === '-' ? -1 : 1;
2992
+ const isDateOnly = String(dateSide).trim().length <= 10;
2993
+ const d = new Date(base.getTime());
2994
+ const n = sign * Number(ivNode.value || 0);
2995
+ switch (String(ivNode.unit || '').toUpperCase()) {
2996
+ case 'DAY': case 'DAYS': d.setDate(d.getDate() + n); break;
2997
+ case 'WEEK': case 'WEEKS': d.setDate(d.getDate() + n * 7); break;
2998
+ case 'MONTH': case 'MONTHS': addMonthsClamped(d, n); break;
2999
+ case 'QUARTER': addMonthsClamped(d, n * 3); break;
3000
+ case 'YEAR': case 'YEARS': addMonthsClamped(d, n * 12); break;
3001
+ case 'HOUR': case 'HOURS': d.setHours(d.getHours() + n); break;
3002
+ case 'MINUTE': case 'MINUTES': d.setMinutes(d.getMinutes() + n); break;
3003
+ case 'SECOND': case 'SECONDS': d.setSeconds(d.getSeconds() + n); break;
3004
+ default: return null;
3005
+ }
3006
+ return fmtSqlDate(isDateOnly, d);
3007
+ }
2912
3008
  const l = resolveOperand(operand.left, row, ctx);
2913
3009
  const r = resolveOperand(operand.right, row, ctx);
2914
3010
  if (l === null || r === null || l === undefined || r === undefined) return null;
@@ -4114,13 +4210,25 @@ function applyScalarFunction(fnNode, row, ctx) {
4114
4210
  }
4115
4211
  function depth(obj) {
4116
4212
  if (typeof obj !== 'object' || obj === null) return 0;
4117
- if (Array.isArray(obj)) {
4118
- if (obj.length === 0) return 1;
4119
- return 1 + Math.max(...obj.map(depth));
4213
+ let max = 0;
4214
+ const stack = [{ o: obj, d: 1 }];
4215
+ while (stack.length) {
4216
+ const { o, d } = stack.pop();
4217
+ if (typeof o !== 'object' || o === null) continue;
4218
+ if (d > max) max = d;
4219
+ if (Array.isArray(o)) {
4220
+ for (const x of o) {
4221
+ if (x && typeof x === 'object') stack.push({ o: x, d: d + 1 });
4222
+ }
4223
+ } else {
4224
+ const ks = Object.keys(o);
4225
+ for (const k of ks) {
4226
+ const x = o[k];
4227
+ if (x && typeof x === 'object') stack.push({ o: x, d: d + 1 });
4228
+ }
4229
+ }
4120
4230
  }
4121
- const keys = Object.keys(obj);
4122
- if (keys.length === 0) return 1;
4123
- return 1 + Math.max(...keys.map(k => depth(obj[k])));
4231
+ return max;
4124
4232
  }
4125
4233
  return depth(data);
4126
4234
  }
@@ -4301,6 +4409,19 @@ function normalizeCmpValue(a, other) {
4301
4409
  return otherIsNumeric ? (a ? 1 : 0) : a;
4302
4410
  }
4303
4411
 
4412
+ // MySQL 默认 collation(utf8mb4_general_ci)下字符串比较不区分大小写,
4413
+ // 与 LIKE 保持一致。cs=true 表示显式 BINARY/COLLATE 强制大小写敏感。
4414
+ function ciEq(a, b, cs) {
4415
+ if (cs) return a === b;
4416
+ if (typeof a === 'string' && typeof b === 'string') return a.toLowerCase() === b.toLowerCase();
4417
+ return String(a) === String(b);
4418
+ }
4419
+ function ciCompare(a, b, cs) {
4420
+ if (cs) return a === b ? 0 : (a < b ? -1 : 1);
4421
+ const as = String(a).toLowerCase(), bs = String(b).toLowerCase();
4422
+ return as === bs ? 0 : (as < bs ? -1 : 1);
4423
+ }
4424
+
4304
4425
  function evaluateExpr(expr, row, ctx) {
4305
4426
  if (expr === null || expr === undefined) return false;
4306
4427
  switch (expr.type) {
@@ -4317,12 +4438,14 @@ function evaluateExpr(expr, row, ctx) {
4317
4438
  case 'compare': {
4318
4439
  const l = resolveOperand(expr.left, row, ctx);
4319
4440
  const r = resolveOperand(expr.right, row, ctx);
4441
+ // BINARY 限定符:任一侧是 BINARY 操作数即强制大小写敏感
4442
+ const cs = !!(expr.cs || (expr.left && expr.left.type === 'binary') || (expr.right && expr.right.type === 'binary'));
4320
4443
  // MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
4321
4444
  if (expr.op === '<=>') {
4322
4445
  const ln = l === null || l === undefined;
4323
4446
  const rn = r === null || r === undefined;
4324
4447
  if (ln || rn) return ln && rn;
4325
- return l === r || String(l) === String(r);
4448
+ return ciEq(l, r, cs);
4326
4449
  }
4327
4450
  // SQL 标准:与 NULL 的比较结果是 UNKNOWN,这里用 null 表示。
4328
4451
  // 不能返回 false —— 否则 NOT (a > 1) 会把 a 为 NULL 的行错误纳入结果。
@@ -4337,11 +4460,19 @@ function evaluateExpr(expr, row, ctx) {
4337
4460
  const mixedNum = (isNum(nl) && isNumStr(nr)) || (isNumStr(nl) && isNum(nr));
4338
4461
  if (expr.op === '=') {
4339
4462
  if (mixedNum) return Number(nl) === Number(nr);
4340
- return nl === nr || String(nl) === String(nr);
4463
+ return ciEq(nl, nr, cs);
4464
+ }
4465
+ if (mixedNum) { const fn = OPERATORS[expr.op]; return fn(Number(nl), Number(nr)); }
4466
+ if (typeof nl === 'number' && typeof nr === 'number') { const fn = OPERATORS[expr.op]; return fn(nl, nr); }
4467
+ const c = ciCompare(nl, nr, cs);
4468
+ switch (expr.op) {
4469
+ case '!=': case '<>': return c !== 0;
4470
+ case '<': return c < 0;
4471
+ case '<=': return c <= 0;
4472
+ case '>': return c > 0;
4473
+ case '>=': return c >= 0;
4474
+ default: return c === 0;
4341
4475
  }
4342
- const fn = OPERATORS[expr.op];
4343
- if (mixedNum) return fn(Number(nl), Number(nr));
4344
- return typeof nl === 'number' && typeof nr === 'number' ? fn(nl, nr) : fn(String(nl), String(nr));
4345
4476
  }
4346
4477
  case 'isNull': {
4347
4478
  const v = resolveOperand(expr.operand, row, ctx);
@@ -4353,6 +4484,8 @@ function evaluateExpr(expr, row, ctx) {
4353
4484
  if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
4354
4485
  if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
4355
4486
  if (!expr.list) return expr.not ? true : false;
4487
+ const cs = !!(expr.cs || (expr.operand && expr.operand.type === 'binary') ||
4488
+ (expr.list && expr.list.some(x => x && x.type === 'binary')));
4356
4489
 
4357
4490
  // 先找匹配:只要有一个非 NULL 元素等于 v,整个 IN 就是 TRUE ——
4358
4491
  // 列表里别的位置有没有 NULL 都不影响。
@@ -4360,9 +4493,12 @@ function evaluateExpr(expr, row, ctx) {
4360
4493
  // 此前是先判「列表里有 NULL 就返回 NULL」,于是 `v IN (10, NULL)`
4361
4494
  // 连 v=10 的行都匹配不上(实测返回 0 行,应为 2 行)。
4362
4495
  // 三值逻辑里 NULL 只在**没匹配上**的时候才让结果变 UNKNOWN。
4363
- const matched = expr.list.some(x =>
4364
- x !== null && x !== undefined && (x === v || String(x) === String(v))
4365
- );
4496
+ const matched = expr.list.some(x => {
4497
+ // list 元素可能是表达式节点(如 IN (BINARY 'ABC')),需先求值;
4498
+ // 原始字面量('ABC' / 10)直接比较,不能走 resolveOperand(会把字符串当列名)。
4499
+ const xv = (x && typeof x === 'object') ? resolveOperand(x, row, ctx) : x;
4500
+ return xv !== null && xv !== undefined && (xv === v || ciEq(xv, v, cs));
4501
+ });
4366
4502
  if (matched) return expr.not ? false : true;
4367
4503
 
4368
4504
  // 没匹配上:列表里有 NULL → UNKNOWN,否则是确定的 FALSE
@@ -5243,22 +5379,38 @@ class SQLExecutor {
5243
5379
  throw createError('ER_PARSE_ERROR', `Table '${statement.table}' has no primary key, UPDATE requires a primary key`);
5244
5380
  }
5245
5381
  const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
5382
+ // UPDATE ... ORDER BY ... LIMIT n:先过滤、排序、截取,再逐行更新
5383
+ let candidates = all.filter(row => !statement.where || evaluateExpr(statement.where, row, this.ctx));
5384
+ if (statement.orderBy) {
5385
+ const cmp = (a, b) => {
5386
+ for (const o of statement.orderBy) {
5387
+ const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
5388
+ const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
5389
+ if (av === bv) continue;
5390
+ if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
5391
+ if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
5392
+ const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
5393
+ if (d !== 0) return o.dir === 'asc' ? d : -d;
5394
+ }
5395
+ return 0;
5396
+ };
5397
+ candidates = candidates.slice().sort(cmp);
5398
+ }
5399
+ if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
5246
5400
  let count = 0;
5247
- for (const row of all) {
5248
- if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
5249
- const id = this._rowPkId(row, pkCols);
5250
- if (id !== undefined) {
5251
- const data = {};
5252
- for (const [col, val] of statement.assignments) {
5253
- data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
5254
- }
5255
- // 必须 await:wasm / native client 的 updateById 是异步的,
5256
- // 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
5257
- // 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
5258
- // lib/database.js 的实现是同步的,await 同步值同样安全。
5259
- await this.engine.updateById(statement.table, id, data);
5260
- count++;
5401
+ for (const row of candidates) {
5402
+ const id = this._rowPkId(row, pkCols);
5403
+ if (id !== undefined) {
5404
+ const data = {};
5405
+ for (const [col, val] of statement.assignments) {
5406
+ data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
5261
5407
  }
5408
+ // 必须 await:wasm / native client 的 updateById 是异步的,
5409
+ // 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
5410
+ // 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
5411
+ // lib/database.js 的实现是同步的,await 同步值同样安全。
5412
+ await this.engine.updateById(statement.table, id, data);
5413
+ count++;
5262
5414
  }
5263
5415
  }
5264
5416
  await this.engine.flush();
@@ -5658,17 +5810,26 @@ class SQLExecutor {
5658
5810
  // - 带表限定的条件(如 "o.amount"):检查表名/别名是否匹配
5659
5811
  // - 不带表限定的条件(如 "amount"):检查当前表的 schema 是否包含该列
5660
5812
  const filter = {};
5813
+ // 字符串列默认 collation 为 CI(大小写不敏感),而存储层 find 的等值
5814
+ // 过滤是严格比较(CS)。字符串等值若下推会被 CS 预筛,绕过 evaluateExpr 的
5815
+ // ciEq,导致 `code = 'abc'` 匹配不到 'ABC'。所以字符串/JSON 列不参与下推,
5816
+ // 交给 evaluateExpr 按 CI 求值;数值/布尔仍下推走索引。
5817
+ const isNonPushdownCol = (d) => {
5818
+ const t = String((d && d.type) || '').toLowerCase();
5819
+ return t === 'text' || t === 'varchar' || t === 'char' || t === 'string' ||
5820
+ t === 'blob' || t === 'clob' || t === 'json' || t === 'object' || t === 'array';
5821
+ };
5661
5822
  for (const [key, val] of Object.entries(allFilters)) {
5662
5823
  if (key.includes('.')) {
5663
5824
  // 带表限定:检查是否匹配当前表的别名或表名
5664
5825
  const tablePart = key.slice(0, key.lastIndexOf('.'));
5665
5826
  if (tablePart === alias || tablePart === table) {
5666
5827
  const colPart = key.slice(key.lastIndexOf('.') + 1);
5667
- if (schema[colPart]) filter[colPart] = this._coerceValue(val, schema[colPart]);
5828
+ if (schema[colPart] && !isNonPushdownCol(schema[colPart])) filter[colPart] = this._coerceValue(val, schema[colPart]);
5668
5829
  }
5669
5830
  } else {
5670
5831
  // 不带表限定:只下推到持有该列的表
5671
- if (schema[key]) filter[key] = this._coerceValue(val, schema[key]);
5832
+ if (schema[key] && !isNonPushdownCol(schema[key])) filter[key] = this._coerceValue(val, schema[key]);
5672
5833
  }
5673
5834
  }
5674
5835
  const useIndex = Object.keys(filter).length > 0 &&
@@ -5915,7 +6076,7 @@ class SQLExecutor {
5915
6076
  _rollupRow(groupReps) {
5916
6077
  const all = [];
5917
6078
  for (const rep of groupReps) {
5918
- if (rep._group) all.push(...rep._group);
6079
+ if (rep._group) for (const __e of rep._group) all.push(__e);
5919
6080
  else all.push(rep);
5920
6081
  }
5921
6082
  const out = {};
@@ -5979,8 +6140,12 @@ class SQLExecutor {
5979
6140
  const numeric = (v) => typeof v === 'number' ||
5980
6141
  (typeof v === 'string' && String(v).trim() !== '' && Number.isFinite(Number(v)));
5981
6142
  if (vs.every(numeric)) {
5982
- const nums = vs.map(v => Number(v));
5983
- return fn === 'MIN' ? Math.min(...nums) : Math.max(...nums);
6143
+ let best = Number(vs[0]);
6144
+ for (let i = 1; i < vs.length; i++) {
6145
+ const n = Number(vs[i]);
6146
+ if (fn === 'MIN' ? n < best : n > best) best = n;
6147
+ }
6148
+ return best;
5984
6149
  }
5985
6150
  let best = vs[0];
5986
6151
  for (const v of vs.slice(1)) {
@@ -7192,24 +7357,42 @@ class SQLExecutor {
7192
7357
  * 同时按「投影列名 → 下标」解析;命中就直接 `row[idx]`,
7193
7358
  * 不再走 resolveOperand 的通用路径。
7194
7359
  */
7360
+ // 把 ORDER BY 引用的输出列(别名或位置序号)解析回该列的**底层取值**。
7361
+ // 非分组路径的行对象键是底层列名(如 'v'),不是别名('val'),
7362
+ // 直接 r['val'] 会取到 undefined,导致 ORDER BY 静默不排序。
7363
+ const resolveByColumn = (c) => {
7364
+ if (!c) return (r) => null;
7365
+ if (typeof c.expr === 'string' && c.expr !== '*') {
7366
+ const name = c.expr;
7367
+ const node = { type: 'column', name };
7368
+ return (r) => {
7369
+ const v = (r == null) ? undefined : r[name];
7370
+ return v !== undefined ? v : resolveOperand(node, r, this.ctx);
7371
+ };
7372
+ }
7373
+ if (c.scalar) return (r) => resolveOperand(c.scalar, r, this.ctx);
7374
+ if (c.aggregate) return (r) => this._aggValue([r], c.aggregate, c.column, c.distinct, c.separator, c.orderBy);
7375
+ const nm = c.alias;
7376
+ return (r) => (r == null ? null : r[nm]);
7377
+ };
7195
7378
  const ordKeys = statement.orderBy.map(o => {
7196
7379
  const dir = o.dir === 'asc' ? 1 : -1;
7197
- // 位置序号:ORDER BY 1
7380
+ // 位置序号:ORDER BY 1(1-based,指第 N 个输出列)
7198
7381
  const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
7199
7382
  ? o.column.value
7200
7383
  : (typeof o.column === 'number' ? o.column : null);
7201
7384
  if (pos !== null && pos !== undefined) {
7202
- const name = ordNames[pos - 1];
7203
- return { dir, at: (name === undefined || name === null) ? () => null : (r) => (r == null ? null : r[name]) };
7385
+ return { dir, at: resolveByColumn(statement.columns[pos - 1]) };
7204
7386
  }
7205
7387
  if (typeof o.column === 'string') {
7206
7388
  const f = o.column;
7207
- // 节点对象提到循环外复用 —— 此前每次比较都新建一个 {type:'column',name}
7389
+ // 命中某个输出列的别名/列名 → 用该列的底层取值(修复别名排序)
7390
+ const idx = ordNames.findIndex(n => n === f);
7391
+ if (idx >= 0) return { dir, at: resolveByColumn(statement.columns[idx]) };
7208
7392
  const node = { type: 'column', name: f };
7209
7393
  return {
7210
7394
  dir,
7211
7395
  at: (r) => {
7212
- // 先按字段名直取;取不到再走 resolveOperand(点号路径、表达式等)
7213
7396
  const v = (r == null) ? undefined : r[f];
7214
7397
  return v !== undefined ? v : resolveOperand(node, r, this.ctx);
7215
7398
  },
package/lib/tui.js CHANGED
@@ -72,7 +72,9 @@ function renderTable(headers, rows, maxWidth) {
72
72
  for (const w of widths) total += w + 3;
73
73
  if (total > maxCol && widths.length > 1) {
74
74
  let overshoot = total - maxCol;
75
- let i = widths.indexOf(Math.max(...widths));
75
+ let maxW = widths[0];
76
+ for (const w of widths) if (w > maxW) maxW = w;
77
+ let i = widths.indexOf(maxW);
76
78
  while (overshoot > 0) {
77
79
  if (i === -1 || widths[i] <= 3) break;
78
80
  const cut = Math.min(widths[i] - 3, overshoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.5.8",
3
+ "version": "6.5.10",
4
4
  "description": "JSQL-NEO \u2014 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",