jsql-neo 1.2.1 → 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;
@@ -39,23 +44,150 @@ class Database {
39
44
  this._plugins = [];
40
45
  this._migrations = [];
41
46
  this._observers = []; // 响应式观察者
47
+ this._views = {}; // 视图 { name: { query, table } }
48
+ this._triggers = {}; // 触发器 { name: { event, table, callback } }
49
+ this._attached = {}; // 附加数据库 { name: db }
42
50
  this._changeStream = null; // 变更流
43
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
+
44
69
  // 从文件加载
45
70
  if (!this._memoryMode && fs.existsSync(this._filePath)) {
46
71
  this._loading = true;
72
+ this._acquireLock();
47
73
  this._load();
48
74
  this._loading = false;
49
75
  }
50
76
  }
51
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
+
52
184
  // ============================================================
53
185
  // 表管理
54
186
  // ============================================================
55
187
 
56
188
  createTable(name, schema) {
57
189
  if (this._tables[name]) {
58
- throw new Error(`Table '${name}' already exists`);
190
+ throw createError('ER_TABLE_EXISTS_ERROR', name);
59
191
  }
60
192
  const table = new Table(name, schema, this);
61
193
  this._tables[name] = table;
@@ -71,14 +203,16 @@ class Database {
71
203
  }
72
204
 
73
205
  this._markDirty();
206
+ this._walWrite({ op: 'createTable', table: name, schema });
74
207
  return table;
75
208
  }
76
209
 
77
210
  dropTable(name) {
78
- if (!this._tables[name]) throw new Error(`Table '${name}' does not exist`);
211
+ if (!this._tables[name]) throw createError('ER_NO_SUCH_TABLE', name);
79
212
  delete this._tables[name];
80
213
  delete this[name];
81
214
  delete this._versions[name];
215
+ this._walWrite({ op: 'dropTable', table: name });
82
216
  this._markDirty();
83
217
  }
84
218
 
@@ -91,35 +225,423 @@ class Database {
91
225
  }
92
226
 
93
227
  // ============================================================
94
- // 事务
228
+ // 视图
95
229
  // ============================================================
96
230
 
97
- begin() {
98
- if (this._transaction) throw new Error('Transaction already in progress');
99
- this._transaction = { rows: {}, autoIncrements: {} };
231
+ /**
232
+ * 创建视图(虚拟表)
233
+ * @param {string} name - 视图名
234
+ * @param {function} queryFn - (db) => Query 实例
235
+ * @example
236
+ * db.createView('adults', db => db.users.where({ age: { $gte: 18 } }).select(['name', 'age']));
237
+ */
238
+ createView(name, queryFn) {
239
+ if (this._views[name]) throw createError('ER_TABLE_EXISTS_ERROR', name);
240
+ this._views[name] = { queryFn };
241
+
242
+ Object.defineProperty(this, name, {
243
+ get: () => {
244
+ const q = queryFn(this);
245
+ return {
246
+ get: () => q.get(),
247
+ first: () => q.first(),
248
+ count: () => q.count(),
249
+ sum: (f) => q.sum(f),
250
+ avg: (f) => q.avg(f),
251
+ min: (f) => q.min(f),
252
+ max: (f) => q.max(f),
253
+ _query: q
254
+ };
255
+ },
256
+ enumerable: true,
257
+ configurable: true
258
+ });
259
+ }
260
+
261
+ dropView(name) {
262
+ if (!this._views[name]) throw createError('ER_NO_SUCH_TABLE', name);
263
+ delete this._views[name];
264
+ delete this[name];
265
+ }
266
+
267
+ getViews() {
268
+ return Object.keys(this._views);
269
+ }
270
+
271
+ // ============================================================
272
+ // 触发器
273
+ // ============================================================
274
+
275
+ /**
276
+ * 创建触发器
277
+ * @param {string} name - 触发器名
278
+ * @param {object} options - { event: 'insert'|'update'|'remove', table: 'tableName', timing: 'before'|'after' }
279
+ * @param {function} callback - 触发时执行的回调
280
+ */
281
+ createTrigger(name, options, callback) {
282
+ if (this._triggers[name]) throw createError('ER_TRIGGER_EXISTS', name);
283
+ const table = this._tables[options.table];
284
+ if (!table) throw createError('ER_NO_SUCH_TABLE', options.table);
285
+
286
+ const hookName = options.timing + options.event.charAt(0).toUpperCase() + options.event.slice(1);
287
+ this._triggers[name] = { event: options.event, table: options.table, callback, hookName };
288
+ table.on(hookName, callback);
289
+ }
290
+
291
+ dropTrigger(name) {
292
+ const trigger = this._triggers[name];
293
+ if (!trigger) throw createError('ER_TRIGGER_NOT_FOUND', name);
294
+ const table = this._tables[trigger.table];
295
+ if (table) table.off(trigger.hookName, trigger.callback);
296
+ delete this._triggers[name];
297
+ }
298
+
299
+ getTriggers() {
300
+ return Object.keys(this._triggers).map(name => ({
301
+ name,
302
+ event: this._triggers[name].event,
303
+ table: this._triggers[name].table
304
+ }));
305
+ }
306
+
307
+ // ============================================================
308
+ // CSV 导入/导出
309
+ // ============================================================
310
+
311
+ /**
312
+ * 从 CSV 文件导入数据到表
313
+ * @param {string} filePath - CSV 文件路径
314
+ * @param {string} tableName - 目标表名
315
+ * @param {object} options - { delimiter: ',', hasHeader: true, skipRows: 0, mapping: { csvCol: schemaCol } }
316
+ */
317
+ importCSV(filePath, tableName, options = {}) {
318
+ const table = this._tables[tableName];
319
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
320
+
321
+ const delimiter = options.delimiter || ',';
322
+ const hasHeader = options.hasHeader !== false;
323
+ const skipRows = options.skipRows || 0;
324
+ const mapping = options.mapping || null;
325
+
326
+ const content = fs.readFileSync(filePath, 'utf8');
327
+ const lines = content.split(/\r?\n/).filter(l => l.trim());
328
+
329
+ let headers = [];
330
+ let startRow = 0;
331
+
332
+ if (hasHeader) {
333
+ headers = this._parseCSVLine(lines[0], delimiter);
334
+ startRow = 1 + skipRows;
335
+ } else {
336
+ startRow = skipRows;
337
+ }
338
+
339
+ let count = 0;
340
+ for (let i = startRow; i < lines.length; i++) {
341
+ const values = this._parseCSVLine(lines[i], delimiter);
342
+ if (values.length === 0) continue;
343
+
344
+ const data = {};
345
+ if (mapping) {
346
+ for (const [csvCol, schemaCol] of Object.entries(mapping)) {
347
+ const idx = hasHeader ? headers.indexOf(csvCol) : parseInt(csvCol);
348
+ if (idx >= 0 && idx < values.length) {
349
+ data[schemaCol] = this._castValue(values[idx], table._schema[schemaCol]);
350
+ }
351
+ }
352
+ } else if (hasHeader) {
353
+ for (let j = 0; j < headers.length; j++) {
354
+ if (j < values.length && table._schema[headers[j]]) {
355
+ data[headers[j]] = this._castValue(values[j], table._schema[headers[j]]);
356
+ }
357
+ }
358
+ }
359
+
360
+ if (Object.keys(data).length > 0) {
361
+ table.insert(data);
362
+ count++;
363
+ }
364
+ }
365
+
366
+ return count;
367
+ }
368
+
369
+ /**
370
+ * 导出表数据到 CSV 文件
371
+ */
372
+ exportCSV(filePath, tableName, query = {}, options = {}) {
373
+ const table = this._tables[tableName];
374
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
375
+
376
+ const delimiter = options.delimiter || ',';
377
+ const rows = table._applyFilter(table._rows, query);
378
+ const fields = options.fields || Object.keys(table._schema).filter(f => f !== '_softDelete');
379
+
380
+ const lines = [];
381
+ lines.push(fields.map(f => this._csvEscape(f, delimiter)).join(delimiter));
382
+
383
+ for (const row of rows) {
384
+ lines.push(fields.map(f => {
385
+ const val = row[f];
386
+ return val === null || val === undefined ? '' : this._csvEscape(String(val), delimiter);
387
+ }).join(delimiter));
388
+ }
389
+
390
+ const dir = path.dirname(filePath);
391
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
392
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
393
+ return lines.length - 1;
394
+ }
395
+
396
+ _parseCSVLine(line, delimiter) {
397
+ const result = [];
398
+ let current = '';
399
+ let inQuotes = false;
400
+
401
+ for (let i = 0; i < line.length; i++) {
402
+ const ch = line[i];
403
+ if (inQuotes) {
404
+ if (ch === '"') {
405
+ if (i + 1 < line.length && line[i + 1] === '"') {
406
+ current += '"';
407
+ i++;
408
+ } else {
409
+ inQuotes = false;
410
+ }
411
+ } else {
412
+ current += ch;
413
+ }
414
+ } else {
415
+ if (ch === '"') {
416
+ inQuotes = true;
417
+ } else if (ch === delimiter) {
418
+ result.push(current.trim());
419
+ current = '';
420
+ } else {
421
+ current += ch;
422
+ }
423
+ }
424
+ }
425
+ result.push(current.trim());
426
+ return result;
427
+ }
428
+
429
+ _csvEscape(value, delimiter) {
430
+ if (value.includes(delimiter) || value.includes('"') || value.includes('\n')) {
431
+ return '"' + value.replace(/"/g, '""') + '"';
432
+ }
433
+ return value;
434
+ }
435
+
436
+ _castValue(value, schemaDef) {
437
+ if (!schemaDef || !value) return value;
438
+ switch (schemaDef.type) {
439
+ case 'integer': return parseInt(value, 10) || 0;
440
+ case 'number': return parseFloat(value) || 0;
441
+ case 'boolean': return value.toLowerCase() === 'true' || value === '1';
442
+ case 'array': try { return JSON.parse(value); } catch(e) { return [value]; }
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;
449
+ default: return value;
450
+ }
451
+ }
452
+
453
+ // ============================================================
454
+ // 数据库统计
455
+ // ============================================================
456
+
457
+ /**
458
+ * 获取数据库统计信息
459
+ */
460
+ stats() {
461
+ const tableStats = {};
462
+ let totalRows = 0;
463
+ let totalIndexes = 0;
464
+ let totalBTrees = 0;
465
+
100
466
  for (const [name, table] of Object.entries(this._tables)) {
101
- this._transaction.rows[name] = table._rows.map(r => ({ ...r }));
102
- this._transaction.autoIncrements[name] = table._autoIncrement;
467
+ if (name === '__meta__') continue;
468
+ const rowCount = table._rows.length;
469
+ const indexCount = Object.keys(table._indexes).length;
470
+ const btreeCount = Object.keys(table._btrees).length;
471
+ const sizeEstimate = JSON.stringify(table._rows).length;
472
+ tableStats[name] = {
473
+ rowCount,
474
+ indexCount: indexCount + btreeCount,
475
+ btreeCount,
476
+ sizeEstimate,
477
+ primaryKey: table._primaryKey,
478
+ columns: Object.keys(table._schema).length,
479
+ softDelete: table._softDelete,
480
+ foreignKeys: table._foreignKeys.length,
481
+ hooks: Object.values(table._hooks).reduce((s, a) => s + a.length, 0)
482
+ };
483
+ totalRows += rowCount;
484
+ totalIndexes += indexCount;
485
+ totalBTrees += btreeCount;
486
+ }
487
+
488
+ return {
489
+ tables: Object.keys(this._tables).filter(t => t !== '__meta__').length,
490
+ views: Object.keys(this._views).length,
491
+ triggers: Object.keys(this._triggers).length,
492
+ migrations: this._migrations.length,
493
+ plugins: this._plugins.length,
494
+ totalRows,
495
+ totalIndexes,
496
+ totalBTrees,
497
+ memoryMode: this._memoryMode,
498
+ versioning: this._versioning,
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,
505
+ tableStats
506
+ };
507
+ }
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);
103
530
  }
104
531
  }
105
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
+
556
+ // ============================================================
557
+ // 多数据库
558
+ // ============================================================
559
+
560
+ /**
561
+ * 附加另一个数据库(跨库查询)
562
+ */
563
+ attach(name, db) {
564
+ if (this._attached[name]) throw createError('ER_DBATTACH_EXISTS', name);
565
+ this._attached[name] = db;
566
+
567
+ Object.defineProperty(this, name, {
568
+ get: () => db,
569
+ enumerable: true,
570
+ configurable: true
571
+ });
572
+ }
573
+
574
+ detach(name) {
575
+ if (!this._attached[name]) throw createError('ER_DBATTACH_NOT_FOUND', name);
576
+ delete this._attached[name];
577
+ delete this[name];
578
+ }
579
+
580
+ getAttached() {
581
+ return Object.keys(this._attached);
582
+ }
583
+
584
+ // ============================================================
585
+ // 事务(支持隔离级别)
586
+ // ============================================================
587
+
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
+ }
615
+ }
616
+
617
+ this._walWrite({ op: 'begin', level });
618
+ }
619
+
106
620
  commit() {
107
- 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 });
108
623
  this._transaction = null;
624
+ this._checkpoint();
109
625
  this._markDirty();
110
626
  }
111
627
 
112
628
  rollback() {
113
- if (!this._transaction) throw new Error('No transaction in progress');
629
+ if (!this._transaction) throw createError('ER_NO_TRANSACTION');
630
+
114
631
  for (const [name, table] of Object.entries(this._tables)) {
115
632
  if (this._transaction.rows[name]) {
116
633
  table._rows = this._transaction.rows[name];
117
634
  table._autoIncrement = this._transaction.autoIncrements[name];
635
+ table._rebuildAllBTrees();
118
636
  for (const field of Object.keys(table._indexes)) {
119
637
  table.createIndex(field);
120
638
  }
639
+ } else if (this._transaction.autoIncrements[name] !== undefined) {
640
+ table._autoIncrement = this._transaction.autoIncrements[name];
121
641
  }
122
642
  }
643
+
644
+ this._walWrite({ op: 'rollback', at: this._transaction.startedAt });
123
645
  this._transaction = null;
124
646
  }
125
647
 
@@ -127,6 +649,24 @@ class Database {
127
649
  return !!this._transaction;
128
650
  }
129
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
+
130
670
  // ============================================================
131
671
  // 版本历史
132
672
  // ============================================================
@@ -140,10 +680,8 @@ class Database {
140
680
  if (!table) return;
141
681
  if (!this._versions[tableName]) this._versions[tableName] = [];
142
682
 
143
- // 空表跳过
144
683
  if (table._rows.length === 0) return;
145
684
 
146
- // 去重:如果当前状态与上次快照相同,跳过
147
685
  const currentHash = JSON.stringify(table._rows);
148
686
  const versions = this._versions[tableName];
149
687
  if (versions.length > 0 && versions[versions.length - 1]._hash === currentHash) return;
@@ -162,15 +700,15 @@ class Database {
162
700
  * 回退到指定版本
163
701
  */
164
702
  undo(tableName, steps = 1) {
165
- if (!this._versioning) throw new Error('Versioning is not enabled');
703
+ if (!this._versioning) throw createError('ER_NOT_SUPPORTED', 'Versioning is not enabled');
166
704
  const versions = this._versions[tableName];
167
- 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');
168
706
  const table = this._tables[tableName];
169
- if (!table) throw new Error(`Table '${tableName}' not found`);
707
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
170
708
 
171
709
  for (let i = 0; i < steps && versions.length > 1; i++) {
172
- versions.pop(); // 丢弃最新版本
173
- const snapshot = versions[versions.length - 1]; // 恢复到上一个版本
710
+ versions.pop();
711
+ const snapshot = versions[versions.length - 1];
174
712
  table._rows = snapshot.rows.map(r => ({ ...r }));
175
713
  table._autoIncrement = 0;
176
714
  for (const row of table._rows) {
@@ -178,10 +716,12 @@ class Database {
178
716
  table._autoIncrement = row[table._autoIncrementField];
179
717
  }
180
718
  }
719
+ table._rebuildAllBTrees();
181
720
  for (const field of Object.keys(table._indexes)) {
182
721
  table.createIndex(field);
183
722
  }
184
723
  }
724
+ this._walWrite({ op: 'undo', table: tableName, steps });
185
725
  this._markDirty();
186
726
  return versions.length;
187
727
  }
@@ -197,6 +737,7 @@ class Database {
197
737
  /**
198
738
  * 监听变更事件
199
739
  * @param {function} callback - (event) => {} event: { type, table, data, timestamp }
740
+ * @returns {function} 取消监听的函数
200
741
  */
201
742
  onChange(callback) {
202
743
  this._observers.push(callback);
@@ -251,7 +792,7 @@ class Database {
251
792
  */
252
793
  use(plugin) {
253
794
  if (typeof plugin.install !== 'function') {
254
- throw new Error('Plugin must have an install() method');
795
+ throw createError('ER_PLUGIN_INVALID', 'Plugin must have an install() method');
255
796
  }
256
797
  this._plugins.push(plugin);
257
798
  plugin.install(this);
@@ -330,7 +871,7 @@ class Database {
330
871
 
331
872
  restore(filePath) {
332
873
  if (!fs.existsSync(filePath)) {
333
- throw new Error(`Backup file not found: ${filePath}`);
874
+ throw createError('ER_FILE_NOT_FOUND', filePath);
334
875
  }
335
876
  const json = this._readFile(filePath);
336
877
  const data = JSON.parse(json);
@@ -339,6 +880,7 @@ class Database {
339
880
  this._tables[name]._loadRows(rows);
340
881
  }
341
882
  }
883
+ this._walWrite({ op: 'restore', from: filePath });
342
884
  this._markDirty();
343
885
  }
344
886
 
@@ -347,7 +889,7 @@ class Database {
347
889
  // ============================================================
348
890
 
349
891
  save() {
350
- // 版本快照(内存模式也支持)
892
+ // 版本快照
351
893
  if (this._versioning) {
352
894
  for (const name of Object.keys(this._tables)) {
353
895
  this.snapshot(name);
@@ -368,6 +910,9 @@ class Database {
368
910
  : JSON.stringify(data);
369
911
  this._writeFile(this._filePath, json);
370
912
  this._dirty = false;
913
+
914
+ // 保存后做检查点
915
+ this._checkpoint();
371
916
  }
372
917
 
373
918
  _load() {
@@ -388,6 +933,9 @@ class Database {
388
933
  this._tables[name]._loadRows(rows);
389
934
  }
390
935
  }
936
+
937
+ // WAL 恢复
938
+ this._recoverFromWAL();
391
939
  } catch (e) {
392
940
  throw new Error(`Failed to load database from '${this._filePath}': ${e.message}`);
393
941
  }
@@ -416,6 +964,7 @@ class Database {
416
964
  if (this._dirty && !this._memoryMode) {
417
965
  this.save();
418
966
  }
967
+ this._releaseLock();
419
968
  this._tables = {};
420
969
  }
421
970
 
@@ -434,6 +983,7 @@ class Database {
434
983
  this._tables[name]._loadRows(rows);
435
984
  }
436
985
  }
986
+ this._walWrite({ op: 'import', tables: Object.keys(data) });
437
987
  this._markDirty();
438
988
  }
439
989