jsql-neo 6.5.22 → 6.5.24
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/native_client.js +91 -34
- package/lib/sql.js +58 -6
- package/lib/table.js +68 -15
- package/lib/wasm_client.js +88 -30
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +3 -3
- package/nativesrc/jsql-neo-core/src/engine/table.rs +263 -42
- package/nativesrc/jsql-neo-core/src/types.rs +2 -0
- package/package.json +3 -3
- package/test/issue-38.test.js +335 -0
- package/wasm/browser.d.ts +34 -22
- package/wasm/browser.mjs +101 -29
- package/wasm/jsql_neo_wasm_bg.wasm +0 -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.24** — 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/native_client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const { executeSQL } = require('./sql');
|
|
4
|
-
const { JSQL_Error, ErrorCodes } = require('./errors');
|
|
4
|
+
const { JSQL_Error, ErrorCodes, createError } = require('./errors');
|
|
5
|
+
const bigintJson = require('./bigint_json');
|
|
5
6
|
const {
|
|
6
7
|
createHookRegistry, applyPlugin, buildPluginContext, registerHook,
|
|
7
8
|
runHooks, getPluginApis
|
|
@@ -155,6 +156,69 @@ function assertRowObject(row, index, isBatch) {
|
|
|
155
156
|
throw new TypeError(`insert(): expected a row object${where}, got ${got}`);
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
const INT64_MIN = -9223372036854775808n;
|
|
160
|
+
const INT64_MAX = 9223372036854775807n;
|
|
161
|
+
const SAFE_INT_MIN = BigInt(Number.MIN_SAFE_INTEGER);
|
|
162
|
+
const SAFE_INT_MAX = BigInt(Number.MAX_SAFE_INTEGER);
|
|
163
|
+
const DECIMAL_INT_RE = /^[+-]?\d+$/;
|
|
164
|
+
|
|
165
|
+
function isIntegerType(t) {
|
|
166
|
+
return t === 'integer' || t === 'int' || t === 'bigint';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 与 lib/table.js 的 normalizeInteger 一致(Issue #38)。 */
|
|
170
|
+
function normalizeInteger(v, type) {
|
|
171
|
+
let b;
|
|
172
|
+
if (typeof v === 'bigint') b = v;
|
|
173
|
+
else {
|
|
174
|
+
try { b = BigInt(String(v).trim()); } catch (_) { throw createError('ER_TRUNCATED_WRONG_VALUE', type, v); }
|
|
175
|
+
}
|
|
176
|
+
if (b < INT64_MIN || b > INT64_MAX) throw createError('ER_TRUNCATED_WRONG_VALUE', type, b.toString());
|
|
177
|
+
return (b >= SAFE_INT_MIN && b <= SAFE_INT_MAX) ? Number(b) : b;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Rust 绑定层把 >2^53 的整数以十进制字符串返回,这里还原为 BigInt。 */
|
|
181
|
+
function restoreInteger(v) {
|
|
182
|
+
if (typeof v === 'bigint') return v;
|
|
183
|
+
if (typeof v === 'string' && DECIMAL_INT_RE.test(v)) {
|
|
184
|
+
const b = BigInt(v);
|
|
185
|
+
return (b >= SAFE_INT_MIN && b <= SAFE_INT_MAX) ? Number(b) : b;
|
|
186
|
+
}
|
|
187
|
+
if (typeof v === 'number' && Number.isInteger(v) && !Number.isSafeInteger(v)) return BigInt(v);
|
|
188
|
+
return v;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeIntegerInput(v, type) {
|
|
192
|
+
if (typeof v === 'bigint') return normalizeInteger(v, type);
|
|
193
|
+
if (typeof v === 'number') {
|
|
194
|
+
if (!Number.isInteger(v) || !Number.isSafeInteger(v)) throw createError('ER_TRUNCATED_WRONG_VALUE', type, v);
|
|
195
|
+
return v;
|
|
196
|
+
}
|
|
197
|
+
if (typeof v === 'string' && DECIMAL_INT_RE.test(v.trim())) return normalizeInteger(v.trim(), type);
|
|
198
|
+
throw createError('ER_TRUNCATED_WRONG_VALUE', type, v);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 按 schema 做插入/更新前的值归一(JSON 列序列化、整数列精确化)。 */
|
|
202
|
+
function normalizeRowForRust(table, schema, row) {
|
|
203
|
+
const out = {};
|
|
204
|
+
for (const [k, v] of Object.entries(row)) {
|
|
205
|
+
const def = schema[k];
|
|
206
|
+
let t = null;
|
|
207
|
+
if (typeof def === 'string') t = def.toLowerCase();
|
|
208
|
+
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
209
|
+
if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
|
|
210
|
+
out[k] = safeStringify(v, table, k);
|
|
211
|
+
} else if (v !== null && v !== undefined && isIntegerType(t)) {
|
|
212
|
+
out[k] = normalizeIntegerInput(v, t);
|
|
213
|
+
} else if (v instanceof Date) {
|
|
214
|
+
out[k] = v.toISOString();
|
|
215
|
+
} else {
|
|
216
|
+
out[k] = v;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
158
222
|
const NATIVE_TYPE_MAP = {
|
|
159
223
|
text: 'string',
|
|
160
224
|
varchar: 'string',
|
|
@@ -169,7 +233,7 @@ const NATIVE_TYPE_MAP = {
|
|
|
169
233
|
array: 'string',
|
|
170
234
|
object: 'string',
|
|
171
235
|
bool: 'boolean',
|
|
172
|
-
bigint: '
|
|
236
|
+
bigint: 'bigint',
|
|
173
237
|
int: 'integer',
|
|
174
238
|
};
|
|
175
239
|
|
|
@@ -260,9 +324,9 @@ function restoreRow(row, schema) {
|
|
|
260
324
|
if (typeof colDef === 'string') t = colDef.toLowerCase();
|
|
261
325
|
else if (colDef && typeof colDef === 'object' && colDef.type) t = String(colDef.type).toLowerCase();
|
|
262
326
|
if (['json', 'array', 'object'].includes(t) && typeof value === 'string') {
|
|
263
|
-
try { out[field] =
|
|
264
|
-
} else if ((t
|
|
265
|
-
out[field] =
|
|
327
|
+
try { out[field] = bigintJson.parse(value); } catch (e) { out[field] = value; }
|
|
328
|
+
} else if (isIntegerType(t)) {
|
|
329
|
+
out[field] = restoreInteger(value);
|
|
266
330
|
} else if ((t === 'float' || t === 'double' || t === 'number') && typeof value === 'string' && !Number.isNaN(Number(value))) {
|
|
267
331
|
out[field] = Number(value);
|
|
268
332
|
} else {
|
|
@@ -274,6 +338,7 @@ function restoreRow(row, schema) {
|
|
|
274
338
|
|
|
275
339
|
function _encodedFieldSize(v) {
|
|
276
340
|
if (v === null || v === undefined) return 1;
|
|
341
|
+
if (typeof v === 'bigint') return 1 + 8;
|
|
277
342
|
if (typeof v === 'number') {
|
|
278
343
|
if (Number.isInteger(v)) return (v >= -2147483648 && v <= 2147483647) ? 1 + 4 : 1 + 8;
|
|
279
344
|
return 1 + 8;
|
|
@@ -287,6 +352,9 @@ function encodeField(buf, off, v) {
|
|
|
287
352
|
if (v === null || v === undefined) {
|
|
288
353
|
buf.writeUInt8(0, off); return off + 1;
|
|
289
354
|
}
|
|
355
|
+
if (typeof v === 'bigint') {
|
|
356
|
+
buf.writeUInt8(INT64_TAG, off); buf.writeBigInt64LE(v, off + 1); return off + 9;
|
|
357
|
+
}
|
|
290
358
|
if (typeof v === 'number') {
|
|
291
359
|
if (Number.isInteger(v)) {
|
|
292
360
|
if (v >= -2147483648 && v <= 2147483647) {
|
|
@@ -554,7 +622,9 @@ class JSQL {
|
|
|
554
622
|
let t = null;
|
|
555
623
|
if (typeof def === 'string') t = def.trim().split(/\s+/)[0].toLowerCase();
|
|
556
624
|
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
557
|
-
if (
|
|
625
|
+
if (isIntegerType(t)) {
|
|
626
|
+
try { return normalizeIntegerInput(id, t); } catch (_) { return id; }
|
|
627
|
+
}
|
|
558
628
|
return id;
|
|
559
629
|
}
|
|
560
630
|
|
|
@@ -578,7 +648,7 @@ class JSQL {
|
|
|
578
648
|
if (pk) {
|
|
579
649
|
if (this._isAutoIntPk(table)) {
|
|
580
650
|
const n = toNonNeg('id', id, NaN);
|
|
581
|
-
if (Number.
|
|
651
|
+
if (typeof id !== 'bigint' && Number.isSafeInteger(n)) {
|
|
582
652
|
// 负 id 会被原生层 `i64 -> u64` 静默回绕成 usize::MAX,导致
|
|
583
653
|
// findById 误报 not found(行明明存在却取不到)。在此显式拒绝。
|
|
584
654
|
if (n < 0) throw new Error('id must be a non-negative number');
|
|
@@ -589,7 +659,7 @@ class JSQL {
|
|
|
589
659
|
}
|
|
590
660
|
const filter = {};
|
|
591
661
|
filter[pk] = this._coercePkId(table, id);
|
|
592
|
-
const r = safeParse(this._n('jsqlFind', table,
|
|
662
|
+
const r = safeParse(this._n('jsqlFind', table, bigintJson.stringify(filter), 1, 0));
|
|
593
663
|
if (r && r.error) return r;
|
|
594
664
|
return Array.isArray(r) && r.length > 0 ? r[0] : null;
|
|
595
665
|
}
|
|
@@ -601,7 +671,7 @@ class JSQL {
|
|
|
601
671
|
if (pk) {
|
|
602
672
|
if (this._isAutoIntPk(table)) {
|
|
603
673
|
const n = toNonNeg('id', id, NaN);
|
|
604
|
-
if (Number.
|
|
674
|
+
if (typeof id !== 'bigint' && Number.isSafeInteger(n)) {
|
|
605
675
|
// 负 id 在原生层回绕为 usize::MAX 后静默 not found,update/remove
|
|
606
676
|
// 会无提示地什么都不做 —— 这里显式拒绝(见 issue #35)。
|
|
607
677
|
if (n < 0) throw new Error('id must be a non-negative number');
|
|
@@ -612,7 +682,7 @@ class JSQL {
|
|
|
612
682
|
}
|
|
613
683
|
const filter = {};
|
|
614
684
|
filter[pk] = this._coercePkId(table, id);
|
|
615
|
-
const r = safeParse(this._n('jsqlFind', table,
|
|
685
|
+
const r = safeParse(this._n('jsqlFind', table, bigintJson.stringify(filter), 1, 0));
|
|
616
686
|
if (Array.isArray(r) && r.length > 0) return r[0].id;
|
|
617
687
|
return null;
|
|
618
688
|
}
|
|
@@ -639,23 +709,7 @@ class JSQL {
|
|
|
639
709
|
const schema = this._schemas[table];
|
|
640
710
|
// 对 JSON/array/object 列保留 safeStringify 以给出可读错误(环形对象等)
|
|
641
711
|
if (schema) {
|
|
642
|
-
rows = rows.map(row =>
|
|
643
|
-
const out = {};
|
|
644
|
-
for (const [k, v] of Object.entries(row)) {
|
|
645
|
-
const def = schema[k];
|
|
646
|
-
let t = null;
|
|
647
|
-
if (typeof def === 'string') t = def.toLowerCase();
|
|
648
|
-
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
649
|
-
if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
|
|
650
|
-
out[k] = safeStringify(v, table, k);
|
|
651
|
-
} else if (v instanceof Date) {
|
|
652
|
-
out[k] = v.toISOString();
|
|
653
|
-
} else {
|
|
654
|
-
out[k] = v;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return out;
|
|
658
|
-
});
|
|
712
|
+
rows = rows.map(row => normalizeRowForRust(table, schema, row));
|
|
659
713
|
}
|
|
660
714
|
// 批量插入优先走二进制格式:整批只在 JS 侧 encode 一次,Rust 直接按
|
|
661
715
|
// tag 解析成 FieldValue,省掉「JS JSON.stringify → Rust serde 解析 →
|
|
@@ -666,7 +720,7 @@ class JSQL {
|
|
|
666
720
|
if (typeof native.jsqlInsertBuf === 'function' && _canEncodeBatch(rows)) {
|
|
667
721
|
r = safeParse(this._n('jsqlInsertBuf', table, encodeBatch(rows)));
|
|
668
722
|
} else if (typeof native.jsqlInsertRaw === 'function') {
|
|
669
|
-
r = safeParse(this._n('jsqlInsertRaw', table,
|
|
723
|
+
r = safeParse(this._n('jsqlInsertRaw', table, bigintJson.stringify(rows)));
|
|
670
724
|
} else {
|
|
671
725
|
r = safeParse(this._n('jsqlInsertBuf', table, encodeBatch(rows)));
|
|
672
726
|
}
|
|
@@ -704,7 +758,7 @@ class JSQL {
|
|
|
704
758
|
for (var table in update) {
|
|
705
759
|
var entries = update[table];
|
|
706
760
|
if (entries.length === 0) continue;
|
|
707
|
-
var r = safeParse(this._n('jsqlUpdateByIds', table,
|
|
761
|
+
var r = safeParse(this._n('jsqlUpdateByIds', table, bigintJson.stringify(entries)));
|
|
708
762
|
this._emit('update', { table, entries, result: r });
|
|
709
763
|
}
|
|
710
764
|
this._opBuffer = { remove: {}, update: {} };
|
|
@@ -844,7 +898,7 @@ class JSQL {
|
|
|
844
898
|
this._flushOpsNow();
|
|
845
899
|
if (!(await this._runHooks('beforeFind', [table, { ids }]))) return null;
|
|
846
900
|
let r;
|
|
847
|
-
if (this._isAutoIntPk(table) && ids.every(id => Number.
|
|
901
|
+
if (this._isAutoIntPk(table) && ids.every(id => typeof id !== 'bigint' && Number.isSafeInteger(Number(id)))) {
|
|
848
902
|
// 自增主键:内部 id 即主键值,一次批量调用即可,避免 N 次往返。
|
|
849
903
|
// #35:负 id 在原生层 `i64 -> u64` 回绕成 usize::MAX,静默 not found;
|
|
850
904
|
// NaN/Infinity 同样不允许。绑定层先校验,绝不把非法值传给原生模块。
|
|
@@ -869,7 +923,7 @@ class JSQL {
|
|
|
869
923
|
async find(table, filter, opts = {}) {
|
|
870
924
|
this._flushOpsNow();
|
|
871
925
|
if (!(await this._runHooks('beforeFind', [table, { filter, opts }]))) return [];
|
|
872
|
-
const filterStr = filter ?
|
|
926
|
+
const filterStr = filter ? bigintJson.stringify(filter) : '';
|
|
873
927
|
const { limit = 100, offset = 0 } = opts;
|
|
874
928
|
// #35:原生层把 limit/offset 按 `as usize` 无符号转换,负数会静默回绕
|
|
875
929
|
// (release 下空结果 / debug 下 usize::MAX+1 溢出 panic)。绑定层先校验,
|
|
@@ -894,10 +948,11 @@ class JSQL {
|
|
|
894
948
|
}
|
|
895
949
|
|
|
896
950
|
async updateByIds(table, entries) {
|
|
951
|
+
const schema = this._schemas[table];
|
|
897
952
|
const pairs = [];
|
|
898
953
|
for (const [id, data] of entries) {
|
|
899
954
|
const seq = this._seqIdByPk(table, id);
|
|
900
|
-
if (seq !== null) pairs.push([seq, data]);
|
|
955
|
+
if (seq !== null) pairs.push([seq, schema ? normalizeRowForRust(table, schema, data) : data]);
|
|
901
956
|
}
|
|
902
957
|
if (!(await this._runHooks('beforeUpdate', [table, pairs]))) return;
|
|
903
958
|
if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
|
|
@@ -922,8 +977,10 @@ class JSQL {
|
|
|
922
977
|
// 改主键 / UNIQUE 列前先查冲突(同步版,与 lib/database.js 的修复对齐)。
|
|
923
978
|
// 此前直接写入,导致 UPDATE t SET id = 1 WHERE id = 2 静默产生两行同主键。
|
|
924
979
|
this._assertNoKeyConflictSync(table, id, data);
|
|
980
|
+
const schema = this._schemas[table];
|
|
981
|
+
const payload = schema ? normalizeRowForRust(table, schema, data) : data;
|
|
925
982
|
if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
|
|
926
|
-
this._opBuffer.update[table].push([seq,
|
|
983
|
+
this._opBuffer.update[table].push([seq, payload]);
|
|
927
984
|
this._scheduleOpFlush();
|
|
928
985
|
}
|
|
929
986
|
|
|
@@ -953,7 +1010,7 @@ class JSQL {
|
|
|
953
1010
|
if (valOf(self, f) === nv) continue; // 值没变
|
|
954
1011
|
// 只需知道「是否还存在另一行占用了这个键值」:limit 2 让原生层命中即停,
|
|
955
1012
|
// 输出量恒定,不随表规模增长。
|
|
956
|
-
const probe = safeParse(this._n('jsqlFind', table,
|
|
1013
|
+
const probe = safeParse(this._n('jsqlFind', table, bigintJson.stringify({ [f]: nv }), 2, 0));
|
|
957
1014
|
if (!Array.isArray(probe)) continue;
|
|
958
1015
|
if (probe.some(r => String(idOf(r)) !== String(id))) {
|
|
959
1016
|
const e = new Error(`Duplicate entry '${nv}' for key '${f}'`);
|
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) {
|