jsql-neo 6.3.3 → 6.3.5
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 +60 -20
- package/index.js +73 -6
- package/lib/btree.js +5 -41
- package/lib/database.js +276 -35
- package/lib/native_client.js +57 -21
- package/lib/sql.js +304 -25
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +8 -1
- package/nativesrc/jsql-neo-native/src/lib.rs +274 -178
- package/package.json +6 -5
- package/test/dml-constraints.test.js +163 -0
- package/test/mysql-compat-hijack.test.js +87 -0
- package/test/native-multi-instance.test.js +139 -0
- package/test/readme-audit.test.js +25 -0
- package/test/sql-parser.test.js +189 -0
- package/test/wal-recovery.test.js +161 -0
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
426
|
+
* 检查点:数据已由 save() 完整落盘,清空 WAL
|
|
427
|
+
*
|
|
428
|
+
* 只在 save() 成功写完快照之后调用。若快照写入失败则不检查点,
|
|
429
|
+
* 保证 WAL 里的记录仍可用于恢复。
|
|
395
430
|
*/
|
|
396
431
|
_checkpoint() {
|
|
397
|
-
if (!this._walEnabled
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -1291,7 +1499,24 @@ class Database {
|
|
|
1291
1499
|
if (!this._runHooks('beforeUpdate', [tableName, id, data])) return;
|
|
1292
1500
|
const row = this._resolveId(table, id);
|
|
1293
1501
|
if (!row) return { ok: false, error: 'not found' };
|
|
1502
|
+
// 改主键/唯一列时要先确认新值不与其它行冲突。
|
|
1503
|
+
// 此前直接 Object.assign,导致
|
|
1504
|
+
// UPDATE t SET id = 1 WHERE id = 2 (t 已有 id=1)
|
|
1505
|
+
// 静默产生两行同主键 —— 主键索引只留一个,SELECT 结果与实际存储不一致。
|
|
1506
|
+
// MySQL / SQLite 都会报 UNIQUE constraint failed。
|
|
1507
|
+
// 注意:UNIQUE 列记录在 table._cachedUniqueFields(不是 _indexes),
|
|
1508
|
+
// 逐行比对即可,未建索引时也能正确拦截。
|
|
1509
|
+
const pk = this._pkOf(table);
|
|
1510
|
+
const uniq = (table._cachedUniqueFields || []).filter(f => f !== pk);
|
|
1511
|
+
for (const field of [pk, ...uniq]) {
|
|
1512
|
+
if (!field || !Object.prototype.hasOwnProperty.call(data, field)) continue;
|
|
1513
|
+
const nv = data[field];
|
|
1514
|
+
if (nv === row[field]) continue; // 值没变,不算冲突
|
|
1515
|
+
const clash = table._rows.find(r => r !== row && r[field] === nv);
|
|
1516
|
+
if (clash) throw createError('ER_DUP_ENTRY', `Duplicate entry '${nv}' for key '${field}'`);
|
|
1517
|
+
}
|
|
1294
1518
|
Object.assign(row, data);
|
|
1519
|
+
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...data } });
|
|
1295
1520
|
this._emit('update', { table: tableName, id, data });
|
|
1296
1521
|
this._runHooks('afterUpdate', [tableName, id, data, { ok: true }]);
|
|
1297
1522
|
this._markDirty(tableName);
|
|
@@ -1303,10 +1528,15 @@ class Database {
|
|
|
1303
1528
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1304
1529
|
const pairs = entries.map(([id, data]) => [id, data]);
|
|
1305
1530
|
if (!this._runHooks('beforeUpdate', [tableName, pairs])) return;
|
|
1531
|
+
const pk = this._pkOf(table);
|
|
1306
1532
|
let count = 0;
|
|
1307
1533
|
for (const [id, data] of pairs) {
|
|
1308
1534
|
const row = this._resolveId(table, id);
|
|
1309
|
-
if (row) {
|
|
1535
|
+
if (row) {
|
|
1536
|
+
Object.assign(row, data);
|
|
1537
|
+
count++;
|
|
1538
|
+
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...data } });
|
|
1539
|
+
}
|
|
1310
1540
|
}
|
|
1311
1541
|
this._emit('update', { table: tableName, entries: pairs, result: { ok: true, count } });
|
|
1312
1542
|
this._runHooks('afterUpdate', [tableName, pairs, { ok: true, count }]);
|
|
@@ -1321,6 +1551,7 @@ class Database {
|
|
|
1321
1551
|
if (table._primaryKey) {
|
|
1322
1552
|
const removed = table.removeById(id);
|
|
1323
1553
|
if (removed === 0) return { ok: false, error: 'not found' };
|
|
1554
|
+
this._walWrite({ op: 'remove', table: tableName, pk: table._primaryKey, id });
|
|
1324
1555
|
this._emit('delete', { table: tableName, id });
|
|
1325
1556
|
this._runHooks('afterDelete', [tableName, id, { ok: true }]);
|
|
1326
1557
|
this._markDirty(tableName);
|
|
@@ -1346,13 +1577,21 @@ class Database {
|
|
|
1346
1577
|
if (table._primaryKey) {
|
|
1347
1578
|
for (const id of ids) removed += table.removeById(id);
|
|
1348
1579
|
} else {
|
|
1580
|
+
// 无主键表没有稳定的行标识,_resolveId 只能把 id 当 1-based 行号,
|
|
1581
|
+
// 而 _rowPkId 在无主键时回退到 row.id(业务列名)——
|
|
1582
|
+
// 两者语义不一致,导致 DELETE ... WHERE id IN (3,5) 删错行。
|
|
1583
|
+
// 这里先按「行对象 === id」尝试(SQL 层可以直接传行引用),
|
|
1584
|
+
// 退化到行号解析;并一次性收集目标后统一移除,避免边删边移位。
|
|
1585
|
+
const targets = [];
|
|
1586
|
+
const seen = new Set();
|
|
1587
|
+
const push = (row) => { if (row && !seen.has(row)) { seen.add(row); targets.push(row); } };
|
|
1349
1588
|
for (const id of ids) {
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1589
|
+
if (id && typeof id === 'object') { push(id); continue; }
|
|
1590
|
+
push(this._resolveId(table, id));
|
|
1591
|
+
}
|
|
1592
|
+
if (targets.length) {
|
|
1593
|
+
table._rows = table._rows.filter(r => !seen.has(r));
|
|
1594
|
+
removed = targets.length;
|
|
1356
1595
|
}
|
|
1357
1596
|
table._rebuildPKIndex();
|
|
1358
1597
|
table._rebuildAllBTrees();
|
|
@@ -1590,6 +1829,8 @@ class Database {
|
|
|
1590
1829
|
this._tables[name]._loadRows(rows);
|
|
1591
1830
|
}
|
|
1592
1831
|
}
|
|
1832
|
+
// WAL 恢复(此前 .jsql 分支提前 return,导致崩溃后数据全丢)
|
|
1833
|
+
this._recoverFromWAL();
|
|
1593
1834
|
return;
|
|
1594
1835
|
}
|
|
1595
1836
|
|
package/lib/native_client.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|