jsql-neo 1.3.0 → 2.0.0

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
@@ -1,13 +1,14 @@
1
1
  // © Vexify 2026 All Rights Reserved.
2
2
  /**
3
- * JSQL Database — 数据库管理器
4
- * 支持加密、插件系统、迁移、版本历史、变更流、响应式查询
3
+ * JSQL Database — 数据库管理器 v2.0
4
+ * WAL、文件锁、事务隔离级别、慢查询日志、错误码体系
5
5
  */
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const crypto = require('crypto');
10
10
  const Table = require('./table');
11
+ const { createError } = require('./errors');
11
12
 
12
13
  class Database {
13
14
  /**
@@ -18,6 +19,10 @@ class Database {
18
19
  * @param {boolean} options.pretty - JSON 是否格式化(默认 true)
19
20
  * @param {string} options.encryptKey - 加密密钥(16/24/32 字节),启用 AES-256-CBC
20
21
  * @param {boolean} options.versioning - 是否启用版本历史(默认 false)
22
+ * @param {boolean} options.wal - 是否启用 WAL 模式(默认 false,内存模式自动禁用)
23
+ * @param {string} options.isolationLevel - 事务隔离级别 'READ_COMMITTED' | 'REPEATABLE_READ'(默认 'READ_COMMITTED')
24
+ * @param {number} options.slowQueryThreshold - 慢查询阈值 ms(默认 100,0 禁用)
25
+ * @param {boolean} options.fileLock - 是否启用文件锁(默认 false)
21
26
  */
22
27
  constructor(filePath, options = {}) {
23
28
  this._filePath = filePath;
@@ -44,21 +49,145 @@ class Database {
44
49
  this._attached = {}; // 附加数据库 { name: db }
45
50
  this._changeStream = null; // 变更流
46
51
 
52
+ // WAL(Write-Ahead Logging)
53
+ this._walEnabled = options.wal === true && !this._memoryMode;
54
+ this._walPath = this._filePath ? this._filePath + '.wal' : null;
55
+ this._walOps = []; // WAL 操作日志
56
+
57
+ // 事务隔离
58
+ this._isolationLevel = options.isolationLevel || 'READ_COMMITTED';
59
+
60
+ // 文件锁
61
+ this._fileLockEnabled = options.fileLock === true && !this._memoryMode;
62
+ this._lockFd = null;
63
+ this._lockPath = this._filePath ? this._filePath + '.lock' : null;
64
+
65
+ // 慢查询日志
66
+ this._slowQueryThreshold = options.slowQueryThreshold !== undefined ? options.slowQueryThreshold : 100;
67
+ this._slowQueries = [];
68
+
47
69
  // 从文件加载
48
70
  if (!this._memoryMode && fs.existsSync(this._filePath)) {
49
71
  this._loading = true;
72
+ this._acquireLock();
50
73
  this._load();
51
74
  this._loading = false;
52
75
  }
53
76
  }
54
77
 
78
+ // ============================================================
79
+ // 文件锁
80
+ // ============================================================
81
+
82
+ _acquireLock() {
83
+ if (!this._fileLockEnabled) return;
84
+ try {
85
+ // 检查锁文件是否存在
86
+ if (fs.existsSync(this._lockPath)) {
87
+ const lockTime = fs.statSync(this._lockPath).mtimeMs;
88
+ // 锁超过 30 秒视为过期
89
+ if (Date.now() - lockTime > 30000) {
90
+ fs.unlinkSync(this._lockPath);
91
+ } else {
92
+ throw createError('ER_LOCK_WAIT_TIMEOUT', 'Database is locked by another process');
93
+ }
94
+ }
95
+ fs.writeFileSync(this._lockPath, String(process.pid), 'utf8');
96
+ this._lockFd = true;
97
+ } catch (e) {
98
+ if (e.code === 'ER_LOCK_WAIT_TIMEOUT') throw e;
99
+ // 权限不足等,降级为无锁模式
100
+ this._fileLockEnabled = false;
101
+ }
102
+ }
103
+
104
+ _releaseLock() {
105
+ if (!this._fileLockEnabled || !this._lockFd) return;
106
+ try {
107
+ if (fs.existsSync(this._lockPath)) {
108
+ fs.unlinkSync(this._lockPath);
109
+ }
110
+ this._lockFd = null;
111
+ } catch (e) {
112
+ // 忽略
113
+ }
114
+ }
115
+
116
+ // ============================================================
117
+ // WAL(Write-Ahead Logging)
118
+ // ============================================================
119
+
120
+ /**
121
+ * 写入 WAL 日志
122
+ */
123
+ _walWrite(op) {
124
+ if (!this._walEnabled) return;
125
+ this._walOps.push({
126
+ ...op,
127
+ timestamp: Date.now(),
128
+ sequence: this._walOps.length + 1
129
+ });
130
+ this._flushWAL();
131
+ }
132
+
133
+ /**
134
+ * 将 WAL 刷入磁盘
135
+ */
136
+ _flushWAL() {
137
+ if (!this._walEnabled || this._walOps.length === 0) return;
138
+ try {
139
+ const dir = path.dirname(this._walPath);
140
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
141
+ fs.writeFileSync(this._walPath, JSON.stringify(this._walOps), 'utf8');
142
+ } catch (e) {
143
+ // WAL 写入失败不应阻塞主流程
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 检查点:将 WAL 应用到数据文件并清空 WAL
149
+ */
150
+ _checkpoint() {
151
+ if (!this._walEnabled || this._walOps.length === 0) return;
152
+ // 所有操作已在内存中生效,此函数仅清除 WAL 文件
153
+ this._walOps = [];
154
+ try {
155
+ if (fs.existsSync(this._walPath)) {
156
+ fs.unlinkSync(this._walPath);
157
+ }
158
+ } catch (e) {
159
+ // 忽略
160
+ }
161
+ }
162
+
163
+ /**
164
+ * 崩溃恢复:从 WAL 重放操作
165
+ */
166
+ _recoverFromWAL() {
167
+ if (!this._walEnabled || !this._walPath || !fs.existsSync(this._walPath)) return;
168
+ try {
169
+ const walData = JSON.parse(fs.readFileSync(this._walPath, 'utf8'));
170
+ if (!Array.isArray(walData) || walData.length === 0) return;
171
+
172
+ // WAL 操作在 v2.0 中仅用于崩溃恢复标记
173
+ // 实际操作已在 save() 时持久化,WAL 主要用于:
174
+ // 1. 标记未完成的事务
175
+ // 2. 在 save() 间隔中保护数据
176
+ this._walOps = [];
177
+ fs.unlinkSync(this._walPath);
178
+ } catch (e) {
179
+ // WAL 损坏,忽略
180
+ try { if (fs.existsSync(this._walPath)) fs.unlinkSync(this._walPath); } catch (_) {}
181
+ }
182
+ }
183
+
55
184
  // ============================================================
56
185
  // 表管理
57
186
  // ============================================================
58
187
 
59
188
  createTable(name, schema) {
60
189
  if (this._tables[name]) {
61
- throw new Error(`Table '${name}' already exists`);
190
+ throw createError('ER_TABLE_EXISTS_ERROR', name);
62
191
  }
63
192
  const table = new Table(name, schema, this);
64
193
  this._tables[name] = table;
@@ -74,14 +203,16 @@ class Database {
74
203
  }
75
204
 
76
205
  this._markDirty();
206
+ this._walWrite({ op: 'createTable', table: name, schema });
77
207
  return table;
78
208
  }
79
209
 
80
210
  dropTable(name) {
81
- if (!this._tables[name]) throw new Error(`Table '${name}' does not exist`);
211
+ if (!this._tables[name]) throw createError('ER_NO_SUCH_TABLE', name);
82
212
  delete this._tables[name];
83
213
  delete this[name];
84
214
  delete this._versions[name];
215
+ this._walWrite({ op: 'dropTable', table: name });
85
216
  this._markDirty();
86
217
  }
87
218
 
@@ -105,10 +236,9 @@ class Database {
105
236
  * db.createView('adults', db => db.users.where({ age: { $gte: 18 } }).select(['name', 'age']));
106
237
  */
107
238
  createView(name, queryFn) {
108
- if (this._views[name]) throw new Error(`View '${name}' already exists`);
239
+ if (this._views[name]) throw createError('ER_TABLE_EXISTS_ERROR', name);
109
240
  this._views[name] = { queryFn };
110
241
 
111
- // 代理到 db 上
112
242
  Object.defineProperty(this, name, {
113
243
  get: () => {
114
244
  const q = queryFn(this);
@@ -129,7 +259,7 @@ class Database {
129
259
  }
130
260
 
131
261
  dropView(name) {
132
- if (!this._views[name]) throw new Error(`View '${name}' does not exist`);
262
+ if (!this._views[name]) throw createError('ER_NO_SUCH_TABLE', name);
133
263
  delete this._views[name];
134
264
  delete this[name];
135
265
  }
@@ -149,9 +279,9 @@ class Database {
149
279
  * @param {function} callback - 触发时执行的回调
150
280
  */
151
281
  createTrigger(name, options, callback) {
152
- if (this._triggers[name]) throw new Error(`Trigger '${name}' already exists`);
282
+ if (this._triggers[name]) throw createError('ER_TRIGGER_EXISTS', name);
153
283
  const table = this._tables[options.table];
154
- if (!table) throw new Error(`Table '${options.table}' does not exist`);
284
+ if (!table) throw createError('ER_NO_SUCH_TABLE', options.table);
155
285
 
156
286
  const hookName = options.timing + options.event.charAt(0).toUpperCase() + options.event.slice(1);
157
287
  this._triggers[name] = { event: options.event, table: options.table, callback, hookName };
@@ -160,7 +290,7 @@ class Database {
160
290
 
161
291
  dropTrigger(name) {
162
292
  const trigger = this._triggers[name];
163
- if (!trigger) throw new Error(`Trigger '${name}' does not exist`);
293
+ if (!trigger) throw createError('ER_TRIGGER_NOT_FOUND', name);
164
294
  const table = this._tables[trigger.table];
165
295
  if (table) table.off(trigger.hookName, trigger.callback);
166
296
  delete this._triggers[name];
@@ -186,7 +316,7 @@ class Database {
186
316
  */
187
317
  importCSV(filePath, tableName, options = {}) {
188
318
  const table = this._tables[tableName];
189
- if (!table) throw new Error(`Table '${tableName}' does not exist`);
319
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
190
320
 
191
321
  const delimiter = options.delimiter || ',';
192
322
  const hasHeader = options.hasHeader !== false;
@@ -238,24 +368,18 @@ class Database {
238
368
 
239
369
  /**
240
370
  * 导出表数据到 CSV 文件
241
- * @param {string} filePath - 输出文件路径
242
- * @param {string} tableName - 表名
243
- * @param {object} query - 筛选条件(可选)
244
- * @param {object} options - { delimiter: ',', fields: ['col1','col2'] }
245
371
  */
246
372
  exportCSV(filePath, tableName, query = {}, options = {}) {
247
373
  const table = this._tables[tableName];
248
- if (!table) throw new Error(`Table '${tableName}' does not exist`);
374
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
249
375
 
250
376
  const delimiter = options.delimiter || ',';
251
377
  const rows = table._applyFilter(table._rows, query);
252
378
  const fields = options.fields || Object.keys(table._schema).filter(f => f !== '_softDelete');
253
379
 
254
380
  const lines = [];
255
- // 表头
256
381
  lines.push(fields.map(f => this._csvEscape(f, delimiter)).join(delimiter));
257
382
 
258
- // 数据行
259
383
  for (const row of rows) {
260
384
  lines.push(fields.map(f => {
261
385
  const val = row[f];
@@ -266,7 +390,7 @@ class Database {
266
390
  const dir = path.dirname(filePath);
267
391
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
268
392
  fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
269
- return lines.length - 1; // 返回数据行数
393
+ return lines.length - 1;
270
394
  }
271
395
 
272
396
  _parseCSVLine(line, delimiter) {
@@ -317,6 +441,11 @@ class Database {
317
441
  case 'boolean': return value.toLowerCase() === 'true' || value === '1';
318
442
  case 'array': try { return JSON.parse(value); } catch(e) { return [value]; }
319
443
  case 'object': try { return JSON.parse(value); } catch(e) { return {}; }
444
+ case 'date':
445
+ case 'datetime':
446
+ case 'timestamp':
447
+ case 'time':
448
+ return value;
320
449
  default: return value;
321
450
  }
322
451
  }
@@ -332,15 +461,18 @@ class Database {
332
461
  const tableStats = {};
333
462
  let totalRows = 0;
334
463
  let totalIndexes = 0;
464
+ let totalBTrees = 0;
335
465
 
336
466
  for (const [name, table] of Object.entries(this._tables)) {
337
467
  if (name === '__meta__') continue;
338
468
  const rowCount = table._rows.length;
339
469
  const indexCount = Object.keys(table._indexes).length;
470
+ const btreeCount = Object.keys(table._btrees).length;
340
471
  const sizeEstimate = JSON.stringify(table._rows).length;
341
472
  tableStats[name] = {
342
473
  rowCount,
343
- indexCount,
474
+ indexCount: indexCount + btreeCount,
475
+ btreeCount,
344
476
  sizeEstimate,
345
477
  primaryKey: table._primaryKey,
346
478
  columns: Object.keys(table._schema).length,
@@ -350,6 +482,7 @@ class Database {
350
482
  };
351
483
  totalRows += rowCount;
352
484
  totalIndexes += indexCount;
485
+ totalBTrees += btreeCount;
353
486
  }
354
487
 
355
488
  return {
@@ -360,24 +493,75 @@ class Database {
360
493
  plugins: this._plugins.length,
361
494
  totalRows,
362
495
  totalIndexes,
496
+ totalBTrees,
363
497
  memoryMode: this._memoryMode,
364
498
  versioning: this._versioning,
365
499
  encrypted: !!this._encryptKey,
500
+ walEnabled: this._walEnabled,
501
+ fileLockEnabled: this._fileLockEnabled,
502
+ isolationLevel: this._isolationLevel,
503
+ slowQueryThreshold: this._slowQueryThreshold,
504
+ slowQueriesCount: this._slowQueries.length,
366
505
  tableStats
367
506
  };
368
507
  }
369
508
 
509
+ // ============================================================
510
+ // 慢查询日志
511
+ // ============================================================
512
+
513
+ /**
514
+ * 记录慢查询
515
+ */
516
+ _logSlowQuery(sql, durationMs, rowsReturned) {
517
+ if (this._slowQueryThreshold <= 0) return;
518
+ if (durationMs < this._slowQueryThreshold) return;
519
+
520
+ this._slowQueries.push({
521
+ sql,
522
+ duration: durationMs,
523
+ rows: rowsReturned,
524
+ timestamp: Date.now()
525
+ });
526
+
527
+ // 限制日志数量
528
+ if (this._slowQueries.length > 1000) {
529
+ this._slowQueries = this._slowQueries.slice(-500);
530
+ }
531
+ }
532
+
533
+ /**
534
+ * 获取慢查询日志
535
+ * @param {number} limit - 返回最近 N 条
536
+ */
537
+ getSlowQueries(limit = 50) {
538
+ return this._slowQueries.slice(-limit).reverse();
539
+ }
540
+
541
+ /**
542
+ * 清空慢查询日志
543
+ */
544
+ clearSlowQueries() {
545
+ this._slowQueries = [];
546
+ }
547
+
548
+ /**
549
+ * 设置慢查询阈值
550
+ * @param {number} ms - 毫秒,0 禁用
551
+ */
552
+ setSlowQueryThreshold(ms) {
553
+ this._slowQueryThreshold = ms;
554
+ }
555
+
370
556
  // ============================================================
371
557
  // 多数据库
372
558
  // ============================================================
373
559
 
374
560
  /**
375
561
  * 附加另一个数据库(跨库查询)
376
- * @param {string} name - 别名
377
- * @param {Database} db - 另一个 Database 实例
378
562
  */
379
563
  attach(name, db) {
380
- if (this._attached[name]) throw new Error(`Database '${name}' already attached`);
564
+ if (this._attached[name]) throw createError('ER_DBATTACH_EXISTS', name);
381
565
  this._attached[name] = db;
382
566
 
383
567
  Object.defineProperty(this, name, {
@@ -388,7 +572,7 @@ class Database {
388
572
  }
389
573
 
390
574
  detach(name) {
391
- if (!this._attached[name]) throw new Error(`Database '${name}' is not attached`);
575
+ if (!this._attached[name]) throw createError('ER_DBATTACH_NOT_FOUND', name);
392
576
  delete this._attached[name];
393
577
  delete this[name];
394
578
  }
@@ -398,35 +582,66 @@ class Database {
398
582
  }
399
583
 
400
584
  // ============================================================
401
- // 事务
585
+ // 事务(支持隔离级别)
402
586
  // ============================================================
403
587
 
404
- begin() {
405
- if (this._transaction) throw new Error('Transaction already in progress');
406
- this._transaction = { rows: {}, autoIncrements: {} };
407
- for (const [name, table] of Object.entries(this._tables)) {
408
- this._transaction.rows[name] = table._rows.map(r => ({ ...r }));
409
- this._transaction.autoIncrements[name] = table._autoIncrement;
588
+ /**
589
+ * 开始事务
590
+ * @param {string} isolationLevel - 覆盖默认隔离级别(可选)
591
+ */
592
+ begin(isolationLevel) {
593
+ if (this._transaction) throw createError('ER_TRANSACTION_ACTIVE');
594
+
595
+ const level = isolationLevel || this._isolationLevel;
596
+
597
+ this._transaction = {
598
+ rows: {},
599
+ autoIncrements: {},
600
+ level,
601
+ startedAt: Date.now()
602
+ };
603
+
604
+ if (level === 'REPEATABLE_READ') {
605
+ // 快照:保存当前所有数据
606
+ for (const [name, table] of Object.entries(this._tables)) {
607
+ this._transaction.rows[name] = table._rows.map(r => ({ ...r }));
608
+ this._transaction.autoIncrements[name] = table._autoIncrement;
609
+ }
610
+ } else {
611
+ // READ_COMMITTED:只保存 autoIncrement
612
+ for (const [name, table] of Object.entries(this._tables)) {
613
+ this._transaction.autoIncrements[name] = table._autoIncrement;
614
+ }
410
615
  }
616
+
617
+ this._walWrite({ op: 'begin', level });
411
618
  }
412
619
 
413
620
  commit() {
414
- if (!this._transaction) throw new Error('No transaction in progress');
621
+ if (!this._transaction) throw createError('ER_NO_TRANSACTION');
622
+ this._walWrite({ op: 'commit', at: this._transaction.startedAt });
415
623
  this._transaction = null;
624
+ this._checkpoint();
416
625
  this._markDirty();
417
626
  }
418
627
 
419
628
  rollback() {
420
- if (!this._transaction) throw new Error('No transaction in progress');
629
+ if (!this._transaction) throw createError('ER_NO_TRANSACTION');
630
+
421
631
  for (const [name, table] of Object.entries(this._tables)) {
422
632
  if (this._transaction.rows[name]) {
423
633
  table._rows = this._transaction.rows[name];
424
634
  table._autoIncrement = this._transaction.autoIncrements[name];
635
+ table._rebuildAllBTrees();
425
636
  for (const field of Object.keys(table._indexes)) {
426
637
  table.createIndex(field);
427
638
  }
639
+ } else if (this._transaction.autoIncrements[name] !== undefined) {
640
+ table._autoIncrement = this._transaction.autoIncrements[name];
428
641
  }
429
642
  }
643
+
644
+ this._walWrite({ op: 'rollback', at: this._transaction.startedAt });
430
645
  this._transaction = null;
431
646
  }
432
647
 
@@ -434,6 +649,24 @@ class Database {
434
649
  return !!this._transaction;
435
650
  }
436
651
 
652
+ /**
653
+ * 获取当前事务隔离级别
654
+ */
655
+ getIsolationLevel() {
656
+ return this._transaction ? this._transaction.level : this._isolationLevel;
657
+ }
658
+
659
+ /**
660
+ * 设置默认事务隔离级别
661
+ * @param {'READ_COMMITTED'|'REPEATABLE_READ'} level
662
+ */
663
+ setIsolationLevel(level) {
664
+ if (!['READ_COMMITTED', 'REPEATABLE_READ'].includes(level)) {
665
+ throw createError('ER_NOT_SUPPORTED', `Isolation level '${level}' is not supported. Use READ_COMMITTED or REPEATABLE_READ.`);
666
+ }
667
+ this._isolationLevel = level;
668
+ }
669
+
437
670
  // ============================================================
438
671
  // 版本历史
439
672
  // ============================================================
@@ -447,10 +680,8 @@ class Database {
447
680
  if (!table) return;
448
681
  if (!this._versions[tableName]) this._versions[tableName] = [];
449
682
 
450
- // 空表跳过
451
683
  if (table._rows.length === 0) return;
452
684
 
453
- // 去重:如果当前状态与上次快照相同,跳过
454
685
  const currentHash = JSON.stringify(table._rows);
455
686
  const versions = this._versions[tableName];
456
687
  if (versions.length > 0 && versions[versions.length - 1]._hash === currentHash) return;
@@ -469,15 +700,15 @@ class Database {
469
700
  * 回退到指定版本
470
701
  */
471
702
  undo(tableName, steps = 1) {
472
- if (!this._versioning) throw new Error('Versioning is not enabled');
703
+ if (!this._versioning) throw createError('ER_NOT_SUPPORTED', 'Versioning is not enabled');
473
704
  const versions = this._versions[tableName];
474
- if (!versions || versions.length < 2) throw new Error('No versions to undo');
705
+ if (!versions || versions.length < 2) throw createError('ER_NOT_SUPPORTED', 'No versions to undo');
475
706
  const table = this._tables[tableName];
476
- if (!table) throw new Error(`Table '${tableName}' not found`);
707
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
477
708
 
478
709
  for (let i = 0; i < steps && versions.length > 1; i++) {
479
- versions.pop(); // 丢弃最新版本
480
- const snapshot = versions[versions.length - 1]; // 恢复到上一个版本
710
+ versions.pop();
711
+ const snapshot = versions[versions.length - 1];
481
712
  table._rows = snapshot.rows.map(r => ({ ...r }));
482
713
  table._autoIncrement = 0;
483
714
  for (const row of table._rows) {
@@ -485,10 +716,12 @@ class Database {
485
716
  table._autoIncrement = row[table._autoIncrementField];
486
717
  }
487
718
  }
719
+ table._rebuildAllBTrees();
488
720
  for (const field of Object.keys(table._indexes)) {
489
721
  table.createIndex(field);
490
722
  }
491
723
  }
724
+ this._walWrite({ op: 'undo', table: tableName, steps });
492
725
  this._markDirty();
493
726
  return versions.length;
494
727
  }
@@ -504,6 +737,7 @@ class Database {
504
737
  /**
505
738
  * 监听变更事件
506
739
  * @param {function} callback - (event) => {} event: { type, table, data, timestamp }
740
+ * @returns {function} 取消监听的函数
507
741
  */
508
742
  onChange(callback) {
509
743
  this._observers.push(callback);
@@ -558,7 +792,7 @@ class Database {
558
792
  */
559
793
  use(plugin) {
560
794
  if (typeof plugin.install !== 'function') {
561
- throw new Error('Plugin must have an install() method');
795
+ throw createError('ER_PLUGIN_INVALID', 'Plugin must have an install() method');
562
796
  }
563
797
  this._plugins.push(plugin);
564
798
  plugin.install(this);
@@ -637,7 +871,7 @@ class Database {
637
871
 
638
872
  restore(filePath) {
639
873
  if (!fs.existsSync(filePath)) {
640
- throw new Error(`Backup file not found: ${filePath}`);
874
+ throw createError('ER_FILE_NOT_FOUND', filePath);
641
875
  }
642
876
  const json = this._readFile(filePath);
643
877
  const data = JSON.parse(json);
@@ -646,6 +880,7 @@ class Database {
646
880
  this._tables[name]._loadRows(rows);
647
881
  }
648
882
  }
883
+ this._walWrite({ op: 'restore', from: filePath });
649
884
  this._markDirty();
650
885
  }
651
886
 
@@ -654,7 +889,7 @@ class Database {
654
889
  // ============================================================
655
890
 
656
891
  save() {
657
- // 版本快照(内存模式也支持)
892
+ // 版本快照
658
893
  if (this._versioning) {
659
894
  for (const name of Object.keys(this._tables)) {
660
895
  this.snapshot(name);
@@ -675,6 +910,9 @@ class Database {
675
910
  : JSON.stringify(data);
676
911
  this._writeFile(this._filePath, json);
677
912
  this._dirty = false;
913
+
914
+ // 保存后做检查点
915
+ this._checkpoint();
678
916
  }
679
917
 
680
918
  _load() {
@@ -695,6 +933,9 @@ class Database {
695
933
  this._tables[name]._loadRows(rows);
696
934
  }
697
935
  }
936
+
937
+ // WAL 恢复
938
+ this._recoverFromWAL();
698
939
  } catch (e) {
699
940
  throw new Error(`Failed to load database from '${this._filePath}': ${e.message}`);
700
941
  }
@@ -723,6 +964,7 @@ class Database {
723
964
  if (this._dirty && !this._memoryMode) {
724
965
  this.save();
725
966
  }
967
+ this._releaseLock();
726
968
  this._tables = {};
727
969
  }
728
970
 
@@ -741,6 +983,7 @@ class Database {
741
983
  this._tables[name]._loadRows(rows);
742
984
  }
743
985
  }
986
+ this._walWrite({ op: 'import', tables: Object.keys(data) });
744
987
  this._markDirty();
745
988
  }
746
989