jsql-neo 5.4.2 → 6.0.0-beta2
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 +198 -14
- package/index.d.ts +29 -0
- package/index.js +12 -2
- package/lib/ast.js +214 -0
- package/lib/database.js +26 -60
- package/lib/mongo_server.js +1 -1
- package/lib/native_client.js +43 -66
- package/lib/plugin.js +227 -3
- package/lib/plugins/audit.js +37 -0
- package/lib/plugins/index.js +72 -0
- package/lib/plugins/logger.js +35 -0
- package/lib/plugins/metrics.js +38 -0
- package/lib/plugins/timestamps.js +43 -0
- package/lib/plugins/validation.js +128 -0
- package/lib/sql.js +1212 -84
- package/lib/wasm_client.js +41 -57
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +8 -1
- package/nativesrc/jsql-neo-core/Cargo.toml +3 -1
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +50 -13
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +46 -23
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +13 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +338 -42
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +245 -32
- package/nativesrc/jsql-neo-native/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-native/Cargo.toml +9 -1
- package/nativesrc/jsql-neo-native/src/lib.rs +14 -2
- package/nativesrc/jsql-neo-wasm/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-wasm/Cargo.toml +8 -1
- package/nativesrc/jsql-neo-wasm/src/lib.rs +18 -2
- package/package.json +4 -3
- package/test/sql-parser.test.js +450 -0
- package/wasm/browser.mjs +12 -0
- package/wasm/browser_bg.mjs +269 -248
- package/wasm/jsql_neo_wasm.d.ts +6 -0
- package/wasm/jsql_neo_wasm.js +248 -127
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +22 -22
- package/wasm/package.json +1 -1
package/lib/sql.js
CHANGED
|
@@ -56,7 +56,13 @@ const KEYWORDS = new Set([
|
|
|
56
56
|
'BETWEEN', 'USING', 'FULL', 'UNSIGNED', 'ZEROFILL', 'TRUNCATE', 'COLLATE', 'CHARACTER',
|
|
57
57
|
'ALTER', 'ADD', 'COLUMN', 'MODIFY', 'CHANGE', 'INDEX', 'FOREIGN', 'REFERENCES',
|
|
58
58
|
'CONSTRAINT', 'RENAME', 'TO', 'AFTER', 'FIRST', 'ENGINE', 'AUTO_INCREMENT', 'SPATIAL',
|
|
59
|
-
'REGEXP', 'TRUE', 'FALSE', 'RLIKE', 'PRAGMA', 'REPLACE', 'BLOB', 'RAISE', 'IGNORE'
|
|
59
|
+
'REGEXP', 'TRUE', 'FALSE', 'RLIKE', 'PRAGMA', 'REPLACE', 'BLOB', 'RAISE', 'IGNORE',
|
|
60
|
+
// 5.5.0 新增:CTE 与集合运算符
|
|
61
|
+
'WITH', 'RECURSIVE', 'INTERSECT', 'EXCEPT'
|
|
62
|
+
// 注意:OVER / PARTITION / UNBOUNDED / PRECEDING / FOLLOWING 等刻意不加入此表。
|
|
63
|
+
// 它们同时是常见标识符(列名 rank、current、row…),若被词法层标记为 keyword
|
|
64
|
+
// 会让 `SELECT rank FROM t` 之类的既有语句解析失败。这些词改由 Parser.isWord()
|
|
65
|
+
// 在语法层按上下文大小写不敏感地匹配,从而不破坏既有解析行为。
|
|
60
66
|
]);
|
|
61
67
|
|
|
62
68
|
function tokenize(sql) {
|
|
@@ -160,6 +166,17 @@ function tokenize(sql) {
|
|
|
160
166
|
continue;
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
// 参数占位符:? (顺序)、?N (编号)、?? (标识符占位)
|
|
170
|
+
if (c === '?') {
|
|
171
|
+
if (sql[i + 1] === '?') { tokens.push(new SQLToken('param', '??', i)); i += 2; continue; }
|
|
172
|
+
let j = i + 1;
|
|
173
|
+
let num = '';
|
|
174
|
+
while (j < n && sql[j] >= '0' && sql[j] <= '9') { num += sql[j]; j++; }
|
|
175
|
+
tokens.push(new SQLToken('param', num ? parseInt(num, 10) : null, i));
|
|
176
|
+
i = j;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
163
180
|
const two = sql.slice(i, i + 2);
|
|
164
181
|
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==') {
|
|
165
182
|
tokens.push(new SQLToken('op', two, i));
|
|
@@ -184,6 +201,7 @@ class Parser {
|
|
|
184
201
|
constructor(tokens) {
|
|
185
202
|
this.tokens = tokens;
|
|
186
203
|
this.pos = 0;
|
|
204
|
+
this.paramCount = 0; // 顺序占位符 ? 的序号(从 0 开始)
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
peek(offset = 0) { return this.tokens[this.pos + offset]; }
|
|
@@ -220,27 +238,70 @@ class Parser {
|
|
|
220
238
|
parseStatement() {
|
|
221
239
|
const t = this.peek();
|
|
222
240
|
if (t.type === 'eof') return null;
|
|
223
|
-
if (t.type !== 'keyword')
|
|
241
|
+
if (t.type !== 'keyword') {
|
|
242
|
+
// 少数语句起始词刻意不进关键字表(它们同时是常见标识符),按上下文识别
|
|
243
|
+
const w = String(t.value).toUpperCase();
|
|
244
|
+
if (w === 'EXPLAIN') {
|
|
245
|
+
this.next();
|
|
246
|
+
if (this.isWord('ANALYZE')) this.next();
|
|
247
|
+
const stmt = this.parseStatement();
|
|
248
|
+
if (!stmt) throw new Error('EXPLAIN requires a following statement');
|
|
249
|
+
return { type: 'explain', statement: stmt };
|
|
250
|
+
}
|
|
251
|
+
if (w === 'SAVEPOINT') {
|
|
252
|
+
this.next();
|
|
253
|
+
const nm = this.next();
|
|
254
|
+
if (nm.type !== 'ident') throw new Error(`Expected savepoint name, got '${nm.value}'`);
|
|
255
|
+
this.optionalTailSemicolon();
|
|
256
|
+
return { type: 'savepoint', name: nm.value };
|
|
257
|
+
}
|
|
258
|
+
if (w === 'RELEASE') {
|
|
259
|
+
this.next();
|
|
260
|
+
if (this.isWord('SAVEPOINT')) this.next();
|
|
261
|
+
const nm = this.next();
|
|
262
|
+
if (nm.type !== 'ident') throw new Error(`Expected savepoint name, got '${nm.value}'`);
|
|
263
|
+
this.optionalTailSemicolon();
|
|
264
|
+
return { type: 'releaseSavepoint', name: nm.value };
|
|
265
|
+
}
|
|
266
|
+
throw new Error(`Expected SQL statement, got '${t.value}'`);
|
|
267
|
+
}
|
|
224
268
|
|
|
225
269
|
switch (t.value) {
|
|
226
270
|
case 'CREATE':
|
|
227
271
|
if (this.isKeyword('TABLE', 1)) return this.parseCreateTable();
|
|
228
272
|
if (this.isKeyword('DATABASE', 1)) return this.parseCreateDatabase();
|
|
273
|
+
if (this.isWord('VIEW', 1) || this.isWord('OR', 1)) return this.parseCreateView();
|
|
229
274
|
throw new Error('Unsupported CREATE statement');
|
|
230
275
|
case 'DROP':
|
|
231
276
|
if (this.isKeyword('TABLE', 1)) return this.parseDropTable();
|
|
232
277
|
if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
|
|
278
|
+
if (this.isWord('VIEW', 1)) return this.parseDropView();
|
|
233
279
|
throw new Error('Unsupported DROP statement');
|
|
234
280
|
case 'INSERT': return this.parseInsert();
|
|
281
|
+
case 'REPLACE': return this.parseReplace();
|
|
235
282
|
case 'ALTER': return this.parseAlter();
|
|
236
283
|
case 'TRUNCATE': this.expectKeyword('TRUNCATE'); if (this.isKeyword('TABLE')) this.next(); return { type: 'truncate', name: this.parseTableName() };
|
|
237
284
|
case 'SELECT': return this.parseSelect();
|
|
285
|
+
case 'WITH': return this.parseWith();
|
|
238
286
|
case 'UPDATE': return this.parseUpdate();
|
|
239
287
|
case 'DELETE': return this.parseDelete();
|
|
240
288
|
case 'BEGIN': this.expectKeyword('BEGIN'); this.optionalTransaction(); return { type: 'begin' };
|
|
241
289
|
case 'START': this.expectKeyword('START'); if (this.isKeyword('TRANSACTION')) this.next(); return { type: 'begin' };
|
|
242
290
|
case 'COMMIT': this.expectKeyword('COMMIT'); this.optionalTransaction(); return { type: 'commit' };
|
|
243
|
-
case 'ROLLBACK':
|
|
291
|
+
case 'ROLLBACK': {
|
|
292
|
+
this.expectKeyword('ROLLBACK');
|
|
293
|
+
// ROLLBACK TO [SAVEPOINT] name —— 回滚到保存点而非回滚整个事务
|
|
294
|
+
if (this.isKeyword('TO')) {
|
|
295
|
+
this.next();
|
|
296
|
+
if (this.isWord('SAVEPOINT')) this.next();
|
|
297
|
+
const nm = this.next();
|
|
298
|
+
if (nm.type !== 'ident') throw new Error(`Expected savepoint name, got '${nm.value}'`);
|
|
299
|
+
this.optionalTailSemicolon();
|
|
300
|
+
return { type: 'rollbackTo', name: nm.value };
|
|
301
|
+
}
|
|
302
|
+
this.optionalTransaction();
|
|
303
|
+
return { type: 'rollback' };
|
|
304
|
+
}
|
|
244
305
|
case 'SHOW': return this.parseShow();
|
|
245
306
|
case 'SET': return this.parseSet();
|
|
246
307
|
case 'DESCRIBE': case 'DESC': return this.parseDescribe();
|
|
@@ -255,6 +316,54 @@ class Parser {
|
|
|
255
316
|
if (this.isKeyword('WORK')) this.next();
|
|
256
317
|
}
|
|
257
318
|
|
|
319
|
+
/**
|
|
320
|
+
* CREATE [OR REPLACE] VIEW name [(cols)] AS SELECT ...
|
|
321
|
+
* 视图在语法层解析成 { type:'createView', name, columns, select };
|
|
322
|
+
* 执行层由引擎侧决定如何注册(不存在视图存储时退化为"可用即查询")。
|
|
323
|
+
*/
|
|
324
|
+
parseCreateView() {
|
|
325
|
+
this.expectKeyword('CREATE');
|
|
326
|
+
let orReplace = false;
|
|
327
|
+
if (this.isWord('OR')) {
|
|
328
|
+
this.next();
|
|
329
|
+
if (!this.isWord('REPLACE')) throw new Error(`Expected REPLACE after OR, got '${this.peek().value}'`);
|
|
330
|
+
this.next();
|
|
331
|
+
orReplace = true;
|
|
332
|
+
}
|
|
333
|
+
if (!this.isWord('VIEW')) throw new Error(`Expected VIEW, got '${this.peek().value}'`);
|
|
334
|
+
this.next();
|
|
335
|
+
const name = this.parseTableName();
|
|
336
|
+
let columns = null;
|
|
337
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
338
|
+
this.next();
|
|
339
|
+
columns = [];
|
|
340
|
+
for (;;) {
|
|
341
|
+
columns.push(this.parseColumnRef());
|
|
342
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
this.expect('op', ')');
|
|
346
|
+
}
|
|
347
|
+
if (!this.isKeyword('AS')) throw new Error(`Expected AS in CREATE VIEW, got '${this.peek().value}'`);
|
|
348
|
+
this.next();
|
|
349
|
+
const select = this.parseSelect();
|
|
350
|
+
this.optionalTailSemicolon();
|
|
351
|
+
return { type: 'createView', name, columns, select, orReplace };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
parseDropView() {
|
|
355
|
+
this.expectKeyword('DROP');
|
|
356
|
+
if (!this.isWord('VIEW')) throw new Error(`Expected VIEW, got '${this.peek().value}'`);
|
|
357
|
+
this.next();
|
|
358
|
+
let ifExists = false;
|
|
359
|
+
if (this.isKeyword('IF')) {
|
|
360
|
+
this.expectKeyword('IF'); this.expectKeyword('EXISTS'); ifExists = true;
|
|
361
|
+
}
|
|
362
|
+
const name = this.parseTableName();
|
|
363
|
+
this.optionalTailSemicolon();
|
|
364
|
+
return { type: 'dropView', name, ifExists };
|
|
365
|
+
}
|
|
366
|
+
|
|
258
367
|
parseTableName() {
|
|
259
368
|
const t = this.next();
|
|
260
369
|
if (t.type !== 'ident') throw new Error(`Expected table name, got '${t.value}'`);
|
|
@@ -425,9 +534,26 @@ class Parser {
|
|
|
425
534
|
}
|
|
426
535
|
}
|
|
427
536
|
|
|
537
|
+
// REPLACE INTO t ... —— 等价于"存在则替换",解析结构与 INSERT 一致
|
|
538
|
+
parseReplace() {
|
|
539
|
+
this.expectKeyword('REPLACE');
|
|
540
|
+
if (this.isKeyword('INTO')) this.next();
|
|
541
|
+
const stmt = this.parseInsertBody();
|
|
542
|
+
stmt.replace = true;
|
|
543
|
+
return stmt;
|
|
544
|
+
}
|
|
545
|
+
|
|
428
546
|
parseInsert() {
|
|
429
547
|
this.expectKeyword('INSERT');
|
|
548
|
+
let ignore = false;
|
|
549
|
+
if (this.isKeyword('IGNORE')) { this.next(); ignore = true; }
|
|
430
550
|
this.expectKeyword('INTO');
|
|
551
|
+
const stmt = this.parseInsertBody();
|
|
552
|
+
stmt.ignore = ignore;
|
|
553
|
+
return stmt;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
parseInsertBody() {
|
|
431
557
|
const name = this.parseTableName();
|
|
432
558
|
let columns = null;
|
|
433
559
|
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
@@ -440,6 +566,12 @@ class Parser {
|
|
|
440
566
|
}
|
|
441
567
|
this.expect('op', ')');
|
|
442
568
|
}
|
|
569
|
+
// INSERT INTO t [(cols)] SELECT ... —— 与 VALUES 互斥的另一条插入来源
|
|
570
|
+
if (this.isKeyword('SELECT')) {
|
|
571
|
+
const select = this.parseSelect();
|
|
572
|
+
this.optionalTailSemicolon();
|
|
573
|
+
return { type: 'insert', name, columns, select, dataRows: null, values: null, onDuplicate: null };
|
|
574
|
+
}
|
|
443
575
|
this.expectKeyword('VALUES');
|
|
444
576
|
const rows = [];
|
|
445
577
|
while (true) {
|
|
@@ -468,31 +600,94 @@ class Parser {
|
|
|
468
600
|
});
|
|
469
601
|
|
|
470
602
|
let onDuplicate = null;
|
|
603
|
+
let onConflict = null;
|
|
471
604
|
if (this.isKeyword('ON')) {
|
|
472
605
|
this.expectKeyword('ON');
|
|
473
|
-
|
|
474
|
-
this.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
606
|
+
// PostgreSQL: ON CONFLICT [(cols)] DO NOTHING | DO UPDATE SET ...
|
|
607
|
+
if (this.isWord('CONFLICT')) {
|
|
608
|
+
this.next();
|
|
609
|
+
let target = null;
|
|
610
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
611
|
+
this.next();
|
|
612
|
+
target = [];
|
|
613
|
+
for (;;) {
|
|
614
|
+
target.push(this.parseColumnRef());
|
|
615
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
this.expect('op', ')');
|
|
619
|
+
}
|
|
620
|
+
if (!this.isWord('DO')) throw new Error(`Expected DO after ON CONFLICT, got '${this.peek().value}'`);
|
|
621
|
+
this.next();
|
|
622
|
+
if (this.isWord('NOTHING')) { this.next(); onConflict = { target, action: 'nothing' }; }
|
|
623
|
+
else if (this.isWord('UPDATE')) {
|
|
624
|
+
this.next();
|
|
625
|
+
if (!this.isKeyword('SET')) throw new Error(`Expected SET after DO UPDATE, got '${this.peek().value}'`);
|
|
626
|
+
this.next();
|
|
627
|
+
const sets = [];
|
|
628
|
+
for (;;) {
|
|
629
|
+
const col = this.parseColumnRef();
|
|
630
|
+
this.expect('op', '=');
|
|
631
|
+
const val = this.parseValue();
|
|
632
|
+
sets.push([col, val]);
|
|
633
|
+
if (this.peek().value === ',') { this.next(); continue; }
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
onConflict = { target, action: 'update', sets };
|
|
637
|
+
} else {
|
|
638
|
+
throw new Error(`Expected NOTHING or UPDATE after DO, got '${this.peek().value}'`);
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
this.expectKeyword('DUPLICATE');
|
|
642
|
+
this.expectKeyword('KEY');
|
|
643
|
+
this.expectKeyword('UPDATE');
|
|
644
|
+
onDuplicate = [];
|
|
645
|
+
while (true) {
|
|
646
|
+
const col = this.parseTableName();
|
|
647
|
+
this.expect('op', '=');
|
|
648
|
+
const val = this.parseValue();
|
|
649
|
+
onDuplicate.push([col, val]);
|
|
650
|
+
if (this.peek().value === ',') { this.next(); continue; }
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
484
653
|
}
|
|
485
654
|
}
|
|
486
655
|
this.optionalTailSemicolon();
|
|
487
656
|
|
|
488
|
-
|
|
657
|
+
// RETURNING * | RETURNING col [, col]...
|
|
658
|
+
const returning = this.parseOptionalReturning();
|
|
659
|
+
return { type: 'insert', name, columns, dataRows: columns ? dataRows : null, values: columns ? null : rows, onDuplicate, onConflict, returning };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* 冲突处理策略 —— 把四种写法归一成一个动作:
|
|
664
|
+
* REPLACE INTO → 'replace'
|
|
665
|
+
* INSERT IGNORE / ON CONFLICT DO NOTHING → 'ignore'
|
|
666
|
+
* ON DUPLICATE KEY UPDATE / ON CONFLICT DO UPDATE → 'update'
|
|
667
|
+
* 默认 → 'throw'(ER_DUP_ENTRY)
|
|
668
|
+
*/
|
|
669
|
+
/** 解析可选的 RETURNING 子句(INSERT / UPDATE / DELETE 通用) */
|
|
670
|
+
parseOptionalReturning() {
|
|
671
|
+
if (!this.isWord('RETURNING')) return null;
|
|
672
|
+
this.next();
|
|
673
|
+
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); return ['*']; }
|
|
674
|
+
const cols = [];
|
|
675
|
+
for (;;) {
|
|
676
|
+
cols.push(this.parseColumnRef());
|
|
677
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
return cols;
|
|
489
681
|
}
|
|
490
682
|
|
|
491
683
|
parseValue() {
|
|
492
684
|
const t = this.next();
|
|
493
685
|
if (t.type === 'number' || t.type === 'string') return t.value;
|
|
494
686
|
if (t.type === 'keyword' && t.value === 'NULL') return null;
|
|
687
|
+
if (t.type === 'keyword' && t.value === 'TRUE') return true;
|
|
688
|
+
if (t.type === 'keyword' && t.value === 'FALSE') return false;
|
|
495
689
|
if (t.type === 'keyword' && t.value === 'DEFAULT') return { _default: true };
|
|
690
|
+
if (t.type === 'param') return { type: 'param', index: t.value, position: this.paramCount++ };
|
|
496
691
|
if (t.type === 'op' && t.value === '-') {
|
|
497
692
|
const num = this.next();
|
|
498
693
|
if (num.type !== 'number') throw new Error('Expected number after -');
|
|
@@ -506,6 +701,126 @@ class Parser {
|
|
|
506
701
|
throw new Error(`Expected value, got '${t.value}'`);
|
|
507
702
|
}
|
|
508
703
|
|
|
704
|
+
/**
|
|
705
|
+
* WITH [RECURSIVE] name [(col, ...)] AS (SELECT ...), ... <statement>
|
|
706
|
+
* CTE 在语法层解析为 { type:'with', ctes:[{name, columns, select}], statement }。
|
|
707
|
+
* 执行层会把 CTE 内联为 FROM 子查询,因此不需要引擎侧临时表。
|
|
708
|
+
*/
|
|
709
|
+
parseWith() {
|
|
710
|
+
this.expectKeyword('WITH');
|
|
711
|
+
let recursive = false;
|
|
712
|
+
if (this.isKeyword('RECURSIVE')) { this.next(); recursive = true; }
|
|
713
|
+
const ctes = [];
|
|
714
|
+
for (;;) {
|
|
715
|
+
const nameTok = this.next();
|
|
716
|
+
if (nameTok.type !== 'ident') throw new Error(`Expected CTE name, got '${nameTok.value}'`);
|
|
717
|
+
let columns = null;
|
|
718
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
719
|
+
this.next();
|
|
720
|
+
columns = [];
|
|
721
|
+
for (;;) {
|
|
722
|
+
columns.push(this.parseColumnRef());
|
|
723
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
this.expect('op', ')');
|
|
727
|
+
}
|
|
728
|
+
this.expectKeyword('AS');
|
|
729
|
+
this.expect('op', '(');
|
|
730
|
+
const select = this.parseSelect();
|
|
731
|
+
this.expect('op', ')');
|
|
732
|
+
ctes.push({ name: nameTok.value, columns, select, recursive });
|
|
733
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
const statement = this.parseStatement();
|
|
737
|
+
if (!statement) throw new Error('WITH requires a following SELECT/INSERT/UPDATE/DELETE statement');
|
|
738
|
+
return { type: 'with', recursive, ctes, statement };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* 可选的窗口定义:OVER ( [PARTITION BY ...] [ORDER BY ...] [frame] ) 或 OVER win_name
|
|
743
|
+
* 只在真的出现 OVER 时才改写节点,因此不影响既有 func / aggregate 节点形状。
|
|
744
|
+
*/
|
|
745
|
+
parseOptionalOver(node) {
|
|
746
|
+
if (!this.isWord('OVER')) return node;
|
|
747
|
+
this.next();
|
|
748
|
+
// 命名窗口:OVER w(后面必须不是 '(' )
|
|
749
|
+
if (!(this.peek().type === 'op' && this.peek().value === '(')) {
|
|
750
|
+
const nameTok = this.next();
|
|
751
|
+
if (nameTok.type === 'eof') throw new Error('Expected window name or "(" after OVER');
|
|
752
|
+
node.over = { windowName: nameTok.value };
|
|
753
|
+
return node;
|
|
754
|
+
}
|
|
755
|
+
this.expect('op', '(');
|
|
756
|
+
const spec = {};
|
|
757
|
+
if (this.isWord('PARTITION')) {
|
|
758
|
+
this.next();
|
|
759
|
+
if (!this.isWord('BY')) throw new Error(`Expected BY after PARTITION, got '${this.peek().value}'`);
|
|
760
|
+
this.next();
|
|
761
|
+
spec.partitionBy = [];
|
|
762
|
+
for (;;) {
|
|
763
|
+
spec.partitionBy.push(this.parseOperand());
|
|
764
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (this.isWord('ORDER')) {
|
|
769
|
+
this.next();
|
|
770
|
+
if (!this.isWord('BY')) throw new Error(`Expected BY after ORDER, got '${this.peek().value}'`);
|
|
771
|
+
this.next();
|
|
772
|
+
spec.orderBy = [];
|
|
773
|
+
for (;;) {
|
|
774
|
+
const col = this.parseOperand();
|
|
775
|
+
let dir = 'asc';
|
|
776
|
+
if (this.isWord('ASC')) { this.next(); }
|
|
777
|
+
else if (this.isWord('DESC')) { this.next(); dir = 'desc'; }
|
|
778
|
+
spec.orderBy.push({ column: col, dir });
|
|
779
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (this.isWord('ROWS') || this.isWord('RANGE')) spec.frame = this.parseFrameSpec();
|
|
784
|
+
this.expect('op', ')');
|
|
785
|
+
node.over = spec;
|
|
786
|
+
return node;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ROWS/RANGE BETWEEN <bound> AND <bound> | ROWS <bound>
|
|
790
|
+
parseFrameSpec() {
|
|
791
|
+
const unit = String(this.next().value).toUpperCase() === 'RANGE' ? 'RANGE' : 'ROWS';
|
|
792
|
+
const bound = () => {
|
|
793
|
+
if (this.isWord('UNBOUNDED')) {
|
|
794
|
+
this.next();
|
|
795
|
+
const d = String(this.next().value).toUpperCase();
|
|
796
|
+
if (d !== 'PRECEDING' && d !== 'FOLLOWING') throw new Error(`Expected PRECEDING or FOLLOWING after UNBOUNDED, got '${d}'`);
|
|
797
|
+
return { type: 'unbounded', direction: d === 'FOLLOWING' ? 'following' : 'preceding' };
|
|
798
|
+
}
|
|
799
|
+
if (this.isWord('CURRENT')) {
|
|
800
|
+
this.next();
|
|
801
|
+
this.next(); // ROW
|
|
802
|
+
return { type: 'currentRow' };
|
|
803
|
+
}
|
|
804
|
+
const nTok = this.next();
|
|
805
|
+
const num = nTok.type === 'number' ? nTok.value : parseInt(String(nTok.value), 10);
|
|
806
|
+
if (!Number.isFinite(num)) throw new Error(`Expected number in window frame bound, got '${nTok.value}'`);
|
|
807
|
+
const d = String(this.next().value).toUpperCase();
|
|
808
|
+
if (d !== 'PRECEDING' && d !== 'FOLLOWING') throw new Error(`Expected PRECEDING or FOLLOWING, got '${d}'`);
|
|
809
|
+
return { type: 'offset', value: num, direction: d === 'FOLLOWING' ? 'following' : 'preceding' };
|
|
810
|
+
};
|
|
811
|
+
let start = null, end = null;
|
|
812
|
+
if (this.isWord('BETWEEN')) {
|
|
813
|
+
this.next();
|
|
814
|
+
start = bound();
|
|
815
|
+
if (!this.isWord('AND')) throw new Error(`Expected AND in window frame, got '${this.peek().value}'`);
|
|
816
|
+
this.next();
|
|
817
|
+
end = bound();
|
|
818
|
+
} else {
|
|
819
|
+
start = bound();
|
|
820
|
+
}
|
|
821
|
+
return { unit, start, end };
|
|
822
|
+
}
|
|
823
|
+
|
|
509
824
|
parseSelect() {
|
|
510
825
|
this.expectKeyword('SELECT');
|
|
511
826
|
let distinct = false;
|
|
@@ -523,8 +838,9 @@ class Parser {
|
|
|
523
838
|
else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
|
|
524
839
|
this.expect('op', ')');
|
|
525
840
|
aggregate = { type: 'COUNT', column: col };
|
|
841
|
+
aggregate = this.parseOptionalOver(aggregate);
|
|
526
842
|
aggregate.alias = this.parseOptionalAlias();
|
|
527
|
-
columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias });
|
|
843
|
+
columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
|
|
528
844
|
} else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
|
|
529
845
|
this.next();
|
|
530
846
|
const fn = t.value;
|
|
@@ -532,8 +848,9 @@ class Parser {
|
|
|
532
848
|
const col = this.parseScalar();
|
|
533
849
|
this.expect('op', ')');
|
|
534
850
|
aggregate = { type: fn, column: col };
|
|
851
|
+
aggregate = this.parseOptionalOver(aggregate);
|
|
535
852
|
aggregate.alias = this.parseOptionalAlias();
|
|
536
|
-
columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias });
|
|
853
|
+
columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
|
|
537
854
|
} else if (t.type === 'op' && t.value === '*') {
|
|
538
855
|
this.next();
|
|
539
856
|
columns.push({ expr: '*' });
|
|
@@ -585,7 +902,28 @@ class Parser {
|
|
|
585
902
|
this.expectKeyword('ORDER'); this.expectKeyword('BY');
|
|
586
903
|
orderBy = [];
|
|
587
904
|
while (true) {
|
|
588
|
-
|
|
905
|
+
// 支持表达式排序:ORDER BY a + b DESC、ORDER BY UPPER(name)
|
|
906
|
+
// 纯列名仍走 parseColumnRef,保持既有字符串形态不破坏下游
|
|
907
|
+
let col;
|
|
908
|
+
const save = this.pos;
|
|
909
|
+
// ORDER BY 项的合法结束符;不是这些就说明是表达式(如 a+b、UPPER(name))
|
|
910
|
+
const ORDER_END = ['ASC', 'DESC', 'LIMIT', 'OFFSET', 'FETCH', 'UNION', 'INTERSECT',
|
|
911
|
+
'EXCEPT', 'RETURNING', 'FOR', 'WITH'];
|
|
912
|
+
const endsClause = (tk) => {
|
|
913
|
+
if (tk.type === 'eof') return true;
|
|
914
|
+
if (tk.type === 'op' && (tk.value === ',' || tk.value === ';' || tk.value === ')')) return true;
|
|
915
|
+
return ORDER_END.some(w => this.isWord(w));
|
|
916
|
+
};
|
|
917
|
+
try {
|
|
918
|
+
col = this.parseColumnRef();
|
|
919
|
+
if (!endsClause(this.peek())) {
|
|
920
|
+
this.pos = save;
|
|
921
|
+
col = this.parseScalar();
|
|
922
|
+
}
|
|
923
|
+
} catch (e) {
|
|
924
|
+
this.pos = save;
|
|
925
|
+
col = this.parseScalar();
|
|
926
|
+
}
|
|
589
927
|
let dir = 'asc';
|
|
590
928
|
if (this.isKeyword('ASC')) { this.next(); }
|
|
591
929
|
else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
|
|
@@ -602,16 +940,49 @@ class Parser {
|
|
|
602
940
|
if (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); if (typeof offset === 'number' && offset < 0) throw new Error(`OFFSET must be a non-negative integer, got ${offset}`); }
|
|
603
941
|
else if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); offset = limit; limit = this.parseValue(); if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`); }
|
|
604
942
|
}
|
|
605
|
-
|
|
943
|
+
// 集合运算:UNION [ALL|DISTINCT] / INTERSECT [ALL|DISTINCT] / EXCEPT [ALL|DISTINCT]
|
|
944
|
+
let union = null, intersect = null, except = null;
|
|
606
945
|
if (this.isKeyword('UNION')) {
|
|
607
946
|
this.next();
|
|
608
947
|
const all = this.isKeyword('ALL');
|
|
609
948
|
if (all) this.next();
|
|
949
|
+
else if (this.isKeyword('DISTINCT')) this.next();
|
|
610
950
|
union = { all, select: this.parseSelect() };
|
|
951
|
+
} else if (this.isKeyword('INTERSECT')) {
|
|
952
|
+
this.next();
|
|
953
|
+
const all = this.isKeyword('ALL');
|
|
954
|
+
if (all) this.next();
|
|
955
|
+
else if (this.isKeyword('DISTINCT')) this.next();
|
|
956
|
+
intersect = { all, select: this.parseSelect() };
|
|
957
|
+
} else if (this.isKeyword('EXCEPT')) {
|
|
958
|
+
this.next();
|
|
959
|
+
const all = this.isKeyword('ALL');
|
|
960
|
+
if (all) this.next();
|
|
961
|
+
else if (this.isKeyword('DISTINCT')) this.next();
|
|
962
|
+
except = { all, select: this.parseSelect() };
|
|
963
|
+
}
|
|
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
|
+
// FETCH FIRST n ROWS ONLY(SQL 标准的 LIMIT 写法)
|
|
972
|
+
if (this.isWord('FETCH')) {
|
|
973
|
+
this.next();
|
|
974
|
+
if (this.isWord('FIRST') || this.isWord('NEXT')) this.next();
|
|
975
|
+
else throw new Error(`Expected FIRST or NEXT after FETCH, got '${this.peek().value}'`);
|
|
976
|
+
const nTok = this.next();
|
|
977
|
+
const n = nTok.type === 'number' ? nTok.value : parseInt(String(nTok.value), 10);
|
|
978
|
+
if (!Number.isFinite(n)) throw new Error(`Expected row count after FETCH FIRST, got '${nTok.value}'`);
|
|
979
|
+
limit = n;
|
|
980
|
+
if (this.isWord('ROW') || this.isWord('ROWS')) this.next();
|
|
981
|
+
if (this.isWord('ONLY')) this.next();
|
|
611
982
|
}
|
|
612
983
|
this.optionalTailSemicolon();
|
|
613
984
|
|
|
614
|
-
return { type: 'select', columns, aggregate, distinct, from, where, groupBy, having, orderBy, limit, offset, union };
|
|
985
|
+
return { type: 'select', columns, aggregate, distinct, from, where, groupBy, having, orderBy, limit, offset, union, intersect, except, rollup };
|
|
615
986
|
}
|
|
616
987
|
|
|
617
988
|
parseFrom() {
|
|
@@ -619,7 +990,12 @@ class Parser {
|
|
|
619
990
|
const joins = [];
|
|
620
991
|
while (true) {
|
|
621
992
|
let type = null;
|
|
622
|
-
if (this.isKeyword('
|
|
993
|
+
if (this.isKeyword('FULL')) {
|
|
994
|
+
this.next();
|
|
995
|
+
if (this.isKeyword('OUTER')) this.next();
|
|
996
|
+
this.expectKeyword('JOIN');
|
|
997
|
+
type = 'full';
|
|
998
|
+
} else if (this.isKeyword('LEFT') || this.isKeyword('RIGHT')) {
|
|
623
999
|
type = this.peek().value.toLowerCase();
|
|
624
1000
|
this.next();
|
|
625
1001
|
if (this.isKeyword('OUTER')) this.next();
|
|
@@ -659,8 +1035,13 @@ class Parser {
|
|
|
659
1035
|
}
|
|
660
1036
|
const table = this.parseTableName();
|
|
661
1037
|
let alias = null;
|
|
1038
|
+
// 这里排除的是"未进关键字表、但会跟在表名之后"的子句起始词,
|
|
1039
|
+
// 否则 `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'];
|
|
662
1043
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
663
|
-
else if (this.peek().type === 'ident' && !
|
|
1044
|
+
else if (this.peek().type === 'ident' && !NOT_ALIAS.some(w => this.isWord(w))) {
|
|
664
1045
|
alias = this.next().value;
|
|
665
1046
|
}
|
|
666
1047
|
return { table, alias };
|
|
@@ -704,8 +1085,9 @@ class Parser {
|
|
|
704
1085
|
}
|
|
705
1086
|
let where = null;
|
|
706
1087
|
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
1088
|
+
const returning = this.parseOptionalReturning();
|
|
707
1089
|
this.optionalTailSemicolon();
|
|
708
|
-
return { type: 'update', table, assignments, where };
|
|
1090
|
+
return { type: 'update', table, assignments, where, returning };
|
|
709
1091
|
}
|
|
710
1092
|
|
|
711
1093
|
parseDelete() {
|
|
@@ -714,8 +1096,9 @@ class Parser {
|
|
|
714
1096
|
const table = this.parseTableName();
|
|
715
1097
|
let where = null;
|
|
716
1098
|
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
1099
|
+
const returning = this.parseOptionalReturning();
|
|
717
1100
|
this.optionalTailSemicolon();
|
|
718
|
-
return { type: 'delete', table, where };
|
|
1101
|
+
return { type: 'delete', table, where, returning };
|
|
719
1102
|
}
|
|
720
1103
|
|
|
721
1104
|
parseDropTable() {
|
|
@@ -1026,14 +1409,28 @@ class Parser {
|
|
|
1026
1409
|
parseNot() {
|
|
1027
1410
|
if (this.isKeyword('NOT')) {
|
|
1028
1411
|
this.next();
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1412
|
+
const inner = this.parseNot();
|
|
1413
|
+
// NOT EXISTS (...) 折叠成 exists.not,而不是包一层 not —— 语义等价且便于执行层物化
|
|
1414
|
+
if (inner && inner.type === 'exists') { inner.not = !inner.not; return inner; }
|
|
1415
|
+
return { type: 'not', expr: inner };
|
|
1416
|
+
}
|
|
1417
|
+
// 括号分组 —— 但 `(SELECT ...)` 是标量子查询,不能当分组吃掉
|
|
1418
|
+
const isSubqueryParen = this.peek().type === 'op' && this.peek().value === '(' &&
|
|
1419
|
+
this.peek(1) && this.peek(1).type === 'keyword' && this.peek(1).value === 'SELECT';
|
|
1420
|
+
if (!isSubqueryParen && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1032
1421
|
this.next();
|
|
1033
1422
|
const e = this.parseExpr();
|
|
1034
1423
|
this.expect('op', ')');
|
|
1035
1424
|
return e;
|
|
1036
1425
|
}
|
|
1426
|
+
// EXISTS (SELECT ...):子查询是否存在至少一行
|
|
1427
|
+
if (this.isKeyword('EXISTS')) {
|
|
1428
|
+
this.next();
|
|
1429
|
+
this.expect('op', '(');
|
|
1430
|
+
const select = this.parseSelect();
|
|
1431
|
+
this.expect('op', ')');
|
|
1432
|
+
return { type: 'exists', select, not: false };
|
|
1433
|
+
}
|
|
1037
1434
|
return this.parseComparison();
|
|
1038
1435
|
}
|
|
1039
1436
|
|
|
@@ -1048,6 +1445,30 @@ class Parser {
|
|
|
1048
1445
|
if (col.type !== 'ident') throw new Error(`Expected column name after '.', got '${col.value}'`);
|
|
1049
1446
|
return { type: 'column', name: t.value + '.' + col.value };
|
|
1050
1447
|
}
|
|
1448
|
+
// CAST(expr AS type)
|
|
1449
|
+
if (String(t.value).toUpperCase() === 'CAST' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1450
|
+
this.next();
|
|
1451
|
+
const expr = this.parseScalar();
|
|
1452
|
+
if (!this.isWord('AS')) throw new Error(`Expected AS in CAST, got '${this.peek().value}'`);
|
|
1453
|
+
this.next();
|
|
1454
|
+
const typeTok = this.next();
|
|
1455
|
+
let dataType = String(typeTok.value).toUpperCase();
|
|
1456
|
+
// VARCHAR(10) / DECIMAL(10,2) 这类带长度的类型
|
|
1457
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
1458
|
+
this.next();
|
|
1459
|
+
let depth = 1;
|
|
1460
|
+
let spec = '';
|
|
1461
|
+
while (depth > 0 && this.peek().type !== 'eof') {
|
|
1462
|
+
const tk = this.next();
|
|
1463
|
+
if (tk.type === 'op' && tk.value === '(') depth++;
|
|
1464
|
+
else if (tk.type === 'op' && tk.value === ')') { depth--; if (depth === 0) break; }
|
|
1465
|
+
spec += tk.value;
|
|
1466
|
+
}
|
|
1467
|
+
dataType += '(' + spec + ')';
|
|
1468
|
+
}
|
|
1469
|
+
this.expect('op', ')');
|
|
1470
|
+
return { type: 'cast', expr, dataType };
|
|
1471
|
+
}
|
|
1051
1472
|
// 函数调用: VERSION() / CONCAT(a, b) / NOW() ...
|
|
1052
1473
|
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
1053
1474
|
const name = t.value;
|
|
@@ -1062,13 +1483,17 @@ class Parser {
|
|
|
1062
1483
|
}
|
|
1063
1484
|
}
|
|
1064
1485
|
this.expect('op', ')');
|
|
1065
|
-
return { type: 'func', name, args };
|
|
1486
|
+
return this.parseOptionalOver({ type: 'func', name, args });
|
|
1066
1487
|
}
|
|
1067
1488
|
return { type: 'column', name: t.value };
|
|
1068
1489
|
}
|
|
1069
1490
|
if (t.type === 'number' || t.type === 'string') return { type: 'value', value: t.value };
|
|
1070
1491
|
if (t.type === 'sysvar') return { type: 'sysvar', name: t.value };
|
|
1492
|
+
if (t.type === 'param') return { type: 'param', index: t.value, position: this.paramCount++ };
|
|
1071
1493
|
if (t.type === 'keyword' && t.value === 'NULL') return { type: 'value', value: null };
|
|
1494
|
+
// MySQL: TRUE/FALSE 是 1/0 的别名,可作为值字面量出现在任意表达式位置
|
|
1495
|
+
if (t.type === 'keyword' && t.value === 'TRUE') return { type: 'value', value: true };
|
|
1496
|
+
if (t.type === 'keyword' && t.value === 'FALSE') return { type: 'value', value: false };
|
|
1072
1497
|
if (t.type === 'keyword' && t.value === 'CASE') return this.parseCase();
|
|
1073
1498
|
if (t.type === 'keyword' && !['SUM', 'AVG', 'MIN', 'MAX', 'COUNT'].includes(t.value) && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1074
1499
|
const name = t.value;
|
|
@@ -1083,7 +1508,7 @@ class Parser {
|
|
|
1083
1508
|
}
|
|
1084
1509
|
}
|
|
1085
1510
|
this.expect('op', ')');
|
|
1086
|
-
return { type: 'func', name, args };
|
|
1511
|
+
return this.parseOptionalOver({ type: 'func', name, args });
|
|
1087
1512
|
}
|
|
1088
1513
|
if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX', 'COUNT'].includes(t.value)) {
|
|
1089
1514
|
const fn = t.value;
|
|
@@ -1092,7 +1517,7 @@ class Parser {
|
|
|
1092
1517
|
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
|
|
1093
1518
|
else if (!(this.peek().type === 'op' && this.peek().value === ')')) column = this.parseScalar();
|
|
1094
1519
|
this.expect('op', ')');
|
|
1095
|
-
return { type: 'aggregate', fn, column };
|
|
1520
|
+
return this.parseOptionalOver({ type: 'aggregate', fn, column });
|
|
1096
1521
|
}
|
|
1097
1522
|
if (t.type === 'op' && t.value === '(' && this.peek().type === 'keyword' && this.peek().value === 'SELECT') {
|
|
1098
1523
|
const sub = this.parseSelect();
|
|
@@ -1206,8 +1631,8 @@ class Parser {
|
|
|
1206
1631
|
not = true;
|
|
1207
1632
|
// NOT 后必须紧跟比较关键字
|
|
1208
1633
|
const n = this.peek();
|
|
1209
|
-
if (!['IN', 'BETWEEN', 'LIKE', 'REGEXP'].includes(n.value)) {
|
|
1210
|
-
throw new Error(`Expected IN, BETWEEN, LIKE
|
|
1634
|
+
if (!['IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', 'ILIKE'].includes(n.value)) {
|
|
1635
|
+
throw new Error(`Expected IN, BETWEEN, LIKE, ILIKE, REGEXP or RLIKE after NOT, got '${n.value}'`);
|
|
1211
1636
|
}
|
|
1212
1637
|
}
|
|
1213
1638
|
const t2 = this.peek();
|
|
@@ -1256,13 +1681,16 @@ class Parser {
|
|
|
1256
1681
|
throw new Error(`Expected AND in BETWEEN, got '${andTok.value}'`);
|
|
1257
1682
|
}
|
|
1258
1683
|
|
|
1259
|
-
|
|
1684
|
+
// ILIKE 刻意不加进 KEYWORDS(避免影响同名标识符),用 isWord 按上下文匹配
|
|
1685
|
+
if ((t2.type === 'keyword' && t2.value === 'LIKE') || this.isWord('ILIKE')) {
|
|
1686
|
+
const isILike = !(t2.type === 'keyword' && t2.value === 'LIKE');
|
|
1260
1687
|
this.next();
|
|
1261
1688
|
const pattern = this.parseValue();
|
|
1262
|
-
return { type: 'like', operand: left, pattern, not };
|
|
1689
|
+
return { type: 'like', operand: left, pattern, not, ci: isILike };
|
|
1263
1690
|
}
|
|
1264
1691
|
|
|
1265
|
-
|
|
1692
|
+
// MySQL 中 RLIKE 是 REGEXP 的同义词
|
|
1693
|
+
if (t2.type === 'keyword' && (t2.value === 'REGEXP' || t2.value === 'RLIKE')) {
|
|
1266
1694
|
this.next();
|
|
1267
1695
|
const pattern = this.parseValue();
|
|
1268
1696
|
return { type: 'regexp', operand: left, pattern, not };
|
|
@@ -1270,6 +1698,15 @@ class Parser {
|
|
|
1270
1698
|
|
|
1271
1699
|
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
|
|
1272
1700
|
this.next();
|
|
1701
|
+
// = ANY (SELECT ...) / > ALL (SELECT ...)
|
|
1702
|
+
const anyAll = this.isWord('ANY') || this.isWord('ALL') || this.isWord('SOME');
|
|
1703
|
+
if (anyAll) {
|
|
1704
|
+
const quantifier = String(this.next().value).toUpperCase() === 'ALL' ? 'ALL' : 'ANY';
|
|
1705
|
+
this.expect('op', '(');
|
|
1706
|
+
const sub = this.parseSelect();
|
|
1707
|
+
this.expect('op', ')');
|
|
1708
|
+
return { type: 'quantified', op: t2.value === '<>' ? '!=' : t2.value, operand: left, select: sub, quantifier, not };
|
|
1709
|
+
}
|
|
1273
1710
|
const right = this.parseOperand();
|
|
1274
1711
|
return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
|
|
1275
1712
|
}
|
|
@@ -1288,6 +1725,34 @@ const OPERATORS = {
|
|
|
1288
1725
|
'>=': (a, b) => a >= b
|
|
1289
1726
|
};
|
|
1290
1727
|
|
|
1728
|
+
/** CAST(expr AS type) 的类型转换 */
|
|
1729
|
+
function applyCast(value, dataType) {
|
|
1730
|
+
const t = String(dataType || '').toUpperCase().replace(/\(.*\)$/, '').trim();
|
|
1731
|
+
if (value === null || value === undefined) return null;
|
|
1732
|
+
if (t === 'INTEGER' || t === 'INT' || t === 'BIGINT' || t === 'SMALLINT' || t === 'TINYINT') {
|
|
1733
|
+
const n = Number(value);
|
|
1734
|
+
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
1735
|
+
}
|
|
1736
|
+
if (t === 'FLOAT' || t === 'DOUBLE' || t === 'REAL' || t === 'NUMERIC' || t === 'DECIMAL') {
|
|
1737
|
+
const n = Number(value);
|
|
1738
|
+
return Number.isFinite(n) ? n : null;
|
|
1739
|
+
}
|
|
1740
|
+
if (t === 'BOOLEAN' || t === 'BOOL') {
|
|
1741
|
+
if (typeof value === 'boolean') return value;
|
|
1742
|
+
if (typeof value === 'number') return value !== 0;
|
|
1743
|
+
const s = String(value).toLowerCase();
|
|
1744
|
+
return s === 'true' || s === '1' || s === 't' || s === 'yes';
|
|
1745
|
+
}
|
|
1746
|
+
if (t === 'TEXT' || t === 'VARCHAR' || t === 'CHAR' || t === 'STRING') return String(value);
|
|
1747
|
+
return value;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// 行对象里是否存在 `alias.col` 形式的键(即该行是否已被加过表前缀)
|
|
1751
|
+
function rowHasPrefixedKeys(row) {
|
|
1752
|
+
for (const k in row) if (k.indexOf('.') !== -1) return true;
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1291
1756
|
function resolveOperand(operand, row, ctx) {
|
|
1292
1757
|
if (operand === null || operand === undefined) return null;
|
|
1293
1758
|
if (typeof operand === 'string') return resolveOperand({ type: 'column', name: operand }, row, ctx);
|
|
@@ -1321,11 +1786,15 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1321
1786
|
const n = operand.name;
|
|
1322
1787
|
if (n === undefined) return undefined;
|
|
1323
1788
|
if (row[n] !== undefined) return row[n];
|
|
1324
|
-
if (n.
|
|
1325
|
-
if (row[n] !== undefined) return row[n];
|
|
1789
|
+
if (n.indexOf('.') !== -1) {
|
|
1326
1790
|
const col = n.slice(n.lastIndexOf('.') + 1);
|
|
1327
|
-
|
|
1791
|
+
// 只有当行完全没有带前缀的键(即行未被 _prefixRow 处理过)时才回退到裸列名。
|
|
1792
|
+
// 否则 `e.dept` 这种未知限定符会静默取到当前行自己的 dept,
|
|
1793
|
+
// 让相关子查询变成“自匹配”,EXISTS 恒为真。
|
|
1794
|
+
if (!rowHasPrefixedKeys(row) && row[col] !== undefined) return row[col];
|
|
1328
1795
|
}
|
|
1796
|
+
// 相关子查询:回退到外层行(ctx.__outer 由 _evalExists 临时注入)
|
|
1797
|
+
if (ctx && ctx.__outer && ctx.__outer[n] !== undefined) return ctx.__outer[n];
|
|
1329
1798
|
return undefined;
|
|
1330
1799
|
}
|
|
1331
1800
|
case 'arith': {
|
|
@@ -1352,8 +1821,35 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1352
1821
|
case 'like':
|
|
1353
1822
|
case 'between':
|
|
1354
1823
|
return evaluateExpr(operand, row, ctx);
|
|
1355
|
-
case '
|
|
1824
|
+
case 'param': {
|
|
1825
|
+
// 原生 ? 占位符。executeSQL 默认会先用 applyParams 内联替换,
|
|
1826
|
+
// 只有在显式传入 ctx.params 时才走这条路径。
|
|
1827
|
+
const params = ctx && ctx.params;
|
|
1828
|
+
if (params == null) return undefined;
|
|
1829
|
+
if (operand.index === '??') {
|
|
1830
|
+
return typeof params === 'object' && !Array.isArray(params) ? params['@@'] : undefined;
|
|
1831
|
+
}
|
|
1832
|
+
const i = operand.index != null && operand.index !== '??' ? operand.index - 1 : (operand.position || 0);
|
|
1833
|
+
return Array.isArray(params) ? params[i] : params[i];
|
|
1834
|
+
}
|
|
1835
|
+
case 'cast': {
|
|
1836
|
+
const v = resolveOperand(operand.expr, row, ctx);
|
|
1837
|
+
return applyCast(v, operand.dataType);
|
|
1838
|
+
}
|
|
1839
|
+
case 'quantified': {
|
|
1840
|
+
// = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
|
|
1841
|
+
const v = resolveOperand(operand.operand, row, ctx);
|
|
1842
|
+
const list = operand._list || [];
|
|
1843
|
+
if (list.length === 0) return operand.quantifier === 'ALL';
|
|
1844
|
+
const fn = OPERATORS[operand.op];
|
|
1845
|
+
if (!fn) return false;
|
|
1846
|
+
const results = list.map(x => (v === null || x === null) ? false : fn(v, x));
|
|
1847
|
+
return operand.quantifier === 'ALL' ? results.every(Boolean) : results.some(Boolean);
|
|
1848
|
+
}
|
|
1356
1849
|
case 'subquery':
|
|
1850
|
+
// 相关子查询:值由 _evalCorrelated 按外层行算好写回 _value
|
|
1851
|
+
return operand._value !== undefined ? operand._value : null;
|
|
1852
|
+
case 'aggregate':
|
|
1357
1853
|
return undefined;
|
|
1358
1854
|
default:
|
|
1359
1855
|
return undefined;
|
|
@@ -1478,13 +1974,14 @@ function seedRand(seed) {
|
|
|
1478
1974
|
};
|
|
1479
1975
|
}
|
|
1480
1976
|
|
|
1481
|
-
|
|
1977
|
+
// ci 默认 true:保持 LIKE 既有的大小写不敏感行为不变;ILIKE 同为不敏感。
|
|
1978
|
+
function likeMatch(value, pattern, ci = true) {
|
|
1482
1979
|
if (typeof value !== 'string') return false;
|
|
1483
1980
|
const regex = pattern
|
|
1484
1981
|
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1485
1982
|
.replace(/%/g, '.*')
|
|
1486
1983
|
.replace(/_/g, '.');
|
|
1487
|
-
return new RegExp('^' + regex + '$', 'i').test(value);
|
|
1984
|
+
return new RegExp('^' + regex + '$', ci ? 'i' : '').test(value);
|
|
1488
1985
|
}
|
|
1489
1986
|
|
|
1490
1987
|
function extractEqualPushdown(expr, schema) {
|
|
@@ -1512,6 +2009,25 @@ function extractEqualPushdown(expr, schema) {
|
|
|
1512
2009
|
return { filter, rest };
|
|
1513
2010
|
}
|
|
1514
2011
|
|
|
2012
|
+
// 行去重(保持首次出现顺序),供 UNION / INTERSECT / EXCEPT 的 DISTINCT 语义使用
|
|
2013
|
+
function dedupeRows(rows) {
|
|
2014
|
+
const seen = new Set();
|
|
2015
|
+
return rows.filter(r => {
|
|
2016
|
+
const k = JSON.stringify(r);
|
|
2017
|
+
if (seen.has(k)) return false;
|
|
2018
|
+
seen.add(k);
|
|
2019
|
+
return true;
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// 比较前把布尔归一化为 1/0,但只在“另一侧是数值/布尔”时做,
|
|
2024
|
+
// 避免把 true 与字符串 'true' 的比较悄悄改成 1 与 'true' 的比较。
|
|
2025
|
+
function normalizeCmpValue(a, other) {
|
|
2026
|
+
if (typeof a !== 'boolean') return a;
|
|
2027
|
+
const otherIsNumeric = typeof other === 'number' || typeof other === 'boolean';
|
|
2028
|
+
return otherIsNumeric ? (a ? 1 : 0) : a;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1515
2031
|
function evaluateExpr(expr, row, ctx) {
|
|
1516
2032
|
if (expr === null || expr === undefined) return false;
|
|
1517
2033
|
switch (expr.type) {
|
|
@@ -1523,9 +2039,12 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
1523
2039
|
const r = resolveOperand(expr.right, row, ctx);
|
|
1524
2040
|
// SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
|
|
1525
2041
|
if (l === null || r === null) return false;
|
|
1526
|
-
|
|
2042
|
+
// 布尔与数值互通(MySQL: 1 = TRUE、0 = FALSE)
|
|
2043
|
+
const nl = normalizeCmpValue(l, r);
|
|
2044
|
+
const nr = normalizeCmpValue(r, l);
|
|
2045
|
+
if (expr.op === '=') return nl === nr || String(nl) === String(nr);
|
|
1527
2046
|
const fn = OPERATORS[expr.op];
|
|
1528
|
-
return typeof
|
|
2047
|
+
return typeof nl === 'number' && typeof nr === 'number' ? fn(nl, nr) : fn(String(nl), String(nr));
|
|
1529
2048
|
}
|
|
1530
2049
|
case 'isNull': {
|
|
1531
2050
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
@@ -1539,9 +2058,12 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
1539
2058
|
return expr.list.some(x => x === v || String(x) === String(v));
|
|
1540
2059
|
}
|
|
1541
2060
|
case 'like': {
|
|
1542
|
-
const
|
|
2061
|
+
const v = resolveOperand(expr.operand, row, ctx);
|
|
2062
|
+
const r = likeMatch(v, expr.pattern, !!expr.ci);
|
|
1543
2063
|
return expr.not ? !r : r;
|
|
1544
2064
|
}
|
|
2065
|
+
case 'quantified':
|
|
2066
|
+
return !!resolveOperand(expr, row, ctx);
|
|
1545
2067
|
case 'regexp': {
|
|
1546
2068
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
1547
2069
|
if (v === null || v === undefined) return false;
|
|
@@ -1566,6 +2088,12 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
1566
2088
|
: String(v) >= String(lo) && String(v) <= String(hi);
|
|
1567
2089
|
return expr.not ? !inRange : inRange;
|
|
1568
2090
|
}
|
|
2091
|
+
case 'exists': {
|
|
2092
|
+
// 执行层会把子查询物化成 expr._rowCount,这里只做布尔判定
|
|
2093
|
+
const n = typeof expr._rowCount === 'number' ? expr._rowCount : (expr._rows ? expr._rows.length : 0);
|
|
2094
|
+
const has = n > 0;
|
|
2095
|
+
return expr.not ? !has : has;
|
|
2096
|
+
}
|
|
1569
2097
|
case 'case': {
|
|
1570
2098
|
for (const b of expr.branches) {
|
|
1571
2099
|
if (evaluateExpr(b.cond, row, ctx)) return resolveOperand(b.val, row, ctx);
|
|
@@ -1602,6 +2130,11 @@ function scalarName(node) {
|
|
|
1602
2130
|
}
|
|
1603
2131
|
|
|
1604
2132
|
function scalarColumnName(c) {
|
|
2133
|
+
// 未起别名的窗口函数列给一个可读的名字(仅影响新增语法,不动既有命名规则)
|
|
2134
|
+
if ((c.over || (c.scalar && c.scalar.over)) && !c.alias) {
|
|
2135
|
+
const fn = String(c.aggregate || (c.scalar && c.scalar.name) || 'WINDOW').toUpperCase();
|
|
2136
|
+
return fn + '() OVER ()';
|
|
2137
|
+
}
|
|
1605
2138
|
if (c.alias) return c.alias;
|
|
1606
2139
|
if (c.scalar) return scalarName(c.scalar);
|
|
1607
2140
|
if (c.aggregate) return c.alias || (c.aggregate === 'COUNT' ? 'COUNT(*)' : c.aggregate + '(' + c.column + ')');
|
|
@@ -1612,7 +2145,15 @@ function scalarColumnName(c) {
|
|
|
1612
2145
|
return dot !== -1 ? c.expr.slice(dot + 1) : c.expr;
|
|
1613
2146
|
}
|
|
1614
2147
|
|
|
2148
|
+
// 窗口函数列:值由 _computeWindows 预先写回行对象,投影时直接取用
|
|
2149
|
+
function windowColumnValue(c, r) {
|
|
2150
|
+
const key = c._windowKey || c.alias || scalarColumnName(c);
|
|
2151
|
+
const v = r ? r[key] : undefined;
|
|
2152
|
+
return v === undefined ? null : v;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1615
2155
|
function scalarColumnValue(c, r, ctx) {
|
|
2156
|
+
if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
|
|
1616
2157
|
if (c.scalar) {
|
|
1617
2158
|
const s = c.scalar;
|
|
1618
2159
|
if (s && s.type === 'func' && ctx && ctx.ctxAggregates && Object.prototype.hasOwnProperty.call(ctx.ctxAggregates, String(s.name).toUpperCase())) {
|
|
@@ -1775,6 +2316,16 @@ class SQLExecutor {
|
|
|
1775
2316
|
});
|
|
1776
2317
|
dataRows = statement.dataRows;
|
|
1777
2318
|
}
|
|
2319
|
+
// INSERT ... SELECT:先跑 SELECT,再把结果行映射成待插入行
|
|
2320
|
+
if (statement.select) {
|
|
2321
|
+
const res = await this.executeSelect(statement.select);
|
|
2322
|
+
const cols = statement.columns && statement.columns.length ? statement.columns : res.columns;
|
|
2323
|
+
dataRows = res.rows.map(arr => {
|
|
2324
|
+
const row = {};
|
|
2325
|
+
cols.forEach((c, i) => { row[c] = arr[i]; });
|
|
2326
|
+
return row;
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
1778
2329
|
if (dataRows === null && statement.values) {
|
|
1779
2330
|
const colNames = Object.keys(schema);
|
|
1780
2331
|
const skipAuto = statement.values[0].length < colNames.length;
|
|
@@ -1798,6 +2349,7 @@ class SQLExecutor {
|
|
|
1798
2349
|
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
1799
2350
|
let toInsert = dataRows;
|
|
1800
2351
|
let updated = 0;
|
|
2352
|
+
let skipped = 0;
|
|
1801
2353
|
if (pkCols.length > 0) {
|
|
1802
2354
|
const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
|
|
1803
2355
|
const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
|
|
@@ -1819,25 +2371,49 @@ class SQLExecutor {
|
|
|
1819
2371
|
fresh.push(d);
|
|
1820
2372
|
}
|
|
1821
2373
|
}
|
|
1822
|
-
|
|
2374
|
+
const conflictAction = this._conflictAction(statement);
|
|
2375
|
+
const conflictSets = this._conflictSets(statement);
|
|
2376
|
+
if (conflicts.length > 0 && conflictAction === 'throw') {
|
|
1823
2377
|
throw new Error('ER_DUP_ENTRY: Duplicate entry for primary key');
|
|
1824
2378
|
}
|
|
1825
|
-
if (
|
|
2379
|
+
if (conflicts.length > 0 && conflictAction === 'ignore') {
|
|
2380
|
+
// INSERT IGNORE / ON CONFLICT DO NOTHING:静默跳过冲突行
|
|
2381
|
+
skipped = conflicts.length;
|
|
2382
|
+
}
|
|
2383
|
+
if (conflicts.length > 0 && conflictAction === 'replace') {
|
|
2384
|
+
// REPLACE INTO:删除旧行后按新行插入
|
|
2385
|
+
const oldIds = [];
|
|
2386
|
+
for (const { existingId } of conflicts) {
|
|
2387
|
+
if (existingId.length === 1) oldIds.push(existingId[0]);
|
|
2388
|
+
}
|
|
2389
|
+
if (oldIds.length > 0) {
|
|
2390
|
+
if (this.engine.removeByIds) await this.engine.removeByIds(statement.name, oldIds);
|
|
2391
|
+
else for (const id of oldIds) await this.engine.removeById(statement.name, id);
|
|
2392
|
+
}
|
|
2393
|
+
toInsert = dataRows;
|
|
2394
|
+
await this.engine.flush();
|
|
2395
|
+
} else if (conflicts.length > 0 && conflictAction === 'update') {
|
|
1826
2396
|
for (const { d, existingId } of conflicts) {
|
|
1827
2397
|
const data = {};
|
|
1828
|
-
|
|
2398
|
+
if (conflictSets) {
|
|
2399
|
+
for (const [col, val] of conflictSets) data[col] = val;
|
|
2400
|
+
} else {
|
|
2401
|
+
for (const k of Object.keys(d)) if (!pkCols.includes(k)) data[k] = d[k];
|
|
2402
|
+
}
|
|
1829
2403
|
if (this.engine.updateById && existingId.length === 1) {
|
|
1830
|
-
this.engine.updateById(statement.name, existingId[0], data);
|
|
2404
|
+
await this.engine.updateById(statement.name, existingId[0], data);
|
|
1831
2405
|
} else if (this.engine.update) {
|
|
1832
2406
|
const filter = {};
|
|
1833
2407
|
pkCols.forEach((c, i) => { filter[c] = existingId[i]; });
|
|
1834
|
-
this.engine.update(statement.name, filter, data);
|
|
2408
|
+
await this.engine.update(statement.name, filter, data);
|
|
1835
2409
|
}
|
|
1836
2410
|
updated++;
|
|
1837
2411
|
}
|
|
1838
2412
|
await this.engine.flush();
|
|
2413
|
+
toInsert = fresh;
|
|
2414
|
+
} else if (conflictAction !== 'replace') {
|
|
2415
|
+
toInsert = fresh;
|
|
1839
2416
|
}
|
|
1840
|
-
toInsert = fresh;
|
|
1841
2417
|
}
|
|
1842
2418
|
const seen = new Map();
|
|
1843
2419
|
let kept = [];
|
|
@@ -1845,10 +2421,12 @@ class SQLExecutor {
|
|
|
1845
2421
|
if (!hasExplicitPk(d)) { kept.push(d); continue; }
|
|
1846
2422
|
const key = keyOf(d);
|
|
1847
2423
|
if (seen.has(key)) {
|
|
1848
|
-
|
|
2424
|
+
const act = this._conflictAction(statement);
|
|
2425
|
+
if (act === 'throw') {
|
|
1849
2426
|
throw new Error('ER_DUP_ENTRY: Duplicate entry for primary key');
|
|
1850
2427
|
}
|
|
1851
|
-
|
|
2428
|
+
if (act === 'ignore') { skipped++; continue; }
|
|
2429
|
+
seen.get(key).row = d; // update / replace:后出现的行覆盖先出现的行
|
|
1852
2430
|
} else {
|
|
1853
2431
|
seen.set(key, { row: d });
|
|
1854
2432
|
}
|
|
@@ -1861,17 +2439,109 @@ class SQLExecutor {
|
|
|
1861
2439
|
ids = await this.engine.insert(statement.name, toInsert);
|
|
1862
2440
|
await this.engine.flush();
|
|
1863
2441
|
}
|
|
1864
|
-
|
|
2442
|
+
const out = {
|
|
1865
2443
|
ok: true, type: 'insert', table: statement.name,
|
|
1866
2444
|
affectedRows: toInsert.length + updated,
|
|
1867
2445
|
insertId: Array.isArray(ids) && ids.length > 0 ? ids[0] : null,
|
|
1868
2446
|
ids,
|
|
1869
2447
|
duplicateUpdated: updated,
|
|
1870
2448
|
};
|
|
2449
|
+
if (skipped > 0) out.duplicateSkipped = skipped;
|
|
2450
|
+
if (statement.returning) this._attachReturning(out, statement, toInsert, schema);
|
|
2451
|
+
return out;
|
|
1871
2452
|
}
|
|
1872
2453
|
case 'select': {
|
|
1873
2454
|
return await this.executeSelect(statement);
|
|
1874
2455
|
}
|
|
2456
|
+
case 'with': {
|
|
2457
|
+
// CTE:按声明顺序把已展开的 CTE 内联进引用它的 FROM(含嵌套 CTE 引用)
|
|
2458
|
+
const map = new Map();
|
|
2459
|
+
for (const cte of statement.ctes) {
|
|
2460
|
+
const sel = JSON.parse(JSON.stringify(cte.select));
|
|
2461
|
+
// CTE 显式列名清单 `name (a, b) AS (...)`:按顺序重命名输出列
|
|
2462
|
+
if (cte.columns && cte.columns.length) {
|
|
2463
|
+
sel.columns.forEach((c, i) => {
|
|
2464
|
+
if (cte.columns[i] && !c.alias) c.alias = cte.columns[i];
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
this._inlineCteRefs(sel, map);
|
|
2468
|
+
map.set(String(cte.name).toLowerCase(), sel);
|
|
2469
|
+
}
|
|
2470
|
+
const stmt = JSON.parse(JSON.stringify(statement.statement));
|
|
2471
|
+
this._inlineCteRefs(stmt, map);
|
|
2472
|
+
return await this.execute(stmt);
|
|
2473
|
+
}
|
|
2474
|
+
case 'explain': {
|
|
2475
|
+
const lines = [];
|
|
2476
|
+
const describe = (s, indent) => {
|
|
2477
|
+
if (!s) return;
|
|
2478
|
+
const pad = indent || '';
|
|
2479
|
+
if (s.type === 'select') {
|
|
2480
|
+
lines.push([pad + 'SELECT', s.from ? '' : '(no FROM)']);
|
|
2481
|
+
if (s.from) {
|
|
2482
|
+
for (const t of (s.from.tables || [])) lines.push([pad + ' SCAN', t.table || '(subquery)']);
|
|
2483
|
+
for (const j of (s.from.joins || [])) lines.push([pad + ' ' + String(j.type).toUpperCase() + ' JOIN', (j.item && (j.item.table || '(subquery)')) || '']);
|
|
2484
|
+
}
|
|
2485
|
+
if (s.where) lines.push([pad + ' FILTER', 'WHERE']);
|
|
2486
|
+
if (s.groupBy) lines.push([pad + ' AGGREGATE', 'GROUP BY ' + s.groupBy.join(', ') + (s.rollup ? ' WITH ROLLUP' : '')]);
|
|
2487
|
+
if (s.having) lines.push([pad + ' FILTER', 'HAVING']);
|
|
2488
|
+
if (s.orderBy) lines.push([pad + ' SORT', 'ORDER BY ' + s.orderBy.map(o => (typeof o.column === 'string' ? o.column : 'expr') + ' ' + o.dir).join(', ')]);
|
|
2489
|
+
if (s.limit !== null && s.limit !== undefined) lines.push([pad + ' LIMIT', String(s.limit)]);
|
|
2490
|
+
if (s.union) describe(s.union.select, pad + ' ');
|
|
2491
|
+
} else if (s.type === 'with') {
|
|
2492
|
+
for (const c of s.ctes) { lines.push([pad + 'CTE ' + c.name, '']); describe(c.select, pad + ' '); }
|
|
2493
|
+
describe(s.statement, indent);
|
|
2494
|
+
} else {
|
|
2495
|
+
lines.push([String(s.type).toUpperCase(), s.table || s.name || '']);
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2498
|
+
describe(statement.statement, '');
|
|
2499
|
+
return {
|
|
2500
|
+
ok: true, type: 'explain', columns: ['step', 'detail'], rows: lines,
|
|
2501
|
+
rowCount: lines.length, affectedRows: 0, message: `${lines.length} step(s) — 无代价模型,仅展示计划结构`,
|
|
2502
|
+
command: 'EXPLAIN', warnings: [],
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
case 'createView': {
|
|
2506
|
+
if (!this.engine._views) this.engine._views = Object.create(null);
|
|
2507
|
+
const key = String(statement.name).toLowerCase();
|
|
2508
|
+
if (this.engine._views[key] && !statement.orReplace) {
|
|
2509
|
+
throw new Error(`View '${statement.name}' already exists (use CREATE OR REPLACE VIEW)`);
|
|
2510
|
+
}
|
|
2511
|
+
this.engine._views[key] = {
|
|
2512
|
+
name: statement.name,
|
|
2513
|
+
columns: statement.columns || null,
|
|
2514
|
+
select: statement.select,
|
|
2515
|
+
};
|
|
2516
|
+
return { ok: true, type: 'createView', message: `View '${statement.name}' created`, affectedRows: 0, warnings: [] };
|
|
2517
|
+
}
|
|
2518
|
+
case 'dropView': {
|
|
2519
|
+
if (!this.engine._views) this.engine._views = Object.create(null);
|
|
2520
|
+
const key = String(statement.name).toLowerCase();
|
|
2521
|
+
if (!this.engine._views[key]) {
|
|
2522
|
+
if (statement.ifExists) return { ok: true, type: 'dropView', message: `View '${statement.name}' did not exist`, affectedRows: 0, skipped: true, warnings: [] };
|
|
2523
|
+
throw new Error(`View '${statement.name}' does not exist`);
|
|
2524
|
+
}
|
|
2525
|
+
delete this.engine._views[key];
|
|
2526
|
+
return { ok: true, type: 'dropView', message: `View '${statement.name}' dropped`, affectedRows: 0, warnings: [] };
|
|
2527
|
+
}
|
|
2528
|
+
case 'savepoint': {
|
|
2529
|
+
// 引擎侧没有保存点/快照能力。这里只登记名字,绝不假装能回滚。
|
|
2530
|
+
if (!this.engine._savepoints) this.engine._savepoints = new Set();
|
|
2531
|
+
this.engine._savepoints.add(String(statement.name).toLowerCase());
|
|
2532
|
+
return { ok: true, type: 'savepoint', message: `Savepoint '${statement.name}' noted`, affectedRows: 0, warnings: [] };
|
|
2533
|
+
}
|
|
2534
|
+
case 'releaseSavepoint': {
|
|
2535
|
+
if (this.engine._savepoints) this.engine._savepoints.delete(String(statement.name).toLowerCase());
|
|
2536
|
+
return { ok: true, type: 'releaseSavepoint', message: `Savepoint '${statement.name}' released`, affectedRows: 0, warnings: [] };
|
|
2537
|
+
}
|
|
2538
|
+
case 'rollbackTo': {
|
|
2539
|
+
// 明确报错而不是静默无效:静默的"回滚成功"会让调用方以为数据已还原。
|
|
2540
|
+
throw new Error(
|
|
2541
|
+
`ROLLBACK TO SAVEPOINT '${statement.name}' is not supported: the engine has no savepoint/snapshot support. ` +
|
|
2542
|
+
`Use ROLLBACK to abort the whole transaction.`
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
1875
2545
|
case 'update': {
|
|
1876
2546
|
const schema = this.engine.getTableSchema
|
|
1877
2547
|
? await this.engine.getTableSchema(statement.table)
|
|
@@ -1893,7 +2563,16 @@ class SQLExecutor {
|
|
|
1893
2563
|
}
|
|
1894
2564
|
}
|
|
1895
2565
|
await this.engine.flush();
|
|
1896
|
-
|
|
2566
|
+
const updOut = { ok: true, type: 'update', table: statement.table, affectedRows: count };
|
|
2567
|
+
if (statement.returning) {
|
|
2568
|
+
const after = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
2569
|
+
const changed = [];
|
|
2570
|
+
for (const row of after) {
|
|
2571
|
+
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) changed.push(row);
|
|
2572
|
+
}
|
|
2573
|
+
this._attachReturning(updOut, statement, changed, schema);
|
|
2574
|
+
}
|
|
2575
|
+
return updOut;
|
|
1897
2576
|
}
|
|
1898
2577
|
case 'delete': {
|
|
1899
2578
|
const schema = this.engine.getTableSchema
|
|
@@ -1902,18 +2581,22 @@ class SQLExecutor {
|
|
|
1902
2581
|
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
1903
2582
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1904
2583
|
const ids = [];
|
|
2584
|
+
const removedRows = [];
|
|
1905
2585
|
for (const row of all) {
|
|
1906
2586
|
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
|
|
1907
2587
|
const id = this._rowPkId(row, pkCols);
|
|
1908
|
-
if (id !== undefined) ids.push(id);
|
|
2588
|
+
if (id !== undefined) { ids.push(id); removedRows.push(row); }
|
|
1909
2589
|
}
|
|
1910
2590
|
}
|
|
1911
2591
|
if (ids.length > 0) {
|
|
1912
|
-
if (this.engine.removeByIds) this.engine.removeByIds(statement.table, ids);
|
|
1913
|
-
else for (const id of ids) this.engine.removeById(statement.table, id);
|
|
2592
|
+
if (this.engine.removeByIds) await this.engine.removeByIds(statement.table, ids);
|
|
2593
|
+
else for (const id of ids) await this.engine.removeById(statement.table, id);
|
|
1914
2594
|
}
|
|
1915
2595
|
await this.engine.flush();
|
|
1916
|
-
|
|
2596
|
+
const delOut = { ok: true, type: 'delete', table: statement.table, affectedRows: ids.length };
|
|
2597
|
+
// DELETE ... RETURNING 返回被删除的行
|
|
2598
|
+
if (statement.returning) this._attachReturning(delOut, statement, removedRows, schema);
|
|
2599
|
+
return delOut;
|
|
1917
2600
|
}
|
|
1918
2601
|
case 'begin':
|
|
1919
2602
|
if (this.engine.beginTx) this.engine._txId = await this.engine.beginTx();
|
|
@@ -2345,15 +3028,23 @@ class SQLExecutor {
|
|
|
2345
3028
|
if (expr.type === 'in' && expr.subquery) {
|
|
2346
3029
|
const res = await this.executeSelect(expr.subquery);
|
|
2347
3030
|
expr.list = res.rows.map(r => r[0]);
|
|
2348
|
-
delete expr.subquery
|
|
3031
|
+
// 不 delete expr.subquery:相关子查询需要在逐行求值时重跑
|
|
2349
3032
|
return;
|
|
2350
3033
|
}
|
|
2351
3034
|
if (expr.type === 'subquery') {
|
|
2352
3035
|
const res = await this.executeSelect(expr.select);
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
3036
|
+
expr._value = res.rows.length > 0 ? res.rows[0][0] : null;
|
|
3037
|
+
// 不改写类型、不 delete select:保留节点以便相关场景逐行重算
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
if (expr.type === 'exists' && expr.select) {
|
|
3041
|
+
// 非相关场景(HAVING / JOIN ON)只求值一次;保留 select 以便 WHERE 里逐行关联求值
|
|
3042
|
+
await this._evalCorrelated(expr, null);
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
if (expr.type === 'quantified' && expr.select) {
|
|
3046
|
+
const res = await this.executeSelect(expr.select);
|
|
3047
|
+
expr._list = res.rows.map(r => (Array.isArray(r) ? r[0] : r));
|
|
2357
3048
|
return;
|
|
2358
3049
|
}
|
|
2359
3050
|
for (const k of Object.keys(expr)) {
|
|
@@ -2400,6 +3091,29 @@ class SQLExecutor {
|
|
|
2400
3091
|
return out;
|
|
2401
3092
|
}
|
|
2402
3093
|
|
|
3094
|
+
/**
|
|
3095
|
+
* 构造 WITH ROLLUP 的汇总行:把各组的原始行合并成一个"超级组",
|
|
3096
|
+
* 分组列不设值(投影时会取 null),聚合列则覆盖全部行。
|
|
3097
|
+
*/
|
|
3098
|
+
_rollupRow(groupReps) {
|
|
3099
|
+
const all = [];
|
|
3100
|
+
for (const rep of groupReps) {
|
|
3101
|
+
if (rep._group) all.push(...rep._group);
|
|
3102
|
+
else all.push(rep);
|
|
3103
|
+
}
|
|
3104
|
+
const out = {};
|
|
3105
|
+
if (all.length > 0) {
|
|
3106
|
+
// 分组列显式置为 null,避免取到某一组的代表值
|
|
3107
|
+
for (const k of Object.keys(all[0])) {
|
|
3108
|
+
if (k === '_group' || k === '_rid') continue;
|
|
3109
|
+
out[k] = null;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
out._group = all;
|
|
3113
|
+
out._rollup = true;
|
|
3114
|
+
return out;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
2403
3117
|
_aggValue(rows, fn, column) {
|
|
2404
3118
|
if (fn === 'COUNT') return rows.length;
|
|
2405
3119
|
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
@@ -2438,6 +3152,331 @@ class SQLExecutor {
|
|
|
2438
3152
|
}
|
|
2439
3153
|
}
|
|
2440
3154
|
|
|
3155
|
+
/**
|
|
3156
|
+
* 收集表达式树里所有「带子查询」的节点:
|
|
3157
|
+
* exists(select) / subquery(select) / in(subquery)。
|
|
3158
|
+
* 这些节点都可能需要按外层行逐行重新求值(相关子查询)。
|
|
3159
|
+
*/
|
|
3160
|
+
_collectSubqueries(node, out = []) {
|
|
3161
|
+
if (!node || typeof node !== 'object') return out;
|
|
3162
|
+
if (Array.isArray(node)) { for (const n of node) this._collectSubqueries(n, out); return out; }
|
|
3163
|
+
if ((node.select || node.subquery) &&
|
|
3164
|
+
(node.type === 'exists' || node.type === 'subquery' || node.type === 'in')) out.push(node);
|
|
3165
|
+
for (const k of Object.keys(node)) {
|
|
3166
|
+
const v = node[k];
|
|
3167
|
+
if (v && typeof v === 'object') this._collectSubqueries(v, out);
|
|
3168
|
+
}
|
|
3169
|
+
return out;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
/**
|
|
3173
|
+
* 冲突处理策略 —— 把四种写法归一成一个动作:
|
|
3174
|
+
* REPLACE INTO → 'replace'
|
|
3175
|
+
* INSERT IGNORE / ON CONFLICT DO NOTHING → 'ignore'
|
|
3176
|
+
* ON DUPLICATE KEY UPDATE / ON CONFLICT DO UPDATE→ 'update'
|
|
3177
|
+
* 默认 → 'throw'(ER_DUP_ENTRY)
|
|
3178
|
+
*/
|
|
3179
|
+
_conflictAction(statement) {
|
|
3180
|
+
if (statement.replace) return 'replace';
|
|
3181
|
+
if (statement.ignore) return 'ignore';
|
|
3182
|
+
const oc = statement.onConflict;
|
|
3183
|
+
if (oc) return oc.action === 'nothing' ? 'ignore' : 'update';
|
|
3184
|
+
if (statement.onDuplicate) return 'update';
|
|
3185
|
+
return 'throw';
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
/** 冲突时要写入的列值(ON DUPLICATE KEY UPDATE / ON CONFLICT DO UPDATE SET) */
|
|
3189
|
+
_conflictSets(statement) {
|
|
3190
|
+
if (statement.onDuplicate) return statement.onDuplicate;
|
|
3191
|
+
if (statement.onConflict && statement.onConflict.action === 'update') return statement.onConflict.sets;
|
|
3192
|
+
return null;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
/**
|
|
3196
|
+
* 把 RETURNING 结果挂到返回对象上。
|
|
3197
|
+
* RETURNING * → 返回完整行;RETURNING a, b → 只取指定列。
|
|
3198
|
+
*/
|
|
3199
|
+
_attachReturning(out, statement, rows, schema) {
|
|
3200
|
+
const cols = Array.isArray(statement.returning) ? statement.returning : [];
|
|
3201
|
+
const isStar = cols.length === 1 && cols[0] === '*';
|
|
3202
|
+
const outCols = isStar
|
|
3203
|
+
? (schema ? Object.keys(schema).filter(k => k !== '_softDelete') : Object.keys((rows && rows[0]) || {}))
|
|
3204
|
+
: cols;
|
|
3205
|
+
out.returning = {
|
|
3206
|
+
columns: outCols,
|
|
3207
|
+
rows: (rows || []).map(r => outCols.map(c => (r && r[c] !== undefined ? r[c] : null))),
|
|
3208
|
+
};
|
|
3209
|
+
out.columns = outCols;
|
|
3210
|
+
out.rows = out.returning.rows;
|
|
3211
|
+
return out;
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
/** 收集子树里所有列名(含限定形式 alias.col) */
|
|
3215
|
+
_collectColumnNames(node, out = []) {
|
|
3216
|
+
if (!node || typeof node !== 'object') return out;
|
|
3217
|
+
if (Array.isArray(node)) { for (const n of node) this._collectColumnNames(n, out); return out; }
|
|
3218
|
+
if (node.type === 'column' && typeof node.name === 'string') out.push(node.name);
|
|
3219
|
+
for (const k of Object.keys(node)) {
|
|
3220
|
+
const v = node[k];
|
|
3221
|
+
if (v && typeof v === 'object') this._collectColumnNames(v, out);
|
|
3222
|
+
}
|
|
3223
|
+
return out;
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
/** 外层 FROM 的表名 / 别名集合 */
|
|
3227
|
+
_outerAliases(statement) {
|
|
3228
|
+
const set = new Set();
|
|
3229
|
+
if (!statement || !statement.from) return set;
|
|
3230
|
+
const add = (it) => { if (it && (it.alias || it.table)) set.add(String(it.alias || it.table).toLowerCase()); };
|
|
3231
|
+
for (const t of (statement.from.tables || [])) add(t);
|
|
3232
|
+
for (const j of (statement.from.joins || [])) add(j.item);
|
|
3233
|
+
return set;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
/**
|
|
3237
|
+
* 子查询是否引用了外层别名(即相关子查询)。
|
|
3238
|
+
* 非相关子查询只求值一次,避免无谓的 O(n) 逐行重跑。
|
|
3239
|
+
*/
|
|
3240
|
+
_isCorrelated(node, outerAliases) {
|
|
3241
|
+
if (!outerAliases || outerAliases.size === 0) return false;
|
|
3242
|
+
for (const n of this._collectColumnNames(node, [])) {
|
|
3243
|
+
const dot = n.indexOf('.');
|
|
3244
|
+
if (dot === -1) continue;
|
|
3245
|
+
if (outerAliases.has(n.slice(0, dot).toLowerCase())) return true;
|
|
3246
|
+
}
|
|
3247
|
+
return false;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
/**
|
|
3251
|
+
* 投影行数组。若 SELECT 列表里有相关子查询(如
|
|
3252
|
+
* `(SELECT COUNT(*) FROM o WHERE o.uid = u.id)`),必须"求值一行、投影一行"交错进行,
|
|
3253
|
+
* 因为子查询节点是共享对象,先全算再投影只会留下最后一行的结果。
|
|
3254
|
+
*/
|
|
3255
|
+
async _projectRows(statement, rows, projectRow) {
|
|
3256
|
+
const outerAliases = this._outerAliases(statement);
|
|
3257
|
+
const found = [];
|
|
3258
|
+
for (const c of statement.columns) {
|
|
3259
|
+
if (c.scalar) this._collectSubqueries(c.scalar, found);
|
|
3260
|
+
if (c.caseExpr) this._collectSubqueries(c.caseExpr, found);
|
|
3261
|
+
}
|
|
3262
|
+
const subs = found.filter(n => this._isCorrelated(n, outerAliases));
|
|
3263
|
+
if (subs.length === 0) return rows.map(projectRow);
|
|
3264
|
+
const out = [];
|
|
3265
|
+
for (const r of rows) {
|
|
3266
|
+
for (const n of subs) await this._evalCorrelated(n, r);
|
|
3267
|
+
out.push(projectRow(r));
|
|
3268
|
+
}
|
|
3269
|
+
return out;
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
/**
|
|
3273
|
+
* 按外层行求值一个子查询节点。
|
|
3274
|
+
* 临时把外层行挂到 ctx.__outer,让 `u.id` 这类外层引用能解析到。
|
|
3275
|
+
* 结果写回节点:exists→_rowCount,subquery→_value,in→list。
|
|
3276
|
+
*/
|
|
3277
|
+
async _evalCorrelated(node, outerRow) {
|
|
3278
|
+
const prevCtx = this.ctx;
|
|
3279
|
+
this.ctx = outerRow ? Object.assign({}, prevCtx || {}, { __outer: outerRow }) : prevCtx;
|
|
3280
|
+
try {
|
|
3281
|
+
if (node.type === 'exists') {
|
|
3282
|
+
const res = await this.executeSelect(node.select);
|
|
3283
|
+
node._rowCount = res.rows.length;
|
|
3284
|
+
} else if (node.type === 'subquery') {
|
|
3285
|
+
const res = await this.executeSelect(node.select);
|
|
3286
|
+
node._value = res.rows.length > 0 ? res.rows[0][0] : null;
|
|
3287
|
+
} else if (node.type === 'in') {
|
|
3288
|
+
const res = await this.executeSelect(node.subquery);
|
|
3289
|
+
node.list = res.rows.map(r => (Array.isArray(r) ? r[0] : r));
|
|
3290
|
+
}
|
|
3291
|
+
} finally {
|
|
3292
|
+
this.ctx = prevCtx;
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
/**
|
|
3297
|
+
* 把 AST 中引用 CTE 名称的 FROM 项替换成对应的子查询(就地改写)。
|
|
3298
|
+
* map: 小写 CTE 名 -> 已展开的 select AST
|
|
3299
|
+
*/
|
|
3300
|
+
_inlineCteRefs(node, map) {
|
|
3301
|
+
if (!node || typeof node !== 'object' || map.size === 0) return;
|
|
3302
|
+
if (Array.isArray(node)) {
|
|
3303
|
+
for (const n of node) this._inlineCteRefs(n, map);
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
if (node.type === 'select' && node.from) {
|
|
3307
|
+
const rewrite = (item) => {
|
|
3308
|
+
if (!item || !item.table) return;
|
|
3309
|
+
const key = String(item.table).toLowerCase();
|
|
3310
|
+
if (!map.has(key)) return;
|
|
3311
|
+
const sub = JSON.parse(JSON.stringify(map.get(key)));
|
|
3312
|
+
item.subquery = sub;
|
|
3313
|
+
item.alias = item.alias || item.table;
|
|
3314
|
+
delete item.table;
|
|
3315
|
+
};
|
|
3316
|
+
for (const t of (node.from.tables || [])) rewrite(t);
|
|
3317
|
+
for (const j of (node.from.joins || [])) rewrite(j.item);
|
|
3318
|
+
}
|
|
3319
|
+
for (const k of Object.keys(node)) {
|
|
3320
|
+
if (k === 'from') continue; // 已单独处理,避免重复改写
|
|
3321
|
+
const v = node[k];
|
|
3322
|
+
if (v && typeof v === 'object') this._inlineCteRefs(v, map);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
/**
|
|
3327
|
+
* 计算 SELECT 列表中的窗口函数列,把结果直接写回每一行(键名为输出列名)。
|
|
3328
|
+
* 放在 WHERE / GROUP BY / HAVING 之后、ORDER BY / LIMIT 之前,符合 SQL 求值顺序。
|
|
3329
|
+
*/
|
|
3330
|
+
_computeWindows(statement, rows) {
|
|
3331
|
+
if (!rows || rows.length === 0) return rows;
|
|
3332
|
+
const specs = [];
|
|
3333
|
+
for (const c of statement.columns) {
|
|
3334
|
+
const sc = c.scalar;
|
|
3335
|
+
const over = (sc && sc.over) || c.over;
|
|
3336
|
+
if (!over) continue;
|
|
3337
|
+
const fn = sc && sc.type === 'aggregate' ? String(sc.fn)
|
|
3338
|
+
: (sc && sc.type === 'func' ? String(sc.name).toUpperCase()
|
|
3339
|
+
: String(c.aggregate || '').toUpperCase());
|
|
3340
|
+
specs.push({ col: c, spec: sc, over, fn, name: scalarColumnName(c) });
|
|
3341
|
+
}
|
|
3342
|
+
if (specs.length === 0) return rows;
|
|
3343
|
+
const toNode = (x) => !x ? null : (typeof x === 'string' ? { type: 'column', name: x } : x);
|
|
3344
|
+
specs.forEach((w, i) => { w.col._windowKey = w.col.alias || ('__win_' + i); });
|
|
3345
|
+
|
|
3346
|
+
for (const w of specs) {
|
|
3347
|
+
const partBy = w.over.partitionBy || [];
|
|
3348
|
+
const orderBy = w.over.orderBy || [];
|
|
3349
|
+
// 分区
|
|
3350
|
+
const buckets = new Map();
|
|
3351
|
+
rows.forEach((r, idx) => {
|
|
3352
|
+
const key = partBy.length
|
|
3353
|
+
? partBy.map(p => JSON.stringify(resolveOperand(p, r, this.ctx))).join('')
|
|
3354
|
+
: '';
|
|
3355
|
+
if (!buckets.has(key)) buckets.set(key, []);
|
|
3356
|
+
buckets.get(key).push(idx);
|
|
3357
|
+
});
|
|
3358
|
+
|
|
3359
|
+
for (const idxs of buckets.values()) {
|
|
3360
|
+
let ordered = idxs.slice();
|
|
3361
|
+
if (orderBy.length) {
|
|
3362
|
+
ordered = ordered.slice().sort((ia, ib) => {
|
|
3363
|
+
const ra = rows[ia], rb = rows[ib];
|
|
3364
|
+
for (const o of orderBy) {
|
|
3365
|
+
const av = resolveOperand(o.column, ra, this.ctx);
|
|
3366
|
+
const bv = resolveOperand(o.column, rb, this.ctx);
|
|
3367
|
+
if (av === bv) continue;
|
|
3368
|
+
if (av === undefined || av === null) return -1;
|
|
3369
|
+
if (bv === undefined || bv === null) return 1;
|
|
3370
|
+
const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
|
|
3371
|
+
if (d !== 0) return o.dir === 'asc' ? d : -d;
|
|
3372
|
+
}
|
|
3373
|
+
return ia - ib;
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
// 帧范围(ROWS 帧;RANGE 在当前实现下退化为整个分区,保守处理)
|
|
3377
|
+
const frameIdx = (i) => {
|
|
3378
|
+
const f = w.over.frame;
|
|
3379
|
+
if (!f) return { start: 0, end: ordered.length - 1 };
|
|
3380
|
+
let start = 0, end = ordered.length - 1;
|
|
3381
|
+
const apply = (b, isStart) => {
|
|
3382
|
+
if (!b) return isStart ? 0 : ordered.length - 1;
|
|
3383
|
+
if (b.type === 'unbounded') return b.direction === 'preceding' ? 0 : ordered.length - 1;
|
|
3384
|
+
if (b.type === 'currentRow') return i;
|
|
3385
|
+
return b.direction === 'preceding' ? i - b.value : i + b.value;
|
|
3386
|
+
};
|
|
3387
|
+
start = apply(f.start, true);
|
|
3388
|
+
end = apply(f.end, false);
|
|
3389
|
+
return { start: Math.max(0, start), end: Math.min(ordered.length - 1, end) };
|
|
3390
|
+
};
|
|
3391
|
+
|
|
3392
|
+
const argNode = toNode((w.spec && (w.spec.column || (w.spec.args && w.spec.args[0]))) || w.col.column || w.col.expr);
|
|
3393
|
+
const valAt = (rowIdx, node) => resolveOperand(node || { type: 'value', value: 1 }, rows[rowIdx], this.ctx);
|
|
3394
|
+
|
|
3395
|
+
ordered.forEach((rowIdx, pos) => {
|
|
3396
|
+
let v = null;
|
|
3397
|
+
switch (w.fn) {
|
|
3398
|
+
case 'ROW_NUMBER': v = pos + 1; break;
|
|
3399
|
+
case 'RANK': {
|
|
3400
|
+
// RANK = 1 + 分区内排在本行之前、且不是同值(peer)的行数
|
|
3401
|
+
v = 1;
|
|
3402
|
+
for (let k = 0; k < pos; k++) {
|
|
3403
|
+
if (this._windowPeerCmp(rows[ordered[k]], rows[rowIdx], orderBy, this.ctx) !== 0) v++;
|
|
3404
|
+
}
|
|
3405
|
+
break;
|
|
3406
|
+
}
|
|
3407
|
+
case 'DENSE_RANK': {
|
|
3408
|
+
// 1 + 排在本行之前的不同取值个数(同值并列,不跳号)
|
|
3409
|
+
const curKey = this._windowPeerKey(rows[rowIdx], orderBy, this.ctx);
|
|
3410
|
+
const seen = new Set();
|
|
3411
|
+
for (let k = 0; k < pos; k++) {
|
|
3412
|
+
const kk = this._windowPeerKey(rows[ordered[k]], orderBy, this.ctx);
|
|
3413
|
+
if (kk !== curKey) seen.add(kk);
|
|
3414
|
+
}
|
|
3415
|
+
v = seen.size + 1;
|
|
3416
|
+
break;
|
|
3417
|
+
}
|
|
3418
|
+
case 'NTILE': {
|
|
3419
|
+
const n = w.spec && w.spec.args && w.spec.args[0] ? Number(resolveOperand(w.spec.args[0], rows[rowIdx], this.ctx)) : 1;
|
|
3420
|
+
const bucketsN = Math.max(1, Math.floor(n) || 1);
|
|
3421
|
+
v = Math.min(bucketsN, Math.floor((pos * bucketsN) / ordered.length) + 1);
|
|
3422
|
+
break;
|
|
3423
|
+
}
|
|
3424
|
+
case 'LAG':
|
|
3425
|
+
case 'LEAD': {
|
|
3426
|
+
const off = w.spec && w.spec.args && w.spec.args[1] ? Number(resolveOperand(w.spec.args[1], rows[rowIdx], this.ctx)) : 1;
|
|
3427
|
+
const target = w.fn === 'LAG' ? pos - off : pos + off;
|
|
3428
|
+
if (target < 0 || target >= ordered.length) {
|
|
3429
|
+
v = w.spec && w.spec.args && w.spec.args[2] ? resolveOperand(w.spec.args[2], rows[rowIdx], this.ctx) : null;
|
|
3430
|
+
} else {
|
|
3431
|
+
v = valAt(ordered[target], argNode);
|
|
3432
|
+
}
|
|
3433
|
+
break;
|
|
3434
|
+
}
|
|
3435
|
+
case 'FIRST_VALUE':
|
|
3436
|
+
case 'LAST_VALUE': {
|
|
3437
|
+
const f = frameIdx(pos);
|
|
3438
|
+
const t = w.fn === 'FIRST_VALUE' ? f.start : f.end;
|
|
3439
|
+
v = valAt(ordered[t], argNode);
|
|
3440
|
+
break;
|
|
3441
|
+
}
|
|
3442
|
+
case 'COUNT':
|
|
3443
|
+
case 'SUM':
|
|
3444
|
+
case 'AVG':
|
|
3445
|
+
case 'MIN':
|
|
3446
|
+
case 'MAX': {
|
|
3447
|
+
const f = frameIdx(pos);
|
|
3448
|
+
const slice = ordered.slice(f.start, f.end + 1).map(i => rows[i]);
|
|
3449
|
+
v = this._aggValue(slice, w.fn, argNode);
|
|
3450
|
+
break;
|
|
3451
|
+
}
|
|
3452
|
+
default: v = null;
|
|
3453
|
+
}
|
|
3454
|
+
rows[rowIdx][w.col._windowKey] = v;
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
return rows;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
_windowPeerKey(row, orderBy, ctx) {
|
|
3462
|
+
if (!orderBy.length) return '';
|
|
3463
|
+
return orderBy.map(o => JSON.stringify(resolveOperand(o.column, row, ctx))).join('');
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
_windowPeerCmp(rowA, rowB, orderBy, ctx) {
|
|
3467
|
+
if (!orderBy.length) return 0;
|
|
3468
|
+
for (const o of orderBy) {
|
|
3469
|
+
const av = resolveOperand(o.column, rowA, ctx);
|
|
3470
|
+
const bv = resolveOperand(o.column, rowB, ctx);
|
|
3471
|
+
if (av === bv) continue;
|
|
3472
|
+
if (av === undefined || av === null) return -1;
|
|
3473
|
+
if (bv === undefined || bv === null) return 1;
|
|
3474
|
+
const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
|
|
3475
|
+
if (d !== 0) return d;
|
|
3476
|
+
}
|
|
3477
|
+
return 0;
|
|
3478
|
+
}
|
|
3479
|
+
|
|
2441
3480
|
_subQueryRows(res) {
|
|
2442
3481
|
return res.rows.map(r => {
|
|
2443
3482
|
if (Array.isArray(r)) {
|
|
@@ -2449,24 +3488,65 @@ class SQLExecutor {
|
|
|
2449
3488
|
});
|
|
2450
3489
|
}
|
|
2451
3490
|
|
|
3491
|
+
/**
|
|
3492
|
+
* 把 FROM 中引用视图的表名展开为视图定义里的子查询(就地改写)。
|
|
3493
|
+
* 与 CTE 一样走子查询内联,不需要引擎侧物化视图。
|
|
3494
|
+
*/
|
|
3495
|
+
_expandViews(statement, depth = 0) {
|
|
3496
|
+
const views = this.engine && this.engine._views;
|
|
3497
|
+
if (!views || !statement || !statement.from) return statement;
|
|
3498
|
+
if (depth > 16) throw new Error('View nesting too deep — possible circular view definition');
|
|
3499
|
+
const rewrite = (item) => {
|
|
3500
|
+
if (!item || !item.table) return;
|
|
3501
|
+
const v = views[String(item.table).toLowerCase()];
|
|
3502
|
+
if (!v) return;
|
|
3503
|
+
const sub = JSON.parse(JSON.stringify(v.select));
|
|
3504
|
+
if (v.columns && v.columns.length) {
|
|
3505
|
+
sub.columns.forEach((c, i) => { if (v.columns[i] && !c.alias) c.alias = v.columns[i]; });
|
|
3506
|
+
}
|
|
3507
|
+
this._expandViews(sub, depth + 1); // 视图里再引用视图
|
|
3508
|
+
item.subquery = sub;
|
|
3509
|
+
item.alias = item.alias || item.table;
|
|
3510
|
+
delete item.table;
|
|
3511
|
+
};
|
|
3512
|
+
for (const t of (statement.from.tables || [])) rewrite(t);
|
|
3513
|
+
for (const j of (statement.from.joins || [])) rewrite(j.item);
|
|
3514
|
+
return statement;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
2452
3517
|
async executeSelect(statement) {
|
|
3518
|
+
// 视图:FROM 里的视图名展开成子查询
|
|
3519
|
+
if (this.engine && this.engine._views) this._expandViews(statement);
|
|
3520
|
+
|
|
2453
3521
|
// UNION 处理
|
|
2454
3522
|
if (statement.union) {
|
|
2455
3523
|
const left = await this.executeSelect({ ...statement, union: null });
|
|
2456
3524
|
const right = await this.executeSelect(statement.union.select);
|
|
2457
3525
|
let rows = left.rows.concat(right.rows);
|
|
2458
|
-
if (!statement.union.all)
|
|
2459
|
-
const seen = new Set();
|
|
2460
|
-
rows = rows.filter(r => {
|
|
2461
|
-
const k = JSON.stringify(r);
|
|
2462
|
-
if (seen.has(k)) return false;
|
|
2463
|
-
seen.add(k);
|
|
2464
|
-
return true;
|
|
2465
|
-
});
|
|
2466
|
-
}
|
|
3526
|
+
if (!statement.union.all) rows = dedupeRows(rows);
|
|
2467
3527
|
return { ok: true, type: 'select', columns: left.columns, rows, raw: rows };
|
|
2468
3528
|
}
|
|
2469
3529
|
|
|
3530
|
+
// INTERSECT:保留同时出现在右侧结果中的行(默认 DISTINCT,与 UNION 的处理保持一致)
|
|
3531
|
+
if (statement.intersect) {
|
|
3532
|
+
const left = await this.executeSelect({ ...statement, intersect: null, except: null });
|
|
3533
|
+
const right = await this.executeSelect(statement.intersect.select);
|
|
3534
|
+
const seen = new Set(right.rows.map(r => JSON.stringify(r)));
|
|
3535
|
+
let out = left.rows.filter(r => seen.has(JSON.stringify(r)));
|
|
3536
|
+
if (!statement.intersect.all) out = dedupeRows(out);
|
|
3537
|
+
return { ok: true, type: 'select', columns: left.columns, rows: out, raw: out };
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// EXCEPT:去掉出现在右侧结果中的行(默认 DISTINCT)
|
|
3541
|
+
if (statement.except) {
|
|
3542
|
+
const left = await this.executeSelect({ ...statement, intersect: null, except: null });
|
|
3543
|
+
const right = await this.executeSelect(statement.except.select);
|
|
3544
|
+
const seen = new Set(right.rows.map(r => JSON.stringify(r)));
|
|
3545
|
+
let out = left.rows.filter(r => !seen.has(JSON.stringify(r)));
|
|
3546
|
+
if (!statement.except.all) out = dedupeRows(out);
|
|
3547
|
+
return { ok: true, type: 'select', columns: left.columns, rows: out, raw: out };
|
|
3548
|
+
}
|
|
3549
|
+
|
|
2470
3550
|
// 物化 WHERE / HAVING / JOIN ON 中的子查询
|
|
2471
3551
|
await this._materialize(statement.where);
|
|
2472
3552
|
await this._materialize(statement.having);
|
|
@@ -2475,6 +3555,8 @@ class SQLExecutor {
|
|
|
2475
3555
|
}
|
|
2476
3556
|
for (const c of statement.columns) {
|
|
2477
3557
|
if (c.caseExpr) await this._materialize(c.caseExpr);
|
|
3558
|
+
// 非相关标量子查询先求值一次;相关子查询稍后由 _projectRows 逐行覆盖
|
|
3559
|
+
if (c.scalar) await this._materialize(c.scalar);
|
|
2478
3560
|
}
|
|
2479
3561
|
|
|
2480
3562
|
let schema = null;
|
|
@@ -2544,11 +3626,11 @@ class SQLExecutor {
|
|
|
2544
3626
|
if (m !== null) {
|
|
2545
3627
|
matched.push({ ...l, ...rightRows[m] });
|
|
2546
3628
|
unmatchedRight.delete(m);
|
|
2547
|
-
} else if (j.type === 'left') {
|
|
3629
|
+
} else if (j.type === 'left' || j.type === 'full') {
|
|
2548
3630
|
matched.push(rightNulls ? { ...rightNulls, ...l } : { ...l });
|
|
2549
3631
|
}
|
|
2550
3632
|
});
|
|
2551
|
-
if (j.type === 'right') {
|
|
3633
|
+
if (j.type === 'right' || j.type === 'full') {
|
|
2552
3634
|
for (const ri of unmatchedRight) matched.push(leftNulls ? { ...leftNulls, ...rightRows[ri] } : { ...rightRows[ri] });
|
|
2553
3635
|
}
|
|
2554
3636
|
rows = matched;
|
|
@@ -2558,7 +3640,20 @@ class SQLExecutor {
|
|
|
2558
3640
|
|
|
2559
3641
|
let rows = all;
|
|
2560
3642
|
if (statement.where) {
|
|
2561
|
-
|
|
3643
|
+
// 相关子查询:必须逐行重新求值,否则外层引用拿不到当前行的值
|
|
3644
|
+
const outerAliases = this._outerAliases(statement);
|
|
3645
|
+
const subNodes = this._collectSubqueries(statement.where)
|
|
3646
|
+
.filter(n => this._isCorrelated(n, outerAliases));
|
|
3647
|
+
if (subNodes.length > 0) {
|
|
3648
|
+
const kept = [];
|
|
3649
|
+
for (const r of rows) {
|
|
3650
|
+
for (const node of subNodes) await this._evalCorrelated(node, r);
|
|
3651
|
+
if (evaluateExpr(statement.where, r, this.ctx)) kept.push(r);
|
|
3652
|
+
}
|
|
3653
|
+
rows = kept;
|
|
3654
|
+
} else {
|
|
3655
|
+
rows = rows.filter(r => evaluateExpr(statement.where, r, this.ctx));
|
|
3656
|
+
}
|
|
2562
3657
|
}
|
|
2563
3658
|
|
|
2564
3659
|
// 分组聚合
|
|
@@ -2596,6 +3691,11 @@ class SQLExecutor {
|
|
|
2596
3691
|
});
|
|
2597
3692
|
}
|
|
2598
3693
|
|
|
3694
|
+
// 窗口函数:在 WHERE/GROUP BY/HAVING 之后、DISTINCT/ORDER BY/LIMIT 之前求值
|
|
3695
|
+
if (statement.columns.some(c => (c.scalar && c.scalar.over) || c.over)) {
|
|
3696
|
+
this._computeWindows(statement, rows);
|
|
3697
|
+
}
|
|
3698
|
+
|
|
2599
3699
|
if (statement.distinct) {
|
|
2600
3700
|
const seen = new Set();
|
|
2601
3701
|
rows = rows.filter(r => {
|
|
@@ -2613,7 +3713,8 @@ class SQLExecutor {
|
|
|
2613
3713
|
|
|
2614
3714
|
// 聚合输出(无 GROUP BY 时)
|
|
2615
3715
|
const hasCustomAgg = this.ctx && this.ctx.aggregates && statement.columns.some(c => c.scalar && c.scalar.type === 'func' && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, String(c.scalar.name).toUpperCase()));
|
|
2616
|
-
|
|
3716
|
+
// 注意:带 OVER 的聚合是窗口函数,不是“整表塌缩成一行”的聚合,必须排除
|
|
3717
|
+
if (!statement.groupBy && (statement.columns.some(c => c.aggregate && !c.window) || hasCustomAgg)) {
|
|
2617
3718
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
2618
3719
|
const valueOf = (c) => {
|
|
2619
3720
|
if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column);
|
|
@@ -2634,18 +3735,26 @@ class SQLExecutor {
|
|
|
2634
3735
|
// GROUP BY 输出:每组的列(含聚合列)
|
|
2635
3736
|
if (statement.groupBy) {
|
|
2636
3737
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
2637
|
-
const
|
|
3738
|
+
const rowsForGroups = (statement.rollup && rows.length > 0)
|
|
3739
|
+
// WITH ROLLUP:追加一个"全表汇总"分组(分组列取 NULL,聚合列覆盖全部行)
|
|
3740
|
+
? rows.concat([this._rollupRow(rows)])
|
|
3741
|
+
: rows;
|
|
3742
|
+
const mapped = await this._projectRows(statement, rowsForGroups, (r) => {
|
|
2638
3743
|
const group = r._group || [r];
|
|
2639
3744
|
return statement.columns.map(c => scalarColumnValue(c, r, { _aggValue: this._aggValue.bind(this), group, ctxAggregates: this.ctx ? this.ctx.aggregates : null }));
|
|
2640
3745
|
});
|
|
2641
|
-
return { ok: true, type: 'select', table: statement.from ? (statement.from.tables[0].table || null) : null, columns: cols, rows: mapped, raw:
|
|
3746
|
+
return { ok: true, type: 'select', table: statement.from ? (statement.from.tables[0].table || null) : null, columns: cols, rows: mapped, raw: rowsForGroups };
|
|
2642
3747
|
}
|
|
2643
3748
|
|
|
2644
3749
|
if (statement.orderBy) {
|
|
3750
|
+
// ORDER BY 项可能是列名字符串,也可能是表达式节点
|
|
3751
|
+
const ordVal = (row, o) => (typeof o.column === 'string'
|
|
3752
|
+
? resolveOperand({ type: 'column', name: o.column }, row, this.ctx)
|
|
3753
|
+
: resolveOperand(o.column, row, this.ctx));
|
|
2645
3754
|
const cmp = (a, b) => {
|
|
2646
3755
|
for (const o of statement.orderBy) {
|
|
2647
|
-
const av =
|
|
2648
|
-
const bv =
|
|
3756
|
+
const av = ordVal(a, o);
|
|
3757
|
+
const bv = ordVal(b, o);
|
|
2649
3758
|
if (av === bv || (av === undefined && bv === undefined)) continue;
|
|
2650
3759
|
if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
|
|
2651
3760
|
if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
|
|
@@ -2680,7 +3789,8 @@ class SQLExecutor {
|
|
|
2680
3789
|
}
|
|
2681
3790
|
|
|
2682
3791
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
2683
|
-
const mapped =
|
|
3792
|
+
const mapped = await this._projectRows(statement, rows, (r) => statement.columns.map(c => {
|
|
3793
|
+
if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
|
|
2684
3794
|
if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
|
|
2685
3795
|
if (c.expr === '*') return null;
|
|
2686
3796
|
if (c.literal !== undefined) return c.literal;
|
|
@@ -2694,18 +3804,25 @@ class SQLExecutor {
|
|
|
2694
3804
|
function splitStatements(sql) {
|
|
2695
3805
|
const statements = [];
|
|
2696
3806
|
let current = '';
|
|
2697
|
-
let inStr = null;
|
|
3807
|
+
let inStr = null; // "'" | '"' | '`'
|
|
2698
3808
|
let i = 0;
|
|
2699
3809
|
while (i < sql.length) {
|
|
2700
3810
|
const c = sql[i];
|
|
2701
3811
|
if (inStr) {
|
|
2702
3812
|
current += c;
|
|
3813
|
+
if (inStr === '`') {
|
|
3814
|
+
// 反引号标识符内部反斜杠不是转义符;两个连续反引号才表示一个字面反引号。
|
|
3815
|
+
if (c === '`' && sql[i + 1] === '`') { current += sql[i + 1]; i += 2; continue; }
|
|
3816
|
+
if (c === '`') inStr = null;
|
|
3817
|
+
i++;
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
2703
3820
|
if (c === '\\' && i + 1 < sql.length) { current += sql[i + 1]; i += 2; continue; }
|
|
2704
3821
|
if (c === inStr) inStr = null;
|
|
2705
3822
|
i++;
|
|
2706
3823
|
continue;
|
|
2707
3824
|
}
|
|
2708
|
-
if (c === "'" || c === '"') { inStr = c; current += c; i++; continue; }
|
|
3825
|
+
if (c === "'" || c === '"' || c === '`') { inStr = c; current += c; i++; continue; }
|
|
2709
3826
|
if (c === '-' && sql[i + 1] === '-') {
|
|
2710
3827
|
while (i < sql.length && sql[i] !== '\n') i++;
|
|
2711
3828
|
continue;
|
|
@@ -2911,6 +4028,7 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
2911
4028
|
}
|
|
2912
4029
|
const ctx = {};
|
|
2913
4030
|
if (opts.session) ctx.session = opts.session;
|
|
4031
|
+
if (opts.params) ctx.params = opts.params; // 原生 ? 占位符(applyParams 之外的另一种用法)
|
|
2914
4032
|
if (opts.functions && typeof opts.functions === 'object') ctx.functions = opts.functions;
|
|
2915
4033
|
if (opts.aggregates && typeof opts.aggregates === 'object') ctx.aggregates = opts.aggregates;
|
|
2916
4034
|
const executor = new SQLExecutor(engine, Object.keys(ctx).length > 0 ? ctx : null);
|
|
@@ -2922,4 +4040,14 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
2922
4040
|
return results.length === 1 ? results[0] : results;
|
|
2923
4041
|
}
|
|
2924
4042
|
|
|
2925
|
-
|
|
4043
|
+
// AST 访问与改写能力(纯新增,不改变既有导出项)
|
|
4044
|
+
const AST = require('./ast.js');
|
|
4045
|
+
|
|
4046
|
+
module.exports = {
|
|
4047
|
+
tokenize, parseSQL, executeSQL, SQLExecutor, Parser,
|
|
4048
|
+
splitStatements, applyParams, escapeValue, escapeId,
|
|
4049
|
+
AST,
|
|
4050
|
+
walk: AST.walk,
|
|
4051
|
+
transform: AST.transform,
|
|
4052
|
+
visit: AST.StatementVisitor
|
|
4053
|
+
};
|