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/table.js CHANGED
@@ -187,9 +187,12 @@ class Table {
187
187
  const pkVal = data[this._primaryKey];
188
188
  if (pkVal !== undefined && pkVal !== null) this._pkIndex.set(pkVal, rowIdx);
189
189
  }
190
+ for (const [field, tree] of Object.entries(this._btrees)) {
191
+ const val = data[field];
192
+ if (val !== undefined && val !== null) tree.insert(val, rowIdx);
193
+ }
190
194
  items[i] = data;
191
195
  }
192
- this._btreesDirty = true;
193
196
  this._dirty = true;
194
197
  this._db._markDirty();
195
198
  for (const hook of this._hooks.afterInsert) {
@@ -353,26 +356,37 @@ class Table {
353
356
 
354
357
  const toRemove = this._applyFilterOptimized(this._getVisibleRows(), query);
355
358
  let count = 0;
359
+ const removeIndices = [];
356
360
 
357
361
  for (const row of toRemove) {
358
362
  const index = this._rows.indexOf(row);
359
363
  if (index === -1) continue;
360
-
361
- // 外键 CASCADE / SET NULL:查找所有引用此表的子表
362
364
  this._cascadeForeignKeys(row);
363
-
364
- if (this._softDelete) {
365
- this._rows[index]._deleted = true;
366
- this._rows[index]._deletedAt = Date.now();
367
- } else {
368
- this._removeFromIndexes(index, row);
369
- this._rows.splice(index, 1);
370
- this._adjustBTreeAfterRemove(index);
371
- }
365
+ this._removeFromIndexes(index, row);
366
+ removeIndices.push(index);
372
367
  count++;
373
368
  }
374
369
 
375
370
  if (count > 0) {
371
+ removeIndices.sort((a, b) => b - a);
372
+ for (const idx of removeIndices) {
373
+ const lastIdx = this._rows.length - 1;
374
+ if (idx < lastIdx) {
375
+ const lastRow = this._rows[lastIdx];
376
+ this._rows[idx] = lastRow;
377
+ if (this._pkIndex && this._primaryKey) {
378
+ this._pkIndex.set(lastRow[this._primaryKey], idx);
379
+ }
380
+ for (const [field, tree] of Object.entries(this._btrees)) {
381
+ const val = lastRow[field];
382
+ if (val !== undefined && val !== null) {
383
+ tree.remove(val, lastIdx);
384
+ tree.insert(val, idx);
385
+ }
386
+ }
387
+ }
388
+ this._rows.pop();
389
+ }
376
390
  this._dirty = true;
377
391
  this._db._markDirty();
378
392
  for (const hook of this._hooks.afterRemove) {
@@ -402,14 +416,23 @@ class Table {
402
416
  const row = this._rows[rowIndex];
403
417
  if (!row || row._deleted) return 0;
404
418
  this._cascadeForeignKeys(row);
405
- if (this._softDelete) {
406
- row._deleted = true;
407
- row._deletedAt = Date.now();
408
- } else {
409
- this._removeFromIndexes(rowIndex, row);
410
- this._rows.splice(rowIndex, 1);
411
- this._adjustBTreeAfterRemove(rowIndex);
419
+ this._removeFromIndexes(rowIndex, row);
420
+ const lastIdx = this._rows.length - 1;
421
+ if (rowIndex < lastIdx) {
422
+ const lastRow = this._rows[lastIdx];
423
+ this._rows[rowIndex] = lastRow;
424
+ if (this._pkIndex && this._primaryKey) {
425
+ this._pkIndex.set(lastRow[this._primaryKey], rowIndex);
426
+ }
427
+ for (const [field, tree] of Object.entries(this._btrees)) {
428
+ const val = lastRow[field];
429
+ if (val !== undefined && val !== null) {
430
+ tree.remove(val, lastIdx);
431
+ tree.insert(val, rowIndex);
432
+ }
433
+ }
412
434
  }
435
+ this._rows.pop();
413
436
  this._dirty = true;
414
437
  this._db._markDirty();
415
438
  for (const hook of this._hooks.afterRemove) {
@@ -992,7 +1015,6 @@ class Table {
992
1015
  }
993
1016
 
994
1017
  _addToIndexes(rowIndex, data) {
995
- this._ensureBTrees();
996
1018
  if (this._pkIndex && this._primaryKey) {
997
1019
  const pkVal = data[this._primaryKey];
998
1020
  if (pkVal !== undefined && pkVal !== null) this._pkIndex.set(pkVal, rowIndex);
@@ -1015,7 +1037,6 @@ class Table {
1015
1037
  }
1016
1038
 
1017
1039
  _removeFromIndexes(rowIndex, data) {
1018
- this._ensureBTrees();
1019
1040
  if (this._pkIndex && this._primaryKey) {
1020
1041
  this._pkIndex.delete(data[this._primaryKey]);
1021
1042
  }
@@ -122,24 +122,119 @@ class JSQL {
122
122
  this._flushThreshold = opts.flushThreshold || 5000;
123
123
  this._buffer = {};
124
124
  this._bufferSize = 0;
125
+ this._tableNames = new Set();
126
+ this._schemas = {};
127
+ this._autoPlugins = opts.modules !== false;
128
+
129
+ this._plugins = [];
130
+ this._hooks = {
131
+ beforeInsert: [], afterInsert: [],
132
+ beforeUpdate: [], afterUpdate: [],
133
+ beforeDelete: [], afterDelete: [],
134
+ beforeFind: [], afterFind: [],
135
+ beforeCreateTable: [], afterCreateTable: [],
136
+ beforeDropTable: [], afterDropTable: [],
137
+ beforeFlush: [], afterFlush: [],
138
+ beforeCount: [], afterCount: [],
139
+ onStart: [], onStop: []
140
+ };
141
+ this._eventListeners = [];
142
+ if (this._autoPlugins) {
143
+ const { ModuleManager } = require('./mod');
144
+ new ModuleManager().applyTo(this);
145
+ }
125
146
  }
126
147
 
127
- async start() {}
148
+ use(plugin) {
149
+ if (typeof plugin === 'function') {
150
+ plugin = { install: plugin };
151
+ }
152
+ if (plugin.hooks) {
153
+ for (const [event, fn] of Object.entries(plugin.hooks)) {
154
+ if (this._hooks[event]) {
155
+ this._hooks[event].push(fn);
156
+ }
157
+ }
158
+ }
159
+ if (typeof plugin.install === 'function') {
160
+ plugin.install(this, this._buildCtx(plugin));
161
+ }
162
+ if (typeof plugin.onEvent === 'function') {
163
+ this._eventListeners.push(plugin.onEvent);
164
+ }
165
+ this._plugins.push(plugin);
166
+ return this;
167
+ }
168
+
169
+ _buildCtx(plugin) {
170
+ const self = this;
171
+ return {
172
+ name: plugin.name || 'anonymous',
173
+ engine: this,
174
+ plugin,
175
+ on(hook, fn) { return self.on(hook, fn); },
176
+ onEvent(fn) { return self.onEvent(fn); },
177
+ emit(eventName, data) { self._emit(eventName, data); },
178
+ tables() { return Array.from(self._tableNames); },
179
+ hasTable(name) { return self._tableNames.has(name); },
180
+ getTableSchema(name) { return self._schemas[name] || null; }
181
+ };
182
+ }
183
+
184
+ on(event, fn) {
185
+ if (this._hooks[event]) {
186
+ this._hooks[event].push(fn);
187
+ }
188
+ return this;
189
+ }
190
+
191
+ onEvent(fn) {
192
+ this._eventListeners.push(fn);
193
+ return this;
194
+ }
195
+
196
+ _emit(eventName, data) {
197
+ for (const fn of this._eventListeners) {
198
+ try { fn(eventName, data); } catch (e) { /* ignore */ }
199
+ }
200
+ }
201
+
202
+ _runHooks(hookName, args) {
203
+ const hooks = this._hooks[hookName];
204
+ if (!hooks || hooks.length === 0) return true;
205
+ for (const fn of hooks) {
206
+ const r = fn(...args);
207
+ if (r === false) return false;
208
+ if (r !== undefined && args.length > 0) {
209
+ args[0] = r;
210
+ }
211
+ }
212
+ return true;
213
+ }
214
+
215
+ async start() {
216
+ this._runHooks('onStart', []);
217
+ this._emit('start', {});
218
+ }
128
219
 
129
220
  async _insertBatch(table, rows) {
130
- const bin = encodeBatch(rows);
131
- const r = safeJsonParse(wasmBindings.jsql_insert_buf(table, bin));
221
+ const r = safeJsonParse(wasmBindings.jsql_insert_json(table, JSON.stringify(rows)));
132
222
  if (r && r.error) throw new Error(r.error);
133
223
  return r;
134
224
  }
135
225
 
136
226
  async _flush() {
227
+ if (!this._runHooks('beforeFlush', [])) return null;
228
+ const flushed = {};
137
229
  for (const [table, rows] of Object.entries(this._buffer)) {
138
230
  if (rows.length === 0) continue;
139
- await this._insertBatch(table, rows);
231
+ const r = await this._insertBatch(table, rows);
232
+ flushed[table] = r;
140
233
  }
141
234
  this._buffer = {};
142
235
  this._bufferSize = 0;
236
+ this._runHooks('afterFlush', []);
237
+ return flushed;
143
238
  }
144
239
 
145
240
  async flush() {
@@ -148,6 +243,7 @@ class JSQL {
148
243
 
149
244
  async insert(table, data) {
150
245
  const arr = Array.isArray(data) ? data : [data];
246
+ if (!this._runHooks('beforeInsert', [table, arr])) return [];
151
247
  if (arr.length > 1) {
152
248
  await this._flush();
153
249
  let result;
@@ -157,68 +253,140 @@ class JSQL {
157
253
  if (r && r.error) throw new Error(r.error);
158
254
  if (!result) result = r;
159
255
  }
256
+ this._emit('insert', { table, count: arr.length, ids: result });
257
+ this._runHooks('afterInsert', [table, arr, result]);
160
258
  return result;
161
259
  }
162
260
  if (!this._buffer[table]) this._buffer[table] = [];
163
261
  this._buffer[table].push(arr[0]);
164
262
  this._bufferSize++;
165
- if (this._bufferSize >= this._flushThreshold) {
166
- await this._flush();
167
- }
263
+ const flushed = await this._flush();
264
+ return flushed && flushed[table] ? flushed[table] : null;
168
265
  }
169
266
 
170
267
  async createTable(name, schema) {
171
268
  await this._flush();
269
+ if (!this._runHooks('beforeCreateTable', [name, schema])) return null;
172
270
  const r = safeJsonParse(wasmBindings.jsql_create_table(name, JSON.stringify(schema)));
173
271
  if (r && r.ok === false) throw new Error(r.error || 'create table failed');
272
+ this._tableNames.add(name);
273
+ this._schemas[name] = schema;
274
+ this._emit('createTable', { name, schema });
275
+ this._runHooks('afterCreateTable', [name, schema]);
174
276
  return r;
175
277
  }
176
278
 
177
279
  async dropTable(name) {
178
280
  await this._flush();
281
+ if (!this._runHooks('beforeDropTable', [name])) return null;
179
282
  const r = safeJsonParse(wasmBindings.jsql_drop_table(name));
180
283
  if (r && r.ok === false) throw new Error(r.error || 'drop table failed');
284
+ this._tableNames.delete(name);
285
+ delete this._schemas[name];
286
+ this._emit('dropTable', { name });
287
+ this._runHooks('afterDropTable', [name]);
181
288
  return r;
182
289
  }
183
290
 
184
291
  async findById(table, id) {
185
292
  await this._flush();
293
+ if (!this._runHooks('beforeFind', [table, { id }])) return null;
186
294
  const r = safeJsonParse(wasmBindings.jsql_find_by_id(table, BigInt(id)));
187
295
  if (r && r.error) throw new Error(r.error);
296
+ this._runHooks('afterFind', [table, { id }, r]);
188
297
  return r;
189
298
  }
190
299
 
191
300
  async find(table, filter, opts = {}) {
192
301
  await this._flush();
302
+ if (!this._runHooks('beforeFind', [table, { filter, opts }])) return [];
193
303
  const filterStr = filter ? JSON.stringify(filter) : '';
194
304
  const { limit = 100, offset = 0 } = opts;
195
305
  const r = safeJsonParse(wasmBindings.jsql_find(table, filterStr, limit, offset));
196
306
  if (r && r.error) throw new Error(r.error);
307
+ this._runHooks('afterFind', [table, { filter, opts }, r]);
197
308
  return r;
198
309
  }
199
310
 
200
311
  async count(table) {
201
312
  await this._flush();
313
+ this._runHooks('beforeCount', [table]);
202
314
  const r = parseInt(wasmBindings.jsql_count(table), 10);
203
- return isNaN(r) ? 0 : r;
315
+ const n = isNaN(r) ? 0 : r;
316
+ this._runHooks('afterCount', [table, n]);
317
+ return n;
318
+ }
319
+
320
+ async updateByIds(table, entries) {
321
+ const pairs = entries.map(([id, data]) => [Number(id), data]);
322
+ if (!this._runHooks('beforeUpdate', [table, pairs])) return;
323
+ await this._flush();
324
+ const r = safeJsonParse(wasmBindings.jsql_update_by_ids(table, JSON.stringify(pairs)));
325
+ if (r && r.error) throw new Error(r.error);
326
+ this._emit('update', { table, entries: pairs, result: r });
327
+ this._runHooks('afterUpdate', [table, pairs, r]);
328
+ return r;
329
+ }
330
+
331
+ async removeByIds(table, ids) {
332
+ if (!this._runHooks('beforeDelete', [table, ids])) return;
333
+ await this._flush();
334
+ const r = safeJsonParse(wasmBindings.jsql_remove_by_ids(table, JSON.stringify(ids)));
335
+ if (r && r.error) throw new Error(r.error);
336
+ this._emit('delete', { table, ids, result: r });
337
+ this._runHooks('afterDelete', [table, ids, r]);
338
+ return r;
339
+ }
340
+
341
+ async findByIds(table, ids) {
342
+ await this._flush();
343
+ if (!this._runHooks('beforeFind', [table, { ids }])) return null;
344
+ const r = safeJsonParse(wasmBindings.jsql_find_by_ids(table, JSON.stringify(ids)));
345
+ if (r && r.error) throw new Error(r.error);
346
+ this._runHooks('afterFind', [table, { ids }, r]);
347
+ return r;
348
+ }
349
+
350
+ findByIdsRaw(table, ids) {
351
+ return this.findByIds(table, ids);
204
352
  }
205
353
 
206
354
  async updateById(table, id, data) {
355
+ if (!this._runHooks('beforeUpdate', [table, id, data])) return;
207
356
  await this._flush();
208
357
  const r = safeJsonParse(wasmBindings.jsql_update_by_id(table, BigInt(id), JSON.stringify(data)));
209
358
  if (r && r.ok === false) throw new Error(r.error || 'update failed');
359
+ this._emit('update', { table, id, data });
360
+ this._runHooks('afterUpdate', [table, id, data, r]);
210
361
  return r;
211
362
  }
212
363
 
213
364
  async removeById(table, id) {
365
+ if (!this._runHooks('beforeDelete', [table, id])) return;
214
366
  await this._flush();
215
367
  const r = safeJsonParse(wasmBindings.jsql_remove_by_id(table, BigInt(id)));
216
368
  if (r && r.ok === false) throw new Error(r.error || 'remove failed');
369
+ this._emit('delete', { table, id });
370
+ this._runHooks('afterDelete', [table, id, r]);
217
371
  return r;
218
372
  }
219
373
 
374
+ async hasTable(name) {
375
+ return this._tableNames.has(name);
376
+ }
377
+
378
+ async getTables() {
379
+ return Array.from(this._tableNames);
380
+ }
381
+
382
+ async getTableSchema(name) {
383
+ return this._schemas[name] || null;
384
+ }
385
+
220
386
  async stop() {
221
387
  await this._flush();
388
+ this._runHooks('onStop', []);
389
+ this._emit('stop', {});
222
390
  }
223
391
  }
224
392
 
Binary file
@@ -0,0 +1,24 @@
1
+ [package]
2
+ name = "jsql-neo-core"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Embedded database engine — core library (supports WASM)"
6
+
7
+ [lib]
8
+ crate-type = ["lib", "cdylib"]
9
+
10
+ [dependencies]
11
+ serde = { version = "1", features = ["derive"] }
12
+ serde_json = { version = "1", features = ["preserve_order"] }
13
+ chrono = { version = "0.4", default-features = false, features = ["clock", "wasmbind"] }
14
+ crc32fast = "1"
15
+ itoa = "1"
16
+ ryu = "1"
17
+ rustc-hash = "2"
18
+ bincode = "1"
19
+ wasm-bindgen = { version = "0.2", optional = true }
20
+ js-sys = { version = "0.3", optional = true }
21
+
22
+ [features]
23
+ default = []
24
+ wasm = ["wasm-bindgen", "js-sys"]