jsql-neo 6.5.26 → 6.5.29

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/lib/sql.js CHANGED
@@ -1533,7 +1533,25 @@ class Parser {
1533
1533
  this.expectKeyword('GROUP'); this.expectKeyword('BY');
1534
1534
  groupBy = [];
1535
1535
  while (true) {
1536
- groupBy.push(this.parseColumnRef());
1536
+ // 支持表达式分组:GROUP BY a%2、GROUP BY UPPER(name)(#51)。
1537
+ // 裸列名仍保留字符串形态,避免破坏既有别名解析与下游处理。
1538
+ let g;
1539
+ const save = this.pos;
1540
+ const GROUP_END = ['HAVING', 'ORDER', 'LIMIT', 'OFFSET', 'UNION', 'INTERSECT',
1541
+ 'EXCEPT', 'RETURNING', 'WITH'];
1542
+ const endsGroupItem = (tk) => {
1543
+ if (tk.type === 'eof') return true;
1544
+ if (tk.type === 'op' && (tk.value === ',' || tk.value === ';' || tk.value === ')')) return true;
1545
+ return GROUP_END.some(w => this.isWord(w));
1546
+ };
1547
+ try {
1548
+ g = this.parseColumnRef();
1549
+ if (!endsGroupItem(this.peek())) { this.pos = save; g = this.parseScalar(); }
1550
+ } catch (e) {
1551
+ this.pos = save;
1552
+ g = this.parseScalar();
1553
+ }
1554
+ groupBy.push(g);
1537
1555
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1538
1556
  break;
1539
1557
  }
@@ -1631,6 +1649,17 @@ class Parser {
1631
1649
  else if (this.isKeyword('DISTINCT')) this.next();
1632
1650
  except = { all, select: this.parseSelect() };
1633
1651
  }
1652
+ // 标准 SQL:组合查询尾部的 ORDER BY / LIMIT / OFFSET 作用于整个结果。
1653
+ // 语法上它们会被解析进最右侧的 SELECT(#57),这里逐层提升到外层,
1654
+ // 否则执行层在 concat 后直接返回,尾部子句被静默丢弃。
1655
+ {
1656
+ const rhsSel = (union || intersect || except || {}).select;
1657
+ if (rhsSel) {
1658
+ if (rhsSel.orderBy) { orderBy = rhsSel.orderBy; rhsSel.orderBy = null; }
1659
+ if (rhsSel.limit !== null && rhsSel.limit !== undefined) { limit = rhsSel.limit; rhsSel.limit = null; }
1660
+ if (rhsSel.offset) { offset = rhsSel.offset; rhsSel.offset = 0; }
1661
+ }
1662
+ }
1634
1663
  // FETCH FIRST n ROWS ONLY(SQL 标准的 LIMIT 写法)
1635
1664
  if (this.isWord('FETCH')) {
1636
1665
  this.next();
@@ -4964,6 +4993,24 @@ function evaluateCaseVal(caseExpr, row, ctx) {
4964
4993
  return caseExpr.elseVal ? resolveOperand(caseExpr.elseVal, row, ctx) : null;
4965
4994
  }
4966
4995
 
4996
+ /**
4997
+ * 聚合参数的可读列名(#54):c.column 通常是 AST 节点({type:'column',name:'b'}),
4998
+ * 直接字符串拼理会得到 "SUM([object Object])"。
4999
+ */
5000
+ function columnLabel(col) {
5001
+ if (col === undefined || col === null) return '*';
5002
+ if (typeof col === 'string') return col;
5003
+ if (typeof col === 'number' || typeof col === 'bigint') return String(col);
5004
+ if (typeof col === 'object') {
5005
+ if (col.type === 'column' && col.name !== undefined) return String(col.name);
5006
+ if (col.type === 'star' || col.type === '*') return '*';
5007
+ if (col.type === 'value' || col.type === 'literal') return String(col.value);
5008
+ if (col.type === 'tuple' && Array.isArray(col.items)) return col.items.map(columnLabel).join(', ');
5009
+ try { return scalarName(col); } catch (e) { return 'expr'; }
5010
+ }
5011
+ return String(col);
5012
+ }
5013
+
4967
5014
  function scalarName(node) {
4968
5015
  if (!node) return 'expr';
4969
5016
  switch (node.type) {
@@ -4976,7 +5023,7 @@ function scalarName(node) {
4976
5023
  case 'literal': return String(node.value);
4977
5024
  case 'arith': return scalarName(node.left) + ' ' + node.op + ' ' + scalarName(node.right);
4978
5025
  case 'case': return 'CASE';
4979
- case 'aggregate': return node.fn + '(' + (node.column || '*') + ')';
5026
+ case 'aggregate': return node.fn + '(' + columnLabel(node.column) + ')';
4980
5027
  default: return 'expr';
4981
5028
  }
4982
5029
  }
@@ -4989,7 +5036,7 @@ function scalarColumnName(c) {
4989
5036
  }
4990
5037
  if (c.alias) return c.alias;
4991
5038
  if (c.scalar) return scalarName(c.scalar);
4992
- if (c.aggregate) return c.alias || (c.aggregate === 'COUNT' ? 'COUNT(*)' : c.aggregate + '(' + c.column + ')');
5039
+ if (c.aggregate) return c.alias || c.aggregate + '(' + columnLabel(c.column) + ')';
4993
5040
  if (c.literal !== undefined) return String(c.literal);
4994
5041
  if (c.caseExpr) return 'CASE';
4995
5042
  if (c.expr === '*') return '*';
@@ -5050,7 +5097,16 @@ function scalarColumnValue(c, r, ctx) {
5050
5097
  }
5051
5098
 
5052
5099
  if (c.literal !== undefined) return c.literal;
5053
- if (c.caseExpr) return projectScalar(evaluateCaseVal(c.caseExpr, r, ctx));
5100
+ if (c.caseExpr) {
5101
+ // CASE WHEN 内嵌聚合(#52):先把聚合按当前分组替换成字面值,
5102
+ // 否则 resolveOperand 不认识 aggregate 节点,条件恒为假 → 恒走 ELSE。
5103
+ if (ctx && ctx._replaceAggregates && containsAggregateNode(c.caseExpr)) {
5104
+ const copy = JSON.parse(JSON.stringify(c.caseExpr));
5105
+ ctx._replaceAggregates(copy, ctx.group || [r]);
5106
+ return projectScalar(evaluateCaseVal(copy, r, ctx));
5107
+ }
5108
+ return projectScalar(evaluateCaseVal(c.caseExpr, r, ctx));
5109
+ }
5054
5110
  if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
5055
5111
  return projectScalar(resolveOperand({ type: 'column', name: c.expr }, r, ctx));
5056
5112
  }
@@ -6524,13 +6580,42 @@ class SQLExecutor {
6524
6580
  return typeof agg.result === 'function' ? agg.result(state) : state;
6525
6581
  }
6526
6582
  }
6583
+ // 整数精确累加(#56):SUM/AVG 若全为整数,用 BigInt 累加,
6584
+ // 避免 >2^53 时 Number 浮点加法把 9007199254740993 + 9007199254740992
6585
+ // 舍入成 18014398509481984。结果在安全范围内仍返回 number。
6586
+ const exactIntSum = () => {
6587
+ let total = 0n;
6588
+ for (const v of values) {
6589
+ if (typeof v === 'bigint') { total += v; continue; }
6590
+ if (typeof v === 'number' && Number.isInteger(v)) { total += BigInt(v); continue; }
6591
+ if (typeof v === 'string' && /^[-+]?\d+$/.test(v.trim())) {
6592
+ try { total += BigInt(v.trim()); continue; } catch (e) { return undefined; }
6593
+ }
6594
+ return undefined;
6595
+ }
6596
+ return total;
6597
+ };
6598
+ const SAFE_MAX = BigInt(Number.MAX_SAFE_INTEGER);
6599
+ const SAFE_MIN = BigInt(Number.MIN_SAFE_INTEGER);
6527
6600
  if (fn === 'SUM') {
6528
6601
  // MySQL 语义:空集(或无任何非 NULL 值)的 SUM 为 NULL,而非 0。
6529
6602
  // AVG/MIN/MAX 已按此处理,此前只有 SUM 漏了。
6530
6603
  if (!values.length) return null;
6604
+ const exact = exactIntSum();
6605
+ if (exact !== undefined) {
6606
+ return (exact >= SAFE_MIN && exact <= SAFE_MAX) ? Number(exact) : exact;
6607
+ }
6531
6608
  return values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
6532
6609
  }
6533
- if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
6610
+ if (fn === 'AVG') {
6611
+ if (!values.length) return null;
6612
+ const exact = exactIntSum();
6613
+ if (exact !== undefined) {
6614
+ // 先精确求和,再除以行数;Number(total) 只在最后一步转换
6615
+ return Number(exact) / values.length;
6616
+ }
6617
+ return values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length;
6618
+ }
6534
6619
  if (fn === 'MIN' || fn === 'MAX') {
6535
6620
  const vs = values.filter(v => v !== null && v !== undefined);
6536
6621
  if (!vs.length) return null;
@@ -7327,36 +7412,108 @@ class SQLExecutor {
7327
7412
  return statement;
7328
7413
  }
7329
7414
 
7415
+ /**
7416
+ * 组合查询(UNION/INTERSECT/EXCEPT)尾部的 ORDER BY / LIMIT / OFFSET(#57)。
7417
+ * 此时 rows 已是投影后的数组,排序键按左分支列名或 1-based 列序号解析。
7418
+ */
7419
+ _finalizeSetOp(rows, columns, statement) {
7420
+ let out = rows;
7421
+ const orderBy = statement.orderBy;
7422
+ if (Array.isArray(orderBy) && orderBy.length) {
7423
+ const idxOf = (name) => {
7424
+ const n = String(name).toLowerCase();
7425
+ for (let i = 0; i < columns.length; i++) {
7426
+ if (String(columns[i]).toLowerCase() === n) return i;
7427
+ }
7428
+ return -1;
7429
+ };
7430
+ const objOf = (r) => {
7431
+ if (r && !Array.isArray(r) && typeof r === 'object') return r;
7432
+ const o = {};
7433
+ for (let i = 0; i < columns.length; i++) o[columns[i]] = r ? r[i] : undefined;
7434
+ return o;
7435
+ };
7436
+ const keyed = out.map((r, i) => ({ r, i, o: objOf(r) }));
7437
+ const keyOf = (entry, o) => {
7438
+ const c = o.column;
7439
+ if (typeof c === 'number') {
7440
+ if (c >= 1 && c <= columns.length) return entry.r[c - 1];
7441
+ throw new Error(`ORDER BY position ${c} is out of range`);
7442
+ }
7443
+ if (typeof c === 'string' || (c && (c.type === 'column' || (c.type === 'value' && typeof c.value === 'number')))) {
7444
+ if (c && c.type === 'value') {
7445
+ const n = Number(c.value);
7446
+ if (n >= 1 && n <= columns.length) return entry.r[n - 1];
7447
+ throw new Error(`ORDER BY position ${n} is out of range`);
7448
+ }
7449
+ const name = typeof c === 'string' ? c : c.name;
7450
+ const idx = idxOf(name);
7451
+ if (idx === -1) throw new Error(`no such column: ${name}`);
7452
+ return entry.r[idx];
7453
+ }
7454
+ // 表达式排序:按列名构造对象后求值
7455
+ return resolveOperand(c, entry.o, this.ctx);
7456
+ };
7457
+ keyed.sort((a, b) => {
7458
+ for (const o of orderBy) {
7459
+ const av = keyOf(a, o);
7460
+ const bv = keyOf(b, o);
7461
+ if (av === bv) continue;
7462
+ const aNull = av === null || av === undefined;
7463
+ const bNull = bv === null || bv === undefined;
7464
+ if (aNull || bNull) {
7465
+ const r = aNull && bNull ? 0 : (aNull ? -1 : 1);
7466
+ return o.dir === 'desc' ? -r : r;
7467
+ }
7468
+ const d = typeof av === 'number' && typeof bv === 'number'
7469
+ ? av - bv : String(av).localeCompare(String(bv));
7470
+ if (d !== 0) return o.dir === 'desc' ? -d : d;
7471
+ }
7472
+ return a.i - b.i;
7473
+ });
7474
+ out = keyed.map(x => x.r);
7475
+ }
7476
+ if (statement.offset) out = out.slice(statement.offset);
7477
+ if (statement.limit !== null && statement.limit !== undefined) out = out.slice(0, statement.limit);
7478
+ return out;
7479
+ }
7480
+
7330
7481
  async executeSelect(statement) {
7331
7482
  // 视图:FROM 里的视图名展开成子查询
7332
7483
  if (this.engine && this.engine._views) this._expandViews(statement);
7333
7484
 
7485
+ // 集合运算:左分支不应用尾部 ORDER BY/LIMIT/OFFSET(它们属于整个组合结果)
7486
+ const setOpBase = { orderBy: null, limit: null, offset: 0, fetch: undefined };
7487
+
7334
7488
  // UNION 处理
7335
7489
  if (statement.union) {
7336
- const left = await this.executeSelect({ ...statement, union: null });
7490
+ const left = await this.executeSelect({ ...statement, ...setOpBase, union: null });
7337
7491
  const right = await this.executeSelect(statement.union.select);
7338
7492
  let rows = left.rows.concat(right.rows);
7339
7493
  if (!statement.union.all) rows = dedupeRows(rows);
7494
+ rows = this._finalizeSetOp(rows, left.columns, statement);
7340
7495
  return { ok: true, type: 'select', columns: left.columns, rows, raw: rows };
7341
7496
  }
7342
7497
 
7343
7498
  // INTERSECT:保留同时出现在右侧结果中的行(默认 DISTINCT,与 UNION 的处理保持一致)
7344
7499
  if (statement.intersect) {
7345
- const left = await this.executeSelect({ ...statement, intersect: null, except: null });
7500
+ const left = await this.executeSelect({ ...statement, ...setOpBase, intersect: null, except: null });
7346
7501
  const right = await this.executeSelect(statement.intersect.select);
7347
7502
  const seen = new Set(right.rows.map(r => JSON.stringify(r)));
7348
7503
  let out = left.rows.filter(r => seen.has(JSON.stringify(r)));
7349
7504
  if (!statement.intersect.all) out = dedupeRows(out);
7505
+ out = this._finalizeSetOp(out, left.columns, statement);
7350
7506
  return { ok: true, type: 'select', columns: left.columns, rows: out, raw: out };
7351
7507
  }
7352
7508
 
7353
7509
  // EXCEPT:去掉出现在右侧结果中的行(默认 DISTINCT)
7354
7510
  if (statement.except) {
7355
- const left = await this.executeSelect({ ...statement, intersect: null, except: null });
7511
+ const left = await this.executeSelect({ ...statement, ...setOpBase, intersect: null, except: null });
7356
7512
  const right = await this.executeSelect(statement.except.select);
7357
7513
  const seen = new Set(right.rows.map(r => JSON.stringify(r)));
7358
7514
  let out = left.rows.filter(r => !seen.has(JSON.stringify(r)));
7359
7515
  if (!statement.except.all) out = dedupeRows(out);
7516
+ out = this._finalizeSetOp(out, left.columns, statement);
7360
7517
  return { ok: true, type: 'select', columns: left.columns, rows: out, raw: out };
7361
7518
  }
7362
7519
 
@@ -7555,7 +7712,8 @@ class SQLExecutor {
7555
7712
  // 此前全用 resolveOperand({type:'column', name:g}) 按列名取,导致 GROUP BY b
7556
7713
  // 取 row['b'],而原始行里没有 b 列(b 是别名),全返回 undefined 并成一组。
7557
7714
  const aliases = {}; // alias名字 → true(存在性检查用)
7558
- const aliasToCol = {}; // alias名字 → 对应的表达式(用于从原始行取值)
7715
+ const aliasToCol = {}; // alias名字 → 对应的列名(用于从原始行取值)
7716
+ const aliasExpr = {}; // alias名字 → 对应的表达式 AST(GROUP BY 表达式别名,#51)
7559
7717
  if (statement.columns) {
7560
7718
  for (const c of statement.columns) {
7561
7719
  if (c.alias) {
@@ -7565,18 +7723,28 @@ class SQLExecutor {
7565
7723
  // expr 可能是字符串 'a'(列名)或对象 {type:'column', name:'a'}
7566
7724
  if (typeof src === 'string') aliasToCol[c.alias.toLowerCase()] = src;
7567
7725
  else if (src && src.type === 'column') aliasToCol[c.alias.toLowerCase()] = src.name;
7726
+ else if (src && typeof src === 'object') aliasExpr[c.alias.toLowerCase()] = src;
7568
7727
  }
7569
7728
  }
7570
7729
  }
7571
7730
  const groups = new Map();
7572
7731
  for (const row of rows) {
7573
7732
  const key = JSON.stringify(statement.groupBy.map(g => {
7733
+ // 表达式分组(#51):直接对原始行求值表达式
7734
+ if (g && typeof g === 'object') {
7735
+ const v = resolveOperand(g, row, this.ctx);
7736
+ return typeof v === 'bigint' ? v.toString() + 'n' : v;
7737
+ }
7574
7738
  const key = String(g).toLowerCase();
7575
7739
  if (aliases[key]) {
7576
7740
  // 优先用行中已有的 alias 值(投影后才有),没有则尝试从原始列取值
7577
7741
  if (row[key] !== undefined) return row[key];
7578
7742
  // alias → 原始列名 的映射(如 b → a),从原始行取该列的值
7579
7743
  if (aliasToCol[key]) return resolveOperand({ type: 'column', name: aliasToCol[key] }, row, this.ctx);
7744
+ if (aliasExpr[key]) {
7745
+ const v = resolveOperand(aliasExpr[key], row, this.ctx);
7746
+ return typeof v === 'bigint' ? v.toString() + 'n' : v;
7747
+ }
7580
7748
  return row[key]; // row[key] 为 undefined,但保持一致
7581
7749
  }
7582
7750
  return resolveOperand({ type: 'column', name: g }, row, this.ctx);
@@ -7647,7 +7815,14 @@ class SQLExecutor {
7647
7815
  return allKeys.map(k => r[k]);
7648
7816
  }
7649
7817
  if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
7650
- if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
7818
+ if (c.caseExpr) {
7819
+ if (containsAggregateNode(c.caseExpr)) {
7820
+ const cc = JSON.parse(JSON.stringify(c.caseExpr));
7821
+ this._replaceAggregates(cc, r._group || [r]);
7822
+ return evaluateCaseVal(cc, r, this.ctx);
7823
+ }
7824
+ return evaluateCaseVal(c.caseExpr, r, this.ctx);
7825
+ }
7651
7826
  if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
7652
7827
  return resolveOperand({ type: 'column', name: c.expr }, r, this.ctx);
7653
7828
  }));
@@ -7663,7 +7838,8 @@ class SQLExecutor {
7663
7838
  // 无 GROUP BY 的整表聚合:顶层是聚合(AVG(x))、自定义聚合(total(x)),
7664
7839
  // 以及嵌套聚合(ROUND(AVG(x),1) —— 聚合藏在 scalar 的参数里,顶层 c.aggregate 为空)
7665
7840
  const hasNestedAgg = !statement.groupBy &&
7666
- statement.columns.some(c => c.scalar && containsAggregateNode(c.scalar));
7841
+ statement.columns.some(c => (c.scalar && containsAggregateNode(c.scalar)) ||
7842
+ (c.caseExpr && containsAggregateNode(c.caseExpr)));
7667
7843
  if (!statement.groupBy && (statement.columns.some(c => c.aggregate && !c.window) || hasCustomAgg || hasNestedAgg)) {
7668
7844
  const cols = statement.columns.map(c => scalarColumnName(c));
7669
7845
  const tableName2 = statement.from ? (statement.from.tables[0].table || null) : null;
@@ -7688,7 +7864,15 @@ class SQLExecutor {
7688
7864
  }
7689
7865
  return projectScalar(resolveOperand(c.scalar, rows[0] || {}, this.ctx));
7690
7866
  }
7691
- if (c.caseExpr) return projectScalar(resolveOperand(c.caseExpr, rows[0] || {}, this.ctx));
7867
+ if (c.caseExpr) {
7868
+ // CASE 内嵌聚合按整表求值替换(#52)
7869
+ if (containsAggregateNode(c.caseExpr)) {
7870
+ const nestedCase = JSON.parse(JSON.stringify(c.caseExpr));
7871
+ this._replaceAggregates(nestedCase, rows);
7872
+ return projectScalar(evaluateCaseVal(nestedCase, rows[0] || {}, this.ctx));
7873
+ }
7874
+ return projectScalar(evaluateCaseVal(c.caseExpr, rows[0] || {}, this.ctx));
7875
+ }
7692
7876
  if (c.expr !== null && c.expr !== '*') return rows[0] ? projectScalar(resolveOperand({ type: 'column', name: c.expr }, rows[0])) : null;
7693
7877
  return null;
7694
7878
  };
@@ -8337,6 +8521,57 @@ function finalizeBigInts(value, seen) {
8337
8521
  return value;
8338
8522
  }
8339
8523
 
8524
+ /**
8525
+ * SQLResult 专用的 BigInt 收尾 —— 保留语义,去掉 20 万次递归和 Set.add。
8526
+ *
8527
+ * 通用 finalizeBigInts 会对 rows(10 万个数组)+ raw(10 万个对象)逐个
8528
+ * 深递归,每进一个对象就 seen.add 一次。node --prof 显示光
8529
+ * SetPrototypeAdd 就占 18 ticks(约 14% JS),加上 finalizeBigInts 自身
8530
+ * 共 ~23% JS ticks,整体 33% 时间花在 GC 上。
8531
+ *
8532
+ * 而结果的形状是规则的:rows 是二维数组,raw 是对象数组,元素绝大多数是
8533
+ * number / string / null —— 根本不可能是 BigInt。用紧凑循环只做 typeof
8534
+ * 检查,遇到嵌套对象才退回通用实现。语义与深递归等价,但不再给每个
8535
+ * 标量行建 Set 条目。
8536
+ */
8537
+ function finalizeResultBigInts(r) {
8538
+ if (!r || typeof r !== 'object' || Array.isArray(r)) return finalizeBigInts(r);
8539
+
8540
+ if (Array.isArray(r.rows)) {
8541
+ const rows = r.rows;
8542
+ for (let i = 0; i < rows.length; i++) {
8543
+ const row = rows[i];
8544
+ if (!Array.isArray(row)) { rows[i] = finalizeBigInts(row); continue; }
8545
+ for (let j = 0; j < row.length; j++) {
8546
+ const v = row[j];
8547
+ if (typeof v === 'bigint') row[j] = v.toString();
8548
+ else if (v && typeof v === 'object') row[j] = finalizeBigInts(v);
8549
+ }
8550
+ }
8551
+ }
8552
+
8553
+ if (Array.isArray(r.raw)) {
8554
+ const raw = r.raw;
8555
+ for (let i = 0; i < raw.length; i++) {
8556
+ const obj = raw[i];
8557
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) continue;
8558
+ for (const k of Object.keys(obj)) {
8559
+ const v = obj[k];
8560
+ if (typeof v === 'bigint') obj[k] = v.toString();
8561
+ else if (v && typeof v === 'object') obj[k] = finalizeBigInts(v);
8562
+ }
8563
+ }
8564
+ }
8565
+
8566
+ // 其余字段(columns / insertId / ids / columnTypes / 元数据)都很小,
8567
+ // 走通用实现,保证不漏掉任何可能含 BigInt 的位置。
8568
+ for (const k of Object.keys(r)) {
8569
+ if (k === 'rows' || k === 'raw') continue;
8570
+ r[k] = finalizeBigInts(r[k]);
8571
+ }
8572
+ return r;
8573
+ }
8574
+
8340
8575
  async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
8341
8576
  // 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
8342
8577
  // 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
@@ -8410,7 +8645,7 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
8410
8645
  let r = await executor.execute(stmt);
8411
8646
  const durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
8412
8647
  r = buildResultEnvelope(stmt, r, Math.round(durationMs * 1000) / 1000);
8413
- r = finalizeBigInts(r);
8648
+ r = finalizeResultBigInts(r);
8414
8649
  if (r && r.columns && r.columnTypes === undefined) {
8415
8650
  r.columnTypes = await inferColumnTypes(engine, stmt, r);
8416
8651
  }
package/lib/table.js CHANGED
@@ -456,24 +456,25 @@ class Table {
456
456
 
457
457
  findById(id) {
458
458
  if (!this._primaryKey) throw createError('ER_PARSE_ERROR', 'Table has no primary key');
459
+ // Issue #44:字符串主键查询值先按列类型归一,否则索引静默未命中
460
+ const key = this._coerceQueryValue(this._primaryKey, id);
459
461
  // Hash 索引 O(1)
460
462
  if (this._pkIndex) {
461
- const rowIndex = this._pkIndex.get(id);
463
+ const rowIndex = this._pkIndex.get(key);
462
464
  if (rowIndex !== undefined) {
463
465
  const row = this._rows[rowIndex];
464
466
  if (row && !row._deleted) return { ...row };
465
467
  }
466
- return null;
467
- }
468
- if (this._btrees[this._primaryKey]) {
469
- const indices = this._btrees[this._primaryKey].search(id);
468
+ } else if (this._btrees[this._primaryKey]) {
469
+ const indices = this._btrees[this._primaryKey].search(key);
470
470
  if (indices.length > 0) {
471
471
  const row = this._rows[indices[0]];
472
472
  if (row && !row._deleted) return { ...row };
473
473
  }
474
- return null;
475
474
  }
476
- return this.findOne({ [this._primaryKey]: id });
475
+ // 索引未命中不等于不存在:回落主键列全表匹配(类型差异 / 索引未覆盖的行)
476
+ const row = this._rows.find(r => r && !r._deleted && r[this._primaryKey] === key);
477
+ return row ? { ...row } : null;
477
478
  }
478
479
 
479
480
  findAll() {
@@ -533,24 +534,25 @@ class Table {
533
534
  updates = hook({ [this._primaryKey]: id }, { ...updates }) || updates;
534
535
  }
535
536
  updates = this._validateDataTypes(updates, true);
537
+ const pkKey = this._coerceQueryValue(this._primaryKey, id);
536
538
  let rowIndex;
537
539
  if (this._pkIndex) {
538
- rowIndex = this._pkIndex.get(id);
539
- } else {
540
- if (this._btrees[this._primaryKey]) {
541
- const indices = this._btrees[this._primaryKey].search(id);
542
- rowIndex = indices.length > 0 ? indices[0] : undefined;
543
- } else {
544
- rowIndex = this._rows.findIndex(r => r[this._primaryKey] === id);
545
- if (rowIndex === -1) rowIndex = undefined;
546
- }
540
+ rowIndex = this._pkIndex.get(pkKey);
541
+ } else if (this._btrees[this._primaryKey]) {
542
+ const indices = this._btrees[this._primaryKey].search(pkKey);
543
+ rowIndex = indices.length > 0 ? indices[0] : undefined;
544
+ }
545
+ if (rowIndex === undefined) {
546
+ // Issue #44:索引未命中时回落全表匹配
547
+ const found = this._rows.findIndex(r => r && !r._deleted && r[this._primaryKey] === pkKey);
548
+ if (found !== -1) rowIndex = found;
547
549
  }
548
550
  if (rowIndex === undefined) return 0;
549
551
  const row = this._rows[rowIndex];
550
552
  if (row && row._deleted) return 0;
551
553
  // P1-3:主键被更新时传播到外键引用(CASCADE / SET NULL / RESTRICT)
552
- if (updates[this._primaryKey] !== undefined && updates[this._primaryKey] !== id) {
553
- this._propagatePKUpdate(id, updates[this._primaryKey]);
554
+ if (updates[this._primaryKey] !== undefined && updates[this._primaryKey] !== pkKey) {
555
+ this._propagatePKUpdate(pkKey, updates[this._primaryKey]);
554
556
  }
555
557
  const oldData = { ...row };
556
558
  const merged = { ...row, ...updates };
@@ -633,17 +635,19 @@ class Table {
633
635
 
634
636
  removeById(id) {
635
637
  if (!this._primaryKey) throw createError('ER_PARSE_ERROR', 'Table has no primary key');
638
+ // Issue #44:同 findById,先按列类型归一查询值
639
+ const pkKey = this._coerceQueryValue(this._primaryKey, id);
636
640
  let rowIndex;
637
641
  if (this._pkIndex) {
638
- rowIndex = this._pkIndex.get(id);
639
- } else {
640
- if (this._btrees[this._primaryKey]) {
641
- const indices = this._btrees[this._primaryKey].search(id);
642
- rowIndex = indices.length > 0 ? indices[0] : undefined;
643
- } else {
644
- rowIndex = this._rows.findIndex(r => r[this._primaryKey] === id);
645
- if (rowIndex === -1) rowIndex = undefined;
646
- }
642
+ rowIndex = this._pkIndex.get(pkKey);
643
+ } else if (this._btrees[this._primaryKey]) {
644
+ const indices = this._btrees[this._primaryKey].search(pkKey);
645
+ rowIndex = indices.length > 0 ? indices[0] : undefined;
646
+ }
647
+ if (rowIndex === undefined) {
648
+ // Issue #44:索引未命中时回落全表匹配
649
+ const found = this._rows.findIndex(r => r && !r._deleted && r[this._primaryKey] === pkKey);
650
+ if (found !== -1) rowIndex = found;
647
651
  }
648
652
  if (rowIndex === undefined) return 0;
649
653
  const row = this._rows[rowIndex];
@@ -1037,32 +1041,129 @@ class Table {
1037
1041
  return end > 0 ? pattern.slice(0, end) : null;
1038
1042
  }
1039
1043
 
1040
- _applyFilterOptimized(rows, query) {
1044
+ /**
1045
+ * Issue #44:按列声明类型归一查询值。
1046
+ *
1047
+ * 索引(B-Tree / hash)命中用的是严格 ===,所以字符串 '5' 永远搜不到
1048
+ * INT 列里的数字 5,表现为「静默返回空」;SQL 的 WHERE 路径早就在
1049
+ * 解析阶段强转过了,文档 API 这条路径没有。
1050
+ *
1051
+ * 归一结果与 insert 时 _validateDataTypes 的产物一致(整数 → number/BigInt、
1052
+ * 浮点 → number、布尔 → boolean、字符串列 → string)。无法归一的值原样返回。
1053
+ */
1054
+ _coerceQueryValue(field, value) {
1055
+ const def = this._schema && this._schema[field];
1056
+ if (!def || typeof def === 'string' || value === null || value === undefined) return value;
1057
+ const t = String(def.type || '').toLowerCase();
1058
+ if (t === 'integer' || t === 'int' || t === 'bigint') {
1059
+ if (typeof value === 'bigint') {
1060
+ return (value >= SAFE_INT_MIN && value <= SAFE_INT_MAX) ? Number(value) : value;
1061
+ }
1062
+ if (typeof value === 'number') return value;
1063
+ if (typeof value === 'string' && DECIMAL_INT_RE.test(value.trim())) {
1064
+ try { return normalizeInteger(value.trim(), t); } catch (_) { return value; }
1065
+ }
1066
+ return value;
1067
+ }
1068
+ if (t === 'float' || t === 'double' || t === 'number' || t === 'real') {
1069
+ if (typeof value === 'number') return value;
1070
+ if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) return Number(value);
1071
+ return value;
1072
+ }
1073
+ if (t === 'boolean' || t === 'bool') {
1074
+ if (typeof value === 'boolean') return value;
1075
+ if (value === 0 || value === 1) return !!value;
1076
+ if (typeof value === 'string' && ['true', 'false', '1', '0'].includes(value.toLowerCase())) {
1077
+ return value.toLowerCase() === 'true' || value === '1';
1078
+ }
1079
+ return value;
1080
+ }
1081
+ if (t === 'string' || t === 'text' || t === 'varchar' || t === 'char') {
1082
+ if (typeof value === 'string') return value;
1083
+ if (typeof value === 'number' || typeof value === 'boolean' || value instanceof Date) return String(value);
1084
+ return value;
1085
+ }
1086
+ return value;
1087
+ }
1088
+
1089
+ /** 按 schema 归一 query 中的等值 / $eq / $in / 范围值(Issue #44)。 */
1090
+ _coerceQuery(query) {
1091
+ let out = null;
1092
+ const set = (field, value) => {
1093
+ if (!out) out = { ...query };
1094
+ out[field] = value;
1095
+ };
1041
1096
  for (const [field, condition] of Object.entries(query)) {
1097
+ if (field === '$or' || field === '$and') {
1098
+ if (Array.isArray(condition)) {
1099
+ const subs = condition.map(c => (c && typeof c === 'object' && !Array.isArray(c)) ? this._coerceQuery(c) : c);
1100
+ if (subs.some((s, i) => s !== condition[i])) set(field, subs);
1101
+ }
1102
+ continue;
1103
+ }
1104
+ if (field === '$not' || field[0] === '$' || !this._schema[field]) continue;
1105
+ const next = this._coerceCondition(field, condition);
1106
+ if (next !== condition) set(field, next);
1107
+ }
1108
+ return out || query;
1109
+ }
1110
+
1111
+ _coerceCondition(field, condition) {
1112
+ if (condition === null || typeof condition !== 'object' || condition instanceof RegExp) {
1113
+ return this._coerceQueryValue(field, condition);
1114
+ }
1115
+ if (Array.isArray(condition)) {
1116
+ const next = condition.map(v => this._coerceQueryValue(field, v));
1117
+ return next.some((v, i) => v !== condition[i]) ? next : condition;
1118
+ }
1119
+ const keys = Object.keys(condition);
1120
+ if (keys.length === 0 || !keys.every(k => k[0] === '$')) return condition;
1121
+ let copy = null;
1122
+ for (const [op, val] of Object.entries(condition)) {
1123
+ let next = val;
1124
+ if (op === '$eq' || op === '$ne' || op === '$gt' || op === '$gte' || op === '$lt' || op === '$lte') {
1125
+ next = this._coerceQueryValue(field, val);
1126
+ } else if ((op === '$in' || op === '$nin' || op === '$between') && Array.isArray(val)) {
1127
+ next = val.map(v => this._coerceQueryValue(field, v));
1128
+ }
1129
+ if (next !== val) {
1130
+ if (!copy) copy = { ...condition };
1131
+ copy[op] = next;
1132
+ }
1133
+ }
1134
+ return copy || condition;
1135
+ }
1136
+
1137
+ _applyFilterOptimized(rows, query) {
1138
+ // Issue #44:索引查找前先把查询值归一成列声明类型,避免 '5' 查不到 5
1139
+ const q = this._coerceQuery(query);
1140
+ for (const [field, condition] of Object.entries(q)) {
1042
1141
  if (field === '$or' || field === '$and' || field === '$not') continue;
1043
1142
  if (typeof condition !== 'object' || condition === null || Array.isArray(condition) || condition instanceof RegExp) {
1044
1143
  if (this._btrees[field]) {
1045
1144
  const indices = this._btrees[field].search(condition);
1046
- if (indices.length === 0) return [];
1047
- return this._applyFilter(this._rowsFromIndices(indices), query);
1145
+ // #44:索引未命中时回落全表过滤——索引键是严格相等的,
1146
+ // 类型/形态不一致的行会被漏掉,不能直接断言「没有数据」。
1147
+ if (indices.length === 0) return this._applyFilter(rows, q);
1148
+ return this._applyFilter(this._rowsFromIndices(indices), q);
1048
1149
  }
1049
1150
  if (this._indexes[field] && this._indexes[field].has(condition)) {
1050
- return this._applyFilter(this._rowsFromIndices(this._indexes[field].get(condition)), query);
1151
+ return this._applyFilter(this._rowsFromIndices(this._indexes[field].get(condition)), q);
1051
1152
  }
1052
- if (this._indexes[field]) return [];
1153
+ if (this._indexes[field]) return this._applyFilter(rows, q);
1053
1154
  }
1054
1155
  if (condition && typeof condition === 'object') {
1055
1156
  if (this._btrees[field] && Array.isArray(condition.$in) && condition.$in.length > 0) {
1056
1157
  const indices = this._btrees[field].searchMany(condition.$in);
1057
- if (indices.length === 0) return [];
1058
- return this._applyFilter(this._rowsFromIndices(indices), query);
1158
+ if (indices.length === 0) return this._applyFilter(rows, q);
1159
+ return this._applyFilter(this._rowsFromIndices(indices), q);
1059
1160
  }
1060
1161
  if (this._btrees[field] && typeof condition.$like === 'string') {
1061
1162
  const prefix = this._likePrefix(condition.$like);
1062
1163
  if (prefix) {
1063
1164
  const indices = this._btrees[field].prefix(prefix);
1064
- if (indices.length === 0) return [];
1065
- return this._applyFilter(this._rowsFromIndices(indices), query);
1165
+ if (indices.length === 0) return this._applyFilter(rows, q);
1166
+ return this._applyFilter(this._rowsFromIndices(indices), q);
1066
1167
  }
1067
1168
  }
1068
1169
  if (this._btrees[field] && (condition.$gt !== undefined || condition.$gte !== undefined ||
@@ -1086,11 +1187,11 @@ class Table {
1086
1187
  } else {
1087
1188
  indices = exclusiveMax ? this._btrees[field].lessThan(max) : this._btrees[field].lessThanEqual(max);
1088
1189
  }
1089
- return this._applyFilter(this._rowsFromIndices(indices), query);
1190
+ return this._applyFilter(this._rowsFromIndices(indices), q);
1090
1191
  }
1091
1192
  }
1092
1193
  }
1093
- return this._applyFilter(rows, query);
1194
+ return this._applyFilter(rows, q);
1094
1195
  }
1095
1196
 
1096
1197
  /**
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "v6.5.29",
3
+ "sha256": "e39295e4bd4f2b705a8b686454c5f2bbe519261bc839810185775146acbadcde",
4
+ "packageVersion": "6.5.29",
5
+ "installedAt": "2026-11-09T04:35:25.960Z",
6
+ "platform": "linux",
7
+ "arch": "x64"
8
+ }