jsql-neo 6.3.2 → 6.3.4

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/lib/database.js CHANGED
@@ -108,7 +108,10 @@ class Database {
108
108
  // WAL(Write-Ahead Logging)
109
109
  this._walEnabled = options.wal === true && !this._memoryMode;
110
110
  this._walPath = this._filePath ? this._filePath + '.wal' : null;
111
- this._walOps = []; // WAL 操作日志
111
+ this._walOps = []; // WAL 操作日志(内存镜像)
112
+ this._walFlushedCount = 0; // 已落盘记录数(append-only 增量追加用)
113
+ this._walReplaying = false; // 回放中:抑制再次写日志
114
+ this._walReplaySummary = null;
112
115
 
113
116
  // 事务隔离:默认 REPEATABLE_READ(begin 时保存行快照,rollback 可回滚)
114
117
  this._isolationLevel = options.isolationLevel || 'REPEATABLE_READ';
@@ -127,11 +130,21 @@ class Database {
127
130
  this._slowQueries = [];
128
131
 
129
132
  // 从文件加载
130
- if (!this._memoryMode && !this._dirMode && fs.existsSync(this._filePath)) {
131
- this._loading = true;
132
- this._acquireLock();
133
- this._load();
134
- this._loading = false;
133
+ // 注意:即使数据文件不存在也要走 _load(),因为可能存在「上次崩溃前
134
+ // 只写了 WAL、快照尚未落盘」的情况 —— 此时必须回放 WAL 才能恢复数据。
135
+ // 旧实现在这里用 fs.existsSync 直接短路,导致新建库后崩溃丢数据。
136
+ if (!this._memoryMode && !this._dirMode) {
137
+ const hasDataFile = this._filePath ? fs.existsSync(this._filePath) : false;
138
+ const hasWal = this._walEnabled && this._walPath && fs.existsSync(this._walPath);
139
+ if (hasDataFile || hasWal) {
140
+ this._loading = true;
141
+ if (hasDataFile) this._acquireLock();
142
+ try {
143
+ this._load();
144
+ } finally {
145
+ this._loading = false;
146
+ }
147
+ }
135
148
  }
136
149
 
137
150
  // 磁盘/混合模式: 目录存储(每表独立 JSql 文件,增量刷盘)
@@ -365,40 +378,62 @@ class Database {
365
378
 
366
379
  /**
367
380
  * 写入 WAL 日志
381
+ *
382
+ * 采用 append-only(每行一条 JSON)而非全量重写:
383
+ * 全量重写会让日志写入本身成为 O(n²),且中途崩溃会损坏整个日志文件。
384
+ * append-only 下崩溃最多丢失最后一行(已 fsync 过的行不会丢)。
368
385
  */
369
386
  _walWrite(op) {
370
387
  if (!this._walEnabled) return;
371
- this._walOps.push({
388
+ const entry = {
372
389
  ...op,
373
390
  timestamp: Date.now(),
374
391
  sequence: this._walOps.length + 1
375
- });
392
+ };
393
+ this._walOps.push(entry);
394
+ // 恢复过程中重放操作不能再次写 WAL,否则会无限增长
395
+ if (this._walReplaying) return;
376
396
  this._flushWAL();
377
397
  }
378
398
 
379
399
  /**
380
- * 将 WAL 刷入磁盘
400
+ * 将新增的 WAL 记录追加到磁盘(append-only)
381
401
  */
382
402
  _flushWAL() {
383
403
  if (!this._walEnabled || this._walOps.length === 0) return;
384
404
  try {
385
405
  const dir = path.dirname(this._walPath);
386
406
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
387
- fs.writeFileSync(this._walPath, JSON.stringify(this._walOps), 'utf8');
407
+ // 只追加尚未落盘的记录
408
+ const pending = this._walOps.slice(this._walFlushedCount);
409
+ if (pending.length === 0) return;
410
+ const lines = pending.map(o => JSON.stringify(o)).join('\n') + '\n';
411
+ const fd = fs.openSync(this._walPath, 'a');
412
+ try {
413
+ fs.writeSync(fd, lines, null, 'utf8');
414
+ // 崩溃安全性:确保数据真正落到磁盘而不仅在页缓存
415
+ try { fs.fsyncSync(fd); } catch (e) { /* 某些文件系统不支持 fsync */ }
416
+ } finally {
417
+ fs.closeSync(fd);
418
+ }
419
+ this._walFlushedCount = this._walOps.length;
388
420
  } catch (e) {
389
421
  // WAL 写入失败不应阻塞主流程
390
422
  }
391
423
  }
392
424
 
393
425
  /**
394
- * 检查点:将 WAL 应用到数据文件并清空 WAL
426
+ * 检查点:数据已由 save() 完整落盘,清空 WAL
427
+ *
428
+ * 只在 save() 成功写完快照之后调用。若快照写入失败则不检查点,
429
+ * 保证 WAL 里的记录仍可用于恢复。
395
430
  */
396
431
  _checkpoint() {
397
- if (!this._walEnabled || this._walOps.length === 0) return;
398
- // 所有操作已在内存中生效,此函数仅清除 WAL 文件
432
+ if (!this._walEnabled) return;
399
433
  this._walOps = [];
434
+ this._walFlushedCount = 0;
400
435
  try {
401
- if (fs.existsSync(this._walPath)) {
436
+ if (this._walPath && fs.existsSync(this._walPath)) {
402
437
  fs.unlinkSync(this._walPath);
403
438
  }
404
439
  } catch (e) {
@@ -407,23 +442,189 @@ class Database {
407
442
  }
408
443
 
409
444
  /**
410
- * 崩溃恢复:从 WAL 重放操作
445
+ * 崩溃恢复:重放 WAL 中尚未被快照覆盖的操作
446
+ *
447
+ * 历史问题(6.3.3 及以前):本函数直接 `fs.unlinkSync` 删除 WAL,
448
+ * 完全没有回放,导致「WAL / 崩溃恢复」是虚假宣传 —— 实测开启 wal:true
449
+ * 后 kill -9 丢数据,重开连表都不存在。现在真正回放。
450
+ *
451
+ * 日志格式为 append-only JSONL;最后一行可能在崩溃时写半,
452
+ * 因此解析时逐行 try/catch,坏行直接丢弃(它必然是未完成的那条)。
411
453
  */
412
454
  _recoverFromWAL() {
413
455
  if (!this._walEnabled || !this._walPath || !fs.existsSync(this._walPath)) return;
456
+
457
+ let raw;
414
458
  try {
415
- const walData = JSON.parse(fs.readFileSync(this._walPath, 'utf8'));
416
- if (!Array.isArray(walData) || walData.length === 0) return;
417
-
418
- // WAL 操作在 v2.0 中仅用于崩溃恢复标记
419
- // 实际操作已在 save() 时持久化,WAL 主要用于:
420
- // 1. 标记未完成的事务
421
- // 2. 在 save() 间隔中保护数据
422
- this._walOps = [];
423
- fs.unlinkSync(this._walPath);
459
+ raw = fs.readFileSync(this._walPath, 'utf8');
424
460
  } catch (e) {
425
- // WAL 损坏,忽略
426
- try { if (fs.existsSync(this._walPath)) fs.unlinkSync(this._walPath); } catch (_) {}
461
+ return;
462
+ }
463
+ if (!raw || !raw.trim()) {
464
+ this._checkpoint();
465
+ return;
466
+ }
467
+
468
+ // 兼容 6.3.3 及以前的旧格式:整个 WAL 是一个 JSON 数组(单行)。
469
+ // 6.3.4 起改为 append-only JSONL(每行一条)。此处按形状识别并归一化,
470
+ // 否则升级后用户遗留的旧 WAL 会被静默丢弃。
471
+ let ops = [];
472
+ const trimmed = raw.trim();
473
+ if (trimmed.startsWith('[')) {
474
+ try {
475
+ const parsed = JSON.parse(trimmed);
476
+ if (Array.isArray(parsed)) {
477
+ ops = parsed.filter(o => o && typeof o.op === 'string');
478
+ }
479
+ } catch (e) {
480
+ // 落到下面的 JSONL 解析
481
+ }
482
+ }
483
+ if (ops.length === 0) {
484
+ for (const line of raw.split('\n')) {
485
+ const t = line.trim();
486
+ if (!t) continue;
487
+ try {
488
+ const o = JSON.parse(t);
489
+ if (o && typeof o.op === 'string') ops.push(o);
490
+ } catch (e) {
491
+ // 崩溃时写半的尾行,丢弃
492
+ }
493
+ }
494
+ }
495
+ if (ops.length === 0) {
496
+ this._checkpoint();
497
+ return;
498
+ }
499
+
500
+ this._walReplaying = true;
501
+ let replayed = 0, failed = 0;
502
+ try {
503
+ // 逐条按 sequence 顺序回放,保证 insert→update→remove 的相对次序。
504
+ // insert 不走 _loadRows()(那是整体替换语义),而是直接 push 到 _rows,
505
+ // 再在末尾统一重建索引;否则逐条回放会互相覆盖。
506
+ const touched = new Set();
507
+ for (const op of ops) {
508
+ try {
509
+ switch (op.op) {
510
+ case 'insert': {
511
+ const t = op.table && this._tables[op.table];
512
+ if (!t || !Array.isArray(op.rows) || op.rows.length === 0) { failed++; break; }
513
+ t._rows.push(...op.rows);
514
+ touched.add(t);
515
+ replayed++;
516
+ break;
517
+ }
518
+ case 'update': {
519
+ const t = this._tables[op.table];
520
+ if (!t || !op.pk || op.row === undefined) { failed++; break; }
521
+ const cur = t._rows.find(r => r[op.pk] === op.id);
522
+ if (!cur) { failed++; break; }
523
+ Object.assign(cur, op.row);
524
+ touched.add(t);
525
+ replayed++;
526
+ break;
527
+ }
528
+ case 'remove': {
529
+ const t = this._tables[op.table];
530
+ if (!t || !op.pk) { failed++; break; }
531
+ const before = t._rows.length;
532
+ t._rows = t._rows.filter(r => r[op.pk] !== op.id);
533
+ if (t._rows.length === before) { failed++; break; }
534
+ touched.add(t);
535
+ replayed++;
536
+ break;
537
+ }
538
+ default:
539
+ if (this._replayWalOp(op)) replayed++;
540
+ else failed++;
541
+ }
542
+ } catch (e) {
543
+ failed++;
544
+ }
545
+ }
546
+ // 重建受影响表的索引与自增计数
547
+ for (const t of touched) {
548
+ if (typeof t._rebuildPKIndex === 'function') t._rebuildPKIndex();
549
+ if (typeof t._rebuildAllBTrees === 'function') t._rebuildAllBTrees();
550
+ for (const field of Object.keys(t._indexes || {})) t.createIndex(field);
551
+ if (t._autoIncrementField) {
552
+ for (const row of t._rows) {
553
+ const v = row[t._autoIncrementField];
554
+ if (typeof v === 'number' && v > t._autoIncrement) t._autoIncrement = v;
555
+ }
556
+ }
557
+ }
558
+ } finally {
559
+ this._walReplaying = false;
560
+ }
561
+
562
+ // 记录回放结果供 getWALInfo() / 排障使用,不打断启动流程
563
+ if (replayed > 0 || failed > 0) {
564
+ this._walReplaySummary = { replayed, failed, at: Date.now() };
565
+ }
566
+
567
+ // 回放后的状态已合并进内存。
568
+ // 必须立即落盘做检查点:否则 WAL 一清空,这批数据就只存在于内存,
569
+ // 下次崩溃又要「没有快照 + 没有 WAL」而彻底丢失。
570
+ // 这也是 WAL 的标准语义:recovery 之后必须 checkpoint。
571
+ if (replayed > 0) {
572
+ try {
573
+ this._walCheckpointPending = true;
574
+ this.save();
575
+ } catch (e) {
576
+ // 落盘失败:保留 WAL 以便下次启动重试
577
+ this._walOps = ops;
578
+ this._walFlushedCount = 0;
579
+ this._walCheckpointPending = false;
580
+ this._walReplaySummary = {
581
+ replayed, failed, at: Date.now(),
582
+ checkpointFailed: true, error: e.message
583
+ };
584
+ return;
585
+ }
586
+ }
587
+
588
+ // save() 内部已调用 _checkpoint(),此处无需重复。
589
+ }
590
+
591
+ /**
592
+ * 回放单条 WAL 记录
593
+ * @returns {boolean} 是否成功应用
594
+ */
595
+ _replayWalOp(op) {
596
+ switch (op.op) {
597
+ case 'createTable':
598
+ if (op.table && !this._tables[op.table]) {
599
+ this.createTable(op.table, op.schema || {});
600
+ }
601
+ return true;
602
+
603
+ case 'dropTable':
604
+ if (op.table && this._tables[op.table]) {
605
+ this.dropTable(op.table);
606
+ }
607
+ return true;
608
+
609
+ case 'insert':
610
+ case 'update':
611
+ case 'remove':
612
+ // 数据类操作由 _recoverFromWAL 按序直接处理(需维护表内行序),
613
+ // 不走这里。
614
+ return false;
615
+
616
+ // 事务标记:begin/commit/rollback/undo 本身不改数据状态,
617
+ // 记录它们是为了可观测性;回放时跳过。
618
+ case 'begin':
619
+ case 'commit':
620
+ case 'rollback':
621
+ case 'undo':
622
+ case 'restore':
623
+ case 'import':
624
+ return true;
625
+
626
+ default:
627
+ return false;
427
628
  }
428
629
  }
429
630
 
@@ -1191,10 +1392,17 @@ class Database {
1191
1392
  if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
1192
1393
  const pk = this._pkOf(table);
1193
1394
  const ids = [];
1395
+ const logged = [];
1194
1396
  for (const row of arr) {
1195
1397
  const r = table.insert(row);
1196
- ids.push(pk ? r[pk] : table._rows.length);
1197
- }
1398
+ const id = pk ? r[pk] : table._rows.length;
1399
+ ids.push(id);
1400
+ // 记录落库后的完整行(含自增主键),保证回放能还原真实数据
1401
+ if (pk) logged.push({ ...r });
1402
+ else logged.push(r);
1403
+ }
1404
+ // 有主键才可幂等回放;无主键表回放会重复插行,故不记录
1405
+ if (pk) this._walWrite({ op: 'insert', table: tableName, rows: logged });
1198
1406
  this._emit('insert', { table: tableName, count: arr.length, ids });
1199
1407
  this._runHooks('afterInsert', [tableName, arr, ids]);
1200
1408
  this._markDirty(tableName);
@@ -1292,6 +1500,8 @@ class Database {
1292
1500
  const row = this._resolveId(table, id);
1293
1501
  if (!row) return { ok: false, error: 'not found' };
1294
1502
  Object.assign(row, data);
1503
+ const pk = this._pkOf(table);
1504
+ if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...data } });
1295
1505
  this._emit('update', { table: tableName, id, data });
1296
1506
  this._runHooks('afterUpdate', [tableName, id, data, { ok: true }]);
1297
1507
  this._markDirty(tableName);
@@ -1303,10 +1513,15 @@ class Database {
1303
1513
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1304
1514
  const pairs = entries.map(([id, data]) => [id, data]);
1305
1515
  if (!this._runHooks('beforeUpdate', [tableName, pairs])) return;
1516
+ const pk = this._pkOf(table);
1306
1517
  let count = 0;
1307
1518
  for (const [id, data] of pairs) {
1308
1519
  const row = this._resolveId(table, id);
1309
- if (row) { Object.assign(row, data); count++; }
1520
+ if (row) {
1521
+ Object.assign(row, data);
1522
+ count++;
1523
+ if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...data } });
1524
+ }
1310
1525
  }
1311
1526
  this._emit('update', { table: tableName, entries: pairs, result: { ok: true, count } });
1312
1527
  this._runHooks('afterUpdate', [tableName, pairs, { ok: true, count }]);
@@ -1321,6 +1536,7 @@ class Database {
1321
1536
  if (table._primaryKey) {
1322
1537
  const removed = table.removeById(id);
1323
1538
  if (removed === 0) return { ok: false, error: 'not found' };
1539
+ this._walWrite({ op: 'remove', table: tableName, pk: table._primaryKey, id });
1324
1540
  this._emit('delete', { table: tableName, id });
1325
1541
  this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1326
1542
  this._markDirty(tableName);
@@ -1590,6 +1806,8 @@ class Database {
1590
1806
  this._tables[name]._loadRows(rows);
1591
1807
  }
1592
1808
  }
1809
+ // WAL 恢复(此前 .jsql 分支提前 return,导致崩溃后数据全丢)
1810
+ this._recoverFromWAL();
1593
1811
  return;
1594
1812
  }
1595
1813
 
@@ -264,12 +264,38 @@ class JSQL {
264
264
  // native 引擎:钩子内再次调用引擎方法会触发 N-API 重入崩溃,需显式防护
265
265
  this._forbidReentry = true;
266
266
  this._eventListeners = [];
267
+
268
+ // 每个 Database 实例分配独立的 native 引擎句柄。
269
+ // 旧版 native 层是进程级单例,导致 new Database(dirA) 后再 new Database(dirB)
270
+ // 会把 dirA 的表全部清空(见 HybridEngine::open 的 clear())。
271
+ // 6.3.4 起 native 层改为句柄注册表,同进程可并存多个独立数据库。
272
+ // 若加载到的是旧版预编译二进制(无 jsqlInstanceNew),则回退到单实例行为。
273
+ this._handle = null;
274
+ this._multiInstance = typeof native.jsqlInstanceNew === 'function';
275
+ if (this._multiInstance) {
276
+ this._handle = native.jsqlInstanceNew();
277
+ }
267
278
  if (this._autoPlugins) {
268
279
  const { ModuleManager } = require('./mod');
269
280
  new ModuleManager().applyTo(this);
270
281
  }
271
282
  }
272
283
 
284
+ /**
285
+ * 调用 native 引擎,自动带上本实例的句柄。
286
+ *
287
+ * 句柄版函数名 = 原名 + 'H'(jsqlFind -> jsqlFindH)。
288
+ * 加载到旧版二进制(无句柄 API)时自动回退到无句柄调用,
289
+ * 保证新版本 JS 能配旧二进制、不会因缺少导出而崩。
290
+ */
291
+ _n(name, ...args) {
292
+ if (this._multiInstance && this._handle !== null) {
293
+ const fn = native[name + 'H'];
294
+ if (typeof fn === 'function') return fn(this._handle, ...args);
295
+ }
296
+ return native[name](...args);
297
+ }
298
+
273
299
  use(plugin, opts) {
274
300
  return applyPlugin(this, plugin, opts);
275
301
  }
@@ -318,7 +344,7 @@ class JSQL {
318
344
  if (!this._path) throw new Error('hybrid/disk mode requires a directory path');
319
345
  let r;
320
346
  try {
321
- r = safeParse(native.jsqlOpen(this._path, this._mode));
347
+ r = safeParse(this._n('jsqlOpen', this._path, this._mode));
322
348
  } catch (e) {
323
349
  throw new Error('failed to open native storage: ' + e.message);
324
350
  }
@@ -329,7 +355,7 @@ class JSQL {
329
355
  }
330
356
  } else {
331
357
  try {
332
- JSON.parse(native.jsqlOpen('', 'memory'));
358
+ JSON.parse(this._n('jsqlOpen', '', 'memory'));
333
359
  } catch (e) {
334
360
  throw new Error('failed to open native memory storage: ' + e.message);
335
361
  }
@@ -337,7 +363,7 @@ class JSQL {
337
363
  if (this._mode !== 'memory') {
338
364
  if (this._flushInterval > 0) {
339
365
  this._diskFlushTimer = setInterval(() => {
340
- try { native.jsqlFlushDirty(); } catch (e) { /* ignore */ }
366
+ try { this._n('jsqlFlushDirty'); } catch (e) { /* ignore */ }
341
367
  }, this._flushInterval);
342
368
  if (this._diskFlushTimer.unref) this._diskFlushTimer.unref();
343
369
  }
@@ -358,7 +384,7 @@ class JSQL {
358
384
  if (process.memoryUsage().rss <= budget) return;
359
385
  for (let i = 0; i < 16; i++) {
360
386
  let r;
361
- try { r = JSON.parse(native.jsqlEvict()); } catch (e) { return; }
387
+ try { r = JSON.parse(this._n('jsqlEvict')); } catch (e) { return; }
362
388
  if (r.ok === false || !r.evicted || r.remaining === 0) return;
363
389
  if (process.memoryUsage().rss <= budget) return;
364
390
  }
@@ -401,11 +427,11 @@ class JSQL {
401
427
  if (pk) {
402
428
  const filter = {};
403
429
  filter[pk] = this._coercePkId(table, id);
404
- const r = safeParse(native.jsqlFind(table, JSON.stringify(filter), 1, 0));
430
+ const r = safeParse(this._n('jsqlFind', table, JSON.stringify(filter), 1, 0));
405
431
  if (r && r.error) return r;
406
432
  return Array.isArray(r) && r.length > 0 ? r[0] : null;
407
433
  }
408
- return safeParse(native.jsqlFindById(table, Number(id)));
434
+ return safeParse(this._n('jsqlFindById', table, Number(id)));
409
435
  }
410
436
 
411
437
  _seqIdByPk(table, id) {
@@ -413,7 +439,7 @@ class JSQL {
413
439
  if (pk) {
414
440
  const filter = {};
415
441
  filter[pk] = this._coercePkId(table, id);
416
- const r = safeParse(native.jsqlFind(table, JSON.stringify(filter), 1, 0));
442
+ const r = safeParse(this._n('jsqlFind', table, JSON.stringify(filter), 1, 0));
417
443
  if (Array.isArray(r) && r.length > 0) return r[0].id;
418
444
  return null;
419
445
  }
@@ -442,7 +468,7 @@ class JSQL {
442
468
  });
443
469
  }
444
470
  const bin = encodeBatch(rows);
445
- const r = safeParse(native.jsqlInsertBuf(table, bin));
471
+ const r = safeParse(this._n('jsqlInsertBuf', table, bin));
446
472
  if (r && r.error) throw new Error(r.error);
447
473
  if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
448
474
  const pk = this._pkOf(table);
@@ -471,13 +497,13 @@ class JSQL {
471
497
  var ids = remove[table];
472
498
  if (ids.size === 0) continue;
473
499
  var idsArr = Array.from(ids);
474
- var r = safeParse(native.jsqlRemoveByIds(table, JSON.stringify(idsArr)));
500
+ var r = safeParse(this._n('jsqlRemoveByIds', table, JSON.stringify(idsArr)));
475
501
  this._emit('delete', { table, ids: idsArr, result: r });
476
502
  }
477
503
  for (var table in update) {
478
504
  var entries = update[table];
479
505
  if (entries.length === 0) continue;
480
- var r = safeParse(native.jsqlUpdateByIds(table, JSON.stringify(entries)));
506
+ var r = safeParse(this._n('jsqlUpdateByIds', table, JSON.stringify(entries)));
481
507
  this._emit('update', { table, entries, result: r });
482
508
  }
483
509
  this._opBuffer = { remove: {}, update: {} };
@@ -531,7 +557,7 @@ class JSQL {
531
557
  await this._flush();
532
558
  this._flushOpsNow();
533
559
  if (this._mode !== 'memory') {
534
- try { native.jsqlFlushDirty(); } catch (e) { /* ignore */ }
560
+ try { this._n('jsqlFlushDirty'); } catch (e) { /* ignore */ }
535
561
  }
536
562
  }
537
563
 
@@ -570,7 +596,7 @@ class JSQL {
570
596
  await this._flush();
571
597
  if (typeof schema === 'string') schema = parseSchemaString(schema);
572
598
  if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
573
- const r = safeParse(native.jsqlCreateTable(name, JSON.stringify(mapNativeSchema(schema))));
599
+ const r = safeParse(this._n('jsqlCreateTable', name, JSON.stringify(mapNativeSchema(schema))));
574
600
  if (r && r.ok === false) throw new Error(r.error || 'create table failed');
575
601
  this._tableNames.add(name);
576
602
  this._schemas[name] = typeof schema === 'string' ? mapNativeSchema(schema) : schema;
@@ -582,7 +608,7 @@ class JSQL {
582
608
  async addColumn(table, name, def) {
583
609
  await this._flush();
584
610
  const mapped = mapNativeSchema({ [name]: def })[name];
585
- const r = safeParse(native.jsqlAddColumn(table, name, JSON.stringify(mapped)));
611
+ const r = safeParse(this._n('jsqlAddColumn', table, name, JSON.stringify(mapped)));
586
612
  if (r && r.ok === false) throw new Error(r.error || 'add column failed');
587
613
  if (!this._schemas[table]) this._schemas[table] = {};
588
614
  this._schemas[table][name] = mapped;
@@ -593,7 +619,7 @@ class JSQL {
593
619
  async dropTable(name) {
594
620
  await this._flush();
595
621
  if (!this._runHooks('beforeDropTable', [name])) return null;
596
- const r = safeParse(native.jsqlDropTable(name));
622
+ const r = safeParse(this._n('jsqlDropTable', name));
597
623
  if (r && r.ok === false) throw new Error(r.error || 'drop table failed');
598
624
  this._tableNames.delete(name);
599
625
  delete this._schemas[name];
@@ -620,7 +646,7 @@ class JSQL {
620
646
  if (this._pkOf(table)) {
621
647
  r = ids.map(id => this._findRawById(table, id)).filter(x => x !== null && !(x && x.error));
622
648
  } else {
623
- r = safeParse(native.jsqlFindByIds(table, JSON.stringify(ids)));
649
+ r = safeParse(this._n('jsqlFindByIds', table, JSON.stringify(ids)));
624
650
  }
625
651
  if (r && r.error) throw new Error(r.error);
626
652
  this._runHooks('afterFind', [table, { ids }, r]);
@@ -638,7 +664,7 @@ class JSQL {
638
664
  if (!this._runHooks('beforeFind', [table, { filter, opts }])) return [];
639
665
  const filterStr = filter ? JSON.stringify(filter) : '';
640
666
  const { limit = 100, offset = 0 } = opts;
641
- const r = safeParse(native.jsqlFind(table, filterStr, limit, offset));
667
+ const r = safeParse(this._n('jsqlFind', table, filterStr, limit, offset));
642
668
  if (r && r.error) throw new Error(r.error);
643
669
  this._runHooks('afterFind', [table, { filter, opts }, r]);
644
670
  const schema = this._schemas[table];
@@ -649,7 +675,7 @@ class JSQL {
649
675
  async count(table) {
650
676
  this._flushOpsNow();
651
677
  this._runHooks('beforeCount', [table]);
652
- const r = parseInt(native.jsqlCount(table), 10);
678
+ const r = parseInt(this._n('jsqlCount', table), 10);
653
679
  const n = isNaN(r) ? 0 : r;
654
680
  this._runHooks('afterCount', [table, n]);
655
681
  return n;
@@ -736,7 +762,17 @@ class JSQL {
736
762
  if (this._diskFlushTimer) { clearInterval(this._diskFlushTimer); this._diskFlushTimer = null; }
737
763
  if (this._evictTimer) { clearInterval(this._evictTimer); this._evictTimer = null; }
738
764
  if (this._mode !== 'memory') {
739
- try { JSON.parse(native.jsqlClose()); } catch (e) { /* ignore */ }
765
+ try { JSON.parse(this._n('jsqlClose')); } catch (e) { /* ignore */ }
766
+ }
767
+ // 释放本实例的引擎句柄,避免 native 侧内存泄漏
768
+ // (多实例下不释放会一直占用,直到进程退出)
769
+ if (this._handle !== null) {
770
+ try {
771
+ if (typeof native.jsqlInstanceFree === 'function') {
772
+ JSON.parse(native.jsqlInstanceFree(this._handle));
773
+ }
774
+ } catch (e) { /* ignore */ }
775
+ this._handle = null;
740
776
  }
741
777
  this._runHooks('onStop', []);
742
778
  this._emit('stop', {});
@@ -744,7 +780,7 @@ class JSQL {
744
780
 
745
781
  async beginTx() {
746
782
  this._flushOpsNow();
747
- const r = safeParse(native.jsqlBeginTx());
783
+ const r = safeParse(this._n('jsqlBeginTx'));
748
784
  if (r && r.ok === false) throw new Error(r.error || 'begin transaction failed');
749
785
  this._txId = r.txId;
750
786
  return r.txId;
@@ -769,13 +805,13 @@ class JSQL {
769
805
  }
770
806
 
771
807
  async commitTx(txId) {
772
- const r = safeParse(native.jsqlCommitTx(String(txId)));
808
+ const r = safeParse(this._n('jsqlCommitTx', String(txId)));
773
809
  if (r && r.ok === false) throw new Error(r.error || 'commit transaction failed');
774
810
  return true;
775
811
  }
776
812
 
777
813
  async rollbackTx(txId) {
778
- const r = safeParse(native.jsqlRollbackTx(String(txId)));
814
+ const r = safeParse(this._n('jsqlRollbackTx', String(txId)));
779
815
  if (r && r.ok === false) throw new Error(r.error || 'rollback transaction failed');
780
816
  return true;
781
817
  }