jsql-neo 4.5.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/mysql_server.js +1 -1
- package/lib/native_client.js +102 -13
- package/lib/sql.js +394 -40
- package/lib/sqlite_compat.js +323 -0
- package/lib/sqlite_worker.js +260 -0
- package/lib/wasm_client.js +85 -1
- package/package.json +3 -2
package/lib/sql.js
CHANGED
|
@@ -55,7 +55,8 @@ const KEYWORDS = new Set([
|
|
|
55
55
|
'GROUP', 'HAVING', 'UNION', 'ALL', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
|
|
56
56
|
'BETWEEN', 'USING', 'FULL', 'UNSIGNED', 'ZEROFILL', 'TRUNCATE', 'COLLATE', 'CHARACTER',
|
|
57
57
|
'ALTER', 'ADD', 'COLUMN', 'MODIFY', 'CHANGE', 'INDEX', 'FOREIGN', 'REFERENCES',
|
|
58
|
-
'CONSTRAINT', 'RENAME', 'TO', 'AFTER', 'FIRST', 'ENGINE', 'AUTO_INCREMENT', 'SPATIAL'
|
|
58
|
+
'CONSTRAINT', 'RENAME', 'TO', 'AFTER', 'FIRST', 'ENGINE', 'AUTO_INCREMENT', 'SPATIAL',
|
|
59
|
+
'REGEXP', 'TRUE', 'FALSE', 'RLIKE', 'PRAGMA', 'REPLACE', 'BLOB', 'RAISE', 'IGNORE'
|
|
59
60
|
]);
|
|
60
61
|
|
|
61
62
|
function tokenize(sql) {
|
|
@@ -244,6 +245,7 @@ class Parser {
|
|
|
244
245
|
case 'SET': return this.parseSet();
|
|
245
246
|
case 'DESCRIBE': case 'DESC': return this.parseDescribe();
|
|
246
247
|
case 'USE': return this.parseUse();
|
|
248
|
+
case 'PRAGMA': return this.parsePragma();
|
|
247
249
|
default: throw new Error(`Unsupported statement: ${t.value}`);
|
|
248
250
|
}
|
|
249
251
|
}
|
|
@@ -379,9 +381,14 @@ class Parser {
|
|
|
379
381
|
if (t.type === 'keyword') {
|
|
380
382
|
switch (t.value) {
|
|
381
383
|
case 'PRIMARY':
|
|
382
|
-
this.next(); this.expectKeyword('KEY'); def.primaryKey = true; def.unique = true;
|
|
384
|
+
this.next(); this.expectKeyword('KEY'); def.primaryKey = true; def.unique = true;
|
|
385
|
+
// SQLite 语义: INTEGER PRIMARY KEY 是 rowid 别名, 自动生成
|
|
386
|
+
if (mapped === 'integer') def.autoIncrement = true;
|
|
387
|
+
break;
|
|
383
388
|
case 'KEY':
|
|
384
|
-
this.next(); def.primaryKey = true; def.unique = true;
|
|
389
|
+
this.next(); def.primaryKey = true; def.unique = true;
|
|
390
|
+
if (mapped === 'integer') def.autoIncrement = true;
|
|
391
|
+
break;
|
|
385
392
|
case 'AUTO_INCREMENT':
|
|
386
393
|
case 'AUTOINCREMENT':
|
|
387
394
|
this.next(); def.autoIncrement = true; break;
|
|
@@ -585,8 +592,9 @@ class Parser {
|
|
|
585
592
|
if (this.isKeyword('LIMIT')) {
|
|
586
593
|
this.next();
|
|
587
594
|
limit = this.parseValue();
|
|
588
|
-
if (
|
|
589
|
-
|
|
595
|
+
if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
|
|
596
|
+
if (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); if (typeof offset === 'number' && offset < 0) throw new Error(`OFFSET must be a non-negative integer, got ${offset}`); }
|
|
597
|
+
else if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); offset = limit; limit = this.parseValue(); if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`); }
|
|
590
598
|
}
|
|
591
599
|
let union = null;
|
|
592
600
|
if (this.isKeyword('UNION')) {
|
|
@@ -684,7 +692,7 @@ class Parser {
|
|
|
684
692
|
while (true) {
|
|
685
693
|
const col = this.parseColumnRef();
|
|
686
694
|
this.expect('op', '=');
|
|
687
|
-
assignments.push([col, this.
|
|
695
|
+
assignments.push([col, this.parseScalar()]);
|
|
688
696
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
689
697
|
break;
|
|
690
698
|
}
|
|
@@ -950,6 +958,37 @@ class Parser {
|
|
|
950
958
|
return { type: 'use', database: db };
|
|
951
959
|
}
|
|
952
960
|
|
|
961
|
+
parsePragma() {
|
|
962
|
+
this.expectKeyword('PRAGMA');
|
|
963
|
+
let name = '';
|
|
964
|
+
const first = this.next();
|
|
965
|
+
if (first.type !== 'ident' && first.type !== 'keyword') throw new Error(`Expected pragma name, got '${first.value}'`);
|
|
966
|
+
name = first.value;
|
|
967
|
+
// pragma 名可能带 db. 前缀: PRAGMA main.table_info(users)
|
|
968
|
+
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
969
|
+
this.next();
|
|
970
|
+
const second = this.next();
|
|
971
|
+
if (second.type !== 'ident' && second.type !== 'keyword') throw new Error(`Expected pragma name after '.', got '${second.value}'`);
|
|
972
|
+
name = first.value + '.' + second.value;
|
|
973
|
+
}
|
|
974
|
+
let arg = null;
|
|
975
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
976
|
+
this.next();
|
|
977
|
+
const a = this.next();
|
|
978
|
+
if (a.type !== 'op' && a.type !== 'eof') arg = a.value;
|
|
979
|
+
if (this.peek().type === 'op' && this.peek().value === ')') this.next();
|
|
980
|
+
} else if (this.peek().type === 'op' && this.peek().value === '=') {
|
|
981
|
+
this.next();
|
|
982
|
+
const v = this.next();
|
|
983
|
+
if (v.type === 'number' || v.type === 'ident' || v.type === 'keyword' || v.type === 'string') arg = v.value;
|
|
984
|
+
} else if (this.peek().type !== 'eof' && !(this.peek().type === 'op' && this.peek().value === ';')) {
|
|
985
|
+
const v = this.next();
|
|
986
|
+
if (v.type === 'number' || v.type === 'ident' || v.type === 'keyword') arg = v.value;
|
|
987
|
+
}
|
|
988
|
+
this.optionalTailSemicolon();
|
|
989
|
+
return { type: 'pragma', name, arg };
|
|
990
|
+
}
|
|
991
|
+
|
|
953
992
|
optionalTailSemicolon() {
|
|
954
993
|
if (this.peek().type === 'op' && this.peek().value === ';') this.next();
|
|
955
994
|
}
|
|
@@ -1010,7 +1049,8 @@ class Parser {
|
|
|
1010
1049
|
const args = [];
|
|
1011
1050
|
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1012
1051
|
for (;;) {
|
|
1013
|
-
|
|
1052
|
+
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
|
|
1053
|
+
else args.push(this.parseOperand());
|
|
1014
1054
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1015
1055
|
break;
|
|
1016
1056
|
}
|
|
@@ -1030,7 +1070,8 @@ class Parser {
|
|
|
1030
1070
|
const args = [];
|
|
1031
1071
|
if (!(this.peek().type === 'op' && this.peek().value === ')')) {
|
|
1032
1072
|
for (;;) {
|
|
1033
|
-
|
|
1073
|
+
if (this.peek().type === 'op' && this.peek().value === '*') { this.next(); args.push({ type: 'star' }); }
|
|
1074
|
+
else args.push(this.parseOperand());
|
|
1034
1075
|
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1035
1076
|
break;
|
|
1036
1077
|
}
|
|
@@ -1073,6 +1114,39 @@ class Parser {
|
|
|
1073
1114
|
}
|
|
1074
1115
|
break;
|
|
1075
1116
|
}
|
|
1117
|
+
// 后缀:expr IN (...)、expr IS [NOT] TRUE/FALSE/NULL(标量上下文,如 SELECT 1 IN (...))
|
|
1118
|
+
for (;;) {
|
|
1119
|
+
const t = this.peek();
|
|
1120
|
+
if (t.type === 'keyword' && t.value === 'IN') {
|
|
1121
|
+
this.next();
|
|
1122
|
+
this.expect('op', '(');
|
|
1123
|
+
if (this.isKeyword('SELECT')) {
|
|
1124
|
+
const sub = this.parseSelect();
|
|
1125
|
+
this.expect('op', ')');
|
|
1126
|
+
node = { type: 'in', operand: node, subquery: sub };
|
|
1127
|
+
} else {
|
|
1128
|
+
const list = [];
|
|
1129
|
+
while (true) {
|
|
1130
|
+
list.push(this.parseValue());
|
|
1131
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
this.expect('op', ')');
|
|
1135
|
+
node = { type: 'in', operand: node, list };
|
|
1136
|
+
}
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (t.type === 'keyword' && t.value === 'IS') {
|
|
1140
|
+
this.next();
|
|
1141
|
+
const not = this.isKeyword('NOT');
|
|
1142
|
+
if (not) this.next();
|
|
1143
|
+
if (this.isKeyword('NULL')) { this.next(); node = { type: 'isNull', operand: node, not: !!not }; continue; }
|
|
1144
|
+
if (this.isKeyword('TRUE')) { this.next(); node = { type: 'isTruth', operand: node, not: !!not, truth: true }; continue; }
|
|
1145
|
+
if (this.isKeyword('FALSE')) { this.next(); node = { type: 'isTruth', operand: node, not: !!not, truth: false }; continue; }
|
|
1146
|
+
throw new Error(`Expected NULL, TRUE or FALSE after IS, got '${this.peek().value}'`);
|
|
1147
|
+
}
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1076
1150
|
return node;
|
|
1077
1151
|
}
|
|
1078
1152
|
|
|
@@ -1120,23 +1194,39 @@ class Parser {
|
|
|
1120
1194
|
parseComparison() {
|
|
1121
1195
|
const left = this.parseOperand();
|
|
1122
1196
|
const t = this.peek();
|
|
1197
|
+
let not = false;
|
|
1198
|
+
if (t.type === 'keyword' && t.value === 'NOT') {
|
|
1199
|
+
this.next();
|
|
1200
|
+
not = true;
|
|
1201
|
+
// NOT 后必须紧跟比较关键字
|
|
1202
|
+
const n = this.peek();
|
|
1203
|
+
if (!['IN', 'BETWEEN', 'LIKE', 'REGEXP'].includes(n.value)) {
|
|
1204
|
+
throw new Error(`Expected IN, BETWEEN, LIKE or REGEXP after NOT, got '${n.value}'`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const t2 = this.peek();
|
|
1123
1208
|
|
|
1124
|
-
if (
|
|
1209
|
+
if (t2.type === 'keyword' && t2.value === 'IS') {
|
|
1125
1210
|
this.next();
|
|
1126
|
-
const
|
|
1127
|
-
if (
|
|
1128
|
-
this.
|
|
1129
|
-
|
|
1211
|
+
const n = this.isKeyword('NOT');
|
|
1212
|
+
if (n) this.next();
|
|
1213
|
+
if (this.isKeyword('NULL')) {
|
|
1214
|
+
this.next();
|
|
1215
|
+
return { type: 'isNull', operand: left, not: not || !!n };
|
|
1216
|
+
}
|
|
1217
|
+
if (this.isKeyword('TRUE')) { this.next(); return { type: 'isTruth', operand: left, not: not || !!n, truth: true }; }
|
|
1218
|
+
if (this.isKeyword('FALSE')) { this.next(); return { type: 'isTruth', operand: left, not: not || !!n, truth: false }; }
|
|
1219
|
+
throw new Error(`Expected NULL, TRUE or FALSE after IS, got '${this.peek().value}'`);
|
|
1130
1220
|
}
|
|
1131
1221
|
|
|
1132
|
-
if (
|
|
1222
|
+
if (t2.type === 'keyword' && t2.value === 'IN') {
|
|
1133
1223
|
this.next();
|
|
1134
1224
|
this.expect('op', '(');
|
|
1135
1225
|
// IN (SELECT ...) 子查询
|
|
1136
1226
|
if (this.isKeyword('SELECT')) {
|
|
1137
1227
|
const sub = this.parseSelect();
|
|
1138
1228
|
this.expect('op', ')');
|
|
1139
|
-
return { type: 'in', operand: left, subquery: sub };
|
|
1229
|
+
return { type: 'in', operand: left, subquery: sub, not };
|
|
1140
1230
|
}
|
|
1141
1231
|
const list = [];
|
|
1142
1232
|
while (true) {
|
|
@@ -1145,13 +1235,12 @@ class Parser {
|
|
|
1145
1235
|
break;
|
|
1146
1236
|
}
|
|
1147
1237
|
this.expect('op', ')');
|
|
1148
|
-
return { type: 'in', operand: left, list };
|
|
1238
|
+
return { type: 'in', operand: left, list, not };
|
|
1149
1239
|
}
|
|
1150
1240
|
|
|
1151
|
-
if (
|
|
1241
|
+
if (t2.type === 'keyword' && t2.value === 'BETWEEN') {
|
|
1152
1242
|
this.next();
|
|
1153
1243
|
const low = this.parseOperand();
|
|
1154
|
-
const not = false;
|
|
1155
1244
|
let andTok = this.peek();
|
|
1156
1245
|
if (andTok.type === 'keyword' && andTok.value === 'AND') {
|
|
1157
1246
|
this.next();
|
|
@@ -1161,19 +1250,25 @@ class Parser {
|
|
|
1161
1250
|
throw new Error(`Expected AND in BETWEEN, got '${andTok.value}'`);
|
|
1162
1251
|
}
|
|
1163
1252
|
|
|
1164
|
-
if (
|
|
1253
|
+
if (t2.type === 'keyword' && t2.value === 'LIKE') {
|
|
1254
|
+
this.next();
|
|
1255
|
+
const pattern = this.parseValue();
|
|
1256
|
+
return { type: 'like', operand: left, pattern, not };
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (t2.type === 'keyword' && t2.value === 'REGEXP') {
|
|
1165
1260
|
this.next();
|
|
1166
1261
|
const pattern = this.parseValue();
|
|
1167
|
-
return { type: '
|
|
1262
|
+
return { type: 'regexp', operand: left, pattern, not };
|
|
1168
1263
|
}
|
|
1169
1264
|
|
|
1170
|
-
if (
|
|
1265
|
+
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
|
|
1171
1266
|
this.next();
|
|
1172
1267
|
const right = this.parseOperand();
|
|
1173
|
-
return { type: 'compare', op:
|
|
1268
|
+
return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
|
|
1174
1269
|
}
|
|
1175
1270
|
|
|
1176
|
-
throw new Error(`Expected comparison operator, got '${
|
|
1271
|
+
throw new Error(`Expected comparison operator, got '${t2.value}'`);
|
|
1177
1272
|
}
|
|
1178
1273
|
}
|
|
1179
1274
|
|
|
@@ -1244,6 +1339,13 @@ function resolveOperand(operand, row, ctx) {
|
|
|
1244
1339
|
return applyScalarFunction(operand, row, ctx);
|
|
1245
1340
|
case 'case':
|
|
1246
1341
|
return evaluateCaseVal(operand, row, ctx);
|
|
1342
|
+
case 'in':
|
|
1343
|
+
case 'isNull':
|
|
1344
|
+
case 'isTruth':
|
|
1345
|
+
case 'regexp':
|
|
1346
|
+
case 'like':
|
|
1347
|
+
case 'between':
|
|
1348
|
+
return evaluateExpr(operand, row, ctx);
|
|
1247
1349
|
case 'aggregate':
|
|
1248
1350
|
case 'subquery':
|
|
1249
1351
|
return undefined;
|
|
@@ -1256,9 +1358,13 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1256
1358
|
const name = (fnNode.name || '').toUpperCase();
|
|
1257
1359
|
const args = (fnNode.args || []).map(a => resolveOperand(a, row, ctx));
|
|
1258
1360
|
const session = ctx && ctx.session;
|
|
1361
|
+
if (ctx && ctx.functions && Object.prototype.hasOwnProperty.call(ctx.functions, name)) {
|
|
1362
|
+
return ctx.functions[name].apply(null, args);
|
|
1363
|
+
}
|
|
1259
1364
|
switch (name) {
|
|
1260
1365
|
case 'VERSION': return '8.0.0-jsql-neo';
|
|
1261
|
-
case 'LAST_INSERT_ID':
|
|
1366
|
+
case 'LAST_INSERT_ID':
|
|
1367
|
+
case 'LAST_INSERT_ROWID': {
|
|
1262
1368
|
if (args.length > 0) {
|
|
1263
1369
|
if (session) session.lastInsertId = args[0];
|
|
1264
1370
|
return args[0];
|
|
@@ -1274,7 +1380,10 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1274
1380
|
case 'CURTIME': return new Date().toISOString().slice(11, 19);
|
|
1275
1381
|
case 'UTC_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
|
|
1276
1382
|
case 'CONCAT': return args.map(a => a === null || a === undefined ? '' : String(a)).join('');
|
|
1277
|
-
case 'CONCAT_WS':
|
|
1383
|
+
case 'CONCAT_WS': {
|
|
1384
|
+
const sep = args[0] == null ? ',' : String(args[0]);
|
|
1385
|
+
return args.slice(1).filter(a => a !== null && a !== undefined).map(a => String(a)).join(sep);
|
|
1386
|
+
}
|
|
1278
1387
|
case 'UPPER': case 'UCASE': return args[0] == null ? null : String(args[0]).toUpperCase();
|
|
1279
1388
|
case 'LOWER': case 'LCASE': return args[0] == null ? null : String(args[0]).toLowerCase();
|
|
1280
1389
|
case 'LENGTH': case 'CHAR_LENGTH': case 'CHARACTER_LENGTH': return args[0] == null ? null : String(args[0]).length;
|
|
@@ -1296,8 +1405,16 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1296
1405
|
case 'SUBSTRING': case 'SUBSTR': {
|
|
1297
1406
|
if (args[0] == null) return null;
|
|
1298
1407
|
const s = String(args[0]);
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1408
|
+
const len = s.length;
|
|
1409
|
+
let start = Number(args[1]);
|
|
1410
|
+
// MySQL 语义:1-based;负数从末尾倒数;0 视为 1(MySQL 返回空串)
|
|
1411
|
+
if (start === 0) return '';
|
|
1412
|
+
if (start < 0) start = len + start + 1;
|
|
1413
|
+
if (args[2] !== undefined) {
|
|
1414
|
+
let n = Number(args[2]);
|
|
1415
|
+
if (n < 0) return '';
|
|
1416
|
+
return s.substr(start - 1, n);
|
|
1417
|
+
}
|
|
1301
1418
|
return s.substr(start - 1);
|
|
1302
1419
|
}
|
|
1303
1420
|
case 'LEFT': return args[0] == null ? null : String(args[0]).slice(0, Number(args[1]));
|
|
@@ -1307,6 +1424,33 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1307
1424
|
const idx = String(args[1]).indexOf(String(args[0]));
|
|
1308
1425
|
return idx + 1;
|
|
1309
1426
|
}
|
|
1427
|
+
case 'REVERSE': return args[0] == null ? null : String(args[0]).split('').reverse().join('');
|
|
1428
|
+
case 'LPAD': {
|
|
1429
|
+
if (args[0] == null) return null;
|
|
1430
|
+
let s = String(args[0]);
|
|
1431
|
+
const n = Number(args[1]);
|
|
1432
|
+
const pad = args[2] == null ? ' ' : String(args[2]);
|
|
1433
|
+
if (n <= s.length) return s.slice(0, n);
|
|
1434
|
+
while (s.length < n) s = pad + s;
|
|
1435
|
+
return s;
|
|
1436
|
+
}
|
|
1437
|
+
case 'RPAD': {
|
|
1438
|
+
if (args[0] == null) return null;
|
|
1439
|
+
let s = String(args[0]);
|
|
1440
|
+
const n = Number(args[1]);
|
|
1441
|
+
const pad = args[2] == null ? ' ' : String(args[2]);
|
|
1442
|
+
if (n <= s.length) return s.slice(0, n);
|
|
1443
|
+
while (s.length < n) s = s + pad;
|
|
1444
|
+
return s;
|
|
1445
|
+
}
|
|
1446
|
+
case 'RAND': return args.length > 0 && args[0] != null ? seedRand(Number(args[0]))() : Math.random();
|
|
1447
|
+
case 'UNIX_TIMESTAMP': {
|
|
1448
|
+
if (args.length > 0 && args[0] != null) {
|
|
1449
|
+
const d = new Date(String(args[0]).replace(' ', 'T'));
|
|
1450
|
+
return isNaN(d.getTime()) ? 0 : Math.floor(d.getTime() / 1000);
|
|
1451
|
+
}
|
|
1452
|
+
return Math.floor(Date.now() / 1000);
|
|
1453
|
+
}
|
|
1310
1454
|
case 'GREATEST': return args.reduce((m, a) => a > m ? a : m, args[0]);
|
|
1311
1455
|
case 'LEAST': return args.reduce((m, a) => a < m ? a : m, args[0]);
|
|
1312
1456
|
case 'UUID': return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
@@ -1319,6 +1463,15 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
1319
1463
|
}
|
|
1320
1464
|
}
|
|
1321
1465
|
|
|
1466
|
+
function seedRand(seed) {
|
|
1467
|
+
let s = Math.abs(seed) % 2147483647;
|
|
1468
|
+
if (s <= 0) s = 1;
|
|
1469
|
+
return () => {
|
|
1470
|
+
s = (s * 16807) % 2147483647;
|
|
1471
|
+
return (s - 1) / 2147483646;
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1322
1475
|
function likeMatch(value, pattern) {
|
|
1323
1476
|
if (typeof value !== 'string') return false;
|
|
1324
1477
|
const regex = pattern
|
|
@@ -1362,8 +1515,9 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
1362
1515
|
case 'compare': {
|
|
1363
1516
|
const l = resolveOperand(expr.left, row, ctx);
|
|
1364
1517
|
const r = resolveOperand(expr.right, row, ctx);
|
|
1365
|
-
|
|
1518
|
+
// SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
|
|
1366
1519
|
if (l === null || r === null) return false;
|
|
1520
|
+
if (expr.op === '=') return l === r || (l !== null && r !== null && String(l) === String(r));
|
|
1367
1521
|
const fn = OPERATORS[expr.op];
|
|
1368
1522
|
return typeof l === 'number' && typeof r === 'number' ? fn(l, r) : fn(String(l), String(r));
|
|
1369
1523
|
}
|
|
@@ -1378,7 +1532,24 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
1378
1532
|
if (!expr.list) return false;
|
|
1379
1533
|
return expr.list.some(x => x === v || String(x) === String(v));
|
|
1380
1534
|
}
|
|
1381
|
-
case 'like':
|
|
1535
|
+
case 'like': {
|
|
1536
|
+
const r = likeMatch(resolveOperand(expr.operand, row, ctx), expr.pattern);
|
|
1537
|
+
return expr.not ? !r : r;
|
|
1538
|
+
}
|
|
1539
|
+
case 'regexp': {
|
|
1540
|
+
const v = resolveOperand(expr.operand, row, ctx);
|
|
1541
|
+
if (v === null || v === undefined) return false;
|
|
1542
|
+
const re = new RegExp(String(expr.pattern), 'i');
|
|
1543
|
+
const r = re.test(String(v));
|
|
1544
|
+
return expr.not ? !r : r;
|
|
1545
|
+
}
|
|
1546
|
+
case 'isTruth': {
|
|
1547
|
+
const v = resolveOperand(expr.operand, row, ctx);
|
|
1548
|
+
const isTrue = v === true || v === 1 || v === '1' || v === 'true' || v === 'TRUE' || v === 't' || (typeof v === 'number' && v !== 0);
|
|
1549
|
+
const isFalse = !isTrue && v !== null && v !== undefined;
|
|
1550
|
+
const result = expr.truth ? isTrue : isFalse;
|
|
1551
|
+
return expr.not ? !result : result;
|
|
1552
|
+
}
|
|
1382
1553
|
case 'between': {
|
|
1383
1554
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
1384
1555
|
const lo = resolveOperand(expr.low, row, ctx);
|
|
@@ -1436,7 +1607,15 @@ function scalarColumnName(c) {
|
|
|
1436
1607
|
}
|
|
1437
1608
|
|
|
1438
1609
|
function scalarColumnValue(c, r, ctx) {
|
|
1439
|
-
if (c.scalar)
|
|
1610
|
+
if (c.scalar) {
|
|
1611
|
+
const s = c.scalar;
|
|
1612
|
+
if (s && s.type === 'func' && ctx && ctx.ctxAggregates && Object.prototype.hasOwnProperty.call(ctx.ctxAggregates, String(s.name).toUpperCase())) {
|
|
1613
|
+
const fn = String(s.name).toUpperCase();
|
|
1614
|
+
const col = s.args && s.args[0];
|
|
1615
|
+
return ctx._aggValue(ctx.group, fn, col);
|
|
1616
|
+
}
|
|
1617
|
+
return resolveOperand(c.scalar, r, ctx);
|
|
1618
|
+
}
|
|
1440
1619
|
if (c.aggregate) return ctx._aggValue(ctx.group, c.aggregate || 'COUNT', c.column);
|
|
1441
1620
|
if (c.literal !== undefined) return c.literal;
|
|
1442
1621
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r, ctx);
|
|
@@ -1698,7 +1877,9 @@ class SQLExecutor {
|
|
|
1698
1877
|
const id = row._rid !== undefined ? row._rid : row.id;
|
|
1699
1878
|
if (id !== undefined) {
|
|
1700
1879
|
const data = {};
|
|
1701
|
-
for (const [col, val] of statement.assignments)
|
|
1880
|
+
for (const [col, val] of statement.assignments) {
|
|
1881
|
+
data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
|
|
1882
|
+
}
|
|
1702
1883
|
this.engine.updateById(statement.table, id, data);
|
|
1703
1884
|
count++;
|
|
1704
1885
|
}
|
|
@@ -1849,6 +2030,16 @@ class SQLExecutor {
|
|
|
1849
2030
|
await this.engine.useDatabase(statement.database);
|
|
1850
2031
|
}
|
|
1851
2032
|
return { ok: true, type: 'use', database: statement.database };
|
|
2033
|
+
case 'pragma': {
|
|
2034
|
+
const rows = this._runPragma(statement.name, statement.arg);
|
|
2035
|
+
const isSelect = ['table_info', 'table_list', 'index_list', 'index_info', 'collation_list', 'database_list', 'module_list', 'function_list', 'pragma_list'].includes(statement.name.toLowerCase());
|
|
2036
|
+
if (isSelect) {
|
|
2037
|
+
const cols = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
2038
|
+
return { ok: true, type: 'select', columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
|
|
2039
|
+
}
|
|
2040
|
+
const simple = rows.length > 0 && Object.keys(rows[0]).length === 1 ? rows[0][Object.keys(rows[0])[0]] : rows;
|
|
2041
|
+
return { ok: true, type: 'pragma', name: statement.name, value: simple };
|
|
2042
|
+
}
|
|
1852
2043
|
case 'describe': {
|
|
1853
2044
|
const schema = this.engine.getTableSchema ? await this.engine.getTableSchema(statement.table) : null;
|
|
1854
2045
|
if (!schema) throw new Error(`Table '${statement.table}' does not exist`);
|
|
@@ -1860,6 +2051,110 @@ class SQLExecutor {
|
|
|
1860
2051
|
}
|
|
1861
2052
|
}
|
|
1862
2053
|
|
|
2054
|
+
_runPragma(name, arg) {
|
|
2055
|
+
const engine = this.engine;
|
|
2056
|
+
const lname = String(name).toLowerCase().replace(/^.*\./, '');
|
|
2057
|
+
const rows = [];
|
|
2058
|
+
switch (lname) {
|
|
2059
|
+
case 'table_info': {
|
|
2060
|
+
const schema = engine.getTableSchema ? engine.getTableSchema(arg) : (engine._schemas ? engine._schemas[arg] : null);
|
|
2061
|
+
if (!schema) throw new Error(`table ${arg} may not be queried: no such table`);
|
|
2062
|
+
let cid = 0;
|
|
2063
|
+
for (const [col, def] of Object.entries(schema)) {
|
|
2064
|
+
let typeName = String(def.type || 'text').toUpperCase();
|
|
2065
|
+
if (typeName === 'NUMBER') typeName = 'REAL';
|
|
2066
|
+
if (typeName === 'OBJECT' || typeName === 'ARRAY') typeName = 'TEXT';
|
|
2067
|
+
rows.push({
|
|
2068
|
+
cid,
|
|
2069
|
+
name: col,
|
|
2070
|
+
type: typeName,
|
|
2071
|
+
notnull: def.required ? 1 : 0,
|
|
2072
|
+
dflt_value: def.default !== undefined ? def.default : null,
|
|
2073
|
+
pk: def.primaryKey ? 1 : 0,
|
|
2074
|
+
});
|
|
2075
|
+
cid++;
|
|
2076
|
+
}
|
|
2077
|
+
break;
|
|
2078
|
+
}
|
|
2079
|
+
case 'table_list': {
|
|
2080
|
+
const tables = engine.listTables ? engine.listTables() : Array.from(engine._tableNames || []);
|
|
2081
|
+
for (const t of tables) {
|
|
2082
|
+
rows.push({ schema: 'main', name: t, type: 'table', ncol: 0, wr: 1, strict: 0 });
|
|
2083
|
+
}
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
case 'index_list': {
|
|
2087
|
+
const schema = engine.getTableSchema ? engine.getTableSchema(arg) : (engine._schemas ? engine._schemas[arg] : null);
|
|
2088
|
+
if (!schema) throw new Error(`table ${arg} may not be queried: no such table`);
|
|
2089
|
+
let seq = 0;
|
|
2090
|
+
for (const [col, def] of Object.entries(schema)) {
|
|
2091
|
+
if (def.primaryKey || def.unique) {
|
|
2092
|
+
rows.push({ seq, name: 'sqlite_autoindex_' + arg + '_' + (seq + 1), unique: def.unique ? 1 : 0, origin: def.primaryKey ? 'pk' : 'u', partial: 0 });
|
|
2093
|
+
seq++;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
break;
|
|
2097
|
+
}
|
|
2098
|
+
case 'index_info': {
|
|
2099
|
+
const schema = engine.getTableSchema ? engine.getTableSchema(arg) : (engine._schemas ? engine._schemas[arg] : null);
|
|
2100
|
+
if (!schema) throw new Error(`no such index: ${arg}`);
|
|
2101
|
+
let seqno = 0;
|
|
2102
|
+
for (const [col, def] of Object.entries(schema)) {
|
|
2103
|
+
if (def.primaryKey || def.unique) {
|
|
2104
|
+
rows.push({ seqno, cid: seqno, name: col });
|
|
2105
|
+
seqno++;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
case 'database_list': {
|
|
2111
|
+
rows.push({ seq: 0, name: 'main', file: '' });
|
|
2112
|
+
break;
|
|
2113
|
+
}
|
|
2114
|
+
case 'user_version': {
|
|
2115
|
+
const val = arg !== undefined && arg !== null ? arg : ((engine._pragmaValues && engine._pragmaValues.user_version) || 0);
|
|
2116
|
+
if (arg !== undefined && arg !== null) {
|
|
2117
|
+
engine._pragmaValues = engine._pragmaValues || {};
|
|
2118
|
+
engine._pragmaValues.user_version = Number(arg);
|
|
2119
|
+
}
|
|
2120
|
+
rows.push({ user_version: val });
|
|
2121
|
+
break;
|
|
2122
|
+
}
|
|
2123
|
+
case 'journal_mode': {
|
|
2124
|
+
rows.push({ journal_mode: arg !== undefined && arg !== null ? arg : 'memory' });
|
|
2125
|
+
break;
|
|
2126
|
+
}
|
|
2127
|
+
case 'foreign_keys': {
|
|
2128
|
+
rows.push({ foreign_keys: arg !== undefined && arg !== null ? arg : 0 });
|
|
2129
|
+
break;
|
|
2130
|
+
}
|
|
2131
|
+
case 'synchronous': {
|
|
2132
|
+
rows.push({ synchronous: arg !== undefined && arg !== null ? arg : 0 });
|
|
2133
|
+
break;
|
|
2134
|
+
}
|
|
2135
|
+
case 'cache_size': {
|
|
2136
|
+
rows.push({ cache_size: arg !== undefined && arg !== null ? arg : 0 });
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
case 'page_size':
|
|
2140
|
+
case 'encoding':
|
|
2141
|
+
case 'auto_vacuum':
|
|
2142
|
+
case 'temp_store':
|
|
2143
|
+
case 'locking_mode':
|
|
2144
|
+
case 'application_id':
|
|
2145
|
+
case 'integrity_check':
|
|
2146
|
+
case 'quick_check': {
|
|
2147
|
+
rows.push({ [lname]: lname === 'encoding' ? 'UTF-8' : (arg !== undefined && arg !== null ? arg : 0) });
|
|
2148
|
+
break;
|
|
2149
|
+
}
|
|
2150
|
+
default: {
|
|
2151
|
+
rows.push({ [lname]: arg !== undefined && arg !== null ? arg : 0 });
|
|
2152
|
+
break;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return rows;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1863
2158
|
async _getSchema(name) {
|
|
1864
2159
|
if (this.engine.hasTable && !this.engine.hasTable(name)) {
|
|
1865
2160
|
throw new Error(`Table '${name}' does not exist`);
|
|
@@ -2074,7 +2369,20 @@ class SQLExecutor {
|
|
|
2074
2369
|
_aggValue(rows, fn, column) {
|
|
2075
2370
|
if (fn === 'COUNT') return rows.length;
|
|
2076
2371
|
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
2077
|
-
const values =
|
|
2372
|
+
const values = op && op.type === 'star'
|
|
2373
|
+
? rows.map(r => 1)
|
|
2374
|
+
: rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
|
|
2375
|
+
if (this.ctx && this.ctx.aggregates && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
|
|
2376
|
+
const agg = this.ctx.aggregates[fn];
|
|
2377
|
+
if (typeof agg === 'function') {
|
|
2378
|
+
return agg(values);
|
|
2379
|
+
}
|
|
2380
|
+
if (agg && typeof agg.step === 'function') {
|
|
2381
|
+
let state = typeof agg.start === 'function' ? agg.start() : undefined;
|
|
2382
|
+
for (const v of values) state = agg.step(state, v);
|
|
2383
|
+
return typeof agg.result === 'function' ? agg.result(state) : state;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2078
2386
|
if (fn === 'SUM') return values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0);
|
|
2079
2387
|
if (fn === 'AVG') return values.length ? values.reduce((s, v) => s + (typeof v === 'number' ? v : Number(v) || 0), 0) / values.length : null;
|
|
2080
2388
|
if (fn === 'MIN') return values.length ? Math.min(...values.map(v => Number(v))) : null;
|
|
@@ -2266,10 +2574,17 @@ class SQLExecutor {
|
|
|
2266
2574
|
}
|
|
2267
2575
|
|
|
2268
2576
|
// 聚合输出(无 GROUP BY 时)
|
|
2269
|
-
|
|
2577
|
+
const hasCustomAgg = this.ctx && this.ctx.aggregates && statement.columns.some(c => c.scalar && c.scalar.type === 'func' && Object.prototype.hasOwnProperty.call(this.ctx.aggregates, String(c.scalar.name).toUpperCase()));
|
|
2578
|
+
if (!statement.groupBy && (statement.columns.some(c => c.aggregate) || hasCustomAgg)) {
|
|
2270
2579
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
2271
2580
|
const valueOf = (c) => {
|
|
2272
2581
|
if (c.aggregate) return this._aggValue(rows, c.aggregate || 'COUNT', c.column);
|
|
2582
|
+
if (c.scalar && c.scalar.type === 'func' && hasCustomAgg) {
|
|
2583
|
+
const fn = String(c.scalar.name).toUpperCase();
|
|
2584
|
+
if (Object.prototype.hasOwnProperty.call(this.ctx.aggregates, fn)) {
|
|
2585
|
+
return this._aggValue(rows, fn, c.scalar.args && c.scalar.args[0]);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2273
2588
|
if (c.scalar) return resolveOperand(c.scalar, rows[0] || {}, this.ctx);
|
|
2274
2589
|
if (c.caseExpr) return resolveOperand(c.caseExpr, rows[0] || {}, this.ctx);
|
|
2275
2590
|
if (c.expr !== null && c.expr !== '*') return rows[0] ? resolveOperand({ type: 'column', name: c.expr }, rows[0]) : null;
|
|
@@ -2283,7 +2598,7 @@ class SQLExecutor {
|
|
|
2283
2598
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
2284
2599
|
const mapped = rows.map(r => {
|
|
2285
2600
|
const group = r._group || [r];
|
|
2286
|
-
return statement.columns.map(c => scalarColumnValue(c, r, { _aggValue: this._aggValue.bind(this), group }));
|
|
2601
|
+
return statement.columns.map(c => scalarColumnValue(c, r, { _aggValue: this._aggValue.bind(this), group, ctxAggregates: this.ctx ? this.ctx.aggregates : null }));
|
|
2287
2602
|
});
|
|
2288
2603
|
return { ok: true, type: 'select', table: statement.from ? (statement.from.tables[0].table || null) : null, columns: cols, rows: mapped, raw: rows };
|
|
2289
2604
|
}
|
|
@@ -2448,6 +2763,11 @@ function applyParams(sql, values) {
|
|
|
2448
2763
|
let idx = 0;
|
|
2449
2764
|
let inStr = null;
|
|
2450
2765
|
let i = 0;
|
|
2766
|
+
const named = {};
|
|
2767
|
+
let hasNamed = false;
|
|
2768
|
+
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
2769
|
+
hasNamed = true;
|
|
2770
|
+
}
|
|
2451
2771
|
while (i < sql.length) {
|
|
2452
2772
|
const c = sql[i];
|
|
2453
2773
|
if (inStr) {
|
|
@@ -2459,23 +2779,53 @@ function applyParams(sql, values) {
|
|
|
2459
2779
|
}
|
|
2460
2780
|
if (c === "'" || c === '"' || c === '`') { inStr = c; out += c; i++; continue; }
|
|
2461
2781
|
if (c === '?' && sql[i + 1] === '?') {
|
|
2462
|
-
if (idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
2463
|
-
out += escapeId(args[idx++]);
|
|
2782
|
+
if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
2783
|
+
out += escapeId(hasNamed ? args['@@'] : args[idx++]);
|
|
2464
2784
|
count++;
|
|
2465
2785
|
i += 2;
|
|
2466
2786
|
continue;
|
|
2467
2787
|
}
|
|
2468
2788
|
if (c === '?') {
|
|
2469
|
-
if (
|
|
2470
|
-
|
|
2789
|
+
if (sql[i + 1] >= '0' && sql[i + 1] <= '9') {
|
|
2790
|
+
// ?N 编号占位符
|
|
2791
|
+
let num = '';
|
|
2792
|
+
let j = i + 1;
|
|
2793
|
+
while (j < sql.length && sql[j] >= '0' && sql[j] <= '9') { num += sql[j]; j++; }
|
|
2794
|
+
const n = parseInt(num, 10);
|
|
2795
|
+
if (hasNamed) {
|
|
2796
|
+
if (!(n in args)) throw new Error(`No value for parameter ?${n}`);
|
|
2797
|
+
out += escapeValue(args[n]);
|
|
2798
|
+
} else {
|
|
2799
|
+
if (n - 1 >= args.length) throw new Error(`Not enough parameters for SQL: expected ?${n}`);
|
|
2800
|
+
out += escapeValue(args[n - 1]);
|
|
2801
|
+
if (n > idx) idx = n;
|
|
2802
|
+
}
|
|
2803
|
+
count++;
|
|
2804
|
+
i = j;
|
|
2805
|
+
continue;
|
|
2806
|
+
}
|
|
2807
|
+
if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
2808
|
+
out += escapeValue(hasNamed ? args['?'] : args[idx++]);
|
|
2471
2809
|
count++;
|
|
2472
2810
|
i++;
|
|
2473
2811
|
continue;
|
|
2474
2812
|
}
|
|
2813
|
+
if ((c === ':' || c === '@' || c === '$') && i + 1 < sql.length && /[A-Za-z_]/.test(sql[i + 1])) {
|
|
2814
|
+
// 命名占位符 :name @name $name
|
|
2815
|
+
let name = '';
|
|
2816
|
+
let j = i + 1;
|
|
2817
|
+
while (j < sql.length && /[A-Za-z0-9_]/.test(sql[j])) { name += sql[j]; j++; }
|
|
2818
|
+
if (!hasNamed) throw new Error(`Named parameter ${c}${name} requires an object of parameters`);
|
|
2819
|
+
if (!(name in args)) throw new Error(`No value for parameter ${c}${name}`);
|
|
2820
|
+
out += escapeValue(args[name]);
|
|
2821
|
+
count++;
|
|
2822
|
+
i = j;
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2475
2825
|
out += c;
|
|
2476
2826
|
i++;
|
|
2477
2827
|
}
|
|
2478
|
-
if (idx !== args.length) {
|
|
2828
|
+
if (!hasNamed && idx !== args.length) {
|
|
2479
2829
|
throw new Error(`Too many parameters for SQL: got ${args.length}, expected ${count}`);
|
|
2480
2830
|
}
|
|
2481
2831
|
return out;
|
|
@@ -2502,7 +2852,11 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
2502
2852
|
if (dangerous) throw new Error(`SQL statement blocked by security policy: ${dangerous}`);
|
|
2503
2853
|
}
|
|
2504
2854
|
}
|
|
2505
|
-
const
|
|
2855
|
+
const ctx = {};
|
|
2856
|
+
if (opts.session) ctx.session = opts.session;
|
|
2857
|
+
if (opts.functions && typeof opts.functions === 'object') ctx.functions = opts.functions;
|
|
2858
|
+
if (opts.aggregates && typeof opts.aggregates === 'object') ctx.aggregates = opts.aggregates;
|
|
2859
|
+
const executor = new SQLExecutor(engine, Object.keys(ctx).length > 0 ? ctx : null);
|
|
2506
2860
|
const results = [];
|
|
2507
2861
|
for (const stmtSql of statements) {
|
|
2508
2862
|
const stmt = parseSQL(stmtSql);
|