jsql-neo 6.0.0 → 6.0.2

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
@@ -178,13 +178,17 @@ function tokenize(sql) {
178
178
  }
179
179
 
180
180
  const two = sql.slice(i, i + 2);
181
- if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==') {
181
+ if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
182
+ two === '<<' || two === '>>') {
182
183
  tokens.push(new SQLToken('op', two, i));
183
184
  i += 2;
184
185
  continue;
185
186
  }
186
187
 
187
- if ('=<>+-*/(),.;'.includes(c)) {
188
+ // 注意:% & | ^ ~ 这些运算符必须在词法层产出 token。
189
+ // 之前 parseTerm() 里写了 '%' 分支但字符集里没有 '%',
190
+ // 导致 SELECT 10 % 3 直接报 "Unexpected character '%'",那段是死代码。
191
+ if ('=<>+-*/(),.;%&|^~'.includes(c)) {
188
192
  tokens.push(new SQLToken('op', c, i));
189
193
  i++;
190
194
  continue;
@@ -197,11 +201,23 @@ function tokenize(sql) {
197
201
  return tokens;
198
202
  }
199
203
 
204
+ // 聚合函数集合:这些必须走 aggregate 节点,否则会被当成逐行标量函数(返回 null 且不折叠行)
205
+ const AGG_FUNCS = new Set(['SUM', 'AVG', 'MIN', 'MAX', 'COUNT',
206
+ 'GROUP_CONCAT', 'STDDEV', 'STDDEV_POP', 'STDDEV_SAMP',
207
+ 'VARIANCE', 'VAR_POP', 'VAR_SAMP', 'FIRST', 'LAST']);
208
+
209
+ // 这些词会跟在表名/子查询之后,不能被误当成表别名。
210
+ // 新增任何"跟在 FROM 之后"的子句时,都要往这里加词。
211
+ const NOT_ALIAS_WORDS = ['JOIN', 'LEFT', 'RIGHT', 'FULL', 'WHERE', 'GROUP', 'ORDER', 'LIMIT',
212
+ 'ON', 'HAVING', 'UNION', 'INNER', 'CROSS', 'OUTER', 'INTERSECT', 'EXCEPT',
213
+ 'FETCH', 'RETURNING', 'WINDOW', 'OFFSET', 'FOR'];
214
+
200
215
  class Parser {
201
216
  constructor(tokens) {
202
217
  this.tokens = tokens;
203
218
  this.pos = 0;
204
219
  this.paramCount = 0; // 顺序占位符 ? 的序号(从 0 开始)
220
+ this.subquerySeq = 0; // FROM 子查询无别名时自动生成 __subN
205
221
  }
206
222
 
207
223
  peek(offset = 0) { return this.tokens[this.pos + offset]; }
@@ -271,11 +287,13 @@ class Parser {
271
287
  if (this.isKeyword('TABLE', 1)) return this.parseCreateTable();
272
288
  if (this.isKeyword('DATABASE', 1)) return this.parseCreateDatabase();
273
289
  if (this.isWord('VIEW', 1) || this.isWord('OR', 1)) return this.parseCreateView();
290
+ if (this.isKeyword('INDEX', 1) || this.isKeyword('UNIQUE', 1)) return this.parseCreateIndex();
274
291
  throw new Error('Unsupported CREATE statement');
275
292
  case 'DROP':
276
293
  if (this.isKeyword('TABLE', 1)) return this.parseDropTable();
277
294
  if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
278
295
  if (this.isWord('VIEW', 1)) return this.parseDropView();
296
+ if (this.isKeyword('INDEX', 1)) return this.parseDropIndex();
279
297
  throw new Error('Unsupported DROP statement');
280
298
  case 'INSERT': return this.parseInsert();
281
299
  case 'REPLACE': return this.parseReplace();
@@ -351,6 +369,45 @@ class Parser {
351
369
  return { type: 'createView', name, columns, select, orReplace };
352
370
  }
353
371
 
372
+ /**
373
+ * CREATE [UNIQUE] INDEX name ON table (col [, col]...)
374
+ * 归一成 { type:'createIndex', name, table, columns, unique }
375
+ */
376
+ parseCreateIndex() {
377
+ this.expectKeyword('CREATE');
378
+ let unique = false;
379
+ if (this.isKeyword('UNIQUE')) { this.next(); unique = true; }
380
+ if (!this.isKeyword('INDEX')) throw new Error(`Expected INDEX, got '${this.peek().value}'`);
381
+ this.next();
382
+ const name = this.parseTableName();
383
+ if (!this.isKeyword('ON')) throw new Error(`Expected ON in CREATE INDEX, got '${this.peek().value}'`);
384
+ this.next();
385
+ const table = this.parseTableName();
386
+ this.expect('op', '(');
387
+ const columns = [];
388
+ for (;;) {
389
+ columns.push(this.parseColumnRef());
390
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
391
+ break;
392
+ }
393
+ this.expect('op', ')');
394
+ this.optionalTailSemicolon();
395
+ return { type: 'createIndex', name, table, columns, unique };
396
+ }
397
+
398
+ /** DROP INDEX name ON table */
399
+ parseDropIndex() {
400
+ this.expectKeyword('DROP');
401
+ this.expectKeyword('INDEX');
402
+ let ifExists = false;
403
+ if (this.isKeyword('IF')) { this.expectKeyword('IF'); this.expectKeyword('EXISTS'); ifExists = true; }
404
+ const name = this.parseTableName();
405
+ let table = null;
406
+ if (this.isKeyword('ON')) { this.next(); table = this.parseTableName(); }
407
+ this.optionalTailSemicolon();
408
+ return { type: 'dropIndex', name, table, ifExists };
409
+ }
410
+
354
411
  parseDropView() {
355
412
  this.expectKeyword('DROP');
356
413
  if (!this.isWord('VIEW')) throw new Error(`Expected VIEW, got '${this.peek().value}'`);
@@ -413,6 +470,9 @@ class Parser {
413
470
  hasPk = true;
414
471
  } else {
415
472
  this.expectKeyword('UNIQUE');
473
+ // UNIQUE [KEY|INDEX] [name] (cols...)
474
+ if (this.isKeyword('KEY') || this.isKeyword('INDEX')) this.next();
475
+ if (this.peek().type === 'ident') this.next();
416
476
  this.expect('op', '(');
417
477
  const uCols = [this.parseTableName()];
418
478
  while (this.peek().type === 'op' && this.peek().value === ',') {
@@ -426,7 +486,12 @@ class Parser {
426
486
  }
427
487
  } else if (t.type === 'keyword' && t.value === 'CONSTRAINT') {
428
488
  this.expectKeyword('CONSTRAINT');
429
- this.next();
489
+ if (this.peek().type === 'ident') this.next(); // 约束名
490
+ this._parseTableConstraint(schema);
491
+ } else if (this.isWord('FOREIGN') || this.isWord('CHECK') ||
492
+ t.type === 'keyword' && (t.value === 'KEY' || t.value === 'INDEX')) {
493
+ // 表级约束:FOREIGN KEY ... REFERENCES ... / CHECK (...) / KEY name (cols)
494
+ this._parseTableConstraint(schema);
430
495
  } else if (t.type === 'eof' || (t.type === 'op' && t.value === ')')) {
431
496
  break;
432
497
  } else {
@@ -517,7 +582,24 @@ class Parser {
517
582
  case 'NULL':
518
583
  this.next(); break;
519
584
  case 'DEFAULT':
520
- this.next(); def.default = this.parseValue(); break;
585
+ this.next();
586
+ // 默认值可以是字面量,也可以是函数/关键字形式(CURRENT_TIMESTAMP / NOW() 等)。
587
+ // 函数形式存成字符串 + defaultExpr 标记,与 JS 侧 `default: 'CURRENT_TIMESTAMP'` 约定一致。
588
+ if (this.peek().type === 'ident' ||
589
+ (this.peek().type === 'keyword' && !['NULL', 'TRUE', 'FALSE'].includes(this.peek().value))) {
590
+ const fnTok = this.next();
591
+ let name = String(fnTok.value);
592
+ if (this.peek().type === 'op' && this.peek().value === '(') {
593
+ this.next();
594
+ this.expect('op', ')');
595
+ name += '()';
596
+ }
597
+ def.default = name;
598
+ def.defaultExpr = true;
599
+ } else {
600
+ def.default = this.parseValue();
601
+ }
602
+ break;
521
603
  case 'COLLATE':
522
604
  this.next(); if (this.peek().type !== 'op' && this.peek().type !== 'eof') this.next(); break;
523
605
  case 'CHARACTER':
@@ -553,6 +635,68 @@ class Parser {
553
635
  return stmt;
554
636
  }
555
637
 
638
+ /** 跳过一对括号(含嵌套),用于表级约束里的列清单 */
639
+ _skipParens() {
640
+ this.expect('op', '(');
641
+ let depth = 1;
642
+ while (depth > 0 && this.peek().type !== 'eof') {
643
+ const tk = this.next();
644
+ if (tk.type === 'op' && tk.value === '(') depth++;
645
+ else if (tk.type === 'op' && tk.value === ')') depth--;
646
+ }
647
+ }
648
+
649
+ /**
650
+ * 解析表级约束(FOREIGN KEY / CHECK / KEY / INDEX / PRIMARY KEY / UNIQUE)。
651
+ * 目前只做语法层面消费并在可能时补充列属性,不做外键实际校验。
652
+ */
653
+ _parseTableConstraint(schema) {
654
+ if (this.isWord('FOREIGN')) {
655
+ this.next();
656
+ if (!this.isKeyword('KEY')) throw new Error(`Expected KEY after FOREIGN, got '${this.peek().value}'`);
657
+ this.next();
658
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
659
+ if (this.isWord('REFERENCES')) {
660
+ this.next();
661
+ this.parseTableName();
662
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
663
+ }
664
+ // ON DELETE / ON UPDATE <action>
665
+ while (this.isKeyword('ON')) {
666
+ this.next();
667
+ this.next(); // DELETE | UPDATE
668
+ const act = String(this.next().value).toUpperCase();
669
+ if (act === 'SET' || act === 'NO') this.next(); // SET NULL / NO ACTION
670
+ }
671
+ return;
672
+ }
673
+ if (this.isWord('CHECK')) {
674
+ this.next();
675
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
676
+ return;
677
+ }
678
+ if (this.isKeyword('PRIMARY')) {
679
+ this.expectKeyword('PRIMARY');
680
+ if (this.isKeyword('KEY')) this.next();
681
+ if (this.peek().type === 'op' && this.peek().value === '(') {
682
+ this.next();
683
+ const cols = [this.parseTableName()];
684
+ while (this.peek().type === 'op' && this.peek().value === ',') { this.next(); cols.push(this.parseTableName()); }
685
+ this.expect('op', ')');
686
+ for (const c of cols) if (schema[c]) schema[c].primaryKey = true;
687
+ }
688
+ return;
689
+ }
690
+ if (this.isKeyword('UNIQUE')) {
691
+ this.next();
692
+ }
693
+ if (this.isKeyword('KEY') || this.isKeyword('INDEX')) {
694
+ this.next();
695
+ if (this.peek().type === 'ident') this.next(); // 索引名
696
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
697
+ }
698
+ }
699
+
556
700
  parseInsertBody() {
557
701
  const name = this.parseTableName();
558
702
  let columns = null;
@@ -683,6 +827,13 @@ class Parser {
683
827
  parseValue() {
684
828
  const t = this.next();
685
829
  if (t.type === 'number' || t.type === 'string') return t.value;
830
+ // MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
831
+ if (t.type === 'keyword' && t.value === 'VALUES' && this.peek().type === 'op' && this.peek().value === '(') {
832
+ this.next();
833
+ const col = this.parseColumnRef();
834
+ this.expect('op', ')');
835
+ return { _valuesOf: col };
836
+ }
686
837
  if (t.type === 'keyword' && t.value === 'NULL') return null;
687
838
  if (t.type === 'keyword' && t.value === 'TRUE') return true;
688
839
  if (t.type === 'keyword' && t.value === 'FALSE') return false;
@@ -833,24 +984,37 @@ class Parser {
833
984
  if (t.type === 'keyword' && t.value === 'COUNT') {
834
985
  this.next();
835
986
  this.expect('op', '(');
987
+ let aggDistinct = false;
988
+ if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct = true; }
836
989
  let col = null;
837
990
  if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
838
991
  else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
992
+ // COUNT(DISTINCT a, b) —— 多列去重(仅对 COUNT 有意义,其它聚合按元组处理)
993
+ if (aggDistinct && this.peek().type === 'op' && this.peek().value === ',') {
994
+ const cols = [col];
995
+ while (this.peek().type === 'op' && this.peek().value === ',') {
996
+ this.next();
997
+ cols.push(this.parseScalar());
998
+ }
999
+ col = { type: 'tuple', items: cols };
1000
+ }
839
1001
  this.expect('op', ')');
840
- aggregate = { type: 'COUNT', column: col };
1002
+ aggregate = { type: 'COUNT', column: col, distinct: aggDistinct };
841
1003
  aggregate = this.parseOptionalOver(aggregate);
842
1004
  aggregate.alias = this.parseOptionalAlias();
843
- columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
1005
+ columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct });
844
1006
  } else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
845
1007
  this.next();
846
1008
  const fn = t.value;
847
1009
  this.expect('op', '(');
1010
+ let aggDistinct2 = false;
1011
+ if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct2 = true; }
848
1012
  const col = this.parseScalar();
849
1013
  this.expect('op', ')');
850
- aggregate = { type: fn, column: col };
1014
+ aggregate = { type: fn, column: col, distinct: aggDistinct2 };
851
1015
  aggregate = this.parseOptionalOver(aggregate);
852
1016
  aggregate.alias = this.parseOptionalAlias();
853
- columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
1017
+ columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct2 });
854
1018
  } else if (t.type === 'op' && t.value === '*') {
855
1019
  this.next();
856
1020
  columns.push({ expr: '*' });
@@ -867,7 +1031,7 @@ class Parser {
867
1031
  if (expr.type === 'star') {
868
1032
  columns.push({ expr: '*' });
869
1033
  } else if (expr.type === 'aggregate') {
870
- columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr });
1034
+ columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr, distinct: expr.distinct, separator: expr.separator, orderBy: expr.orderBy });
871
1035
  } else if (expr.type === 'column') {
872
1036
  columns.push({ expr: expr.name, scalar: expr, alias });
873
1037
  } else {
@@ -886,6 +1050,7 @@ class Parser {
886
1050
  let where = null;
887
1051
  if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
888
1052
  let groupBy = null;
1053
+ let rollup = false;
889
1054
  if (this.isKeyword('GROUP')) {
890
1055
  this.expectKeyword('GROUP'); this.expectKeyword('BY');
891
1056
  groupBy = [];
@@ -894,6 +1059,12 @@ class Parser {
894
1059
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
895
1060
  break;
896
1061
  }
1062
+ // WITH ROLLUP 必须紧跟 GROUP BY 列表,之后才是 HAVING / ORDER BY / LIMIT
1063
+ // (标准顺序:GROUP BY <list> [WITH ROLLUP] [HAVING] [ORDER BY] [LIMIT])
1064
+ if (this.isKeyword('WITH') && this.isWord('ROLLUP', 1)) {
1065
+ this.next(); this.next();
1066
+ rollup = true;
1067
+ }
897
1068
  }
898
1069
  let having = null;
899
1070
  if (this.isKeyword('HAVING')) { this.next(); having = this.parseExpr(); }
@@ -961,13 +1132,6 @@ class Parser {
961
1132
  else if (this.isKeyword('DISTINCT')) this.next();
962
1133
  except = { all, select: this.parseSelect() };
963
1134
  }
964
- // GROUP BY ... WITH ROLLUP
965
- let rollup = false;
966
- if (groupBy && this.isKeyword('WITH')) {
967
- this.next();
968
- if (this.isWord('ROLLUP')) { this.next(); rollup = true; }
969
- else throw new Error(`Expected ROLLUP after WITH, got '${this.peek().value}'`);
970
- }
971
1135
  // FETCH FIRST n ROWS ONLY(SQL 标准的 LIMIT 写法)
972
1136
  if (this.isWord('FETCH')) {
973
1137
  this.next();
@@ -1029,19 +1193,18 @@ class Parser {
1029
1193
  this.expect('op', ')');
1030
1194
  let alias = null;
1031
1195
  if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
1032
- else if (this.peek().type === 'ident') { alias = this.next().value; }
1033
- if (!alias) throw new Error('Subquery in FROM requires an alias');
1196
+ else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) { alias = this.next().value; }
1197
+ // 标准 SQL 要求 FROM 子查询必须带别名,这里放宽为自动生成,
1198
+ // 避免 README 里 SELECT ... FROM (SELECT ...) 这类示例直接报错
1199
+ if (!alias) alias = '__sub' + (++this.subquerySeq);
1034
1200
  return { subquery: sub, alias };
1035
1201
  }
1036
1202
  const table = this.parseTableName();
1037
1203
  let alias = null;
1038
1204
  // 这里排除的是"未进关键字表、但会跟在表名之后"的子句起始词,
1039
1205
  // 否则 `FROM t FETCH FIRST 10 ROWS ONLY` 会把 FETCH 当成表别名吃掉。
1040
- const NOT_ALIAS = ['JOIN', 'LEFT', 'RIGHT', 'FULL', 'WHERE', 'GROUP', 'ORDER', 'LIMIT',
1041
- 'ON', 'HAVING', 'UNION', 'INNER', 'CROSS', 'OUTER', 'INTERSECT', 'EXCEPT',
1042
- 'FETCH', 'RETURNING', 'WINDOW'];
1043
1206
  if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
1044
- else if (this.peek().type === 'ident' && !NOT_ALIAS.some(w => this.isWord(w))) {
1207
+ else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) {
1045
1208
  alias = this.next().value;
1046
1209
  }
1047
1210
  return { table, alias };
@@ -1096,9 +1259,28 @@ class Parser {
1096
1259
  const table = this.parseTableName();
1097
1260
  let where = null;
1098
1261
  if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
1262
+ let orderBy = null, limit = null;
1263
+ if (this.isKeyword('ORDER')) {
1264
+ this.next(); this.expectKeyword('BY');
1265
+ orderBy = [];
1266
+ for (;;) {
1267
+ const col = this.parseColumnRef();
1268
+ let dir = 'asc';
1269
+ if (this.isKeyword('ASC')) this.next();
1270
+ else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
1271
+ orderBy.push({ column: col, dir });
1272
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1273
+ break;
1274
+ }
1275
+ }
1276
+ if (this.isKeyword('LIMIT')) {
1277
+ this.next();
1278
+ limit = this.parseValue();
1279
+ if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
1280
+ }
1099
1281
  const returning = this.parseOptionalReturning();
1100
1282
  this.optionalTailSemicolon();
1101
- return { type: 'delete', table, where, returning };
1283
+ return { type: 'delete', table, where, orderBy, limit, returning };
1102
1284
  }
1103
1285
 
1104
1286
  parseDropTable() {
@@ -1386,6 +1568,65 @@ class Parser {
1386
1568
  return this.parseOr();
1387
1569
  }
1388
1570
 
1571
+ /**
1572
+ * 函数实参用的表达式:与 parseExpr 相同,但叶子允许是"裸操作数"。
1573
+ * 例如 IF(1 > 0, 'y', 'n') 的第一个参数是完整比较表达式,
1574
+ * 而 IF(age, 'y', 'n') 这种裸列名也必须能解析。
1575
+ */
1576
+ parseArgExpr() {
1577
+ this.bareExprDepth = (this.bareExprDepth || 0) + 1;
1578
+ try {
1579
+ return this.parseOr();
1580
+ } finally {
1581
+ this.bareExprDepth--;
1582
+ }
1583
+ }
1584
+
1585
+ // 位运算优先级(低 → 高):| < ^ < & < << >> < 加减 < 乘除模
1586
+ parseBitwise() {
1587
+ let node = this.parseBitXor();
1588
+ for (;;) {
1589
+ const t = this.peek();
1590
+ if (t.type === 'op' && t.value === '|') { this.next(); node = { type: 'arith', op: '|', left: node, right: this.parseBitXor() }; continue; }
1591
+ break;
1592
+ }
1593
+ return node;
1594
+ }
1595
+
1596
+ parseBitXor() {
1597
+ let node = this.parseBitAnd();
1598
+ for (;;) {
1599
+ const t = this.peek();
1600
+ if (t.type === 'op' && t.value === '^') { this.next(); node = { type: 'arith', op: '^', left: node, right: this.parseBitAnd() }; continue; }
1601
+ break;
1602
+ }
1603
+ return node;
1604
+ }
1605
+
1606
+ parseBitAnd() {
1607
+ let node = this.parseShift();
1608
+ for (;;) {
1609
+ const t = this.peek();
1610
+ if (t.type === 'op' && t.value === '&') { this.next(); node = { type: 'arith', op: '&', left: node, right: this.parseShift() }; continue; }
1611
+ break;
1612
+ }
1613
+ return node;
1614
+ }
1615
+
1616
+ parseShift() {
1617
+ let node = this.parseAdditive();
1618
+ for (;;) {
1619
+ const t = this.peek();
1620
+ if (t.type === 'op' && (t.value === '<<' || t.value === '>>')) {
1621
+ this.next();
1622
+ node = { type: 'arith', op: t.value, left: node, right: this.parseAdditive() };
1623
+ continue;
1624
+ }
1625
+ break;
1626
+ }
1627
+ return node;
1628
+ }
1629
+
1389
1630
  parseOr() {
1390
1631
  let left = this.parseAnd();
1391
1632
  while (this.isKeyword('OR')) {
@@ -1434,9 +1675,69 @@ class Parser {
1434
1675
  return this.parseComparison();
1435
1676
  }
1436
1677
 
1678
+ /**
1679
+ * 解析聚合调用:fn ( [DISTINCT] expr [, expr*] [ORDER BY ...] [SEPARATOR 'x'] )
1680
+ * 以及 COUNT(*) 这类无参形式。
1681
+ */
1682
+ parseAggregateCall(fn) {
1683
+ this.expect('op', '(');
1684
+ const distinct = this.isKeyword('DISTINCT');
1685
+ if (distinct) this.next();
1686
+ let column = null;
1687
+ let separator = null;
1688
+ let innerOrder = null;
1689
+ if (this.peek().type === 'op' && this.peek().value === '*') {
1690
+ this.next();
1691
+ } else if (!(this.peek().type === 'op' && this.peek().value === ')')) {
1692
+ column = this.parseScalar();
1693
+ // COUNT(DISTINCT a, b):多列去重
1694
+ if (distinct && this.peek().type === 'op' && this.peek().value === ',') {
1695
+ const cols = [column];
1696
+ while (this.peek().type === 'op' && this.peek().value === ',') {
1697
+ this.next();
1698
+ cols.push(this.parseScalar());
1699
+ }
1700
+ column = { type: 'tuple', items: cols };
1701
+ } else if (this.peek().type === 'op' && this.peek().value === ',') {
1702
+ // GROUP_CONCAT(expr, 'sep')
1703
+ this.next();
1704
+ separator = this.parseValue();
1705
+ }
1706
+ // GROUP_CONCAT(expr ORDER BY col [DESC])
1707
+ if (this.isKeyword('ORDER')) {
1708
+ this.next();
1709
+ if (!this.isKeyword('BY')) throw new Error('Expected BY after ORDER in aggregate, got ' + "'" + this.peek().value + "'");
1710
+ this.next();
1711
+ innerOrder = [];
1712
+ for (;;) {
1713
+ const oCol = this.parseColumnRef();
1714
+ let oDir = 'asc';
1715
+ if (this.isKeyword('ASC')) this.next();
1716
+ else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
1717
+ innerOrder.push({ column: oCol, dir: oDir });
1718
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1719
+ break;
1720
+ }
1721
+ }
1722
+ // GROUP_CONCAT(expr SEPARATOR 'sep')
1723
+ if (this.isWord('SEPARATOR')) {
1724
+ this.next();
1725
+ separator = this.parseValue();
1726
+ }
1727
+ }
1728
+ this.expect('op', ')');
1729
+ return this.parseOptionalOver({ type: 'aggregate', fn, column, distinct, separator, orderBy: innerOrder });
1730
+ }
1731
+
1437
1732
  parseOperand() {
1438
1733
  const t = this.next();
1439
1734
  if (t.type === 'ident') {
1735
+ // 聚合函数名(GROUP_CONCAT / STDDEV / FIRST...)走 aggregate 节点,
1736
+ // 否则会被当作逐行标量函数:既返回 null,又不会折叠分组
1737
+ if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
1738
+ this.peek().type === 'op' && this.peek().value === '(') {
1739
+ return this.parseAggregateCall(String(t.value).toUpperCase());
1740
+ }
1440
1741
  // 支持 alias.column 引用
1441
1742
  if (this.peek().type === 'op' && this.peek().value === '.') {
1442
1743
  this.next();
@@ -1469,6 +1770,60 @@ class Parser {
1469
1770
  this.expect('op', ')');
1470
1771
  return { type: 'cast', expr, dataType };
1471
1772
  }
1773
+ // EXTRACT(YEAR FROM expr) —— 标准 SQL 的 FROM 形式
1774
+ if (String(t.value).toUpperCase() === 'EXTRACT' && this.peek().type === 'op' && this.peek().value === '(') {
1775
+ this.next();
1776
+ const unitTok = this.next();
1777
+ const unit = String(unitTok.value).toUpperCase();
1778
+ if (!this.isWord('FROM')) throw new Error(`Expected FROM in EXTRACT, got '${this.peek().value}'`);
1779
+ this.next();
1780
+ const arg = this.parseOperand();
1781
+ this.expect('op', ')');
1782
+ return { type: 'func', name: 'EXTRACT', args: [{ type: 'value', value: unit }, arg] };
1783
+ }
1784
+ // SUBSTR(s FROM n [FOR m]) / TRIM([LEADING|TRAILING|BOTH] ['x'] FROM s)
1785
+ if (['SUBSTR', 'SUBSTRING'].includes(String(t.value).toUpperCase()) && this.peek().type === 'op' && this.peek().value === '(') {
1786
+ const name = String(t.value).toUpperCase();
1787
+ this.next();
1788
+ const s = this.parseOperand();
1789
+ let fromV = null, forV = null;
1790
+ if (this.isWord('FROM')) {
1791
+ this.next();
1792
+ fromV = this.parseOperand();
1793
+ if (this.isWord('FOR')) { this.next(); forV = this.parseOperand(); }
1794
+ } else if (this.peek().type === 'op' && this.peek().value === ',') {
1795
+ this.next();
1796
+ fromV = this.parseOperand();
1797
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); forV = this.parseOperand(); }
1798
+ }
1799
+ this.expect('op', ')');
1800
+ const args = [s];
1801
+ if (fromV) args.push(fromV);
1802
+ if (forV) args.push(forV);
1803
+ return { type: 'func', name, args };
1804
+ }
1805
+ if (String(t.value).toUpperCase() === 'TRIM' && this.peek().type === 'op' && this.peek().value === '(') {
1806
+ this.next();
1807
+ let spec = null;
1808
+ if (this.isWord('LEADING') || this.isWord('TRAILING') || this.isWord('BOTH')) {
1809
+ spec = String(this.next().value).toUpperCase();
1810
+ }
1811
+ const first = this.parseOperand();
1812
+ let target = null;
1813
+ if (this.isWord('FROM')) {
1814
+ this.next();
1815
+ target = this.parseOperand();
1816
+ } else if (this.peek().type === 'op' && this.peek().value === ',') {
1817
+ this.next();
1818
+ target = this.parseOperand();
1819
+ }
1820
+ this.expect('op', ')');
1821
+ // 归一成 TRIM(target[, chars[, spec]]),与 applyScalarFunction 的既有签名兼容
1822
+ if (target === null) return { type: 'func', name: 'TRIM', args: [first] };
1823
+ const args = [target, first];
1824
+ if (spec) args.push({ type: 'value', value: spec });
1825
+ return { type: 'func', name: 'TRIM', args };
1826
+ }
1472
1827
  // 函数调用: VERSION() / CONCAT(a, b) / NOW() ...
1473
1828
  if (this.peek().type === 'op' && this.peek().value === '(') {
1474
1829
  const name = t.value;
@@ -1477,7 +1832,42 @@ class Parser {
1477
1832
  if (!(this.peek().type === 'op' && this.peek().value === ')')) {
1478
1833
  for (;;) {
1479
1834
  if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
1480
- else args.push(this.parseOperand());
1835
+ // INTERVAL n UNIT(DATE_ADD/DATE_SUB 用)
1836
+ else if (this.isWord('INTERVAL')) {
1837
+ this.next();
1838
+ const nTok = this.peek();
1839
+ let nVal;
1840
+ if (nTok.type === 'op' && (nTok.value === '-' || nTok.value === '+')) {
1841
+ this.next();
1842
+ const numTok = this.next();
1843
+ nVal = (nTok.value === '-' ? -1 : 1) * Number(numTok.value);
1844
+ } else {
1845
+ this.next();
1846
+ nVal = Number(nTok.value);
1847
+ }
1848
+ const unitTok = this.next();
1849
+ args.push({ type: 'interval', value: nVal, unit: String(unitTok.value).toUpperCase() });
1850
+ }
1851
+ // 实参允许完整的比较/布尔表达式,例如 IF(status = 'paid', amount, 0)
1852
+ else args.push(this.parseArgExpr());
1853
+ // GROUP_CONCAT(name ORDER BY age):聚合函数内部的排序
1854
+ if (this.isKeyword('ORDER')) {
1855
+ this.next();
1856
+ if (!this.isKeyword('BY')) throw new Error(`Expected BY after ORDER in aggregate, got '${this.peek().value}'`);
1857
+ this.next();
1858
+ const innerOrder = [];
1859
+ for (;;) {
1860
+ const oCol = this.parseColumnRef();
1861
+ let oDir = 'asc';
1862
+ if (this.isKeyword('ASC')) this.next();
1863
+ else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
1864
+ innerOrder.push({ column: oCol, dir: oDir });
1865
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1866
+ break;
1867
+ }
1868
+ this.expect('op', ')');
1869
+ return this.parseOptionalOver({ type: 'func', name, args, orderBy: innerOrder });
1870
+ }
1481
1871
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1482
1872
  break;
1483
1873
  }
@@ -1502,7 +1892,7 @@ class Parser {
1502
1892
  if (!(this.peek().type === 'op' && this.peek().value === ')')) {
1503
1893
  for (;;) {
1504
1894
  if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
1505
- else args.push(this.parseOperand());
1895
+ else args.push(this.parseArgExpr());
1506
1896
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1507
1897
  break;
1508
1898
  }
@@ -1510,14 +1900,13 @@ class Parser {
1510
1900
  this.expect('op', ')');
1511
1901
  return this.parseOptionalOver({ type: 'func', name, args });
1512
1902
  }
1513
- if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX', 'COUNT'].includes(t.value)) {
1514
- const fn = t.value;
1515
- this.expect('op', '(');
1516
- let column = null;
1517
- if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
1518
- else if (!(this.peek().type === 'op' && this.peek().value === ')')) column = this.parseScalar();
1519
- this.expect('op', ')');
1520
- return this.parseOptionalOver({ type: 'aggregate', fn, column });
1903
+ if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
1904
+ this.peek().type === 'op' && this.peek().value === '(') {
1905
+ return this.parseAggregateCall(String(t.value).toUpperCase());
1906
+ }
1907
+ // 一元按位取反 ~x
1908
+ if (t.type === 'op' && t.value === '~') {
1909
+ return { type: 'bitnot', expr: this.parseOperand() };
1521
1910
  }
1522
1911
  if (t.type === 'op' && t.value === '(' && this.peek().type === 'keyword' && this.peek().value === 'SELECT') {
1523
1912
  const sub = this.parseSelect();
@@ -1532,8 +1921,8 @@ class Parser {
1532
1921
  throw new Error(`Expected value or column, got '${t.value}'`);
1533
1922
  }
1534
1923
 
1535
- // 算术表达式: + - * / % (左结合, * / 优先)
1536
- parseScalar() {
1924
+ // 加减(左结合)
1925
+ parseAdditive() {
1537
1926
  let node = this.parseTerm();
1538
1927
  for (;;) {
1539
1928
  const t = this.peek();
@@ -1545,6 +1934,16 @@ class Parser {
1545
1934
  }
1546
1935
  break;
1547
1936
  }
1937
+ return node;
1938
+ }
1939
+
1940
+ /**
1941
+ * 标量表达式:算术 + 位运算,外加 IN / IS 后缀。
1942
+ * 位运算要走到这里,否则 SELECT 列表里的 `SELECT 1 << 8` 无法解析
1943
+ * (parseComparison 只在 WHERE 等布尔上下文被调用)。
1944
+ */
1945
+ parseScalar() {
1946
+ let node = this.parseBitwise();
1548
1947
  // 后缀:expr IN (...)、expr IS [NOT] TRUE/FALSE/NULL(标量上下文,如 SELECT 1 IN (...))
1549
1948
  for (;;) {
1550
1949
  const t = this.peek();
@@ -1623,7 +2022,7 @@ class Parser {
1623
2022
  }
1624
2023
 
1625
2024
  parseComparison() {
1626
- const left = this.parseOperand();
2025
+ const left = this.parseBitwise();
1627
2026
  const t = this.peek();
1628
2027
  let not = false;
1629
2028
  if (t.type === 'keyword' && t.value === 'NOT') {
@@ -1707,10 +2106,13 @@ class Parser {
1707
2106
  this.expect('op', ')');
1708
2107
  return { type: 'quantified', op: t2.value === '<>' ? '!=' : t2.value, operand: left, select: sub, quantifier, not };
1709
2108
  }
1710
- const right = this.parseOperand();
2109
+ const right = this.parseBitwise();
1711
2110
  return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
1712
2111
  }
1713
2112
 
2113
+ // 函数实参场景允许裸操作数(IF(age, ...) / SUM(IF(...)) 等)
2114
+ if (this.bareExprDepth > 0) return left;
2115
+
1714
2116
  throw new Error(`Expected comparison operator, got '${t2.value}'`);
1715
2117
  }
1716
2118
  }
@@ -1807,16 +2209,31 @@ function resolveOperand(operand, row, ctx) {
1807
2209
  case '*': return l * r;
1808
2210
  case '/': return r === 0 ? null : l / r;
1809
2211
  case '%': return r === 0 ? null : l % r;
2212
+ // 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
2213
+ case '&': return (Number(l) | 0) & (Number(r) | 0);
2214
+ case '|': return (Number(l) | 0) | (Number(r) | 0);
2215
+ case '^': return (Number(l) | 0) ^ (Number(r) | 0);
2216
+ case '<<': return (Number(l) | 0) << (Number(r) | 0);
2217
+ case '>>': return (Number(l) | 0) >> (Number(r) | 0);
1810
2218
  }
1811
2219
  return null;
1812
2220
  }
2221
+ case 'bitnot':
2222
+ return ~(Number(resolveOperand(operand.expr, row, ctx)) | 0);
1813
2223
  case 'func':
1814
2224
  return applyScalarFunction(operand, row, ctx);
1815
2225
  case 'case':
1816
2226
  return evaluateCaseVal(operand, row, ctx);
2227
+ // 布尔类节点出现在"取值"位置时(如 IF(status = 'paid', ...)、SUM(IF(a > b,1,0))),
2228
+ // 求值成 true/false 供上层按真值判断
2229
+ case 'compare':
2230
+ case 'and':
2231
+ case 'or':
2232
+ case 'not':
1817
2233
  case 'in':
1818
2234
  case 'isNull':
1819
2235
  case 'isTruth':
2236
+ case 'exists':
1820
2237
  case 'regexp':
1821
2238
  case 'like':
1822
2239
  case 'between':
@@ -1856,9 +2273,30 @@ function resolveOperand(operand, row, ctx) {
1856
2273
  }
1857
2274
  }
1858
2275
 
2276
+ /** 解析 SQL 里的日期文本('YYYY-MM-DD' / 'YYYY-MM-DD HH:MM:SS' / Date) */
2277
+ function parseSqlDate(v) {
2278
+ if (v === null || v === undefined) return null;
2279
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
2280
+ const s = String(v).trim().replace(' ', 'T');
2281
+ const d = new Date(s.length <= 10 ? s + 'T00:00:00' : s);
2282
+ return Number.isNaN(d.getTime()) ? null : d;
2283
+ }
2284
+
2285
+ function pad2(n) { return String(n).padStart(2, '0'); }
2286
+
2287
+ /** 按输入形态回写日期:纯日期返回 YYYY-MM-DD,带时间返回 YYYY-MM-DD HH:MM:SS */
2288
+ function fmtSqlDate(dateOnly, d) {
2289
+ const ymd = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
2290
+ return dateOnly ? ymd : ymd + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
2291
+ }
2292
+
1859
2293
  function applyScalarFunction(fnNode, row, ctx) {
1860
2294
  const name = (fnNode.name || '').toUpperCase();
1861
- const args = (fnNode.args || []).map(a => resolveOperand(a, row, ctx));
2295
+ const rawArgs = fnNode.args || [];
2296
+ // INTERVAL n UNIT 是语法结构而非普通值,原样传给 DATE_ADD / DATE_SUB
2297
+ const args = rawArgs.map(a => (a && a.type === 'interval')
2298
+ ? { __interval: true, value: a.value, unit: a.unit }
2299
+ : resolveOperand(a, row, ctx));
1862
2300
  const session = ctx && ctx.session;
1863
2301
  if (ctx && ctx.functions && Object.prototype.hasOwnProperty.call(ctx.functions, name)) {
1864
2302
  return ctx.functions[name].apply(null, args);
@@ -1889,7 +2327,65 @@ function applyScalarFunction(fnNode, row, ctx) {
1889
2327
  case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
1890
2328
  case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
1891
2329
  case 'LENGTH': case 'CHAR_LENGTH': case 'CHARACTER_LENGTH': return args[0] == null ? null : String(args[0]).length;
1892
- case 'TRIM': return args[0] == null ? null : String(args[0]).trim();
2330
+ // TRIM(s) / TRIM([LEADING|TRAILING|BOTH] chars FROM s) / TRIM(s, chars)
2331
+ case 'TRIM': {
2332
+ if (args[0] == null) return null;
2333
+ const s = String(args[0]);
2334
+ if (args.length === 1) return s.trim();
2335
+ const chars = args[1] == null ? '' : String(args[1]);
2336
+ if (!chars) return s.trim();
2337
+ const spec = args[2] ? String(args[2]).toUpperCase() : 'BOTH';
2338
+ const esc = chars.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2339
+ const head = new RegExp('^[' + esc + ']+');
2340
+ const tail = new RegExp('[' + esc + ']+$');
2341
+ if (spec === 'LEADING') return s.replace(head, '');
2342
+ if (spec === 'TRAILING') return s.replace(tail, '');
2343
+ return s.replace(head, '').replace(tail, '');
2344
+ }
2345
+ // EXTRACT(YEAR FROM d) —— 解析层会把 unit 作为第一个参数传入
2346
+ case 'EXTRACT': {
2347
+ const unit = String(args[0] == null ? '' : args[0]).toUpperCase();
2348
+ const d = parseSqlDate(args[1]);
2349
+ if (!d) return null;
2350
+ switch (unit) {
2351
+ case 'YEAR': return d.getFullYear();
2352
+ case 'MONTH': return d.getMonth() + 1;
2353
+ case 'DAY': return d.getDate();
2354
+ case 'HOUR': return d.getHours();
2355
+ case 'MINUTE': return d.getMinutes();
2356
+ case 'SECOND': return d.getSeconds();
2357
+ case 'DOW': return d.getDay(); // 0 = 周日
2358
+ case 'DAYOFWEEK': return d.getDay() + 1; // 1 = 周日(MySQL 语义)
2359
+ case 'WEEK': {
2360
+ const start = new Date(d.getFullYear(), 0, 1);
2361
+ return Math.floor((d - start) / 604800000) + 1;
2362
+ }
2363
+ default: return null;
2364
+ }
2365
+ }
2366
+ case 'DATE_ADD': case 'ADDDATE':
2367
+ case 'DATE_SUB': case 'SUBDATE': {
2368
+ const sign = (name === 'DATE_SUB' || name === 'SUBDATE') ? -1 : 1;
2369
+ const base = parseSqlDate(args[0]);
2370
+ if (!base) return null;
2371
+ const iv = args[1];
2372
+ if (!iv || !iv.__interval) return fmtSqlDate(String(args[0]).length <= 10, base);
2373
+ const isDateOnly = String(args[0]).trim().length <= 10;
2374
+ const d = new Date(base.getTime());
2375
+ const n = sign * Number(iv.value || 0);
2376
+ switch (String(iv.unit || '').toUpperCase()) {
2377
+ case 'DAY': case 'DAYS': d.setDate(d.getDate() + n); break;
2378
+ case 'WEEK': case 'WEEKS': d.setDate(d.getDate() + n * 7); break;
2379
+ case 'MONTH': case 'MONTHS': d.setMonth(d.getMonth() + n); break;
2380
+ case 'QUARTER': d.setMonth(d.getMonth() + n * 3); break;
2381
+ case 'YEAR': case 'YEARS': d.setFullYear(d.getFullYear() + n); break;
2382
+ case 'HOUR': case 'HOURS': d.setHours(d.getHours() + n); break;
2383
+ case 'MINUTE': case 'MINUTES': d.setMinutes(d.getMinutes() + n); break;
2384
+ case 'SECOND': case 'SECONDS': d.setSeconds(d.getSeconds() + n); break;
2385
+ default: return null;
2386
+ }
2387
+ return fmtSqlDate(isDateOnly, d);
2388
+ }
1893
2389
  case 'LTRIM': return args[0] == null ? null : String(args[0]).replace(/^\s+/, '');
1894
2390
  case 'RTRIM': return args[0] == null ? null : String(args[0]).replace(/\s+$/, '');
1895
2391
  case 'ABS': return args[0] == null ? null : Math.abs(args[0]);
@@ -2154,16 +2650,22 @@ function windowColumnValue(c, r) {
2154
2650
 
2155
2651
  function scalarColumnValue(c, r, ctx) {
2156
2652
  if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
2653
+ // 聚合列优先:GROUP_CONCAT / STDDEV 等同时带 scalar 与 aggregate
2654
+ if (c.aggregate) {
2655
+ return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column,
2656
+ c.distinct, c.separator !== undefined ? c.separator : (c.scalar && c.scalar.separator),
2657
+ c.orderBy || (c.scalar && c.scalar.orderBy));
2658
+ }
2157
2659
  if (c.scalar) {
2158
2660
  const s = c.scalar;
2159
2661
  if (s && s.type === 'func' && ctx && ctx.ctxAggregates && Object.prototype.hasOwnProperty.call(ctx.ctxAggregates, String(s.name).toUpperCase())) {
2160
2662
  const fn = String(s.name).toUpperCase();
2161
2663
  const col = s.args && s.args[0];
2162
- return ctx._aggValue(ctx.group, fn, col);
2664
+ return ctx._aggValue(ctx.group, fn, col, c.aggregate ? c.distinct : (c.scalar && c.scalar.distinct));
2163
2665
  }
2164
2666
  return resolveOperand(c.scalar, r, ctx);
2165
2667
  }
2166
- if (c.aggregate) return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column);
2668
+
2167
2669
  if (c.literal !== undefined) return c.literal;
2168
2670
  if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r, ctx);
2169
2671
  if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
@@ -2186,7 +2688,10 @@ function buildCreateTableSql(name, schema) {
2186
2688
  const seg = ['`' + col + '`', sqlTypeName(def.type)];
2187
2689
  if (def.autoIncrement) seg.push('AUTO_INCREMENT');
2188
2690
  if (def.nullable === false) seg.push('NOT NULL');
2189
- if (def.default !== undefined) seg.push('DEFAULT ' + (typeof def.default === 'string' ? "'" + def.default + "'" : def.default));
2691
+ if (def.default !== undefined) {
2692
+ // defaultExpr(CURRENT_TIMESTAMP 等)不加引号,普通字符串默认值才加
2693
+ seg.push('DEFAULT ' + (typeof def.default === 'string' && !def.defaultExpr ? "'" + def.default + "'" : def.default));
2694
+ }
2190
2695
  return seg.join(' ');
2191
2696
  });
2192
2697
  const pks = Object.keys(schema).filter(k => schema[k].primaryKey);
@@ -2396,7 +2901,12 @@ class SQLExecutor {
2396
2901
  for (const { d, existingId } of conflicts) {
2397
2902
  const data = {};
2398
2903
  if (conflictSets) {
2399
- for (const [col, val] of conflictSets) data[col] = val;
2904
+ for (const [col, val] of conflictSets) {
2905
+ // VALUES(col) 引用本次本应插入的值
2906
+ data[col] = (val && typeof val === 'object' && val._valuesOf !== undefined)
2907
+ ? (d[val._valuesOf] !== undefined ? d[val._valuesOf] : null)
2908
+ : val;
2909
+ }
2400
2910
  } else {
2401
2911
  for (const k of Object.keys(d)) if (!pkCols.includes(k)) data[k] = d[k];
2402
2912
  }
@@ -2525,6 +3035,52 @@ class SQLExecutor {
2525
3035
  delete this.engine._views[key];
2526
3036
  return { ok: true, type: 'dropView', message: `View '${statement.name}' dropped`, affectedRows: 0, warnings: [] };
2527
3037
  }
3038
+ case 'createIndex': {
3039
+ const tName = statement.table;
3040
+ if (typeof this.engine.hasTable === 'function' && !this.engine.hasTable(tName)) {
3041
+ throw new Error(`Table '${tName}' does not exist`);
3042
+ }
3043
+ let handled = false;
3044
+ if (typeof this.engine.createIndex === 'function') {
3045
+ await this.engine.createIndex(tName, statement.columns, { unique: statement.unique, name: statement.name });
3046
+ handled = true;
3047
+ } else if (typeof this.engine._ensureTable === 'function') {
3048
+ const tbl = this.engine._ensureTable(tName) || (this.engine._tables && this.engine._tables[tName]);
3049
+ if (tbl && typeof tbl.createIndex === 'function') {
3050
+ // Table.createIndex 目前是单字段接口,复合索引按逐列建
3051
+ for (const col of statement.columns) tbl.createIndex(col);
3052
+ handled = true;
3053
+ }
3054
+ }
3055
+ if (!handled) {
3056
+ throw new Error(
3057
+ `CREATE INDEX is not supported by this engine (no createIndex/_ensureTable on the engine object)`
3058
+ );
3059
+ }
3060
+ return {
3061
+ ok: true, type: 'createIndex', table: tName, name: statement.name,
3062
+ affectedRows: 0, warnings: [],
3063
+ message: `Index '${statement.name}' created on ${tName}(${statement.columns.join(', ')})`,
3064
+ };
3065
+ }
3066
+ case 'dropIndex': {
3067
+ const tName2 = statement.table;
3068
+ let handled = false;
3069
+ if (typeof this.engine.dropIndex === 'function') {
3070
+ await this.engine.dropIndex(tName2, statement.name);
3071
+ handled = true;
3072
+ } else if (tName2 && typeof this.engine._ensureTable === 'function') {
3073
+ const tbl = this.engine._ensureTable(tName2) || (this.engine._tables && this.engine._tables[tName2]);
3074
+ if (tbl && typeof tbl.dropIndex === 'function') { tbl.dropIndex(statement.name); handled = true; }
3075
+ }
3076
+ if (!handled) {
3077
+ if (statement.ifExists) {
3078
+ return { ok: true, type: 'dropIndex', affectedRows: 0, skipped: true, warnings: [], message: `Index '${statement.name}' did not exist` };
3079
+ }
3080
+ throw new Error(`DROP INDEX is not supported by this engine (no dropIndex on the engine/table object)`);
3081
+ }
3082
+ return { ok: true, type: 'dropIndex', table: tName2, name: statement.name, affectedRows: 0, warnings: [], message: `Index '${statement.name}' dropped` };
3083
+ }
2528
3084
  case 'savepoint': {
2529
3085
  // 引擎侧没有保存点/快照能力。这里只登记名字,绝不假装能回滚。
2530
3086
  if (!this.engine._savepoints) this.engine._savepoints = new Set();
@@ -2582,11 +3138,27 @@ class SQLExecutor {
2582
3138
  const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
2583
3139
  const ids = [];
2584
3140
  const removedRows = [];
2585
- for (const row of all) {
2586
- if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
2587
- const id = this._rowPkId(row, pkCols);
2588
- if (id !== undefined) { ids.push(id); removedRows.push(row); }
2589
- }
3141
+ let candidates = all.filter(row => !statement.where || evaluateExpr(statement.where, row, this.ctx));
3142
+ // DELETE ... ORDER BY ... LIMIT n
3143
+ if (statement.orderBy) {
3144
+ const cmp = (a, b) => {
3145
+ for (const o of statement.orderBy) {
3146
+ const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
3147
+ const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
3148
+ if (av === bv) continue;
3149
+ if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
3150
+ if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
3151
+ const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
3152
+ if (d !== 0) return o.dir === 'asc' ? d : -d;
3153
+ }
3154
+ return 0;
3155
+ };
3156
+ candidates = candidates.slice().sort(cmp);
3157
+ }
3158
+ if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
3159
+ for (const row of candidates) {
3160
+ const id = this._rowPkId(row, pkCols);
3161
+ if (id !== undefined) { ids.push(id); removedRows.push(row); }
2590
3162
  }
2591
3163
  if (ids.length > 0) {
2592
3164
  if (this.engine.removeByIds) await this.engine.removeByIds(statement.table, ids);
@@ -3114,12 +3686,28 @@ class SQLExecutor {
3114
3686
  return out;
3115
3687
  }
3116
3688
 
3117
- _aggValue(rows, fn, column) {
3118
- if (fn === 'COUNT') return rows.length;
3689
+ _aggValue(rows, fn, column, distinct, separator, orderBy) {
3119
3690
  const op = typeof column === 'string' ? { type: 'column', name: column } : column;
3120
- const values = op && op.type === 'star'
3691
+ // COUNT(*) / COUNT():参数为 star 或缺失,直接按行数算
3692
+ const noColumn = !op || op.type === 'star';
3693
+ let values = noColumn
3121
3694
  ? rows.map(r => 1)
3122
- : rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
3695
+ : op && op.type === 'tuple'
3696
+ // COUNT(DISTINCT a, b):先把多列拼成元组再聚合
3697
+ ? rows.map(r => JSON.stringify(op.items.map(it => resolveOperand(it, r, this.ctx))))
3698
+ .filter(v => v !== null && v !== undefined)
3699
+ : rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
3700
+ // COUNT(DISTINCT x) / SUM(DISTINCT x):先按值去重再聚合
3701
+ if (distinct) {
3702
+ const seen = new Set();
3703
+ values = values.filter(v => {
3704
+ const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
3705
+ if (seen.has(k)) return false;
3706
+ seen.add(k);
3707
+ return true;
3708
+ });
3709
+ }
3710
+ if (fn === 'COUNT') return values.length;
3123
3711
  if (this.ctx && this.ctx.aggregates && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
3124
3712
  const agg = this.ctx.aggregates[fn];
3125
3713
  if (typeof agg === 'function') {
@@ -3135,6 +3723,54 @@ class SQLExecutor {
3135
3723
  if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
3136
3724
  if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
3137
3725
  if (fn === 'MAX') return values.length ? Math.max(...values.map(v => Number(v))) : null;
3726
+ if (fn === 'FIRST') return values.length ? values[0] : null;
3727
+ if (fn === 'LAST') return values.length ? values[values.length - 1] : null;
3728
+ if (fn === 'GROUP_CONCAT') {
3729
+ let src = rows;
3730
+ // GROUP_CONCAT(expr ORDER BY col [DESC]):先按内部排序键排好行,再取值拼接
3731
+ if (orderBy && orderBy.length) {
3732
+ const cmp = (a, b) => {
3733
+ for (const o of orderBy) {
3734
+ const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
3735
+ const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
3736
+ if (av === bv) continue;
3737
+ if (av === null || av === undefined) return o.dir === 'asc' ? 1 : -1;
3738
+ if (bv === null || bv === undefined) return o.dir === 'asc' ? -1 : 1;
3739
+ const d = (typeof av === 'number' && typeof bv === 'number')
3740
+ ? av - bv : String(av).localeCompare(String(bv));
3741
+ if (d !== 0) return o.dir === 'asc' ? d : -d;
3742
+ }
3743
+ return 0;
3744
+ };
3745
+ src = rows.slice().sort(cmp);
3746
+ }
3747
+ const parts = src.map(r => resolveOperand(op, r, this.ctx));
3748
+ let joined = parts.filter(v => v !== null && v !== undefined);
3749
+ if (distinct) {
3750
+ const seen = new Set();
3751
+ joined = joined.filter(v => {
3752
+ const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
3753
+ if (seen.has(k)) return false;
3754
+ seen.add(k);
3755
+ return true;
3756
+ });
3757
+ }
3758
+ if (joined.length === 0) return null;
3759
+ const sep = (separator !== undefined && separator !== null) ? String(separator) : ',';
3760
+ return joined.map(v => (v === null || v === undefined ? '' : String(v))).join(sep);
3761
+ }
3762
+ // 标准差 / 方差族:_POP 为总体、_SAMP(及无后缀 VAR/STDDEV)为样本
3763
+ if (fn === 'STDDEV' || fn === 'STDDEV_POP' || fn === 'STDDEV_SAMP' ||
3764
+ fn === 'VARIANCE' || fn === 'VAR_POP' || fn === 'VAR_SAMP') {
3765
+ const nums = values.map(v => Number(v)).filter(v => Number.isFinite(v));
3766
+ if (nums.length === 0) return null;
3767
+ const pop = (fn === 'STDDEV_POP' || fn === 'VAR_POP');
3768
+ const mean = nums.reduce((s, v) => s + v, 0) / nums.length;
3769
+ const denom = pop ? nums.length : nums.length - 1; // 样本方差用 n-1
3770
+ if (denom <= 0) return null;
3771
+ const variance = nums.reduce((s, v) => s + (v - mean) * (v - mean), 0) / denom;
3772
+ return fn.indexOf('STDDEV') === 0 ? Math.sqrt(variance) : variance;
3773
+ }
3138
3774
  return null;
3139
3775
  }
3140
3776
 
@@ -3142,7 +3778,7 @@ class SQLExecutor {
3142
3778
  if (!node || typeof node !== 'object') return;
3143
3779
  if (node.type === 'aggregate') {
3144
3780
  node.type = 'value';
3145
- node.value = this._aggValue(group, node.fn, node.column);
3781
+ node.value = this._aggValue(group, node.fn, node.column, node.distinct, node.separator, node.orderBy);
3146
3782
  delete node.fn;
3147
3783
  delete node.column;
3148
3784
  return;
@@ -3211,6 +3847,61 @@ class SQLExecutor {
3211
3847
  return out;
3212
3848
  }
3213
3849
 
3850
+ /**
3851
+ * 校验语句里引用的列是否真实存在。
3852
+ * 之前 `SELECT nosuchcol FROM t` 会静默返回 null 而不是报错 —— 调用方拿到的
3853
+ * 是"一列 null",往往被当成"数据为空",比直接报 no such column 更难排查。
3854
+ *
3855
+ * 只在能确定列名集合时校验(取样本行的键),空表不敢断言,宁可不报。
3856
+ * 不深入子查询节点:它们有自己的 FROM,列属于另一个作用域。
3857
+ */
3858
+ _validateColumnRefs(statement, rows) {
3859
+ if (!statement || !Array.isArray(rows) || rows.length === 0) return;
3860
+ const sample = rows[0];
3861
+ if (!sample || typeof sample !== 'object') return;
3862
+ const known = new Set();
3863
+ for (const k of Object.keys(sample)) known.add(String(k).toLowerCase());
3864
+ // HAVING / ORDER BY 允许引用 SELECT 的输出别名(如 HAVING cnt >= 1),
3865
+ // 这些名字不在表里,必须一并视为已知,否则会误报 no such column
3866
+ if (Array.isArray(statement.columns)) {
3867
+ for (const c of statement.columns) {
3868
+ if (c && typeof c.alias === 'string' && c.alias) known.add(c.alias.toLowerCase());
3869
+ }
3870
+ }
3871
+
3872
+ const walk = (node) => {
3873
+ if (!node || typeof node !== 'object') return;
3874
+ if (Array.isArray(node)) { for (const n of node) walk(n); return; }
3875
+ // 子查询/嵌套 SELECT 有自己的表,跳过
3876
+ if (node.type === 'select' || node.type === 'subquery') return;
3877
+ if (node.type === 'column' && typeof node.name === 'string') {
3878
+ const n = node.name.toLowerCase();
3879
+ const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
3880
+ if (!known.has(n) && !known.has(base)) {
3881
+ throw new Error(`no such column: ${node.name}`);
3882
+ }
3883
+ }
3884
+ for (const k of Object.keys(node)) {
3885
+ if (k === 'select' || k === 'subquery') continue;
3886
+ walk(node[k]);
3887
+ }
3888
+ };
3889
+
3890
+ walk(statement.columns);
3891
+ walk(statement.where);
3892
+ walk(statement.having);
3893
+ walk(statement.orderBy);
3894
+ if (Array.isArray(statement.groupBy)) {
3895
+ // groupBy 是列名字符串数组
3896
+ for (const g of statement.groupBy) {
3897
+ if (typeof g !== 'string') continue;
3898
+ const n = g.toLowerCase();
3899
+ const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
3900
+ if (!known.has(n) && !known.has(base)) throw new Error(`no such column: ${g}`);
3901
+ }
3902
+ }
3903
+ }
3904
+
3214
3905
  /** 收集子树里所有列名(含限定形式 alias.col) */
3215
3906
  _collectColumnNames(node, out = []) {
3216
3907
  if (!node || typeof node !== 'object') return out;
@@ -3446,7 +4137,7 @@ class SQLExecutor {
3446
4137
  case 'MAX': {
3447
4138
  const f = frameIdx(pos);
3448
4139
  const slice = ordered.slice(f.start, f.end + 1).map(i => rows[i]);
3449
- v = this._aggValue(slice, w.fn, argNode);
4140
+ v = this._aggValue(slice, w.fn, argNode, !!(w.spec && w.spec.distinct), w.spec && w.spec.separator);
3450
4141
  break;
3451
4142
  }
3452
4143
  default: v = null;
@@ -3639,6 +4330,7 @@ class SQLExecutor {
3639
4330
  }
3640
4331
 
3641
4332
  let rows = all;
4333
+ this._validateColumnRefs(statement, rows);
3642
4334
  if (statement.where) {
3643
4335
  // 相关子查询:必须逐行重新求值,否则外层引用拿不到当前行的值
3644
4336
  const outerAliases = this._outerAliases(statement);
@@ -3681,7 +4373,7 @@ class SQLExecutor {
3681
4373
  for (const c of statement.columns) {
3682
4374
  if (c.aggregate) {
3683
4375
  const group = r._group || [r];
3684
- const v = this._aggValue(group, c.aggregate || 'COUNT', c.column);
4376
+ const v = this._aggValue(group, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
3685
4377
  ctx['__agg_' + (c.alias || c.column || 'COUNT(*)')] = v;
3686
4378
  if (c.column) ctx[c.column] = v;
3687
4379
  if (c.alias) ctx[c.alias] = v;
@@ -3702,7 +4394,7 @@ class SQLExecutor {
3702
4394
  const key = JSON.stringify(statement.columns.map(c => {
3703
4395
  if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
3704
4396
  if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
3705
- if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column);
4397
+ if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
3706
4398
  return resolveOperand({ type: 'column', name: c.expr }, r, this.ctx);
3707
4399
  }));
3708
4400
  if (seen.has(key)) return false;
@@ -3717,7 +4409,7 @@ class SQLExecutor {
3717
4409
  if (!statement.groupBy && (statement.columns.some(c => c.aggregate && !c.window) || hasCustomAgg)) {
3718
4410
  const cols = statement.columns.map(c => scalarColumnName(c));
3719
4411
  const valueOf = (c) => {
3720
- if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column);
4412
+ if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
3721
4413
  if (c.scalar && c.scalar.type === 'func' && hasCustomAgg) {
3722
4414
  const fn = String(c.scalar.name).toUpperCase();
3723
4415
  if (Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {