jsql-neo 6.3.3 → 6.3.5
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 +60 -20
- package/index.js +73 -6
- package/lib/btree.js +5 -41
- package/lib/database.js +276 -35
- package/lib/native_client.js +57 -21
- package/lib/sql.js +304 -25
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +8 -1
- package/nativesrc/jsql-neo-native/src/lib.rs +274 -178
- package/package.json +6 -5
- package/test/dml-constraints.test.js +163 -0
- package/test/mysql-compat-hijack.test.js +87 -0
- package/test/native-multi-instance.test.js +139 -0
- package/test/readme-audit.test.js +25 -0
- package/test/sql-parser.test.js +189 -0
- package/test/wal-recovery.test.js +161 -0
package/lib/sql.js
CHANGED
|
@@ -610,6 +610,17 @@ class Parser {
|
|
|
610
610
|
|
|
611
611
|
while (true) {
|
|
612
612
|
const t = this.peek();
|
|
613
|
+
// 列级 CHECK (age INT CHECK(age >= 0))。
|
|
614
|
+
// CHECK 刻意不在 KEYWORDS 里(避免影响同名标识符),所以用 isWord 匹配,
|
|
615
|
+
// 放在 keyword switch 之外。表级 CHECK 已能被解析(同样只是跳过),
|
|
616
|
+
// 列级此前会直接 return def,把 CHECK 留在流里 →
|
|
617
|
+
// "Expected ',' or ')' in CREATE TABLE, got 'CHECK'"。
|
|
618
|
+
// 与表级保持一致:解析并跳过,条件文本存到 def.check 供后续实现。
|
|
619
|
+
if (this.isWord('CHECK')) {
|
|
620
|
+
this.next();
|
|
621
|
+
if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
613
624
|
if (t.type === 'keyword') {
|
|
614
625
|
switch (t.value) {
|
|
615
626
|
case 'PRIMARY':
|
|
@@ -1073,8 +1084,19 @@ class Parser {
|
|
|
1073
1084
|
this.expect('op', ')');
|
|
1074
1085
|
aggregate = { type: 'COUNT', column: col, distinct: aggDistinct };
|
|
1075
1086
|
aggregate = this.parseOptionalOver(aggregate);
|
|
1076
|
-
|
|
1077
|
-
|
|
1087
|
+
// 聚合后紧跟运算符:SUM(v)/COUNT(*) / COUNT(*)-1 …
|
|
1088
|
+
// 顶层聚合默认被表示为独立的 aggregate 字段,不参与后续运算,
|
|
1089
|
+
// 运算符会留在流里 → "Unexpected token '/' after statement"。
|
|
1090
|
+
// 此时整体降级成 scalar 表达式(arith 里嵌 aggregate),
|
|
1091
|
+
// 由执行层的嵌套聚合替换逻辑求值。
|
|
1092
|
+
// 注意:先看尾随运算符,再取别名 —— 顺序反了会把 AS 当成运算符的一部分。
|
|
1093
|
+
const cntTail = this.parseArithTail(aggregate);
|
|
1094
|
+
if (cntTail) {
|
|
1095
|
+
columns.push({ expr: null, scalar: cntTail, alias: this.parseOptionalAlias() });
|
|
1096
|
+
} else {
|
|
1097
|
+
aggregate.alias = this.parseOptionalAlias();
|
|
1098
|
+
columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct });
|
|
1099
|
+
}
|
|
1078
1100
|
} else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
|
|
1079
1101
|
this.next();
|
|
1080
1102
|
const fn = t.value;
|
|
@@ -1085,8 +1107,14 @@ class Parser {
|
|
|
1085
1107
|
this.expect('op', ')');
|
|
1086
1108
|
aggregate = { type: fn, column: col, distinct: aggDistinct2 };
|
|
1087
1109
|
aggregate = this.parseOptionalOver(aggregate);
|
|
1088
|
-
|
|
1089
|
-
|
|
1110
|
+
// 同 COUNT 分支:聚合参与后续算术时降级成 scalar 表达式
|
|
1111
|
+
const fnTail = this.parseArithTail(aggregate);
|
|
1112
|
+
if (fnTail) {
|
|
1113
|
+
columns.push({ expr: null, scalar: fnTail, alias: this.parseOptionalAlias() });
|
|
1114
|
+
} else {
|
|
1115
|
+
aggregate.alias = this.parseOptionalAlias();
|
|
1116
|
+
columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct2 });
|
|
1117
|
+
}
|
|
1090
1118
|
} else if (t.type === 'op' && t.value === '*') {
|
|
1091
1119
|
this.next();
|
|
1092
1120
|
columns.push({ expr: '*' });
|
|
@@ -1762,9 +1790,49 @@ class Parser {
|
|
|
1762
1790
|
this.peek(1) && this.peek(1).type === 'keyword' && this.peek(1).value === 'SELECT';
|
|
1763
1791
|
if (!isSubqueryParen && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1764
1792
|
this.next();
|
|
1765
|
-
|
|
1793
|
+
// 递归解析括号内的完整表达式(到 ')' 为止)。
|
|
1794
|
+
// 此处不能用 parseExpr():它一路走到 parseComparison,
|
|
1795
|
+
// 而内层看不到 ')' 会抛 "Expected comparison operator, got ')'"。
|
|
1796
|
+
// 用 bareExprDepth>0 的 parseArgExpr 走同一套优先级,但允许裸操作数收尾。
|
|
1797
|
+
this.bareExprDepth = (this.bareExprDepth || 0) + 1;
|
|
1798
|
+
let left;
|
|
1799
|
+
try {
|
|
1800
|
+
left = this.parseOr();
|
|
1801
|
+
} finally {
|
|
1802
|
+
this.bareExprDepth--;
|
|
1803
|
+
}
|
|
1766
1804
|
this.expect('op', ')');
|
|
1767
|
-
|
|
1805
|
+
// 括号整体是一个完整的左操作数,后面若紧跟运算符必须继续参与运算:
|
|
1806
|
+
// SELECT (1+2)*3 / SELECT (a+b)/2 / WHERE (x+1) > 5
|
|
1807
|
+
// 直接 return 会把运算符留在流里 → "Unexpected token '*' after statement"
|
|
1808
|
+
// (2*(3+4) 正常是因为 '(' 出现在右操作数位置,不走这个分支。)
|
|
1809
|
+
// 先乘除后加减,与 parseTerm/parseAdditive 的优先级一致。
|
|
1810
|
+
for (;;) {
|
|
1811
|
+
const t = this.peek();
|
|
1812
|
+
if (t.type !== 'op' || !['*', '/', '%'].includes(t.value)) break;
|
|
1813
|
+
this.next();
|
|
1814
|
+
const right = this.parseExpr();
|
|
1815
|
+
left = { type: 'arith', op: t.value, left, right };
|
|
1816
|
+
}
|
|
1817
|
+
for (;;) {
|
|
1818
|
+
const t = this.peek();
|
|
1819
|
+
if (t.type !== 'op' || !['+', '-'].includes(t.value)) break;
|
|
1820
|
+
this.next();
|
|
1821
|
+
const right = this.parseExpr();
|
|
1822
|
+
left = { type: 'arith', op: t.value, left, right };
|
|
1823
|
+
}
|
|
1824
|
+
// 尾随比较/条件运算符:WHERE (a) = 1 / (a) IN (...) / (a) LIKE 'x' /
|
|
1825
|
+
// (a) IS NULL / (a) BETWEEN 1 AND 5 / (a) NOT IN (...)。
|
|
1826
|
+
// 一律交给 parseComparison —— 它是完整实现,覆盖这些全部形式。
|
|
1827
|
+
const nx = this.peek();
|
|
1828
|
+
const isCmp = nx.type === 'op' &&
|
|
1829
|
+
['=', '!=', '<>', '<', '<=', '>', '>=', '<=>', '@>', '<@'].includes(nx.value);
|
|
1830
|
+
const isCondKw = nx.type === 'keyword' &&
|
|
1831
|
+
['IN', 'LIKE', 'ILIKE', 'IS', 'BETWEEN', 'REGEXP', 'RLIKE', 'NOT'].includes(nx.value);
|
|
1832
|
+
if (isCmp || isCondKw) {
|
|
1833
|
+
return this.parseComparison(left);
|
|
1834
|
+
}
|
|
1835
|
+
return left;
|
|
1768
1836
|
}
|
|
1769
1837
|
// EXISTS (SELECT ...):子查询是否存在至少一行
|
|
1770
1838
|
if (this.isKeyword('EXISTS')) {
|
|
@@ -1777,6 +1845,42 @@ class Parser {
|
|
|
1777
1845
|
return this.parseComparison();
|
|
1778
1846
|
}
|
|
1779
1847
|
|
|
1848
|
+
/**
|
|
1849
|
+
* 聚合(或任意已解析好的左操作数)之后若紧跟算术运算符,继续解析并返回
|
|
1850
|
+
* 组合后的 arith 节点;没有运算符则返回 null(调用方走原有的 aggregate 列路径)。
|
|
1851
|
+
* SELECT SUM(v)/COUNT(*) → arith('/', SUM, arith('/', COUNT))
|
|
1852
|
+
* 优先级与 parseTerm / parseAdditive 一致:先乘除后加减。
|
|
1853
|
+
*/
|
|
1854
|
+
parseArithTail(left) {
|
|
1855
|
+
// 投影分支里的 aggregate 对象是 { type: 'SUM', column, distinct } ——
|
|
1856
|
+
// type 直接是聚合函数名,不是 'aggregate'。而执行层的 _replaceAggregates
|
|
1857
|
+
// 只认 type === 'aggregate',不规范化就会漏替换 → 整个表达式求值为 null。
|
|
1858
|
+
// 这里包一层 { type:'aggregate', fn, column, … } 供替换逻辑识别。
|
|
1859
|
+
const seed = left && left.type !== 'aggregate' && typeof left.type === 'string' &&
|
|
1860
|
+
/^[A-Z][A-Z0-9_]*$/.test(left.type)
|
|
1861
|
+
? { type: 'aggregate', fn: left.type, column: left.column,
|
|
1862
|
+
distinct: left.distinct, separator: left.separator, orderBy: left.orderBy }
|
|
1863
|
+
: left;
|
|
1864
|
+
|
|
1865
|
+
let node = seed;
|
|
1866
|
+
let touched = false;
|
|
1867
|
+
for (;;) {
|
|
1868
|
+
const t = this.peek();
|
|
1869
|
+
if (t.type !== 'op' || !['*', '/', '%'].includes(t.value)) break;
|
|
1870
|
+
this.next();
|
|
1871
|
+
node = { type: 'arith', op: t.value, left: node, right: this.parseScalar() };
|
|
1872
|
+
touched = true;
|
|
1873
|
+
}
|
|
1874
|
+
for (;;) {
|
|
1875
|
+
const t = this.peek();
|
|
1876
|
+
if (t.type !== 'op' || !['+', '-'].includes(t.value)) break;
|
|
1877
|
+
this.next();
|
|
1878
|
+
node = { type: 'arith', op: t.value, left: node, right: this.parseScalar() };
|
|
1879
|
+
touched = true;
|
|
1880
|
+
}
|
|
1881
|
+
return touched ? node : null;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1780
1884
|
/**
|
|
1781
1885
|
* 解析聚合调用:fn ( [DISTINCT] expr [, expr*] [ORDER BY ...] [SEPARATOR 'x'] )
|
|
1782
1886
|
* 以及 COUNT(*) 这类无参形式。
|
|
@@ -1838,7 +1942,27 @@ class Parser {
|
|
|
1838
1942
|
// 否则会被当作逐行标量函数:既返回 null,又不会折叠分组
|
|
1839
1943
|
if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
|
|
1840
1944
|
this.peek().type === 'op' && this.peek().value === '(') {
|
|
1841
|
-
|
|
1945
|
+
const agg = this.parseAggregateCall(String(t.value).toUpperCase());
|
|
1946
|
+
// 聚合结果也是一个完整的左操作数,后面可紧跟运算符:
|
|
1947
|
+
// SELECT SUM(sal)/COUNT(*) AS avg ← 算平均值的标准写法
|
|
1948
|
+
// SELECT SUM(sal) - MIN(sal) ← 极差
|
|
1949
|
+
// SELECT MAX(sal) * 2
|
|
1950
|
+
// 不处理的话运算符会留在流里 → "Unexpected token '/' after statement"。
|
|
1951
|
+
// (加括号写成 (SUM(sal))/2 之所以正常,是走了括号分组分支。)
|
|
1952
|
+
let left = agg;
|
|
1953
|
+
for (;;) {
|
|
1954
|
+
const nx = this.peek();
|
|
1955
|
+
if (nx.type !== 'op' || !['*', '/', '%'].includes(nx.value)) break;
|
|
1956
|
+
this.next();
|
|
1957
|
+
left = { type: 'arith', op: nx.value, left, right: this.parseOperand() };
|
|
1958
|
+
}
|
|
1959
|
+
for (;;) {
|
|
1960
|
+
const nx = this.peek();
|
|
1961
|
+
if (nx.type !== 'op' || !['+', '-'].includes(nx.value)) break;
|
|
1962
|
+
this.next();
|
|
1963
|
+
left = { type: 'arith', op: nx.value, left, right: this.parseOperand() };
|
|
1964
|
+
}
|
|
1965
|
+
return left;
|
|
1842
1966
|
}
|
|
1843
1967
|
// 支持 alias.column 引用
|
|
1844
1968
|
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
@@ -2039,10 +2163,33 @@ class Parser {
|
|
|
2039
2163
|
}
|
|
2040
2164
|
// 括号分组:支持 (1+2)、2*(3+4) 等出现在算术/函数参数任意位置
|
|
2041
2165
|
// 注意:parseOperand 开头已通过 const t = this.next() 消费了 '(', 此处不可再 next()
|
|
2166
|
+
//
|
|
2167
|
+
// 括号整体是一个完整的左操作数,后面若紧跟运算符必须继续参与运算,
|
|
2168
|
+
// 否则运算符会留在流里:
|
|
2169
|
+
// SELECT (1+2)*3 → Unexpected token '*' after statement
|
|
2170
|
+
// WHERE (a) = 1 → Expected comparison operator, got ')'
|
|
2042
2171
|
if (t.type === 'op' && t.value === '(') {
|
|
2043
|
-
|
|
2172
|
+
let left = this.parseArgExpr(); // 容忍裸操作数,允许纯算术分组
|
|
2044
2173
|
this.expect('op', ')');
|
|
2045
|
-
|
|
2174
|
+
// 先乘除后加减,与 parseTerm / parseAdditive 的优先级一致
|
|
2175
|
+
for (;;) {
|
|
2176
|
+
const nx = this.peek();
|
|
2177
|
+
if (nx.type !== 'op' || !['*', '/', '%'].includes(nx.value)) break;
|
|
2178
|
+
this.next();
|
|
2179
|
+
left = { type: 'arith', op: nx.value, left, right: this.parseArgExpr() };
|
|
2180
|
+
}
|
|
2181
|
+
for (;;) {
|
|
2182
|
+
const nx = this.peek();
|
|
2183
|
+
if (nx.type !== 'op' || !['+', '-'].includes(nx.value)) break;
|
|
2184
|
+
this.next();
|
|
2185
|
+
left = { type: 'arith', op: nx.value, left, right: this.parseArgExpr() };
|
|
2186
|
+
}
|
|
2187
|
+
// 尾随比较运算符交给 parseComparison(完整实现,覆盖 =/</>/LIKE/IN/IS…)
|
|
2188
|
+
const cx = this.peek();
|
|
2189
|
+
if (cx.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(cx.value)) {
|
|
2190
|
+
return this.parseComparison(left);
|
|
2191
|
+
}
|
|
2192
|
+
return left;
|
|
2046
2193
|
}
|
|
2047
2194
|
if (t.type === 'op' && (t.value === '-' || t.value === '+')) {
|
|
2048
2195
|
const num = this.next();
|
|
@@ -2190,7 +2337,15 @@ class Parser {
|
|
|
2190
2337
|
const v = this.parseOperand();
|
|
2191
2338
|
cond = { type: 'compare', op: '=', left: base, right: v };
|
|
2192
2339
|
} else {
|
|
2193
|
-
|
|
2340
|
+
// 条件可能是完整比较(WHEN a > 1),也可能是裸值/裸列(WHEN 1、WHEN flag)。
|
|
2341
|
+
// parseComparison 只接受比较式,裸值会抛 "Expected comparison operator, got 'THEN'"。
|
|
2342
|
+
// 先看清下一个 token 决定走哪条路,避免用 try/catch 回溯吞掉真实错误。
|
|
2343
|
+
const nx = this.peek();
|
|
2344
|
+
const isCmp = (nx.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(nx.value)) ||
|
|
2345
|
+
(nx.type === 'keyword' && ['IN', 'LIKE', 'ILIKE', 'IS', 'BETWEEN', 'REGEXP', 'RLIKE', 'NOT'].includes(nx.value)) ||
|
|
2346
|
+
this.isWord('NOT');
|
|
2347
|
+
cond = isCmp ? this.parseComparison()
|
|
2348
|
+
: { type: 'isTruth', operand: this.parseArgExpr(), not: false, truth: true };
|
|
2194
2349
|
}
|
|
2195
2350
|
this.expectKeyword('THEN');
|
|
2196
2351
|
const val = this.parseOperand();
|
|
@@ -2202,8 +2357,13 @@ class Parser {
|
|
|
2202
2357
|
return { type: 'case', branches, elseVal };
|
|
2203
2358
|
}
|
|
2204
2359
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2360
|
+
/**
|
|
2361
|
+
* 解析比较表达式。
|
|
2362
|
+
* preLeft:已解析好的左操作数(括号分组场景 —— parseNot 消费到 ')' 后传进来),
|
|
2363
|
+
* 省略时正常从 parseBitwise() 取左操作数。
|
|
2364
|
+
*/
|
|
2365
|
+
parseComparison(preLeft) {
|
|
2366
|
+
const left = preLeft !== undefined ? preLeft : this.parseBitwise();
|
|
2207
2367
|
const t = this.peek();
|
|
2208
2368
|
let not = false;
|
|
2209
2369
|
if (t.type === 'keyword' && t.value === 'NOT') {
|
|
@@ -2347,7 +2507,10 @@ const OPERATORS = {
|
|
|
2347
2507
|
function applyCast(value, dataType) {
|
|
2348
2508
|
const t = String(dataType || '').toUpperCase().replace(/\(.*\)$/, '').trim();
|
|
2349
2509
|
if (value === null || value === undefined) return null;
|
|
2350
|
-
|
|
2510
|
+
// SIGNED / UNSIGNED 是 MySQL 的整型别名(CAST(x AS SIGNED)),
|
|
2511
|
+
// 此前没列入整型分支,导致 CAST('12abc' AS SIGNED) 原样返回字符串。
|
|
2512
|
+
if (t === 'INTEGER' || t === 'INT' || t === 'BIGINT' || t === 'SMALLINT' ||
|
|
2513
|
+
t === 'TINYINT' || t === 'SIGNED' || t === 'UNSIGNED' || t === 'MEDIUMINT') {
|
|
2351
2514
|
// MySQL CAST(... AS SIGNED/INT):取数字前缀('12abc'→12),无法解析时返回 0(而非 null)。
|
|
2352
2515
|
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
2353
2516
|
if (typeof value === 'number') return Number.isFinite(value) ? Math.trunc(value) : 0;
|
|
@@ -2374,6 +2537,23 @@ function rowHasPrefixedKeys(row) {
|
|
|
2374
2537
|
return false;
|
|
2375
2538
|
}
|
|
2376
2539
|
|
|
2540
|
+
/**
|
|
2541
|
+
* 字符串能否当作数值参与算术运算。
|
|
2542
|
+
* MySQL 里 '5' + 5 = 10、'5' + '5' = 10、'abc' + 1 = 1(非数字按 0)。
|
|
2543
|
+
* 纯数字(含小数/负号/科学计数)才返回数值,否则返回 null。
|
|
2544
|
+
*/
|
|
2545
|
+
function numericIfNumeric(v) {
|
|
2546
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
|
|
2547
|
+
if (typeof v === 'boolean') return v ? 1 : 0;
|
|
2548
|
+
if (typeof v !== 'string') return null;
|
|
2549
|
+
const s = v.trim();
|
|
2550
|
+
if (s === '') return null;
|
|
2551
|
+
// 严格数字:可选符号 + 数字(含小数与指数),不接受 '12abc' 这类前缀形式
|
|
2552
|
+
if (!/^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/.test(s)) return null;
|
|
2553
|
+
const n = Number(s);
|
|
2554
|
+
return Number.isFinite(n) ? n : null;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2377
2557
|
function resolveOperand(operand, row, ctx) {
|
|
2378
2558
|
if (operand === null || operand === undefined) return null;
|
|
2379
2559
|
if (typeof operand === 'string') return resolveOperand({ type: 'column', name: operand }, row, ctx);
|
|
@@ -2423,11 +2603,28 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2423
2603
|
const r = resolveOperand(operand.right, row, ctx);
|
|
2424
2604
|
if (l === null || r === null || l === undefined || r === undefined) return null;
|
|
2425
2605
|
switch (operand.op) {
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
case '
|
|
2606
|
+
// MySQL/SQLite 里 + 永远是算术加法,字符串拼接只走 || 或 CONCAT。
|
|
2607
|
+
// 直接用 JS 的 l + r 会让 '5' + 5 得到 '55'(静默返回字符串),
|
|
2608
|
+
// 数字与字符串混合的算术会一路错下去。
|
|
2609
|
+
// 判据:任一侧是 number,或字符串能被完整解析为数字 → 走数值加法。
|
|
2610
|
+
case '+': {
|
|
2611
|
+
// MySQL / SQLite 语义(已用 sqlite3 实测核对):
|
|
2612
|
+
// '5' + 5 = 10 '5' + '5' = 10 'abc' + 1 = 1 'a' + 'b' = 0
|
|
2613
|
+
// 任一侧是「数值」时做算术加法,另一侧非数值按 0;
|
|
2614
|
+
// 两侧都不是数值才是 0(JS 的 l + r 会把 '5' + 5 拼成 '55')。
|
|
2615
|
+
if (typeof l === 'number' || typeof r === 'number') {
|
|
2616
|
+
const ln2 = numericIfNumeric(l), rn2 = numericIfNumeric(r);
|
|
2617
|
+
return (ln2 === null ? 0 : ln2) + (rn2 === null ? 0 : rn2);
|
|
2618
|
+
}
|
|
2619
|
+
const ln = numericIfNumeric(l), rn = numericIfNumeric(r);
|
|
2620
|
+
if (ln !== null) return ln + (rn === null ? 0 : rn);
|
|
2621
|
+
if (rn !== null) return rn; // 左侧非数值按 0
|
|
2622
|
+
return 0;
|
|
2623
|
+
}
|
|
2624
|
+
case '-': return Number(l) - Number(r);
|
|
2625
|
+
case '*': return Number(l) * Number(r);
|
|
2626
|
+
case '/': return r === 0 ? null : Number(l) / Number(r);
|
|
2627
|
+
case '%': return r === 0 ? null : Number(l) % Number(r);
|
|
2431
2628
|
// MySQL 整数除法 DIV(结果向零取整)
|
|
2432
2629
|
case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
|
|
2433
2630
|
// 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
|
|
@@ -2894,7 +3091,12 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
2894
3091
|
const b = parseSqlDate(args[1]);
|
|
2895
3092
|
if (!a || !b) return null;
|
|
2896
3093
|
const ms = a.getTime() - b.getTime();
|
|
2897
|
-
|
|
3094
|
+
// MySQL 的整月差:先算日历月数差,再按「结束日的日 < 起始日的日」退一档。
|
|
3095
|
+
// 只算日历月差会高估:
|
|
3096
|
+
// TIMESTAMPDIFF(MONTH,'2026-01-15','2026-03-14') = 1(差 1 个月零 29 天,不足 2 个月)
|
|
3097
|
+
// TIMESTAMPDIFF(MONTH,'2026-01-31','2026-03-01') = 1(1/31+1月=2/28,2/28+1月=3/28,够不到 3/1)
|
|
3098
|
+
let months = (a.getFullYear() - b.getFullYear()) * 12 + (a.getMonth() - b.getMonth());
|
|
3099
|
+
if (a.getDate() < b.getDate()) months--;
|
|
2898
3100
|
switch (unit) {
|
|
2899
3101
|
case 'MICROSECOND': return Math.floor(ms * 1000);
|
|
2900
3102
|
case 'SECOND': return Math.floor(ms / 1000);
|
|
@@ -2904,7 +3106,7 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
2904
3106
|
case 'WEEK': return Math.floor(ms / (86400000 * 7));
|
|
2905
3107
|
case 'MONTH': return months;
|
|
2906
3108
|
case 'QUARTER': return Math.floor(months / 3);
|
|
2907
|
-
case 'YEAR': return
|
|
3109
|
+
case 'YEAR': return Math.floor(months / 12);
|
|
2908
3110
|
default: return null;
|
|
2909
3111
|
}
|
|
2910
3112
|
}
|
|
@@ -2995,6 +3197,30 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
2995
3197
|
case 'LTRIM': return args[0] == null ? null : String(args[0]).replace(/^\s+/, '');
|
|
2996
3198
|
case 'RTRIM': return args[0] == null ? null : String(args[0]).replace(/\s+$/, '');
|
|
2997
3199
|
case 'ABS': return args[0] == null ? null : Math.abs(args[0]);
|
|
3200
|
+
case 'SIGN': { // MySQL:正数 1 / 0 → 0 / 负数 -1;NULL → NULL
|
|
3201
|
+
const v = numericIfNumeric(args[0]);
|
|
3202
|
+
if (v === null) return args[0] == null ? null : 0;
|
|
3203
|
+
return v > 0 ? 1 : (v < 0 ? -1 : 0);
|
|
3204
|
+
}
|
|
3205
|
+
case 'TRUNCATE': { // MySQL:直接截断,不四舍五入;NULL → NULL
|
|
3206
|
+
const x = numericIfNumeric(args[0]);
|
|
3207
|
+
if (x === null) return args[0] == null ? null : null;
|
|
3208
|
+
const digits = args[1] === undefined || args[1] === null ? 0 : Math.trunc(Number(args[1])) || 0;
|
|
3209
|
+
const factor = Math.pow(10, digits);
|
|
3210
|
+
if (!Number.isFinite(factor)) return x;
|
|
3211
|
+
return Math.trunc(x * factor) / factor;
|
|
3212
|
+
}
|
|
3213
|
+
case 'SUBSTRING_INDEX': { // MySQL:按分隔符切,返回左边 count 段(count<0 从右数)
|
|
3214
|
+
const s = args[0] == null ? null : String(args[0]);
|
|
3215
|
+
const d = args[1] == null ? null : String(args[1]);
|
|
3216
|
+
if (s === null || d === null || d === '') return null;
|
|
3217
|
+
const n = Math.trunc(Number(args[2]));
|
|
3218
|
+
if (!Number.isFinite(n)) return null;
|
|
3219
|
+
if (n === 0) return '';
|
|
3220
|
+
const parts = s.split(d);
|
|
3221
|
+
if (n > 0) return parts.slice(0, n).join(d);
|
|
3222
|
+
return parts.slice(n).join(d); // n<0:取末尾 |n| 段
|
|
3223
|
+
}
|
|
2998
3224
|
case 'ROUND': {
|
|
2999
3225
|
// MySQL 语义:四舍五入「半远离零」(Math.round 是半向 +∞,对负数会错,如 -3.5→-3)
|
|
3000
3226
|
// 位数 D 可为负(向小数点左侧舍入);非数值/非法输入返回 null,绝不抛 TypeError/RangeError。
|
|
@@ -3974,7 +4200,10 @@ class SQLExecutor {
|
|
|
3974
4200
|
}
|
|
3975
4201
|
if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
|
|
3976
4202
|
for (const row of candidates) {
|
|
3977
|
-
|
|
4203
|
+
// 无主键表没有稳定行标识:_rowPkId 会回退到 row.id(业务列名),
|
|
4204
|
+
// 而存储层的 _resolveId 把 id 当 1-based 行号 —— 两者语义不一致,
|
|
4205
|
+
// DELETE ... WHERE id IN (3,5) 会删错行。此时直接传行对象,存储层按引用删。
|
|
4206
|
+
const id = pkCols.length > 0 ? this._rowPkId(row, pkCols) : row;
|
|
3978
4207
|
if (id !== undefined) { ids.push(id); removedRows.push(row); }
|
|
3979
4208
|
}
|
|
3980
4209
|
if (ids.length > 0) {
|
|
@@ -4259,9 +4488,48 @@ class SQLExecutor {
|
|
|
4259
4488
|
return row ? row.id : undefined;
|
|
4260
4489
|
}
|
|
4261
4490
|
|
|
4262
|
-
|
|
4491
|
+
/**
|
|
4492
|
+
* 从 WHERE 表达式里提取「可直接下推给存储层」的等值条件。
|
|
4493
|
+
*
|
|
4494
|
+
* 此前 _readTable 一律 `find(table, {}, ...)` 拉全表回内存再逐行 evaluateExpr,
|
|
4495
|
+
* WHERE 完全没有下推 —— 2 万行表上 `WHERE id = ?` 也要扫 2 万行(约 88ms/次),
|
|
4496
|
+
* 而主键索引本可以做到微秒级。
|
|
4497
|
+
*
|
|
4498
|
+
* 只认最安全的一类:column = <字面量>(含 AND 连接)。
|
|
4499
|
+
* 其它条件(范围、OR、函数、IN、子查询)一律不下推,交回内存过滤,
|
|
4500
|
+
* 避免为了快而改错结果。
|
|
4501
|
+
*/
|
|
4502
|
+
_pushdownEqualities(statement) {
|
|
4503
|
+
const out = {};
|
|
4504
|
+
if (!statement || !statement.where || typeof statement.where !== 'object') return out;
|
|
4505
|
+
const visit = (node) => {
|
|
4506
|
+
if (!node || typeof node !== 'object') return;
|
|
4507
|
+
if (node.type === 'and') { visit(node.left); visit(node.right); return; }
|
|
4508
|
+
if (node.type !== 'compare' || node.op !== '=') return;
|
|
4509
|
+
const isCol = n => n && n.type === 'column' && typeof n.name === 'string';
|
|
4510
|
+
const isVal = n => n && n.type === 'value' && n.value !== null && n.value !== undefined;
|
|
4511
|
+
let col = null, val = null;
|
|
4512
|
+
if (isCol(node.left) && isVal(node.right)) { col = node.left.name; val = node.right.value; }
|
|
4513
|
+
else if (isCol(node.right) && isVal(node.left)) { col = node.right.name; val = node.left.value; }
|
|
4514
|
+
if (col === null) return;
|
|
4515
|
+
const key = col.includes('.') ? col.slice(col.lastIndexOf('.') + 1) : col;
|
|
4516
|
+
// 同一列出现多个等值条件(如 id=1 AND id=2)时不推,交给内存求值
|
|
4517
|
+
if (Object.prototype.hasOwnProperty.call(out, key)) delete out[key];
|
|
4518
|
+
else out[key] = val;
|
|
4519
|
+
};
|
|
4520
|
+
visit(statement.where);
|
|
4521
|
+
return out;
|
|
4522
|
+
}
|
|
4523
|
+
|
|
4524
|
+
async _readTable(table, statement) {
|
|
4263
4525
|
const schema = await this._getSchema(table);
|
|
4264
|
-
|
|
4526
|
+
// 有可下推的等值条件时交给存储层走索引;没有则维持原来的全表读取
|
|
4527
|
+
const filter = statement ? this._pushdownEqualities(statement) : {};
|
|
4528
|
+
const useIndex = Object.keys(filter).length > 0 &&
|
|
4529
|
+
(typeof this.engine.exists === 'function' || typeof this.engine.find === 'function');
|
|
4530
|
+
const rows = useIndex
|
|
4531
|
+
? (await this.engine.find(table, filter, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema))
|
|
4532
|
+
: (await this.engine.find(table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
4265
4533
|
return { schema, rows };
|
|
4266
4534
|
}
|
|
4267
4535
|
|
|
@@ -4622,6 +4890,17 @@ class SQLExecutor {
|
|
|
4622
4890
|
delete node.column;
|
|
4623
4891
|
return;
|
|
4624
4892
|
}
|
|
4893
|
+
// 投影分支构造的聚合节点形如 { type: 'SUM', column, distinct }(type 就是函数名),
|
|
4894
|
+
// 与 parseAggregateCall 产出的 { type:'aggregate', fn } 并存。
|
|
4895
|
+
// 两种都要替换,否则 SUM(v)/COUNT(*) 整式求值为 null。
|
|
4896
|
+
if (typeof node.type === 'string' && AGG_FUNCS.has(node.type) &&
|
|
4897
|
+
(node.column !== undefined || node.over === undefined)) {
|
|
4898
|
+
const fn = node.type;
|
|
4899
|
+
node.type = 'value';
|
|
4900
|
+
node.value = this._aggValue(group, fn, node.column, node.distinct, node.separator, node.orderBy);
|
|
4901
|
+
delete node.column;
|
|
4902
|
+
return;
|
|
4903
|
+
}
|
|
4625
4904
|
for (const k of Object.keys(node)) {
|
|
4626
4905
|
if (node[k] && typeof node[k] === 'object') this._replaceAggregates(node[k], group);
|
|
4627
4906
|
}
|
|
@@ -5133,7 +5412,7 @@ class SQLExecutor {
|
|
|
5133
5412
|
const res = await this.executeSelect(firstItem.subquery);
|
|
5134
5413
|
rowsAll = { rows: this._subQueryRows(res), schema: null, columns: res.columns };
|
|
5135
5414
|
} else {
|
|
5136
|
-
rowsAll = await this._readTable(firstItem.table);
|
|
5415
|
+
rowsAll = await this._readTable(firstItem.table, statement);
|
|
5137
5416
|
}
|
|
5138
5417
|
schema = rowsAll.schema;
|
|
5139
5418
|
const firstPrefix = firstItem.alias || firstItem.table;
|
|
@@ -5142,7 +5421,7 @@ class SQLExecutor {
|
|
|
5142
5421
|
for (const j of statement.from.joins) {
|
|
5143
5422
|
const rightRes = j.item.subquery
|
|
5144
5423
|
? await this.executeSelect(j.item.subquery)
|
|
5145
|
-
: await this._readTable(j.item.table);
|
|
5424
|
+
: await this._readTable(j.item.table, statement);
|
|
5146
5425
|
const rightPrefix = j.item.alias || j.item.table;
|
|
5147
5426
|
const rightRows = (j.item.subquery ? this._subQueryRows(rightRes) : rightRes.rows).map(r => this._prefixRow(r, rightPrefix));
|
|
5148
5427
|
|
|
Binary file
|
|
@@ -60,7 +60,14 @@ impl HybridEngine {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/// 打开磁盘存储目录(mode: 'hybrid' | 'disk' | 'memory';memory 或空目录忽略)
|
|
63
|
-
///
|
|
63
|
+
///
|
|
64
|
+
/// 注意(6.3.4):这里仍然会重置引擎状态,但语义已改变 ——
|
|
65
|
+
/// 以前全进程只有一个引擎,`open(dirB)` 会把 `open(dirA)` 的表全部清空,
|
|
66
|
+
/// 导致同进程无法持有两个独立数据库。现在每个 JS `Database` 实例有独立的
|
|
67
|
+
/// 引擎句柄,`open()` 只重置自己那一份。
|
|
68
|
+
///
|
|
69
|
+
/// 但对**同一个句柄**重复 open 不同目录仍会丢弃上一个目录的内存表,
|
|
70
|
+
/// 因此调用方(native_client.js)应为每个 Database 分配独立句柄。
|
|
64
71
|
pub fn open(&mut self, dir: &str, mode: &str) -> Result<(), String> {
|
|
65
72
|
self.mem.borrow_mut().clear();
|
|
66
73
|
self.meta.borrow_mut().clear();
|