jsql-neo 6.3.6 → 6.3.10

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
@@ -16,14 +16,22 @@ const DANGEROUS_SQL = [
16
16
  const DEFAULT_MAX_SQL_LENGTH = 64 * 1024 * 1024;
17
17
 
18
18
  function findDangerousSQL(tokens) {
19
- for (let i = 0; i < tokens.length; i++) {
20
- const t = tokens[i];
19
+ // 先把注释去掉,防止通过 /*comment*/DROP 绕过检测
20
+ const commentStripped = [];
21
+ let skip = false;
22
+ for (const t of tokens) {
23
+ if (t.type === 'comment') { skip = true; continue; }
24
+ if (!skip) commentStripped.push(t);
25
+ else skip = false;
26
+ }
27
+ for (let i = 0; i < commentStripped.length; i++) {
28
+ const t = commentStripped[i];
21
29
  if (t.type !== 'keyword' && t.type !== 'ident') continue;
22
30
  const upper = String(t.value).toUpperCase();
23
31
  for (const d of DANGEROUS_SQL) {
24
32
  if (d.re.test(upper)) {
25
33
  if (d.next) {
26
- const nxt = tokens[i + 1];
34
+ const nxt = commentStripped[i + 1];
27
35
  if (nxt && d.next.test(String(nxt.value).toUpperCase())) {
28
36
  return d.name;
29
37
  }
@@ -69,6 +77,57 @@ const KEYWORDS = new Set([
69
77
  // 在语法层按上下文大小写不敏感地匹配,从而不破坏既有解析行为。
70
78
  ]);
71
79
 
80
+ /**
81
+ * MySQL 里属于「非保留」的关键字 —— 可以不加反引号直接当表名/列名。
82
+ *
83
+ * 词法层为了识别语法把它们标成了 keyword(parseColumnDef 还依赖这一点来认类型),
84
+ * 但**在「期望一个名字」的位置必须放行**。否则下面这些最普通的写法都会被拒:
85
+ * CREATE TABLE t (date DATE, text TEXT, count INT)
86
+ * SELECT date, count FROM t
87
+ * 实测 64 个常见列名里有 31 个被拒,其中 `date` / `text` / `count` / `engine` /
88
+ * `first` / `after` 在 MySQL 里都不是保留字,用户没有理由必须加反引号。
89
+ *
90
+ * 这里的做法与文件末尾 OVER / PARTITION 那条注释同一个思路:
91
+ * 不让词法层去区分,改由语法层按上下文决定。真正的保留字
92
+ * (TABLE / SELECT / KEY / ORDER / INDEX / GROUP ...)仍然要求反引号。
93
+ */
94
+ const NON_RESERVED_KEYWORDS = new Set([
95
+ // 数据类型
96
+ 'DATE', 'DATETIME', 'TIME', 'TIMESTAMP', 'TEXT', 'BLOB', 'VARCHAR', 'CHAR',
97
+ 'INT', 'INTEGER', 'BIGINT', 'SMALLINT', 'TINYINT', 'FLOAT', 'DOUBLE', 'REAL',
98
+ 'NUMERIC', 'DECIMAL', 'BOOLEAN', 'BOOL', 'JSON', 'STRING', 'OBJECT', 'ARRAY', 'ANY',
99
+ // 列属性 / ALTER 的位置标记
100
+ 'AFTER', 'FIRST', 'UNSIGNED', 'ZEROFILL', 'ENGINE', 'IGNORE', 'SPATIAL',
101
+ 'AUTOINCREMENT', 'AUTO_INCREMENT', 'NOTNULL',
102
+ // 其余非保留字(含库名/describe 这类信息模式用语)
103
+ 'START', 'WORK', 'TRANSACTION', 'DUPLICATE', 'MODIFY', 'CHANGE', 'CONSTRAINT',
104
+ 'COLLATE', 'CHARACTER', 'DATABASE', 'DATABASES', 'TABLES', 'SHOW', 'DESCRIBE',
105
+ 'RAISE', 'PRAGMA', 'REGEXP', 'RLIKE',
106
+ // 聚合函数名:MySQL 里它们根本不是关键字
107
+ 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX',
108
+ ]);
109
+
110
+ /**
111
+ * 这个 token 能不能当作一个名字(表名 / 列名)。
112
+ * ident 自然可以;上面那张表里的 keyword 也可以;其余 keyword 不行。
113
+ */
114
+ function isUsableNameToken(t) {
115
+ if (!t) return false;
116
+ if (t.type === 'ident') return true;
117
+ return t.type === 'keyword' && NON_RESERVED_KEYWORDS.has(String(t.value).toUpperCase());
118
+ }
119
+
120
+ /**
121
+ * 取一个名字 token 的拼写。
122
+ *
123
+ * 关键字 token 的 value 在词法层被统一大写了('date' → 'DATE'),
124
+ * 拿它当列名会让 `CREATE TABLE t (date TEXT)` 建出名为 DATE 的列。
125
+ * 所以优先用词法层留下的原样文本。
126
+ */
127
+ function nameOfToken(t) {
128
+ return t && t.text !== undefined ? t.text : (t ? t.value : undefined);
129
+ }
130
+
72
131
  function tokenize(sql) {
73
132
  const tokens = [];
74
133
  let i = 0;
@@ -129,10 +188,13 @@ function tokenize(sql) {
129
188
  if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(sql[i + 1] || ''))) {
130
189
  let j = i;
131
190
  let isFloat = false;
191
+ let dotCount = 0;
132
192
  while (j < n && /[0-9.]/.test(sql[j])) {
133
- if (sql[j] === '.') isFloat = true;
193
+ if (sql[j] === '.') { isFloat = true; dotCount++; }
134
194
  j++;
135
195
  }
196
+ // P2-2: SELECT 1.2.3 静默返回 1.2——多个点的数字字面量应报错
197
+ if (dotCount > 1) throw new Error(`Invalid number literal: ${sql.slice(i, j)}`);
136
198
  if (sql[j] === 'e' || sql[j] === 'E') {
137
199
  j++;
138
200
  if (sql[j] === '+' || sql[j] === '-') j++;
@@ -140,7 +202,16 @@ function tokenize(sql) {
140
202
  isFloat = true;
141
203
  }
142
204
  const raw = sql.slice(i, j);
143
- tokens.push(new SQLToken('number', isFloat ? parseFloat(raw) : parseInt(raw, 10), i));
205
+ // parseInt 会错误处理某些前导零('07' 在非严格模式被当八进制),
206
+ // 大数 parseInt 也丢失精度。统一用 Number()(bigint 用 BigInt())。
207
+ let numVal;
208
+ if (raw.includes('.') || raw.includes('e') || raw.includes('E')) {
209
+ numVal = parseFloat(raw);
210
+ } else {
211
+ const n = BigInt(raw);
212
+ numVal = n <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(n) : n;
213
+ }
214
+ tokens.push(new SQLToken('number', numVal, i));
144
215
  i = j;
145
216
  continue;
146
217
  }
@@ -150,11 +221,15 @@ function tokenize(sql) {
150
221
  while (j < n && /[a-zA-Z0-9_$]/.test(sql[j])) j++;
151
222
  const word = sql.slice(i, j);
152
223
  const upper = word.toUpperCase();
153
- tokens.push(new SQLToken(KEYWORDS.has(upper) ? 'keyword' : 'ident', upper === word ? word : word, i));
224
+ const tok = new SQLToken(KEYWORDS.has(upper) ? 'keyword' : 'ident', word, i);
225
+ // 原样拼写要留下来:关键字被当作列名/表名时(date、count、first…)
226
+ // 得还原成用户写的大小写,否则列名会变成 DATE 而用户写的是 date。
227
+ tok.text = word;
154
228
  if (KEYWORDS.has(upper)) {
155
- tokens[tokens.length - 1].value = upper;
156
- tokens[tokens.length - 1].isKeyword = true;
229
+ tok.value = upper;
230
+ tok.isKeyword = true;
157
231
  }
232
+ tokens.push(tok);
158
233
  i = j;
159
234
  continue;
160
235
  }
@@ -203,7 +278,7 @@ function tokenize(sql) {
203
278
  let j = i + 1;
204
279
  let num = '';
205
280
  while (j < n && sql[j] >= '0' && sql[j] <= '9') { num += sql[j]; j++; }
206
- tokens.push(new SQLToken('param', num ? parseInt(num, 10) : null, i));
281
+ tokens.push(new SQLToken('param', num ? Number(num) : null, i));
207
282
  i = j;
208
283
  continue;
209
284
  }
@@ -467,16 +542,16 @@ class Parser {
467
542
 
468
543
  parseTableName() {
469
544
  const t = this.next();
470
- if (t.type !== 'ident') throw new Error(`Expected table name, got '${t.value}'`);
545
+ if (!isUsableNameToken(t)) throw new Error(`Expected table name, got '${t.value}'`);
471
546
  if (this.peek().type === 'op' && this.peek().value === '.') {
472
547
  this.next();
473
548
  const t2 = this.next();
474
- if (t2.type !== 'ident' && !(t2.type === 'keyword' && this._isSchemaView(t2.value))) {
549
+ if (!isUsableNameToken(t2) && !(t2.type === 'keyword' && this._isSchemaView(t2.value))) {
475
550
  throw new Error(`Expected table name after '.', got '${t2.value}'`);
476
551
  }
477
- return t.value + '.' + t2.value;
552
+ return nameOfToken(t) + '.' + nameOfToken(t2);
478
553
  }
479
- return t.value;
554
+ return nameOfToken(t);
480
555
  }
481
556
 
482
557
  _isSchemaView(v) {
@@ -535,7 +610,22 @@ class Parser {
535
610
  } else if (this.isWord('FOREIGN') || this.isWord('CHECK') ||
536
611
  t.type === 'keyword' && (t.value === 'KEY' || t.value === 'INDEX')) {
537
612
  // 表级约束:FOREIGN KEY ... REFERENCES ... / CHECK (...) / KEY name (cols)
538
- this._parseTableConstraint(schema);
613
+ // P1-24: CREATE TABLE t (key TEXT) 报错——key 被当作表级约束
614
+ // 如果 KEY/INDEX 后面跟着列类型(TEXT/INT/VARCHAR 等),应解析为列定义
615
+ if (t.type === 'keyword' && (t.value === 'KEY' || t.value === 'INDEX')) {
616
+ const nextTok = this.peek(1);
617
+ if (nextTok && (nextTok.type === 'ident' || nextTok.type === 'keyword') &&
618
+ ['TEXT', 'INT', 'INTEGER', 'VARCHAR', 'CHAR', 'DATE', 'DATETIME', 'TIMESTAMP', 'BOOLEAN', 'BOOL', 'FLOAT', 'DOUBLE', 'DECIMAL', 'BLOB', 'JSON'].includes(String(nextTok.value).toUpperCase())) {
619
+ const col = this.parseTableName();
620
+ const def = this.parseColumnDef();
621
+ schema[col] = def;
622
+ if (def.primaryKey) hasPk = true;
623
+ } else {
624
+ this._parseTableConstraint(schema);
625
+ }
626
+ } else {
627
+ this._parseTableConstraint(schema);
628
+ }
539
629
  } else if (t.type === 'eof' || (t.type === 'op' && t.value === ')')) {
540
630
  break;
541
631
  } else {
@@ -917,6 +1007,18 @@ class Parser {
917
1007
  if (num.type !== 'number') throw new Error('Expected number after +');
918
1008
  return num.value;
919
1009
  }
1010
+ // P1-19/P2-3: parseValue 不支持表达式 RHS,如 col = col + 1
1011
+ // 支持简单的二元表达式:left op right
1012
+ if (t.type === 'ident') {
1013
+ const left = this.parseColumnRef();
1014
+ const opTok = this.peek();
1015
+ if (opTok.type === 'op' && ['+', '-', '*', '/', '%', '<<', '>>', '&', '|', '^'].includes(opTok.value)) {
1016
+ this.next();
1017
+ const right = this.parseValue();
1018
+ return { type: 'binary', op: opTok.value, left, right };
1019
+ }
1020
+ return left;
1021
+ }
920
1022
  throw new Error(`Expected value, got '${t.value}'`);
921
1023
  }
922
1024
 
@@ -946,6 +1048,9 @@ class Parser {
946
1048
  if (this.isKeyword('RECURSIVE')) { this.next(); recursive = true; }
947
1049
  const ctes = [];
948
1050
  for (;;) {
1051
+ // 部分方言把 RECURSIVE 放在第一个 CTE 之后(WITH a AS (...), RECURSIVE c AS (...)),
1052
+ // 标准 SQL 里它是 WITH 后的可选关键字 —— 两种写法都接受。
1053
+ if (!recursive && ctes.length && this.isKeyword('RECURSIVE')) { this.next(); recursive = true; ctes[0].recursive = true; }
949
1054
  const nameTok = this.next();
950
1055
  if (nameTok.type !== 'ident') throw new Error(`Expected CTE name, got '${nameTok.value}'`);
951
1056
  let columns = null;
@@ -1064,7 +1169,11 @@ class Parser {
1064
1169
  let aggregate = null;
1065
1170
  while (true) {
1066
1171
  const t = this.peek();
1067
- if (t.type === 'keyword' && t.value === 'COUNT') {
1172
+ // 只有后面紧跟 '(' 才算 COUNT(...) 聚合调用。
1173
+ // MySQL 里 count 不是保留字,`SELECT count FROM t` 是合法的列引用;
1174
+ // 光看词本身会让它误判成函数、进而要求 '('。
1175
+ if (t.type === 'keyword' && t.value === 'COUNT' &&
1176
+ this.peek(1).type === 'op' && this.peek(1).value === '(') {
1068
1177
  this.next();
1069
1178
  this.expect('op', '(');
1070
1179
  let aggDistinct = false;
@@ -1253,7 +1362,7 @@ class Parser {
1253
1362
  if (this.isWord('FIRST') || this.isWord('NEXT')) this.next();
1254
1363
  else throw new Error(`Expected FIRST or NEXT after FETCH, got '${this.peek().value}'`);
1255
1364
  const nTok = this.next();
1256
- const n = nTok.type === 'number' ? nTok.value : parseInt(String(nTok.value), 10);
1365
+ const n = nTok.type === 'number' ? nTok.value : Number(nTok.value);
1257
1366
  if (!Number.isFinite(n)) throw new Error(`Expected row count after FETCH FIRST, got '${nTok.value}'`);
1258
1367
  limit = n;
1259
1368
  if (this.isWord('ROW') || this.isWord('ROWS')) this.next();
@@ -1328,7 +1437,10 @@ class Parser {
1328
1437
  parseAlias() {
1329
1438
  const t = this.next();
1330
1439
  if (t.type !== 'ident' && t.type !== 'keyword') throw new Error(`Expected alias, got '${t.value}'`);
1331
- return t.value;
1440
+ // 用原样拼写:别名的 value 会被词法层大写(AS avg → 'AVG'),
1441
+ // 而 ORDER BY avg / GROUP BY avg 引用时用的是原样拼写,两边必须一致,
1442
+ // 否则按别名排序会静默失效。
1443
+ return nameOfToken(t);
1332
1444
  }
1333
1445
 
1334
1446
  parseOptionalAlias() {
@@ -1339,14 +1451,14 @@ class Parser {
1339
1451
 
1340
1452
  parseColumnRef() {
1341
1453
  const t = this.next();
1342
- if (t.type !== 'ident') throw new Error(`Expected column name, got '${t.value}'`);
1454
+ if (!isUsableNameToken(t)) throw new Error(`Expected column name, got '${t.value}'`);
1343
1455
  if (this.peek().type === 'op' && this.peek().value === '.') {
1344
1456
  this.next();
1345
1457
  const col = this.next();
1346
- if (col.type !== 'ident') throw new Error(`Expected column name after '.', got '${col.value}'`);
1347
- return t.value + '.' + col.value;
1458
+ if (!isUsableNameToken(col)) throw new Error(`Expected column name after '.', got '${col.value}'`);
1459
+ return nameOfToken(t) + '.' + nameOfToken(col);
1348
1460
  }
1349
- return t.value;
1461
+ return nameOfToken(t);
1350
1462
  }
1351
1463
 
1352
1464
  parseUpdate() {
@@ -1516,8 +1628,21 @@ class Parser {
1516
1628
  case 'DROP': {
1517
1629
  if (this.isKeyword('COLUMN')) this.next();
1518
1630
  if (this.isKeyword('PRIMARY')) { this.expectKeyword('PRIMARY'); this.expectKeyword('KEY'); ops.push({ op: 'dropPrimary' }); break; }
1519
- if (this.isKeyword('FOREIGN')) { this.next(); this.expectKeyword('KEY'); if (this.peek().type === 'ident') this.next(); ops.push({ op: 'dropIndex' }); break; }
1520
- if (this.isKeyword('INDEX') || this.isKeyword('KEY')) { this.next(); if (this.peek().type === 'ident') this.next(); ops.push({ op: 'dropIndex' }); break; }
1631
+ if (this.isKeyword('FOREIGN')) {
1632
+ // 与 DROP INDEX 分开标记:两者都要名字,但执行器要给出不同的报错
1633
+ this.next(); this.expectKeyword('KEY');
1634
+ const fkName = this.peek().type === 'ident' ? this.next().value : null;
1635
+ ops.push({ op: 'dropForeign', name: fkName });
1636
+ break;
1637
+ }
1638
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY')) {
1639
+ // 名字必须留下来 —— 此前只是 next() 跳过、丢弃,
1640
+ // 执行器拿到 name=undefined 就无从知道删哪个索引。
1641
+ this.next();
1642
+ const idxName = this.peek().type === 'ident' ? this.next().value : null;
1643
+ ops.push({ op: 'dropIndex', name: idxName });
1644
+ break;
1645
+ }
1521
1646
  const column = this.parseTableName();
1522
1647
  ops.push({ op: 'dropColumn', column });
1523
1648
  break;
@@ -1534,7 +1659,28 @@ class Parser {
1534
1659
  break;
1535
1660
  }
1536
1661
  case 'RENAME': {
1537
- if (this.isKeyword('TO')) this.next();
1662
+ // RENAME COLUMN old TO new —— SQL 标准写法。
1663
+ // 此前这里不区分,直接把 COLUMN 当新表名去解析,
1664
+ // 于是报 "Expected table name, got 'COLUMN'"。
1665
+ if (this.isKeyword('COLUMN')) {
1666
+ this.next();
1667
+ const oldColumn = this.parseTableName();
1668
+ if (this.isKeyword('TO') || this.isKeyword('AS')) this.next();
1669
+ else throw new Error(`Expected TO after '${oldColumn}', got '${this.peek().value}'`);
1670
+ const newColumn = this.parseTableName();
1671
+ ops.push({ op: 'renameColumn', column: oldColumn, newColumn });
1672
+ break;
1673
+ }
1674
+ // MySQL 的 RENAME INDEX/KEY 没实现 —— 明确报错,
1675
+ // 不要落进 default 变成「返回成功但什么都没做」。
1676
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY')) {
1677
+ throw new Error(
1678
+ `ALTER TABLE ... RENAME INDEX is not supported. ` +
1679
+ `Use DROP INDEX followed by CREATE INDEX instead.`
1680
+ );
1681
+ }
1682
+ // RENAME TO new / RENAME AS new
1683
+ if (this.isKeyword('TO') || this.isKeyword('AS')) this.next();
1538
1684
  const newName = this.parseTableName();
1539
1685
  ops.push({ op: 'rename', newName });
1540
1686
  break;
@@ -1937,6 +2083,15 @@ class Parser {
1937
2083
 
1938
2084
  parseOperand() {
1939
2085
  const t = this.next();
2086
+ // P1-20: SELECT -col 报错——parseOperand 不处理负号
2087
+ if (t.type === 'op' && t.value === '-') {
2088
+ const operand = this.parseOperand();
2089
+ return { type: 'negate', operand };
2090
+ }
2091
+ if (t.type === 'op' && t.value === '+') {
2092
+ const operand = this.parseOperand();
2093
+ return { type: 'negate', operand, positive: true };
2094
+ }
1940
2095
  if (t.type === 'ident') {
1941
2096
  // 聚合函数名(GROUP_CONCAT / STDDEV / FIRST...)走 aggregate 节点,
1942
2097
  // 否则会被当作逐行标量函数:既返回 null,又不会折叠分组
@@ -1969,8 +2124,8 @@ class Parser {
1969
2124
  this.next();
1970
2125
  const col = this.next();
1971
2126
  if (col.type === 'op' && col.value === '*') return { type: 'star', alias: t.value };
1972
- if (col.type !== 'ident') throw new Error(`Expected column name after '.', got '${col.value}'`);
1973
- return { type: 'column', name: t.value + '.' + col.value };
2127
+ if (!isUsableNameToken(col)) throw new Error(`Expected column name after '.', got '${col.value}'`);
2128
+ return { type: 'column', name: nameOfToken(t) + '.' + nameOfToken(col) };
1974
2129
  }
1975
2130
  // CAST(expr AS type)
1976
2131
  if (String(t.value).toUpperCase() === 'CAST' && this.peek().type === 'op' && this.peek().value === '(') {
@@ -2115,7 +2270,9 @@ class Parser {
2115
2270
  !(this.peek().type === 'op' && (this.peek().value === '(' || this.peek().value === '.'))) {
2116
2271
  return { type: 'unit', name: String(t.value).toUpperCase() };
2117
2272
  }
2118
- return { type: 'column', name: t.value };
2273
+ // 用原样拼写:关键字的 value 被词法层大写了,拿它当列名会让
2274
+ // `SELECT date FROM t`(列名 date)取不到值。
2275
+ return { type: 'column', name: nameOfToken(t) };
2119
2276
  }
2120
2277
  if (t.type === 'number' || t.type === 'string') return { type: 'value', value: t.value };
2121
2278
  if (t.type === 'sysvar') return { type: 'sysvar', name: t.value };
@@ -2150,7 +2307,8 @@ class Parser {
2150
2307
  // 也不满足上面的聚合调用分支(后面是 DESC 不是 '('),最终会抛 "got 'AVG'"。
2151
2308
  // ORDER BY 引用输出别名是标准 SQL 行为,SQLite / MySQL 都允许。
2152
2309
  if (t.type === 'keyword' && !(this.peek().type === 'op' && this.peek().value === '(')) {
2153
- return { type: 'column', name: t.value };
2310
+ // 同样要用原样拼写,否则按别名排序/分组时会拿大写名去找而落空
2311
+ return { type: 'column', name: nameOfToken(t) };
2154
2312
  }
2155
2313
  // 一元按位取反 ~x
2156
2314
  if (t.type === 'op' && t.value === '~') {
@@ -2193,8 +2351,15 @@ class Parser {
2193
2351
  }
2194
2352
  if (t.type === 'op' && (t.value === '-' || t.value === '+')) {
2195
2353
  const num = this.next();
2196
- if (num.type !== 'number') throw new Error('Expected number after sign');
2197
- return { type: 'value', value: t.value === '-' ? -num.value : num.value };
2354
+ if (num.type === 'number') {
2355
+ return { type: 'value', value: t.value === '-' ? -num.value : num.value };
2356
+ }
2357
+ // 负号/正号后面是列名或表达式:解析为 unary 运算符
2358
+ if (num.type === 'column' || num.type === 'value') {
2359
+ const operand = num.type === 'column' ? { type: 'column', name: num.value } : { type: 'value', value: num.value };
2360
+ return { type: 'negate', operand };
2361
+ }
2362
+ throw new Error('Expected number or column after sign');
2198
2363
  }
2199
2364
  throw new Error(`Expected value or column, got '${t.value}'`);
2200
2365
  }
@@ -2621,23 +2786,51 @@ function resolveOperand(operand, row, ctx) {
2621
2786
  if (rn !== null) return rn; // 左侧非数值按 0
2622
2787
  return 0;
2623
2788
  }
2624
- case '-': return Number(l) - Number(r);
2625
- case '*': return Number(l) * Number(r);
2626
- case '/': return r === 0 ? null : Number(l) / Number(r);
2627
- case '%': return r === 0 ? null : Number(l) % Number(r);
2789
+ case '-': {
2790
+ // P1-21: 'abc'-1 → NaN,应返回 null
2791
+ const ln = Number(l), rn = Number(r);
2792
+ if (isNaN(ln) || isNaN(rn)) return null;
2793
+ return ln - rn;
2794
+ }
2795
+ case '*': {
2796
+ const ln = Number(l), rn = Number(r);
2797
+ if (isNaN(ln) || isNaN(rn)) return null;
2798
+ return ln * rn;
2799
+ }
2800
+ case '/': {
2801
+ const ln = Number(l), rn = Number(r);
2802
+ if (isNaN(ln) || isNaN(rn) || rn === 0) return null;
2803
+ return ln / rn;
2804
+ }
2805
+ case '%': {
2806
+ const ln = Number(l), rn = Number(r);
2807
+ if (isNaN(ln) || isNaN(rn) || rn === 0) return null;
2808
+ return ln % rn;
2809
+ }
2628
2810
  // MySQL 整数除法 DIV(结果向零取整)
2629
2811
  case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
2630
2812
  // 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
2631
2813
  case '&': return (Number(l) | 0) & (Number(r) | 0);
2632
2814
  case '|': return (Number(l) | 0) | (Number(r) | 0);
2633
2815
  case '^': return (Number(l) | 0) ^ (Number(r) | 0);
2634
- case '<<': return (Number(l) | 0) << (Number(r) | 0);
2635
- case '>>': return (Number(l) | 0) >> (Number(r) | 0);
2816
+ // P1-22: JavaScript 位运算只使用 32 位整数,移位量对 32 取模
2817
+ // 1 << 32 = 1 << 0 = 1,但 SQL 中 1 << 32 = 4294967296
2818
+ case '<<': return Number(l) * Math.pow(2, Number(r));
2819
+ case '>>': return Math.floor(Number(l) / Math.pow(2, Number(r)));
2636
2820
  }
2637
2821
  return null;
2638
2822
  }
2639
2823
  case 'bitnot':
2640
2824
  return ~(Number(resolveOperand(operand.expr, row, ctx)) | 0);
2825
+ case 'negate': {
2826
+ const v = resolveOperand(operand.operand, row, ctx);
2827
+ if (v === null || v === undefined) return null;
2828
+ return -Number(v);
2829
+ }
2830
+ // P1-19: ON DUPLICATE KEY UPDATE 表达式赋值,如 count = count + 1
2831
+ case 'binary': {
2832
+ return evaluateExpr({ type: 'arith', op: operand.op, left: operand.left, right: operand.right }, row, ctx);
2833
+ }
2641
2834
  case 'func':
2642
2835
  return applyScalarFunction(operand, row, ctx);
2643
2836
  case 'case':
@@ -2950,9 +3143,21 @@ function applyScalarFunction(fnNode, row, ctx) {
2950
3143
  case 'CURDATE': case 'CURRENT_DATE': return sqlLocalDateTime().slice(0, 10);
2951
3144
  case 'CURTIME': return sqlLocalDateTime().slice(11, 19);
2952
3145
  case 'UTC_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
2953
- case 'CONCAT': return args.map(a => a === null || a === undefined ? '' : String(a)).join('');
3146
+ case 'CONCAT': {
3147
+ // 任一参数是 NULL 就返回 NULL —— MySQL 语义,也是本引擎其余所有标量
3148
+ // 函数(UPPER / LENGTH / TRIM / ABS / SUBSTRING / REPLACE ...)的统一约定。
3149
+ // 此前把 NULL 当成空串,会悄悄拼出一个「看起来正常」的错值:
3150
+ // CONCAT(first, ' ', last) 里 last 为 NULL 时会得到 "John " 而不是 NULL,
3151
+ // 缺失的字段就此被掩盖。
3152
+ if (args.some(a => a == null)) return null;
3153
+ return args.map(a => String(a)).join('');
3154
+ }
2954
3155
  case 'CONCAT_WS': {
2955
- const sep = args[0] == null ? ',' : String(args[0]);
3156
+ // 与 CONCAT 相反:CONCAT_WS 跳过值为 NULL 的参数(MySQL 语义)。
3157
+ // 但分隔符本身为 NULL 时整体返回 NULL —— 此前会悄悄退回逗号,
3158
+ // 拼出一个用户没要求的分隔符。
3159
+ if (args[0] == null) return null;
3160
+ const sep = String(args[0]);
2956
3161
  return args.slice(1).filter(a => a !== null && a !== undefined).map(a => String(a)).join(sep);
2957
3162
  }
2958
3163
  case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
@@ -3247,7 +3452,13 @@ function applyScalarFunction(fnNode, row, ctx) {
3247
3452
  case 'POWER': case 'POW': return args[0] == null ? null : Math.pow(args[0], args[1]);
3248
3453
  case 'SQRT': return args[0] == null ? null : Math.sqrt(args[0]);
3249
3454
  case 'IFNULL': case 'NVL': return args[0] != null ? args[0] : args[1];
3250
- case 'COALESCE': return args.find(a => a != null);
3455
+ case 'COALESCE': {
3456
+ // 不能直接 return find(...):全都取不到值时 find 返回 undefined,
3457
+ // 而 undefined 在结果行里会整个键消失(JSON.stringify 会丢掉它),
3458
+ // 于是这一列凭空不见了。统一成 null,与 IFNULL / CASE 的行为一致。
3459
+ const hit = args.find(a => a != null);
3460
+ return hit === undefined ? null : hit;
3461
+ }
3251
3462
  case 'NULLIF': return args[0] === args[1] ? null : args[0];
3252
3463
  case 'IF': return args[0] ? args[1] : args[2];
3253
3464
  case 'REPLACE': return args[0] == null ? null : String(args[0]).split(args[1]).join(args[2]);
@@ -3314,8 +3525,18 @@ function applyScalarFunction(fnNode, row, ctx) {
3314
3525
  }
3315
3526
  return Math.floor(Date.now() / 1000);
3316
3527
  }
3317
- case 'GREATEST': return args.reduce((m, a) => a > m ? a : m, args[0]);
3318
- case 'LEAST': return args.reduce((m, a) => a < m ? a : m, args[0]);
3528
+ case 'GREATEST': {
3529
+ // 任一参数是 NULL 就返回 NULL(MySQL 语义)。
3530
+ // 此前直接拿 JS 的 > 比较,而 JS 里 null 会被强转成 0 ——
3531
+ // 于是 GREATEST(NULL, 1) 得 1 而 LEAST(NULL, 1) 得 NULL:
3532
+ // 这一对函数自相矛盾,总有一个是错的。
3533
+ if (!args.length || args.some(a => a == null)) return null;
3534
+ return args.reduce((m, a) => (a > m ? a : m));
3535
+ }
3536
+ case 'LEAST': {
3537
+ if (!args.length || args.some(a => a == null)) return null;
3538
+ return args.reduce((m, a) => (a < m ? a : m));
3539
+ }
3319
3540
  case 'UUID': return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
3320
3541
  const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8);
3321
3542
  return v.toString(16);
@@ -3731,7 +3952,8 @@ class SQLExecutor {
3731
3952
  return { ok: true, type: 'truncate', table: statement.name, affectedRows: 0 };
3732
3953
  }
3733
3954
  case 'alterTable': {
3734
- const table = this.engine._tables ? this.engine._tables[statement.name] : null;
3955
+ // let 而非 const:RENAME 会把表挪到新名字下,需要重新取
3956
+ let table = this.engine._tables ? this.engine._tables[statement.name] : null;
3735
3957
  if (!table) throw new Error(`Table '${statement.name}' does not exist`);
3736
3958
  for (const op of statement.ops) {
3737
3959
  switch (op.op) {
@@ -3769,6 +3991,9 @@ class SQLExecutor {
3769
3991
  }
3770
3992
  case 'dropPrimary': {
3771
3993
  for (const def of Object.values(table._schema)) { if (def && typeof def === 'object') { def.primaryKey = false; } }
3994
+ // P1-23: 清除主键缓存,否则后续插入仍会检查主键冲突
3995
+ table._primaryKey = null;
3996
+ table._pkIndex = null;
3772
3997
  break;
3773
3998
  }
3774
3999
  case 'addIndex': {
@@ -3776,11 +4001,79 @@ class SQLExecutor {
3776
4001
  for (const c of op.columns) table._schema[c].unique = true;
3777
4002
  break;
3778
4003
  }
4004
+ case 'rename': {
4005
+ // 真正改表名。此前落到 default 直接 break ——
4006
+ // 返回 ok:true 但表名没变,调用方以为改成功了。
4007
+ if (typeof this.engine.renameTable !== 'function') {
4008
+ throw new Error(`ALTER TABLE ... RENAME is not supported by this engine (no renameTable method)`);
4009
+ }
4010
+ this.engine.renameTable(statement.name, op.newName);
4011
+ table = this.engine._tables[op.newName] || table;
4012
+ statement.name = op.newName;
4013
+ break;
4014
+ }
4015
+ case 'renameColumn': {
4016
+ const oldCol = op.column;
4017
+ const newCol = op.newColumn;
4018
+ if (!(oldCol in table._schema)) {
4019
+ throw new Error(`Unknown column '${oldCol}' in '${statement.name}'`);
4020
+ }
4021
+ if (oldCol === newCol) break;
4022
+ if (newCol in table._schema) {
4023
+ throw new Error(`Duplicate column name '${newCol}'`);
4024
+ }
4025
+ // 按原顺序重建而不是改键:直接 delete+赋值会把这一列挪到末尾,
4026
+ // SELECT * 的列序、以及依赖列序的代码就都变了。
4027
+ const newSchema = {};
4028
+ for (const [f, def] of Object.entries(table._schema)) {
4029
+ if (f === oldCol) newSchema[newCol] = def;
4030
+ else newSchema[f] = def;
4031
+ }
4032
+ table._schema = newSchema;
4033
+ for (const row of table._rows) {
4034
+ if (!(oldCol in row)) continue;
4035
+ const rebuilt = {};
4036
+ for (const [k, v] of Object.entries(row)) {
4037
+ if (k === oldCol) rebuilt[newCol] = v;
4038
+ else rebuilt[k] = v;
4039
+ }
4040
+ for (const k of Object.keys(row)) delete row[k];
4041
+ Object.assign(row, rebuilt);
4042
+ }
4043
+ break;
4044
+ }
4045
+ case 'dropIndex': {
4046
+ // ALTER TABLE t DROP INDEX idx / DROP KEY idx
4047
+ const idxName = op.name;
4048
+ if (!idxName) throw new Error(`ALTER TABLE ... DROP INDEX requires an index name`);
4049
+ // 索引名 → 列:优先查登记表(复合索引登记了多列),
4050
+ // 查不到就把名字当列名(兼容单列同名的老用法)。
4051
+ const reg = this.engine._indexRegistry
4052
+ && this.engine._indexRegistry.get(String(idxName).toLowerCase());
4053
+ const fields = (reg && reg.columns && reg.columns.length) ? reg.columns.slice() : [idxName];
4054
+ const present = fields.filter(f => table._indexes[f] || table._btrees[f]);
4055
+ if (!present.length) {
4056
+ throw new Error(`Index '${idxName}' does not exist on '${statement.name}'`);
4057
+ }
4058
+ // 复合索引是逐列建的,必须全部清掉,只删首列会留残余索引
4059
+ for (const f of present) table.dropIndex(f);
4060
+ if (this.engine._indexRegistry) {
4061
+ this.engine._indexRegistry.delete(String(idxName).toLowerCase());
4062
+ }
4063
+ break;
4064
+ }
3779
4065
  case 'addForeign':
3780
- case 'dropIndex':
3781
- case 'rename':
4066
+ case 'dropForeign':
4067
+ // 外键在这个引擎里没有实现(连 CREATE TABLE 的表级 FOREIGN KEY
4068
+ // 都只是解析掉、不记录、不强制)。与其返回成功让人以为约束生效,
4069
+ // 不如明确报错。要支持得先做引用完整性检查。
4070
+ throw new Error(
4071
+ `ALTER TABLE ... ${op.op === 'addForeign' ? 'ADD FOREIGN KEY' : 'DROP FOREIGN KEY'} is not supported: ` +
4072
+ `this engine does not implement foreign key constraints.`
4073
+ );
3782
4074
  default:
3783
- break;
4075
+ // 绝不能静默 break —— 那会返回 ok:true 却什么都没做
4076
+ throw new Error(`ALTER TABLE: unsupported operation '${op.op}'`);
3784
4077
  }
3785
4078
  }
3786
4079
  await this._rebuildTableCache(table);
@@ -3946,6 +4239,9 @@ class SQLExecutor {
3946
4239
  data[col] = d[val._valuesOf] !== undefined ? d[val._valuesOf] : null;
3947
4240
  } else if (val && typeof val === 'object' && val._excludedOf !== undefined) {
3948
4241
  data[col] = d[val._excludedOf] !== undefined ? d[val._excludedOf] : null;
4242
+ } else if (val && typeof val === 'object' && val.type === 'binary') {
4243
+ // P1-19: ON DUPLICATE KEY UPDATE 支持表达式赋值,如 count = count + 1
4244
+ data[col] = evaluateExpr(val, d, this.ctx);
3949
4245
  } else {
3950
4246
  data[col] = val;
3951
4247
  }
@@ -4009,7 +4305,21 @@ class SQLExecutor {
4009
4305
  case 'with': {
4010
4306
  // CTE:按声明顺序把已展开的 CTE 内联进引用它的 FROM(含嵌套 CTE 引用)
4011
4307
  const map = new Map();
4308
+ // 递归 CTE:自身引用不能内联成子查询(会无限展开 / 找不到列),
4309
+ // 改由 _execRecursiveCte 迭代求解,这里先把名字登记进 map 供其识别。
4310
+ const recursiveNames = new Set();
4012
4311
  for (const cte of statement.ctes) {
4312
+ const key = String(cte.name).toLowerCase();
4313
+ // 不要求显式写 RECURSIVE:只要 CTE 引用自身就按递归求解。
4314
+ // 否则 `WITH c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c ...)`
4315
+ // 这种省略 RECURSIVE 的写法会走普通内联路径 → 找不到表 c。
4316
+ // 必须有 UNION:自引用但无 UNION 的 CTE 无法终止,不是递归 CTE。
4317
+ const hasUnion = !!cte.select.union;
4318
+ if (hasUnion && this._cteIsRecursive(cte, key)) {
4319
+ recursiveNames.add(key);
4320
+ map.set(key, cte.select); // 占位,实际不参与内联
4321
+ continue;
4322
+ }
4013
4323
  const sel = JSON.parse(JSON.stringify(cte.select));
4014
4324
  // CTE 显式列名清单 `name (a, b) AS (...)`:按顺序重命名输出列
4015
4325
  if (cte.columns && cte.columns.length) {
@@ -4018,7 +4328,10 @@ class SQLExecutor {
4018
4328
  });
4019
4329
  }
4020
4330
  this._inlineCteRefs(sel, map);
4021
- map.set(String(cte.name).toLowerCase(), sel);
4331
+ map.set(key, sel);
4332
+ }
4333
+ if (recursiveNames.size) {
4334
+ return await this._execWithRecursiveCtes(statement, map, recursiveNames);
4022
4335
  }
4023
4336
  const stmt = JSON.parse(JSON.stringify(statement.statement));
4024
4337
  this._inlineCteRefs(stmt, map);
@@ -4100,6 +4413,14 @@ class SQLExecutor {
4100
4413
  `CREATE INDEX is not supported by this engine (no createIndex/_ensureTable on the engine object)`
4101
4414
  );
4102
4415
  }
4416
+ // 登记 索引名 → 表 + 列,供 DROP INDEX 按索引名反查
4417
+ // (标准 SQL 的 DROP INDEX 不带表名,而存储层 dropIndex 收的是列名)
4418
+ if (this.engine) {
4419
+ if (!this.engine._indexRegistry) this.engine._indexRegistry = new Map();
4420
+ this.engine._indexRegistry.set(String(statement.name).toLowerCase(), {
4421
+ table: tName, columns: statement.columns.slice(),
4422
+ });
4423
+ }
4103
4424
  return {
4104
4425
  ok: true, type: 'createIndex', table: tName, name: statement.name,
4105
4426
  affectedRows: 0, warnings: [],
@@ -4107,14 +4428,49 @@ class SQLExecutor {
4107
4428
  };
4108
4429
  }
4109
4430
  case 'dropIndex': {
4110
- const tName2 = statement.table;
4431
+ // DROP INDEX 只给索引名(标准 SQL 不要求写表名),而存储层的
4432
+ // Table.dropIndex(field) 收的是**列名**。此前这里直接把索引名当列名传,
4433
+ // 又因 table=null 拿不到表对象 → 一律报「not supported」。
4434
+ // 现在:CREATE INDEX 时登记 索引名 → 表 + 列,删除时据此反查。
4435
+ let tName2 = statement.table;
4436
+ const rawName = String(statement.name || '');
4437
+ // 支持 `DROP INDEX t.idx_a` 形式
4438
+ if (!tName2 && rawName.includes('.')) {
4439
+ const parts = rawName.split('.');
4440
+ tName2 = parts[0];
4441
+ statement.name = parts[parts.length - 1];
4442
+ }
4443
+ if (!tName2 && this.engine && this.engine._indexRegistry) {
4444
+ const hit = this.engine._indexRegistry.get(String(statement.name || '').toLowerCase());
4445
+ if (hit) tName2 = hit.table;
4446
+ }
4111
4447
  let handled = false;
4448
+ let droppedField = null;
4449
+ const findTable = () => (typeof this.engine._ensureTable === 'function')
4450
+ ? (this.engine._ensureTable(tName2) || (this.engine._tables && this.engine._tables[tName2]))
4451
+ : null;
4112
4452
  if (typeof this.engine.dropIndex === 'function') {
4113
4453
  await this.engine.dropIndex(tName2, statement.name);
4114
4454
  handled = true;
4115
- } else if (tName2 && typeof this.engine._ensureTable === 'function') {
4116
- const tbl = this.engine._ensureTable(tName2) || (this.engine._tables && this.engine._tables[tName2]);
4117
- if (tbl && typeof tbl.dropIndex === 'function') { tbl.dropIndex(statement.name); handled = true; }
4455
+ } else {
4456
+ const tbl = tName2 ? findTable() : null;
4457
+ if (tbl && typeof tbl.dropIndex === 'function') {
4458
+ // 索引名 → 列名:优先用登记信息,否则当列名直接删(兼容单列同名的老用法)
4459
+ const reg = this.engine._indexRegistry && this.engine._indexRegistry.get(String(statement.name || '').toLowerCase());
4460
+ // 复合索引是逐列建的(a,b 各建一个),删除时必须全部清掉,
4461
+ // 否则只删首列会留下残余索引,白占内存还让查询走错路径。
4462
+ const fields = (reg && reg.columns && reg.columns.length) ? reg.columns.slice() : [statement.name];
4463
+ const present = fields.filter(f => tbl._indexes[f] || tbl._btrees[f]);
4464
+ if (present.length === 0) {
4465
+ if (statement.ifExists) {
4466
+ return { ok: true, type: 'dropIndex', affectedRows: 0, skipped: true, warnings: [], message: `Index '${statement.name}' did not exist` };
4467
+ }
4468
+ throw new Error(`Index '${statement.name}' does not exist`);
4469
+ }
4470
+ for (const f of present) tbl.dropIndex(f);
4471
+ droppedField = present.length === 1 ? present[0] : present.join(',');
4472
+ handled = true;
4473
+ }
4118
4474
  }
4119
4475
  if (!handled) {
4120
4476
  if (statement.ifExists) {
@@ -4122,30 +4478,48 @@ class SQLExecutor {
4122
4478
  }
4123
4479
  throw new Error(`DROP INDEX is not supported by this engine (no dropIndex on the engine/table object)`);
4124
4480
  }
4125
- return { ok: true, type: 'dropIndex', table: tName2, name: statement.name, affectedRows: 0, warnings: [], message: `Index '${statement.name}' dropped` };
4481
+ if (this.engine && this.engine._indexRegistry) this.engine._indexRegistry.delete(String(statement.name || '').toLowerCase());
4482
+ return {
4483
+ ok: true, type: 'dropIndex', table: tName2, name: statement.name,
4484
+ affectedRows: 0, warnings: [],
4485
+ message: `Index '${statement.name}' dropped` + (droppedField ? ` (column '${droppedField}')` : ''),
4486
+ };
4126
4487
  }
4127
4488
  case 'savepoint': {
4128
- // 引擎侧没有保存点/快照能力。这里只登记名字,绝不假装能回滚。
4129
- if (!this.engine._savepoints) this.engine._savepoints = new Set();
4130
- this.engine._savepoints.add(String(statement.name).toLowerCase());
4131
- return { ok: true, type: 'savepoint', message: `Savepoint '${statement.name}' noted`, affectedRows: 0, warnings: [] };
4489
+ requireSavepointEngine(this.engine, 'SAVEPOINT');
4490
+ this.engine.savepoint(statement.name);
4491
+ return {
4492
+ ok: true, type: 'savepoint', name: statement.name,
4493
+ message: `Savepoint '${statement.name}' created`,
4494
+ affectedRows: 0, warnings: []
4495
+ };
4132
4496
  }
4133
4497
  case 'releaseSavepoint': {
4134
- if (this.engine._savepoints) this.engine._savepoints.delete(String(statement.name).toLowerCase());
4135
- return { ok: true, type: 'releaseSavepoint', message: `Savepoint '${statement.name}' released`, affectedRows: 0, warnings: [] };
4498
+ requireSavepointEngine(this.engine, 'RELEASE SAVEPOINT');
4499
+ this.engine.releaseSavepoint(statement.name);
4500
+ return {
4501
+ ok: true, type: 'releaseSavepoint', name: statement.name,
4502
+ message: `Savepoint '${statement.name}' released`,
4503
+ affectedRows: 0, warnings: []
4504
+ };
4136
4505
  }
4137
4506
  case 'rollbackTo': {
4138
- // 明确报错而不是静默无效:静默的"回滚成功"会让调用方以为数据已还原。
4139
- throw new Error(
4140
- `ROLLBACK TO SAVEPOINT '${statement.name}' is not supported: the engine has no savepoint/snapshot support. ` +
4141
- `Use ROLLBACK to abort the whole transaction.`
4142
- );
4507
+ requireSavepointEngine(this.engine, 'ROLLBACK TO SAVEPOINT');
4508
+ this.engine.rollbackTo(statement.name);
4509
+ return {
4510
+ ok: true, type: 'rollbackTo', name: statement.name,
4511
+ message: `Rolled back to savepoint '${statement.name}'`,
4512
+ affectedRows: 0, warnings: []
4513
+ };
4143
4514
  }
4144
4515
  case 'update': {
4145
4516
  const schema = this.engine.getTableSchema
4146
4517
  ? await this.engine.getTableSchema(statement.table)
4147
4518
  : (this.engine._schemas ? this.engine._schemas[statement.table] : null);
4148
4519
  const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
4520
+ if (pkCols.length === 0) {
4521
+ throw createError('ER_PARSE_ERROR', `Table '${statement.table}' has no primary key, UPDATE requires a primary key`);
4522
+ }
4149
4523
  const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
4150
4524
  let count = 0;
4151
4525
  for (const row of all) {
@@ -4503,6 +4877,33 @@ class SQLExecutor {
4503
4877
  * 其它条件(范围、OR、函数、IN、子查询)一律不下推,交回内存过滤,
4504
4878
  * 避免为了快而改错结果。
4505
4879
  */
4880
+ /**
4881
+ * 将查询值转换为与列类型匹配的类型,避免隐式类型转换不一致。
4882
+ * 例如:WHERE int_col='1' 中 '1' 应转为整数 1。
4883
+ */
4884
+ _coerceValue(val, colDef) {
4885
+ if (val === null || val === undefined) return val;
4886
+ const type = colDef && colDef.type;
4887
+ if (!type) return val;
4888
+ if (type === 'integer' || type === 'int' || type === 'bigint' || type === 'smallint' || type === 'tinyint') {
4889
+ const n = Number(val);
4890
+ return Number.isNaN(n) ? val : n;
4891
+ }
4892
+ if (type === 'float' || type === 'double' || type === 'decimal' || type === 'real' || type === 'number') {
4893
+ const n = Number(val);
4894
+ return Number.isNaN(n) ? val : n;
4895
+ }
4896
+ if (type === 'boolean' || type === 'bool') {
4897
+ if (typeof val === 'string') {
4898
+ const lower = val.toLowerCase();
4899
+ if (lower === 'true' || lower === '1') return true;
4900
+ if (lower === 'false' || lower === '0') return false;
4901
+ }
4902
+ return val;
4903
+ }
4904
+ return val;
4905
+ }
4906
+
4506
4907
  _pushdownEqualities(statement) {
4507
4908
  const out = {};
4508
4909
  if (!statement || !statement.where || typeof statement.where !== 'object') return out;
@@ -4516,7 +4917,9 @@ class SQLExecutor {
4516
4917
  if (isCol(node.left) && isVal(node.right)) { col = node.left.name; val = node.right.value; }
4517
4918
  else if (isCol(node.right) && isVal(node.left)) { col = node.right.name; val = node.left.value; }
4518
4919
  if (col === null) return;
4519
- const key = col.includes('.') ? col.slice(col.lastIndexOf('.') + 1) : col;
4920
+ // 保留表限定信息:有前缀用 "table.col",无前缀用 "col"
4921
+ // 这样 _readTable 可以判断该条件是否属于当前表
4922
+ const key = col;
4520
4923
  // 同一列出现多个等值条件(如 id=1 AND id=2)时不推,交给内存求值
4521
4924
  if (Object.prototype.hasOwnProperty.call(out, key)) delete out[key];
4522
4925
  else out[key] = val;
@@ -4525,10 +4928,27 @@ class SQLExecutor {
4525
4928
  return out;
4526
4929
  }
4527
4930
 
4528
- async _readTable(table, statement) {
4931
+ async _readTable(table, statement, alias) {
4529
4932
  const schema = await this._getSchema(table);
4530
4933
  // 有可下推的等值条件时交给存储层走索引;没有则维持原来的全表读取
4531
- const filter = statement ? this._pushdownEqualities(statement) : {};
4934
+ const allFilters = statement ? this._pushdownEqualities(statement) : {};
4935
+ // 只保留属于当前表的条件:
4936
+ // - 带表限定的条件(如 "o.amount"):检查表名/别名是否匹配
4937
+ // - 不带表限定的条件(如 "amount"):检查当前表的 schema 是否包含该列
4938
+ const filter = {};
4939
+ for (const [key, val] of Object.entries(allFilters)) {
4940
+ if (key.includes('.')) {
4941
+ // 带表限定:检查是否匹配当前表的别名或表名
4942
+ const tablePart = key.slice(0, key.lastIndexOf('.'));
4943
+ if (tablePart === alias || tablePart === table) {
4944
+ const colPart = key.slice(key.lastIndexOf('.') + 1);
4945
+ if (schema[colPart]) filter[colPart] = this._coerceValue(val, schema[colPart]);
4946
+ }
4947
+ } else {
4948
+ // 不带表限定:只下推到持有该列的表
4949
+ if (schema[key]) filter[key] = this._coerceValue(val, schema[key]);
4950
+ }
4951
+ }
4532
4952
  const useIndex = Object.keys(filter).length > 0 &&
4533
4953
  (typeof this.engine.exists === 'function' || typeof this.engine.find === 'function');
4534
4954
  const rows = useIndex
@@ -4539,10 +4959,19 @@ class SQLExecutor {
4539
4959
 
4540
4960
  async _execFromItem(item) {
4541
4961
  if (item.subquery) {
4962
+ // 递归 CTE 物化出来的派生表:直接携带行数据
4963
+ if (item.subquery._virtualSource) {
4964
+ return { schema: null, rows: item.subquery._virtualSource.map(r => Object.assign({}, r)) };
4965
+ }
4542
4966
  const res = await this.executeSelect(item.subquery);
4543
4967
  return { schema: null, rows: res.rows.map(r => Object.assign({}, r)) };
4544
4968
  }
4545
- return this._readTable(item.table);
4969
+ // 递归 CTE 迭代用:把已累积的行当作一张临时表
4970
+ // (否则 FROM 会去找真实表,报 no such table / no such column)
4971
+ if (item._virtualRows) {
4972
+ return { schema: null, rows: item._virtualRows.map(r => Object.assign({}, r)) };
4973
+ }
4974
+ return this._readTable(item.table, item._stmt, item.alias || item.table);
4546
4975
  }
4547
4976
 
4548
4977
  _infoSchemaColumns(view) {
@@ -4676,6 +5105,11 @@ class SQLExecutor {
4676
5105
  }
4677
5106
  }
4678
5107
  table._rows.forEach((row, idx) => {
5108
+ // 重建 _pkIndex
5109
+ if (table._pkIndex && table._primaryKey) {
5110
+ const pkVal = row[table._primaryKey];
5111
+ if (pkVal !== undefined && pkVal !== null) table._pkIndex.set(pkVal, idx);
5112
+ }
4679
5113
  for (const [f, tree] of Object.entries(table._btrees)) {
4680
5114
  const v = row[f];
4681
5115
  if (v !== undefined && v !== null) tree.insert(v, idx);
@@ -5123,6 +5557,242 @@ class SQLExecutor {
5123
5557
  * 把 AST 中引用 CTE 名称的 FROM 项替换成对应的子查询(就地改写)。
5124
5558
  * map: 小写 CTE 名 -> 已展开的 select AST
5125
5559
  */
5560
+ /**
5561
+ * 判断某个 CTE 是否引用自身(因而需要递归求解)。
5562
+ * 自引用必须出现在 UNION / UNION ALL 的**递归项**(右侧)里;
5563
+ * 左侧(锚点)出现自引用无法终止,按普通 CTE 处理即可。
5564
+ */
5565
+ _cteIsRecursive(cte, key) {
5566
+ // 既接受 {select: ...}(CTE 节点),也接受裸 select 节点(UNION 链上的分支)。
5567
+ // 分支自身没有 union(链上最内层),所以只看「本分支内有没有自引用」,
5568
+ // 不要求存在 union —— 否则链上最后一个分支永远判不出递归。
5569
+ const sel = cte && cte.type === 'select' ? cte : (cte && cte.select);
5570
+ if (!sel) return false;
5571
+ let found = false;
5572
+ const visit = (n) => {
5573
+ if (found || !n || typeof n !== 'object') return;
5574
+ if (Array.isArray(n)) { for (const x of n) visit(x); return; }
5575
+ if (n.from) {
5576
+ for (const t of (n.from.tables || [])) {
5577
+ if (t && t.table && String(t.table).toLowerCase() === key) { found = true; return; }
5578
+ }
5579
+ for (const j of (n.from.joins || [])) {
5580
+ if (j && j.item && j.item.table && String(j.item.table).toLowerCase() === key) { found = true; return; }
5581
+ }
5582
+ }
5583
+ for (const k of Object.keys(n)) { if (k !== 'from') visit(n[k]); }
5584
+ };
5585
+ visit(sel);
5586
+ return found;
5587
+ }
5588
+
5589
+ /**
5590
+ * 求解带递归 CTE 的 WITH 语句。
5591
+ *
5592
+ * 标准语义:递归 CTE = 锚点(anchor)SELECT UNION [ALL] 递归项 SELECT。
5593
+ * 递归项里的自身引用指向「到目前为止累积的所有行」,反复迭代直到不再产生新行。
5594
+ * UNION ALL 允许重复行与无限递归风险,故设迭代上限兜底。
5595
+ */
5596
+ async _execWithRecursiveCtes(statement, map, recursiveNames) {
5597
+ // 先把递归 CTE 逐个求值,结果作为「派生表」供后续 CTE 与主查询引用
5598
+ const resolved = new Map();
5599
+ for (const cte of statement.ctes) {
5600
+ const key = String(cte.name).toLowerCase();
5601
+ if (!recursiveNames.has(key)) {
5602
+ resolved.set(key, map.get(key));
5603
+ continue;
5604
+ }
5605
+ const rows = await this._evalRecursiveCte(cte, key, resolved);
5606
+ // 递归结果物化成派生表,主查询与其它 CTE 都能引用。
5607
+ // 保留输出列名(CTE 声明的列名清单或首行列名),外层才能按 alias 引用。
5608
+ const outCols = (cte.columns && cte.columns.length)
5609
+ ? cte.columns.slice()
5610
+ : Object.keys(rows[0] || { c0: 1 });
5611
+ const sub = { type: 'select', columns: outCols.map(c => ({ expr: c, alias: c })), _virtualSource: rows };
5612
+ resolved.set(key, sub);
5613
+ }
5614
+ // 主查询:把递归名替换成携带行数据的子查询
5615
+ const stmt = JSON.parse(JSON.stringify(statement.statement));
5616
+ const inject = (node) => {
5617
+ if (!node || typeof node !== 'object') return;
5618
+ if (Array.isArray(node)) { for (const x of node) inject(x); return; }
5619
+ if (node.type === 'select' && node.from) {
5620
+ const fix = (item) => {
5621
+ if (!item || !item.table) return;
5622
+ const k = String(item.table).toLowerCase();
5623
+ if (!resolved.has(k)) return;
5624
+ if (process.env.JSQL_DBG) console.error('[dbg] fix hit:', k, 'alias=', item.alias);
5625
+ const src = resolved.get(k);
5626
+ if (src && src._virtualSource) {
5627
+ // 保留别名并清掉 table 名,否则外层 `FROM c c1` 找不到物化结果
5628
+ item.subquery = JSON.parse(JSON.stringify(src));
5629
+ item.alias = item.alias || item.table;
5630
+ delete item.table;
5631
+ } else {
5632
+ item.subquery = JSON.parse(JSON.stringify(src));
5633
+ item.alias = item.alias || item.table;
5634
+ delete item.table;
5635
+ }
5636
+ };
5637
+ for (const t of (node.from.tables || [])) fix(t);
5638
+ for (const j of (node.from.joins || [])) fix(j.item);
5639
+ }
5640
+ for (const k of Object.keys(node)) { if (k !== 'from') inject(node[k]); }
5641
+ };
5642
+ inject(stmt);
5643
+ if (process.env.JSQL_DBG) console.error('[dbg] inject done, FROM=', JSON.stringify(stmt.from));
5644
+ return await this.execute(stmt);
5645
+ }
5646
+
5647
+ /** 迭代求解单个递归 CTE,返回全部累积行 */
5648
+ async _evalRecursiveCte(cte, key, resolved) {
5649
+ const sel = cte.select;
5650
+ // 兜底:UNION ALL 允许重复行,条件写错(如 WHERE 子句恒真)会导致无限增长。
5651
+ // 单看迭代次数不够 —— 每轮累积行数可能翻倍,1000 轮就是 2^1000 行,
5652
+ // 内存会先于迭代上限被打爆(实测 OOM)。所以同时限制总行数。
5653
+ const MAX_ITER = 1000;
5654
+ const MAX_ROWS = 100000;
5655
+ let accumulated = [];
5656
+ let recSel = null; // 声明在 if 块外:循环里要用
5657
+
5658
+ if (sel.union) {
5659
+ // UNION 链里可能有多个分支(`SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT n+1 FROM c`)。
5660
+ // 语义:含自身引用的分支是**递归项**(只能有一个,取最后一个),
5661
+ // 前面所有分支都是**锚点**,结果并入起始集。
5662
+ // 此前只拆一层(左侧当锚点、右侧当递归项),
5663
+ // 多个锚点时中间分支被当成递归项喂回 → 行数暴涨。
5664
+ const branches = [];
5665
+ let cur = sel;
5666
+ while (cur && cur.union) { branches.push(cur); cur = cur.union.select; }
5667
+ if (cur) branches.push(cur); // 最内层(无 union)
5668
+
5669
+ const selfRef = (n) => this._cteIsRecursive({ select: n }, key);
5670
+ const isRec = (n) => selfRef(n);
5671
+ // 最后一个含自引用的分支 = 递归项;其后的分支(若有)也并入锚点
5672
+ let recIdx = -1;
5673
+ branches.forEach((b, i) => { if (isRec(b)) recIdx = i; });
5674
+ let anchorBranches;
5675
+ if (recIdx === -1) { anchorBranches = branches; recSel = null; }
5676
+ else {
5677
+ anchorBranches = branches.slice(0, recIdx);
5678
+ recSel = branches[recIdx];
5679
+ // 递归项之后还挂着的分支(非标准写法)也当锚点,避免丢结果
5680
+ for (let i = recIdx + 1; i < branches.length; i++) anchorBranches.push(branches[i]);
5681
+ }
5682
+
5683
+ // 执行锚点:逐个分支求值后合并
5684
+ const want = (cte.columns && cte.columns.length) || (sel.columns || []).length;
5685
+ for (const b of anchorBranches) {
5686
+ // 关键:分支是 UNION 链上的一节,可能还挂着更深的 union.select。
5687
+ // 只清 union=null 不够 —— 残留的深层分支里若含 FROM c 就会去查真实表。
5688
+ // 这里只取该分支自身的 SELECT(含它自己的 from/where/columns),
5689
+ // 其 union 指向的分支已在 branches 里单独处理。
5690
+ const anchorStmt = {
5691
+ type: 'select',
5692
+ columns: b.columns,
5693
+ from: b.from,
5694
+ where: b.where,
5695
+ groupBy: b.groupBy,
5696
+ having: b.having,
5697
+ orderBy: null,
5698
+ limit: null,
5699
+ offset: 0,
5700
+ union: null, intersect: null, except: null, rollup: null,
5701
+ aggregate: b.aggregate, distinct: b.distinct,
5702
+ };
5703
+ if (want) {
5704
+ anchorStmt.columns = (anchorStmt.columns || []).slice(0, want)
5705
+ .map((c, i) => ({ ...c, alias: (cte.columns && cte.columns[i]) || c.alias || ('c' + i) }));
5706
+ }
5707
+ const anchorRes = await this.executeSelect(anchorStmt);
5708
+ accumulated = accumulated.concat(anchorRes.rows.map(r => Object.assign({}, r)));
5709
+ }
5710
+ if (!recSel) return accumulated;
5711
+ } else {
5712
+ return [];
5713
+ }
5714
+
5715
+ // 显式列名清单:把输出列改名,便于递归项引用
5716
+ if (cte.columns && cte.columns.length) {
5717
+ accumulated = accumulated.map((r) => {
5718
+ const keys = Object.keys(r);
5719
+ const out = {};
5720
+ cte.columns.forEach((c, idx) => { out[c] = r[keys[idx]]; });
5721
+ return out;
5722
+ });
5723
+ }
5724
+
5725
+ const isAll = !!sel.union.all;
5726
+ // semi-naive 求值:每轮只对本轮「新增」的行做一次递归。
5727
+ // 若把全部累积行都喂回递归项,每行都会再产出一行 → 行数按轮次递增,
5728
+ // 结果里出现大量重复(1..5 会得到 1,2,2,3,2,3,3,4,…)。
5729
+ let frontier = accumulated;
5730
+ for (let iter = 0; iter < MAX_ITER; iter++) {
5731
+ if (frontier.length === 0) break;
5732
+ // 把自身引用换成「本轮新增的行」。
5733
+ // 只取递归项分支自身的 SELECT:它可能挂着更深的 union.select,
5734
+ // 整段丢给 executeSelect 会再跑一轮 UNION,把自引用当普通表查。
5735
+ const recBranch = JSON.parse(JSON.stringify(recSel));
5736
+ const step = {
5737
+ type: 'select',
5738
+ columns: recBranch.columns,
5739
+ from: recBranch.from,
5740
+ where: recBranch.where,
5741
+ groupBy: recBranch.groupBy,
5742
+ having: recBranch.having,
5743
+ orderBy: null, limit: null, offset: 0,
5744
+ union: null, intersect: null, except: null, rollup: null,
5745
+ aggregate: recBranch.aggregate, distinct: recBranch.distinct,
5746
+ };
5747
+ const bind = (node) => {
5748
+ if (!node || typeof node !== 'object') return;
5749
+ if (Array.isArray(node)) { for (const x of node) bind(x); return; }
5750
+ if (node.type === 'select' && node.from) {
5751
+ const fix = (item) => {
5752
+ if (item && item.table && String(item.table).toLowerCase() === key) {
5753
+ item._virtualRows = frontier;
5754
+ item.alias = item.alias || item.table;
5755
+ delete item.table;
5756
+ }
5757
+ };
5758
+ for (const t of (node.from.tables || [])) fix(t);
5759
+ for (const j of (node.from.joins || [])) fix(j.item);
5760
+ }
5761
+ for (const k of Object.keys(node)) { if (k !== 'from') bind(node[k]); }
5762
+ };
5763
+ bind(step);
5764
+ // 递归项的输出列强制用 CTE 声明的列名 —— 表达式文本('n + 1')不能当列名,
5765
+ // 否则下一轮迭代取不到列。
5766
+ if (cte.columns && cte.columns.length) {
5767
+ step.columns = (step.columns || []).slice(0, cte.columns.length)
5768
+ .map((c, i) => ({ ...c, alias: cte.columns[i] }));
5769
+ }
5770
+ const res = await this.executeSelect(step);
5771
+ let next = res.rows.map(r => Object.assign({}, r));
5772
+ if (cte.columns && cte.columns.length) {
5773
+ next = next.map(r => {
5774
+ const keys = Object.keys(r);
5775
+ const out = {};
5776
+ cte.columns.forEach((c, idx) => { out[c] = r[keys[idx]]; });
5777
+ return out;
5778
+ });
5779
+ }
5780
+ if (!isAll) {
5781
+ const seen = new Set(accumulated.map(r => JSON.stringify(r)));
5782
+ next = next.filter(r => !seen.has(JSON.stringify(r)));
5783
+ }
5784
+ if (next.length === 0) break;
5785
+ // 超过行数上限说明递归条件写错(恒真),提前收手而不是撑爆内存
5786
+ if (accumulated.length + next.length > MAX_ROWS) {
5787
+ accumulated = accumulated.concat(next.slice(0, MAX_ROWS - accumulated.length));
5788
+ break;
5789
+ }
5790
+ accumulated = accumulated.concat(next);
5791
+ frontier = next; // 下一轮只喂本轮新增的行
5792
+ }
5793
+ return accumulated;
5794
+ }
5795
+
5126
5796
  _inlineCteRefs(node, map) {
5127
5797
  if (!node || typeof node !== 'object' || map.size === 0) return;
5128
5798
  if (Array.isArray(node)) {
@@ -5413,21 +6083,59 @@ class SQLExecutor {
5413
6083
  return { ok: true, type: 'select', table: firstItem.table, columns: outCols, rows, raw: filtered };
5414
6084
  }
5415
6085
  if (firstItem.subquery) {
5416
- const res = await this.executeSelect(firstItem.subquery);
5417
- rowsAll = { rows: this._subQueryRows(res), schema: null, columns: res.columns };
6086
+ // 递归 CTE 物化出来的派生表:直接携带行数据
6087
+ if (firstItem.subquery._virtualSource) {
6088
+ rowsAll = { rows: firstItem.subquery._virtualSource.map(r => Object.assign({}, r)), schema: null, columns: null };
6089
+ } else {
6090
+ const res = await this.executeSelect(firstItem.subquery);
6091
+ rowsAll = { rows: this._subQueryRows(res), schema: null, columns: res.columns };
6092
+ }
6093
+ } else if (firstItem._virtualRows) {
6094
+ // 递归 CTE 迭代中:自身引用绑定到已累积的行
6095
+ rowsAll = { rows: firstItem._virtualRows.map(r => Object.assign({}, r)), schema: null, columns: null };
5418
6096
  } else {
5419
- rowsAll = await this._readTable(firstItem.table, statement);
6097
+ rowsAll = await this._readTable(firstItem.table, statement, firstItem.alias || firstItem.table);
5420
6098
  }
5421
6099
  schema = rowsAll.schema;
5422
6100
  const firstPrefix = firstItem.alias || firstItem.table;
5423
- let rows = rowsAll.rows.map(r => this._prefixRow(r, firstPrefix));
6101
+ // 虚拟行分两种,语义不同:
6102
+ // - _virtualRows:递归 CTE **迭代过程内部**,FROM 是自身引用,列名不带前缀
6103
+ // (否则递归项里的 `n+1` 取不到值)
6104
+ // - subquery._virtualSource:递归 CTE **求值完成后**物化成的派生表,
6105
+ // 与普通子查询一样按别名加前缀,外层才能写 c.n / x.n
6106
+ const isVirtualSource = !!(firstItem.subquery && firstItem.subquery._virtualSource);
6107
+ // 物化派生表:行键需要同时有 'c.n'(限定引用)与 'n'(非限定引用)。
6108
+ // _prefixRow 正是这么做的 —— 但 SELECT * 会把两者都投影,同一列输出两次。
6109
+ // 所以额外记下真实列名,SELECT * 时按它们去重。
6110
+ this._curStarCols = isVirtualSource
6111
+ ? (firstItem.subquery.columns || []).map(c => {
6112
+ const a = c.alias || c.expr;
6113
+ return (a && String(a).includes('.') ? String(a) : `${firstPrefix}.${a}`);
6114
+ }).filter(Boolean)
6115
+ : null;
6116
+ let rows = isVirtualSource
6117
+ ? rowsAll.rows.map(r => this._prefixRow(r, firstPrefix))
6118
+ : (firstItem._virtualRows ? rowsAll.rows : rowsAll.rows.map(r => this._prefixRow(r, firstPrefix)));
5424
6119
 
5425
6120
  for (const j of statement.from.joins) {
5426
- const rightRes = j.item.subquery
5427
- ? await this.executeSelect(j.item.subquery)
5428
- : await this._readTable(j.item.table, statement);
6121
+ // 虚拟行(递归 CTE 物化的派生表)要直接取行,不能当普通子查询执行
6122
+ const jVirtual = j.item._virtualRows ||
6123
+ (j.item.subquery && j.item.subquery._virtualSource);
6124
+ const rightRes = jVirtual
6125
+ ? { rows: (j.item._virtualRows || j.item.subquery._virtualSource).map(r => Object.assign({}, r)) }
6126
+ : j.item.subquery
6127
+ ? await this.executeSelect(j.item.subquery)
6128
+ : await this._readTable(j.item.table, statement, j.item.alias || j.item.table);
5429
6129
  const rightPrefix = j.item.alias || j.item.table;
5430
- const rightRows = (j.item.subquery ? this._subQueryRows(rightRes) : rightRes.rows).map(r => this._prefixRow(r, rightPrefix));
6130
+ // 迭代内部的虚拟行不加前缀;物化后的派生表要加(与普通子查询一致)
6131
+ const jRaw = j.item._virtualRows
6132
+ ? j.item._virtualRows
6133
+ : (j.item.subquery && j.item.subquery._virtualSource
6134
+ ? j.item.subquery._virtualSource
6135
+ : (j.item.subquery ? this._subQueryRows(rightRes) : rightRes.rows));
6136
+ const rightRows = (j.item._virtualRows && !j.item.subquery)
6137
+ ? jRaw
6138
+ : jRaw.map(r => this._prefixRow(r, rightPrefix));
5431
6139
 
5432
6140
  // 匹配(INNER/LEFT/RIGHT)
5433
6141
  if (j.type === 'cross' || (!j.on)) {
@@ -5537,7 +6245,21 @@ class SQLExecutor {
5537
6245
  const key = JSON.stringify(statement.columns.map(c => {
5538
6246
  // SELECT DISTINCT *:按整行去重(此前把 '*' 当列名解析成 null,导致所有行同 key 只剩 1 行)
5539
6247
  if (c.expr === '*') {
5540
- return Object.keys(r).filter(k => !String(k).startsWith('_')).sort().map(k => r[k]);
6248
+ const allKeys = Object.keys(r).filter(k => !String(k).startsWith('_'));
6249
+ // 物化派生表(递归 CTE 结果)的行同时带 'c.n' 与 'n' 两个键 ——
6250
+ // 前者供限定引用、后者供非限定引用。SELECT * 时两者都会被投影,
6251
+ // 同一列会输出两次,所以按 FROM 里的声明列去重,保留带前缀的那个。
6252
+ // 现算而不是读实例字段:投影发生在另一次调用里,字段已被覆盖。
6253
+ const fi = statement.from && statement.from.tables && statement.from.tables[0];
6254
+ const vs = fi && fi.subquery && fi.subquery._virtualSource && fi.subquery.columns;
6255
+ if (vs && vs.length) {
6256
+ return vs.map(c => {
6257
+ const a = c.alias || c.expr;
6258
+ const key = (a && String(a).includes('.')) ? String(a) : `${fi.alias || fi.table}.${a}`;
6259
+ return r[key] !== undefined ? r[key] : r[a];
6260
+ });
6261
+ }
6262
+ return allKeys.map(k => r[k]);
5541
6263
  }
5542
6264
  if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
5543
6265
  if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
@@ -5669,7 +6391,8 @@ class SQLExecutor {
5669
6391
  if (av === bv || (av === undefined && bv === undefined)) continue;
5670
6392
  if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
5671
6393
  if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
5672
- const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
6394
+ // P2-1: localeCompare 对字符串排序受 locale 影响,应使用简单字符串比较
6395
+ const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : (String(av) < String(bv) ? -1 : String(av) > String(bv) ? 1 : 0);
5673
6396
  if (r !== 0) return o.dir === 'asc' ? r : -r;
5674
6397
  }
5675
6398
  return 0;
@@ -5689,21 +6412,56 @@ class SQLExecutor {
5689
6412
  const pkCols = schemaKeys.filter(k => schema && schema[k] && schema[k].primaryKey);
5690
6413
  const pk = pkCols.length > 0 ? pkCols[0] : (schemaKeys[0] || 'id');
5691
6414
  let cols;
5692
- if (schema) {
6415
+ // 有 JOIN 时,SELECT * 应展开所有表的列(合并后的行包含所有前缀列)
6416
+ const hasJoins = statement.from && statement.from.joins && statement.from.joins.length > 0;
6417
+ if (schema && !hasJoins) {
5693
6418
  cols = [pk, ...schemaKeys.filter(k => k !== pk)];
5694
6419
  } else if (rowsAll && rowsAll.columns) {
5695
6420
  cols = rowsAll.columns;
5696
6421
  } else {
5697
- cols = Object.keys(all[0] || {}).filter((v, i, a) => a.indexOf(v) === i);
6422
+ // JOIN 时:只使用带前缀的键,避免同一列重复
6423
+ if (hasJoins) {
6424
+ const seen = new Set();
6425
+ cols = [];
6426
+ for (const r of rows) {
6427
+ for (const k of Object.keys(r)) {
6428
+ const bare = k.includes('.') ? k.slice(k.indexOf('.') + 1) : k;
6429
+ if (!seen.has(bare)) { seen.add(bare); cols.push(k); }
6430
+ }
6431
+ }
6432
+ } else {
6433
+ const allKeys = [];
6434
+ for (const r of rows) {
6435
+ for (const k of Object.keys(r)) {
6436
+ if (!allKeys.includes(k)) allKeys.push(k);
6437
+ }
6438
+ }
6439
+ cols = allKeys;
6440
+ }
6441
+ }
6442
+ // 物化派生表(递归 CTE 结果)的行同时带 'c.n' 与 'n':前者供限定引用、
6443
+ // 后者供非限定引用。SELECT * 会把两者都输出,同一列重复。
6444
+ // 按声明的列去重,优先取带前缀的那个键。
6445
+ const fi = statement.from && statement.from.tables && statement.from.tables[0];
6446
+ const vs = fi && fi.subquery && fi.subquery._virtualSource && fi.subquery.columns;
6447
+ if (vs && vs.length) {
6448
+ cols = vs.map(c => {
6449
+ const a = c.alias || c.expr;
6450
+ return (a && String(a).includes('.')) ? String(a) : `${fi.alias || fi.table}.${a}`;
6451
+ });
5698
6452
  }
5699
- return { ok: true, type: 'select', table: tableName, columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
6453
+ return { ok: true, type: 'select', table: tableName, columns: cols, rows: rows.map(r => cols.map(c => (r[c] !== undefined ? r[c] : r[String(c).slice(String(c).indexOf('.') + 1)]))), raw: rows };
5700
6454
  }
5701
6455
 
5702
6456
  const cols = statement.columns.map(c => scalarColumnName(c));
5703
6457
  const mapped = await this._projectRows(statement, rows, (r) => statement.columns.map(c => {
5704
6458
  if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
5705
6459
  if (c.scalar) return projectScalar(resolveOperand(c.scalar, r, this.ctx));
5706
- if (c.expr === '*') return null;
6460
+ // P1-18: SELECT *, col 星号列输出 null——应展开为所有列
6461
+ if (c.expr === '*') {
6462
+ // 展开星号:返回行的所有值
6463
+ return Object.keys(r).map(k => r[k]);
6464
+ }
5707
6465
  if (c.literal !== undefined) return c.literal;
5708
6466
  if (c.caseExpr) return projectScalar(evaluateCaseVal(c.caseExpr, r));
5709
6467
  return projectScalar(resolveOperand({ type: 'column', name: c.expr }, r, this.ctx));
@@ -5846,7 +6604,7 @@ function escapeValue(value) {
5846
6604
  }
5847
6605
  return value.map(escapeValue).join(', ');
5848
6606
  }
5849
- if (typeof value === 'object') return "'" + JSON.stringify(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
6607
+ if (typeof value === 'object') return "'" + JSON.stringify(value).replace(/'/g, "\\'") + "'";
5850
6608
  const str = String(value)
5851
6609
  .replace(/\\/g, '\\\\')
5852
6610
  .replace(/\0/g, '\\0')
@@ -6075,6 +6833,23 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
6075
6833
  // AST 访问与改写能力(纯新增,不改变既有导出项)
6076
6834
  const AST = require('./ast.js');
6077
6835
 
6836
+ /**
6837
+ * 保存点需要引擎侧真的能做行级快照,否则「回滚成功」是假的。
6838
+ *
6839
+ * 内置的 Database 引擎有完整实现;wasm / native 引擎的事务在 Rust 侧,
6840
+ * 目前还没有保存点 —— 那种情况必须明确报错,不能静默假装成功。
6841
+ */
6842
+ function requireSavepointEngine(engine, op) {
6843
+ if (engine && typeof engine.savepoint === 'function') return;
6844
+ const who = (engine && engine.constructor && engine.constructor.name) || 'this engine';
6845
+ throw new Error(
6846
+ `${op} is not supported by ${who}: it has no savepoint support. ` +
6847
+ `The built-in Database engine (require('jsql-neo').Database) supports it; ` +
6848
+ `the wasm/native engines do not yet. ` +
6849
+ `Use ROLLBACK to abort the whole transaction instead.`
6850
+ );
6851
+ }
6852
+
6078
6853
  module.exports = {
6079
6854
  tokenize, parseSQL, executeSQL, SQLExecutor, Parser,
6080
6855
  splitStatements, applyParams, escapeValue, escapeId,