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/README.md +33 -0
- package/index.js +6 -3
- package/lib/btree.js +326 -0
- package/lib/database.js +286 -43
- package/lib/date-types.js +183 -0
- package/lib/errors.js +84 -0
- package/lib/query.js +251 -70
- package/lib/table.js +281 -175
- package/package.json +7 -3
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
282
|
+
if (this._triggers[name]) throw createError('ER_TRIGGER_EXISTS', name);
|
|
153
283
|
const table = this._tables[options.table];
|
|
154
|
-
if (!table) throw
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|