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/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; break;
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; break;
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 (this.isKeyword('OFFSET')) { this.next(); offset = this.parseValue(); }
589
- else if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); offset = limit; limit = this.parseValue(); }
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.parseValue()]);
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
- args.push(this.parseOperand());
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
- args.push(this.parseOperand());
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 (t.type === 'keyword' && t.value === 'IS') {
1209
+ if (t2.type === 'keyword' && t2.value === 'IS') {
1125
1210
  this.next();
1126
- const not = this.isKeyword('NOT');
1127
- if (not) this.next();
1128
- this.expectKeyword('NULL');
1129
- return { type: 'isNull', operand: left, not: !!not };
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 (t.type === 'keyword' && t.value === 'IN') {
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 (t.type === 'keyword' && t.value === 'BETWEEN') {
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 (t.type === 'keyword' && t.value === 'LIKE') {
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: 'like', operand: left, pattern };
1262
+ return { type: 'regexp', operand: left, pattern, not };
1168
1263
  }
1169
1264
 
1170
- if (t.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t.value)) {
1265
+ if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
1171
1266
  this.next();
1172
1267
  const right = this.parseOperand();
1173
- return { type: 'compare', op: t.value === '<>' ? '!=' : t.value, left, right };
1268
+ return { type: 'compare', op: t2.value === '<>' ? '!=' : t2.value, left, right };
1174
1269
  }
1175
1270
 
1176
- throw new Error(`Expected comparison operator, got '${t.value}'`);
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': return (args.slice(1).map(a => a === null || a === undefined ? '' : String(a))).join(args[0] == null ? ',' : String(args[0]));
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 start = Number(args[1]);
1300
- if (args[2] !== undefined) return s.substr(start - 1, Number(args[2]));
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
- if (expr.op === '=') return l === r || (l === null && r === null) || (l !== null && r !== null && String(l) === String(r));
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': return likeMatch(resolveOperand(expr.operand, row, ctx), expr.pattern);
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) return resolveOperand(c.scalar, r, ctx);
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) data[col] = val;
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 = rows.map(r => resolveOperand(op, r, this.ctx)).filter(v => v !== null && v !== undefined);
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
- if (!statement.groupBy && statement.columns.some(c => c.aggregate)) {
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 (idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
2470
- out += escapeValue(args[idx++]);
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 executor = new SQLExecutor(engine, opts.session ? { session: opts.session } : null);
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);