jsql-neo 4.0.1 → 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/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 && nxt.type === 'keyword' && d.next.test(String(nxt.value).toUpperCase())) {
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': return this.parseCreateTable();
196
- case 'DROP': return this.parseDropTable();
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: true });
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.parseTableName();
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 table = null;
482
+ let from = null;
447
483
  if (this.isKeyword('FROM')) {
448
484
  this.next();
449
- table = this.parseTableName();
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, table, where, orderBy, limit, offset };
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') return { type: 'column', name: t.value };
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
- return row[operand.name];
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.value : undefined;
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,13 +931,39 @@ 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 };
@@ -747,10 +989,16 @@ class SQLExecutor {
747
989
  async execute(statement) {
748
990
  switch (statement.type) {
749
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
+ }
750
995
  const r = await this.engine.createTable(statement.name, statement.schema);
751
996
  return { ok: true, type: 'createTable', table: statement.name, affectedRows: 0, result: r };
752
997
  }
753
998
  case 'dropTable': {
999
+ if (statement.ifExists && !this.engine.hasTable(statement.table)) {
1000
+ return { ok: true, type: 'dropTable', table: statement.table, affectedRows: 0 };
1001
+ }
754
1002
  await this.engine.dropTable(statement.table);
755
1003
  return { ok: true, type: 'dropTable', table: statement.table, affectedRows: 0 };
756
1004
  }
@@ -850,92 +1098,7 @@ class SQLExecutor {
850
1098
  };
851
1099
  }
852
1100
  case 'select': {
853
- let schema = null;
854
- let all;
855
- if (statement.table === null) {
856
- all = [{ _virtual: true }];
857
- } else {
858
- if (this.engine.hasTable && !this.engine.hasTable(statement.table)) {
859
- throw new Error(`Table '${statement.table}' does not exist`);
860
- }
861
- schema = this.engine.getTableSchema
862
- ? await this.engine.getTableSchema(statement.table)
863
- : (this.engine._schemas ? this.engine._schemas[statement.table] : null);
864
- if (!schema) {
865
- throw new Error(`Table '${statement.table}' does not exist`);
866
- }
867
- const pushdown = extractEqualPushdown(statement.where, schema);
868
- const engineFilter = pushdown ? pushdown.filter : {};
869
- all = (await this.engine.find(statement.table, engineFilter, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
870
- if (pushdown && pushdown.rest) {
871
- all = all.filter(r => evaluateExpr(pushdown.rest, r));
872
- }
873
- }
874
-
875
- let rows = all;
876
- if (statement.where) {
877
- rows = rows.filter(r => evaluateExpr(statement.where, r));
878
- }
879
- if (statement.distinct) {
880
- const seen = new Set();
881
- rows = rows.filter(r => {
882
- const key = JSON.stringify(statement.columns.map(c => r[c.expr]));
883
- if (seen.has(key)) return false;
884
- seen.add(key);
885
- return true;
886
- });
887
- }
888
- if (statement.orderBy) {
889
- const cmp = (a, b) => {
890
- for (const o of statement.orderBy) {
891
- const av = a[o.column], bv = b[o.column];
892
- if (av === bv || (av === undefined && bv === undefined)) continue;
893
- if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
894
- if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
895
- const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
896
- if (r !== 0) return o.dir === 'asc' ? r : -r;
897
- }
898
- return 0;
899
- };
900
- rows = rows.slice().sort(cmp);
901
- }
902
-
903
- if (statement.aggregate) {
904
- const agg = statement.aggregate;
905
- const rowsOnly = rows;
906
- const aggName = agg.alias || (agg.type === 'COUNT' ? 'COUNT(*)' : agg.type + '(' + agg.column + ')');
907
- if (agg.type === 'COUNT') {
908
- return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[rowsOnly.length]], aggregate: agg };
909
- }
910
- const values = rowsOnly.map(r => r[agg.column]).filter(v => v !== null && v !== undefined);
911
- let value;
912
- if (agg.type === 'SUM') value = values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
913
- else if (agg.type === 'AVG') value = values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
914
- else if (agg.type === 'MIN') value = values.length ? Math.min(...values.map(v => Number(v))) : null;
915
- else if (agg.type === 'MAX') value = values.length ? Math.max(...values.map(v => Number(v))) : null;
916
- return { ok: true, type: 'select', table: statement.table, columns: [aggName], rows: [[value]], aggregate: agg };
917
- }
918
-
919
- if (statement.limit !== null) {
920
- const start = statement.offset || 0;
921
- rows = rows.slice(start, start + statement.limit);
922
- }
923
-
924
- if (statement.columns.length === 1 && statement.columns[0].expr === '*') {
925
- const schemaKeys = schema ? Object.keys(schema) : [];
926
- const pkCols = schemaKeys.filter(k => schema[k].primaryKey);
927
- const pk = pkCols.length > 0 ? pkCols[0] : (schemaKeys[0] || 'id');
928
- const cols = schema ? [pk, ...schemaKeys.filter(k => k !== pk)] : Object.keys(all[0] || {});
929
- return { ok: true, type: 'select', table: statement.table, columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
930
- }
931
-
932
- const cols = statement.columns.map(c => c.alias || (c.literal !== undefined ? (c.expr === null ? String(c.literal) : c.expr) : c.expr));
933
- const mapped = rows.map(r => statement.columns.map(c => {
934
- if (c.expr === '*') return null;
935
- if (c.literal !== undefined) return c.literal;
936
- return r[c.expr];
937
- }));
938
- return { ok: true, type: 'select', table: statement.table, columns: cols, rows: mapped, raw: rows };
1101
+ return await this.executeSelect(statement);
939
1102
  }
940
1103
  case 'update': {
941
1104
  const schema = this.engine.getTableSchema
@@ -976,27 +1139,385 @@ class SQLExecutor {
976
1139
  await this.engine.flush();
977
1140
  return { ok: true, type: 'delete', table: statement.table, affectedRows: ids.length };
978
1141
  }
979
- case 'begin': return { ok: true, type: 'begin' };
980
- case 'commit': return { ok: true, type: 'commit' };
981
- case 'rollback': return { ok: true, type: 'rollback' };
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' };
982
1158
  case 'showTables': {
983
1159
  const tables = this.engine.getTables ? this.engine.getTables() : (this.engine.tables ? this.engine.tables() : []);
984
1160
  return { ok: true, type: 'showTables', columns: ['Tables'], rows: tables.map(t => [t]) };
985
1161
  }
986
- case 'showDatabases':
987
- return { ok: true, type: 'showDatabases', columns: ['Database'], rows: [['jsql']] };
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 };
988
1183
  case 'describe': {
989
1184
  const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
990
1185
  if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
991
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]);
992
1187
  return { ok: true, type: 'describe', table: statement.table, columns: ['Field', 'Type', 'Key', 'Extra', 'Default'], rows };
993
1188
  }
994
- case 'use':
995
- return { ok: true, type: 'use', database: statement.database };
996
1189
  default:
997
1190
  throw new Error(`Unsupported statement type: ${statement.type}`);
998
1191
  }
999
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
+ }
1000
1521
  }
1001
1522
 
1002
1523
  function splitStatements(sql) {
@@ -1014,6 +1535,21 @@ function splitStatements(sql) {
1014
1535
  continue;
1015
1536
  }
1016
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
+ }
1017
1553
  if (c === ';') {
1018
1554
  if (current.trim()) statements.push(current.trim());
1019
1555
  current = '';