jsql-neo 6.0.0 → 6.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
@@ -79,6 +79,11 @@ function tokenize(sql) {
79
79
  while (i < n && sql[i] !== '\n') i++;
80
80
  continue;
81
81
  }
82
+ // PG 的 JSON 路径操作符 #> / #>> 尚未实现。
83
+ // 若不在这里拦截,'#' 会被当成行注释吃掉,导致 SQL 后半段被静默丢弃。
84
+ if (c === '#' && sql[i + 1] === '>') {
85
+ throw new Error("JSON 路径操作符 '#>' / '#>>' 暂不支持,请改用 '->' / '->>'");
86
+ }
82
87
  if (c === '#' || (c === '/' && sql[i + 1] === '*')) {
83
88
  if (c === '#') { while (i < n && sql[i] !== '\n') i++; continue; }
84
89
  i += 2;
@@ -95,7 +100,10 @@ function tokenize(sql) {
95
100
  if (sql[j] === '\\' && j + 1 < n) {
96
101
  const esc = sql[j + 1];
97
102
  const map = { n: '\n', t: '\t', r: '\r', '0': '\0', "'": "'", '"': '"', '\\': '\\', b: '\b', Z: '\x1a', a: '\a' };
98
- str += map[esc] !== undefined ? map[esc] : esc;
103
+ // MySQL 语义:\% 与 \_ 保留反斜杠(供 LIKE 匹配字面量通配符),其余未知转义忽略反斜杠
104
+ if (map[esc] !== undefined) str += map[esc];
105
+ else if (esc === '%' || esc === '_') str += '\\' + esc;
106
+ else str += esc;
99
107
  j += 2;
100
108
  } else if (sql[j] === quote) {
101
109
  break;
@@ -150,6 +158,12 @@ function tokenize(sql) {
150
158
  continue;
151
159
  }
152
160
 
161
+ // PG 的 JSON 包含操作符 @> 尚未实现。
162
+ // 若不拦截,'@' 会被 sysvar 分支静默吞掉、'>' 退化成普通大于号,产生错误结果。
163
+ if (c === '@' && sql[i + 1] === '>') {
164
+ throw new Error("JSON 包含操作符 '@>' 暂不支持");
165
+ }
166
+
153
167
  if (c === '@') {
154
168
  let j = i;
155
169
  while (j < n && sql[j] === '@') j++;
@@ -177,14 +191,29 @@ function tokenize(sql) {
177
191
  continue;
178
192
  }
179
193
 
194
+ const three = sql.slice(i, i + 3);
195
+ if (sql.slice(i, i + 2) === '<@') {
196
+ throw new Error("JSON 包含操作符 '<@' 暂不支持");
197
+ }
198
+ if (three === '->>' || three === '<=>') {
199
+ tokens.push(new SQLToken('op', three, i));
200
+ i += 3;
201
+ continue;
202
+ }
203
+
180
204
  const two = sql.slice(i, i + 2);
181
- if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==') {
205
+ if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
206
+ two === '<<' || two === '>>' || two === '->' || two === '::' ||
207
+ two === '&&' || two === '||') {
182
208
  tokens.push(new SQLToken('op', two, i));
183
209
  i += 2;
184
210
  continue;
185
211
  }
186
212
 
187
- if ('=<>+-*/(),.;'.includes(c)) {
213
+ // 注意:% & | ^ ~ 这些运算符必须在词法层产出 token。
214
+ // 之前 parseTerm() 里写了 '%' 分支但字符集里没有 '%',
215
+ // 导致 SELECT 10 % 3 直接报 "Unexpected character '%'",那段是死代码。
216
+ if ('=<>+-*/(),.;%&|^~'.includes(c)) {
188
217
  tokens.push(new SQLToken('op', c, i));
189
218
  i++;
190
219
  continue;
@@ -197,11 +226,23 @@ function tokenize(sql) {
197
226
  return tokens;
198
227
  }
199
228
 
229
+ // 聚合函数集合:这些必须走 aggregate 节点,否则会被当成逐行标量函数(返回 null 且不折叠行)
230
+ const AGG_FUNCS = new Set(['SUM', 'AVG', 'MIN', 'MAX', 'COUNT',
231
+ 'GROUP_CONCAT', 'STDDEV', 'STDDEV_POP', 'STDDEV_SAMP',
232
+ 'VARIANCE', 'VAR_POP', 'VAR_SAMP', 'FIRST', 'LAST']);
233
+
234
+ // 这些词会跟在表名/子查询之后,不能被误当成表别名。
235
+ // 新增任何"跟在 FROM 之后"的子句时,都要往这里加词。
236
+ const NOT_ALIAS_WORDS = ['JOIN', 'LEFT', 'RIGHT', 'FULL', 'WHERE', 'GROUP', 'ORDER', 'LIMIT',
237
+ 'ON', 'HAVING', 'UNION', 'INNER', 'CROSS', 'OUTER', 'INTERSECT', 'EXCEPT',
238
+ 'FETCH', 'RETURNING', 'WINDOW', 'OFFSET', 'FOR'];
239
+
200
240
  class Parser {
201
241
  constructor(tokens) {
202
242
  this.tokens = tokens;
203
243
  this.pos = 0;
204
244
  this.paramCount = 0; // 顺序占位符 ? 的序号(从 0 开始)
245
+ this.subquerySeq = 0; // FROM 子查询无别名时自动生成 __subN
205
246
  }
206
247
 
207
248
  peek(offset = 0) { return this.tokens[this.pos + offset]; }
@@ -271,11 +312,13 @@ class Parser {
271
312
  if (this.isKeyword('TABLE', 1)) return this.parseCreateTable();
272
313
  if (this.isKeyword('DATABASE', 1)) return this.parseCreateDatabase();
273
314
  if (this.isWord('VIEW', 1) || this.isWord('OR', 1)) return this.parseCreateView();
315
+ if (this.isKeyword('INDEX', 1) || this.isKeyword('UNIQUE', 1)) return this.parseCreateIndex();
274
316
  throw new Error('Unsupported CREATE statement');
275
317
  case 'DROP':
276
318
  if (this.isKeyword('TABLE', 1)) return this.parseDropTable();
277
319
  if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
278
320
  if (this.isWord('VIEW', 1)) return this.parseDropView();
321
+ if (this.isKeyword('INDEX', 1)) return this.parseDropIndex();
279
322
  throw new Error('Unsupported DROP statement');
280
323
  case 'INSERT': return this.parseInsert();
281
324
  case 'REPLACE': return this.parseReplace();
@@ -351,6 +394,45 @@ class Parser {
351
394
  return { type: 'createView', name, columns, select, orReplace };
352
395
  }
353
396
 
397
+ /**
398
+ * CREATE [UNIQUE] INDEX name ON table (col [, col]...)
399
+ * 归一成 { type:'createIndex', name, table, columns, unique }
400
+ */
401
+ parseCreateIndex() {
402
+ this.expectKeyword('CREATE');
403
+ let unique = false;
404
+ if (this.isKeyword('UNIQUE')) { this.next(); unique = true; }
405
+ if (!this.isKeyword('INDEX')) throw new Error(`Expected INDEX, got '${this.peek().value}'`);
406
+ this.next();
407
+ const name = this.parseTableName();
408
+ if (!this.isKeyword('ON')) throw new Error(`Expected ON in CREATE INDEX, got '${this.peek().value}'`);
409
+ this.next();
410
+ const table = this.parseTableName();
411
+ this.expect('op', '(');
412
+ const columns = [];
413
+ for (;;) {
414
+ columns.push(this.parseColumnRef());
415
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
416
+ break;
417
+ }
418
+ this.expect('op', ')');
419
+ this.optionalTailSemicolon();
420
+ return { type: 'createIndex', name, table, columns, unique };
421
+ }
422
+
423
+ /** DROP INDEX name ON table */
424
+ parseDropIndex() {
425
+ this.expectKeyword('DROP');
426
+ this.expectKeyword('INDEX');
427
+ let ifExists = false;
428
+ if (this.isKeyword('IF')) { this.expectKeyword('IF'); this.expectKeyword('EXISTS'); ifExists = true; }
429
+ const name = this.parseTableName();
430
+ let table = null;
431
+ if (this.isKeyword('ON')) { this.next(); table = this.parseTableName(); }
432
+ this.optionalTailSemicolon();
433
+ return { type: 'dropIndex', name, table, ifExists };
434
+ }
435
+
354
436
  parseDropView() {
355
437
  this.expectKeyword('DROP');
356
438
  if (!this.isWord('VIEW')) throw new Error(`Expected VIEW, got '${this.peek().value}'`);
@@ -413,6 +495,9 @@ class Parser {
413
495
  hasPk = true;
414
496
  } else {
415
497
  this.expectKeyword('UNIQUE');
498
+ // UNIQUE [KEY|INDEX] [name] (cols...)
499
+ if (this.isKeyword('KEY') || this.isKeyword('INDEX')) this.next();
500
+ if (this.peek().type === 'ident') this.next();
416
501
  this.expect('op', '(');
417
502
  const uCols = [this.parseTableName()];
418
503
  while (this.peek().type === 'op' && this.peek().value === ',') {
@@ -426,7 +511,12 @@ class Parser {
426
511
  }
427
512
  } else if (t.type === 'keyword' && t.value === 'CONSTRAINT') {
428
513
  this.expectKeyword('CONSTRAINT');
429
- this.next();
514
+ if (this.peek().type === 'ident') this.next(); // 约束名
515
+ this._parseTableConstraint(schema);
516
+ } else if (this.isWord('FOREIGN') || this.isWord('CHECK') ||
517
+ t.type === 'keyword' && (t.value === 'KEY' || t.value === 'INDEX')) {
518
+ // 表级约束:FOREIGN KEY ... REFERENCES ... / CHECK (...) / KEY name (cols)
519
+ this._parseTableConstraint(schema);
430
520
  } else if (t.type === 'eof' || (t.type === 'op' && t.value === ')')) {
431
521
  break;
432
522
  } else {
@@ -458,19 +548,27 @@ class Parser {
458
548
  parseColumnDef() {
459
549
  const def = {};
460
550
  const typeTok = this.next();
461
- if (typeTok.type !== 'keyword') throw new Error(`Expected column type, got '${typeTok.value}'`);
462
- const type = typeTok.value.toLowerCase();
551
+ const typeRaw = String(typeTok.value);
552
+ // 这些类型不在 KEYWORDS 里(避免影响同名标识符),按上下文识别
553
+ const IDENT_TYPES = /^(BIG)?SERIAL$|^JSONB$|^TIMESTAMPTZ$|^BYTEA$|^UUID$/i;
554
+ const isIdentType = IDENT_TYPES.test(typeRaw);
555
+ if (typeTok.type !== 'keyword' && !isIdentType) throw new Error(`Expected column type, got '${typeTok.value}'`);
556
+ const type = typeRaw.toLowerCase();
463
557
  const typeMap = {
464
558
  integer: 'integer', int: 'integer', bigint: 'integer', tinyint: 'integer', smallint: 'integer',
465
559
  string: 'string', text: 'string', varchar: 'string', char: 'string',
466
560
  float: 'number', double: 'number', real: 'number', numeric: 'number', decimal: 'number',
467
561
  boolean: 'boolean', bool: 'boolean',
468
- date: 'date', datetime: 'datetime', timestamp: 'timestamp',
469
- any: 'any', object: 'object', json: 'object', array: 'array'
562
+ date: 'date', datetime: 'datetime', timestamp: 'timestamp', timestamptz: 'timestamp',
563
+ any: 'any', object: 'object', json: 'object', jsonb: 'object', array: 'array',
564
+ bytea: 'string', uuid: 'string',
565
+ // PG 自增序列类型:等价于整型 + 自增 + NOT NULL
566
+ serial: 'integer', bigserial: 'integer'
470
567
  };
471
568
  const mapped = typeMap[type];
472
569
  if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
473
570
  def.type = mapped;
571
+ if (/^(BIG)?SERIAL$/i.test(typeRaw)) { def.autoIncrement = true; def.required = true; }
474
572
 
475
573
  // 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
476
574
  if (this.peek().type === 'op' && this.peek().value === '(') {
@@ -517,7 +615,29 @@ class Parser {
517
615
  case 'NULL':
518
616
  this.next(); break;
519
617
  case 'DEFAULT':
520
- this.next(); def.default = this.parseValue(); break;
618
+ this.next();
619
+ // 默认值可以是字面量,也可以是函数/关键字形式(CURRENT_TIMESTAMP / NOW() 等)。
620
+ // 函数形式存成字符串 + defaultExpr 标记,与 JS 侧 `default: 'CURRENT_TIMESTAMP'` 约定一致。
621
+ if (this.peek().type === 'ident' ||
622
+ (this.peek().type === 'keyword' && !['NULL', 'TRUE', 'FALSE'].includes(this.peek().value))) {
623
+ const fnTok = this.next();
624
+ let name = String(fnTok.value);
625
+ if (this.peek().type === 'op' && this.peek().value === '(') {
626
+ this.next();
627
+ this.expect('op', ')');
628
+ name += '()';
629
+ }
630
+ def.default = name;
631
+ def.defaultExpr = true;
632
+ } else {
633
+ def.default = this.parseValue();
634
+ // 允许字面量后带 :: 类型转换:DEFAULT '{}'::jsonb
635
+ if (this.peek().type === 'op' && this.peek().value === '::') {
636
+ this.next();
637
+ if (this.peek().type === 'keyword' || this.peek().type === 'ident') this.next();
638
+ }
639
+ }
640
+ break;
521
641
  case 'COLLATE':
522
642
  this.next(); if (this.peek().type !== 'op' && this.peek().type !== 'eof') this.next(); break;
523
643
  case 'CHARACTER':
@@ -553,6 +673,68 @@ class Parser {
553
673
  return stmt;
554
674
  }
555
675
 
676
+ /** 跳过一对括号(含嵌套),用于表级约束里的列清单 */
677
+ _skipParens() {
678
+ this.expect('op', '(');
679
+ let depth = 1;
680
+ while (depth > 0 && this.peek().type !== 'eof') {
681
+ const tk = this.next();
682
+ if (tk.type === 'op' && tk.value === '(') depth++;
683
+ else if (tk.type === 'op' && tk.value === ')') depth--;
684
+ }
685
+ }
686
+
687
+ /**
688
+ * 解析表级约束(FOREIGN KEY / CHECK / KEY / INDEX / PRIMARY KEY / UNIQUE)。
689
+ * 目前只做语法层面消费并在可能时补充列属性,不做外键实际校验。
690
+ */
691
+ _parseTableConstraint(schema) {
692
+ if (this.isWord('FOREIGN')) {
693
+ this.next();
694
+ if (!this.isKeyword('KEY')) throw new Error(`Expected KEY after FOREIGN, got '${this.peek().value}'`);
695
+ this.next();
696
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
697
+ if (this.isWord('REFERENCES')) {
698
+ this.next();
699
+ this.parseTableName();
700
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
701
+ }
702
+ // ON DELETE / ON UPDATE <action>
703
+ while (this.isKeyword('ON')) {
704
+ this.next();
705
+ this.next(); // DELETE | UPDATE
706
+ const act = String(this.next().value).toUpperCase();
707
+ if (act === 'SET' || act === 'NO') this.next(); // SET NULL / NO ACTION
708
+ }
709
+ return;
710
+ }
711
+ if (this.isWord('CHECK')) {
712
+ this.next();
713
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
714
+ return;
715
+ }
716
+ if (this.isKeyword('PRIMARY')) {
717
+ this.expectKeyword('PRIMARY');
718
+ if (this.isKeyword('KEY')) this.next();
719
+ if (this.peek().type === 'op' && this.peek().value === '(') {
720
+ this.next();
721
+ const cols = [this.parseTableName()];
722
+ while (this.peek().type === 'op' && this.peek().value === ',') { this.next(); cols.push(this.parseTableName()); }
723
+ this.expect('op', ')');
724
+ for (const c of cols) if (schema[c]) schema[c].primaryKey = true;
725
+ }
726
+ return;
727
+ }
728
+ if (this.isKeyword('UNIQUE')) {
729
+ this.next();
730
+ }
731
+ if (this.isKeyword('KEY') || this.isKeyword('INDEX')) {
732
+ this.next();
733
+ if (this.peek().type === 'ident') this.next(); // 索引名
734
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
735
+ }
736
+ }
737
+
556
738
  parseInsertBody() {
557
739
  const name = this.parseTableName();
558
740
  let columns = null;
@@ -628,7 +810,7 @@ class Parser {
628
810
  for (;;) {
629
811
  const col = this.parseColumnRef();
630
812
  this.expect('op', '=');
631
- const val = this.parseValue();
813
+ const val = this.parseConflictValue();
632
814
  sets.push([col, val]);
633
815
  if (this.peek().value === ',') { this.next(); continue; }
634
816
  break;
@@ -683,6 +865,13 @@ class Parser {
683
865
  parseValue() {
684
866
  const t = this.next();
685
867
  if (t.type === 'number' || t.type === 'string') return t.value;
868
+ // MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
869
+ if (t.type === 'keyword' && t.value === 'VALUES' && this.peek().type === 'op' && this.peek().value === '(') {
870
+ this.next();
871
+ const col = this.parseColumnRef();
872
+ this.expect('op', ')');
873
+ return { _valuesOf: col };
874
+ }
686
875
  if (t.type === 'keyword' && t.value === 'NULL') return null;
687
876
  if (t.type === 'keyword' && t.value === 'TRUE') return true;
688
877
  if (t.type === 'keyword' && t.value === 'FALSE') return false;
@@ -701,6 +890,21 @@ class Parser {
701
890
  throw new Error(`Expected value, got '${t.value}'`);
702
891
  }
703
892
 
893
+ /**
894
+ * ON CONFLICT DO UPDATE 赋值右侧:
895
+ * EXCLUDED.col —— PG:引用"本次本应插入的值"
896
+ * VALUES(col) —— MySQL 同义(在 parseValue 中处理)
897
+ * 其他字面量
898
+ */
899
+ parseConflictValue() {
900
+ if (this.isWord('EXCLUDED') && this.peek(1).type === 'op' && this.peek(1).value === '.') {
901
+ this.next(); this.next();
902
+ const col = this.parseColumnRef();
903
+ return { _excludedOf: col };
904
+ }
905
+ return this.parseValue();
906
+ }
907
+
704
908
  /**
705
909
  * WITH [RECURSIVE] name [(col, ...)] AS (SELECT ...), ... <statement>
706
910
  * CTE 在语法层解析为 { type:'with', ctes:[{name, columns, select}], statement }。
@@ -833,24 +1037,37 @@ class Parser {
833
1037
  if (t.type === 'keyword' && t.value === 'COUNT') {
834
1038
  this.next();
835
1039
  this.expect('op', '(');
1040
+ let aggDistinct = false;
1041
+ if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct = true; }
836
1042
  let col = null;
837
1043
  if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
838
1044
  else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
1045
+ // COUNT(DISTINCT a, b) —— 多列去重(仅对 COUNT 有意义,其它聚合按元组处理)
1046
+ if (aggDistinct && this.peek().type === 'op' && this.peek().value === ',') {
1047
+ const cols = [col];
1048
+ while (this.peek().type === 'op' && this.peek().value === ',') {
1049
+ this.next();
1050
+ cols.push(this.parseScalar());
1051
+ }
1052
+ col = { type: 'tuple', items: cols };
1053
+ }
839
1054
  this.expect('op', ')');
840
- aggregate = { type: 'COUNT', column: col };
1055
+ aggregate = { type: 'COUNT', column: col, distinct: aggDistinct };
841
1056
  aggregate = this.parseOptionalOver(aggregate);
842
1057
  aggregate.alias = this.parseOptionalAlias();
843
- columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
1058
+ columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct });
844
1059
  } else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
845
1060
  this.next();
846
1061
  const fn = t.value;
847
1062
  this.expect('op', '(');
1063
+ let aggDistinct2 = false;
1064
+ if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct2 = true; }
848
1065
  const col = this.parseScalar();
849
1066
  this.expect('op', ')');
850
- aggregate = { type: fn, column: col };
1067
+ aggregate = { type: fn, column: col, distinct: aggDistinct2 };
851
1068
  aggregate = this.parseOptionalOver(aggregate);
852
1069
  aggregate.alias = this.parseOptionalAlias();
853
- columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
1070
+ columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct2 });
854
1071
  } else if (t.type === 'op' && t.value === '*') {
855
1072
  this.next();
856
1073
  columns.push({ expr: '*' });
@@ -867,7 +1084,7 @@ class Parser {
867
1084
  if (expr.type === 'star') {
868
1085
  columns.push({ expr: '*' });
869
1086
  } else if (expr.type === 'aggregate') {
870
- columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr });
1087
+ columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr, distinct: expr.distinct, separator: expr.separator, orderBy: expr.orderBy });
871
1088
  } else if (expr.type === 'column') {
872
1089
  columns.push({ expr: expr.name, scalar: expr, alias });
873
1090
  } else {
@@ -886,6 +1103,7 @@ class Parser {
886
1103
  let where = null;
887
1104
  if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
888
1105
  let groupBy = null;
1106
+ let rollup = false;
889
1107
  if (this.isKeyword('GROUP')) {
890
1108
  this.expectKeyword('GROUP'); this.expectKeyword('BY');
891
1109
  groupBy = [];
@@ -894,6 +1112,12 @@ class Parser {
894
1112
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
895
1113
  break;
896
1114
  }
1115
+ // WITH ROLLUP 必须紧跟 GROUP BY 列表,之后才是 HAVING / ORDER BY / LIMIT
1116
+ // (标准顺序:GROUP BY <list> [WITH ROLLUP] [HAVING] [ORDER BY] [LIMIT])
1117
+ if (this.isKeyword('WITH') && this.isWord('ROLLUP', 1)) {
1118
+ this.next(); this.next();
1119
+ rollup = true;
1120
+ }
897
1121
  }
898
1122
  let having = null;
899
1123
  if (this.isKeyword('HAVING')) { this.next(); having = this.parseExpr(); }
@@ -961,13 +1185,6 @@ class Parser {
961
1185
  else if (this.isKeyword('DISTINCT')) this.next();
962
1186
  except = { all, select: this.parseSelect() };
963
1187
  }
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
1188
  // FETCH FIRST n ROWS ONLY(SQL 标准的 LIMIT 写法)
972
1189
  if (this.isWord('FETCH')) {
973
1190
  this.next();
@@ -1029,19 +1246,18 @@ class Parser {
1029
1246
  this.expect('op', ')');
1030
1247
  let alias = null;
1031
1248
  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');
1249
+ else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) { alias = this.next().value; }
1250
+ // 标准 SQL 要求 FROM 子查询必须带别名,这里放宽为自动生成,
1251
+ // 避免 README 里 SELECT ... FROM (SELECT ...) 这类示例直接报错
1252
+ if (!alias) alias = '__sub' + (++this.subquerySeq);
1034
1253
  return { subquery: sub, alias };
1035
1254
  }
1036
1255
  const table = this.parseTableName();
1037
1256
  let alias = null;
1038
1257
  // 这里排除的是"未进关键字表、但会跟在表名之后"的子句起始词,
1039
1258
  // 否则 `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
1259
  if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
1044
- else if (this.peek().type === 'ident' && !NOT_ALIAS.some(w => this.isWord(w))) {
1260
+ else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) {
1045
1261
  alias = this.next().value;
1046
1262
  }
1047
1263
  return { table, alias };
@@ -1096,9 +1312,28 @@ class Parser {
1096
1312
  const table = this.parseTableName();
1097
1313
  let where = null;
1098
1314
  if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
1315
+ let orderBy = null, limit = null;
1316
+ if (this.isKeyword('ORDER')) {
1317
+ this.next(); this.expectKeyword('BY');
1318
+ orderBy = [];
1319
+ for (;;) {
1320
+ const col = this.parseColumnRef();
1321
+ let dir = 'asc';
1322
+ if (this.isKeyword('ASC')) this.next();
1323
+ else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
1324
+ orderBy.push({ column: col, dir });
1325
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1326
+ break;
1327
+ }
1328
+ }
1329
+ if (this.isKeyword('LIMIT')) {
1330
+ this.next();
1331
+ limit = this.parseValue();
1332
+ if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
1333
+ }
1099
1334
  const returning = this.parseOptionalReturning();
1100
1335
  this.optionalTailSemicolon();
1101
- return { type: 'delete', table, where, returning };
1336
+ return { type: 'delete', table, where, orderBy, limit, returning };
1102
1337
  }
1103
1338
 
1104
1339
  parseDropTable() {
@@ -1386,19 +1621,93 @@ class Parser {
1386
1621
  return this.parseOr();
1387
1622
  }
1388
1623
 
1624
+ /**
1625
+ * 函数实参用的表达式:与 parseExpr 相同,但叶子允许是"裸操作数"。
1626
+ * 例如 IF(1 > 0, 'y', 'n') 的第一个参数是完整比较表达式,
1627
+ * 而 IF(age, 'y', 'n') 这种裸列名也必须能解析。
1628
+ */
1629
+ parseArgExpr() {
1630
+ this.bareExprDepth = (this.bareExprDepth || 0) + 1;
1631
+ try {
1632
+ return this.parseOr();
1633
+ } finally {
1634
+ this.bareExprDepth--;
1635
+ }
1636
+ }
1637
+
1638
+ // 位运算优先级(低 → 高):| < ^ < & < << >> < 加减 < 乘除模
1639
+ parseBitwise() {
1640
+ let node = this.parseBitXor();
1641
+ for (;;) {
1642
+ const t = this.peek();
1643
+ if (t.type === 'op' && t.value === '|') { this.next(); node = { type: 'arith', op: '|', left: node, right: this.parseBitXor() }; continue; }
1644
+ break;
1645
+ }
1646
+ return node;
1647
+ }
1648
+
1649
+ parseBitXor() {
1650
+ let node = this.parseBitAnd();
1651
+ for (;;) {
1652
+ const t = this.peek();
1653
+ if (t.type === 'op' && t.value === '^') { this.next(); node = { type: 'arith', op: '^', left: node, right: this.parseBitAnd() }; continue; }
1654
+ break;
1655
+ }
1656
+ return node;
1657
+ }
1658
+
1659
+ parseBitAnd() {
1660
+ let node = this.parseShift();
1661
+ for (;;) {
1662
+ const t = this.peek();
1663
+ if (t.type === 'op' && t.value === '&') { this.next(); node = { type: 'arith', op: '&', left: node, right: this.parseShift() }; continue; }
1664
+ break;
1665
+ }
1666
+ return node;
1667
+ }
1668
+
1669
+ parseShift() {
1670
+ let node = this.parseAdditive();
1671
+ for (;;) {
1672
+ const t = this.peek();
1673
+ if (t.type === 'op' && (t.value === '<<' || t.value === '>>')) {
1674
+ this.next();
1675
+ node = { type: 'arith', op: t.value, left: node, right: this.parseAdditive() };
1676
+ continue;
1677
+ }
1678
+ break;
1679
+ }
1680
+ return node;
1681
+ }
1682
+
1389
1683
  parseOr() {
1684
+ let left = this.parseXor();
1685
+ for (;;) {
1686
+ const isOr = this.isKeyword('OR') || (this.peek().type === 'op' && this.peek().value === '||');
1687
+ if (!isOr) break;
1688
+ this.next();
1689
+ const right = this.parseXor();
1690
+ left = { type: 'or', left, right };
1691
+ }
1692
+ return left;
1693
+ }
1694
+
1695
+ // 逻辑异或 XOR(MySQL 优先级:OR < XOR < AND)
1696
+ parseXor() {
1390
1697
  let left = this.parseAnd();
1391
- while (this.isKeyword('OR')) {
1698
+ while (this.isWord('XOR')) {
1392
1699
  this.next();
1393
1700
  const right = this.parseAnd();
1394
- left = { type: 'or', left, right };
1701
+ left = { type: 'xor', left, right };
1395
1702
  }
1396
1703
  return left;
1397
1704
  }
1398
1705
 
1399
1706
  parseAnd() {
1400
1707
  let left = this.parseNot();
1401
- while (this.isKeyword('AND')) {
1708
+ for (;;) {
1709
+ const isAnd = this.isKeyword('AND') || (this.peek().type === 'op' && this.peek().value === '&&');
1710
+ if (!isAnd) break;
1402
1711
  this.next();
1403
1712
  const right = this.parseNot();
1404
1713
  left = { type: 'and', left, right };
@@ -1434,9 +1743,69 @@ class Parser {
1434
1743
  return this.parseComparison();
1435
1744
  }
1436
1745
 
1746
+ /**
1747
+ * 解析聚合调用:fn ( [DISTINCT] expr [, expr*] [ORDER BY ...] [SEPARATOR 'x'] )
1748
+ * 以及 COUNT(*) 这类无参形式。
1749
+ */
1750
+ parseAggregateCall(fn) {
1751
+ this.expect('op', '(');
1752
+ const distinct = this.isKeyword('DISTINCT');
1753
+ if (distinct) this.next();
1754
+ let column = null;
1755
+ let separator = null;
1756
+ let innerOrder = null;
1757
+ if (this.peek().type === 'op' && this.peek().value === '*') {
1758
+ this.next();
1759
+ } else if (!(this.peek().type === 'op' && this.peek().value === ')')) {
1760
+ column = this.parseScalar();
1761
+ // COUNT(DISTINCT a, b):多列去重
1762
+ if (distinct && this.peek().type === 'op' && this.peek().value === ',') {
1763
+ const cols = [column];
1764
+ while (this.peek().type === 'op' && this.peek().value === ',') {
1765
+ this.next();
1766
+ cols.push(this.parseScalar());
1767
+ }
1768
+ column = { type: 'tuple', items: cols };
1769
+ } else if (this.peek().type === 'op' && this.peek().value === ',') {
1770
+ // GROUP_CONCAT(expr, 'sep')
1771
+ this.next();
1772
+ separator = this.parseValue();
1773
+ }
1774
+ // GROUP_CONCAT(expr ORDER BY col [DESC])
1775
+ if (this.isKeyword('ORDER')) {
1776
+ this.next();
1777
+ if (!this.isKeyword('BY')) throw new Error('Expected BY after ORDER in aggregate, got ' + "'" + this.peek().value + "'");
1778
+ this.next();
1779
+ innerOrder = [];
1780
+ for (;;) {
1781
+ const oCol = this.parseColumnRef();
1782
+ let oDir = 'asc';
1783
+ if (this.isKeyword('ASC')) this.next();
1784
+ else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
1785
+ innerOrder.push({ column: oCol, dir: oDir });
1786
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1787
+ break;
1788
+ }
1789
+ }
1790
+ // GROUP_CONCAT(expr SEPARATOR 'sep')
1791
+ if (this.isWord('SEPARATOR')) {
1792
+ this.next();
1793
+ separator = this.parseValue();
1794
+ }
1795
+ }
1796
+ this.expect('op', ')');
1797
+ return this.parseOptionalOver({ type: 'aggregate', fn, column, distinct, separator, orderBy: innerOrder });
1798
+ }
1799
+
1437
1800
  parseOperand() {
1438
1801
  const t = this.next();
1439
1802
  if (t.type === 'ident') {
1803
+ // 聚合函数名(GROUP_CONCAT / STDDEV / FIRST...)走 aggregate 节点,
1804
+ // 否则会被当作逐行标量函数:既返回 null,又不会折叠分组
1805
+ if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
1806
+ this.peek().type === 'op' && this.peek().value === '(') {
1807
+ return this.parseAggregateCall(String(t.value).toUpperCase());
1808
+ }
1440
1809
  // 支持 alias.column 引用
1441
1810
  if (this.peek().type === 'op' && this.peek().value === '.') {
1442
1811
  this.next();
@@ -1469,6 +1838,60 @@ class Parser {
1469
1838
  this.expect('op', ')');
1470
1839
  return { type: 'cast', expr, dataType };
1471
1840
  }
1841
+ // EXTRACT(YEAR FROM expr) —— 标准 SQL 的 FROM 形式
1842
+ if (String(t.value).toUpperCase() === 'EXTRACT' && this.peek().type === 'op' && this.peek().value === '(') {
1843
+ this.next();
1844
+ const unitTok = this.next();
1845
+ const unit = String(unitTok.value).toUpperCase();
1846
+ if (!this.isWord('FROM')) throw new Error(`Expected FROM in EXTRACT, got '${this.peek().value}'`);
1847
+ this.next();
1848
+ const arg = this.parseOperand();
1849
+ this.expect('op', ')');
1850
+ return { type: 'func', name: 'EXTRACT', args: [{ type: 'value', value: unit }, arg] };
1851
+ }
1852
+ // SUBSTR(s FROM n [FOR m]) / TRIM([LEADING|TRAILING|BOTH] ['x'] FROM s)
1853
+ if (['SUBSTR', 'SUBSTRING'].includes(String(t.value).toUpperCase()) && this.peek().type === 'op' && this.peek().value === '(') {
1854
+ const name = String(t.value).toUpperCase();
1855
+ this.next();
1856
+ const s = this.parseOperand();
1857
+ let fromV = null, forV = null;
1858
+ if (this.isWord('FROM')) {
1859
+ this.next();
1860
+ fromV = this.parseOperand();
1861
+ if (this.isWord('FOR')) { this.next(); forV = this.parseOperand(); }
1862
+ } else if (this.peek().type === 'op' && this.peek().value === ',') {
1863
+ this.next();
1864
+ fromV = this.parseOperand();
1865
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); forV = this.parseOperand(); }
1866
+ }
1867
+ this.expect('op', ')');
1868
+ const args = [s];
1869
+ if (fromV) args.push(fromV);
1870
+ if (forV) args.push(forV);
1871
+ return { type: 'func', name, args };
1872
+ }
1873
+ if (String(t.value).toUpperCase() === 'TRIM' && this.peek().type === 'op' && this.peek().value === '(') {
1874
+ this.next();
1875
+ let spec = null;
1876
+ if (this.isWord('LEADING') || this.isWord('TRAILING') || this.isWord('BOTH')) {
1877
+ spec = String(this.next().value).toUpperCase();
1878
+ }
1879
+ const first = this.parseOperand();
1880
+ let target = null;
1881
+ if (this.isWord('FROM')) {
1882
+ this.next();
1883
+ target = this.parseOperand();
1884
+ } else if (this.peek().type === 'op' && this.peek().value === ',') {
1885
+ this.next();
1886
+ target = this.parseOperand();
1887
+ }
1888
+ this.expect('op', ')');
1889
+ // 归一成 TRIM(target[, chars[, spec]]),与 applyScalarFunction 的既有签名兼容
1890
+ if (target === null) return { type: 'func', name: 'TRIM', args: [first] };
1891
+ const args = [target, first];
1892
+ if (spec) args.push({ type: 'value', value: spec });
1893
+ return { type: 'func', name: 'TRIM', args };
1894
+ }
1472
1895
  // 函数调用: VERSION() / CONCAT(a, b) / NOW() ...
1473
1896
  if (this.peek().type === 'op' && this.peek().value === '(') {
1474
1897
  const name = t.value;
@@ -1477,7 +1900,42 @@ class Parser {
1477
1900
  if (!(this.peek().type === 'op' && this.peek().value === ')')) {
1478
1901
  for (;;) {
1479
1902
  if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
1480
- else args.push(this.parseOperand());
1903
+ // INTERVAL n UNIT(DATE_ADD/DATE_SUB 用)
1904
+ else if (this.isWord('INTERVAL')) {
1905
+ this.next();
1906
+ const nTok = this.peek();
1907
+ let nVal;
1908
+ if (nTok.type === 'op' && (nTok.value === '-' || nTok.value === '+')) {
1909
+ this.next();
1910
+ const numTok = this.next();
1911
+ nVal = (nTok.value === '-' ? -1 : 1) * Number(numTok.value);
1912
+ } else {
1913
+ this.next();
1914
+ nVal = Number(nTok.value);
1915
+ }
1916
+ const unitTok = this.next();
1917
+ args.push({ type: 'interval', value: nVal, unit: String(unitTok.value).toUpperCase() });
1918
+ }
1919
+ // 实参允许完整的比较/布尔表达式,例如 IF(status = 'paid', amount, 0)
1920
+ else args.push(this.parseArgExpr());
1921
+ // GROUP_CONCAT(name ORDER BY age):聚合函数内部的排序
1922
+ if (this.isKeyword('ORDER')) {
1923
+ this.next();
1924
+ if (!this.isKeyword('BY')) throw new Error(`Expected BY after ORDER in aggregate, got '${this.peek().value}'`);
1925
+ this.next();
1926
+ const innerOrder = [];
1927
+ for (;;) {
1928
+ const oCol = this.parseColumnRef();
1929
+ let oDir = 'asc';
1930
+ if (this.isKeyword('ASC')) this.next();
1931
+ else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
1932
+ innerOrder.push({ column: oCol, dir: oDir });
1933
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1934
+ break;
1935
+ }
1936
+ this.expect('op', ')');
1937
+ return this.parseOptionalOver({ type: 'func', name, args, orderBy: innerOrder });
1938
+ }
1481
1939
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1482
1940
  break;
1483
1941
  }
@@ -1502,7 +1960,7 @@ class Parser {
1502
1960
  if (!(this.peek().type === 'op' && this.peek().value === ')')) {
1503
1961
  for (;;) {
1504
1962
  if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
1505
- else args.push(this.parseOperand());
1963
+ else args.push(this.parseArgExpr());
1506
1964
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1507
1965
  break;
1508
1966
  }
@@ -1510,14 +1968,13 @@ class Parser {
1510
1968
  this.expect('op', ')');
1511
1969
  return this.parseOptionalOver({ type: 'func', name, args });
1512
1970
  }
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 });
1971
+ if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
1972
+ this.peek().type === 'op' && this.peek().value === '(') {
1973
+ return this.parseAggregateCall(String(t.value).toUpperCase());
1974
+ }
1975
+ // 一元按位取反 ~x
1976
+ if (t.type === 'op' && t.value === '~') {
1977
+ return { type: 'bitnot', expr: this.parseOperand() };
1521
1978
  }
1522
1979
  if (t.type === 'op' && t.value === '(' && this.peek().type === 'keyword' && this.peek().value === 'SELECT') {
1523
1980
  const sub = this.parseSelect();
@@ -1532,8 +1989,8 @@ class Parser {
1532
1989
  throw new Error(`Expected value or column, got '${t.value}'`);
1533
1990
  }
1534
1991
 
1535
- // 算术表达式: + - * / % (左结合, * / 优先)
1536
- parseScalar() {
1992
+ // 加减(左结合)
1993
+ parseAdditive() {
1537
1994
  let node = this.parseTerm();
1538
1995
  for (;;) {
1539
1996
  const t = this.peek();
@@ -1545,6 +2002,16 @@ class Parser {
1545
2002
  }
1546
2003
  break;
1547
2004
  }
2005
+ return node;
2006
+ }
2007
+
2008
+ /**
2009
+ * 标量表达式:算术 + 位运算,外加 IN / IS 后缀。
2010
+ * 位运算要走到这里,否则 SELECT 列表里的 `SELECT 1 << 8` 无法解析
2011
+ * (parseComparison 只在 WHERE 等布尔上下文被调用)。
2012
+ */
2013
+ parseScalar() {
2014
+ let node = this.parseBitwise();
1548
2015
  // 后缀:expr IN (...)、expr IS [NOT] TRUE/FALSE/NULL(标量上下文,如 SELECT 1 IN (...))
1549
2016
  for (;;) {
1550
2017
  const t = this.peek();
@@ -1582,15 +2049,58 @@ class Parser {
1582
2049
  }
1583
2050
 
1584
2051
  parseTerm() {
1585
- let node = this.parseOperand();
2052
+ let node = this.parsePostfix(this.parseOperand());
1586
2053
  for (;;) {
1587
2054
  const t = this.peek();
1588
2055
  if (t.type === 'op' && (t.value === '*' || t.value === '/' || t.value === '%')) {
1589
2056
  this.next();
1590
- const right = this.parseOperand();
2057
+ const right = this.parsePostfix(this.parseOperand());
1591
2058
  node = { type: 'arith', op: t.value, left: node, right };
1592
2059
  continue;
1593
2060
  }
2061
+ // MySQL 整数除法:7 DIV 2 → 3
2062
+ if (this.isWord('DIV')) {
2063
+ this.next();
2064
+ const right = this.parsePostfix(this.parseOperand());
2065
+ node = { type: 'arith', op: 'DIV', left: node, right };
2066
+ continue;
2067
+ }
2068
+ break;
2069
+ }
2070
+ return node;
2071
+ }
2072
+
2073
+ /**
2074
+ * 后缀运算符(绑定最紧):PG 风格 `::` 类型转换、`->` / `->>` JSON 取值。
2075
+ * 例:'5'::INT、meta->>'name'、meta->'addr'->>'city'。
2076
+ */
2077
+ parsePostfix(node) {
2078
+ for (;;) {
2079
+ const t = this.peek();
2080
+ if (t.type === 'op' && t.value === '::') {
2081
+ this.next();
2082
+ const typeTok = this.next();
2083
+ let dataType = String(typeTok.value).toUpperCase();
2084
+ if (this.peek().type === 'op' && this.peek().value === '(') {
2085
+ this.next();
2086
+ let depth = 1, spec = '';
2087
+ while (depth > 0 && this.peek().type !== 'eof') {
2088
+ const tk = this.next();
2089
+ if (tk.type === 'op' && tk.value === '(') depth++;
2090
+ else if (tk.type === 'op' && tk.value === ')') { depth--; if (depth === 0) break; }
2091
+ spec += tk.value;
2092
+ }
2093
+ dataType += '(' + spec + ')';
2094
+ }
2095
+ node = { type: 'cast', expr: node, dataType };
2096
+ continue;
2097
+ }
2098
+ if (t.type === 'op' && (t.value === '->' || t.value === '->>')) {
2099
+ this.next();
2100
+ const key = this.parseValue();
2101
+ node = { type: 'jsonAccess', operand: node, key, asText: t.value === '->>' };
2102
+ continue;
2103
+ }
1594
2104
  break;
1595
2105
  }
1596
2106
  return node;
@@ -1623,7 +2133,7 @@ class Parser {
1623
2133
  }
1624
2134
 
1625
2135
  parseComparison() {
1626
- const left = this.parseOperand();
2136
+ const left = this.parseBitwise();
1627
2137
  const t = this.peek();
1628
2138
  let not = false;
1629
2139
  if (t.type === 'keyword' && t.value === 'NOT') {
@@ -1686,7 +2196,10 @@ class Parser {
1686
2196
  const isILike = !(t2.type === 'keyword' && t2.value === 'LIKE');
1687
2197
  this.next();
1688
2198
  const pattern = this.parseValue();
1689
- return { type: 'like', operand: left, pattern, not, ci: isILike };
2199
+ // 可选 ESCAPE 子句:LIKE '...' ESCAPE '!'
2200
+ let escape = null;
2201
+ if (this.isWord('ESCAPE')) { this.next(); escape = this.parseValue(); }
2202
+ return { type: 'like', operand: left, pattern, not, ci: isILike, escape };
1690
2203
  }
1691
2204
 
1692
2205
  // MySQL 中 RLIKE 是 REGEXP 的同义词
@@ -1696,7 +2209,7 @@ class Parser {
1696
2209
  return { type: 'regexp', operand: left, pattern, not };
1697
2210
  }
1698
2211
 
1699
- if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
2212
+ if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(t2.value)) {
1700
2213
  this.next();
1701
2214
  // = ANY (SELECT ...) / > ALL (SELECT ...)
1702
2215
  const anyAll = this.isWord('ANY') || this.isWord('ALL') || this.isWord('SOME');
@@ -1707,10 +2220,13 @@ class Parser {
1707
2220
  this.expect('op', ')');
1708
2221
  return { type: 'quantified', op: t2.value === '<>' ? '!=' : t2.value, operand: left, select: sub, quantifier, not };
1709
2222
  }
1710
- const right = this.parseOperand();
2223
+ const right = this.parseBitwise();
1711
2224
  return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
1712
2225
  }
1713
2226
 
2227
+ // 函数实参场景允许裸操作数(IF(age, ...) / SUM(IF(...)) 等)
2228
+ if (this.bareExprDepth > 0) return left;
2229
+
1714
2230
  throw new Error(`Expected comparison operator, got '${t2.value}'`);
1715
2231
  }
1716
2232
  }
@@ -1807,16 +2323,34 @@ function resolveOperand(operand, row, ctx) {
1807
2323
  case '*': return l * r;
1808
2324
  case '/': return r === 0 ? null : l / r;
1809
2325
  case '%': return r === 0 ? null : l % r;
2326
+ // MySQL 整数除法 DIV(结果向零取整)
2327
+ case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
2328
+ // 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
2329
+ case '&': return (Number(l) | 0) & (Number(r) | 0);
2330
+ case '|': return (Number(l) | 0) | (Number(r) | 0);
2331
+ case '^': return (Number(l) | 0) ^ (Number(r) | 0);
2332
+ case '<<': return (Number(l) | 0) << (Number(r) | 0);
2333
+ case '>>': return (Number(l) | 0) >> (Number(r) | 0);
1810
2334
  }
1811
2335
  return null;
1812
2336
  }
2337
+ case 'bitnot':
2338
+ return ~(Number(resolveOperand(operand.expr, row, ctx)) | 0);
1813
2339
  case 'func':
1814
2340
  return applyScalarFunction(operand, row, ctx);
1815
2341
  case 'case':
1816
2342
  return evaluateCaseVal(operand, row, ctx);
2343
+ // 布尔类节点出现在"取值"位置时(如 IF(status = 'paid', ...)、SUM(IF(a > b,1,0))),
2344
+ // 求值成 true/false 供上层按真值判断
2345
+ case 'compare':
2346
+ case 'and':
2347
+ case 'or':
2348
+ case 'xor':
2349
+ case 'not':
1817
2350
  case 'in':
1818
2351
  case 'isNull':
1819
2352
  case 'isTruth':
2353
+ case 'exists':
1820
2354
  case 'regexp':
1821
2355
  case 'like':
1822
2356
  case 'between':
@@ -1836,6 +2370,30 @@ function resolveOperand(operand, row, ctx) {
1836
2370
  const v = resolveOperand(operand.expr, row, ctx);
1837
2371
  return applyCast(v, operand.dataType);
1838
2372
  }
2373
+ case 'jsonAccess': {
2374
+ let base = resolveOperand(operand.operand, row, ctx);
2375
+ if (base === null || base === undefined) return null;
2376
+ // JSON 列可能以字符串形式存储,先解析
2377
+ if (typeof base === 'string') {
2378
+ try { base = JSON.parse(base); } catch (e) { return null; }
2379
+ }
2380
+ const key = operand.key;
2381
+ let v;
2382
+ if (Array.isArray(base)) {
2383
+ const idx = Number(key);
2384
+ v = Number.isInteger(idx) ? (idx < 0 ? base[base.length + idx] : base[idx]) : undefined;
2385
+ } else if (base && typeof base === 'object') {
2386
+ v = base[key];
2387
+ } else {
2388
+ return null;
2389
+ }
2390
+ if (v === undefined) return null;
2391
+ if (operand.asText) {
2392
+ if (v === null) return null;
2393
+ return typeof v === 'object' ? JSON.stringify(v) : String(v);
2394
+ }
2395
+ return v;
2396
+ }
1839
2397
  case 'quantified': {
1840
2398
  // = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
1841
2399
  const v = resolveOperand(operand.operand, row, ctx);
@@ -1856,9 +2414,30 @@ function resolveOperand(operand, row, ctx) {
1856
2414
  }
1857
2415
  }
1858
2416
 
2417
+ /** 解析 SQL 里的日期文本('YYYY-MM-DD' / 'YYYY-MM-DD HH:MM:SS' / Date) */
2418
+ function parseSqlDate(v) {
2419
+ if (v === null || v === undefined) return null;
2420
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
2421
+ const s = String(v).trim().replace(' ', 'T');
2422
+ const d = new Date(s.length <= 10 ? s + 'T00:00:00' : s);
2423
+ return Number.isNaN(d.getTime()) ? null : d;
2424
+ }
2425
+
2426
+ function pad2(n) { return String(n).padStart(2, '0'); }
2427
+
2428
+ /** 按输入形态回写日期:纯日期返回 YYYY-MM-DD,带时间返回 YYYY-MM-DD HH:MM:SS */
2429
+ function fmtSqlDate(dateOnly, d) {
2430
+ const ymd = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
2431
+ return dateOnly ? ymd : ymd + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
2432
+ }
2433
+
1859
2434
  function applyScalarFunction(fnNode, row, ctx) {
1860
2435
  const name = (fnNode.name || '').toUpperCase();
1861
- const args = (fnNode.args || []).map(a => resolveOperand(a, row, ctx));
2436
+ const rawArgs = fnNode.args || [];
2437
+ // INTERVAL n UNIT 是语法结构而非普通值,原样传给 DATE_ADD / DATE_SUB
2438
+ const args = rawArgs.map(a => (a && a.type === 'interval')
2439
+ ? { __interval: true, value: a.value, unit: a.unit }
2440
+ : resolveOperand(a, row, ctx));
1862
2441
  const session = ctx && ctx.session;
1863
2442
  if (ctx && ctx.functions && Object.prototype.hasOwnProperty.call(ctx.functions, name)) {
1864
2443
  return ctx.functions[name].apply(null, args);
@@ -1889,7 +2468,65 @@ function applyScalarFunction(fnNode, row, ctx) {
1889
2468
  case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
1890
2469
  case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
1891
2470
  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();
2471
+ // TRIM(s) / TRIM([LEADING|TRAILING|BOTH] chars FROM s) / TRIM(s, chars)
2472
+ case 'TRIM': {
2473
+ if (args[0] == null) return null;
2474
+ const s = String(args[0]);
2475
+ if (args.length === 1) return s.trim();
2476
+ const chars = args[1] == null ? '' : String(args[1]);
2477
+ if (!chars) return s.trim();
2478
+ const spec = args[2] ? String(args[2]).toUpperCase() : 'BOTH';
2479
+ const esc = chars.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2480
+ const head = new RegExp('^[' + esc + ']+');
2481
+ const tail = new RegExp('[' + esc + ']+$');
2482
+ if (spec === 'LEADING') return s.replace(head, '');
2483
+ if (spec === 'TRAILING') return s.replace(tail, '');
2484
+ return s.replace(head, '').replace(tail, '');
2485
+ }
2486
+ // EXTRACT(YEAR FROM d) —— 解析层会把 unit 作为第一个参数传入
2487
+ case 'EXTRACT': {
2488
+ const unit = String(args[0] == null ? '' : args[0]).toUpperCase();
2489
+ const d = parseSqlDate(args[1]);
2490
+ if (!d) return null;
2491
+ switch (unit) {
2492
+ case 'YEAR': return d.getFullYear();
2493
+ case 'MONTH': return d.getMonth() + 1;
2494
+ case 'DAY': return d.getDate();
2495
+ case 'HOUR': return d.getHours();
2496
+ case 'MINUTE': return d.getMinutes();
2497
+ case 'SECOND': return d.getSeconds();
2498
+ case 'DOW': return d.getDay(); // 0 = 周日
2499
+ case 'DAYOFWEEK': return d.getDay() + 1; // 1 = 周日(MySQL 语义)
2500
+ case 'WEEK': {
2501
+ const start = new Date(d.getFullYear(), 0, 1);
2502
+ return Math.floor((d - start) / 604800000) + 1;
2503
+ }
2504
+ default: return null;
2505
+ }
2506
+ }
2507
+ case 'DATE_ADD': case 'ADDDATE':
2508
+ case 'DATE_SUB': case 'SUBDATE': {
2509
+ const sign = (name === 'DATE_SUB' || name === 'SUBDATE') ? -1 : 1;
2510
+ const base = parseSqlDate(args[0]);
2511
+ if (!base) return null;
2512
+ const iv = args[1];
2513
+ if (!iv || !iv.__interval) return fmtSqlDate(String(args[0]).length <= 10, base);
2514
+ const isDateOnly = String(args[0]).trim().length <= 10;
2515
+ const d = new Date(base.getTime());
2516
+ const n = sign * Number(iv.value || 0);
2517
+ switch (String(iv.unit || '').toUpperCase()) {
2518
+ case 'DAY': case 'DAYS': d.setDate(d.getDate() + n); break;
2519
+ case 'WEEK': case 'WEEKS': d.setDate(d.getDate() + n * 7); break;
2520
+ case 'MONTH': case 'MONTHS': d.setMonth(d.getMonth() + n); break;
2521
+ case 'QUARTER': d.setMonth(d.getMonth() + n * 3); break;
2522
+ case 'YEAR': case 'YEARS': d.setFullYear(d.getFullYear() + n); break;
2523
+ case 'HOUR': case 'HOURS': d.setHours(d.getHours() + n); break;
2524
+ case 'MINUTE': case 'MINUTES': d.setMinutes(d.getMinutes() + n); break;
2525
+ case 'SECOND': case 'SECONDS': d.setSeconds(d.getSeconds() + n); break;
2526
+ default: return null;
2527
+ }
2528
+ return fmtSqlDate(isDateOnly, d);
2529
+ }
1893
2530
  case 'LTRIM': return args[0] == null ? null : String(args[0]).replace(/^\s+/, '');
1894
2531
  case 'RTRIM': return args[0] == null ? null : String(args[0]).replace(/\s+$/, '');
1895
2532
  case 'ABS': return args[0] == null ? null : Math.abs(args[0]);
@@ -1975,13 +2612,25 @@ function seedRand(seed) {
1975
2612
  }
1976
2613
 
1977
2614
  // ci 默认 true:保持 LIKE 既有的大小写不敏感行为不变;ILIKE 同为不敏感。
1978
- function likeMatch(value, pattern, ci = true) {
2615
+ // escape:可选转义符(ESCAPE 子句),默认反斜杠;转义符后跟 % _ 或转义符本身按字面量匹配。
2616
+ function likeMatch(value, pattern, ci = true, escape = '\\') {
1979
2617
  if (typeof value !== 'string') return false;
1980
- const regex = pattern
1981
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
1982
- .replace(/%/g, '.*')
1983
- .replace(/_/g, '.');
1984
- return new RegExp('^' + regex + '$', ci ? 'i' : '').test(value);
2618
+ const p = String(pattern);
2619
+ const e = (escape === null || escape === undefined || escape === '') ? '\\' : String(escape);
2620
+ const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2621
+ let out = '';
2622
+ for (let i = 0; i < p.length; i++) {
2623
+ const ch = p[i];
2624
+ if (ch === e && i + 1 < p.length && (p[i + 1] === '%' || p[i + 1] === '_' || p[i + 1] === e)) {
2625
+ out += reEsc(p[i + 1]);
2626
+ i++;
2627
+ continue;
2628
+ }
2629
+ if (ch === '%') out += '.*';
2630
+ else if (ch === '_') out += '.';
2631
+ else out += reEsc(ch);
2632
+ }
2633
+ return new RegExp('^' + out + '$', ci ? 'i' : '').test(value);
1985
2634
  }
1986
2635
 
1987
2636
  function extractEqualPushdown(expr, schema) {
@@ -2033,10 +2682,18 @@ function evaluateExpr(expr, row, ctx) {
2033
2682
  switch (expr.type) {
2034
2683
  case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
2035
2684
  case 'or': return evaluateExpr(expr.left, row, ctx) || evaluateExpr(expr.right, row, ctx);
2685
+ case 'xor': return Boolean(evaluateExpr(expr.left, row, ctx)) !== Boolean(evaluateExpr(expr.right, row, ctx));
2036
2686
  case 'not': return !evaluateExpr(expr.expr, row, ctx);
2037
2687
  case 'compare': {
2038
2688
  const l = resolveOperand(expr.left, row, ctx);
2039
2689
  const r = resolveOperand(expr.right, row, ctx);
2690
+ // MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
2691
+ if (expr.op === '<=>') {
2692
+ const ln = l === null || l === undefined;
2693
+ const rn = r === null || r === undefined;
2694
+ if (ln || rn) return ln && rn;
2695
+ return l === r || String(l) === String(r);
2696
+ }
2040
2697
  // SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
2041
2698
  if (l === null || r === null) return false;
2042
2699
  // 布尔与数值互通(MySQL: 1 = TRUE、0 = FALSE)
@@ -2059,7 +2716,7 @@ function evaluateExpr(expr, row, ctx) {
2059
2716
  }
2060
2717
  case 'like': {
2061
2718
  const v = resolveOperand(expr.operand, row, ctx);
2062
- const r = likeMatch(v, expr.pattern, !!expr.ci);
2719
+ const r = likeMatch(v, expr.pattern, !!expr.ci, expr.escape);
2063
2720
  return expr.not ? !r : r;
2064
2721
  }
2065
2722
  case 'quantified':
@@ -2154,16 +2811,22 @@ function windowColumnValue(c, r) {
2154
2811
 
2155
2812
  function scalarColumnValue(c, r, ctx) {
2156
2813
  if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
2814
+ // 聚合列优先:GROUP_CONCAT / STDDEV 等同时带 scalar 与 aggregate
2815
+ if (c.aggregate) {
2816
+ return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column,
2817
+ c.distinct, c.separator !== undefined ? c.separator : (c.scalar && c.scalar.separator),
2818
+ c.orderBy || (c.scalar && c.scalar.orderBy));
2819
+ }
2157
2820
  if (c.scalar) {
2158
2821
  const s = c.scalar;
2159
2822
  if (s && s.type === 'func' && ctx && ctx.ctxAggregates && Object.prototype.hasOwnProperty.call(ctx.ctxAggregates, String(s.name).toUpperCase())) {
2160
2823
  const fn = String(s.name).toUpperCase();
2161
2824
  const col = s.args && s.args[0];
2162
- return ctx._aggValue(ctx.group, fn, col);
2825
+ return ctx._aggValue(ctx.group, fn, col, c.aggregate ? c.distinct : (c.scalar && c.scalar.distinct));
2163
2826
  }
2164
2827
  return resolveOperand(c.scalar, r, ctx);
2165
2828
  }
2166
- if (c.aggregate) return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column);
2829
+
2167
2830
  if (c.literal !== undefined) return c.literal;
2168
2831
  if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r, ctx);
2169
2832
  if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
@@ -2186,7 +2849,10 @@ function buildCreateTableSql(name, schema) {
2186
2849
  const seg = ['`' + col + '`', sqlTypeName(def.type)];
2187
2850
  if (def.autoIncrement) seg.push('AUTO_INCREMENT');
2188
2851
  if (def.nullable === false) seg.push('NOT NULL');
2189
- if (def.default !== undefined) seg.push('DEFAULT ' + (typeof def.default === 'string' ? "'" + def.default + "'" : def.default));
2852
+ if (def.default !== undefined) {
2853
+ // defaultExpr(CURRENT_TIMESTAMP 等)不加引号,普通字符串默认值才加
2854
+ seg.push('DEFAULT ' + (typeof def.default === 'string' && !def.defaultExpr ? "'" + def.default + "'" : def.default));
2855
+ }
2190
2856
  return seg.join(' ');
2191
2857
  });
2192
2858
  const pks = Object.keys(schema).filter(k => schema[k].primaryKey);
@@ -2353,7 +3019,23 @@ class SQLExecutor {
2353
3019
  if (pkCols.length > 0) {
2354
3020
  const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
2355
3021
  const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
2356
- const explicit = dataRows.filter(hasExplicitPk);
3022
+ // 冲突判定的额外列组:
3023
+ // ON CONFLICT (col) DO UPDATE → 指定列(PG)
3024
+ // ON DUPLICATE KEY UPDATE → 各唯一列(MySQL:任一唯一键冲突都触发)
3025
+ // 此前只认主键,导致这两者在唯一列冲突时会插入重复行而不是更新。
3026
+ const ocTarget = (statement.onConflict && Array.isArray(statement.onConflict.target))
3027
+ ? statement.onConflict.target.map(c => (c && c.name !== undefined ? c.name : c)).filter(Boolean)
3028
+ : null;
3029
+ const extraIdx = [];
3030
+ if (ocTarget && ocTarget.length) extraIdx.push(ocTarget);
3031
+ else if (statement.onDuplicate && schema) {
3032
+ for (const [c, def] of Object.entries(schema)) {
3033
+ if (def && def.unique && !def.primaryKey) extraIdx.push([c]);
3034
+ }
3035
+ }
3036
+ const hasAllOf = (cols) => (row) => cols.every(c => row[c] !== undefined && row[c] !== null);
3037
+
3038
+ const explicit = dataRows.filter((row) => hasExplicitPk(row) || extraIdx.some((cols) => hasAllOf(cols)(row)));
2357
3039
  if (explicit.length > 0) {
2358
3040
  const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
2359
3041
  const pkMap = new Map();
@@ -2361,15 +3043,31 @@ class SQLExecutor {
2361
3043
  const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
2362
3044
  if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
2363
3045
  }
3046
+ const pkValsOf = (row) => pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
3047
+ const extraMaps = extraIdx.map((cols) => {
3048
+ const m = new Map();
3049
+ const pick = hasAllOf(cols);
3050
+ const key = (row) => cols.map(c => String(row[c])).join('|');
3051
+ for (const row of all) {
3052
+ if (!pick(row)) continue;
3053
+ m.set(key(row), pkValsOf(row));
3054
+ }
3055
+ return { pick, key, m };
3056
+ });
2364
3057
  const conflicts = [];
2365
3058
  const fresh = [];
2366
3059
  for (const d of dataRows) {
2367
- const key = keyOf(d);
2368
- if (hasExplicitPk(d) && pkMap.has(key)) {
2369
- conflicts.push({ d, existingId: pkMap.get(key) });
2370
- } else {
2371
- fresh.push(d);
3060
+ let existingId = null;
3061
+ if (hasExplicitPk(d) && pkMap.has(keyOf(d))) existingId = pkMap.get(keyOf(d));
3062
+ if (!existingId) {
3063
+ for (const { pick, key, m } of extraMaps) {
3064
+ if (!pick(d)) continue;
3065
+ const k = key(d);
3066
+ if (m.has(k)) { existingId = m.get(k); break; }
3067
+ }
2372
3068
  }
3069
+ if (existingId) conflicts.push({ d, existingId });
3070
+ else fresh.push(d);
2373
3071
  }
2374
3072
  const conflictAction = this._conflictAction(statement);
2375
3073
  const conflictSets = this._conflictSets(statement);
@@ -2396,7 +3094,16 @@ class SQLExecutor {
2396
3094
  for (const { d, existingId } of conflicts) {
2397
3095
  const data = {};
2398
3096
  if (conflictSets) {
2399
- for (const [col, val] of conflictSets) data[col] = val;
3097
+ for (const [col, val] of conflictSets) {
3098
+ // VALUES(col) 引用本次本应插入的值;EXCLUDED.col 为 PG 同义写法
3099
+ if (val && typeof val === 'object' && val._valuesOf !== undefined) {
3100
+ data[col] = d[val._valuesOf] !== undefined ? d[val._valuesOf] : null;
3101
+ } else if (val && typeof val === 'object' && val._excludedOf !== undefined) {
3102
+ data[col] = d[val._excludedOf] !== undefined ? d[val._excludedOf] : null;
3103
+ } else {
3104
+ data[col] = val;
3105
+ }
3106
+ }
2400
3107
  } else {
2401
3108
  for (const k of Object.keys(d)) if (!pkCols.includes(k)) data[k] = d[k];
2402
3109
  }
@@ -2525,6 +3232,52 @@ class SQLExecutor {
2525
3232
  delete this.engine._views[key];
2526
3233
  return { ok: true, type: 'dropView', message: `View '${statement.name}' dropped`, affectedRows: 0, warnings: [] };
2527
3234
  }
3235
+ case 'createIndex': {
3236
+ const tName = statement.table;
3237
+ if (typeof this.engine.hasTable === 'function' && !this.engine.hasTable(tName)) {
3238
+ throw new Error(`Table '${tName}' does not exist`);
3239
+ }
3240
+ let handled = false;
3241
+ if (typeof this.engine.createIndex === 'function') {
3242
+ await this.engine.createIndex(tName, statement.columns, { unique: statement.unique, name: statement.name });
3243
+ handled = true;
3244
+ } else if (typeof this.engine._ensureTable === 'function') {
3245
+ const tbl = this.engine._ensureTable(tName) || (this.engine._tables && this.engine._tables[tName]);
3246
+ if (tbl && typeof tbl.createIndex === 'function') {
3247
+ // Table.createIndex 目前是单字段接口,复合索引按逐列建
3248
+ for (const col of statement.columns) tbl.createIndex(col);
3249
+ handled = true;
3250
+ }
3251
+ }
3252
+ if (!handled) {
3253
+ throw new Error(
3254
+ `CREATE INDEX is not supported by this engine (no createIndex/_ensureTable on the engine object)`
3255
+ );
3256
+ }
3257
+ return {
3258
+ ok: true, type: 'createIndex', table: tName, name: statement.name,
3259
+ affectedRows: 0, warnings: [],
3260
+ message: `Index '${statement.name}' created on ${tName}(${statement.columns.join(', ')})`,
3261
+ };
3262
+ }
3263
+ case 'dropIndex': {
3264
+ const tName2 = statement.table;
3265
+ let handled = false;
3266
+ if (typeof this.engine.dropIndex === 'function') {
3267
+ await this.engine.dropIndex(tName2, statement.name);
3268
+ handled = true;
3269
+ } else if (tName2 && typeof this.engine._ensureTable === 'function') {
3270
+ const tbl = this.engine._ensureTable(tName2) || (this.engine._tables && this.engine._tables[tName2]);
3271
+ if (tbl && typeof tbl.dropIndex === 'function') { tbl.dropIndex(statement.name); handled = true; }
3272
+ }
3273
+ if (!handled) {
3274
+ if (statement.ifExists) {
3275
+ return { ok: true, type: 'dropIndex', affectedRows: 0, skipped: true, warnings: [], message: `Index '${statement.name}' did not exist` };
3276
+ }
3277
+ throw new Error(`DROP INDEX is not supported by this engine (no dropIndex on the engine/table object)`);
3278
+ }
3279
+ return { ok: true, type: 'dropIndex', table: tName2, name: statement.name, affectedRows: 0, warnings: [], message: `Index '${statement.name}' dropped` };
3280
+ }
2528
3281
  case 'savepoint': {
2529
3282
  // 引擎侧没有保存点/快照能力。这里只登记名字,绝不假装能回滚。
2530
3283
  if (!this.engine._savepoints) this.engine._savepoints = new Set();
@@ -2582,11 +3335,27 @@ class SQLExecutor {
2582
3335
  const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
2583
3336
  const ids = [];
2584
3337
  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
- }
3338
+ let candidates = all.filter(row => !statement.where || evaluateExpr(statement.where, row, this.ctx));
3339
+ // DELETE ... ORDER BY ... LIMIT n
3340
+ if (statement.orderBy) {
3341
+ const cmp = (a, b) => {
3342
+ for (const o of statement.orderBy) {
3343
+ const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
3344
+ const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
3345
+ if (av === bv) continue;
3346
+ if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
3347
+ if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
3348
+ const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
3349
+ if (d !== 0) return o.dir === 'asc' ? d : -d;
3350
+ }
3351
+ return 0;
3352
+ };
3353
+ candidates = candidates.slice().sort(cmp);
3354
+ }
3355
+ if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
3356
+ for (const row of candidates) {
3357
+ const id = this._rowPkId(row, pkCols);
3358
+ if (id !== undefined) { ids.push(id); removedRows.push(row); }
2590
3359
  }
2591
3360
  if (ids.length > 0) {
2592
3361
  if (this.engine.removeByIds) await this.engine.removeByIds(statement.table, ids);
@@ -3114,12 +3883,28 @@ class SQLExecutor {
3114
3883
  return out;
3115
3884
  }
3116
3885
 
3117
- _aggValue(rows, fn, column) {
3118
- if (fn === 'COUNT') return rows.length;
3886
+ _aggValue(rows, fn, column, distinct, separator, orderBy) {
3119
3887
  const op = typeof column === 'string' ? { type: 'column', name: column } : column;
3120
- const values = op && op.type === 'star'
3888
+ // COUNT(*) / COUNT():参数为 star 或缺失,直接按行数算
3889
+ const noColumn = !op || op.type === 'star';
3890
+ let values = noColumn
3121
3891
  ? rows.map(r => 1)
3122
- : rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
3892
+ : op && op.type === 'tuple'
3893
+ // COUNT(DISTINCT a, b):先把多列拼成元组再聚合
3894
+ ? rows.map(r => JSON.stringify(op.items.map(it => resolveOperand(it, r, this.ctx))))
3895
+ .filter(v => v !== null && v !== undefined)
3896
+ : rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
3897
+ // COUNT(DISTINCT x) / SUM(DISTINCT x):先按值去重再聚合
3898
+ if (distinct) {
3899
+ const seen = new Set();
3900
+ values = values.filter(v => {
3901
+ const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
3902
+ if (seen.has(k)) return false;
3903
+ seen.add(k);
3904
+ return true;
3905
+ });
3906
+ }
3907
+ if (fn === 'COUNT') return values.length;
3123
3908
  if (this.ctx && this.ctx.aggregates && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
3124
3909
  const agg = this.ctx.aggregates[fn];
3125
3910
  if (typeof agg === 'function') {
@@ -3135,6 +3920,54 @@ class SQLExecutor {
3135
3920
  if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
3136
3921
  if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
3137
3922
  if (fn === 'MAX') return values.length ? Math.max(...values.map(v => Number(v))) : null;
3923
+ if (fn === 'FIRST') return values.length ? values[0] : null;
3924
+ if (fn === 'LAST') return values.length ? values[values.length - 1] : null;
3925
+ if (fn === 'GROUP_CONCAT') {
3926
+ let src = rows;
3927
+ // GROUP_CONCAT(expr ORDER BY col [DESC]):先按内部排序键排好行,再取值拼接
3928
+ if (orderBy && orderBy.length) {
3929
+ const cmp = (a, b) => {
3930
+ for (const o of orderBy) {
3931
+ const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
3932
+ const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
3933
+ if (av === bv) continue;
3934
+ if (av === null || av === undefined) return o.dir === 'asc' ? 1 : -1;
3935
+ if (bv === null || bv === undefined) return o.dir === 'asc' ? -1 : 1;
3936
+ const d = (typeof av === 'number' && typeof bv === 'number')
3937
+ ? av - bv : String(av).localeCompare(String(bv));
3938
+ if (d !== 0) return o.dir === 'asc' ? d : -d;
3939
+ }
3940
+ return 0;
3941
+ };
3942
+ src = rows.slice().sort(cmp);
3943
+ }
3944
+ const parts = src.map(r => resolveOperand(op, r, this.ctx));
3945
+ let joined = parts.filter(v => v !== null && v !== undefined);
3946
+ if (distinct) {
3947
+ const seen = new Set();
3948
+ joined = joined.filter(v => {
3949
+ const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
3950
+ if (seen.has(k)) return false;
3951
+ seen.add(k);
3952
+ return true;
3953
+ });
3954
+ }
3955
+ if (joined.length === 0) return null;
3956
+ const sep = (separator !== undefined && separator !== null) ? String(separator) : ',';
3957
+ return joined.map(v => (v === null || v === undefined ? '' : String(v))).join(sep);
3958
+ }
3959
+ // 标准差 / 方差族:_POP 为总体、_SAMP(及无后缀 VAR/STDDEV)为样本
3960
+ if (fn === 'STDDEV' || fn === 'STDDEV_POP' || fn === 'STDDEV_SAMP' ||
3961
+ fn === 'VARIANCE' || fn === 'VAR_POP' || fn === 'VAR_SAMP') {
3962
+ const nums = values.map(v => Number(v)).filter(v => Number.isFinite(v));
3963
+ if (nums.length === 0) return null;
3964
+ const pop = (fn === 'STDDEV_POP' || fn === 'VAR_POP');
3965
+ const mean = nums.reduce((s, v) => s + v, 0) / nums.length;
3966
+ const denom = pop ? nums.length : nums.length - 1; // 样本方差用 n-1
3967
+ if (denom <= 0) return null;
3968
+ const variance = nums.reduce((s, v) => s + (v - mean) * (v - mean), 0) / denom;
3969
+ return fn.indexOf('STDDEV') === 0 ? Math.sqrt(variance) : variance;
3970
+ }
3138
3971
  return null;
3139
3972
  }
3140
3973
 
@@ -3142,7 +3975,7 @@ class SQLExecutor {
3142
3975
  if (!node || typeof node !== 'object') return;
3143
3976
  if (node.type === 'aggregate') {
3144
3977
  node.type = 'value';
3145
- node.value = this._aggValue(group, node.fn, node.column);
3978
+ node.value = this._aggValue(group, node.fn, node.column, node.distinct, node.separator, node.orderBy);
3146
3979
  delete node.fn;
3147
3980
  delete node.column;
3148
3981
  return;
@@ -3211,6 +4044,61 @@ class SQLExecutor {
3211
4044
  return out;
3212
4045
  }
3213
4046
 
4047
+ /**
4048
+ * 校验语句里引用的列是否真实存在。
4049
+ * 之前 `SELECT nosuchcol FROM t` 会静默返回 null 而不是报错 —— 调用方拿到的
4050
+ * 是"一列 null",往往被当成"数据为空",比直接报 no such column 更难排查。
4051
+ *
4052
+ * 只在能确定列名集合时校验(取样本行的键),空表不敢断言,宁可不报。
4053
+ * 不深入子查询节点:它们有自己的 FROM,列属于另一个作用域。
4054
+ */
4055
+ _validateColumnRefs(statement, rows) {
4056
+ if (!statement || !Array.isArray(rows) || rows.length === 0) return;
4057
+ const sample = rows[0];
4058
+ if (!sample || typeof sample !== 'object') return;
4059
+ const known = new Set();
4060
+ for (const k of Object.keys(sample)) known.add(String(k).toLowerCase());
4061
+ // HAVING / ORDER BY 允许引用 SELECT 的输出别名(如 HAVING cnt >= 1),
4062
+ // 这些名字不在表里,必须一并视为已知,否则会误报 no such column
4063
+ if (Array.isArray(statement.columns)) {
4064
+ for (const c of statement.columns) {
4065
+ if (c && typeof c.alias === 'string' && c.alias) known.add(c.alias.toLowerCase());
4066
+ }
4067
+ }
4068
+
4069
+ const walk = (node) => {
4070
+ if (!node || typeof node !== 'object') return;
4071
+ if (Array.isArray(node)) { for (const n of node) walk(n); return; }
4072
+ // 子查询/嵌套 SELECT 有自己的表,跳过
4073
+ if (node.type === 'select' || node.type === 'subquery') return;
4074
+ if (node.type === 'column' && typeof node.name === 'string') {
4075
+ const n = node.name.toLowerCase();
4076
+ const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
4077
+ if (!known.has(n) && !known.has(base)) {
4078
+ throw new Error(`no such column: ${node.name}`);
4079
+ }
4080
+ }
4081
+ for (const k of Object.keys(node)) {
4082
+ if (k === 'select' || k === 'subquery') continue;
4083
+ walk(node[k]);
4084
+ }
4085
+ };
4086
+
4087
+ walk(statement.columns);
4088
+ walk(statement.where);
4089
+ walk(statement.having);
4090
+ walk(statement.orderBy);
4091
+ if (Array.isArray(statement.groupBy)) {
4092
+ // groupBy 是列名字符串数组
4093
+ for (const g of statement.groupBy) {
4094
+ if (typeof g !== 'string') continue;
4095
+ const n = g.toLowerCase();
4096
+ const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
4097
+ if (!known.has(n) && !known.has(base)) throw new Error(`no such column: ${g}`);
4098
+ }
4099
+ }
4100
+ }
4101
+
3214
4102
  /** 收集子树里所有列名(含限定形式 alias.col) */
3215
4103
  _collectColumnNames(node, out = []) {
3216
4104
  if (!node || typeof node !== 'object') return out;
@@ -3446,7 +4334,7 @@ class SQLExecutor {
3446
4334
  case 'MAX': {
3447
4335
  const f = frameIdx(pos);
3448
4336
  const slice = ordered.slice(f.start, f.end + 1).map(i => rows[i]);
3449
- v = this._aggValue(slice, w.fn, argNode);
4337
+ v = this._aggValue(slice, w.fn, argNode, !!(w.spec && w.spec.distinct), w.spec && w.spec.separator);
3450
4338
  break;
3451
4339
  }
3452
4340
  default: v = null;
@@ -3639,6 +4527,7 @@ class SQLExecutor {
3639
4527
  }
3640
4528
 
3641
4529
  let rows = all;
4530
+ this._validateColumnRefs(statement, rows);
3642
4531
  if (statement.where) {
3643
4532
  // 相关子查询:必须逐行重新求值,否则外层引用拿不到当前行的值
3644
4533
  const outerAliases = this._outerAliases(statement);
@@ -3681,7 +4570,7 @@ class SQLExecutor {
3681
4570
  for (const c of statement.columns) {
3682
4571
  if (c.aggregate) {
3683
4572
  const group = r._group || [r];
3684
- const v = this._aggValue(group, c.aggregate || 'COUNT', c.column);
4573
+ const v = this._aggValue(group, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
3685
4574
  ctx['__agg_' + (c.alias || c.column || 'COUNT(*)')] = v;
3686
4575
  if (c.column) ctx[c.column] = v;
3687
4576
  if (c.alias) ctx[c.alias] = v;
@@ -3702,7 +4591,7 @@ class SQLExecutor {
3702
4591
  const key = JSON.stringify(statement.columns.map(c => {
3703
4592
  if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
3704
4593
  if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
3705
- if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column);
4594
+ if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
3706
4595
  return resolveOperand({ type: 'column', name: c.expr }, r, this.ctx);
3707
4596
  }));
3708
4597
  if (seen.has(key)) return false;
@@ -3717,7 +4606,7 @@ class SQLExecutor {
3717
4606
  if (!statement.groupBy && (statement.columns.some(c => c.aggregate && !c.window) || hasCustomAgg)) {
3718
4607
  const cols = statement.columns.map(c => scalarColumnName(c));
3719
4608
  const valueOf = (c) => {
3720
- if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column);
4609
+ if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
3721
4610
  if (c.scalar && c.scalar.type === 'func' && hasCustomAgg) {
3722
4611
  const fn = String(c.scalar.name).toUpperCase();
3723
4612
  if (Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
@@ -3986,6 +4875,44 @@ function applyParams(sql, values) {
3986
4875
  return out;
3987
4876
  }
3988
4877
 
4878
+ // 把执行器结果补齐为 README 承诺的结果信封
4879
+ // (columnTypes / rowCount / message / command / durationMs / warnings)
4880
+ function buildResultEnvelope(stmt, r, durationMs) {
4881
+ if (r === null || typeof r !== 'object' || Array.isArray(r)) return r;
4882
+ const out = { ...r };
4883
+ const t = stmt && stmt.type;
4884
+ if (out.command === undefined) out.command = String(t || '').toUpperCase();
4885
+ if (out.affectedRows === undefined) out.affectedRows = 0;
4886
+ if (out.rowCount === undefined) {
4887
+ out.rowCount = Array.isArray(out.rows) ? out.rows.length
4888
+ : (typeof out.affectedRows === 'number' ? out.affectedRows : 0);
4889
+ }
4890
+ if (out.warnings === undefined) out.warnings = [];
4891
+ if (out.durationMs === undefined) out.durationMs = durationMs;
4892
+ if (out.message === undefined) {
4893
+ out.message = t === 'select'
4894
+ ? `${out.rowCount} row${out.rowCount === 1 ? '' : 's'} selected`
4895
+ : `${out.affectedRows} row${out.affectedRows === 1 ? '' : 's'} affected`;
4896
+ }
4897
+ return out;
4898
+ }
4899
+
4900
+ // SELECT 结果的列类型:单表查询按 schema 推断;表达式/多表查询该列为 null
4901
+ async function inferColumnTypes(engine, stmt, r) {
4902
+ if (!r || !Array.isArray(r.columns)) return undefined;
4903
+ let schema = null;
4904
+ try {
4905
+ const t = stmt && stmt.type === 'select' && stmt.from &&
4906
+ Array.isArray(stmt.from.tables) && stmt.from.tables.length === 1
4907
+ ? stmt.from.tables[0].table : null;
4908
+ if (t && engine && typeof engine.getTableSchema === 'function') schema = await engine.getTableSchema(t);
4909
+ } catch (e) { schema = null; }
4910
+ return r.columns.map((name) => {
4911
+ const def = schema && schema[name];
4912
+ return def && def.type ? sqlTypeName(def.type).toUpperCase() : null;
4913
+ });
4914
+ }
4915
+
3989
4916
  async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
3990
4917
  // 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
3991
4918
  // 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
@@ -4035,7 +4962,14 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
4035
4962
  const results = [];
4036
4963
  for (const stmtSql of statements) {
4037
4964
  const stmt = parseSQL(stmtSql);
4038
- results.push(await executor.execute(stmt));
4965
+ const t0 = process.hrtime.bigint();
4966
+ let r = await executor.execute(stmt);
4967
+ const durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
4968
+ r = buildResultEnvelope(stmt, r, Math.round(durationMs * 1000) / 1000);
4969
+ if (r && r.columns && r.columnTypes === undefined) {
4970
+ r.columnTypes = await inferColumnTypes(engine, stmt, r);
4971
+ }
4972
+ results.push(r);
4039
4973
  }
4040
4974
  return results.length === 1 ? results[0] : results;
4041
4975
  }