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 +1 -1
- package/index.js +1 -1
- package/lib/bigint_json.js +39 -0
- package/lib/database.js +14 -13
- package/lib/jsql_format.js +11 -1
- package/lib/sql.js +58 -6
- package/lib/table.js +68 -15
- package/package.json +3 -3
- package/test/issue-38.test.js +279 -0
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.
|
|
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
|

|
|
9
9
|

|
package/index.js
CHANGED
|
@@ -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 =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
?
|
|
2238
|
-
:
|
|
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 =
|
|
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
|
-
?
|
|
2300
|
-
:
|
|
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 =
|
|
2332
|
+
const data = bigintJson.parse(json);
|
|
2332
2333
|
|
|
2333
2334
|
if (data.__schema__) {
|
|
2334
2335
|
for (const [name, schema] of Object.entries(data.__schema__)) {
|
package/lib/jsql_format.js
CHANGED
|
@@ -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:
|
|
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 =
|
|
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 =
|
|
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
|
|
323
|
-
|
|
324
|
-
|
|
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'
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
|
1541
|
-
if (typeof v === '
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
} else if (typeof v === '
|
|
1545
|
-
|
|
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
|
|
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.
|
|
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-
|
|
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
|
+
})();
|