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
@@ -1,733 +1,733 @@
1
- const path = require('path');
2
- const os = require('os');
3
- const { executeSQL } = require('./sql');
4
- const native = require(path.join(__dirname, '..', 'native', 'jsql-neo-native.node'));
5
- const INT64_TAG = 1;
6
- const FLOAT_TAG = 2;
7
- const STR_TAG = 3;
8
- const BOOL_TAG = 4;
9
- const INT32_TAG = 5;
10
-
11
- function safeParse(str, fallback) {
12
- if (typeof str !== 'string') return fallback;
13
- try { return JSON.parse(str); } catch (e) { return fallback; }
14
- }
15
-
16
- const NATIVE_TYPE_MAP = {
17
- text: 'string',
18
- varchar: 'string',
19
- double: 'float',
20
- number: 'float',
21
- numeric: 'float',
22
- decimal: 'float',
23
- date: 'string',
24
- timestamp: 'string',
25
- datetime: 'string',
26
- json: 'string',
27
- array: 'string',
28
- object: 'string',
29
- bool: 'boolean',
30
- bigint: 'string',
31
- int: 'integer',
32
- };
33
-
34
- function parseShorthandSchema(str) {
35
- const def = { type: str };
36
- const strip = (re) => {
37
- const next = def.type.replace(re, '').trim();
38
- def.type = next || def.type;
39
- };
40
- if (/\bprimary\s+key\b/i.test(def.type)) { def.primaryKey = true; strip(/\bprimary\s+key\b/gi); }
41
- if (/\bauto_?increment\b/i.test(def.type)) { def.autoIncrement = true; strip(/\bauto_?increment\b/gi); }
42
- if (/\bnot\s+null\b/i.test(def.type)) { def.required = true; strip(/\bnot\s+null\b/gi); }
43
- if (/\bunique\b/i.test(def.type)) { def.unique = true; strip(/\bunique\b/gi); }
44
- if (/\bdefault\s+(\S+)/i.test(def.type)) {
45
- const m = def.type.match(/\bdefault\s+(\S+)/i);
46
- def.default = m[1].replace(/^['"]|['"]$/g, '');
47
- strip(/\bdefault\s+\S+/gi);
48
- }
49
- return def;
50
- }
51
-
52
- function parseSchemaString(str) {
53
- const out = {};
54
- for (const seg of str.split(',')) {
55
- const trimmed = seg.trim();
56
- if (!trimmed) continue;
57
- const parts = trimmed.split(/\s+/);
58
- const name = parts.shift();
59
- const def = parseShorthandSchema(parts.join(' ').toLowerCase().trim());
60
- if (def.type.length === 0) def.type = 'string';
61
- out[name] = def;
62
- }
63
- return out;
64
- }
65
-
66
- function mapNativeSchema(schema) {
67
- if (typeof schema === 'string') schema = parseSchemaString(schema);
68
- const mapped = {};
69
- for (const [field, def] of Object.entries(schema || {})) {
70
- if (typeof def === 'string') {
71
- const parsed = parseShorthandSchema(def.toLowerCase().trim());
72
- if (parsed.type.length === 0) parsed.type = 'string';
73
- mapped[field] = { ...parsed, type: NATIVE_TYPE_MAP[parsed.type] || parsed.type };
74
- } else if (def && typeof def === 'object') {
75
- const t = (def.type || 'string').toLowerCase();
76
- const out = { ...def, type: NATIVE_TYPE_MAP[t] || t };
77
- if (out.primary_key !== undefined && out.primaryKey === undefined) { out.primaryKey = out.primary_key; delete out.primary_key; }
78
- if (out.auto_increment !== undefined && out.autoIncrement === undefined) { out.autoIncrement = out.auto_increment; delete out.auto_increment; }
79
- mapped[field] = out;
80
- } else {
81
- mapped[field] = { type: 'string' };
82
- }
83
- }
84
- return mapped;
85
- }
86
-
87
- function restoreRow(row, schema) {
88
- if (typeof row !== 'object' || row === null) return row;
89
- let src = row;
90
- if (row.fields && typeof row.fields === 'object' && row.fields !== null) {
91
- src = { ...row.fields };
92
- if (row.id !== undefined) {
93
- const pkCols = [];
94
- if (schema) {
95
- for (const [k, def] of Object.entries(schema)) {
96
- if (def && typeof def === 'object' && def.autoIncrement) continue;
97
- const isPk = typeof def === 'string'
98
- ? /\bprimary\s+key\b/i.test(def)
99
- : !!(def && (def.primaryKey || def.primary_key));
100
- if (isPk) pkCols.push(k);
101
- }
102
- } else if (src.id === undefined) {
103
- pkCols.push('id');
104
- }
105
- for (const c of pkCols) {
106
- const v = src[c];
107
- if (v === undefined || v === null || v === 0 || v === '' || v === false) src[c] = row.id;
108
- }
109
- }
110
- if (row.created_at !== undefined) src.created_at = row.created_at;
111
- if (row.updated_at !== undefined) src.updated_at = row.updated_at;
112
- }
113
- if (!schema) return src;
114
- const out = {};
115
- for (const [field, value] of Object.entries(src)) {
116
- const colDef = schema[field];
117
- let t = null;
118
- if (typeof colDef === 'string') t = colDef.toLowerCase();
119
- else if (colDef && typeof colDef === 'object' && colDef.type) t = String(colDef.type).toLowerCase();
120
- if (['json', 'array', 'object'].includes(t) && typeof value === 'string') {
121
- try { out[field] = JSON.parse(value); } catch (e) { out[field] = value; }
122
- } else if ((t === 'integer' || t === 'int' || t === 'bigint') && typeof value === 'string' && /^-?\d+$/.test(value)) {
123
- out[field] = Number(value);
124
- } else if ((t === 'float' || t === 'double' || t === 'number') && typeof value === 'string' && !Number.isNaN(Number(value))) {
125
- out[field] = Number(value);
126
- } else {
127
- out[field] = value;
128
- }
129
- }
130
- return out;
131
- }
132
-
133
- function _encodedFieldSize(v) {
134
- if (v === null || v === undefined) return 1;
135
- if (typeof v === 'number') {
136
- if (Number.isInteger(v)) return (v >= -2147483648 && v <= 2147483647) ? 1 + 4 : 1 + 8;
137
- return 1 + 8;
138
- }
139
- if (typeof v === 'boolean') return 1 + 1;
140
- const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
141
- return 1 + 4 + Buffer.byteLength(s, 'utf8');
142
- }
143
-
144
- function encodeField(buf, off, v) {
145
- if (v === null || v === undefined) {
146
- buf.writeUInt8(0, off); return off + 1;
147
- }
148
- if (typeof v === 'number') {
149
- if (Number.isInteger(v)) {
150
- if (v >= -2147483648 && v <= 2147483647) {
151
- buf.writeUInt8(INT32_TAG, off); buf.writeInt32LE(v, off + 1); return off + 5;
152
- }
153
- buf.writeUInt8(INT64_TAG, off); buf.writeBigInt64LE(BigInt(v), off + 1); return off + 9;
154
- }
155
- buf.writeUInt8(FLOAT_TAG, off); buf.writeDoubleLE(v, off + 1); return off + 9;
156
- }
157
- if (typeof v === 'string') {
158
- const sl = Buffer.byteLength(v, 'utf8');
159
- buf.writeUInt8(STR_TAG, off);
160
- buf.writeUInt32LE(sl, off + 1);
161
- buf.write(v, off + 5, sl, 'utf8');
162
- return off + 5 + sl;
163
- }
164
- if (typeof v === 'boolean') {
165
- buf.writeUInt8(BOOL_TAG, off); buf.writeUInt8(v ? 1 : 0, off + 1); return off + 2;
166
- }
167
- const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
168
- const sl = Buffer.byteLength(s, 'utf8');
169
- buf.writeUInt8(STR_TAG, off);
170
- buf.writeUInt32LE(sl, off + 1);
171
- buf.write(s, off + 5, sl, 'utf8');
172
- return off + 5 + sl;
173
- }
174
-
175
- function encodeBatch(rows) {
176
- if (rows.length === 0) return new Uint8Array(0);
177
- const fieldNames = Object.keys(rows[0]);
178
- const nFields = fieldNames.length;
179
- let size = 1;
180
- for (const s of fieldNames) size += 1 + Buffer.byteLength(s, 'utf8');
181
- size += 4;
182
- for (const row of rows) {
183
- for (let fi = 0; fi < nFields; fi++) size += _encodedFieldSize(row[fieldNames[fi]]);
184
- }
185
- const buf = Buffer.allocUnsafe(size);
186
- let off = 0;
187
-
188
- off = buf.writeUInt8(nFields, off);
189
- for (let fi = 0; fi < nFields; fi++) {
190
- const s = fieldNames[fi];
191
- const nlen = Buffer.byteLength(s, 'utf8');
192
- off = buf.writeUInt8(nlen, off);
193
- off += buf.write(s, off, nlen, 'utf8');
194
- }
195
- off = buf.writeUInt32LE(rows.length, off);
196
-
197
- for (let ri = 0; ri < rows.length; ri++) {
198
- const row = rows[ri];
199
- for (let fi = 0; fi < nFields; fi++) {
200
- off = encodeField(buf, off, row[fieldNames[fi]]);
201
- }
202
- }
203
-
204
- return buf.slice(0, off);
205
- }
206
-
207
- class JSQL {
208
- constructor(opts = {}) {
209
- this._flushThreshold = opts.flushThreshold || 5000;
210
- this._buffer = {};
211
- this._bufferSize = 0;
212
- this._opBuffer = { remove: {}, update: {} };
213
- this._opTimer = null;
214
- this._opFlushInterval = opts.opFlushInterval || 50;
215
- this._tableNames = new Set();
216
- this._schemas = {};
217
- this._txId = undefined;
218
- this._autoPlugins = opts.modules !== false;
219
-
220
- // 持久化模式: 'memory' | 'hybrid' | 'disk'
221
- this._path = opts.path || null;
222
- this._mode = opts.mode || (this._path ? 'hybrid' : 'memory');
223
- this._memReserveMB = opts.memReserveMB !== undefined ? opts.memReserveMB : 512;
224
- this._flushInterval = opts.flushInterval !== undefined
225
- ? opts.flushInterval
226
- : (this._mode === 'disk' ? 50 : 200);
227
- this._evictInterval = opts.evictInterval !== undefined ? opts.evictInterval : 1000;
228
- this._diskFlushTimer = null;
229
- this._evictTimer = null;
230
-
231
- this._plugins = [];
232
- this._hooks = {
233
- beforeInsert: [], afterInsert: [],
234
- beforeUpdate: [], afterUpdate: [],
235
- beforeDelete: [], afterDelete: [],
236
- beforeFind: [], afterFind: [],
237
- beforeCreateTable: [], afterCreateTable: [],
238
- beforeDropTable: [], afterDropTable: [],
239
- beforeFlush: [], afterFlush: [],
240
- beforeCount: [], afterCount: [],
241
- onStart: [], onStop: []
242
- };
243
- this._eventListeners = [];
244
- if (this._autoPlugins) {
245
- const { ModuleManager } = require('./mod');
246
- new ModuleManager().applyTo(this);
247
- }
248
- }
249
-
250
- use(plugin) {
251
- if (typeof plugin === 'function') {
252
- plugin = { install: plugin };
253
- }
254
- if (plugin.hooks) {
255
- for (const [event, fn] of Object.entries(plugin.hooks)) {
256
- if (this._hooks[event]) {
257
- this._hooks[event].push(fn);
258
- }
259
- }
260
- }
261
- if (typeof plugin.install === 'function') {
262
- plugin.install(this, this._buildCtx(plugin));
263
- }
264
- if (typeof plugin.onEvent === 'function') {
265
- this._eventListeners.push(plugin.onEvent);
266
- }
267
- this._plugins.push(plugin);
268
- return this;
269
- }
270
-
271
- _buildCtx(plugin) {
272
- const self = this;
273
- return {
274
- name: plugin.name || 'anonymous',
275
- engine: this,
276
- plugin,
277
- on(hook, fn) { return self.on(hook, fn); },
278
- onEvent(fn) { return self.onEvent(fn); },
279
- emit(eventName, data) { self._emit(eventName, data); },
280
- tables() { return Array.from(self._tableNames); },
281
- hasTable(name) { return self._tableNames.has(name); },
282
- getTableSchema(name) { return self._schemas[name] || null; }
283
- };
284
- }
285
-
286
- on(event, fn) {
287
- if (this._hooks[event]) {
288
- this._hooks[event].push(fn);
289
- }
290
- return this;
291
- }
292
-
293
- onEvent(fn) {
294
- this._eventListeners.push(fn);
295
- return this;
296
- }
297
-
298
- _emit(eventName, data) {
299
- for (const fn of this._eventListeners) {
300
- try { fn(eventName, data); } catch (e) { /* ignore */ }
301
- }
302
- }
303
-
304
- _runHooks(hookName, args) {
305
- const hooks = this._hooks[hookName];
306
- if (!hooks || hooks.length === 0) return true;
307
- if (this._inHook) {
308
- // 防止插件 hook 内重入 native 调用导致 N-API 崩溃(段错误)
309
- throw new Error('ER_PLUGIN_REENTRY: plugin hook "' + hookName + '" re-entered native call; plugin must not call engine methods inside its own hook');
310
- }
311
- this._inHook = true;
312
- try {
313
- for (const fn of hooks) {
314
- const r = fn(...args);
315
- if (r === false) return false;
316
- if (r !== undefined && args.length > 0) {
317
- args[0] = r;
318
- }
319
- }
320
- return true;
321
- } finally {
322
- this._inHook = false;
323
- }
324
- }
325
-
326
- async start() {
327
- if (this._mode !== 'memory') {
328
- if (!this._path) throw new Error('hybrid/disk mode requires a directory path');
329
- let r;
330
- try {
331
- r = safeParse(native.jsqlOpen(this._path, this._mode));
332
- } catch (e) {
333
- throw new Error('failed to open native storage: ' + e.message);
334
- }
335
- if (r && r.ok === false) throw new Error(r.error || 'open storage failed');
336
- if (r && Array.isArray(r.tables)) {
337
- this._tableNames = new Set(r.tables);
338
- if (r.schemas) this._schemas = r.schemas;
339
- }
340
- } else {
341
- try {
342
- JSON.parse(native.jsqlOpen('', 'memory'));
343
- } catch (e) {
344
- throw new Error('failed to open native memory storage: ' + e.message);
345
- }
346
- }
347
- if (this._mode !== 'memory') {
348
- if (this._flushInterval > 0) {
349
- this._diskFlushTimer = setInterval(() => {
350
- try { native.jsqlFlushDirty(); } catch (e) { /* ignore */ }
351
- }, this._flushInterval);
352
- if (this._diskFlushTimer.unref) this._diskFlushTimer.unref();
353
- }
354
- this._evictTimer = setInterval(() => {
355
- try { this._checkMemory(); } catch (e) { /* ignore */ }
356
- }, this._evictInterval);
357
- if (this._evictTimer.unref) this._evictTimer.unref();
358
- }
359
- this._runHooks('onStart', []);
360
- this._emit('start', {});
361
- }
362
-
363
- _checkMemory() {
364
- const total = os.totalmem();
365
- if (!total) return;
366
- const budget = total - this._memReserveMB * 1024 * 1024;
367
- if (budget <= 0) return;
368
- if (process.memoryUsage().rss <= budget) return;
369
- for (let i = 0; i < 16; i++) {
370
- let r;
371
- try { r = JSON.parse(native.jsqlEvict()); } catch (e) { return; }
372
- if (r.ok === false || !r.evicted || r.remaining === 0) return;
373
- if (process.memoryUsage().rss <= budget) return;
374
- }
375
- }
376
-
377
- _pkOf(table) {
378
- const schema = this._schemas[table];
379
- if (!schema) return null;
380
- for (const [k, def] of Object.entries(schema)) {
381
- const isPk = (typeof def === 'string' && /\bprimary\s+key\b/i.test(def))
382
- || !!(def && typeof def === 'object' && (def.primaryKey || def.primary_key));
383
- if (isPk) return k;
384
- }
385
- return null;
386
- }
387
-
388
- _pkAutoIncrement(table) {
389
- const schema = this._schemas[table];
390
- const pk = this._pkOf(table);
391
- if (!pk || !schema) return false;
392
- const def = schema[pk];
393
- if (typeof def === 'string') return /\bauto_?increment\b/i.test(def);
394
- return !!(def && def.autoIncrement);
395
- }
396
-
397
- _coercePkId(table, id) {
398
- const schema = this._schemas[table];
399
- const pk = this._pkOf(table);
400
- if (!schema || !pk) return id;
401
- const def = schema[pk];
402
- let t = null;
403
- if (typeof def === 'string') t = def.toLowerCase();
404
- else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
405
- if (['integer', 'int', 'bigint'].includes(t)) return Number(id);
406
- return id;
407
- }
408
-
409
- _findRawById(table, id) {
410
- const pk = this._pkOf(table);
411
- if (pk) {
412
- const filter = {};
413
- filter[pk] = this._coercePkId(table, id);
414
- const r = safeParse(native.jsqlFind(table, JSON.stringify(filter), 1, 0));
415
- if (r && r.error) return r;
416
- return Array.isArray(r) && r.length > 0 ? r[0] : null;
417
- }
418
- return safeParse(native.jsqlFindById(table, Number(id)));
419
- }
420
-
421
- _seqIdByPk(table, id) {
422
- const pk = this._pkOf(table);
423
- if (pk) {
424
- const filter = {};
425
- filter[pk] = this._coercePkId(table, id);
426
- const r = safeParse(native.jsqlFind(table, JSON.stringify(filter), 1, 0));
427
- if (Array.isArray(r) && r.length > 0) return r[0].id;
428
- return null;
429
- }
430
- return Number(id);
431
- }
432
-
433
- async _insertBatch(table, rows) {
434
- const schema = this._schemas[table];
435
- if (schema) {
436
- rows = rows.map(row => {
437
- const out = {};
438
- for (const [k, v] of Object.entries(row)) {
439
- if (v instanceof Date) out[k] = v.toISOString();
440
- else out[k] = v;
441
- }
442
- return out;
443
- });
444
- }
445
- const bin = encodeBatch(rows);
446
- const r = safeParse(native.jsqlInsertBuf(table, bin));
447
- if (r && r.error) throw new Error(r.error);
448
- if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
449
- const pk = this._pkOf(table);
450
- if (pk) {
451
- return r.map((seq, i) => {
452
- const row = rows[i];
453
- return (row && row[pk] !== undefined && row[pk] !== null) ? row[pk] : seq;
454
- });
455
- }
456
- }
457
- return r;
458
- }
459
-
460
- _scheduleOpFlush() {
461
- if (this._opTimer) return;
462
- this._opTimer = setTimeout(() => {
463
- this._opTimer = null;
464
- this._flushOps();
465
- }, this._opFlushInterval);
466
- }
467
-
468
- _flushOps() {
469
- var remove = this._opBuffer.remove;
470
- var update = this._opBuffer.update;
471
- for (var table in remove) {
472
- var ids = remove[table];
473
- if (ids.size === 0) continue;
474
- var idsArr = Array.from(ids);
475
- var r = safeParse(native.jsqlRemoveByIds(table, JSON.stringify(idsArr)));
476
- this._emit('delete', { table, ids: idsArr, result: r });
477
- }
478
- for (var table in update) {
479
- var entries = update[table];
480
- if (entries.length === 0) continue;
481
- var r = safeParse(native.jsqlUpdateByIds(table, JSON.stringify(entries)));
482
- this._emit('update', { table, entries, result: r });
483
- }
484
- this._opBuffer = { remove: {}, update: {} };
485
- }
486
-
487
- async _flush() {
488
- this._flushOpsNow();
489
- if (!this._runHooks('beforeFlush', [])) return null;
490
- const flushed = {};
491
- for (const [table, rows] of Object.entries(this._buffer)) {
492
- if (rows.length === 0) continue;
493
- const r = await this._insertBatch(table, rows);
494
- flushed[table] = r;
495
- }
496
- this._buffer = {};
497
- this._bufferSize = 0;
498
- this._runHooks('afterFlush', []);
499
- return flushed;
500
- }
501
-
502
- _flushOpsNow() {
503
- if (this._opTimer) {
504
- clearTimeout(this._opTimer);
505
- this._opTimer = null;
506
- }
507
- this._flushOps();
508
- }
509
-
510
- async flush() {
511
- await this._flush();
512
- this._flushOpsNow();
513
- if (this._mode !== 'memory') {
514
- try { native.jsqlFlushDirty(); } catch (e) { /* ignore */ }
515
- }
516
- }
517
-
518
- async insert(table, data) {
519
- const arr = Array.isArray(data) ? data : [data];
520
- var filtered = arr;
521
- if (!this._runHooks('beforeInsert', [table, filtered])) return [];
522
- if (arr.length > 1) {
523
- await this._flush();
524
- let result;
525
- for (let i = 0; i < arr.length; i += this._flushThreshold) {
526
- const chunk = arr.slice(i, i + this._flushThreshold);
527
- const r = await this._insertBatch(table, chunk);
528
- if (r && r.error) throw new Error(r.error);
529
- if (!result) result = r;
530
- }
531
- this._emit('insert', { table, count: arr.length, ids: result });
532
- this._runHooks('afterInsert', [table, filtered, result]);
533
- return result;
534
- }
535
- if (!this._buffer[table]) this._buffer[table] = [];
536
- this._buffer[table].push(arr[0]);
537
- this._bufferSize++;
538
- const flushed = await this._flush();
539
- return flushed && flushed[table] ? flushed[table] : null;
540
- }
541
-
542
- async createTable(name, schema) {
543
- await this._flush();
544
- if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
545
- const r = safeParse(native.jsqlCreateTable(name, JSON.stringify(mapNativeSchema(schema))));
546
- if (r && r.ok === false) throw new Error(r.error || 'create table failed');
547
- this._tableNames.add(name);
548
- this._schemas[name] = typeof schema === 'string' ? mapNativeSchema(schema) : schema;
549
- this._emit('createTable', { name, schema });
550
- this._runHooks('afterCreateTable', [name, schema]);
551
- return r;
552
- }
553
-
554
- async dropTable(name) {
555
- await this._flush();
556
- if (!this._runHooks('beforeDropTable', [name])) return null;
557
- const r = safeParse(native.jsqlDropTable(name));
558
- if (r && r.ok === false) throw new Error(r.error || 'drop table failed');
559
- this._tableNames.delete(name);
560
- delete this._schemas[name];
561
- this._emit('dropTable', { name });
562
- this._runHooks('afterDropTable', [name]);
563
- return r;
564
- }
565
-
566
- findById(table, id) {
567
- this._flushOpsNow();
568
- if (!this._runHooks('beforeFind', [table, { id }])) return null;
569
- const raw = this._findRawById(table, id);
570
- if (raw && raw.error) throw new Error(raw.error);
571
- this._runHooks('afterFind', [table, { id }, raw]);
572
- const schema = this._schemas[table];
573
- if (schema && raw && typeof raw === 'object') return restoreRow(raw, schema);
574
- return raw;
575
- }
576
-
577
- findByIds(table, ids) {
578
- this._flushOpsNow();
579
- if (!this._runHooks('beforeFind', [table, { ids }])) return null;
580
- let r;
581
- if (this._pkOf(table)) {
582
- r = ids.map(id => this._findRawById(table, id)).filter(x => x !== null && !(x && x.error));
583
- } else {
584
- r = safeParse(native.jsqlFindByIds(table, JSON.stringify(ids)));
585
- }
586
- if (r && r.error) throw new Error(r.error);
587
- this._runHooks('afterFind', [table, { ids }, r]);
588
- const schema = this._schemas[table];
589
- if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
590
- return r;
591
- }
592
-
593
- findByIdsRaw(table, ids) {
594
- return this.findByIds(table, ids);
595
- }
596
-
597
- async find(table, filter, opts = {}) {
598
- this._flushOpsNow();
599
- if (!this._runHooks('beforeFind', [table, { filter, opts }])) return [];
600
- const filterStr = filter ? JSON.stringify(filter) : '';
601
- const { limit = 100, offset = 0 } = opts;
602
- const r = safeParse(native.jsqlFind(table, filterStr, limit, offset));
603
- if (r && r.error) throw new Error(r.error);
604
- this._runHooks('afterFind', [table, { filter, opts }, r]);
605
- const schema = this._schemas[table];
606
- if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
607
- return r;
608
- }
609
-
610
- async count(table) {
611
- this._flushOpsNow();
612
- this._runHooks('beforeCount', [table]);
613
- const r = parseInt(native.jsqlCount(table), 10);
614
- const n = isNaN(r) ? 0 : r;
615
- this._runHooks('afterCount', [table, n]);
616
- return n;
617
- }
618
-
619
- updateByIds(table, entries) {
620
- const pairs = [];
621
- for (const [id, data] of entries) {
622
- const seq = this._seqIdByPk(table, id);
623
- if (seq !== null) pairs.push([seq, data]);
624
- }
625
- if (!this._runHooks('beforeUpdate', [table, pairs])) return;
626
- if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
627
- this._opBuffer.update[table].push(...pairs);
628
- this._scheduleOpFlush();
629
- }
630
-
631
- removeByIds(table, ids) {
632
- if (!this._runHooks('beforeDelete', [table, ids])) return;
633
- if (!this._opBuffer.remove[table]) this._opBuffer.remove[table] = new Set();
634
- for (const id of ids) {
635
- const seq = this._seqIdByPk(table, id);
636
- if (seq !== null) this._opBuffer.remove[table].add(seq);
637
- }
638
- this._scheduleOpFlush();
639
- }
640
-
641
- updateById(table, id, data) {
642
- if (!this._runHooks('beforeUpdate', [table, id, data])) return;
643
- const seq = this._seqIdByPk(table, id);
644
- if (seq === null) return;
645
- if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
646
- this._opBuffer.update[table].push([seq, data]);
647
- this._scheduleOpFlush();
648
- }
649
-
650
- removeById(table, id) {
651
- if (!this._runHooks('beforeDelete', [table, id])) return;
652
- const seq = this._seqIdByPk(table, id);
653
- if (seq === null) return;
654
- if (!this._opBuffer.remove[table]) this._opBuffer.remove[table] = new Set();
655
- this._opBuffer.remove[table].add(seq);
656
- this._scheduleOpFlush();
657
- }
658
-
659
- hasTable(name) {
660
- return this._tableNames.has(name);
661
- }
662
-
663
- getTables() {
664
- return Array.from(this._tableNames);
665
- }
666
-
667
- getTableSchema(name) {
668
- return this._schemas[name] || null;
669
- }
670
-
671
- /**
672
- * 直接执行原生 SQL 语句。本实例自身即充当 engine,
673
- * 支持 SELECT / INSERT / UPDATE / DELETE / CREATE TABLE 等常用语句。
674
- * @example
675
- * const rows = await db.query("SELECT * FROM users WHERE age > ?", [21]);
676
- * await db.query("UPDATE users SET age = age + 1 WHERE name = 'A'");
677
- */
678
- async query(sql, paramsOrOpts, opts) {
679
- return executeSQL(this, sql, paramsOrOpts, opts);
680
- }
681
-
682
- async stop() {
683
- this._flushOpsNow();
684
- await this._flush();
685
- if (this._diskFlushTimer) { clearInterval(this._diskFlushTimer); this._diskFlushTimer = null; }
686
- if (this._evictTimer) { clearInterval(this._evictTimer); this._evictTimer = null; }
687
- if (this._mode !== 'memory') {
688
- try { JSON.parse(native.jsqlClose()); } catch (e) { /* ignore */ }
689
- }
690
- this._runHooks('onStop', []);
691
- this._emit('stop', {});
692
- }
693
-
694
- async beginTx() {
695
- this._flushOpsNow();
696
- const r = safeParse(native.jsqlBeginTx());
697
- if (r && r.ok === false) throw new Error(r.error || 'begin transaction failed');
698
- this._txId = r.txId;
699
- return r.txId;
700
- }
701
-
702
- async beginTransaction() {
703
- return this.beginTx();
704
- }
705
-
706
- async commit() {
707
- if (this._txId === undefined) return true;
708
- const r = await this.commitTx(this._txId);
709
- this._txId = undefined;
710
- return r;
711
- }
712
-
713
- async rollback() {
714
- if (this._txId === undefined) return true;
715
- const r = await this.rollbackTx(this._txId);
716
- this._txId = undefined;
717
- return r;
718
- }
719
-
720
- async commitTx(txId) {
721
- const r = safeParse(native.jsqlCommitTx(String(txId)));
722
- if (r && r.ok === false) throw new Error(r.error || 'commit transaction failed');
723
- return true;
724
- }
725
-
726
- async rollbackTx(txId) {
727
- const r = safeParse(native.jsqlRollbackTx(String(txId)));
728
- if (r && r.ok === false) throw new Error(r.error || 'rollback transaction failed');
729
- return true;
730
- }
731
- }
732
-
1
+ const path = require('path');
2
+ const os = require('os');
3
+ const { executeSQL } = require('./sql');
4
+ const native = require(path.join(__dirname, '..', 'native', 'jsql-neo-native.node'));
5
+ const INT64_TAG = 1;
6
+ const FLOAT_TAG = 2;
7
+ const STR_TAG = 3;
8
+ const BOOL_TAG = 4;
9
+ const INT32_TAG = 5;
10
+
11
+ function safeParse(str, fallback) {
12
+ if (typeof str !== 'string') return fallback;
13
+ try { return JSON.parse(str); } catch (e) { return fallback; }
14
+ }
15
+
16
+ const NATIVE_TYPE_MAP = {
17
+ text: 'string',
18
+ varchar: 'string',
19
+ double: 'float',
20
+ number: 'float',
21
+ numeric: 'float',
22
+ decimal: 'float',
23
+ date: 'string',
24
+ timestamp: 'string',
25
+ datetime: 'string',
26
+ json: 'string',
27
+ array: 'string',
28
+ object: 'string',
29
+ bool: 'boolean',
30
+ bigint: 'string',
31
+ int: 'integer',
32
+ };
33
+
34
+ function parseShorthandSchema(str) {
35
+ const def = { type: str };
36
+ const strip = (re) => {
37
+ const next = def.type.replace(re, '').trim();
38
+ def.type = next || def.type;
39
+ };
40
+ if (/\bprimary\s+key\b/i.test(def.type)) { def.primaryKey = true; strip(/\bprimary\s+key\b/gi); }
41
+ if (/\bauto_?increment\b/i.test(def.type)) { def.autoIncrement = true; strip(/\bauto_?increment\b/gi); }
42
+ if (/\bnot\s+null\b/i.test(def.type)) { def.required = true; strip(/\bnot\s+null\b/gi); }
43
+ if (/\bunique\b/i.test(def.type)) { def.unique = true; strip(/\bunique\b/gi); }
44
+ if (/\bdefault\s+(\S+)/i.test(def.type)) {
45
+ const m = def.type.match(/\bdefault\s+(\S+)/i);
46
+ def.default = m[1].replace(/^['"]|['"]$/g, '');
47
+ strip(/\bdefault\s+\S+/gi);
48
+ }
49
+ return def;
50
+ }
51
+
52
+ function parseSchemaString(str) {
53
+ const out = {};
54
+ for (const seg of str.split(',')) {
55
+ const trimmed = seg.trim();
56
+ if (!trimmed) continue;
57
+ const parts = trimmed.split(/\s+/);
58
+ const name = parts.shift();
59
+ const def = parseShorthandSchema(parts.join(' ').toLowerCase().trim());
60
+ if (def.type.length === 0) def.type = 'string';
61
+ out[name] = def;
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function mapNativeSchema(schema) {
67
+ if (typeof schema === 'string') schema = parseSchemaString(schema);
68
+ const mapped = {};
69
+ for (const [field, def] of Object.entries(schema || {})) {
70
+ if (typeof def === 'string') {
71
+ const parsed = parseShorthandSchema(def.toLowerCase().trim());
72
+ if (parsed.type.length === 0) parsed.type = 'string';
73
+ mapped[field] = { ...parsed, type: NATIVE_TYPE_MAP[parsed.type] || parsed.type };
74
+ } else if (def && typeof def === 'object') {
75
+ const t = (def.type || 'string').toLowerCase();
76
+ const out = { ...def, type: NATIVE_TYPE_MAP[t] || t };
77
+ if (out.primary_key !== undefined && out.primaryKey === undefined) { out.primaryKey = out.primary_key; delete out.primary_key; }
78
+ if (out.auto_increment !== undefined && out.autoIncrement === undefined) { out.autoIncrement = out.auto_increment; delete out.auto_increment; }
79
+ mapped[field] = out;
80
+ } else {
81
+ mapped[field] = { type: 'string' };
82
+ }
83
+ }
84
+ return mapped;
85
+ }
86
+
87
+ function restoreRow(row, schema) {
88
+ if (typeof row !== 'object' || row === null) return row;
89
+ let src = row;
90
+ if (row.fields && typeof row.fields === 'object' && row.fields !== null) {
91
+ src = { ...row.fields };
92
+ if (row.id !== undefined) {
93
+ const pkCols = [];
94
+ if (schema) {
95
+ for (const [k, def] of Object.entries(schema)) {
96
+ if (def && typeof def === 'object' && def.autoIncrement) continue;
97
+ const isPk = typeof def === 'string'
98
+ ? /\bprimary\s+key\b/i.test(def)
99
+ : !!(def && (def.primaryKey || def.primary_key));
100
+ if (isPk) pkCols.push(k);
101
+ }
102
+ } else if (src.id === undefined) {
103
+ pkCols.push('id');
104
+ }
105
+ for (const c of pkCols) {
106
+ const v = src[c];
107
+ if (v === undefined || v === null || v === 0 || v === '' || v === false) src[c] = row.id;
108
+ }
109
+ }
110
+ if (row.created_at !== undefined) src.created_at = row.created_at;
111
+ if (row.updated_at !== undefined) src.updated_at = row.updated_at;
112
+ }
113
+ if (!schema) return src;
114
+ const out = {};
115
+ for (const [field, value] of Object.entries(src)) {
116
+ const colDef = schema[field];
117
+ let t = null;
118
+ if (typeof colDef === 'string') t = colDef.toLowerCase();
119
+ else if (colDef && typeof colDef === 'object' && colDef.type) t = String(colDef.type).toLowerCase();
120
+ if (['json', 'array', 'object'].includes(t) && typeof value === 'string') {
121
+ try { out[field] = JSON.parse(value); } catch (e) { out[field] = value; }
122
+ } else if ((t === 'integer' || t === 'int' || t === 'bigint') && typeof value === 'string' && /^-?\d+$/.test(value)) {
123
+ out[field] = Number(value);
124
+ } else if ((t === 'float' || t === 'double' || t === 'number') && typeof value === 'string' && !Number.isNaN(Number(value))) {
125
+ out[field] = Number(value);
126
+ } else {
127
+ out[field] = value;
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+
133
+ function _encodedFieldSize(v) {
134
+ if (v === null || v === undefined) return 1;
135
+ if (typeof v === 'number') {
136
+ if (Number.isInteger(v)) return (v >= -2147483648 && v <= 2147483647) ? 1 + 4 : 1 + 8;
137
+ return 1 + 8;
138
+ }
139
+ if (typeof v === 'boolean') return 1 + 1;
140
+ const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
141
+ return 1 + 4 + Buffer.byteLength(s, 'utf8');
142
+ }
143
+
144
+ function encodeField(buf, off, v) {
145
+ if (v === null || v === undefined) {
146
+ buf.writeUInt8(0, off); return off + 1;
147
+ }
148
+ if (typeof v === 'number') {
149
+ if (Number.isInteger(v)) {
150
+ if (v >= -2147483648 && v <= 2147483647) {
151
+ buf.writeUInt8(INT32_TAG, off); buf.writeInt32LE(v, off + 1); return off + 5;
152
+ }
153
+ buf.writeUInt8(INT64_TAG, off); buf.writeBigInt64LE(BigInt(v), off + 1); return off + 9;
154
+ }
155
+ buf.writeUInt8(FLOAT_TAG, off); buf.writeDoubleLE(v, off + 1); return off + 9;
156
+ }
157
+ if (typeof v === 'string') {
158
+ const sl = Buffer.byteLength(v, 'utf8');
159
+ buf.writeUInt8(STR_TAG, off);
160
+ buf.writeUInt32LE(sl, off + 1);
161
+ buf.write(v, off + 5, sl, 'utf8');
162
+ return off + 5 + sl;
163
+ }
164
+ if (typeof v === 'boolean') {
165
+ buf.writeUInt8(BOOL_TAG, off); buf.writeUInt8(v ? 1 : 0, off + 1); return off + 2;
166
+ }
167
+ const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
168
+ const sl = Buffer.byteLength(s, 'utf8');
169
+ buf.writeUInt8(STR_TAG, off);
170
+ buf.writeUInt32LE(sl, off + 1);
171
+ buf.write(s, off + 5, sl, 'utf8');
172
+ return off + 5 + sl;
173
+ }
174
+
175
+ function encodeBatch(rows) {
176
+ if (rows.length === 0) return new Uint8Array(0);
177
+ const fieldNames = Object.keys(rows[0]);
178
+ const nFields = fieldNames.length;
179
+ let size = 1;
180
+ for (const s of fieldNames) size += 1 + Buffer.byteLength(s, 'utf8');
181
+ size += 4;
182
+ for (const row of rows) {
183
+ for (let fi = 0; fi < nFields; fi++) size += _encodedFieldSize(row[fieldNames[fi]]);
184
+ }
185
+ const buf = Buffer.allocUnsafe(size);
186
+ let off = 0;
187
+
188
+ off = buf.writeUInt8(nFields, off);
189
+ for (let fi = 0; fi < nFields; fi++) {
190
+ const s = fieldNames[fi];
191
+ const nlen = Buffer.byteLength(s, 'utf8');
192
+ off = buf.writeUInt8(nlen, off);
193
+ off += buf.write(s, off, nlen, 'utf8');
194
+ }
195
+ off = buf.writeUInt32LE(rows.length, off);
196
+
197
+ for (let ri = 0; ri < rows.length; ri++) {
198
+ const row = rows[ri];
199
+ for (let fi = 0; fi < nFields; fi++) {
200
+ off = encodeField(buf, off, row[fieldNames[fi]]);
201
+ }
202
+ }
203
+
204
+ return buf.slice(0, off);
205
+ }
206
+
207
+ class JSQL {
208
+ constructor(opts = {}) {
209
+ this._flushThreshold = opts.flushThreshold || 5000;
210
+ this._buffer = {};
211
+ this._bufferSize = 0;
212
+ this._opBuffer = { remove: {}, update: {} };
213
+ this._opTimer = null;
214
+ this._opFlushInterval = opts.opFlushInterval || 50;
215
+ this._tableNames = new Set();
216
+ this._schemas = {};
217
+ this._txId = undefined;
218
+ this._autoPlugins = opts.modules !== false;
219
+
220
+ // 持久化模式: 'memory' | 'hybrid' | 'disk'
221
+ this._path = opts.path || null;
222
+ this._mode = opts.mode || (this._path ? 'hybrid' : 'memory');
223
+ this._memReserveMB = opts.memReserveMB !== undefined ? opts.memReserveMB : 512;
224
+ this._flushInterval = opts.flushInterval !== undefined
225
+ ? opts.flushInterval
226
+ : (this._mode === 'disk' ? 50 : 200);
227
+ this._evictInterval = opts.evictInterval !== undefined ? opts.evictInterval : 1000;
228
+ this._diskFlushTimer = null;
229
+ this._evictTimer = null;
230
+
231
+ this._plugins = [];
232
+ this._hooks = {
233
+ beforeInsert: [], afterInsert: [],
234
+ beforeUpdate: [], afterUpdate: [],
235
+ beforeDelete: [], afterDelete: [],
236
+ beforeFind: [], afterFind: [],
237
+ beforeCreateTable: [], afterCreateTable: [],
238
+ beforeDropTable: [], afterDropTable: [],
239
+ beforeFlush: [], afterFlush: [],
240
+ beforeCount: [], afterCount: [],
241
+ onStart: [], onStop: []
242
+ };
243
+ this._eventListeners = [];
244
+ if (this._autoPlugins) {
245
+ const { ModuleManager } = require('./mod');
246
+ new ModuleManager().applyTo(this);
247
+ }
248
+ }
249
+
250
+ use(plugin) {
251
+ if (typeof plugin === 'function') {
252
+ plugin = { install: plugin };
253
+ }
254
+ if (plugin.hooks) {
255
+ for (const [event, fn] of Object.entries(plugin.hooks)) {
256
+ if (this._hooks[event]) {
257
+ this._hooks[event].push(fn);
258
+ }
259
+ }
260
+ }
261
+ if (typeof plugin.install === 'function') {
262
+ plugin.install(this, this._buildCtx(plugin));
263
+ }
264
+ if (typeof plugin.onEvent === 'function') {
265
+ this._eventListeners.push(plugin.onEvent);
266
+ }
267
+ this._plugins.push(plugin);
268
+ return this;
269
+ }
270
+
271
+ _buildCtx(plugin) {
272
+ const self = this;
273
+ return {
274
+ name: plugin.name || 'anonymous',
275
+ engine: this,
276
+ plugin,
277
+ on(hook, fn) { return self.on(hook, fn); },
278
+ onEvent(fn) { return self.onEvent(fn); },
279
+ emit(eventName, data) { self._emit(eventName, data); },
280
+ tables() { return Array.from(self._tableNames); },
281
+ hasTable(name) { return self._tableNames.has(name); },
282
+ getTableSchema(name) { return self._schemas[name] || null; }
283
+ };
284
+ }
285
+
286
+ on(event, fn) {
287
+ if (this._hooks[event]) {
288
+ this._hooks[event].push(fn);
289
+ }
290
+ return this;
291
+ }
292
+
293
+ onEvent(fn) {
294
+ this._eventListeners.push(fn);
295
+ return this;
296
+ }
297
+
298
+ _emit(eventName, data) {
299
+ for (const fn of this._eventListeners) {
300
+ try { fn(eventName, data); } catch (e) { /* ignore */ }
301
+ }
302
+ }
303
+
304
+ _runHooks(hookName, args) {
305
+ const hooks = this._hooks[hookName];
306
+ if (!hooks || hooks.length === 0) return true;
307
+ if (this._inHook) {
308
+ // 防止插件 hook 内重入 native 调用导致 N-API 崩溃(段错误)
309
+ throw new Error('ER_PLUGIN_REENTRY: plugin hook "' + hookName + '" re-entered native call; plugin must not call engine methods inside its own hook');
310
+ }
311
+ this._inHook = true;
312
+ try {
313
+ for (const fn of hooks) {
314
+ const r = fn(...args);
315
+ if (r === false) return false;
316
+ if (r !== undefined && args.length > 0) {
317
+ args[0] = r;
318
+ }
319
+ }
320
+ return true;
321
+ } finally {
322
+ this._inHook = false;
323
+ }
324
+ }
325
+
326
+ async start() {
327
+ if (this._mode !== 'memory') {
328
+ if (!this._path) throw new Error('hybrid/disk mode requires a directory path');
329
+ let r;
330
+ try {
331
+ r = safeParse(native.jsqlOpen(this._path, this._mode));
332
+ } catch (e) {
333
+ throw new Error('failed to open native storage: ' + e.message);
334
+ }
335
+ if (r && r.ok === false) throw new Error(r.error || 'open storage failed');
336
+ if (r && Array.isArray(r.tables)) {
337
+ this._tableNames = new Set(r.tables);
338
+ if (r.schemas) this._schemas = r.schemas;
339
+ }
340
+ } else {
341
+ try {
342
+ JSON.parse(native.jsqlOpen('', 'memory'));
343
+ } catch (e) {
344
+ throw new Error('failed to open native memory storage: ' + e.message);
345
+ }
346
+ }
347
+ if (this._mode !== 'memory') {
348
+ if (this._flushInterval > 0) {
349
+ this._diskFlushTimer = setInterval(() => {
350
+ try { native.jsqlFlushDirty(); } catch (e) { /* ignore */ }
351
+ }, this._flushInterval);
352
+ if (this._diskFlushTimer.unref) this._diskFlushTimer.unref();
353
+ }
354
+ this._evictTimer = setInterval(() => {
355
+ try { this._checkMemory(); } catch (e) { /* ignore */ }
356
+ }, this._evictInterval);
357
+ if (this._evictTimer.unref) this._evictTimer.unref();
358
+ }
359
+ this._runHooks('onStart', []);
360
+ this._emit('start', {});
361
+ }
362
+
363
+ _checkMemory() {
364
+ const total = os.totalmem();
365
+ if (!total) return;
366
+ const budget = total - this._memReserveMB * 1024 * 1024;
367
+ if (budget <= 0) return;
368
+ if (process.memoryUsage().rss <= budget) return;
369
+ for (let i = 0; i < 16; i++) {
370
+ let r;
371
+ try { r = JSON.parse(native.jsqlEvict()); } catch (e) { return; }
372
+ if (r.ok === false || !r.evicted || r.remaining === 0) return;
373
+ if (process.memoryUsage().rss <= budget) return;
374
+ }
375
+ }
376
+
377
+ _pkOf(table) {
378
+ const schema = this._schemas[table];
379
+ if (!schema) return null;
380
+ for (const [k, def] of Object.entries(schema)) {
381
+ const isPk = (typeof def === 'string' && /\bprimary\s+key\b/i.test(def))
382
+ || !!(def && typeof def === 'object' && (def.primaryKey || def.primary_key));
383
+ if (isPk) return k;
384
+ }
385
+ return null;
386
+ }
387
+
388
+ _pkAutoIncrement(table) {
389
+ const schema = this._schemas[table];
390
+ const pk = this._pkOf(table);
391
+ if (!pk || !schema) return false;
392
+ const def = schema[pk];
393
+ if (typeof def === 'string') return /\bauto_?increment\b/i.test(def);
394
+ return !!(def && def.autoIncrement);
395
+ }
396
+
397
+ _coercePkId(table, id) {
398
+ const schema = this._schemas[table];
399
+ const pk = this._pkOf(table);
400
+ if (!schema || !pk) return id;
401
+ const def = schema[pk];
402
+ let t = null;
403
+ if (typeof def === 'string') t = def.toLowerCase();
404
+ else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
405
+ if (['integer', 'int', 'bigint'].includes(t)) return Number(id);
406
+ return id;
407
+ }
408
+
409
+ _findRawById(table, id) {
410
+ const pk = this._pkOf(table);
411
+ if (pk) {
412
+ const filter = {};
413
+ filter[pk] = this._coercePkId(table, id);
414
+ const r = safeParse(native.jsqlFind(table, JSON.stringify(filter), 1, 0));
415
+ if (r && r.error) return r;
416
+ return Array.isArray(r) && r.length > 0 ? r[0] : null;
417
+ }
418
+ return safeParse(native.jsqlFindById(table, Number(id)));
419
+ }
420
+
421
+ _seqIdByPk(table, id) {
422
+ const pk = this._pkOf(table);
423
+ if (pk) {
424
+ const filter = {};
425
+ filter[pk] = this._coercePkId(table, id);
426
+ const r = safeParse(native.jsqlFind(table, JSON.stringify(filter), 1, 0));
427
+ if (Array.isArray(r) && r.length > 0) return r[0].id;
428
+ return null;
429
+ }
430
+ return Number(id);
431
+ }
432
+
433
+ async _insertBatch(table, rows) {
434
+ const schema = this._schemas[table];
435
+ if (schema) {
436
+ rows = rows.map(row => {
437
+ const out = {};
438
+ for (const [k, v] of Object.entries(row)) {
439
+ if (v instanceof Date) out[k] = v.toISOString();
440
+ else out[k] = v;
441
+ }
442
+ return out;
443
+ });
444
+ }
445
+ const bin = encodeBatch(rows);
446
+ const r = safeParse(native.jsqlInsertBuf(table, bin));
447
+ if (r && r.error) throw new Error(r.error);
448
+ if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
449
+ const pk = this._pkOf(table);
450
+ if (pk) {
451
+ return r.map((seq, i) => {
452
+ const row = rows[i];
453
+ return (row && row[pk] !== undefined && row[pk] !== null) ? row[pk] : seq;
454
+ });
455
+ }
456
+ }
457
+ return r;
458
+ }
459
+
460
+ _scheduleOpFlush() {
461
+ if (this._opTimer) return;
462
+ this._opTimer = setTimeout(() => {
463
+ this._opTimer = null;
464
+ this._flushOps();
465
+ }, this._opFlushInterval);
466
+ }
467
+
468
+ _flushOps() {
469
+ var remove = this._opBuffer.remove;
470
+ var update = this._opBuffer.update;
471
+ for (var table in remove) {
472
+ var ids = remove[table];
473
+ if (ids.size === 0) continue;
474
+ var idsArr = Array.from(ids);
475
+ var r = safeParse(native.jsqlRemoveByIds(table, JSON.stringify(idsArr)));
476
+ this._emit('delete', { table, ids: idsArr, result: r });
477
+ }
478
+ for (var table in update) {
479
+ var entries = update[table];
480
+ if (entries.length === 0) continue;
481
+ var r = safeParse(native.jsqlUpdateByIds(table, JSON.stringify(entries)));
482
+ this._emit('update', { table, entries, result: r });
483
+ }
484
+ this._opBuffer = { remove: {}, update: {} };
485
+ }
486
+
487
+ async _flush() {
488
+ this._flushOpsNow();
489
+ if (!this._runHooks('beforeFlush', [])) return null;
490
+ const flushed = {};
491
+ for (const [table, rows] of Object.entries(this._buffer)) {
492
+ if (rows.length === 0) continue;
493
+ const r = await this._insertBatch(table, rows);
494
+ flushed[table] = r;
495
+ }
496
+ this._buffer = {};
497
+ this._bufferSize = 0;
498
+ this._runHooks('afterFlush', []);
499
+ return flushed;
500
+ }
501
+
502
+ _flushOpsNow() {
503
+ if (this._opTimer) {
504
+ clearTimeout(this._opTimer);
505
+ this._opTimer = null;
506
+ }
507
+ this._flushOps();
508
+ }
509
+
510
+ async flush() {
511
+ await this._flush();
512
+ this._flushOpsNow();
513
+ if (this._mode !== 'memory') {
514
+ try { native.jsqlFlushDirty(); } catch (e) { /* ignore */ }
515
+ }
516
+ }
517
+
518
+ async insert(table, data) {
519
+ const arr = Array.isArray(data) ? data : [data];
520
+ var filtered = arr;
521
+ if (!this._runHooks('beforeInsert', [table, filtered])) return [];
522
+ if (arr.length > 1) {
523
+ await this._flush();
524
+ let result;
525
+ for (let i = 0; i < arr.length; i += this._flushThreshold) {
526
+ const chunk = arr.slice(i, i + this._flushThreshold);
527
+ const r = await this._insertBatch(table, chunk);
528
+ if (r && r.error) throw new Error(r.error);
529
+ if (!result) result = r;
530
+ }
531
+ this._emit('insert', { table, count: arr.length, ids: result });
532
+ this._runHooks('afterInsert', [table, filtered, result]);
533
+ return result;
534
+ }
535
+ if (!this._buffer[table]) this._buffer[table] = [];
536
+ this._buffer[table].push(arr[0]);
537
+ this._bufferSize++;
538
+ const flushed = await this._flush();
539
+ return flushed && flushed[table] ? flushed[table] : null;
540
+ }
541
+
542
+ async createTable(name, schema) {
543
+ await this._flush();
544
+ if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
545
+ const r = safeParse(native.jsqlCreateTable(name, JSON.stringify(mapNativeSchema(schema))));
546
+ if (r && r.ok === false) throw new Error(r.error || 'create table failed');
547
+ this._tableNames.add(name);
548
+ this._schemas[name] = typeof schema === 'string' ? mapNativeSchema(schema) : schema;
549
+ this._emit('createTable', { name, schema });
550
+ this._runHooks('afterCreateTable', [name, schema]);
551
+ return r;
552
+ }
553
+
554
+ async dropTable(name) {
555
+ await this._flush();
556
+ if (!this._runHooks('beforeDropTable', [name])) return null;
557
+ const r = safeParse(native.jsqlDropTable(name));
558
+ if (r && r.ok === false) throw new Error(r.error || 'drop table failed');
559
+ this._tableNames.delete(name);
560
+ delete this._schemas[name];
561
+ this._emit('dropTable', { name });
562
+ this._runHooks('afterDropTable', [name]);
563
+ return r;
564
+ }
565
+
566
+ findById(table, id) {
567
+ this._flushOpsNow();
568
+ if (!this._runHooks('beforeFind', [table, { id }])) return null;
569
+ const raw = this._findRawById(table, id);
570
+ if (raw && raw.error) throw new Error(raw.error);
571
+ this._runHooks('afterFind', [table, { id }, raw]);
572
+ const schema = this._schemas[table];
573
+ if (schema && raw && typeof raw === 'object') return restoreRow(raw, schema);
574
+ return raw;
575
+ }
576
+
577
+ findByIds(table, ids) {
578
+ this._flushOpsNow();
579
+ if (!this._runHooks('beforeFind', [table, { ids }])) return null;
580
+ let r;
581
+ if (this._pkOf(table)) {
582
+ r = ids.map(id => this._findRawById(table, id)).filter(x => x !== null && !(x && x.error));
583
+ } else {
584
+ r = safeParse(native.jsqlFindByIds(table, JSON.stringify(ids)));
585
+ }
586
+ if (r && r.error) throw new Error(r.error);
587
+ this._runHooks('afterFind', [table, { ids }, r]);
588
+ const schema = this._schemas[table];
589
+ if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
590
+ return r;
591
+ }
592
+
593
+ findByIdsRaw(table, ids) {
594
+ return this.findByIds(table, ids);
595
+ }
596
+
597
+ async find(table, filter, opts = {}) {
598
+ this._flushOpsNow();
599
+ if (!this._runHooks('beforeFind', [table, { filter, opts }])) return [];
600
+ const filterStr = filter ? JSON.stringify(filter) : '';
601
+ const { limit = 100, offset = 0 } = opts;
602
+ const r = safeParse(native.jsqlFind(table, filterStr, limit, offset));
603
+ if (r && r.error) throw new Error(r.error);
604
+ this._runHooks('afterFind', [table, { filter, opts }, r]);
605
+ const schema = this._schemas[table];
606
+ if (schema && Array.isArray(r)) return r.map(row => restoreRow(row, schema));
607
+ return r;
608
+ }
609
+
610
+ async count(table) {
611
+ this._flushOpsNow();
612
+ this._runHooks('beforeCount', [table]);
613
+ const r = parseInt(native.jsqlCount(table), 10);
614
+ const n = isNaN(r) ? 0 : r;
615
+ this._runHooks('afterCount', [table, n]);
616
+ return n;
617
+ }
618
+
619
+ updateByIds(table, entries) {
620
+ const pairs = [];
621
+ for (const [id, data] of entries) {
622
+ const seq = this._seqIdByPk(table, id);
623
+ if (seq !== null) pairs.push([seq, data]);
624
+ }
625
+ if (!this._runHooks('beforeUpdate', [table, pairs])) return;
626
+ if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
627
+ this._opBuffer.update[table].push(...pairs);
628
+ this._scheduleOpFlush();
629
+ }
630
+
631
+ removeByIds(table, ids) {
632
+ if (!this._runHooks('beforeDelete', [table, ids])) return;
633
+ if (!this._opBuffer.remove[table]) this._opBuffer.remove[table] = new Set();
634
+ for (const id of ids) {
635
+ const seq = this._seqIdByPk(table, id);
636
+ if (seq !== null) this._opBuffer.remove[table].add(seq);
637
+ }
638
+ this._scheduleOpFlush();
639
+ }
640
+
641
+ updateById(table, id, data) {
642
+ if (!this._runHooks('beforeUpdate', [table, id, data])) return;
643
+ const seq = this._seqIdByPk(table, id);
644
+ if (seq === null) return;
645
+ if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
646
+ this._opBuffer.update[table].push([seq, data]);
647
+ this._scheduleOpFlush();
648
+ }
649
+
650
+ removeById(table, id) {
651
+ if (!this._runHooks('beforeDelete', [table, id])) return;
652
+ const seq = this._seqIdByPk(table, id);
653
+ if (seq === null) return;
654
+ if (!this._opBuffer.remove[table]) this._opBuffer.remove[table] = new Set();
655
+ this._opBuffer.remove[table].add(seq);
656
+ this._scheduleOpFlush();
657
+ }
658
+
659
+ hasTable(name) {
660
+ return this._tableNames.has(name);
661
+ }
662
+
663
+ getTables() {
664
+ return Array.from(this._tableNames);
665
+ }
666
+
667
+ getTableSchema(name) {
668
+ return this._schemas[name] || null;
669
+ }
670
+
671
+ /**
672
+ * 直接执行原生 SQL 语句。本实例自身即充当 engine,
673
+ * 支持 SELECT / INSERT / UPDATE / DELETE / CREATE TABLE 等常用语句。
674
+ * @example
675
+ * const rows = await db.query("SELECT * FROM users WHERE age > ?", [21]);
676
+ * await db.query("UPDATE users SET age = age + 1 WHERE name = 'A'");
677
+ */
678
+ async query(sql, paramsOrOpts, opts) {
679
+ return executeSQL(this, sql, paramsOrOpts, opts);
680
+ }
681
+
682
+ async stop() {
683
+ this._flushOpsNow();
684
+ await this._flush();
685
+ if (this._diskFlushTimer) { clearInterval(this._diskFlushTimer); this._diskFlushTimer = null; }
686
+ if (this._evictTimer) { clearInterval(this._evictTimer); this._evictTimer = null; }
687
+ if (this._mode !== 'memory') {
688
+ try { JSON.parse(native.jsqlClose()); } catch (e) { /* ignore */ }
689
+ }
690
+ this._runHooks('onStop', []);
691
+ this._emit('stop', {});
692
+ }
693
+
694
+ async beginTx() {
695
+ this._flushOpsNow();
696
+ const r = safeParse(native.jsqlBeginTx());
697
+ if (r && r.ok === false) throw new Error(r.error || 'begin transaction failed');
698
+ this._txId = r.txId;
699
+ return r.txId;
700
+ }
701
+
702
+ async beginTransaction() {
703
+ return this.beginTx();
704
+ }
705
+
706
+ async commit() {
707
+ if (this._txId === undefined) return true;
708
+ const r = await this.commitTx(this._txId);
709
+ this._txId = undefined;
710
+ return r;
711
+ }
712
+
713
+ async rollback() {
714
+ if (this._txId === undefined) return true;
715
+ const r = await this.rollbackTx(this._txId);
716
+ this._txId = undefined;
717
+ return r;
718
+ }
719
+
720
+ async commitTx(txId) {
721
+ const r = safeParse(native.jsqlCommitTx(String(txId)));
722
+ if (r && r.ok === false) throw new Error(r.error || 'commit transaction failed');
723
+ return true;
724
+ }
725
+
726
+ async rollbackTx(txId) {
727
+ const r = safeParse(native.jsqlRollbackTx(String(txId)));
728
+ if (r && r.ok === false) throw new Error(r.error || 'rollback transaction failed');
729
+ return true;
730
+ }
731
+ }
732
+
733
733
  module.exports = { JSQL };