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.
- package/LICENSE +202 -0
- package/bin/jsql +97 -0
- package/index.js +20 -0
- package/lib/database.js +484 -28
- package/lib/mod.js +316 -0
- package/lib/mysql_compat.js +232 -0
- package/lib/mysql_server.js +514 -0
- package/lib/native_client.js +470 -0
- package/lib/nedb_compat.js +506 -0
- package/lib/plugin.js +35 -0
- package/lib/sql.js +1160 -0
- package/lib/table.js +42 -21
- package/lib/wasm_client.js +176 -8
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.toml +24 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +449 -0
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +147 -0
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +41 -0
- package/nativesrc/jsql-neo-core/src/engine/table.rs +664 -0
- package/nativesrc/jsql-neo-core/src/lib.rs +3 -0
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +1 -0
- package/nativesrc/jsql-neo-core/src/storage/persistent.rs +2 -0
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -0
- package/nativesrc/jsql-neo-core/src/types.rs +94 -0
- package/nativesrc/jsql-neo-native/Cargo.lock +606 -0
- package/nativesrc/jsql-neo-native/Cargo.toml +16 -0
- package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/package.json +7 -0
- package/nativesrc/jsql-neo-native/src/lib.rs +281 -0
- package/package.json +9 -1
- package/wasm/jsql_neo_wasm.d.ts +8 -0
- package/wasm/jsql_neo_wasm.js +455 -6
- package/wasm/jsql_neo_wasm_bg.js +22 -0
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +4 -0
- 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 -
|
|
17
|
+
* @param {string} filePath - 数据库文件/目录路径,传 null 或 ':memory:' 使用内存模式
|
|
17
18
|
* @param {object} options
|
|
18
|
-
* @param {
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
800
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1475
|
+
this._markDirtyLegacy();
|
|
1020
1476
|
}
|
|
1021
1477
|
|
|
1022
1478
|
// ============================================================
|