jsql-neo 6.5.9 → 6.5.11

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  > **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
4
4
  > MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
5
5
 
6
- > **v6.5.9** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.5.11** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
7
7
 
8
8
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.9 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.11 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/sql.js CHANGED
@@ -708,6 +708,11 @@ class Parser {
708
708
  break;
709
709
  } else {
710
710
  const col = this.parseTableName();
711
+ // 重复列名必须报错:对象键覆盖会静默丢列(schema[col] = def 直接覆盖),
712
+ // 用户 CREATE TABLE d(a INT, a INT) 会悄悄变成单列 a。
713
+ if (schema[col] !== undefined) {
714
+ throw new Error(`Duplicate column name '${col}' in table '${name}'`);
715
+ }
711
716
  const def = this.parseColumnDef();
712
717
  schema[col] = def;
713
718
  if (def.primaryKey) hasPk = true;
@@ -755,6 +760,8 @@ class Parser {
755
760
  const mapped = typeMap[type];
756
761
  if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
757
762
  def.type = mapped;
763
+ // JSON / JSONB 列保留原始标记,供数据层校验非法 JSON({bad json 这类字符串会被 JSON.parse 拒绝)
764
+ if (type === 'json' || type === 'jsonb') def.jsonType = true;
758
765
  if (/^(BIG)?SERIAL$/i.test(typeRaw)) { def.autoIncrement = true; def.required = true; }
759
766
 
760
767
  // 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
@@ -1079,6 +1086,22 @@ class Parser {
1079
1086
  }
1080
1087
 
1081
1088
  parseValueAtom() {
1089
+ // MySQL 前缀运算符:BINARY expr —— 强制大小写敏感比较。
1090
+ // 仅当后面确实是值起始 token 才当作运算符,避免误吞裸列名 `binary`
1091
+ // (`SELECT binary FROM t` / `WHERE binary = 1` 仍按列名解析)。
1092
+ if (this.isWord('BINARY')) {
1093
+ const nt = this.peek(1);
1094
+ const startVal = nt && (nt.type === 'number' || nt.type === 'string' || nt.type === 'ident' || nt.type === 'param' ||
1095
+ (nt.type === 'op' && (nt.value === '(' || nt.value === '-' || nt.value === '+')) ||
1096
+ (nt.type === 'keyword' && (nt.value === 'NULL' || nt.value === 'TRUE' || nt.value === 'FALSE' || nt.value === 'DEFAULT')));
1097
+ if (startVal) {
1098
+ this.next(); // 消费 BINARY
1099
+ const lt = this.peek();
1100
+ if (lt.type === 'string' || lt.type === 'number') { const lit = this.next(); return { type: 'binary', operand: { type: 'literal', value: lit.value } }; }
1101
+ const operand = this.parseValue();
1102
+ return { type: 'binary', operand };
1103
+ }
1104
+ }
1082
1105
  const t = this.next();
1083
1106
  if (t.type === 'number' || t.type === 'string') return t.value;
1084
1107
  // MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
@@ -1583,9 +1606,30 @@ class Parser {
1583
1606
  let where = null;
1584
1607
  if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
1585
1608
  else console.warn(`⚠️ UPDATE without WHERE clause — this will modify ALL rows in '${table}'`);
1609
+ // UPDATE ... ORDER BY ... LIMIT n(与 DELETE 对称;按排序后的前 n 行更新)
1610
+ let orderBy = null, limit = null;
1611
+ if (this.isKeyword('ORDER')) {
1612
+ this.next(); this.expectKeyword('BY');
1613
+ orderBy = [];
1614
+ for (;;) {
1615
+ const col = this.parseColumnRef();
1616
+ let dir = 'asc';
1617
+ if (this.isKeyword('ASC')) this.next();
1618
+ else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
1619
+ orderBy.push({ column: col, dir });
1620
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1621
+ break;
1622
+ }
1623
+ }
1624
+ if (this.isKeyword('LIMIT')) {
1625
+ this.next();
1626
+ limit = this.parseValue();
1627
+ if (typeof limit === 'bigint') limit = Number(limit <= BigInt(Number.MAX_SAFE_INTEGER) ? limit : BigInt(Number.MAX_SAFE_INTEGER));
1628
+ if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
1629
+ }
1586
1630
  const returning = this.parseOptionalReturning();
1587
1631
  this.optionalTailSemicolon();
1588
- return { type: 'update', table, assignments, where, returning };
1632
+ return { type: 'update', table, assignments, where, orderBy, limit, returning };
1589
1633
  }
1590
1634
 
1591
1635
  parseDelete() {
@@ -2191,6 +2235,37 @@ class Parser {
2191
2235
  }
2192
2236
 
2193
2237
  parseOperand() {
2238
+ // MySQL 前缀运算符:BINARY expr —— 强制大小写敏感比较。
2239
+ if (this.isWord('BINARY')) {
2240
+ const nt = this.peek(1);
2241
+ const startVal = nt && (nt.type === 'number' || nt.type === 'string' || nt.type === 'ident' || nt.type === 'param' ||
2242
+ (nt.type === 'op' && (nt.value === '(' || nt.value === '-' || nt.value === '+')) ||
2243
+ (nt.type === 'keyword' && (nt.value === 'NULL' || nt.value === 'TRUE' || nt.value === 'FALSE' || nt.value === 'DEFAULT')));
2244
+ if (startVal) {
2245
+ this.next(); // 消费 BINARY
2246
+ // 字面量要包成 literal,否则 resolveOperand 把 'ABC' 当列名
2247
+ const lt = this.peek();
2248
+ if (lt.type === 'string' || lt.type === 'number') { const lit = this.next(); return { type: 'binary', operand: { type: 'literal', value: lit.value } }; }
2249
+ const operand = this.parseValue();
2250
+ return { type: 'binary', operand };
2251
+ }
2252
+ }
2253
+ // MySQL 区间字面量:INTERVAL n UNIT(date + INTERVAL 3 DAY / date - INTERVAL 1 MONTH)
2254
+ if (this.isWord('INTERVAL')) {
2255
+ this.next();
2256
+ const nTok = this.peek();
2257
+ let nVal;
2258
+ if (nTok.type === 'op' && (nTok.value === '-' || nTok.value === '+')) {
2259
+ this.next();
2260
+ const numTok = this.next();
2261
+ nVal = (nTok.value === '-' ? -1 : 1) * Number(numTok.value);
2262
+ } else {
2263
+ this.next();
2264
+ nVal = Number(nTok.value);
2265
+ }
2266
+ const unitTok = this.next();
2267
+ return { type: 'interval', value: nVal, unit: String(unitTok.value).toUpperCase() };
2268
+ }
2194
2269
  const t = this.next();
2195
2270
  // P1-20: SELECT -col 报错——parseOperand 不处理负号
2196
2271
  if (t.type === 'op' && t.value === '-') {
@@ -2871,6 +2946,8 @@ function resolveOperand(operand, row, ctx) {
2871
2946
  case 'value':
2872
2947
  case 'literal':
2873
2948
  return operand.value;
2949
+ case 'binary':
2950
+ return resolveOperand(operand.operand, row, ctx);
2874
2951
  case 'sysvar': {
2875
2952
  const s = (ctx && ctx.session && ctx.session.sysvars) || {};
2876
2953
  const name = String(operand.name).toLowerCase();
@@ -2909,6 +2986,32 @@ function resolveOperand(operand, row, ctx) {
2909
2986
  return undefined;
2910
2987
  }
2911
2988
  case 'arith': {
2989
+ // date + INTERVAL n UNIT / date - INTERVAL n UNIT(MySQL 区间算术)
2990
+ const lIv = operand.left && operand.left.type === 'interval';
2991
+ const rIv = operand.right && operand.right.type === 'interval';
2992
+ if ((operand.op === '+' || operand.op === '-') && (lIv || rIv)) {
2993
+ if (lIv && operand.op === '-') return null; // INTERVAL - date 无意义
2994
+ const ivNode = lIv ? operand.left : operand.right;
2995
+ const dateSide = lIv ? resolveOperand(operand.right, row, ctx) : resolveOperand(operand.left, row, ctx);
2996
+ const base = parseSqlDate(dateSide);
2997
+ if (!base) return null;
2998
+ const sign = operand.op === '-' ? -1 : 1;
2999
+ const isDateOnly = String(dateSide).trim().length <= 10;
3000
+ const d = new Date(base.getTime());
3001
+ const n = sign * Number(ivNode.value || 0);
3002
+ switch (String(ivNode.unit || '').toUpperCase()) {
3003
+ case 'DAY': case 'DAYS': d.setDate(d.getDate() + n); break;
3004
+ case 'WEEK': case 'WEEKS': d.setDate(d.getDate() + n * 7); break;
3005
+ case 'MONTH': case 'MONTHS': addMonthsClamped(d, n); break;
3006
+ case 'QUARTER': addMonthsClamped(d, n * 3); break;
3007
+ case 'YEAR': case 'YEARS': addMonthsClamped(d, n * 12); break;
3008
+ case 'HOUR': case 'HOURS': d.setHours(d.getHours() + n); break;
3009
+ case 'MINUTE': case 'MINUTES': d.setMinutes(d.getMinutes() + n); break;
3010
+ case 'SECOND': case 'SECONDS': d.setSeconds(d.getSeconds() + n); break;
3011
+ default: return null;
3012
+ }
3013
+ return fmtSqlDate(isDateOnly, d);
3014
+ }
2912
3015
  const l = resolveOperand(operand.left, row, ctx);
2913
3016
  const r = resolveOperand(operand.right, row, ctx);
2914
3017
  if (l === null || r === null || l === undefined || r === undefined) return null;
@@ -3014,10 +3117,6 @@ function resolveOperand(operand, row, ctx) {
3014
3117
  // 布尔类节点出现在"取值"位置时(如 IF(status = 'paid', ...)、SUM(IF(a > b,1,0))),
3015
3118
  // 求值成 true/false 供上层按真值判断
3016
3119
  case 'compare':
3017
- case 'and':
3018
- case 'or':
3019
- case 'xor':
3020
- case 'not':
3021
3120
  case 'in':
3022
3121
  case 'isNull':
3023
3122
  case 'isTruth':
@@ -3026,6 +3125,25 @@ function resolveOperand(operand, row, ctx) {
3026
3125
  case 'like':
3027
3126
  case 'between':
3028
3127
  return evaluateExpr(operand, row, ctx);
3128
+ case 'and': {
3129
+ const a = sqlTruthy(resolveOperand(operand.left, row, ctx));
3130
+ const b = sqlTruthy(resolveOperand(operand.right, row, ctx));
3131
+ return sqlAnd(a, b);
3132
+ }
3133
+ case 'or': {
3134
+ const a = sqlTruthy(resolveOperand(operand.left, row, ctx));
3135
+ const b = sqlTruthy(resolveOperand(operand.right, row, ctx));
3136
+ return sqlOr(a, b);
3137
+ }
3138
+ case 'xor': {
3139
+ const a = sqlTruthy(resolveOperand(operand.left, row, ctx));
3140
+ const b = sqlTruthy(resolveOperand(operand.right, row, ctx));
3141
+ return sqlXor(a, b);
3142
+ }
3143
+ case 'not': {
3144
+ const v = sqlTruthy(resolveOperand(operand.expr, row, ctx));
3145
+ return v === null ? null : (v ? 0 : 1);
3146
+ }
3029
3147
  case 'unit': {
3030
3148
  // 裸时间单位(MONTH / DAY …):优先当列取,取不到就返回单位名本身
3031
3149
  const lower = String(operand.name).toLowerCase();
@@ -4313,6 +4431,46 @@ function normalizeCmpValue(a, other) {
4313
4431
  return otherIsNumeric ? (a ? 1 : 0) : a;
4314
4432
  }
4315
4433
 
4434
+ // MySQL 默认 collation(utf8mb4_general_ci)下字符串比较不区分大小写,
4435
+ // 与 LIKE 保持一致。cs=true 表示显式 BINARY/COLLATE 强制大小写敏感。
4436
+ function ciEq(a, b, cs) {
4437
+ if (cs) return a === b;
4438
+ if (typeof a === 'string' && typeof b === 'string') return a.toLowerCase() === b.toLowerCase();
4439
+ return String(a) === String(b);
4440
+ }
4441
+ function ciCompare(a, b, cs) {
4442
+ if (cs) return a === b ? 0 : (a < b ? -1 : 1);
4443
+ const as = String(a).toLowerCase(), bs = String(b).toLowerCase();
4444
+ return as === bs ? 0 : (as < bs ? -1 : 1);
4445
+ }
4446
+
4447
+ // ── 三值逻辑(SQL 标准:TRUE / FALSE / UNKNOWN)──────────────────────
4448
+ // NULL 参与布尔运算时结果不是 0/1,而是 UNKNOWN(用 null 表示)。
4449
+ // 此前 resolveOperand 没有 and/or/xor/not 分支,SELECT TRUE AND TRUE 落到
4450
+ // default 返回 undefined → 数值化成 0,所有布尔表达式全部失效。
4451
+ function sqlTruthy(v) {
4452
+ if (v === null || v === undefined) return null;
4453
+ if (typeof v === 'boolean') return v;
4454
+ if (typeof v === 'number') return v !== 0;
4455
+ if (typeof v === 'string') return v !== '' && !(Number(v) === 0 && String(v).trim() !== '');
4456
+ return Boolean(v);
4457
+ }
4458
+ // 三值真值表,入参已归一化为 true / false / null,返回 1 / 0 / null
4459
+ function sqlAnd(a, b) {
4460
+ if (a === false || b === false) return 0;
4461
+ if (a === null || b === null) return null;
4462
+ return 1;
4463
+ }
4464
+ function sqlOr(a, b) {
4465
+ if (a === true || b === true) return 1;
4466
+ if (a === null || b === null) return null;
4467
+ return 0;
4468
+ }
4469
+ function sqlXor(a, b) {
4470
+ if (a === null || b === null) return null;
4471
+ return a !== b ? 1 : 0;
4472
+ }
4473
+
4316
4474
  function evaluateExpr(expr, row, ctx) {
4317
4475
  if (expr === null || expr === undefined) return false;
4318
4476
  switch (expr.type) {
@@ -4329,12 +4487,14 @@ function evaluateExpr(expr, row, ctx) {
4329
4487
  case 'compare': {
4330
4488
  const l = resolveOperand(expr.left, row, ctx);
4331
4489
  const r = resolveOperand(expr.right, row, ctx);
4490
+ // BINARY 限定符:任一侧是 BINARY 操作数即强制大小写敏感
4491
+ const cs = !!(expr.cs || (expr.left && expr.left.type === 'binary') || (expr.right && expr.right.type === 'binary'));
4332
4492
  // MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
4333
4493
  if (expr.op === '<=>') {
4334
4494
  const ln = l === null || l === undefined;
4335
4495
  const rn = r === null || r === undefined;
4336
4496
  if (ln || rn) return ln && rn;
4337
- return l === r || String(l) === String(r);
4497
+ return ciEq(l, r, cs);
4338
4498
  }
4339
4499
  // SQL 标准:与 NULL 的比较结果是 UNKNOWN,这里用 null 表示。
4340
4500
  // 不能返回 false —— 否则 NOT (a > 1) 会把 a 为 NULL 的行错误纳入结果。
@@ -4349,11 +4509,19 @@ function evaluateExpr(expr, row, ctx) {
4349
4509
  const mixedNum = (isNum(nl) && isNumStr(nr)) || (isNumStr(nl) && isNum(nr));
4350
4510
  if (expr.op === '=') {
4351
4511
  if (mixedNum) return Number(nl) === Number(nr);
4352
- return nl === nr || String(nl) === String(nr);
4512
+ return ciEq(nl, nr, cs);
4513
+ }
4514
+ if (mixedNum) { const fn = OPERATORS[expr.op]; return fn(Number(nl), Number(nr)); }
4515
+ if (typeof nl === 'number' && typeof nr === 'number') { const fn = OPERATORS[expr.op]; return fn(nl, nr); }
4516
+ const c = ciCompare(nl, nr, cs);
4517
+ switch (expr.op) {
4518
+ case '!=': case '<>': return c !== 0;
4519
+ case '<': return c < 0;
4520
+ case '<=': return c <= 0;
4521
+ case '>': return c > 0;
4522
+ case '>=': return c >= 0;
4523
+ default: return c === 0;
4353
4524
  }
4354
- const fn = OPERATORS[expr.op];
4355
- if (mixedNum) return fn(Number(nl), Number(nr));
4356
- return typeof nl === 'number' && typeof nr === 'number' ? fn(nl, nr) : fn(String(nl), String(nr));
4357
4525
  }
4358
4526
  case 'isNull': {
4359
4527
  const v = resolveOperand(expr.operand, row, ctx);
@@ -4365,6 +4533,8 @@ function evaluateExpr(expr, row, ctx) {
4365
4533
  if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
4366
4534
  if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
4367
4535
  if (!expr.list) return expr.not ? true : false;
4536
+ const cs = !!(expr.cs || (expr.operand && expr.operand.type === 'binary') ||
4537
+ (expr.list && expr.list.some(x => x && x.type === 'binary')));
4368
4538
 
4369
4539
  // 先找匹配:只要有一个非 NULL 元素等于 v,整个 IN 就是 TRUE ——
4370
4540
  // 列表里别的位置有没有 NULL 都不影响。
@@ -4372,9 +4542,12 @@ function evaluateExpr(expr, row, ctx) {
4372
4542
  // 此前是先判「列表里有 NULL 就返回 NULL」,于是 `v IN (10, NULL)`
4373
4543
  // 连 v=10 的行都匹配不上(实测返回 0 行,应为 2 行)。
4374
4544
  // 三值逻辑里 NULL 只在**没匹配上**的时候才让结果变 UNKNOWN。
4375
- const matched = expr.list.some(x =>
4376
- x !== null && x !== undefined && (x === v || String(x) === String(v))
4377
- );
4545
+ const matched = expr.list.some(x => {
4546
+ // list 元素可能是表达式节点(如 IN (BINARY 'ABC')),需先求值;
4547
+ // 原始字面量('ABC' / 10)直接比较,不能走 resolveOperand(会把字符串当列名)。
4548
+ const xv = (x && typeof x === 'object') ? resolveOperand(x, row, ctx) : x;
4549
+ return xv !== null && xv !== undefined && (xv === v || ciEq(xv, v, cs));
4550
+ });
4378
4551
  if (matched) return expr.not ? false : true;
4379
4552
 
4380
4553
  // 没匹配上:列表里有 NULL → UNKNOWN,否则是确定的 FALSE
@@ -5255,22 +5428,38 @@ class SQLExecutor {
5255
5428
  throw createError('ER_PARSE_ERROR', `Table '${statement.table}' has no primary key, UPDATE requires a primary key`);
5256
5429
  }
5257
5430
  const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
5431
+ // UPDATE ... ORDER BY ... LIMIT n:先过滤、排序、截取,再逐行更新
5432
+ let candidates = all.filter(row => !statement.where || evaluateExpr(statement.where, row, this.ctx));
5433
+ if (statement.orderBy) {
5434
+ const cmp = (a, b) => {
5435
+ for (const o of statement.orderBy) {
5436
+ const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
5437
+ const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
5438
+ if (av === bv) continue;
5439
+ if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
5440
+ if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
5441
+ const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
5442
+ if (d !== 0) return o.dir === 'asc' ? d : -d;
5443
+ }
5444
+ return 0;
5445
+ };
5446
+ candidates = candidates.slice().sort(cmp);
5447
+ }
5448
+ if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
5258
5449
  let count = 0;
5259
- for (const row of all) {
5260
- if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
5261
- const id = this._rowPkId(row, pkCols);
5262
- if (id !== undefined) {
5263
- const data = {};
5264
- for (const [col, val] of statement.assignments) {
5265
- data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
5266
- }
5267
- // 必须 await:wasm / native client 的 updateById 是异步的,
5268
- // 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
5269
- // 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
5270
- // lib/database.js 的实现是同步的,await 同步值同样安全。
5271
- await this.engine.updateById(statement.table, id, data);
5272
- count++;
5450
+ for (const row of candidates) {
5451
+ const id = this._rowPkId(row, pkCols);
5452
+ if (id !== undefined) {
5453
+ const data = {};
5454
+ for (const [col, val] of statement.assignments) {
5455
+ data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
5273
5456
  }
5457
+ // 必须 await:wasm / native client 的 updateById 是异步的,
5458
+ // 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
5459
+ // 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
5460
+ // lib/database.js 的实现是同步的,await 同步值同样安全。
5461
+ await this.engine.updateById(statement.table, id, data);
5462
+ count++;
5274
5463
  }
5275
5464
  }
5276
5465
  await this.engine.flush();
@@ -5670,17 +5859,26 @@ class SQLExecutor {
5670
5859
  // - 带表限定的条件(如 "o.amount"):检查表名/别名是否匹配
5671
5860
  // - 不带表限定的条件(如 "amount"):检查当前表的 schema 是否包含该列
5672
5861
  const filter = {};
5862
+ // 字符串列默认 collation 为 CI(大小写不敏感),而存储层 find 的等值
5863
+ // 过滤是严格比较(CS)。字符串等值若下推会被 CS 预筛,绕过 evaluateExpr 的
5864
+ // ciEq,导致 `code = 'abc'` 匹配不到 'ABC'。所以字符串/JSON 列不参与下推,
5865
+ // 交给 evaluateExpr 按 CI 求值;数值/布尔仍下推走索引。
5866
+ const isNonPushdownCol = (d) => {
5867
+ const t = String((d && d.type) || '').toLowerCase();
5868
+ return t === 'text' || t === 'varchar' || t === 'char' || t === 'string' ||
5869
+ t === 'blob' || t === 'clob' || t === 'json' || t === 'object' || t === 'array';
5870
+ };
5673
5871
  for (const [key, val] of Object.entries(allFilters)) {
5674
5872
  if (key.includes('.')) {
5675
5873
  // 带表限定:检查是否匹配当前表的别名或表名
5676
5874
  const tablePart = key.slice(0, key.lastIndexOf('.'));
5677
5875
  if (tablePart === alias || tablePart === table) {
5678
5876
  const colPart = key.slice(key.lastIndexOf('.') + 1);
5679
- if (schema[colPart]) filter[colPart] = this._coerceValue(val, schema[colPart]);
5877
+ if (schema[colPart] && !isNonPushdownCol(schema[colPart])) filter[colPart] = this._coerceValue(val, schema[colPart]);
5680
5878
  }
5681
5879
  } else {
5682
5880
  // 不带表限定:只下推到持有该列的表
5683
- if (schema[key]) filter[key] = this._coerceValue(val, schema[key]);
5881
+ if (schema[key] && !isNonPushdownCol(schema[key])) filter[key] = this._coerceValue(val, schema[key]);
5684
5882
  }
5685
5883
  }
5686
5884
  const useIndex = Object.keys(filter).length > 0 &&
@@ -6198,6 +6396,18 @@ class SQLExecutor {
6198
6396
  walk(statement.where);
6199
6397
  walk(statement.having);
6200
6398
  walk(statement.orderBy);
6399
+ // ORDER BY 的列在解析器里是裸字符串(非 {type:'column'} 对象),walk 检不到。
6400
+ // 字符串可能是输出别名/位置序号(已在 known 里豁免),也可能是真实列;
6401
+ // 两者都不是 → 必须报错(此前静默忽略,用户 ORDER BY nonexistent_col 拿到原序)。
6402
+ if (Array.isArray(statement.orderBy)) {
6403
+ for (const o of statement.orderBy) {
6404
+ const c = o && o.column;
6405
+ if (typeof c !== 'string') continue; // 位置序号(数字)跳过
6406
+ const n = c.toLowerCase();
6407
+ const base = n.indexOf('.') >= 0 ? n.slice(n.indexOf('.') + 1) : n;
6408
+ if (!known.has(n) && !known.has(base)) throw new Error(`no such column: ${c}`);
6409
+ }
6410
+ }
6201
6411
  if (Array.isArray(statement.groupBy)) {
6202
6412
  // groupBy 是列名字符串数组
6203
6413
  for (const g of statement.groupBy) {
@@ -7208,24 +7418,42 @@ class SQLExecutor {
7208
7418
  * 同时按「投影列名 → 下标」解析;命中就直接 `row[idx]`,
7209
7419
  * 不再走 resolveOperand 的通用路径。
7210
7420
  */
7421
+ // 把 ORDER BY 引用的输出列(别名或位置序号)解析回该列的**底层取值**。
7422
+ // 非分组路径的行对象键是底层列名(如 'v'),不是别名('val'),
7423
+ // 直接 r['val'] 会取到 undefined,导致 ORDER BY 静默不排序。
7424
+ const resolveByColumn = (c) => {
7425
+ if (!c) return (r) => null;
7426
+ if (typeof c.expr === 'string' && c.expr !== '*') {
7427
+ const name = c.expr;
7428
+ const node = { type: 'column', name };
7429
+ return (r) => {
7430
+ const v = (r == null) ? undefined : r[name];
7431
+ return v !== undefined ? v : resolveOperand(node, r, this.ctx);
7432
+ };
7433
+ }
7434
+ if (c.scalar) return (r) => resolveOperand(c.scalar, r, this.ctx);
7435
+ if (c.aggregate) return (r) => this._aggValue([r], c.aggregate, c.column, c.distinct, c.separator, c.orderBy);
7436
+ const nm = c.alias;
7437
+ return (r) => (r == null ? null : r[nm]);
7438
+ };
7211
7439
  const ordKeys = statement.orderBy.map(o => {
7212
7440
  const dir = o.dir === 'asc' ? 1 : -1;
7213
- // 位置序号:ORDER BY 1
7441
+ // 位置序号:ORDER BY 1(1-based,指第 N 个输出列)
7214
7442
  const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
7215
7443
  ? o.column.value
7216
7444
  : (typeof o.column === 'number' ? o.column : null);
7217
7445
  if (pos !== null && pos !== undefined) {
7218
- const name = ordNames[pos - 1];
7219
- return { dir, at: (name === undefined || name === null) ? () => null : (r) => (r == null ? null : r[name]) };
7446
+ return { dir, at: resolveByColumn(statement.columns[pos - 1]) };
7220
7447
  }
7221
7448
  if (typeof o.column === 'string') {
7222
7449
  const f = o.column;
7223
- // 节点对象提到循环外复用 —— 此前每次比较都新建一个 {type:'column',name}
7450
+ // 命中某个输出列的别名/列名 → 用该列的底层取值(修复别名排序)
7451
+ const idx = ordNames.findIndex(n => n === f);
7452
+ if (idx >= 0) return { dir, at: resolveByColumn(statement.columns[idx]) };
7224
7453
  const node = { type: 'column', name: f };
7225
7454
  return {
7226
7455
  dir,
7227
7456
  at: (r) => {
7228
- // 先按字段名直取;取不到再走 resolveOperand(点号路径、表达式等)
7229
7457
  const v = (r == null) ? undefined : r[f];
7230
7458
  return v !== undefined ? v : resolveOperand(node, r, this.ctx);
7231
7459
  },
@@ -7709,6 +7937,30 @@ async function inferColumnTypes(engine, stmt, r) {
7709
7937
  });
7710
7938
  }
7711
7939
 
7940
+ // 结果里的大整数(> MAX_SAFE_INTEGER 的 BigInt)在 JSON.stringify / wasm 绑定下
7941
+ // 无法序列化("Do not know how to serialize a BigInt")。输出前递归转成字符串,
7942
+ // 保留精度且可序列化;内部运算仍走 BigInt,精度不丢。
7943
+ function finalizeBigInts(value, seen) {
7944
+ if (typeof value === 'bigint') return value.toString();
7945
+ if (Array.isArray(value)) {
7946
+ if (!seen) seen = new Set();
7947
+ if (seen.has(value)) return value;
7948
+ seen.add(value);
7949
+ for (let i = 0; i < value.length; i++) value[i] = finalizeBigInts(value[i], seen);
7950
+ return value;
7951
+ }
7952
+ if (value && typeof value === 'object') {
7953
+ // Buffer / Date / RegExp 等特殊对象不深入(Buffer 是 Uint8Array,逐字节递归会爆栈)
7954
+ if (value instanceof Buffer || value instanceof Date || value instanceof RegExp) return value;
7955
+ if (!seen) seen = new Set();
7956
+ if (seen.has(value)) return value;
7957
+ seen.add(value);
7958
+ for (const k of Object.keys(value)) value[k] = finalizeBigInts(value[k], seen);
7959
+ return value;
7960
+ }
7961
+ return value;
7962
+ }
7963
+
7712
7964
  async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
7713
7965
  // 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
7714
7966
  // 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
@@ -7767,6 +8019,7 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
7767
8019
  let r = await executor.execute(stmt);
7768
8020
  const durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
7769
8021
  r = buildResultEnvelope(stmt, r, Math.round(durationMs * 1000) / 1000);
8022
+ r = finalizeBigInts(r);
7770
8023
  if (r && r.columns && r.columnTypes === undefined) {
7771
8024
  r.columnTypes = await inferColumnTypes(engine, stmt, r);
7772
8025
  }
package/lib/table.js CHANGED
@@ -1543,6 +1543,11 @@ class Table {
1543
1543
  else if (typeof v === 'number' || typeof v === 'boolean' || v instanceof Date) result[field] = String(v);
1544
1544
  else if (Buffer.isBuffer(v)) result[field] = v.toString('utf8');
1545
1545
  else throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
1546
+ } else if ((t === 'object' || t === 'array' || t === 'json') && (def.jsonType || t === 'json')) {
1547
+ // JSON / JSONB 列:字符串形式必须是合法 JSON({bad json 这类会被 JSON.parse 拒绝)
1548
+ if (typeof v === 'string') {
1549
+ try { JSON.parse(v); } catch (e) { throw createError('ER_TRUNCATED_WRONG_VALUE', t, v); }
1550
+ }
1546
1551
  }
1547
1552
  }
1548
1553
  return result;
@@ -561,6 +561,18 @@ class JSQL {
561
561
  // 难以定位的 "Cannot convert undefined or null to object",这里直接给明确 TypeError
562
562
  const arr = Array.isArray(data) ? data : [data];
563
563
  arr.forEach((row, i) => assertRowObject(row, i, arr.length > 1 || Array.isArray(data)));
564
+ // JSON / JSONB 列:字符串形式必须是合法 JSON({bad json 这类会被 JSON.parse 拒绝)
565
+ const schema = this._schemas[table];
566
+ if (schema) {
567
+ for (const row of arr) {
568
+ for (const [f, def] of Object.entries(schema)) {
569
+ if (def && def.jsonType && typeof row[f] === 'string') {
570
+ try { JSON.parse(row[f]); }
571
+ catch (e) { throw new Error(`Truncated incorrect object value: '${row[f]}'`); }
572
+ }
573
+ }
574
+ }
575
+ }
564
576
  if (!(await this._runHooks('beforeInsert', [table, arr]))) return [];
565
577
  if (arr.length > 1) {
566
578
  await this._flush();
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.5.9",
3
+ "version": "6.5.11",
4
4
  "description": "JSQL-NEO \u2014 Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,87 @@
1
+ /*
2
+ * Bug 回归测试:此前发现的 5 个缺陷,逐一固化,防止回退。
3
+ *
4
+ * 1. 布尔求值器整体失效:TRUE AND TRUE → 0、NULL AND TRUE → 0、NULL OR FALSE → 0。
5
+ * 正确三值逻辑:1 / NULL / NULL。根因是 resolveOperand 没有 and/or/xor/not 分支,
6
+ * 落到 default 返回 undefined → 数值化成 0。
7
+ * 2. CREATE TABLE d(a INT, a INT) 不拒绝重复列名(对象键覆盖静默丢列)。
8
+ * 3. JSONB 列能塞进 {bad json 这种非法 JSON(两引擎:Database _validateDataTypes + wasm insert)。
9
+ * 4. ORDER BY 不存在的列静默忽略(解析产物是裸字符串,_validateColumnRefs 的 walk 检不到)。
10
+ * 5. SELECT 超大整数抛 "Do not know how to serialize a BigInt"(输出前应转字符串)。
11
+ */
12
+ const path = require('path');
13
+
14
+ const ROOT = path.join(__dirname, '..');
15
+ const { JSQL } = require(path.join(ROOT, 'index.js'));
16
+ const { executeSQL } = require(path.join(ROOT, 'lib/sql.js'));
17
+
18
+ const OPTS = { safety: false };
19
+ let pass = 0, fail = 0;
20
+
21
+ function ok(name, cond, extra) {
22
+ if (cond) { pass++; console.log('[OK] ', name); }
23
+ else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 150) : ''); }
24
+ }
25
+ const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
26
+
27
+ (async () => {
28
+ const db = new JSQL(':memory:');
29
+ await db.start();
30
+ const q = (sql) => executeSQL(db, sql, OPTS);
31
+ const rows = async (sql) => (await q(sql)).rows;
32
+ const one = async (sql) => (await q(sql)).rows[0][0];
33
+ const fails = async (sql) => { try { await q(sql); return null; } catch (e) { return e.message; } };
34
+
35
+ // ── 1. 三值布尔逻辑 ────────────────────────────────────────────
36
+ console.log('--- 1. 布尔三值逻辑 ---');
37
+ ok('TRUE AND TRUE = 1', (await one('SELECT TRUE AND TRUE')) === 1);
38
+ ok('TRUE AND FALSE = 0', (await one('SELECT TRUE AND FALSE')) === 0);
39
+ ok('NULL AND TRUE = NULL', (await one('SELECT NULL AND TRUE')) === null);
40
+ ok('NULL AND FALSE = 0', (await one('SELECT NULL AND FALSE')) === 0);
41
+ ok('NULL OR FALSE = NULL', (await one('SELECT NULL OR FALSE')) === null);
42
+ ok('NULL OR TRUE = 1', (await one('SELECT NULL OR TRUE')) === 1);
43
+ ok('FALSE OR FALSE = 0', (await one('SELECT FALSE OR FALSE')) === 0);
44
+ ok('NOT TRUE = 0', (await one('SELECT NOT TRUE')) === 0);
45
+ ok('NOT NULL = NULL', (await one('SELECT NOT NULL')) === null);
46
+ ok('1 AND 0 = 0', (await one('SELECT 1 AND 0')) === 0);
47
+ ok('1 XOR 0 = 1', (await one('SELECT 1 XOR 0')) === 1);
48
+ ok('TRUE OR NULL = 1', (await one('SELECT TRUE OR NULL')) === 1);
49
+
50
+ // ── 2. CREATE TABLE 重复列名 ───────────────────────────────────
51
+ console.log('--- 2. 重复列名 ---');
52
+ ok('d(a INT, a INT) 报错', /Duplicate column/i.test(await fails('CREATE TABLE r1(a INT, a INT)')));
53
+ ok('d(x,y,x) 报错', /Duplicate column/i.test(await fails('CREATE TABLE r2(x INT, y INT, x INT)')));
54
+ ok('正常列不报错', (await fails('CREATE TABLE r3(a INT, b TEXT)')) === null);
55
+ ok('id PRIMARY KEY + id 重复报错', /Duplicate column/i.test(await fails('CREATE TABLE r4(id INT PRIMARY KEY, id INT)')));
56
+
57
+ // ── 3. JSONB 非法 JSON ─────────────────────────────────────────
58
+ console.log('--- 3. JSONB 校验 ---');
59
+ await q('CREATE TABLE rj(c JSONB)');
60
+ ok('{bad json 拒绝', /Truncated incorrect object value/.test(await fails(`INSERT INTO rj VALUES ('{bad json')`)));
61
+ ok('[1,2 拒绝', /Truncated incorrect object value/.test(await fails(`INSERT INTO rj VALUES ('[1,2')`)));
62
+ ok('合法 {"a":1} 通过', (await fails(`INSERT INTO rj VALUES ('{"a":1}')`)) === null);
63
+ await q('CREATE TABLE rj2(c JSON)');
64
+ ok('JSON 列 nope 拒绝', /Truncated incorrect object value/.test(await fails(`INSERT INTO rj2 VALUES ('nope')`)));
65
+
66
+ // ── 4. ORDER BY 不存在列 ───────────────────────────────────────
67
+ console.log('--- 4. ORDER BY 校验 ---');
68
+ await q('CREATE TABLE ro (id INT PRIMARY KEY, v INT, s TEXT)');
69
+ await q('INSERT INTO ro VALUES (1,2,\'x\'),(2,1,\'y\')');
70
+ ok('ORDER BY nonexistent_col 报错', /no such column: nonexistent_col/.test(await fails('SELECT id FROM ro ORDER BY nonexistent_col')));
71
+ ok('WHERE 不存在列报错', /no such column: nosuch/.test(await fails('SELECT id FROM ro WHERE nosuch > 1')));
72
+ ok('ORDER BY 真列 v 正常', eq(await rows('SELECT id FROM ro ORDER BY v'), [[2], [1]]));
73
+ ok('ORDER BY 位置序号 1 正常', eq(await rows('SELECT id FROM ro ORDER BY 1'), [[1], [2]]));
74
+ ok('ORDER BY 输出别名 vv 正常', eq(await rows('SELECT v AS vv FROM ro ORDER BY vv'), [[1], [2]]));
75
+
76
+ // ── 5. 超大整数序列化 ──────────────────────────────────────────
77
+ console.log('--- 5. 超大整数 ---');
78
+ const big = await one('SELECT 1000000000000000000000');
79
+ ok('超大整数可序列化(字符串)', typeof big === 'string' && big === '1000000000000000000000');
80
+ ok('超界精确值 9007199254740993', (await one('SELECT 9007199254740993')) === '9007199254740993');
81
+ const bigRows = await rows('SELECT 1000000000000000000000');
82
+ ok('JSON.stringify 不炸', (() => { try { JSON.stringify(bigRows); return true; } catch (e) { return false; } })());
83
+
84
+ await db.stop();
85
+ console.log(`\nRESULT: ${pass} passed, ${fail} failed`);
86
+ process.exit(fail ? 1 : 0);
87
+ })().catch((e) => { console.error('FATAL', e); process.exit(1); });