jsql-neo 6.1.0 → 6.3.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 +32 -20
- package/lib/date-types.js +30 -13
- package/lib/migrate.js +126 -30
- package/lib/sql.js +722 -83
- package/package.json +1 -1
- package/postinstall.js +61 -17
- package/test/readme-audit.test.js +186 -4
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,10 +83,12 @@ function tokenize(sql) {
|
|
|
79
83
|
while (i < n && sql[i] !== '\n') i++;
|
|
80
84
|
continue;
|
|
81
85
|
}
|
|
82
|
-
// PG 的 JSON 路径操作符 #> /
|
|
83
|
-
// 若不在这里拦截,'#' 会被当成行注释吃掉,导致 SQL 后半段被静默丢弃。
|
|
86
|
+
// PG 的 JSON 路径操作符 #> / #>>(必须在 '#' 行注释之前识别)
|
|
84
87
|
if (c === '#' && sql[i + 1] === '>') {
|
|
85
|
-
|
|
88
|
+
const op = sql[i + 2] === '>' ? '#>>' : '#>';
|
|
89
|
+
tokens.push(new SQLToken('op', op, i));
|
|
90
|
+
i += op.length;
|
|
91
|
+
continue;
|
|
86
92
|
}
|
|
87
93
|
if (c === '#' || (c === '/' && sql[i + 1] === '*')) {
|
|
88
94
|
if (c === '#') { while (i < n && sql[i] !== '\n') i++; continue; }
|
|
@@ -106,12 +112,15 @@ function tokenize(sql) {
|
|
|
106
112
|
else str += esc;
|
|
107
113
|
j += 2;
|
|
108
114
|
} else if (sql[j] === quote) {
|
|
115
|
+
// SQL 标准转义:字符串内两个连续引号表示一个字面量引号('O''Brien' → O'Brien)
|
|
116
|
+
if (sql[j + 1] === quote) { str += quote; j += 2; continue; }
|
|
109
117
|
break;
|
|
110
118
|
} else {
|
|
111
119
|
str += sql[j];
|
|
112
120
|
j++;
|
|
113
121
|
}
|
|
114
122
|
}
|
|
123
|
+
if (j >= n) throw new Error(`Unterminated ${quote === '"' ? 'double' : 'single'}-quoted string at position ${i}`);
|
|
115
124
|
tokens.push(new SQLToken('string', str, i));
|
|
116
125
|
i = j + 1;
|
|
117
126
|
continue;
|
|
@@ -153,15 +162,17 @@ function tokenize(sql) {
|
|
|
153
162
|
if (c === '`') {
|
|
154
163
|
let j = i + 1;
|
|
155
164
|
while (j < n && sql[j] !== '`') j++;
|
|
165
|
+
if (j >= n) throw new Error(`Unterminated backtick-quoted identifier at position ${i}`);
|
|
156
166
|
tokens.push(new SQLToken('ident', sql.slice(i + 1, j), i));
|
|
157
167
|
i = j + 1;
|
|
158
168
|
continue;
|
|
159
169
|
}
|
|
160
170
|
|
|
161
|
-
// PG 的 JSON 包含操作符 @>
|
|
162
|
-
// 若不拦截,'@' 会被 sysvar 分支静默吞掉、'>' 退化成普通大于号,产生错误结果。
|
|
171
|
+
// PG 的 JSON 包含操作符 @>
|
|
163
172
|
if (c === '@' && sql[i + 1] === '>') {
|
|
164
|
-
|
|
173
|
+
tokens.push(new SQLToken('op', '@>', i));
|
|
174
|
+
i += 2;
|
|
175
|
+
continue;
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
if (c === '@') {
|
|
@@ -181,6 +192,12 @@ function tokenize(sql) {
|
|
|
181
192
|
}
|
|
182
193
|
|
|
183
194
|
// 参数占位符:? (顺序)、?N (编号)、?? (标识符占位)
|
|
195
|
+
// 注意:?| / ?& 是 PG 的 JSON 键存在操作符,必须优先识别
|
|
196
|
+
if (c === '?' && (sql[i + 1] === '|' || sql[i + 1] === '&')) {
|
|
197
|
+
tokens.push(new SQLToken('op', '?' + sql[i + 1], i));
|
|
198
|
+
i += 2;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
184
201
|
if (c === '?') {
|
|
185
202
|
if (sql[i + 1] === '?') { tokens.push(new SQLToken('param', '??', i)); i += 2; continue; }
|
|
186
203
|
let j = i + 1;
|
|
@@ -193,9 +210,11 @@ function tokenize(sql) {
|
|
|
193
210
|
|
|
194
211
|
const three = sql.slice(i, i + 3);
|
|
195
212
|
if (sql.slice(i, i + 2) === '<@') {
|
|
196
|
-
|
|
213
|
+
tokens.push(new SQLToken('op', '<@', i));
|
|
214
|
+
i += 2;
|
|
215
|
+
continue;
|
|
197
216
|
}
|
|
198
|
-
if (three === '->>' || three === '<=>') {
|
|
217
|
+
if (three === '->>' || three === '<=>' || three === '!~*') {
|
|
199
218
|
tokens.push(new SQLToken('op', three, i));
|
|
200
219
|
i += 3;
|
|
201
220
|
continue;
|
|
@@ -204,7 +223,7 @@ function tokenize(sql) {
|
|
|
204
223
|
const two = sql.slice(i, i + 2);
|
|
205
224
|
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
|
|
206
225
|
two === '<<' || two === '>>' || two === '->' || two === '::' ||
|
|
207
|
-
two === '&&' || two === '||') {
|
|
226
|
+
two === '&&' || two === '||' || two === '~*' || two === '!~') {
|
|
208
227
|
tokens.push(new SQLToken('op', two, i));
|
|
209
228
|
i += 2;
|
|
210
229
|
continue;
|
|
@@ -1838,6 +1857,13 @@ class Parser {
|
|
|
1838
1857
|
this.expect('op', ')');
|
|
1839
1858
|
return { type: 'cast', expr, dataType };
|
|
1840
1859
|
}
|
|
1860
|
+
// 零参时间函数:CURRENT_DATE / CURRENT_TIME / CURRENT_TIMESTAMP(可省略括号)。
|
|
1861
|
+
// 此前不在 KEYWORDS 里,被当成列名解析 → "no such column: CURRENT_DATE"
|
|
1862
|
+
if (['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP'].includes(String(t.value).toUpperCase())) {
|
|
1863
|
+
const name = String(t.value).toUpperCase();
|
|
1864
|
+
if (this.peek().type === 'op' && this.peek().value === '(') { this.next(); this.expect('op', ')'); }
|
|
1865
|
+
return { type: 'func', name, args: [] };
|
|
1866
|
+
}
|
|
1841
1867
|
// EXTRACT(YEAR FROM expr) —— 标准 SQL 的 FROM 形式
|
|
1842
1868
|
if (String(t.value).toUpperCase() === 'EXTRACT' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1843
1869
|
this.next();
|
|
@@ -1943,6 +1969,13 @@ class Parser {
|
|
|
1943
1969
|
this.expect('op', ')');
|
|
1944
1970
|
return this.parseOptionalOver({ type: 'func', name, args });
|
|
1945
1971
|
}
|
|
1972
|
+
// 时间单位裸关键字(TIMESTAMPDIFF(MONTH, a, b) 里的 MONTH):
|
|
1973
|
+
// 解析成 unit 节点,取值时优先当列、取不到才当单位名,
|
|
1974
|
+
// 这样既支持裸写法,也不影响真正名为 month/year 的列。
|
|
1975
|
+
if (TIME_UNITS.has(String(t.value).toUpperCase()) &&
|
|
1976
|
+
!(this.peek().type === 'op' && (this.peek().value === '(' || this.peek().value === '.'))) {
|
|
1977
|
+
return { type: 'unit', name: String(t.value).toUpperCase() };
|
|
1978
|
+
}
|
|
1946
1979
|
return { type: 'column', name: t.value };
|
|
1947
1980
|
}
|
|
1948
1981
|
if (t.type === 'number' || t.type === 'string') return { type: 'value', value: t.value };
|
|
@@ -2101,6 +2134,13 @@ class Parser {
|
|
|
2101
2134
|
node = { type: 'jsonAccess', operand: node, key, asText: t.value === '->>' };
|
|
2102
2135
|
continue;
|
|
2103
2136
|
}
|
|
2137
|
+
// PG 路径取值:meta#>'{a,b}' / meta#>>'{a,b}'
|
|
2138
|
+
if (t.type === 'op' && (t.value === '#>' || t.value === '#>>')) {
|
|
2139
|
+
this.next();
|
|
2140
|
+
const key = this.parseValue();
|
|
2141
|
+
node = { type: 'jsonAccess', operand: node, key, asText: t.value === '#>>', isPath: true };
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2104
2144
|
break;
|
|
2105
2145
|
}
|
|
2106
2146
|
return node;
|
|
@@ -2193,13 +2233,38 @@ class Parser {
|
|
|
2193
2233
|
|
|
2194
2234
|
// ILIKE 刻意不加进 KEYWORDS(避免影响同名标识符),用 isWord 按上下文匹配
|
|
2195
2235
|
if ((t2.type === 'keyword' && t2.value === 'LIKE') || this.isWord('ILIKE')) {
|
|
2196
|
-
const isILike = !(t2.type === 'keyword' && t2.value === 'LIKE');
|
|
2197
2236
|
this.next();
|
|
2237
|
+
// 可选 BINARY 关键字:LIKE BINARY '...' 才区分大小写
|
|
2238
|
+
let binary = false;
|
|
2239
|
+
if (this.isWord('BINARY')) { this.next(); binary = true; }
|
|
2198
2240
|
const pattern = this.parseValue();
|
|
2199
2241
|
// 可选 ESCAPE 子句:LIKE '...' ESCAPE '!'
|
|
2200
2242
|
let escape = null;
|
|
2201
2243
|
if (this.isWord('ESCAPE')) { this.next(); escape = this.parseValue(); }
|
|
2202
|
-
|
|
2244
|
+
// MySQL 的 LIKE 默认不区分大小写(默认 collation 为 CI),
|
|
2245
|
+
// 此前写成 ci: isILike,导致 LIKE 恒为大小写敏感。
|
|
2246
|
+
return { type: 'like', operand: left, pattern, not, ci: !binary, escape };
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// PG 正则操作符(双目位置):~(大小写敏感)/ ~*(不敏感)/ !~ / !~*
|
|
2250
|
+
if (t2.type === 'op' && (t2.value === '~' || t2.value === '~*' || t2.value === '!~' || t2.value === '!~*')) {
|
|
2251
|
+
this.next();
|
|
2252
|
+
const pattern = this.parseValue();
|
|
2253
|
+
return {
|
|
2254
|
+
type: 'regexp', operand: left, pattern,
|
|
2255
|
+
not: t2.value.startsWith('!') || not,
|
|
2256
|
+
ci: t2.value.endsWith('*'),
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// PG JSON 键存在:? / ?| / ?&
|
|
2261
|
+
// 裸 `?` 在此处处于"运算符位置",与值位置上的 `?` 参数占位符不冲突。
|
|
2262
|
+
if ((t2.type === 'op' && (t2.value === '?|' || t2.value === '?&')) ||
|
|
2263
|
+
(t2.type === 'param' && t2.value === null)) {
|
|
2264
|
+
const mode = t2.type === 'param' ? '?' : t2.value;
|
|
2265
|
+
this.next();
|
|
2266
|
+
const keys = this.parseValue();
|
|
2267
|
+
return { type: 'jsonKeyExists', operand: left, keys, mode, not };
|
|
2203
2268
|
}
|
|
2204
2269
|
|
|
2205
2270
|
// MySQL 中 RLIKE 是 REGEXP 的同义词
|
|
@@ -2209,6 +2274,13 @@ class Parser {
|
|
|
2209
2274
|
return { type: 'regexp', operand: left, pattern, not };
|
|
2210
2275
|
}
|
|
2211
2276
|
|
|
2277
|
+
// PG JSON 包含操作符
|
|
2278
|
+
if (t2.type === 'op' && (t2.value === '@>' || t2.value === '<@')) {
|
|
2279
|
+
this.next();
|
|
2280
|
+
const right = this.parseBitwise();
|
|
2281
|
+
return { type: 'jsonContains', op: t2.value, left, right, not };
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2212
2284
|
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(t2.value)) {
|
|
2213
2285
|
this.next();
|
|
2214
2286
|
// = ANY (SELECT ...) / > ALL (SELECT ...)
|
|
@@ -2355,6 +2427,12 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2355
2427
|
case 'like':
|
|
2356
2428
|
case 'between':
|
|
2357
2429
|
return evaluateExpr(operand, row, ctx);
|
|
2430
|
+
case 'unit': {
|
|
2431
|
+
// 裸时间单位(MONTH / DAY …):优先当列取,取不到就返回单位名本身
|
|
2432
|
+
const lower = String(operand.name).toLowerCase();
|
|
2433
|
+
const v = row ? row[lower] : undefined;
|
|
2434
|
+
return v !== undefined ? v : String(operand.name).toUpperCase();
|
|
2435
|
+
}
|
|
2358
2436
|
case 'param': {
|
|
2359
2437
|
// 原生 ? 占位符。executeSQL 默认会先用 applyParams 内联替换,
|
|
2360
2438
|
// 只有在显式传入 ctx.params 时才走这条路径。
|
|
@@ -2377,15 +2455,18 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2377
2455
|
if (typeof base === 'string') {
|
|
2378
2456
|
try { base = JSON.parse(base); } catch (e) { return null; }
|
|
2379
2457
|
}
|
|
2380
|
-
const
|
|
2381
|
-
let v;
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2458
|
+
const keys = operand.isPath ? parsePgPath(operand.key) : [operand.key];
|
|
2459
|
+
let v = base;
|
|
2460
|
+
for (const key of keys) {
|
|
2461
|
+
if (v === null || v === undefined) return null;
|
|
2462
|
+
if (Array.isArray(v)) {
|
|
2463
|
+
const idx = Number(key);
|
|
2464
|
+
v = Number.isInteger(idx) ? (idx < 0 ? v[v.length + idx] : v[idx]) : undefined;
|
|
2465
|
+
} else if (typeof v === 'object') {
|
|
2466
|
+
v = v[key];
|
|
2467
|
+
} else {
|
|
2468
|
+
return null;
|
|
2469
|
+
}
|
|
2389
2470
|
}
|
|
2390
2471
|
if (v === undefined) return null;
|
|
2391
2472
|
if (operand.asText) {
|
|
@@ -2394,6 +2475,17 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2394
2475
|
}
|
|
2395
2476
|
return v;
|
|
2396
2477
|
}
|
|
2478
|
+
case 'jsonContains': {
|
|
2479
|
+
const l = resolveOperand(operand.left, row, ctx);
|
|
2480
|
+
const r = resolveOperand(operand.right, row, ctx);
|
|
2481
|
+
const res = jsonContains(l, r, operand.op === '<@');
|
|
2482
|
+
return operand.not ? !res : res;
|
|
2483
|
+
}
|
|
2484
|
+
case 'jsonKeyExists': {
|
|
2485
|
+
const base = resolveOperand(operand.operand, row, ctx);
|
|
2486
|
+
const res = jsonKeyExists(base, operand.keys, operand.mode);
|
|
2487
|
+
return operand.not ? !res : res;
|
|
2488
|
+
}
|
|
2397
2489
|
case 'quantified': {
|
|
2398
2490
|
// = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
|
|
2399
2491
|
const v = resolveOperand(operand.operand, row, ctx);
|
|
@@ -2425,12 +2517,161 @@ function parseSqlDate(v) {
|
|
|
2425
2517
|
|
|
2426
2518
|
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
2427
2519
|
|
|
2520
|
+
/** 本地时区的 'YYYY-MM-DD HH:mm:ss'(MySQL 的 NOW() 语义;区别于 UTC_TIMESTAMP) */
|
|
2521
|
+
function sqlLocalDateTime(d = new Date()) {
|
|
2522
|
+
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' +
|
|
2523
|
+
pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2428
2526
|
/** 按输入形态回写日期:纯日期返回 YYYY-MM-DD,带时间返回 YYYY-MM-DD HH:MM:SS */
|
|
2429
2527
|
function fmtSqlDate(dateOnly, d) {
|
|
2430
2528
|
const ymd = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
|
2431
2529
|
return dateOnly ? ymd : ymd + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
|
|
2432
2530
|
}
|
|
2433
2531
|
|
|
2532
|
+
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
2533
|
+
const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
2534
|
+
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
2535
|
+
|
|
2536
|
+
/** 可作为裸关键字出现的时间单位(TIMESTAMPDIFF(MONTH, a, b) / DATE_ADD 的 INTERVAL n unit) */
|
|
2537
|
+
const TIME_UNITS = new Set([
|
|
2538
|
+
'MICROSECOND', 'SECOND', 'MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR',
|
|
2539
|
+
'SECONDS', 'MINUTES', 'HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'YEARS',
|
|
2540
|
+
]);
|
|
2541
|
+
|
|
2542
|
+
/** MySQL DATE_FORMAT 的 % 说明符 */
|
|
2543
|
+
function strftime(d, fmt) {
|
|
2544
|
+
const h12 = ((d.getHours() % 12) || 12);
|
|
2545
|
+
const map = {
|
|
2546
|
+
'%Y': String(d.getFullYear()), '%y': pad2(d.getFullYear() % 100),
|
|
2547
|
+
'%m': pad2(d.getMonth() + 1), '%c': String(d.getMonth() + 1),
|
|
2548
|
+
'%d': pad2(d.getDate()), '%e': String(d.getDate()),
|
|
2549
|
+
'%H': pad2(d.getHours()), '%k': String(d.getHours()),
|
|
2550
|
+
'%h': pad2(h12), '%I': pad2(h12), '%l': String(h12),
|
|
2551
|
+
'%i': pad2(d.getMinutes()), '%S': pad2(d.getSeconds()), '%s': pad2(d.getSeconds()),
|
|
2552
|
+
'%p': d.getHours() < 12 ? 'AM' : 'PM',
|
|
2553
|
+
'%M': MONTH_NAMES[d.getMonth()], '%b': MONTH_NAMES[d.getMonth()].slice(0, 3),
|
|
2554
|
+
'%W': DAY_NAMES[d.getDay()], '%a': DAY_NAMES[d.getDay()].slice(0, 3),
|
|
2555
|
+
'%w': String(d.getDay()),
|
|
2556
|
+
'%j': String(Math.round((d - new Date(d.getFullYear(), 0, 0)) / 86400000)),
|
|
2557
|
+
'%%': '%',
|
|
2558
|
+
};
|
|
2559
|
+
return String(fmt).replace(/%[A-Za-z%]/g, (m) => (m in map ? map[m] : m));
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
/** PG 日期格式串(YYYY-MM-DD / HH24:MI:SS)→ MySQL 的 % 说明符 */
|
|
2563
|
+
function pgFmtToMysql(fmt) {
|
|
2564
|
+
const s = String(fmt);
|
|
2565
|
+
if (s.indexOf('%') !== -1) return s;
|
|
2566
|
+
// 长模式先替换成占位符,避免被短模式(MM 吃掉 MMMM)误匹配
|
|
2567
|
+
const rules = [
|
|
2568
|
+
[/YYYY/g, '\u0001'], [/YY/g, '\u0002'],
|
|
2569
|
+
[/MMMM/g, '\u0003'], [/MMM/g, '\u0004'], [/MM/g, '\u0005'],
|
|
2570
|
+
[/DDDD/g, '\u0006'], [/DDD/g, '\u0007'], [/DD/g, '\u0008'],
|
|
2571
|
+
[/HH24/g, '\u0009'], [/HH12/g, '\u0010'], [/HH/g, '\u0011'],
|
|
2572
|
+
[/MI/g, '\u0012'], [/SS/g, '\u0013'],
|
|
2573
|
+
[/AM/g, '\u0014'], [/PM/g, '\u0014'],
|
|
2574
|
+
];
|
|
2575
|
+
let out = s;
|
|
2576
|
+
for (const [re, ch] of rules) out = out.replace(re, ch);
|
|
2577
|
+
const back = { '\u0001': '%Y', '\u0002': '%y', '\u0003': '%M', '\u0004': '%b', '\u0005': '%m',
|
|
2578
|
+
'\u0006': '%W', '\u0007': '%a', '\u0008': '%d', '\u0009': '%H', '\u0010': '%h', '\u0011': '%H',
|
|
2579
|
+
'\u0012': '%i', '\u0013': '%S', '\u0014': '%p' };
|
|
2580
|
+
return out.replace(/[\u0001-\u0014]/g, (c) => back[c] || c);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/** STR_TO_DATE / TO_DATE:按格式串反解出 Date */
|
|
2584
|
+
function strToDate(s, fmt) {
|
|
2585
|
+
const spec = {
|
|
2586
|
+
Y: '(\\d{4})', y: '(\\d{2})', m: '(\\d{1,2})', c: '(\\d{1,2})',
|
|
2587
|
+
d: '(\\d{1,2})', e: '(\\d{1,2})',
|
|
2588
|
+
H: '(\\d{1,2})', k: '(\\d{1,2})', h: '(\\d{1,2})', I: '(\\d{1,2})', l: '(\\d{1,2})',
|
|
2589
|
+
i: '(\\d{1,2})', S: '(\\d{1,2})', s: '(\\d{1,2})',
|
|
2590
|
+
M: '([A-Za-z]+)', b: '([A-Za-z]+)', p: '(?:AM|PM|am|pm)?',
|
|
2591
|
+
};
|
|
2592
|
+
let pattern = '';
|
|
2593
|
+
const order = [];
|
|
2594
|
+
for (let i = 0; i < String(fmt).length;) {
|
|
2595
|
+
if (String(fmt)[i] === '%' && i + 1 < String(fmt).length) {
|
|
2596
|
+
const c = String(fmt)[i + 1];
|
|
2597
|
+
if (c === '%') { pattern += '%'; i += 2; continue; }
|
|
2598
|
+
if (spec[c]) { pattern += spec[c]; order.push(c); i += 2; continue; }
|
|
2599
|
+
pattern += c; i += 2; continue;
|
|
2600
|
+
}
|
|
2601
|
+
pattern += String(fmt)[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2602
|
+
i++;
|
|
2603
|
+
}
|
|
2604
|
+
const m = new RegExp('^' + pattern + '$').exec(String(s));
|
|
2605
|
+
if (!m) return null;
|
|
2606
|
+
let year = 1970, month = 0, day = 1, hour = 0, min = 0, sec = 0;
|
|
2607
|
+
order.forEach((c, idx) => {
|
|
2608
|
+
const v = m[idx + 1];
|
|
2609
|
+
if (v === undefined) return;
|
|
2610
|
+
switch (c) {
|
|
2611
|
+
case 'Y': year = Number(v); break;
|
|
2612
|
+
case 'y': year = Number(v) < 70 ? 2000 + Number(v) : 1900 + Number(v); break;
|
|
2613
|
+
case 'm': case 'c': month = Number(v) - 1; break;
|
|
2614
|
+
case 'M': case 'b': {
|
|
2615
|
+
const mi = MONTH_NAMES.findIndex((x) => x.toLowerCase().startsWith(String(v).toLowerCase().slice(0, 3)));
|
|
2616
|
+
if (mi >= 0) month = mi;
|
|
2617
|
+
break;
|
|
2618
|
+
}
|
|
2619
|
+
case 'd': case 'e': day = Number(v); break;
|
|
2620
|
+
case 'H': case 'k': hour = Number(v); break;
|
|
2621
|
+
case 'h': case 'I': case 'l': {
|
|
2622
|
+
const hh = Number(v) % 12;
|
|
2623
|
+
hour = /pm/i.test(String(s)) ? hh + 12 : hh;
|
|
2624
|
+
break;
|
|
2625
|
+
}
|
|
2626
|
+
case 'i': min = Number(v); break;
|
|
2627
|
+
case 'S': case 's': sec = Number(v); break;
|
|
2628
|
+
default: break;
|
|
2629
|
+
}
|
|
2630
|
+
});
|
|
2631
|
+
const d = new Date(year, month, day, hour, min, sec);
|
|
2632
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/** PG DATE_PART / EXTRACT 的部件取值 */
|
|
2636
|
+
function pgDatePart(part, d) {
|
|
2637
|
+
switch (String(part).toLowerCase()) {
|
|
2638
|
+
case 'year': case 'y': return d.getFullYear();
|
|
2639
|
+
case 'month': case 'mon': case 'mm': return d.getMonth() + 1;
|
|
2640
|
+
case 'day': case 'dd': case 'd': return d.getDate();
|
|
2641
|
+
case 'hour': case 'hh': return d.getHours();
|
|
2642
|
+
case 'minute': case 'mi': return d.getMinutes();
|
|
2643
|
+
case 'second': case 'ss': return d.getSeconds();
|
|
2644
|
+
case 'dow': return d.getDay();
|
|
2645
|
+
case 'doy': return Math.round((d - new Date(d.getFullYear(), 0, 0)) / 86400000);
|
|
2646
|
+
case 'quarter': return Math.floor(d.getMonth() / 3) + 1;
|
|
2647
|
+
case 'epoch': return Math.floor(d.getTime() / 1000);
|
|
2648
|
+
case 'week': {
|
|
2649
|
+
// ISO 8601 周序号
|
|
2650
|
+
const t = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
2651
|
+
const dayNr = (t.getDay() + 6) % 7;
|
|
2652
|
+
t.setDate(t.getDate() - dayNr + 3);
|
|
2653
|
+
const firstThursday = new Date(t.getFullYear(), 0, 4);
|
|
2654
|
+
const fdayNr = (firstThursday.getDay() + 6) % 7;
|
|
2655
|
+
firstThursday.setDate(firstThursday.getDate() - fdayNr + 3);
|
|
2656
|
+
return 1 + Math.round((t - firstThursday) / (7 * 86400000));
|
|
2657
|
+
}
|
|
2658
|
+
default: return null;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
/** PG AGE(from, to) 的间隔文本:'6 years 7 mons 11 days' */
|
|
2663
|
+
function pgAge(from, to) {
|
|
2664
|
+
let years = to.getFullYear() - from.getFullYear();
|
|
2665
|
+
let months = to.getMonth() - from.getMonth();
|
|
2666
|
+
let days = to.getDate() - from.getDate();
|
|
2667
|
+
if (days < 0) {
|
|
2668
|
+
months -= 1;
|
|
2669
|
+
days += new Date(to.getFullYear(), to.getMonth(), 0).getDate();
|
|
2670
|
+
}
|
|
2671
|
+
if (months < 0) { years -= 1; months += 12; }
|
|
2672
|
+
return `${years} years ${months} mons ${days} days`;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2434
2675
|
function applyScalarFunction(fnNode, row, ctx) {
|
|
2435
2676
|
const name = (fnNode.name || '').toUpperCase();
|
|
2436
2677
|
const rawArgs = fnNode.args || [];
|
|
@@ -2456,9 +2697,11 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
2456
2697
|
case 'FOUND_ROWS': return session && session.foundRows !== undefined ? session.foundRows : 0;
|
|
2457
2698
|
case 'CONNECTION_ID': return session && session.connectionId !== undefined ? session.connectionId : 0;
|
|
2458
2699
|
case 'DATABASE': case 'SCHEMA': return session && session.currentDb ? session.currentDb : 'default';
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
case '
|
|
2700
|
+
// NOW() / CURDATE() / CURTIME() 返回会话时区(本地)时间,不能用 toISOString()(UTC)——
|
|
2701
|
+
// 否则东八区凌晨会整整差一天(2024-02-29 被存成 2024-02-28)。
|
|
2702
|
+
case 'NOW': case 'CURRENT_TIMESTAMP': return sqlLocalDateTime();
|
|
2703
|
+
case 'CURDATE': case 'CURRENT_DATE': return sqlLocalDateTime().slice(0, 10);
|
|
2704
|
+
case 'CURTIME': return sqlLocalDateTime().slice(11, 19);
|
|
2462
2705
|
case 'UTC_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
|
|
2463
2706
|
case 'CONCAT': return args.map(a => a === null || a === undefined ? '' : String(a)).join('');
|
|
2464
2707
|
case 'CONCAT_WS': {
|
|
@@ -2527,6 +2770,178 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
2527
2770
|
}
|
|
2528
2771
|
return fmtSqlDate(isDateOnly, d);
|
|
2529
2772
|
}
|
|
2773
|
+
// ===== 日期/时间(补齐 README 契约:此前这些全部缺失,静默返回 null)=====
|
|
2774
|
+
case 'YEAR': {
|
|
2775
|
+
const d = parseSqlDate(args[0]);
|
|
2776
|
+
return d ? d.getFullYear() : null;
|
|
2777
|
+
}
|
|
2778
|
+
case 'MONTH': {
|
|
2779
|
+
const d = parseSqlDate(args[0]);
|
|
2780
|
+
return d ? d.getMonth() + 1 : null;
|
|
2781
|
+
}
|
|
2782
|
+
case 'DAY': case 'DAYOFMONTH': {
|
|
2783
|
+
const d = parseSqlDate(args[0]);
|
|
2784
|
+
return d ? d.getDate() : null;
|
|
2785
|
+
}
|
|
2786
|
+
case 'HOUR': {
|
|
2787
|
+
const d = parseSqlDate(args[0]);
|
|
2788
|
+
return d ? d.getHours() : null;
|
|
2789
|
+
}
|
|
2790
|
+
case 'MINUTE': {
|
|
2791
|
+
const d = parseSqlDate(args[0]);
|
|
2792
|
+
return d ? d.getMinutes() : null;
|
|
2793
|
+
}
|
|
2794
|
+
case 'SECOND': {
|
|
2795
|
+
const d = parseSqlDate(args[0]);
|
|
2796
|
+
return d ? d.getSeconds() : null;
|
|
2797
|
+
}
|
|
2798
|
+
case 'QUARTER': {
|
|
2799
|
+
const d = parseSqlDate(args[0]);
|
|
2800
|
+
return d ? Math.floor(d.getMonth() / 3) + 1 : null;
|
|
2801
|
+
}
|
|
2802
|
+
case 'WEEK': {
|
|
2803
|
+
const d = parseSqlDate(args[0]);
|
|
2804
|
+
if (!d) return null;
|
|
2805
|
+
const mode = args[1] != null ? Number(args[1]) : 0;
|
|
2806
|
+
// mode 1/3(周一为一周之始且首周需 ≥4 天)走 ISO 周序;其余按 MySQL 默认 mode 0
|
|
2807
|
+
if (mode === 1 || mode === 3) return pgDatePart('week', d);
|
|
2808
|
+
// mode 0:周日为一周之始,第 1 周是本年第一个含周日的周(之前为第 0 周)
|
|
2809
|
+
const jan1Dow = new Date(d.getFullYear(), 0, 1).getDay(); // 0=周日
|
|
2810
|
+
const firstSunday = jan1Dow === 0 ? 1 : 8 - jan1Dow;
|
|
2811
|
+
const doy = Math.round((d - new Date(d.getFullYear(), 0, 0)) / 86400000);
|
|
2812
|
+
if (doy < firstSunday) return 0;
|
|
2813
|
+
return Math.floor((doy - firstSunday) / 7) + 1;
|
|
2814
|
+
}
|
|
2815
|
+
case 'DAYOFWEEK': { // MySQL:1=周日 … 7=周六
|
|
2816
|
+
const d = parseSqlDate(args[0]);
|
|
2817
|
+
return d ? d.getDay() + 1 : null;
|
|
2818
|
+
}
|
|
2819
|
+
case 'DATE': {
|
|
2820
|
+
const d = parseSqlDate(args[0]);
|
|
2821
|
+
return d ? fmtSqlDate(true, d) : null;
|
|
2822
|
+
}
|
|
2823
|
+
case 'TIME': {
|
|
2824
|
+
const d = parseSqlDate(args[0]);
|
|
2825
|
+
return d ? pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds()) : null;
|
|
2826
|
+
}
|
|
2827
|
+
case 'DATEDIFF': {
|
|
2828
|
+
const a = parseSqlDate(args[0]);
|
|
2829
|
+
const b = parseSqlDate(args[1]);
|
|
2830
|
+
if (!a || !b) return null;
|
|
2831
|
+
// MySQL 语义:按日期相减(忽略时间部分)
|
|
2832
|
+
const ua = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
|
|
2833
|
+
const ub = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
|
|
2834
|
+
return Math.round((ua - ub) / 86400000);
|
|
2835
|
+
}
|
|
2836
|
+
case 'TIMESTAMPDIFF': {
|
|
2837
|
+
// unit 允许写成裸关键字(TIMESTAMPDIFF(MONTH, a, b)),此时会被解析成列名而取不到值,
|
|
2838
|
+
// 回退用列名文本;不影响真正名为 month/year 的列。
|
|
2839
|
+
const raw0 = rawArgs[0];
|
|
2840
|
+
const unit = String(args[0] != null ? args[0]
|
|
2841
|
+
: (raw0 && raw0.type === 'column' ? raw0.name : '') || '').toUpperCase();
|
|
2842
|
+
// MySQL 语义:TIMESTAMPDIFF(unit, start, end) = end - start
|
|
2843
|
+
const a = parseSqlDate(args[2]);
|
|
2844
|
+
const b = parseSqlDate(args[1]);
|
|
2845
|
+
if (!a || !b) return null;
|
|
2846
|
+
const ms = a.getTime() - b.getTime();
|
|
2847
|
+
const months = (a.getFullYear() - b.getFullYear()) * 12 + (a.getMonth() - b.getMonth());
|
|
2848
|
+
switch (unit) {
|
|
2849
|
+
case 'MICROSECOND': return Math.floor(ms * 1000);
|
|
2850
|
+
case 'SECOND': return Math.floor(ms / 1000);
|
|
2851
|
+
case 'MINUTE': return Math.floor(ms / 60000);
|
|
2852
|
+
case 'HOUR': return Math.floor(ms / 3600000);
|
|
2853
|
+
case 'DAY': return Math.floor(ms / 86400000);
|
|
2854
|
+
case 'WEEK': return Math.floor(ms / (86400000 * 7));
|
|
2855
|
+
case 'MONTH': return months;
|
|
2856
|
+
case 'QUARTER': return Math.floor(months / 3);
|
|
2857
|
+
case 'YEAR': return a.getFullYear() - b.getFullYear();
|
|
2858
|
+
default: return null;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
case 'DATE_FORMAT': {
|
|
2862
|
+
const d = parseSqlDate(args[0]);
|
|
2863
|
+
return d ? strftime(d, String(args[1] || '%Y-%m-%d')) : null;
|
|
2864
|
+
}
|
|
2865
|
+
case 'STR_TO_DATE': {
|
|
2866
|
+
const d = strToDate(String(args[0] == null ? '' : args[0]), String(args[1] || ''));
|
|
2867
|
+
return d ? fmtSqlDate(!/%[HhIiklsS]/.test(String(args[1] || '')), d) : null;
|
|
2868
|
+
}
|
|
2869
|
+
case 'TO_CHAR': { // PG:TO_CHAR(d, fmt) —— 格式串用 PG 风格(YYYY-MM-DD)
|
|
2870
|
+
const d = parseSqlDate(args[0]);
|
|
2871
|
+
return d ? strftime(d, pgFmtToMysql(args[1] || '%Y-%m-%d')) : null;
|
|
2872
|
+
}
|
|
2873
|
+
case 'TO_DATE': { // PG:TO_DATE(s, fmt)
|
|
2874
|
+
const d = strToDate(String(args[0] == null ? '' : args[0]), pgFmtToMysql(String(args[1] || '')));
|
|
2875
|
+
return d ? fmtSqlDate(true, d) : null;
|
|
2876
|
+
}
|
|
2877
|
+
case 'DATE_PART': { // PG:DATE_PART('year', d)
|
|
2878
|
+
const d = parseSqlDate(args[1]);
|
|
2879
|
+
return d ? pgDatePart(args[0], d) : null;
|
|
2880
|
+
}
|
|
2881
|
+
case 'AGE': { // PG:AGE(a, b) —— a 相对 b 的间隔
|
|
2882
|
+
const a = parseSqlDate(args[0]);
|
|
2883
|
+
const b = args[1] !== undefined && args[1] !== null ? parseSqlDate(args[1]) : new Date();
|
|
2884
|
+
return (a && b) ? pgAge(b, a) : null;
|
|
2885
|
+
}
|
|
2886
|
+
case 'FROM_UNIXTIME': {
|
|
2887
|
+
const ts = Number(args[0]);
|
|
2888
|
+
if (!Number.isFinite(ts)) return null;
|
|
2889
|
+
const d = new Date(ts * 1000);
|
|
2890
|
+
return args[1] !== undefined && args[1] !== null ? strftime(d, String(args[1])) : fmtSqlDate(false, d);
|
|
2891
|
+
}
|
|
2892
|
+
case 'LAST_DAY': {
|
|
2893
|
+
const d = parseSqlDate(args[0]);
|
|
2894
|
+
return d ? fmtSqlDate(true, new Date(d.getFullYear(), d.getMonth() + 1, 0)) : null;
|
|
2895
|
+
}
|
|
2896
|
+
case 'WEEKDAY': { // MySQL:0=周一 … 6=周日
|
|
2897
|
+
const d = parseSqlDate(args[0]);
|
|
2898
|
+
return d ? (d.getDay() + 6) % 7 : null;
|
|
2899
|
+
}
|
|
2900
|
+
case 'DAYOFYEAR': {
|
|
2901
|
+
const d = parseSqlDate(args[0]);
|
|
2902
|
+
return d ? Math.round((d - new Date(d.getFullYear(), 0, 0)) / 86400000) : null;
|
|
2903
|
+
}
|
|
2904
|
+
case 'DAYNAME': {
|
|
2905
|
+
const d = parseSqlDate(args[0]);
|
|
2906
|
+
return d ? DAY_NAMES[d.getDay()] : null;
|
|
2907
|
+
}
|
|
2908
|
+
case 'MONTHNAME': {
|
|
2909
|
+
const d = parseSqlDate(args[0]);
|
|
2910
|
+
return d ? MONTH_NAMES[d.getMonth()] : null;
|
|
2911
|
+
}
|
|
2912
|
+
case 'MAKEDATE': {
|
|
2913
|
+
const y = Number(args[0]);
|
|
2914
|
+
const doy = Number(args[1]);
|
|
2915
|
+
if (!Number.isFinite(y) || !Number.isFinite(doy)) return null;
|
|
2916
|
+
const d = new Date(y, 0, 1);
|
|
2917
|
+
d.setDate(doy);
|
|
2918
|
+
return fmtSqlDate(true, d);
|
|
2919
|
+
}
|
|
2920
|
+
case 'PERIOD_DIFF': {
|
|
2921
|
+
const p1 = String(args[0] == null ? '' : args[0]);
|
|
2922
|
+
const p2 = String(args[1] == null ? '' : args[1]);
|
|
2923
|
+
if (!/^\d{6}$/.test(p1) || !/^\d{6}$/.test(p2)) return null;
|
|
2924
|
+
return (Number(p1.slice(0, 4)) * 12 + Number(p1.slice(4))) -
|
|
2925
|
+
(Number(p2.slice(0, 4)) * 12 + Number(p2.slice(4)));
|
|
2926
|
+
}
|
|
2927
|
+
case 'DATE_TRUNC': { // PG:DATE_TRUNC('month', d)
|
|
2928
|
+
const part = String(args[0] || '').toLowerCase();
|
|
2929
|
+
const d = parseSqlDate(args[1]);
|
|
2930
|
+
if (!d) return null;
|
|
2931
|
+
let nd;
|
|
2932
|
+
switch (part) {
|
|
2933
|
+
case 'year': nd = new Date(d.getFullYear(), 0, 1); break;
|
|
2934
|
+
case 'quarter': nd = new Date(d.getFullYear(), Math.floor(d.getMonth() / 3) * 3, 1); break;
|
|
2935
|
+
case 'month': nd = new Date(d.getFullYear(), d.getMonth(), 1); break;
|
|
2936
|
+
case 'week': { const wd = (d.getDay() + 6) % 7; nd = new Date(d.getFullYear(), d.getMonth(), d.getDate() - wd); break; }
|
|
2937
|
+
case 'day': nd = new Date(d.getFullYear(), d.getMonth(), d.getDate()); break;
|
|
2938
|
+
case 'hour': nd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()); break;
|
|
2939
|
+
case 'minute': nd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()); break;
|
|
2940
|
+
case 'second': nd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()); break;
|
|
2941
|
+
default: return null;
|
|
2942
|
+
}
|
|
2943
|
+
return fmtSqlDate(false, nd);
|
|
2944
|
+
}
|
|
2530
2945
|
case 'LTRIM': return args[0] == null ? null : String(args[0]).replace(/^\s+/, '');
|
|
2531
2946
|
case 'RTRIM': return args[0] == null ? null : String(args[0]).replace(/\s+$/, '');
|
|
2532
2947
|
case 'ABS': return args[0] == null ? null : Math.abs(args[0]);
|
|
@@ -2633,6 +3048,57 @@ function likeMatch(value, pattern, ci = true, escape = '\\') {
|
|
|
2633
3048
|
return new RegExp('^' + out + '$', ci ? 'i' : '').test(value);
|
|
2634
3049
|
}
|
|
2635
3050
|
|
|
3051
|
+
/** 解析 PG 的路径字面量:'{a,b}' 或数组 → ['a','b'] */
|
|
3052
|
+
function parsePgPath(key) {
|
|
3053
|
+
if (Array.isArray(key)) return key.map(String);
|
|
3054
|
+
if (typeof key === 'string') {
|
|
3055
|
+
const s = key.trim();
|
|
3056
|
+
if (s.startsWith('{') && s.endsWith('}')) {
|
|
3057
|
+
return s.slice(1, -1).split(',').map(x => x.trim().replace(/^"(.*)"$/, '$1')).filter(x => x !== '');
|
|
3058
|
+
}
|
|
3059
|
+
return [s];
|
|
3060
|
+
}
|
|
3061
|
+
return [String(key)];
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
/** 把可能是 JSON 字符串的值归一为 JS 值 */
|
|
3065
|
+
function toJsonValue(v) {
|
|
3066
|
+
if (typeof v === 'string') { try { return JSON.parse(v); } catch (e) { return v; } }
|
|
3067
|
+
return v;
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
/** JSON 包含语义(PG 的 @> / <@)。swap=true 时判断 target 是否包含 container。 */
|
|
3071
|
+
function jsonContains(a, b, swap) {
|
|
3072
|
+
const container = toJsonValue(swap ? b : a);
|
|
3073
|
+
const target = toJsonValue(swap ? a : b);
|
|
3074
|
+
return jsonContainsValue(container, target);
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
function jsonContainsValue(container, target) {
|
|
3078
|
+
if (target === null || target === undefined) return true;
|
|
3079
|
+
if (Array.isArray(target)) {
|
|
3080
|
+
if (!Array.isArray(container)) return false;
|
|
3081
|
+
return target.every(t => container.some(c => jsonContainsValue(c, t)));
|
|
3082
|
+
}
|
|
3083
|
+
if (typeof target === 'object') {
|
|
3084
|
+
if (container === null || typeof container !== 'object' || Array.isArray(container)) return false;
|
|
3085
|
+
return Object.keys(target).every(k => Object.prototype.hasOwnProperty.call(container, k) && jsonContainsValue(container[k], target[k]));
|
|
3086
|
+
}
|
|
3087
|
+
if (Array.isArray(container)) return container.some(c => c === target || String(c) === String(target));
|
|
3088
|
+
if (container !== null && typeof container === 'object') return false;
|
|
3089
|
+
return container === target || String(container) === String(target);
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
/** JSON 键存在(PG 的 ? / ?| / ?&)。keys 为单个键名或 '{a,b}' 形式的键列表。 */
|
|
3093
|
+
function jsonKeyExists(base, keys, mode) {
|
|
3094
|
+
const v = toJsonValue(base);
|
|
3095
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
3096
|
+
const has = (k) => Object.prototype.hasOwnProperty.call(v, k);
|
|
3097
|
+
if (mode === '?|') return parsePgPath(keys).some(has);
|
|
3098
|
+
if (mode === '?&') return parsePgPath(keys).every(has);
|
|
3099
|
+
return has(String(keys));
|
|
3100
|
+
}
|
|
3101
|
+
|
|
2636
3102
|
function extractEqualPushdown(expr, schema) {
|
|
2637
3103
|
if (!expr || !schema) return null;
|
|
2638
3104
|
const filter = {};
|
|
@@ -2683,7 +3149,11 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2683
3149
|
case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
|
|
2684
3150
|
case 'or': return evaluateExpr(expr.left, row, ctx) || evaluateExpr(expr.right, row, ctx);
|
|
2685
3151
|
case 'xor': return Boolean(evaluateExpr(expr.left, row, ctx)) !== Boolean(evaluateExpr(expr.right, row, ctx));
|
|
2686
|
-
case 'not':
|
|
3152
|
+
case 'not': {
|
|
3153
|
+
// 三值逻辑:NOT UNKNOWN = UNKNOWN(返回 null,在 WHERE/ON/HAVING 中视为不选中)
|
|
3154
|
+
const v = evaluateExpr(expr.expr, row, ctx);
|
|
3155
|
+
return v === null ? null : !v;
|
|
3156
|
+
}
|
|
2687
3157
|
case 'compare': {
|
|
2688
3158
|
const l = resolveOperand(expr.left, row, ctx);
|
|
2689
3159
|
const r = resolveOperand(expr.right, row, ctx);
|
|
@@ -2694,13 +3164,23 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2694
3164
|
if (ln || rn) return ln && rn;
|
|
2695
3165
|
return l === r || String(l) === String(r);
|
|
2696
3166
|
}
|
|
2697
|
-
// SQL
|
|
2698
|
-
|
|
3167
|
+
// SQL 标准:与 NULL 的比较结果是 UNKNOWN,这里用 null 表示。
|
|
3168
|
+
// 不能返回 false —— 否则 NOT (a > 1) 会把 a 为 NULL 的行错误纳入结果。
|
|
3169
|
+
if (l === null || l === undefined || r === null || r === undefined) return null;
|
|
2699
3170
|
// 布尔与数值互通(MySQL: 1 = TRUE、0 = FALSE)
|
|
2700
3171
|
const nl = normalizeCmpValue(l, r);
|
|
2701
3172
|
const nr = normalizeCmpValue(r, l);
|
|
2702
|
-
|
|
3173
|
+
const isNum = (v) => typeof v === 'number';
|
|
3174
|
+
const isNumStr = (v) => typeof v === 'string' && String(v).trim() !== '' && Number.isFinite(Number(v));
|
|
3175
|
+
// 数值 vs 数值字符串(age < '10'、age = '09')必须按数值比,
|
|
3176
|
+
// 否则退化成字典序:'9' < '10' 为 false、'9' = '09' 为 false
|
|
3177
|
+
const mixedNum = (isNum(nl) && isNumStr(nr)) || (isNumStr(nl) && isNum(nr));
|
|
3178
|
+
if (expr.op === '=') {
|
|
3179
|
+
if (mixedNum) return Number(nl) === Number(nr);
|
|
3180
|
+
return nl === nr || String(nl) === String(nr);
|
|
3181
|
+
}
|
|
2703
3182
|
const fn = OPERATORS[expr.op];
|
|
3183
|
+
if (mixedNum) return fn(Number(nl), Number(nr));
|
|
2704
3184
|
return typeof nl === 'number' && typeof nr === 'number' ? fn(nl, nr) : fn(String(nl), String(nr));
|
|
2705
3185
|
}
|
|
2706
3186
|
case 'isNull': {
|
|
@@ -2712,22 +3192,30 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2712
3192
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2713
3193
|
if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
|
|
2714
3194
|
if (!expr.list) return false;
|
|
2715
|
-
|
|
3195
|
+
const r = expr.list.some(x => x === v || String(x) === String(v));
|
|
3196
|
+
return expr.not ? !r : r; // NOT IN:此前漏了取反,导致 NOT IN 与 IN 结果相同
|
|
2716
3197
|
}
|
|
2717
3198
|
case 'like': {
|
|
2718
3199
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2719
|
-
|
|
3200
|
+
// MySQL 的 LIKE 默认不区分大小写;只有显式 BINARY / ci=false 才敏感。
|
|
3201
|
+
// 此前写成 !!expr.ci,把 likeMatch 的 ci 默认值 true 覆盖成了 false。
|
|
3202
|
+
const r = likeMatch(v, expr.pattern, expr.ci !== false, expr.escape);
|
|
2720
3203
|
return expr.not ? !r : r;
|
|
2721
3204
|
}
|
|
2722
3205
|
case 'quantified':
|
|
2723
3206
|
return !!resolveOperand(expr, row, ctx);
|
|
3207
|
+
case 'jsonContains':
|
|
3208
|
+
return !!resolveOperand(expr, row, ctx);
|
|
2724
3209
|
case 'regexp': {
|
|
2725
3210
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2726
3211
|
if (v === null || v === undefined) return false;
|
|
2727
|
-
|
|
3212
|
+
// REGEXP / RLIKE 历史上就是大小写不敏感的;`~` 敏感、`~*` 不敏感
|
|
3213
|
+
const re = new RegExp(String(expr.pattern), expr.ci === false ? '' : 'i');
|
|
2728
3214
|
const r = re.test(String(v));
|
|
2729
3215
|
return expr.not ? !r : r;
|
|
2730
3216
|
}
|
|
3217
|
+
case 'jsonKeyExists':
|
|
3218
|
+
return !!resolveOperand(expr, row, ctx);
|
|
2731
3219
|
case 'isTruth': {
|
|
2732
3220
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2733
3221
|
const isTrue = v === true || v === 1 || v === '1' || v === 'true' || v === 'TRUE' || v === 't' || (typeof v === 'number' && v !== 0);
|
|
@@ -3013,9 +3501,30 @@ class SQLExecutor {
|
|
|
3013
3501
|
dataRows = dataRows.map(stripDefault);
|
|
3014
3502
|
}
|
|
3015
3503
|
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
3504
|
+
const uniqueCols = schema ? Object.keys(schema).filter(k => schema[k].unique && !schema[k].primaryKey) : [];
|
|
3016
3505
|
let toInsert = dataRows;
|
|
3017
3506
|
let updated = 0;
|
|
3018
3507
|
let skipped = 0;
|
|
3508
|
+
// 非主键 UNIQUE 约束:默认动作(无 ON CONFLICT / IGNORE / REPLACE)下违反即 ER_DUP_ENTRY。
|
|
3509
|
+
// 主键冲突由下面的冲突逻辑处理;这里补唯一列(含批内重复)。
|
|
3510
|
+
if (uniqueCols.length > 0 && this._conflictAction(statement) === 'throw') {
|
|
3511
|
+
const existing = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 }))
|
|
3512
|
+
.map(r => normalizeRow(r, schema));
|
|
3513
|
+
for (const col of uniqueCols) {
|
|
3514
|
+
const used = new Set();
|
|
3515
|
+
for (const r of existing) {
|
|
3516
|
+
const v = r[col];
|
|
3517
|
+
if (v !== undefined && v !== null) used.add(String(v));
|
|
3518
|
+
}
|
|
3519
|
+
for (const d of dataRows) {
|
|
3520
|
+
const v = d[col];
|
|
3521
|
+
if (v === undefined || v === null) continue; // UNIQUE 允许多个 NULL
|
|
3522
|
+
const s = String(v);
|
|
3523
|
+
if (used.has(s)) throw new Error(`ER_DUP_ENTRY: Duplicate entry '${s}' for unique column '${col}'`);
|
|
3524
|
+
used.add(s);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3019
3528
|
if (pkCols.length > 0) {
|
|
3020
3529
|
const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
|
|
3021
3530
|
const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
|
|
@@ -3028,11 +3537,8 @@ class SQLExecutor {
|
|
|
3028
3537
|
: null;
|
|
3029
3538
|
const extraIdx = [];
|
|
3030
3539
|
if (ocTarget && ocTarget.length) extraIdx.push(ocTarget);
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
if (def && def.unique && !def.primaryKey) extraIdx.push([c]);
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3540
|
+
// 唯一列始终参与冲突判定(PG 的 ON CONFLICT 与 MySQL 的 ON DUPLICATE 都包含唯一键)
|
|
3541
|
+
for (const c of uniqueCols) extraIdx.push([c]);
|
|
3036
3542
|
const hasAllOf = (cols) => (row) => cols.every(c => row[c] !== undefined && row[c] !== null);
|
|
3037
3543
|
|
|
3038
3544
|
const explicit = dataRows.filter((row) => hasExplicitPk(row) || extraIdx.some((cols) => hasAllOf(cols)(row)));
|
|
@@ -3918,8 +4424,25 @@ class SQLExecutor {
|
|
|
3918
4424
|
}
|
|
3919
4425
|
if (fn === 'SUM') return values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
|
|
3920
4426
|
if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
3921
|
-
if (fn === 'MIN'
|
|
3922
|
-
|
|
4427
|
+
if (fn === 'MIN' || fn === 'MAX') {
|
|
4428
|
+
const vs = values.filter(v => v !== null && v !== undefined);
|
|
4429
|
+
if (!vs.length) return null;
|
|
4430
|
+
// 数值按数值比;字符串/日期按字典序/时间序。
|
|
4431
|
+
// 此前一律 Number(v) —— 字符串列会得到 NaN,MIN(name) 直接变 null。
|
|
4432
|
+
const numeric = (v) => typeof v === 'number' ||
|
|
4433
|
+
(typeof v === 'string' && String(v).trim() !== '' && Number.isFinite(Number(v)));
|
|
4434
|
+
if (vs.every(numeric)) {
|
|
4435
|
+
const nums = vs.map(v => Number(v));
|
|
4436
|
+
return fn === 'MIN' ? Math.min(...nums) : Math.max(...nums);
|
|
4437
|
+
}
|
|
4438
|
+
let best = vs[0];
|
|
4439
|
+
for (const v of vs.slice(1)) {
|
|
4440
|
+
const a = String(v);
|
|
4441
|
+
const b = String(best);
|
|
4442
|
+
if (fn === 'MIN' ? a < b : a > b) best = v;
|
|
4443
|
+
}
|
|
4444
|
+
return best;
|
|
4445
|
+
}
|
|
3923
4446
|
if (fn === 'FIRST') return values.length ? values[0] : null;
|
|
3924
4447
|
if (fn === 'LAST') return values.length ? values[values.length - 1] : null;
|
|
3925
4448
|
if (fn === 'GROUP_CONCAT') {
|
|
@@ -4058,6 +4581,15 @@ class SQLExecutor {
|
|
|
4058
4581
|
if (!sample || typeof sample !== 'object') return;
|
|
4059
4582
|
const known = new Set();
|
|
4060
4583
|
for (const k of Object.keys(sample)) known.add(String(k).toLowerCase());
|
|
4584
|
+
// 相关子查询:外层行的列也算已知。
|
|
4585
|
+
// _evalCorrelated 会把外层行挂到 ctx.__outer,子查询(`WHERE EXISTS (SELECT ... WHERE b.av = a.v)`)
|
|
4586
|
+
// 里的 `a.v` 属于外层作用域,不在本层 rows 里,不纳入就会误报 no such column。
|
|
4587
|
+
const outer = this.ctx && this.ctx.__outer;
|
|
4588
|
+
if (outer && typeof outer === 'object') {
|
|
4589
|
+
for (const k of Object.keys(outer)) known.add(String(k).toLowerCase());
|
|
4590
|
+
}
|
|
4591
|
+
// 本层 FROM 的表名/别名,用于判断限定引用是否指向外层
|
|
4592
|
+
const localAliases = this._outerAliases(statement);
|
|
4061
4593
|
// HAVING / ORDER BY 允许引用 SELECT 的输出别名(如 HAVING cnt >= 1),
|
|
4062
4594
|
// 这些名字不在表里,必须一并视为已知,否则会误报 no such column
|
|
4063
4595
|
if (Array.isArray(statement.columns)) {
|
|
@@ -4073,7 +4605,11 @@ class SQLExecutor {
|
|
|
4073
4605
|
if (node.type === 'select' || node.type === 'subquery') return;
|
|
4074
4606
|
if (node.type === 'column' && typeof node.name === 'string') {
|
|
4075
4607
|
const n = node.name.toLowerCase();
|
|
4076
|
-
const
|
|
4608
|
+
const dot = n.indexOf('.');
|
|
4609
|
+
const base = dot >= 0 ? n.slice(dot + 1) : n;
|
|
4610
|
+
// 限定列引用了本层 FROM 之外的表/别名 → 属于外层作用域(相关子查询),放行。
|
|
4611
|
+
// 否则 `WHERE EXISTS (SELECT 1 FROM b WHERE b.av = a.v)` 里的 a.v 会被误杀。
|
|
4612
|
+
if (dot >= 0 && !localAliases.has(n.slice(0, dot))) return;
|
|
4077
4613
|
if (!known.has(n) && !known.has(base)) {
|
|
4078
4614
|
throw new Error(`no such column: ${node.name}`);
|
|
4079
4615
|
}
|
|
@@ -4504,16 +5040,16 @@ class SQLExecutor {
|
|
|
4504
5040
|
const rightNulls = (rightRes.schema) ? this._nullPrefixedRow(rightRes.schema, rightPrefix) : null;
|
|
4505
5041
|
const leftNulls = (rowsAll && rowsAll.schema) ? this._nullPrefixedRow(rowsAll.schema, firstPrefix) : null;
|
|
4506
5042
|
rows.forEach(l => {
|
|
4507
|
-
|
|
5043
|
+
// 一对多:一个左行可能匹配多个右行,必须全部收进来(此前只取第一个)
|
|
5044
|
+
const hits = [];
|
|
4508
5045
|
for (let ri = 0; ri < rightRows.length; ri++) {
|
|
4509
|
-
if (evaluateExpr(j.on, { ...l, ...rightRows[ri] }))
|
|
4510
|
-
m = ri;
|
|
4511
|
-
break;
|
|
4512
|
-
}
|
|
5046
|
+
if (evaluateExpr(j.on, { ...l, ...rightRows[ri] })) hits.push(ri);
|
|
4513
5047
|
}
|
|
4514
|
-
if (
|
|
4515
|
-
|
|
4516
|
-
|
|
5048
|
+
if (hits.length > 0) {
|
|
5049
|
+
for (const ri of hits) {
|
|
5050
|
+
matched.push({ ...l, ...rightRows[ri] });
|
|
5051
|
+
unmatchedRight.delete(ri);
|
|
5052
|
+
}
|
|
4517
5053
|
} else if (j.type === 'left' || j.type === 'full') {
|
|
4518
5054
|
matched.push(rightNulls ? { ...rightNulls, ...l } : { ...l });
|
|
4519
5055
|
}
|
|
@@ -4561,6 +5097,14 @@ class SQLExecutor {
|
|
|
4561
5097
|
});
|
|
4562
5098
|
}
|
|
4563
5099
|
|
|
5100
|
+
// 无 GROUP BY 时整张表就是一个分组。必须先给每行挂上 _group=全表,
|
|
5101
|
+
// 否则 HAVING 里的 COUNT(*) 会被逐行算成 1(r._group || [r]),
|
|
5102
|
+
// 于是 HAVING COUNT(*) > 2 会把所有行滤掉、最终输出 0 而不是真实计数。
|
|
5103
|
+
if (statement.having && !statement.groupBy) {
|
|
5104
|
+
const wholeTable = rows.slice();
|
|
5105
|
+
rows = rows.map((r) => ({ ...r, _group: wholeTable }));
|
|
5106
|
+
}
|
|
5107
|
+
|
|
4564
5108
|
if (statement.having) {
|
|
4565
5109
|
const havingAst = JSON.parse(JSON.stringify(statement.having));
|
|
4566
5110
|
rows = rows.filter(r => {
|
|
@@ -4589,6 +5133,10 @@ class SQLExecutor {
|
|
|
4589
5133
|
const seen = new Set();
|
|
4590
5134
|
rows = rows.filter(r => {
|
|
4591
5135
|
const key = JSON.stringify(statement.columns.map(c => {
|
|
5136
|
+
// SELECT DISTINCT *:按整行去重(此前把 '*' 当列名解析成 null,导致所有行同 key 只剩 1 行)
|
|
5137
|
+
if (c.expr === '*') {
|
|
5138
|
+
return Object.keys(r).filter(k => !String(k).startsWith('_')).sort().map(k => r[k]);
|
|
5139
|
+
}
|
|
4592
5140
|
if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
|
|
4593
5141
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
4594
5142
|
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
@@ -4605,6 +5153,11 @@ class SQLExecutor {
|
|
|
4605
5153
|
// 注意:带 OVER 的聚合是窗口函数,不是“整表塌缩成一行”的聚合,必须排除
|
|
4606
5154
|
if (!statement.groupBy && (statement.columns.some(c => c.aggregate && !c.window) || hasCustomAgg)) {
|
|
4607
5155
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
5156
|
+
const tableName2 = statement.from ? (statement.from.tables[0].table || null) : null;
|
|
5157
|
+
// HAVING 判定不通过时 rows 已为空,此时整表聚合也应为空,不能无条件补一行
|
|
5158
|
+
if (statement.having && rows.length === 0) {
|
|
5159
|
+
return { ok: true, type: 'select', table: tableName2, columns: cols, rows: [], aggregate: statement.aggregate };
|
|
5160
|
+
}
|
|
4608
5161
|
const valueOf = (c) => {
|
|
4609
5162
|
if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
4610
5163
|
if (c.scalar && c.scalar.type === 'func' && hasCustomAgg) {
|
|
@@ -4618,7 +5171,7 @@ class SQLExecutor {
|
|
|
4618
5171
|
if (c.expr !== null && c.expr !== '*') return rows[0] ? resolveOperand({ type: 'column', name: c.expr }, rows[0]) : null;
|
|
4619
5172
|
return null;
|
|
4620
5173
|
};
|
|
4621
|
-
return { ok: true, type: 'select', table:
|
|
5174
|
+
return { ok: true, type: 'select', table: tableName2, columns: cols, rows: [[...statement.columns.map(valueOf)]], aggregate: statement.aggregate };
|
|
4622
5175
|
}
|
|
4623
5176
|
|
|
4624
5177
|
// GROUP BY 输出:每组的列(含聚合列)
|
|
@@ -4636,10 +5189,20 @@ class SQLExecutor {
|
|
|
4636
5189
|
}
|
|
4637
5190
|
|
|
4638
5191
|
if (statement.orderBy) {
|
|
4639
|
-
// ORDER BY
|
|
4640
|
-
const
|
|
4641
|
-
|
|
4642
|
-
|
|
5192
|
+
// ORDER BY 项可能是列名字符串、表达式节点,或列的位置序号(ORDER BY 1,1-based)
|
|
5193
|
+
const ordNames = statement.columns.map(c => scalarColumnName(c));
|
|
5194
|
+
const ordVal = (row, o) => {
|
|
5195
|
+
const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
|
|
5196
|
+
? o.column.value
|
|
5197
|
+
: (typeof o.column === 'number' ? o.column : null);
|
|
5198
|
+
if (pos !== null && pos !== undefined) {
|
|
5199
|
+
const name = ordNames[pos - 1];
|
|
5200
|
+
if (name !== undefined && name !== null) return resolveOperand({ type: 'column', name }, row, this.ctx);
|
|
5201
|
+
}
|
|
5202
|
+
return typeof o.column === 'string'
|
|
5203
|
+
? resolveOperand({ type: 'column', name: o.column }, row, this.ctx)
|
|
5204
|
+
: resolveOperand(o.column, row, this.ctx);
|
|
5205
|
+
};
|
|
4643
5206
|
const cmp = (a, b) => {
|
|
4644
5207
|
for (const o of statement.orderBy) {
|
|
4645
5208
|
const av = ordVal(a, o);
|
|
@@ -4690,53 +5253,88 @@ class SQLExecutor {
|
|
|
4690
5253
|
}
|
|
4691
5254
|
}
|
|
4692
5255
|
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
while (i < sql.length) {
|
|
5256
|
+
/** 跳过一个引号串(含转义/反引号成对),返回闭合引号之后的下标 */
|
|
5257
|
+
function skipQuoted(sql, i, q) {
|
|
5258
|
+
const n = sql.length;
|
|
5259
|
+
i++; // 跳过起始引号
|
|
5260
|
+
while (i < n) {
|
|
4699
5261
|
const c = sql[i];
|
|
4700
|
-
if (
|
|
4701
|
-
|
|
4702
|
-
if (
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
if (c === '`') inStr = null;
|
|
4706
|
-
i++;
|
|
4707
|
-
continue;
|
|
5262
|
+
if (q === '`') {
|
|
5263
|
+
// 反引号标识符内部反斜杠不是转义符;两个连续反引号表示一个字面反引号。
|
|
5264
|
+
if (c === '`') {
|
|
5265
|
+
if (sql[i + 1] === '`') { i += 2; continue; }
|
|
5266
|
+
return i + 1;
|
|
4708
5267
|
}
|
|
4709
|
-
if (c === '\\' && i + 1 < sql.length) { current += sql[i + 1]; i += 2; continue; }
|
|
4710
|
-
if (c === inStr) inStr = null;
|
|
4711
5268
|
i++;
|
|
4712
5269
|
continue;
|
|
4713
5270
|
}
|
|
4714
|
-
if (c ===
|
|
5271
|
+
if (c === '\\') { i += 2; continue; }
|
|
5272
|
+
if (c === q) return i + 1;
|
|
5273
|
+
i++;
|
|
5274
|
+
}
|
|
5275
|
+
return n; // 未闭合:到结尾
|
|
5276
|
+
}
|
|
5277
|
+
|
|
5278
|
+
function splitStatements(sql, opts = {}) {
|
|
5279
|
+
if (typeof sql !== 'string') {
|
|
5280
|
+
throw new Error('splitStatements: sql must be a string');
|
|
5281
|
+
}
|
|
5282
|
+
const maxLength = opts.maxLength == null ? DEFAULT_MAX_SQL_LENGTH : opts.maxLength;
|
|
5283
|
+
if (typeof maxLength === 'number' && Number.isFinite(maxLength) && sql.length > maxLength) {
|
|
5284
|
+
throw new Error(
|
|
5285
|
+
`SQL text too large: ${sql.length} chars exceeds limit of ${maxLength} ` +
|
|
5286
|
+
`(raise opts.maxSqlLength to allow larger input)`
|
|
5287
|
+
);
|
|
5288
|
+
}
|
|
5289
|
+
const statements = [];
|
|
5290
|
+
// 用「区间切片」收集,而不是逐字符拼接:既避免 `current += c` 的 O(n²) 拷贝放大,
|
|
5291
|
+
// 也避免为每个字符建一个小字符串对象(20MB 无分号输入下两者都会打爆默认堆)。
|
|
5292
|
+
let parts = [];
|
|
5293
|
+
const take = end => { parts.push(sql.slice(segStart, end)); };
|
|
5294
|
+
const flush = () => {
|
|
5295
|
+
if (parts.length === 0) return;
|
|
5296
|
+
const stmt = parts.join('').trim();
|
|
5297
|
+
parts = [];
|
|
5298
|
+
if (stmt) statements.push(stmt);
|
|
5299
|
+
};
|
|
5300
|
+
|
|
5301
|
+
let segStart = 0;
|
|
5302
|
+
let i = 0;
|
|
5303
|
+
const n = sql.length;
|
|
5304
|
+
while (i < n) {
|
|
5305
|
+
const c = sql[i];
|
|
5306
|
+
if (c === "'" || c === '"' || c === '`') { i = skipQuoted(sql, i, c); continue; }
|
|
4715
5307
|
if (c === '-' && sql[i + 1] === '-') {
|
|
4716
|
-
|
|
5308
|
+
// 行注释:丢弃注释文本本身(与原实现一致),保留其后的换行
|
|
5309
|
+
take(i);
|
|
5310
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
5311
|
+
segStart = i;
|
|
4717
5312
|
continue;
|
|
4718
5313
|
}
|
|
4719
|
-
if (c === '#') {
|
|
4720
|
-
|
|
5314
|
+
if (c === '#' && sql[i + 1] !== '>') {
|
|
5315
|
+
take(i);
|
|
5316
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
5317
|
+
segStart = i;
|
|
4721
5318
|
continue;
|
|
4722
5319
|
}
|
|
4723
5320
|
if (c === '/' && sql[i + 1] === '*') {
|
|
4724
|
-
|
|
5321
|
+
// 块注释:原实现会保留 `/* */` 原文,这里通过切片一并保留
|
|
4725
5322
|
i += 2;
|
|
4726
|
-
while (i + 1 <
|
|
4727
|
-
|
|
5323
|
+
while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
|
|
5324
|
+
i = i + 1 < n ? i + 2 : n;
|
|
4728
5325
|
continue;
|
|
4729
5326
|
}
|
|
4730
5327
|
if (c === ';') {
|
|
4731
|
-
|
|
4732
|
-
|
|
5328
|
+
take(i);
|
|
5329
|
+
flush();
|
|
4733
5330
|
i++;
|
|
5331
|
+
segStart = i;
|
|
4734
5332
|
continue;
|
|
4735
5333
|
}
|
|
4736
|
-
current += c;
|
|
4737
5334
|
i++;
|
|
4738
5335
|
}
|
|
4739
|
-
|
|
5336
|
+
take(n);
|
|
5337
|
+
flush();
|
|
4740
5338
|
return statements;
|
|
4741
5339
|
}
|
|
4742
5340
|
|
|
@@ -4763,7 +5361,7 @@ function hasComments(sql) {
|
|
|
4763
5361
|
if (c === "'" || c === '"') { inStr = c; i++; continue; }
|
|
4764
5362
|
if (c === '`') { i++; while (i < sql.length && sql[i] !== '`') i++; i++; continue; }
|
|
4765
5363
|
if (c === '-' && sql[i + 1] === '-') return true;
|
|
4766
|
-
if (c === '#') return true;
|
|
5364
|
+
if (c === '#' && sql[i + 1] !== '>') return true; // '#>' / '#>>' 是 JSON 操作符,不是注释
|
|
4767
5365
|
if (c === '/' && sql[i + 1] === '*') return true;
|
|
4768
5366
|
i++;
|
|
4769
5367
|
}
|
|
@@ -4812,16 +5410,34 @@ function applyParams(sql, values) {
|
|
|
4812
5410
|
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
4813
5411
|
hasNamed = true;
|
|
4814
5412
|
}
|
|
4815
|
-
|
|
5413
|
+
const n = sql.length;
|
|
5414
|
+
while (i < n) {
|
|
4816
5415
|
const c = sql[i];
|
|
4817
5416
|
if (inStr) {
|
|
4818
5417
|
out += c;
|
|
4819
|
-
if (c === '\\' && i + 1 <
|
|
5418
|
+
if (c === '\\' && i + 1 < n) { out += sql[i + 1]; i += 2; continue; }
|
|
4820
5419
|
if (c === inStr) inStr = null;
|
|
4821
5420
|
i++;
|
|
4822
5421
|
continue;
|
|
4823
5422
|
}
|
|
4824
5423
|
if (c === "'" || c === '"' || c === '`') { inStr = c; out += c; i++; continue; }
|
|
5424
|
+
// 跳过注释:`--` 和 `#`(但 `#>` / `#>>` 是 JSON 操作符,不是注释)
|
|
5425
|
+
if (c === '-' && sql[i + 1] === '-') {
|
|
5426
|
+
// 行注释:整段跳过,保留换行(不在注释里的字符不进入 out)
|
|
5427
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
5428
|
+
continue;
|
|
5429
|
+
}
|
|
5430
|
+
if (c === '#' && sql[i + 1] !== '>') {
|
|
5431
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
5432
|
+
continue;
|
|
5433
|
+
}
|
|
5434
|
+
if (c === '/' && sql[i + 1] === '*') {
|
|
5435
|
+
// 块注释:整段跳过
|
|
5436
|
+
i += 2;
|
|
5437
|
+
while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
|
|
5438
|
+
if (i + 1 < n) i += 2; else i = n;
|
|
5439
|
+
continue;
|
|
5440
|
+
}
|
|
4825
5441
|
if (c === '?' && sql[i + 1] === '?') {
|
|
4826
5442
|
if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
4827
5443
|
out += escapeId(hasNamed ? args['@@'] : args[idx++]);
|
|
@@ -4854,6 +5470,24 @@ function applyParams(sql, values) {
|
|
|
4854
5470
|
i++;
|
|
4855
5471
|
continue;
|
|
4856
5472
|
}
|
|
5473
|
+
// $N 编号占位符(PG 风格):$1 / $2 ...
|
|
5474
|
+
if (c === '$' && i + 1 < sql.length && sql[i + 1] >= '0' && sql[i + 1] <= '9') {
|
|
5475
|
+
let num = '';
|
|
5476
|
+
let j = i + 1;
|
|
5477
|
+
while (j < sql.length && sql[j] >= '0' && sql[j] <= '9') { num += sql[j]; j++; }
|
|
5478
|
+
const n = parseInt(num, 10);
|
|
5479
|
+
if (hasNamed) {
|
|
5480
|
+
if (!(n in args)) throw new Error(`No value for parameter $${n}`);
|
|
5481
|
+
out += escapeValue(args[n]);
|
|
5482
|
+
} else {
|
|
5483
|
+
if (n - 1 >= args.length) throw new Error(`Not enough parameters for SQL: expected $${n}`);
|
|
5484
|
+
out += escapeValue(args[n - 1]);
|
|
5485
|
+
if (n > idx) idx = n;
|
|
5486
|
+
}
|
|
5487
|
+
count++;
|
|
5488
|
+
i = j;
|
|
5489
|
+
continue;
|
|
5490
|
+
}
|
|
4857
5491
|
if ((c === ':' || c === '@' || c === '$') && i + 1 < sql.length && /[A-Za-z_]/.test(sql[i + 1])) {
|
|
4858
5492
|
// 命名占位符 :name @name $name
|
|
4859
5493
|
let name = '';
|
|
@@ -4936,14 +5570,19 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
4936
5570
|
if (Array.isArray(paramsOrOpts)) {
|
|
4937
5571
|
sql = applyParams(sql, paramsOrOpts);
|
|
4938
5572
|
} else if (paramsOrOpts && typeof paramsOrOpts === 'object') {
|
|
4939
|
-
opts
|
|
5573
|
+
// 第三参为对象时:含已知 opts 键 → 视为 opts;否则视为命名参数({id: 1} → :id / $id)
|
|
5574
|
+
const OPTS_KEYS = ['safety', 'session', 'params', 'functions', 'aggregates',
|
|
5575
|
+
'allowComments', 'maxStatements', 'maxSqlLength', 'context', 'timeout', 'dialect'];
|
|
5576
|
+
const looksLikeOpts = Object.keys(paramsOrOpts).some(k => OPTS_KEYS.includes(k));
|
|
5577
|
+
if (looksLikeOpts) opts = paramsOrOpts;
|
|
5578
|
+
else sql = applyParams(sql, paramsOrOpts);
|
|
4940
5579
|
}
|
|
4941
5580
|
if (opts.safety !== false) {
|
|
4942
5581
|
if (!opts.allowComments && hasComments(sql)) {
|
|
4943
5582
|
throw new Error('SQL comments are disabled for security (--, #, /* */)');
|
|
4944
5583
|
}
|
|
4945
5584
|
}
|
|
4946
|
-
let statements = splitStatements(sql);
|
|
5585
|
+
let statements = splitStatements(sql, { maxLength: opts.maxSqlLength });
|
|
4947
5586
|
if (opts.maxStatements != null && statements.length > opts.maxStatements) {
|
|
4948
5587
|
throw new Error(`too many statements (${statements.length} > ${opts.maxStatements})`);
|
|
4949
5588
|
}
|