jsql-neo 5.4.2 → 5.5.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.
Files changed (72) hide show
  1. package/LICENSE +202 -202
  2. package/README.md +8097 -7971
  3. package/bin/jsql +320 -320
  4. package/bin/jsql-server +90 -90
  5. package/bin/jsql-server-control.js +177 -177
  6. package/index.d.ts +385 -357
  7. package/index.js +124 -118
  8. package/lib/ast.js +214 -0
  9. package/lib/btree.js +413 -413
  10. package/lib/cache.js +328 -328
  11. package/lib/client.js +125 -125
  12. package/lib/database.js +1744 -1744
  13. package/lib/date-types.js +182 -182
  14. package/lib/errors.js +89 -89
  15. package/lib/jsql_format.js +497 -497
  16. package/lib/migrate.js +254 -254
  17. package/lib/mod.js +292 -292
  18. package/lib/mongo_server.js +607 -607
  19. package/lib/multiserver.js +140 -140
  20. package/lib/mysql_compat.js +413 -413
  21. package/lib/mysql_server.js +1237 -1237
  22. package/lib/native_client.js +732 -732
  23. package/lib/nedb_compat.js +506 -506
  24. package/lib/pg_server.js +864 -864
  25. package/lib/plugin.js +34 -34
  26. package/lib/query.js +815 -815
  27. package/lib/redis_server.js +571 -571
  28. package/lib/sql.js +3460 -2925
  29. package/lib/sqlite_compat.js +323 -323
  30. package/lib/sqlite_worker.js +264 -264
  31. package/lib/table.js +1304 -1304
  32. package/lib/tui.js +502 -502
  33. package/lib/wasm.js +1 -1
  34. package/lib/wasm_client.js +718 -718
  35. package/lib/web_ui.js +257 -257
  36. package/native/jsql-neo-native.node +0 -0
  37. package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
  38. package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
  39. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
  40. package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
  41. package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
  42. package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
  43. package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
  44. package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
  45. package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
  46. package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
  47. package/nativesrc/jsql-neo-core/src/types.rs +93 -93
  48. package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
  49. package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
  50. package/nativesrc/jsql-neo-native/build.rs +3 -3
  51. package/nativesrc/jsql-neo-native/package.json +7 -7
  52. package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
  53. package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
  54. package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
  55. package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
  56. package/package.json +87 -86
  57. package/postinstall.js +158 -158
  58. package/test/btree.test.js +128 -128
  59. package/test/coverage.js +187 -187
  60. package/test/join.test.js +110 -110
  61. package/test/native.test.js +158 -158
  62. package/test/regress-5.1.0.js +271 -271
  63. package/test/smoke.js +58 -58
  64. package/test/sql-parser.test.js +273 -0
  65. package/test/wasm.test.js +110 -110
  66. package/wasm/browser.d.ts +89 -89
  67. package/wasm/browser.mjs +569 -569
  68. package/wasm/browser_bg.mjs +597 -597
  69. package/wasm/jsql_neo_wasm.d.ts +38 -38
  70. package/wasm/jsql_neo_wasm.js +520 -520
  71. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
  72. package/wasm/package.json +11 -11
package/lib/database.js CHANGED
@@ -1,1745 +1,1745 @@
1
- // © Vexify 2026 All Rights Reserved.
2
- /**
3
- * JSQL Database — 数据库管理器 v2.0
4
- * WAL、文件锁、事务隔离级别、慢查询日志、错误码体系
5
- */
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
- const os = require('os');
10
- const crypto = require('crypto');
11
- const Table = require('./table');
12
- const { createError } = require('./errors');
13
- const JSQLFormat = require('./jsql_format');
14
-
15
- /**
16
- * 解析字段简写类型字符串,如 'integer primary key'、'string unique'、
17
- * 'integer primary key auto_increment'、'datetime default now'
18
- * @param {string} str - 简写字符串(已小写化)
19
- * @returns {object} { type, primaryKey, unique, autoIncrement, required, ... }
20
- */
21
- function parseFieldShorthand(str) {
22
- const def = { type: str };
23
- const strip = (re) => {
24
- const next = def.type.replace(re, '').trim();
25
- def.type = next || def.type;
26
- };
27
- if (/\bprimary\s+key\b/i.test(def.type)) { def.primaryKey = true; strip(/\bprimary\s+key\b/gi); }
28
- if (/\bauto_?increment\b/i.test(def.type)) { def.autoIncrement = true; strip(/\bauto_?increment\b/gi); }
29
- if (/\bnot\s+null\b/i.test(def.type)) { def.required = true; strip(/\bnot\s+null\b/gi); }
30
- if (/\bunique\b/i.test(def.type)) { def.unique = true; strip(/\bunique\b/gi); }
31
- if (/\bdefault\s+(\S+)/i.test(def.type)) {
32
- const m = def.type.match(/\bdefault\s+(\S+)/i);
33
- def.default = m[1].replace(/^['"]|['"]$/g, '');
34
- strip(/\bdefault\s+\S+/gi);
35
- }
36
- return def;
37
- }
38
-
39
- class Database {
40
- /**
41
- * @param {string} filePath - 数据库文件/目录路径,传 null 或 ':memory:' 使用内存模式
42
- * @param {object} options
43
- * @param {string} options.mode - 存储模式:
44
- * 'memory'(默认):纯内存,不落盘
45
- * 'hybrid'(混合,Redis 式):先写内存,后台异步慢慢刷盘;内存充足(默认预留 0.5GB 余量)
46
- * 则数据常驻内存作为缓存;内存紧张时按 LRU 驱逐最冷表(已落盘),查询时自动从磁盘加载
47
- * 'disk'(磁盘模式):写入后尽快刷盘,内存只做读写缓存
48
- * @param {number} options.memReserveMB - 内存余量 MB(默认 512,即预留 0.5GB)
49
- * @param {number} options.flushInterval - 脏表后台刷盘间隔 ms(默认 hybrid 200 / disk 50,0 = 每次写后立即刷)
50
- * @param {number} options.evictInterval - 内存压力检查间隔 ms(默认 1000)
51
- * @param {boolean} options.autoSave - 是否自动保存(默认 true,仅旧文件模式)
52
- * @param {number} options.autoSaveInterval - 自动保存间隔 ms(默认 0,仅旧文件模式)
53
- * @param {boolean} options.pretty - JSON 是否格式化(默认 true,仅旧文件模式)
54
- * @param {string} options.encryptKey - 加密密钥(16/24/32 字节),启用 AES-256-CBC
55
- * @param {boolean} options.versioning - 是否启用版本历史(默认 false)
56
- * @param {boolean} options.wal - 是否启用 WAL 模式(默认 false,内存模式自动禁用)
57
- * @param {string} options.isolationLevel - 事务隔离级别 'READ_COMMITTED' | 'REPEATABLE_READ'(默认 'READ_COMMITTED')
58
- * @param {number} options.slowQueryThreshold - 慢查询阈值 ms(默认 100,0 记录全部,<0 禁用)
59
- * @param {boolean} options.fileLock - 是否启用文件锁(默认 false)
60
- */
61
- constructor(filePath, options = {}) {
62
- this._filePath = filePath;
63
- this._memoryMode = !filePath || filePath === ':memory:';
64
- this._options = {
65
- autoSave: options.autoSave !== false,
66
- autoSaveInterval: options.autoSaveInterval || 0,
67
- pretty: options.pretty !== false
68
- };
69
-
70
- // 存储模式: 'memory' | 'hybrid' | 'disk' | null(旧文件模式: 全量 save)
71
- this._mode = options.mode || (this._memoryMode ? 'memory' : null);
72
- this._dirMode = this._mode === 'hybrid' || this._mode === 'disk';
73
- this._memReserve = (options.memReserveMB !== undefined ? options.memReserveMB : 512) * 1024 * 1024;
74
- this._flushInterval = options.flushInterval !== undefined
75
- ? options.flushInterval
76
- : (this._mode === 'disk' ? 50 : 200);
77
- this._evictInterval = options.evictInterval !== undefined ? options.evictInterval : 1000;
78
- this._dirtyTables = new Set();
79
- this._flushTimer = null;
80
- this._monitorTimer = null;
81
- this._lastAccess = {};
82
- this._meta = { version: 1, tables: {} };
83
-
84
- this._tables = {};
85
- this._dirty = false;
86
- this._autoSaveTimer = null;
87
- this._loading = false;
88
- this._transaction = null;
89
- this._encryptKey = options.encryptKey || null;
90
- this._versioning = options.versioning || false;
91
- this._versions = {}; // tableName -> [{ rows, timestamp }]
92
- this._maxVersions = options.maxVersions || 50;
93
- this._plugins = [];
94
- this._hooks = {
95
- beforeInsert: [], afterInsert: [],
96
- beforeUpdate: [], afterUpdate: [],
97
- beforeDelete: [], afterDelete: [],
98
- beforeFind: [], afterFind: [],
99
- beforeCreateTable: [], afterCreateTable: [],
100
- beforeDropTable: [], afterDropTable: [],
101
- beforeFlush: [], afterFlush: [],
102
- beforeCount: [], afterCount: [],
103
- onStart: [], onStop: []
104
- };
105
- this._eventListeners = [];
106
- this._tableNames = new Set(Object.keys(this._tables));
107
- this._migrations = [];
108
- this._observers = []; // 响应式观察者
109
- this._views = {}; // 视图 { name: { query, table } }
110
- this._triggers = {}; // 触发器 { name: { event, table, callback } }
111
- this._attached = {}; // 附加数据库 { name: db }
112
- this._changeStream = null; // 变更流
113
-
114
- // WAL(Write-Ahead Logging)
115
- this._walEnabled = options.wal === true && !this._memoryMode;
116
- this._walPath = this._filePath ? this._filePath + '.wal' : null;
117
- this._walOps = []; // WAL 操作日志
118
-
119
- // 事务隔离:默认 REPEATABLE_READ(begin 时保存行快照,rollback 可回滚)
120
- this._isolationLevel = options.isolationLevel || 'REPEATABLE_READ';
121
-
122
- // JSql 格式 (binary block-based)
123
- this._jsqlMode = this._filePath && this._filePath.endsWith('.jsql');
124
- this._jsqlFormat = this._jsqlMode ? new JSQLFormat(this._filePath) : null;
125
-
126
- // 文件锁
127
- this._fileLockEnabled = options.fileLock === true && !this._memoryMode;
128
- this._lockFd = null;
129
- this._lockPath = this._filePath ? this._filePath + '.lock' : null;
130
-
131
- // 慢查询日志
132
- this._slowQueryThreshold = options.slowQueryThreshold !== undefined ? options.slowQueryThreshold : 100;
133
- this._slowQueries = [];
134
-
135
- // 从文件加载
136
- if (!this._memoryMode && !this._dirMode && fs.existsSync(this._filePath)) {
137
- this._loading = true;
138
- this._acquireLock();
139
- this._load();
140
- this._loading = false;
141
- }
142
-
143
- // 磁盘/混合模式: 目录存储(每表独立 JSql 文件,增量刷盘)
144
- if (this._dirMode) {
145
- this._initDirStore();
146
- }
147
- }
148
-
149
- // ============================================================
150
- // 磁盘/混合存储(Redis 式: 内存缓存 + 异步刷盘 + LRU 驱逐)
151
- // ============================================================
152
-
153
- _initDirStore() {
154
- if (!this._filePath) throw new Error('hybrid/disk mode requires a directory path');
155
- fs.mkdirSync(this._filePath, { recursive: true });
156
- this._metaPath = path.join(this._filePath, 'meta.json');
157
- this._meta = { version: 1, tables: {} };
158
- try {
159
- if (fs.existsSync(this._metaPath)) {
160
- const parsed = JSON.parse(fs.readFileSync(this._metaPath, 'utf8'));
161
- if (parsed && parsed.tables) {
162
- this._meta = parsed;
163
- // 清洗 meta 中的 file 字段:拒绝绝对路径/.. 穿越,防恶意 meta.json 写任意位置
164
- for (const name of Object.keys(this._meta.tables)) {
165
- const meta = this._meta.tables[name];
166
- if (meta && meta.file !== undefined) {
167
- if (this._safeTableFile(meta.file) === null) {
168
- delete this._meta.tables[name];
169
- }
170
- }
171
- }
172
- }
173
- }
174
- } catch (e) {
175
- this._meta = { version: 1, tables: {} };
176
- }
177
- for (const name of Object.keys(this._meta.tables)) {
178
- if (this[name] === undefined && !(name in this)) {
179
- Object.defineProperty(this, name, {
180
- get: () => this._ensureTable(name) || this._tables[name],
181
- enumerable: true,
182
- configurable: true
183
- });
184
- }
185
- }
186
- this._startMonitor();
187
- }
188
-
189
- _tableFile(name) {
190
- const meta = this._meta.tables[name];
191
- if (meta && meta.file) {
192
- const safe = this._safeTableFile(meta.file);
193
- if (safe) return path.join(this._filePath, safe);
194
- }
195
- return path.join(this._filePath, encodeURIComponent(name) + '.jsql');
196
- }
197
-
198
- _safeTableFile(file) {
199
- if (typeof file !== 'string' || file.length === 0) return null;
200
- if (path.isAbsolute(file)) return null;
201
- if (file.includes('/') || file.includes('\\')) return null;
202
- const base = path.basename(file);
203
- if (base !== file || base === '.' || base === '..') return null;
204
- if (file.includes('\0')) return null;
205
- return file;
206
- }
207
-
208
- _saveMeta() {
209
- if (!this._dirMode) return;
210
- try {
211
- fs.writeFileSync(this._metaPath, JSON.stringify(this._meta, null, 2), 'utf8');
212
- } catch (e) {
213
- // 忽略 meta 写入失败
214
- }
215
- }
216
-
217
- _touchTable(name) {
218
- this._lastAccess[name] = Date.now();
219
- }
220
-
221
- _ensureTable(name) {
222
- if (this._tables[name]) return this._tables[name];
223
- if (!this._dirMode) return null;
224
- const meta = this._meta.tables[name];
225
- if (!meta) return null;
226
- const file = this._tableFile(name);
227
- if (!fs.existsSync(file)) return null;
228
- try {
229
- const fmt = new JSQLFormat(file);
230
- const { rows } = fmt.readTableSync(name);
231
- const table = new Table(name, meta.schema, this);
232
- if (rows && rows.length > 0) table._loadRows(rows);
233
- this._tables[name] = table;
234
- if (this._versioning) this._versions[name] = [];
235
- this._touchTable(name);
236
- return table;
237
- } catch (e) {
238
- return null;
239
- }
240
- }
241
-
242
- _markDirty(name) {
243
- if (!this._dirMode) {
244
- this._dirty = true;
245
- return;
246
- }
247
- this._touchTable(name);
248
- this._dirtyTables.add(name);
249
- this._scheduleFlush();
250
- }
251
-
252
- _scheduleFlush() {
253
- if (this._flushTimer) return;
254
- if (this._flushInterval <= 0) {
255
- this._flushTimer = setImmediate(() => {
256
- this._flushTimer = null;
257
- try { this._flushDirty(); } catch (e) { this._emit('error', e); }
258
- });
259
- return;
260
- }
261
- this._flushTimer = setTimeout(() => {
262
- this._flushTimer = null;
263
- try { this._flushDirty(); } catch (e) { this._emit('error', e); }
264
- }, this._flushInterval);
265
- if (this._flushTimer.unref) this._flushTimer.unref();
266
- }
267
-
268
- _flushDirty() {
269
- if (!this._dirMode || this._dirtyTables.size === 0) return;
270
- const names = [...this._dirtyTables];
271
- for (const name of names) {
272
- try {
273
- this._flushTable(name);
274
- this._dirtyTables.delete(name);
275
- } catch (e) {
276
- // 保留脏标记,下次 flush 重试;错误对外传播而非静默
277
- this._emit('error', e);
278
- }
279
- }
280
- this._saveMeta();
281
- }
282
-
283
- _flushTable(name) {
284
- const table = this._tables[name];
285
- if (!table) return;
286
- const meta = this._meta.tables[name];
287
- let file = meta && meta.file ? meta.file : encodeURIComponent(name) + '.jsql';
288
- const fmt = new JSQLFormat(path.join(this._filePath, file));
289
- try {
290
- const pkField = Object.keys(table._schema).find(f => table._schema[f].primaryKey) || null;
291
- fmt.writeTable(name, table._schema, table._rows, pkField);
292
- this._meta.tables[name] = { file, schema: table._schema };
293
- } finally {
294
- fmt._close();
295
- }
296
- }
297
-
298
- _startMonitor() {
299
- if (this._monitorTimer) return;
300
- this._monitorTimer = setInterval(() => {
301
- try { this._checkMemory(); } catch (e) {}
302
- }, this._evictInterval);
303
- if (this._monitorTimer.unref) this._monitorTimer.unref();
304
- }
305
-
306
- _checkMemory() {
307
- const total = os.totalmem();
308
- if (!total) return;
309
- const budget = total - this._memReserve;
310
- if (budget <= 0) return;
311
- const rss = process.memoryUsage().rss;
312
- if (rss <= budget) return;
313
- this._evictTables(budget);
314
- }
315
-
316
- _evictTables(budget) {
317
- const candidates = Object.keys(this._tables)
318
- .filter(n => n !== '__meta__' && !this._versioning && this._meta.tables[n]);
319
- candidates.sort((a, b) => (this._lastAccess[a] || 0) - (this._lastAccess[b] || 0));
320
- for (const name of candidates) {
321
- if (budget !== undefined && process.memoryUsage().rss <= budget) break;
322
- try { this._flushTable(name); } catch (e) { continue; }
323
- delete this._tables[name];
324
- delete this[name];
325
- this._dirtyTables.delete(name);
326
- }
327
- this._saveMeta();
328
- }
329
-
330
- // ============================================================
331
- // 文件锁
332
- // ============================================================
333
-
334
- _acquireLock() {
335
- if (!this._fileLockEnabled) return;
336
- try {
337
- // 检查锁文件是否存在
338
- if (fs.existsSync(this._lockPath)) {
339
- const lockTime = fs.statSync(this._lockPath).mtimeMs;
340
- // 锁超过 30 秒视为过期
341
- if (Date.now() - lockTime > 30000) {
342
- fs.unlinkSync(this._lockPath);
343
- } else {
344
- throw createError('ER_LOCK_WAIT_TIMEOUT', 'Database is locked by another process');
345
- }
346
- }
347
- fs.writeFileSync(this._lockPath, String(process.pid), 'utf8');
348
- this._lockFd = true;
349
- } catch (e) {
350
- if (e.code === 'ER_LOCK_WAIT_TIMEOUT') throw e;
351
- // 权限不足等,降级为无锁模式
352
- this._fileLockEnabled = false;
353
- }
354
- }
355
-
356
- _releaseLock() {
357
- if (!this._fileLockEnabled || !this._lockFd) return;
358
- try {
359
- if (fs.existsSync(this._lockPath)) {
360
- fs.unlinkSync(this._lockPath);
361
- }
362
- this._lockFd = null;
363
- } catch (e) {
364
- // 忽略
365
- }
366
- }
367
-
368
- // ============================================================
369
- // WAL(Write-Ahead Logging)
370
- // ============================================================
371
-
372
- /**
373
- * 写入 WAL 日志
374
- */
375
- _walWrite(op) {
376
- if (!this._walEnabled) return;
377
- this._walOps.push({
378
- ...op,
379
- timestamp: Date.now(),
380
- sequence: this._walOps.length + 1
381
- });
382
- this._flushWAL();
383
- }
384
-
385
- /**
386
- * 将 WAL 刷入磁盘
387
- */
388
- _flushWAL() {
389
- if (!this._walEnabled || this._walOps.length === 0) return;
390
- try {
391
- const dir = path.dirname(this._walPath);
392
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
393
- fs.writeFileSync(this._walPath, JSON.stringify(this._walOps), 'utf8');
394
- } catch (e) {
395
- // WAL 写入失败不应阻塞主流程
396
- }
397
- }
398
-
399
- /**
400
- * 检查点:将 WAL 应用到数据文件并清空 WAL
401
- */
402
- _checkpoint() {
403
- if (!this._walEnabled || this._walOps.length === 0) return;
404
- // 所有操作已在内存中生效,此函数仅清除 WAL 文件
405
- this._walOps = [];
406
- try {
407
- if (fs.existsSync(this._walPath)) {
408
- fs.unlinkSync(this._walPath);
409
- }
410
- } catch (e) {
411
- // 忽略
412
- }
413
- }
414
-
415
- /**
416
- * 崩溃恢复:从 WAL 重放操作
417
- */
418
- _recoverFromWAL() {
419
- if (!this._walEnabled || !this._walPath || !fs.existsSync(this._walPath)) return;
420
- try {
421
- const walData = JSON.parse(fs.readFileSync(this._walPath, 'utf8'));
422
- if (!Array.isArray(walData) || walData.length === 0) return;
423
-
424
- // WAL 操作在 v2.0 中仅用于崩溃恢复标记
425
- // 实际操作已在 save() 时持久化,WAL 主要用于:
426
- // 1. 标记未完成的事务
427
- // 2. 在 save() 间隔中保护数据
428
- this._walOps = [];
429
- fs.unlinkSync(this._walPath);
430
- } catch (e) {
431
- // WAL 损坏,忽略
432
- try { if (fs.existsSync(this._walPath)) fs.unlinkSync(this._walPath); } catch (_) {}
433
- }
434
- }
435
-
436
- // ============================================================
437
- // 表管理
438
- // ============================================================
439
-
440
- createTable(name, schema) {
441
- if (!name || name.length > 64 || name === '.' || name === '..' || /[\x00-\x1f/\\]|\.\./.test(name)) {
442
- throw createError('ER_BAD_TABLE_NAME', name);
443
- }
444
- if (!this._runHooks('beforeCreateTable', [name, schema])) throw createError('ER_PLUGIN_ABORT', 'createTable aborted by plugin');
445
- if (this._tables[name] || (this._dirMode && this._meta.tables[name])) {
446
- throw createError('ER_TABLE_EXISTS_ERROR', name);
447
- }
448
- const normalized = {};
449
- for (const [field, def] of Object.entries(schema || {})) {
450
- if (typeof def === 'string') normalized[field] = parseFieldShorthand(def.toLowerCase());
451
- else normalized[field] = def;
452
- }
453
- const table = new Table(name, normalized, this);
454
- this._tables[name] = table;
455
- this._tableNames.add(name);
456
-
457
- Object.defineProperty(this, name, {
458
- get: () => this._ensureTable(name) || this._tables[name],
459
- enumerable: true,
460
- configurable: true
461
- });
462
-
463
- if (this._versioning) {
464
- this._versions[name] = [];
465
- }
466
- if (this._dirMode) {
467
- this._meta.tables[name] = { file: encodeURIComponent(name) + '.jsql', schema: normalized };
468
- this._saveMeta();
469
- }
470
-
471
- this._markDirty(name);
472
- this._walWrite({ op: 'createTable', table: name, schema });
473
- this._emit('createTable', { name, schema });
474
- this._runHooks('afterCreateTable', [name, schema]);
475
- return table;
476
- }
477
-
478
- dropTable(name) {
479
- if (!this._tables[name] && !(this._dirMode && this._meta.tables[name])) throw createError('ER_NO_SUCH_TABLE', name);
480
- if (!this._runHooks('beforeDropTable', [name])) return;
481
- if (this._tables[name]) {
482
- delete this._tables[name];
483
- }
484
- delete this[name];
485
- delete this._versions[name];
486
- this._tableNames.delete(name);
487
- if (this._dirMode) {
488
- const meta = this._meta.tables[name];
489
- if (meta && meta.file) {
490
- try { fs.unlinkSync(path.join(this._filePath, meta.file)); } catch (e) {}
491
- }
492
- delete this._meta.tables[name];
493
- this._dirtyTables.delete(name);
494
- this._saveMeta();
495
- }
496
- this._walWrite({ op: 'dropTable', table: name });
497
- this._markDirty(name);
498
- this._emit('dropTable', { name });
499
- this._runHooks('afterDropTable', [name]);
500
- }
501
-
502
- hasTable(name) {
503
- return !!this._tables[name] || (this._dirMode && !!this._meta.tables[name]);
504
- }
505
-
506
- getTables() {
507
- const names = new Set(Object.keys(this._tables));
508
- if (this._dirMode) {
509
- for (const name of Object.keys(this._meta.tables)) names.add(name);
510
- }
511
- return [...names];
512
- }
513
-
514
- getTableSchema(name) {
515
- const table = this._ensureTable(name);
516
- if (table) return table._schema;
517
- if (this._dirMode && this._meta.tables[name]) return this._meta.tables[name].schema;
518
- return null;
519
- }
520
-
521
- // ============================================================
522
- // 视图
523
- // ============================================================
524
-
525
- /**
526
- * 创建视图(虚拟表)
527
- * @param {string} name - 视图名
528
- * @param {function} queryFn - (db) => Query 实例
529
- * @example
530
- * db.createView('adults', db => db.users.where({ age: { $gte: 18 } }).select(['name', 'age']));
531
- */
532
- createView(name, queryFn) {
533
- if (this._views[name]) throw createError('ER_TABLE_EXISTS_ERROR', name);
534
- this._views[name] = { queryFn };
535
-
536
- Object.defineProperty(this, name, {
537
- get: () => {
538
- const q = queryFn(this);
539
- return {
540
- get: () => q.get(),
541
- first: () => q.first(),
542
- count: () => q.count(),
543
- sum: (f) => q.sum(f),
544
- avg: (f) => q.avg(f),
545
- min: (f) => q.min(f),
546
- max: (f) => q.max(f),
547
- _query: q
548
- };
549
- },
550
- enumerable: true,
551
- configurable: true
552
- });
553
- }
554
-
555
- dropView(name) {
556
- if (!this._views[name]) throw createError('ER_NO_SUCH_TABLE', name);
557
- delete this._views[name];
558
- delete this[name];
559
- }
560
-
561
- getViews() {
562
- return Object.keys(this._views);
563
- }
564
-
565
- // ============================================================
566
- // 触发器
567
- // ============================================================
568
-
569
- /**
570
- * 创建触发器
571
- * @param {string} name - 触发器名
572
- * @param {object} options - { event: 'insert'|'update'|'remove', table: 'tableName', timing: 'before'|'after' }
573
- * @param {function} callback - 触发时执行的回调
574
- */
575
- createTrigger(name, options, callback) {
576
- if (this._triggers[name]) throw createError('ER_TRIGGER_EXISTS', name);
577
- const table = this._ensureTable(options.table);
578
- if (!table) throw createError('ER_NO_SUCH_TABLE', options.table);
579
-
580
- const hookName = options.timing + options.event.charAt(0).toUpperCase() + options.event.slice(1);
581
- this._triggers[name] = { event: options.event, table: options.table, callback, hookName };
582
- table.on(hookName, callback);
583
- }
584
-
585
- dropTrigger(name) {
586
- const trigger = this._triggers[name];
587
- if (!trigger) throw createError('ER_TRIGGER_NOT_FOUND', name);
588
- const table = this._ensureTable(trigger.table);
589
- if (table) table.off(trigger.hookName, trigger.callback);
590
- delete this._triggers[name];
591
- }
592
-
593
- getTriggers() {
594
- return Object.keys(this._triggers).map(name => ({
595
- name,
596
- event: this._triggers[name].event,
597
- table: this._triggers[name].table
598
- }));
599
- }
600
-
601
- // ============================================================
602
- // CSV 导入/导出
603
- // ============================================================
604
-
605
- /**
606
- * 从 CSV 文件导入数据到表
607
- * @param {string} filePath - CSV 文件路径
608
- * @param {string} tableName - 目标表名
609
- * @param {object} options - { delimiter: ',', hasHeader: true, skipRows: 0, mapping: { csvCol: schemaCol } }
610
- */
611
- importCSV(filePath, tableName, options = {}) {
612
- const table = this._ensureTable(tableName);
613
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
614
-
615
- const delimiter = options.delimiter || ',';
616
- const hasHeader = options.hasHeader !== false;
617
- const skipRows = options.skipRows || 0;
618
- const mapping = options.mapping || null;
619
-
620
- const content = fs.readFileSync(filePath, 'utf8');
621
- const lines = content.split(/\r?\n/).filter(l => l.trim());
622
-
623
- let headers = [];
624
- let startRow = 0;
625
-
626
- if (hasHeader) {
627
- headers = this._parseCSVLine(lines[0], delimiter);
628
- startRow = 1 + skipRows;
629
- } else {
630
- startRow = skipRows;
631
- }
632
-
633
- let count = 0;
634
- for (let i = startRow; i < lines.length; i++) {
635
- const values = this._parseCSVLine(lines[i], delimiter);
636
- if (values.length === 0) continue;
637
-
638
- const data = {};
639
- if (mapping) {
640
- for (const [csvCol, schemaCol] of Object.entries(mapping)) {
641
- const idx = hasHeader ? headers.indexOf(csvCol) : parseInt(csvCol);
642
- if (idx >= 0 && idx < values.length) {
643
- data[schemaCol] = this._castValue(values[idx], table._schema[schemaCol]);
644
- }
645
- }
646
- } else if (hasHeader) {
647
- for (let j = 0; j < headers.length; j++) {
648
- if (j < values.length && table._schema[headers[j]]) {
649
- data[headers[j]] = this._castValue(values[j], table._schema[headers[j]]);
650
- }
651
- }
652
- }
653
-
654
- if (Object.keys(data).length > 0) {
655
- table.insert(data);
656
- count++;
657
- }
658
- }
659
-
660
- return count;
661
- }
662
-
663
- /**
664
- * 导出表数据到 CSV 文件
665
- */
666
- exportCSV(filePath, tableName, query = {}, options = {}) {
667
- const table = this._ensureTable(tableName);
668
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
669
-
670
- const delimiter = options.delimiter || ',';
671
- const rows = table._applyFilter(table._rows, query);
672
- const fields = options.fields || Object.keys(table._schema).filter(f => f !== '_softDelete');
673
-
674
- const lines = [];
675
- lines.push(fields.map(f => this._csvEscape(f, delimiter)).join(delimiter));
676
-
677
- for (const row of rows) {
678
- lines.push(fields.map(f => {
679
- const val = row[f];
680
- return val === null || val === undefined ? '' : this._csvEscape(String(val), delimiter);
681
- }).join(delimiter));
682
- }
683
-
684
- const dir = path.dirname(filePath);
685
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
686
- fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
687
- return lines.length - 1;
688
- }
689
-
690
- _parseCSVLine(line, delimiter) {
691
- const result = [];
692
- let current = '';
693
- let inQuotes = false;
694
-
695
- for (let i = 0; i < line.length; i++) {
696
- const ch = line[i];
697
- if (inQuotes) {
698
- if (ch === '"') {
699
- if (i + 1 < line.length && line[i + 1] === '"') {
700
- current += '"';
701
- i++;
702
- } else {
703
- inQuotes = false;
704
- }
705
- } else {
706
- current += ch;
707
- }
708
- } else {
709
- if (ch === '"') {
710
- inQuotes = true;
711
- } else if (ch === delimiter) {
712
- result.push(current.trim());
713
- current = '';
714
- } else {
715
- current += ch;
716
- }
717
- }
718
- }
719
- result.push(current.trim());
720
- return result;
721
- }
722
-
723
- _csvEscape(value, delimiter) {
724
- if (value.includes(delimiter) || value.includes('"') || value.includes('\n')) {
725
- return '"' + value.replace(/"/g, '""') + '"';
726
- }
727
- return value;
728
- }
729
-
730
- _castValue(value, schemaDef) {
731
- if (!schemaDef || !value) return value;
732
- switch (schemaDef.type) {
733
- case 'integer': return parseInt(value, 10) || 0;
734
- case 'number': return parseFloat(value) || 0;
735
- case 'boolean': return value.toLowerCase() === 'true' || value === '1';
736
- case 'array': try { return JSON.parse(value); } catch(e) { return [value]; }
737
- case 'object': try { return JSON.parse(value); } catch(e) { return {}; }
738
- case 'date':
739
- case 'datetime':
740
- case 'timestamp':
741
- case 'time':
742
- return value;
743
- default: return value;
744
- }
745
- }
746
-
747
- // ============================================================
748
- // 数据库统计
749
- // ============================================================
750
-
751
- /**
752
- * 获取数据库统计信息
753
- */
754
- stats() {
755
- const tableStats = {};
756
- let totalRows = 0;
757
- let totalIndexes = 0;
758
- let totalBTrees = 0;
759
-
760
- for (const [name, table] of Object.entries(this._tables)) {
761
- if (name === '__meta__') continue;
762
- const rowCount = table._rows.length;
763
- const indexCount = Object.keys(table._indexes).length;
764
- const btreeCount = Object.keys(table._btrees).length;
765
- const sizeEstimate = JSON.stringify(table._rows).length;
766
- tableStats[name] = {
767
- rowCount,
768
- indexCount: indexCount + btreeCount,
769
- btreeCount,
770
- sizeEstimate,
771
- primaryKey: table._primaryKey,
772
- columns: Object.keys(table._schema).length,
773
- softDelete: table._softDelete,
774
- foreignKeys: table._foreignKeys.length,
775
- hooks: Object.values(table._hooks).reduce((s, a) => s + a.length, 0)
776
- };
777
- totalRows += rowCount;
778
- totalIndexes += indexCount;
779
- totalBTrees += btreeCount;
780
- }
781
-
782
- return {
783
- tables: Object.keys(this._tables).filter(t => t !== '__meta__').length,
784
- views: Object.keys(this._views).length,
785
- triggers: Object.keys(this._triggers).length,
786
- migrations: this._migrations.length,
787
- plugins: this._plugins.length,
788
- totalRows,
789
- totalIndexes,
790
- totalBTrees,
791
- memoryMode: this._memoryMode,
792
- versioning: this._versioning,
793
- encrypted: !!this._encryptKey,
794
- walEnabled: this._walEnabled,
795
- fileLockEnabled: this._fileLockEnabled,
796
- isolationLevel: this._isolationLevel,
797
- slowQueryThreshold: this._slowQueryThreshold,
798
- slowQueriesCount: this._slowQueries.length,
799
- tableStats
800
- };
801
- }
802
-
803
- // ============================================================
804
- // 慢查询日志
805
- // ============================================================
806
-
807
- /**
808
- * 记录慢查询
809
- */
810
- _logSlowQuery(sql, durationMs, rowsReturned) {
811
- if (this._slowQueryThreshold < 0) return;
812
- if (this._slowQueryThreshold > 0 && durationMs < this._slowQueryThreshold) return;
813
-
814
- this._slowQueries.push({
815
- sql,
816
- duration: durationMs,
817
- rows: rowsReturned,
818
- timestamp: Date.now()
819
- });
820
-
821
- // 限制日志数量
822
- if (this._slowQueries.length > 1000) {
823
- this._slowQueries = this._slowQueries.slice(-500);
824
- }
825
- }
826
-
827
- /**
828
- * 获取慢查询日志
829
- * @param {number} limit - 返回最近 N 条
830
- */
831
- getSlowQueries(limit = 50) {
832
- return this._slowQueries.slice(-limit).reverse();
833
- }
834
-
835
- /**
836
- * 清空慢查询日志
837
- */
838
- clearSlowQueries() {
839
- this._slowQueries = [];
840
- }
841
-
842
- /**
843
- * 设置慢查询阈值
844
- * @param {number} ms - 毫秒,0 禁用
845
- */
846
- setSlowQueryThreshold(ms) {
847
- this._slowQueryThreshold = ms;
848
- }
849
-
850
- // ============================================================
851
- // 多数据库
852
- // ============================================================
853
-
854
- /**
855
- * 附加另一个数据库(跨库查询)
856
- */
857
- attach(name, db) {
858
- if (this._attached[name]) throw createError('ER_DBATTACH_EXISTS', name);
859
- this._attached[name] = db;
860
-
861
- Object.defineProperty(this, name, {
862
- get: () => db,
863
- enumerable: true,
864
- configurable: true
865
- });
866
- }
867
-
868
- detach(name) {
869
- if (!this._attached[name]) throw createError('ER_DBATTACH_NOT_FOUND', name);
870
- delete this._attached[name];
871
- delete this[name];
872
- }
873
-
874
- getAttached() {
875
- return Object.keys(this._attached);
876
- }
877
-
878
- // ============================================================
879
- // 事务(支持隔离级别)
880
- // ============================================================
881
-
882
- /**
883
- * 开始事务
884
- * @param {string} isolationLevel - 覆盖默认隔离级别(可选)
885
- */
886
- begin(isolationLevel) {
887
- if (this._transaction) throw createError('ER_TRANSACTION_ACTIVE');
888
-
889
- const level = isolationLevel || this._isolationLevel;
890
-
891
- this._transaction = {
892
- rows: {},
893
- autoIncrements: {},
894
- level,
895
- startedAt: Date.now()
896
- };
897
-
898
- if (level === 'REPEATABLE_READ') {
899
- // 快照:保存当前所有数据(深拷贝,保证嵌套对象可回滚)
900
- for (const [name, table] of Object.entries(this._tables)) {
901
- this._transaction.rows[name] = JSON.parse(JSON.stringify(table._rows));
902
- this._transaction.autoIncrements[name] = table._autoIncrement;
903
- }
904
- } else {
905
- // READ_COMMITTED:只保存 autoIncrement
906
- for (const [name, table] of Object.entries(this._tables)) {
907
- this._transaction.autoIncrements[name] = table._autoIncrement;
908
- }
909
- }
910
-
911
- this._walWrite({ op: 'begin', level });
912
- }
913
-
914
- commit() {
915
- if (!this._transaction) throw createError('ER_NO_TRANSACTION');
916
- this._walWrite({ op: 'commit', at: this._transaction.startedAt });
917
- this._transaction = null;
918
- this._checkpoint();
919
- this._markDirtyLegacy();
920
- }
921
-
922
- rollback() {
923
- if (!this._transaction) throw createError('ER_NO_TRANSACTION');
924
-
925
- for (const [name, table] of Object.entries(this._tables)) {
926
- if (this._transaction.rows[name]) {
927
- table._rows = this._transaction.rows[name];
928
- table._autoIncrement = this._transaction.autoIncrements[name];
929
- table._rebuildAllBTrees();
930
- for (const field of Object.keys(table._indexes)) {
931
- table.createIndex(field);
932
- }
933
- } else if (this._transaction.autoIncrements[name] !== undefined) {
934
- table._autoIncrement = this._transaction.autoIncrements[name];
935
- }
936
- }
937
-
938
- this._walWrite({ op: 'rollback', at: this._transaction.startedAt });
939
- this._transaction = null;
940
- }
941
-
942
- inTransaction() {
943
- return !!this._transaction;
944
- }
945
-
946
- /**
947
- * 获取当前事务隔离级别
948
- */
949
- getIsolationLevel() {
950
- return this._transaction ? this._transaction.level : this._isolationLevel;
951
- }
952
-
953
- /**
954
- * 设置默认事务隔离级别
955
- * @param {'READ_COMMITTED'|'REPEATABLE_READ'} level
956
- */
957
- setIsolationLevel(level) {
958
- if (!['READ_COMMITTED', 'REPEATABLE_READ'].includes(level)) {
959
- throw createError('ER_NOT_SUPPORTED', `Isolation level '${level}' is not supported. Use READ_COMMITTED or REPEATABLE_READ.`);
960
- }
961
- this._isolationLevel = level;
962
- }
963
-
964
- // ============================================================
965
- // 版本历史
966
- // ============================================================
967
-
968
- /**
969
- * 保存当前快照
970
- */
971
- snapshot(tableName) {
972
- if (!this._versioning) return;
973
- const table = this._tables[tableName];
974
- if (!table) return;
975
- if (!this._versions[tableName]) this._versions[tableName] = [];
976
-
977
- if (table._rows.length === 0) return;
978
-
979
- const currentHash = JSON.stringify(table._rows);
980
- const versions = this._versions[tableName];
981
- if (versions.length > 0 && versions[versions.length - 1]._hash === currentHash) return;
982
-
983
- versions.push({
984
- rows: table._rows.map(r => ({ ...r })),
985
- timestamp: Date.now(),
986
- _hash: currentHash
987
- });
988
- if (versions.length > this._maxVersions) {
989
- versions.shift();
990
- }
991
- }
992
-
993
- /**
994
- * 回退到指定版本
995
- */
996
- undo(tableName, steps = 1) {
997
- if (!this._versioning) throw createError('ER_NOT_SUPPORTED', 'Versioning is not enabled');
998
- const versions = this._versions[tableName];
999
- if (!versions || versions.length < 2) throw createError('ER_NOT_SUPPORTED', 'No versions to undo');
1000
- const table = this._ensureTable(tableName);
1001
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1002
-
1003
- for (let i = 0; i < steps && versions.length > 1; i++) {
1004
- versions.pop();
1005
- const snapshot = versions[versions.length - 1];
1006
- table._rows = snapshot.rows.map(r => ({ ...r }));
1007
- table._autoIncrement = 0;
1008
- for (const row of table._rows) {
1009
- if (table._autoIncrementField && row[table._autoIncrementField] > table._autoIncrement) {
1010
- table._autoIncrement = row[table._autoIncrementField];
1011
- }
1012
- }
1013
- table._rebuildAllBTrees();
1014
- for (const field of Object.keys(table._indexes)) {
1015
- table.createIndex(field);
1016
- }
1017
- }
1018
- this._walWrite({ op: 'undo', table: tableName, steps });
1019
- this._markDirtyLegacy();
1020
- return versions.length;
1021
- }
1022
-
1023
- getVersions(tableName) {
1024
- return this._versions[tableName] || [];
1025
- }
1026
-
1027
- // ============================================================
1028
- // 变更流 & 响应式
1029
- // ============================================================
1030
-
1031
- /**
1032
- * 监听变更事件
1033
- * @param {function} callback - (event) => {} event: { type, table, data, timestamp }
1034
- * @returns {function} 取消监听的函数
1035
- */
1036
- onChange(callback) {
1037
- this._observers.push(callback);
1038
- return () => {
1039
- this._observers = this._observers.filter(cb => cb !== callback);
1040
- };
1041
- }
1042
-
1043
- _emitChange(type, table, data) {
1044
- const event = { type, table, data, timestamp: Date.now() };
1045
- for (const cb of this._observers) {
1046
- try { cb(event); } catch (e) { /* ignore */ }
1047
- }
1048
- }
1049
-
1050
- /**
1051
- * 创建变更流(返回异步迭代器)
1052
- */
1053
- createChangeStream() {
1054
- const events = [];
1055
- let resolve = null;
1056
- const unsubscribe = this.onChange(event => {
1057
- events.push(event);
1058
- if (resolve) {
1059
- resolve();
1060
- resolve = null;
1061
- }
1062
- });
1063
-
1064
- return {
1065
- [Symbol.asyncIterator]() {
1066
- return {
1067
- next: async () => {
1068
- while (events.length === 0) {
1069
- await new Promise(r => resolve = r);
1070
- }
1071
- return { value: events.shift(), done: false };
1072
- }
1073
- };
1074
- },
1075
- close: unsubscribe
1076
- };
1077
- }
1078
-
1079
- // ============================================================
1080
- // 插件系统
1081
- // ============================================================
1082
-
1083
- /**
1084
- * 注册插件
1085
- * @param {object|function} plugin - { name, install(db, ctx), hooks, onEvent } 或函数 (作为 install)
1086
- */
1087
- use(plugin) {
1088
- if (typeof plugin === 'function') {
1089
- plugin = { install: plugin };
1090
- }
1091
- if (plugin.hooks) {
1092
- for (const [event, fn] of Object.entries(plugin.hooks)) {
1093
- if (this._hooks[event]) {
1094
- this._hooks[event].push(fn);
1095
- }
1096
- }
1097
- }
1098
- if (typeof plugin.install === 'function') {
1099
- plugin.install(this, this._buildCtx(plugin));
1100
- }
1101
- if (typeof plugin.onEvent === 'function') {
1102
- this._eventListeners.push(plugin.onEvent);
1103
- }
1104
- this._plugins.push(plugin);
1105
- return this;
1106
- }
1107
-
1108
- _buildCtx(plugin) {
1109
- const self = this;
1110
- return {
1111
- name: plugin.name || 'anonymous',
1112
- engine: this,
1113
- plugin,
1114
- on(hook, fn) { return self.on(hook, fn); },
1115
- onEvent(fn) { return self.onEvent(fn); },
1116
- emit(eventName, data) { self._emit(eventName, data); },
1117
- tables() { return Array.from(self._tableNames); },
1118
- hasTable(name) { return self._tableNames.has(name); },
1119
- getTableSchema(name) {
1120
- const t = self._tables[name];
1121
- return t ? t._schema : null;
1122
- },
1123
- table(name) { return self._tables[name] || null; }
1124
- };
1125
- }
1126
-
1127
- on(event, fn) {
1128
- if (this._hooks[event]) {
1129
- this._hooks[event].push(fn);
1130
- }
1131
- return this;
1132
- }
1133
-
1134
- onEvent(fn) {
1135
- this._eventListeners.push(fn);
1136
- return this;
1137
- }
1138
-
1139
- _emit(eventName, data) {
1140
- for (const fn of this._eventListeners) {
1141
- try { fn(eventName, data); } catch (e) { /* ignore */ }
1142
- }
1143
- }
1144
-
1145
- _runHooks(hookName, args) {
1146
- const hooks = this._hooks[hookName];
1147
- if (!hooks || hooks.length === 0) return true;
1148
- for (const fn of hooks) {
1149
- const r = fn(...args);
1150
- if (r === false) return false;
1151
- if (r !== undefined && args.length > 0) {
1152
- args[0] = r;
1153
- }
1154
- }
1155
- return true;
1156
- }
1157
-
1158
- // ============================================================
1159
- // 统一高层 API(与 Native/WASM 引擎对齐)
1160
- // ============================================================
1161
-
1162
- _pkOf(table) {
1163
- return table._primaryKey || table._autoIncrementField || null;
1164
- }
1165
-
1166
- async start() {
1167
- this._runHooks('onStart', []);
1168
- this._emit('start', {});
1169
- if (this._dirMode && !this._sigInstalled) {
1170
- this._sigInstalled = true;
1171
- Database._installSignalHandler(this);
1172
- }
1173
- return this;
1174
- }
1175
-
1176
- /**
1177
- * 进程退出时兜底刷盘:拦截 SIGINT/SIGTERM,同步落盘脏表后再退出。
1178
- * 使用静态共享 handler,避免多个数据库实例重复注册/互相覆盖。
1179
- * @private
1180
- */
1181
- static _installSignalHandler(db) {
1182
- if (!Database._signalHandlerInstalled) {
1183
- Database._signalHandlerInstalled = true;
1184
- Database._signalDBs = new Set();
1185
- const handler = (sig) => {
1186
- for (const d of Database._signalDBs) {
1187
- try {
1188
- if (d._dirMode) {
1189
- if (d._flushTimer) { clearTimeout(d._flushTimer); d._flushTimer = null; }
1190
- d._flushDirty();
1191
- d._saveMeta();
1192
- }
1193
- } catch (e) { /* 兜底失败不阻塞退出 */ }
1194
- }
1195
- process.exit(128 + (sig === 'SIGINT' ? 2 : 15));
1196
- };
1197
- process.on('SIGINT', handler);
1198
- process.on('SIGTERM', handler);
1199
- Database._signalHandler = handler;
1200
- }
1201
- Database._signalDBs.add(db);
1202
- }
1203
-
1204
- /** 从进程退出兜底集合移除(stop 时调用) */
1205
- static _uninstallSignalHandler(db) {
1206
- if (Database._signalDBs) Database._signalDBs.delete(db);
1207
- }
1208
-
1209
- insert(tableName, data) {
1210
- const table = this._ensureTable(tableName);
1211
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1212
- const arr = Array.isArray(data) ? data : [data];
1213
- if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
1214
- const pk = this._pkOf(table);
1215
- const ids = [];
1216
- for (const row of arr) {
1217
- const r = table.insert(row);
1218
- ids.push(pk ? r[pk] : table._rows.length);
1219
- }
1220
- this._emit('insert', { table: tableName, count: arr.length, ids });
1221
- this._runHooks('afterInsert', [tableName, arr, ids]);
1222
- this._markDirty(tableName);
1223
- return ids;
1224
- }
1225
-
1226
- async flush() {
1227
- if (!this._runHooks('beforeFlush', [])) return;
1228
- if (this._dirMode) {
1229
- this._flushDirty();
1230
- this._saveMeta();
1231
- } else if (!this._memoryMode) {
1232
- this.save();
1233
- }
1234
- this._runHooks('afterFlush', []);
1235
- return this;
1236
- }
1237
-
1238
- _resolveId(table, id) {
1239
- const pk = this._pkOf(table);
1240
- if (pk) return table._rows.find(r => r[pk] === id) || null;
1241
- return table._rows[id - 1] || null;
1242
- }
1243
-
1244
- find(tableName, filter, opts = {}) {
1245
- const table = this._ensureTable(tableName);
1246
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1247
- if (!this._runHooks('beforeFind', [tableName, { filter, opts }])) return [];
1248
- const { limit, offset = 0 } = opts;
1249
- let rows = table._applyFilter(table._rows, filter || {});
1250
- if (limit > 0) rows = rows.slice(offset, offset + limit);
1251
- else if (offset > 0) rows = rows.slice(offset);
1252
- this._touchTable(tableName);
1253
- this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
1254
- return rows;
1255
- }
1256
-
1257
- findById(tableName, id) {
1258
- const table = this._ensureTable(tableName);
1259
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1260
- if (!this._runHooks('beforeFind', [tableName, { id }])) return null;
1261
- const row = this._resolveId(table, id);
1262
- this._touchTable(tableName);
1263
- this._runHooks('afterFind', [tableName, { id }, row]);
1264
- return row;
1265
- }
1266
-
1267
- findByIds(tableName, ids) {
1268
- const table = this._ensureTable(tableName);
1269
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1270
- if (!this._runHooks('beforeFind', [tableName, { ids }])) return null;
1271
- const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null);
1272
- this._touchTable(tableName);
1273
- this._runHooks('afterFind', [tableName, { ids }, rows]);
1274
- return rows;
1275
- }
1276
-
1277
- findByIdsRaw(tableName, ids) {
1278
- return this.findByIds(tableName, ids);
1279
- }
1280
-
1281
- count(tableName) {
1282
- const table = this._ensureTable(tableName);
1283
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1284
- this._runHooks('beforeCount', [tableName]);
1285
- const n = table._rows.length;
1286
- this._touchTable(tableName);
1287
- this._runHooks('afterCount', [tableName, n]);
1288
- return n;
1289
- }
1290
-
1291
- updateById(tableName, id, data) {
1292
- const table = this._ensureTable(tableName);
1293
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1294
- if (!this._runHooks('beforeUpdate', [tableName, id, data])) return;
1295
- const row = this._resolveId(table, id);
1296
- if (!row) return { ok: false, error: 'not found' };
1297
- Object.assign(row, data);
1298
- this._emit('update', { table: tableName, id, data });
1299
- this._runHooks('afterUpdate', [tableName, id, data, { ok: true }]);
1300
- this._markDirty(tableName);
1301
- return { ok: true };
1302
- }
1303
-
1304
- updateByIds(tableName, entries) {
1305
- const table = this._ensureTable(tableName);
1306
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1307
- const pairs = entries.map(([id, data]) => [id, data]);
1308
- if (!this._runHooks('beforeUpdate', [tableName, pairs])) return;
1309
- let count = 0;
1310
- for (const [id, data] of pairs) {
1311
- const row = this._resolveId(table, id);
1312
- if (row) { Object.assign(row, data); count++; }
1313
- }
1314
- this._emit('update', { table: tableName, entries: pairs, result: { ok: true, count } });
1315
- this._runHooks('afterUpdate', [tableName, pairs, { ok: true, count }]);
1316
- this._markDirty(tableName);
1317
- return { ok: true, count };
1318
- }
1319
-
1320
- removeById(tableName, id) {
1321
- const table = this._ensureTable(tableName);
1322
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1323
- if (!this._runHooks('beforeDelete', [tableName, id])) return;
1324
- if (table._primaryKey) {
1325
- const removed = table.removeById(id);
1326
- if (removed === 0) return { ok: false, error: 'not found' };
1327
- this._emit('delete', { table: tableName, id });
1328
- this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1329
- this._markDirty(tableName);
1330
- return { ok: true };
1331
- }
1332
- const idx = table._rows.findIndex(r => r === this._resolveId(table, id));
1333
- if (idx === -1) return { ok: false, error: 'not found' };
1334
- table._rows.splice(idx, 1);
1335
- table._rebuildPKIndex();
1336
- table._rebuildAllBTrees();
1337
- for (const field of Object.keys(table._indexes)) table.createIndex(field);
1338
- this._emit('delete', { table: tableName, id });
1339
- this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1340
- this._markDirty(tableName);
1341
- return { ok: true };
1342
- }
1343
-
1344
- removeByIds(tableName, ids) {
1345
- const table = this._ensureTable(tableName);
1346
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1347
- if (!this._runHooks('beforeDelete', [tableName, ids])) return;
1348
- let removed = 0;
1349
- if (table._primaryKey) {
1350
- for (const id of ids) removed += table.removeById(id);
1351
- } else {
1352
- for (const id of ids) {
1353
- const row = this._resolveId(table, id);
1354
- const idx = row ? table._rows.findIndex(r => r === row) : -1;
1355
- if (idx !== -1) {
1356
- table._rows.splice(idx, 1);
1357
- removed++;
1358
- }
1359
- }
1360
- table._rebuildPKIndex();
1361
- table._rebuildAllBTrees();
1362
- for (const field of Object.keys(table._indexes)) table.createIndex(field);
1363
- }
1364
- this._emit('delete', { table: tableName, ids, result: { ok: true, count: removed } });
1365
- this._runHooks('afterDelete', [tableName, ids, { ok: true, count: removed }]);
1366
- this._markDirty(tableName);
1367
- return { ok: true, count: removed };
1368
- }
1369
-
1370
- removeWhere(tableName, filter) {
1371
- const table = this._ensureTable(tableName);
1372
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1373
- if (!this._runHooks('beforeDelete', [tableName, [filter]])) return { ok: true, count: 0 };
1374
- const before = table._rows.length;
1375
- const matched = table._applyFilter(table._rows, filter || {});
1376
- const matchSet = new Set(matched);
1377
- table._rows = table._rows.filter(r => !matchSet.has(r));
1378
- const removed = before - table._rows.length;
1379
- table._rebuildPKIndex();
1380
- table._rebuildAllBTrees();
1381
- for (const field of Object.keys(table._indexes)) table.createIndex(field);
1382
- this._emit('delete', { table: tableName, ids: [], result: { ok: true, count: removed } });
1383
- this._runHooks('afterDelete', [tableName, [filter], { ok: true, count: removed }]);
1384
- this._markDirty(tableName);
1385
- return { ok: true, count: removed };
1386
- }
1387
-
1388
- insertMany(tableName, items) {
1389
- const table = this._ensureTable(tableName);
1390
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1391
- const arr = Array.isArray(items) ? items : [items];
1392
- if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
1393
- const inserted = table.insertMany(arr);
1394
- this._emit('insert', { table: tableName, count: arr.length });
1395
- this._runHooks('afterInsert', [tableName, arr, inserted]);
1396
- this._markDirty(tableName);
1397
- return inserted;
1398
- }
1399
-
1400
- update(tableName, query, updates) {
1401
- const table = this._ensureTable(tableName);
1402
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1403
- if (!this._runHooks('beforeUpdate', [tableName, query, updates])) return 0;
1404
- const count = table.update(query, updates);
1405
- this._emit('update', { table: tableName, query, count });
1406
- this._runHooks('afterUpdate', [tableName, query, updates, count]);
1407
- this._markDirty(tableName);
1408
- return count;
1409
- }
1410
-
1411
- delete(tableName, query = {}) {
1412
- const table = this._ensureTable(tableName);
1413
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1414
- if (!this._runHooks('beforeDelete', [tableName, query])) return 0;
1415
- const count = table.remove(query);
1416
- this._emit('delete', { table: tableName, query, count });
1417
- this._runHooks('afterDelete', [tableName, query, count]);
1418
- this._markDirty(tableName);
1419
- return count;
1420
- }
1421
-
1422
- truncate(tableName) {
1423
- const table = this._ensureTable(tableName);
1424
- if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1425
- const count = table.truncate();
1426
- this._emit('delete', { table: tableName, count });
1427
- this._markDirty(tableName);
1428
- return count;
1429
- }
1430
-
1431
- async stop() {
1432
- this._runHooks('onStop', []);
1433
- this._emit('stop', {});
1434
- if (this._dirMode) {
1435
- if (this._flushTimer) { clearTimeout(this._flushTimer); this._flushTimer = null; }
1436
- try {
1437
- this._flushDirty();
1438
- this._saveMeta();
1439
- } catch (e) {}
1440
- }
1441
- if (this._sigInstalled) {
1442
- this._sigInstalled = false;
1443
- Database._uninstallSignalHandler(this);
1444
- }
1445
- return this;
1446
- }
1447
-
1448
- // ============================================================
1449
- // 迁移
1450
- // ============================================================
1451
-
1452
- /**
1453
- * 注册迁移
1454
- * @param {object} migration - { version, name, up(db), down(db) }
1455
- */
1456
- addMigration(migration) {
1457
- this._migrations.push(migration);
1458
- this._migrations.sort((a, b) => a.version - b.version);
1459
- }
1460
-
1461
- /**
1462
- * 执行迁移到指定版本
1463
- */
1464
- migrate(targetVersion) {
1465
- const current = this._getCurrentVersion();
1466
- if (targetVersion === undefined) {
1467
- targetVersion = this._migrations.length > 0
1468
- ? this._migrations[this._migrations.length - 1].version
1469
- : 0;
1470
- }
1471
-
1472
- if (targetVersion > current) {
1473
- for (const m of this._migrations) {
1474
- if (m.version > current && m.version <= targetVersion) {
1475
- m.up(this);
1476
- this._setCurrentVersion(m.version);
1477
- }
1478
- }
1479
- } else if (targetVersion < current) {
1480
- for (const m of [...this._migrations].reverse()) {
1481
- if (m.version <= current && m.version > targetVersion) {
1482
- m.down(this);
1483
- this._setCurrentVersion(m.version - 1);
1484
- }
1485
- }
1486
- }
1487
- }
1488
-
1489
- _getCurrentVersion() {
1490
- return this._tables['__meta__']
1491
- ? (this._tables['__meta__'].findOne({ key: 'version' })?.value || 0)
1492
- : 0;
1493
- }
1494
-
1495
- _setCurrentVersion(version) {
1496
- if (!this._tables['__meta__']) {
1497
- this.createTable('__meta__', {
1498
- key: { type: 'string', primaryKey: true },
1499
- value: { type: 'any' }
1500
- });
1501
- }
1502
- this._tables['__meta__'].upsert({ key: 'version', value: version });
1503
- }
1504
-
1505
- // ============================================================
1506
- // 备份与恢复
1507
- // ============================================================
1508
-
1509
- backup(filePath) {
1510
- const data = this.export();
1511
- const json = this._options.pretty
1512
- ? JSON.stringify(data, null, 2)
1513
- : JSON.stringify(data);
1514
- this._writeFile(filePath, json);
1515
- return filePath;
1516
- }
1517
-
1518
- restore(filePath) {
1519
- if (!fs.existsSync(filePath)) {
1520
- throw createError('ER_FILE_NOT_FOUND', filePath);
1521
- }
1522
- const json = this._readFile(filePath);
1523
- const data = JSON.parse(json);
1524
- for (const [name, rows] of Object.entries(data)) {
1525
- if (this._tables[name]) {
1526
- this._tables[name]._loadRows(rows);
1527
- }
1528
- }
1529
- this._walWrite({ op: 'restore', from: filePath });
1530
- this._markDirtyLegacy();
1531
- }
1532
-
1533
- // ============================================================
1534
- // 持久化
1535
- // ============================================================
1536
-
1537
- save() {
1538
- // 版本快照
1539
- if (this._versioning) {
1540
- for (const name of Object.keys(this._tables)) {
1541
- this.snapshot(name);
1542
- }
1543
- }
1544
-
1545
- if (this._memoryMode) return;
1546
- if (this._dirMode) {
1547
- this._flushDirty();
1548
- this._saveMeta();
1549
- return;
1550
- }
1551
-
1552
- if (this._jsqlMode) {
1553
- const all = {};
1554
- for (const [name, table] of Object.entries(this._tables)) {
1555
- if (name === '__meta__') continue;
1556
- const pkField = Object.keys(table._schema).find(f => table._schema[f].primaryKey);
1557
- all[name] = { schema: table._schema, rows: table._rows, pkField };
1558
- }
1559
- this._jsqlFormat.saveAll(all);
1560
- this._jsqlFormat._close();
1561
- this._dirty = false;
1562
- this._checkpoint();
1563
- return;
1564
- }
1565
-
1566
- const data = { __schema__: {}, __meta__: { version: this._getCurrentVersion() } };
1567
- for (const [name, table] of Object.entries(this._tables)) {
1568
- if (name === '__meta__') continue;
1569
- data.__schema__[name] = table._schema;
1570
- data[name] = table.toJSON();
1571
- }
1572
-
1573
- const json = this._options.pretty
1574
- ? JSON.stringify(data, null, 2)
1575
- : JSON.stringify(data);
1576
- this._writeFile(this._filePath, json);
1577
- this._dirty = false;
1578
-
1579
- // 保存后做检查点
1580
- this._checkpoint();
1581
- }
1582
-
1583
- _load() {
1584
- try {
1585
- if (this._jsqlMode) {
1586
- this._jsqlFormat._open();
1587
- for (const [name, t] of this._jsqlFormat.tables) {
1588
- if (!this._tables[name]) this.createTable(name, t.schema);
1589
- if (t.rowCount > 0) {
1590
- const { rows } = this._jsqlFormat.readTableSync(name);
1591
- this._tables[name]._loadRows(rows);
1592
- }
1593
- }
1594
- return;
1595
- }
1596
-
1597
- const json = this._readFile(this._filePath);
1598
- const data = JSON.parse(json);
1599
-
1600
- if (data.__schema__) {
1601
- for (const [name, schema] of Object.entries(data.__schema__)) {
1602
- if (!this._tables[name]) {
1603
- this.createTable(name, schema);
1604
- }
1605
- }
1606
- }
1607
-
1608
- for (const [name, rows] of Object.entries(data)) {
1609
- if (name !== '__schema__' && name !== '__meta__' && this._tables[name]) {
1610
- this._tables[name]._loadRows(rows);
1611
- }
1612
- }
1613
-
1614
- // WAL 恢复
1615
- this._recoverFromWAL();
1616
- } catch (e) {
1617
- throw new Error(`Failed to load database from '${this._filePath}': ${e.message}`);
1618
- }
1619
- }
1620
-
1621
- _markDirtyLegacy() {
1622
- this._dirty = true;
1623
- if (this._loading) return;
1624
- if (this._options.autoSave && !this._autoSaveTimer) {
1625
- if (this._options.autoSaveInterval > 0) {
1626
- this._autoSaveTimer = setTimeout(() => {
1627
- this._autoSaveTimer = null;
1628
- if (this._dirty) this.save();
1629
- }, this._options.autoSaveInterval);
1630
- } else {
1631
- this.save();
1632
- }
1633
- }
1634
- }
1635
-
1636
- close() {
1637
- if (this._autoSaveTimer) {
1638
- clearTimeout(this._autoSaveTimer);
1639
- this._autoSaveTimer = null;
1640
- }
1641
- if (this._dirMode) {
1642
- try {
1643
- this._flushDirty();
1644
- this._saveMeta();
1645
- } catch (e) {
1646
- this._emit('error', e);
1647
- }
1648
- } else if (this._dirty && !this._memoryMode) {
1649
- this.save();
1650
- }
1651
- if (this._jsqlFormat) this._jsqlFormat._close();
1652
- this._releaseLock();
1653
- this._tables = {};
1654
- }
1655
-
1656
- export() {
1657
- const data = {};
1658
- for (const [name, table] of Object.entries(this._tables)) {
1659
- if (name === '__meta__') continue;
1660
- data[name] = table.toJSON();
1661
- }
1662
- return data;
1663
- }
1664
-
1665
- import(data) {
1666
- for (const [name, rows] of Object.entries(data)) {
1667
- if (this._tables[name]) {
1668
- this._tables[name]._loadRows(rows);
1669
- }
1670
- }
1671
- this._walWrite({ op: 'import', tables: Object.keys(data) });
1672
- this._markDirtyLegacy();
1673
- }
1674
-
1675
- async beginTx() {
1676
- this._txSnapshot = {};
1677
- for (const [name, table] of Object.entries(this._tables)) {
1678
- this._txSnapshot[name] = { rows: JSON.parse(JSON.stringify(table.toJSON())), schema: table._schema };
1679
- }
1680
- this._txId = (this._txId || 0) + 1;
1681
- return this._txId;
1682
- }
1683
-
1684
- async commitTx() {
1685
- this._txSnapshot = null;
1686
- this._txId = undefined;
1687
- if (this._dirMode) {
1688
- try { this._flushDirty(); } catch (e) {}
1689
- } else if (!this._memoryMode) {
1690
- this.save();
1691
- }
1692
- }
1693
-
1694
- async rollbackTx() {
1695
- const snap = this._txSnapshot;
1696
- this._txSnapshot = null;
1697
- this._txId = undefined;
1698
- if (!snap) return;
1699
- for (const name of Object.keys(this._tables)) {
1700
- if (!snap[name]) delete this._tables[name];
1701
- }
1702
- for (const [name, entry] of Object.entries(snap)) {
1703
- if (this._tables[name]) {
1704
- this._tables[name]._loadRows(entry.rows);
1705
- } else {
1706
- const table = new Table(name, entry.schema, this);
1707
- table._loadRows(entry.rows);
1708
- this._tables[name] = table;
1709
- }
1710
- }
1711
- this._dirty = true;
1712
- }
1713
-
1714
- // ============================================================
1715
- // 加密(内部)
1716
- // ============================================================
1717
-
1718
- _writeFile(filePath, content) {
1719
- if (this._encryptKey) {
1720
- const iv = crypto.randomBytes(16);
1721
- const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(this._encryptKey.padEnd(32).slice(0, 32)), iv);
1722
- const encrypted = Buffer.concat([cipher.update(content, 'utf8'), cipher.final()]);
1723
- const data = Buffer.concat([iv, encrypted]);
1724
- fs.writeFileSync(filePath, data.toString('base64'), 'utf8');
1725
- } else {
1726
- const dir = path.dirname(filePath);
1727
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1728
- fs.writeFileSync(filePath, content, 'utf8');
1729
- }
1730
- }
1731
-
1732
- _readFile(filePath) {
1733
- if (this._encryptKey) {
1734
- const data = Buffer.from(fs.readFileSync(filePath, 'utf8'), 'base64');
1735
- const iv = data.slice(0, 16);
1736
- const encrypted = data.slice(16);
1737
- const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this._encryptKey.padEnd(32).slice(0, 32)), iv);
1738
- return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
1739
- }
1740
- return fs.readFileSync(filePath, 'utf8');
1741
- }
1742
- }
1743
-
1744
- module.exports = Database;
1
+ // © Vexify 2026 All Rights Reserved.
2
+ /**
3
+ * JSQL Database — 数据库管理器 v2.0
4
+ * WAL、文件锁、事务隔离级别、慢查询日志、错误码体系
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+ const crypto = require('crypto');
11
+ const Table = require('./table');
12
+ const { createError } = require('./errors');
13
+ const JSQLFormat = require('./jsql_format');
14
+
15
+ /**
16
+ * 解析字段简写类型字符串,如 'integer primary key'、'string unique'、
17
+ * 'integer primary key auto_increment'、'datetime default now'
18
+ * @param {string} str - 简写字符串(已小写化)
19
+ * @returns {object} { type, primaryKey, unique, autoIncrement, required, ... }
20
+ */
21
+ function parseFieldShorthand(str) {
22
+ const def = { type: str };
23
+ const strip = (re) => {
24
+ const next = def.type.replace(re, '').trim();
25
+ def.type = next || def.type;
26
+ };
27
+ if (/\bprimary\s+key\b/i.test(def.type)) { def.primaryKey = true; strip(/\bprimary\s+key\b/gi); }
28
+ if (/\bauto_?increment\b/i.test(def.type)) { def.autoIncrement = true; strip(/\bauto_?increment\b/gi); }
29
+ if (/\bnot\s+null\b/i.test(def.type)) { def.required = true; strip(/\bnot\s+null\b/gi); }
30
+ if (/\bunique\b/i.test(def.type)) { def.unique = true; strip(/\bunique\b/gi); }
31
+ if (/\bdefault\s+(\S+)/i.test(def.type)) {
32
+ const m = def.type.match(/\bdefault\s+(\S+)/i);
33
+ def.default = m[1].replace(/^['"]|['"]$/g, '');
34
+ strip(/\bdefault\s+\S+/gi);
35
+ }
36
+ return def;
37
+ }
38
+
39
+ class Database {
40
+ /**
41
+ * @param {string} filePath - 数据库文件/目录路径,传 null 或 ':memory:' 使用内存模式
42
+ * @param {object} options
43
+ * @param {string} options.mode - 存储模式:
44
+ * 'memory'(默认):纯内存,不落盘
45
+ * 'hybrid'(混合,Redis 式):先写内存,后台异步慢慢刷盘;内存充足(默认预留 0.5GB 余量)
46
+ * 则数据常驻内存作为缓存;内存紧张时按 LRU 驱逐最冷表(已落盘),查询时自动从磁盘加载
47
+ * 'disk'(磁盘模式):写入后尽快刷盘,内存只做读写缓存
48
+ * @param {number} options.memReserveMB - 内存余量 MB(默认 512,即预留 0.5GB)
49
+ * @param {number} options.flushInterval - 脏表后台刷盘间隔 ms(默认 hybrid 200 / disk 50,0 = 每次写后立即刷)
50
+ * @param {number} options.evictInterval - 内存压力检查间隔 ms(默认 1000)
51
+ * @param {boolean} options.autoSave - 是否自动保存(默认 true,仅旧文件模式)
52
+ * @param {number} options.autoSaveInterval - 自动保存间隔 ms(默认 0,仅旧文件模式)
53
+ * @param {boolean} options.pretty - JSON 是否格式化(默认 true,仅旧文件模式)
54
+ * @param {string} options.encryptKey - 加密密钥(16/24/32 字节),启用 AES-256-CBC
55
+ * @param {boolean} options.versioning - 是否启用版本历史(默认 false)
56
+ * @param {boolean} options.wal - 是否启用 WAL 模式(默认 false,内存模式自动禁用)
57
+ * @param {string} options.isolationLevel - 事务隔离级别 'READ_COMMITTED' | 'REPEATABLE_READ'(默认 'READ_COMMITTED')
58
+ * @param {number} options.slowQueryThreshold - 慢查询阈值 ms(默认 100,0 记录全部,<0 禁用)
59
+ * @param {boolean} options.fileLock - 是否启用文件锁(默认 false)
60
+ */
61
+ constructor(filePath, options = {}) {
62
+ this._filePath = filePath;
63
+ this._memoryMode = !filePath || filePath === ':memory:';
64
+ this._options = {
65
+ autoSave: options.autoSave !== false,
66
+ autoSaveInterval: options.autoSaveInterval || 0,
67
+ pretty: options.pretty !== false
68
+ };
69
+
70
+ // 存储模式: 'memory' | 'hybrid' | 'disk' | null(旧文件模式: 全量 save)
71
+ this._mode = options.mode || (this._memoryMode ? 'memory' : null);
72
+ this._dirMode = this._mode === 'hybrid' || this._mode === 'disk';
73
+ this._memReserve = (options.memReserveMB !== undefined ? options.memReserveMB : 512) * 1024 * 1024;
74
+ this._flushInterval = options.flushInterval !== undefined
75
+ ? options.flushInterval
76
+ : (this._mode === 'disk' ? 50 : 200);
77
+ this._evictInterval = options.evictInterval !== undefined ? options.evictInterval : 1000;
78
+ this._dirtyTables = new Set();
79
+ this._flushTimer = null;
80
+ this._monitorTimer = null;
81
+ this._lastAccess = {};
82
+ this._meta = { version: 1, tables: {} };
83
+
84
+ this._tables = {};
85
+ this._dirty = false;
86
+ this._autoSaveTimer = null;
87
+ this._loading = false;
88
+ this._transaction = null;
89
+ this._encryptKey = options.encryptKey || null;
90
+ this._versioning = options.versioning || false;
91
+ this._versions = {}; // tableName -> [{ rows, timestamp }]
92
+ this._maxVersions = options.maxVersions || 50;
93
+ this._plugins = [];
94
+ this._hooks = {
95
+ beforeInsert: [], afterInsert: [],
96
+ beforeUpdate: [], afterUpdate: [],
97
+ beforeDelete: [], afterDelete: [],
98
+ beforeFind: [], afterFind: [],
99
+ beforeCreateTable: [], afterCreateTable: [],
100
+ beforeDropTable: [], afterDropTable: [],
101
+ beforeFlush: [], afterFlush: [],
102
+ beforeCount: [], afterCount: [],
103
+ onStart: [], onStop: []
104
+ };
105
+ this._eventListeners = [];
106
+ this._tableNames = new Set(Object.keys(this._tables));
107
+ this._migrations = [];
108
+ this._observers = []; // 响应式观察者
109
+ this._views = {}; // 视图 { name: { query, table } }
110
+ this._triggers = {}; // 触发器 { name: { event, table, callback } }
111
+ this._attached = {}; // 附加数据库 { name: db }
112
+ this._changeStream = null; // 变更流
113
+
114
+ // WAL(Write-Ahead Logging)
115
+ this._walEnabled = options.wal === true && !this._memoryMode;
116
+ this._walPath = this._filePath ? this._filePath + '.wal' : null;
117
+ this._walOps = []; // WAL 操作日志
118
+
119
+ // 事务隔离:默认 REPEATABLE_READ(begin 时保存行快照,rollback 可回滚)
120
+ this._isolationLevel = options.isolationLevel || 'REPEATABLE_READ';
121
+
122
+ // JSql 格式 (binary block-based)
123
+ this._jsqlMode = this._filePath && this._filePath.endsWith('.jsql');
124
+ this._jsqlFormat = this._jsqlMode ? new JSQLFormat(this._filePath) : null;
125
+
126
+ // 文件锁
127
+ this._fileLockEnabled = options.fileLock === true && !this._memoryMode;
128
+ this._lockFd = null;
129
+ this._lockPath = this._filePath ? this._filePath + '.lock' : null;
130
+
131
+ // 慢查询日志
132
+ this._slowQueryThreshold = options.slowQueryThreshold !== undefined ? options.slowQueryThreshold : 100;
133
+ this._slowQueries = [];
134
+
135
+ // 从文件加载
136
+ if (!this._memoryMode && !this._dirMode && fs.existsSync(this._filePath)) {
137
+ this._loading = true;
138
+ this._acquireLock();
139
+ this._load();
140
+ this._loading = false;
141
+ }
142
+
143
+ // 磁盘/混合模式: 目录存储(每表独立 JSql 文件,增量刷盘)
144
+ if (this._dirMode) {
145
+ this._initDirStore();
146
+ }
147
+ }
148
+
149
+ // ============================================================
150
+ // 磁盘/混合存储(Redis 式: 内存缓存 + 异步刷盘 + LRU 驱逐)
151
+ // ============================================================
152
+
153
+ _initDirStore() {
154
+ if (!this._filePath) throw new Error('hybrid/disk mode requires a directory path');
155
+ fs.mkdirSync(this._filePath, { recursive: true });
156
+ this._metaPath = path.join(this._filePath, 'meta.json');
157
+ this._meta = { version: 1, tables: {} };
158
+ try {
159
+ if (fs.existsSync(this._metaPath)) {
160
+ const parsed = JSON.parse(fs.readFileSync(this._metaPath, 'utf8'));
161
+ if (parsed && parsed.tables) {
162
+ this._meta = parsed;
163
+ // 清洗 meta 中的 file 字段:拒绝绝对路径/.. 穿越,防恶意 meta.json 写任意位置
164
+ for (const name of Object.keys(this._meta.tables)) {
165
+ const meta = this._meta.tables[name];
166
+ if (meta && meta.file !== undefined) {
167
+ if (this._safeTableFile(meta.file) === null) {
168
+ delete this._meta.tables[name];
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ } catch (e) {
175
+ this._meta = { version: 1, tables: {} };
176
+ }
177
+ for (const name of Object.keys(this._meta.tables)) {
178
+ if (this[name] === undefined && !(name in this)) {
179
+ Object.defineProperty(this, name, {
180
+ get: () => this._ensureTable(name) || this._tables[name],
181
+ enumerable: true,
182
+ configurable: true
183
+ });
184
+ }
185
+ }
186
+ this._startMonitor();
187
+ }
188
+
189
+ _tableFile(name) {
190
+ const meta = this._meta.tables[name];
191
+ if (meta && meta.file) {
192
+ const safe = this._safeTableFile(meta.file);
193
+ if (safe) return path.join(this._filePath, safe);
194
+ }
195
+ return path.join(this._filePath, encodeURIComponent(name) + '.jsql');
196
+ }
197
+
198
+ _safeTableFile(file) {
199
+ if (typeof file !== 'string' || file.length === 0) return null;
200
+ if (path.isAbsolute(file)) return null;
201
+ if (file.includes('/') || file.includes('\\')) return null;
202
+ const base = path.basename(file);
203
+ if (base !== file || base === '.' || base === '..') return null;
204
+ if (file.includes('\0')) return null;
205
+ return file;
206
+ }
207
+
208
+ _saveMeta() {
209
+ if (!this._dirMode) return;
210
+ try {
211
+ fs.writeFileSync(this._metaPath, JSON.stringify(this._meta, null, 2), 'utf8');
212
+ } catch (e) {
213
+ // 忽略 meta 写入失败
214
+ }
215
+ }
216
+
217
+ _touchTable(name) {
218
+ this._lastAccess[name] = Date.now();
219
+ }
220
+
221
+ _ensureTable(name) {
222
+ if (this._tables[name]) return this._tables[name];
223
+ if (!this._dirMode) return null;
224
+ const meta = this._meta.tables[name];
225
+ if (!meta) return null;
226
+ const file = this._tableFile(name);
227
+ if (!fs.existsSync(file)) return null;
228
+ try {
229
+ const fmt = new JSQLFormat(file);
230
+ const { rows } = fmt.readTableSync(name);
231
+ const table = new Table(name, meta.schema, this);
232
+ if (rows && rows.length > 0) table._loadRows(rows);
233
+ this._tables[name] = table;
234
+ if (this._versioning) this._versions[name] = [];
235
+ this._touchTable(name);
236
+ return table;
237
+ } catch (e) {
238
+ return null;
239
+ }
240
+ }
241
+
242
+ _markDirty(name) {
243
+ if (!this._dirMode) {
244
+ this._dirty = true;
245
+ return;
246
+ }
247
+ this._touchTable(name);
248
+ this._dirtyTables.add(name);
249
+ this._scheduleFlush();
250
+ }
251
+
252
+ _scheduleFlush() {
253
+ if (this._flushTimer) return;
254
+ if (this._flushInterval <= 0) {
255
+ this._flushTimer = setImmediate(() => {
256
+ this._flushTimer = null;
257
+ try { this._flushDirty(); } catch (e) { this._emit('error', e); }
258
+ });
259
+ return;
260
+ }
261
+ this._flushTimer = setTimeout(() => {
262
+ this._flushTimer = null;
263
+ try { this._flushDirty(); } catch (e) { this._emit('error', e); }
264
+ }, this._flushInterval);
265
+ if (this._flushTimer.unref) this._flushTimer.unref();
266
+ }
267
+
268
+ _flushDirty() {
269
+ if (!this._dirMode || this._dirtyTables.size === 0) return;
270
+ const names = [...this._dirtyTables];
271
+ for (const name of names) {
272
+ try {
273
+ this._flushTable(name);
274
+ this._dirtyTables.delete(name);
275
+ } catch (e) {
276
+ // 保留脏标记,下次 flush 重试;错误对外传播而非静默
277
+ this._emit('error', e);
278
+ }
279
+ }
280
+ this._saveMeta();
281
+ }
282
+
283
+ _flushTable(name) {
284
+ const table = this._tables[name];
285
+ if (!table) return;
286
+ const meta = this._meta.tables[name];
287
+ let file = meta && meta.file ? meta.file : encodeURIComponent(name) + '.jsql';
288
+ const fmt = new JSQLFormat(path.join(this._filePath, file));
289
+ try {
290
+ const pkField = Object.keys(table._schema).find(f => table._schema[f].primaryKey) || null;
291
+ fmt.writeTable(name, table._schema, table._rows, pkField);
292
+ this._meta.tables[name] = { file, schema: table._schema };
293
+ } finally {
294
+ fmt._close();
295
+ }
296
+ }
297
+
298
+ _startMonitor() {
299
+ if (this._monitorTimer) return;
300
+ this._monitorTimer = setInterval(() => {
301
+ try { this._checkMemory(); } catch (e) {}
302
+ }, this._evictInterval);
303
+ if (this._monitorTimer.unref) this._monitorTimer.unref();
304
+ }
305
+
306
+ _checkMemory() {
307
+ const total = os.totalmem();
308
+ if (!total) return;
309
+ const budget = total - this._memReserve;
310
+ if (budget <= 0) return;
311
+ const rss = process.memoryUsage().rss;
312
+ if (rss <= budget) return;
313
+ this._evictTables(budget);
314
+ }
315
+
316
+ _evictTables(budget) {
317
+ const candidates = Object.keys(this._tables)
318
+ .filter(n => n !== '__meta__' && !this._versioning && this._meta.tables[n]);
319
+ candidates.sort((a, b) => (this._lastAccess[a] || 0) - (this._lastAccess[b] || 0));
320
+ for (const name of candidates) {
321
+ if (budget !== undefined && process.memoryUsage().rss <= budget) break;
322
+ try { this._flushTable(name); } catch (e) { continue; }
323
+ delete this._tables[name];
324
+ delete this[name];
325
+ this._dirtyTables.delete(name);
326
+ }
327
+ this._saveMeta();
328
+ }
329
+
330
+ // ============================================================
331
+ // 文件锁
332
+ // ============================================================
333
+
334
+ _acquireLock() {
335
+ if (!this._fileLockEnabled) return;
336
+ try {
337
+ // 检查锁文件是否存在
338
+ if (fs.existsSync(this._lockPath)) {
339
+ const lockTime = fs.statSync(this._lockPath).mtimeMs;
340
+ // 锁超过 30 秒视为过期
341
+ if (Date.now() - lockTime > 30000) {
342
+ fs.unlinkSync(this._lockPath);
343
+ } else {
344
+ throw createError('ER_LOCK_WAIT_TIMEOUT', 'Database is locked by another process');
345
+ }
346
+ }
347
+ fs.writeFileSync(this._lockPath, String(process.pid), 'utf8');
348
+ this._lockFd = true;
349
+ } catch (e) {
350
+ if (e.code === 'ER_LOCK_WAIT_TIMEOUT') throw e;
351
+ // 权限不足等,降级为无锁模式
352
+ this._fileLockEnabled = false;
353
+ }
354
+ }
355
+
356
+ _releaseLock() {
357
+ if (!this._fileLockEnabled || !this._lockFd) return;
358
+ try {
359
+ if (fs.existsSync(this._lockPath)) {
360
+ fs.unlinkSync(this._lockPath);
361
+ }
362
+ this._lockFd = null;
363
+ } catch (e) {
364
+ // 忽略
365
+ }
366
+ }
367
+
368
+ // ============================================================
369
+ // WAL(Write-Ahead Logging)
370
+ // ============================================================
371
+
372
+ /**
373
+ * 写入 WAL 日志
374
+ */
375
+ _walWrite(op) {
376
+ if (!this._walEnabled) return;
377
+ this._walOps.push({
378
+ ...op,
379
+ timestamp: Date.now(),
380
+ sequence: this._walOps.length + 1
381
+ });
382
+ this._flushWAL();
383
+ }
384
+
385
+ /**
386
+ * 将 WAL 刷入磁盘
387
+ */
388
+ _flushWAL() {
389
+ if (!this._walEnabled || this._walOps.length === 0) return;
390
+ try {
391
+ const dir = path.dirname(this._walPath);
392
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
393
+ fs.writeFileSync(this._walPath, JSON.stringify(this._walOps), 'utf8');
394
+ } catch (e) {
395
+ // WAL 写入失败不应阻塞主流程
396
+ }
397
+ }
398
+
399
+ /**
400
+ * 检查点:将 WAL 应用到数据文件并清空 WAL
401
+ */
402
+ _checkpoint() {
403
+ if (!this._walEnabled || this._walOps.length === 0) return;
404
+ // 所有操作已在内存中生效,此函数仅清除 WAL 文件
405
+ this._walOps = [];
406
+ try {
407
+ if (fs.existsSync(this._walPath)) {
408
+ fs.unlinkSync(this._walPath);
409
+ }
410
+ } catch (e) {
411
+ // 忽略
412
+ }
413
+ }
414
+
415
+ /**
416
+ * 崩溃恢复:从 WAL 重放操作
417
+ */
418
+ _recoverFromWAL() {
419
+ if (!this._walEnabled || !this._walPath || !fs.existsSync(this._walPath)) return;
420
+ try {
421
+ const walData = JSON.parse(fs.readFileSync(this._walPath, 'utf8'));
422
+ if (!Array.isArray(walData) || walData.length === 0) return;
423
+
424
+ // WAL 操作在 v2.0 中仅用于崩溃恢复标记
425
+ // 实际操作已在 save() 时持久化,WAL 主要用于:
426
+ // 1. 标记未完成的事务
427
+ // 2. 在 save() 间隔中保护数据
428
+ this._walOps = [];
429
+ fs.unlinkSync(this._walPath);
430
+ } catch (e) {
431
+ // WAL 损坏,忽略
432
+ try { if (fs.existsSync(this._walPath)) fs.unlinkSync(this._walPath); } catch (_) {}
433
+ }
434
+ }
435
+
436
+ // ============================================================
437
+ // 表管理
438
+ // ============================================================
439
+
440
+ createTable(name, schema) {
441
+ if (!name || name.length > 64 || name === '.' || name === '..' || /[\x00-\x1f/\\]|\.\./.test(name)) {
442
+ throw createError('ER_BAD_TABLE_NAME', name);
443
+ }
444
+ if (!this._runHooks('beforeCreateTable', [name, schema])) throw createError('ER_PLUGIN_ABORT', 'createTable aborted by plugin');
445
+ if (this._tables[name] || (this._dirMode && this._meta.tables[name])) {
446
+ throw createError('ER_TABLE_EXISTS_ERROR', name);
447
+ }
448
+ const normalized = {};
449
+ for (const [field, def] of Object.entries(schema || {})) {
450
+ if (typeof def === 'string') normalized[field] = parseFieldShorthand(def.toLowerCase());
451
+ else normalized[field] = def;
452
+ }
453
+ const table = new Table(name, normalized, this);
454
+ this._tables[name] = table;
455
+ this._tableNames.add(name);
456
+
457
+ Object.defineProperty(this, name, {
458
+ get: () => this._ensureTable(name) || this._tables[name],
459
+ enumerable: true,
460
+ configurable: true
461
+ });
462
+
463
+ if (this._versioning) {
464
+ this._versions[name] = [];
465
+ }
466
+ if (this._dirMode) {
467
+ this._meta.tables[name] = { file: encodeURIComponent(name) + '.jsql', schema: normalized };
468
+ this._saveMeta();
469
+ }
470
+
471
+ this._markDirty(name);
472
+ this._walWrite({ op: 'createTable', table: name, schema });
473
+ this._emit('createTable', { name, schema });
474
+ this._runHooks('afterCreateTable', [name, schema]);
475
+ return table;
476
+ }
477
+
478
+ dropTable(name) {
479
+ if (!this._tables[name] && !(this._dirMode && this._meta.tables[name])) throw createError('ER_NO_SUCH_TABLE', name);
480
+ if (!this._runHooks('beforeDropTable', [name])) return;
481
+ if (this._tables[name]) {
482
+ delete this._tables[name];
483
+ }
484
+ delete this[name];
485
+ delete this._versions[name];
486
+ this._tableNames.delete(name);
487
+ if (this._dirMode) {
488
+ const meta = this._meta.tables[name];
489
+ if (meta && meta.file) {
490
+ try { fs.unlinkSync(path.join(this._filePath, meta.file)); } catch (e) {}
491
+ }
492
+ delete this._meta.tables[name];
493
+ this._dirtyTables.delete(name);
494
+ this._saveMeta();
495
+ }
496
+ this._walWrite({ op: 'dropTable', table: name });
497
+ this._markDirty(name);
498
+ this._emit('dropTable', { name });
499
+ this._runHooks('afterDropTable', [name]);
500
+ }
501
+
502
+ hasTable(name) {
503
+ return !!this._tables[name] || (this._dirMode && !!this._meta.tables[name]);
504
+ }
505
+
506
+ getTables() {
507
+ const names = new Set(Object.keys(this._tables));
508
+ if (this._dirMode) {
509
+ for (const name of Object.keys(this._meta.tables)) names.add(name);
510
+ }
511
+ return [...names];
512
+ }
513
+
514
+ getTableSchema(name) {
515
+ const table = this._ensureTable(name);
516
+ if (table) return table._schema;
517
+ if (this._dirMode && this._meta.tables[name]) return this._meta.tables[name].schema;
518
+ return null;
519
+ }
520
+
521
+ // ============================================================
522
+ // 视图
523
+ // ============================================================
524
+
525
+ /**
526
+ * 创建视图(虚拟表)
527
+ * @param {string} name - 视图名
528
+ * @param {function} queryFn - (db) => Query 实例
529
+ * @example
530
+ * db.createView('adults', db => db.users.where({ age: { $gte: 18 } }).select(['name', 'age']));
531
+ */
532
+ createView(name, queryFn) {
533
+ if (this._views[name]) throw createError('ER_TABLE_EXISTS_ERROR', name);
534
+ this._views[name] = { queryFn };
535
+
536
+ Object.defineProperty(this, name, {
537
+ get: () => {
538
+ const q = queryFn(this);
539
+ return {
540
+ get: () => q.get(),
541
+ first: () => q.first(),
542
+ count: () => q.count(),
543
+ sum: (f) => q.sum(f),
544
+ avg: (f) => q.avg(f),
545
+ min: (f) => q.min(f),
546
+ max: (f) => q.max(f),
547
+ _query: q
548
+ };
549
+ },
550
+ enumerable: true,
551
+ configurable: true
552
+ });
553
+ }
554
+
555
+ dropView(name) {
556
+ if (!this._views[name]) throw createError('ER_NO_SUCH_TABLE', name);
557
+ delete this._views[name];
558
+ delete this[name];
559
+ }
560
+
561
+ getViews() {
562
+ return Object.keys(this._views);
563
+ }
564
+
565
+ // ============================================================
566
+ // 触发器
567
+ // ============================================================
568
+
569
+ /**
570
+ * 创建触发器
571
+ * @param {string} name - 触发器名
572
+ * @param {object} options - { event: 'insert'|'update'|'remove', table: 'tableName', timing: 'before'|'after' }
573
+ * @param {function} callback - 触发时执行的回调
574
+ */
575
+ createTrigger(name, options, callback) {
576
+ if (this._triggers[name]) throw createError('ER_TRIGGER_EXISTS', name);
577
+ const table = this._ensureTable(options.table);
578
+ if (!table) throw createError('ER_NO_SUCH_TABLE', options.table);
579
+
580
+ const hookName = options.timing + options.event.charAt(0).toUpperCase() + options.event.slice(1);
581
+ this._triggers[name] = { event: options.event, table: options.table, callback, hookName };
582
+ table.on(hookName, callback);
583
+ }
584
+
585
+ dropTrigger(name) {
586
+ const trigger = this._triggers[name];
587
+ if (!trigger) throw createError('ER_TRIGGER_NOT_FOUND', name);
588
+ const table = this._ensureTable(trigger.table);
589
+ if (table) table.off(trigger.hookName, trigger.callback);
590
+ delete this._triggers[name];
591
+ }
592
+
593
+ getTriggers() {
594
+ return Object.keys(this._triggers).map(name => ({
595
+ name,
596
+ event: this._triggers[name].event,
597
+ table: this._triggers[name].table
598
+ }));
599
+ }
600
+
601
+ // ============================================================
602
+ // CSV 导入/导出
603
+ // ============================================================
604
+
605
+ /**
606
+ * 从 CSV 文件导入数据到表
607
+ * @param {string} filePath - CSV 文件路径
608
+ * @param {string} tableName - 目标表名
609
+ * @param {object} options - { delimiter: ',', hasHeader: true, skipRows: 0, mapping: { csvCol: schemaCol } }
610
+ */
611
+ importCSV(filePath, tableName, options = {}) {
612
+ const table = this._ensureTable(tableName);
613
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
614
+
615
+ const delimiter = options.delimiter || ',';
616
+ const hasHeader = options.hasHeader !== false;
617
+ const skipRows = options.skipRows || 0;
618
+ const mapping = options.mapping || null;
619
+
620
+ const content = fs.readFileSync(filePath, 'utf8');
621
+ const lines = content.split(/\r?\n/).filter(l => l.trim());
622
+
623
+ let headers = [];
624
+ let startRow = 0;
625
+
626
+ if (hasHeader) {
627
+ headers = this._parseCSVLine(lines[0], delimiter);
628
+ startRow = 1 + skipRows;
629
+ } else {
630
+ startRow = skipRows;
631
+ }
632
+
633
+ let count = 0;
634
+ for (let i = startRow; i < lines.length; i++) {
635
+ const values = this._parseCSVLine(lines[i], delimiter);
636
+ if (values.length === 0) continue;
637
+
638
+ const data = {};
639
+ if (mapping) {
640
+ for (const [csvCol, schemaCol] of Object.entries(mapping)) {
641
+ const idx = hasHeader ? headers.indexOf(csvCol) : parseInt(csvCol);
642
+ if (idx >= 0 && idx < values.length) {
643
+ data[schemaCol] = this._castValue(values[idx], table._schema[schemaCol]);
644
+ }
645
+ }
646
+ } else if (hasHeader) {
647
+ for (let j = 0; j < headers.length; j++) {
648
+ if (j < values.length && table._schema[headers[j]]) {
649
+ data[headers[j]] = this._castValue(values[j], table._schema[headers[j]]);
650
+ }
651
+ }
652
+ }
653
+
654
+ if (Object.keys(data).length > 0) {
655
+ table.insert(data);
656
+ count++;
657
+ }
658
+ }
659
+
660
+ return count;
661
+ }
662
+
663
+ /**
664
+ * 导出表数据到 CSV 文件
665
+ */
666
+ exportCSV(filePath, tableName, query = {}, options = {}) {
667
+ const table = this._ensureTable(tableName);
668
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
669
+
670
+ const delimiter = options.delimiter || ',';
671
+ const rows = table._applyFilter(table._rows, query);
672
+ const fields = options.fields || Object.keys(table._schema).filter(f => f !== '_softDelete');
673
+
674
+ const lines = [];
675
+ lines.push(fields.map(f => this._csvEscape(f, delimiter)).join(delimiter));
676
+
677
+ for (const row of rows) {
678
+ lines.push(fields.map(f => {
679
+ const val = row[f];
680
+ return val === null || val === undefined ? '' : this._csvEscape(String(val), delimiter);
681
+ }).join(delimiter));
682
+ }
683
+
684
+ const dir = path.dirname(filePath);
685
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
686
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
687
+ return lines.length - 1;
688
+ }
689
+
690
+ _parseCSVLine(line, delimiter) {
691
+ const result = [];
692
+ let current = '';
693
+ let inQuotes = false;
694
+
695
+ for (let i = 0; i < line.length; i++) {
696
+ const ch = line[i];
697
+ if (inQuotes) {
698
+ if (ch === '"') {
699
+ if (i + 1 < line.length && line[i + 1] === '"') {
700
+ current += '"';
701
+ i++;
702
+ } else {
703
+ inQuotes = false;
704
+ }
705
+ } else {
706
+ current += ch;
707
+ }
708
+ } else {
709
+ if (ch === '"') {
710
+ inQuotes = true;
711
+ } else if (ch === delimiter) {
712
+ result.push(current.trim());
713
+ current = '';
714
+ } else {
715
+ current += ch;
716
+ }
717
+ }
718
+ }
719
+ result.push(current.trim());
720
+ return result;
721
+ }
722
+
723
+ _csvEscape(value, delimiter) {
724
+ if (value.includes(delimiter) || value.includes('"') || value.includes('\n')) {
725
+ return '"' + value.replace(/"/g, '""') + '"';
726
+ }
727
+ return value;
728
+ }
729
+
730
+ _castValue(value, schemaDef) {
731
+ if (!schemaDef || !value) return value;
732
+ switch (schemaDef.type) {
733
+ case 'integer': return parseInt(value, 10) || 0;
734
+ case 'number': return parseFloat(value) || 0;
735
+ case 'boolean': return value.toLowerCase() === 'true' || value === '1';
736
+ case 'array': try { return JSON.parse(value); } catch(e) { return [value]; }
737
+ case 'object': try { return JSON.parse(value); } catch(e) { return {}; }
738
+ case 'date':
739
+ case 'datetime':
740
+ case 'timestamp':
741
+ case 'time':
742
+ return value;
743
+ default: return value;
744
+ }
745
+ }
746
+
747
+ // ============================================================
748
+ // 数据库统计
749
+ // ============================================================
750
+
751
+ /**
752
+ * 获取数据库统计信息
753
+ */
754
+ stats() {
755
+ const tableStats = {};
756
+ let totalRows = 0;
757
+ let totalIndexes = 0;
758
+ let totalBTrees = 0;
759
+
760
+ for (const [name, table] of Object.entries(this._tables)) {
761
+ if (name === '__meta__') continue;
762
+ const rowCount = table._rows.length;
763
+ const indexCount = Object.keys(table._indexes).length;
764
+ const btreeCount = Object.keys(table._btrees).length;
765
+ const sizeEstimate = JSON.stringify(table._rows).length;
766
+ tableStats[name] = {
767
+ rowCount,
768
+ indexCount: indexCount + btreeCount,
769
+ btreeCount,
770
+ sizeEstimate,
771
+ primaryKey: table._primaryKey,
772
+ columns: Object.keys(table._schema).length,
773
+ softDelete: table._softDelete,
774
+ foreignKeys: table._foreignKeys.length,
775
+ hooks: Object.values(table._hooks).reduce((s, a) => s + a.length, 0)
776
+ };
777
+ totalRows += rowCount;
778
+ totalIndexes += indexCount;
779
+ totalBTrees += btreeCount;
780
+ }
781
+
782
+ return {
783
+ tables: Object.keys(this._tables).filter(t => t !== '__meta__').length,
784
+ views: Object.keys(this._views).length,
785
+ triggers: Object.keys(this._triggers).length,
786
+ migrations: this._migrations.length,
787
+ plugins: this._plugins.length,
788
+ totalRows,
789
+ totalIndexes,
790
+ totalBTrees,
791
+ memoryMode: this._memoryMode,
792
+ versioning: this._versioning,
793
+ encrypted: !!this._encryptKey,
794
+ walEnabled: this._walEnabled,
795
+ fileLockEnabled: this._fileLockEnabled,
796
+ isolationLevel: this._isolationLevel,
797
+ slowQueryThreshold: this._slowQueryThreshold,
798
+ slowQueriesCount: this._slowQueries.length,
799
+ tableStats
800
+ };
801
+ }
802
+
803
+ // ============================================================
804
+ // 慢查询日志
805
+ // ============================================================
806
+
807
+ /**
808
+ * 记录慢查询
809
+ */
810
+ _logSlowQuery(sql, durationMs, rowsReturned) {
811
+ if (this._slowQueryThreshold < 0) return;
812
+ if (this._slowQueryThreshold > 0 && durationMs < this._slowQueryThreshold) return;
813
+
814
+ this._slowQueries.push({
815
+ sql,
816
+ duration: durationMs,
817
+ rows: rowsReturned,
818
+ timestamp: Date.now()
819
+ });
820
+
821
+ // 限制日志数量
822
+ if (this._slowQueries.length > 1000) {
823
+ this._slowQueries = this._slowQueries.slice(-500);
824
+ }
825
+ }
826
+
827
+ /**
828
+ * 获取慢查询日志
829
+ * @param {number} limit - 返回最近 N 条
830
+ */
831
+ getSlowQueries(limit = 50) {
832
+ return this._slowQueries.slice(-limit).reverse();
833
+ }
834
+
835
+ /**
836
+ * 清空慢查询日志
837
+ */
838
+ clearSlowQueries() {
839
+ this._slowQueries = [];
840
+ }
841
+
842
+ /**
843
+ * 设置慢查询阈值
844
+ * @param {number} ms - 毫秒,0 禁用
845
+ */
846
+ setSlowQueryThreshold(ms) {
847
+ this._slowQueryThreshold = ms;
848
+ }
849
+
850
+ // ============================================================
851
+ // 多数据库
852
+ // ============================================================
853
+
854
+ /**
855
+ * 附加另一个数据库(跨库查询)
856
+ */
857
+ attach(name, db) {
858
+ if (this._attached[name]) throw createError('ER_DBATTACH_EXISTS', name);
859
+ this._attached[name] = db;
860
+
861
+ Object.defineProperty(this, name, {
862
+ get: () => db,
863
+ enumerable: true,
864
+ configurable: true
865
+ });
866
+ }
867
+
868
+ detach(name) {
869
+ if (!this._attached[name]) throw createError('ER_DBATTACH_NOT_FOUND', name);
870
+ delete this._attached[name];
871
+ delete this[name];
872
+ }
873
+
874
+ getAttached() {
875
+ return Object.keys(this._attached);
876
+ }
877
+
878
+ // ============================================================
879
+ // 事务(支持隔离级别)
880
+ // ============================================================
881
+
882
+ /**
883
+ * 开始事务
884
+ * @param {string} isolationLevel - 覆盖默认隔离级别(可选)
885
+ */
886
+ begin(isolationLevel) {
887
+ if (this._transaction) throw createError('ER_TRANSACTION_ACTIVE');
888
+
889
+ const level = isolationLevel || this._isolationLevel;
890
+
891
+ this._transaction = {
892
+ rows: {},
893
+ autoIncrements: {},
894
+ level,
895
+ startedAt: Date.now()
896
+ };
897
+
898
+ if (level === 'REPEATABLE_READ') {
899
+ // 快照:保存当前所有数据(深拷贝,保证嵌套对象可回滚)
900
+ for (const [name, table] of Object.entries(this._tables)) {
901
+ this._transaction.rows[name] = JSON.parse(JSON.stringify(table._rows));
902
+ this._transaction.autoIncrements[name] = table._autoIncrement;
903
+ }
904
+ } else {
905
+ // READ_COMMITTED:只保存 autoIncrement
906
+ for (const [name, table] of Object.entries(this._tables)) {
907
+ this._transaction.autoIncrements[name] = table._autoIncrement;
908
+ }
909
+ }
910
+
911
+ this._walWrite({ op: 'begin', level });
912
+ }
913
+
914
+ commit() {
915
+ if (!this._transaction) throw createError('ER_NO_TRANSACTION');
916
+ this._walWrite({ op: 'commit', at: this._transaction.startedAt });
917
+ this._transaction = null;
918
+ this._checkpoint();
919
+ this._markDirtyLegacy();
920
+ }
921
+
922
+ rollback() {
923
+ if (!this._transaction) throw createError('ER_NO_TRANSACTION');
924
+
925
+ for (const [name, table] of Object.entries(this._tables)) {
926
+ if (this._transaction.rows[name]) {
927
+ table._rows = this._transaction.rows[name];
928
+ table._autoIncrement = this._transaction.autoIncrements[name];
929
+ table._rebuildAllBTrees();
930
+ for (const field of Object.keys(table._indexes)) {
931
+ table.createIndex(field);
932
+ }
933
+ } else if (this._transaction.autoIncrements[name] !== undefined) {
934
+ table._autoIncrement = this._transaction.autoIncrements[name];
935
+ }
936
+ }
937
+
938
+ this._walWrite({ op: 'rollback', at: this._transaction.startedAt });
939
+ this._transaction = null;
940
+ }
941
+
942
+ inTransaction() {
943
+ return !!this._transaction;
944
+ }
945
+
946
+ /**
947
+ * 获取当前事务隔离级别
948
+ */
949
+ getIsolationLevel() {
950
+ return this._transaction ? this._transaction.level : this._isolationLevel;
951
+ }
952
+
953
+ /**
954
+ * 设置默认事务隔离级别
955
+ * @param {'READ_COMMITTED'|'REPEATABLE_READ'} level
956
+ */
957
+ setIsolationLevel(level) {
958
+ if (!['READ_COMMITTED', 'REPEATABLE_READ'].includes(level)) {
959
+ throw createError('ER_NOT_SUPPORTED', `Isolation level '${level}' is not supported. Use READ_COMMITTED or REPEATABLE_READ.`);
960
+ }
961
+ this._isolationLevel = level;
962
+ }
963
+
964
+ // ============================================================
965
+ // 版本历史
966
+ // ============================================================
967
+
968
+ /**
969
+ * 保存当前快照
970
+ */
971
+ snapshot(tableName) {
972
+ if (!this._versioning) return;
973
+ const table = this._tables[tableName];
974
+ if (!table) return;
975
+ if (!this._versions[tableName]) this._versions[tableName] = [];
976
+
977
+ if (table._rows.length === 0) return;
978
+
979
+ const currentHash = JSON.stringify(table._rows);
980
+ const versions = this._versions[tableName];
981
+ if (versions.length > 0 && versions[versions.length - 1]._hash === currentHash) return;
982
+
983
+ versions.push({
984
+ rows: table._rows.map(r => ({ ...r })),
985
+ timestamp: Date.now(),
986
+ _hash: currentHash
987
+ });
988
+ if (versions.length > this._maxVersions) {
989
+ versions.shift();
990
+ }
991
+ }
992
+
993
+ /**
994
+ * 回退到指定版本
995
+ */
996
+ undo(tableName, steps = 1) {
997
+ if (!this._versioning) throw createError('ER_NOT_SUPPORTED', 'Versioning is not enabled');
998
+ const versions = this._versions[tableName];
999
+ if (!versions || versions.length < 2) throw createError('ER_NOT_SUPPORTED', 'No versions to undo');
1000
+ const table = this._ensureTable(tableName);
1001
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1002
+
1003
+ for (let i = 0; i < steps && versions.length > 1; i++) {
1004
+ versions.pop();
1005
+ const snapshot = versions[versions.length - 1];
1006
+ table._rows = snapshot.rows.map(r => ({ ...r }));
1007
+ table._autoIncrement = 0;
1008
+ for (const row of table._rows) {
1009
+ if (table._autoIncrementField && row[table._autoIncrementField] > table._autoIncrement) {
1010
+ table._autoIncrement = row[table._autoIncrementField];
1011
+ }
1012
+ }
1013
+ table._rebuildAllBTrees();
1014
+ for (const field of Object.keys(table._indexes)) {
1015
+ table.createIndex(field);
1016
+ }
1017
+ }
1018
+ this._walWrite({ op: 'undo', table: tableName, steps });
1019
+ this._markDirtyLegacy();
1020
+ return versions.length;
1021
+ }
1022
+
1023
+ getVersions(tableName) {
1024
+ return this._versions[tableName] || [];
1025
+ }
1026
+
1027
+ // ============================================================
1028
+ // 变更流 & 响应式
1029
+ // ============================================================
1030
+
1031
+ /**
1032
+ * 监听变更事件
1033
+ * @param {function} callback - (event) => {} event: { type, table, data, timestamp }
1034
+ * @returns {function} 取消监听的函数
1035
+ */
1036
+ onChange(callback) {
1037
+ this._observers.push(callback);
1038
+ return () => {
1039
+ this._observers = this._observers.filter(cb => cb !== callback);
1040
+ };
1041
+ }
1042
+
1043
+ _emitChange(type, table, data) {
1044
+ const event = { type, table, data, timestamp: Date.now() };
1045
+ for (const cb of this._observers) {
1046
+ try { cb(event); } catch (e) { /* ignore */ }
1047
+ }
1048
+ }
1049
+
1050
+ /**
1051
+ * 创建变更流(返回异步迭代器)
1052
+ */
1053
+ createChangeStream() {
1054
+ const events = [];
1055
+ let resolve = null;
1056
+ const unsubscribe = this.onChange(event => {
1057
+ events.push(event);
1058
+ if (resolve) {
1059
+ resolve();
1060
+ resolve = null;
1061
+ }
1062
+ });
1063
+
1064
+ return {
1065
+ [Symbol.asyncIterator]() {
1066
+ return {
1067
+ next: async () => {
1068
+ while (events.length === 0) {
1069
+ await new Promise(r => resolve = r);
1070
+ }
1071
+ return { value: events.shift(), done: false };
1072
+ }
1073
+ };
1074
+ },
1075
+ close: unsubscribe
1076
+ };
1077
+ }
1078
+
1079
+ // ============================================================
1080
+ // 插件系统
1081
+ // ============================================================
1082
+
1083
+ /**
1084
+ * 注册插件
1085
+ * @param {object|function} plugin - { name, install(db, ctx), hooks, onEvent } 或函数 (作为 install)
1086
+ */
1087
+ use(plugin) {
1088
+ if (typeof plugin === 'function') {
1089
+ plugin = { install: plugin };
1090
+ }
1091
+ if (plugin.hooks) {
1092
+ for (const [event, fn] of Object.entries(plugin.hooks)) {
1093
+ if (this._hooks[event]) {
1094
+ this._hooks[event].push(fn);
1095
+ }
1096
+ }
1097
+ }
1098
+ if (typeof plugin.install === 'function') {
1099
+ plugin.install(this, this._buildCtx(plugin));
1100
+ }
1101
+ if (typeof plugin.onEvent === 'function') {
1102
+ this._eventListeners.push(plugin.onEvent);
1103
+ }
1104
+ this._plugins.push(plugin);
1105
+ return this;
1106
+ }
1107
+
1108
+ _buildCtx(plugin) {
1109
+ const self = this;
1110
+ return {
1111
+ name: plugin.name || 'anonymous',
1112
+ engine: this,
1113
+ plugin,
1114
+ on(hook, fn) { return self.on(hook, fn); },
1115
+ onEvent(fn) { return self.onEvent(fn); },
1116
+ emit(eventName, data) { self._emit(eventName, data); },
1117
+ tables() { return Array.from(self._tableNames); },
1118
+ hasTable(name) { return self._tableNames.has(name); },
1119
+ getTableSchema(name) {
1120
+ const t = self._tables[name];
1121
+ return t ? t._schema : null;
1122
+ },
1123
+ table(name) { return self._tables[name] || null; }
1124
+ };
1125
+ }
1126
+
1127
+ on(event, fn) {
1128
+ if (this._hooks[event]) {
1129
+ this._hooks[event].push(fn);
1130
+ }
1131
+ return this;
1132
+ }
1133
+
1134
+ onEvent(fn) {
1135
+ this._eventListeners.push(fn);
1136
+ return this;
1137
+ }
1138
+
1139
+ _emit(eventName, data) {
1140
+ for (const fn of this._eventListeners) {
1141
+ try { fn(eventName, data); } catch (e) { /* ignore */ }
1142
+ }
1143
+ }
1144
+
1145
+ _runHooks(hookName, args) {
1146
+ const hooks = this._hooks[hookName];
1147
+ if (!hooks || hooks.length === 0) return true;
1148
+ for (const fn of hooks) {
1149
+ const r = fn(...args);
1150
+ if (r === false) return false;
1151
+ if (r !== undefined && args.length > 0) {
1152
+ args[0] = r;
1153
+ }
1154
+ }
1155
+ return true;
1156
+ }
1157
+
1158
+ // ============================================================
1159
+ // 统一高层 API(与 Native/WASM 引擎对齐)
1160
+ // ============================================================
1161
+
1162
+ _pkOf(table) {
1163
+ return table._primaryKey || table._autoIncrementField || null;
1164
+ }
1165
+
1166
+ async start() {
1167
+ this._runHooks('onStart', []);
1168
+ this._emit('start', {});
1169
+ if (this._dirMode && !this._sigInstalled) {
1170
+ this._sigInstalled = true;
1171
+ Database._installSignalHandler(this);
1172
+ }
1173
+ return this;
1174
+ }
1175
+
1176
+ /**
1177
+ * 进程退出时兜底刷盘:拦截 SIGINT/SIGTERM,同步落盘脏表后再退出。
1178
+ * 使用静态共享 handler,避免多个数据库实例重复注册/互相覆盖。
1179
+ * @private
1180
+ */
1181
+ static _installSignalHandler(db) {
1182
+ if (!Database._signalHandlerInstalled) {
1183
+ Database._signalHandlerInstalled = true;
1184
+ Database._signalDBs = new Set();
1185
+ const handler = (sig) => {
1186
+ for (const d of Database._signalDBs) {
1187
+ try {
1188
+ if (d._dirMode) {
1189
+ if (d._flushTimer) { clearTimeout(d._flushTimer); d._flushTimer = null; }
1190
+ d._flushDirty();
1191
+ d._saveMeta();
1192
+ }
1193
+ } catch (e) { /* 兜底失败不阻塞退出 */ }
1194
+ }
1195
+ process.exit(128 + (sig === 'SIGINT' ? 2 : 15));
1196
+ };
1197
+ process.on('SIGINT', handler);
1198
+ process.on('SIGTERM', handler);
1199
+ Database._signalHandler = handler;
1200
+ }
1201
+ Database._signalDBs.add(db);
1202
+ }
1203
+
1204
+ /** 从进程退出兜底集合移除(stop 时调用) */
1205
+ static _uninstallSignalHandler(db) {
1206
+ if (Database._signalDBs) Database._signalDBs.delete(db);
1207
+ }
1208
+
1209
+ insert(tableName, data) {
1210
+ const table = this._ensureTable(tableName);
1211
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1212
+ const arr = Array.isArray(data) ? data : [data];
1213
+ if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
1214
+ const pk = this._pkOf(table);
1215
+ const ids = [];
1216
+ for (const row of arr) {
1217
+ const r = table.insert(row);
1218
+ ids.push(pk ? r[pk] : table._rows.length);
1219
+ }
1220
+ this._emit('insert', { table: tableName, count: arr.length, ids });
1221
+ this._runHooks('afterInsert', [tableName, arr, ids]);
1222
+ this._markDirty(tableName);
1223
+ return ids;
1224
+ }
1225
+
1226
+ async flush() {
1227
+ if (!this._runHooks('beforeFlush', [])) return;
1228
+ if (this._dirMode) {
1229
+ this._flushDirty();
1230
+ this._saveMeta();
1231
+ } else if (!this._memoryMode) {
1232
+ this.save();
1233
+ }
1234
+ this._runHooks('afterFlush', []);
1235
+ return this;
1236
+ }
1237
+
1238
+ _resolveId(table, id) {
1239
+ const pk = this._pkOf(table);
1240
+ if (pk) return table._rows.find(r => r[pk] === id) || null;
1241
+ return table._rows[id - 1] || null;
1242
+ }
1243
+
1244
+ find(tableName, filter, opts = {}) {
1245
+ const table = this._ensureTable(tableName);
1246
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1247
+ if (!this._runHooks('beforeFind', [tableName, { filter, opts }])) return [];
1248
+ const { limit, offset = 0 } = opts;
1249
+ let rows = table._applyFilter(table._rows, filter || {});
1250
+ if (limit > 0) rows = rows.slice(offset, offset + limit);
1251
+ else if (offset > 0) rows = rows.slice(offset);
1252
+ this._touchTable(tableName);
1253
+ this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
1254
+ return rows;
1255
+ }
1256
+
1257
+ findById(tableName, id) {
1258
+ const table = this._ensureTable(tableName);
1259
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1260
+ if (!this._runHooks('beforeFind', [tableName, { id }])) return null;
1261
+ const row = this._resolveId(table, id);
1262
+ this._touchTable(tableName);
1263
+ this._runHooks('afterFind', [tableName, { id }, row]);
1264
+ return row;
1265
+ }
1266
+
1267
+ findByIds(tableName, ids) {
1268
+ const table = this._ensureTable(tableName);
1269
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1270
+ if (!this._runHooks('beforeFind', [tableName, { ids }])) return null;
1271
+ const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null);
1272
+ this._touchTable(tableName);
1273
+ this._runHooks('afterFind', [tableName, { ids }, rows]);
1274
+ return rows;
1275
+ }
1276
+
1277
+ findByIdsRaw(tableName, ids) {
1278
+ return this.findByIds(tableName, ids);
1279
+ }
1280
+
1281
+ count(tableName) {
1282
+ const table = this._ensureTable(tableName);
1283
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1284
+ this._runHooks('beforeCount', [tableName]);
1285
+ const n = table._rows.length;
1286
+ this._touchTable(tableName);
1287
+ this._runHooks('afterCount', [tableName, n]);
1288
+ return n;
1289
+ }
1290
+
1291
+ updateById(tableName, id, data) {
1292
+ const table = this._ensureTable(tableName);
1293
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1294
+ if (!this._runHooks('beforeUpdate', [tableName, id, data])) return;
1295
+ const row = this._resolveId(table, id);
1296
+ if (!row) return { ok: false, error: 'not found' };
1297
+ Object.assign(row, data);
1298
+ this._emit('update', { table: tableName, id, data });
1299
+ this._runHooks('afterUpdate', [tableName, id, data, { ok: true }]);
1300
+ this._markDirty(tableName);
1301
+ return { ok: true };
1302
+ }
1303
+
1304
+ updateByIds(tableName, entries) {
1305
+ const table = this._ensureTable(tableName);
1306
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1307
+ const pairs = entries.map(([id, data]) => [id, data]);
1308
+ if (!this._runHooks('beforeUpdate', [tableName, pairs])) return;
1309
+ let count = 0;
1310
+ for (const [id, data] of pairs) {
1311
+ const row = this._resolveId(table, id);
1312
+ if (row) { Object.assign(row, data); count++; }
1313
+ }
1314
+ this._emit('update', { table: tableName, entries: pairs, result: { ok: true, count } });
1315
+ this._runHooks('afterUpdate', [tableName, pairs, { ok: true, count }]);
1316
+ this._markDirty(tableName);
1317
+ return { ok: true, count };
1318
+ }
1319
+
1320
+ removeById(tableName, id) {
1321
+ const table = this._ensureTable(tableName);
1322
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1323
+ if (!this._runHooks('beforeDelete', [tableName, id])) return;
1324
+ if (table._primaryKey) {
1325
+ const removed = table.removeById(id);
1326
+ if (removed === 0) return { ok: false, error: 'not found' };
1327
+ this._emit('delete', { table: tableName, id });
1328
+ this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1329
+ this._markDirty(tableName);
1330
+ return { ok: true };
1331
+ }
1332
+ const idx = table._rows.findIndex(r => r === this._resolveId(table, id));
1333
+ if (idx === -1) return { ok: false, error: 'not found' };
1334
+ table._rows.splice(idx, 1);
1335
+ table._rebuildPKIndex();
1336
+ table._rebuildAllBTrees();
1337
+ for (const field of Object.keys(table._indexes)) table.createIndex(field);
1338
+ this._emit('delete', { table: tableName, id });
1339
+ this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1340
+ this._markDirty(tableName);
1341
+ return { ok: true };
1342
+ }
1343
+
1344
+ removeByIds(tableName, ids) {
1345
+ const table = this._ensureTable(tableName);
1346
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1347
+ if (!this._runHooks('beforeDelete', [tableName, ids])) return;
1348
+ let removed = 0;
1349
+ if (table._primaryKey) {
1350
+ for (const id of ids) removed += table.removeById(id);
1351
+ } else {
1352
+ for (const id of ids) {
1353
+ const row = this._resolveId(table, id);
1354
+ const idx = row ? table._rows.findIndex(r => r === row) : -1;
1355
+ if (idx !== -1) {
1356
+ table._rows.splice(idx, 1);
1357
+ removed++;
1358
+ }
1359
+ }
1360
+ table._rebuildPKIndex();
1361
+ table._rebuildAllBTrees();
1362
+ for (const field of Object.keys(table._indexes)) table.createIndex(field);
1363
+ }
1364
+ this._emit('delete', { table: tableName, ids, result: { ok: true, count: removed } });
1365
+ this._runHooks('afterDelete', [tableName, ids, { ok: true, count: removed }]);
1366
+ this._markDirty(tableName);
1367
+ return { ok: true, count: removed };
1368
+ }
1369
+
1370
+ removeWhere(tableName, filter) {
1371
+ const table = this._ensureTable(tableName);
1372
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1373
+ if (!this._runHooks('beforeDelete', [tableName, [filter]])) return { ok: true, count: 0 };
1374
+ const before = table._rows.length;
1375
+ const matched = table._applyFilter(table._rows, filter || {});
1376
+ const matchSet = new Set(matched);
1377
+ table._rows = table._rows.filter(r => !matchSet.has(r));
1378
+ const removed = before - table._rows.length;
1379
+ table._rebuildPKIndex();
1380
+ table._rebuildAllBTrees();
1381
+ for (const field of Object.keys(table._indexes)) table.createIndex(field);
1382
+ this._emit('delete', { table: tableName, ids: [], result: { ok: true, count: removed } });
1383
+ this._runHooks('afterDelete', [tableName, [filter], { ok: true, count: removed }]);
1384
+ this._markDirty(tableName);
1385
+ return { ok: true, count: removed };
1386
+ }
1387
+
1388
+ insertMany(tableName, items) {
1389
+ const table = this._ensureTable(tableName);
1390
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1391
+ const arr = Array.isArray(items) ? items : [items];
1392
+ if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
1393
+ const inserted = table.insertMany(arr);
1394
+ this._emit('insert', { table: tableName, count: arr.length });
1395
+ this._runHooks('afterInsert', [tableName, arr, inserted]);
1396
+ this._markDirty(tableName);
1397
+ return inserted;
1398
+ }
1399
+
1400
+ update(tableName, query, updates) {
1401
+ const table = this._ensureTable(tableName);
1402
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1403
+ if (!this._runHooks('beforeUpdate', [tableName, query, updates])) return 0;
1404
+ const count = table.update(query, updates);
1405
+ this._emit('update', { table: tableName, query, count });
1406
+ this._runHooks('afterUpdate', [tableName, query, updates, count]);
1407
+ this._markDirty(tableName);
1408
+ return count;
1409
+ }
1410
+
1411
+ delete(tableName, query = {}) {
1412
+ const table = this._ensureTable(tableName);
1413
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1414
+ if (!this._runHooks('beforeDelete', [tableName, query])) return 0;
1415
+ const count = table.remove(query);
1416
+ this._emit('delete', { table: tableName, query, count });
1417
+ this._runHooks('afterDelete', [tableName, query, count]);
1418
+ this._markDirty(tableName);
1419
+ return count;
1420
+ }
1421
+
1422
+ truncate(tableName) {
1423
+ const table = this._ensureTable(tableName);
1424
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1425
+ const count = table.truncate();
1426
+ this._emit('delete', { table: tableName, count });
1427
+ this._markDirty(tableName);
1428
+ return count;
1429
+ }
1430
+
1431
+ async stop() {
1432
+ this._runHooks('onStop', []);
1433
+ this._emit('stop', {});
1434
+ if (this._dirMode) {
1435
+ if (this._flushTimer) { clearTimeout(this._flushTimer); this._flushTimer = null; }
1436
+ try {
1437
+ this._flushDirty();
1438
+ this._saveMeta();
1439
+ } catch (e) {}
1440
+ }
1441
+ if (this._sigInstalled) {
1442
+ this._sigInstalled = false;
1443
+ Database._uninstallSignalHandler(this);
1444
+ }
1445
+ return this;
1446
+ }
1447
+
1448
+ // ============================================================
1449
+ // 迁移
1450
+ // ============================================================
1451
+
1452
+ /**
1453
+ * 注册迁移
1454
+ * @param {object} migration - { version, name, up(db), down(db) }
1455
+ */
1456
+ addMigration(migration) {
1457
+ this._migrations.push(migration);
1458
+ this._migrations.sort((a, b) => a.version - b.version);
1459
+ }
1460
+
1461
+ /**
1462
+ * 执行迁移到指定版本
1463
+ */
1464
+ migrate(targetVersion) {
1465
+ const current = this._getCurrentVersion();
1466
+ if (targetVersion === undefined) {
1467
+ targetVersion = this._migrations.length > 0
1468
+ ? this._migrations[this._migrations.length - 1].version
1469
+ : 0;
1470
+ }
1471
+
1472
+ if (targetVersion > current) {
1473
+ for (const m of this._migrations) {
1474
+ if (m.version > current && m.version <= targetVersion) {
1475
+ m.up(this);
1476
+ this._setCurrentVersion(m.version);
1477
+ }
1478
+ }
1479
+ } else if (targetVersion < current) {
1480
+ for (const m of [...this._migrations].reverse()) {
1481
+ if (m.version <= current && m.version > targetVersion) {
1482
+ m.down(this);
1483
+ this._setCurrentVersion(m.version - 1);
1484
+ }
1485
+ }
1486
+ }
1487
+ }
1488
+
1489
+ _getCurrentVersion() {
1490
+ return this._tables['__meta__']
1491
+ ? (this._tables['__meta__'].findOne({ key: 'version' })?.value || 0)
1492
+ : 0;
1493
+ }
1494
+
1495
+ _setCurrentVersion(version) {
1496
+ if (!this._tables['__meta__']) {
1497
+ this.createTable('__meta__', {
1498
+ key: { type: 'string', primaryKey: true },
1499
+ value: { type: 'any' }
1500
+ });
1501
+ }
1502
+ this._tables['__meta__'].upsert({ key: 'version', value: version });
1503
+ }
1504
+
1505
+ // ============================================================
1506
+ // 备份与恢复
1507
+ // ============================================================
1508
+
1509
+ backup(filePath) {
1510
+ const data = this.export();
1511
+ const json = this._options.pretty
1512
+ ? JSON.stringify(data, null, 2)
1513
+ : JSON.stringify(data);
1514
+ this._writeFile(filePath, json);
1515
+ return filePath;
1516
+ }
1517
+
1518
+ restore(filePath) {
1519
+ if (!fs.existsSync(filePath)) {
1520
+ throw createError('ER_FILE_NOT_FOUND', filePath);
1521
+ }
1522
+ const json = this._readFile(filePath);
1523
+ const data = JSON.parse(json);
1524
+ for (const [name, rows] of Object.entries(data)) {
1525
+ if (this._tables[name]) {
1526
+ this._tables[name]._loadRows(rows);
1527
+ }
1528
+ }
1529
+ this._walWrite({ op: 'restore', from: filePath });
1530
+ this._markDirtyLegacy();
1531
+ }
1532
+
1533
+ // ============================================================
1534
+ // 持久化
1535
+ // ============================================================
1536
+
1537
+ save() {
1538
+ // 版本快照
1539
+ if (this._versioning) {
1540
+ for (const name of Object.keys(this._tables)) {
1541
+ this.snapshot(name);
1542
+ }
1543
+ }
1544
+
1545
+ if (this._memoryMode) return;
1546
+ if (this._dirMode) {
1547
+ this._flushDirty();
1548
+ this._saveMeta();
1549
+ return;
1550
+ }
1551
+
1552
+ if (this._jsqlMode) {
1553
+ const all = {};
1554
+ for (const [name, table] of Object.entries(this._tables)) {
1555
+ if (name === '__meta__') continue;
1556
+ const pkField = Object.keys(table._schema).find(f => table._schema[f].primaryKey);
1557
+ all[name] = { schema: table._schema, rows: table._rows, pkField };
1558
+ }
1559
+ this._jsqlFormat.saveAll(all);
1560
+ this._jsqlFormat._close();
1561
+ this._dirty = false;
1562
+ this._checkpoint();
1563
+ return;
1564
+ }
1565
+
1566
+ const data = { __schema__: {}, __meta__: { version: this._getCurrentVersion() } };
1567
+ for (const [name, table] of Object.entries(this._tables)) {
1568
+ if (name === '__meta__') continue;
1569
+ data.__schema__[name] = table._schema;
1570
+ data[name] = table.toJSON();
1571
+ }
1572
+
1573
+ const json = this._options.pretty
1574
+ ? JSON.stringify(data, null, 2)
1575
+ : JSON.stringify(data);
1576
+ this._writeFile(this._filePath, json);
1577
+ this._dirty = false;
1578
+
1579
+ // 保存后做检查点
1580
+ this._checkpoint();
1581
+ }
1582
+
1583
+ _load() {
1584
+ try {
1585
+ if (this._jsqlMode) {
1586
+ this._jsqlFormat._open();
1587
+ for (const [name, t] of this._jsqlFormat.tables) {
1588
+ if (!this._tables[name]) this.createTable(name, t.schema);
1589
+ if (t.rowCount > 0) {
1590
+ const { rows } = this._jsqlFormat.readTableSync(name);
1591
+ this._tables[name]._loadRows(rows);
1592
+ }
1593
+ }
1594
+ return;
1595
+ }
1596
+
1597
+ const json = this._readFile(this._filePath);
1598
+ const data = JSON.parse(json);
1599
+
1600
+ if (data.__schema__) {
1601
+ for (const [name, schema] of Object.entries(data.__schema__)) {
1602
+ if (!this._tables[name]) {
1603
+ this.createTable(name, schema);
1604
+ }
1605
+ }
1606
+ }
1607
+
1608
+ for (const [name, rows] of Object.entries(data)) {
1609
+ if (name !== '__schema__' && name !== '__meta__' && this._tables[name]) {
1610
+ this._tables[name]._loadRows(rows);
1611
+ }
1612
+ }
1613
+
1614
+ // WAL 恢复
1615
+ this._recoverFromWAL();
1616
+ } catch (e) {
1617
+ throw new Error(`Failed to load database from '${this._filePath}': ${e.message}`);
1618
+ }
1619
+ }
1620
+
1621
+ _markDirtyLegacy() {
1622
+ this._dirty = true;
1623
+ if (this._loading) return;
1624
+ if (this._options.autoSave && !this._autoSaveTimer) {
1625
+ if (this._options.autoSaveInterval > 0) {
1626
+ this._autoSaveTimer = setTimeout(() => {
1627
+ this._autoSaveTimer = null;
1628
+ if (this._dirty) this.save();
1629
+ }, this._options.autoSaveInterval);
1630
+ } else {
1631
+ this.save();
1632
+ }
1633
+ }
1634
+ }
1635
+
1636
+ close() {
1637
+ if (this._autoSaveTimer) {
1638
+ clearTimeout(this._autoSaveTimer);
1639
+ this._autoSaveTimer = null;
1640
+ }
1641
+ if (this._dirMode) {
1642
+ try {
1643
+ this._flushDirty();
1644
+ this._saveMeta();
1645
+ } catch (e) {
1646
+ this._emit('error', e);
1647
+ }
1648
+ } else if (this._dirty && !this._memoryMode) {
1649
+ this.save();
1650
+ }
1651
+ if (this._jsqlFormat) this._jsqlFormat._close();
1652
+ this._releaseLock();
1653
+ this._tables = {};
1654
+ }
1655
+
1656
+ export() {
1657
+ const data = {};
1658
+ for (const [name, table] of Object.entries(this._tables)) {
1659
+ if (name === '__meta__') continue;
1660
+ data[name] = table.toJSON();
1661
+ }
1662
+ return data;
1663
+ }
1664
+
1665
+ import(data) {
1666
+ for (const [name, rows] of Object.entries(data)) {
1667
+ if (this._tables[name]) {
1668
+ this._tables[name]._loadRows(rows);
1669
+ }
1670
+ }
1671
+ this._walWrite({ op: 'import', tables: Object.keys(data) });
1672
+ this._markDirtyLegacy();
1673
+ }
1674
+
1675
+ async beginTx() {
1676
+ this._txSnapshot = {};
1677
+ for (const [name, table] of Object.entries(this._tables)) {
1678
+ this._txSnapshot[name] = { rows: JSON.parse(JSON.stringify(table.toJSON())), schema: table._schema };
1679
+ }
1680
+ this._txId = (this._txId || 0) + 1;
1681
+ return this._txId;
1682
+ }
1683
+
1684
+ async commitTx() {
1685
+ this._txSnapshot = null;
1686
+ this._txId = undefined;
1687
+ if (this._dirMode) {
1688
+ try { this._flushDirty(); } catch (e) {}
1689
+ } else if (!this._memoryMode) {
1690
+ this.save();
1691
+ }
1692
+ }
1693
+
1694
+ async rollbackTx() {
1695
+ const snap = this._txSnapshot;
1696
+ this._txSnapshot = null;
1697
+ this._txId = undefined;
1698
+ if (!snap) return;
1699
+ for (const name of Object.keys(this._tables)) {
1700
+ if (!snap[name]) delete this._tables[name];
1701
+ }
1702
+ for (const [name, entry] of Object.entries(snap)) {
1703
+ if (this._tables[name]) {
1704
+ this._tables[name]._loadRows(entry.rows);
1705
+ } else {
1706
+ const table = new Table(name, entry.schema, this);
1707
+ table._loadRows(entry.rows);
1708
+ this._tables[name] = table;
1709
+ }
1710
+ }
1711
+ this._dirty = true;
1712
+ }
1713
+
1714
+ // ============================================================
1715
+ // 加密(内部)
1716
+ // ============================================================
1717
+
1718
+ _writeFile(filePath, content) {
1719
+ if (this._encryptKey) {
1720
+ const iv = crypto.randomBytes(16);
1721
+ const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(this._encryptKey.padEnd(32).slice(0, 32)), iv);
1722
+ const encrypted = Buffer.concat([cipher.update(content, 'utf8'), cipher.final()]);
1723
+ const data = Buffer.concat([iv, encrypted]);
1724
+ fs.writeFileSync(filePath, data.toString('base64'), 'utf8');
1725
+ } else {
1726
+ const dir = path.dirname(filePath);
1727
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1728
+ fs.writeFileSync(filePath, content, 'utf8');
1729
+ }
1730
+ }
1731
+
1732
+ _readFile(filePath) {
1733
+ if (this._encryptKey) {
1734
+ const data = Buffer.from(fs.readFileSync(filePath, 'utf8'), 'base64');
1735
+ const iv = data.slice(0, 16);
1736
+ const encrypted = data.slice(16);
1737
+ const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this._encryptKey.padEnd(32).slice(0, 32)), iv);
1738
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
1739
+ }
1740
+ return fs.readFileSync(filePath, 'utf8');
1741
+ }
1742
+ }
1743
+
1744
+ module.exports = Database;
1745
1745
  module.exports.parseFieldShorthand = parseFieldShorthand;