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/README.md +26 -1
- package/bin/jsql +32 -0
- package/index.d.ts +8 -1
- package/index.js +5 -1
- package/lib/ast.js +21 -14
- package/lib/btree.js +14 -8
- package/lib/database.js +5 -1
- package/lib/date-types.js +29 -1
- package/lib/mongo_server.js +21 -5
- package/lib/mysql_compat.js +1 -1
- package/lib/query.js +71 -2
- package/lib/sql.js +248 -13
- package/lib/table.js +139 -38
- package/native/.installed.json +8 -0
- package/native/jsql-neo-native.node +0 -0
- package/package.json +5 -5
- package/postinstall.js +136 -6
- package/test/coverage.js +3 -1
- package/test/finalize-bigint.test.js +55 -0
- package/test/issue-44.test.js +92 -0
- package/test/issue-45.test.js +62 -0
- package/test/issue-46.test.js +42 -0
- package/test/issue-47.test.js +65 -0
- package/test/issue-48.test.js +39 -0
- package/test/issue-50.test.js +83 -0
- package/test/issue-51.test.js +56 -0
- package/test/issue-52.test.js +62 -0
- package/test/issue-53.test.js +50 -0
- package/test/issue-54.test.js +53 -0
- package/test/issue-55.test.js +86 -0
- package/test/issue-56.test.js +70 -0
- package/test/issue-57.test.js +70 -0
- package/test/issue-58.test.js +52 -0
- package/test/native-cli.test.js +122 -0
- package/test/native-install.test.js +93 -0
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
|
-
|
|
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 ||
|
|
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)
|
|
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')
|
|
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)
|
|
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)
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
539
|
-
} else {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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] !==
|
|
553
|
-
this._propagatePKUpdate(
|
|
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(
|
|
639
|
-
} else {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1047
|
-
|
|
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)),
|
|
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),
|
|
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),
|
|
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),
|
|
1190
|
+
return this._applyFilter(this._rowsFromIndices(indices), q);
|
|
1090
1191
|
}
|
|
1091
1192
|
}
|
|
1092
1193
|
}
|
|
1093
|
-
return this._applyFilter(rows,
|
|
1194
|
+
return this._applyFilter(rows, q);
|
|
1094
1195
|
}
|
|
1095
1196
|
|
|
1096
1197
|
/**
|