jsql-neo 6.5.20 → 6.5.23

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.20** — 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.23** — 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/bin/jsql CHANGED
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
- const yaggs = require('@vexify-org/yaggs');
4
+ // @vexify-org/yaggs 是可选依赖:缺失时(离线安装/私有源不可达/lockfile 漂移)
5
+ // 用内置 shim 兜底,保证 --help/--version 及核心命令仍能启动(#24)。
6
+ let yaggs;
7
+ try { yaggs = require('@vexify-org/yaggs'); }
8
+ catch (e) { yaggs = null; }
5
9
  const { ModuleManager } = require('../lib/mod');
6
10
  const Database = require('../lib/database');
7
11
  const migrate = require('../lib/migrate');
@@ -29,7 +33,10 @@ const manager = new ModuleManager();
29
33
 
30
34
  const RAW_ARGV = process.argv.slice(2);
31
35
 
32
- const cli = yaggs({ pkg: require('../package.json') })
36
+ const _pkg = require('../package.json');
37
+ const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
38
+ pkg: _pkg,
39
+ })
33
40
  .usage('jsql <command> [options]')
34
41
  .option('help', { alias: ['h'], type: 'boolean', description: 'Show help' })
35
42
  .command('mod', 'Module management', (sub) => {
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Minimal @vexify-org/yaggs-compatible CLI shim.
5
+ *
6
+ * Used when the optional dependency @vexify-org/yaggs is not installed
7
+ * (offline install, private registry unreachable, lockfile drift, …).
8
+ * Without this shim, bin/jsql crashed on every invocation — even --help /
9
+ * --version — because the top-level `require('@vexify-org/yaggs')` threw.
10
+ *
11
+ * The shim mirrors the fluent API surface bin/jsql relies on:
12
+ * .usage(str).option(name, cfg).command(name, desc, builder, handler)
13
+ * .middleware(fn).run(argv)
14
+ *
15
+ * It captures every command handler registered via the fluent chain and
16
+ * dispatches to it with a minimal `--key value` / `--flag` argument parse.
17
+ * This keeps --help/--version and the core commands (serve, export, import,
18
+ * mod, plugin, …) working even when the optional dependency is absent.
19
+ */
20
+
21
+ function buildYaggsFallback(pkg) {
22
+ // 与 @vexify-org/yaggs 的调用形态一致:buildYaggsFallback(pkg)({ pkg }) 返回 CLI 对象。
23
+ return function makeCli(opts) {
24
+ const realPkg = (opts && opts.pkg) || pkg;
25
+ const commands = new Map();
26
+ let usageStr = '';
27
+ const self = {
28
+ usage(u) { usageStr = u; return self; },
29
+ option() { return self; },
30
+ positional() { return self; },
31
+ middleware() { return self; },
32
+ command(name, description, builder, handler) {
33
+ // 兼容两种签名:command(name, descObj) 或 command(name, desc, builder, handler)
34
+ let h = handler;
35
+ let desc = description;
36
+ if (description && typeof description === 'object') {
37
+ desc = description.description;
38
+ if (description.handler) h = description.handler;
39
+ }
40
+ commands.set(name, { desc: desc || '', handler: h, builder: builder || (() => {}) });
41
+ return self;
42
+ },
43
+ async run(argv) {
44
+ const args = (argv || []).slice();
45
+ const first = args[0];
46
+
47
+ if (!first || first === '--help' || first === '-h' || first === 'help') {
48
+ console.log(usageStr || `jsql ${realPkg.version} — embedded SQL CLI`);
49
+ console.log('Usage: jsql <command> [options]');
50
+ if (commands.size) {
51
+ console.log('');
52
+ console.log('Commands:');
53
+ for (const [name, c] of commands) {
54
+ console.log(` ${name.padEnd(12)} ${c.desc}`);
55
+ }
56
+ }
57
+ console.log('');
58
+ console.log('Note: optional dependency @vexify-org/yaggs is not installed; full argument parsing is unavailable.');
59
+ console.log(' Install it with `npm install @vexify-org/yaggs` to restore rich argument parsing.');
60
+ return;
61
+ }
62
+
63
+ if (first === '--version' || first === '-v' || first === 'version') {
64
+ console.log(realPkg.version);
65
+ return;
66
+ }
67
+
68
+ const cmd = commands.get(first);
69
+ if (!cmd) {
70
+ console.error(`Unknown command: ${first}`);
71
+ console.error('Run `jsql --help` for the list of commands.');
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+
76
+ // 极简参数解析:--key value / --key=value / --flag,以及位置参数。
77
+ const parsed = { _: [] };
78
+ for (let i = 1; i < args.length; i++) {
79
+ const a = args[i];
80
+ if (a.startsWith('--')) {
81
+ const eq = a.indexOf('=');
82
+ if (eq !== -1) {
83
+ parsed[a.slice(2, eq)] = a.slice(eq + 1);
84
+ } else {
85
+ const nxt = args[i + 1];
86
+ if (nxt !== undefined && !nxt.startsWith('-')) { parsed[a.slice(2)] = nxt; i++; }
87
+ else parsed[a.slice(2)] = true;
88
+ }
89
+ } else if (a.startsWith('-') && a.length > 1) {
90
+ const key = a.slice(1);
91
+ const nxt = args[i + 1];
92
+ if (nxt !== undefined && !nxt.startsWith('-')) { parsed[key] = nxt; i++; }
93
+ else parsed[key] = true;
94
+ } else {
95
+ parsed._.push(a);
96
+ }
97
+ }
98
+
99
+ if (cmd.handler) return cmd.handler(parsed);
100
+ console.error(`Command '${first}' has no handler.`);
101
+ process.exitCode = 1;
102
+ },
103
+ };
104
+ return self;
105
+ };
106
+ }
107
+
108
+ module.exports = { buildYaggsFallback };
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.20 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.23 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
@@ -0,0 +1,39 @@
1
+ // © Vexify 2026 All Rights Reserved.
2
+ /**
3
+ * BigInt 安全的 JSON 序列化(Issue #38)。
4
+ *
5
+ * 行数据里可能出现 >2^53 的 BigInt(integer/int/bigint 列)。原生
6
+ * JSON.stringify 遇到 BigInt 直接抛 "Do not know how to serialize a BigInt",
7
+ * 于是 WAL、事务快照、统计等路径会整条失败。
8
+ *
9
+ * 这里用一个单键标记对象 `{ __jsql_bigint__: "9223372036854775807" }`
10
+ * 在 JSON 中无损往返;反序列化时还原成 BigInt。只有「对象恰好只有这一个键」
11
+ * 时才还原,尽量避免误伤用户自己的同名字段。
12
+ */
13
+
14
+ const MARKER = '__jsql_bigint__';
15
+
16
+ function replacer(_key, value) {
17
+ if (typeof value === 'bigint') return { [MARKER]: value.toString() };
18
+ return value;
19
+ }
20
+
21
+ function reviver(_key, value) {
22
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
23
+ const keys = Object.keys(value);
24
+ if (keys.length === 1 && keys[0] === MARKER && typeof value[MARKER] === 'string') {
25
+ try { return BigInt(value[MARKER]); } catch (_) { return value; }
26
+ }
27
+ }
28
+ return value;
29
+ }
30
+
31
+ function stringify(value, space) {
32
+ return JSON.stringify(value, replacer, space);
33
+ }
34
+
35
+ function parse(text) {
36
+ return JSON.parse(text, reviver);
37
+ }
38
+
39
+ module.exports = { stringify, parse, MARKER };
package/lib/database.js CHANGED
@@ -10,6 +10,7 @@ const os = require('os');
10
10
  const crypto = require('crypto');
11
11
  const Table = require('./table');
12
12
  const { createError, JSQL_Error, ErrorCodes } = require('./errors');
13
+ const bigintJson = require('./bigint_json');
13
14
 
14
15
  /**
15
16
  * 构造带补充说明的错误。
@@ -463,7 +464,7 @@ class Database {
463
464
  // 只追加尚未落盘的记录
464
465
  const pending = this._walOps.slice(this._walFlushedCount);
465
466
  if (pending.length === 0) return;
466
- const lines = pending.map(o => JSON.stringify(o)).join('\n') + '\n';
467
+ const lines = pending.map(o => bigintJson.stringify(o)).join('\n') + '\n';
467
468
  const fd = fs.openSync(this._walPath, 'a');
468
469
  try {
469
470
  fs.writeSync(fd, lines, null, 'utf8');
@@ -528,7 +529,7 @@ class Database {
528
529
  const trimmed = raw.trim();
529
530
  if (trimmed.startsWith('[')) {
530
531
  try {
531
- const parsed = JSON.parse(trimmed);
532
+ const parsed = bigintJson.parse(trimmed);
532
533
  if (Array.isArray(parsed)) {
533
534
  ops = parsed.filter(o => o && typeof o.op === 'string');
534
535
  }
@@ -541,7 +542,7 @@ class Database {
541
542
  const t = line.trim();
542
543
  if (!t) continue;
543
544
  try {
544
- const o = JSON.parse(t);
545
+ const o = bigintJson.parse(t);
545
546
  if (o && typeof o.op === 'string') ops.push(o);
546
547
  } catch (e) {
547
548
  // 崩溃时写半的尾行,丢弃
@@ -1129,7 +1130,7 @@ class Database {
1129
1130
  const rowCount = table._rows.length;
1130
1131
  const indexCount = Object.keys(table._indexes).length;
1131
1132
  const btreeCount = Object.keys(table._btrees).length;
1132
- const sizeEstimate = JSON.stringify(table._rows).length;
1133
+ const sizeEstimate = bigintJson.stringify(table._rows).length;
1133
1134
  tableStats[name] = {
1134
1135
  rowCount,
1135
1136
  indexCount: indexCount + btreeCount,
@@ -1263,7 +1264,7 @@ class Database {
1263
1264
  const snap = {};
1264
1265
  for (const [name, table] of Object.entries(this._tables)) {
1265
1266
  snap[name] = {
1266
- rows: JSON.parse(JSON.stringify(table.toJSON())),
1267
+ rows: bigintJson.parse(bigintJson.stringify(table.toJSON())),
1267
1268
  schema: table._schema,
1268
1269
  // 自增计数器也要存:否则回滚掉刚插入的行之后,
1269
1270
  // 下一次插入会跳过已用过的号(用户看到 id 从 2 开始会困惑)。
@@ -1290,7 +1291,7 @@ class Database {
1290
1291
  if (!snap[name]) delete this._tables[name];
1291
1292
  }
1292
1293
  for (const [name, entry] of Object.entries(snap)) {
1293
- const rows = JSON.parse(JSON.stringify(entry.rows));
1294
+ const rows = bigintJson.parse(bigintJson.stringify(entry.rows));
1294
1295
  let table = this._tables[name];
1295
1296
  if (!table) {
1296
1297
  table = new Table(name, entry.schema, this);
@@ -1507,7 +1508,7 @@ class Database {
1507
1508
 
1508
1509
  if (table._rows.length === 0) return;
1509
1510
 
1510
- const currentHash = JSON.stringify(table._rows);
1511
+ const currentHash = bigintJson.stringify(table._rows);
1511
1512
  const versions = this._versions[tableName];
1512
1513
  if (versions.length > 0 && versions[versions.length - 1]._hash === currentHash) return;
1513
1514
 
@@ -1948,7 +1949,23 @@ class Database {
1948
1949
  const clash = table._rows.find(r => r !== row && r[field] === nv);
1949
1950
  if (clash) throw createError('ER_DUP_ENTRY', `Duplicate entry '${nv}' for key '${field}'`);
1950
1951
  }
1951
- Object.assign(row, validated);
1952
+ // 外键:改主键时按 ON UPDATE 传播到引用它的子表;改自身外键列时校验引用存在。
1953
+ // 此前这里直接 Object.assign 绕过 Table.updateById,ON UPDATE CASCADE / RESTRICT
1954
+ // 与 FK 校验全部失效(外键形同虚设)。
1955
+ if (pk && Object.prototype.hasOwnProperty.call(validated, pk) && validated[pk] !== row[pk]) {
1956
+ table._propagatePKUpdate(row[pk], validated[pk]);
1957
+ }
1958
+ if (table._foreignKeys && table._foreignKeys.length) {
1959
+ table._validateForeignKeys({ ...row, ...validated });
1960
+ }
1961
+ // 改主键/唯一值时索引必须同步:否则 _pkIndex 还指向旧值,
1962
+ // 后续 FK 校验 findOne({id:新值}) 查不到刚更新的父行。
1963
+ {
1964
+ const rowIndex = table._rows.indexOf(row);
1965
+ if (rowIndex !== -1) table._removeFromIndexes(rowIndex, row);
1966
+ Object.assign(row, validated);
1967
+ if (rowIndex !== -1) table._addToIndexes(rowIndex, row);
1968
+ }
1952
1969
  if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...validated } });
1953
1970
  this._emit('update', { table: tableName, id, data: validated });
1954
1971
  await this._runHooks('afterUpdate', [tableName, id, validated, { ok: true }]);
@@ -2218,8 +2235,8 @@ class Database {
2218
2235
  backup(filePath) {
2219
2236
  const data = this.export();
2220
2237
  const json = this._options.pretty
2221
- ? JSON.stringify(data, null, 2)
2222
- : JSON.stringify(data);
2238
+ ? bigintJson.stringify(data, 2)
2239
+ : bigintJson.stringify(data);
2223
2240
  this._writeFile(filePath, json);
2224
2241
  return filePath;
2225
2242
  }
@@ -2229,7 +2246,7 @@ class Database {
2229
2246
  throw createError('ER_FILE_NOT_FOUND', filePath);
2230
2247
  }
2231
2248
  const json = this._readFile(filePath);
2232
- const data = JSON.parse(json);
2249
+ const data = bigintJson.parse(json);
2233
2250
  for (const [name, rows] of Object.entries(data)) {
2234
2251
  if (this._tables[name]) {
2235
2252
  this._tables[name]._loadRows(rows);
@@ -2280,8 +2297,8 @@ class Database {
2280
2297
  }
2281
2298
 
2282
2299
  const json = this._options.pretty
2283
- ? JSON.stringify(data, null, 2)
2284
- : JSON.stringify(data);
2300
+ ? bigintJson.stringify(data, 2)
2301
+ : bigintJson.stringify(data);
2285
2302
  this._writeFile(this._filePath, json);
2286
2303
  this._dirty = false;
2287
2304
 
@@ -2312,7 +2329,7 @@ class Database {
2312
2329
  }
2313
2330
 
2314
2331
  const json = this._readFile(this._filePath);
2315
- const data = JSON.parse(json);
2332
+ const data = bigintJson.parse(json);
2316
2333
 
2317
2334
  if (data.__schema__) {
2318
2335
  for (const [name, schema] of Object.entries(data.__schema__)) {
@@ -25,6 +25,7 @@ function tag(v) {
25
25
  }
26
26
  return TYPE.F64;
27
27
  }
28
+ if (typeof v==='bigint') return TYPE.I64;
28
29
  if (typeof v==='string') return TYPE.STR;
29
30
  if (v instanceof Buffer) return TYPE.BUF;
30
31
  return Array.isArray(v)?TYPE.ARR:TYPE.OBJ;
@@ -51,7 +52,12 @@ function dec(buf, off) {
51
52
  case TYPE.NUL: return {v:null,off};
52
53
  case TYPE.BOL: return {v:buf[off]!==0,off:off+1};
53
54
  case TYPE.I32: return {v:buf.readInt32LE(off),off:off+4};
54
- case TYPE.I64: return {v:Number(buf.readBigInt64LE(off)),off:off+8};
55
+ case TYPE.I64: {
56
+ // 安全范围内的值仍还原成 number(兼容旧数据/行为);超出范围返回 BigInt,避免精度丢失(Issue #38)
57
+ const b = buf.readBigInt64LE(off);
58
+ const safe = b >= BigInt(Number.MIN_SAFE_INTEGER) && b <= BigInt(Number.MAX_SAFE_INTEGER);
59
+ return {v: safe ? Number(b) : b, off:off+8};
60
+ }
55
61
  case TYPE.F64: return {v:buf.readDoubleLE(off),off:off+8};
56
62
  case TYPE.STR: { const l=buf.readUInt32LE(off);off+=4; return {v:buf.toString('utf8',off,off+l),off:off+l}; }
57
63
  case TYPE.BUF: { const l=buf.readUInt32LE(off);off+=4; return {v:Buffer.from(buf.subarray(off,off+l)),off:off+l}; }
@@ -78,6 +84,10 @@ function writeEnc(buf, off, v) {
78
84
  }
79
85
  buf[off] = 3; buf.writeDoubleLE(v, off+1); return off + 9;
80
86
  }
87
+ if (typeof v === 'bigint') {
88
+ // Issue #38:64 位整数精确保存,读回时再决定还原成 number 还是 BigInt
89
+ buf[off] = 8; buf.writeBigInt64LE(v, off+1); return off + 9;
90
+ }
81
91
  if (typeof v === 'string') {
82
92
  buf[off] = 4;
83
93
  const encoded = Buffer.from(v, 'utf8');
@@ -281,7 +281,8 @@ function columnDefinition(column) {
281
281
 
282
282
  function columnTypeFromSchema(def) {
283
283
  const type = def && def.type ? String(def.type).toLowerCase() : 'string';
284
- if (type === 'integer' || type === 'int' || type === 'bigint') return MYSQL_TYPE_LONG;
284
+ if (type === 'integer' || type === 'int') return MYSQL_TYPE_LONG;
285
+ if (type === 'bigint') return MYSQL_TYPE_LONGLONG;
285
286
  if (type === 'number' || type === 'float' || type === 'double' || type === 'real') return MYSQL_TYPE_DOUBLE;
286
287
  if (type === 'boolean' || type === 'bool') return MYSQL_TYPE_TINY;
287
288
  if (type === 'date') return MYSQL_TYPE_DATE;
@@ -394,15 +395,26 @@ function binaryResultSetPacket(result, tableSchema, baseSeq) {
394
395
  if (v === null || v === undefined) return null;
395
396
  if (type === MYSQL_TYPE_LONG || type === MYSQL_TYPE_LONGLONG || type === MYSQL_TYPE_DOUBLE) {
396
397
  const num = Number(v);
398
+ if (!Number.isFinite(num)) return encodeLenencString(String(v));
397
399
  if (type === MYSQL_TYPE_DOUBLE) {
398
- if (!Number.isFinite(num)) return encodeLenencString(String(v));
399
400
  const b = Buffer.alloc(8); b.writeDoubleLE(num, 0); return b;
400
401
  }
401
- if (Number.isInteger(num) && num <= 2147483647 && num >= -2147483648) {
402
- const b = Buffer.alloc(4); b.writeInt32LE(num, 0); return b;
403
- }
404
402
  if (!Number.isFinite(num)) return encodeLenencString(String(v));
405
- const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(Math.trunc(num)), 0); return b;
403
+ if (type === MYSQL_TYPE_LONG) {
404
+ // 4 字节 LONG:总是写 4 字节(#32)。超出 int32 范围时钳位,
405
+ // 不能写 8 字节,否则二进制结果集的行布局被破坏(下一列错位)。
406
+ let n = Math.trunc(num);
407
+ if (n > 2147483647) n = 2147483647;
408
+ if (n < -2147483648) n = -2147483648;
409
+ const b = Buffer.alloc(4); b.writeInt32LE(n, 0); return b;
410
+ }
411
+ // MYSQL_TYPE_LONGLONG:始终写 8 字节,超出 int64 范围时钳位(保持行布局)。
412
+ let bi = BigInt(Math.trunc(num));
413
+ const INT64_MAX = 9223372036854775807n;
414
+ const INT64_MIN = -9223372036854775808n;
415
+ if (bi > INT64_MAX) bi = INT64_MAX;
416
+ if (bi < INT64_MIN) bi = INT64_MIN;
417
+ const b = Buffer.alloc(8); b.writeBigInt64LE(bi, 0); return b;
406
418
  }
407
419
  if (type === MYSQL_TYPE_TINY) {
408
420
  const b = Buffer.alloc(1); b.writeInt8(v ? 1 : 0, 0); return b;
@@ -1102,8 +1114,10 @@ class MysqlServer {
1102
1114
  if (t && t.indexOf('.') !== -1) {
1103
1115
  const re = new RegExp('\\b' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
1104
1116
  s = s.replace(re, (match, offset) => {
1105
- // 如果匹配位置在字符串字面量或注释中,不替换
1106
- if (masked[offset] !== ' ') return match;
1117
+ // 如果匹配位置在字符串字面量或注释中,不替换。
1118
+ // _maskStringsAndComments 把字符串/注释内的字符置为空格,
1119
+ // 因此 masked[offset] === ' ' 表示该位置在字面量/注释里(#31)。
1120
+ if (masked[offset] === ' ') return match;
1107
1121
  return t.split('.')[1];
1108
1122
  });
1109
1123
  }
@@ -57,6 +57,19 @@ function _trackInstance(inst) {
57
57
  }
58
58
 
59
59
  const INT64_TAG = 1;
60
+
61
+ /**
62
+ * 把可能是字符串/数字的 limit / offset / id 规范成非负整数,供原生 usize/u64 使用。
63
+ * 负值(或 NaN/Infinity)会在绑定层直接抛错,而不是让 Rust 侧 debug panic 或
64
+ * release 静默空结果(#35)。返回 Math.trunc(n)(对非负浮点向下取整,与 usize 语义一致)。
65
+ */
66
+ function toNonNeg(name, v, def) {
67
+ if (v === undefined || v === null) return def;
68
+ const n = Number(v);
69
+ if (!Number.isFinite(n)) throw new Error(`jsql-neo: ${name} must be a finite number, got ${v}`);
70
+ if (n < 0) throw new Error(`jsql-neo: ${name} must be non-negative, got ${v}`);
71
+ return Math.trunc(n);
72
+ }
60
73
  const FLOAT_TAG = 2;
61
74
  const STR_TAG = 3;
62
75
  const BOOL_TAG = 4;
@@ -539,7 +552,7 @@ class JSQL {
539
552
  if (!schema || !pk) return id;
540
553
  const def = schema[pk];
541
554
  let t = null;
542
- if (typeof def === 'string') t = def.toLowerCase();
555
+ if (typeof def === 'string') t = def.trim().split(/\s+/)[0].toLowerCase();
543
556
  else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
544
557
  if (['integer', 'int', 'bigint'].includes(t)) return Number(id);
545
558
  return id;
@@ -552,8 +565,11 @@ class JSQL {
552
565
  _isAutoIntPk(table) {
553
566
  if (!this._pkAutoIncrement(table)) return false;
554
567
  const def = this._schemas[table][this._pkOf(table)];
555
- const t = typeof def === 'string' ? def.toLowerCase()
556
- : (def && def.type ? String(def.type).toLowerCase() : '');
568
+ // 简写字符串 schema(如 'integer primary key auto_increment')取首个 token 作为类型;
569
+ // 对象 schema 直接读 def.type。二者都必须精确匹配整数类型,否则负 id 校验不生效(issue #35)。
570
+ let t = '';
571
+ if (typeof def === 'string') t = def.trim().split(/\s+/)[0].toLowerCase();
572
+ else if (def && def.type) t = String(def.type).toLowerCase();
557
573
  return ['integer', 'int', 'bigint'].includes(t);
558
574
  }
559
575
 
@@ -561,8 +577,11 @@ class JSQL {
561
577
  const pk = this._pkOf(table);
562
578
  if (pk) {
563
579
  if (this._isAutoIntPk(table)) {
564
- const n = Number(id);
580
+ const n = toNonNeg('id', id, NaN);
565
581
  if (Number.isFinite(n)) {
582
+ // 负 id 会被原生层 `i64 -> u64` 静默回绕成 usize::MAX,导致
583
+ // findById 误报 not found(行明明存在却取不到)。在此显式拒绝。
584
+ if (n < 0) throw new Error('id must be a non-negative number');
566
585
  const r = safeParse(this._n('jsqlFindById', table, n));
567
586
  if (r && r.error) return r;
568
587
  return r || null;
@@ -574,15 +593,18 @@ class JSQL {
574
593
  if (r && r.error) return r;
575
594
  return Array.isArray(r) && r.length > 0 ? r[0] : null;
576
595
  }
577
- return safeParse(this._n('jsqlFindById', table, Number(id)));
596
+ return safeParse(this._n('jsqlFindById', table, toNonNeg('id', id, NaN)));
578
597
  }
579
598
 
580
599
  _seqIdByPk(table, id) {
581
600
  const pk = this._pkOf(table);
582
601
  if (pk) {
583
602
  if (this._isAutoIntPk(table)) {
584
- const n = Number(id);
603
+ const n = toNonNeg('id', id, NaN);
585
604
  if (Number.isFinite(n)) {
605
+ // 负 id 在原生层回绕为 usize::MAX 后静默 not found,update/remove
606
+ // 会无提示地什么都不做 —— 这里显式拒绝(见 issue #35)。
607
+ if (n < 0) throw new Error('id must be a non-negative number');
586
608
  const r = safeParse(this._n('jsqlFindById', table, n));
587
609
  if (r && !r.error && r.id !== undefined) return r.id;
588
610
  }
@@ -824,7 +846,10 @@ class JSQL {
824
846
  let r;
825
847
  if (this._isAutoIntPk(table) && ids.every(id => Number.isFinite(Number(id)))) {
826
848
  // 自增主键:内部 id 即主键值,一次批量调用即可,避免 N 次往返。
827
- r = safeParse(this._n('jsqlFindByIds', table, JSON.stringify(ids.map(Number))));
849
+ // #35:负 id 在原生层 `i64 -> u64` 回绕成 usize::MAX,静默 not found;
850
+ // NaN/Infinity 同样不允许。绑定层先校验,绝不把非法值传给原生模块。
851
+ const nums = ids.map(id => toNonNeg('id', id, 0));
852
+ r = safeParse(this._n('jsqlFindByIds', table, JSON.stringify(nums)));
828
853
  } else if (this._pkOf(table)) {
829
854
  r = ids.map(id => this._findRawById(table, id)).filter(x => x !== null && !(x && x.error));
830
855
  } else {
@@ -846,7 +871,12 @@ class JSQL {
846
871
  if (!(await this._runHooks('beforeFind', [table, { filter, opts }]))) return [];
847
872
  const filterStr = filter ? JSON.stringify(filter) : '';
848
873
  const { limit = 100, offset = 0 } = opts;
849
- const r = safeParse(this._n('jsqlFind', table, filterStr, limit, offset));
874
+ // #35:原生层把 limit/offset 按 `as usize` 无符号转换,负数会静默回绕
875
+ // (release 下空结果 / debug 下 usize::MAX+1 溢出 panic)。绑定层先校验,
876
+ // 与 WASM 端的 usize 签名保持一致,绝不把负值传给原生模块。
877
+ const lim = toNonNeg('limit', limit, 100);
878
+ const off = toNonNeg('offset', offset, 0);
879
+ const r = safeParse(this._n('jsqlFind', table, filterStr, lim, off));
850
880
  if (r && r.error) throw new Error(r.error);
851
881
  await this._runHooks('afterFind', [table, { filter, opts }, r]);
852
882
  const schema = this._schemas[table];
package/lib/pg_server.js CHANGED
@@ -711,7 +711,11 @@ class PgConnection {
711
711
  const tag = `SELECT ${(res.rows || []).length}`;
712
712
  this._send(buildCommandComplete(tag));
713
713
  } else if (res.type === 'insert' || res.type === 'update' || res.type === 'delete') {
714
- const tag = `${String(res.type).toUpperCase()} ${res.affectedRows != null ? res.affectedRows : 0}`;
714
+ // PG CommandComplete tag:INSERT 是 "INSERT <oid> <count>",UPDATE/DELETE 是
715
+ // "UPDATE <count>" / "DELETE <count>"(#33)。缺了 OID 段会让客户端把 count
716
+ // 解析到 OID 槽位、rowCount 变 NaN。
717
+ const count = res.affectedRows != null ? res.affectedRows : 0;
718
+ const tag = res.type === 'insert' ? `INSERT 0 ${count}` : `${String(res.type).toUpperCase()} ${count}`;
715
719
  this._send(buildCommandComplete(tag));
716
720
  } else if (res.type === 'begin' || res.type === 'commit' || res.type === 'rollback') {
717
721
  this._inTransaction = res.type === 'begin';
package/lib/query.js CHANGED
@@ -227,9 +227,11 @@ class Query {
227
227
  const startTime = Date.now();
228
228
  if (this._explain) return this._getExplain();
229
229
 
230
- // 缓存命中
230
+ // 缓存命中: 返回每行的浅拷贝,与未命中路径一致,避免调用方修改污染缓存
231
231
  if (this._cache && this._cache.result && (Date.now() - this._cache.timestamp < this._cache.ttl)) {
232
- return this._cache.result;
232
+ // #37:命中返回副本,而不是内部数组引用——调用方对返回值做变更
233
+ // (push/splice/改字段)会污染缓存;未命中路径返回的是新数组,行为应一致。
234
+ return this._cache.result.map(row => (row && typeof row === 'object' ? { ...row } : row));
233
235
  }
234
236
 
235
237
  // P1-4: 软删除模式下 QueryBuilder 也不得泄露已删除行
@@ -327,9 +329,10 @@ class Query {
327
329
  rows = copy;
328
330
  }
329
331
 
330
- // 缓存存储
332
+ // 缓存存储:#37 存副本,未命中路径返回的 rows 直接给调用方随意变更,
333
+ // 不能把同一个数组塞进缓存(否则调用方 push/改字段会污染后续命中)。
331
334
  if (this._cache) {
332
- this._cache.result = rows;
335
+ this._cache.result = rows.map(row => (row && typeof row === 'object' ? { ...row } : row));
333
336
  this._cache.timestamp = Date.now();
334
337
  }
335
338