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/README.md +33 -0
- package/index.js +7 -15
- package/lib/btree.js +326 -0
- package/lib/database.js +572 -22
- package/lib/date-types.js +183 -0
- package/lib/errors.js +84 -0
- package/lib/query.js +412 -51
- package/lib/table.js +320 -142
- 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;
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|