jsql-neo 6.4.0-beta.8 → 6.4.0
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 +1 -1
- package/index.js +1 -1
- package/lib/btree.js +8 -6
- package/lib/database.js +9 -0
- package/lib/multiserver.js +5 -4
- package/lib/query.js +6 -0
- package/lib/sql.js +589 -14
- package/package.json +2 -2
- package/test/bug-hunt.test.js +344 -0
- package/test/json-functions.test.js +172 -0
package/lib/sql.js
CHANGED
|
@@ -392,6 +392,7 @@ class Parser {
|
|
|
392
392
|
this.pos = 0;
|
|
393
393
|
this.paramCount = 0; // 顺序占位符 ? 的序号(从 0 开始)
|
|
394
394
|
this.subquerySeq = 0; // FROM 子查询无别名时自动生成 __subN
|
|
395
|
+
this.depth = 0; // 子查询递归深度
|
|
395
396
|
}
|
|
396
397
|
|
|
397
398
|
peek(offset = 0) { return this.tokens[this.pos + offset]; }
|
|
@@ -1220,6 +1221,8 @@ class Parser {
|
|
|
1220
1221
|
}
|
|
1221
1222
|
|
|
1222
1223
|
parseSelect() {
|
|
1224
|
+
this.depth++;
|
|
1225
|
+
if (this.depth > 200) throw new Error('Subquery nesting too deep (max 200)');
|
|
1223
1226
|
this.expectKeyword('SELECT');
|
|
1224
1227
|
let distinct = false;
|
|
1225
1228
|
if (this.isKeyword('DISTINCT')) { this.next(); distinct = true; }
|
|
@@ -1390,9 +1393,18 @@ class Parser {
|
|
|
1390
1393
|
if (this.isKeyword('LIMIT')) {
|
|
1391
1394
|
this.next();
|
|
1392
1395
|
limit = this.parseValue();
|
|
1396
|
+
if (typeof limit === 'bigint') limit = Number(limit <= BigInt(Number.MAX_SAFE_INTEGER) ? limit : BigInt(Number.MAX_SAFE_INTEGER));
|
|
1393
1397
|
if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
|
|
1394
|
-
if (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); if (typeof offset === 'number' && offset < 0) throw new Error(`OFFSET must be a non-negative integer, got ${offset}`); }
|
|
1395
|
-
else if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); offset = limit; limit = this.parseValue(); if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`); }
|
|
1398
|
+
if (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); if (typeof offset === 'bigint') offset = Number(offset <= BigInt(Number.MAX_SAFE_INTEGER) ? offset : BigInt(Number.MAX_SAFE_INTEGER)); if (typeof offset === 'number' && offset < 0) throw new Error(`OFFSET must be a non-negative integer, got ${offset}`); }
|
|
1399
|
+
else if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); offset = limit; limit = this.parseValue(); if (typeof limit === 'bigint') limit = Number(limit <= BigInt(Number.MAX_SAFE_INTEGER) ? limit : BigInt(Number.MAX_SAFE_INTEGER)); if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`); }
|
|
1400
|
+
}
|
|
1401
|
+
// OFFSET without LIMIT (MySQL compatible)
|
|
1402
|
+
if (limit === null && this.isKeyword('OFFSET')) {
|
|
1403
|
+
this.next();
|
|
1404
|
+
offset = this.parseValue();
|
|
1405
|
+
if (typeof offset === 'bigint') offset = Number(offset <= BigInt(Number.MAX_SAFE_INTEGER) ? offset : BigInt(Number.MAX_SAFE_INTEGER));
|
|
1406
|
+
if (typeof offset === 'number' && offset < 0) throw new Error(`OFFSET must be a non-negative integer, got ${offset}`);
|
|
1407
|
+
limit = Number.MAX_SAFE_INTEGER;
|
|
1396
1408
|
}
|
|
1397
1409
|
// 集合运算:UNION [ALL|DISTINCT] / INTERSECT [ALL|DISTINCT] / EXCEPT [ALL|DISTINCT]
|
|
1398
1410
|
let union = null, intersect = null, except = null;
|
|
@@ -1421,13 +1433,15 @@ class Parser {
|
|
|
1421
1433
|
if (this.isWord('FIRST') || this.isWord('NEXT')) this.next();
|
|
1422
1434
|
else throw new Error(`Expected FIRST or NEXT after FETCH, got '${this.peek().value}'`);
|
|
1423
1435
|
const nTok = this.next();
|
|
1424
|
-
|
|
1436
|
+
let n = nTok.type === 'number' ? nTok.value : Number(nTok.value);
|
|
1437
|
+
if (typeof n === 'bigint') n = Number(n <= BigInt(Number.MAX_SAFE_INTEGER) ? n : BigInt(Number.MAX_SAFE_INTEGER));
|
|
1425
1438
|
if (!Number.isFinite(n)) throw new Error(`Expected row count after FETCH FIRST, got '${nTok.value}'`);
|
|
1426
1439
|
limit = n;
|
|
1427
1440
|
if (this.isWord('ROW') || this.isWord('ROWS')) this.next();
|
|
1428
1441
|
if (this.isWord('ONLY')) this.next();
|
|
1429
1442
|
}
|
|
1430
1443
|
this.optionalTailSemicolon();
|
|
1444
|
+
this.depth--;
|
|
1431
1445
|
|
|
1432
1446
|
return { type: 'select', columns, aggregate, distinct, from, where, groupBy, having, orderBy, limit, offset, union, intersect, except, rollup };
|
|
1433
1447
|
}
|
|
@@ -1534,6 +1548,7 @@ class Parser {
|
|
|
1534
1548
|
}
|
|
1535
1549
|
let where = null;
|
|
1536
1550
|
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
1551
|
+
else console.warn(`⚠️ UPDATE without WHERE clause — this will modify ALL rows in '${table}'`);
|
|
1537
1552
|
const returning = this.parseOptionalReturning();
|
|
1538
1553
|
this.optionalTailSemicolon();
|
|
1539
1554
|
return { type: 'update', table, assignments, where, returning };
|
|
@@ -1562,6 +1577,7 @@ class Parser {
|
|
|
1562
1577
|
if (this.isKeyword('LIMIT')) {
|
|
1563
1578
|
this.next();
|
|
1564
1579
|
limit = this.parseValue();
|
|
1580
|
+
if (typeof limit === 'bigint') limit = Number(limit <= BigInt(Number.MAX_SAFE_INTEGER) ? limit : BigInt(Number.MAX_SAFE_INTEGER));
|
|
1565
1581
|
if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
|
|
1566
1582
|
}
|
|
1567
1583
|
const returning = this.parseOptionalReturning();
|
|
@@ -2892,7 +2908,36 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2892
2908
|
}
|
|
2893
2909
|
// P1-19: ON DUPLICATE KEY UPDATE 表达式赋值,如 count = count + 1
|
|
2894
2910
|
case 'binary': {
|
|
2895
|
-
|
|
2911
|
+
// binary(含算术/位运算)直接求值,不走递归(避免无限递归)
|
|
2912
|
+
// left/right 可能是原始值(数字/字符串)或 AST 节点;
|
|
2913
|
+
// 字符串在这里是列名引用(如 cnt),需用 resolveOperand 解析。
|
|
2914
|
+
const resolve = (v) => v != null && typeof v === 'object' ? resolveOperand(v, row, ctx) : (typeof v === 'string' ? resolveOperand({ type: 'column', name: v }, row, ctx) : v);
|
|
2915
|
+
const l = resolve(operand.left);
|
|
2916
|
+
const r = resolve(operand.right);
|
|
2917
|
+
if (l === null || r === null || l === undefined || r === undefined) return null;
|
|
2918
|
+
switch (operand.op) {
|
|
2919
|
+
case '+': {
|
|
2920
|
+
if (typeof l === 'number' || typeof r === 'number') {
|
|
2921
|
+
const ln = numericIfNumeric(l), rn = numericIfNumeric(r);
|
|
2922
|
+
return (ln === null ? 0 : ln) + (rn === null ? 0 : rn);
|
|
2923
|
+
}
|
|
2924
|
+
const ln = numericIfNumeric(l), rn = numericIfNumeric(r);
|
|
2925
|
+
if (ln !== null) return ln + (rn === null ? 0 : rn);
|
|
2926
|
+
if (rn !== null) return rn;
|
|
2927
|
+
return 0;
|
|
2928
|
+
}
|
|
2929
|
+
case '-': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln - rn; }
|
|
2930
|
+
case '*': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln * rn; }
|
|
2931
|
+
case '/': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) || rn === 0 ? null : ln / rn; }
|
|
2932
|
+
case '%': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) || rn === 0 ? null : ln % rn; }
|
|
2933
|
+
case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
|
|
2934
|
+
case '&': return (Number(l) | 0) & (Number(r) | 0);
|
|
2935
|
+
case '|': return (Number(l) | 0) | (Number(r) | 0);
|
|
2936
|
+
case '^': return (Number(l) | 0) ^ (Number(r) | 0);
|
|
2937
|
+
case '<<': return Number(l) * Math.pow(2, Number(r));
|
|
2938
|
+
case '>>': return Math.floor(Number(l) / Math.pow(2, Number(r)));
|
|
2939
|
+
default: return null;
|
|
2940
|
+
}
|
|
2896
2941
|
}
|
|
2897
2942
|
case 'func':
|
|
2898
2943
|
return applyScalarFunction(operand, row, ctx);
|
|
@@ -3605,6 +3650,428 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
3605
3650
|
return v.toString(16);
|
|
3606
3651
|
});
|
|
3607
3652
|
case 'DATABASE': case 'SCHEMA': return row && row.__db !== undefined ? row.__db : 'default';
|
|
3653
|
+
// ===== JSON 函数(B2 修复:此前未实现,静默返回 NULL)=====
|
|
3654
|
+
// 辅助函数:获取 JSON 值的类型
|
|
3655
|
+
function getJsonType(val) {
|
|
3656
|
+
if (val === null) return 'null';
|
|
3657
|
+
if (Array.isArray(val)) return 'array';
|
|
3658
|
+
if (typeof val === 'object') return 'object';
|
|
3659
|
+
if (typeof val === 'string') return 'string';
|
|
3660
|
+
if (typeof val === 'number') return Number.isInteger(val) ? 'integer' : 'number';
|
|
3661
|
+
if (typeof val === 'boolean') return 'boolean';
|
|
3662
|
+
return 'unknown';
|
|
3663
|
+
}
|
|
3664
|
+
case 'JSON_OBJECT': {
|
|
3665
|
+
// JSON_OBJECT(k1, v1, k2, v2, ...) → { k1: v1, k2: v2 }
|
|
3666
|
+
const obj = {};
|
|
3667
|
+
for (let i = 0; i + 1 < args.length; i += 2) {
|
|
3668
|
+
const k = String(args[i] == null ? '' : args[i]);
|
|
3669
|
+
const v = args[i + 1];
|
|
3670
|
+
obj[k] = v === undefined ? null : v;
|
|
3671
|
+
}
|
|
3672
|
+
return obj;
|
|
3673
|
+
}
|
|
3674
|
+
case 'JSON_ARRAY': {
|
|
3675
|
+
// JSON_ARRAY(v1, v2, ...) → [v1, v2]
|
|
3676
|
+
return args.map(v => v === undefined ? null : v);
|
|
3677
|
+
}
|
|
3678
|
+
case 'JSON_EXTRACT': {
|
|
3679
|
+
// JSON_EXTRACT(json, '$.key' or '$.a.b') → 值
|
|
3680
|
+
const json = args[0];
|
|
3681
|
+
const path = args[1] == null ? '' : String(args[1]);
|
|
3682
|
+
if (json == null || path == null) return null;
|
|
3683
|
+
// 解析 JSON 字符串或对象
|
|
3684
|
+
let data = json;
|
|
3685
|
+
if (typeof json === 'string') {
|
|
3686
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3687
|
+
}
|
|
3688
|
+
// 解析 JSONPath: $.key 或 $.a.b 或 $.a[0].b
|
|
3689
|
+
const m = path.match(/^\$\.?([^.\[]+)/);
|
|
3690
|
+
if (!m) {
|
|
3691
|
+
// 尝试 $ 开头后直接跟 key
|
|
3692
|
+
if (path === '$') return data;
|
|
3693
|
+
return null;
|
|
3694
|
+
}
|
|
3695
|
+
const firstKey = m[1];
|
|
3696
|
+
if (firstKey === '') return data;
|
|
3697
|
+
let current = data;
|
|
3698
|
+
// 逐段解析路径
|
|
3699
|
+
const segments = path.replace(/^\$\.?/, '').split('.');
|
|
3700
|
+
for (const seg of segments) {
|
|
3701
|
+
if (current == null) return null;
|
|
3702
|
+
// 处理数组索引: key[0] 或 [0]
|
|
3703
|
+
const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3704
|
+
if (arrMatch) {
|
|
3705
|
+
const key = arrMatch[1];
|
|
3706
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
3707
|
+
if (key) current = current[key];
|
|
3708
|
+
if (Array.isArray(current)) current = current[idx];
|
|
3709
|
+
else return null;
|
|
3710
|
+
} else {
|
|
3711
|
+
if (typeof current !== 'object' || current === null) return null;
|
|
3712
|
+
current = current[seg];
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
return current === undefined ? null : current;
|
|
3716
|
+
}
|
|
3717
|
+
case 'JSON_KEYS': {
|
|
3718
|
+
// JSON_KEYS(json) → [key1, key2, ...]
|
|
3719
|
+
const json = args[0];
|
|
3720
|
+
if (json == null) return null;
|
|
3721
|
+
let data = json;
|
|
3722
|
+
if (typeof json === 'string') {
|
|
3723
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3724
|
+
}
|
|
3725
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) return null;
|
|
3726
|
+
return Object.keys(data);
|
|
3727
|
+
}
|
|
3728
|
+
case 'JSON_LENGTH': {
|
|
3729
|
+
// JSON_LENGTH(json[, path]) → 元素数/长度
|
|
3730
|
+
const json = args[0];
|
|
3731
|
+
const path = args[1];
|
|
3732
|
+
if (json == null) return null;
|
|
3733
|
+
let data = json;
|
|
3734
|
+
if (typeof json === 'string') {
|
|
3735
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3736
|
+
}
|
|
3737
|
+
if (path != null) {
|
|
3738
|
+
// 按路径提取后取长度
|
|
3739
|
+
const p = String(path).replace(/^\$\.?/, '');
|
|
3740
|
+
if (p === '') return Array.isArray(data) ? data.length : (typeof data === 'object' && data !== null ? Object.keys(data).length : 0);
|
|
3741
|
+
let current = data;
|
|
3742
|
+
for (const seg of p.split('.')) {
|
|
3743
|
+
if (current == null) return null;
|
|
3744
|
+
const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3745
|
+
if (arrMatch) {
|
|
3746
|
+
const key = arrMatch[1];
|
|
3747
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
3748
|
+
if (key) current = current[key];
|
|
3749
|
+
if (Array.isArray(current)) current = current[idx];
|
|
3750
|
+
else return null;
|
|
3751
|
+
} else {
|
|
3752
|
+
if (typeof current !== 'object' || current === null) return null;
|
|
3753
|
+
current = current[seg];
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
if (current == null) return null;
|
|
3757
|
+
if (Array.isArray(current)) return current.length;
|
|
3758
|
+
if (typeof current === 'object') return Object.keys(current).length;
|
|
3759
|
+
if (typeof current === 'string') return current.length;
|
|
3760
|
+
return 1;
|
|
3761
|
+
}
|
|
3762
|
+
if (Array.isArray(data)) return data.length;
|
|
3763
|
+
if (typeof data === 'object' && data !== null) return Object.keys(data).length;
|
|
3764
|
+
if (typeof data === 'string') return data.length;
|
|
3765
|
+
return 1;
|
|
3766
|
+
}
|
|
3767
|
+
case 'JSON_MERGE': case 'JSON_MERGE_PRESERVE': {
|
|
3768
|
+
// JSON_MERGE_PRESERVE(json1, json2, ...) → 合并后的 JSON
|
|
3769
|
+
const items = args.filter(a => a != null);
|
|
3770
|
+
if (!items.length) return null;
|
|
3771
|
+
const parsed = items.map(item => {
|
|
3772
|
+
if (typeof item === 'string') { try { return JSON.parse(item); } catch (e) { return null; } }
|
|
3773
|
+
return item;
|
|
3774
|
+
}).filter(a => a != null);
|
|
3775
|
+
if (!parsed.length) return null;
|
|
3776
|
+
// 递归合并
|
|
3777
|
+
function mergePreserve(target, source) {
|
|
3778
|
+
if (Array.isArray(target) && Array.isArray(source)) {
|
|
3779
|
+
return [...target, ...source];
|
|
3780
|
+
}
|
|
3781
|
+
if (typeof target === 'object' && target !== null && typeof source === 'object' && source !== null && !Array.isArray(target) && !Array.isArray(source)) {
|
|
3782
|
+
const result = { ...target };
|
|
3783
|
+
for (const k of Object.keys(source)) {
|
|
3784
|
+
if (k in result) result[k] = mergePreserve(result[k], source[k]);
|
|
3785
|
+
else result[k] = source[k];
|
|
3786
|
+
}
|
|
3787
|
+
return result;
|
|
3788
|
+
}
|
|
3789
|
+
return source;
|
|
3790
|
+
}
|
|
3791
|
+
return parsed.reduce((acc, cur) => mergePreserve(acc, cur));
|
|
3792
|
+
}
|
|
3793
|
+
case 'JSON_MERGE_PATCH': {
|
|
3794
|
+
// JSON_MERGE_PATCH(json1, json2, ...) → RFC 7396 合并
|
|
3795
|
+
const items = args.filter(a => a != null);
|
|
3796
|
+
if (!items.length) return null;
|
|
3797
|
+
const parsed = items.map(item => {
|
|
3798
|
+
if (typeof item === 'string') { try { return JSON.parse(item); } catch (e) { return null; } }
|
|
3799
|
+
return item;
|
|
3800
|
+
}).filter(a => a != null);
|
|
3801
|
+
if (!parsed.length) return null;
|
|
3802
|
+
function mergePatch(target, source) {
|
|
3803
|
+
if (typeof source !== 'object' || source === null || Array.isArray(source)) return source;
|
|
3804
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target)) return { ...source };
|
|
3805
|
+
const result = { ...target };
|
|
3806
|
+
for (const k of Object.keys(source)) {
|
|
3807
|
+
if (source[k] === null) delete result[k];
|
|
3808
|
+
else result[k] = mergePatch(result[k], source[k]);
|
|
3809
|
+
}
|
|
3810
|
+
return result;
|
|
3811
|
+
}
|
|
3812
|
+
return parsed.reduce((acc, cur) => mergePatch(acc, cur));
|
|
3813
|
+
}
|
|
3814
|
+
case 'JSON_OBJECTAGG': {
|
|
3815
|
+
// JSON_OBJECTAGG(key, value) → { key: value } (聚合函数)
|
|
3816
|
+
// 在 JSQL 中作为普通函数调用,接收 key 和 value
|
|
3817
|
+
const key = args[0];
|
|
3818
|
+
const value = args[1];
|
|
3819
|
+
if (key == null) return null;
|
|
3820
|
+
const obj = {};
|
|
3821
|
+
obj[String(key)] = value === undefined ? null : value;
|
|
3822
|
+
return obj;
|
|
3823
|
+
}
|
|
3824
|
+
case 'JSON_ARRAYAGG': {
|
|
3825
|
+
// JSON_ARRAYAGG(expr) → [expr] (聚合函数)
|
|
3826
|
+
// 在 JSQL 中作为普通函数调用,接收单个值
|
|
3827
|
+
const val = args[0];
|
|
3828
|
+
return [val === undefined ? null : val];
|
|
3829
|
+
}
|
|
3830
|
+
case 'JSON_SET': {
|
|
3831
|
+
// JSON_SET(json, path, val, ...) → 设置值(路径存在则更新,不存在则创建)
|
|
3832
|
+
const json = args[0];
|
|
3833
|
+
if (json == null) return null;
|
|
3834
|
+
let data = json;
|
|
3835
|
+
if (typeof json === 'string') {
|
|
3836
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3837
|
+
}
|
|
3838
|
+
// 深拷贝
|
|
3839
|
+
data = JSON.parse(JSON.stringify(data));
|
|
3840
|
+
for (let i = 1; i + 1 < args.length; i += 2) {
|
|
3841
|
+
const path = String(args[i]);
|
|
3842
|
+
const val = args[i + 1];
|
|
3843
|
+
const p = path.replace(/^\$\.?/, '');
|
|
3844
|
+
if (p === '') continue;
|
|
3845
|
+
const segments = p.split('.');
|
|
3846
|
+
let current = data;
|
|
3847
|
+
for (let j = 0; j < segments.length - 1; j++) {
|
|
3848
|
+
const seg = segments[j];
|
|
3849
|
+
const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3850
|
+
if (arrMatch) {
|
|
3851
|
+
const key = arrMatch[1];
|
|
3852
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
3853
|
+
if (key) current = current[key];
|
|
3854
|
+
if (Array.isArray(current)) current = current[idx];
|
|
3855
|
+
else { current = null; break; }
|
|
3856
|
+
} else {
|
|
3857
|
+
if (typeof current !== 'object' || current === null) { current = null; break; }
|
|
3858
|
+
if (!(seg in current)) current[seg] = {};
|
|
3859
|
+
current = current[seg];
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
if (current == null) continue;
|
|
3863
|
+
const last = segments[segments.length - 1];
|
|
3864
|
+
const arrMatch = last.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3865
|
+
if (arrMatch) {
|
|
3866
|
+
const key = arrMatch[1];
|
|
3867
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
3868
|
+
if (key) current = current[key];
|
|
3869
|
+
if (Array.isArray(current)) current[idx] = val;
|
|
3870
|
+
} else {
|
|
3871
|
+
current[last] = val;
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
return data;
|
|
3875
|
+
}
|
|
3876
|
+
case 'JSON_INSERT': {
|
|
3877
|
+
// JSON_INSERT(json, path, val, ...) → 仅路径不存在时插入
|
|
3878
|
+
const json = args[0];
|
|
3879
|
+
if (json == null) return null;
|
|
3880
|
+
let data = json;
|
|
3881
|
+
if (typeof json === 'string') {
|
|
3882
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3883
|
+
}
|
|
3884
|
+
data = JSON.parse(JSON.stringify(data));
|
|
3885
|
+
for (let i = 1; i + 1 < args.length; i += 2) {
|
|
3886
|
+
const path = String(args[i]);
|
|
3887
|
+
const val = args[i + 1];
|
|
3888
|
+
const p = path.replace(/^\$\.?/, '');
|
|
3889
|
+
if (p === '') continue;
|
|
3890
|
+
const segments = p.split('.');
|
|
3891
|
+
let current = data;
|
|
3892
|
+
let exists = true;
|
|
3893
|
+
for (let j = 0; j < segments.length - 1; j++) {
|
|
3894
|
+
const seg = segments[j];
|
|
3895
|
+
const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3896
|
+
if (arrMatch) {
|
|
3897
|
+
const key = arrMatch[1];
|
|
3898
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
3899
|
+
if (key) current = current[key];
|
|
3900
|
+
if (Array.isArray(current)) current = current[idx];
|
|
3901
|
+
else { exists = false; break; }
|
|
3902
|
+
} else {
|
|
3903
|
+
if (typeof current !== 'object' || current === null || !(seg in current)) { exists = false; break; }
|
|
3904
|
+
current = current[seg];
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
if (!exists) continue;
|
|
3908
|
+
const last = segments[segments.length - 1];
|
|
3909
|
+
if (typeof current === 'object' && current !== null && !(last in current)) {
|
|
3910
|
+
current[last] = val;
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
return data;
|
|
3914
|
+
}
|
|
3915
|
+
case 'JSON_REMOVE': {
|
|
3916
|
+
// JSON_REMOVE(json, path, ...) → 删除路径
|
|
3917
|
+
const json = args[0];
|
|
3918
|
+
if (json == null) return null;
|
|
3919
|
+
let data = json;
|
|
3920
|
+
if (typeof json === 'string') {
|
|
3921
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3922
|
+
}
|
|
3923
|
+
data = JSON.parse(JSON.stringify(data));
|
|
3924
|
+
for (let i = 1; i < args.length; i++) {
|
|
3925
|
+
const path = String(args[i]);
|
|
3926
|
+
const p = path.replace(/^\$\.?/, '');
|
|
3927
|
+
if (p === '') continue;
|
|
3928
|
+
const segments = p.split('.');
|
|
3929
|
+
let current = data;
|
|
3930
|
+
for (let j = 0; j < segments.length - 1; j++) {
|
|
3931
|
+
const seg = segments[j];
|
|
3932
|
+
const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3933
|
+
if (arrMatch) {
|
|
3934
|
+
const key = arrMatch[1];
|
|
3935
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
3936
|
+
if (key) current = current[key];
|
|
3937
|
+
if (Array.isArray(current)) current = current[idx];
|
|
3938
|
+
else { current = null; break; }
|
|
3939
|
+
} else {
|
|
3940
|
+
if (typeof current !== 'object' || current === null) { current = null; break; }
|
|
3941
|
+
current = current[seg];
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
if (current == null) continue;
|
|
3945
|
+
const last = segments[segments.length - 1];
|
|
3946
|
+
const arrMatch = last.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3947
|
+
if (arrMatch) {
|
|
3948
|
+
const key = arrMatch[1];
|
|
3949
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
3950
|
+
if (key) current = current[key];
|
|
3951
|
+
if (Array.isArray(current)) current.splice(idx, 1);
|
|
3952
|
+
} else {
|
|
3953
|
+
delete current[last];
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
return data;
|
|
3957
|
+
}
|
|
3958
|
+
case 'JSON_UNQUOTE': {
|
|
3959
|
+
// JSON_UNQUOTE(json_val) → 去掉 JSON 字符串的引号
|
|
3960
|
+
const val = args[0];
|
|
3961
|
+
if (val == null) return null;
|
|
3962
|
+
if (typeof val === 'string') {
|
|
3963
|
+
// 如果是 JSON 字符串(带引号),解析它
|
|
3964
|
+
const trimmed = val.trim();
|
|
3965
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
3966
|
+
try { return JSON.parse(trimmed); } catch (e) { return trimmed.slice(1, -1); }
|
|
3967
|
+
}
|
|
3968
|
+
return val;
|
|
3969
|
+
}
|
|
3970
|
+
return val;
|
|
3971
|
+
}
|
|
3972
|
+
case 'JSON_QUOTE': {
|
|
3973
|
+
// JSON_QUOTE(str) → 给字符串加引号
|
|
3974
|
+
const val = args[0];
|
|
3975
|
+
if (val == null) return null;
|
|
3976
|
+
return JSON.stringify(String(val));
|
|
3977
|
+
}
|
|
3978
|
+
case 'JSON_TYPE': {
|
|
3979
|
+
// JSON_TYPE(json[, path]) → 返回类型
|
|
3980
|
+
const json = args[0];
|
|
3981
|
+
const path = args[1];
|
|
3982
|
+
if (json == null) return null;
|
|
3983
|
+
let data = json;
|
|
3984
|
+
if (typeof json === 'string') {
|
|
3985
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3986
|
+
}
|
|
3987
|
+
if (path != null) {
|
|
3988
|
+
const p = String(path).replace(/^\$\.?/, '');
|
|
3989
|
+
if (p === '') return getJsonType(data);
|
|
3990
|
+
let current = data;
|
|
3991
|
+
for (const seg of p.split('.')) {
|
|
3992
|
+
if (current == null) return null;
|
|
3993
|
+
const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3994
|
+
if (arrMatch) {
|
|
3995
|
+
const key = arrMatch[1];
|
|
3996
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
3997
|
+
if (key) current = current[key];
|
|
3998
|
+
if (Array.isArray(current)) current = current[idx];
|
|
3999
|
+
else return null;
|
|
4000
|
+
} else {
|
|
4001
|
+
if (typeof current !== 'object' || current === null) return null;
|
|
4002
|
+
current = current[seg];
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
return getJsonType(current);
|
|
4006
|
+
}
|
|
4007
|
+
return getJsonType(data);
|
|
4008
|
+
}
|
|
4009
|
+
case 'JSON_DEPTH': {
|
|
4010
|
+
// JSON_DEPTH(json) → 返回深度
|
|
4011
|
+
const json = args[0];
|
|
4012
|
+
if (json == null) return null;
|
|
4013
|
+
let data = json;
|
|
4014
|
+
if (typeof json === 'string') {
|
|
4015
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
4016
|
+
}
|
|
4017
|
+
function depth(obj) {
|
|
4018
|
+
if (typeof obj !== 'object' || obj === null) return 0;
|
|
4019
|
+
if (Array.isArray(obj)) {
|
|
4020
|
+
if (obj.length === 0) return 1;
|
|
4021
|
+
return 1 + Math.max(...obj.map(depth));
|
|
4022
|
+
}
|
|
4023
|
+
const keys = Object.keys(obj);
|
|
4024
|
+
if (keys.length === 0) return 1;
|
|
4025
|
+
return 1 + Math.max(...keys.map(k => depth(obj[k])));
|
|
4026
|
+
}
|
|
4027
|
+
return depth(data);
|
|
4028
|
+
}
|
|
4029
|
+
case 'JSON_VALID': {
|
|
4030
|
+
// JSON_VALID(json) → 1 if valid JSON, 0 otherwise
|
|
4031
|
+
const val = args[0];
|
|
4032
|
+
if (val == null) return 0;
|
|
4033
|
+
if (typeof val !== 'string') return 1; // 非字符串视为有效
|
|
4034
|
+
try { JSON.parse(val); return 1; } catch (e) { return 0; }
|
|
4035
|
+
}
|
|
4036
|
+
case 'JSON_SEARCH': {
|
|
4037
|
+
// JSON_SEARCH(json, one_or_all, search_str) → 搜索路径
|
|
4038
|
+
const json = args[0];
|
|
4039
|
+
const mode = args[1];
|
|
4040
|
+
const search = args[2];
|
|
4041
|
+
if (json == null || search == null) return null;
|
|
4042
|
+
let data = json;
|
|
4043
|
+
if (typeof json === 'string') {
|
|
4044
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
4045
|
+
}
|
|
4046
|
+
const searchStr = String(search).toLowerCase();
|
|
4047
|
+
const results = [];
|
|
4048
|
+
function searchObj(obj, currentPath) {
|
|
4049
|
+
if (typeof obj === 'string' && obj.toLowerCase().includes(searchStr)) {
|
|
4050
|
+
results.push(currentPath);
|
|
4051
|
+
} else if (Array.isArray(obj)) {
|
|
4052
|
+
obj.forEach((item, i) => searchObj(item, currentPath + '[' + i + ']'));
|
|
4053
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
4054
|
+
for (const k of Object.keys(obj)) {
|
|
4055
|
+
searchObj(obj[k], currentPath ? currentPath + '.' + k : '$.' + k);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
searchObj(data, '$');
|
|
4060
|
+
if (results.length === 0) return null;
|
|
4061
|
+
if (mode === 'one') return results[0];
|
|
4062
|
+
return results;
|
|
4063
|
+
}
|
|
4064
|
+
case 'JSON_CONTAINS': {
|
|
4065
|
+
// JSON_CONTAINS(json, value) → 1 or 0
|
|
4066
|
+
const json = args[0];
|
|
4067
|
+
const val = args[1];
|
|
4068
|
+
if (json == null) return null;
|
|
4069
|
+
let data = json;
|
|
4070
|
+
if (typeof json === 'string') {
|
|
4071
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
4072
|
+
}
|
|
4073
|
+
return JSON.stringify(data).includes(JSON.stringify(val)) ? 1 : 0;
|
|
4074
|
+
}
|
|
3608
4075
|
default:
|
|
3609
4076
|
return null;
|
|
3610
4077
|
}
|
|
@@ -3785,10 +4252,14 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
3785
4252
|
}
|
|
3786
4253
|
case 'in': {
|
|
3787
4254
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
4255
|
+
if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
|
|
3788
4256
|
if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
|
|
3789
|
-
if (!expr.list) return false;
|
|
4257
|
+
if (!expr.list) return expr.not ? true : false;
|
|
4258
|
+
// 列表中出现 NULL 时:三值逻辑 → NULL IN (1,2,NULL) 为 NULL(UNKNOWN)
|
|
4259
|
+
const hasNull = expr.list.some(x => x === null || x === undefined);
|
|
4260
|
+
if (hasNull) return null;
|
|
3790
4261
|
const r = expr.list.some(x => x === v || String(x) === String(v));
|
|
3791
|
-
return expr.not ? !r : r;
|
|
4262
|
+
return expr.not ? !r : r;
|
|
3792
4263
|
}
|
|
3793
4264
|
case 'like': {
|
|
3794
4265
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
@@ -3842,6 +4313,38 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
3842
4313
|
}
|
|
3843
4314
|
return expr.elseVal ? resolveOperand(expr.elseVal, row, ctx) : null;
|
|
3844
4315
|
}
|
|
4316
|
+
case 'binary': {
|
|
4317
|
+
// 算术/位运算表达式(如 ON DUPLICATE KEY UPDATE cnt = cnt + 1)
|
|
4318
|
+
// 注意:二进制比较运算符(=, <, > 等)走的是 case 'compare',不会到这里
|
|
4319
|
+
// 复用 resolveOperand 的 case 'binary' 逻辑
|
|
4320
|
+
const resolve = (v) => v != null && typeof v === 'object' ? resolveOperand(v, row, ctx) : (typeof v === 'string' ? resolveOperand({ type: 'column', name: v }, row, ctx) : v);
|
|
4321
|
+
const l = resolve(expr.left);
|
|
4322
|
+
const r = resolve(expr.right);
|
|
4323
|
+
if (l === null || r === null || l === undefined || r === undefined) return null;
|
|
4324
|
+
switch (expr.op) {
|
|
4325
|
+
case '+': {
|
|
4326
|
+
if (typeof l === 'number' || typeof r === 'number') {
|
|
4327
|
+
const ln = numericIfNumeric(l), rn = numericIfNumeric(r);
|
|
4328
|
+
return (ln === null ? 0 : ln) + (rn === null ? 0 : rn);
|
|
4329
|
+
}
|
|
4330
|
+
const ln = numericIfNumeric(l), rn = numericIfNumeric(r);
|
|
4331
|
+
if (ln !== null) return ln + (rn === null ? 0 : rn);
|
|
4332
|
+
if (rn !== null) return rn;
|
|
4333
|
+
return 0;
|
|
4334
|
+
}
|
|
4335
|
+
case '-': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln - rn; }
|
|
4336
|
+
case '*': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln * rn; }
|
|
4337
|
+
case '/': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) || rn === 0 ? null : ln / rn; }
|
|
4338
|
+
case '%': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) || rn === 0 ? null : ln % rn; }
|
|
4339
|
+
case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
|
|
4340
|
+
case '&': return (Number(l) | 0) & (Number(r) | 0);
|
|
4341
|
+
case '|': return (Number(l) | 0) | (Number(r) | 0);
|
|
4342
|
+
case '^': return (Number(l) | 0) ^ (Number(r) | 0);
|
|
4343
|
+
case '<<': return Number(l) * Math.pow(2, Number(r));
|
|
4344
|
+
case '>>': return Math.floor(Number(l) / Math.pow(2, Number(r)));
|
|
4345
|
+
default: return null;
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
3845
4348
|
default: return false;
|
|
3846
4349
|
}
|
|
3847
4350
|
}
|
|
@@ -4329,6 +4832,12 @@ class SQLExecutor {
|
|
|
4329
4832
|
} else if (conflicts.length > 0 && conflictAction === 'update') {
|
|
4330
4833
|
for (const { d, existingId } of conflicts) {
|
|
4331
4834
|
const data = {};
|
|
4835
|
+
// 查找冲突行的「旧版本」—— 用于 ON DUPLICATE KEY UPDATE 表达式
|
|
4836
|
+
// 如 cnt = cnt + 1,需要用旧行的 cnt 值(0),不是新行的 cnt(10)
|
|
4837
|
+
const existingRow = all.find(r => {
|
|
4838
|
+
const keys = pkCols.map(c => r[c]).filter(v => v !== undefined && v !== null);
|
|
4839
|
+
return keys.length === pkCols.length && keys.every((v, i) => String(v) === String(existingId[i]));
|
|
4840
|
+
});
|
|
4332
4841
|
if (conflictSets) {
|
|
4333
4842
|
for (const [col, val] of conflictSets) {
|
|
4334
4843
|
// VALUES(col) 引用本次本应插入的值;EXCLUDED.col 为 PG 同义写法
|
|
@@ -4338,7 +4847,11 @@ class SQLExecutor {
|
|
|
4338
4847
|
data[col] = d[val._excludedOf] !== undefined ? d[val._excludedOf] : null;
|
|
4339
4848
|
} else if (val && typeof val === 'object' && val.type === 'binary') {
|
|
4340
4849
|
// P1-19: ON DUPLICATE KEY UPDATE 支持表达式赋值,如 count = count + 1
|
|
4341
|
-
|
|
4850
|
+
// 算术/位运算表达式需要从旧行取值:用 existingRow 替换 d 中的同名键
|
|
4851
|
+
const evalRow = existingRow
|
|
4852
|
+
? Object.fromEntries([...Object.entries(d), ...Object.entries(existingRow)])
|
|
4853
|
+
: d;
|
|
4854
|
+
data[col] = evaluateExpr(val, evalRow, this.ctx);
|
|
4342
4855
|
} else {
|
|
4343
4856
|
data[col] = val;
|
|
4344
4857
|
}
|
|
@@ -6160,7 +6673,30 @@ class SQLExecutor {
|
|
|
6160
6673
|
all = [{ _virtual: true }];
|
|
6161
6674
|
} else {
|
|
6162
6675
|
const items = statement.from.tables.concat(statement.from.joins.map(j => j.item));
|
|
6163
|
-
|
|
6676
|
+
// B5 修复:自连接(同表多次出现且无别名)时,用 t1/t2 等自动别名(MySQL 惯例),
|
|
6677
|
+
// 而非直接用表名。不同表无别名时仍用表名,保持与 SQL 查询中的表名限定一致。
|
|
6678
|
+
const tableCount = new Map();
|
|
6679
|
+
for (const item of items) {
|
|
6680
|
+
if (item.table) {
|
|
6681
|
+
const c = (tableCount.get(item.table) || 0) + 1;
|
|
6682
|
+
tableCount.set(item.table, c);
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
const autoAliasMap = new Map();
|
|
6686
|
+
let autoIdx = 0;
|
|
6687
|
+
const prefix = item => {
|
|
6688
|
+
if (item.alias) return item.alias;
|
|
6689
|
+
if (item.subquery) return item.subquery.alias || item.alias;
|
|
6690
|
+
// 无别名且同表出现多次时生成 t1, t2;否则用表名
|
|
6691
|
+
if (item.table && (tableCount.get(item.table) || 0) > 1) {
|
|
6692
|
+
if (!autoAliasMap.has(item)) {
|
|
6693
|
+
autoIdx++;
|
|
6694
|
+
autoAliasMap.set(item, `t${autoIdx}`);
|
|
6695
|
+
}
|
|
6696
|
+
return autoAliasMap.get(item);
|
|
6697
|
+
}
|
|
6698
|
+
return item.table;
|
|
6699
|
+
};
|
|
6164
6700
|
|
|
6165
6701
|
// 读第一表
|
|
6166
6702
|
const firstItem = statement.from.tables[0];
|
|
@@ -6191,10 +6727,10 @@ class SQLExecutor {
|
|
|
6191
6727
|
// 递归 CTE 迭代中:自身引用绑定到已累积的行
|
|
6192
6728
|
rowsAll = { rows: firstItem._virtualRows.map(r => Object.assign({}, r)), schema: null, columns: null };
|
|
6193
6729
|
} else {
|
|
6194
|
-
rowsAll = await this._readTable(firstItem.table, statement, firstItem
|
|
6730
|
+
rowsAll = await this._readTable(firstItem.table, statement, prefix(firstItem));
|
|
6195
6731
|
}
|
|
6196
6732
|
schema = rowsAll.schema;
|
|
6197
|
-
const firstPrefix = firstItem
|
|
6733
|
+
const firstPrefix = prefix(firstItem);
|
|
6198
6734
|
// 虚拟行分两种,语义不同:
|
|
6199
6735
|
// - _virtualRows:递归 CTE **迭代过程内部**,FROM 是自身引用,列名不带前缀
|
|
6200
6736
|
// (否则递归项里的 `n+1` 取不到值)
|
|
@@ -6222,8 +6758,8 @@ class SQLExecutor {
|
|
|
6222
6758
|
? { rows: (j.item._virtualRows || j.item.subquery._virtualSource).map(r => Object.assign({}, r)) }
|
|
6223
6759
|
: j.item.subquery
|
|
6224
6760
|
? await this.executeSelect(j.item.subquery)
|
|
6225
|
-
: await this._readTable(j.item.table, statement, j.item
|
|
6226
|
-
const rightPrefix = j.item
|
|
6761
|
+
: await this._readTable(j.item.table, statement, prefix(j.item));
|
|
6762
|
+
const rightPrefix = prefix(j.item);
|
|
6227
6763
|
// 迭代内部的虚拟行不加前缀;物化后的派生表要加(与普通子查询一致)
|
|
6228
6764
|
const jRaw = j.item._virtualRows
|
|
6229
6765
|
? j.item._virtualRows
|
|
@@ -6290,9 +6826,39 @@ class SQLExecutor {
|
|
|
6290
6826
|
|
|
6291
6827
|
// 分组聚合
|
|
6292
6828
|
if (statement.groupBy) {
|
|
6829
|
+
// MySQL 支持 GROUP BY alias,而 alias 指向 SELECT 列表的输出列名。
|
|
6830
|
+
// 解析时 groupBy[] 存的是原始字符串(如 "b"),必须先在 SELECT 别名里查找:
|
|
6831
|
+
// - 找到 → 取输出行里的 alias 值(因为 alias 已求出值了)
|
|
6832
|
+
// - 找不到 → 当列名取输入行里对应列的值(标准 SQL 行为)
|
|
6833
|
+
// 此前全用 resolveOperand({type:'column', name:g}) 按列名取,导致 GROUP BY b
|
|
6834
|
+
// 取 row['b'],而原始行里没有 b 列(b 是别名),全返回 undefined 并成一组。
|
|
6835
|
+
const aliases = {}; // alias名字 → true(存在性检查用)
|
|
6836
|
+
const aliasToCol = {}; // alias名字 → 对应的表达式(用于从原始行取值)
|
|
6837
|
+
if (statement.columns) {
|
|
6838
|
+
for (const c of statement.columns) {
|
|
6839
|
+
if (c.alias) {
|
|
6840
|
+
aliases[c.alias.toLowerCase()] = true;
|
|
6841
|
+
// 别名指向的表达式:尝试从 expr/scalar 里提取列名
|
|
6842
|
+
const src = c.expr || c.scalar;
|
|
6843
|
+
// expr 可能是字符串 'a'(列名)或对象 {type:'column', name:'a'}
|
|
6844
|
+
if (typeof src === 'string') aliasToCol[c.alias.toLowerCase()] = src;
|
|
6845
|
+
else if (src && src.type === 'column') aliasToCol[c.alias.toLowerCase()] = src.name;
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6293
6849
|
const groups = new Map();
|
|
6294
6850
|
for (const row of rows) {
|
|
6295
|
-
const key = JSON.stringify(statement.groupBy.map(g =>
|
|
6851
|
+
const key = JSON.stringify(statement.groupBy.map(g => {
|
|
6852
|
+
const key = String(g).toLowerCase();
|
|
6853
|
+
if (aliases[key]) {
|
|
6854
|
+
// 优先用行中已有的 alias 值(投影后才有),没有则尝试从原始列取值
|
|
6855
|
+
if (row[key] !== undefined) return row[key];
|
|
6856
|
+
// alias → 原始列名 的映射(如 b → a),从原始行取该列的值
|
|
6857
|
+
if (aliasToCol[key]) return resolveOperand({ type: 'column', name: aliasToCol[key] }, row, this.ctx);
|
|
6858
|
+
return row[key]; // row[key] 为 undefined,但保持一致
|
|
6859
|
+
}
|
|
6860
|
+
return resolveOperand({ type: 'column', name: g }, row, this.ctx);
|
|
6861
|
+
}));
|
|
6296
6862
|
if (!groups.has(key)) groups.set(key, []);
|
|
6297
6863
|
groups.get(key).push(row);
|
|
6298
6864
|
}
|
|
@@ -6499,7 +7065,16 @@ class SQLExecutor {
|
|
|
6499
7065
|
|
|
6500
7066
|
if (statement.limit !== null) {
|
|
6501
7067
|
const start = statement.offset || 0;
|
|
6502
|
-
|
|
7068
|
+
// B5 修复:超大 LIMIT 时 start + limit 可能超过 Number.MAX_SAFE_INTEGER,
|
|
7069
|
+
// 导致 JS 精度丢失,slice 行为异常。这里加友好错误处理。
|
|
7070
|
+
if (statement.limit > rows.length - start) {
|
|
7071
|
+
// LIMIT 超过实际行数是合法的,直接返回全部剩余行
|
|
7072
|
+
rows = rows.slice(start);
|
|
7073
|
+
} else if (start + statement.limit > Number.MAX_SAFE_INTEGER) {
|
|
7074
|
+
throw new Error(`LIMIT value ${statement.limit} is too large; maximum safe value is ${Number.MAX_SAFE_INTEGER - start}`);
|
|
7075
|
+
} else {
|
|
7076
|
+
rows = rows.slice(start, start + statement.limit);
|
|
7077
|
+
}
|
|
6503
7078
|
}
|
|
6504
7079
|
|
|
6505
7080
|
// 无分组、无聚合的正常输出
|