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/README.md +78 -8239
- package/index.js +1 -1
- package/lib/cache.js +33 -27
- package/lib/client.js +11 -2
- package/lib/database.js +370 -56
- package/lib/date-types.js +51 -13
- package/lib/errors.js +4 -1
- package/lib/jsql_format.js +30 -21
- package/lib/mod.js +27 -6
- package/lib/mongo_server.js +143 -26
- package/lib/mysql_compat.js +57 -11
- package/lib/mysql_server.js +104 -4
- package/lib/nedb_compat.js +9 -6
- package/lib/pg_server.js +4 -11
- package/lib/plugin.js +20 -2
- package/lib/prepared.js +10 -0
- package/lib/query.js +9 -2
- package/lib/redis_server.js +88 -16
- package/lib/sql.js +856 -81
- package/lib/sqlite_compat.js +2 -1
- package/lib/sqlite_worker.js +8 -2
- package/lib/table.js +68 -26
- package/lib/tui.js +1 -1
- package/lib/web_ui.js +24 -8
- package/native/jsql-neo-native.node +0 -0
- package/package.json +10 -3
- package/test/alter-table.test.js +289 -0
- package/test/identifier-keywords.test.js +140 -0
- package/test/index-ddl.test.js +203 -0
- package/test/mongo-group.test.js +105 -0
- package/test/null-semantics.test.js +126 -0
- package/test/recursive-cte.test.js +123 -0
- package/test/savepoint.test.js +388 -0
- package/test/sql-parser.test.js +30 -5
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
|
-
|
|
20
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
552
|
+
return nameOfToken(t) + '.' + nameOfToken(t2);
|
|
478
553
|
}
|
|
479
|
-
return t
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|
|
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
|
|
1347
|
-
return t
|
|
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
|
|
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')) {
|
|
1520
|
-
|
|
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
|
-
|
|
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
|
|
1973
|
-
return { type: 'column', name: t
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2197
|
-
|
|
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 '-':
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
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
|
-
|
|
2635
|
-
|
|
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':
|
|
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
|
-
|
|
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':
|
|
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':
|
|
3318
|
-
|
|
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
|
-
|
|
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 '
|
|
3781
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
4116
|
-
const tbl =
|
|
4117
|
-
if (tbl && typeof tbl.dropIndex === 'function') {
|
|
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
|
-
|
|
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
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
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
|
-
|
|
4135
|
-
|
|
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
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|