jsql-neo 4.0.2 → 4.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/bin/jsql +69 -0
- package/bin/jsql-server +90 -0
- package/bin/jsql-server-control.js +177 -0
- package/lib/database.js +98 -9
- package/lib/errors.js +6 -0
- package/lib/jsql_format.js +33 -14
- package/lib/mod.js +43 -67
- package/lib/mysql_compat.js +134 -17
- package/lib/mysql_server.js +315 -22
- package/lib/native_client.js +95 -32
- package/lib/sql.js +646 -122
- package/lib/table.js +114 -12
- package/lib/wasm_client.js +59 -6
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +455 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +12 -6
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +28 -7
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +22 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +53 -23
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -1
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +1 -0
- package/nativesrc/jsql-neo-native/build.rs +3 -0
- package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/src/lib.rs +75 -87
- package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -0
- package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -0
- package/nativesrc/jsql-neo-wasm/src/lib.rs +423 -0
- package/package.json +8 -1
- package/wasm/jsql_neo_wasm.d.ts +2 -0
- package/wasm/jsql_neo_wasm.js +5 -0
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +1 -0
package/lib/sql.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const DANGEROUS_SQL = [
|
|
2
2
|
{ re: /^INTO$/, next: /^(OUTFILE|DUMPFILE)$/, name: 'INTO OUTFILE/DUMPFILE' },
|
|
3
3
|
{ re: /^LOAD$/, next: /^(FILE|DATA)$/, name: 'LOAD_FILE/LOAD DATA' },
|
|
4
|
+
{ re: /^LOAD_FILE$/, name: 'LOAD_FILE' },
|
|
5
|
+
{ re: /^LOAD_DATA$/, next: /^(INFILE)$/, name: 'LOAD DATA INFILE' },
|
|
4
6
|
{ re: /^SLEEP$/, name: 'SLEEP' },
|
|
5
7
|
{ re: /^BENCHMARK$/, name: 'BENCHMARK' },
|
|
6
8
|
{ re: /^GET_LOCK$/, name: 'GET_LOCK' },
|
|
@@ -18,7 +20,7 @@ function findDangerousSQL(tokens) {
|
|
|
18
20
|
if (d.re.test(upper)) {
|
|
19
21
|
if (d.next) {
|
|
20
22
|
const nxt = tokens[i + 1];
|
|
21
|
-
if (nxt &&
|
|
23
|
+
if (nxt && d.next.test(String(nxt.value).toUpperCase())) {
|
|
22
24
|
return d.name;
|
|
23
25
|
}
|
|
24
26
|
} else {
|
|
@@ -42,12 +44,15 @@ const KEYWORDS = new Set([
|
|
|
42
44
|
'CREATE', 'TABLE', 'DROP', 'INSERT', 'INTO', 'VALUES', 'SELECT', 'FROM',
|
|
43
45
|
'WHERE', 'UPDATE', 'SET', 'DELETE', 'AND', 'OR', 'NOT', 'NULL', 'IS',
|
|
44
46
|
'LIKE', 'IN', 'LIMIT', 'OFFSET', 'ORDER', 'BY', 'ASC', 'DESC', 'PRIMARY',
|
|
45
|
-
'KEY', 'AUTO_INCREMENT', 'INTEGER', 'INT', 'BIGINT', 'STRING', 'TEXT',
|
|
46
|
-
'FLOAT', 'DOUBLE', 'REAL', 'BOOLEAN', 'BOOL', 'DATE', 'DATETIME',
|
|
47
|
-
'TIMESTAMP', 'ANY', 'OBJECT', 'ARRAY', 'BEGIN', 'COMMIT', 'ROLLBACK',
|
|
47
|
+
'KEY', 'AUTO_INCREMENT', 'AUTOINCREMENT', 'INTEGER', 'INT', 'BIGINT', 'STRING', 'TEXT',
|
|
48
|
+
'VARCHAR', 'CHAR', 'FLOAT', 'DOUBLE', 'REAL', 'NUMERIC', 'DECIMAL', 'BOOLEAN', 'BOOL', 'DATE', 'DATETIME',
|
|
49
|
+
'TIMESTAMP', 'ANY', 'OBJECT', 'ARRAY', 'JSON', 'SMALLINT', 'TINYINT', 'BEGIN', 'COMMIT', 'ROLLBACK',
|
|
48
50
|
'TRANSACTION', 'WORK', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'AS', 'UNIQUE',
|
|
49
51
|
'NOTNULL', 'DEFAULT', 'IF', 'EXISTS', 'DISTINCT', 'SHOW', 'USE', 'TABLES',
|
|
50
|
-
'DATABASES', 'DESCRIBE', 'DESC', 'ON', 'DUPLICATE'
|
|
52
|
+
'DATABASES', 'DATABASE', 'DESCRIBE', 'DESC', 'ON', 'DUPLICATE',
|
|
53
|
+
'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'CROSS',
|
|
54
|
+
'GROUP', 'HAVING', 'UNION', 'ALL', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
|
|
55
|
+
'BETWEEN', 'USING', 'FULL'
|
|
51
56
|
]);
|
|
52
57
|
|
|
53
58
|
function tokenize(sql) {
|
|
@@ -192,8 +197,14 @@ class Parser {
|
|
|
192
197
|
if (t.type !== 'keyword') throw new Error(`Expected SQL statement, got '${t.value}'`);
|
|
193
198
|
|
|
194
199
|
switch (t.value) {
|
|
195
|
-
case 'CREATE':
|
|
196
|
-
|
|
200
|
+
case 'CREATE':
|
|
201
|
+
if (this.isKeyword('TABLE', 1)) return this.parseCreateTable();
|
|
202
|
+
if (this.isKeyword('DATABASE', 1)) return this.parseCreateDatabase();
|
|
203
|
+
throw new Error('Unsupported CREATE statement');
|
|
204
|
+
case 'DROP':
|
|
205
|
+
if (this.isKeyword('TABLE', 1)) return this.parseDropTable();
|
|
206
|
+
if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
|
|
207
|
+
throw new Error('Unsupported DROP statement');
|
|
197
208
|
case 'INSERT': return this.parseInsert();
|
|
198
209
|
case 'SELECT': return this.parseSelect();
|
|
199
210
|
case 'UPDATE': return this.parseUpdate();
|
|
@@ -292,6 +303,25 @@ class Parser {
|
|
|
292
303
|
if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
|
|
293
304
|
def.type = mapped;
|
|
294
305
|
|
|
306
|
+
// 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
|
|
307
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
308
|
+
this.next();
|
|
309
|
+
const lenTok = this.next();
|
|
310
|
+
if (lenTok.type !== 'number') throw new Error(`Expected length after type '(', got '${lenTok.value}'`);
|
|
311
|
+
const len = lenTok.value;
|
|
312
|
+
if (this.peek().type === 'op' && this.peek().value === ',') {
|
|
313
|
+
this.next();
|
|
314
|
+
const decTok = this.next();
|
|
315
|
+
if (decTok.type !== 'number') throw new Error(`Expected precision after ',', got '${decTok.value}'`);
|
|
316
|
+
def.precision = decTok.value;
|
|
317
|
+
}
|
|
318
|
+
this.expect('op', ')');
|
|
319
|
+
if (len > 0) {
|
|
320
|
+
def.length = len;
|
|
321
|
+
if (mapped === 'string') def.maxLength = len;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
295
325
|
while (true) {
|
|
296
326
|
const t = this.peek();
|
|
297
327
|
if (t.type === 'keyword') {
|
|
@@ -301,6 +331,7 @@ class Parser {
|
|
|
301
331
|
case 'KEY':
|
|
302
332
|
this.next(); def.primaryKey = true; def.unique = true; break;
|
|
303
333
|
case 'AUTO_INCREMENT':
|
|
334
|
+
case 'AUTOINCREMENT':
|
|
304
335
|
this.next(); def.autoIncrement = true; break;
|
|
305
336
|
case 'UNIQUE':
|
|
306
337
|
this.next(); def.unique = true; break;
|
|
@@ -415,12 +446,12 @@ class Parser {
|
|
|
415
446
|
this.expect('op', ')');
|
|
416
447
|
aggregate = { type: 'COUNT' };
|
|
417
448
|
if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
|
|
418
|
-
columns.push({ expr: null, aggregate:
|
|
449
|
+
columns.push({ expr: null, aggregate: 'COUNT' });
|
|
419
450
|
} else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
|
|
420
451
|
this.next();
|
|
421
452
|
const fn = t.value;
|
|
422
453
|
this.expect('op', '(');
|
|
423
|
-
const col = this.
|
|
454
|
+
const col = this.parseColumnRef();
|
|
424
455
|
this.expect('op', ')');
|
|
425
456
|
aggregate = { type: fn, column: col };
|
|
426
457
|
if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
|
|
@@ -433,6 +464,11 @@ class Parser {
|
|
|
433
464
|
let alias = null;
|
|
434
465
|
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
435
466
|
columns.push({ expr: null, literal: t.value, alias });
|
|
467
|
+
} else if (t.type === 'keyword' && t.value === 'CASE') {
|
|
468
|
+
const caseExpr = this.parseOperand();
|
|
469
|
+
let alias = null;
|
|
470
|
+
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
471
|
+
columns.push({ expr: null, caseExpr, alias });
|
|
436
472
|
} else {
|
|
437
473
|
const col = this.parseColumnRef();
|
|
438
474
|
let alias = null;
|
|
@@ -443,13 +479,25 @@ class Parser {
|
|
|
443
479
|
break;
|
|
444
480
|
}
|
|
445
481
|
|
|
446
|
-
let
|
|
482
|
+
let from = null;
|
|
447
483
|
if (this.isKeyword('FROM')) {
|
|
448
484
|
this.next();
|
|
449
|
-
|
|
485
|
+
from = this.parseFrom();
|
|
450
486
|
}
|
|
451
487
|
let where = null;
|
|
452
488
|
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
489
|
+
let groupBy = null;
|
|
490
|
+
if (this.isKeyword('GROUP')) {
|
|
491
|
+
this.expectKeyword('GROUP'); this.expectKeyword('BY');
|
|
492
|
+
groupBy = [];
|
|
493
|
+
while (true) {
|
|
494
|
+
groupBy.push(this.parseColumnRef());
|
|
495
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
let having = null;
|
|
500
|
+
if (this.isKeyword('HAVING')) { this.next(); having = this.parseExpr(); }
|
|
453
501
|
let orderBy = null;
|
|
454
502
|
if (this.isKeyword('ORDER')) {
|
|
455
503
|
this.expectKeyword('ORDER'); this.expectKeyword('BY');
|
|
@@ -471,9 +519,68 @@ class Parser {
|
|
|
471
519
|
if (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); }
|
|
472
520
|
else if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); offset = limit; limit = this.parseValue(); }
|
|
473
521
|
}
|
|
522
|
+
let union = null;
|
|
523
|
+
if (this.isKeyword('UNION')) {
|
|
524
|
+
this.next();
|
|
525
|
+
const all = this.isKeyword('ALL');
|
|
526
|
+
if (all) this.next();
|
|
527
|
+
union = { all, select: this.parseSelect() };
|
|
528
|
+
}
|
|
474
529
|
this.optionalTailSemicolon();
|
|
475
530
|
|
|
476
|
-
return { type: 'select', columns, aggregate, distinct,
|
|
531
|
+
return { type: 'select', columns, aggregate, distinct, from, where, groupBy, having, orderBy, limit, offset, union };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
parseFrom() {
|
|
535
|
+
const tables = [this.parseFromItem()];
|
|
536
|
+
const joins = [];
|
|
537
|
+
while (true) {
|
|
538
|
+
let type = null;
|
|
539
|
+
if (this.isKeyword('LEFT') || this.isKeyword('RIGHT')) {
|
|
540
|
+
type = this.peek().value.toLowerCase();
|
|
541
|
+
this.next();
|
|
542
|
+
if (this.isKeyword('OUTER')) this.next();
|
|
543
|
+
this.expectKeyword('JOIN');
|
|
544
|
+
} else if (this.isKeyword('INNER') || this.isKeyword('CROSS')) {
|
|
545
|
+
this.next();
|
|
546
|
+
this.expectKeyword('JOIN');
|
|
547
|
+
type = 'inner';
|
|
548
|
+
} else if (this.isKeyword('JOIN')) {
|
|
549
|
+
this.next();
|
|
550
|
+
type = 'inner';
|
|
551
|
+
} else if (this.peek().type === 'op' && this.peek().value === ',') {
|
|
552
|
+
this.next();
|
|
553
|
+
type = 'cross';
|
|
554
|
+
} else {
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
const item = this.parseFromItem();
|
|
558
|
+
let on = null;
|
|
559
|
+
if (this.isKeyword('ON')) { this.next(); on = this.parseExpr(); }
|
|
560
|
+
joins.push({ type, item, on });
|
|
561
|
+
}
|
|
562
|
+
return { tables, joins };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
parseFromItem() {
|
|
566
|
+
// 子查询: (SELECT ...) [AS] alias
|
|
567
|
+
if (this.peek().type === 'op' && this.peek().value === '(' && this.isKeyword('SELECT', 1)) {
|
|
568
|
+
this.next();
|
|
569
|
+
const sub = this.parseSelect();
|
|
570
|
+
this.expect('op', ')');
|
|
571
|
+
let alias = null;
|
|
572
|
+
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
573
|
+
else if (this.peek().type === 'ident') { alias = this.next().value; }
|
|
574
|
+
if (!alias) throw new Error('Subquery in FROM requires an alias');
|
|
575
|
+
return { subquery: sub, alias };
|
|
576
|
+
}
|
|
577
|
+
const table = this.parseTableName();
|
|
578
|
+
let alias = null;
|
|
579
|
+
if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
|
|
580
|
+
else if (this.peek().type === 'ident' && !this.isKeyword('JOIN') && !this.isKeyword('LEFT') && !this.isKeyword('RIGHT') && !this.isKeyword('WHERE') && !this.isKeyword('GROUP') && !this.isKeyword('ORDER') && !this.isKeyword('LIMIT') && !this.isKeyword('ON') && !this.isKeyword('HAVING') && !this.isKeyword('UNION') && !this.isKeyword('INNER') && !this.isKeyword('CROSS')) {
|
|
581
|
+
alias = this.next().value;
|
|
582
|
+
}
|
|
583
|
+
return { table, alias };
|
|
477
584
|
}
|
|
478
585
|
|
|
479
586
|
parseAlias() {
|
|
@@ -485,6 +592,12 @@ class Parser {
|
|
|
485
592
|
parseColumnRef() {
|
|
486
593
|
const t = this.next();
|
|
487
594
|
if (t.type !== 'ident') throw new Error(`Expected column name, got '${t.value}'`);
|
|
595
|
+
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
596
|
+
this.next();
|
|
597
|
+
const col = this.next();
|
|
598
|
+
if (col.type !== 'ident') throw new Error(`Expected column name after '.', got '${col.value}'`);
|
|
599
|
+
return t.value + '.' + col.value;
|
|
600
|
+
}
|
|
488
601
|
return t.value;
|
|
489
602
|
}
|
|
490
603
|
|
|
@@ -528,6 +641,31 @@ class Parser {
|
|
|
528
641
|
return { type: 'dropTable', table, ifExists };
|
|
529
642
|
}
|
|
530
643
|
|
|
644
|
+
parseCreateDatabase() {
|
|
645
|
+
this.expectKeyword('CREATE');
|
|
646
|
+
this.expectKeyword('DATABASE');
|
|
647
|
+
let ifNotExists = false;
|
|
648
|
+
if (this.isKeyword('IF')) {
|
|
649
|
+
this.expectKeyword('IF'); this.expectKeyword('NOT'); this.expectKeyword('EXISTS');
|
|
650
|
+
ifNotExists = true;
|
|
651
|
+
}
|
|
652
|
+
const name = this.parseTableName();
|
|
653
|
+
this.optionalTailSemicolon();
|
|
654
|
+
return { type: 'createDatabase', database: name, ifNotExists };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
parseDropDatabase() {
|
|
658
|
+
this.expectKeyword('DROP');
|
|
659
|
+
this.expectKeyword('DATABASE');
|
|
660
|
+
let ifExists = false;
|
|
661
|
+
if (this.isKeyword('IF')) {
|
|
662
|
+
this.expectKeyword('IF'); this.expectKeyword('EXISTS'); ifExists = true;
|
|
663
|
+
}
|
|
664
|
+
const name = this.parseTableName();
|
|
665
|
+
this.optionalTailSemicolon();
|
|
666
|
+
return { type: 'dropDatabase', database: name, ifExists };
|
|
667
|
+
}
|
|
668
|
+
|
|
531
669
|
parseShow() {
|
|
532
670
|
this.expectKeyword('SHOW');
|
|
533
671
|
if (this.isKeyword('TABLES')) { this.next(); this.optionalTailSemicolon(); return { type: 'showTables' }; }
|
|
@@ -593,9 +731,33 @@ class Parser {
|
|
|
593
731
|
|
|
594
732
|
parseOperand() {
|
|
595
733
|
const t = this.next();
|
|
596
|
-
if (t.type === 'ident')
|
|
734
|
+
if (t.type === 'ident') {
|
|
735
|
+
// 支持 alias.column 引用
|
|
736
|
+
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
737
|
+
this.next();
|
|
738
|
+
const col = this.next();
|
|
739
|
+
if (col.type !== 'ident') throw new Error(`Expected column name after '.', got '${col.value}'`);
|
|
740
|
+
return { type: 'column', name: t.value + '.' + col.value };
|
|
741
|
+
}
|
|
742
|
+
return { type: 'column', name: t.value };
|
|
743
|
+
}
|
|
597
744
|
if (t.type === 'number' || t.type === 'string') return { type: 'value', value: t.value };
|
|
598
745
|
if (t.type === 'keyword' && t.value === 'NULL') return { type: 'value', value: null };
|
|
746
|
+
if (t.type === 'keyword' && t.value === 'CASE') return this.parseCase();
|
|
747
|
+
if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX', 'COUNT'].includes(t.value)) {
|
|
748
|
+
const fn = t.value;
|
|
749
|
+
this.expect('op', '(');
|
|
750
|
+
let column = null;
|
|
751
|
+
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
|
|
752
|
+
else column = this.parseColumnRef();
|
|
753
|
+
this.expect('op', ')');
|
|
754
|
+
return { type: 'aggregate', fn, column };
|
|
755
|
+
}
|
|
756
|
+
if (t.type === 'op' && t.value === '(' && this.peek().type === 'keyword' && this.peek().value === 'SELECT') {
|
|
757
|
+
const sub = this.parseSelect();
|
|
758
|
+
this.expect('op', ')');
|
|
759
|
+
return { type: 'subquery', select: sub };
|
|
760
|
+
}
|
|
599
761
|
if (t.type === 'op' && (t.value === '-' || t.value === '+')) {
|
|
600
762
|
const num = this.next();
|
|
601
763
|
if (num.type !== 'number') throw new Error('Expected number after sign');
|
|
@@ -604,6 +766,32 @@ class Parser {
|
|
|
604
766
|
throw new Error(`Expected value or column, got '${t.value}'`);
|
|
605
767
|
}
|
|
606
768
|
|
|
769
|
+
parseCase() {
|
|
770
|
+
let base = null;
|
|
771
|
+
if (!this.isKeyword('WHEN')) {
|
|
772
|
+
base = this.parseOperand();
|
|
773
|
+
}
|
|
774
|
+
const branches = [];
|
|
775
|
+
while (this.isKeyword('WHEN')) {
|
|
776
|
+
this.next();
|
|
777
|
+
// 简单 CASE: CASE x WHEN v THEN ... ; 搜索 CASE: CASE WHEN cond THEN ...
|
|
778
|
+
let cond;
|
|
779
|
+
if (base) {
|
|
780
|
+
const v = this.parseOperand();
|
|
781
|
+
cond = { type: 'compare', op: '=', left: base, right: v };
|
|
782
|
+
} else {
|
|
783
|
+
cond = this.parseComparison();
|
|
784
|
+
}
|
|
785
|
+
this.expectKeyword('THEN');
|
|
786
|
+
const val = this.parseOperand();
|
|
787
|
+
branches.push({ cond, val });
|
|
788
|
+
}
|
|
789
|
+
let elseVal = null;
|
|
790
|
+
if (this.isKeyword('ELSE')) { this.next(); elseVal = this.parseOperand(); }
|
|
791
|
+
this.expectKeyword('END');
|
|
792
|
+
return { type: 'case', branches, elseVal };
|
|
793
|
+
}
|
|
794
|
+
|
|
607
795
|
parseComparison() {
|
|
608
796
|
const left = this.parseOperand();
|
|
609
797
|
const t = this.peek();
|
|
@@ -619,6 +807,12 @@ class Parser {
|
|
|
619
807
|
if (t.type === 'keyword' && t.value === 'IN') {
|
|
620
808
|
this.next();
|
|
621
809
|
this.expect('op', '(');
|
|
810
|
+
// IN (SELECT ...) 子查询
|
|
811
|
+
if (this.isKeyword('SELECT')) {
|
|
812
|
+
const sub = this.parseSelect();
|
|
813
|
+
this.expect('op', ')');
|
|
814
|
+
return { type: 'in', operand: left, subquery: sub };
|
|
815
|
+
}
|
|
622
816
|
const list = [];
|
|
623
817
|
while (true) {
|
|
624
818
|
list.push(this.parseValue());
|
|
@@ -629,6 +823,19 @@ class Parser {
|
|
|
629
823
|
return { type: 'in', operand: left, list };
|
|
630
824
|
}
|
|
631
825
|
|
|
826
|
+
if (t.type === 'keyword' && t.value === 'BETWEEN') {
|
|
827
|
+
this.next();
|
|
828
|
+
const low = this.parseOperand();
|
|
829
|
+
const not = false;
|
|
830
|
+
let andTok = this.peek();
|
|
831
|
+
if (andTok.type === 'keyword' && andTok.value === 'AND') {
|
|
832
|
+
this.next();
|
|
833
|
+
const high = this.parseOperand();
|
|
834
|
+
return { type: 'between', operand: left, low, high, not };
|
|
835
|
+
}
|
|
836
|
+
throw new Error(`Expected AND in BETWEEN, got '${andTok.value}'`);
|
|
837
|
+
}
|
|
838
|
+
|
|
632
839
|
if (t.type === 'keyword' && t.value === 'LIKE') {
|
|
633
840
|
this.next();
|
|
634
841
|
const pattern = this.parseValue();
|
|
@@ -657,7 +864,16 @@ const OPERATORS = {
|
|
|
657
864
|
|
|
658
865
|
function resolveOperand(operand, row) {
|
|
659
866
|
if (operand.type === 'value') return operand.value;
|
|
660
|
-
|
|
867
|
+
const n = operand.name;
|
|
868
|
+
if (n === undefined) return undefined;
|
|
869
|
+
if (row[n] !== undefined) return row[n];
|
|
870
|
+
if (n.includes('.')) {
|
|
871
|
+
const k = n;
|
|
872
|
+
if (row[k] !== undefined) return row[k];
|
|
873
|
+
const col = n.slice(n.lastIndexOf('.') + 1);
|
|
874
|
+
return row[col];
|
|
875
|
+
}
|
|
876
|
+
return undefined;
|
|
661
877
|
}
|
|
662
878
|
|
|
663
879
|
function likeMatch(value, pattern) {
|
|
@@ -678,7 +894,7 @@ function extractEqualPushdown(expr, schema) {
|
|
|
678
894
|
if (node.type === 'and') { walk(node.left); walk(node.right); return; }
|
|
679
895
|
if (node.type === 'compare' && node.op === '=') {
|
|
680
896
|
const col = node.left && node.left.type === 'column' ? node.left.name : null;
|
|
681
|
-
const val = node.right && (node.right.type === 'literal' || node.right.type === 'value'
|
|
897
|
+
const val = node.right && (node.right.type === 'literal' || node.right.type === 'value') ? node.right.value : undefined;
|
|
682
898
|
if (col && val !== undefined && val !== null && schema[col] && !(schema[col].primaryKey && schema[col].autoIncrement === false)) {
|
|
683
899
|
filter[col] = val;
|
|
684
900
|
return;
|
|
@@ -715,23 +931,48 @@ function evaluateExpr(expr, row) {
|
|
|
715
931
|
}
|
|
716
932
|
case 'in': {
|
|
717
933
|
const v = resolveOperand(expr.operand, row);
|
|
934
|
+
if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
|
|
935
|
+
if (!expr.list) return false;
|
|
718
936
|
return expr.list.some(x => x === v || String(x) === String(v));
|
|
719
937
|
}
|
|
720
938
|
case 'like': return likeMatch(resolveOperand(expr.operand, row), expr.pattern);
|
|
939
|
+
case 'between': {
|
|
940
|
+
const v = resolveOperand(expr.operand, row);
|
|
941
|
+
const lo = resolveOperand(expr.low, row);
|
|
942
|
+
const hi = resolveOperand(expr.high, row);
|
|
943
|
+
if (v === null || lo === null || hi === null) return false;
|
|
944
|
+
const inRange = typeof v === 'number' && typeof lo === 'number' && typeof hi === 'number'
|
|
945
|
+
? v >= lo && v <= hi
|
|
946
|
+
: String(v) >= String(lo) && String(v) <= String(hi);
|
|
947
|
+
return expr.not ? !inRange : inRange;
|
|
948
|
+
}
|
|
949
|
+
case 'case': {
|
|
950
|
+
for (const b of expr.branches) {
|
|
951
|
+
if (evaluateExpr(b.cond, row)) return resolveOperand(b.val, row);
|
|
952
|
+
}
|
|
953
|
+
return expr.elseVal ? resolveOperand(expr.elseVal, row) : null;
|
|
954
|
+
}
|
|
721
955
|
default: return false;
|
|
722
956
|
}
|
|
723
957
|
}
|
|
724
958
|
|
|
959
|
+
function evaluateCaseVal(caseExpr, row) {
|
|
960
|
+
if (!caseExpr || caseExpr.type !== 'case') return undefined;
|
|
961
|
+
for (const b of caseExpr.branches) {
|
|
962
|
+
if (evaluateExpr(b.cond, row)) return resolveOperand(b.val, row);
|
|
963
|
+
}
|
|
964
|
+
return caseExpr.elseVal ? resolveOperand(caseExpr.elseVal, row) : null;
|
|
965
|
+
}
|
|
966
|
+
|
|
725
967
|
function normalizeRow(row, schema) {
|
|
726
968
|
if (row && typeof row === 'object' && row.fields && typeof row.fields === 'object') {
|
|
727
969
|
const flat = { ...row.fields };
|
|
728
970
|
if (schema) {
|
|
729
971
|
const pkCols = Object.keys(schema).filter(k => schema[k].primaryKey);
|
|
730
972
|
for (const c of pkCols) {
|
|
731
|
-
|
|
732
|
-
if (row.id !== undefined && (v === undefined || v === null || v === 0 || v === '')) flat[c] = row.id;
|
|
973
|
+
if ((flat[c] === undefined || flat[c] === null) && row.id !== undefined) flat[c] = row.id;
|
|
733
974
|
}
|
|
734
|
-
} else if (row.id !== undefined &&
|
|
975
|
+
} else if (row.id !== undefined && flat.id === undefined) {
|
|
735
976
|
flat.id = row.id;
|
|
736
977
|
}
|
|
737
978
|
flat._rid = row.id;
|
|
@@ -748,10 +989,16 @@ class SQLExecutor {
|
|
|
748
989
|
async execute(statement) {
|
|
749
990
|
switch (statement.type) {
|
|
750
991
|
case 'createTable': {
|
|
992
|
+
if (statement.ifNotExists && this.engine.hasTable && this.engine.hasTable(statement.name)) {
|
|
993
|
+
return { ok: true, type: 'createTable', table: statement.name, affectedRows: 0, skipped: true };
|
|
994
|
+
}
|
|
751
995
|
const r = await this.engine.createTable(statement.name, statement.schema);
|
|
752
996
|
return { ok: true, type: 'createTable', table: statement.name, affectedRows: 0, result: r };
|
|
753
997
|
}
|
|
754
998
|
case 'dropTable': {
|
|
999
|
+
if (statement.ifExists && !this.engine.hasTable(statement.table)) {
|
|
1000
|
+
return { ok: true, type: 'dropTable', table: statement.table, affectedRows: 0 };
|
|
1001
|
+
}
|
|
755
1002
|
await this.engine.dropTable(statement.table);
|
|
756
1003
|
return { ok: true, type: 'dropTable', table: statement.table, affectedRows: 0 };
|
|
757
1004
|
}
|
|
@@ -851,103 +1098,7 @@ class SQLExecutor {
|
|
|
851
1098
|
};
|
|
852
1099
|
}
|
|
853
1100
|
case 'select': {
|
|
854
|
-
|
|
855
|
-
let all;
|
|
856
|
-
if (statement.table === null) {
|
|
857
|
-
all = [{ _virtual: true }];
|
|
858
|
-
} else {
|
|
859
|
-
if (this.engine.hasTable && !this.engine.hasTable(statement.table)) {
|
|
860
|
-
throw new Error(`Table '${statement.table}' does not exist`);
|
|
861
|
-
}
|
|
862
|
-
if (statement.aggregate && statement.aggregate.type === 'COUNT' && !statement.where && !statement.distinct) {
|
|
863
|
-
const aggName = statement.aggregate.alias || 'COUNT(*)';
|
|
864
|
-
const n = this.engine.count ? await this.engine.count(statement.table) : 0;
|
|
865
|
-
return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[n]], aggregate: statement.aggregate };
|
|
866
|
-
}
|
|
867
|
-
schema = this.engine.getTableSchema
|
|
868
|
-
? await this.engine.getTableSchema(statement.table)
|
|
869
|
-
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
870
|
-
if (!schema) {
|
|
871
|
-
throw new Error(`Table '${statement.table}' does not exist`);
|
|
872
|
-
}
|
|
873
|
-
const pushdown = extractEqualPushdown(statement.where, schema);
|
|
874
|
-
const engineFilter = pushdown ? pushdown.filter : {};
|
|
875
|
-
all = (await this.engine.find(statement.table, engineFilter, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
876
|
-
if (pushdown && pushdown.rest) {
|
|
877
|
-
all = all.filter(r => evaluateExpr(pushdown.rest, r));
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
let rows = all;
|
|
882
|
-
if (statement.where) {
|
|
883
|
-
rows = rows.filter(r => evaluateExpr(statement.where, r));
|
|
884
|
-
}
|
|
885
|
-
if (statement.distinct) {
|
|
886
|
-
const seen = new Set();
|
|
887
|
-
rows = rows.filter(r => {
|
|
888
|
-
const key = JSON.stringify(statement.columns.map(c => r[c.expr]));
|
|
889
|
-
if (seen.has(key)) return false;
|
|
890
|
-
seen.add(key);
|
|
891
|
-
return true;
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
if (statement.orderBy) {
|
|
895
|
-
const cmp = (a, b) => {
|
|
896
|
-
for (const o of statement.orderBy) {
|
|
897
|
-
const av = a[o.column], bv = b[o.column];
|
|
898
|
-
if (av === bv || (av === undefined && bv === undefined)) continue;
|
|
899
|
-
if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
|
|
900
|
-
if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
|
|
901
|
-
const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
|
|
902
|
-
if (r !== 0) return o.dir === 'asc' ? r : -r;
|
|
903
|
-
}
|
|
904
|
-
return 0;
|
|
905
|
-
};
|
|
906
|
-
rows = rows.slice().sort(cmp);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
if (statement.aggregate) {
|
|
910
|
-
const agg = statement.aggregate;
|
|
911
|
-
const rowsOnly = rows;
|
|
912
|
-
const aggName = agg.alias || (agg.type === 'COUNT' ? 'COUNT(*)' : agg.type + '(' + agg.column + ')');
|
|
913
|
-
if (agg.type === 'COUNT') {
|
|
914
|
-
let n;
|
|
915
|
-
if (!statement.where && !statement.distinct) {
|
|
916
|
-
n = this.engine.count ? await this.engine.count(statement.table) : rowsOnly.length;
|
|
917
|
-
} else {
|
|
918
|
-
n = rowsOnly.length;
|
|
919
|
-
}
|
|
920
|
-
return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[n]], aggregate: agg };
|
|
921
|
-
}
|
|
922
|
-
const values = rowsOnly.map(r => r[agg.column]).filter(v => v !== null && v !== undefined);
|
|
923
|
-
let value;
|
|
924
|
-
if (agg.type === 'SUM') value = values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
|
|
925
|
-
else if (agg.type === 'AVG') value = values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
926
|
-
else if (agg.type === 'MIN') value = values.length ? Math.min(...values.map(v => Number(v))) : null;
|
|
927
|
-
else if (agg.type === 'MAX') value = values.length ? Math.max(...values.map(v => Number(v))) : null;
|
|
928
|
-
return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[value]], aggregate: agg };
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
if (statement.limit !== null) {
|
|
932
|
-
const start = statement.offset || 0;
|
|
933
|
-
rows = rows.slice(start, start + statement.limit);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
if (statement.columns.length === 1 && statement.columns[0].expr === '*') {
|
|
937
|
-
const schemaKeys = schema ? Object.keys(schema) : [];
|
|
938
|
-
const pkCols = schemaKeys.filter(k => schema[k].primaryKey);
|
|
939
|
-
const pk = pkCols.length > 0 ? pkCols[0] : (schemaKeys[0] || 'id');
|
|
940
|
-
const cols = schema ? [pk, ...schemaKeys.filter(k => k !== pk)] : Object.keys(all[0] || {});
|
|
941
|
-
return { ok: true, type: 'select', table: statement.table, columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
const cols = statement.columns.map(c => c.alias || (c.literal !== undefined ? (c.expr === null ? String(c.literal) : c.expr) : c.expr));
|
|
945
|
-
const mapped = rows.map(r => statement.columns.map(c => {
|
|
946
|
-
if (c.expr === '*') return null;
|
|
947
|
-
if (c.literal !== undefined) return c.literal;
|
|
948
|
-
return r[c.expr];
|
|
949
|
-
}));
|
|
950
|
-
return { ok: true, type: 'select', table: statement.table, columns: cols, rows: mapped, raw: rows };
|
|
1101
|
+
return await this.executeSelect(statement);
|
|
951
1102
|
}
|
|
952
1103
|
case 'update': {
|
|
953
1104
|
const schema = this.engine.getTableSchema
|
|
@@ -988,27 +1139,385 @@ class SQLExecutor {
|
|
|
988
1139
|
await this.engine.flush();
|
|
989
1140
|
return { ok: true, type: 'delete', table: statement.table, affectedRows: ids.length };
|
|
990
1141
|
}
|
|
991
|
-
case 'begin':
|
|
992
|
-
|
|
993
|
-
|
|
1142
|
+
case 'begin':
|
|
1143
|
+
if (this.engine.beginTx) this.engine._txId = await this.engine.beginTx();
|
|
1144
|
+
else if (this.engine.begin) await this.engine.begin();
|
|
1145
|
+
return { ok: true, type: 'begin' };
|
|
1146
|
+
case 'commit':
|
|
1147
|
+
if (this.engine._txId !== undefined && this.engine.commitTx) {
|
|
1148
|
+
await this.engine.commitTx(this.engine._txId);
|
|
1149
|
+
this.engine._txId = undefined;
|
|
1150
|
+
} else if (this.engine.commit) await this.engine.commit();
|
|
1151
|
+
return { ok: true, type: 'commit' };
|
|
1152
|
+
case 'rollback':
|
|
1153
|
+
if (this.engine._txId !== undefined && this.engine.rollbackTx) {
|
|
1154
|
+
await this.engine.rollbackTx(this.engine._txId);
|
|
1155
|
+
this.engine._txId = undefined;
|
|
1156
|
+
} else if (this.engine.rollback) await this.engine.rollback();
|
|
1157
|
+
return { ok: true, type: 'rollback' };
|
|
994
1158
|
case 'showTables': {
|
|
995
1159
|
const tables = this.engine.getTables ? this.engine.getTables() : (this.engine.tables ? this.engine.tables() : []);
|
|
996
1160
|
return { ok: true, type: 'showTables', columns: ['Tables'], rows: tables.map(t => [t]) };
|
|
997
1161
|
}
|
|
998
|
-
case 'showDatabases':
|
|
999
|
-
|
|
1162
|
+
case 'showDatabases': {
|
|
1163
|
+
const list = this.engine.listDatabases
|
|
1164
|
+
? await this.engine.listDatabases()
|
|
1165
|
+
: ['jsql'];
|
|
1166
|
+
return { ok: true, type: 'showDatabases', columns: ['Database'], rows: list.map(d => [d]) };
|
|
1167
|
+
}
|
|
1168
|
+
case 'createDatabase': {
|
|
1169
|
+
if (!this.engine.createDatabase) throw new Error('CREATE DATABASE is not supported by this engine');
|
|
1170
|
+
await this.engine.createDatabase(statement.database, { ifNotExists: statement.ifNotExists });
|
|
1171
|
+
return { ok: true, type: 'createDatabase', database: statement.database };
|
|
1172
|
+
}
|
|
1173
|
+
case 'dropDatabase': {
|
|
1174
|
+
if (!this.engine.dropDatabase) throw new Error('DROP DATABASE is not supported by this engine');
|
|
1175
|
+
await this.engine.dropDatabase(statement.database, { ifExists: statement.ifExists });
|
|
1176
|
+
return { ok: true, type: 'dropDatabase', database: statement.database };
|
|
1177
|
+
}
|
|
1178
|
+
case 'use':
|
|
1179
|
+
if (this.engine.useDatabase) {
|
|
1180
|
+
await this.engine.useDatabase(statement.database);
|
|
1181
|
+
}
|
|
1182
|
+
return { ok: true, type: 'use', database: statement.database };
|
|
1000
1183
|
case 'describe': {
|
|
1001
1184
|
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
1002
1185
|
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
1003
1186
|
const rows = Object.entries(schema).map(([col, def]) => [col, def.type, def.primaryKey ? 'PRI' : '', def.autoIncrement ? 'auto_increment' : null, def.default !== undefined ? def.default : null]);
|
|
1004
1187
|
return { ok: true, type: 'describe', table: statement.table, columns: ['Field', 'Type', 'Key', 'Extra', 'Default'], rows };
|
|
1005
1188
|
}
|
|
1006
|
-
case 'use':
|
|
1007
|
-
return { ok: true, type: 'use', database: statement.database };
|
|
1008
1189
|
default:
|
|
1009
1190
|
throw new Error(`Unsupported statement type: ${statement.type}`);
|
|
1010
1191
|
}
|
|
1011
1192
|
}
|
|
1193
|
+
|
|
1194
|
+
async _getSchema(name) {
|
|
1195
|
+
if (this.engine.hasTable && !this.engine.hasTable(name)) {
|
|
1196
|
+
throw new Error(`Table '${name}' does not exist`);
|
|
1197
|
+
}
|
|
1198
|
+
const schema = this.engine.getTableSchema
|
|
1199
|
+
? await this.engine.getTableSchema(name)
|
|
1200
|
+
: (this.engine._schemas ? this.engine._schemas[name] : null);
|
|
1201
|
+
if (!schema) throw new Error(`Table '${name}' does not exist`);
|
|
1202
|
+
return schema;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
async _readTable(table) {
|
|
1206
|
+
const schema = await this._getSchema(table);
|
|
1207
|
+
const rows = (await this.engine.find(table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1208
|
+
return { schema, rows };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async _execFromItem(item) {
|
|
1212
|
+
if (item.subquery) {
|
|
1213
|
+
const res = await this.executeSelect(item.subquery);
|
|
1214
|
+
return { schema: null, rows: res.rows.map(r => Object.assign({}, r)) };
|
|
1215
|
+
}
|
|
1216
|
+
return this._readTable(item.table);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// 物化表达式树中的子查询:IN (SELECT ...) -> expr.list
|
|
1220
|
+
async _materialize(expr) {
|
|
1221
|
+
if (!expr || typeof expr !== 'object') return;
|
|
1222
|
+
if (expr.type === 'in' && expr.subquery) {
|
|
1223
|
+
const res = await this.executeSelect(expr.subquery);
|
|
1224
|
+
expr.list = res.rows.map(r => r[0]);
|
|
1225
|
+
delete expr.subquery;
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (expr.type === 'subquery') {
|
|
1229
|
+
const res = await this.executeSelect(expr.select);
|
|
1230
|
+
const v = res.rows.length > 0 ? res.rows[0][0] : null;
|
|
1231
|
+
expr.type = 'value';
|
|
1232
|
+
expr.value = v;
|
|
1233
|
+
delete expr.select;
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
for (const k of Object.keys(expr)) {
|
|
1237
|
+
if (expr[k] && typeof expr[k] === 'object') await this._materialize(expr[k]);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// 物化 FROM 子查询(把子查询替换为行数组),返回 { rows, alias }
|
|
1242
|
+
async _materializeFrom(item) {
|
|
1243
|
+
if (item.subquery) {
|
|
1244
|
+
const res = await this.executeSelect(item.subquery);
|
|
1245
|
+
const rows = res.rows.map(r => {
|
|
1246
|
+
if (Array.isArray(r)) {
|
|
1247
|
+
const obj = {};
|
|
1248
|
+
res.columns.forEach((c, i) => { obj[c] = r[i]; });
|
|
1249
|
+
return obj;
|
|
1250
|
+
}
|
|
1251
|
+
return Object.assign({}, r);
|
|
1252
|
+
});
|
|
1253
|
+
return { rows, alias: item.alias };
|
|
1254
|
+
}
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
_prefixRow(row, prefix) {
|
|
1259
|
+
const out = {};
|
|
1260
|
+
for (const k of Object.keys(row)) {
|
|
1261
|
+
out[prefix + '.' + k] = row[k];
|
|
1262
|
+
if (out[k] === undefined) out[k] = row[k];
|
|
1263
|
+
}
|
|
1264
|
+
return out;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
_aggValue(rows, fn, column) {
|
|
1268
|
+
if (fn === 'COUNT') return rows.length;
|
|
1269
|
+
const values = rows.map(r => resolveOperand({ type: 'column', name: column }, r)).filter(v => v !== null && v !== undefined);
|
|
1270
|
+
if (fn === 'SUM') return values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
|
|
1271
|
+
if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
1272
|
+
if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
|
|
1273
|
+
if (fn === 'MAX') return values.length ? Math.max(...values.map(v => Number(v))) : null;
|
|
1274
|
+
return null;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
_replaceAggregates(node, group) {
|
|
1278
|
+
if (!node || typeof node !== 'object') return;
|
|
1279
|
+
if (node.type === 'aggregate') {
|
|
1280
|
+
node.type = 'value';
|
|
1281
|
+
node.value = this._aggValue(group, node.fn, node.column);
|
|
1282
|
+
delete node.fn;
|
|
1283
|
+
delete node.column;
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
for (const k of Object.keys(node)) {
|
|
1287
|
+
if (node[k] && typeof node[k] === 'object') this._replaceAggregates(node[k], group);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
_subQueryRows(res) {
|
|
1292
|
+
return res.rows.map(r => {
|
|
1293
|
+
if (Array.isArray(r)) {
|
|
1294
|
+
const obj = {};
|
|
1295
|
+
res.columns.forEach((c, i) => { obj[c] = r[i]; });
|
|
1296
|
+
return obj;
|
|
1297
|
+
}
|
|
1298
|
+
return Object.assign({}, r);
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
async executeSelect(statement) {
|
|
1303
|
+
// UNION 处理
|
|
1304
|
+
if (statement.union) {
|
|
1305
|
+
const left = await this.executeSelect({ ...statement, union: null });
|
|
1306
|
+
const right = await this.executeSelect(statement.union.select);
|
|
1307
|
+
let rows = left.rows.concat(right.rows);
|
|
1308
|
+
if (!statement.union.all) {
|
|
1309
|
+
const seen = new Set();
|
|
1310
|
+
rows = rows.filter(r => {
|
|
1311
|
+
const k = JSON.stringify(r);
|
|
1312
|
+
if (seen.has(k)) return false;
|
|
1313
|
+
seen.add(k);
|
|
1314
|
+
return true;
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
return { ok: true, type: 'select', columns: left.columns, rows, raw: rows };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// 物化 WHERE / HAVING / JOIN ON 中的子查询
|
|
1321
|
+
await this._materialize(statement.where);
|
|
1322
|
+
await this._materialize(statement.having);
|
|
1323
|
+
if (statement.from) {
|
|
1324
|
+
for (const j of statement.from.joins) await this._materialize(j.on);
|
|
1325
|
+
}
|
|
1326
|
+
for (const c of statement.columns) {
|
|
1327
|
+
if (c.caseExpr) await this._materialize(c.caseExpr);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
let schema = null;
|
|
1331
|
+
let all;
|
|
1332
|
+
if (!statement.from) {
|
|
1333
|
+
// 无 FROM:虚拟行(SELECT 1, 'a')
|
|
1334
|
+
all = [{ _virtual: true }];
|
|
1335
|
+
} else {
|
|
1336
|
+
const items = statement.from.tables.concat(statement.from.joins.map(j => j.item));
|
|
1337
|
+
const prefix = item => item.alias || (item.subquery ? item.alias : item.table);
|
|
1338
|
+
|
|
1339
|
+
// 读第一表
|
|
1340
|
+
const firstItem = statement.from.tables[0];
|
|
1341
|
+
let rowsAll;
|
|
1342
|
+
if (firstItem.subquery) {
|
|
1343
|
+
const res = await this.executeSelect(firstItem.subquery);
|
|
1344
|
+
rowsAll = { rows: this._subQueryRows(res), schema: null };
|
|
1345
|
+
} else {
|
|
1346
|
+
rowsAll = await this._readTable(firstItem.table);
|
|
1347
|
+
}
|
|
1348
|
+
schema = rowsAll.schema;
|
|
1349
|
+
const firstPrefix = firstItem.alias || firstItem.table;
|
|
1350
|
+
let rows = rowsAll.rows.map(r => this._prefixRow(r, firstPrefix));
|
|
1351
|
+
|
|
1352
|
+
for (const j of statement.from.joins) {
|
|
1353
|
+
const rightRes = j.item.subquery
|
|
1354
|
+
? await this.executeSelect(j.item.subquery)
|
|
1355
|
+
: await this._readTable(j.item.table);
|
|
1356
|
+
const rightPrefix = j.item.alias || j.item.table;
|
|
1357
|
+
const rightRows = (j.item.subquery ? this._subQueryRows(rightRes) : rightRes.rows).map(r => this._prefixRow(r, rightPrefix));
|
|
1358
|
+
|
|
1359
|
+
// 匹配(INNER/LEFT/RIGHT)
|
|
1360
|
+
if (j.type === 'cross' || (!j.on)) {
|
|
1361
|
+
// CROSS JOIN / 逗号:笛卡尔积
|
|
1362
|
+
rows = rows.flatMap(l => rightRows.map(r => ({ ...l, ...r })));
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
const matched = [];
|
|
1366
|
+
const unmatchedRight = new Set(rightRows.map((r, i) => i));
|
|
1367
|
+
rows.forEach(l => {
|
|
1368
|
+
let m = null;
|
|
1369
|
+
for (let ri = 0; ri < rightRows.length; ri++) {
|
|
1370
|
+
if (evaluateExpr(j.on, { ...l, ...rightRows[ri] })) {
|
|
1371
|
+
m = ri;
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (m !== null) {
|
|
1376
|
+
matched.push({ ...l, ...rightRows[m] });
|
|
1377
|
+
unmatchedRight.delete(m);
|
|
1378
|
+
} else if (j.type === 'left') {
|
|
1379
|
+
matched.push({ ...l });
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
if (j.type === 'right') {
|
|
1383
|
+
for (const ri of unmatchedRight) matched.push({ ...rightRows[ri] });
|
|
1384
|
+
}
|
|
1385
|
+
rows = matched;
|
|
1386
|
+
}
|
|
1387
|
+
all = rows;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
let rows = all;
|
|
1391
|
+
if (statement.where) {
|
|
1392
|
+
rows = rows.filter(r => evaluateExpr(statement.where, r));
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// 分组聚合
|
|
1396
|
+
if (statement.groupBy) {
|
|
1397
|
+
const groups = new Map();
|
|
1398
|
+
for (const row of rows) {
|
|
1399
|
+
const key = JSON.stringify(statement.groupBy.map(g => resolveOperand({ type: 'column', name: g }, row)));
|
|
1400
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
1401
|
+
groups.get(key).push(row);
|
|
1402
|
+
}
|
|
1403
|
+
const groupList = [...groups.values()];
|
|
1404
|
+
rows = groupList.map(g => {
|
|
1405
|
+
const rep = g[0];
|
|
1406
|
+
const out = { ...rep, _group: g };
|
|
1407
|
+
return out;
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
if (statement.having) {
|
|
1412
|
+
const havingAst = JSON.parse(JSON.stringify(statement.having));
|
|
1413
|
+
rows = rows.filter(r => {
|
|
1414
|
+
const ctx = { ...r };
|
|
1415
|
+
const h = JSON.parse(JSON.stringify(havingAst));
|
|
1416
|
+
this._replaceAggregates(h, r._group || [r]);
|
|
1417
|
+
for (const c of statement.columns) {
|
|
1418
|
+
if (c.aggregate) {
|
|
1419
|
+
const group = r._group || [r];
|
|
1420
|
+
const v = this._aggValue(group, c.aggregate || 'COUNT', c.column);
|
|
1421
|
+
ctx['__agg_' + (c.alias || c.column || 'COUNT(*)')] = v;
|
|
1422
|
+
if (c.column) ctx[c.column] = v;
|
|
1423
|
+
if (c.alias) ctx[c.alias] = v;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
return evaluateExpr(h, ctx);
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (statement.distinct) {
|
|
1431
|
+
const seen = new Set();
|
|
1432
|
+
rows = rows.filter(r => {
|
|
1433
|
+
const key = JSON.stringify(statement.columns.map(c => {
|
|
1434
|
+
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column);
|
|
1435
|
+
return resolveOperand(c.caseExpr || { type: 'column', name: c.expr }, r);
|
|
1436
|
+
}));
|
|
1437
|
+
if (seen.has(key)) return false;
|
|
1438
|
+
seen.add(key);
|
|
1439
|
+
return true;
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// 聚合输出(无 GROUP BY 时)
|
|
1444
|
+
if (statement.aggregate && !statement.groupBy) {
|
|
1445
|
+
const agg = statement.aggregate;
|
|
1446
|
+
const rowsOnly = rows;
|
|
1447
|
+
const aggName = agg.alias || (agg.type === 'COUNT' ? 'COUNT(*)' : agg.type + '(' + agg.column + ')');
|
|
1448
|
+
const value = this._aggValue(rowsOnly, agg.type, agg.column);
|
|
1449
|
+
return { ok: true, type: 'select', table: statement.from ? (statement.from.tables[0].table || null) : null, columns: [aggName], rows: [[value]], aggregate: agg };
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// GROUP BY 输出:每组的列(含聚合列)
|
|
1453
|
+
if (statement.groupBy) {
|
|
1454
|
+
const aggName = (c) => c.alias || (c.aggregate === 'COUNT' ? 'COUNT(*)' : c.aggregate + '(' + c.column + ')');
|
|
1455
|
+
const cols = statement.columns.map(c => {
|
|
1456
|
+
if (c.alias) return c.alias;
|
|
1457
|
+
if (c.aggregate) return aggName(c);
|
|
1458
|
+
if (c.literal !== undefined) return String(c.literal);
|
|
1459
|
+
if (c.caseExpr) return 'CASE';
|
|
1460
|
+
const dot = c.expr.indexOf('.');
|
|
1461
|
+
return dot !== -1 ? c.expr.slice(dot + 1) : c.expr;
|
|
1462
|
+
});
|
|
1463
|
+
const mapped = rows.map(r => {
|
|
1464
|
+
const group = r._group || [r];
|
|
1465
|
+
return statement.columns.map(c => {
|
|
1466
|
+
if (c.aggregate) return this._aggValue(group, c.aggregate || 'COUNT', c.column);
|
|
1467
|
+
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
1468
|
+
if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
|
|
1469
|
+
return resolveOperand({ type: 'column', name: c.expr }, r);
|
|
1470
|
+
});
|
|
1471
|
+
});
|
|
1472
|
+
return { ok: true, type: 'select', table: statement.from ? (statement.from.tables[0].table || null) : null, columns: cols, rows: mapped, raw: rows };
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (statement.orderBy) {
|
|
1476
|
+
const cmp = (a, b) => {
|
|
1477
|
+
for (const o of statement.orderBy) {
|
|
1478
|
+
const av = resolveOperand({ type: 'column', name: o.column }, a);
|
|
1479
|
+
const bv = resolveOperand({ type: 'column', name: o.column }, b);
|
|
1480
|
+
if (av === bv || (av === undefined && bv === undefined)) continue;
|
|
1481
|
+
if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
|
|
1482
|
+
if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
|
|
1483
|
+
const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
|
|
1484
|
+
if (r !== 0) return o.dir === 'asc' ? r : -r;
|
|
1485
|
+
}
|
|
1486
|
+
return 0;
|
|
1487
|
+
};
|
|
1488
|
+
rows = rows.slice().sort(cmp);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (statement.limit !== null) {
|
|
1492
|
+
const start = statement.offset || 0;
|
|
1493
|
+
rows = rows.slice(start, start + statement.limit);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// 无分组、无聚合的正常输出
|
|
1497
|
+
const tableName = statement.from ? (statement.from.tables[0].table || null) : null;
|
|
1498
|
+
if (statement.columns.length === 1 && statement.columns[0].expr === '*') {
|
|
1499
|
+
const schemaKeys = schema ? Object.keys(schema) : [];
|
|
1500
|
+
const pkCols = schemaKeys.filter(k => schema && schema[k] && schema[k].primaryKey);
|
|
1501
|
+
const pk = pkCols.length > 0 ? pkCols[0] : (schemaKeys[0] || 'id');
|
|
1502
|
+
const cols = schema ? [pk, ...schemaKeys.filter(k => k !== pk)] : Object.keys(all[0] || {}).filter((v, i, a) => a.indexOf(v) === i);
|
|
1503
|
+
return { ok: true, type: 'select', table: tableName, columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const cols = statement.columns.map(c => {
|
|
1507
|
+
if (c.alias) return c.alias;
|
|
1508
|
+
if (c.literal !== undefined) return String(c.literal);
|
|
1509
|
+
if (c.caseExpr) return 'CASE';
|
|
1510
|
+
const dot = c.expr.indexOf('.');
|
|
1511
|
+
return dot !== -1 ? c.expr.slice(dot + 1) : c.expr;
|
|
1512
|
+
});
|
|
1513
|
+
const mapped = rows.map(r => statement.columns.map(c => {
|
|
1514
|
+
if (c.expr === '*') return null;
|
|
1515
|
+
if (c.literal !== undefined) return c.literal;
|
|
1516
|
+
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
1517
|
+
return resolveOperand({ type: 'column', name: c.expr }, r);
|
|
1518
|
+
}));
|
|
1519
|
+
return { ok: true, type: 'select', table: tableName, columns: cols, rows: mapped, raw: rows };
|
|
1520
|
+
}
|
|
1012
1521
|
}
|
|
1013
1522
|
|
|
1014
1523
|
function splitStatements(sql) {
|
|
@@ -1026,6 +1535,21 @@ function splitStatements(sql) {
|
|
|
1026
1535
|
continue;
|
|
1027
1536
|
}
|
|
1028
1537
|
if (c === "'" || c === '"') { inStr = c; current += c; i++; continue; }
|
|
1538
|
+
if (c === '-' && sql[i + 1] === '-') {
|
|
1539
|
+
while (i < sql.length && sql[i] !== '\n') { current += sql[i]; i++; }
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (c === '#') {
|
|
1543
|
+
while (i < sql.length && sql[i] !== '\n') { current += sql[i]; i++; }
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
if (c === '/' && sql[i + 1] === '*') {
|
|
1547
|
+
current += '/*';
|
|
1548
|
+
i += 2;
|
|
1549
|
+
while (i + 1 < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) { current += sql[i]; i++; }
|
|
1550
|
+
if (i + 1 < sql.length) { current += '*/'; i += 2; }
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1029
1553
|
if (c === ';') {
|
|
1030
1554
|
if (current.trim()) statements.push(current.trim());
|
|
1031
1555
|
current = '';
|