jsql-neo 6.5.22 → 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.22** — 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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.22 — 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
 
@@ -2234,8 +2235,8 @@ class Database {
2234
2235
  backup(filePath) {
2235
2236
  const data = this.export();
2236
2237
  const json = this._options.pretty
2237
- ? JSON.stringify(data, null, 2)
2238
- : JSON.stringify(data);
2238
+ ? bigintJson.stringify(data, 2)
2239
+ : bigintJson.stringify(data);
2239
2240
  this._writeFile(filePath, json);
2240
2241
  return filePath;
2241
2242
  }
@@ -2245,7 +2246,7 @@ class Database {
2245
2246
  throw createError('ER_FILE_NOT_FOUND', filePath);
2246
2247
  }
2247
2248
  const json = this._readFile(filePath);
2248
- const data = JSON.parse(json);
2249
+ const data = bigintJson.parse(json);
2249
2250
  for (const [name, rows] of Object.entries(data)) {
2250
2251
  if (this._tables[name]) {
2251
2252
  this._tables[name]._loadRows(rows);
@@ -2296,8 +2297,8 @@ class Database {
2296
2297
  }
2297
2298
 
2298
2299
  const json = this._options.pretty
2299
- ? JSON.stringify(data, null, 2)
2300
- : JSON.stringify(data);
2300
+ ? bigintJson.stringify(data, 2)
2301
+ : bigintJson.stringify(data);
2301
2302
  this._writeFile(this._filePath, json);
2302
2303
  this._dirty = false;
2303
2304
 
@@ -2328,7 +2329,7 @@ class Database {
2328
2329
  }
2329
2330
 
2330
2331
  const json = this._readFile(this._filePath);
2331
- const data = JSON.parse(json);
2332
+ const data = bigintJson.parse(json);
2332
2333
 
2333
2334
  if (data.__schema__) {
2334
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');
package/lib/sql.js CHANGED
@@ -201,6 +201,19 @@ function jsonPathGet(data, path) {
201
201
  return current === undefined ? null : current;
202
202
  }
203
203
 
204
+ /**
205
+ * 16 进制整数字面量(0x?? / X'??')的精确解析(Issue #38)。
206
+ *
207
+ * 旧的 parseInt(hexStr, 16) 在 >2^53 时直接丢精度,且与十进制字面量
208
+ * (已被解析成 BigInt)行为不一致;这里统一成同样的规则:
209
+ * 安全范围内 → number;超出 → BigInt(精确值保留给 int64 列)。
210
+ */
211
+ function parseHexIntegerLiteral(hexStr) {
212
+ if (hexStr.length === 0) throw new Error('Invalid hexadecimal literal');
213
+ const b = BigInt('0x' + hexStr);
214
+ return b <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(b) : b;
215
+ }
216
+
204
217
  function tokenize(sql) {
205
218
  const tokens = [];
206
219
  let i = 0;
@@ -303,7 +316,7 @@ function tokenize(sql) {
303
316
  while (j < n && /[0-9A-Fa-f]/.test(sql[j])) j++;
304
317
  if (j === i + 2) throw new Error(`Invalid hexadecimal literal at position ${i}`);
305
318
  const hexStr = sql.slice(i + 2, j);
306
- const numVal = parseInt(hexStr, 16);
319
+ const numVal = parseHexIntegerLiteral(hexStr);
307
320
  tokens.push(new SQLToken('number', numVal, i));
308
321
  i = j;
309
322
  continue;
@@ -317,7 +330,7 @@ function tokenize(sql) {
317
330
  j++;
318
331
  }
319
332
  if (j >= n) throw new Error(`Unterminated hexadecimal literal at position ${i}`);
320
- const numVal = parseInt(hexStr, 16);
333
+ const numVal = parseHexIntegerLiteral(hexStr);
321
334
  tokens.push(new SQLToken('number', numVal, i));
322
335
  i = j + 1;
323
336
  continue;
@@ -3121,6 +3134,26 @@ function mysqlNum(v) {
3121
3134
  return 0;
3122
3135
  }
3123
3136
 
3137
+ /**
3138
+ * BigInt 参与的整数算术(Issue #38)。
3139
+ *
3140
+ * BigInt 与 number 不能直接运算(TypeError),而 Number(BigInt) 会丢精度。
3141
+ * 两侧都是整数(BigInt 或整数值 number)且至少一侧是 BigInt 时,用 BigInt
3142
+ * 精确计算;否则返回 undefined,调用方退回 double 语义。
3143
+ */
3144
+ function bigintExactArith(l, r, op) {
3145
+ if (typeof l !== 'bigint' && typeof r !== 'bigint') return undefined;
3146
+ const lb = typeof l === 'bigint' ? l : (typeof l === 'number' && Number.isInteger(l) ? BigInt(l) : null);
3147
+ const rb = typeof r === 'bigint' ? r : (typeof r === 'number' && Number.isInteger(r) ? BigInt(r) : null);
3148
+ if (lb === null || rb === null) return undefined;
3149
+ switch (op) {
3150
+ case '+': return lb + rb;
3151
+ case '-': return lb - rb;
3152
+ case '*': return lb * rb;
3153
+ default: return undefined;
3154
+ }
3155
+ }
3156
+
3124
3157
  function resolveOperand(operand, row, ctx) {
3125
3158
  if (operand === null || operand === undefined) return null;
3126
3159
  if (typeof operand === 'string') return resolveOperand({ type: 'column', name: operand }, row, ctx);
@@ -3203,6 +3236,8 @@ function resolveOperand(operand, row, ctx) {
3203
3236
  // 数字与字符串混合的算术会一路错下去。
3204
3237
  // 判据:任一侧是 number,或字符串能被完整解析为数字 → 走数值加法。
3205
3238
  case '+': {
3239
+ const bx = bigintExactArith(l, r, '+');
3240
+ if (bx !== undefined) return bx;
3206
3241
  // MySQL / SQLite 语义(已用 sqlite3 实测核对):
3207
3242
  // '5' + 5 = 10 '5' + '5' = 10 'abc' + 1 = 1 'a' + 'b' = 0
3208
3243
  // 任一侧是「数值」时做算术加法,另一侧非数值按 0;
@@ -3217,12 +3252,16 @@ function resolveOperand(operand, row, ctx) {
3217
3252
  return 0;
3218
3253
  }
3219
3254
  case '-': {
3255
+ const bx = bigintExactArith(l, r, '-');
3256
+ if (bx !== undefined) return bx;
3220
3257
  // P1-21: 'abc'-1 → NaN,应返回 null
3221
3258
  const ln = Number(l), rn = Number(r);
3222
3259
  if (isNaN(ln) || isNaN(rn)) return null;
3223
3260
  return ln - rn;
3224
3261
  }
3225
3262
  case '*': {
3263
+ const bx = bigintExactArith(l, r, '*');
3264
+ if (bx !== undefined) return bx;
3226
3265
  const ln = Number(l), rn = Number(r);
3227
3266
  if (isNaN(ln) || isNaN(rn)) return null;
3228
3267
  return ln * rn;
@@ -3270,6 +3309,8 @@ function resolveOperand(operand, row, ctx) {
3270
3309
  if (l === null || r === null || l === undefined || r === undefined) return null;
3271
3310
  switch (operand.op) {
3272
3311
  case '+': {
3312
+ const bx = bigintExactArith(l, r, '+');
3313
+ if (bx !== undefined) return bx;
3273
3314
  if (typeof l === 'number' || typeof r === 'number') {
3274
3315
  const ln = numericIfNumeric(l), rn = numericIfNumeric(r);
3275
3316
  return (ln === null ? 0 : ln) + (rn === null ? 0 : rn);
@@ -3279,8 +3320,8 @@ function resolveOperand(operand, row, ctx) {
3279
3320
  if (rn !== null) return rn;
3280
3321
  return 0;
3281
3322
  }
3282
- case '-': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln - rn; }
3283
- case '*': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln * rn; }
3323
+ case '-': { const bx = bigintExactArith(l, r, '-'); if (bx !== undefined) return bx; const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln - rn; }
3324
+ case '*': { const bx = bigintExactArith(l, r, '*'); if (bx !== undefined) return bx; const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln * rn; }
3284
3325
  case '/': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) || rn === 0 ? null : ln / rn; }
3285
3326
  case '%': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) || rn === 0 ? null : ln % rn; }
3286
3327
  case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
@@ -4880,6 +4921,8 @@ function evaluateExpr(expr, row, ctx) {
4880
4921
  if (l === null || r === null || l === undefined || r === undefined) return null;
4881
4922
  switch (expr.op) {
4882
4923
  case '+': {
4924
+ const bx = bigintExactArith(l, r, '+');
4925
+ if (bx !== undefined) return bx;
4883
4926
  if (typeof l === 'number' || typeof r === 'number') {
4884
4927
  const ln = numericIfNumeric(l), rn = numericIfNumeric(r);
4885
4928
  return (ln === null ? 0 : ln) + (rn === null ? 0 : rn);
@@ -4889,8 +4932,8 @@ function evaluateExpr(expr, row, ctx) {
4889
4932
  if (rn !== null) return rn;
4890
4933
  return 0;
4891
4934
  }
4892
- case '-': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln - rn; }
4893
- case '*': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln * rn; }
4935
+ case '-': { const bx = bigintExactArith(l, r, '-'); if (bx !== undefined) return bx; const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln - rn; }
4936
+ case '*': { const bx = bigintExactArith(l, r, '*'); if (bx !== undefined) return bx; const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) ? null : ln * rn; }
4894
4937
  case '/': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) || rn === 0 ? null : ln / rn; }
4895
4938
  case '%': { const ln = Number(l), rn = Number(r); return isNaN(ln) || isNaN(rn) || rn === 0 ? null : ln % rn; }
4896
4939
  case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
@@ -6108,6 +6151,14 @@ class SQLExecutor {
6108
6151
  const type = colDef && colDef.type;
6109
6152
  if (!type) return val;
6110
6153
  if (type === 'integer' || type === 'int' || type === 'bigint' || type === 'smallint' || type === 'tinyint') {
6154
+ // BigInt 必须原样保留:Number(val) 会在 >2^53 时丢精度,
6155
+ // 下推成索引后与行里的 BigInt 严格比较永远不相等(Issue #38)。
6156
+ if (typeof val === 'bigint') return val;
6157
+ // 十进制字符串(参数内联后常见)同样要精确解析,不能用 Number()
6158
+ if (typeof val === 'string' && /^[+-]?\d+$/.test(val.trim())) {
6159
+ const b = BigInt(val.trim());
6160
+ return (b >= BigInt(Number.MIN_SAFE_INTEGER) && b <= BigInt(Number.MAX_SAFE_INTEGER)) ? Number(b) : b;
6161
+ }
6111
6162
  const n = Number(val);
6112
6163
  return Number.isNaN(n) ? val : n;
6113
6164
  }
@@ -8045,6 +8096,7 @@ function escapeId(name) {
8045
8096
 
8046
8097
  function escapeValue(value) {
8047
8098
  if (value === null || value === undefined) return 'NULL';
8099
+ if (typeof value === 'bigint') return value.toString(); // Issue #38:精确的整数字面量
8048
8100
  if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'NULL';
8049
8101
  if (typeof value === 'boolean') return value ? 'true' : 'false';
8050
8102
  if (value instanceof Date) {
package/lib/table.js CHANGED
@@ -9,6 +9,45 @@ const BTree = require('./btree');
9
9
  const { validateDateType, now } = require('./date-types');
10
10
  const { createError } = require('./errors');
11
11
 
12
+ // ---------------------------------------------------------------------------
13
+ // 64 位整数(Issue #38)
14
+ //
15
+ // JS 的 number 只能精确表示 |v| <= 2^53-1。integer/int/bigint 列原先用
16
+ // Number(v) 归一,>2^53 的输入(无论 number 还是十进制字符串)都会被静默
17
+ // 截断成 53 位浮点,SQL 路径则因为字面量被解析成 BigInt 而报 1292 —— 两条
18
+ // 路径行为不一致且都丢精度。
19
+ //
20
+ // 现在的语义:
21
+ // • BigInt 输入 → 精确接受(在 int64 范围内);
22
+ // • 十进制字符串输入 → 精确解析成 BigInt,安全范围内仍存成 number(兼容旧行为);
23
+ // • 超安全范围的 number → 明确报 1292(调用方在 JS 层就已丢精度,不能静默入库);
24
+ // • 超出 int64 范围 → 明确报 1292(持久化格式是 8 字节有符号整数)。
25
+ // ---------------------------------------------------------------------------
26
+ const INT64_MIN = -9223372036854775808n;
27
+ const INT64_MAX = 9223372036854775807n;
28
+ const SAFE_INT_MIN = BigInt(Number.MIN_SAFE_INTEGER);
29
+ const SAFE_INT_MAX = BigInt(Number.MAX_SAFE_INTEGER);
30
+ const DECIMAL_INT_RE = /^[+-]?\d+$/;
31
+
32
+ function integerError(type, v) {
33
+ return createError('ER_TRUNCATED_WRONG_VALUE', type, v);
34
+ }
35
+
36
+ /**
37
+ * 把已经确认是十进制整数的字符串/BigInt 归一成 number(安全范围内)或 BigInt。
38
+ * 超出 int64 抛 1292,避免持久化时被 BigInt64 写入静默回绕。
39
+ */
40
+ function normalizeInteger(v, type) {
41
+ let b;
42
+ if (typeof v === 'bigint') b = v;
43
+ else {
44
+ try { b = BigInt(v); } catch (_) { throw integerError(type, v); }
45
+ }
46
+ if (b < INT64_MIN || b > INT64_MAX) throw integerError(type, b.toString());
47
+ if (b >= SAFE_INT_MIN && b <= SAFE_INT_MAX) return Number(b);
48
+ return b;
49
+ }
50
+
12
51
  /**
13
52
  * 会被 _compileMatcher 编译成快路径的操作符。
14
53
  *
@@ -318,10 +357,14 @@ class Table {
318
357
  uniqueTracker[field].add(data[field]);
319
358
  }
320
359
  if (def.length && typeof data[field] === 'string' && data[field].length > def.length) throw createError('ER_DATA_TOO_LONG', field);
321
- if (def.type === 'integer' || def.type === 'number') {
322
- const v = def.type === 'integer' ? Number(data[field]) : data[field];
323
- if (data[field] !== undefined && data[field] !== null && def.min !== undefined && v < def.min) throw createError('ER_OUT_OF_RANGE', field);
324
- if (data[field] !== undefined && data[field] !== null && def.max !== undefined && v > def.max) throw createError('ER_OUT_OF_RANGE', field);
360
+ if (def.type === 'integer' || def.type === 'int' || def.type === 'bigint' || def.type === 'number') {
361
+ const raw = data[field];
362
+ // BigInt 与 number 的 </> 比较是精确的,不要先 Number() 丢精度
363
+ const v = typeof raw === 'bigint' ? raw : (def.type === 'number' ? raw : Number(raw));
364
+ const min = typeof raw === 'bigint' && Number.isInteger(def.min) ? BigInt(def.min) : def.min;
365
+ const max = typeof raw === 'bigint' && Number.isInteger(def.max) ? BigInt(def.max) : def.max;
366
+ if (raw !== undefined && raw !== null && def.min !== undefined && v < min) throw createError('ER_OUT_OF_RANGE', field);
367
+ if (raw !== undefined && raw !== null && def.max !== undefined && v > max) throw createError('ER_OUT_OF_RANGE', field);
325
368
  }
326
369
  if (def.check && data[field] !== undefined && !def.check(data[field], data)) throw createError('ER_CHECK_CONSTRAINT', this._name + '.' + field);
327
370
  }
@@ -1430,10 +1473,14 @@ class Table {
1430
1473
  }
1431
1474
 
1432
1475
  // 数值范围
1433
- if (def.type === 'integer' && data[field] !== undefined && data[field] !== null) {
1434
- const v = Number(data[field]);
1435
- if (def.min !== undefined && v < def.min) throw createError('ER_OUT_OF_RANGE', field);
1436
- if (def.max !== undefined && v > def.max) throw createError('ER_OUT_OF_RANGE', field);
1476
+ if ((def.type === 'integer' || def.type === 'int' || def.type === 'bigint')
1477
+ && data[field] !== undefined && data[field] !== null) {
1478
+ const raw = data[field];
1479
+ const v = typeof raw === 'bigint' ? raw : Number(raw);
1480
+ const min = typeof raw === 'bigint' && Number.isInteger(def.min) ? BigInt(def.min) : def.min;
1481
+ const max = typeof raw === 'bigint' && Number.isInteger(def.max) ? BigInt(def.max) : def.max;
1482
+ if (def.min !== undefined && v < min) throw createError('ER_OUT_OF_RANGE', field);
1483
+ if (def.max !== undefined && v > max) throw createError('ER_OUT_OF_RANGE', field);
1437
1484
  }
1438
1485
  }
1439
1486
 
@@ -1537,14 +1584,20 @@ class Table {
1537
1584
  if (v === undefined || v === null) continue;
1538
1585
  const t = String(def.type || '').toLowerCase();
1539
1586
  if (t === 'integer' || t === 'int' || t === 'bigint') {
1540
- if (typeof v === 'boolean') throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
1541
- if (typeof v === 'number') {
1542
- if (Number.isInteger(v)) result[field] = v;
1543
- else throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
1544
- } else if (typeof v === 'string' && v.trim() !== '' && Number.isInteger(Number(v))) {
1545
- result[field] = Number(v);
1587
+ if (typeof v === 'boolean') throw integerError(t, v);
1588
+ if (typeof v === 'bigint') {
1589
+ // 显式 BigInt:>2^53 也能精确保存(Issue #38)。
1590
+ result[field] = normalizeInteger(v, t);
1591
+ } else if (typeof v === 'number') {
1592
+ if (!Number.isInteger(v)) throw integerError(t, v);
1593
+ // 超过 2^53-1 的 number 在调用方就已经丢失精度,不能再假装
1594
+ // 存进去了:明确报错,要求传 BigInt 或十进制字符串。
1595
+ if (!Number.isSafeInteger(v)) throw integerError(t, v);
1596
+ result[field] = v;
1597
+ } else if (typeof v === 'string' && DECIMAL_INT_RE.test(v.trim())) {
1598
+ result[field] = normalizeInteger(v.trim(), t);
1546
1599
  } else {
1547
- throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
1600
+ throw integerError(t, v);
1548
1601
  }
1549
1602
  } else if (t === 'float' || t === 'double' || t === 'number' || t === 'real') {
1550
1603
  if (typeof v === 'boolean') throw createError('ER_TRUNCATED_WRONG_VALUE', t, v);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.5.22",
3
+ "version": "6.5.23",
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",
@@ -50,12 +50,12 @@
50
50
  "scripts": {
51
51
  "postinstall": "node postinstall.js",
52
52
  "build:native": "node scripts/build-native.js",
53
- "test": "node test/smoke.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js && node test/star-nag.test.js && node test/plugin-xql.test.js && node test/bug-regression-2.test.js && node test/issue-34-35-36.test.js && node test/issue-g4.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
53
+ "test": "node test/smoke.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js && node test/star-nag.test.js && node test/plugin-xql.test.js && node test/bug-regression-2.test.js && node test/issue-34-35-36.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
54
54
  "test:parser": "node test/sql-parser.test.js",
55
55
  "test:btree": "node test/btree.test.js",
56
56
  "test:native": "node test/native.test.js",
57
57
  "test:regress": "node test/regress-5.1.0.js",
58
- "test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js && node test/issue-34-35-36.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
58
+ "test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/issue-34-35-36.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
59
59
  "test:coverage": "c8 --all --include='lib/**' --exclude='lib/web_ui.js' --reporter=text --reporter=lcov node test/coverage.js",
60
60
  "test:orms": "node examples/orms/run-all.js",
61
61
  "test:dml": "node test/dml-constraints.test.js",
@@ -0,0 +1,279 @@
1
+ /*
2
+ * Regression tests for issue #38.
3
+ * node test/issue-38.test.js
4
+ *
5
+ * #38 INTEGER / INT / BIGINT 列无法保存 > 2^53 的整数:
6
+ * · SQL 路径抛 ER_TRUNCATED_WRONG_VALUE (1292)
7
+ * · 编程式 API 静默截断成 53 位浮点(最危险)
8
+ *
9
+ * 修复后的约定:
10
+ * · BigInt / 十进制字符串 → 精确入库,安全范围内仍以 number 返回;
11
+ * · 超出 2^53 的值以 BigInt 保存,持久化格式是 8 字节有符号整数;
12
+ * · 调用方直接传「已经丢精度」的 unsafe number → 明确报 1292,不静默截断;
13
+ * · SQL 与 API 对越界值给出一致的 1292。
14
+ */
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const ROOT = path.join(__dirname, '..');
19
+
20
+ let passed = 0, failed = 0;
21
+ function ok(name, cond, extra) {
22
+ if (cond) { passed++; console.log('[OK]', name); }
23
+ else { failed++; console.log('[FAIL]', name, extra !== undefined ? JSON.stringify(extra, (k, v) => typeof v === 'bigint' ? v.toString() + 'n' : v) : ''); }
24
+ }
25
+
26
+ // 所有测试共用独立临时目录,结束后清理
27
+ const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-i38-'));
28
+ process.on('exit', () => { try { fs.rmSync(TMP, { recursive: true, force: true }); } catch (_) { } });
29
+
30
+ function tmpPath(name) { return path.join(TMP, name); }
31
+ function fresh(name, opts) {
32
+ const p = tmpPath(name);
33
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch (_) { }
34
+ return p;
35
+ }
36
+
37
+ function codeOf(e) { return e && (e.code !== undefined ? e.code : (e.details && e.details.code)); }
38
+
39
+ async function makeDb() {
40
+ const Database = require(path.join(ROOT, 'lib', 'database'));
41
+ const db = new Database(':memory:');
42
+ await db.createTable('big', {
43
+ id: { type: 'integer', primaryKey: true, autoIncrement: true },
44
+ v: { type: 'bigint' }
45
+ });
46
+ return db;
47
+ }
48
+
49
+ /* ---------- 编程式 API 路径 ---------- */
50
+ async function testApi() {
51
+ const db = await makeDb();
52
+ const t = db.big;
53
+
54
+ // 1. BigInt 精确保存
55
+ const r1 = t.insert({ v: 9223372036854775807n });
56
+ ok('#38 API BigInt int64 max stored exactly', r1.v === 9223372036854775807n, r1);
57
+
58
+ // 2. 十进制字符串精确保存(>2^53)
59
+ const r2 = t.insert({ v: '9007199254740993' });
60
+ ok('#38 API numeric string >2^53 stored exactly', r2.v === 9007199254740993n, r2);
61
+ ok('#38 API numeric string is not rounded to 9007199254740992', r2.v !== 9007199254740992, r2.v);
62
+
63
+ // 3. unsafe number 明确报错,不静默截断
64
+ let threw = null;
65
+ try { t.insert({ v: 9007199254740993 }); } catch (e) { threw = e; }
66
+ ok('#38 API unsafe number throws 1292', threw && codeOf(threw) === 1292, threw && threw.message);
67
+ ok('#38 API unsafe number does not insert a rounded row', t.count() === 2, t.count());
68
+
69
+ // 4. 超出 int64 报 1292(持久化是 8 字节整数,不能回绕)
70
+ threw = null;
71
+ try { t.insert({ v: '9223372036854775808' }); } catch (e) { threw = e; }
72
+ ok('#38 API string beyond int64 throws 1292', threw && codeOf(threw) === 1292, threw && threw.message);
73
+
74
+ // 5. 安全范围内的 number 保持旧行为(number,不是 BigInt)
75
+ const r3 = t.insert({ v: 42 });
76
+ ok('#38 API safe number stays number', typeof r3.v === 'number' && r3.v === 42, typeof r3.v);
77
+
78
+ // 6. 更新路径同样精确
79
+ t.update({ id: r2.id }, { v: '9223372036854775806' });
80
+ ok('#38 API update accepts decimal string exactly', t.findById(r2.id).v === 9223372036854775806n, t.findById(r2.id).v);
81
+
82
+ // 7. 过滤条件用 BigInt 可以命中(索引/快路径不受影响)
83
+ const hit = t.find({ v: 9223372036854775807n });
84
+ ok('#38 API filter with BigInt matches', hit.length === 1 && hit[0].id === r1.id, hit);
85
+
86
+ // 8. 非法值仍然报错
87
+ threw = null;
88
+ try { t.insert({ v: 1.5 }); } catch (e) { threw = e; }
89
+ ok('#38 API float into bigint column throws 1292', threw && codeOf(threw) === 1292);
90
+ threw = null;
91
+ try { t.insert({ v: true }); } catch (e) { threw = e; }
92
+ ok('#38 API boolean into bigint column throws 1292', threw && codeOf(threw) === 1292);
93
+
94
+ // 9. 负数边界
95
+ const r4 = t.insert({ v: -9223372036854775808n });
96
+ ok('#38 API int64 min stored exactly', r4.v === -9223372036854775808n, r4);
97
+ }
98
+
99
+ /* ---------- SQL 路径 ---------- */
100
+ async function testSql() {
101
+ const db = await makeDb();
102
+
103
+ // INSERT 十进制字面量(>2^53)不再报 1292
104
+ const ins = await db.query('INSERT INTO big (v) VALUES (9007199254740993), (9223372036854775807)');
105
+ ok('#38 SQL insert >2^53 literals succeed', ins.ok === true && ins.affectedRows === 2, ins);
106
+
107
+ // SELECT 输出为精确十进制字符串(BigInt 无法 JSON 序列化,输出层转字符串)
108
+ const sel = await db.query('SELECT v FROM big ORDER BY v');
109
+ ok('#38 SQL select keeps exact digits', sel.rows[0][0] === '9007199254740993', sel.rows);
110
+ ok('#38 SQL select int64 max exact', sel.rows[1][0] === '9223372036854775807', sel.rows);
111
+
112
+ // 底层行仍是 BigInt(不是字符串),与 API 一致
113
+ const raw = db.big.find({});
114
+ ok('#38 SQL insert stores BigInt in row', raw[0].v === 9007199254740993n, raw[0].v);
115
+
116
+ // WHERE = 字面量:下推过滤不能把 BigInt 转成 number
117
+ const eq = await db.query('SELECT v FROM big WHERE v = 9007199254740993');
118
+ ok('#38 SQL WHERE = large literal matches', eq.rows.length === 1 && eq.rows[0][0] === '9007199254740993', eq.rows);
119
+
120
+ // WHERE = 参数(参数内联成字符串,仍需精确比较)
121
+ const eqp = await db.query('SELECT v FROM big WHERE v = ?', [9007199254740993n]);
122
+ ok('#38 SQL WHERE = BigInt param matches', eqp.rows.length === 1 && eqp.rows[0][0] === '9007199254740993', eqp.rows);
123
+
124
+ // 算术保持精度
125
+ const plus = await db.query('SELECT v + 1 AS x FROM big WHERE v = 9007199254740993');
126
+ ok('#38 SQL v + 1 exact', plus.rows[0][0] === '9007199254740994', plus.rows);
127
+
128
+ // 16 进制字面量同样精确(0x20000000000001 = 9007199254740993)
129
+ await db.query('DELETE FROM big WHERE id > 0');
130
+ const hex = await db.query("INSERT INTO big (v) VALUES (0x20000000000001)");
131
+ ok('#38 SQL hex literal insert succeeds', hex.ok === true, hex);
132
+ const hexSel = await db.query('SELECT v FROM big');
133
+ ok('#38 SQL hex literal keeps precision', hexSel.rows[0][0] === '9007199254740993', hexSel.rows);
134
+ const hexQ = await db.query("INSERT INTO big (v) VALUES (X'7FFFFFFFFFFFFFFF')");
135
+ ok("#38 SQL X'' literal insert succeeds", hexQ.ok === true, hexQ);
136
+
137
+ // 越界:SQL 与 API 都报 1292
138
+ let sqlErr = null;
139
+ try { await db.query('INSERT INTO big (v) VALUES (9223372036854775808)'); } catch (e) { sqlErr = e; }
140
+ ok('#38 SQL beyond int64 throws 1292', sqlErr && codeOf(sqlErr) === 1292, sqlErr && sqlErr.message);
141
+
142
+ // SQL 与 API 对 unsafe number 的错误码一致
143
+ let apiErr = null;
144
+ try { db.big.insert({ v: 9223372036854775807 }); } catch (e) { apiErr = e; }
145
+ ok('#38 SQL and API error codes agree (1292)',
146
+ sqlErr && apiErr && codeOf(sqlErr) === codeOf(apiErr) && codeOf(apiErr) === 1292,
147
+ { sql: codeOf(sqlErr), api: codeOf(apiErr) });
148
+
149
+ // 小数/非整数字符串仍拒绝
150
+ let e2 = null;
151
+ try { await db.query("INSERT INTO big (v) VALUES ('1.5')"); } catch (e) { e2 = e; }
152
+ ok('#38 SQL non-integer string rejected', e2 && codeOf(e2) === 1292);
153
+ }
154
+
155
+ /* ---------- 持久化(.jsql / JSON / dir 模式) ---------- */
156
+ async function testPersistence() {
157
+ const Database = require(path.join(ROOT, 'lib', 'database'));
158
+
159
+ // .jsql 单文件(I64 编码)
160
+ {
161
+ const p = fresh('big.jsql');
162
+ let db = new Database(p);
163
+ await db.createTable('big', {
164
+ id: { type: 'integer', primaryKey: true, autoIncrement: true },
165
+ v: { type: 'bigint' }
166
+ });
167
+ db.big.insert({ v: 9223372036854775807n });
168
+ db.big.insert({ v: -9223372036854775808n });
169
+ db.save();
170
+
171
+ db = new Database(p);
172
+ const rows = db.big.find({}).sort((a, b) => (a.v < b.v ? -1 : 1));
173
+ ok('#38 .jsql persistence keeps int64 min/max',
174
+ rows.length === 2 && rows[0].v === -9223372036854775808n && rows[1].v === 9223372036854775807n, rows);
175
+ }
176
+
177
+ // 单文件 JSON(bigint_json 标记往返)
178
+ {
179
+ const p = fresh('big.json');
180
+ let db = new Database(p);
181
+ await db.createTable('big', {
182
+ id: { type: 'integer', primaryKey: true, autoIncrement: true },
183
+ v: { type: 'bigint' }
184
+ });
185
+ db.big.insert({ v: '9007199254740993' });
186
+ db.save();
187
+
188
+ db = new Database(p);
189
+ ok('#38 JSON persistence keeps >2^53 exactly', db.big.find({})[0].v === 9007199254740993n, db.big.find({}));
190
+ }
191
+
192
+ // dir 模式(disk)逐表 .jsql
193
+ {
194
+ const dir = fresh('big-dir');
195
+ fs.mkdirSync(dir, { recursive: true });
196
+ let db = new Database(dir, { mode: 'disk' });
197
+ await db.createTable('big', {
198
+ id: { type: 'integer', primaryKey: true, autoIncrement: true },
199
+ v: { type: 'bigint' }
200
+ });
201
+ db.big.insert({ v: 9007199254740993n });
202
+ db.save();
203
+
204
+ db = new Database(dir, { mode: 'disk' });
205
+ ok('#38 dir-mode persistence keeps >2^53 exactly', db.big.find({})[0].v === 9007199254740993n, db.big.find({}));
206
+ }
207
+ }
208
+
209
+ /* ---------- BigInt 与事务 / 统计 / 备份 ---------- */
210
+ async function testBigIntJsonPaths() {
211
+ const Database = require(path.join(ROOT, 'lib', 'database'));
212
+
213
+ // REPEATABLE_READ 事务快照(内部 JSON 深拷贝)不能因 BigInt 抛错
214
+ {
215
+ const db = await makeDb();
216
+ db.big.insert({ v: 9007199254740993n });
217
+ db.begin('REPEATABLE_READ');
218
+ db.big.insert({ v: 9223372036854775807n });
219
+ db.rollback();
220
+ const rows = db.big.find({});
221
+ ok('#38 tx snapshot/rollback with BigInt works', rows.length === 1 && rows[0].v === 9007199254740993n, rows);
222
+ }
223
+
224
+ // stats() 里的 JSON.stringify 不能因 BigInt 抛错
225
+ {
226
+ const db = await makeDb();
227
+ db.big.insert({ v: 9007199254740993n });
228
+ let threw = null, st = null;
229
+ try { st = db.stats(); } catch (e) { threw = e; }
230
+ ok('#38 stats() tolerates BigInt rows', !threw && st && st.tableStats && st.tableStats.big && st.tableStats.big.rowCount === 1 && st.tableStats.big.sizeEstimate > 0,
231
+ threw ? threw.message : st && st.tableStats);
232
+ }
233
+
234
+ // backup/restore 往返
235
+ {
236
+ const db = await makeDb();
237
+ db.big.insert({ v: '9007199254740993' });
238
+ const bak = tmpPath('backup.json');
239
+ db.backup(bak);
240
+ db.big.remove({});
241
+ db.restore(bak);
242
+ ok('#38 backup/restore keeps >2^53 exactly', db.big.find({})[0].v === 9007199254740993n, db.big.find({}));
243
+ }
244
+ }
245
+
246
+ /* ---------- 回归:普通类型不受影响 ---------- */
247
+ async function testNoRegression() {
248
+ const Database = require(path.join(ROOT, 'lib', 'database'));
249
+ const db = new Database(':memory:');
250
+ await db.createTable('t', {
251
+ id: { type: 'integer', primaryKey: true, autoIncrement: true },
252
+ n: { type: 'integer' },
253
+ f: { type: 'float' },
254
+ s: { type: 'string' }
255
+ });
256
+ db.t.insert({ n: 42, f: 1.5, s: 'hello' });
257
+ db.t.insert({ n: '43', f: '2.5', s: 7 });
258
+ const rows = db.t.find({});
259
+ ok('#38 regression integer/float/string coercion intact',
260
+ rows.length === 2 && rows[0].n === 42 && rows[1].n === 43 && rows[1].f === 2.5 && rows[1].s === '7', rows);
261
+ ok('#38 regression integer still rejects 1.5', (() => {
262
+ try { db.t.insert({ n: 1.5 }); return false; } catch (e) { return codeOf(e) === 1292; }
263
+ })());
264
+ }
265
+
266
+ (async () => {
267
+ try {
268
+ await testApi();
269
+ await testSql();
270
+ await testPersistence();
271
+ await testBigIntJsonPaths();
272
+ await testNoRegression();
273
+ } catch (e) {
274
+ failed++;
275
+ console.log('[FAIL] unexpected exception:', e && e.stack || e);
276
+ }
277
+ console.log(`\n${passed} passed, ${failed} failed`);
278
+ if (failed > 0) process.exit(1);
279
+ })();