jsql-neo 3.5.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +202 -0
  2. package/bin/jsql +97 -0
  3. package/index.js +20 -0
  4. package/lib/database.js +484 -28
  5. package/lib/mod.js +316 -0
  6. package/lib/mysql_compat.js +232 -0
  7. package/lib/mysql_server.js +514 -0
  8. package/lib/native_client.js +470 -0
  9. package/lib/nedb_compat.js +506 -0
  10. package/lib/plugin.js +35 -0
  11. package/lib/sql.js +1160 -0
  12. package/lib/table.js +42 -21
  13. package/lib/wasm_client.js +176 -8
  14. package/native/jsql-neo-native.node +0 -0
  15. package/nativesrc/jsql-neo-core/Cargo.toml +24 -0
  16. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +449 -0
  17. package/nativesrc/jsql-neo-core/src/engine/memory.rs +147 -0
  18. package/nativesrc/jsql-neo-core/src/engine/mod.rs +41 -0
  19. package/nativesrc/jsql-neo-core/src/engine/table.rs +664 -0
  20. package/nativesrc/jsql-neo-core/src/lib.rs +3 -0
  21. package/nativesrc/jsql-neo-core/src/storage/mod.rs +1 -0
  22. package/nativesrc/jsql-neo-core/src/storage/persistent.rs +2 -0
  23. package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -0
  24. package/nativesrc/jsql-neo-core/src/types.rs +94 -0
  25. package/nativesrc/jsql-neo-native/Cargo.lock +606 -0
  26. package/nativesrc/jsql-neo-native/Cargo.toml +16 -0
  27. package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
  28. package/nativesrc/jsql-neo-native/package.json +7 -0
  29. package/nativesrc/jsql-neo-native/src/lib.rs +281 -0
  30. package/package.json +9 -1
  31. package/wasm/jsql_neo_wasm.d.ts +8 -0
  32. package/wasm/jsql_neo_wasm.js +455 -6
  33. package/wasm/jsql_neo_wasm_bg.js +22 -0
  34. package/wasm/jsql_neo_wasm_bg.wasm +0 -0
  35. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +4 -0
  36. package/wasm/package.json +1 -8
package/lib/database.js CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const os = require('os');
9
10
  const crypto = require('crypto');
10
11
  const Table = require('./table');
11
12
  const { createError } = require('./errors');
@@ -13,11 +14,19 @@ const JSQLFormat = require('./jsql_format');
13
14
 
14
15
  class Database {
15
16
  /**
16
- * @param {string} filePath - 数据库文件路径,传 null 或 ':memory:' 使用内存模式
17
+ * @param {string} filePath - 数据库文件/目录路径,传 null 或 ':memory:' 使用内存模式
17
18
  * @param {object} options
18
- * @param {boolean} options.autoSave - 是否自动保存(默认 true)
19
- * @param {number} options.autoSaveInterval - 自动保存间隔 ms(默认 0)
20
- * @param {boolean} options.pretty - JSON 是否格式化(默认 true)
19
+ * @param {string} options.mode - 存储模式:
20
+ * 'memory'(默认):纯内存,不落盘
21
+ * 'hybrid'(混合,Redis 式):先写内存,后台异步慢慢刷盘;内存充足(默认预留 0.5GB 余量)
22
+ * 则数据常驻内存作为缓存;内存紧张时按 LRU 驱逐最冷表(已落盘),查询时自动从磁盘加载
23
+ * 'disk'(磁盘模式):写入后尽快刷盘,内存只做读写缓存
24
+ * @param {number} options.memReserveMB - 内存余量 MB(默认 512,即预留 0.5GB)
25
+ * @param {number} options.flushInterval - 脏表后台刷盘间隔 ms(默认 hybrid 200 / disk 50,0 = 每次写后立即刷)
26
+ * @param {number} options.evictInterval - 内存压力检查间隔 ms(默认 1000)
27
+ * @param {boolean} options.autoSave - 是否自动保存(默认 true,仅旧文件模式)
28
+ * @param {number} options.autoSaveInterval - 自动保存间隔 ms(默认 0,仅旧文件模式)
29
+ * @param {boolean} options.pretty - JSON 是否格式化(默认 true,仅旧文件模式)
21
30
  * @param {string} options.encryptKey - 加密密钥(16/24/32 字节),启用 AES-256-CBC
22
31
  * @param {boolean} options.versioning - 是否启用版本历史(默认 false)
23
32
  * @param {boolean} options.wal - 是否启用 WAL 模式(默认 false,内存模式自动禁用)
@@ -33,6 +42,21 @@ class Database {
33
42
  autoSaveInterval: options.autoSaveInterval || 0,
34
43
  pretty: options.pretty !== false
35
44
  };
45
+
46
+ // 存储模式: 'memory' | 'hybrid' | 'disk' | null(旧文件模式: 全量 save)
47
+ this._mode = options.mode || (this._memoryMode ? 'memory' : null);
48
+ this._dirMode = this._mode === 'hybrid' || this._mode === 'disk';
49
+ this._memReserve = (options.memReserveMB !== undefined ? options.memReserveMB : 512) * 1024 * 1024;
50
+ this._flushInterval = options.flushInterval !== undefined
51
+ ? options.flushInterval
52
+ : (this._mode === 'disk' ? 50 : 200);
53
+ this._evictInterval = options.evictInterval !== undefined ? options.evictInterval : 1000;
54
+ this._dirtyTables = new Set();
55
+ this._flushTimer = null;
56
+ this._monitorTimer = null;
57
+ this._lastAccess = {};
58
+ this._meta = { version: 1, tables: {} };
59
+
36
60
  this._tables = {};
37
61
  this._dirty = false;
38
62
  this._autoSaveTimer = null;
@@ -43,6 +67,19 @@ class Database {
43
67
  this._versions = {}; // tableName -> [{ rows, timestamp }]
44
68
  this._maxVersions = options.maxVersions || 50;
45
69
  this._plugins = [];
70
+ this._hooks = {
71
+ beforeInsert: [], afterInsert: [],
72
+ beforeUpdate: [], afterUpdate: [],
73
+ beforeDelete: [], afterDelete: [],
74
+ beforeFind: [], afterFind: [],
75
+ beforeCreateTable: [], afterCreateTable: [],
76
+ beforeDropTable: [], afterDropTable: [],
77
+ beforeFlush: [], afterFlush: [],
78
+ beforeCount: [], afterCount: [],
79
+ onStart: [], onStop: []
80
+ };
81
+ this._eventListeners = [];
82
+ this._tableNames = new Set(Object.keys(this._tables));
46
83
  this._migrations = [];
47
84
  this._observers = []; // 响应式观察者
48
85
  this._views = {}; // 视图 { name: { query, table } }
@@ -72,12 +109,158 @@ class Database {
72
109
  this._slowQueries = [];
73
110
 
74
111
  // 从文件加载
75
- if (!this._memoryMode && fs.existsSync(this._filePath)) {
112
+ if (!this._memoryMode && !this._dirMode && fs.existsSync(this._filePath)) {
76
113
  this._loading = true;
77
114
  this._acquireLock();
78
115
  this._load();
79
116
  this._loading = false;
80
117
  }
118
+
119
+ // 磁盘/混合模式: 目录存储(每表独立 JSql 文件,增量刷盘)
120
+ if (this._dirMode) {
121
+ this._initDirStore();
122
+ }
123
+ }
124
+
125
+ // ============================================================
126
+ // 磁盘/混合存储(Redis 式: 内存缓存 + 异步刷盘 + LRU 驱逐)
127
+ // ============================================================
128
+
129
+ _initDirStore() {
130
+ if (!this._filePath) throw new Error('hybrid/disk mode requires a directory path');
131
+ fs.mkdirSync(this._filePath, { recursive: true });
132
+ this._metaPath = path.join(this._filePath, 'meta.json');
133
+ this._meta = { version: 1, tables: {} };
134
+ try {
135
+ if (fs.existsSync(this._metaPath)) {
136
+ const parsed = JSON.parse(fs.readFileSync(this._metaPath, 'utf8'));
137
+ if (parsed && parsed.tables) this._meta = parsed;
138
+ }
139
+ } catch (e) {
140
+ this._meta = { version: 1, tables: {} };
141
+ }
142
+ this._startMonitor();
143
+ }
144
+
145
+ _tableFile(name) {
146
+ const meta = this._meta.tables[name];
147
+ if (meta && meta.file) return path.join(this._filePath, meta.file);
148
+ return path.join(this._filePath, encodeURIComponent(name) + '.jsql');
149
+ }
150
+
151
+ _saveMeta() {
152
+ if (!this._dirMode) return;
153
+ try {
154
+ fs.writeFileSync(this._metaPath, JSON.stringify(this._meta, null, 2), 'utf8');
155
+ } catch (e) {
156
+ // 忽略 meta 写入失败
157
+ }
158
+ }
159
+
160
+ _touchTable(name) {
161
+ this._lastAccess[name] = Date.now();
162
+ }
163
+
164
+ _ensureTable(name) {
165
+ if (this._tables[name]) return this._tables[name];
166
+ if (!this._dirMode) return null;
167
+ const meta = this._meta.tables[name];
168
+ if (!meta) return null;
169
+ const file = path.join(this._filePath, meta.file);
170
+ if (!fs.existsSync(file)) return null;
171
+ try {
172
+ const fmt = new JSQLFormat(file);
173
+ const { rows } = fmt.readTableSync(name);
174
+ const table = new Table(name, meta.schema, this);
175
+ if (rows && rows.length > 0) table._loadRows(rows);
176
+ this._tables[name] = table;
177
+ if (this._versioning) this._versions[name] = [];
178
+ this._touchTable(name);
179
+ return table;
180
+ } catch (e) {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ _markDirty(name) {
186
+ if (!this._dirMode) {
187
+ this._dirty = true;
188
+ return;
189
+ }
190
+ this._touchTable(name);
191
+ this._dirtyTables.add(name);
192
+ this._scheduleFlush();
193
+ }
194
+
195
+ _scheduleFlush() {
196
+ if (this._flushTimer) return;
197
+ if (this._flushInterval <= 0) {
198
+ this._flushTimer = setImmediate(() => {
199
+ this._flushTimer = null;
200
+ try { this._flushDirty(); } catch (e) {}
201
+ });
202
+ return;
203
+ }
204
+ this._flushTimer = setTimeout(() => {
205
+ this._flushTimer = null;
206
+ try { this._flushDirty(); } catch (e) {}
207
+ }, this._flushInterval);
208
+ if (this._flushTimer.unref) this._flushTimer.unref();
209
+ }
210
+
211
+ _flushDirty() {
212
+ if (!this._dirMode || this._dirtyTables.size === 0) return;
213
+ const names = [...this._dirtyTables];
214
+ this._dirtyTables.clear();
215
+ for (const name of names) this._flushTable(name);
216
+ this._saveMeta();
217
+ }
218
+
219
+ _flushTable(name) {
220
+ const table = this._tables[name];
221
+ if (!table) return;
222
+ const meta = this._meta.tables[name];
223
+ let file = meta && meta.file ? meta.file : encodeURIComponent(name) + '.jsql';
224
+ const fmt = new JSQLFormat(path.join(this._filePath, file));
225
+ try {
226
+ const pkField = Object.keys(table._schema).find(f => table._schema[f].primaryKey) || null;
227
+ fmt.writeTable(name, table._schema, table._rows, pkField);
228
+ this._meta.tables[name] = { file, schema: table._schema };
229
+ } finally {
230
+ fmt._close();
231
+ }
232
+ }
233
+
234
+ _startMonitor() {
235
+ if (this._monitorTimer) return;
236
+ this._monitorTimer = setInterval(() => {
237
+ try { this._checkMemory(); } catch (e) {}
238
+ }, this._evictInterval);
239
+ if (this._monitorTimer.unref) this._monitorTimer.unref();
240
+ }
241
+
242
+ _checkMemory() {
243
+ const total = os.totalmem();
244
+ if (!total) return;
245
+ const budget = total - this._memReserve;
246
+ if (budget <= 0) return;
247
+ const rss = process.memoryUsage().rss;
248
+ if (rss <= budget) return;
249
+ this._evictTables(budget);
250
+ }
251
+
252
+ _evictTables(budget) {
253
+ const candidates = Object.keys(this._tables)
254
+ .filter(n => n !== '__meta__' && !this._versioning && this._meta.tables[n]);
255
+ candidates.sort((a, b) => (this._lastAccess[a] || 0) - (this._lastAccess[b] || 0));
256
+ for (const name of candidates) {
257
+ if (budget !== undefined && process.memoryUsage().rss <= budget) break;
258
+ try { this._flushTable(name); } catch (e) { continue; }
259
+ delete this._tables[name];
260
+ delete this[name];
261
+ this._dirtyTables.delete(name);
262
+ }
263
+ this._saveMeta();
81
264
  }
82
265
 
83
266
  // ============================================================
@@ -191,14 +374,21 @@ class Database {
191
374
  // ============================================================
192
375
 
193
376
  createTable(name, schema) {
194
- if (this._tables[name]) {
377
+ if (!this._runHooks('beforeCreateTable', [name, schema])) throw createError('ER_PLUGIN_ABORT', 'createTable aborted by plugin');
378
+ if (this._tables[name] || (this._dirMode && this._meta.tables[name])) {
195
379
  throw createError('ER_TABLE_EXISTS_ERROR', name);
196
380
  }
197
- const table = new Table(name, schema, this);
381
+ const normalized = {};
382
+ for (const [field, def] of Object.entries(schema || {})) {
383
+ if (typeof def === 'string') normalized[field] = { type: def.toLowerCase() };
384
+ else normalized[field] = def;
385
+ }
386
+ const table = new Table(name, normalized, this);
198
387
  this._tables[name] = table;
388
+ this._tableNames.add(name);
199
389
 
200
390
  Object.defineProperty(this, name, {
201
- get: () => this._tables[name],
391
+ get: () => this._ensureTable(name) || this._tables[name],
202
392
  enumerable: true,
203
393
  configurable: true
204
394
  });
@@ -206,27 +396,59 @@ class Database {
206
396
  if (this._versioning) {
207
397
  this._versions[name] = [];
208
398
  }
399
+ if (this._dirMode) {
400
+ this._meta.tables[name] = { file: encodeURIComponent(name) + '.jsql', schema: normalized };
401
+ this._saveMeta();
402
+ }
209
403
 
210
- this._markDirty();
404
+ this._markDirty(name);
211
405
  this._walWrite({ op: 'createTable', table: name, schema });
406
+ this._emit('createTable', { name, schema });
407
+ this._runHooks('afterCreateTable', [name, schema]);
212
408
  return table;
213
409
  }
214
410
 
215
411
  dropTable(name) {
216
- if (!this._tables[name]) throw createError('ER_NO_SUCH_TABLE', name);
217
- delete this._tables[name];
412
+ if (!this._tables[name] && !(this._dirMode && this._meta.tables[name])) throw createError('ER_NO_SUCH_TABLE', name);
413
+ if (!this._runHooks('beforeDropTable', [name])) return;
414
+ if (this._tables[name]) {
415
+ delete this._tables[name];
416
+ }
218
417
  delete this[name];
219
418
  delete this._versions[name];
419
+ this._tableNames.delete(name);
420
+ if (this._dirMode) {
421
+ const meta = this._meta.tables[name];
422
+ if (meta && meta.file) {
423
+ try { fs.unlinkSync(path.join(this._filePath, meta.file)); } catch (e) {}
424
+ }
425
+ delete this._meta.tables[name];
426
+ this._dirtyTables.delete(name);
427
+ this._saveMeta();
428
+ }
220
429
  this._walWrite({ op: 'dropTable', table: name });
221
- this._markDirty();
430
+ this._markDirty(name);
431
+ this._emit('dropTable', { name });
432
+ this._runHooks('afterDropTable', [name]);
222
433
  }
223
434
 
224
435
  hasTable(name) {
225
- return !!this._tables[name];
436
+ return !!this._tables[name] || (this._dirMode && !!this._meta.tables[name]);
226
437
  }
227
438
 
228
439
  getTables() {
229
- return Object.keys(this._tables);
440
+ const names = new Set(Object.keys(this._tables));
441
+ if (this._dirMode) {
442
+ for (const name of Object.keys(this._meta.tables)) names.add(name);
443
+ }
444
+ return [...names];
445
+ }
446
+
447
+ getTableSchema(name) {
448
+ const table = this._ensureTable(name);
449
+ if (table) return table._schema;
450
+ if (this._dirMode && this._meta.tables[name]) return this._meta.tables[name].schema;
451
+ return null;
230
452
  }
231
453
 
232
454
  // ============================================================
@@ -285,7 +507,7 @@ class Database {
285
507
  */
286
508
  createTrigger(name, options, callback) {
287
509
  if (this._triggers[name]) throw createError('ER_TRIGGER_EXISTS', name);
288
- const table = this._tables[options.table];
510
+ const table = this._ensureTable(options.table);
289
511
  if (!table) throw createError('ER_NO_SUCH_TABLE', options.table);
290
512
 
291
513
  const hookName = options.timing + options.event.charAt(0).toUpperCase() + options.event.slice(1);
@@ -296,7 +518,7 @@ class Database {
296
518
  dropTrigger(name) {
297
519
  const trigger = this._triggers[name];
298
520
  if (!trigger) throw createError('ER_TRIGGER_NOT_FOUND', name);
299
- const table = this._tables[trigger.table];
521
+ const table = this._ensureTable(trigger.table);
300
522
  if (table) table.off(trigger.hookName, trigger.callback);
301
523
  delete this._triggers[name];
302
524
  }
@@ -320,7 +542,7 @@ class Database {
320
542
  * @param {object} options - { delimiter: ',', hasHeader: true, skipRows: 0, mapping: { csvCol: schemaCol } }
321
543
  */
322
544
  importCSV(filePath, tableName, options = {}) {
323
- const table = this._tables[tableName];
545
+ const table = this._ensureTable(tableName);
324
546
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
325
547
 
326
548
  const delimiter = options.delimiter || ',';
@@ -375,7 +597,7 @@ class Database {
375
597
  * 导出表数据到 CSV 文件
376
598
  */
377
599
  exportCSV(filePath, tableName, query = {}, options = {}) {
378
- const table = this._tables[tableName];
600
+ const table = this._ensureTable(tableName);
379
601
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
380
602
 
381
603
  const delimiter = options.delimiter || ',';
@@ -627,7 +849,7 @@ class Database {
627
849
  this._walWrite({ op: 'commit', at: this._transaction.startedAt });
628
850
  this._transaction = null;
629
851
  this._checkpoint();
630
- this._markDirty();
852
+ this._markDirtyLegacy();
631
853
  }
632
854
 
633
855
  rollback() {
@@ -708,7 +930,7 @@ class Database {
708
930
  if (!this._versioning) throw createError('ER_NOT_SUPPORTED', 'Versioning is not enabled');
709
931
  const versions = this._versions[tableName];
710
932
  if (!versions || versions.length < 2) throw createError('ER_NOT_SUPPORTED', 'No versions to undo');
711
- const table = this._tables[tableName];
933
+ const table = this._ensureTable(tableName);
712
934
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
713
935
 
714
936
  for (let i = 0; i < steps && versions.length > 1; i++) {
@@ -727,7 +949,7 @@ class Database {
727
949
  }
728
950
  }
729
951
  this._walWrite({ op: 'undo', table: tableName, steps });
730
- this._markDirty();
952
+ this._markDirtyLegacy();
731
953
  return versions.length;
732
954
  }
733
955
 
@@ -793,14 +1015,243 @@ class Database {
793
1015
 
794
1016
  /**
795
1017
  * 注册插件
796
- * @param {object} plugin - { name, install(db) }
1018
+ * @param {object|function} plugin - { name, install(db, ctx), hooks, onEvent } 或函数 (作为 install)
797
1019
  */
798
1020
  use(plugin) {
799
- if (typeof plugin.install !== 'function') {
800
- throw createError('ER_PLUGIN_INVALID', 'Plugin must have an install() method');
1021
+ if (typeof plugin === 'function') {
1022
+ plugin = { install: plugin };
1023
+ }
1024
+ if (plugin.hooks) {
1025
+ for (const [event, fn] of Object.entries(plugin.hooks)) {
1026
+ if (this._hooks[event]) {
1027
+ this._hooks[event].push(fn);
1028
+ }
1029
+ }
1030
+ }
1031
+ if (typeof plugin.install === 'function') {
1032
+ plugin.install(this, this._buildCtx(plugin));
1033
+ }
1034
+ if (typeof plugin.onEvent === 'function') {
1035
+ this._eventListeners.push(plugin.onEvent);
801
1036
  }
802
1037
  this._plugins.push(plugin);
803
- plugin.install(this);
1038
+ return this;
1039
+ }
1040
+
1041
+ _buildCtx(plugin) {
1042
+ const self = this;
1043
+ return {
1044
+ name: plugin.name || 'anonymous',
1045
+ engine: this,
1046
+ plugin,
1047
+ on(hook, fn) { return self.on(hook, fn); },
1048
+ onEvent(fn) { return self.onEvent(fn); },
1049
+ emit(eventName, data) { self._emit(eventName, data); },
1050
+ tables() { return Array.from(self._tableNames); },
1051
+ hasTable(name) { return self._tableNames.has(name); },
1052
+ getTableSchema(name) {
1053
+ const t = self._tables[name];
1054
+ return t ? t._schema : null;
1055
+ },
1056
+ table(name) { return self._tables[name] || null; }
1057
+ };
1058
+ }
1059
+
1060
+ on(event, fn) {
1061
+ if (this._hooks[event]) {
1062
+ this._hooks[event].push(fn);
1063
+ }
1064
+ return this;
1065
+ }
1066
+
1067
+ onEvent(fn) {
1068
+ this._eventListeners.push(fn);
1069
+ return this;
1070
+ }
1071
+
1072
+ _emit(eventName, data) {
1073
+ for (const fn of this._eventListeners) {
1074
+ try { fn(eventName, data); } catch (e) { /* ignore */ }
1075
+ }
1076
+ }
1077
+
1078
+ _runHooks(hookName, args) {
1079
+ const hooks = this._hooks[hookName];
1080
+ if (!hooks || hooks.length === 0) return true;
1081
+ for (const fn of hooks) {
1082
+ const r = fn(...args);
1083
+ if (r === false) return false;
1084
+ if (r !== undefined && args.length > 0) {
1085
+ args[0] = r;
1086
+ }
1087
+ }
1088
+ return true;
1089
+ }
1090
+
1091
+ // ============================================================
1092
+ // 统一高层 API(与 Native/WASM 引擎对齐)
1093
+ // ============================================================
1094
+
1095
+ _pkOf(table) {
1096
+ return table._primaryKey || table._autoIncrementField || null;
1097
+ }
1098
+
1099
+ async start() {
1100
+ this._runHooks('onStart', []);
1101
+ this._emit('start', {});
1102
+ return this;
1103
+ }
1104
+
1105
+ insert(tableName, data) {
1106
+ const table = this._ensureTable(tableName);
1107
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1108
+ const arr = Array.isArray(data) ? data : [data];
1109
+ if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
1110
+ const pk = this._pkOf(table);
1111
+ const ids = [];
1112
+ for (const row of arr) {
1113
+ const r = table.insert(row);
1114
+ ids.push(pk ? r[pk] : (table._rows.indexOf(r) + 1));
1115
+ }
1116
+ this._emit('insert', { table: tableName, count: arr.length, ids });
1117
+ this._runHooks('afterInsert', [tableName, arr, ids]);
1118
+ this._markDirty(tableName);
1119
+ return ids;
1120
+ }
1121
+
1122
+ async flush() {
1123
+ if (!this._runHooks('beforeFlush', [])) return;
1124
+ if (this._dirMode) {
1125
+ this._flushDirty();
1126
+ this._saveMeta();
1127
+ } else if (!this._memoryMode) {
1128
+ this.save();
1129
+ }
1130
+ this._runHooks('afterFlush', []);
1131
+ return this;
1132
+ }
1133
+
1134
+ _resolveId(table, id) {
1135
+ const pk = this._pkOf(table);
1136
+ if (pk) return table._rows.find(r => r[pk] === id) || null;
1137
+ return table._rows[id - 1] || null;
1138
+ }
1139
+
1140
+ find(tableName, filter, opts = {}) {
1141
+ const table = this._ensureTable(tableName);
1142
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1143
+ if (!this._runHooks('beforeFind', [tableName, { filter, opts }])) return [];
1144
+ const { limit = 100, offset = 0 } = opts;
1145
+ let rows = table._applyFilter(table._rows, filter || {});
1146
+ rows = rows.slice(offset, offset + limit);
1147
+ this._touchTable(tableName);
1148
+ this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
1149
+ return rows;
1150
+ }
1151
+
1152
+ findById(tableName, id) {
1153
+ const table = this._ensureTable(tableName);
1154
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1155
+ if (!this._runHooks('beforeFind', [tableName, { id }])) return null;
1156
+ const row = this._resolveId(table, id);
1157
+ this._touchTable(tableName);
1158
+ this._runHooks('afterFind', [tableName, { id }, row]);
1159
+ return row;
1160
+ }
1161
+
1162
+ findByIds(tableName, ids) {
1163
+ const table = this._ensureTable(tableName);
1164
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1165
+ if (!this._runHooks('beforeFind', [tableName, { ids }])) return null;
1166
+ const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null);
1167
+ this._touchTable(tableName);
1168
+ this._runHooks('afterFind', [tableName, { ids }, rows]);
1169
+ return rows;
1170
+ }
1171
+
1172
+ findByIdsRaw(tableName, ids) {
1173
+ return this.findByIds(tableName, ids);
1174
+ }
1175
+
1176
+ count(tableName) {
1177
+ const table = this._ensureTable(tableName);
1178
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1179
+ this._runHooks('beforeCount', [tableName]);
1180
+ const n = table._rows.length;
1181
+ this._touchTable(tableName);
1182
+ this._runHooks('afterCount', [tableName, n]);
1183
+ return n;
1184
+ }
1185
+
1186
+ updateById(tableName, id, data) {
1187
+ const table = this._ensureTable(tableName);
1188
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1189
+ if (!this._runHooks('beforeUpdate', [tableName, id, data])) return;
1190
+ const row = this._resolveId(table, id);
1191
+ if (!row) return { ok: false, error: 'not found' };
1192
+ Object.assign(row, data);
1193
+ this._emit('update', { table: tableName, id, data });
1194
+ this._runHooks('afterUpdate', [tableName, id, data, { ok: true }]);
1195
+ this._markDirty(tableName);
1196
+ return { ok: true };
1197
+ }
1198
+
1199
+ updateByIds(tableName, entries) {
1200
+ const table = this._ensureTable(tableName);
1201
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1202
+ const pairs = entries.map(([id, data]) => [id, data]);
1203
+ if (!this._runHooks('beforeUpdate', [tableName, pairs])) return;
1204
+ let count = 0;
1205
+ for (const [id, data] of pairs) {
1206
+ const row = this._resolveId(table, id);
1207
+ if (row) { Object.assign(row, data); count++; }
1208
+ }
1209
+ this._emit('update', { table: tableName, entries: pairs, result: { ok: true, count } });
1210
+ this._runHooks('afterUpdate', [tableName, pairs, { ok: true, count }]);
1211
+ this._markDirty(tableName);
1212
+ return { ok: true, count };
1213
+ }
1214
+
1215
+ removeById(tableName, id) {
1216
+ const table = this._ensureTable(tableName);
1217
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1218
+ if (!this._runHooks('beforeDelete', [tableName, id])) return;
1219
+ const idx = table._rows.findIndex(r => r === this._resolveId(table, id));
1220
+ if (idx === -1) return { ok: false, error: 'not found' };
1221
+ table._rows.splice(idx, 1);
1222
+ this._emit('delete', { table: tableName, id });
1223
+ this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1224
+ this._markDirty(tableName);
1225
+ return { ok: true };
1226
+ }
1227
+
1228
+ removeByIds(tableName, ids) {
1229
+ const table = this._ensureTable(tableName);
1230
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1231
+ if (!this._runHooks('beforeDelete', [tableName, ids])) return;
1232
+ let removed = 0;
1233
+ for (const id of ids) {
1234
+ const idx = table._rows.findIndex(r => r === this._resolveId(table, id));
1235
+ if (idx !== -1) {
1236
+ table._rows.splice(idx, 1);
1237
+ removed++;
1238
+ }
1239
+ }
1240
+ this._emit('delete', { table: tableName, ids, result: { ok: true, count: removed } });
1241
+ this._runHooks('afterDelete', [tableName, ids, { ok: true, count: removed }]);
1242
+ this._markDirty(tableName);
1243
+ return { ok: true, count: removed };
1244
+ }
1245
+
1246
+ async stop() {
1247
+ this._runHooks('onStop', []);
1248
+ this._emit('stop', {});
1249
+ if (this._dirMode) {
1250
+ try {
1251
+ this._flushDirty();
1252
+ this._saveMeta();
1253
+ } catch (e) {}
1254
+ }
804
1255
  return this;
805
1256
  }
806
1257
 
@@ -886,7 +1337,7 @@ class Database {
886
1337
  }
887
1338
  }
888
1339
  this._walWrite({ op: 'restore', from: filePath });
889
- this._markDirty();
1340
+ this._markDirtyLegacy();
890
1341
  }
891
1342
 
892
1343
  // ============================================================
@@ -902,6 +1353,11 @@ class Database {
902
1353
  }
903
1354
 
904
1355
  if (this._memoryMode) return;
1356
+ if (this._dirMode) {
1357
+ this._flushDirty();
1358
+ this._saveMeta();
1359
+ return;
1360
+ }
905
1361
 
906
1362
  if (this._jsqlMode) {
907
1363
  const all = {};
@@ -972,7 +1428,7 @@ class Database {
972
1428
  }
973
1429
  }
974
1430
 
975
- _markDirty() {
1431
+ _markDirtyLegacy() {
976
1432
  this._dirty = true;
977
1433
  if (this._loading) return;
978
1434
  if (this._options.autoSave && !this._autoSaveTimer) {
@@ -1016,7 +1472,7 @@ class Database {
1016
1472
  }
1017
1473
  }
1018
1474
  this._walWrite({ op: 'import', tables: Object.keys(data) });
1019
- this._markDirty();
1475
+ this._markDirtyLegacy();
1020
1476
  }
1021
1477
 
1022
1478
  // ============================================================