jsql-neo 6.5.23 → 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 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.23** — 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.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
  ![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.23 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.24 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
@@ -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: 'string',
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] = JSON.parse(value); } catch (e) { out[field] = value; }
264
- } else if ((t === 'integer' || t === 'int' || t === 'bigint') && typeof value === 'string' && /^-?\d+$/.test(value)) {
265
- out[field] = Number(value);
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 (['integer', 'int', 'bigint'].includes(t)) return Number(id);
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.isFinite(n)) {
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, JSON.stringify(filter), 1, 0));
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.isFinite(n)) {
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, JSON.stringify(filter), 1, 0));
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, JSON.stringify(rows)));
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, JSON.stringify(entries)));
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.isFinite(Number(id)))) {
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 ? JSON.stringify(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, data]);
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, JSON.stringify({ [f]: nv }), 2, 0));
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}'`);
@@ -1,5 +1,7 @@
1
1
  const path = require('path');
2
2
  const { executeSQL } = require('./sql');
3
+ const { createError } = require('./errors');
4
+ const bigintJson = require('./bigint_json');
3
5
  const {
4
6
  createHookRegistry, applyPlugin, buildPluginContext, registerHook,
5
7
  runHooks, getPluginApis
@@ -77,6 +79,73 @@ function assertRowObject(row, index, isBatch) {
77
79
  throw new TypeError(`insert(): expected a row object${where}, got ${got}`);
78
80
  }
79
81
 
82
+ const INT64_MIN = -9223372036854775808n;
83
+ const INT64_MAX = 9223372036854775807n;
84
+ const SAFE_INT_MIN = BigInt(Number.MIN_SAFE_INTEGER);
85
+ const SAFE_INT_MAX = BigInt(Number.MAX_SAFE_INTEGER);
86
+ const DECIMAL_INT_RE = /^[+-]?\d+$/;
87
+
88
+ function isIntegerType(t) {
89
+ return t === 'integer' || t === 'int' || t === 'bigint';
90
+ }
91
+
92
+ /** 与 lib/table.js 的 normalizeInteger 一致(Issue #38)。 */
93
+ function normalizeInteger(v, type) {
94
+ let b;
95
+ if (typeof v === 'bigint') b = v;
96
+ else {
97
+ try { b = BigInt(String(v).trim()); } catch (_) { throw createError('ER_TRUNCATED_WRONG_VALUE', type, v); }
98
+ }
99
+ if (b < INT64_MIN || b > INT64_MAX) throw createError('ER_TRUNCATED_WRONG_VALUE', type, b.toString());
100
+ return (b >= SAFE_INT_MIN && b <= SAFE_INT_MAX) ? Number(b) : b;
101
+ }
102
+
103
+ /** Rust 绑定层把 >2^53 的整数以十进制字符串返回,这里还原为 BigInt。 */
104
+ function restoreInteger(v) {
105
+ if (typeof v === 'bigint') return v;
106
+ if (typeof v === 'string' && DECIMAL_INT_RE.test(v)) {
107
+ const b = BigInt(v);
108
+ return (b >= SAFE_INT_MIN && b <= SAFE_INT_MAX) ? Number(b) : b;
109
+ }
110
+ if (typeof v === 'number' && Number.isInteger(v) && !Number.isSafeInteger(v)) return BigInt(v);
111
+ return v;
112
+ }
113
+
114
+ /**
115
+ * 传给 Rust 的整数列值:BigInt / 十进制字符串 / 安全 number 转成精确值;
116
+ * 越界或小数/布尔直接抛 1292(与 JS 引擎一致)。
117
+ */
118
+ function normalizeIntegerInput(v, type) {
119
+ if (typeof v === 'bigint') return normalizeInteger(v, type);
120
+ if (typeof v === 'number') {
121
+ if (!Number.isInteger(v) || !Number.isSafeInteger(v)) throw createError('ER_TRUNCATED_WRONG_VALUE', type, v);
122
+ return v;
123
+ }
124
+ if (typeof v === 'string' && DECIMAL_INT_RE.test(v.trim())) return normalizeInteger(v.trim(), type);
125
+ throw createError('ER_TRUNCATED_WRONG_VALUE', type, v);
126
+ }
127
+
128
+ /** 按 schema 做插入/更新前的值归一(JSON 列序列化、整数列精确化)。 */
129
+ function normalizeRowForRust(table, schema, row) {
130
+ const out = {};
131
+ for (const [k, v] of Object.entries(row)) {
132
+ const def = schema[k];
133
+ let t = null;
134
+ if (typeof def === 'string') t = def.toLowerCase();
135
+ else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
136
+ if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
137
+ out[k] = safeStringify(v, table, k);
138
+ } else if (v !== null && v !== undefined && isIntegerType(t)) {
139
+ out[k] = normalizeIntegerInput(v, t);
140
+ } else if (v instanceof Date) {
141
+ out[k] = v.toISOString();
142
+ } else {
143
+ out[k] = v;
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+
80
149
  const NATIVE_TYPE_MAP = {
81
150
  text: 'string',
82
151
  varchar: 'string',
@@ -91,7 +160,7 @@ const NATIVE_TYPE_MAP = {
91
160
  array: 'string',
92
161
  object: 'string',
93
162
  bool: 'boolean',
94
- bigint: 'string',
163
+ bigint: 'bigint',
95
164
  int: 'integer',
96
165
  };
97
166
 
@@ -182,9 +251,9 @@ function restoreRow(row, schema) {
182
251
  if (typeof colDef === 'string') t = colDef.toLowerCase();
183
252
  else if (colDef && typeof colDef === 'object' && colDef.type) t = String(colDef.type).toLowerCase();
184
253
  if (['json', 'array', 'object'].includes(t) && typeof value === 'string') {
185
- try { out[field] = JSON.parse(value); } catch (e) { out[field] = value; }
186
- } else if ((t === 'integer' || t === 'int' || t === 'bigint') && typeof value === 'string' && /^-?\d+$/.test(value)) {
187
- out[field] = Number(value);
254
+ try { out[field] = bigintJson.parse(value); } catch (e) { out[field] = value; }
255
+ } else if (isIntegerType(t)) {
256
+ out[field] = restoreInteger(value);
188
257
  } else if ((t === 'float' || t === 'double' || t === 'number') && typeof value === 'string' && !Number.isNaN(Number(value))) {
189
258
  out[field] = Number(value);
190
259
  } else {
@@ -450,7 +519,9 @@ class JSQL {
450
519
  let t = null;
451
520
  if (typeof def === 'string') t = def.toLowerCase();
452
521
  else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
453
- if (['integer', 'int', 'bigint'].includes(t)) return Number(id);
522
+ if (isIntegerType(t)) {
523
+ try { return normalizeIntegerInput(id, t); } catch (_) { return id; }
524
+ }
454
525
  return id;
455
526
  }
456
527
 
@@ -462,7 +533,7 @@ class JSQL {
462
533
  if (Number.isFinite(nid) && nid < 0) throw new Error('id must be a non-negative number');
463
534
  const filter = {};
464
535
  filter[pk] = this._coercePkId(table, id);
465
- const r = safeJsonParse(wasmBindings.jsql_find(table, JSON.stringify(filter), 1, 0));
536
+ const r = safeJsonParse(wasmBindings.jsql_find(table, bigintJson.stringify(filter), 1, 0));
466
537
  if (r && r.error) return r;
467
538
  return Array.isArray(r) && r.length > 0 ? r[0] : null;
468
539
  }
@@ -479,7 +550,7 @@ class JSQL {
479
550
  if (pk) {
480
551
  const filter = {};
481
552
  filter[pk] = this._coercePkId(table, id);
482
- const r = safeJsonParse(wasmBindings.jsql_find(table, JSON.stringify(filter), 1, 0));
553
+ const r = safeJsonParse(wasmBindings.jsql_find(table, bigintJson.stringify(filter), 1, 0));
483
554
  if (Array.isArray(r) && r.length > 0) return r[0].id;
484
555
  return null;
485
556
  }
@@ -504,25 +575,9 @@ class JSQL {
504
575
  // schema-aware 转换:JSON 列用 safeStringify 给出可读错误(环形对象等)
505
576
  const schema = this._schemas[table];
506
577
  if (schema) {
507
- rows = rows.map(row => {
508
- const out = {};
509
- for (const [k, v] of Object.entries(row)) {
510
- const def = schema[k];
511
- let t = null;
512
- if (typeof def === 'string') t = def.toLowerCase();
513
- else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
514
- if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
515
- out[k] = safeStringify(v, table, k);
516
- } else if (v instanceof Date) {
517
- out[k] = v.toISOString();
518
- } else {
519
- out[k] = v;
520
- }
521
- }
522
- return out;
523
- });
578
+ rows = rows.map(row => normalizeRowForRust(table, schema, row));
524
579
  }
525
- const rawJson = JSON.stringify(rows);
580
+ const rawJson = bigintJson.stringify(rows);
526
581
  const pk = this._pkOf(table);
527
582
  const isAutoPk = this._pkAutoIncrement(table);
528
583
  // #36:旧 WASM 二进制对自增主键表按「保留区间 [next_id, next_id+n)」批量分配 id,
@@ -547,7 +602,7 @@ class JSQL {
547
602
  throw new Error(`ER_DUP_ENTRY: Duplicate entry '${explicitPk}' for key '${pk}'`);
548
603
  }
549
604
  }
550
- const r = safeJsonParse(wasmBindings.jsql_insert_json(table, JSON.stringify([row])));
605
+ const r = safeJsonParse(wasmBindings.jsql_insert_json(table, bigintJson.stringify([row])));
551
606
  if (r && r.error) throw new Error(r.error);
552
607
  ids.push(explicitPk !== null ? explicitPk : (Array.isArray(r) ? r[0] : undefined));
553
608
  }
@@ -699,7 +754,7 @@ class JSQL {
699
754
  async find(table, filter, opts = {}) {
700
755
  await this._flush();
701
756
  if (!(await this._runHooks('beforeFind', [table, { filter, opts }]))) return [];
702
- const filterStr = filter ? JSON.stringify(filter) : '';
757
+ const filterStr = filter ? bigintJson.stringify(filter) : '';
703
758
  const { limit = 100, offset = 0 } = opts;
704
759
  // WASM 端 jsql_find 的 limit/offset 是 usize(无符号),负数会经 wasm-bindgen
705
760
  // 静默回绕成巨大值,导致空结果或溢出。#35:绑定层先校验(同 native_client.js)。
@@ -723,14 +778,15 @@ class JSQL {
723
778
  }
724
779
 
725
780
  async updateByIds(table, entries) {
781
+ const schema = this._schemas[table];
726
782
  const pairs = [];
727
783
  for (const [id, data] of entries) {
728
784
  const seq = this._seqIdByPk(table, id);
729
- if (seq !== null) pairs.push([seq, data]);
785
+ if (seq !== null) pairs.push([seq, schema ? normalizeRowForRust(table, schema, data) : data]);
730
786
  }
731
787
  if (!(await this._runHooks('beforeUpdate', [table, pairs]))) return;
732
788
  await this._flush();
733
- const r = safeJsonParse(wasmBindings.jsql_update_by_ids(table, JSON.stringify(pairs)));
789
+ const r = safeJsonParse(wasmBindings.jsql_update_by_ids(table, bigintJson.stringify(pairs)));
734
790
  if (r && r.error) throw new Error(r.error);
735
791
  this._emit('update', { table, entries: pairs, result: r });
736
792
  await this._runHooks('afterUpdate', [table, pairs, r]);
@@ -779,7 +835,9 @@ class JSQL {
779
835
  // 静默产生两行同主键 —— 主键索引只留一个,索引与实际行数组不一致。
780
836
  // MySQL / SQLite 都会报 UNIQUE constraint failed。
781
837
  await this._assertNoKeyConflict(table, id, data, seq);
782
- const r = safeJsonParse(wasmBindings.jsql_update_by_id(table, BigInt(seq), JSON.stringify(data)));
838
+ const schema = this._schemas[table];
839
+ const payload = schema ? normalizeRowForRust(table, schema, data) : data;
840
+ const r = safeJsonParse(wasmBindings.jsql_update_by_id(table, BigInt(seq), bigintJson.stringify(payload)));
783
841
  if (r && r.ok === false) throw new Error(r.error || 'update failed');
784
842
  this._emit('update', { table, id, data });
785
843
  await this._runHooks('afterUpdate', [table, id, data, r]);
Binary file
@@ -109,7 +109,7 @@ impl Engine for MemoryEngine {
109
109
 
110
110
  fn update_by_id(&mut self, table: &str, id: u64, data: HashMap<String, serde_json::Value>) -> Result<bool, String> {
111
111
  let t = self.tables.get_mut(table).ok_or_else(|| format!("table '{}' not found", table))?;
112
- Ok(t.update_row(id, data))
112
+ t.update_row(id, data)
113
113
  }
114
114
 
115
115
  fn remove_by_id(&mut self, table: &str, id: u64) -> Result<bool, String> {
@@ -124,7 +124,7 @@ impl Engine for MemoryEngine {
124
124
 
125
125
  fn update_by_ids(&mut self, table: &str, batch: Vec<(u64, HashMap<String, serde_json::Value>)>) -> Result<usize, String> {
126
126
  let t = self.tables.get_mut(table).ok_or_else(|| format!("table '{}' not found", table))?;
127
- Ok(t.update_rows(batch))
127
+ t.update_rows(batch)
128
128
  }
129
129
 
130
130
  fn update_by_filter(&mut self, table: &str, filter: &Option<HashMap<String, serde_json::Value>>,
@@ -147,7 +147,7 @@ impl Engine for MemoryEngine {
147
147
  };
148
148
  let count = ids.len();
149
149
  for id in &ids {
150
- t.update_row_ref(*id, &data);
150
+ t.update_row_ref(*id, &data)?;
151
151
  }
152
152
  Ok(count)
153
153
  }