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/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.type === 'value' || node.right.type === 'param') ? 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,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
- const v = flat[c];
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 && (flat.id === undefined || flat.id === null || flat.id === 0 || flat.id === '')) {
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
- let schema = null;
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': return { ok: true, type: 'begin' };
992
- case 'commit': return { ok: true, type: 'commit' };
993
- 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' };
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
- 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 };
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 = '';