jsql-neo 6.0.2 → 6.0.4
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 +40 -35
- package/lib/native_client.js +69 -8
- package/lib/sql.js +523 -65
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/Cargo.lock +2 -2
- package/package.json +1 -1
- package/test/readme-audit.test.js +153 -14
package/lib/sql.js
CHANGED
|
@@ -11,6 +11,10 @@ const DANGEROUS_SQL = [
|
|
|
11
11
|
{ re: /^SYSEXEC$|^SYS_EXEC$/, name: 'sys_exec' },
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
+
// SQL 文本体积上限(默认 64 MiB):防止超大、无分号的输入造成无界内存消耗
|
|
15
|
+
// (CWE-770)。可在 executeSQL 通过 opts.maxSqlLength 放宽/关闭(传 Infinity)。
|
|
16
|
+
const DEFAULT_MAX_SQL_LENGTH = 64 * 1024 * 1024;
|
|
17
|
+
|
|
14
18
|
function findDangerousSQL(tokens) {
|
|
15
19
|
for (let i = 0; i < tokens.length; i++) {
|
|
16
20
|
const t = tokens[i];
|
|
@@ -79,6 +83,13 @@ function tokenize(sql) {
|
|
|
79
83
|
while (i < n && sql[i] !== '\n') i++;
|
|
80
84
|
continue;
|
|
81
85
|
}
|
|
86
|
+
// PG 的 JSON 路径操作符 #> / #>>(必须在 '#' 行注释之前识别)
|
|
87
|
+
if (c === '#' && sql[i + 1] === '>') {
|
|
88
|
+
const op = sql[i + 2] === '>' ? '#>>' : '#>';
|
|
89
|
+
tokens.push(new SQLToken('op', op, i));
|
|
90
|
+
i += op.length;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
82
93
|
if (c === '#' || (c === '/' && sql[i + 1] === '*')) {
|
|
83
94
|
if (c === '#') { while (i < n && sql[i] !== '\n') i++; continue; }
|
|
84
95
|
i += 2;
|
|
@@ -95,7 +106,10 @@ function tokenize(sql) {
|
|
|
95
106
|
if (sql[j] === '\\' && j + 1 < n) {
|
|
96
107
|
const esc = sql[j + 1];
|
|
97
108
|
const map = { n: '\n', t: '\t', r: '\r', '0': '\0', "'": "'", '"': '"', '\\': '\\', b: '\b', Z: '\x1a', a: '\a' };
|
|
98
|
-
|
|
109
|
+
// MySQL 语义:\% 与 \_ 保留反斜杠(供 LIKE 匹配字面量通配符),其余未知转义忽略反斜杠
|
|
110
|
+
if (map[esc] !== undefined) str += map[esc];
|
|
111
|
+
else if (esc === '%' || esc === '_') str += '\\' + esc;
|
|
112
|
+
else str += esc;
|
|
99
113
|
j += 2;
|
|
100
114
|
} else if (sql[j] === quote) {
|
|
101
115
|
break;
|
|
@@ -104,6 +118,7 @@ function tokenize(sql) {
|
|
|
104
118
|
j++;
|
|
105
119
|
}
|
|
106
120
|
}
|
|
121
|
+
if (j >= n) throw new Error(`Unterminated ${quote === '"' ? 'double' : 'single'}-quoted string at position ${i}`);
|
|
107
122
|
tokens.push(new SQLToken('string', str, i));
|
|
108
123
|
i = j + 1;
|
|
109
124
|
continue;
|
|
@@ -145,11 +160,19 @@ function tokenize(sql) {
|
|
|
145
160
|
if (c === '`') {
|
|
146
161
|
let j = i + 1;
|
|
147
162
|
while (j < n && sql[j] !== '`') j++;
|
|
163
|
+
if (j >= n) throw new Error(`Unterminated backtick-quoted identifier at position ${i}`);
|
|
148
164
|
tokens.push(new SQLToken('ident', sql.slice(i + 1, j), i));
|
|
149
165
|
i = j + 1;
|
|
150
166
|
continue;
|
|
151
167
|
}
|
|
152
168
|
|
|
169
|
+
// PG 的 JSON 包含操作符 @>
|
|
170
|
+
if (c === '@' && sql[i + 1] === '>') {
|
|
171
|
+
tokens.push(new SQLToken('op', '@>', i));
|
|
172
|
+
i += 2;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
153
176
|
if (c === '@') {
|
|
154
177
|
let j = i;
|
|
155
178
|
while (j < n && sql[j] === '@') j++;
|
|
@@ -167,6 +190,12 @@ function tokenize(sql) {
|
|
|
167
190
|
}
|
|
168
191
|
|
|
169
192
|
// 参数占位符:? (顺序)、?N (编号)、?? (标识符占位)
|
|
193
|
+
// 注意:?| / ?& 是 PG 的 JSON 键存在操作符,必须优先识别
|
|
194
|
+
if (c === '?' && (sql[i + 1] === '|' || sql[i + 1] === '&')) {
|
|
195
|
+
tokens.push(new SQLToken('op', '?' + sql[i + 1], i));
|
|
196
|
+
i += 2;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
170
199
|
if (c === '?') {
|
|
171
200
|
if (sql[i + 1] === '?') { tokens.push(new SQLToken('param', '??', i)); i += 2; continue; }
|
|
172
201
|
let j = i + 1;
|
|
@@ -177,9 +206,22 @@ function tokenize(sql) {
|
|
|
177
206
|
continue;
|
|
178
207
|
}
|
|
179
208
|
|
|
209
|
+
const three = sql.slice(i, i + 3);
|
|
210
|
+
if (sql.slice(i, i + 2) === '<@') {
|
|
211
|
+
tokens.push(new SQLToken('op', '<@', i));
|
|
212
|
+
i += 2;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (three === '->>' || three === '<=>' || three === '!~*') {
|
|
216
|
+
tokens.push(new SQLToken('op', three, i));
|
|
217
|
+
i += 3;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
180
221
|
const two = sql.slice(i, i + 2);
|
|
181
222
|
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
|
|
182
|
-
two === '<<' || two === '>>'
|
|
223
|
+
two === '<<' || two === '>>' || two === '->' || two === '::' ||
|
|
224
|
+
two === '&&' || two === '||' || two === '~*' || two === '!~') {
|
|
183
225
|
tokens.push(new SQLToken('op', two, i));
|
|
184
226
|
i += 2;
|
|
185
227
|
continue;
|
|
@@ -523,19 +565,27 @@ class Parser {
|
|
|
523
565
|
parseColumnDef() {
|
|
524
566
|
const def = {};
|
|
525
567
|
const typeTok = this.next();
|
|
526
|
-
|
|
527
|
-
|
|
568
|
+
const typeRaw = String(typeTok.value);
|
|
569
|
+
// 这些类型不在 KEYWORDS 里(避免影响同名标识符),按上下文识别
|
|
570
|
+
const IDENT_TYPES = /^(BIG)?SERIAL$|^JSONB$|^TIMESTAMPTZ$|^BYTEA$|^UUID$/i;
|
|
571
|
+
const isIdentType = IDENT_TYPES.test(typeRaw);
|
|
572
|
+
if (typeTok.type !== 'keyword' && !isIdentType) throw new Error(`Expected column type, got '${typeTok.value}'`);
|
|
573
|
+
const type = typeRaw.toLowerCase();
|
|
528
574
|
const typeMap = {
|
|
529
575
|
integer: 'integer', int: 'integer', bigint: 'integer', tinyint: 'integer', smallint: 'integer',
|
|
530
576
|
string: 'string', text: 'string', varchar: 'string', char: 'string',
|
|
531
577
|
float: 'number', double: 'number', real: 'number', numeric: 'number', decimal: 'number',
|
|
532
578
|
boolean: 'boolean', bool: 'boolean',
|
|
533
|
-
date: 'date', datetime: 'datetime', timestamp: 'timestamp',
|
|
534
|
-
any: 'any', object: 'object', json: 'object', array: 'array'
|
|
579
|
+
date: 'date', datetime: 'datetime', timestamp: 'timestamp', timestamptz: 'timestamp',
|
|
580
|
+
any: 'any', object: 'object', json: 'object', jsonb: 'object', array: 'array',
|
|
581
|
+
bytea: 'string', uuid: 'string',
|
|
582
|
+
// PG 自增序列类型:等价于整型 + 自增 + NOT NULL
|
|
583
|
+
serial: 'integer', bigserial: 'integer'
|
|
535
584
|
};
|
|
536
585
|
const mapped = typeMap[type];
|
|
537
586
|
if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
|
|
538
587
|
def.type = mapped;
|
|
588
|
+
if (/^(BIG)?SERIAL$/i.test(typeRaw)) { def.autoIncrement = true; def.required = true; }
|
|
539
589
|
|
|
540
590
|
// 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
|
|
541
591
|
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
@@ -598,6 +648,11 @@ class Parser {
|
|
|
598
648
|
def.defaultExpr = true;
|
|
599
649
|
} else {
|
|
600
650
|
def.default = this.parseValue();
|
|
651
|
+
// 允许字面量后带 :: 类型转换:DEFAULT '{}'::jsonb
|
|
652
|
+
if (this.peek().type === 'op' && this.peek().value === '::') {
|
|
653
|
+
this.next();
|
|
654
|
+
if (this.peek().type === 'keyword' || this.peek().type === 'ident') this.next();
|
|
655
|
+
}
|
|
601
656
|
}
|
|
602
657
|
break;
|
|
603
658
|
case 'COLLATE':
|
|
@@ -772,7 +827,7 @@ class Parser {
|
|
|
772
827
|
for (;;) {
|
|
773
828
|
const col = this.parseColumnRef();
|
|
774
829
|
this.expect('op', '=');
|
|
775
|
-
const val = this.
|
|
830
|
+
const val = this.parseConflictValue();
|
|
776
831
|
sets.push([col, val]);
|
|
777
832
|
if (this.peek().value === ',') { this.next(); continue; }
|
|
778
833
|
break;
|
|
@@ -852,6 +907,21 @@ class Parser {
|
|
|
852
907
|
throw new Error(`Expected value, got '${t.value}'`);
|
|
853
908
|
}
|
|
854
909
|
|
|
910
|
+
/**
|
|
911
|
+
* ON CONFLICT DO UPDATE 赋值右侧:
|
|
912
|
+
* EXCLUDED.col —— PG:引用"本次本应插入的值"
|
|
913
|
+
* VALUES(col) —— MySQL 同义(在 parseValue 中处理)
|
|
914
|
+
* 其他字面量
|
|
915
|
+
*/
|
|
916
|
+
parseConflictValue() {
|
|
917
|
+
if (this.isWord('EXCLUDED') && this.peek(1).type === 'op' && this.peek(1).value === '.') {
|
|
918
|
+
this.next(); this.next();
|
|
919
|
+
const col = this.parseColumnRef();
|
|
920
|
+
return { _excludedOf: col };
|
|
921
|
+
}
|
|
922
|
+
return this.parseValue();
|
|
923
|
+
}
|
|
924
|
+
|
|
855
925
|
/**
|
|
856
926
|
* WITH [RECURSIVE] name [(col, ...)] AS (SELECT ...), ... <statement>
|
|
857
927
|
* CTE 在语法层解析为 { type:'with', ctes:[{name, columns, select}], statement }。
|
|
@@ -1628,18 +1698,33 @@ class Parser {
|
|
|
1628
1698
|
}
|
|
1629
1699
|
|
|
1630
1700
|
parseOr() {
|
|
1701
|
+
let left = this.parseXor();
|
|
1702
|
+
for (;;) {
|
|
1703
|
+
const isOr = this.isKeyword('OR') || (this.peek().type === 'op' && this.peek().value === '||');
|
|
1704
|
+
if (!isOr) break;
|
|
1705
|
+
this.next();
|
|
1706
|
+
const right = this.parseXor();
|
|
1707
|
+
left = { type: 'or', left, right };
|
|
1708
|
+
}
|
|
1709
|
+
return left;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// 逻辑异或 XOR(MySQL 优先级:OR < XOR < AND)
|
|
1713
|
+
parseXor() {
|
|
1631
1714
|
let left = this.parseAnd();
|
|
1632
|
-
while (this.
|
|
1715
|
+
while (this.isWord('XOR')) {
|
|
1633
1716
|
this.next();
|
|
1634
1717
|
const right = this.parseAnd();
|
|
1635
|
-
left = { type: '
|
|
1718
|
+
left = { type: 'xor', left, right };
|
|
1636
1719
|
}
|
|
1637
1720
|
return left;
|
|
1638
1721
|
}
|
|
1639
1722
|
|
|
1640
1723
|
parseAnd() {
|
|
1641
1724
|
let left = this.parseNot();
|
|
1642
|
-
|
|
1725
|
+
for (;;) {
|
|
1726
|
+
const isAnd = this.isKeyword('AND') || (this.peek().type === 'op' && this.peek().value === '&&');
|
|
1727
|
+
if (!isAnd) break;
|
|
1643
1728
|
this.next();
|
|
1644
1729
|
const right = this.parseNot();
|
|
1645
1730
|
left = { type: 'and', left, right };
|
|
@@ -1981,15 +2066,65 @@ class Parser {
|
|
|
1981
2066
|
}
|
|
1982
2067
|
|
|
1983
2068
|
parseTerm() {
|
|
1984
|
-
let node = this.parseOperand();
|
|
2069
|
+
let node = this.parsePostfix(this.parseOperand());
|
|
1985
2070
|
for (;;) {
|
|
1986
2071
|
const t = this.peek();
|
|
1987
2072
|
if (t.type === 'op' && (t.value === '*' || t.value === '/' || t.value === '%')) {
|
|
1988
2073
|
this.next();
|
|
1989
|
-
const right = this.parseOperand();
|
|
2074
|
+
const right = this.parsePostfix(this.parseOperand());
|
|
1990
2075
|
node = { type: 'arith', op: t.value, left: node, right };
|
|
1991
2076
|
continue;
|
|
1992
2077
|
}
|
|
2078
|
+
// MySQL 整数除法:7 DIV 2 → 3
|
|
2079
|
+
if (this.isWord('DIV')) {
|
|
2080
|
+
this.next();
|
|
2081
|
+
const right = this.parsePostfix(this.parseOperand());
|
|
2082
|
+
node = { type: 'arith', op: 'DIV', left: node, right };
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
break;
|
|
2086
|
+
}
|
|
2087
|
+
return node;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* 后缀运算符(绑定最紧):PG 风格 `::` 类型转换、`->` / `->>` JSON 取值。
|
|
2092
|
+
* 例:'5'::INT、meta->>'name'、meta->'addr'->>'city'。
|
|
2093
|
+
*/
|
|
2094
|
+
parsePostfix(node) {
|
|
2095
|
+
for (;;) {
|
|
2096
|
+
const t = this.peek();
|
|
2097
|
+
if (t.type === 'op' && t.value === '::') {
|
|
2098
|
+
this.next();
|
|
2099
|
+
const typeTok = this.next();
|
|
2100
|
+
let dataType = String(typeTok.value).toUpperCase();
|
|
2101
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
2102
|
+
this.next();
|
|
2103
|
+
let depth = 1, spec = '';
|
|
2104
|
+
while (depth > 0 && this.peek().type !== 'eof') {
|
|
2105
|
+
const tk = this.next();
|
|
2106
|
+
if (tk.type === 'op' && tk.value === '(') depth++;
|
|
2107
|
+
else if (tk.type === 'op' && tk.value === ')') { depth--; if (depth === 0) break; }
|
|
2108
|
+
spec += tk.value;
|
|
2109
|
+
}
|
|
2110
|
+
dataType += '(' + spec + ')';
|
|
2111
|
+
}
|
|
2112
|
+
node = { type: 'cast', expr: node, dataType };
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
if (t.type === 'op' && (t.value === '->' || t.value === '->>')) {
|
|
2116
|
+
this.next();
|
|
2117
|
+
const key = this.parseValue();
|
|
2118
|
+
node = { type: 'jsonAccess', operand: node, key, asText: t.value === '->>' };
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
// PG 路径取值:meta#>'{a,b}' / meta#>>'{a,b}'
|
|
2122
|
+
if (t.type === 'op' && (t.value === '#>' || t.value === '#>>')) {
|
|
2123
|
+
this.next();
|
|
2124
|
+
const key = this.parseValue();
|
|
2125
|
+
node = { type: 'jsonAccess', operand: node, key, asText: t.value === '#>>', isPath: true };
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
1993
2128
|
break;
|
|
1994
2129
|
}
|
|
1995
2130
|
return node;
|
|
@@ -2085,7 +2220,31 @@ class Parser {
|
|
|
2085
2220
|
const isILike = !(t2.type === 'keyword' && t2.value === 'LIKE');
|
|
2086
2221
|
this.next();
|
|
2087
2222
|
const pattern = this.parseValue();
|
|
2088
|
-
|
|
2223
|
+
// 可选 ESCAPE 子句:LIKE '...' ESCAPE '!'
|
|
2224
|
+
let escape = null;
|
|
2225
|
+
if (this.isWord('ESCAPE')) { this.next(); escape = this.parseValue(); }
|
|
2226
|
+
return { type: 'like', operand: left, pattern, not, ci: isILike, escape };
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// PG 正则操作符(双目位置):~(大小写敏感)/ ~*(不敏感)/ !~ / !~*
|
|
2230
|
+
if (t2.type === 'op' && (t2.value === '~' || t2.value === '~*' || t2.value === '!~' || t2.value === '!~*')) {
|
|
2231
|
+
this.next();
|
|
2232
|
+
const pattern = this.parseValue();
|
|
2233
|
+
return {
|
|
2234
|
+
type: 'regexp', operand: left, pattern,
|
|
2235
|
+
not: t2.value.startsWith('!') || not,
|
|
2236
|
+
ci: t2.value.endsWith('*'),
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// PG JSON 键存在:? / ?| / ?&
|
|
2241
|
+
// 裸 `?` 在此处处于"运算符位置",与值位置上的 `?` 参数占位符不冲突。
|
|
2242
|
+
if ((t2.type === 'op' && (t2.value === '?|' || t2.value === '?&')) ||
|
|
2243
|
+
(t2.type === 'param' && t2.value === null)) {
|
|
2244
|
+
const mode = t2.type === 'param' ? '?' : t2.value;
|
|
2245
|
+
this.next();
|
|
2246
|
+
const keys = this.parseValue();
|
|
2247
|
+
return { type: 'jsonKeyExists', operand: left, keys, mode, not };
|
|
2089
2248
|
}
|
|
2090
2249
|
|
|
2091
2250
|
// MySQL 中 RLIKE 是 REGEXP 的同义词
|
|
@@ -2095,7 +2254,14 @@ class Parser {
|
|
|
2095
2254
|
return { type: 'regexp', operand: left, pattern, not };
|
|
2096
2255
|
}
|
|
2097
2256
|
|
|
2098
|
-
|
|
2257
|
+
// PG JSON 包含操作符
|
|
2258
|
+
if (t2.type === 'op' && (t2.value === '@>' || t2.value === '<@')) {
|
|
2259
|
+
this.next();
|
|
2260
|
+
const right = this.parseBitwise();
|
|
2261
|
+
return { type: 'jsonContains', op: t2.value, left, right, not };
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(t2.value)) {
|
|
2099
2265
|
this.next();
|
|
2100
2266
|
// = ANY (SELECT ...) / > ALL (SELECT ...)
|
|
2101
2267
|
const anyAll = this.isWord('ANY') || this.isWord('ALL') || this.isWord('SOME');
|
|
@@ -2209,6 +2375,8 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2209
2375
|
case '*': return l * r;
|
|
2210
2376
|
case '/': return r === 0 ? null : l / r;
|
|
2211
2377
|
case '%': return r === 0 ? null : l % r;
|
|
2378
|
+
// MySQL 整数除法 DIV(结果向零取整)
|
|
2379
|
+
case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
|
|
2212
2380
|
// 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
|
|
2213
2381
|
case '&': return (Number(l) | 0) & (Number(r) | 0);
|
|
2214
2382
|
case '|': return (Number(l) | 0) | (Number(r) | 0);
|
|
@@ -2229,6 +2397,7 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2229
2397
|
case 'compare':
|
|
2230
2398
|
case 'and':
|
|
2231
2399
|
case 'or':
|
|
2400
|
+
case 'xor':
|
|
2232
2401
|
case 'not':
|
|
2233
2402
|
case 'in':
|
|
2234
2403
|
case 'isNull':
|
|
@@ -2253,6 +2422,44 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2253
2422
|
const v = resolveOperand(operand.expr, row, ctx);
|
|
2254
2423
|
return applyCast(v, operand.dataType);
|
|
2255
2424
|
}
|
|
2425
|
+
case 'jsonAccess': {
|
|
2426
|
+
let base = resolveOperand(operand.operand, row, ctx);
|
|
2427
|
+
if (base === null || base === undefined) return null;
|
|
2428
|
+
// JSON 列可能以字符串形式存储,先解析
|
|
2429
|
+
if (typeof base === 'string') {
|
|
2430
|
+
try { base = JSON.parse(base); } catch (e) { return null; }
|
|
2431
|
+
}
|
|
2432
|
+
const keys = operand.isPath ? parsePgPath(operand.key) : [operand.key];
|
|
2433
|
+
let v = base;
|
|
2434
|
+
for (const key of keys) {
|
|
2435
|
+
if (v === null || v === undefined) return null;
|
|
2436
|
+
if (Array.isArray(v)) {
|
|
2437
|
+
const idx = Number(key);
|
|
2438
|
+
v = Number.isInteger(idx) ? (idx < 0 ? v[v.length + idx] : v[idx]) : undefined;
|
|
2439
|
+
} else if (typeof v === 'object') {
|
|
2440
|
+
v = v[key];
|
|
2441
|
+
} else {
|
|
2442
|
+
return null;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (v === undefined) return null;
|
|
2446
|
+
if (operand.asText) {
|
|
2447
|
+
if (v === null) return null;
|
|
2448
|
+
return typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
2449
|
+
}
|
|
2450
|
+
return v;
|
|
2451
|
+
}
|
|
2452
|
+
case 'jsonContains': {
|
|
2453
|
+
const l = resolveOperand(operand.left, row, ctx);
|
|
2454
|
+
const r = resolveOperand(operand.right, row, ctx);
|
|
2455
|
+
const res = jsonContains(l, r, operand.op === '<@');
|
|
2456
|
+
return operand.not ? !res : res;
|
|
2457
|
+
}
|
|
2458
|
+
case 'jsonKeyExists': {
|
|
2459
|
+
const base = resolveOperand(operand.operand, row, ctx);
|
|
2460
|
+
const res = jsonKeyExists(base, operand.keys, operand.mode);
|
|
2461
|
+
return operand.not ? !res : res;
|
|
2462
|
+
}
|
|
2256
2463
|
case 'quantified': {
|
|
2257
2464
|
// = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
|
|
2258
2465
|
const v = resolveOperand(operand.operand, row, ctx);
|
|
@@ -2471,13 +2678,76 @@ function seedRand(seed) {
|
|
|
2471
2678
|
}
|
|
2472
2679
|
|
|
2473
2680
|
// ci 默认 true:保持 LIKE 既有的大小写不敏感行为不变;ILIKE 同为不敏感。
|
|
2474
|
-
|
|
2681
|
+
// escape:可选转义符(ESCAPE 子句),默认反斜杠;转义符后跟 % _ 或转义符本身按字面量匹配。
|
|
2682
|
+
function likeMatch(value, pattern, ci = true, escape = '\\') {
|
|
2475
2683
|
if (typeof value !== 'string') return false;
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2684
|
+
const p = String(pattern);
|
|
2685
|
+
const e = (escape === null || escape === undefined || escape === '') ? '\\' : String(escape);
|
|
2686
|
+
const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2687
|
+
let out = '';
|
|
2688
|
+
for (let i = 0; i < p.length; i++) {
|
|
2689
|
+
const ch = p[i];
|
|
2690
|
+
if (ch === e && i + 1 < p.length && (p[i + 1] === '%' || p[i + 1] === '_' || p[i + 1] === e)) {
|
|
2691
|
+
out += reEsc(p[i + 1]);
|
|
2692
|
+
i++;
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
if (ch === '%') out += '.*';
|
|
2696
|
+
else if (ch === '_') out += '.';
|
|
2697
|
+
else out += reEsc(ch);
|
|
2698
|
+
}
|
|
2699
|
+
return new RegExp('^' + out + '$', ci ? 'i' : '').test(value);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
/** 解析 PG 的路径字面量:'{a,b}' 或数组 → ['a','b'] */
|
|
2703
|
+
function parsePgPath(key) {
|
|
2704
|
+
if (Array.isArray(key)) return key.map(String);
|
|
2705
|
+
if (typeof key === 'string') {
|
|
2706
|
+
const s = key.trim();
|
|
2707
|
+
if (s.startsWith('{') && s.endsWith('}')) {
|
|
2708
|
+
return s.slice(1, -1).split(',').map(x => x.trim().replace(/^"(.*)"$/, '$1')).filter(x => x !== '');
|
|
2709
|
+
}
|
|
2710
|
+
return [s];
|
|
2711
|
+
}
|
|
2712
|
+
return [String(key)];
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
/** 把可能是 JSON 字符串的值归一为 JS 值 */
|
|
2716
|
+
function toJsonValue(v) {
|
|
2717
|
+
if (typeof v === 'string') { try { return JSON.parse(v); } catch (e) { return v; } }
|
|
2718
|
+
return v;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
/** JSON 包含语义(PG 的 @> / <@)。swap=true 时判断 target 是否包含 container。 */
|
|
2722
|
+
function jsonContains(a, b, swap) {
|
|
2723
|
+
const container = toJsonValue(swap ? b : a);
|
|
2724
|
+
const target = toJsonValue(swap ? a : b);
|
|
2725
|
+
return jsonContainsValue(container, target);
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
function jsonContainsValue(container, target) {
|
|
2729
|
+
if (target === null || target === undefined) return true;
|
|
2730
|
+
if (Array.isArray(target)) {
|
|
2731
|
+
if (!Array.isArray(container)) return false;
|
|
2732
|
+
return target.every(t => container.some(c => jsonContainsValue(c, t)));
|
|
2733
|
+
}
|
|
2734
|
+
if (typeof target === 'object') {
|
|
2735
|
+
if (container === null || typeof container !== 'object' || Array.isArray(container)) return false;
|
|
2736
|
+
return Object.keys(target).every(k => Object.prototype.hasOwnProperty.call(container, k) && jsonContainsValue(container[k], target[k]));
|
|
2737
|
+
}
|
|
2738
|
+
if (Array.isArray(container)) return container.some(c => c === target || String(c) === String(target));
|
|
2739
|
+
if (container !== null && typeof container === 'object') return false;
|
|
2740
|
+
return container === target || String(container) === String(target);
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
/** JSON 键存在(PG 的 ? / ?| / ?&)。keys 为单个键名或 '{a,b}' 形式的键列表。 */
|
|
2744
|
+
function jsonKeyExists(base, keys, mode) {
|
|
2745
|
+
const v = toJsonValue(base);
|
|
2746
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
2747
|
+
const has = (k) => Object.prototype.hasOwnProperty.call(v, k);
|
|
2748
|
+
if (mode === '?|') return parsePgPath(keys).some(has);
|
|
2749
|
+
if (mode === '?&') return parsePgPath(keys).every(has);
|
|
2750
|
+
return has(String(keys));
|
|
2481
2751
|
}
|
|
2482
2752
|
|
|
2483
2753
|
function extractEqualPushdown(expr, schema) {
|
|
@@ -2529,10 +2799,18 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2529
2799
|
switch (expr.type) {
|
|
2530
2800
|
case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
|
|
2531
2801
|
case 'or': return evaluateExpr(expr.left, row, ctx) || evaluateExpr(expr.right, row, ctx);
|
|
2802
|
+
case 'xor': return Boolean(evaluateExpr(expr.left, row, ctx)) !== Boolean(evaluateExpr(expr.right, row, ctx));
|
|
2532
2803
|
case 'not': return !evaluateExpr(expr.expr, row, ctx);
|
|
2533
2804
|
case 'compare': {
|
|
2534
2805
|
const l = resolveOperand(expr.left, row, ctx);
|
|
2535
2806
|
const r = resolveOperand(expr.right, row, ctx);
|
|
2807
|
+
// MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
|
|
2808
|
+
if (expr.op === '<=>') {
|
|
2809
|
+
const ln = l === null || l === undefined;
|
|
2810
|
+
const rn = r === null || r === undefined;
|
|
2811
|
+
if (ln || rn) return ln && rn;
|
|
2812
|
+
return l === r || String(l) === String(r);
|
|
2813
|
+
}
|
|
2536
2814
|
// SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
|
|
2537
2815
|
if (l === null || r === null) return false;
|
|
2538
2816
|
// 布尔与数值互通(MySQL: 1 = TRUE、0 = FALSE)
|
|
@@ -2555,18 +2833,23 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2555
2833
|
}
|
|
2556
2834
|
case 'like': {
|
|
2557
2835
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2558
|
-
const r = likeMatch(v, expr.pattern, !!expr.ci);
|
|
2836
|
+
const r = likeMatch(v, expr.pattern, !!expr.ci, expr.escape);
|
|
2559
2837
|
return expr.not ? !r : r;
|
|
2560
2838
|
}
|
|
2561
2839
|
case 'quantified':
|
|
2562
2840
|
return !!resolveOperand(expr, row, ctx);
|
|
2841
|
+
case 'jsonContains':
|
|
2842
|
+
return !!resolveOperand(expr, row, ctx);
|
|
2563
2843
|
case 'regexp': {
|
|
2564
2844
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2565
2845
|
if (v === null || v === undefined) return false;
|
|
2566
|
-
|
|
2846
|
+
// REGEXP / RLIKE 历史上就是大小写不敏感的;`~` 敏感、`~*` 不敏感
|
|
2847
|
+
const re = new RegExp(String(expr.pattern), expr.ci === false ? '' : 'i');
|
|
2567
2848
|
const r = re.test(String(v));
|
|
2568
2849
|
return expr.not ? !r : r;
|
|
2569
2850
|
}
|
|
2851
|
+
case 'jsonKeyExists':
|
|
2852
|
+
return !!resolveOperand(expr, row, ctx);
|
|
2570
2853
|
case 'isTruth': {
|
|
2571
2854
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2572
2855
|
const isTrue = v === true || v === 1 || v === '1' || v === 'true' || v === 'TRUE' || v === 't' || (typeof v === 'number' && v !== 0);
|
|
@@ -2852,13 +3135,47 @@ class SQLExecutor {
|
|
|
2852
3135
|
dataRows = dataRows.map(stripDefault);
|
|
2853
3136
|
}
|
|
2854
3137
|
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
3138
|
+
const uniqueCols = schema ? Object.keys(schema).filter(k => schema[k].unique && !schema[k].primaryKey) : [];
|
|
2855
3139
|
let toInsert = dataRows;
|
|
2856
3140
|
let updated = 0;
|
|
2857
3141
|
let skipped = 0;
|
|
3142
|
+
// 非主键 UNIQUE 约束:默认动作(无 ON CONFLICT / IGNORE / REPLACE)下违反即 ER_DUP_ENTRY。
|
|
3143
|
+
// 主键冲突由下面的冲突逻辑处理;这里补唯一列(含批内重复)。
|
|
3144
|
+
if (uniqueCols.length > 0 && this._conflictAction(statement) === 'throw') {
|
|
3145
|
+
const existing = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 }))
|
|
3146
|
+
.map(r => normalizeRow(r, schema));
|
|
3147
|
+
for (const col of uniqueCols) {
|
|
3148
|
+
const used = new Set();
|
|
3149
|
+
for (const r of existing) {
|
|
3150
|
+
const v = r[col];
|
|
3151
|
+
if (v !== undefined && v !== null) used.add(String(v));
|
|
3152
|
+
}
|
|
3153
|
+
for (const d of dataRows) {
|
|
3154
|
+
const v = d[col];
|
|
3155
|
+
if (v === undefined || v === null) continue; // UNIQUE 允许多个 NULL
|
|
3156
|
+
const s = String(v);
|
|
3157
|
+
if (used.has(s)) throw new Error(`ER_DUP_ENTRY: Duplicate entry '${s}' for unique column '${col}'`);
|
|
3158
|
+
used.add(s);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
2858
3162
|
if (pkCols.length > 0) {
|
|
2859
3163
|
const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
|
|
2860
3164
|
const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
|
|
2861
|
-
|
|
3165
|
+
// 冲突判定的额外列组:
|
|
3166
|
+
// ON CONFLICT (col) DO UPDATE → 指定列(PG)
|
|
3167
|
+
// ON DUPLICATE KEY UPDATE → 各唯一列(MySQL:任一唯一键冲突都触发)
|
|
3168
|
+
// 此前只认主键,导致这两者在唯一列冲突时会插入重复行而不是更新。
|
|
3169
|
+
const ocTarget = (statement.onConflict && Array.isArray(statement.onConflict.target))
|
|
3170
|
+
? statement.onConflict.target.map(c => (c && c.name !== undefined ? c.name : c)).filter(Boolean)
|
|
3171
|
+
: null;
|
|
3172
|
+
const extraIdx = [];
|
|
3173
|
+
if (ocTarget && ocTarget.length) extraIdx.push(ocTarget);
|
|
3174
|
+
// 唯一列始终参与冲突判定(PG 的 ON CONFLICT 与 MySQL 的 ON DUPLICATE 都包含唯一键)
|
|
3175
|
+
for (const c of uniqueCols) extraIdx.push([c]);
|
|
3176
|
+
const hasAllOf = (cols) => (row) => cols.every(c => row[c] !== undefined && row[c] !== null);
|
|
3177
|
+
|
|
3178
|
+
const explicit = dataRows.filter((row) => hasExplicitPk(row) || extraIdx.some((cols) => hasAllOf(cols)(row)));
|
|
2862
3179
|
if (explicit.length > 0) {
|
|
2863
3180
|
const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
2864
3181
|
const pkMap = new Map();
|
|
@@ -2866,15 +3183,31 @@ class SQLExecutor {
|
|
|
2866
3183
|
const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
|
|
2867
3184
|
if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
|
|
2868
3185
|
}
|
|
3186
|
+
const pkValsOf = (row) => pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
|
|
3187
|
+
const extraMaps = extraIdx.map((cols) => {
|
|
3188
|
+
const m = new Map();
|
|
3189
|
+
const pick = hasAllOf(cols);
|
|
3190
|
+
const key = (row) => cols.map(c => String(row[c])).join('|');
|
|
3191
|
+
for (const row of all) {
|
|
3192
|
+
if (!pick(row)) continue;
|
|
3193
|
+
m.set(key(row), pkValsOf(row));
|
|
3194
|
+
}
|
|
3195
|
+
return { pick, key, m };
|
|
3196
|
+
});
|
|
2869
3197
|
const conflicts = [];
|
|
2870
3198
|
const fresh = [];
|
|
2871
3199
|
for (const d of dataRows) {
|
|
2872
|
-
|
|
2873
|
-
if (hasExplicitPk(d) && pkMap.has(
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
3200
|
+
let existingId = null;
|
|
3201
|
+
if (hasExplicitPk(d) && pkMap.has(keyOf(d))) existingId = pkMap.get(keyOf(d));
|
|
3202
|
+
if (!existingId) {
|
|
3203
|
+
for (const { pick, key, m } of extraMaps) {
|
|
3204
|
+
if (!pick(d)) continue;
|
|
3205
|
+
const k = key(d);
|
|
3206
|
+
if (m.has(k)) { existingId = m.get(k); break; }
|
|
3207
|
+
}
|
|
2877
3208
|
}
|
|
3209
|
+
if (existingId) conflicts.push({ d, existingId });
|
|
3210
|
+
else fresh.push(d);
|
|
2878
3211
|
}
|
|
2879
3212
|
const conflictAction = this._conflictAction(statement);
|
|
2880
3213
|
const conflictSets = this._conflictSets(statement);
|
|
@@ -2902,10 +3235,14 @@ class SQLExecutor {
|
|
|
2902
3235
|
const data = {};
|
|
2903
3236
|
if (conflictSets) {
|
|
2904
3237
|
for (const [col, val] of conflictSets) {
|
|
2905
|
-
// VALUES(col)
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3238
|
+
// VALUES(col) 引用本次本应插入的值;EXCLUDED.col 为 PG 同义写法
|
|
3239
|
+
if (val && typeof val === 'object' && val._valuesOf !== undefined) {
|
|
3240
|
+
data[col] = d[val._valuesOf] !== undefined ? d[val._valuesOf] : null;
|
|
3241
|
+
} else if (val && typeof val === 'object' && val._excludedOf !== undefined) {
|
|
3242
|
+
data[col] = d[val._excludedOf] !== undefined ? d[val._excludedOf] : null;
|
|
3243
|
+
} else {
|
|
3244
|
+
data[col] = val;
|
|
3245
|
+
}
|
|
2909
3246
|
}
|
|
2910
3247
|
} else {
|
|
2911
3248
|
for (const k of Object.keys(d)) if (!pkCols.includes(k)) data[k] = d[k];
|
|
@@ -4493,53 +4830,88 @@ class SQLExecutor {
|
|
|
4493
4830
|
}
|
|
4494
4831
|
}
|
|
4495
4832
|
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
while (i < sql.length) {
|
|
4833
|
+
/** 跳过一个引号串(含转义/反引号成对),返回闭合引号之后的下标 */
|
|
4834
|
+
function skipQuoted(sql, i, q) {
|
|
4835
|
+
const n = sql.length;
|
|
4836
|
+
i++; // 跳过起始引号
|
|
4837
|
+
while (i < n) {
|
|
4502
4838
|
const c = sql[i];
|
|
4503
|
-
if (
|
|
4504
|
-
|
|
4505
|
-
if (
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
if (c === '`') inStr = null;
|
|
4509
|
-
i++;
|
|
4510
|
-
continue;
|
|
4839
|
+
if (q === '`') {
|
|
4840
|
+
// 反引号标识符内部反斜杠不是转义符;两个连续反引号表示一个字面反引号。
|
|
4841
|
+
if (c === '`') {
|
|
4842
|
+
if (sql[i + 1] === '`') { i += 2; continue; }
|
|
4843
|
+
return i + 1;
|
|
4511
4844
|
}
|
|
4512
|
-
if (c === '\\' && i + 1 < sql.length) { current += sql[i + 1]; i += 2; continue; }
|
|
4513
|
-
if (c === inStr) inStr = null;
|
|
4514
4845
|
i++;
|
|
4515
4846
|
continue;
|
|
4516
4847
|
}
|
|
4517
|
-
if (c ===
|
|
4848
|
+
if (c === '\\') { i += 2; continue; }
|
|
4849
|
+
if (c === q) return i + 1;
|
|
4850
|
+
i++;
|
|
4851
|
+
}
|
|
4852
|
+
return n; // 未闭合:到结尾
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
function splitStatements(sql, opts = {}) {
|
|
4856
|
+
if (typeof sql !== 'string') {
|
|
4857
|
+
throw new Error('splitStatements: sql must be a string');
|
|
4858
|
+
}
|
|
4859
|
+
const maxLength = opts.maxLength == null ? DEFAULT_MAX_SQL_LENGTH : opts.maxLength;
|
|
4860
|
+
if (typeof maxLength === 'number' && Number.isFinite(maxLength) && sql.length > maxLength) {
|
|
4861
|
+
throw new Error(
|
|
4862
|
+
`SQL text too large: ${sql.length} chars exceeds limit of ${maxLength} ` +
|
|
4863
|
+
`(raise opts.maxSqlLength to allow larger input)`
|
|
4864
|
+
);
|
|
4865
|
+
}
|
|
4866
|
+
const statements = [];
|
|
4867
|
+
// 用「区间切片」收集,而不是逐字符拼接:既避免 `current += c` 的 O(n²) 拷贝放大,
|
|
4868
|
+
// 也避免为每个字符建一个小字符串对象(20MB 无分号输入下两者都会打爆默认堆)。
|
|
4869
|
+
let parts = [];
|
|
4870
|
+
const take = end => { parts.push(sql.slice(segStart, end)); };
|
|
4871
|
+
const flush = () => {
|
|
4872
|
+
if (parts.length === 0) return;
|
|
4873
|
+
const stmt = parts.join('').trim();
|
|
4874
|
+
parts = [];
|
|
4875
|
+
if (stmt) statements.push(stmt);
|
|
4876
|
+
};
|
|
4877
|
+
|
|
4878
|
+
let segStart = 0;
|
|
4879
|
+
let i = 0;
|
|
4880
|
+
const n = sql.length;
|
|
4881
|
+
while (i < n) {
|
|
4882
|
+
const c = sql[i];
|
|
4883
|
+
if (c === "'" || c === '"' || c === '`') { i = skipQuoted(sql, i, c); continue; }
|
|
4518
4884
|
if (c === '-' && sql[i + 1] === '-') {
|
|
4519
|
-
|
|
4885
|
+
// 行注释:丢弃注释文本本身(与原实现一致),保留其后的换行
|
|
4886
|
+
take(i);
|
|
4887
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
4888
|
+
segStart = i;
|
|
4520
4889
|
continue;
|
|
4521
4890
|
}
|
|
4522
|
-
if (c === '#') {
|
|
4523
|
-
|
|
4891
|
+
if (c === '#' && sql[i + 1] !== '>') {
|
|
4892
|
+
take(i);
|
|
4893
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
4894
|
+
segStart = i;
|
|
4524
4895
|
continue;
|
|
4525
4896
|
}
|
|
4526
4897
|
if (c === '/' && sql[i + 1] === '*') {
|
|
4527
|
-
|
|
4898
|
+
// 块注释:原实现会保留 `/* */` 原文,这里通过切片一并保留
|
|
4528
4899
|
i += 2;
|
|
4529
|
-
while (i + 1 <
|
|
4530
|
-
|
|
4900
|
+
while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
|
|
4901
|
+
i = i + 1 < n ? i + 2 : n;
|
|
4531
4902
|
continue;
|
|
4532
4903
|
}
|
|
4533
4904
|
if (c === ';') {
|
|
4534
|
-
|
|
4535
|
-
|
|
4905
|
+
take(i);
|
|
4906
|
+
flush();
|
|
4536
4907
|
i++;
|
|
4908
|
+
segStart = i;
|
|
4537
4909
|
continue;
|
|
4538
4910
|
}
|
|
4539
|
-
current += c;
|
|
4540
4911
|
i++;
|
|
4541
4912
|
}
|
|
4542
|
-
|
|
4913
|
+
take(n);
|
|
4914
|
+
flush();
|
|
4543
4915
|
return statements;
|
|
4544
4916
|
}
|
|
4545
4917
|
|
|
@@ -4566,7 +4938,7 @@ function hasComments(sql) {
|
|
|
4566
4938
|
if (c === "'" || c === '"') { inStr = c; i++; continue; }
|
|
4567
4939
|
if (c === '`') { i++; while (i < sql.length && sql[i] !== '`') i++; i++; continue; }
|
|
4568
4940
|
if (c === '-' && sql[i + 1] === '-') return true;
|
|
4569
|
-
if (c === '#') return true;
|
|
4941
|
+
if (c === '#' && sql[i + 1] !== '>') return true; // '#>' / '#>>' 是 JSON 操作符,不是注释
|
|
4570
4942
|
if (c === '/' && sql[i + 1] === '*') return true;
|
|
4571
4943
|
i++;
|
|
4572
4944
|
}
|
|
@@ -4615,16 +4987,34 @@ function applyParams(sql, values) {
|
|
|
4615
4987
|
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
4616
4988
|
hasNamed = true;
|
|
4617
4989
|
}
|
|
4618
|
-
|
|
4990
|
+
const n = sql.length;
|
|
4991
|
+
while (i < n) {
|
|
4619
4992
|
const c = sql[i];
|
|
4620
4993
|
if (inStr) {
|
|
4621
4994
|
out += c;
|
|
4622
|
-
if (c === '\\' && i + 1 <
|
|
4995
|
+
if (c === '\\' && i + 1 < n) { out += sql[i + 1]; i += 2; continue; }
|
|
4623
4996
|
if (c === inStr) inStr = null;
|
|
4624
4997
|
i++;
|
|
4625
4998
|
continue;
|
|
4626
4999
|
}
|
|
4627
5000
|
if (c === "'" || c === '"' || c === '`') { inStr = c; out += c; i++; continue; }
|
|
5001
|
+
// 跳过注释:`--` 和 `#`(但 `#>` / `#>>` 是 JSON 操作符,不是注释)
|
|
5002
|
+
if (c === '-' && sql[i + 1] === '-') {
|
|
5003
|
+
// 行注释:整段跳过,保留换行(不在注释里的字符不进入 out)
|
|
5004
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
5005
|
+
continue;
|
|
5006
|
+
}
|
|
5007
|
+
if (c === '#' && sql[i + 1] !== '>') {
|
|
5008
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
5009
|
+
continue;
|
|
5010
|
+
}
|
|
5011
|
+
if (c === '/' && sql[i + 1] === '*') {
|
|
5012
|
+
// 块注释:整段跳过
|
|
5013
|
+
i += 2;
|
|
5014
|
+
while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
|
|
5015
|
+
if (i + 1 < n) i += 2; else i = n;
|
|
5016
|
+
continue;
|
|
5017
|
+
}
|
|
4628
5018
|
if (c === '?' && sql[i + 1] === '?') {
|
|
4629
5019
|
if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
4630
5020
|
out += escapeId(hasNamed ? args['@@'] : args[idx++]);
|
|
@@ -4657,6 +5047,24 @@ function applyParams(sql, values) {
|
|
|
4657
5047
|
i++;
|
|
4658
5048
|
continue;
|
|
4659
5049
|
}
|
|
5050
|
+
// $N 编号占位符(PG 风格):$1 / $2 ...
|
|
5051
|
+
if (c === '$' && i + 1 < sql.length && sql[i + 1] >= '0' && sql[i + 1] <= '9') {
|
|
5052
|
+
let num = '';
|
|
5053
|
+
let j = i + 1;
|
|
5054
|
+
while (j < sql.length && sql[j] >= '0' && sql[j] <= '9') { num += sql[j]; j++; }
|
|
5055
|
+
const n = parseInt(num, 10);
|
|
5056
|
+
if (hasNamed) {
|
|
5057
|
+
if (!(n in args)) throw new Error(`No value for parameter $${n}`);
|
|
5058
|
+
out += escapeValue(args[n]);
|
|
5059
|
+
} else {
|
|
5060
|
+
if (n - 1 >= args.length) throw new Error(`Not enough parameters for SQL: expected $${n}`);
|
|
5061
|
+
out += escapeValue(args[n - 1]);
|
|
5062
|
+
if (n > idx) idx = n;
|
|
5063
|
+
}
|
|
5064
|
+
count++;
|
|
5065
|
+
i = j;
|
|
5066
|
+
continue;
|
|
5067
|
+
}
|
|
4660
5068
|
if ((c === ':' || c === '@' || c === '$') && i + 1 < sql.length && /[A-Za-z_]/.test(sql[i + 1])) {
|
|
4661
5069
|
// 命名占位符 :name @name $name
|
|
4662
5070
|
let name = '';
|
|
@@ -4678,6 +5086,44 @@ function applyParams(sql, values) {
|
|
|
4678
5086
|
return out;
|
|
4679
5087
|
}
|
|
4680
5088
|
|
|
5089
|
+
// 把执行器结果补齐为 README 承诺的结果信封
|
|
5090
|
+
// (columnTypes / rowCount / message / command / durationMs / warnings)
|
|
5091
|
+
function buildResultEnvelope(stmt, r, durationMs) {
|
|
5092
|
+
if (r === null || typeof r !== 'object' || Array.isArray(r)) return r;
|
|
5093
|
+
const out = { ...r };
|
|
5094
|
+
const t = stmt && stmt.type;
|
|
5095
|
+
if (out.command === undefined) out.command = String(t || '').toUpperCase();
|
|
5096
|
+
if (out.affectedRows === undefined) out.affectedRows = 0;
|
|
5097
|
+
if (out.rowCount === undefined) {
|
|
5098
|
+
out.rowCount = Array.isArray(out.rows) ? out.rows.length
|
|
5099
|
+
: (typeof out.affectedRows === 'number' ? out.affectedRows : 0);
|
|
5100
|
+
}
|
|
5101
|
+
if (out.warnings === undefined) out.warnings = [];
|
|
5102
|
+
if (out.durationMs === undefined) out.durationMs = durationMs;
|
|
5103
|
+
if (out.message === undefined) {
|
|
5104
|
+
out.message = t === 'select'
|
|
5105
|
+
? `${out.rowCount} row${out.rowCount === 1 ? '' : 's'} selected`
|
|
5106
|
+
: `${out.affectedRows} row${out.affectedRows === 1 ? '' : 's'} affected`;
|
|
5107
|
+
}
|
|
5108
|
+
return out;
|
|
5109
|
+
}
|
|
5110
|
+
|
|
5111
|
+
// SELECT 结果的列类型:单表查询按 schema 推断;表达式/多表查询该列为 null
|
|
5112
|
+
async function inferColumnTypes(engine, stmt, r) {
|
|
5113
|
+
if (!r || !Array.isArray(r.columns)) return undefined;
|
|
5114
|
+
let schema = null;
|
|
5115
|
+
try {
|
|
5116
|
+
const t = stmt && stmt.type === 'select' && stmt.from &&
|
|
5117
|
+
Array.isArray(stmt.from.tables) && stmt.from.tables.length === 1
|
|
5118
|
+
? stmt.from.tables[0].table : null;
|
|
5119
|
+
if (t && engine && typeof engine.getTableSchema === 'function') schema = await engine.getTableSchema(t);
|
|
5120
|
+
} catch (e) { schema = null; }
|
|
5121
|
+
return r.columns.map((name) => {
|
|
5122
|
+
const def = schema && schema[name];
|
|
5123
|
+
return def && def.type ? sqlTypeName(def.type).toUpperCase() : null;
|
|
5124
|
+
});
|
|
5125
|
+
}
|
|
5126
|
+
|
|
4681
5127
|
async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
4682
5128
|
// 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
|
|
4683
5129
|
// 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
|
|
@@ -4701,14 +5147,19 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
4701
5147
|
if (Array.isArray(paramsOrOpts)) {
|
|
4702
5148
|
sql = applyParams(sql, paramsOrOpts);
|
|
4703
5149
|
} else if (paramsOrOpts && typeof paramsOrOpts === 'object') {
|
|
4704
|
-
opts
|
|
5150
|
+
// 第三参为对象时:含已知 opts 键 → 视为 opts;否则视为命名参数({id: 1} → :id / $id)
|
|
5151
|
+
const OPTS_KEYS = ['safety', 'session', 'params', 'functions', 'aggregates',
|
|
5152
|
+
'allowComments', 'maxStatements', 'maxSqlLength', 'context', 'timeout', 'dialect'];
|
|
5153
|
+
const looksLikeOpts = Object.keys(paramsOrOpts).some(k => OPTS_KEYS.includes(k));
|
|
5154
|
+
if (looksLikeOpts) opts = paramsOrOpts;
|
|
5155
|
+
else sql = applyParams(sql, paramsOrOpts);
|
|
4705
5156
|
}
|
|
4706
5157
|
if (opts.safety !== false) {
|
|
4707
5158
|
if (!opts.allowComments && hasComments(sql)) {
|
|
4708
5159
|
throw new Error('SQL comments are disabled for security (--, #, /* */)');
|
|
4709
5160
|
}
|
|
4710
5161
|
}
|
|
4711
|
-
let statements = splitStatements(sql);
|
|
5162
|
+
let statements = splitStatements(sql, { maxLength: opts.maxSqlLength });
|
|
4712
5163
|
if (opts.maxStatements != null && statements.length > opts.maxStatements) {
|
|
4713
5164
|
throw new Error(`too many statements (${statements.length} > ${opts.maxStatements})`);
|
|
4714
5165
|
}
|
|
@@ -4727,7 +5178,14 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
4727
5178
|
const results = [];
|
|
4728
5179
|
for (const stmtSql of statements) {
|
|
4729
5180
|
const stmt = parseSQL(stmtSql);
|
|
4730
|
-
|
|
5181
|
+
const t0 = process.hrtime.bigint();
|
|
5182
|
+
let r = await executor.execute(stmt);
|
|
5183
|
+
const durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
5184
|
+
r = buildResultEnvelope(stmt, r, Math.round(durationMs * 1000) / 1000);
|
|
5185
|
+
if (r && r.columns && r.columnTypes === undefined) {
|
|
5186
|
+
r.columnTypes = await inferColumnTypes(engine, stmt, r);
|
|
5187
|
+
}
|
|
5188
|
+
results.push(r);
|
|
4731
5189
|
}
|
|
4732
5190
|
return results.length === 1 ? results[0] : results;
|
|
4733
5191
|
}
|