jsql-neo 6.0.2 → 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,9 +206,22 @@ 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
222
  if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
182
- two === '<<' || two === '>>') {
223
+ two === '<<' || two === '>>' || two === '->' || two === '::' ||
224
+ two === '&&' || two === '||' || two === '~*' || two === '!~') {
183
225
  tokens.push(new SQLToken('op', two, i));
184
226
  i += 2;
185
227
  continue;
@@ -523,19 +565,27 @@ class Parser {
523
565
  parseColumnDef() {
524
566
  const def = {};
525
567
  const typeTok = this.next();
526
- if (typeTok.type !== 'keyword') throw new Error(`Expected column type, got '${typeTok.value}'`);
527
- 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();
528
574
  const typeMap = {
529
575
  integer: 'integer', int: 'integer', bigint: 'integer', tinyint: 'integer', smallint: 'integer',
530
576
  string: 'string', text: 'string', varchar: 'string', char: 'string',
531
577
  float: 'number', double: 'number', real: 'number', numeric: 'number', decimal: 'number',
532
578
  boolean: 'boolean', bool: 'boolean',
533
- date: 'date', datetime: 'datetime', timestamp: 'timestamp',
534
- 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'
535
584
  };
536
585
  const mapped = typeMap[type];
537
586
  if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
538
587
  def.type = mapped;
588
+ if (/^(BIG)?SERIAL$/i.test(typeRaw)) { def.autoIncrement = true; def.required = true; }
539
589
 
540
590
  // 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
541
591
  if (this.peek().type === 'op' && this.peek().value === '(') {
@@ -598,6 +648,11 @@ class Parser {
598
648
  def.defaultExpr = true;
599
649
  } else {
600
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
+ }
601
656
  }
602
657
  break;
603
658
  case 'COLLATE':
@@ -772,7 +827,7 @@ class Parser {
772
827
  for (;;) {
773
828
  const col = this.parseColumnRef();
774
829
  this.expect('op', '=');
775
- const val = this.parseValue();
830
+ const val = this.parseConflictValue();
776
831
  sets.push([col, val]);
777
832
  if (this.peek().value === ',') { this.next(); continue; }
778
833
  break;
@@ -852,6 +907,21 @@ class Parser {
852
907
  throw new Error(`Expected value, got '${t.value}'`);
853
908
  }
854
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
+
855
925
  /**
856
926
  * WITH [RECURSIVE] name [(col, ...)] AS (SELECT ...), ... <statement>
857
927
  * CTE 在语法层解析为 { type:'with', ctes:[{name, columns, select}], statement }。
@@ -1628,18 +1698,33 @@ class Parser {
1628
1698
  }
1629
1699
 
1630
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() {
1631
1714
  let left = this.parseAnd();
1632
- while (this.isKeyword('OR')) {
1715
+ while (this.isWord('XOR')) {
1633
1716
  this.next();
1634
1717
  const right = this.parseAnd();
1635
- left = { type: 'or', left, right };
1718
+ left = { type: 'xor', left, right };
1636
1719
  }
1637
1720
  return left;
1638
1721
  }
1639
1722
 
1640
1723
  parseAnd() {
1641
1724
  let left = this.parseNot();
1642
- while (this.isKeyword('AND')) {
1725
+ for (;;) {
1726
+ const isAnd = this.isKeyword('AND') || (this.peek().type === 'op' && this.peek().value === '&&');
1727
+ if (!isAnd) break;
1643
1728
  this.next();
1644
1729
  const right = this.parseNot();
1645
1730
  left = { type: 'and', left, right };
@@ -1981,15 +2066,65 @@ class Parser {
1981
2066
  }
1982
2067
 
1983
2068
  parseTerm() {
1984
- let node = this.parseOperand();
2069
+ let node = this.parsePostfix(this.parseOperand());
1985
2070
  for (;;) {
1986
2071
  const t = this.peek();
1987
2072
  if (t.type === 'op' && (t.value === '*' || t.value === '/' || t.value === '%')) {
1988
2073
  this.next();
1989
- const right = this.parseOperand();
2074
+ const right = this.parsePostfix(this.parseOperand());
1990
2075
  node = { type: 'arith', op: t.value, left: node, right };
1991
2076
  continue;
1992
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
+ }
1993
2128
  break;
1994
2129
  }
1995
2130
  return node;
@@ -2085,7 +2220,31 @@ class Parser {
2085
2220
  const isILike = !(t2.type === 'keyword' && t2.value === 'LIKE');
2086
2221
  this.next();
2087
2222
  const pattern = this.parseValue();
2088
- 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 };
2089
2248
  }
2090
2249
 
2091
2250
  // MySQL 中 RLIKE 是 REGEXP 的同义词
@@ -2095,7 +2254,14 @@ class Parser {
2095
2254
  return { type: 'regexp', operand: left, pattern, not };
2096
2255
  }
2097
2256
 
2098
- 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)) {
2099
2265
  this.next();
2100
2266
  // = ANY (SELECT ...) / > ALL (SELECT ...)
2101
2267
  const anyAll = this.isWord('ANY') || this.isWord('ALL') || this.isWord('SOME');
@@ -2209,6 +2375,8 @@ function resolveOperand(operand, row, ctx) {
2209
2375
  case '*': return l * r;
2210
2376
  case '/': return r === 0 ? null : l / r;
2211
2377
  case '%': return r === 0 ? null : l % r;
2378
+ // MySQL 整数除法 DIV(结果向零取整)
2379
+ case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
2212
2380
  // 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
2213
2381
  case '&': return (Number(l) | 0) & (Number(r) | 0);
2214
2382
  case '|': return (Number(l) | 0) | (Number(r) | 0);
@@ -2229,6 +2397,7 @@ function resolveOperand(operand, row, ctx) {
2229
2397
  case 'compare':
2230
2398
  case 'and':
2231
2399
  case 'or':
2400
+ case 'xor':
2232
2401
  case 'not':
2233
2402
  case 'in':
2234
2403
  case 'isNull':
@@ -2253,6 +2422,44 @@ function resolveOperand(operand, row, ctx) {
2253
2422
  const v = resolveOperand(operand.expr, row, ctx);
2254
2423
  return applyCast(v, operand.dataType);
2255
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
+ }
2256
2463
  case 'quantified': {
2257
2464
  // = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
2258
2465
  const v = resolveOperand(operand.operand, row, ctx);
@@ -2471,13 +2678,76 @@ function seedRand(seed) {
2471
2678
  }
2472
2679
 
2473
2680
  // ci 默认 true:保持 LIKE 既有的大小写不敏感行为不变;ILIKE 同为不敏感。
2474
- function likeMatch(value, pattern, ci = true) {
2681
+ // escape:可选转义符(ESCAPE 子句),默认反斜杠;转义符后跟 % _ 或转义符本身按字面量匹配。
2682
+ function likeMatch(value, pattern, ci = true, escape = '\\') {
2475
2683
  if (typeof value !== 'string') return false;
2476
- const regex = pattern
2477
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
2478
- .replace(/%/g, '.*')
2479
- .replace(/_/g, '.');
2480
- 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));
2481
2751
  }
2482
2752
 
2483
2753
  function extractEqualPushdown(expr, schema) {
@@ -2529,10 +2799,18 @@ function evaluateExpr(expr, row, ctx) {
2529
2799
  switch (expr.type) {
2530
2800
  case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
2531
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));
2532
2803
  case 'not': return !evaluateExpr(expr.expr, row, ctx);
2533
2804
  case 'compare': {
2534
2805
  const l = resolveOperand(expr.left, row, ctx);
2535
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
+ }
2536
2814
  // SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
2537
2815
  if (l === null || r === null) return false;
2538
2816
  // 布尔与数值互通(MySQL: 1 = TRUE、0 = FALSE)
@@ -2555,18 +2833,23 @@ function evaluateExpr(expr, row, ctx) {
2555
2833
  }
2556
2834
  case 'like': {
2557
2835
  const v = resolveOperand(expr.operand, row, ctx);
2558
- const r = likeMatch(v, expr.pattern, !!expr.ci);
2836
+ const r = likeMatch(v, expr.pattern, !!expr.ci, expr.escape);
2559
2837
  return expr.not ? !r : r;
2560
2838
  }
2561
2839
  case 'quantified':
2562
2840
  return !!resolveOperand(expr, row, ctx);
2841
+ case 'jsonContains':
2842
+ return !!resolveOperand(expr, row, ctx);
2563
2843
  case 'regexp': {
2564
2844
  const v = resolveOperand(expr.operand, row, ctx);
2565
2845
  if (v === null || v === undefined) return false;
2566
- const re = new RegExp(String(expr.pattern), 'i');
2846
+ // REGEXP / RLIKE 历史上就是大小写不敏感的;`~` 敏感、`~*` 不敏感
2847
+ const re = new RegExp(String(expr.pattern), expr.ci === false ? '' : 'i');
2567
2848
  const r = re.test(String(v));
2568
2849
  return expr.not ? !r : r;
2569
2850
  }
2851
+ case 'jsonKeyExists':
2852
+ return !!resolveOperand(expr, row, ctx);
2570
2853
  case 'isTruth': {
2571
2854
  const v = resolveOperand(expr.operand, row, ctx);
2572
2855
  const isTrue = v === true || v === 1 || v === '1' || v === 'true' || v === 'TRUE' || v === 't' || (typeof v === 'number' && v !== 0);
@@ -2852,13 +3135,47 @@ class SQLExecutor {
2852
3135
  dataRows = dataRows.map(stripDefault);
2853
3136
  }
2854
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) : [];
2855
3139
  let toInsert = dataRows;
2856
3140
  let updated = 0;
2857
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
+ }
2858
3162
  if (pkCols.length > 0) {
2859
3163
  const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
2860
3164
  const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
2861
- 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)));
2862
3179
  if (explicit.length > 0) {
2863
3180
  const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
2864
3181
  const pkMap = new Map();
@@ -2866,15 +3183,31 @@ class SQLExecutor {
2866
3183
  const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
2867
3184
  if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
2868
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
+ });
2869
3197
  const conflicts = [];
2870
3198
  const fresh = [];
2871
3199
  for (const d of dataRows) {
2872
- const key = keyOf(d);
2873
- if (hasExplicitPk(d) && pkMap.has(key)) {
2874
- conflicts.push({ d, existingId: pkMap.get(key) });
2875
- } else {
2876
- 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
+ }
2877
3208
  }
3209
+ if (existingId) conflicts.push({ d, existingId });
3210
+ else fresh.push(d);
2878
3211
  }
2879
3212
  const conflictAction = this._conflictAction(statement);
2880
3213
  const conflictSets = this._conflictSets(statement);
@@ -2902,10 +3235,14 @@ class SQLExecutor {
2902
3235
  const data = {};
2903
3236
  if (conflictSets) {
2904
3237
  for (const [col, val] of conflictSets) {
2905
- // VALUES(col) 引用本次本应插入的值
2906
- data[col] = (val && typeof val === 'object' && val._valuesOf !== undefined)
2907
- ? (d[val._valuesOf] !== undefined ? d[val._valuesOf] : null)
2908
- : val;
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
+ }
2909
3246
  }
2910
3247
  } else {
2911
3248
  for (const k of Object.keys(d)) if (!pkCols.includes(k)) data[k] = d[k];
@@ -4493,53 +4830,88 @@ class SQLExecutor {
4493
4830
  }
4494
4831
  }
4495
4832
 
4496
- function splitStatements(sql) {
4497
- const statements = [];
4498
- let current = '';
4499
- let inStr = null; // "'" | '"' | '`'
4500
- let i = 0;
4501
- while (i < sql.length) {
4833
+ /** 跳过一个引号串(含转义/反引号成对),返回闭合引号之后的下标 */
4834
+ function skipQuoted(sql, i, q) {
4835
+ const n = sql.length;
4836
+ i++; // 跳过起始引号
4837
+ while (i < n) {
4502
4838
  const c = sql[i];
4503
- if (inStr) {
4504
- current += c;
4505
- if (inStr === '`') {
4506
- // 反引号标识符内部反斜杠不是转义符;两个连续反引号才表示一个字面反引号。
4507
- if (c === '`' && sql[i + 1] === '`') { current += sql[i + 1]; i += 2; continue; }
4508
- if (c === '`') inStr = null;
4509
- i++;
4510
- continue;
4839
+ if (q === '`') {
4840
+ // 反引号标识符内部反斜杠不是转义符;两个连续反引号表示一个字面反引号。
4841
+ if (c === '`') {
4842
+ if (sql[i + 1] === '`') { i += 2; continue; }
4843
+ return i + 1;
4511
4844
  }
4512
- if (c === '\\' && i + 1 < sql.length) { current += sql[i + 1]; i += 2; continue; }
4513
- if (c === inStr) inStr = null;
4514
4845
  i++;
4515
4846
  continue;
4516
4847
  }
4517
- 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; }
4518
4884
  if (c === '-' && sql[i + 1] === '-') {
4519
- while (i < sql.length && sql[i] !== '\n') i++;
4885
+ // 行注释:丢弃注释文本本身(与原实现一致),保留其后的换行
4886
+ take(i);
4887
+ while (i < n && sql[i] !== '\n') i++;
4888
+ segStart = i;
4520
4889
  continue;
4521
4890
  }
4522
- if (c === '#') {
4523
- 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;
4524
4895
  continue;
4525
4896
  }
4526
4897
  if (c === '/' && sql[i + 1] === '*') {
4527
- current += '/*';
4898
+ // 块注释:原实现会保留 `/* */` 原文,这里通过切片一并保留
4528
4899
  i += 2;
4529
- while (i + 1 < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) { current += sql[i]; i++; }
4530
- 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;
4531
4902
  continue;
4532
4903
  }
4533
4904
  if (c === ';') {
4534
- if (current.trim()) statements.push(current.trim());
4535
- current = '';
4905
+ take(i);
4906
+ flush();
4536
4907
  i++;
4908
+ segStart = i;
4537
4909
  continue;
4538
4910
  }
4539
- current += c;
4540
4911
  i++;
4541
4912
  }
4542
- if (current.trim()) statements.push(current.trim());
4913
+ take(n);
4914
+ flush();
4543
4915
  return statements;
4544
4916
  }
4545
4917
 
@@ -4566,7 +4938,7 @@ function hasComments(sql) {
4566
4938
  if (c === "'" || c === '"') { inStr = c; i++; continue; }
4567
4939
  if (c === '`') { i++; while (i < sql.length && sql[i] !== '`') i++; i++; continue; }
4568
4940
  if (c === '-' && sql[i + 1] === '-') return true;
4569
- if (c === '#') return true;
4941
+ if (c === '#' && sql[i + 1] !== '>') return true; // '#>' / '#>>' 是 JSON 操作符,不是注释
4570
4942
  if (c === '/' && sql[i + 1] === '*') return true;
4571
4943
  i++;
4572
4944
  }
@@ -4615,16 +4987,34 @@ function applyParams(sql, values) {
4615
4987
  if (args && typeof args === 'object' && !Array.isArray(args)) {
4616
4988
  hasNamed = true;
4617
4989
  }
4618
- while (i < sql.length) {
4990
+ const n = sql.length;
4991
+ while (i < n) {
4619
4992
  const c = sql[i];
4620
4993
  if (inStr) {
4621
4994
  out += c;
4622
- 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; }
4623
4996
  if (c === inStr) inStr = null;
4624
4997
  i++;
4625
4998
  continue;
4626
4999
  }
4627
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
+ }
4628
5018
  if (c === '?' && sql[i + 1] === '?') {
4629
5019
  if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
4630
5020
  out += escapeId(hasNamed ? args['@@'] : args[idx++]);
@@ -4657,6 +5047,24 @@ function applyParams(sql, values) {
4657
5047
  i++;
4658
5048
  continue;
4659
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
+ }
4660
5068
  if ((c === ':' || c === '@' || c === '$') && i + 1 < sql.length && /[A-Za-z_]/.test(sql[i + 1])) {
4661
5069
  // 命名占位符 :name @name $name
4662
5070
  let name = '';
@@ -4678,6 +5086,44 @@ function applyParams(sql, values) {
4678
5086
  return out;
4679
5087
  }
4680
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
+
4681
5127
  async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
4682
5128
  // 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
4683
5129
  // 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
@@ -4701,14 +5147,19 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
4701
5147
  if (Array.isArray(paramsOrOpts)) {
4702
5148
  sql = applyParams(sql, paramsOrOpts);
4703
5149
  } else if (paramsOrOpts && typeof paramsOrOpts === 'object') {
4704
- 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);
4705
5156
  }
4706
5157
  if (opts.safety !== false) {
4707
5158
  if (!opts.allowComments && hasComments(sql)) {
4708
5159
  throw new Error('SQL comments are disabled for security (--, #, /* */)');
4709
5160
  }
4710
5161
  }
4711
- let statements = splitStatements(sql);
5162
+ let statements = splitStatements(sql, { maxLength: opts.maxSqlLength });
4712
5163
  if (opts.maxStatements != null && statements.length > opts.maxStatements) {
4713
5164
  throw new Error(`too many statements (${statements.length} > ${opts.maxStatements})`);
4714
5165
  }
@@ -4727,7 +5178,14 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
4727
5178
  const results = [];
4728
5179
  for (const stmtSql of statements) {
4729
5180
  const stmt = parseSQL(stmtSql);
4730
- 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);
4731
5189
  }
4732
5190
  return results.length === 1 ? results[0] : results;
4733
5191
  }