jsql-neo 6.0.0 → 6.0.4

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
@@ -11,6 +11,10 @@ const DANGEROUS_SQL = [
11
11
  { re: /^SYSEXEC$|^SYS_EXEC$/, name: 'sys_exec' },
12
12
  ];
13
13
 
14
+ // SQL 文本体积上限(默认 64 MiB):防止超大、无分号的输入造成无界内存消耗
15
+ // (CWE-770)。可在 executeSQL 通过 opts.maxSqlLength 放宽/关闭(传 Infinity)。
16
+ const DEFAULT_MAX_SQL_LENGTH = 64 * 1024 * 1024;
17
+
14
18
  function findDangerousSQL(tokens) {
15
19
  for (let i = 0; i < tokens.length; i++) {
16
20
  const t = tokens[i];
@@ -79,6 +83,13 @@ function tokenize(sql) {
79
83
  while (i < n && sql[i] !== '\n') i++;
80
84
  continue;
81
85
  }
86
+ // PG 的 JSON 路径操作符 #> / #>>(必须在 '#' 行注释之前识别)
87
+ if (c === '#' && sql[i + 1] === '>') {
88
+ const op = sql[i + 2] === '>' ? '#>>' : '#>';
89
+ tokens.push(new SQLToken('op', op, i));
90
+ i += op.length;
91
+ continue;
92
+ }
82
93
  if (c === '#' || (c === '/' && sql[i + 1] === '*')) {
83
94
  if (c === '#') { while (i < n && sql[i] !== '\n') i++; continue; }
84
95
  i += 2;
@@ -95,7 +106,10 @@ function tokenize(sql) {
95
106
  if (sql[j] === '\\' && j + 1 < n) {
96
107
  const esc = sql[j + 1];
97
108
  const map = { n: '\n', t: '\t', r: '\r', '0': '\0', "'": "'", '"': '"', '\\': '\\', b: '\b', Z: '\x1a', a: '\a' };
98
- str += map[esc] !== undefined ? map[esc] : esc;
109
+ // MySQL 语义:\% 与 \_ 保留反斜杠(供 LIKE 匹配字面量通配符),其余未知转义忽略反斜杠
110
+ if (map[esc] !== undefined) str += map[esc];
111
+ else if (esc === '%' || esc === '_') str += '\\' + esc;
112
+ else str += esc;
99
113
  j += 2;
100
114
  } else if (sql[j] === quote) {
101
115
  break;
@@ -104,6 +118,7 @@ function tokenize(sql) {
104
118
  j++;
105
119
  }
106
120
  }
121
+ if (j >= n) throw new Error(`Unterminated ${quote === '"' ? 'double' : 'single'}-quoted string at position ${i}`);
107
122
  tokens.push(new SQLToken('string', str, i));
108
123
  i = j + 1;
109
124
  continue;
@@ -145,11 +160,19 @@ function tokenize(sql) {
145
160
  if (c === '`') {
146
161
  let j = i + 1;
147
162
  while (j < n && sql[j] !== '`') j++;
163
+ if (j >= n) throw new Error(`Unterminated backtick-quoted identifier at position ${i}`);
148
164
  tokens.push(new SQLToken('ident', sql.slice(i + 1, j), i));
149
165
  i = j + 1;
150
166
  continue;
151
167
  }
152
168
 
169
+ // PG 的 JSON 包含操作符 @>
170
+ if (c === '@' && sql[i + 1] === '>') {
171
+ tokens.push(new SQLToken('op', '@>', i));
172
+ i += 2;
173
+ continue;
174
+ }
175
+
153
176
  if (c === '@') {
154
177
  let j = i;
155
178
  while (j < n && sql[j] === '@') j++;
@@ -167,6 +190,12 @@ function tokenize(sql) {
167
190
  }
168
191
 
169
192
  // 参数占位符:? (顺序)、?N (编号)、?? (标识符占位)
193
+ // 注意:?| / ?& 是 PG 的 JSON 键存在操作符,必须优先识别
194
+ if (c === '?' && (sql[i + 1] === '|' || sql[i + 1] === '&')) {
195
+ tokens.push(new SQLToken('op', '?' + sql[i + 1], i));
196
+ i += 2;
197
+ continue;
198
+ }
170
199
  if (c === '?') {
171
200
  if (sql[i + 1] === '?') { tokens.push(new SQLToken('param', '??', i)); i += 2; continue; }
172
201
  let j = i + 1;
@@ -177,14 +206,31 @@ function tokenize(sql) {
177
206
  continue;
178
207
  }
179
208
 
209
+ const three = sql.slice(i, i + 3);
210
+ if (sql.slice(i, i + 2) === '<@') {
211
+ tokens.push(new SQLToken('op', '<@', i));
212
+ i += 2;
213
+ continue;
214
+ }
215
+ if (three === '->>' || three === '<=>' || three === '!~*') {
216
+ tokens.push(new SQLToken('op', three, i));
217
+ i += 3;
218
+ continue;
219
+ }
220
+
180
221
  const two = sql.slice(i, i + 2);
181
- if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==') {
222
+ if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
223
+ two === '<<' || two === '>>' || two === '->' || two === '::' ||
224
+ two === '&&' || two === '||' || two === '~*' || two === '!~') {
182
225
  tokens.push(new SQLToken('op', two, i));
183
226
  i += 2;
184
227
  continue;
185
228
  }
186
229
 
187
- if ('=<>+-*/(),.;'.includes(c)) {
230
+ // 注意:% & | ^ ~ 这些运算符必须在词法层产出 token。
231
+ // 之前 parseTerm() 里写了 '%' 分支但字符集里没有 '%',
232
+ // 导致 SELECT 10 % 3 直接报 "Unexpected character '%'",那段是死代码。
233
+ if ('=<>+-*/(),.;%&|^~'.includes(c)) {
188
234
  tokens.push(new SQLToken('op', c, i));
189
235
  i++;
190
236
  continue;
@@ -197,11 +243,23 @@ function tokenize(sql) {
197
243
  return tokens;
198
244
  }
199
245
 
246
+ // 聚合函数集合:这些必须走 aggregate 节点,否则会被当成逐行标量函数(返回 null 且不折叠行)
247
+ const AGG_FUNCS = new Set(['SUM', 'AVG', 'MIN', 'MAX', 'COUNT',
248
+ 'GROUP_CONCAT', 'STDDEV', 'STDDEV_POP', 'STDDEV_SAMP',
249
+ 'VARIANCE', 'VAR_POP', 'VAR_SAMP', 'FIRST', 'LAST']);
250
+
251
+ // 这些词会跟在表名/子查询之后,不能被误当成表别名。
252
+ // 新增任何"跟在 FROM 之后"的子句时,都要往这里加词。
253
+ const NOT_ALIAS_WORDS = ['JOIN', 'LEFT', 'RIGHT', 'FULL', 'WHERE', 'GROUP', 'ORDER', 'LIMIT',
254
+ 'ON', 'HAVING', 'UNION', 'INNER', 'CROSS', 'OUTER', 'INTERSECT', 'EXCEPT',
255
+ 'FETCH', 'RETURNING', 'WINDOW', 'OFFSET', 'FOR'];
256
+
200
257
  class Parser {
201
258
  constructor(tokens) {
202
259
  this.tokens = tokens;
203
260
  this.pos = 0;
204
261
  this.paramCount = 0; // 顺序占位符 ? 的序号(从 0 开始)
262
+ this.subquerySeq = 0; // FROM 子查询无别名时自动生成 __subN
205
263
  }
206
264
 
207
265
  peek(offset = 0) { return this.tokens[this.pos + offset]; }
@@ -271,11 +329,13 @@ class Parser {
271
329
  if (this.isKeyword('TABLE', 1)) return this.parseCreateTable();
272
330
  if (this.isKeyword('DATABASE', 1)) return this.parseCreateDatabase();
273
331
  if (this.isWord('VIEW', 1) || this.isWord('OR', 1)) return this.parseCreateView();
332
+ if (this.isKeyword('INDEX', 1) || this.isKeyword('UNIQUE', 1)) return this.parseCreateIndex();
274
333
  throw new Error('Unsupported CREATE statement');
275
334
  case 'DROP':
276
335
  if (this.isKeyword('TABLE', 1)) return this.parseDropTable();
277
336
  if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
278
337
  if (this.isWord('VIEW', 1)) return this.parseDropView();
338
+ if (this.isKeyword('INDEX', 1)) return this.parseDropIndex();
279
339
  throw new Error('Unsupported DROP statement');
280
340
  case 'INSERT': return this.parseInsert();
281
341
  case 'REPLACE': return this.parseReplace();
@@ -351,6 +411,45 @@ class Parser {
351
411
  return { type: 'createView', name, columns, select, orReplace };
352
412
  }
353
413
 
414
+ /**
415
+ * CREATE [UNIQUE] INDEX name ON table (col [, col]...)
416
+ * 归一成 { type:'createIndex', name, table, columns, unique }
417
+ */
418
+ parseCreateIndex() {
419
+ this.expectKeyword('CREATE');
420
+ let unique = false;
421
+ if (this.isKeyword('UNIQUE')) { this.next(); unique = true; }
422
+ if (!this.isKeyword('INDEX')) throw new Error(`Expected INDEX, got '${this.peek().value}'`);
423
+ this.next();
424
+ const name = this.parseTableName();
425
+ if (!this.isKeyword('ON')) throw new Error(`Expected ON in CREATE INDEX, got '${this.peek().value}'`);
426
+ this.next();
427
+ const table = this.parseTableName();
428
+ this.expect('op', '(');
429
+ const columns = [];
430
+ for (;;) {
431
+ columns.push(this.parseColumnRef());
432
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
433
+ break;
434
+ }
435
+ this.expect('op', ')');
436
+ this.optionalTailSemicolon();
437
+ return { type: 'createIndex', name, table, columns, unique };
438
+ }
439
+
440
+ /** DROP INDEX name ON table */
441
+ parseDropIndex() {
442
+ this.expectKeyword('DROP');
443
+ this.expectKeyword('INDEX');
444
+ let ifExists = false;
445
+ if (this.isKeyword('IF')) { this.expectKeyword('IF'); this.expectKeyword('EXISTS'); ifExists = true; }
446
+ const name = this.parseTableName();
447
+ let table = null;
448
+ if (this.isKeyword('ON')) { this.next(); table = this.parseTableName(); }
449
+ this.optionalTailSemicolon();
450
+ return { type: 'dropIndex', name, table, ifExists };
451
+ }
452
+
354
453
  parseDropView() {
355
454
  this.expectKeyword('DROP');
356
455
  if (!this.isWord('VIEW')) throw new Error(`Expected VIEW, got '${this.peek().value}'`);
@@ -413,6 +512,9 @@ class Parser {
413
512
  hasPk = true;
414
513
  } else {
415
514
  this.expectKeyword('UNIQUE');
515
+ // UNIQUE [KEY|INDEX] [name] (cols...)
516
+ if (this.isKeyword('KEY') || this.isKeyword('INDEX')) this.next();
517
+ if (this.peek().type === 'ident') this.next();
416
518
  this.expect('op', '(');
417
519
  const uCols = [this.parseTableName()];
418
520
  while (this.peek().type === 'op' && this.peek().value === ',') {
@@ -426,7 +528,12 @@ class Parser {
426
528
  }
427
529
  } else if (t.type === 'keyword' && t.value === 'CONSTRAINT') {
428
530
  this.expectKeyword('CONSTRAINT');
429
- this.next();
531
+ if (this.peek().type === 'ident') this.next(); // 约束名
532
+ this._parseTableConstraint(schema);
533
+ } else if (this.isWord('FOREIGN') || this.isWord('CHECK') ||
534
+ t.type === 'keyword' && (t.value === 'KEY' || t.value === 'INDEX')) {
535
+ // 表级约束:FOREIGN KEY ... REFERENCES ... / CHECK (...) / KEY name (cols)
536
+ this._parseTableConstraint(schema);
430
537
  } else if (t.type === 'eof' || (t.type === 'op' && t.value === ')')) {
431
538
  break;
432
539
  } else {
@@ -458,19 +565,27 @@ class Parser {
458
565
  parseColumnDef() {
459
566
  const def = {};
460
567
  const typeTok = this.next();
461
- if (typeTok.type !== 'keyword') throw new Error(`Expected column type, got '${typeTok.value}'`);
462
- const type = typeTok.value.toLowerCase();
568
+ const typeRaw = String(typeTok.value);
569
+ // 这些类型不在 KEYWORDS 里(避免影响同名标识符),按上下文识别
570
+ const IDENT_TYPES = /^(BIG)?SERIAL$|^JSONB$|^TIMESTAMPTZ$|^BYTEA$|^UUID$/i;
571
+ const isIdentType = IDENT_TYPES.test(typeRaw);
572
+ if (typeTok.type !== 'keyword' && !isIdentType) throw new Error(`Expected column type, got '${typeTok.value}'`);
573
+ const type = typeRaw.toLowerCase();
463
574
  const typeMap = {
464
575
  integer: 'integer', int: 'integer', bigint: 'integer', tinyint: 'integer', smallint: 'integer',
465
576
  string: 'string', text: 'string', varchar: 'string', char: 'string',
466
577
  float: 'number', double: 'number', real: 'number', numeric: 'number', decimal: 'number',
467
578
  boolean: 'boolean', bool: 'boolean',
468
- date: 'date', datetime: 'datetime', timestamp: 'timestamp',
469
- any: 'any', object: 'object', json: 'object', array: 'array'
579
+ date: 'date', datetime: 'datetime', timestamp: 'timestamp', timestamptz: 'timestamp',
580
+ any: 'any', object: 'object', json: 'object', jsonb: 'object', array: 'array',
581
+ bytea: 'string', uuid: 'string',
582
+ // PG 自增序列类型:等价于整型 + 自增 + NOT NULL
583
+ serial: 'integer', bigserial: 'integer'
470
584
  };
471
585
  const mapped = typeMap[type];
472
586
  if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
473
587
  def.type = mapped;
588
+ if (/^(BIG)?SERIAL$/i.test(typeRaw)) { def.autoIncrement = true; def.required = true; }
474
589
 
475
590
  // 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
476
591
  if (this.peek().type === 'op' && this.peek().value === '(') {
@@ -517,7 +632,29 @@ class Parser {
517
632
  case 'NULL':
518
633
  this.next(); break;
519
634
  case 'DEFAULT':
520
- this.next(); def.default = this.parseValue(); break;
635
+ this.next();
636
+ // 默认值可以是字面量,也可以是函数/关键字形式(CURRENT_TIMESTAMP / NOW() 等)。
637
+ // 函数形式存成字符串 + defaultExpr 标记,与 JS 侧 `default: 'CURRENT_TIMESTAMP'` 约定一致。
638
+ if (this.peek().type === 'ident' ||
639
+ (this.peek().type === 'keyword' && !['NULL', 'TRUE', 'FALSE'].includes(this.peek().value))) {
640
+ const fnTok = this.next();
641
+ let name = String(fnTok.value);
642
+ if (this.peek().type === 'op' && this.peek().value === '(') {
643
+ this.next();
644
+ this.expect('op', ')');
645
+ name += '()';
646
+ }
647
+ def.default = name;
648
+ def.defaultExpr = true;
649
+ } else {
650
+ def.default = this.parseValue();
651
+ // 允许字面量后带 :: 类型转换:DEFAULT '{}'::jsonb
652
+ if (this.peek().type === 'op' && this.peek().value === '::') {
653
+ this.next();
654
+ if (this.peek().type === 'keyword' || this.peek().type === 'ident') this.next();
655
+ }
656
+ }
657
+ break;
521
658
  case 'COLLATE':
522
659
  this.next(); if (this.peek().type !== 'op' && this.peek().type !== 'eof') this.next(); break;
523
660
  case 'CHARACTER':
@@ -553,6 +690,68 @@ class Parser {
553
690
  return stmt;
554
691
  }
555
692
 
693
+ /** 跳过一对括号(含嵌套),用于表级约束里的列清单 */
694
+ _skipParens() {
695
+ this.expect('op', '(');
696
+ let depth = 1;
697
+ while (depth > 0 && this.peek().type !== 'eof') {
698
+ const tk = this.next();
699
+ if (tk.type === 'op' && tk.value === '(') depth++;
700
+ else if (tk.type === 'op' && tk.value === ')') depth--;
701
+ }
702
+ }
703
+
704
+ /**
705
+ * 解析表级约束(FOREIGN KEY / CHECK / KEY / INDEX / PRIMARY KEY / UNIQUE)。
706
+ * 目前只做语法层面消费并在可能时补充列属性,不做外键实际校验。
707
+ */
708
+ _parseTableConstraint(schema) {
709
+ if (this.isWord('FOREIGN')) {
710
+ this.next();
711
+ if (!this.isKeyword('KEY')) throw new Error(`Expected KEY after FOREIGN, got '${this.peek().value}'`);
712
+ this.next();
713
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
714
+ if (this.isWord('REFERENCES')) {
715
+ this.next();
716
+ this.parseTableName();
717
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
718
+ }
719
+ // ON DELETE / ON UPDATE <action>
720
+ while (this.isKeyword('ON')) {
721
+ this.next();
722
+ this.next(); // DELETE | UPDATE
723
+ const act = String(this.next().value).toUpperCase();
724
+ if (act === 'SET' || act === 'NO') this.next(); // SET NULL / NO ACTION
725
+ }
726
+ return;
727
+ }
728
+ if (this.isWord('CHECK')) {
729
+ this.next();
730
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
731
+ return;
732
+ }
733
+ if (this.isKeyword('PRIMARY')) {
734
+ this.expectKeyword('PRIMARY');
735
+ if (this.isKeyword('KEY')) this.next();
736
+ if (this.peek().type === 'op' && this.peek().value === '(') {
737
+ this.next();
738
+ const cols = [this.parseTableName()];
739
+ while (this.peek().type === 'op' && this.peek().value === ',') { this.next(); cols.push(this.parseTableName()); }
740
+ this.expect('op', ')');
741
+ for (const c of cols) if (schema[c]) schema[c].primaryKey = true;
742
+ }
743
+ return;
744
+ }
745
+ if (this.isKeyword('UNIQUE')) {
746
+ this.next();
747
+ }
748
+ if (this.isKeyword('KEY') || this.isKeyword('INDEX')) {
749
+ this.next();
750
+ if (this.peek().type === 'ident') this.next(); // 索引名
751
+ if (this.peek().type === 'op' && this.peek().value === '(') this._skipParens();
752
+ }
753
+ }
754
+
556
755
  parseInsertBody() {
557
756
  const name = this.parseTableName();
558
757
  let columns = null;
@@ -628,7 +827,7 @@ class Parser {
628
827
  for (;;) {
629
828
  const col = this.parseColumnRef();
630
829
  this.expect('op', '=');
631
- const val = this.parseValue();
830
+ const val = this.parseConflictValue();
632
831
  sets.push([col, val]);
633
832
  if (this.peek().value === ',') { this.next(); continue; }
634
833
  break;
@@ -683,6 +882,13 @@ class Parser {
683
882
  parseValue() {
684
883
  const t = this.next();
685
884
  if (t.type === 'number' || t.type === 'string') return t.value;
885
+ // MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
886
+ if (t.type === 'keyword' && t.value === 'VALUES' && this.peek().type === 'op' && this.peek().value === '(') {
887
+ this.next();
888
+ const col = this.parseColumnRef();
889
+ this.expect('op', ')');
890
+ return { _valuesOf: col };
891
+ }
686
892
  if (t.type === 'keyword' && t.value === 'NULL') return null;
687
893
  if (t.type === 'keyword' && t.value === 'TRUE') return true;
688
894
  if (t.type === 'keyword' && t.value === 'FALSE') return false;
@@ -701,6 +907,21 @@ class Parser {
701
907
  throw new Error(`Expected value, got '${t.value}'`);
702
908
  }
703
909
 
910
+ /**
911
+ * ON CONFLICT DO UPDATE 赋值右侧:
912
+ * EXCLUDED.col —— PG:引用"本次本应插入的值"
913
+ * VALUES(col) —— MySQL 同义(在 parseValue 中处理)
914
+ * 其他字面量
915
+ */
916
+ parseConflictValue() {
917
+ if (this.isWord('EXCLUDED') && this.peek(1).type === 'op' && this.peek(1).value === '.') {
918
+ this.next(); this.next();
919
+ const col = this.parseColumnRef();
920
+ return { _excludedOf: col };
921
+ }
922
+ return this.parseValue();
923
+ }
924
+
704
925
  /**
705
926
  * WITH [RECURSIVE] name [(col, ...)] AS (SELECT ...), ... <statement>
706
927
  * CTE 在语法层解析为 { type:'with', ctes:[{name, columns, select}], statement }。
@@ -833,24 +1054,37 @@ class Parser {
833
1054
  if (t.type === 'keyword' && t.value === 'COUNT') {
834
1055
  this.next();
835
1056
  this.expect('op', '(');
1057
+ let aggDistinct = false;
1058
+ if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct = true; }
836
1059
  let col = null;
837
1060
  if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); }
838
1061
  else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
1062
+ // COUNT(DISTINCT a, b) —— 多列去重(仅对 COUNT 有意义,其它聚合按元组处理)
1063
+ if (aggDistinct && this.peek().type === 'op' && this.peek().value === ',') {
1064
+ const cols = [col];
1065
+ while (this.peek().type === 'op' && this.peek().value === ',') {
1066
+ this.next();
1067
+ cols.push(this.parseScalar());
1068
+ }
1069
+ col = { type: 'tuple', items: cols };
1070
+ }
839
1071
  this.expect('op', ')');
840
- aggregate = { type: 'COUNT', column: col };
1072
+ aggregate = { type: 'COUNT', column: col, distinct: aggDistinct };
841
1073
  aggregate = this.parseOptionalOver(aggregate);
842
1074
  aggregate.alias = this.parseOptionalAlias();
843
- columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
1075
+ columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct });
844
1076
  } else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
845
1077
  this.next();
846
1078
  const fn = t.value;
847
1079
  this.expect('op', '(');
1080
+ let aggDistinct2 = false;
1081
+ if (this.isKeyword('DISTINCT')) { this.next(); aggDistinct2 = true; }
848
1082
  const col = this.parseScalar();
849
1083
  this.expect('op', ')');
850
- aggregate = { type: fn, column: col };
1084
+ aggregate = { type: fn, column: col, distinct: aggDistinct2 };
851
1085
  aggregate = this.parseOptionalOver(aggregate);
852
1086
  aggregate.alias = this.parseOptionalAlias();
853
- columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over });
1087
+ columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias, over: aggregate.over || null, window: !!aggregate.over, distinct: aggDistinct2 });
854
1088
  } else if (t.type === 'op' && t.value === '*') {
855
1089
  this.next();
856
1090
  columns.push({ expr: '*' });
@@ -867,7 +1101,7 @@ class Parser {
867
1101
  if (expr.type === 'star') {
868
1102
  columns.push({ expr: '*' });
869
1103
  } else if (expr.type === 'aggregate') {
870
- columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr });
1104
+ columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr, distinct: expr.distinct, separator: expr.separator, orderBy: expr.orderBy });
871
1105
  } else if (expr.type === 'column') {
872
1106
  columns.push({ expr: expr.name, scalar: expr, alias });
873
1107
  } else {
@@ -886,6 +1120,7 @@ class Parser {
886
1120
  let where = null;
887
1121
  if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
888
1122
  let groupBy = null;
1123
+ let rollup = false;
889
1124
  if (this.isKeyword('GROUP')) {
890
1125
  this.expectKeyword('GROUP'); this.expectKeyword('BY');
891
1126
  groupBy = [];
@@ -894,6 +1129,12 @@ class Parser {
894
1129
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
895
1130
  break;
896
1131
  }
1132
+ // WITH ROLLUP 必须紧跟 GROUP BY 列表,之后才是 HAVING / ORDER BY / LIMIT
1133
+ // (标准顺序:GROUP BY <list> [WITH ROLLUP] [HAVING] [ORDER BY] [LIMIT])
1134
+ if (this.isKeyword('WITH') && this.isWord('ROLLUP', 1)) {
1135
+ this.next(); this.next();
1136
+ rollup = true;
1137
+ }
897
1138
  }
898
1139
  let having = null;
899
1140
  if (this.isKeyword('HAVING')) { this.next(); having = this.parseExpr(); }
@@ -961,13 +1202,6 @@ class Parser {
961
1202
  else if (this.isKeyword('DISTINCT')) this.next();
962
1203
  except = { all, select: this.parseSelect() };
963
1204
  }
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
1205
  // FETCH FIRST n ROWS ONLY(SQL 标准的 LIMIT 写法)
972
1206
  if (this.isWord('FETCH')) {
973
1207
  this.next();
@@ -1029,19 +1263,18 @@ class Parser {
1029
1263
  this.expect('op', ')');
1030
1264
  let alias = null;
1031
1265
  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');
1266
+ else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) { alias = this.next().value; }
1267
+ // 标准 SQL 要求 FROM 子查询必须带别名,这里放宽为自动生成,
1268
+ // 避免 README 里 SELECT ... FROM (SELECT ...) 这类示例直接报错
1269
+ if (!alias) alias = '__sub' + (++this.subquerySeq);
1034
1270
  return { subquery: sub, alias };
1035
1271
  }
1036
1272
  const table = this.parseTableName();
1037
1273
  let alias = null;
1038
1274
  // 这里排除的是"未进关键字表、但会跟在表名之后"的子句起始词,
1039
1275
  // 否则 `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
1276
  if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
1044
- else if (this.peek().type === 'ident' && !NOT_ALIAS.some(w => this.isWord(w))) {
1277
+ else if (this.peek().type === 'ident' && !NOT_ALIAS_WORDS.some(w => this.isWord(w))) {
1045
1278
  alias = this.next().value;
1046
1279
  }
1047
1280
  return { table, alias };
@@ -1096,9 +1329,28 @@ class Parser {
1096
1329
  const table = this.parseTableName();
1097
1330
  let where = null;
1098
1331
  if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
1332
+ let orderBy = null, limit = null;
1333
+ if (this.isKeyword('ORDER')) {
1334
+ this.next(); this.expectKeyword('BY');
1335
+ orderBy = [];
1336
+ for (;;) {
1337
+ const col = this.parseColumnRef();
1338
+ let dir = 'asc';
1339
+ if (this.isKeyword('ASC')) this.next();
1340
+ else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
1341
+ orderBy.push({ column: col, dir });
1342
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1343
+ break;
1344
+ }
1345
+ }
1346
+ if (this.isKeyword('LIMIT')) {
1347
+ this.next();
1348
+ limit = this.parseValue();
1349
+ if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
1350
+ }
1099
1351
  const returning = this.parseOptionalReturning();
1100
1352
  this.optionalTailSemicolon();
1101
- return { type: 'delete', table, where, returning };
1353
+ return { type: 'delete', table, where, orderBy, limit, returning };
1102
1354
  }
1103
1355
 
1104
1356
  parseDropTable() {
@@ -1386,19 +1638,93 @@ class Parser {
1386
1638
  return this.parseOr();
1387
1639
  }
1388
1640
 
1641
+ /**
1642
+ * 函数实参用的表达式:与 parseExpr 相同,但叶子允许是"裸操作数"。
1643
+ * 例如 IF(1 > 0, 'y', 'n') 的第一个参数是完整比较表达式,
1644
+ * 而 IF(age, 'y', 'n') 这种裸列名也必须能解析。
1645
+ */
1646
+ parseArgExpr() {
1647
+ this.bareExprDepth = (this.bareExprDepth || 0) + 1;
1648
+ try {
1649
+ return this.parseOr();
1650
+ } finally {
1651
+ this.bareExprDepth--;
1652
+ }
1653
+ }
1654
+
1655
+ // 位运算优先级(低 → 高):| < ^ < & < << >> < 加减 < 乘除模
1656
+ parseBitwise() {
1657
+ let node = this.parseBitXor();
1658
+ for (;;) {
1659
+ const t = this.peek();
1660
+ if (t.type === 'op' && t.value === '|') { this.next(); node = { type: 'arith', op: '|', left: node, right: this.parseBitXor() }; continue; }
1661
+ break;
1662
+ }
1663
+ return node;
1664
+ }
1665
+
1666
+ parseBitXor() {
1667
+ let node = this.parseBitAnd();
1668
+ for (;;) {
1669
+ const t = this.peek();
1670
+ if (t.type === 'op' && t.value === '^') { this.next(); node = { type: 'arith', op: '^', left: node, right: this.parseBitAnd() }; continue; }
1671
+ break;
1672
+ }
1673
+ return node;
1674
+ }
1675
+
1676
+ parseBitAnd() {
1677
+ let node = this.parseShift();
1678
+ for (;;) {
1679
+ const t = this.peek();
1680
+ if (t.type === 'op' && t.value === '&') { this.next(); node = { type: 'arith', op: '&', left: node, right: this.parseShift() }; continue; }
1681
+ break;
1682
+ }
1683
+ return node;
1684
+ }
1685
+
1686
+ parseShift() {
1687
+ let node = this.parseAdditive();
1688
+ for (;;) {
1689
+ const t = this.peek();
1690
+ if (t.type === 'op' && (t.value === '<<' || t.value === '>>')) {
1691
+ this.next();
1692
+ node = { type: 'arith', op: t.value, left: node, right: this.parseAdditive() };
1693
+ continue;
1694
+ }
1695
+ break;
1696
+ }
1697
+ return node;
1698
+ }
1699
+
1389
1700
  parseOr() {
1701
+ let left = this.parseXor();
1702
+ for (;;) {
1703
+ const isOr = this.isKeyword('OR') || (this.peek().type === 'op' && this.peek().value === '||');
1704
+ if (!isOr) break;
1705
+ this.next();
1706
+ const right = this.parseXor();
1707
+ left = { type: 'or', left, right };
1708
+ }
1709
+ return left;
1710
+ }
1711
+
1712
+ // 逻辑异或 XOR(MySQL 优先级:OR < XOR < AND)
1713
+ parseXor() {
1390
1714
  let left = this.parseAnd();
1391
- while (this.isKeyword('OR')) {
1715
+ while (this.isWord('XOR')) {
1392
1716
  this.next();
1393
1717
  const right = this.parseAnd();
1394
- left = { type: 'or', left, right };
1718
+ left = { type: 'xor', left, right };
1395
1719
  }
1396
1720
  return left;
1397
1721
  }
1398
1722
 
1399
1723
  parseAnd() {
1400
1724
  let left = this.parseNot();
1401
- while (this.isKeyword('AND')) {
1725
+ for (;;) {
1726
+ const isAnd = this.isKeyword('AND') || (this.peek().type === 'op' && this.peek().value === '&&');
1727
+ if (!isAnd) break;
1402
1728
  this.next();
1403
1729
  const right = this.parseNot();
1404
1730
  left = { type: 'and', left, right };
@@ -1434,9 +1760,69 @@ class Parser {
1434
1760
  return this.parseComparison();
1435
1761
  }
1436
1762
 
1763
+ /**
1764
+ * 解析聚合调用:fn ( [DISTINCT] expr [, expr*] [ORDER BY ...] [SEPARATOR 'x'] )
1765
+ * 以及 COUNT(*) 这类无参形式。
1766
+ */
1767
+ parseAggregateCall(fn) {
1768
+ this.expect('op', '(');
1769
+ const distinct = this.isKeyword('DISTINCT');
1770
+ if (distinct) this.next();
1771
+ let column = null;
1772
+ let separator = null;
1773
+ let innerOrder = null;
1774
+ if (this.peek().type === 'op' && this.peek().value === '*') {
1775
+ this.next();
1776
+ } else if (!(this.peek().type === 'op' && this.peek().value === ')')) {
1777
+ column = this.parseScalar();
1778
+ // COUNT(DISTINCT a, b):多列去重
1779
+ if (distinct && this.peek().type === 'op' && this.peek().value === ',') {
1780
+ const cols = [column];
1781
+ while (this.peek().type === 'op' && this.peek().value === ',') {
1782
+ this.next();
1783
+ cols.push(this.parseScalar());
1784
+ }
1785
+ column = { type: 'tuple', items: cols };
1786
+ } else if (this.peek().type === 'op' && this.peek().value === ',') {
1787
+ // GROUP_CONCAT(expr, 'sep')
1788
+ this.next();
1789
+ separator = this.parseValue();
1790
+ }
1791
+ // GROUP_CONCAT(expr ORDER BY col [DESC])
1792
+ if (this.isKeyword('ORDER')) {
1793
+ this.next();
1794
+ if (!this.isKeyword('BY')) throw new Error('Expected BY after ORDER in aggregate, got ' + "'" + this.peek().value + "'");
1795
+ this.next();
1796
+ innerOrder = [];
1797
+ for (;;) {
1798
+ const oCol = this.parseColumnRef();
1799
+ let oDir = 'asc';
1800
+ if (this.isKeyword('ASC')) this.next();
1801
+ else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
1802
+ innerOrder.push({ column: oCol, dir: oDir });
1803
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1804
+ break;
1805
+ }
1806
+ }
1807
+ // GROUP_CONCAT(expr SEPARATOR 'sep')
1808
+ if (this.isWord('SEPARATOR')) {
1809
+ this.next();
1810
+ separator = this.parseValue();
1811
+ }
1812
+ }
1813
+ this.expect('op', ')');
1814
+ return this.parseOptionalOver({ type: 'aggregate', fn, column, distinct, separator, orderBy: innerOrder });
1815
+ }
1816
+
1437
1817
  parseOperand() {
1438
1818
  const t = this.next();
1439
1819
  if (t.type === 'ident') {
1820
+ // 聚合函数名(GROUP_CONCAT / STDDEV / FIRST...)走 aggregate 节点,
1821
+ // 否则会被当作逐行标量函数:既返回 null,又不会折叠分组
1822
+ if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
1823
+ this.peek().type === 'op' && this.peek().value === '(') {
1824
+ return this.parseAggregateCall(String(t.value).toUpperCase());
1825
+ }
1440
1826
  // 支持 alias.column 引用
1441
1827
  if (this.peek().type === 'op' && this.peek().value === '.') {
1442
1828
  this.next();
@@ -1469,6 +1855,60 @@ class Parser {
1469
1855
  this.expect('op', ')');
1470
1856
  return { type: 'cast', expr, dataType };
1471
1857
  }
1858
+ // EXTRACT(YEAR FROM expr) —— 标准 SQL 的 FROM 形式
1859
+ if (String(t.value).toUpperCase() === 'EXTRACT' && this.peek().type === 'op' && this.peek().value === '(') {
1860
+ this.next();
1861
+ const unitTok = this.next();
1862
+ const unit = String(unitTok.value).toUpperCase();
1863
+ if (!this.isWord('FROM')) throw new Error(`Expected FROM in EXTRACT, got '${this.peek().value}'`);
1864
+ this.next();
1865
+ const arg = this.parseOperand();
1866
+ this.expect('op', ')');
1867
+ return { type: 'func', name: 'EXTRACT', args: [{ type: 'value', value: unit }, arg] };
1868
+ }
1869
+ // SUBSTR(s FROM n [FOR m]) / TRIM([LEADING|TRAILING|BOTH] ['x'] FROM s)
1870
+ if (['SUBSTR', 'SUBSTRING'].includes(String(t.value).toUpperCase()) && this.peek().type === 'op' && this.peek().value === '(') {
1871
+ const name = String(t.value).toUpperCase();
1872
+ this.next();
1873
+ const s = this.parseOperand();
1874
+ let fromV = null, forV = null;
1875
+ if (this.isWord('FROM')) {
1876
+ this.next();
1877
+ fromV = this.parseOperand();
1878
+ if (this.isWord('FOR')) { this.next(); forV = this.parseOperand(); }
1879
+ } else if (this.peek().type === 'op' && this.peek().value === ',') {
1880
+ this.next();
1881
+ fromV = this.parseOperand();
1882
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); forV = this.parseOperand(); }
1883
+ }
1884
+ this.expect('op', ')');
1885
+ const args = [s];
1886
+ if (fromV) args.push(fromV);
1887
+ if (forV) args.push(forV);
1888
+ return { type: 'func', name, args };
1889
+ }
1890
+ if (String(t.value).toUpperCase() === 'TRIM' && this.peek().type === 'op' && this.peek().value === '(') {
1891
+ this.next();
1892
+ let spec = null;
1893
+ if (this.isWord('LEADING') || this.isWord('TRAILING') || this.isWord('BOTH')) {
1894
+ spec = String(this.next().value).toUpperCase();
1895
+ }
1896
+ const first = this.parseOperand();
1897
+ let target = null;
1898
+ if (this.isWord('FROM')) {
1899
+ this.next();
1900
+ target = this.parseOperand();
1901
+ } else if (this.peek().type === 'op' && this.peek().value === ',') {
1902
+ this.next();
1903
+ target = this.parseOperand();
1904
+ }
1905
+ this.expect('op', ')');
1906
+ // 归一成 TRIM(target[, chars[, spec]]),与 applyScalarFunction 的既有签名兼容
1907
+ if (target === null) return { type: 'func', name: 'TRIM', args: [first] };
1908
+ const args = [target, first];
1909
+ if (spec) args.push({ type: 'value', value: spec });
1910
+ return { type: 'func', name: 'TRIM', args };
1911
+ }
1472
1912
  // 函数调用: VERSION() / CONCAT(a, b) / NOW() ...
1473
1913
  if (this.peek().type === 'op' && this.peek().value === '(') {
1474
1914
  const name = t.value;
@@ -1477,7 +1917,42 @@ class Parser {
1477
1917
  if (!(this.peek().type === 'op' && this.peek().value === ')')) {
1478
1918
  for (;;) {
1479
1919
  if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
1480
- else args.push(this.parseOperand());
1920
+ // INTERVAL n UNIT(DATE_ADD/DATE_SUB 用)
1921
+ else if (this.isWord('INTERVAL')) {
1922
+ this.next();
1923
+ const nTok = this.peek();
1924
+ let nVal;
1925
+ if (nTok.type === 'op' && (nTok.value === '-' || nTok.value === '+')) {
1926
+ this.next();
1927
+ const numTok = this.next();
1928
+ nVal = (nTok.value === '-' ? -1 : 1) * Number(numTok.value);
1929
+ } else {
1930
+ this.next();
1931
+ nVal = Number(nTok.value);
1932
+ }
1933
+ const unitTok = this.next();
1934
+ args.push({ type: 'interval', value: nVal, unit: String(unitTok.value).toUpperCase() });
1935
+ }
1936
+ // 实参允许完整的比较/布尔表达式,例如 IF(status = 'paid', amount, 0)
1937
+ else args.push(this.parseArgExpr());
1938
+ // GROUP_CONCAT(name ORDER BY age):聚合函数内部的排序
1939
+ if (this.isKeyword('ORDER')) {
1940
+ this.next();
1941
+ if (!this.isKeyword('BY')) throw new Error(`Expected BY after ORDER in aggregate, got '${this.peek().value}'`);
1942
+ this.next();
1943
+ const innerOrder = [];
1944
+ for (;;) {
1945
+ const oCol = this.parseColumnRef();
1946
+ let oDir = 'asc';
1947
+ if (this.isKeyword('ASC')) this.next();
1948
+ else if (this.isKeyword('DESC')) { this.next(); oDir = 'desc'; }
1949
+ innerOrder.push({ column: oCol, dir: oDir });
1950
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1951
+ break;
1952
+ }
1953
+ this.expect('op', ')');
1954
+ return this.parseOptionalOver({ type: 'func', name, args, orderBy: innerOrder });
1955
+ }
1481
1956
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1482
1957
  break;
1483
1958
  }
@@ -1502,7 +1977,7 @@ class Parser {
1502
1977
  if (!(this.peek().type === 'op' && this.peek().value === ')')) {
1503
1978
  for (;;) {
1504
1979
  if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
1505
- else args.push(this.parseOperand());
1980
+ else args.push(this.parseArgExpr());
1506
1981
  if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1507
1982
  break;
1508
1983
  }
@@ -1510,14 +1985,13 @@ class Parser {
1510
1985
  this.expect('op', ')');
1511
1986
  return this.parseOptionalOver({ type: 'func', name, args });
1512
1987
  }
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 });
1988
+ if (AGG_FUNCS.has(String(t.value).toUpperCase()) &&
1989
+ this.peek().type === 'op' && this.peek().value === '(') {
1990
+ return this.parseAggregateCall(String(t.value).toUpperCase());
1991
+ }
1992
+ // 一元按位取反 ~x
1993
+ if (t.type === 'op' && t.value === '~') {
1994
+ return { type: 'bitnot', expr: this.parseOperand() };
1521
1995
  }
1522
1996
  if (t.type === 'op' && t.value === '(' && this.peek().type === 'keyword' && this.peek().value === 'SELECT') {
1523
1997
  const sub = this.parseSelect();
@@ -1532,8 +2006,8 @@ class Parser {
1532
2006
  throw new Error(`Expected value or column, got '${t.value}'`);
1533
2007
  }
1534
2008
 
1535
- // 算术表达式: + - * / % (左结合, * / 优先)
1536
- parseScalar() {
2009
+ // 加减(左结合)
2010
+ parseAdditive() {
1537
2011
  let node = this.parseTerm();
1538
2012
  for (;;) {
1539
2013
  const t = this.peek();
@@ -1545,6 +2019,16 @@ class Parser {
1545
2019
  }
1546
2020
  break;
1547
2021
  }
2022
+ return node;
2023
+ }
2024
+
2025
+ /**
2026
+ * 标量表达式:算术 + 位运算,外加 IN / IS 后缀。
2027
+ * 位运算要走到这里,否则 SELECT 列表里的 `SELECT 1 << 8` 无法解析
2028
+ * (parseComparison 只在 WHERE 等布尔上下文被调用)。
2029
+ */
2030
+ parseScalar() {
2031
+ let node = this.parseBitwise();
1548
2032
  // 后缀:expr IN (...)、expr IS [NOT] TRUE/FALSE/NULL(标量上下文,如 SELECT 1 IN (...))
1549
2033
  for (;;) {
1550
2034
  const t = this.peek();
@@ -1582,15 +2066,65 @@ class Parser {
1582
2066
  }
1583
2067
 
1584
2068
  parseTerm() {
1585
- let node = this.parseOperand();
2069
+ let node = this.parsePostfix(this.parseOperand());
1586
2070
  for (;;) {
1587
2071
  const t = this.peek();
1588
2072
  if (t.type === 'op' && (t.value === '*' || t.value === '/' || t.value === '%')) {
1589
2073
  this.next();
1590
- const right = this.parseOperand();
2074
+ const right = this.parsePostfix(this.parseOperand());
1591
2075
  node = { type: 'arith', op: t.value, left: node, right };
1592
2076
  continue;
1593
2077
  }
2078
+ // MySQL 整数除法:7 DIV 2 → 3
2079
+ if (this.isWord('DIV')) {
2080
+ this.next();
2081
+ const right = this.parsePostfix(this.parseOperand());
2082
+ node = { type: 'arith', op: 'DIV', left: node, right };
2083
+ continue;
2084
+ }
2085
+ break;
2086
+ }
2087
+ return node;
2088
+ }
2089
+
2090
+ /**
2091
+ * 后缀运算符(绑定最紧):PG 风格 `::` 类型转换、`->` / `->>` JSON 取值。
2092
+ * 例:'5'::INT、meta->>'name'、meta->'addr'->>'city'。
2093
+ */
2094
+ parsePostfix(node) {
2095
+ for (;;) {
2096
+ const t = this.peek();
2097
+ if (t.type === 'op' && t.value === '::') {
2098
+ this.next();
2099
+ const typeTok = this.next();
2100
+ let dataType = String(typeTok.value).toUpperCase();
2101
+ if (this.peek().type === 'op' && this.peek().value === '(') {
2102
+ this.next();
2103
+ let depth = 1, spec = '';
2104
+ while (depth > 0 && this.peek().type !== 'eof') {
2105
+ const tk = this.next();
2106
+ if (tk.type === 'op' && tk.value === '(') depth++;
2107
+ else if (tk.type === 'op' && tk.value === ')') { depth--; if (depth === 0) break; }
2108
+ spec += tk.value;
2109
+ }
2110
+ dataType += '(' + spec + ')';
2111
+ }
2112
+ node = { type: 'cast', expr: node, dataType };
2113
+ continue;
2114
+ }
2115
+ if (t.type === 'op' && (t.value === '->' || t.value === '->>')) {
2116
+ this.next();
2117
+ const key = this.parseValue();
2118
+ node = { type: 'jsonAccess', operand: node, key, asText: t.value === '->>' };
2119
+ continue;
2120
+ }
2121
+ // PG 路径取值:meta#>'{a,b}' / meta#>>'{a,b}'
2122
+ if (t.type === 'op' && (t.value === '#>' || t.value === '#>>')) {
2123
+ this.next();
2124
+ const key = this.parseValue();
2125
+ node = { type: 'jsonAccess', operand: node, key, asText: t.value === '#>>', isPath: true };
2126
+ continue;
2127
+ }
1594
2128
  break;
1595
2129
  }
1596
2130
  return node;
@@ -1623,7 +2157,7 @@ class Parser {
1623
2157
  }
1624
2158
 
1625
2159
  parseComparison() {
1626
- const left = this.parseOperand();
2160
+ const left = this.parseBitwise();
1627
2161
  const t = this.peek();
1628
2162
  let not = false;
1629
2163
  if (t.type === 'keyword' && t.value === 'NOT') {
@@ -1686,7 +2220,31 @@ class Parser {
1686
2220
  const isILike = !(t2.type === 'keyword' && t2.value === 'LIKE');
1687
2221
  this.next();
1688
2222
  const pattern = this.parseValue();
1689
- return { type: 'like', operand: left, pattern, not, ci: isILike };
2223
+ // 可选 ESCAPE 子句:LIKE '...' ESCAPE '!'
2224
+ let escape = null;
2225
+ if (this.isWord('ESCAPE')) { this.next(); escape = this.parseValue(); }
2226
+ return { type: 'like', operand: left, pattern, not, ci: isILike, escape };
2227
+ }
2228
+
2229
+ // PG 正则操作符(双目位置):~(大小写敏感)/ ~*(不敏感)/ !~ / !~*
2230
+ if (t2.type === 'op' && (t2.value === '~' || t2.value === '~*' || t2.value === '!~' || t2.value === '!~*')) {
2231
+ this.next();
2232
+ const pattern = this.parseValue();
2233
+ return {
2234
+ type: 'regexp', operand: left, pattern,
2235
+ not: t2.value.startsWith('!') || not,
2236
+ ci: t2.value.endsWith('*'),
2237
+ };
2238
+ }
2239
+
2240
+ // PG JSON 键存在:? / ?| / ?&
2241
+ // 裸 `?` 在此处处于"运算符位置",与值位置上的 `?` 参数占位符不冲突。
2242
+ if ((t2.type === 'op' && (t2.value === '?|' || t2.value === '?&')) ||
2243
+ (t2.type === 'param' && t2.value === null)) {
2244
+ const mode = t2.type === 'param' ? '?' : t2.value;
2245
+ this.next();
2246
+ const keys = this.parseValue();
2247
+ return { type: 'jsonKeyExists', operand: left, keys, mode, not };
1690
2248
  }
1691
2249
 
1692
2250
  // MySQL 中 RLIKE 是 REGEXP 的同义词
@@ -1696,7 +2254,14 @@ class Parser {
1696
2254
  return { type: 'regexp', operand: left, pattern, not };
1697
2255
  }
1698
2256
 
1699
- if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
2257
+ // PG JSON 包含操作符
2258
+ if (t2.type === 'op' && (t2.value === '@>' || t2.value === '<@')) {
2259
+ this.next();
2260
+ const right = this.parseBitwise();
2261
+ return { type: 'jsonContains', op: t2.value, left, right, not };
2262
+ }
2263
+
2264
+ if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(t2.value)) {
1700
2265
  this.next();
1701
2266
  // = ANY (SELECT ...) / > ALL (SELECT ...)
1702
2267
  const anyAll = this.isWord('ANY') || this.isWord('ALL') || this.isWord('SOME');
@@ -1707,10 +2272,13 @@ class Parser {
1707
2272
  this.expect('op', ')');
1708
2273
  return { type: 'quantified', op: t2.value === '<>' ? '!=' : t2.value, operand: left, select: sub, quantifier, not };
1709
2274
  }
1710
- const right = this.parseOperand();
2275
+ const right = this.parseBitwise();
1711
2276
  return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
1712
2277
  }
1713
2278
 
2279
+ // 函数实参场景允许裸操作数(IF(age, ...) / SUM(IF(...)) 等)
2280
+ if (this.bareExprDepth > 0) return left;
2281
+
1714
2282
  throw new Error(`Expected comparison operator, got '${t2.value}'`);
1715
2283
  }
1716
2284
  }
@@ -1807,16 +2375,34 @@ function resolveOperand(operand, row, ctx) {
1807
2375
  case '*': return l * r;
1808
2376
  case '/': return r === 0 ? null : l / r;
1809
2377
  case '%': return r === 0 ? null : l % r;
2378
+ // MySQL 整数除法 DIV(结果向零取整)
2379
+ case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
2380
+ // 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
2381
+ case '&': return (Number(l) | 0) & (Number(r) | 0);
2382
+ case '|': return (Number(l) | 0) | (Number(r) | 0);
2383
+ case '^': return (Number(l) | 0) ^ (Number(r) | 0);
2384
+ case '<<': return (Number(l) | 0) << (Number(r) | 0);
2385
+ case '>>': return (Number(l) | 0) >> (Number(r) | 0);
1810
2386
  }
1811
2387
  return null;
1812
2388
  }
2389
+ case 'bitnot':
2390
+ return ~(Number(resolveOperand(operand.expr, row, ctx)) | 0);
1813
2391
  case 'func':
1814
2392
  return applyScalarFunction(operand, row, ctx);
1815
2393
  case 'case':
1816
2394
  return evaluateCaseVal(operand, row, ctx);
2395
+ // 布尔类节点出现在"取值"位置时(如 IF(status = 'paid', ...)、SUM(IF(a > b,1,0))),
2396
+ // 求值成 true/false 供上层按真值判断
2397
+ case 'compare':
2398
+ case 'and':
2399
+ case 'or':
2400
+ case 'xor':
2401
+ case 'not':
1817
2402
  case 'in':
1818
2403
  case 'isNull':
1819
2404
  case 'isTruth':
2405
+ case 'exists':
1820
2406
  case 'regexp':
1821
2407
  case 'like':
1822
2408
  case 'between':
@@ -1836,6 +2422,44 @@ function resolveOperand(operand, row, ctx) {
1836
2422
  const v = resolveOperand(operand.expr, row, ctx);
1837
2423
  return applyCast(v, operand.dataType);
1838
2424
  }
2425
+ case 'jsonAccess': {
2426
+ let base = resolveOperand(operand.operand, row, ctx);
2427
+ if (base === null || base === undefined) return null;
2428
+ // JSON 列可能以字符串形式存储,先解析
2429
+ if (typeof base === 'string') {
2430
+ try { base = JSON.parse(base); } catch (e) { return null; }
2431
+ }
2432
+ const keys = operand.isPath ? parsePgPath(operand.key) : [operand.key];
2433
+ let v = base;
2434
+ for (const key of keys) {
2435
+ if (v === null || v === undefined) return null;
2436
+ if (Array.isArray(v)) {
2437
+ const idx = Number(key);
2438
+ v = Number.isInteger(idx) ? (idx < 0 ? v[v.length + idx] : v[idx]) : undefined;
2439
+ } else if (typeof v === 'object') {
2440
+ v = v[key];
2441
+ } else {
2442
+ return null;
2443
+ }
2444
+ }
2445
+ if (v === undefined) return null;
2446
+ if (operand.asText) {
2447
+ if (v === null) return null;
2448
+ return typeof v === 'object' ? JSON.stringify(v) : String(v);
2449
+ }
2450
+ return v;
2451
+ }
2452
+ case 'jsonContains': {
2453
+ const l = resolveOperand(operand.left, row, ctx);
2454
+ const r = resolveOperand(operand.right, row, ctx);
2455
+ const res = jsonContains(l, r, operand.op === '<@');
2456
+ return operand.not ? !res : res;
2457
+ }
2458
+ case 'jsonKeyExists': {
2459
+ const base = resolveOperand(operand.operand, row, ctx);
2460
+ const res = jsonKeyExists(base, operand.keys, operand.mode);
2461
+ return operand.not ? !res : res;
2462
+ }
1839
2463
  case 'quantified': {
1840
2464
  // = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
1841
2465
  const v = resolveOperand(operand.operand, row, ctx);
@@ -1856,9 +2480,30 @@ function resolveOperand(operand, row, ctx) {
1856
2480
  }
1857
2481
  }
1858
2482
 
2483
+ /** 解析 SQL 里的日期文本('YYYY-MM-DD' / 'YYYY-MM-DD HH:MM:SS' / Date) */
2484
+ function parseSqlDate(v) {
2485
+ if (v === null || v === undefined) return null;
2486
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
2487
+ const s = String(v).trim().replace(' ', 'T');
2488
+ const d = new Date(s.length <= 10 ? s + 'T00:00:00' : s);
2489
+ return Number.isNaN(d.getTime()) ? null : d;
2490
+ }
2491
+
2492
+ function pad2(n) { return String(n).padStart(2, '0'); }
2493
+
2494
+ /** 按输入形态回写日期:纯日期返回 YYYY-MM-DD,带时间返回 YYYY-MM-DD HH:MM:SS */
2495
+ function fmtSqlDate(dateOnly, d) {
2496
+ const ymd = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
2497
+ return dateOnly ? ymd : ymd + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
2498
+ }
2499
+
1859
2500
  function applyScalarFunction(fnNode, row, ctx) {
1860
2501
  const name = (fnNode.name || '').toUpperCase();
1861
- const args = (fnNode.args || []).map(a => resolveOperand(a, row, ctx));
2502
+ const rawArgs = fnNode.args || [];
2503
+ // INTERVAL n UNIT 是语法结构而非普通值,原样传给 DATE_ADD / DATE_SUB
2504
+ const args = rawArgs.map(a => (a && a.type === 'interval')
2505
+ ? { __interval: true, value: a.value, unit: a.unit }
2506
+ : resolveOperand(a, row, ctx));
1862
2507
  const session = ctx && ctx.session;
1863
2508
  if (ctx && ctx.functions && Object.prototype.hasOwnProperty.call(ctx.functions, name)) {
1864
2509
  return ctx.functions[name].apply(null, args);
@@ -1889,7 +2534,65 @@ function applyScalarFunction(fnNode, row, ctx) {
1889
2534
  case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
1890
2535
  case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
1891
2536
  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();
2537
+ // TRIM(s) / TRIM([LEADING|TRAILING|BOTH] chars FROM s) / TRIM(s, chars)
2538
+ case 'TRIM': {
2539
+ if (args[0] == null) return null;
2540
+ const s = String(args[0]);
2541
+ if (args.length === 1) return s.trim();
2542
+ const chars = args[1] == null ? '' : String(args[1]);
2543
+ if (!chars) return s.trim();
2544
+ const spec = args[2] ? String(args[2]).toUpperCase() : 'BOTH';
2545
+ const esc = chars.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2546
+ const head = new RegExp('^[' + esc + ']+');
2547
+ const tail = new RegExp('[' + esc + ']+$');
2548
+ if (spec === 'LEADING') return s.replace(head, '');
2549
+ if (spec === 'TRAILING') return s.replace(tail, '');
2550
+ return s.replace(head, '').replace(tail, '');
2551
+ }
2552
+ // EXTRACT(YEAR FROM d) —— 解析层会把 unit 作为第一个参数传入
2553
+ case 'EXTRACT': {
2554
+ const unit = String(args[0] == null ? '' : args[0]).toUpperCase();
2555
+ const d = parseSqlDate(args[1]);
2556
+ if (!d) return null;
2557
+ switch (unit) {
2558
+ case 'YEAR': return d.getFullYear();
2559
+ case 'MONTH': return d.getMonth() + 1;
2560
+ case 'DAY': return d.getDate();
2561
+ case 'HOUR': return d.getHours();
2562
+ case 'MINUTE': return d.getMinutes();
2563
+ case 'SECOND': return d.getSeconds();
2564
+ case 'DOW': return d.getDay(); // 0 = 周日
2565
+ case 'DAYOFWEEK': return d.getDay() + 1; // 1 = 周日(MySQL 语义)
2566
+ case 'WEEK': {
2567
+ const start = new Date(d.getFullYear(), 0, 1);
2568
+ return Math.floor((d - start) / 604800000) + 1;
2569
+ }
2570
+ default: return null;
2571
+ }
2572
+ }
2573
+ case 'DATE_ADD': case 'ADDDATE':
2574
+ case 'DATE_SUB': case 'SUBDATE': {
2575
+ const sign = (name === 'DATE_SUB' || name === 'SUBDATE') ? -1 : 1;
2576
+ const base = parseSqlDate(args[0]);
2577
+ if (!base) return null;
2578
+ const iv = args[1];
2579
+ if (!iv || !iv.__interval) return fmtSqlDate(String(args[0]).length <= 10, base);
2580
+ const isDateOnly = String(args[0]).trim().length <= 10;
2581
+ const d = new Date(base.getTime());
2582
+ const n = sign * Number(iv.value || 0);
2583
+ switch (String(iv.unit || '').toUpperCase()) {
2584
+ case 'DAY': case 'DAYS': d.setDate(d.getDate() + n); break;
2585
+ case 'WEEK': case 'WEEKS': d.setDate(d.getDate() + n * 7); break;
2586
+ case 'MONTH': case 'MONTHS': d.setMonth(d.getMonth() + n); break;
2587
+ case 'QUARTER': d.setMonth(d.getMonth() + n * 3); break;
2588
+ case 'YEAR': case 'YEARS': d.setFullYear(d.getFullYear() + n); break;
2589
+ case 'HOUR': case 'HOURS': d.setHours(d.getHours() + n); break;
2590
+ case 'MINUTE': case 'MINUTES': d.setMinutes(d.getMinutes() + n); break;
2591
+ case 'SECOND': case 'SECONDS': d.setSeconds(d.getSeconds() + n); break;
2592
+ default: return null;
2593
+ }
2594
+ return fmtSqlDate(isDateOnly, d);
2595
+ }
1893
2596
  case 'LTRIM': return args[0] == null ? null : String(args[0]).replace(/^\s+/, '');
1894
2597
  case 'RTRIM': return args[0] == null ? null : String(args[0]).replace(/\s+$/, '');
1895
2598
  case 'ABS': return args[0] == null ? null : Math.abs(args[0]);
@@ -1975,13 +2678,76 @@ function seedRand(seed) {
1975
2678
  }
1976
2679
 
1977
2680
  // ci 默认 true:保持 LIKE 既有的大小写不敏感行为不变;ILIKE 同为不敏感。
1978
- function likeMatch(value, pattern, ci = true) {
2681
+ // escape:可选转义符(ESCAPE 子句),默认反斜杠;转义符后跟 % _ 或转义符本身按字面量匹配。
2682
+ function likeMatch(value, pattern, ci = true, escape = '\\') {
1979
2683
  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);
2684
+ const p = String(pattern);
2685
+ const e = (escape === null || escape === undefined || escape === '') ? '\\' : String(escape);
2686
+ const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2687
+ let out = '';
2688
+ for (let i = 0; i < p.length; i++) {
2689
+ const ch = p[i];
2690
+ if (ch === e && i + 1 < p.length && (p[i + 1] === '%' || p[i + 1] === '_' || p[i + 1] === e)) {
2691
+ out += reEsc(p[i + 1]);
2692
+ i++;
2693
+ continue;
2694
+ }
2695
+ if (ch === '%') out += '.*';
2696
+ else if (ch === '_') out += '.';
2697
+ else out += reEsc(ch);
2698
+ }
2699
+ return new RegExp('^' + out + '$', ci ? 'i' : '').test(value);
2700
+ }
2701
+
2702
+ /** 解析 PG 的路径字面量:'{a,b}' 或数组 → ['a','b'] */
2703
+ function parsePgPath(key) {
2704
+ if (Array.isArray(key)) return key.map(String);
2705
+ if (typeof key === 'string') {
2706
+ const s = key.trim();
2707
+ if (s.startsWith('{') && s.endsWith('}')) {
2708
+ return s.slice(1, -1).split(',').map(x => x.trim().replace(/^"(.*)"$/, '$1')).filter(x => x !== '');
2709
+ }
2710
+ return [s];
2711
+ }
2712
+ return [String(key)];
2713
+ }
2714
+
2715
+ /** 把可能是 JSON 字符串的值归一为 JS 值 */
2716
+ function toJsonValue(v) {
2717
+ if (typeof v === 'string') { try { return JSON.parse(v); } catch (e) { return v; } }
2718
+ return v;
2719
+ }
2720
+
2721
+ /** JSON 包含语义(PG 的 @> / <@)。swap=true 时判断 target 是否包含 container。 */
2722
+ function jsonContains(a, b, swap) {
2723
+ const container = toJsonValue(swap ? b : a);
2724
+ const target = toJsonValue(swap ? a : b);
2725
+ return jsonContainsValue(container, target);
2726
+ }
2727
+
2728
+ function jsonContainsValue(container, target) {
2729
+ if (target === null || target === undefined) return true;
2730
+ if (Array.isArray(target)) {
2731
+ if (!Array.isArray(container)) return false;
2732
+ return target.every(t => container.some(c => jsonContainsValue(c, t)));
2733
+ }
2734
+ if (typeof target === 'object') {
2735
+ if (container === null || typeof container !== 'object' || Array.isArray(container)) return false;
2736
+ return Object.keys(target).every(k => Object.prototype.hasOwnProperty.call(container, k) && jsonContainsValue(container[k], target[k]));
2737
+ }
2738
+ if (Array.isArray(container)) return container.some(c => c === target || String(c) === String(target));
2739
+ if (container !== null && typeof container === 'object') return false;
2740
+ return container === target || String(container) === String(target);
2741
+ }
2742
+
2743
+ /** JSON 键存在(PG 的 ? / ?| / ?&)。keys 为单个键名或 '{a,b}' 形式的键列表。 */
2744
+ function jsonKeyExists(base, keys, mode) {
2745
+ const v = toJsonValue(base);
2746
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
2747
+ const has = (k) => Object.prototype.hasOwnProperty.call(v, k);
2748
+ if (mode === '?|') return parsePgPath(keys).some(has);
2749
+ if (mode === '?&') return parsePgPath(keys).every(has);
2750
+ return has(String(keys));
1985
2751
  }
1986
2752
 
1987
2753
  function extractEqualPushdown(expr, schema) {
@@ -2033,10 +2799,18 @@ function evaluateExpr(expr, row, ctx) {
2033
2799
  switch (expr.type) {
2034
2800
  case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
2035
2801
  case 'or': return evaluateExpr(expr.left, row, ctx) || evaluateExpr(expr.right, row, ctx);
2802
+ case 'xor': return Boolean(evaluateExpr(expr.left, row, ctx)) !== Boolean(evaluateExpr(expr.right, row, ctx));
2036
2803
  case 'not': return !evaluateExpr(expr.expr, row, ctx);
2037
2804
  case 'compare': {
2038
2805
  const l = resolveOperand(expr.left, row, ctx);
2039
2806
  const r = resolveOperand(expr.right, row, ctx);
2807
+ // MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
2808
+ if (expr.op === '<=>') {
2809
+ const ln = l === null || l === undefined;
2810
+ const rn = r === null || r === undefined;
2811
+ if (ln || rn) return ln && rn;
2812
+ return l === r || String(l) === String(r);
2813
+ }
2040
2814
  // SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
2041
2815
  if (l === null || r === null) return false;
2042
2816
  // 布尔与数值互通(MySQL: 1 = TRUE、0 = FALSE)
@@ -2059,18 +2833,23 @@ function evaluateExpr(expr, row, ctx) {
2059
2833
  }
2060
2834
  case 'like': {
2061
2835
  const v = resolveOperand(expr.operand, row, ctx);
2062
- const r = likeMatch(v, expr.pattern, !!expr.ci);
2836
+ const r = likeMatch(v, expr.pattern, !!expr.ci, expr.escape);
2063
2837
  return expr.not ? !r : r;
2064
2838
  }
2065
2839
  case 'quantified':
2066
2840
  return !!resolveOperand(expr, row, ctx);
2841
+ case 'jsonContains':
2842
+ return !!resolveOperand(expr, row, ctx);
2067
2843
  case 'regexp': {
2068
2844
  const v = resolveOperand(expr.operand, row, ctx);
2069
2845
  if (v === null || v === undefined) return false;
2070
- const re = new RegExp(String(expr.pattern), 'i');
2846
+ // REGEXP / RLIKE 历史上就是大小写不敏感的;`~` 敏感、`~*` 不敏感
2847
+ const re = new RegExp(String(expr.pattern), expr.ci === false ? '' : 'i');
2071
2848
  const r = re.test(String(v));
2072
2849
  return expr.not ? !r : r;
2073
2850
  }
2851
+ case 'jsonKeyExists':
2852
+ return !!resolveOperand(expr, row, ctx);
2074
2853
  case 'isTruth': {
2075
2854
  const v = resolveOperand(expr.operand, row, ctx);
2076
2855
  const isTrue = v === true || v === 1 || v === '1' || v === 'true' || v === 'TRUE' || v === 't' || (typeof v === 'number' && v !== 0);
@@ -2154,16 +2933,22 @@ function windowColumnValue(c, r) {
2154
2933
 
2155
2934
  function scalarColumnValue(c, r, ctx) {
2156
2935
  if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
2936
+ // 聚合列优先:GROUP_CONCAT / STDDEV 等同时带 scalar 与 aggregate
2937
+ if (c.aggregate) {
2938
+ return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column,
2939
+ c.distinct, c.separator !== undefined ? c.separator : (c.scalar && c.scalar.separator),
2940
+ c.orderBy || (c.scalar && c.scalar.orderBy));
2941
+ }
2157
2942
  if (c.scalar) {
2158
2943
  const s = c.scalar;
2159
2944
  if (s && s.type === 'func' && ctx && ctx.ctxAggregates && Object.prototype.hasOwnProperty.call(ctx.ctxAggregates, String(s.name).toUpperCase())) {
2160
2945
  const fn = String(s.name).toUpperCase();
2161
2946
  const col = s.args && s.args[0];
2162
- return ctx._aggValue(ctx.group, fn, col);
2947
+ return ctx._aggValue(ctx.group, fn, col, c.aggregate ? c.distinct : (c.scalar && c.scalar.distinct));
2163
2948
  }
2164
2949
  return resolveOperand(c.scalar, r, ctx);
2165
2950
  }
2166
- if (c.aggregate) return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column);
2951
+
2167
2952
  if (c.literal !== undefined) return c.literal;
2168
2953
  if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r, ctx);
2169
2954
  if (c.expr === '*') return r[Object.keys(r).find(k => !k.startsWith('_'))];
@@ -2186,7 +2971,10 @@ function buildCreateTableSql(name, schema) {
2186
2971
  const seg = ['`' + col + '`', sqlTypeName(def.type)];
2187
2972
  if (def.autoIncrement) seg.push('AUTO_INCREMENT');
2188
2973
  if (def.nullable === false) seg.push('NOT NULL');
2189
- if (def.default !== undefined) seg.push('DEFAULT ' + (typeof def.default === 'string' ? "'" + def.default + "'" : def.default));
2974
+ if (def.default !== undefined) {
2975
+ // defaultExpr(CURRENT_TIMESTAMP 等)不加引号,普通字符串默认值才加
2976
+ seg.push('DEFAULT ' + (typeof def.default === 'string' && !def.defaultExpr ? "'" + def.default + "'" : def.default));
2977
+ }
2190
2978
  return seg.join(' ');
2191
2979
  });
2192
2980
  const pks = Object.keys(schema).filter(k => schema[k].primaryKey);
@@ -2347,13 +3135,47 @@ class SQLExecutor {
2347
3135
  dataRows = dataRows.map(stripDefault);
2348
3136
  }
2349
3137
  const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
3138
+ const uniqueCols = schema ? Object.keys(schema).filter(k => schema[k].unique && !schema[k].primaryKey) : [];
2350
3139
  let toInsert = dataRows;
2351
3140
  let updated = 0;
2352
3141
  let skipped = 0;
3142
+ // 非主键 UNIQUE 约束:默认动作(无 ON CONFLICT / IGNORE / REPLACE)下违反即 ER_DUP_ENTRY。
3143
+ // 主键冲突由下面的冲突逻辑处理;这里补唯一列(含批内重复)。
3144
+ if (uniqueCols.length > 0 && this._conflictAction(statement) === 'throw') {
3145
+ const existing = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 }))
3146
+ .map(r => normalizeRow(r, schema));
3147
+ for (const col of uniqueCols) {
3148
+ const used = new Set();
3149
+ for (const r of existing) {
3150
+ const v = r[col];
3151
+ if (v !== undefined && v !== null) used.add(String(v));
3152
+ }
3153
+ for (const d of dataRows) {
3154
+ const v = d[col];
3155
+ if (v === undefined || v === null) continue; // UNIQUE 允许多个 NULL
3156
+ const s = String(v);
3157
+ if (used.has(s)) throw new Error(`ER_DUP_ENTRY: Duplicate entry '${s}' for unique column '${col}'`);
3158
+ used.add(s);
3159
+ }
3160
+ }
3161
+ }
2353
3162
  if (pkCols.length > 0) {
2354
3163
  const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
2355
3164
  const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
2356
- const explicit = dataRows.filter(hasExplicitPk);
3165
+ // 冲突判定的额外列组:
3166
+ // ON CONFLICT (col) DO UPDATE → 指定列(PG)
3167
+ // ON DUPLICATE KEY UPDATE → 各唯一列(MySQL:任一唯一键冲突都触发)
3168
+ // 此前只认主键,导致这两者在唯一列冲突时会插入重复行而不是更新。
3169
+ const ocTarget = (statement.onConflict && Array.isArray(statement.onConflict.target))
3170
+ ? statement.onConflict.target.map(c => (c && c.name !== undefined ? c.name : c)).filter(Boolean)
3171
+ : null;
3172
+ const extraIdx = [];
3173
+ if (ocTarget && ocTarget.length) extraIdx.push(ocTarget);
3174
+ // 唯一列始终参与冲突判定(PG 的 ON CONFLICT 与 MySQL 的 ON DUPLICATE 都包含唯一键)
3175
+ for (const c of uniqueCols) extraIdx.push([c]);
3176
+ const hasAllOf = (cols) => (row) => cols.every(c => row[c] !== undefined && row[c] !== null);
3177
+
3178
+ const explicit = dataRows.filter((row) => hasExplicitPk(row) || extraIdx.some((cols) => hasAllOf(cols)(row)));
2357
3179
  if (explicit.length > 0) {
2358
3180
  const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
2359
3181
  const pkMap = new Map();
@@ -2361,15 +3183,31 @@ class SQLExecutor {
2361
3183
  const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
2362
3184
  if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
2363
3185
  }
3186
+ const pkValsOf = (row) => pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
3187
+ const extraMaps = extraIdx.map((cols) => {
3188
+ const m = new Map();
3189
+ const pick = hasAllOf(cols);
3190
+ const key = (row) => cols.map(c => String(row[c])).join('|');
3191
+ for (const row of all) {
3192
+ if (!pick(row)) continue;
3193
+ m.set(key(row), pkValsOf(row));
3194
+ }
3195
+ return { pick, key, m };
3196
+ });
2364
3197
  const conflicts = [];
2365
3198
  const fresh = [];
2366
3199
  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);
3200
+ let existingId = null;
3201
+ if (hasExplicitPk(d) && pkMap.has(keyOf(d))) existingId = pkMap.get(keyOf(d));
3202
+ if (!existingId) {
3203
+ for (const { pick, key, m } of extraMaps) {
3204
+ if (!pick(d)) continue;
3205
+ const k = key(d);
3206
+ if (m.has(k)) { existingId = m.get(k); break; }
3207
+ }
2372
3208
  }
3209
+ if (existingId) conflicts.push({ d, existingId });
3210
+ else fresh.push(d);
2373
3211
  }
2374
3212
  const conflictAction = this._conflictAction(statement);
2375
3213
  const conflictSets = this._conflictSets(statement);
@@ -2396,7 +3234,16 @@ class SQLExecutor {
2396
3234
  for (const { d, existingId } of conflicts) {
2397
3235
  const data = {};
2398
3236
  if (conflictSets) {
2399
- for (const [col, val] of conflictSets) data[col] = val;
3237
+ for (const [col, val] of conflictSets) {
3238
+ // VALUES(col) 引用本次本应插入的值;EXCLUDED.col 为 PG 同义写法
3239
+ if (val && typeof val === 'object' && val._valuesOf !== undefined) {
3240
+ data[col] = d[val._valuesOf] !== undefined ? d[val._valuesOf] : null;
3241
+ } else if (val && typeof val === 'object' && val._excludedOf !== undefined) {
3242
+ data[col] = d[val._excludedOf] !== undefined ? d[val._excludedOf] : null;
3243
+ } else {
3244
+ data[col] = val;
3245
+ }
3246
+ }
2400
3247
  } else {
2401
3248
  for (const k of Object.keys(d)) if (!pkCols.includes(k)) data[k] = d[k];
2402
3249
  }
@@ -2525,6 +3372,52 @@ class SQLExecutor {
2525
3372
  delete this.engine._views[key];
2526
3373
  return { ok: true, type: 'dropView', message: `View '${statement.name}' dropped`, affectedRows: 0, warnings: [] };
2527
3374
  }
3375
+ case 'createIndex': {
3376
+ const tName = statement.table;
3377
+ if (typeof this.engine.hasTable === 'function' && !this.engine.hasTable(tName)) {
3378
+ throw new Error(`Table '${tName}' does not exist`);
3379
+ }
3380
+ let handled = false;
3381
+ if (typeof this.engine.createIndex === 'function') {
3382
+ await this.engine.createIndex(tName, statement.columns, { unique: statement.unique, name: statement.name });
3383
+ handled = true;
3384
+ } else if (typeof this.engine._ensureTable === 'function') {
3385
+ const tbl = this.engine._ensureTable(tName) || (this.engine._tables && this.engine._tables[tName]);
3386
+ if (tbl && typeof tbl.createIndex === 'function') {
3387
+ // Table.createIndex 目前是单字段接口,复合索引按逐列建
3388
+ for (const col of statement.columns) tbl.createIndex(col);
3389
+ handled = true;
3390
+ }
3391
+ }
3392
+ if (!handled) {
3393
+ throw new Error(
3394
+ `CREATE INDEX is not supported by this engine (no createIndex/_ensureTable on the engine object)`
3395
+ );
3396
+ }
3397
+ return {
3398
+ ok: true, type: 'createIndex', table: tName, name: statement.name,
3399
+ affectedRows: 0, warnings: [],
3400
+ message: `Index '${statement.name}' created on ${tName}(${statement.columns.join(', ')})`,
3401
+ };
3402
+ }
3403
+ case 'dropIndex': {
3404
+ const tName2 = statement.table;
3405
+ let handled = false;
3406
+ if (typeof this.engine.dropIndex === 'function') {
3407
+ await this.engine.dropIndex(tName2, statement.name);
3408
+ handled = true;
3409
+ } else if (tName2 && typeof this.engine._ensureTable === 'function') {
3410
+ const tbl = this.engine._ensureTable(tName2) || (this.engine._tables && this.engine._tables[tName2]);
3411
+ if (tbl && typeof tbl.dropIndex === 'function') { tbl.dropIndex(statement.name); handled = true; }
3412
+ }
3413
+ if (!handled) {
3414
+ if (statement.ifExists) {
3415
+ return { ok: true, type: 'dropIndex', affectedRows: 0, skipped: true, warnings: [], message: `Index '${statement.name}' did not exist` };
3416
+ }
3417
+ throw new Error(`DROP INDEX is not supported by this engine (no dropIndex on the engine/table object)`);
3418
+ }
3419
+ return { ok: true, type: 'dropIndex', table: tName2, name: statement.name, affectedRows: 0, warnings: [], message: `Index '${statement.name}' dropped` };
3420
+ }
2528
3421
  case 'savepoint': {
2529
3422
  // 引擎侧没有保存点/快照能力。这里只登记名字,绝不假装能回滚。
2530
3423
  if (!this.engine._savepoints) this.engine._savepoints = new Set();
@@ -2582,11 +3475,27 @@ class SQLExecutor {
2582
3475
  const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
2583
3476
  const ids = [];
2584
3477
  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
- }
3478
+ let candidates = all.filter(row => !statement.where || evaluateExpr(statement.where, row, this.ctx));
3479
+ // DELETE ... ORDER BY ... LIMIT n
3480
+ if (statement.orderBy) {
3481
+ const cmp = (a, b) => {
3482
+ for (const o of statement.orderBy) {
3483
+ const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
3484
+ const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
3485
+ if (av === bv) continue;
3486
+ if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
3487
+ if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
3488
+ const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
3489
+ if (d !== 0) return o.dir === 'asc' ? d : -d;
3490
+ }
3491
+ return 0;
3492
+ };
3493
+ candidates = candidates.slice().sort(cmp);
3494
+ }
3495
+ if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
3496
+ for (const row of candidates) {
3497
+ const id = this._rowPkId(row, pkCols);
3498
+ if (id !== undefined) { ids.push(id); removedRows.push(row); }
2590
3499
  }
2591
3500
  if (ids.length > 0) {
2592
3501
  if (this.engine.removeByIds) await this.engine.removeByIds(statement.table, ids);
@@ -3114,12 +4023,28 @@ class SQLExecutor {
3114
4023
  return out;
3115
4024
  }
3116
4025
 
3117
- _aggValue(rows, fn, column) {
3118
- if (fn === 'COUNT') return rows.length;
4026
+ _aggValue(rows, fn, column, distinct, separator, orderBy) {
3119
4027
  const op = typeof column === 'string' ? { type: 'column', name: column } : column;
3120
- const values = op && op.type === 'star'
4028
+ // COUNT(*) / COUNT():参数为 star 或缺失,直接按行数算
4029
+ const noColumn = !op || op.type === 'star';
4030
+ let values = noColumn
3121
4031
  ? rows.map(r => 1)
3122
- : rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
4032
+ : op && op.type === 'tuple'
4033
+ // COUNT(DISTINCT a, b):先把多列拼成元组再聚合
4034
+ ? rows.map(r => JSON.stringify(op.items.map(it => resolveOperand(it, r, this.ctx))))
4035
+ .filter(v => v !== null && v !== undefined)
4036
+ : rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
4037
+ // COUNT(DISTINCT x) / SUM(DISTINCT x):先按值去重再聚合
4038
+ if (distinct) {
4039
+ const seen = new Set();
4040
+ values = values.filter(v => {
4041
+ const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
4042
+ if (seen.has(k)) return false;
4043
+ seen.add(k);
4044
+ return true;
4045
+ });
4046
+ }
4047
+ if (fn === 'COUNT') return values.length;
3123
4048
  if (this.ctx && this.ctx.aggregates && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
3124
4049
  const agg = this.ctx.aggregates[fn];
3125
4050
  if (typeof agg === 'function') {
@@ -3135,6 +4060,54 @@ class SQLExecutor {
3135
4060
  if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
3136
4061
  if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
3137
4062
  if (fn === 'MAX') return values.length ? Math.max(...values.map(v => Number(v))) : null;
4063
+ if (fn === 'FIRST') return values.length ? values[0] : null;
4064
+ if (fn === 'LAST') return values.length ? values[values.length - 1] : null;
4065
+ if (fn === 'GROUP_CONCAT') {
4066
+ let src = rows;
4067
+ // GROUP_CONCAT(expr ORDER BY col [DESC]):先按内部排序键排好行,再取值拼接
4068
+ if (orderBy && orderBy.length) {
4069
+ const cmp = (a, b) => {
4070
+ for (const o of orderBy) {
4071
+ const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
4072
+ const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
4073
+ if (av === bv) continue;
4074
+ if (av === null || av === undefined) return o.dir === 'asc' ? 1 : -1;
4075
+ if (bv === null || bv === undefined) return o.dir === 'asc' ? -1 : 1;
4076
+ const d = (typeof av === 'number' && typeof bv === 'number')
4077
+ ? av - bv : String(av).localeCompare(String(bv));
4078
+ if (d !== 0) return o.dir === 'asc' ? d : -d;
4079
+ }
4080
+ return 0;
4081
+ };
4082
+ src = rows.slice().sort(cmp);
4083
+ }
4084
+ const parts = src.map(r => resolveOperand(op, r, this.ctx));
4085
+ let joined = parts.filter(v => v !== null && v !== undefined);
4086
+ if (distinct) {
4087
+ const seen = new Set();
4088
+ joined = joined.filter(v => {
4089
+ const k = typeof v === 'object' ? JSON.stringify(v) : String(v);
4090
+ if (seen.has(k)) return false;
4091
+ seen.add(k);
4092
+ return true;
4093
+ });
4094
+ }
4095
+ if (joined.length === 0) return null;
4096
+ const sep = (separator !== undefined && separator !== null) ? String(separator) : ',';
4097
+ return joined.map(v => (v === null || v === undefined ? '' : String(v))).join(sep);
4098
+ }
4099
+ // 标准差 / 方差族:_POP 为总体、_SAMP(及无后缀 VAR/STDDEV)为样本
4100
+ if (fn === 'STDDEV' || fn === 'STDDEV_POP' || fn === 'STDDEV_SAMP' ||
4101
+ fn === 'VARIANCE' || fn === 'VAR_POP' || fn === 'VAR_SAMP') {
4102
+ const nums = values.map(v => Number(v)).filter(v => Number.isFinite(v));
4103
+ if (nums.length === 0) return null;
4104
+ const pop = (fn === 'STDDEV_POP' || fn === 'VAR_POP');
4105
+ const mean = nums.reduce((s, v) => s + v, 0) / nums.length;
4106
+ const denom = pop ? nums.length : nums.length - 1; // 样本方差用 n-1
4107
+ if (denom <= 0) return null;
4108
+ const variance = nums.reduce((s, v) => s + (v - mean) * (v - mean), 0) / denom;
4109
+ return fn.indexOf('STDDEV') === 0 ? Math.sqrt(variance) : variance;
4110
+ }
3138
4111
  return null;
3139
4112
  }
3140
4113
 
@@ -3142,7 +4115,7 @@ class SQLExecutor {
3142
4115
  if (!node || typeof node !== 'object') return;
3143
4116
  if (node.type === 'aggregate') {
3144
4117
  node.type = 'value';
3145
- node.value = this._aggValue(group, node.fn, node.column);
4118
+ node.value = this._aggValue(group, node.fn, node.column, node.distinct, node.separator, node.orderBy);
3146
4119
  delete node.fn;
3147
4120
  delete node.column;
3148
4121
  return;
@@ -3211,6 +4184,61 @@ class SQLExecutor {
3211
4184
  return out;
3212
4185
  }
3213
4186
 
4187
+ /**
4188
+ * 校验语句里引用的列是否真实存在。
4189
+ * 之前 `SELECT nosuchcol FROM t` 会静默返回 null 而不是报错 —— 调用方拿到的
4190
+ * 是"一列 null",往往被当成"数据为空",比直接报 no such column 更难排查。
4191
+ *
4192
+ * 只在能确定列名集合时校验(取样本行的键),空表不敢断言,宁可不报。
4193
+ * 不深入子查询节点:它们有自己的 FROM,列属于另一个作用域。
4194
+ */
4195
+ _validateColumnRefs(statement, rows) {
4196
+ if (!statement || !Array.isArray(rows) || rows.length === 0) return;
4197
+ const sample = rows[0];
4198
+ if (!sample || typeof sample !== 'object') return;
4199
+ const known = new Set();
4200
+ for (const k of Object.keys(sample)) known.add(String(k).toLowerCase());
4201
+ // HAVING / ORDER BY 允许引用 SELECT 的输出别名(如 HAVING cnt >= 1),
4202
+ // 这些名字不在表里,必须一并视为已知,否则会误报 no such column
4203
+ if (Array.isArray(statement.columns)) {
4204
+ for (const c of statement.columns) {
4205
+ if (c && typeof c.alias === 'string' && c.alias) known.add(c.alias.toLowerCase());
4206
+ }
4207
+ }
4208
+
4209
+ const walk = (node) => {
4210
+ if (!node || typeof node !== 'object') return;
4211
+ if (Array.isArray(node)) { for (const n of node) walk(n); return; }
4212
+ // 子查询/嵌套 SELECT 有自己的表,跳过
4213
+ if (node.type === 'select' || node.type === 'subquery') return;
4214
+ if (node.type === 'column' && typeof node.name === 'string') {
4215
+ const n = node.name.toLowerCase();
4216
+ const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
4217
+ if (!known.has(n) && !known.has(base)) {
4218
+ throw new Error(`no such column: ${node.name}`);
4219
+ }
4220
+ }
4221
+ for (const k of Object.keys(node)) {
4222
+ if (k === 'select' || k === 'subquery') continue;
4223
+ walk(node[k]);
4224
+ }
4225
+ };
4226
+
4227
+ walk(statement.columns);
4228
+ walk(statement.where);
4229
+ walk(statement.having);
4230
+ walk(statement.orderBy);
4231
+ if (Array.isArray(statement.groupBy)) {
4232
+ // groupBy 是列名字符串数组
4233
+ for (const g of statement.groupBy) {
4234
+ if (typeof g !== 'string') continue;
4235
+ const n = g.toLowerCase();
4236
+ const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
4237
+ if (!known.has(n) && !known.has(base)) throw new Error(`no such column: ${g}`);
4238
+ }
4239
+ }
4240
+ }
4241
+
3214
4242
  /** 收集子树里所有列名(含限定形式 alias.col) */
3215
4243
  _collectColumnNames(node, out = []) {
3216
4244
  if (!node || typeof node !== 'object') return out;
@@ -3446,7 +4474,7 @@ class SQLExecutor {
3446
4474
  case 'MAX': {
3447
4475
  const f = frameIdx(pos);
3448
4476
  const slice = ordered.slice(f.start, f.end + 1).map(i => rows[i]);
3449
- v = this._aggValue(slice, w.fn, argNode);
4477
+ v = this._aggValue(slice, w.fn, argNode, !!(w.spec && w.spec.distinct), w.spec && w.spec.separator);
3450
4478
  break;
3451
4479
  }
3452
4480
  default: v = null;
@@ -3639,6 +4667,7 @@ class SQLExecutor {
3639
4667
  }
3640
4668
 
3641
4669
  let rows = all;
4670
+ this._validateColumnRefs(statement, rows);
3642
4671
  if (statement.where) {
3643
4672
  // 相关子查询:必须逐行重新求值,否则外层引用拿不到当前行的值
3644
4673
  const outerAliases = this._outerAliases(statement);
@@ -3681,7 +4710,7 @@ class SQLExecutor {
3681
4710
  for (const c of statement.columns) {
3682
4711
  if (c.aggregate) {
3683
4712
  const group = r._group || [r];
3684
- const v = this._aggValue(group, c.aggregate || 'COUNT', c.column);
4713
+ const v = this._aggValue(group, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
3685
4714
  ctx['__agg_' + (c.alias || c.column || 'COUNT(*)')] = v;
3686
4715
  if (c.column) ctx[c.column] = v;
3687
4716
  if (c.alias) ctx[c.alias] = v;
@@ -3702,7 +4731,7 @@ class SQLExecutor {
3702
4731
  const key = JSON.stringify(statement.columns.map(c => {
3703
4732
  if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
3704
4733
  if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
3705
- if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column);
4734
+ if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
3706
4735
  return resolveOperand({ type: 'column', name: c.expr }, r, this.ctx);
3707
4736
  }));
3708
4737
  if (seen.has(key)) return false;
@@ -3717,7 +4746,7 @@ class SQLExecutor {
3717
4746
  if (!statement.groupBy && (statement.columns.some(c => c.aggregate && !c.window) || hasCustomAgg)) {
3718
4747
  const cols = statement.columns.map(c => scalarColumnName(c));
3719
4748
  const valueOf = (c) => {
3720
- if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column);
4749
+ if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
3721
4750
  if (c.scalar && c.scalar.type === 'func' && hasCustomAgg) {
3722
4751
  const fn = String(c.scalar.name).toUpperCase();
3723
4752
  if (Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
@@ -3801,53 +4830,88 @@ class SQLExecutor {
3801
4830
  }
3802
4831
  }
3803
4832
 
3804
- function splitStatements(sql) {
3805
- const statements = [];
3806
- let current = '';
3807
- let inStr = null; // "'" | '"' | '`'
3808
- let i = 0;
3809
- while (i < sql.length) {
4833
+ /** 跳过一个引号串(含转义/反引号成对),返回闭合引号之后的下标 */
4834
+ function skipQuoted(sql, i, q) {
4835
+ const n = sql.length;
4836
+ i++; // 跳过起始引号
4837
+ while (i < n) {
3810
4838
  const c = sql[i];
3811
- if (inStr) {
3812
- current += c;
3813
- if (inStr === '`') {
3814
- // 反引号标识符内部反斜杠不是转义符;两个连续反引号才表示一个字面反引号。
3815
- if (c === '`' && sql[i + 1] === '`') { current += sql[i + 1]; i += 2; continue; }
3816
- if (c === '`') inStr = null;
3817
- i++;
3818
- continue;
4839
+ if (q === '`') {
4840
+ // 反引号标识符内部反斜杠不是转义符;两个连续反引号表示一个字面反引号。
4841
+ if (c === '`') {
4842
+ if (sql[i + 1] === '`') { i += 2; continue; }
4843
+ return i + 1;
3819
4844
  }
3820
- if (c === '\\' && i + 1 < sql.length) { current += sql[i + 1]; i += 2; continue; }
3821
- if (c === inStr) inStr = null;
3822
4845
  i++;
3823
4846
  continue;
3824
4847
  }
3825
- if (c === "'" || c === '"' || c === '`') { inStr = c; current += c; i++; continue; }
4848
+ if (c === '\\') { i += 2; continue; }
4849
+ if (c === q) return i + 1;
4850
+ i++;
4851
+ }
4852
+ return n; // 未闭合:到结尾
4853
+ }
4854
+
4855
+ function splitStatements(sql, opts = {}) {
4856
+ if (typeof sql !== 'string') {
4857
+ throw new Error('splitStatements: sql must be a string');
4858
+ }
4859
+ const maxLength = opts.maxLength == null ? DEFAULT_MAX_SQL_LENGTH : opts.maxLength;
4860
+ if (typeof maxLength === 'number' && Number.isFinite(maxLength) && sql.length > maxLength) {
4861
+ throw new Error(
4862
+ `SQL text too large: ${sql.length} chars exceeds limit of ${maxLength} ` +
4863
+ `(raise opts.maxSqlLength to allow larger input)`
4864
+ );
4865
+ }
4866
+ const statements = [];
4867
+ // 用「区间切片」收集,而不是逐字符拼接:既避免 `current += c` 的 O(n²) 拷贝放大,
4868
+ // 也避免为每个字符建一个小字符串对象(20MB 无分号输入下两者都会打爆默认堆)。
4869
+ let parts = [];
4870
+ const take = end => { parts.push(sql.slice(segStart, end)); };
4871
+ const flush = () => {
4872
+ if (parts.length === 0) return;
4873
+ const stmt = parts.join('').trim();
4874
+ parts = [];
4875
+ if (stmt) statements.push(stmt);
4876
+ };
4877
+
4878
+ let segStart = 0;
4879
+ let i = 0;
4880
+ const n = sql.length;
4881
+ while (i < n) {
4882
+ const c = sql[i];
4883
+ if (c === "'" || c === '"' || c === '`') { i = skipQuoted(sql, i, c); continue; }
3826
4884
  if (c === '-' && sql[i + 1] === '-') {
3827
- while (i < sql.length && sql[i] !== '\n') i++;
4885
+ // 行注释:丢弃注释文本本身(与原实现一致),保留其后的换行
4886
+ take(i);
4887
+ while (i < n && sql[i] !== '\n') i++;
4888
+ segStart = i;
3828
4889
  continue;
3829
4890
  }
3830
- if (c === '#') {
3831
- while (i < sql.length && sql[i] !== '\n') i++;
4891
+ if (c === '#' && sql[i + 1] !== '>') {
4892
+ take(i);
4893
+ while (i < n && sql[i] !== '\n') i++;
4894
+ segStart = i;
3832
4895
  continue;
3833
4896
  }
3834
4897
  if (c === '/' && sql[i + 1] === '*') {
3835
- current += '/*';
4898
+ // 块注释:原实现会保留 `/* */` 原文,这里通过切片一并保留
3836
4899
  i += 2;
3837
- while (i + 1 < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) { current += sql[i]; i++; }
3838
- if (i + 1 < sql.length) { current += '*/'; i += 2; }
4900
+ while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
4901
+ i = i + 1 < n ? i + 2 : n;
3839
4902
  continue;
3840
4903
  }
3841
4904
  if (c === ';') {
3842
- if (current.trim()) statements.push(current.trim());
3843
- current = '';
4905
+ take(i);
4906
+ flush();
3844
4907
  i++;
4908
+ segStart = i;
3845
4909
  continue;
3846
4910
  }
3847
- current += c;
3848
4911
  i++;
3849
4912
  }
3850
- if (current.trim()) statements.push(current.trim());
4913
+ take(n);
4914
+ flush();
3851
4915
  return statements;
3852
4916
  }
3853
4917
 
@@ -3874,7 +4938,7 @@ function hasComments(sql) {
3874
4938
  if (c === "'" || c === '"') { inStr = c; i++; continue; }
3875
4939
  if (c === '`') { i++; while (i < sql.length && sql[i] !== '`') i++; i++; continue; }
3876
4940
  if (c === '-' && sql[i + 1] === '-') return true;
3877
- if (c === '#') return true;
4941
+ if (c === '#' && sql[i + 1] !== '>') return true; // '#>' / '#>>' 是 JSON 操作符,不是注释
3878
4942
  if (c === '/' && sql[i + 1] === '*') return true;
3879
4943
  i++;
3880
4944
  }
@@ -3923,16 +4987,34 @@ function applyParams(sql, values) {
3923
4987
  if (args && typeof args === 'object' && !Array.isArray(args)) {
3924
4988
  hasNamed = true;
3925
4989
  }
3926
- while (i < sql.length) {
4990
+ const n = sql.length;
4991
+ while (i < n) {
3927
4992
  const c = sql[i];
3928
4993
  if (inStr) {
3929
4994
  out += c;
3930
- if (c === '\\' && i + 1 < sql.length) { out += sql[i + 1]; i += 2; continue; }
4995
+ if (c === '\\' && i + 1 < n) { out += sql[i + 1]; i += 2; continue; }
3931
4996
  if (c === inStr) inStr = null;
3932
4997
  i++;
3933
4998
  continue;
3934
4999
  }
3935
5000
  if (c === "'" || c === '"' || c === '`') { inStr = c; out += c; i++; continue; }
5001
+ // 跳过注释:`--` 和 `#`(但 `#>` / `#>>` 是 JSON 操作符,不是注释)
5002
+ if (c === '-' && sql[i + 1] === '-') {
5003
+ // 行注释:整段跳过,保留换行(不在注释里的字符不进入 out)
5004
+ while (i < n && sql[i] !== '\n') i++;
5005
+ continue;
5006
+ }
5007
+ if (c === '#' && sql[i + 1] !== '>') {
5008
+ while (i < n && sql[i] !== '\n') i++;
5009
+ continue;
5010
+ }
5011
+ if (c === '/' && sql[i + 1] === '*') {
5012
+ // 块注释:整段跳过
5013
+ i += 2;
5014
+ while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
5015
+ if (i + 1 < n) i += 2; else i = n;
5016
+ continue;
5017
+ }
3936
5018
  if (c === '?' && sql[i + 1] === '?') {
3937
5019
  if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
3938
5020
  out += escapeId(hasNamed ? args['@@'] : args[idx++]);
@@ -3965,6 +5047,24 @@ function applyParams(sql, values) {
3965
5047
  i++;
3966
5048
  continue;
3967
5049
  }
5050
+ // $N 编号占位符(PG 风格):$1 / $2 ...
5051
+ if (c === '$' && i + 1 < sql.length && sql[i + 1] >= '0' && sql[i + 1] <= '9') {
5052
+ let num = '';
5053
+ let j = i + 1;
5054
+ while (j < sql.length && sql[j] >= '0' && sql[j] <= '9') { num += sql[j]; j++; }
5055
+ const n = parseInt(num, 10);
5056
+ if (hasNamed) {
5057
+ if (!(n in args)) throw new Error(`No value for parameter $${n}`);
5058
+ out += escapeValue(args[n]);
5059
+ } else {
5060
+ if (n - 1 >= args.length) throw new Error(`Not enough parameters for SQL: expected $${n}`);
5061
+ out += escapeValue(args[n - 1]);
5062
+ if (n > idx) idx = n;
5063
+ }
5064
+ count++;
5065
+ i = j;
5066
+ continue;
5067
+ }
3968
5068
  if ((c === ':' || c === '@' || c === '$') && i + 1 < sql.length && /[A-Za-z_]/.test(sql[i + 1])) {
3969
5069
  // 命名占位符 :name @name $name
3970
5070
  let name = '';
@@ -3986,6 +5086,44 @@ function applyParams(sql, values) {
3986
5086
  return out;
3987
5087
  }
3988
5088
 
5089
+ // 把执行器结果补齐为 README 承诺的结果信封
5090
+ // (columnTypes / rowCount / message / command / durationMs / warnings)
5091
+ function buildResultEnvelope(stmt, r, durationMs) {
5092
+ if (r === null || typeof r !== 'object' || Array.isArray(r)) return r;
5093
+ const out = { ...r };
5094
+ const t = stmt && stmt.type;
5095
+ if (out.command === undefined) out.command = String(t || '').toUpperCase();
5096
+ if (out.affectedRows === undefined) out.affectedRows = 0;
5097
+ if (out.rowCount === undefined) {
5098
+ out.rowCount = Array.isArray(out.rows) ? out.rows.length
5099
+ : (typeof out.affectedRows === 'number' ? out.affectedRows : 0);
5100
+ }
5101
+ if (out.warnings === undefined) out.warnings = [];
5102
+ if (out.durationMs === undefined) out.durationMs = durationMs;
5103
+ if (out.message === undefined) {
5104
+ out.message = t === 'select'
5105
+ ? `${out.rowCount} row${out.rowCount === 1 ? '' : 's'} selected`
5106
+ : `${out.affectedRows} row${out.affectedRows === 1 ? '' : 's'} affected`;
5107
+ }
5108
+ return out;
5109
+ }
5110
+
5111
+ // SELECT 结果的列类型:单表查询按 schema 推断;表达式/多表查询该列为 null
5112
+ async function inferColumnTypes(engine, stmt, r) {
5113
+ if (!r || !Array.isArray(r.columns)) return undefined;
5114
+ let schema = null;
5115
+ try {
5116
+ const t = stmt && stmt.type === 'select' && stmt.from &&
5117
+ Array.isArray(stmt.from.tables) && stmt.from.tables.length === 1
5118
+ ? stmt.from.tables[0].table : null;
5119
+ if (t && engine && typeof engine.getTableSchema === 'function') schema = await engine.getTableSchema(t);
5120
+ } catch (e) { schema = null; }
5121
+ return r.columns.map((name) => {
5122
+ const def = schema && schema[name];
5123
+ return def && def.type ? sqlTypeName(def.type).toUpperCase() : null;
5124
+ });
5125
+ }
5126
+
3989
5127
  async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
3990
5128
  // 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
3991
5129
  // 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
@@ -4009,14 +5147,19 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
4009
5147
  if (Array.isArray(paramsOrOpts)) {
4010
5148
  sql = applyParams(sql, paramsOrOpts);
4011
5149
  } else if (paramsOrOpts && typeof paramsOrOpts === 'object') {
4012
- opts = paramsOrOpts;
5150
+ // 第三参为对象时:含已知 opts 键 → 视为 opts;否则视为命名参数({id: 1} → :id / $id)
5151
+ const OPTS_KEYS = ['safety', 'session', 'params', 'functions', 'aggregates',
5152
+ 'allowComments', 'maxStatements', 'maxSqlLength', 'context', 'timeout', 'dialect'];
5153
+ const looksLikeOpts = Object.keys(paramsOrOpts).some(k => OPTS_KEYS.includes(k));
5154
+ if (looksLikeOpts) opts = paramsOrOpts;
5155
+ else sql = applyParams(sql, paramsOrOpts);
4013
5156
  }
4014
5157
  if (opts.safety !== false) {
4015
5158
  if (!opts.allowComments && hasComments(sql)) {
4016
5159
  throw new Error('SQL comments are disabled for security (--, #, /* */)');
4017
5160
  }
4018
5161
  }
4019
- let statements = splitStatements(sql);
5162
+ let statements = splitStatements(sql, { maxLength: opts.maxSqlLength });
4020
5163
  if (opts.maxStatements != null && statements.length > opts.maxStatements) {
4021
5164
  throw new Error(`too many statements (${statements.length} > ${opts.maxStatements})`);
4022
5165
  }
@@ -4035,7 +5178,14 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
4035
5178
  const results = [];
4036
5179
  for (const stmtSql of statements) {
4037
5180
  const stmt = parseSQL(stmtSql);
4038
- results.push(await executor.execute(stmt));
5181
+ const t0 = process.hrtime.bigint();
5182
+ let r = await executor.execute(stmt);
5183
+ const durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
5184
+ r = buildResultEnvelope(stmt, r, Math.round(durationMs * 1000) / 1000);
5185
+ if (r && r.columns && r.columnTypes === undefined) {
5186
+ r.columnTypes = await inferColumnTypes(engine, stmt, r);
5187
+ }
5188
+ results.push(r);
4039
5189
  }
4040
5190
  return results.length === 1 ? results[0] : results;
4041
5191
  }