jsql-neo 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -58
- 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 +1018 -84
- 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 +353 -0
package/lib/sql.js
CHANGED
|
@@ -79,6 +79,11 @@ function tokenize(sql) {
|
|
|
79
79
|
while (i < n && sql[i] !== '\n') i++;
|
|
80
80
|
continue;
|
|
81
81
|
}
|
|
82
|
+
// PG 的 JSON 路径操作符 #> / #>> 尚未实现。
|
|
83
|
+
// 若不在这里拦截,'#' 会被当成行注释吃掉,导致 SQL 后半段被静默丢弃。
|
|
84
|
+
if (c === '#' && sql[i + 1] === '>') {
|
|
85
|
+
throw new Error("JSON 路径操作符 '#>' / '#>>' 暂不支持,请改用 '->' / '->>'");
|
|
86
|
+
}
|
|
82
87
|
if (c === '#' || (c === '/' && sql[i + 1] === '*')) {
|
|
83
88
|
if (c === '#') { while (i < n && sql[i] !== '\n') i++; continue; }
|
|
84
89
|
i += 2;
|
|
@@ -95,7 +100,10 @@ function tokenize(sql) {
|
|
|
95
100
|
if (sql[j] === '\\' && j + 1 < n) {
|
|
96
101
|
const esc = sql[j + 1];
|
|
97
102
|
const map = { n: '\n', t: '\t', r: '\r', '0': '\0', "'": "'", '"': '"', '\\': '\\', b: '\b', Z: '\x1a', a: '\a' };
|
|
98
|
-
|
|
103
|
+
// MySQL 语义:\% 与 \_ 保留反斜杠(供 LIKE 匹配字面量通配符),其余未知转义忽略反斜杠
|
|
104
|
+
if (map[esc] !== undefined) str += map[esc];
|
|
105
|
+
else if (esc === '%' || esc === '_') str += '\\' + esc;
|
|
106
|
+
else str += esc;
|
|
99
107
|
j += 2;
|
|
100
108
|
} else if (sql[j] === quote) {
|
|
101
109
|
break;
|
|
@@ -150,6 +158,12 @@ function tokenize(sql) {
|
|
|
150
158
|
continue;
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
// PG 的 JSON 包含操作符 @> 尚未实现。
|
|
162
|
+
// 若不拦截,'@' 会被 sysvar 分支静默吞掉、'>' 退化成普通大于号,产生错误结果。
|
|
163
|
+
if (c === '@' && sql[i + 1] === '>') {
|
|
164
|
+
throw new Error("JSON 包含操作符 '@>' 暂不支持");
|
|
165
|
+
}
|
|
166
|
+
|
|
153
167
|
if (c === '@') {
|
|
154
168
|
let j = i;
|
|
155
169
|
while (j < n && sql[j] === '@') j++;
|
|
@@ -177,14 +191,29 @@ function tokenize(sql) {
|
|
|
177
191
|
continue;
|
|
178
192
|
}
|
|
179
193
|
|
|
194
|
+
const three = sql.slice(i, i + 3);
|
|
195
|
+
if (sql.slice(i, i + 2) === '<@') {
|
|
196
|
+
throw new Error("JSON 包含操作符 '<@' 暂不支持");
|
|
197
|
+
}
|
|
198
|
+
if (three === '->>' || three === '<=>') {
|
|
199
|
+
tokens.push(new SQLToken('op', three, i));
|
|
200
|
+
i += 3;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
180
204
|
const two = sql.slice(i, i + 2);
|
|
181
|
-
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '=='
|
|
205
|
+
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
|
|
206
|
+
two === '<<' || two === '>>' || two === '->' || two === '::' ||
|
|
207
|
+
two === '&&' || two === '||') {
|
|
182
208
|
tokens.push(new SQLToken('op', two, i));
|
|
183
209
|
i += 2;
|
|
184
210
|
continue;
|
|
185
211
|
}
|
|
186
212
|
|
|
187
|
-
|
|
213
|
+
// 注意:% & | ^ ~ 这些运算符必须在词法层产出 token。
|
|
214
|
+
// 之前 parseTerm() 里写了 '%' 分支但字符集里没有 '%',
|
|
215
|
+
// 导致 SELECT 10 % 3 直接报 "Unexpected character '%'",那段是死代码。
|
|
216
|
+
if ('=<>+-*/(),.;%&|^~'.includes(c)) {
|
|
188
217
|
tokens.push(new SQLToken('op', c, i));
|
|
189
218
|
i++;
|
|
190
219
|
continue;
|
|
@@ -197,11 +226,23 @@ function tokenize(sql) {
|
|
|
197
226
|
return tokens;
|
|
198
227
|
}
|
|
199
228
|
|
|
229
|
+
// 聚合函数集合:这些必须走 aggregate 节点,否则会被当成逐行标量函数(返回 null 且不折叠行)
|
|
230
|
+
const AGG_FUNCS = new Set(['SUM', 'AVG', 'MIN', 'MAX', 'COUNT',
|
|
231
|
+
'GROUP_CONCAT', 'STDDEV', 'STDDEV_POP', 'STDDEV_SAMP',
|
|
232
|
+
'VARIANCE', 'VAR_POP', 'VAR_SAMP', 'FIRST', 'LAST']);
|
|
233
|
+
|
|
234
|
+
// 这些词会跟在表名/子查询之后,不能被误当成表别名。
|
|
235
|
+
// 新增任何"跟在 FROM 之后"的子句时,都要往这里加词。
|
|
236
|
+
const NOT_ALIAS_WORDS = ['JOIN', 'LEFT', 'RIGHT', 'FULL', 'WHERE', 'GROUP', 'ORDER', 'LIMIT',
|
|
237
|
+
'ON', 'HAVING', 'UNION', 'INNER', 'CROSS', 'OUTER', 'INTERSECT', 'EXCEPT',
|
|
238
|
+
'FETCH', 'RETURNING', 'WINDOW', 'OFFSET', 'FOR'];
|
|
239
|
+
|
|
200
240
|
class Parser {
|
|
201
241
|
constructor(tokens) {
|
|
202
242
|
this.tokens = tokens;
|
|
203
243
|
this.pos = 0;
|
|
204
244
|
this.paramCount = 0; // 顺序占位符 ? 的序号(从 0 开始)
|
|
245
|
+
this.subquerySeq = 0; // FROM 子查询无别名时自动生成 __subN
|
|
205
246
|
}
|
|
206
247
|
|
|
207
248
|
peek(offset = 0) { return this.tokens[this.pos + offset]; }
|
|
@@ -271,11 +312,13 @@ class Parser {
|
|
|
271
312
|
if (this.isKeyword('TABLE', 1)) return this.parseCreateTable();
|
|
272
313
|
if (this.isKeyword('DATABASE', 1)) return this.parseCreateDatabase();
|
|
273
314
|
if (this.isWord('VIEW', 1) || this.isWord('OR', 1)) return this.parseCreateView();
|
|
315
|
+
if (this.isKeyword('INDEX', 1) || this.isKeyword('UNIQUE', 1)) return this.parseCreateIndex();
|
|
274
316
|
throw new Error('Unsupported CREATE statement');
|
|
275
317
|
case 'DROP':
|
|
276
318
|
if (this.isKeyword('TABLE', 1)) return this.parseDropTable();
|
|
277
319
|
if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
|
|
278
320
|
if (this.isWord('VIEW', 1)) return this.parseDropView();
|
|
321
|
+
if (this.isKeyword('INDEX', 1)) return this.parseDropIndex();
|
|
279
322
|
throw new Error('Unsupported DROP statement');
|
|
280
323
|
case 'INSERT': return this.parseInsert();
|
|
281
324
|
case 'REPLACE': return this.parseReplace();
|
|
@@ -351,6 +394,45 @@ class Parser {
|
|
|
351
394
|
return { type: 'createView', name, columns, select, orReplace };
|
|
352
395
|
}
|
|
353
396
|
|
|
397
|
+
/**
|
|
398
|
+
* CREATE [UNIQUE] INDEX name ON table (col [, col]...)
|
|
399
|
+
* 归一成 { type:'createIndex', name, table, columns, unique }
|
|
400
|
+
*/
|
|
401
|
+
parseCreateIndex() {
|
|
402
|
+
this.expectKeyword('CREATE');
|
|
403
|
+
let unique = false;
|
|
404
|
+
if (this.isKeyword('UNIQUE')) { this.next(); unique = true; }
|
|
405
|
+
if (!this.isKeyword('INDEX')) throw new Error(`Expected INDEX, got '${this.peek().value}'`);
|
|
406
|
+
this.next();
|
|
407
|
+
const name = this.parseTableName();
|
|
408
|
+
if (!this.isKeyword('ON')) throw new Error(`Expected ON in CREATE INDEX, got '${this.peek().value}'`);
|
|
409
|
+
this.next();
|
|
410
|
+
const table = this.parseTableName();
|
|
411
|
+
this.expect('op', '(');
|
|
412
|
+
const columns = [];
|
|
413
|
+
for (;;) {
|
|
414
|
+
columns.push(this.parseColumnRef());
|
|
415
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
this.expect('op', ')');
|
|
419
|
+
this.optionalTailSemicolon();
|
|
420
|
+
return { type: 'createIndex', name, table, columns, unique };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** DROP INDEX name ON table */
|
|
424
|
+
parseDropIndex() {
|
|
425
|
+
this.expectKeyword('DROP');
|
|
426
|
+
this.expectKeyword('INDEX');
|
|
427
|
+
let ifExists = false;
|
|
428
|
+
if (this.isKeyword('IF')) { this.expectKeyword('IF'); this.expectKeyword('EXISTS'); ifExists = true; }
|
|
429
|
+
const name = this.parseTableName();
|
|
430
|
+
let table = null;
|
|
431
|
+
if (this.isKeyword('ON')) { this.next(); table = this.parseTableName(); }
|
|
432
|
+
this.optionalTailSemicolon();
|
|
433
|
+
return { type: 'dropIndex', name, table, ifExists };
|
|
434
|
+
}
|
|
435
|
+
|
|
354
436
|
parseDropView() {
|
|
355
437
|
this.expectKeyword('DROP');
|
|
356
438
|
if (!this.isWord('VIEW')) throw new Error(`Expected VIEW, got '${this.peek().value}'`);
|
|
@@ -413,6 +495,9 @@ class Parser {
|
|
|
413
495
|
hasPk = true;
|
|
414
496
|
} else {
|
|
415
497
|
this.expectKeyword('UNIQUE');
|
|
498
|
+
// UNIQUE [KEY|INDEX] [name] (cols...)
|
|
499
|
+
if (this.isKeyword('KEY') || this.isKeyword('INDEX')) this.next();
|
|
500
|
+
if (this.peek().type === 'ident') this.next();
|
|
416
501
|
this.expect('op', '(');
|
|
417
502
|
const uCols = [this.parseTableName()];
|
|
418
503
|
while (this.peek().type === 'op' && this.peek().value === ',') {
|
|
@@ -426,7 +511,12 @@ class Parser {
|
|
|
426
511
|
}
|
|
427
512
|
} else if (t.type === 'keyword' && t.value === 'CONSTRAINT') {
|
|
428
513
|
this.expectKeyword('CONSTRAINT');
|
|
429
|
-
this.next();
|
|
514
|
+
if (this.peek().type === 'ident') this.next(); // 约束名
|
|
515
|
+
this._parseTableConstraint(schema);
|
|
516
|
+
} else if (this.isWord('FOREIGN') || this.isWord('CHECK') ||
|
|
517
|
+
t.type === 'keyword' && (t.value === 'KEY' || t.value === 'INDEX')) {
|
|
518
|
+
// 表级约束:FOREIGN KEY ... REFERENCES ... / CHECK (...) / KEY name (cols)
|
|
519
|
+
this._parseTableConstraint(schema);
|
|
430
520
|
} else if (t.type === 'eof' || (t.type === 'op' && t.value === ')')) {
|
|
431
521
|
break;
|
|
432
522
|
} else {
|
|
@@ -458,19 +548,27 @@ class Parser {
|
|
|
458
548
|
parseColumnDef() {
|
|
459
549
|
const def = {};
|
|
460
550
|
const typeTok = this.next();
|
|
461
|
-
|
|
462
|
-
|
|
551
|
+
const typeRaw = String(typeTok.value);
|
|
552
|
+
// 这些类型不在 KEYWORDS 里(避免影响同名标识符),按上下文识别
|
|
553
|
+
const IDENT_TYPES = /^(BIG)?SERIAL$|^JSONB$|^TIMESTAMPTZ$|^BYTEA$|^UUID$/i;
|
|
554
|
+
const isIdentType = IDENT_TYPES.test(typeRaw);
|
|
555
|
+
if (typeTok.type !== 'keyword' && !isIdentType) throw new Error(`Expected column type, got '${typeTok.value}'`);
|
|
556
|
+
const type = typeRaw.toLowerCase();
|
|
463
557
|
const typeMap = {
|
|
464
558
|
integer: 'integer', int: 'integer', bigint: 'integer', tinyint: 'integer', smallint: 'integer',
|
|
465
559
|
string: 'string', text: 'string', varchar: 'string', char: 'string',
|
|
466
560
|
float: 'number', double: 'number', real: 'number', numeric: 'number', decimal: 'number',
|
|
467
561
|
boolean: 'boolean', bool: 'boolean',
|
|
468
|
-
date: 'date', datetime: 'datetime', timestamp: 'timestamp',
|
|
469
|
-
any: 'any', object: 'object', json: 'object', array: 'array'
|
|
562
|
+
date: 'date', datetime: 'datetime', timestamp: 'timestamp', timestamptz: 'timestamp',
|
|
563
|
+
any: 'any', object: 'object', json: 'object', jsonb: 'object', array: 'array',
|
|
564
|
+
bytea: 'string', uuid: 'string',
|
|
565
|
+
// PG 自增序列类型:等价于整型 + 自增 + NOT NULL
|
|
566
|
+
serial: 'integer', bigserial: 'integer'
|
|
470
567
|
};
|
|
471
568
|
const mapped = typeMap[type];
|
|
472
569
|
if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
|
|
473
570
|
def.type = mapped;
|
|
571
|
+
if (/^(BIG)?SERIAL$/i.test(typeRaw)) { def.autoIncrement = true; def.required = true; }
|
|
474
572
|
|
|
475
573
|
// 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
|
|
476
574
|
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
@@ -517,7 +615,29 @@ class Parser {
|
|
|
517
615
|
case 'NULL':
|
|
518
616
|
this.next(); break;
|
|
519
617
|
case 'DEFAULT':
|
|
520
|
-
this.next();
|
|
618
|
+
this.next();
|
|
619
|
+
// 默认值可以是字面量,也可以是函数/关键字形式(CURRENT_TIMESTAMP / NOW() 等)。
|
|
620
|
+
// 函数形式存成字符串 + defaultExpr 标记,与 JS 侧 `default: 'CURRENT_TIMESTAMP'` 约定一致。
|
|
621
|
+
if (this.peek().type === 'ident' ||
|
|
622
|
+
(this.peek().type === 'keyword' && !['NULL', 'TRUE', 'FALSE'].includes(this.peek().value))) {
|
|
623
|
+
const fnTok = this.next();
|
|
624
|
+
let name = String(fnTok.value);
|
|
625
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
626
|
+
this.next();
|
|
627
|
+
this.expect('op', ')');
|
|
628
|
+
name += '()';
|
|
629
|
+
}
|
|
630
|
+
def.default = name;
|
|
631
|
+
def.defaultExpr = true;
|
|
632
|
+
} else {
|
|
633
|
+
def.default = this.parseValue();
|
|
634
|
+
// 允许字面量后带 :: 类型转换:DEFAULT '{}'::jsonb
|
|
635
|
+
if (this.peek().type === 'op' && this.peek().value === '::') {
|
|
636
|
+
this.next();
|
|
637
|
+
if (this.peek().type === 'keyword' || this.peek().type === 'ident') this.next();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
break;
|
|
521
641
|
case 'COLLATE':
|
|
522
642
|
this.next(); if (this.peek().type !== 'op' && this.peek().type !== 'eof') this.next(); break;
|
|
523
643
|
case 'CHARACTER':
|
|
@@ -553,6 +673,68 @@ class Parser {
|
|
|
553
673
|
return stmt;
|
|
554
674
|
}
|
|
555
675
|
|
|
676
|
+
/** 跳过一对括号(含嵌套),用于表级约束里的列清单 */
|
|
677
|
+
_skipParens() {
|
|
678
|
+
this.expect('op', '(');
|
|
679
|
+
let depth = 1;
|
|
680
|
+
while (depth > 0 && this.peek().type !== 'eof') {
|
|
681
|
+
const tk = this.next();
|
|
682
|
+
if (tk.type === 'op' && tk.value === '(') depth++;
|
|
683
|
+
else if (tk.type === 'op' && tk.value === ')') depth--;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* 解析表级约束(FOREIGN KEY / CHECK / KEY / INDEX / PRIMARY KEY / UNIQUE)。
|
|
689
|
+
* 目前只做语法层面消费并在可能时补充列属性,不做外键实际校验。
|
|
690
|
+
*/
|
|
691
|
+
_parseTableConstraint(schema) {
|
|
692
|
+
if (this.isWord('FOREIGN')) {
|
|
693
|
+
this.next();
|
|
694
|
+
if (!this.isKeyword('KEY')) throw new Error(`Expected KEY after FOREIGN, got '${this.peek().value}'`);
|
|
695
|
+
this.next();
|
|
696
|
+
if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
|
|
697
|
+
if (this.isWord('REFERENCES')) {
|
|
698
|
+
this.next();
|
|
699
|
+
this.parseTableName();
|
|
700
|
+
if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
|
|
701
|
+
}
|
|
702
|
+
// ON DELETE / ON UPDATE <action>
|
|
703
|
+
while (this.isKeyword('ON')) {
|
|
704
|
+
this.next();
|
|
705
|
+
this.next(); // DELETE | UPDATE
|
|
706
|
+
const act = String(this.next().value).toUpperCase();
|
|
707
|
+
if (act === 'SET' || act === 'NO') this.next(); // SET NULL / NO ACTION
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (this.isWord('CHECK')) {
|
|
712
|
+
this.next();
|
|
713
|
+
if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (this.isKeyword('PRIMARY')) {
|
|
717
|
+
this.expectKeyword('PRIMARY');
|
|
718
|
+
if (this.isKeyword('KEY')) this.next();
|
|
719
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
720
|
+
this.next();
|
|
721
|
+
const cols = [this.parseTableName()];
|
|
722
|
+
while (this.peek().type === 'op' && this.peek().value === ',') { this.next(); cols.push(this.parseTableName()); }
|
|
723
|
+
this.expect('op', ')');
|
|
724
|
+
for (const c of cols) if (schema[c]) schema[c].primaryKey = true;
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (this.isKeyword('UNIQUE')) {
|
|
729
|
+
this.next();
|
|
730
|
+
}
|
|
731
|
+
if (this.isKeyword('KEY') || this.isKeyword('INDEX')) {
|
|
732
|
+
this.next();
|
|
733
|
+
if (this.peek().type === 'ident') this.next(); // 索引名
|
|
734
|
+
if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
556
738
|
parseInsertBody() {
|
|
557
739
|
const name = this.parseTableName();
|
|
558
740
|
let columns = null;
|
|
@@ -628,7 +810,7 @@ class Parser {
|
|
|
628
810
|
for (;;) {
|
|
629
811
|
const col = this.parseColumnRef();
|
|
630
812
|
this.expect('op', '=');
|
|
631
|
-
const val = this.
|
|
813
|
+
const val = this.parseConflictValue();
|
|
632
814
|
sets.push([col, val]);
|
|
633
815
|
if (this.peek().value === ',') { this.next(); continue; }
|
|
634
816
|
break;
|
|
@@ -683,6 +865,13 @@ class Parser {
|
|
|
683
865
|
parseValue() {
|
|
684
866
|
const t = this.next();
|
|
685
867
|
if (t.type === 'number' || t.type === 'string') return t.value;
|
|
868
|
+
// MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
|
|
869
|
+
if (t.type === 'keyword' && t.value === 'VALUES' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
870
|
+
this.next();
|
|
871
|
+
const col = this.parseColumnRef();
|
|
872
|
+
this.expect('op', ')');
|
|
873
|
+
return { _valuesOf: col };
|
|
874
|
+
}
|
|
686
875
|
if (t.type === 'keyword' && t.value === 'NULL') return null;
|
|
687
876
|
if (t.type === 'keyword' && t.value === 'TRUE') return true;
|
|
688
877
|
if (t.type === 'keyword' && t.value === 'FALSE') return false;
|
|
@@ -701,6 +890,21 @@ class Parser {
|
|
|
701
890
|
throw new Error(`Expected value, got '${t.value}'`);
|
|
702
891
|
}
|
|
703
892
|
|
|
893
|
+
/**
|
|
894
|
+
* ON CONFLICT DO UPDATE 赋值右侧:
|
|
895
|
+
* EXCLUDED.col —— PG:引用"本次本应插入的值"
|
|
896
|
+
* VALUES(col) —— MySQL 同义(在 parseValue 中处理)
|
|
897
|
+
* 其他字面量
|
|
898
|
+
*/
|
|
899
|
+
parseConflictValue() {
|
|
900
|
+
if (this.isWord('EXCLUDED') && this.peek(1).type === 'op' && this.peek(1).value === '.') {
|
|
901
|
+
this.next(); this.next();
|
|
902
|
+
const col = this.parseColumnRef();
|
|
903
|
+
return { _excludedOf: col };
|
|
904
|
+
}
|
|
905
|
+
return this.parseValue();
|
|
906
|
+
}
|
|
907
|
+
|
|
704
908
|
/**
|
|
705
909
|
* WITH [RECURSIVE] name [(col, ...)] AS (SELECT ...), ... <statement>
|
|
706
910
|
* CTE 在语法层解析为 { type:'with', ctes:[{name, columns, select}], statement }。
|
|
@@ -833,24 +1037,37 @@ class Parser {
|
|
|
833
1037
|
if (t.type === 'keyword' && t.value === 'COUNT') {
|
|
834
1038
|
this.next();
|
|
835
1039
|
this.expect('op', '(');
|
|
1040
|
+
let aggDistinct = false;
|
|
1041
|
+
if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct = true; }
|
|
836
1042
|
let col = null;
|
|
837
1043
|
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
|
|
838
1044
|
else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
|
|
1045
|
+
// COUNT(DISTINCT a, b) —— 多列去重(仅对 COUNT 有意义,其它聚合按元组处理)
|
|
1046
|
+
if (aggDistinct && this.peek().type === 'op' && this.peek().value === ',') {
|
|
1047
|
+
const cols = [col];
|
|
1048
|
+
while (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1049
|
+
this.next();
|
|
1050
|
+
cols.push(this.parseScalar());
|
|
1051
|
+
}
|
|
1052
|
+
col = { type: 'tuple', items: cols };
|
|
1053
|
+
}
|
|
839
1054
|
this.expect('op', ')');
|
|
840
|
-
aggregate = { type: 'COUNT', column: col };
|
|
1055
|
+
aggregate = { type: 'COUNT', column: col, distinct: aggDistinct };
|
|
841
1056
|
aggregate = this.parseOptionalOver(aggregate);
|
|
842
1057
|
aggregate.alias = this.parseOptionalAlias();
|
|
843
|
-
columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
|
|
1058
|
+
columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct });
|
|
844
1059
|
} else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
|
|
845
1060
|
this.next();
|
|
846
1061
|
const fn = t.value;
|
|
847
1062
|
this.expect('op', '(');
|
|
1063
|
+
let aggDistinct2 = false;
|
|
1064
|
+
if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct2 = true; }
|
|
848
1065
|
const col = this.parseScalar();
|
|
849
1066
|
this.expect('op', ')');
|
|
850
|
-
aggregate = { type: fn, column: col };
|
|
1067
|
+
aggregate = { type: fn, column: col, distinct: aggDistinct2 };
|
|
851
1068
|
aggregate = this.parseOptionalOver(aggregate);
|
|
852
1069
|
aggregate.alias = this.parseOptionalAlias();
|
|
853
|
-
columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
|
|
1070
|
+
columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct2 });
|
|
854
1071
|
} else if (t.type === 'op' && t.value === '*') {
|
|
855
1072
|
this.next();
|
|
856
1073
|
columns.push({ expr: '*' });
|
|
@@ -867,7 +1084,7 @@ class Parser {
|
|
|
867
1084
|
if (expr.type === 'star') {
|
|
868
1085
|
columns.push({ expr: '*' });
|
|
869
1086
|
} else if (expr.type === 'aggregate') {
|
|
870
|
-
columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr });
|
|
1087
|
+
columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr, distinct: expr.distinct, separator: expr.separator, orderBy: expr.orderBy });
|
|
871
1088
|
} else if (expr.type === 'column') {
|
|
872
1089
|
columns.push({ expr: expr.name, scalar: expr, alias });
|
|
873
1090
|
} else {
|
|
@@ -886,6 +1103,7 @@ class Parser {
|
|
|
886
1103
|
let where = null;
|
|
887
1104
|
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
888
1105
|
let groupBy = null;
|
|
1106
|
+
let rollup = false;
|
|
889
1107
|
if (this.isKeyword('GROUP')) {
|
|
890
1108
|
this.expectKeyword('GROUP'); this.expectKeyword('BY');
|
|
891
1109
|
groupBy = [];
|
|
@@ -894,6 +1112,12 @@ class Parser {
|
|
|
894
1112
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
895
1113
|
break;
|
|
896
1114
|
}
|
|
1115
|
+
// WITH ROLLUP 必须紧跟 GROUP BY 列表,之后才是 HAVING / ORDER BY / LIMIT
|
|
1116
|
+
// (标准顺序:GROUP BY <list> [WITH ROLLUP] [HAVING] [ORDER BY] [LIMIT])
|
|
1117
|
+
if (this.isKeyword('WITH') && this.isWord('ROLLUP', 1)) {
|
|
1118
|
+
this.next(); this.next();
|
|
1119
|
+
rollup = true;
|
|
1120
|
+
}
|
|
897
1121
|
}
|
|
898
1122
|
let having = null;
|
|
899
1123
|
if (this.isKeyword('HAVING')) { this.next(); having = this.parseExpr(); }
|
|
@@ -961,13 +1185,6 @@ class Parser {
|
|
|
961
1185
|
else if (this.isKeyword('DISTINCT')) this.next();
|
|
962
1186
|
except = { all, select: this.parseSelect() };
|
|
963
1187
|
}
|
|
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
1188
|
// FETCH FIRST n ROWS ONLY(SQL 标准的 LIMIT 写法)
|
|
972
1189
|
if (this.isWord('FETCH')) {
|
|
973
1190
|
this.next();
|
|
@@ -1029,19 +1246,18 @@ class Parser {
|
|
|
1029
1246
|
this.expect('op', ')');
|
|
1030
1247
|
let alias = null;
|
|
1031
1248
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
1032
|
-
else if (this.peek().type === 'ident') { alias = this.next().value; }
|
|
1033
|
-
|
|
1249
|
+
else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) { alias = this.next().value; }
|
|
1250
|
+
// 标准 SQL 要求 FROM 子查询必须带别名,这里放宽为自动生成,
|
|
1251
|
+
// 避免 README 里 SELECT ... FROM (SELECT ...) 这类示例直接报错
|
|
1252
|
+
if (!alias) alias = '__sub' + (++this.subquerySeq);
|
|
1034
1253
|
return { subquery: sub, alias };
|
|
1035
1254
|
}
|
|
1036
1255
|
const table = this.parseTableName();
|
|
1037
1256
|
let alias = null;
|
|
1038
1257
|
// 这里排除的是"未进关键字表、但会跟在表名之后"的子句起始词,
|
|
1039
1258
|
// 否则 `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
1259
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
1044
|
-
else if (this.peek().type === 'ident' && !
|
|
1260
|
+
else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) {
|
|
1045
1261
|
alias = this.next().value;
|
|
1046
1262
|
}
|
|
1047
1263
|
return { table, alias };
|
|
@@ -1096,9 +1312,28 @@ class Parser {
|
|
|
1096
1312
|
const table = this.parseTableName();
|
|
1097
1313
|
let where = null;
|
|
1098
1314
|
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
1315
|
+
let orderBy = null, limit = null;
|
|
1316
|
+
if (this.isKeyword('ORDER')) {
|
|
1317
|
+
this.next(); this.expectKeyword('BY');
|
|
1318
|
+
orderBy = [];
|
|
1319
|
+
for (;;) {
|
|
1320
|
+
const col = this.parseColumnRef();
|
|
1321
|
+
let dir = 'asc';
|
|
1322
|
+
if (this.isKeyword('ASC')) this.next();
|
|
1323
|
+
else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
|
|
1324
|
+
orderBy.push({ column: col, dir });
|
|
1325
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1326
|
+
break;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (this.isKeyword('LIMIT')) {
|
|
1330
|
+
this.next();
|
|
1331
|
+
limit = this.parseValue();
|
|
1332
|
+
if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
|
|
1333
|
+
}
|
|
1099
1334
|
const returning = this.parseOptionalReturning();
|
|
1100
1335
|
this.optionalTailSemicolon();
|
|
1101
|
-
return { type: 'delete', table, where, returning };
|
|
1336
|
+
return { type: 'delete', table, where, orderBy, limit, returning };
|
|
1102
1337
|
}
|
|
1103
1338
|
|
|
1104
1339
|
parseDropTable() {
|
|
@@ -1386,19 +1621,93 @@ class Parser {
|
|
|
1386
1621
|
return this.parseOr();
|
|
1387
1622
|
}
|
|
1388
1623
|
|
|
1624
|
+
/**
|
|
1625
|
+
* 函数实参用的表达式:与 parseExpr 相同,但叶子允许是"裸操作数"。
|
|
1626
|
+
* 例如 IF(1 > 0, 'y', 'n') 的第一个参数是完整比较表达式,
|
|
1627
|
+
* 而 IF(age, 'y', 'n') 这种裸列名也必须能解析。
|
|
1628
|
+
*/
|
|
1629
|
+
parseArgExpr() {
|
|
1630
|
+
this.bareExprDepth = (this.bareExprDepth || 0) + 1;
|
|
1631
|
+
try {
|
|
1632
|
+
return this.parseOr();
|
|
1633
|
+
} finally {
|
|
1634
|
+
this.bareExprDepth--;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// 位运算优先级(低 → 高):| < ^ < & < << >> < 加减 < 乘除模
|
|
1639
|
+
parseBitwise() {
|
|
1640
|
+
let node = this.parseBitXor();
|
|
1641
|
+
for (;;) {
|
|
1642
|
+
const t = this.peek();
|
|
1643
|
+
if (t.type === 'op' && t.value === '|') { this.next(); node = { type: 'arith', op: '|', left: node, right: this.parseBitXor() }; continue; }
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
return node;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
parseBitXor() {
|
|
1650
|
+
let node = this.parseBitAnd();
|
|
1651
|
+
for (;;) {
|
|
1652
|
+
const t = this.peek();
|
|
1653
|
+
if (t.type === 'op' && t.value === '^') { this.next(); node = { type: 'arith', op: '^', left: node, right: this.parseBitAnd() }; continue; }
|
|
1654
|
+
break;
|
|
1655
|
+
}
|
|
1656
|
+
return node;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
parseBitAnd() {
|
|
1660
|
+
let node = this.parseShift();
|
|
1661
|
+
for (;;) {
|
|
1662
|
+
const t = this.peek();
|
|
1663
|
+
if (t.type === 'op' && t.value === '&') { this.next(); node = { type: 'arith', op: '&', left: node, right: this.parseShift() }; continue; }
|
|
1664
|
+
break;
|
|
1665
|
+
}
|
|
1666
|
+
return node;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
parseShift() {
|
|
1670
|
+
let node = this.parseAdditive();
|
|
1671
|
+
for (;;) {
|
|
1672
|
+
const t = this.peek();
|
|
1673
|
+
if (t.type === 'op' && (t.value === '<<' || t.value === '>>')) {
|
|
1674
|
+
this.next();
|
|
1675
|
+
node = { type: 'arith', op: t.value, left: node, right: this.parseAdditive() };
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1680
|
+
return node;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1389
1683
|
parseOr() {
|
|
1684
|
+
let left = this.parseXor();
|
|
1685
|
+
for (;;) {
|
|
1686
|
+
const isOr = this.isKeyword('OR') || (this.peek().type === 'op' && this.peek().value === '||');
|
|
1687
|
+
if (!isOr) break;
|
|
1688
|
+
this.next();
|
|
1689
|
+
const right = this.parseXor();
|
|
1690
|
+
left = { type: 'or', left, right };
|
|
1691
|
+
}
|
|
1692
|
+
return left;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// 逻辑异或 XOR(MySQL 优先级:OR < XOR < AND)
|
|
1696
|
+
parseXor() {
|
|
1390
1697
|
let left = this.parseAnd();
|
|
1391
|
-
while (this.
|
|
1698
|
+
while (this.isWord('XOR')) {
|
|
1392
1699
|
this.next();
|
|
1393
1700
|
const right = this.parseAnd();
|
|
1394
|
-
left = { type: '
|
|
1701
|
+
left = { type: 'xor', left, right };
|
|
1395
1702
|
}
|
|
1396
1703
|
return left;
|
|
1397
1704
|
}
|
|
1398
1705
|
|
|
1399
1706
|
parseAnd() {
|
|
1400
1707
|
let left = this.parseNot();
|
|
1401
|
-
|
|
1708
|
+
for (;;) {
|
|
1709
|
+
const isAnd = this.isKeyword('AND') || (this.peek().type === 'op' && this.peek().value === '&&');
|
|
1710
|
+
if (!isAnd) break;
|
|
1402
1711
|
this.next();
|
|
1403
1712
|
const right = this.parseNot();
|
|
1404
1713
|
left = { type: 'and', left, right };
|
|
@@ -1434,9 +1743,69 @@ class Parser {
|
|
|
1434
1743
|
return this.parseComparison();
|
|
1435
1744
|
}
|
|
1436
1745
|
|
|
1746
|
+
/**
|
|
1747
|
+
* 解析聚合调用:fn ( [DISTINCT] expr [, expr*] [ORDER BY ...] [SEPARATOR 'x'] )
|
|
1748
|
+
* 以及 COUNT(*) 这类无参形式。
|
|
1749
|
+
*/
|
|
1750
|
+
parseAggregateCall(fn) {
|
|
1751
|
+
this.expect('op', '(');
|
|
1752
|
+
const distinct = this.isKeyword('DISTINCT');
|
|
1753
|
+
if (distinct) this.next();
|
|
1754
|
+
let column = null;
|
|
1755
|
+
let separator = null;
|
|
1756
|
+
let innerOrder = null;
|
|
1757
|
+
if (this.peek().type === 'op' && this.peek().value === '*') {
|
|
1758
|
+
this.next();
|
|
1759
|
+
} else if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1760
|
+
column = this.parseScalar();
|
|
1761
|
+
// COUNT(DISTINCT a, b):多列去重
|
|
1762
|
+
if (distinct && this.peek().type === 'op' && this.peek().value === ',') {
|
|
1763
|
+
const cols = [column];
|
|
1764
|
+
while (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1765
|
+
this.next();
|
|
1766
|
+
cols.push(this.parseScalar());
|
|
1767
|
+
}
|
|
1768
|
+
column = { type: 'tuple', items: cols };
|
|
1769
|
+
} else if (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1770
|
+
// GROUP_CONCAT(expr, 'sep')
|
|
1771
|
+
this.next();
|
|
1772
|
+
separator = this.parseValue();
|
|
1773
|
+
}
|
|
1774
|
+
// GROUP_CONCAT(expr ORDER BY col [DESC])
|
|
1775
|
+
if (this.isKeyword('ORDER')) {
|
|
1776
|
+
this.next();
|
|
1777
|
+
if (!this.isKeyword('BY')) throw new Error('Expected BY after ORDER in aggregate, got ' + "'" + this.peek().value + "'");
|
|
1778
|
+
this.next();
|
|
1779
|
+
innerOrder = [];
|
|
1780
|
+
for (;;) {
|
|
1781
|
+
const oCol = this.parseColumnRef();
|
|
1782
|
+
let oDir = 'asc';
|
|
1783
|
+
if (this.isKeyword('ASC')) this.next();
|
|
1784
|
+
else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
|
|
1785
|
+
innerOrder.push({ column: oCol, dir: oDir });
|
|
1786
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
// GROUP_CONCAT(expr SEPARATOR 'sep')
|
|
1791
|
+
if (this.isWord('SEPARATOR')) {
|
|
1792
|
+
this.next();
|
|
1793
|
+
separator = this.parseValue();
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
this.expect('op', ')');
|
|
1797
|
+
return this.parseOptionalOver({ type: 'aggregate', fn, column, distinct, separator, orderBy: innerOrder });
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1437
1800
|
parseOperand() {
|
|
1438
1801
|
const t = this.next();
|
|
1439
1802
|
if (t.type === 'ident') {
|
|
1803
|
+
// 聚合函数名(GROUP_CONCAT / STDDEV / FIRST...)走 aggregate 节点,
|
|
1804
|
+
// 否则会被当作逐行标量函数:既返回 null,又不会折叠分组
|
|
1805
|
+
if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
|
|
1806
|
+
this.peek().type === 'op' && this.peek().value === '(') {
|
|
1807
|
+
return this.parseAggregateCall(String(t.value).toUpperCase());
|
|
1808
|
+
}
|
|
1440
1809
|
// 支持 alias.column 引用
|
|
1441
1810
|
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
1442
1811
|
this.next();
|
|
@@ -1469,6 +1838,60 @@ class Parser {
|
|
|
1469
1838
|
this.expect('op', ')');
|
|
1470
1839
|
return { type: 'cast', expr, dataType };
|
|
1471
1840
|
}
|
|
1841
|
+
// EXTRACT(YEAR FROM expr) —— 标准 SQL 的 FROM 形式
|
|
1842
|
+
if (String(t.value).toUpperCase() === 'EXTRACT' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1843
|
+
this.next();
|
|
1844
|
+
const unitTok = this.next();
|
|
1845
|
+
const unit = String(unitTok.value).toUpperCase();
|
|
1846
|
+
if (!this.isWord('FROM')) throw new Error(`Expected FROM in EXTRACT, got '${this.peek().value}'`);
|
|
1847
|
+
this.next();
|
|
1848
|
+
const arg = this.parseOperand();
|
|
1849
|
+
this.expect('op', ')');
|
|
1850
|
+
return { type: 'func', name: 'EXTRACT', args: [{ type: 'value', value: unit }, arg] };
|
|
1851
|
+
}
|
|
1852
|
+
// SUBSTR(s FROM n [FOR m]) / TRIM([LEADING|TRAILING|BOTH] ['x'] FROM s)
|
|
1853
|
+
if (['SUBSTR', 'SUBSTRING'].includes(String(t.value).toUpperCase()) && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1854
|
+
const name = String(t.value).toUpperCase();
|
|
1855
|
+
this.next();
|
|
1856
|
+
const s = this.parseOperand();
|
|
1857
|
+
let fromV = null, forV = null;
|
|
1858
|
+
if (this.isWord('FROM')) {
|
|
1859
|
+
this.next();
|
|
1860
|
+
fromV = this.parseOperand();
|
|
1861
|
+
if (this.isWord('FOR')) { this.next(); forV = this.parseOperand(); }
|
|
1862
|
+
} else if (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1863
|
+
this.next();
|
|
1864
|
+
fromV = this.parseOperand();
|
|
1865
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); forV = this.parseOperand(); }
|
|
1866
|
+
}
|
|
1867
|
+
this.expect('op', ')');
|
|
1868
|
+
const args = [s];
|
|
1869
|
+
if (fromV) args.push(fromV);
|
|
1870
|
+
if (forV) args.push(forV);
|
|
1871
|
+
return { type: 'func', name, args };
|
|
1872
|
+
}
|
|
1873
|
+
if (String(t.value).toUpperCase() === 'TRIM' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1874
|
+
this.next();
|
|
1875
|
+
let spec = null;
|
|
1876
|
+
if (this.isWord('LEADING') || this.isWord('TRAILING') || this.isWord('BOTH')) {
|
|
1877
|
+
spec = String(this.next().value).toUpperCase();
|
|
1878
|
+
}
|
|
1879
|
+
const first = this.parseOperand();
|
|
1880
|
+
let target = null;
|
|
1881
|
+
if (this.isWord('FROM')) {
|
|
1882
|
+
this.next();
|
|
1883
|
+
target = this.parseOperand();
|
|
1884
|
+
} else if (this.peek().type === 'op' && this.peek().value === ',') {
|
|
1885
|
+
this.next();
|
|
1886
|
+
target = this.parseOperand();
|
|
1887
|
+
}
|
|
1888
|
+
this.expect('op', ')');
|
|
1889
|
+
// 归一成 TRIM(target[, chars[, spec]]),与 applyScalarFunction 的既有签名兼容
|
|
1890
|
+
if (target === null) return { type: 'func', name: 'TRIM', args: [first] };
|
|
1891
|
+
const args = [target, first];
|
|
1892
|
+
if (spec) args.push({ type: 'value', value: spec });
|
|
1893
|
+
return { type: 'func', name: 'TRIM', args };
|
|
1894
|
+
}
|
|
1472
1895
|
// 函数调用: VERSION() / CONCAT(a, b) / NOW() ...
|
|
1473
1896
|
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
1474
1897
|
const name = t.value;
|
|
@@ -1477,7 +1900,42 @@ class Parser {
|
|
|
1477
1900
|
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1478
1901
|
for (;;) {
|
|
1479
1902
|
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
|
|
1480
|
-
|
|
1903
|
+
// INTERVAL n UNIT(DATE_ADD/DATE_SUB 用)
|
|
1904
|
+
else if (this.isWord('INTERVAL')) {
|
|
1905
|
+
this.next();
|
|
1906
|
+
const nTok = this.peek();
|
|
1907
|
+
let nVal;
|
|
1908
|
+
if (nTok.type === 'op' && (nTok.value === '-' || nTok.value === '+')) {
|
|
1909
|
+
this.next();
|
|
1910
|
+
const numTok = this.next();
|
|
1911
|
+
nVal = (nTok.value === '-' ? -1 : 1) * Number(numTok.value);
|
|
1912
|
+
} else {
|
|
1913
|
+
this.next();
|
|
1914
|
+
nVal = Number(nTok.value);
|
|
1915
|
+
}
|
|
1916
|
+
const unitTok = this.next();
|
|
1917
|
+
args.push({ type: 'interval', value: nVal, unit: String(unitTok.value).toUpperCase() });
|
|
1918
|
+
}
|
|
1919
|
+
// 实参允许完整的比较/布尔表达式,例如 IF(status = 'paid', amount, 0)
|
|
1920
|
+
else args.push(this.parseArgExpr());
|
|
1921
|
+
// GROUP_CONCAT(name ORDER BY age):聚合函数内部的排序
|
|
1922
|
+
if (this.isKeyword('ORDER')) {
|
|
1923
|
+
this.next();
|
|
1924
|
+
if (!this.isKeyword('BY')) throw new Error(`Expected BY after ORDER in aggregate, got '${this.peek().value}'`);
|
|
1925
|
+
this.next();
|
|
1926
|
+
const innerOrder = [];
|
|
1927
|
+
for (;;) {
|
|
1928
|
+
const oCol = this.parseColumnRef();
|
|
1929
|
+
let oDir = 'asc';
|
|
1930
|
+
if (this.isKeyword('ASC')) this.next();
|
|
1931
|
+
else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
|
|
1932
|
+
innerOrder.push({ column: oCol, dir: oDir });
|
|
1933
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1934
|
+
break;
|
|
1935
|
+
}
|
|
1936
|
+
this.expect('op', ')');
|
|
1937
|
+
return this.parseOptionalOver({ type: 'func', name, args, orderBy: innerOrder });
|
|
1938
|
+
}
|
|
1481
1939
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1482
1940
|
break;
|
|
1483
1941
|
}
|
|
@@ -1502,7 +1960,7 @@ class Parser {
|
|
|
1502
1960
|
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1503
1961
|
for (;;) {
|
|
1504
1962
|
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
|
|
1505
|
-
else args.push(this.
|
|
1963
|
+
else args.push(this.parseArgExpr());
|
|
1506
1964
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1507
1965
|
break;
|
|
1508
1966
|
}
|
|
@@ -1510,14 +1968,13 @@ class Parser {
|
|
|
1510
1968
|
this.expect('op', ')');
|
|
1511
1969
|
return this.parseOptionalOver({ type: 'func', name, args });
|
|
1512
1970
|
}
|
|
1513
|
-
if (
|
|
1514
|
-
|
|
1515
|
-
this.
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
return this.parseOptionalOver({ type: 'aggregate', fn, column });
|
|
1971
|
+
if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
|
|
1972
|
+
this.peek().type === 'op' && this.peek().value === '(') {
|
|
1973
|
+
return this.parseAggregateCall(String(t.value).toUpperCase());
|
|
1974
|
+
}
|
|
1975
|
+
// 一元按位取反 ~x
|
|
1976
|
+
if (t.type === 'op' && t.value === '~') {
|
|
1977
|
+
return { type: 'bitnot', expr: this.parseOperand() };
|
|
1521
1978
|
}
|
|
1522
1979
|
if (t.type === 'op' && t.value === '(' && this.peek().type === 'keyword' && this.peek().value === 'SELECT') {
|
|
1523
1980
|
const sub = this.parseSelect();
|
|
@@ -1532,8 +1989,8 @@ class Parser {
|
|
|
1532
1989
|
throw new Error(`Expected value or column, got '${t.value}'`);
|
|
1533
1990
|
}
|
|
1534
1991
|
|
|
1535
|
-
//
|
|
1536
|
-
|
|
1992
|
+
// 加减(左结合)
|
|
1993
|
+
parseAdditive() {
|
|
1537
1994
|
let node = this.parseTerm();
|
|
1538
1995
|
for (;;) {
|
|
1539
1996
|
const t = this.peek();
|
|
@@ -1545,6 +2002,16 @@ class Parser {
|
|
|
1545
2002
|
}
|
|
1546
2003
|
break;
|
|
1547
2004
|
}
|
|
2005
|
+
return node;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* 标量表达式:算术 + 位运算,外加 IN / IS 后缀。
|
|
2010
|
+
* 位运算要走到这里,否则 SELECT 列表里的 `SELECT 1 << 8` 无法解析
|
|
2011
|
+
* (parseComparison 只在 WHERE 等布尔上下文被调用)。
|
|
2012
|
+
*/
|
|
2013
|
+
parseScalar() {
|
|
2014
|
+
let node = this.parseBitwise();
|
|
1548
2015
|
// 后缀:expr IN (...)、expr IS [NOT] TRUE/FALSE/NULL(标量上下文,如 SELECT 1 IN (...))
|
|
1549
2016
|
for (;;) {
|
|
1550
2017
|
const t = this.peek();
|
|
@@ -1582,15 +2049,58 @@ class Parser {
|
|
|
1582
2049
|
}
|
|
1583
2050
|
|
|
1584
2051
|
parseTerm() {
|
|
1585
|
-
let node = this.parseOperand();
|
|
2052
|
+
let node = this.parsePostfix(this.parseOperand());
|
|
1586
2053
|
for (;;) {
|
|
1587
2054
|
const t = this.peek();
|
|
1588
2055
|
if (t.type === 'op' && (t.value === '*' || t.value === '/' || t.value === '%')) {
|
|
1589
2056
|
this.next();
|
|
1590
|
-
const right = this.parseOperand();
|
|
2057
|
+
const right = this.parsePostfix(this.parseOperand());
|
|
1591
2058
|
node = { type: 'arith', op: t.value, left: node, right };
|
|
1592
2059
|
continue;
|
|
1593
2060
|
}
|
|
2061
|
+
// MySQL 整数除法:7 DIV 2 → 3
|
|
2062
|
+
if (this.isWord('DIV')) {
|
|
2063
|
+
this.next();
|
|
2064
|
+
const right = this.parsePostfix(this.parseOperand());
|
|
2065
|
+
node = { type: 'arith', op: 'DIV', left: node, right };
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
break;
|
|
2069
|
+
}
|
|
2070
|
+
return node;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* 后缀运算符(绑定最紧):PG 风格 `::` 类型转换、`->` / `->>` JSON 取值。
|
|
2075
|
+
* 例:'5'::INT、meta->>'name'、meta->'addr'->>'city'。
|
|
2076
|
+
*/
|
|
2077
|
+
parsePostfix(node) {
|
|
2078
|
+
for (;;) {
|
|
2079
|
+
const t = this.peek();
|
|
2080
|
+
if (t.type === 'op' && t.value === '::') {
|
|
2081
|
+
this.next();
|
|
2082
|
+
const typeTok = this.next();
|
|
2083
|
+
let dataType = String(typeTok.value).toUpperCase();
|
|
2084
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
2085
|
+
this.next();
|
|
2086
|
+
let depth = 1, spec = '';
|
|
2087
|
+
while (depth > 0 && this.peek().type !== 'eof') {
|
|
2088
|
+
const tk = this.next();
|
|
2089
|
+
if (tk.type === 'op' && tk.value === '(') depth++;
|
|
2090
|
+
else if (tk.type === 'op' && tk.value === ')') { depth--; if (depth === 0) break; }
|
|
2091
|
+
spec += tk.value;
|
|
2092
|
+
}
|
|
2093
|
+
dataType += '(' + spec + ')';
|
|
2094
|
+
}
|
|
2095
|
+
node = { type: 'cast', expr: node, dataType };
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (t.type === 'op' && (t.value === '->' || t.value === '->>')) {
|
|
2099
|
+
this.next();
|
|
2100
|
+
const key = this.parseValue();
|
|
2101
|
+
node = { type: 'jsonAccess', operand: node, key, asText: t.value === '->>' };
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
1594
2104
|
break;
|
|
1595
2105
|
}
|
|
1596
2106
|
return node;
|
|
@@ -1623,7 +2133,7 @@ class Parser {
|
|
|
1623
2133
|
}
|
|
1624
2134
|
|
|
1625
2135
|
parseComparison() {
|
|
1626
|
-
const left = this.
|
|
2136
|
+
const left = this.parseBitwise();
|
|
1627
2137
|
const t = this.peek();
|
|
1628
2138
|
let not = false;
|
|
1629
2139
|
if (t.type === 'keyword' && t.value === 'NOT') {
|
|
@@ -1686,7 +2196,10 @@ class Parser {
|
|
|
1686
2196
|
const isILike = !(t2.type === 'keyword' && t2.value === 'LIKE');
|
|
1687
2197
|
this.next();
|
|
1688
2198
|
const pattern = this.parseValue();
|
|
1689
|
-
|
|
2199
|
+
// 可选 ESCAPE 子句:LIKE '...' ESCAPE '!'
|
|
2200
|
+
let escape = null;
|
|
2201
|
+
if (this.isWord('ESCAPE')) { this.next(); escape = this.parseValue(); }
|
|
2202
|
+
return { type: 'like', operand: left, pattern, not, ci: isILike, escape };
|
|
1690
2203
|
}
|
|
1691
2204
|
|
|
1692
2205
|
// MySQL 中 RLIKE 是 REGEXP 的同义词
|
|
@@ -1696,7 +2209,7 @@ class Parser {
|
|
|
1696
2209
|
return { type: 'regexp', operand: left, pattern, not };
|
|
1697
2210
|
}
|
|
1698
2211
|
|
|
1699
|
-
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
|
|
2212
|
+
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(t2.value)) {
|
|
1700
2213
|
this.next();
|
|
1701
2214
|
// = ANY (SELECT ...) / > ALL (SELECT ...)
|
|
1702
2215
|
const anyAll = this.isWord('ANY') || this.isWord('ALL') || this.isWord('SOME');
|
|
@@ -1707,10 +2220,13 @@ class Parser {
|
|
|
1707
2220
|
this.expect('op', ')');
|
|
1708
2221
|
return { type: 'quantified', op: t2.value === '<>' ? '!=' : t2.value, operand: left, select: sub, quantifier, not };
|
|
1709
2222
|
}
|
|
1710
|
-
const right = this.
|
|
2223
|
+
const right = this.parseBitwise();
|
|
1711
2224
|
return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
|
|
1712
2225
|
}
|
|
1713
2226
|
|
|
2227
|
+
// 函数实参场景允许裸操作数(IF(age, ...) / SUM(IF(...)) 等)
|
|
2228
|
+
if (this.bareExprDepth > 0) return left;
|
|
2229
|
+
|
|
1714
2230
|
throw new Error(`Expected comparison operator, got '${t2.value}'`);
|
|
1715
2231
|
}
|
|
1716
2232
|
}
|
|
@@ -1807,16 +2323,34 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1807
2323
|
case '*': return l * r;
|
|
1808
2324
|
case '/': return r === 0 ? null : l / r;
|
|
1809
2325
|
case '%': return r === 0 ? null : l % r;
|
|
2326
|
+
// MySQL 整数除法 DIV(结果向零取整)
|
|
2327
|
+
case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
|
|
2328
|
+
// 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
|
|
2329
|
+
case '&': return (Number(l) | 0) & (Number(r) | 0);
|
|
2330
|
+
case '|': return (Number(l) | 0) | (Number(r) | 0);
|
|
2331
|
+
case '^': return (Number(l) | 0) ^ (Number(r) | 0);
|
|
2332
|
+
case '<<': return (Number(l) | 0) << (Number(r) | 0);
|
|
2333
|
+
case '>>': return (Number(l) | 0) >> (Number(r) | 0);
|
|
1810
2334
|
}
|
|
1811
2335
|
return null;
|
|
1812
2336
|
}
|
|
2337
|
+
case 'bitnot':
|
|
2338
|
+
return ~(Number(resolveOperand(operand.expr, row, ctx)) | 0);
|
|
1813
2339
|
case 'func':
|
|
1814
2340
|
return applyScalarFunction(operand, row, ctx);
|
|
1815
2341
|
case 'case':
|
|
1816
2342
|
return evaluateCaseVal(operand, row, ctx);
|
|
2343
|
+
// 布尔类节点出现在"取值"位置时(如 IF(status = 'paid', ...)、SUM(IF(a > b,1,0))),
|
|
2344
|
+
// 求值成 true/false 供上层按真值判断
|
|
2345
|
+
case 'compare':
|
|
2346
|
+
case 'and':
|
|
2347
|
+
case 'or':
|
|
2348
|
+
case 'xor':
|
|
2349
|
+
case 'not':
|
|
1817
2350
|
case 'in':
|
|
1818
2351
|
case 'isNull':
|
|
1819
2352
|
case 'isTruth':
|
|
2353
|
+
case 'exists':
|
|
1820
2354
|
case 'regexp':
|
|
1821
2355
|
case 'like':
|
|
1822
2356
|
case 'between':
|
|
@@ -1836,6 +2370,30 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1836
2370
|
const v = resolveOperand(operand.expr, row, ctx);
|
|
1837
2371
|
return applyCast(v, operand.dataType);
|
|
1838
2372
|
}
|
|
2373
|
+
case 'jsonAccess': {
|
|
2374
|
+
let base = resolveOperand(operand.operand, row, ctx);
|
|
2375
|
+
if (base === null || base === undefined) return null;
|
|
2376
|
+
// JSON 列可能以字符串形式存储,先解析
|
|
2377
|
+
if (typeof base === 'string') {
|
|
2378
|
+
try { base = JSON.parse(base); } catch (e) { return null; }
|
|
2379
|
+
}
|
|
2380
|
+
const key = operand.key;
|
|
2381
|
+
let v;
|
|
2382
|
+
if (Array.isArray(base)) {
|
|
2383
|
+
const idx = Number(key);
|
|
2384
|
+
v = Number.isInteger(idx) ? (idx < 0 ? base[base.length + idx] : base[idx]) : undefined;
|
|
2385
|
+
} else if (base && typeof base === 'object') {
|
|
2386
|
+
v = base[key];
|
|
2387
|
+
} else {
|
|
2388
|
+
return null;
|
|
2389
|
+
}
|
|
2390
|
+
if (v === undefined) return null;
|
|
2391
|
+
if (operand.asText) {
|
|
2392
|
+
if (v === null) return null;
|
|
2393
|
+
return typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
2394
|
+
}
|
|
2395
|
+
return v;
|
|
2396
|
+
}
|
|
1839
2397
|
case 'quantified': {
|
|
1840
2398
|
// = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
|
|
1841
2399
|
const v = resolveOperand(operand.operand, row, ctx);
|
|
@@ -1856,9 +2414,30 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1856
2414
|
}
|
|
1857
2415
|
}
|
|
1858
2416
|
|
|
2417
|
+
/** 解析 SQL 里的日期文本('YYYY-MM-DD' / 'YYYY-MM-DD HH:MM:SS' / Date) */
|
|
2418
|
+
function parseSqlDate(v) {
|
|
2419
|
+
if (v === null || v === undefined) return null;
|
|
2420
|
+
if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
|
|
2421
|
+
const s = String(v).trim().replace(' ', 'T');
|
|
2422
|
+
const d = new Date(s.length <= 10 ? s + 'T00:00:00' : s);
|
|
2423
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
2427
|
+
|
|
2428
|
+
/** 按输入形态回写日期:纯日期返回 YYYY-MM-DD,带时间返回 YYYY-MM-DD HH:MM:SS */
|
|
2429
|
+
function fmtSqlDate(dateOnly, d) {
|
|
2430
|
+
const ymd = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
|
2431
|
+
return dateOnly ? ymd : ymd + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
|
|
2432
|
+
}
|
|
2433
|
+
|
|
1859
2434
|
function applyScalarFunction(fnNode, row, ctx) {
|
|
1860
2435
|
const name = (fnNode.name || '').toUpperCase();
|
|
1861
|
-
const
|
|
2436
|
+
const rawArgs = fnNode.args || [];
|
|
2437
|
+
// INTERVAL n UNIT 是语法结构而非普通值,原样传给 DATE_ADD / DATE_SUB
|
|
2438
|
+
const args = rawArgs.map(a => (a && a.type === 'interval')
|
|
2439
|
+
? { __interval: true, value: a.value, unit: a.unit }
|
|
2440
|
+
: resolveOperand(a, row, ctx));
|
|
1862
2441
|
const session = ctx && ctx.session;
|
|
1863
2442
|
if (ctx && ctx.functions && Object.prototype.hasOwnProperty.call(ctx.functions, name)) {
|
|
1864
2443
|
return ctx.functions[name].apply(null, args);
|
|
@@ -1889,7 +2468,65 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1889
2468
|
case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
|
|
1890
2469
|
case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
|
|
1891
2470
|
case 'LENGTH': case 'CHAR_LENGTH': case 'CHARACTER_LENGTH': return args[0] == null ? null : String(args[0]).length;
|
|
1892
|
-
|
|
2471
|
+
// TRIM(s) / TRIM([LEADING|TRAILING|BOTH] chars FROM s) / TRIM(s, chars)
|
|
2472
|
+
case 'TRIM': {
|
|
2473
|
+
if (args[0] == null) return null;
|
|
2474
|
+
const s = String(args[0]);
|
|
2475
|
+
if (args.length === 1) return s.trim();
|
|
2476
|
+
const chars = args[1] == null ? '' : String(args[1]);
|
|
2477
|
+
if (!chars) return s.trim();
|
|
2478
|
+
const spec = args[2] ? String(args[2]).toUpperCase() : 'BOTH';
|
|
2479
|
+
const esc = chars.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2480
|
+
const head = new RegExp('^[' + esc + ']+');
|
|
2481
|
+
const tail = new RegExp('[' + esc + ']+$');
|
|
2482
|
+
if (spec === 'LEADING') return s.replace(head, '');
|
|
2483
|
+
if (spec === 'TRAILING') return s.replace(tail, '');
|
|
2484
|
+
return s.replace(head, '').replace(tail, '');
|
|
2485
|
+
}
|
|
2486
|
+
// EXTRACT(YEAR FROM d) —— 解析层会把 unit 作为第一个参数传入
|
|
2487
|
+
case 'EXTRACT': {
|
|
2488
|
+
const unit = String(args[0] == null ? '' : args[0]).toUpperCase();
|
|
2489
|
+
const d = parseSqlDate(args[1]);
|
|
2490
|
+
if (!d) return null;
|
|
2491
|
+
switch (unit) {
|
|
2492
|
+
case 'YEAR': return d.getFullYear();
|
|
2493
|
+
case 'MONTH': return d.getMonth() + 1;
|
|
2494
|
+
case 'DAY': return d.getDate();
|
|
2495
|
+
case 'HOUR': return d.getHours();
|
|
2496
|
+
case 'MINUTE': return d.getMinutes();
|
|
2497
|
+
case 'SECOND': return d.getSeconds();
|
|
2498
|
+
case 'DOW': return d.getDay(); // 0 = 周日
|
|
2499
|
+
case 'DAYOFWEEK': return d.getDay() + 1; // 1 = 周日(MySQL 语义)
|
|
2500
|
+
case 'WEEK': {
|
|
2501
|
+
const start = new Date(d.getFullYear(), 0, 1);
|
|
2502
|
+
return Math.floor((d - start) / 604800000) + 1;
|
|
2503
|
+
}
|
|
2504
|
+
default: return null;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
case 'DATE_ADD': case 'ADDDATE':
|
|
2508
|
+
case 'DATE_SUB': case 'SUBDATE': {
|
|
2509
|
+
const sign = (name === 'DATE_SUB' || name === 'SUBDATE') ? -1 : 1;
|
|
2510
|
+
const base = parseSqlDate(args[0]);
|
|
2511
|
+
if (!base) return null;
|
|
2512
|
+
const iv = args[1];
|
|
2513
|
+
if (!iv || !iv.__interval) return fmtSqlDate(String(args[0]).length <= 10, base);
|
|
2514
|
+
const isDateOnly = String(args[0]).trim().length <= 10;
|
|
2515
|
+
const d = new Date(base.getTime());
|
|
2516
|
+
const n = sign * Number(iv.value || 0);
|
|
2517
|
+
switch (String(iv.unit || '').toUpperCase()) {
|
|
2518
|
+
case 'DAY': case 'DAYS': d.setDate(d.getDate() + n); break;
|
|
2519
|
+
case 'WEEK': case 'WEEKS': d.setDate(d.getDate() + n * 7); break;
|
|
2520
|
+
case 'MONTH': case 'MONTHS': d.setMonth(d.getMonth() + n); break;
|
|
2521
|
+
case 'QUARTER': d.setMonth(d.getMonth() + n * 3); break;
|
|
2522
|
+
case 'YEAR': case 'YEARS': d.setFullYear(d.getFullYear() + n); break;
|
|
2523
|
+
case 'HOUR': case 'HOURS': d.setHours(d.getHours() + n); break;
|
|
2524
|
+
case 'MINUTE': case 'MINUTES': d.setMinutes(d.getMinutes() + n); break;
|
|
2525
|
+
case 'SECOND': case 'SECONDS': d.setSeconds(d.getSeconds() + n); break;
|
|
2526
|
+
default: return null;
|
|
2527
|
+
}
|
|
2528
|
+
return fmtSqlDate(isDateOnly, d);
|
|
2529
|
+
}
|
|
1893
2530
|
case 'LTRIM': return args[0] == null ? null : String(args[0]).replace(/^\s+/, '');
|
|
1894
2531
|
case 'RTRIM': return args[0] == null ? null : String(args[0]).replace(/\s+$/, '');
|
|
1895
2532
|
case 'ABS': return args[0] == null ? null : Math.abs(args[0]);
|
|
@@ -1975,13 +2612,25 @@ function seedRand(seed) {
|
|
|
1975
2612
|
}
|
|
1976
2613
|
|
|
1977
2614
|
// ci 默认 true:保持 LIKE 既有的大小写不敏感行为不变;ILIKE 同为不敏感。
|
|
1978
|
-
|
|
2615
|
+
// escape:可选转义符(ESCAPE 子句),默认反斜杠;转义符后跟 % _ 或转义符本身按字面量匹配。
|
|
2616
|
+
function likeMatch(value, pattern, ci = true, escape = '\\') {
|
|
1979
2617
|
if (typeof value !== 'string') return false;
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2618
|
+
const p = String(pattern);
|
|
2619
|
+
const e = (escape === null || escape === undefined || escape === '') ? '\\' : String(escape);
|
|
2620
|
+
const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2621
|
+
let out = '';
|
|
2622
|
+
for (let i = 0; i < p.length; i++) {
|
|
2623
|
+
const ch = p[i];
|
|
2624
|
+
if (ch === e && i + 1 < p.length && (p[i + 1] === '%' || p[i + 1] === '_' || p[i + 1] === e)) {
|
|
2625
|
+
out += reEsc(p[i + 1]);
|
|
2626
|
+
i++;
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
if (ch === '%') out += '.*';
|
|
2630
|
+
else if (ch === '_') out += '.';
|
|
2631
|
+
else out += reEsc(ch);
|
|
2632
|
+
}
|
|
2633
|
+
return new RegExp('^' + out + '$', ci ? 'i' : '').test(value);
|
|
1985
2634
|
}
|
|
1986
2635
|
|
|
1987
2636
|
function extractEqualPushdown(expr, schema) {
|
|
@@ -2033,10 +2682,18 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2033
2682
|
switch (expr.type) {
|
|
2034
2683
|
case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
|
|
2035
2684
|
case 'or': return evaluateExpr(expr.left, row, ctx) || evaluateExpr(expr.right, row, ctx);
|
|
2685
|
+
case 'xor': return Boolean(evaluateExpr(expr.left, row, ctx)) !== Boolean(evaluateExpr(expr.right, row, ctx));
|
|
2036
2686
|
case 'not': return !evaluateExpr(expr.expr, row, ctx);
|
|
2037
2687
|
case 'compare': {
|
|
2038
2688
|
const l = resolveOperand(expr.left, row, ctx);
|
|
2039
2689
|
const r = resolveOperand(expr.right, row, ctx);
|
|
2690
|
+
// MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
|
|
2691
|
+
if (expr.op === '<=>') {
|
|
2692
|
+
const ln = l === null || l === undefined;
|
|
2693
|
+
const rn = r === null || r === undefined;
|
|
2694
|
+
if (ln || rn) return ln && rn;
|
|
2695
|
+
return l === r || String(l) === String(r);
|
|
2696
|
+
}
|
|
2040
2697
|
// SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
|
|
2041
2698
|
if (l === null || r === null) return false;
|
|
2042
2699
|
// 布尔与数值互通(MySQL: 1 = TRUE、0 = FALSE)
|
|
@@ -2059,7 +2716,7 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2059
2716
|
}
|
|
2060
2717
|
case 'like': {
|
|
2061
2718
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2062
|
-
const r = likeMatch(v, expr.pattern, !!expr.ci);
|
|
2719
|
+
const r = likeMatch(v, expr.pattern, !!expr.ci, expr.escape);
|
|
2063
2720
|
return expr.not ? !r : r;
|
|
2064
2721
|
}
|
|
2065
2722
|
case 'quantified':
|
|
@@ -2154,16 +2811,22 @@ function windowColumnValue(c, r) {
|
|
|
2154
2811
|
|
|
2155
2812
|
function scalarColumnValue(c, r, ctx) {
|
|
2156
2813
|
if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
|
|
2814
|
+
// 聚合列优先:GROUP_CONCAT / STDDEV 等同时带 scalar 与 aggregate
|
|
2815
|
+
if (c.aggregate) {
|
|
2816
|
+
return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column,
|
|
2817
|
+
c.distinct, c.separator !== undefined ? c.separator : (c.scalar && c.scalar.separator),
|
|
2818
|
+
c.orderBy || (c.scalar && c.scalar.orderBy));
|
|
2819
|
+
}
|
|
2157
2820
|
if (c.scalar) {
|
|
2158
2821
|
const s = c.scalar;
|
|
2159
2822
|
if (s && s.type === 'func' && ctx && ctx.ctxAggregates && Object.prototype.hasOwnProperty.call(ctx.ctxAggregates, String(s.name).toUpperCase())) {
|
|
2160
2823
|
const fn = String(s.name).toUpperCase();
|
|
2161
2824
|
const col = s.args && s.args[0];
|
|
2162
|
-
return ctx._aggValue(ctx.group, fn, col);
|
|
2825
|
+
return ctx._aggValue(ctx.group, fn, col, c.aggregate ? c.distinct : (c.scalar && c.scalar.distinct));
|
|
2163
2826
|
}
|
|
2164
2827
|
return resolveOperand(c.scalar, r, ctx);
|
|
2165
2828
|
}
|
|
2166
|
-
|
|
2829
|
+
|
|
2167
2830
|
if (c.literal !== undefined) return c.literal;
|
|
2168
2831
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r, ctx);
|
|
2169
2832
|
if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
|
|
@@ -2186,7 +2849,10 @@ function buildCreateTableSql(name, schema) {
|
|
|
2186
2849
|
const seg = ['`' + col + '`', sqlTypeName(def.type)];
|
|
2187
2850
|
if (def.autoIncrement) seg.push('AUTO_INCREMENT');
|
|
2188
2851
|
if (def.nullable === false) seg.push('NOT NULL');
|
|
2189
|
-
if (def.default !== undefined)
|
|
2852
|
+
if (def.default !== undefined) {
|
|
2853
|
+
// defaultExpr(CURRENT_TIMESTAMP 等)不加引号,普通字符串默认值才加
|
|
2854
|
+
seg.push('DEFAULT ' + (typeof def.default === 'string' && !def.defaultExpr ? "'" + def.default + "'" : def.default));
|
|
2855
|
+
}
|
|
2190
2856
|
return seg.join(' ');
|
|
2191
2857
|
});
|
|
2192
2858
|
const pks = Object.keys(schema).filter(k => schema[k].primaryKey);
|
|
@@ -2353,7 +3019,23 @@ class SQLExecutor {
|
|
|
2353
3019
|
if (pkCols.length > 0) {
|
|
2354
3020
|
const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
|
|
2355
3021
|
const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
|
|
2356
|
-
|
|
3022
|
+
// 冲突判定的额外列组:
|
|
3023
|
+
// ON CONFLICT (col) DO UPDATE → 指定列(PG)
|
|
3024
|
+
// ON DUPLICATE KEY UPDATE → 各唯一列(MySQL:任一唯一键冲突都触发)
|
|
3025
|
+
// 此前只认主键,导致这两者在唯一列冲突时会插入重复行而不是更新。
|
|
3026
|
+
const ocTarget = (statement.onConflict && Array.isArray(statement.onConflict.target))
|
|
3027
|
+
? statement.onConflict.target.map(c => (c && c.name !== undefined ? c.name : c)).filter(Boolean)
|
|
3028
|
+
: null;
|
|
3029
|
+
const extraIdx = [];
|
|
3030
|
+
if (ocTarget && ocTarget.length) extraIdx.push(ocTarget);
|
|
3031
|
+
else if (statement.onDuplicate && schema) {
|
|
3032
|
+
for (const [c, def] of Object.entries(schema)) {
|
|
3033
|
+
if (def && def.unique && !def.primaryKey) extraIdx.push([c]);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
const hasAllOf = (cols) => (row) => cols.every(c => row[c] !== undefined && row[c] !== null);
|
|
3037
|
+
|
|
3038
|
+
const explicit = dataRows.filter((row) => hasExplicitPk(row) || extraIdx.some((cols) => hasAllOf(cols)(row)));
|
|
2357
3039
|
if (explicit.length > 0) {
|
|
2358
3040
|
const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
2359
3041
|
const pkMap = new Map();
|
|
@@ -2361,15 +3043,31 @@ class SQLExecutor {
|
|
|
2361
3043
|
const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
|
|
2362
3044
|
if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
|
|
2363
3045
|
}
|
|
3046
|
+
const pkValsOf = (row) => pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
|
|
3047
|
+
const extraMaps = extraIdx.map((cols) => {
|
|
3048
|
+
const m = new Map();
|
|
3049
|
+
const pick = hasAllOf(cols);
|
|
3050
|
+
const key = (row) => cols.map(c => String(row[c])).join('|');
|
|
3051
|
+
for (const row of all) {
|
|
3052
|
+
if (!pick(row)) continue;
|
|
3053
|
+
m.set(key(row), pkValsOf(row));
|
|
3054
|
+
}
|
|
3055
|
+
return { pick, key, m };
|
|
3056
|
+
});
|
|
2364
3057
|
const conflicts = [];
|
|
2365
3058
|
const fresh = [];
|
|
2366
3059
|
for (const d of dataRows) {
|
|
2367
|
-
|
|
2368
|
-
if (hasExplicitPk(d) && pkMap.has(
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
3060
|
+
let existingId = null;
|
|
3061
|
+
if (hasExplicitPk(d) && pkMap.has(keyOf(d))) existingId = pkMap.get(keyOf(d));
|
|
3062
|
+
if (!existingId) {
|
|
3063
|
+
for (const { pick, key, m } of extraMaps) {
|
|
3064
|
+
if (!pick(d)) continue;
|
|
3065
|
+
const k = key(d);
|
|
3066
|
+
if (m.has(k)) { existingId = m.get(k); break; }
|
|
3067
|
+
}
|
|
2372
3068
|
}
|
|
3069
|
+
if (existingId) conflicts.push({ d, existingId });
|
|
3070
|
+
else fresh.push(d);
|
|
2373
3071
|
}
|
|
2374
3072
|
const conflictAction = this._conflictAction(statement);
|
|
2375
3073
|
const conflictSets = this._conflictSets(statement);
|
|
@@ -2396,7 +3094,16 @@ class SQLExecutor {
|
|
|
2396
3094
|
for (const { d, existingId } of conflicts) {
|
|
2397
3095
|
const data = {};
|
|
2398
3096
|
if (conflictSets) {
|
|
2399
|
-
for (const [col, val] of conflictSets)
|
|
3097
|
+
for (const [col, val] of conflictSets) {
|
|
3098
|
+
// VALUES(col) 引用本次本应插入的值;EXCLUDED.col 为 PG 同义写法
|
|
3099
|
+
if (val && typeof val === 'object' && val._valuesOf !== undefined) {
|
|
3100
|
+
data[col] = d[val._valuesOf] !== undefined ? d[val._valuesOf] : null;
|
|
3101
|
+
} else if (val && typeof val === 'object' && val._excludedOf !== undefined) {
|
|
3102
|
+
data[col] = d[val._excludedOf] !== undefined ? d[val._excludedOf] : null;
|
|
3103
|
+
} else {
|
|
3104
|
+
data[col] = val;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
2400
3107
|
} else {
|
|
2401
3108
|
for (const k of Object.keys(d)) if (!pkCols.includes(k)) data[k] = d[k];
|
|
2402
3109
|
}
|
|
@@ -2525,6 +3232,52 @@ class SQLExecutor {
|
|
|
2525
3232
|
delete this.engine._views[key];
|
|
2526
3233
|
return { ok: true, type: 'dropView', message: `View '${statement.name}' dropped`, affectedRows: 0, warnings: [] };
|
|
2527
3234
|
}
|
|
3235
|
+
case 'createIndex': {
|
|
3236
|
+
const tName = statement.table;
|
|
3237
|
+
if (typeof this.engine.hasTable === 'function' && !this.engine.hasTable(tName)) {
|
|
3238
|
+
throw new Error(`Table '${tName}' does not exist`);
|
|
3239
|
+
}
|
|
3240
|
+
let handled = false;
|
|
3241
|
+
if (typeof this.engine.createIndex === 'function') {
|
|
3242
|
+
await this.engine.createIndex(tName, statement.columns, { unique: statement.unique, name: statement.name });
|
|
3243
|
+
handled = true;
|
|
3244
|
+
} else if (typeof this.engine._ensureTable === 'function') {
|
|
3245
|
+
const tbl = this.engine._ensureTable(tName) || (this.engine._tables && this.engine._tables[tName]);
|
|
3246
|
+
if (tbl && typeof tbl.createIndex === 'function') {
|
|
3247
|
+
// Table.createIndex 目前是单字段接口,复合索引按逐列建
|
|
3248
|
+
for (const col of statement.columns) tbl.createIndex(col);
|
|
3249
|
+
handled = true;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
if (!handled) {
|
|
3253
|
+
throw new Error(
|
|
3254
|
+
`CREATE INDEX is not supported by this engine (no createIndex/_ensureTable on the engine object)`
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
return {
|
|
3258
|
+
ok: true, type: 'createIndex', table: tName, name: statement.name,
|
|
3259
|
+
affectedRows: 0, warnings: [],
|
|
3260
|
+
message: `Index '${statement.name}' created on ${tName}(${statement.columns.join(', ')})`,
|
|
3261
|
+
};
|
|
3262
|
+
}
|
|
3263
|
+
case 'dropIndex': {
|
|
3264
|
+
const tName2 = statement.table;
|
|
3265
|
+
let handled = false;
|
|
3266
|
+
if (typeof this.engine.dropIndex === 'function') {
|
|
3267
|
+
await this.engine.dropIndex(tName2, statement.name);
|
|
3268
|
+
handled = true;
|
|
3269
|
+
} else if (tName2 && typeof this.engine._ensureTable === 'function') {
|
|
3270
|
+
const tbl = this.engine._ensureTable(tName2) || (this.engine._tables && this.engine._tables[tName2]);
|
|
3271
|
+
if (tbl && typeof tbl.dropIndex === 'function') { tbl.dropIndex(statement.name); handled = true; }
|
|
3272
|
+
}
|
|
3273
|
+
if (!handled) {
|
|
3274
|
+
if (statement.ifExists) {
|
|
3275
|
+
return { ok: true, type: 'dropIndex', affectedRows: 0, skipped: true, warnings: [], message: `Index '${statement.name}' did not exist` };
|
|
3276
|
+
}
|
|
3277
|
+
throw new Error(`DROP INDEX is not supported by this engine (no dropIndex on the engine/table object)`);
|
|
3278
|
+
}
|
|
3279
|
+
return { ok: true, type: 'dropIndex', table: tName2, name: statement.name, affectedRows: 0, warnings: [], message: `Index '${statement.name}' dropped` };
|
|
3280
|
+
}
|
|
2528
3281
|
case 'savepoint': {
|
|
2529
3282
|
// 引擎侧没有保存点/快照能力。这里只登记名字,绝不假装能回滚。
|
|
2530
3283
|
if (!this.engine._savepoints) this.engine._savepoints = new Set();
|
|
@@ -2582,11 +3335,27 @@ class SQLExecutor {
|
|
|
2582
3335
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
2583
3336
|
const ids = [];
|
|
2584
3337
|
const removedRows = [];
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
3338
|
+
let candidates = all.filter(row => !statement.where || evaluateExpr(statement.where, row, this.ctx));
|
|
3339
|
+
// DELETE ... ORDER BY ... LIMIT n
|
|
3340
|
+
if (statement.orderBy) {
|
|
3341
|
+
const cmp = (a, b) => {
|
|
3342
|
+
for (const o of statement.orderBy) {
|
|
3343
|
+
const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
|
|
3344
|
+
const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
|
|
3345
|
+
if (av === bv) continue;
|
|
3346
|
+
if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
|
|
3347
|
+
if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
|
|
3348
|
+
const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
|
|
3349
|
+
if (d !== 0) return o.dir === 'asc' ? d : -d;
|
|
3350
|
+
}
|
|
3351
|
+
return 0;
|
|
3352
|
+
};
|
|
3353
|
+
candidates = candidates.slice().sort(cmp);
|
|
3354
|
+
}
|
|
3355
|
+
if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
|
|
3356
|
+
for (const row of candidates) {
|
|
3357
|
+
const id = this._rowPkId(row, pkCols);
|
|
3358
|
+
if (id !== undefined) { ids.push(id); removedRows.push(row); }
|
|
2590
3359
|
}
|
|
2591
3360
|
if (ids.length > 0) {
|
|
2592
3361
|
if (this.engine.removeByIds) await this.engine.removeByIds(statement.table, ids);
|
|
@@ -3114,12 +3883,28 @@ class SQLExecutor {
|
|
|
3114
3883
|
return out;
|
|
3115
3884
|
}
|
|
3116
3885
|
|
|
3117
|
-
_aggValue(rows, fn, column) {
|
|
3118
|
-
if (fn === 'COUNT') return rows.length;
|
|
3886
|
+
_aggValue(rows, fn, column, distinct, separator, orderBy) {
|
|
3119
3887
|
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
3120
|
-
|
|
3888
|
+
// COUNT(*) / COUNT():参数为 star 或缺失,直接按行数算
|
|
3889
|
+
const noColumn = !op || op.type === 'star';
|
|
3890
|
+
let values = noColumn
|
|
3121
3891
|
? rows.map(r => 1)
|
|
3122
|
-
:
|
|
3892
|
+
: op && op.type === 'tuple'
|
|
3893
|
+
// COUNT(DISTINCT a, b):先把多列拼成元组再聚合
|
|
3894
|
+
? rows.map(r => JSON.stringify(op.items.map(it => resolveOperand(it, r, this.ctx))))
|
|
3895
|
+
.filter(v => v !== null && v !== undefined)
|
|
3896
|
+
: rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
|
|
3897
|
+
// COUNT(DISTINCT x) / SUM(DISTINCT x):先按值去重再聚合
|
|
3898
|
+
if (distinct) {
|
|
3899
|
+
const seen = new Set();
|
|
3900
|
+
values = values.filter(v => {
|
|
3901
|
+
const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
3902
|
+
if (seen.has(k)) return false;
|
|
3903
|
+
seen.add(k);
|
|
3904
|
+
return true;
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
if (fn === 'COUNT') return values.length;
|
|
3123
3908
|
if (this.ctx && this.ctx.aggregates && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
|
|
3124
3909
|
const agg = this.ctx.aggregates[fn];
|
|
3125
3910
|
if (typeof agg === 'function') {
|
|
@@ -3135,6 +3920,54 @@ class SQLExecutor {
|
|
|
3135
3920
|
if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
3136
3921
|
if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
|
|
3137
3922
|
if (fn === 'MAX') return values.length ? Math.max(...values.map(v => Number(v))) : null;
|
|
3923
|
+
if (fn === 'FIRST') return values.length ? values[0] : null;
|
|
3924
|
+
if (fn === 'LAST') return values.length ? values[values.length - 1] : null;
|
|
3925
|
+
if (fn === 'GROUP_CONCAT') {
|
|
3926
|
+
let src = rows;
|
|
3927
|
+
// GROUP_CONCAT(expr ORDER BY col [DESC]):先按内部排序键排好行,再取值拼接
|
|
3928
|
+
if (orderBy && orderBy.length) {
|
|
3929
|
+
const cmp = (a, b) => {
|
|
3930
|
+
for (const o of orderBy) {
|
|
3931
|
+
const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
|
|
3932
|
+
const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
|
|
3933
|
+
if (av === bv) continue;
|
|
3934
|
+
if (av === null || av === undefined) return o.dir === 'asc' ? 1 : -1;
|
|
3935
|
+
if (bv === null || bv === undefined) return o.dir === 'asc' ? -1 : 1;
|
|
3936
|
+
const d = (typeof av === 'number' && typeof bv === 'number')
|
|
3937
|
+
? av - bv : String(av).localeCompare(String(bv));
|
|
3938
|
+
if (d !== 0) return o.dir === 'asc' ? d : -d;
|
|
3939
|
+
}
|
|
3940
|
+
return 0;
|
|
3941
|
+
};
|
|
3942
|
+
src = rows.slice().sort(cmp);
|
|
3943
|
+
}
|
|
3944
|
+
const parts = src.map(r => resolveOperand(op, r, this.ctx));
|
|
3945
|
+
let joined = parts.filter(v => v !== null && v !== undefined);
|
|
3946
|
+
if (distinct) {
|
|
3947
|
+
const seen = new Set();
|
|
3948
|
+
joined = joined.filter(v => {
|
|
3949
|
+
const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
3950
|
+
if (seen.has(k)) return false;
|
|
3951
|
+
seen.add(k);
|
|
3952
|
+
return true;
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
if (joined.length === 0) return null;
|
|
3956
|
+
const sep = (separator !== undefined && separator !== null) ? String(separator) : ',';
|
|
3957
|
+
return joined.map(v => (v === null || v === undefined ? '' : String(v))).join(sep);
|
|
3958
|
+
}
|
|
3959
|
+
// 标准差 / 方差族:_POP 为总体、_SAMP(及无后缀 VAR/STDDEV)为样本
|
|
3960
|
+
if (fn === 'STDDEV' || fn === 'STDDEV_POP' || fn === 'STDDEV_SAMP' ||
|
|
3961
|
+
fn === 'VARIANCE' || fn === 'VAR_POP' || fn === 'VAR_SAMP') {
|
|
3962
|
+
const nums = values.map(v => Number(v)).filter(v => Number.isFinite(v));
|
|
3963
|
+
if (nums.length === 0) return null;
|
|
3964
|
+
const pop = (fn === 'STDDEV_POP' || fn === 'VAR_POP');
|
|
3965
|
+
const mean = nums.reduce((s, v) => s + v, 0) / nums.length;
|
|
3966
|
+
const denom = pop ? nums.length : nums.length - 1; // 样本方差用 n-1
|
|
3967
|
+
if (denom <= 0) return null;
|
|
3968
|
+
const variance = nums.reduce((s, v) => s + (v - mean) * (v - mean), 0) / denom;
|
|
3969
|
+
return fn.indexOf('STDDEV') === 0 ? Math.sqrt(variance) : variance;
|
|
3970
|
+
}
|
|
3138
3971
|
return null;
|
|
3139
3972
|
}
|
|
3140
3973
|
|
|
@@ -3142,7 +3975,7 @@ class SQLExecutor {
|
|
|
3142
3975
|
if (!node || typeof node !== 'object') return;
|
|
3143
3976
|
if (node.type === 'aggregate') {
|
|
3144
3977
|
node.type = 'value';
|
|
3145
|
-
node.value = this._aggValue(group, node.fn, node.column);
|
|
3978
|
+
node.value = this._aggValue(group, node.fn, node.column, node.distinct, node.separator, node.orderBy);
|
|
3146
3979
|
delete node.fn;
|
|
3147
3980
|
delete node.column;
|
|
3148
3981
|
return;
|
|
@@ -3211,6 +4044,61 @@ class SQLExecutor {
|
|
|
3211
4044
|
return out;
|
|
3212
4045
|
}
|
|
3213
4046
|
|
|
4047
|
+
/**
|
|
4048
|
+
* 校验语句里引用的列是否真实存在。
|
|
4049
|
+
* 之前 `SELECT nosuchcol FROM t` 会静默返回 null 而不是报错 —— 调用方拿到的
|
|
4050
|
+
* 是"一列 null",往往被当成"数据为空",比直接报 no such column 更难排查。
|
|
4051
|
+
*
|
|
4052
|
+
* 只在能确定列名集合时校验(取样本行的键),空表不敢断言,宁可不报。
|
|
4053
|
+
* 不深入子查询节点:它们有自己的 FROM,列属于另一个作用域。
|
|
4054
|
+
*/
|
|
4055
|
+
_validateColumnRefs(statement, rows) {
|
|
4056
|
+
if (!statement || !Array.isArray(rows) || rows.length === 0) return;
|
|
4057
|
+
const sample = rows[0];
|
|
4058
|
+
if (!sample || typeof sample !== 'object') return;
|
|
4059
|
+
const known = new Set();
|
|
4060
|
+
for (const k of Object.keys(sample)) known.add(String(k).toLowerCase());
|
|
4061
|
+
// HAVING / ORDER BY 允许引用 SELECT 的输出别名(如 HAVING cnt >= 1),
|
|
4062
|
+
// 这些名字不在表里,必须一并视为已知,否则会误报 no such column
|
|
4063
|
+
if (Array.isArray(statement.columns)) {
|
|
4064
|
+
for (const c of statement.columns) {
|
|
4065
|
+
if (c && typeof c.alias === 'string' && c.alias) known.add(c.alias.toLowerCase());
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
const walk = (node) => {
|
|
4070
|
+
if (!node || typeof node !== 'object') return;
|
|
4071
|
+
if (Array.isArray(node)) { for (const n of node) walk(n); return; }
|
|
4072
|
+
// 子查询/嵌套 SELECT 有自己的表,跳过
|
|
4073
|
+
if (node.type === 'select' || node.type === 'subquery') return;
|
|
4074
|
+
if (node.type === 'column' && typeof node.name === 'string') {
|
|
4075
|
+
const n = node.name.toLowerCase();
|
|
4076
|
+
const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
|
|
4077
|
+
if (!known.has(n) && !known.has(base)) {
|
|
4078
|
+
throw new Error(`no such column: ${node.name}`);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
for (const k of Object.keys(node)) {
|
|
4082
|
+
if (k === 'select' || k === 'subquery') continue;
|
|
4083
|
+
walk(node[k]);
|
|
4084
|
+
}
|
|
4085
|
+
};
|
|
4086
|
+
|
|
4087
|
+
walk(statement.columns);
|
|
4088
|
+
walk(statement.where);
|
|
4089
|
+
walk(statement.having);
|
|
4090
|
+
walk(statement.orderBy);
|
|
4091
|
+
if (Array.isArray(statement.groupBy)) {
|
|
4092
|
+
// groupBy 是列名字符串数组
|
|
4093
|
+
for (const g of statement.groupBy) {
|
|
4094
|
+
if (typeof g !== 'string') continue;
|
|
4095
|
+
const n = g.toLowerCase();
|
|
4096
|
+
const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
|
|
4097
|
+
if (!known.has(n) && !known.has(base)) throw new Error(`no such column: ${g}`);
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
|
|
3214
4102
|
/** 收集子树里所有列名(含限定形式 alias.col) */
|
|
3215
4103
|
_collectColumnNames(node, out = []) {
|
|
3216
4104
|
if (!node || typeof node !== 'object') return out;
|
|
@@ -3446,7 +4334,7 @@ class SQLExecutor {
|
|
|
3446
4334
|
case 'MAX': {
|
|
3447
4335
|
const f = frameIdx(pos);
|
|
3448
4336
|
const slice = ordered.slice(f.start, f.end + 1).map(i => rows[i]);
|
|
3449
|
-
v = this._aggValue(slice, w.fn, argNode);
|
|
4337
|
+
v = this._aggValue(slice, w.fn, argNode, !!(w.spec && w.spec.distinct), w.spec && w.spec.separator);
|
|
3450
4338
|
break;
|
|
3451
4339
|
}
|
|
3452
4340
|
default: v = null;
|
|
@@ -3639,6 +4527,7 @@ class SQLExecutor {
|
|
|
3639
4527
|
}
|
|
3640
4528
|
|
|
3641
4529
|
let rows = all;
|
|
4530
|
+
this._validateColumnRefs(statement, rows);
|
|
3642
4531
|
if (statement.where) {
|
|
3643
4532
|
// 相关子查询:必须逐行重新求值,否则外层引用拿不到当前行的值
|
|
3644
4533
|
const outerAliases = this._outerAliases(statement);
|
|
@@ -3681,7 +4570,7 @@ class SQLExecutor {
|
|
|
3681
4570
|
for (const c of statement.columns) {
|
|
3682
4571
|
if (c.aggregate) {
|
|
3683
4572
|
const group = r._group || [r];
|
|
3684
|
-
const v = this._aggValue(group, c.aggregate || 'COUNT', c.column);
|
|
4573
|
+
const v = this._aggValue(group, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
3685
4574
|
ctx['__agg_' + (c.alias || c.column || 'COUNT(*)')] = v;
|
|
3686
4575
|
if (c.column) ctx[c.column] = v;
|
|
3687
4576
|
if (c.alias) ctx[c.alias] = v;
|
|
@@ -3702,7 +4591,7 @@ class SQLExecutor {
|
|
|
3702
4591
|
const key = JSON.stringify(statement.columns.map(c => {
|
|
3703
4592
|
if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
|
|
3704
4593
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
3705
|
-
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column);
|
|
4594
|
+
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
3706
4595
|
return resolveOperand({ type: 'column', name: c.expr }, r, this.ctx);
|
|
3707
4596
|
}));
|
|
3708
4597
|
if (seen.has(key)) return false;
|
|
@@ -3717,7 +4606,7 @@ class SQLExecutor {
|
|
|
3717
4606
|
if (!statement.groupBy && (statement.columns.some(c => c.aggregate && !c.window) || hasCustomAgg)) {
|
|
3718
4607
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
3719
4608
|
const valueOf = (c) => {
|
|
3720
|
-
if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column);
|
|
4609
|
+
if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
3721
4610
|
if (c.scalar && c.scalar.type === 'func' && hasCustomAgg) {
|
|
3722
4611
|
const fn = String(c.scalar.name).toUpperCase();
|
|
3723
4612
|
if (Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
|
|
@@ -3986,6 +4875,44 @@ function applyParams(sql, values) {
|
|
|
3986
4875
|
return out;
|
|
3987
4876
|
}
|
|
3988
4877
|
|
|
4878
|
+
// 把执行器结果补齐为 README 承诺的结果信封
|
|
4879
|
+
// (columnTypes / rowCount / message / command / durationMs / warnings)
|
|
4880
|
+
function buildResultEnvelope(stmt, r, durationMs) {
|
|
4881
|
+
if (r === null || typeof r !== 'object' || Array.isArray(r)) return r;
|
|
4882
|
+
const out = { ...r };
|
|
4883
|
+
const t = stmt && stmt.type;
|
|
4884
|
+
if (out.command === undefined) out.command = String(t || '').toUpperCase();
|
|
4885
|
+
if (out.affectedRows === undefined) out.affectedRows = 0;
|
|
4886
|
+
if (out.rowCount === undefined) {
|
|
4887
|
+
out.rowCount = Array.isArray(out.rows) ? out.rows.length
|
|
4888
|
+
: (typeof out.affectedRows === 'number' ? out.affectedRows : 0);
|
|
4889
|
+
}
|
|
4890
|
+
if (out.warnings === undefined) out.warnings = [];
|
|
4891
|
+
if (out.durationMs === undefined) out.durationMs = durationMs;
|
|
4892
|
+
if (out.message === undefined) {
|
|
4893
|
+
out.message = t === 'select'
|
|
4894
|
+
? `${out.rowCount} row${out.rowCount === 1 ? '' : 's'} selected`
|
|
4895
|
+
: `${out.affectedRows} row${out.affectedRows === 1 ? '' : 's'} affected`;
|
|
4896
|
+
}
|
|
4897
|
+
return out;
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
// SELECT 结果的列类型:单表查询按 schema 推断;表达式/多表查询该列为 null
|
|
4901
|
+
async function inferColumnTypes(engine, stmt, r) {
|
|
4902
|
+
if (!r || !Array.isArray(r.columns)) return undefined;
|
|
4903
|
+
let schema = null;
|
|
4904
|
+
try {
|
|
4905
|
+
const t = stmt && stmt.type === 'select' && stmt.from &&
|
|
4906
|
+
Array.isArray(stmt.from.tables) && stmt.from.tables.length === 1
|
|
4907
|
+
? stmt.from.tables[0].table : null;
|
|
4908
|
+
if (t && engine && typeof engine.getTableSchema === 'function') schema = await engine.getTableSchema(t);
|
|
4909
|
+
} catch (e) { schema = null; }
|
|
4910
|
+
return r.columns.map((name) => {
|
|
4911
|
+
const def = schema && schema[name];
|
|
4912
|
+
return def && def.type ? sqlTypeName(def.type).toUpperCase() : null;
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
|
|
3989
4916
|
async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
3990
4917
|
// 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
|
|
3991
4918
|
// 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
|
|
@@ -4035,7 +4962,14 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
4035
4962
|
const results = [];
|
|
4036
4963
|
for (const stmtSql of statements) {
|
|
4037
4964
|
const stmt = parseSQL(stmtSql);
|
|
4038
|
-
|
|
4965
|
+
const t0 = process.hrtime.bigint();
|
|
4966
|
+
let r = await executor.execute(stmt);
|
|
4967
|
+
const durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
4968
|
+
r = buildResultEnvelope(stmt, r, Math.round(durationMs * 1000) / 1000);
|
|
4969
|
+
if (r && r.columns && r.columnTypes === undefined) {
|
|
4970
|
+
r.columnTypes = await inferColumnTypes(engine, stmt, r);
|
|
4971
|
+
}
|
|
4972
|
+
results.push(r);
|
|
4039
4973
|
}
|
|
4040
4974
|
return results.length === 1 ? results[0] : results;
|
|
4041
4975
|
}
|