jsql-neo 6.0.0 → 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 +169 -56
- package/lib/native_client.js +69 -8
- package/lib/pg_server.js +7 -1
- package/lib/redis_server.js +58 -28
- package/lib/sql.js +1267 -117
- package/lib/wasm_client.js +58 -8
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/Cargo.lock +2 -2
- package/package.json +4 -3
- package/test/readme-audit.test.js +420 -0
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,14 +206,31 @@ 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
|
-
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '=='
|
|
222
|
+
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
|
|
223
|
+
two === '<<' || two === '>>' || two === '->' || two === '::' ||
|
|
224
|
+
two === '&&' || two === '||' || two === '~*' || two === '!~') {
|
|
182
225
|
tokens.push(new SQLToken('op', two, i));
|
|
183
226
|
i += 2;
|
|
184
227
|
continue;
|
|
185
228
|
}
|
|
186
229
|
|
|
187
|
-
|
|
230
|
+
// 注意:% & | ^ ~ 这些运算符必须在词法层产出 token。
|
|
231
|
+
// 之前 parseTerm() 里写了 '%' 分支但字符集里没有 '%',
|
|
232
|
+
// 导致 SELECT 10 % 3 直接报 "Unexpected character '%'",那段是死代码。
|
|
233
|
+
if ('=<>+-*/(),.;%&|^~'.includes(c)) {
|
|
188
234
|
tokens.push(new SQLToken('op', c, i));
|
|
189
235
|
i++;
|
|
190
236
|
continue;
|
|
@@ -197,11 +243,23 @@ function tokenize(sql) {
|
|
|
197
243
|
return tokens;
|
|
198
244
|
}
|
|
199
245
|
|
|
246
|
+
// 聚合函数集合:这些必须走 aggregate 节点,否则会被当成逐行标量函数(返回 null 且不折叠行)
|
|
247
|
+
const AGG_FUNCS = new Set(['SUM', 'AVG', 'MIN', 'MAX', 'COUNT',
|
|
248
|
+
'GROUP_CONCAT', 'STDDEV', 'STDDEV_POP', 'STDDEV_SAMP',
|
|
249
|
+
'VARIANCE', 'VAR_POP', 'VAR_SAMP', 'FIRST', 'LAST']);
|
|
250
|
+
|
|
251
|
+
// 这些词会跟在表名/子查询之后,不能被误当成表别名。
|
|
252
|
+
// 新增任何"跟在 FROM 之后"的子句时,都要往这里加词。
|
|
253
|
+
const NOT_ALIAS_WORDS = ['JOIN', 'LEFT', 'RIGHT', 'FULL', 'WHERE', 'GROUP', 'ORDER', 'LIMIT',
|
|
254
|
+
'ON', 'HAVING', 'UNION', 'INNER', 'CROSS', 'OUTER', 'INTERSECT', 'EXCEPT',
|
|
255
|
+
'FETCH', 'RETURNING', 'WINDOW', 'OFFSET', 'FOR'];
|
|
256
|
+
|
|
200
257
|
class Parser {
|
|
201
258
|
constructor(tokens) {
|
|
202
259
|
this.tokens = tokens;
|
|
203
260
|
this.pos = 0;
|
|
204
261
|
this.paramCount = 0; // 顺序占位符 ? 的序号(从 0 开始)
|
|
262
|
+
this.subquerySeq = 0; // FROM 子查询无别名时自动生成 __subN
|
|
205
263
|
}
|
|
206
264
|
|
|
207
265
|
peek(offset = 0) { return this.tokens[this.pos + offset]; }
|
|
@@ -271,11 +329,13 @@ class Parser {
|
|
|
271
329
|
if (this.isKeyword('TABLE', 1)) return this.parseCreateTable();
|
|
272
330
|
if (this.isKeyword('DATABASE', 1)) return this.parseCreateDatabase();
|
|
273
331
|
if (this.isWord('VIEW', 1) || this.isWord('OR', 1)) return this.parseCreateView();
|
|
332
|
+
if (this.isKeyword('INDEX', 1) || this.isKeyword('UNIQUE', 1)) return this.parseCreateIndex();
|
|
274
333
|
throw new Error('Unsupported CREATE statement');
|
|
275
334
|
case 'DROP':
|
|
276
335
|
if (this.isKeyword('TABLE', 1)) return this.parseDropTable();
|
|
277
336
|
if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
|
|
278
337
|
if (this.isWord('VIEW', 1)) return this.parseDropView();
|
|
338
|
+
if (this.isKeyword('INDEX', 1)) return this.parseDropIndex();
|
|
279
339
|
throw new Error('Unsupported DROP statement');
|
|
280
340
|
case 'INSERT': return this.parseInsert();
|
|
281
341
|
case 'REPLACE': return this.parseReplace();
|
|
@@ -351,6 +411,45 @@ class Parser {
|
|
|
351
411
|
return { type: 'createView', name, columns, select, orReplace };
|
|
352
412
|
}
|
|
353
413
|
|
|
414
|
+
/**
|
|
415
|
+
* CREATE [UNIQUE] INDEX name ON table (col [, col]...)
|
|
416
|
+
* 归一成 { type:'createIndex', name, table, columns, unique }
|
|
417
|
+
*/
|
|
418
|
+
parseCreateIndex() {
|
|
419
|
+
this.expectKeyword('CREATE');
|
|
420
|
+
let unique = false;
|
|
421
|
+
if (this.isKeyword('UNIQUE')) { this.next(); unique = true; }
|
|
422
|
+
if (!this.isKeyword('INDEX')) throw new Error(`Expected INDEX, got '${this.peek().value}'`);
|
|
423
|
+
this.next();
|
|
424
|
+
const name = this.parseTableName();
|
|
425
|
+
if (!this.isKeyword('ON')) throw new Error(`Expected ON in CREATE INDEX, got '${this.peek().value}'`);
|
|
426
|
+
this.next();
|
|
427
|
+
const table = this.parseTableName();
|
|
428
|
+
this.expect('op', '(');
|
|
429
|
+
const columns = [];
|
|
430
|
+
for (;;) {
|
|
431
|
+
columns.push(this.parseColumnRef());
|
|
432
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
this.expect('op', ')');
|
|
436
|
+
this.optionalTailSemicolon();
|
|
437
|
+
return { type: 'createIndex', name, table, columns, unique };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** DROP INDEX name ON table */
|
|
441
|
+
parseDropIndex() {
|
|
442
|
+
this.expectKeyword('DROP');
|
|
443
|
+
this.expectKeyword('INDEX');
|
|
444
|
+
let ifExists = false;
|
|
445
|
+
if (this.isKeyword('IF')) { this.expectKeyword('IF'); this.expectKeyword('EXISTS'); ifExists = true; }
|
|
446
|
+
const name = this.parseTableName();
|
|
447
|
+
let table = null;
|
|
448
|
+
if (this.isKeyword('ON')) { this.next(); table = this.parseTableName(); }
|
|
449
|
+
this.optionalTailSemicolon();
|
|
450
|
+
return { type: 'dropIndex', name, table, ifExists };
|
|
451
|
+
}
|
|
452
|
+
|
|
354
453
|
parseDropView() {
|
|
355
454
|
this.expectKeyword('DROP');
|
|
356
455
|
if (!this.isWord('VIEW')) throw new Error(`Expected VIEW, got '${this.peek().value}'`);
|
|
@@ -413,6 +512,9 @@ class Parser {
|
|
|
413
512
|
hasPk = true;
|
|
414
513
|
} else {
|
|
415
514
|
this.expectKeyword('UNIQUE');
|
|
515
|
+
// UNIQUE [KEY|INDEX] [name] (cols...)
|
|
516
|
+
if (this.isKeyword('KEY') || this.isKeyword('INDEX')) this.next();
|
|
517
|
+
if (this.peek().type === 'ident') this.next();
|
|
416
518
|
this.expect('op', '(');
|
|
417
519
|
const uCols = [this.parseTableName()];
|
|
418
520
|
while (this.peek().type === 'op' && this.peek().value === ',') {
|
|
@@ -426,7 +528,12 @@ class Parser {
|
|
|
426
528
|
}
|
|
427
529
|
} else if (t.type === 'keyword' && t.value === 'CONSTRAINT') {
|
|
428
530
|
this.expectKeyword('CONSTRAINT');
|
|
429
|
-
this.next();
|
|
531
|
+
if (this.peek().type === 'ident') this.next(); // 约束名
|
|
532
|
+
this._parseTableConstraint(schema);
|
|
533
|
+
} else if (this.isWord('FOREIGN') || this.isWord('CHECK') ||
|
|
534
|
+
t.type === 'keyword' && (t.value === 'KEY' || t.value === 'INDEX')) {
|
|
535
|
+
// 表级约束:FOREIGN KEY ... REFERENCES ... / CHECK (...) / KEY name (cols)
|
|
536
|
+
this._parseTableConstraint(schema);
|
|
430
537
|
} else if (t.type === 'eof' || (t.type === 'op' && t.value === ')')) {
|
|
431
538
|
break;
|
|
432
539
|
} else {
|
|
@@ -458,19 +565,27 @@ class Parser {
|
|
|
458
565
|
parseColumnDef() {
|
|
459
566
|
const def = {};
|
|
460
567
|
const typeTok = this.next();
|
|
461
|
-
|
|
462
|
-
|
|
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();
|
|
463
574
|
const typeMap = {
|
|
464
575
|
integer: 'integer', int: 'integer', bigint: 'integer', tinyint: 'integer', smallint: 'integer',
|
|
465
576
|
string: 'string', text: 'string', varchar: 'string', char: 'string',
|
|
466
577
|
float: 'number', double: 'number', real: 'number', numeric: 'number', decimal: 'number',
|
|
467
578
|
boolean: 'boolean', bool: 'boolean',
|
|
468
|
-
date: 'date', datetime: 'datetime', timestamp: 'timestamp',
|
|
469
|
-
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'
|
|
470
584
|
};
|
|
471
585
|
const mapped = typeMap[type];
|
|
472
586
|
if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
|
|
473
587
|
def.type = mapped;
|
|
588
|
+
if (/^(BIG)?SERIAL$/i.test(typeRaw)) { def.autoIncrement = true; def.required = true; }
|
|
474
589
|
|
|
475
590
|
// 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
|
|
476
591
|
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
@@ -517,7 +632,29 @@ class Parser {
|
|
|
517
632
|
case 'NULL':
|
|
518
633
|
this.next(); break;
|
|
519
634
|
case 'DEFAULT':
|
|
520
|
-
this.next();
|
|
635
|
+
this.next();
|
|
636
|
+
// 默认值可以是字面量,也可以是函数/关键字形式(CURRENT_TIMESTAMP / NOW() 等)。
|
|
637
|
+
// 函数形式存成字符串 + defaultExpr 标记,与 JS 侧 `default: 'CURRENT_TIMESTAMP'` 约定一致。
|
|
638
|
+
if (this.peek().type === 'ident' ||
|
|
639
|
+
(this.peek().type === 'keyword' && !['NULL', 'TRUE', 'FALSE'].includes(this.peek().value))) {
|
|
640
|
+
const fnTok = this.next();
|
|
641
|
+
let name = String(fnTok.value);
|
|
642
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
643
|
+
this.next();
|
|
644
|
+
this.expect('op', ')');
|
|
645
|
+
name += '()';
|
|
646
|
+
}
|
|
647
|
+
def.default = name;
|
|
648
|
+
def.defaultExpr = true;
|
|
649
|
+
} else {
|
|
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
|
+
}
|
|
656
|
+
}
|
|
657
|
+
break;
|
|
521
658
|
case 'COLLATE':
|
|
522
659
|
this.next(); if (this.peek().type !== 'op' && this.peek().type !== 'eof') this.next(); break;
|
|
523
660
|
case 'CHARACTER':
|
|
@@ -553,6 +690,68 @@ class Parser {
|
|
|
553
690
|
return stmt;
|
|
554
691
|
}
|
|
555
692
|
|
|
693
|
+
/** 跳过一对括号(含嵌套),用于表级约束里的列清单 */
|
|
694
|
+
_skipParens() {
|
|
695
|
+
this.expect('op', '(');
|
|
696
|
+
let depth = 1;
|
|
697
|
+
while (depth > 0 && this.peek().type !== 'eof') {
|
|
698
|
+
const tk = this.next();
|
|
699
|
+
if (tk.type === 'op' && tk.value === '(') depth++;
|
|
700
|
+
else if (tk.type === 'op' && tk.value === ')') depth--;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* 解析表级约束(FOREIGN KEY / CHECK / KEY / INDEX / PRIMARY KEY / UNIQUE)。
|
|
706
|
+
* 目前只做语法层面消费并在可能时补充列属性,不做外键实际校验。
|
|
707
|
+
*/
|
|
708
|
+
_parseTableConstraint(schema) {
|
|
709
|
+
if (this.isWord('FOREIGN')) {
|
|
710
|
+
this.next();
|
|
711
|
+
if (!this.isKeyword('KEY')) throw new Error(`Expected KEY after FOREIGN, got '${this.peek().value}'`);
|
|
712
|
+
this.next();
|
|
713
|
+
if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
|
|
714
|
+
if (this.isWord('REFERENCES')) {
|
|
715
|
+
this.next();
|
|
716
|
+
this.parseTableName();
|
|
717
|
+
if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
|
|
718
|
+
}
|
|
719
|
+
// ON DELETE / ON UPDATE <action>
|
|
720
|
+
while (this.isKeyword('ON')) {
|
|
721
|
+
this.next();
|
|
722
|
+
this.next(); // DELETE | UPDATE
|
|
723
|
+
const act = String(this.next().value).toUpperCase();
|
|
724
|
+
if (act === 'SET' || act === 'NO') this.next(); // SET NULL / NO ACTION
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (this.isWord('CHECK')) {
|
|
729
|
+
this.next();
|
|
730
|
+
if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (this.isKeyword('PRIMARY')) {
|
|
734
|
+
this.expectKeyword('PRIMARY');
|
|
735
|
+
if (this.isKeyword('KEY')) this.next();
|
|
736
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
737
|
+
this.next();
|
|
738
|
+
const cols = [this.parseTableName()];
|
|
739
|
+
while (this.peek().type === 'op' && this.peek().value === ',') { this.next(); cols.push(this.parseTableName()); }
|
|
740
|
+
this.expect('op', ')');
|
|
741
|
+
for (const c of cols) if (schema[c]) schema[c].primaryKey = true;
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (this.isKeyword('UNIQUE')) {
|
|
746
|
+
this.next();
|
|
747
|
+
}
|
|
748
|
+
if (this.isKeyword('KEY') || this.isKeyword('INDEX')) {
|
|
749
|
+
this.next();
|
|
750
|
+
if (this.peek().type === 'ident') this.next(); // 索引名
|
|
751
|
+
if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
556
755
|
parseInsertBody() {
|
|
557
756
|
const name = this.parseTableName();
|
|
558
757
|
let columns = null;
|
|
@@ -628,7 +827,7 @@ class Parser {
|
|
|
628
827
|
for (;;) {
|
|
629
828
|
const col = this.parseColumnRef();
|
|
630
829
|
this.expect('op', '=');
|
|
631
|
-
const val = this.
|
|
830
|
+
const val = this.parseConflictValue();
|
|
632
831
|
sets.push([col, val]);
|
|
633
832
|
if (this.peek().value === ',') { this.next(); continue; }
|
|
634
833
|
break;
|
|
@@ -683,6 +882,13 @@ class Parser {
|
|
|
683
882
|
parseValue() {
|
|
684
883
|
const t = this.next();
|
|
685
884
|
if (t.type === 'number' || t.type === 'string') return t.value;
|
|
885
|
+
// MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
|
|
886
|
+
if (t.type === 'keyword' && t.value === 'VALUES' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
887
|
+
this.next();
|
|
888
|
+
const col = this.parseColumnRef();
|
|
889
|
+
this.expect('op', ')');
|
|
890
|
+
return { _valuesOf: col };
|
|
891
|
+
}
|
|
686
892
|
if (t.type === 'keyword' && t.value === 'NULL') return null;
|
|
687
893
|
if (t.type === 'keyword' && t.value === 'TRUE') return true;
|
|
688
894
|
if (t.type === 'keyword' && t.value === 'FALSE') return false;
|
|
@@ -701,6 +907,21 @@ class Parser {
|
|
|
701
907
|
throw new Error(`Expected value, got '${t.value}'`);
|
|
702
908
|
}
|
|
703
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
|
+
|
|
704
925
|
/**
|
|
705
926
|
* WITH [RECURSIVE] name [(col, ...)] AS (SELECT ...), ... <statement>
|
|
706
927
|
* CTE 在语法层解析为 { type:'with', ctes:[{name, columns, select}], statement }。
|
|
@@ -833,24 +1054,37 @@ class Parser {
|
|
|
833
1054
|
if (t.type === 'keyword' && t.value === 'COUNT') {
|
|
834
1055
|
this.next();
|
|
835
1056
|
this.expect('op', '(');
|
|
1057
|
+
let aggDistinct = false;
|
|
1058
|
+
if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct = true; }
|
|
836
1059
|
let col = null;
|
|
837
1060
|
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
|
|
838
1061
|
else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
|
|
1062
|
+
// COUNT(DISTINCT a, b) —— 多列去重(仅对 COUNT 有意义,其它聚合按元组处理)
|
|
1063
|
+
if (aggDistinct && this.peek().type === 'op' && this.peek().value === ',') {
|
|
1064
|
+
const cols = [col];
|
|
1065
|
+
while (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1066
|
+
this.next();
|
|
1067
|
+
cols.push(this.parseScalar());
|
|
1068
|
+
}
|
|
1069
|
+
col = { type: 'tuple', items: cols };
|
|
1070
|
+
}
|
|
839
1071
|
this.expect('op', ')');
|
|
840
|
-
aggregate = { type: 'COUNT', column: col };
|
|
1072
|
+
aggregate = { type: 'COUNT', column: col, distinct: aggDistinct };
|
|
841
1073
|
aggregate = this.parseOptionalOver(aggregate);
|
|
842
1074
|
aggregate.alias = this.parseOptionalAlias();
|
|
843
|
-
columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
|
|
1075
|
+
columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct });
|
|
844
1076
|
} else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
|
|
845
1077
|
this.next();
|
|
846
1078
|
const fn = t.value;
|
|
847
1079
|
this.expect('op', '(');
|
|
1080
|
+
let aggDistinct2 = false;
|
|
1081
|
+
if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct2 = true; }
|
|
848
1082
|
const col = this.parseScalar();
|
|
849
1083
|
this.expect('op', ')');
|
|
850
|
-
aggregate = { type: fn, column: col };
|
|
1084
|
+
aggregate = { type: fn, column: col, distinct: aggDistinct2 };
|
|
851
1085
|
aggregate = this.parseOptionalOver(aggregate);
|
|
852
1086
|
aggregate.alias = this.parseOptionalAlias();
|
|
853
|
-
columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
|
|
1087
|
+
columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct2 });
|
|
854
1088
|
} else if (t.type === 'op' && t.value === '*') {
|
|
855
1089
|
this.next();
|
|
856
1090
|
columns.push({ expr: '*' });
|
|
@@ -867,7 +1101,7 @@ class Parser {
|
|
|
867
1101
|
if (expr.type === 'star') {
|
|
868
1102
|
columns.push({ expr: '*' });
|
|
869
1103
|
} else if (expr.type === 'aggregate') {
|
|
870
|
-
columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr });
|
|
1104
|
+
columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr, distinct: expr.distinct, separator: expr.separator, orderBy: expr.orderBy });
|
|
871
1105
|
} else if (expr.type === 'column') {
|
|
872
1106
|
columns.push({ expr: expr.name, scalar: expr, alias });
|
|
873
1107
|
} else {
|
|
@@ -886,6 +1120,7 @@ class Parser {
|
|
|
886
1120
|
let where = null;
|
|
887
1121
|
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
888
1122
|
let groupBy = null;
|
|
1123
|
+
let rollup = false;
|
|
889
1124
|
if (this.isKeyword('GROUP')) {
|
|
890
1125
|
this.expectKeyword('GROUP'); this.expectKeyword('BY');
|
|
891
1126
|
groupBy = [];
|
|
@@ -894,6 +1129,12 @@ class Parser {
|
|
|
894
1129
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
895
1130
|
break;
|
|
896
1131
|
}
|
|
1132
|
+
// WITH ROLLUP 必须紧跟 GROUP BY 列表,之后才是 HAVING / ORDER BY / LIMIT
|
|
1133
|
+
// (标准顺序:GROUP BY <list> [WITH ROLLUP] [HAVING] [ORDER BY] [LIMIT])
|
|
1134
|
+
if (this.isKeyword('WITH') && this.isWord('ROLLUP', 1)) {
|
|
1135
|
+
this.next(); this.next();
|
|
1136
|
+
rollup = true;
|
|
1137
|
+
}
|
|
897
1138
|
}
|
|
898
1139
|
let having = null;
|
|
899
1140
|
if (this.isKeyword('HAVING')) { this.next(); having = this.parseExpr(); }
|
|
@@ -961,13 +1202,6 @@ class Parser {
|
|
|
961
1202
|
else if (this.isKeyword('DISTINCT')) this.next();
|
|
962
1203
|
except = { all, select: this.parseSelect() };
|
|
963
1204
|
}
|
|
964
|
-
// GROUP BY ... WITH ROLLUP
|
|
965
|
-
let rollup = false;
|
|
966
|
-
if (groupBy && this.isKeyword('WITH')) {
|
|
967
|
-
this.next();
|
|
968
|
-
if (this.isWord('ROLLUP')) { this.next(); rollup = true; }
|
|
969
|
-
else throw new Error(`Expected ROLLUP after WITH, got '${this.peek().value}'`);
|
|
970
|
-
}
|
|
971
1205
|
// FETCH FIRST n ROWS ONLY(SQL 标准的 LIMIT 写法)
|
|
972
1206
|
if (this.isWord('FETCH')) {
|
|
973
1207
|
this.next();
|
|
@@ -1029,19 +1263,18 @@ class Parser {
|
|
|
1029
1263
|
this.expect('op', ')');
|
|
1030
1264
|
let alias = null;
|
|
1031
1265
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
1032
|
-
else if (this.peek().type === 'ident') { alias = this.next().value; }
|
|
1033
|
-
|
|
1266
|
+
else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) { alias = this.next().value; }
|
|
1267
|
+
// 标准 SQL 要求 FROM 子查询必须带别名,这里放宽为自动生成,
|
|
1268
|
+
// 避免 README 里 SELECT ... FROM (SELECT ...) 这类示例直接报错
|
|
1269
|
+
if (!alias) alias = '__sub' + (++this.subquerySeq);
|
|
1034
1270
|
return { subquery: sub, alias };
|
|
1035
1271
|
}
|
|
1036
1272
|
const table = this.parseTableName();
|
|
1037
1273
|
let alias = null;
|
|
1038
1274
|
// 这里排除的是"未进关键字表、但会跟在表名之后"的子句起始词,
|
|
1039
1275
|
// 否则 `FROM t FETCH FIRST 10 ROWS ONLY` 会把 FETCH 当成表别名吃掉。
|
|
1040
|
-
const NOT_ALIAS = ['JOIN', 'LEFT', 'RIGHT', 'FULL', 'WHERE', 'GROUP', 'ORDER', 'LIMIT',
|
|
1041
|
-
'ON', 'HAVING', 'UNION', 'INNER', 'CROSS', 'OUTER', 'INTERSECT', 'EXCEPT',
|
|
1042
|
-
'FETCH', 'RETURNING', 'WINDOW'];
|
|
1043
1276
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
1044
|
-
else if (this.peek().type === 'ident' && !
|
|
1277
|
+
else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) {
|
|
1045
1278
|
alias = this.next().value;
|
|
1046
1279
|
}
|
|
1047
1280
|
return { table, alias };
|
|
@@ -1096,9 +1329,28 @@ class Parser {
|
|
|
1096
1329
|
const table = this.parseTableName();
|
|
1097
1330
|
let where = null;
|
|
1098
1331
|
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
1332
|
+
let orderBy = null, limit = null;
|
|
1333
|
+
if (this.isKeyword('ORDER')) {
|
|
1334
|
+
this.next(); this.expectKeyword('BY');
|
|
1335
|
+
orderBy = [];
|
|
1336
|
+
for (;;) {
|
|
1337
|
+
const col = this.parseColumnRef();
|
|
1338
|
+
let dir = 'asc';
|
|
1339
|
+
if (this.isKeyword('ASC')) this.next();
|
|
1340
|
+
else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
|
|
1341
|
+
orderBy.push({ column: col, dir });
|
|
1342
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (this.isKeyword('LIMIT')) {
|
|
1347
|
+
this.next();
|
|
1348
|
+
limit = this.parseValue();
|
|
1349
|
+
if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
|
|
1350
|
+
}
|
|
1099
1351
|
const returning = this.parseOptionalReturning();
|
|
1100
1352
|
this.optionalTailSemicolon();
|
|
1101
|
-
return { type: 'delete', table, where, returning };
|
|
1353
|
+
return { type: 'delete', table, where, orderBy, limit, returning };
|
|
1102
1354
|
}
|
|
1103
1355
|
|
|
1104
1356
|
parseDropTable() {
|
|
@@ -1386,19 +1638,93 @@ class Parser {
|
|
|
1386
1638
|
return this.parseOr();
|
|
1387
1639
|
}
|
|
1388
1640
|
|
|
1641
|
+
/**
|
|
1642
|
+
* 函数实参用的表达式:与 parseExpr 相同,但叶子允许是"裸操作数"。
|
|
1643
|
+
* 例如 IF(1 > 0, 'y', 'n') 的第一个参数是完整比较表达式,
|
|
1644
|
+
* 而 IF(age, 'y', 'n') 这种裸列名也必须能解析。
|
|
1645
|
+
*/
|
|
1646
|
+
parseArgExpr() {
|
|
1647
|
+
this.bareExprDepth = (this.bareExprDepth || 0) + 1;
|
|
1648
|
+
try {
|
|
1649
|
+
return this.parseOr();
|
|
1650
|
+
} finally {
|
|
1651
|
+
this.bareExprDepth--;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// 位运算优先级(低 → 高):| < ^ < & < << >> < 加减 < 乘除模
|
|
1656
|
+
parseBitwise() {
|
|
1657
|
+
let node = this.parseBitXor();
|
|
1658
|
+
for (;;) {
|
|
1659
|
+
const t = this.peek();
|
|
1660
|
+
if (t.type === 'op' && t.value === '|') { this.next(); node = { type: 'arith', op: '|', left: node, right: this.parseBitXor() }; continue; }
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
return node;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
parseBitXor() {
|
|
1667
|
+
let node = this.parseBitAnd();
|
|
1668
|
+
for (;;) {
|
|
1669
|
+
const t = this.peek();
|
|
1670
|
+
if (t.type === 'op' && t.value === '^') { this.next(); node = { type: 'arith', op: '^', left: node, right: this.parseBitAnd() }; continue; }
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
return node;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
parseBitAnd() {
|
|
1677
|
+
let node = this.parseShift();
|
|
1678
|
+
for (;;) {
|
|
1679
|
+
const t = this.peek();
|
|
1680
|
+
if (t.type === 'op' && t.value === '&') { this.next(); node = { type: 'arith', op: '&', left: node, right: this.parseShift() }; continue; }
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
return node;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
parseShift() {
|
|
1687
|
+
let node = this.parseAdditive();
|
|
1688
|
+
for (;;) {
|
|
1689
|
+
const t = this.peek();
|
|
1690
|
+
if (t.type === 'op' && (t.value === '<<' || t.value === '>>')) {
|
|
1691
|
+
this.next();
|
|
1692
|
+
node = { type: 'arith', op: t.value, left: node, right: this.parseAdditive() };
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
break;
|
|
1696
|
+
}
|
|
1697
|
+
return node;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1389
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() {
|
|
1390
1714
|
let left = this.parseAnd();
|
|
1391
|
-
while (this.
|
|
1715
|
+
while (this.isWord('XOR')) {
|
|
1392
1716
|
this.next();
|
|
1393
1717
|
const right = this.parseAnd();
|
|
1394
|
-
left = { type: '
|
|
1718
|
+
left = { type: 'xor', left, right };
|
|
1395
1719
|
}
|
|
1396
1720
|
return left;
|
|
1397
1721
|
}
|
|
1398
1722
|
|
|
1399
1723
|
parseAnd() {
|
|
1400
1724
|
let left = this.parseNot();
|
|
1401
|
-
|
|
1725
|
+
for (;;) {
|
|
1726
|
+
const isAnd = this.isKeyword('AND') || (this.peek().type === 'op' && this.peek().value === '&&');
|
|
1727
|
+
if (!isAnd) break;
|
|
1402
1728
|
this.next();
|
|
1403
1729
|
const right = this.parseNot();
|
|
1404
1730
|
left = { type: 'and', left, right };
|
|
@@ -1434,9 +1760,69 @@ class Parser {
|
|
|
1434
1760
|
return this.parseComparison();
|
|
1435
1761
|
}
|
|
1436
1762
|
|
|
1763
|
+
/**
|
|
1764
|
+
* 解析聚合调用:fn ( [DISTINCT] expr [, expr*] [ORDER BY ...] [SEPARATOR 'x'] )
|
|
1765
|
+
* 以及 COUNT(*) 这类无参形式。
|
|
1766
|
+
*/
|
|
1767
|
+
parseAggregateCall(fn) {
|
|
1768
|
+
this.expect('op', '(');
|
|
1769
|
+
const distinct = this.isKeyword('DISTINCT');
|
|
1770
|
+
if (distinct) this.next();
|
|
1771
|
+
let column = null;
|
|
1772
|
+
let separator = null;
|
|
1773
|
+
let innerOrder = null;
|
|
1774
|
+
if (this.peek().type === 'op' && this.peek().value === '*') {
|
|
1775
|
+
this.next();
|
|
1776
|
+
} else if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1777
|
+
column = this.parseScalar();
|
|
1778
|
+
// COUNT(DISTINCT a, b):多列去重
|
|
1779
|
+
if (distinct && this.peek().type === 'op' && this.peek().value === ',') {
|
|
1780
|
+
const cols = [column];
|
|
1781
|
+
while (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1782
|
+
this.next();
|
|
1783
|
+
cols.push(this.parseScalar());
|
|
1784
|
+
}
|
|
1785
|
+
column = { type: 'tuple', items: cols };
|
|
1786
|
+
} else if (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1787
|
+
// GROUP_CONCAT(expr, 'sep')
|
|
1788
|
+
this.next();
|
|
1789
|
+
separator = this.parseValue();
|
|
1790
|
+
}
|
|
1791
|
+
// GROUP_CONCAT(expr ORDER BY col [DESC])
|
|
1792
|
+
if (this.isKeyword('ORDER')) {
|
|
1793
|
+
this.next();
|
|
1794
|
+
if (!this.isKeyword('BY')) throw new Error('Expected BY after ORDER in aggregate, got ' + "'" + this.peek().value + "'");
|
|
1795
|
+
this.next();
|
|
1796
|
+
innerOrder = [];
|
|
1797
|
+
for (;;) {
|
|
1798
|
+
const oCol = this.parseColumnRef();
|
|
1799
|
+
let oDir = 'asc';
|
|
1800
|
+
if (this.isKeyword('ASC')) this.next();
|
|
1801
|
+
else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
|
|
1802
|
+
innerOrder.push({ column: oCol, dir: oDir });
|
|
1803
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
// GROUP_CONCAT(expr SEPARATOR 'sep')
|
|
1808
|
+
if (this.isWord('SEPARATOR')) {
|
|
1809
|
+
this.next();
|
|
1810
|
+
separator = this.parseValue();
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
this.expect('op', ')');
|
|
1814
|
+
return this.parseOptionalOver({ type: 'aggregate', fn, column, distinct, separator, orderBy: innerOrder });
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1437
1817
|
parseOperand() {
|
|
1438
1818
|
const t = this.next();
|
|
1439
1819
|
if (t.type === 'ident') {
|
|
1820
|
+
// 聚合函数名(GROUP_CONCAT / STDDEV / FIRST...)走 aggregate 节点,
|
|
1821
|
+
// 否则会被当作逐行标量函数:既返回 null,又不会折叠分组
|
|
1822
|
+
if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
|
|
1823
|
+
this.peek().type === 'op' && this.peek().value === '(') {
|
|
1824
|
+
return this.parseAggregateCall(String(t.value).toUpperCase());
|
|
1825
|
+
}
|
|
1440
1826
|
// 支持 alias.column 引用
|
|
1441
1827
|
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
1442
1828
|
this.next();
|
|
@@ -1469,6 +1855,60 @@ class Parser {
|
|
|
1469
1855
|
this.expect('op', ')');
|
|
1470
1856
|
return { type: 'cast', expr, dataType };
|
|
1471
1857
|
}
|
|
1858
|
+
// EXTRACT(YEAR FROM expr) —— 标准 SQL 的 FROM 形式
|
|
1859
|
+
if (String(t.value).toUpperCase() === 'EXTRACT' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1860
|
+
this.next();
|
|
1861
|
+
const unitTok = this.next();
|
|
1862
|
+
const unit = String(unitTok.value).toUpperCase();
|
|
1863
|
+
if (!this.isWord('FROM')) throw new Error(`Expected FROM in EXTRACT, got '${this.peek().value}'`);
|
|
1864
|
+
this.next();
|
|
1865
|
+
const arg = this.parseOperand();
|
|
1866
|
+
this.expect('op', ')');
|
|
1867
|
+
return { type: 'func', name: 'EXTRACT', args: [{ type: 'value', value: unit }, arg] };
|
|
1868
|
+
}
|
|
1869
|
+
// SUBSTR(s FROM n [FOR m]) / TRIM([LEADING|TRAILING|BOTH] ['x'] FROM s)
|
|
1870
|
+
if (['SUBSTR', 'SUBSTRING'].includes(String(t.value).toUpperCase()) && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1871
|
+
const name = String(t.value).toUpperCase();
|
|
1872
|
+
this.next();
|
|
1873
|
+
const s = this.parseOperand();
|
|
1874
|
+
let fromV = null, forV = null;
|
|
1875
|
+
if (this.isWord('FROM')) {
|
|
1876
|
+
this.next();
|
|
1877
|
+
fromV = this.parseOperand();
|
|
1878
|
+
if (this.isWord('FOR')) { this.next(); forV = this.parseOperand(); }
|
|
1879
|
+
} else if (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1880
|
+
this.next();
|
|
1881
|
+
fromV = this.parseOperand();
|
|
1882
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); forV = this.parseOperand(); }
|
|
1883
|
+
}
|
|
1884
|
+
this.expect('op', ')');
|
|
1885
|
+
const args = [s];
|
|
1886
|
+
if (fromV) args.push(fromV);
|
|
1887
|
+
if (forV) args.push(forV);
|
|
1888
|
+
return { type: 'func', name, args };
|
|
1889
|
+
}
|
|
1890
|
+
if (String(t.value).toUpperCase() === 'TRIM' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1891
|
+
this.next();
|
|
1892
|
+
let spec = null;
|
|
1893
|
+
if (this.isWord('LEADING') || this.isWord('TRAILING') || this.isWord('BOTH')) {
|
|
1894
|
+
spec = String(this.next().value).toUpperCase();
|
|
1895
|
+
}
|
|
1896
|
+
const first = this.parseOperand();
|
|
1897
|
+
let target = null;
|
|
1898
|
+
if (this.isWord('FROM')) {
|
|
1899
|
+
this.next();
|
|
1900
|
+
target = this.parseOperand();
|
|
1901
|
+
} else if (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1902
|
+
this.next();
|
|
1903
|
+
target = this.parseOperand();
|
|
1904
|
+
}
|
|
1905
|
+
this.expect('op', ')');
|
|
1906
|
+
// 归一成 TRIM(target[, chars[, spec]]),与 applyScalarFunction 的既有签名兼容
|
|
1907
|
+
if (target === null) return { type: 'func', name: 'TRIM', args: [first] };
|
|
1908
|
+
const args = [target, first];
|
|
1909
|
+
if (spec) args.push({ type: 'value', value: spec });
|
|
1910
|
+
return { type: 'func', name: 'TRIM', args };
|
|
1911
|
+
}
|
|
1472
1912
|
// 函数调用: VERSION() / CONCAT(a, b) / NOW() ...
|
|
1473
1913
|
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
1474
1914
|
const name = t.value;
|
|
@@ -1477,7 +1917,42 @@ class Parser {
|
|
|
1477
1917
|
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1478
1918
|
for (;;) {
|
|
1479
1919
|
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
|
|
1480
|
-
|
|
1920
|
+
// INTERVAL n UNIT(DATE_ADD/DATE_SUB 用)
|
|
1921
|
+
else if (this.isWord('INTERVAL')) {
|
|
1922
|
+
this.next();
|
|
1923
|
+
const nTok = this.peek();
|
|
1924
|
+
let nVal;
|
|
1925
|
+
if (nTok.type === 'op' && (nTok.value === '-' || nTok.value === '+')) {
|
|
1926
|
+
this.next();
|
|
1927
|
+
const numTok = this.next();
|
|
1928
|
+
nVal = (nTok.value === '-' ? -1 : 1) * Number(numTok.value);
|
|
1929
|
+
} else {
|
|
1930
|
+
this.next();
|
|
1931
|
+
nVal = Number(nTok.value);
|
|
1932
|
+
}
|
|
1933
|
+
const unitTok = this.next();
|
|
1934
|
+
args.push({ type: 'interval', value: nVal, unit: String(unitTok.value).toUpperCase() });
|
|
1935
|
+
}
|
|
1936
|
+
// 实参允许完整的比较/布尔表达式,例如 IF(status = 'paid', amount, 0)
|
|
1937
|
+
else args.push(this.parseArgExpr());
|
|
1938
|
+
// GROUP_CONCAT(name ORDER BY age):聚合函数内部的排序
|
|
1939
|
+
if (this.isKeyword('ORDER')) {
|
|
1940
|
+
this.next();
|
|
1941
|
+
if (!this.isKeyword('BY')) throw new Error(`Expected BY after ORDER in aggregate, got '${this.peek().value}'`);
|
|
1942
|
+
this.next();
|
|
1943
|
+
const innerOrder = [];
|
|
1944
|
+
for (;;) {
|
|
1945
|
+
const oCol = this.parseColumnRef();
|
|
1946
|
+
let oDir = 'asc';
|
|
1947
|
+
if (this.isKeyword('ASC')) this.next();
|
|
1948
|
+
else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
|
|
1949
|
+
innerOrder.push({ column: oCol, dir: oDir });
|
|
1950
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1951
|
+
break;
|
|
1952
|
+
}
|
|
1953
|
+
this.expect('op', ')');
|
|
1954
|
+
return this.parseOptionalOver({ type: 'func', name, args, orderBy: innerOrder });
|
|
1955
|
+
}
|
|
1481
1956
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1482
1957
|
break;
|
|
1483
1958
|
}
|
|
@@ -1502,7 +1977,7 @@ class Parser {
|
|
|
1502
1977
|
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1503
1978
|
for (;;) {
|
|
1504
1979
|
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
|
|
1505
|
-
else args.push(this.
|
|
1980
|
+
else args.push(this.parseArgExpr());
|
|
1506
1981
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1507
1982
|
break;
|
|
1508
1983
|
}
|
|
@@ -1510,14 +1985,13 @@ class Parser {
|
|
|
1510
1985
|
this.expect('op', ')');
|
|
1511
1986
|
return this.parseOptionalOver({ type: 'func', name, args });
|
|
1512
1987
|
}
|
|
1513
|
-
if (
|
|
1514
|
-
|
|
1515
|
-
this.
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
return this.parseOptionalOver({ type: 'aggregate', fn, column });
|
|
1988
|
+
if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
|
|
1989
|
+
this.peek().type === 'op' && this.peek().value === '(') {
|
|
1990
|
+
return this.parseAggregateCall(String(t.value).toUpperCase());
|
|
1991
|
+
}
|
|
1992
|
+
// 一元按位取反 ~x
|
|
1993
|
+
if (t.type === 'op' && t.value === '~') {
|
|
1994
|
+
return { type: 'bitnot', expr: this.parseOperand() };
|
|
1521
1995
|
}
|
|
1522
1996
|
if (t.type === 'op' && t.value === '(' && this.peek().type === 'keyword' && this.peek().value === 'SELECT') {
|
|
1523
1997
|
const sub = this.parseSelect();
|
|
@@ -1532,8 +2006,8 @@ class Parser {
|
|
|
1532
2006
|
throw new Error(`Expected value or column, got '${t.value}'`);
|
|
1533
2007
|
}
|
|
1534
2008
|
|
|
1535
|
-
//
|
|
1536
|
-
|
|
2009
|
+
// 加减(左结合)
|
|
2010
|
+
parseAdditive() {
|
|
1537
2011
|
let node = this.parseTerm();
|
|
1538
2012
|
for (;;) {
|
|
1539
2013
|
const t = this.peek();
|
|
@@ -1545,6 +2019,16 @@ class Parser {
|
|
|
1545
2019
|
}
|
|
1546
2020
|
break;
|
|
1547
2021
|
}
|
|
2022
|
+
return node;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/**
|
|
2026
|
+
* 标量表达式:算术 + 位运算,外加 IN / IS 后缀。
|
|
2027
|
+
* 位运算要走到这里,否则 SELECT 列表里的 `SELECT 1 << 8` 无法解析
|
|
2028
|
+
* (parseComparison 只在 WHERE 等布尔上下文被调用)。
|
|
2029
|
+
*/
|
|
2030
|
+
parseScalar() {
|
|
2031
|
+
let node = this.parseBitwise();
|
|
1548
2032
|
// 后缀:expr IN (...)、expr IS [NOT] TRUE/FALSE/NULL(标量上下文,如 SELECT 1 IN (...))
|
|
1549
2033
|
for (;;) {
|
|
1550
2034
|
const t = this.peek();
|
|
@@ -1582,15 +2066,65 @@ class Parser {
|
|
|
1582
2066
|
}
|
|
1583
2067
|
|
|
1584
2068
|
parseTerm() {
|
|
1585
|
-
let node = this.parseOperand();
|
|
2069
|
+
let node = this.parsePostfix(this.parseOperand());
|
|
1586
2070
|
for (;;) {
|
|
1587
2071
|
const t = this.peek();
|
|
1588
2072
|
if (t.type === 'op' && (t.value === '*' || t.value === '/' || t.value === '%')) {
|
|
1589
2073
|
this.next();
|
|
1590
|
-
const right = this.parseOperand();
|
|
2074
|
+
const right = this.parsePostfix(this.parseOperand());
|
|
1591
2075
|
node = { type: 'arith', op: t.value, left: node, right };
|
|
1592
2076
|
continue;
|
|
1593
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
|
+
}
|
|
1594
2128
|
break;
|
|
1595
2129
|
}
|
|
1596
2130
|
return node;
|
|
@@ -1623,7 +2157,7 @@ class Parser {
|
|
|
1623
2157
|
}
|
|
1624
2158
|
|
|
1625
2159
|
parseComparison() {
|
|
1626
|
-
const left = this.
|
|
2160
|
+
const left = this.parseBitwise();
|
|
1627
2161
|
const t = this.peek();
|
|
1628
2162
|
let not = false;
|
|
1629
2163
|
if (t.type === 'keyword' && t.value === 'NOT') {
|
|
@@ -1686,7 +2220,31 @@ class Parser {
|
|
|
1686
2220
|
const isILike = !(t2.type === 'keyword' && t2.value === 'LIKE');
|
|
1687
2221
|
this.next();
|
|
1688
2222
|
const pattern = this.parseValue();
|
|
1689
|
-
|
|
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 };
|
|
1690
2248
|
}
|
|
1691
2249
|
|
|
1692
2250
|
// MySQL 中 RLIKE 是 REGEXP 的同义词
|
|
@@ -1696,7 +2254,14 @@ class Parser {
|
|
|
1696
2254
|
return { type: 'regexp', operand: left, pattern, not };
|
|
1697
2255
|
}
|
|
1698
2256
|
|
|
1699
|
-
|
|
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)) {
|
|
1700
2265
|
this.next();
|
|
1701
2266
|
// = ANY (SELECT ...) / > ALL (SELECT ...)
|
|
1702
2267
|
const anyAll = this.isWord('ANY') || this.isWord('ALL') || this.isWord('SOME');
|
|
@@ -1707,10 +2272,13 @@ class Parser {
|
|
|
1707
2272
|
this.expect('op', ')');
|
|
1708
2273
|
return { type: 'quantified', op: t2.value === '<>' ? '!=' : t2.value, operand: left, select: sub, quantifier, not };
|
|
1709
2274
|
}
|
|
1710
|
-
const right = this.
|
|
2275
|
+
const right = this.parseBitwise();
|
|
1711
2276
|
return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
|
|
1712
2277
|
}
|
|
1713
2278
|
|
|
2279
|
+
// 函数实参场景允许裸操作数(IF(age, ...) / SUM(IF(...)) 等)
|
|
2280
|
+
if (this.bareExprDepth > 0) return left;
|
|
2281
|
+
|
|
1714
2282
|
throw new Error(`Expected comparison operator, got '${t2.value}'`);
|
|
1715
2283
|
}
|
|
1716
2284
|
}
|
|
@@ -1807,16 +2375,34 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1807
2375
|
case '*': return l * r;
|
|
1808
2376
|
case '/': return r === 0 ? null : l / r;
|
|
1809
2377
|
case '%': return r === 0 ? null : l % r;
|
|
2378
|
+
// MySQL 整数除法 DIV(结果向零取整)
|
|
2379
|
+
case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
|
|
2380
|
+
// 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
|
|
2381
|
+
case '&': return (Number(l) | 0) & (Number(r) | 0);
|
|
2382
|
+
case '|': return (Number(l) | 0) | (Number(r) | 0);
|
|
2383
|
+
case '^': return (Number(l) | 0) ^ (Number(r) | 0);
|
|
2384
|
+
case '<<': return (Number(l) | 0) << (Number(r) | 0);
|
|
2385
|
+
case '>>': return (Number(l) | 0) >> (Number(r) | 0);
|
|
1810
2386
|
}
|
|
1811
2387
|
return null;
|
|
1812
2388
|
}
|
|
2389
|
+
case 'bitnot':
|
|
2390
|
+
return ~(Number(resolveOperand(operand.expr, row, ctx)) | 0);
|
|
1813
2391
|
case 'func':
|
|
1814
2392
|
return applyScalarFunction(operand, row, ctx);
|
|
1815
2393
|
case 'case':
|
|
1816
2394
|
return evaluateCaseVal(operand, row, ctx);
|
|
2395
|
+
// 布尔类节点出现在"取值"位置时(如 IF(status = 'paid', ...)、SUM(IF(a > b,1,0))),
|
|
2396
|
+
// 求值成 true/false 供上层按真值判断
|
|
2397
|
+
case 'compare':
|
|
2398
|
+
case 'and':
|
|
2399
|
+
case 'or':
|
|
2400
|
+
case 'xor':
|
|
2401
|
+
case 'not':
|
|
1817
2402
|
case 'in':
|
|
1818
2403
|
case 'isNull':
|
|
1819
2404
|
case 'isTruth':
|
|
2405
|
+
case 'exists':
|
|
1820
2406
|
case 'regexp':
|
|
1821
2407
|
case 'like':
|
|
1822
2408
|
case 'between':
|
|
@@ -1836,6 +2422,44 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1836
2422
|
const v = resolveOperand(operand.expr, row, ctx);
|
|
1837
2423
|
return applyCast(v, operand.dataType);
|
|
1838
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
|
+
}
|
|
1839
2463
|
case 'quantified': {
|
|
1840
2464
|
// = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
|
|
1841
2465
|
const v = resolveOperand(operand.operand, row, ctx);
|
|
@@ -1856,9 +2480,30 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1856
2480
|
}
|
|
1857
2481
|
}
|
|
1858
2482
|
|
|
2483
|
+
/** 解析 SQL 里的日期文本('YYYY-MM-DD' / 'YYYY-MM-DD HH:MM:SS' / Date) */
|
|
2484
|
+
function parseSqlDate(v) {
|
|
2485
|
+
if (v === null || v === undefined) return null;
|
|
2486
|
+
if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
|
|
2487
|
+
const s = String(v).trim().replace(' ', 'T');
|
|
2488
|
+
const d = new Date(s.length <= 10 ? s + 'T00:00:00' : s);
|
|
2489
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
2493
|
+
|
|
2494
|
+
/** 按输入形态回写日期:纯日期返回 YYYY-MM-DD,带时间返回 YYYY-MM-DD HH:MM:SS */
|
|
2495
|
+
function fmtSqlDate(dateOnly, d) {
|
|
2496
|
+
const ymd = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
|
2497
|
+
return dateOnly ? ymd : ymd + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
|
|
2498
|
+
}
|
|
2499
|
+
|
|
1859
2500
|
function applyScalarFunction(fnNode, row, ctx) {
|
|
1860
2501
|
const name = (fnNode.name || '').toUpperCase();
|
|
1861
|
-
const
|
|
2502
|
+
const rawArgs = fnNode.args || [];
|
|
2503
|
+
// INTERVAL n UNIT 是语法结构而非普通值,原样传给 DATE_ADD / DATE_SUB
|
|
2504
|
+
const args = rawArgs.map(a => (a && a.type === 'interval')
|
|
2505
|
+
? { __interval: true, value: a.value, unit: a.unit }
|
|
2506
|
+
: resolveOperand(a, row, ctx));
|
|
1862
2507
|
const session = ctx && ctx.session;
|
|
1863
2508
|
if (ctx && ctx.functions && Object.prototype.hasOwnProperty.call(ctx.functions, name)) {
|
|
1864
2509
|
return ctx.functions[name].apply(null, args);
|
|
@@ -1889,7 +2534,65 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1889
2534
|
case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
|
|
1890
2535
|
case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
|
|
1891
2536
|
case 'LENGTH': case 'CHAR_LENGTH': case 'CHARACTER_LENGTH': return args[0] == null ? null : String(args[0]).length;
|
|
1892
|
-
|
|
2537
|
+
// TRIM(s) / TRIM([LEADING|TRAILING|BOTH] chars FROM s) / TRIM(s, chars)
|
|
2538
|
+
case 'TRIM': {
|
|
2539
|
+
if (args[0] == null) return null;
|
|
2540
|
+
const s = String(args[0]);
|
|
2541
|
+
if (args.length === 1) return s.trim();
|
|
2542
|
+
const chars = args[1] == null ? '' : String(args[1]);
|
|
2543
|
+
if (!chars) return s.trim();
|
|
2544
|
+
const spec = args[2] ? String(args[2]).toUpperCase() : 'BOTH';
|
|
2545
|
+
const esc = chars.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2546
|
+
const head = new RegExp('^[' + esc + ']+');
|
|
2547
|
+
const tail = new RegExp('[' + esc + ']+$');
|
|
2548
|
+
if (spec === 'LEADING') return s.replace(head, '');
|
|
2549
|
+
if (spec === 'TRAILING') return s.replace(tail, '');
|
|
2550
|
+
return s.replace(head, '').replace(tail, '');
|
|
2551
|
+
}
|
|
2552
|
+
// EXTRACT(YEAR FROM d) —— 解析层会把 unit 作为第一个参数传入
|
|
2553
|
+
case 'EXTRACT': {
|
|
2554
|
+
const unit = String(args[0] == null ? '' : args[0]).toUpperCase();
|
|
2555
|
+
const d = parseSqlDate(args[1]);
|
|
2556
|
+
if (!d) return null;
|
|
2557
|
+
switch (unit) {
|
|
2558
|
+
case 'YEAR': return d.getFullYear();
|
|
2559
|
+
case 'MONTH': return d.getMonth() + 1;
|
|
2560
|
+
case 'DAY': return d.getDate();
|
|
2561
|
+
case 'HOUR': return d.getHours();
|
|
2562
|
+
case 'MINUTE': return d.getMinutes();
|
|
2563
|
+
case 'SECOND': return d.getSeconds();
|
|
2564
|
+
case 'DOW': return d.getDay(); // 0 = 周日
|
|
2565
|
+
case 'DAYOFWEEK': return d.getDay() + 1; // 1 = 周日(MySQL 语义)
|
|
2566
|
+
case 'WEEK': {
|
|
2567
|
+
const start = new Date(d.getFullYear(), 0, 1);
|
|
2568
|
+
return Math.floor((d - start) / 604800000) + 1;
|
|
2569
|
+
}
|
|
2570
|
+
default: return null;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
case 'DATE_ADD': case 'ADDDATE':
|
|
2574
|
+
case 'DATE_SUB': case 'SUBDATE': {
|
|
2575
|
+
const sign = (name === 'DATE_SUB' || name === 'SUBDATE') ? -1 : 1;
|
|
2576
|
+
const base = parseSqlDate(args[0]);
|
|
2577
|
+
if (!base) return null;
|
|
2578
|
+
const iv = args[1];
|
|
2579
|
+
if (!iv || !iv.__interval) return fmtSqlDate(String(args[0]).length <= 10, base);
|
|
2580
|
+
const isDateOnly = String(args[0]).trim().length <= 10;
|
|
2581
|
+
const d = new Date(base.getTime());
|
|
2582
|
+
const n = sign * Number(iv.value || 0);
|
|
2583
|
+
switch (String(iv.unit || '').toUpperCase()) {
|
|
2584
|
+
case 'DAY': case 'DAYS': d.setDate(d.getDate() + n); break;
|
|
2585
|
+
case 'WEEK': case 'WEEKS': d.setDate(d.getDate() + n * 7); break;
|
|
2586
|
+
case 'MONTH': case 'MONTHS': d.setMonth(d.getMonth() + n); break;
|
|
2587
|
+
case 'QUARTER': d.setMonth(d.getMonth() + n * 3); break;
|
|
2588
|
+
case 'YEAR': case 'YEARS': d.setFullYear(d.getFullYear() + n); break;
|
|
2589
|
+
case 'HOUR': case 'HOURS': d.setHours(d.getHours() + n); break;
|
|
2590
|
+
case 'MINUTE': case 'MINUTES': d.setMinutes(d.getMinutes() + n); break;
|
|
2591
|
+
case 'SECOND': case 'SECONDS': d.setSeconds(d.getSeconds() + n); break;
|
|
2592
|
+
default: return null;
|
|
2593
|
+
}
|
|
2594
|
+
return fmtSqlDate(isDateOnly, d);
|
|
2595
|
+
}
|
|
1893
2596
|
case 'LTRIM': return args[0] == null ? null : String(args[0]).replace(/^\s+/, '');
|
|
1894
2597
|
case 'RTRIM': return args[0] == null ? null : String(args[0]).replace(/\s+$/, '');
|
|
1895
2598
|
case 'ABS': return args[0] == null ? null : Math.abs(args[0]);
|
|
@@ -1975,13 +2678,76 @@ function seedRand(seed) {
|
|
|
1975
2678
|
}
|
|
1976
2679
|
|
|
1977
2680
|
// ci 默认 true:保持 LIKE 既有的大小写不敏感行为不变;ILIKE 同为不敏感。
|
|
1978
|
-
|
|
2681
|
+
// escape:可选转义符(ESCAPE 子句),默认反斜杠;转义符后跟 % _ 或转义符本身按字面量匹配。
|
|
2682
|
+
function likeMatch(value, pattern, ci = true, escape = '\\') {
|
|
1979
2683
|
if (typeof value !== 'string') return false;
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
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));
|
|
1985
2751
|
}
|
|
1986
2752
|
|
|
1987
2753
|
function extractEqualPushdown(expr, schema) {
|
|
@@ -2033,10 +2799,18 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2033
2799
|
switch (expr.type) {
|
|
2034
2800
|
case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
|
|
2035
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));
|
|
2036
2803
|
case 'not': return !evaluateExpr(expr.expr, row, ctx);
|
|
2037
2804
|
case 'compare': {
|
|
2038
2805
|
const l = resolveOperand(expr.left, row, ctx);
|
|
2039
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
|
+
}
|
|
2040
2814
|
// SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
|
|
2041
2815
|
if (l === null || r === null) return false;
|
|
2042
2816
|
// 布尔与数值互通(MySQL: 1 = TRUE、0 = FALSE)
|
|
@@ -2059,18 +2833,23 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2059
2833
|
}
|
|
2060
2834
|
case 'like': {
|
|
2061
2835
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2062
|
-
const r = likeMatch(v, expr.pattern, !!expr.ci);
|
|
2836
|
+
const r = likeMatch(v, expr.pattern, !!expr.ci, expr.escape);
|
|
2063
2837
|
return expr.not ? !r : r;
|
|
2064
2838
|
}
|
|
2065
2839
|
case 'quantified':
|
|
2066
2840
|
return !!resolveOperand(expr, row, ctx);
|
|
2841
|
+
case 'jsonContains':
|
|
2842
|
+
return !!resolveOperand(expr, row, ctx);
|
|
2067
2843
|
case 'regexp': {
|
|
2068
2844
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2069
2845
|
if (v === null || v === undefined) return false;
|
|
2070
|
-
|
|
2846
|
+
// REGEXP / RLIKE 历史上就是大小写不敏感的;`~` 敏感、`~*` 不敏感
|
|
2847
|
+
const re = new RegExp(String(expr.pattern), expr.ci === false ? '' : 'i');
|
|
2071
2848
|
const r = re.test(String(v));
|
|
2072
2849
|
return expr.not ? !r : r;
|
|
2073
2850
|
}
|
|
2851
|
+
case 'jsonKeyExists':
|
|
2852
|
+
return !!resolveOperand(expr, row, ctx);
|
|
2074
2853
|
case 'isTruth': {
|
|
2075
2854
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2076
2855
|
const isTrue = v === true || v === 1 || v === '1' || v === 'true' || v === 'TRUE' || v === 't' || (typeof v === 'number' && v !== 0);
|
|
@@ -2154,16 +2933,22 @@ function windowColumnValue(c, r) {
|
|
|
2154
2933
|
|
|
2155
2934
|
function scalarColumnValue(c, r, ctx) {
|
|
2156
2935
|
if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
|
|
2936
|
+
// 聚合列优先:GROUP_CONCAT / STDDEV 等同时带 scalar 与 aggregate
|
|
2937
|
+
if (c.aggregate) {
|
|
2938
|
+
return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column,
|
|
2939
|
+
c.distinct, c.separator !== undefined ? c.separator : (c.scalar && c.scalar.separator),
|
|
2940
|
+
c.orderBy || (c.scalar && c.scalar.orderBy));
|
|
2941
|
+
}
|
|
2157
2942
|
if (c.scalar) {
|
|
2158
2943
|
const s = c.scalar;
|
|
2159
2944
|
if (s && s.type === 'func' && ctx && ctx.ctxAggregates && Object.prototype.hasOwnProperty.call(ctx.ctxAggregates, String(s.name).toUpperCase())) {
|
|
2160
2945
|
const fn = String(s.name).toUpperCase();
|
|
2161
2946
|
const col = s.args && s.args[0];
|
|
2162
|
-
return ctx._aggValue(ctx.group, fn, col);
|
|
2947
|
+
return ctx._aggValue(ctx.group, fn, col, c.aggregate ? c.distinct : (c.scalar && c.scalar.distinct));
|
|
2163
2948
|
}
|
|
2164
2949
|
return resolveOperand(c.scalar, r, ctx);
|
|
2165
2950
|
}
|
|
2166
|
-
|
|
2951
|
+
|
|
2167
2952
|
if (c.literal !== undefined) return c.literal;
|
|
2168
2953
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r, ctx);
|
|
2169
2954
|
if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
|
|
@@ -2186,7 +2971,10 @@ function buildCreateTableSql(name, schema) {
|
|
|
2186
2971
|
const seg = ['`' + col + '`', sqlTypeName(def.type)];
|
|
2187
2972
|
if (def.autoIncrement) seg.push('AUTO_INCREMENT');
|
|
2188
2973
|
if (def.nullable === false) seg.push('NOT NULL');
|
|
2189
|
-
if (def.default !== undefined)
|
|
2974
|
+
if (def.default !== undefined) {
|
|
2975
|
+
// defaultExpr(CURRENT_TIMESTAMP 等)不加引号,普通字符串默认值才加
|
|
2976
|
+
seg.push('DEFAULT ' + (typeof def.default === 'string' && !def.defaultExpr ? "'" + def.default + "'" : def.default));
|
|
2977
|
+
}
|
|
2190
2978
|
return seg.join(' ');
|
|
2191
2979
|
});
|
|
2192
2980
|
const pks = Object.keys(schema).filter(k => schema[k].primaryKey);
|
|
@@ -2347,13 +3135,47 @@ class SQLExecutor {
|
|
|
2347
3135
|
dataRows = dataRows.map(stripDefault);
|
|
2348
3136
|
}
|
|
2349
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) : [];
|
|
2350
3139
|
let toInsert = dataRows;
|
|
2351
3140
|
let updated = 0;
|
|
2352
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
|
+
}
|
|
2353
3162
|
if (pkCols.length > 0) {
|
|
2354
3163
|
const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
|
|
2355
3164
|
const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
|
|
2356
|
-
|
|
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)));
|
|
2357
3179
|
if (explicit.length > 0) {
|
|
2358
3180
|
const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
2359
3181
|
const pkMap = new Map();
|
|
@@ -2361,15 +3183,31 @@ class SQLExecutor {
|
|
|
2361
3183
|
const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
|
|
2362
3184
|
if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
|
|
2363
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
|
+
});
|
|
2364
3197
|
const conflicts = [];
|
|
2365
3198
|
const fresh = [];
|
|
2366
3199
|
for (const d of dataRows) {
|
|
2367
|
-
|
|
2368
|
-
if (hasExplicitPk(d) && pkMap.has(
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
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
|
+
}
|
|
2372
3208
|
}
|
|
3209
|
+
if (existingId) conflicts.push({ d, existingId });
|
|
3210
|
+
else fresh.push(d);
|
|
2373
3211
|
}
|
|
2374
3212
|
const conflictAction = this._conflictAction(statement);
|
|
2375
3213
|
const conflictSets = this._conflictSets(statement);
|
|
@@ -2396,7 +3234,16 @@ class SQLExecutor {
|
|
|
2396
3234
|
for (const { d, existingId } of conflicts) {
|
|
2397
3235
|
const data = {};
|
|
2398
3236
|
if (conflictSets) {
|
|
2399
|
-
for (const [col, val] of conflictSets)
|
|
3237
|
+
for (const [col, val] of conflictSets) {
|
|
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
|
+
}
|
|
3246
|
+
}
|
|
2400
3247
|
} else {
|
|
2401
3248
|
for (const k of Object.keys(d)) if (!pkCols.includes(k)) data[k] = d[k];
|
|
2402
3249
|
}
|
|
@@ -2525,6 +3372,52 @@ class SQLExecutor {
|
|
|
2525
3372
|
delete this.engine._views[key];
|
|
2526
3373
|
return { ok: true, type: 'dropView', message: `View '${statement.name}' dropped`, affectedRows: 0, warnings: [] };
|
|
2527
3374
|
}
|
|
3375
|
+
case 'createIndex': {
|
|
3376
|
+
const tName = statement.table;
|
|
3377
|
+
if (typeof this.engine.hasTable === 'function' && !this.engine.hasTable(tName)) {
|
|
3378
|
+
throw new Error(`Table '${tName}' does not exist`);
|
|
3379
|
+
}
|
|
3380
|
+
let handled = false;
|
|
3381
|
+
if (typeof this.engine.createIndex === 'function') {
|
|
3382
|
+
await this.engine.createIndex(tName, statement.columns, { unique: statement.unique, name: statement.name });
|
|
3383
|
+
handled = true;
|
|
3384
|
+
} else if (typeof this.engine._ensureTable === 'function') {
|
|
3385
|
+
const tbl = this.engine._ensureTable(tName) || (this.engine._tables && this.engine._tables[tName]);
|
|
3386
|
+
if (tbl && typeof tbl.createIndex === 'function') {
|
|
3387
|
+
// Table.createIndex 目前是单字段接口,复合索引按逐列建
|
|
3388
|
+
for (const col of statement.columns) tbl.createIndex(col);
|
|
3389
|
+
handled = true;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
if (!handled) {
|
|
3393
|
+
throw new Error(
|
|
3394
|
+
`CREATE INDEX is not supported by this engine (no createIndex/_ensureTable on the engine object)`
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
return {
|
|
3398
|
+
ok: true, type: 'createIndex', table: tName, name: statement.name,
|
|
3399
|
+
affectedRows: 0, warnings: [],
|
|
3400
|
+
message: `Index '${statement.name}' created on ${tName}(${statement.columns.join(', ')})`,
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
case 'dropIndex': {
|
|
3404
|
+
const tName2 = statement.table;
|
|
3405
|
+
let handled = false;
|
|
3406
|
+
if (typeof this.engine.dropIndex === 'function') {
|
|
3407
|
+
await this.engine.dropIndex(tName2, statement.name);
|
|
3408
|
+
handled = true;
|
|
3409
|
+
} else if (tName2 && typeof this.engine._ensureTable === 'function') {
|
|
3410
|
+
const tbl = this.engine._ensureTable(tName2) || (this.engine._tables && this.engine._tables[tName2]);
|
|
3411
|
+
if (tbl && typeof tbl.dropIndex === 'function') { tbl.dropIndex(statement.name); handled = true; }
|
|
3412
|
+
}
|
|
3413
|
+
if (!handled) {
|
|
3414
|
+
if (statement.ifExists) {
|
|
3415
|
+
return { ok: true, type: 'dropIndex', affectedRows: 0, skipped: true, warnings: [], message: `Index '${statement.name}' did not exist` };
|
|
3416
|
+
}
|
|
3417
|
+
throw new Error(`DROP INDEX is not supported by this engine (no dropIndex on the engine/table object)`);
|
|
3418
|
+
}
|
|
3419
|
+
return { ok: true, type: 'dropIndex', table: tName2, name: statement.name, affectedRows: 0, warnings: [], message: `Index '${statement.name}' dropped` };
|
|
3420
|
+
}
|
|
2528
3421
|
case 'savepoint': {
|
|
2529
3422
|
// 引擎侧没有保存点/快照能力。这里只登记名字,绝不假装能回滚。
|
|
2530
3423
|
if (!this.engine._savepoints) this.engine._savepoints = new Set();
|
|
@@ -2582,11 +3475,27 @@ class SQLExecutor {
|
|
|
2582
3475
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
2583
3476
|
const ids = [];
|
|
2584
3477
|
const removedRows = [];
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
3478
|
+
let candidates = all.filter(row => !statement.where || evaluateExpr(statement.where, row, this.ctx));
|
|
3479
|
+
// DELETE ... ORDER BY ... LIMIT n
|
|
3480
|
+
if (statement.orderBy) {
|
|
3481
|
+
const cmp = (a, b) => {
|
|
3482
|
+
for (const o of statement.orderBy) {
|
|
3483
|
+
const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
|
|
3484
|
+
const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
|
|
3485
|
+
if (av === bv) continue;
|
|
3486
|
+
if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
|
|
3487
|
+
if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
|
|
3488
|
+
const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
|
|
3489
|
+
if (d !== 0) return o.dir === 'asc' ? d : -d;
|
|
3490
|
+
}
|
|
3491
|
+
return 0;
|
|
3492
|
+
};
|
|
3493
|
+
candidates = candidates.slice().sort(cmp);
|
|
3494
|
+
}
|
|
3495
|
+
if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
|
|
3496
|
+
for (const row of candidates) {
|
|
3497
|
+
const id = this._rowPkId(row, pkCols);
|
|
3498
|
+
if (id !== undefined) { ids.push(id); removedRows.push(row); }
|
|
2590
3499
|
}
|
|
2591
3500
|
if (ids.length > 0) {
|
|
2592
3501
|
if (this.engine.removeByIds) await this.engine.removeByIds(statement.table, ids);
|
|
@@ -3114,12 +4023,28 @@ class SQLExecutor {
|
|
|
3114
4023
|
return out;
|
|
3115
4024
|
}
|
|
3116
4025
|
|
|
3117
|
-
_aggValue(rows, fn, column) {
|
|
3118
|
-
if (fn === 'COUNT') return rows.length;
|
|
4026
|
+
_aggValue(rows, fn, column, distinct, separator, orderBy) {
|
|
3119
4027
|
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
3120
|
-
|
|
4028
|
+
// COUNT(*) / COUNT():参数为 star 或缺失,直接按行数算
|
|
4029
|
+
const noColumn = !op || op.type === 'star';
|
|
4030
|
+
let values = noColumn
|
|
3121
4031
|
? rows.map(r => 1)
|
|
3122
|
-
:
|
|
4032
|
+
: op && op.type === 'tuple'
|
|
4033
|
+
// COUNT(DISTINCT a, b):先把多列拼成元组再聚合
|
|
4034
|
+
? rows.map(r => JSON.stringify(op.items.map(it => resolveOperand(it, r, this.ctx))))
|
|
4035
|
+
.filter(v => v !== null && v !== undefined)
|
|
4036
|
+
: rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
|
|
4037
|
+
// COUNT(DISTINCT x) / SUM(DISTINCT x):先按值去重再聚合
|
|
4038
|
+
if (distinct) {
|
|
4039
|
+
const seen = new Set();
|
|
4040
|
+
values = values.filter(v => {
|
|
4041
|
+
const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
4042
|
+
if (seen.has(k)) return false;
|
|
4043
|
+
seen.add(k);
|
|
4044
|
+
return true;
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
4047
|
+
if (fn === 'COUNT') return values.length;
|
|
3123
4048
|
if (this.ctx && this.ctx.aggregates && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
|
|
3124
4049
|
const agg = this.ctx.aggregates[fn];
|
|
3125
4050
|
if (typeof agg === 'function') {
|
|
@@ -3135,6 +4060,54 @@ class SQLExecutor {
|
|
|
3135
4060
|
if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
3136
4061
|
if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
|
|
3137
4062
|
if (fn === 'MAX') return values.length ? Math.max(...values.map(v => Number(v))) : null;
|
|
4063
|
+
if (fn === 'FIRST') return values.length ? values[0] : null;
|
|
4064
|
+
if (fn === 'LAST') return values.length ? values[values.length - 1] : null;
|
|
4065
|
+
if (fn === 'GROUP_CONCAT') {
|
|
4066
|
+
let src = rows;
|
|
4067
|
+
// GROUP_CONCAT(expr ORDER BY col [DESC]):先按内部排序键排好行,再取值拼接
|
|
4068
|
+
if (orderBy && orderBy.length) {
|
|
4069
|
+
const cmp = (a, b) => {
|
|
4070
|
+
for (const o of orderBy) {
|
|
4071
|
+
const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
|
|
4072
|
+
const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
|
|
4073
|
+
if (av === bv) continue;
|
|
4074
|
+
if (av === null || av === undefined) return o.dir === 'asc' ? 1 : -1;
|
|
4075
|
+
if (bv === null || bv === undefined) return o.dir === 'asc' ? -1 : 1;
|
|
4076
|
+
const d = (typeof av === 'number' && typeof bv === 'number')
|
|
4077
|
+
? av - bv : String(av).localeCompare(String(bv));
|
|
4078
|
+
if (d !== 0) return o.dir === 'asc' ? d : -d;
|
|
4079
|
+
}
|
|
4080
|
+
return 0;
|
|
4081
|
+
};
|
|
4082
|
+
src = rows.slice().sort(cmp);
|
|
4083
|
+
}
|
|
4084
|
+
const parts = src.map(r => resolveOperand(op, r, this.ctx));
|
|
4085
|
+
let joined = parts.filter(v => v !== null && v !== undefined);
|
|
4086
|
+
if (distinct) {
|
|
4087
|
+
const seen = new Set();
|
|
4088
|
+
joined = joined.filter(v => {
|
|
4089
|
+
const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
4090
|
+
if (seen.has(k)) return false;
|
|
4091
|
+
seen.add(k);
|
|
4092
|
+
return true;
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
if (joined.length === 0) return null;
|
|
4096
|
+
const sep = (separator !== undefined && separator !== null) ? String(separator) : ',';
|
|
4097
|
+
return joined.map(v => (v === null || v === undefined ? '' : String(v))).join(sep);
|
|
4098
|
+
}
|
|
4099
|
+
// 标准差 / 方差族:_POP 为总体、_SAMP(及无后缀 VAR/STDDEV)为样本
|
|
4100
|
+
if (fn === 'STDDEV' || fn === 'STDDEV_POP' || fn === 'STDDEV_SAMP' ||
|
|
4101
|
+
fn === 'VARIANCE' || fn === 'VAR_POP' || fn === 'VAR_SAMP') {
|
|
4102
|
+
const nums = values.map(v => Number(v)).filter(v => Number.isFinite(v));
|
|
4103
|
+
if (nums.length === 0) return null;
|
|
4104
|
+
const pop = (fn === 'STDDEV_POP' || fn === 'VAR_POP');
|
|
4105
|
+
const mean = nums.reduce((s, v) => s + v, 0) / nums.length;
|
|
4106
|
+
const denom = pop ? nums.length : nums.length - 1; // 样本方差用 n-1
|
|
4107
|
+
if (denom <= 0) return null;
|
|
4108
|
+
const variance = nums.reduce((s, v) => s + (v - mean) * (v - mean), 0) / denom;
|
|
4109
|
+
return fn.indexOf('STDDEV') === 0 ? Math.sqrt(variance) : variance;
|
|
4110
|
+
}
|
|
3138
4111
|
return null;
|
|
3139
4112
|
}
|
|
3140
4113
|
|
|
@@ -3142,7 +4115,7 @@ class SQLExecutor {
|
|
|
3142
4115
|
if (!node || typeof node !== 'object') return;
|
|
3143
4116
|
if (node.type === 'aggregate') {
|
|
3144
4117
|
node.type = 'value';
|
|
3145
|
-
node.value = this._aggValue(group, node.fn, node.column);
|
|
4118
|
+
node.value = this._aggValue(group, node.fn, node.column, node.distinct, node.separator, node.orderBy);
|
|
3146
4119
|
delete node.fn;
|
|
3147
4120
|
delete node.column;
|
|
3148
4121
|
return;
|
|
@@ -3211,6 +4184,61 @@ class SQLExecutor {
|
|
|
3211
4184
|
return out;
|
|
3212
4185
|
}
|
|
3213
4186
|
|
|
4187
|
+
/**
|
|
4188
|
+
* 校验语句里引用的列是否真实存在。
|
|
4189
|
+
* 之前 `SELECT nosuchcol FROM t` 会静默返回 null 而不是报错 —— 调用方拿到的
|
|
4190
|
+
* 是"一列 null",往往被当成"数据为空",比直接报 no such column 更难排查。
|
|
4191
|
+
*
|
|
4192
|
+
* 只在能确定列名集合时校验(取样本行的键),空表不敢断言,宁可不报。
|
|
4193
|
+
* 不深入子查询节点:它们有自己的 FROM,列属于另一个作用域。
|
|
4194
|
+
*/
|
|
4195
|
+
_validateColumnRefs(statement, rows) {
|
|
4196
|
+
if (!statement || !Array.isArray(rows) || rows.length === 0) return;
|
|
4197
|
+
const sample = rows[0];
|
|
4198
|
+
if (!sample || typeof sample !== 'object') return;
|
|
4199
|
+
const known = new Set();
|
|
4200
|
+
for (const k of Object.keys(sample)) known.add(String(k).toLowerCase());
|
|
4201
|
+
// HAVING / ORDER BY 允许引用 SELECT 的输出别名(如 HAVING cnt >= 1),
|
|
4202
|
+
// 这些名字不在表里,必须一并视为已知,否则会误报 no such column
|
|
4203
|
+
if (Array.isArray(statement.columns)) {
|
|
4204
|
+
for (const c of statement.columns) {
|
|
4205
|
+
if (c && typeof c.alias === 'string' && c.alias) known.add(c.alias.toLowerCase());
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
const walk = (node) => {
|
|
4210
|
+
if (!node || typeof node !== 'object') return;
|
|
4211
|
+
if (Array.isArray(node)) { for (const n of node) walk(n); return; }
|
|
4212
|
+
// 子查询/嵌套 SELECT 有自己的表,跳过
|
|
4213
|
+
if (node.type === 'select' || node.type === 'subquery') return;
|
|
4214
|
+
if (node.type === 'column' && typeof node.name === 'string') {
|
|
4215
|
+
const n = node.name.toLowerCase();
|
|
4216
|
+
const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
|
|
4217
|
+
if (!known.has(n) && !known.has(base)) {
|
|
4218
|
+
throw new Error(`no such column: ${node.name}`);
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
for (const k of Object.keys(node)) {
|
|
4222
|
+
if (k === 'select' || k === 'subquery') continue;
|
|
4223
|
+
walk(node[k]);
|
|
4224
|
+
}
|
|
4225
|
+
};
|
|
4226
|
+
|
|
4227
|
+
walk(statement.columns);
|
|
4228
|
+
walk(statement.where);
|
|
4229
|
+
walk(statement.having);
|
|
4230
|
+
walk(statement.orderBy);
|
|
4231
|
+
if (Array.isArray(statement.groupBy)) {
|
|
4232
|
+
// groupBy 是列名字符串数组
|
|
4233
|
+
for (const g of statement.groupBy) {
|
|
4234
|
+
if (typeof g !== 'string') continue;
|
|
4235
|
+
const n = g.toLowerCase();
|
|
4236
|
+
const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
|
|
4237
|
+
if (!known.has(n) && !known.has(base)) throw new Error(`no such column: ${g}`);
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
|
|
3214
4242
|
/** 收集子树里所有列名(含限定形式 alias.col) */
|
|
3215
4243
|
_collectColumnNames(node, out = []) {
|
|
3216
4244
|
if (!node || typeof node !== 'object') return out;
|
|
@@ -3446,7 +4474,7 @@ class SQLExecutor {
|
|
|
3446
4474
|
case 'MAX': {
|
|
3447
4475
|
const f = frameIdx(pos);
|
|
3448
4476
|
const slice = ordered.slice(f.start, f.end + 1).map(i => rows[i]);
|
|
3449
|
-
v = this._aggValue(slice, w.fn, argNode);
|
|
4477
|
+
v = this._aggValue(slice, w.fn, argNode, !!(w.spec && w.spec.distinct), w.spec && w.spec.separator);
|
|
3450
4478
|
break;
|
|
3451
4479
|
}
|
|
3452
4480
|
default: v = null;
|
|
@@ -3639,6 +4667,7 @@ class SQLExecutor {
|
|
|
3639
4667
|
}
|
|
3640
4668
|
|
|
3641
4669
|
let rows = all;
|
|
4670
|
+
this._validateColumnRefs(statement, rows);
|
|
3642
4671
|
if (statement.where) {
|
|
3643
4672
|
// 相关子查询:必须逐行重新求值,否则外层引用拿不到当前行的值
|
|
3644
4673
|
const outerAliases = this._outerAliases(statement);
|
|
@@ -3681,7 +4710,7 @@ class SQLExecutor {
|
|
|
3681
4710
|
for (const c of statement.columns) {
|
|
3682
4711
|
if (c.aggregate) {
|
|
3683
4712
|
const group = r._group || [r];
|
|
3684
|
-
const v = this._aggValue(group, c.aggregate || 'COUNT', c.column);
|
|
4713
|
+
const v = this._aggValue(group, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
3685
4714
|
ctx['__agg_' + (c.alias || c.column || 'COUNT(*)')] = v;
|
|
3686
4715
|
if (c.column) ctx[c.column] = v;
|
|
3687
4716
|
if (c.alias) ctx[c.alias] = v;
|
|
@@ -3702,7 +4731,7 @@ class SQLExecutor {
|
|
|
3702
4731
|
const key = JSON.stringify(statement.columns.map(c => {
|
|
3703
4732
|
if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
|
|
3704
4733
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
3705
|
-
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column);
|
|
4734
|
+
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
3706
4735
|
return resolveOperand({ type: 'column', name: c.expr }, r, this.ctx);
|
|
3707
4736
|
}));
|
|
3708
4737
|
if (seen.has(key)) return false;
|
|
@@ -3717,7 +4746,7 @@ class SQLExecutor {
|
|
|
3717
4746
|
if (!statement.groupBy && (statement.columns.some(c => c.aggregate && !c.window) || hasCustomAgg)) {
|
|
3718
4747
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
3719
4748
|
const valueOf = (c) => {
|
|
3720
|
-
if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column);
|
|
4749
|
+
if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
3721
4750
|
if (c.scalar && c.scalar.type === 'func' && hasCustomAgg) {
|
|
3722
4751
|
const fn = String(c.scalar.name).toUpperCase();
|
|
3723
4752
|
if (Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
|
|
@@ -3801,53 +4830,88 @@ class SQLExecutor {
|
|
|
3801
4830
|
}
|
|
3802
4831
|
}
|
|
3803
4832
|
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
while (i < sql.length) {
|
|
4833
|
+
/** 跳过一个引号串(含转义/反引号成对),返回闭合引号之后的下标 */
|
|
4834
|
+
function skipQuoted(sql, i, q) {
|
|
4835
|
+
const n = sql.length;
|
|
4836
|
+
i++; // 跳过起始引号
|
|
4837
|
+
while (i < n) {
|
|
3810
4838
|
const c = sql[i];
|
|
3811
|
-
if (
|
|
3812
|
-
|
|
3813
|
-
if (
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
if (c === '`') inStr = null;
|
|
3817
|
-
i++;
|
|
3818
|
-
continue;
|
|
4839
|
+
if (q === '`') {
|
|
4840
|
+
// 反引号标识符内部反斜杠不是转义符;两个连续反引号表示一个字面反引号。
|
|
4841
|
+
if (c === '`') {
|
|
4842
|
+
if (sql[i + 1] === '`') { i += 2; continue; }
|
|
4843
|
+
return i + 1;
|
|
3819
4844
|
}
|
|
3820
|
-
if (c === '\\' && i + 1 < sql.length) { current += sql[i + 1]; i += 2; continue; }
|
|
3821
|
-
if (c === inStr) inStr = null;
|
|
3822
4845
|
i++;
|
|
3823
4846
|
continue;
|
|
3824
4847
|
}
|
|
3825
|
-
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; }
|
|
3826
4884
|
if (c === '-' && sql[i + 1] === '-') {
|
|
3827
|
-
|
|
4885
|
+
// 行注释:丢弃注释文本本身(与原实现一致),保留其后的换行
|
|
4886
|
+
take(i);
|
|
4887
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
4888
|
+
segStart = i;
|
|
3828
4889
|
continue;
|
|
3829
4890
|
}
|
|
3830
|
-
if (c === '#') {
|
|
3831
|
-
|
|
4891
|
+
if (c === '#' && sql[i + 1] !== '>') {
|
|
4892
|
+
take(i);
|
|
4893
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
4894
|
+
segStart = i;
|
|
3832
4895
|
continue;
|
|
3833
4896
|
}
|
|
3834
4897
|
if (c === '/' && sql[i + 1] === '*') {
|
|
3835
|
-
|
|
4898
|
+
// 块注释:原实现会保留 `/* */` 原文,这里通过切片一并保留
|
|
3836
4899
|
i += 2;
|
|
3837
|
-
while (i + 1 <
|
|
3838
|
-
|
|
4900
|
+
while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
|
|
4901
|
+
i = i + 1 < n ? i + 2 : n;
|
|
3839
4902
|
continue;
|
|
3840
4903
|
}
|
|
3841
4904
|
if (c === ';') {
|
|
3842
|
-
|
|
3843
|
-
|
|
4905
|
+
take(i);
|
|
4906
|
+
flush();
|
|
3844
4907
|
i++;
|
|
4908
|
+
segStart = i;
|
|
3845
4909
|
continue;
|
|
3846
4910
|
}
|
|
3847
|
-
current += c;
|
|
3848
4911
|
i++;
|
|
3849
4912
|
}
|
|
3850
|
-
|
|
4913
|
+
take(n);
|
|
4914
|
+
flush();
|
|
3851
4915
|
return statements;
|
|
3852
4916
|
}
|
|
3853
4917
|
|
|
@@ -3874,7 +4938,7 @@ function hasComments(sql) {
|
|
|
3874
4938
|
if (c === "'" || c === '"') { inStr = c; i++; continue; }
|
|
3875
4939
|
if (c === '`') { i++; while (i < sql.length && sql[i] !== '`') i++; i++; continue; }
|
|
3876
4940
|
if (c === '-' && sql[i + 1] === '-') return true;
|
|
3877
|
-
if (c === '#') return true;
|
|
4941
|
+
if (c === '#' && sql[i + 1] !== '>') return true; // '#>' / '#>>' 是 JSON 操作符,不是注释
|
|
3878
4942
|
if (c === '/' && sql[i + 1] === '*') return true;
|
|
3879
4943
|
i++;
|
|
3880
4944
|
}
|
|
@@ -3923,16 +4987,34 @@ function applyParams(sql, values) {
|
|
|
3923
4987
|
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
3924
4988
|
hasNamed = true;
|
|
3925
4989
|
}
|
|
3926
|
-
|
|
4990
|
+
const n = sql.length;
|
|
4991
|
+
while (i < n) {
|
|
3927
4992
|
const c = sql[i];
|
|
3928
4993
|
if (inStr) {
|
|
3929
4994
|
out += c;
|
|
3930
|
-
if (c === '\\' && i + 1 <
|
|
4995
|
+
if (c === '\\' && i + 1 < n) { out += sql[i + 1]; i += 2; continue; }
|
|
3931
4996
|
if (c === inStr) inStr = null;
|
|
3932
4997
|
i++;
|
|
3933
4998
|
continue;
|
|
3934
4999
|
}
|
|
3935
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
|
+
}
|
|
3936
5018
|
if (c === '?' && sql[i + 1] === '?') {
|
|
3937
5019
|
if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
3938
5020
|
out += escapeId(hasNamed ? args['@@'] : args[idx++]);
|
|
@@ -3965,6 +5047,24 @@ function applyParams(sql, values) {
|
|
|
3965
5047
|
i++;
|
|
3966
5048
|
continue;
|
|
3967
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
|
+
}
|
|
3968
5068
|
if ((c === ':' || c === '@' || c === '$') && i + 1 < sql.length && /[A-Za-z_]/.test(sql[i + 1])) {
|
|
3969
5069
|
// 命名占位符 :name @name $name
|
|
3970
5070
|
let name = '';
|
|
@@ -3986,6 +5086,44 @@ function applyParams(sql, values) {
|
|
|
3986
5086
|
return out;
|
|
3987
5087
|
}
|
|
3988
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
|
+
|
|
3989
5127
|
async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
3990
5128
|
// 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
|
|
3991
5129
|
// 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
|
|
@@ -4009,14 +5147,19 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
4009
5147
|
if (Array.isArray(paramsOrOpts)) {
|
|
4010
5148
|
sql = applyParams(sql, paramsOrOpts);
|
|
4011
5149
|
} else if (paramsOrOpts && typeof paramsOrOpts === 'object') {
|
|
4012
|
-
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);
|
|
4013
5156
|
}
|
|
4014
5157
|
if (opts.safety !== false) {
|
|
4015
5158
|
if (!opts.allowComments && hasComments(sql)) {
|
|
4016
5159
|
throw new Error('SQL comments are disabled for security (--, #, /* */)');
|
|
4017
5160
|
}
|
|
4018
5161
|
}
|
|
4019
|
-
let statements = splitStatements(sql);
|
|
5162
|
+
let statements = splitStatements(sql, { maxLength: opts.maxSqlLength });
|
|
4020
5163
|
if (opts.maxStatements != null && statements.length > opts.maxStatements) {
|
|
4021
5164
|
throw new Error(`too many statements (${statements.length} > ${opts.maxStatements})`);
|
|
4022
5165
|
}
|
|
@@ -4035,7 +5178,14 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
4035
5178
|
const results = [];
|
|
4036
5179
|
for (const stmtSql of statements) {
|
|
4037
5180
|
const stmt = parseSQL(stmtSql);
|
|
4038
|
-
|
|
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);
|
|
4039
5189
|
}
|
|
4040
5190
|
return results.length === 1 ? results[0] : results;
|
|
4041
5191
|
}
|