jsql-neo 5.4.2 → 6.0.0-beta2

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 (39) hide show
  1. package/README.md +198 -14
  2. package/index.d.ts +29 -0
  3. package/index.js +12 -2
  4. package/lib/ast.js +214 -0
  5. package/lib/database.js +26 -60
  6. package/lib/mongo_server.js +1 -1
  7. package/lib/native_client.js +43 -66
  8. package/lib/plugin.js +227 -3
  9. package/lib/plugins/audit.js +37 -0
  10. package/lib/plugins/index.js +72 -0
  11. package/lib/plugins/logger.js +35 -0
  12. package/lib/plugins/metrics.js +38 -0
  13. package/lib/plugins/timestamps.js +43 -0
  14. package/lib/plugins/validation.js +128 -0
  15. package/lib/sql.js +1212 -84
  16. package/lib/wasm_client.js +41 -57
  17. package/native/jsql-neo-native.node +0 -0
  18. package/nativesrc/jsql-neo-core/Cargo.lock +8 -1
  19. package/nativesrc/jsql-neo-core/Cargo.toml +3 -1
  20. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +50 -13
  21. package/nativesrc/jsql-neo-core/src/engine/memory.rs +46 -23
  22. package/nativesrc/jsql-neo-core/src/engine/mod.rs +13 -1
  23. package/nativesrc/jsql-neo-core/src/engine/table.rs +338 -42
  24. package/nativesrc/jsql-neo-core/src/storage/wal.rs +245 -32
  25. package/nativesrc/jsql-neo-native/Cargo.lock +9 -2
  26. package/nativesrc/jsql-neo-native/Cargo.toml +9 -1
  27. package/nativesrc/jsql-neo-native/src/lib.rs +14 -2
  28. package/nativesrc/jsql-neo-wasm/Cargo.lock +9 -2
  29. package/nativesrc/jsql-neo-wasm/Cargo.toml +8 -1
  30. package/nativesrc/jsql-neo-wasm/src/lib.rs +18 -2
  31. package/package.json +4 -3
  32. package/test/sql-parser.test.js +450 -0
  33. package/wasm/browser.mjs +12 -0
  34. package/wasm/browser_bg.mjs +269 -248
  35. package/wasm/jsql_neo_wasm.d.ts +6 -0
  36. package/wasm/jsql_neo_wasm.js +248 -127
  37. package/wasm/jsql_neo_wasm_bg.wasm +0 -0
  38. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +22 -22
  39. package/wasm/package.json +1 -1
@@ -1,5 +1,9 @@
1
1
  const path = require('path');
2
2
  const { executeSQL } = require('./sql');
3
+ const {
4
+ createHookRegistry, applyPlugin, buildPluginContext, registerHook,
5
+ runHooks, getPluginApis
6
+ } = require('./plugin');
3
7
  const wasmBindings = require(path.join(__dirname, '..', 'wasm', 'jsql_neo_wasm.js'));
4
8
 
5
9
  function safeJsonParse(str) {
@@ -294,17 +298,7 @@ class JSQL {
294
298
  this._autoPlugins = opts.modules !== false;
295
299
 
296
300
  this._plugins = [];
297
- this._hooks = {
298
- beforeInsert: [], afterInsert: [],
299
- beforeUpdate: [], afterUpdate: [],
300
- beforeDelete: [], afterDelete: [],
301
- beforeFind: [], afterFind: [],
302
- beforeCreateTable: [], afterCreateTable: [],
303
- beforeDropTable: [], afterDropTable: [],
304
- beforeFlush: [], afterFlush: [],
305
- beforeCount: [], afterCount: [],
306
- onStart: [], onStop: []
307
- };
301
+ this._hooks = createHookRegistry();
308
302
  this._eventListeners = [];
309
303
  if (this._autoPlugins) {
310
304
  const { ModuleManager } = require('./mod');
@@ -312,47 +306,16 @@ class JSQL {
312
306
  }
313
307
  }
314
308
 
315
- use(plugin) {
316
- if (typeof plugin === 'function') {
317
- plugin = { install: plugin };
318
- }
319
- if (plugin.hooks) {
320
- for (const [event, fn] of Object.entries(plugin.hooks)) {
321
- if (this._hooks[event]) {
322
- this._hooks[event].push(fn);
323
- }
324
- }
325
- }
326
- if (typeof plugin.install === 'function') {
327
- plugin.install(this, this._buildCtx(plugin));
328
- }
329
- if (typeof plugin.onEvent === 'function') {
330
- this._eventListeners.push(plugin.onEvent);
331
- }
332
- this._plugins.push(plugin);
333
- return this;
309
+ use(plugin, opts) {
310
+ return applyPlugin(this, plugin, opts);
334
311
  }
335
312
 
336
313
  _buildCtx(plugin) {
337
- const self = this;
338
- return {
339
- name: plugin.name || 'anonymous',
340
- engine: this,
341
- plugin,
342
- on(hook, fn) { return self.on(hook, fn); },
343
- onEvent(fn) { return self.onEvent(fn); },
344
- emit(eventName, data) { self._emit(eventName, data); },
345
- tables() { return Array.from(self._tableNames); },
346
- hasTable(name) { return self._tableNames.has(name); },
347
- getTableSchema(name) { return self._schemas[name] || null; }
348
- };
314
+ return buildPluginContext(this, plugin);
349
315
  }
350
316
 
351
317
  on(event, fn) {
352
- if (this._hooks[event]) {
353
- this._hooks[event].push(fn);
354
- }
355
- return this;
318
+ return registerHook(this, event, fn);
356
319
  }
357
320
 
358
321
  onEvent(fn) {
@@ -360,6 +323,22 @@ class JSQL {
360
323
  return this;
361
324
  }
362
325
 
326
+ /** 广播事件(插件上下文 / 外部均可调用) */
327
+ emit(eventName, data) {
328
+ this._emit(eventName, data);
329
+ return this;
330
+ }
331
+
332
+ /** 主动触发一个钩子(含插件自定义钩子) */
333
+ runHook(hookName, ...args) {
334
+ return this._runHooks(hookName, args);
335
+ }
336
+
337
+ /** 取插件对外暴露的 API(由 plugin.api 声明) */
338
+ plugin(name) {
339
+ return getPluginApis(this).get(name) || null;
340
+ }
341
+
363
342
  _emit(eventName, data) {
364
343
  for (const fn of this._eventListeners) {
365
344
  try { fn(eventName, data); } catch (e) { /* ignore */ }
@@ -367,16 +346,7 @@ class JSQL {
367
346
  }
368
347
 
369
348
  _runHooks(hookName, args) {
370
- const hooks = this._hooks[hookName];
371
- if (!hooks || hooks.length === 0) return true;
372
- for (const fn of hooks) {
373
- const r = fn(...args);
374
- if (r === false) return false;
375
- if (r !== undefined && args.length > 0) {
376
- args[0] = r;
377
- }
378
- }
379
- return true;
349
+ return runHooks(this, hookName, args);
380
350
  }
381
351
 
382
352
  async start() {
@@ -533,6 +503,17 @@ class JSQL {
533
503
  return r;
534
504
  }
535
505
 
506
+ async addColumn(table, name, def) {
507
+ await this._flush();
508
+ const mapped = mapNativeSchema({ [name]: def })[name];
509
+ const r = safeJsonParse(wasmBindings.jsql_add_column(table, name, JSON.stringify(mapped)));
510
+ if (r && r.ok === false) throw new Error(r.error || 'add column failed');
511
+ if (!this._schemas[table]) this._schemas[table] = {};
512
+ this._schemas[table][name] = mapped;
513
+ this._emit('addColumn', { table, name, def: mapped });
514
+ return r;
515
+ }
516
+
536
517
  async dropTable(name) {
537
518
  await this._flush();
538
519
  if (!this._runHooks('beforeDropTable', [name])) return null;
@@ -705,7 +686,10 @@ class JSQL {
705
686
  * const rows = await db.query("SELECT * FROM users WHERE age > ?", [21]);
706
687
  */
707
688
  async query(sql, paramsOrOpts, opts) {
708
- return executeSQL(this, sql, paramsOrOpts, opts);
689
+ if (!this._runHooks('beforeQuery', [sql, paramsOrOpts, opts])) return null;
690
+ const result = await executeSQL(this, sql, paramsOrOpts, opts);
691
+ this._runHooks('afterQuery', [sql, paramsOrOpts, opts, result]);
692
+ return result;
709
693
  }
710
694
 
711
695
  async stop() {
Binary file
@@ -171,13 +171,14 @@ dependencies = [
171
171
 
172
172
  [[package]]
173
173
  name = "jsql-neo-core"
174
- version = "0.1.0"
174
+ version = "6.0.0"
175
175
  dependencies = [
176
176
  "bincode",
177
177
  "chrono",
178
178
  "crc32fast",
179
179
  "itoa",
180
180
  "js-sys",
181
+ "regex-lite",
181
182
  "rustc-hash",
182
183
  "ryu",
183
184
  "serde",
@@ -242,6 +243,12 @@ dependencies = [
242
243
  "proc-macro2",
243
244
  ]
244
245
 
246
+ [[package]]
247
+ name = "regex-lite"
248
+ version = "0.1.9"
249
+ source = "registry+https://github.com/rust-lang/crates.io-index"
250
+ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
251
+
245
252
  [[package]]
246
253
  name = "rustc-hash"
247
254
  version = "2.1.3"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsql-neo-core"
3
- version = "5.4.0"
3
+ version = "6.0.0"
4
4
  edition = "2021"
5
5
  description = "Embedded database engine — core library (supports WASM)"
6
6
 
@@ -16,6 +16,8 @@ itoa = "1"
16
16
  ryu = "1"
17
17
  rustc-hash = "2"
18
18
  bincode = "1"
19
+ # 线性时间、无回溯的正则引擎:用于 $like / $regex 模糊查询(无 ReDoS 风险)
20
+ regex-lite = "0.1"
19
21
  wasm-bindgen = { version = "0.2", optional = true }
20
22
  js-sys = { version = "0.3", optional = true }
21
23
 
@@ -161,6 +161,13 @@ impl HybridEngine {
161
161
 
162
162
  /// 把单个表写盘(原子:tmp + rename)
163
163
  pub fn flush_table(&self, name: &str) -> Result<(), String> {
164
+ self.flush_table_inner(name)?;
165
+ self.save_meta();
166
+ Ok(())
167
+ }
168
+
169
+ /// 写盘但不落 meta.json —— 批量 flush 时由调用方统一写一次,避免 N 次 meta 写放大
170
+ fn flush_table_inner(&self, name: &str) -> Result<(), String> {
164
171
  if self.store_dir.is_none() { return Ok(()); }
165
172
  let json = {
166
173
  let mem = self.mem.borrow();
@@ -190,7 +197,6 @@ impl HybridEngine {
190
197
  fs::write(&tmp, json).map_err(|e| format!("write {}: {}", tmp.display(), e))?;
191
198
  let final_path = self.table_path(name, &file);
192
199
  fs::rename(&tmp, &final_path).map_err(|e| format!("rename {}: {}", final_path.display(), e))?;
193
- self.save_meta();
194
200
  Ok(())
195
201
  }
196
202
 
@@ -201,7 +207,7 @@ impl HybridEngine {
201
207
  if names.is_empty() { return Ok(0); }
202
208
  let mut flushed = 0;
203
209
  for name in &names {
204
- if self.flush_table(name).is_ok() {
210
+ if self.flush_table_inner(name).is_ok() {
205
211
  self.dirty.borrow_mut().remove(name);
206
212
  flushed += 1;
207
213
  }
@@ -212,17 +218,25 @@ impl HybridEngine {
212
218
 
213
219
  /// LRU 驱逐一个最冷已落盘表(返回被驱逐的表名)
214
220
  pub fn evict_one(&self) -> Option<String> {
215
- let names: Vec<String> = self.mem.borrow().tables.keys().cloned().collect();
216
- let dirty = self.dirty.borrow();
217
- let candidates: Vec<String> = names.into_iter()
218
- .filter(|n| !dirty.contains(n) && self.meta.borrow().contains_key(n))
219
- .collect();
220
- if candidates.is_empty() { return None; }
221
- let mut sorted = candidates;
222
- sorted.sort_by_key(|n| self.last_access.borrow().get(n).copied().unwrap_or(0));
223
- let victim = sorted.remove(0);
224
- self.mem.borrow_mut().tables.remove(&victim);
225
- Some(victim)
221
+ let victim = {
222
+ let mem = self.mem.borrow();
223
+ let dirty = self.dirty.borrow();
224
+ let meta = self.meta.borrow();
225
+ let access = self.last_access.borrow();
226
+ let mut best: Option<(u64, &str)> = None;
227
+ for name in mem.tables.keys() {
228
+ if dirty.contains(name) || !meta.contains_key(name) { continue; }
229
+ let ts = access.get(name).copied().unwrap_or(0);
230
+ if best.map_or(true, |(bts, _)| ts < bts) {
231
+ best = Some((ts, name.as_str()));
232
+ }
233
+ }
234
+ best.map(|(_, n)| n.to_string())
235
+ };
236
+ if let Some(ref v) = victim {
237
+ self.mem.borrow_mut().tables.remove(v);
238
+ }
239
+ victim
226
240
  }
227
241
 
228
242
  /// 批量二进制插入(native_client encodeBatch 格式),兼容原 jsql_insert_buf 性能路径
@@ -324,6 +338,19 @@ impl Engine for HybridEngine {
324
338
  Ok(())
325
339
  }
326
340
 
341
+ fn add_column(&mut self, table: &str, name: &str, fs: FieldSchema) -> Result<(), String> {
342
+ // 先更新内存表结构(含已有行回填),再同步持久化元数据中的 schema。
343
+ self.mem.borrow_mut().add_column(table, name, fs.clone())?;
344
+ if let Some(tm) = self.meta.borrow_mut().get_mut(table) {
345
+ if !tm.schema.iter().any(|(n, _)| n == name) {
346
+ tm.schema.push((name.to_string(), fs));
347
+ }
348
+ self.save_meta();
349
+ self.mark_dirty(table);
350
+ }
351
+ Ok(())
352
+ }
353
+
327
354
  fn drop_table(&mut self, name: &str) -> Result<(), String> {
328
355
  let in_mem = self.mem.borrow().tables.contains_key(name);
329
356
  let in_meta = self.meta.borrow().contains_key(name);
@@ -391,6 +418,16 @@ impl Engine for HybridEngine {
391
418
  r
392
419
  }
393
420
 
421
+ fn find_json(&self, table: &str, filter: &Option<HashMap<String, serde_json::Value>>,
422
+ limit: Option<usize>, offset: Option<usize>,
423
+ cursor: Option<String>,
424
+ order_by: &Option<String>, order: &Option<String>) -> Result<(String, Option<String>, bool), String> {
425
+ self.ensure_table(table);
426
+ let r = self.mem.borrow().find_json(table, filter, limit, offset, cursor, order_by, order);
427
+ if r.is_ok() { self.touch(table); }
428
+ r
429
+ }
430
+
394
431
  fn count(&self, table: &str) -> Result<usize, String> {
395
432
  self.ensure_table(table);
396
433
  let r = self.mem.borrow().count(table);
@@ -1,7 +1,7 @@
1
1
  use std::collections::HashMap;
2
- use crate::engine::table::Table;
2
+ use crate::engine::table::{Table, matches_filter};
3
3
  use crate::engine::{validate_table_name, Engine};
4
- use crate::types::{Row, TableDefinition};
4
+ use crate::types::{Row, TableDefinition, FieldSchema};
5
5
 
6
6
  pub struct MemoryEngine {
7
7
  pub tables: HashMap<String, Table>,
@@ -35,6 +35,14 @@ impl Engine for MemoryEngine {
35
35
  Ok(())
36
36
  }
37
37
 
38
+ fn add_column(&mut self, table: &str, name: &str, fs: FieldSchema) -> Result<(), String> {
39
+ let t = self.tables.get_mut(table).ok_or_else(|| format!("table '{}' not found", table))?;
40
+ if !t.add_column(name, &fs) {
41
+ return Err(format!("column '{}' already exists in table '{}'", name, table));
42
+ }
43
+ Ok(())
44
+ }
45
+
38
46
  fn drop_table(&mut self, name: &str) -> Result<(), String> {
39
47
  self.tables.remove(name).map(|_| ()).ok_or_else(|| format!("table '{}' not found", name))
40
48
  }
@@ -82,6 +90,18 @@ impl Engine for MemoryEngine {
82
90
  Ok((rows, next_cursor, has_more))
83
91
  }
84
92
 
93
+ fn find_json(&self, table: &str, filter: &Option<HashMap<String, serde_json::Value>>,
94
+ limit: Option<usize>, offset: Option<usize>,
95
+ cursor: Option<String>,
96
+ order_by: &Option<String>, order: &Option<String>) -> Result<(String, Option<String>, bool), String> {
97
+ let t = self.tables.get(table).ok_or_else(|| format!("table '{}' not found", table))?;
98
+ let off = cursor.as_deref().and_then(|c| c.strip_prefix('c')?.parse().ok()).or(offset);
99
+ let off_val = off.unwrap_or(0);
100
+ let (json, has_more) = t.find_rows_json(filter, limit, off, order_by, order);
101
+ let next_cursor = if has_more { Some(format!("c{}", off_val + limit.unwrap())) } else { None };
102
+ Ok((json, next_cursor, has_more))
103
+ }
104
+
85
105
  fn count(&self, table: &str) -> Result<usize, String> {
86
106
  let t = self.tables.get(table).ok_or_else(|| format!("table '{}' not found", table))?;
87
107
  Ok(t.count())
@@ -109,17 +129,20 @@ impl Engine for MemoryEngine {
109
129
 
110
130
  fn update_by_filter(&mut self, table: &str, filter: &Option<HashMap<String, serde_json::Value>>,
111
131
  data: HashMap<String, serde_json::Value>) -> Result<usize, String> {
112
- let ids: Vec<u64> = {
113
- let t = self.tables.get(table).ok_or_else(|| format!("table '{}' not found", table))?;
114
- t.rows.iter()
115
- .filter(|r| filter.as_ref().map_or(true, |f| t.row_matches(r, f)))
116
- .map(|r| r.id)
117
- .collect()
132
+ let t = self.tables.get_mut(table).ok_or_else(|| format!("table '{}' not found", table))?;
133
+ let ids: Vec<u64> = match filter {
134
+ Some(f) => {
135
+ let conds = t.parse_filter(f);
136
+ t.rows.iter()
137
+ .filter(|r| matches_filter(t.get_row_values(r), &conds))
138
+ .map(|r| r.id)
139
+ .collect()
140
+ }
141
+ None => t.rows.iter().map(|r| r.id).collect(),
118
142
  };
119
143
  let count = ids.len();
120
144
  for id in &ids {
121
- let t = self.tables.get_mut(table).ok_or_else(|| format!("table '{}' not found", table))?;
122
- t.update_row(*id, data.clone());
145
+ t.update_row_ref(*id, &data);
123
146
  }
124
147
  Ok(count)
125
148
  }
@@ -127,19 +150,19 @@ impl Engine for MemoryEngine {
127
150
  fn remove_by_filter(&mut self, table: &str, filter: &Option<HashMap<String, serde_json::Value>>) -> Result<usize, String> {
128
151
  let t = self.tables.get_mut(table).ok_or_else(|| format!("table '{}' not found", table))?;
129
152
  let len_before = t.rows.len();
130
- if filter.is_none() {
131
- t.rows.clear();
132
- t.pk_index.clear();
133
- return Ok(len_before);
134
- }
135
- let f = filter.as_ref().unwrap();
136
- let mut to_remove = Vec::new();
137
- for (i, r) in t.rows.iter().enumerate() {
138
- let matches = f.iter().all(|(k, v)| {
139
- t.field_index.get(k).and_then(|&pos| t.get_row_values(r).get(pos)).map_or(false, |fv| fv.eq_json(v))
140
- });
141
- if matches { to_remove.push(i); }
142
- }
153
+ let f = match filter {
154
+ None => {
155
+ t.rows.clear();
156
+ t.pk_index.clear();
157
+ return Ok(len_before);
158
+ }
159
+ Some(f) => f,
160
+ };
161
+ let conds = t.parse_filter(f);
162
+ let to_remove: Vec<usize> = t.rows.iter().enumerate()
163
+ .filter(|(_, r)| matches_filter(t.get_row_values(r), &conds))
164
+ .map(|(i, _)| i)
165
+ .collect();
143
166
  for i in to_remove.into_iter().rev() {
144
167
  t.remove_row_by_idx(i);
145
168
  }
@@ -30,10 +30,12 @@ pub fn validate_table_name(name: &str) -> Result<(), String> {
30
30
  }
31
31
 
32
32
  use std::collections::HashMap;
33
- use crate::types::{Row, TableDefinition};
33
+ use crate::types::{Row, TableDefinition, FieldSchema};
34
34
 
35
35
  pub trait Engine {
36
36
  fn create_table(&mut self, def: TableDefinition) -> Result<(), String>;
37
+ /// 运行期加列:扩展表结构并为已有行回填默认值(见 `Table::add_column`)。
38
+ fn add_column(&mut self, table: &str, name: &str, fs: FieldSchema) -> Result<(), String>;
37
39
  fn drop_table(&mut self, name: &str) -> Result<(), String>;
38
40
  fn has_table(&self, name: &str) -> bool;
39
41
  fn list_tables(&self) -> Vec<String>;
@@ -46,6 +48,16 @@ pub trait Engine {
46
48
  limit: Option<usize>, offset: Option<usize>,
47
49
  cursor: Option<String>,
48
50
  order_by: &Option<String>, order: &Option<String>) -> Result<(Vec<Row>, Option<String>, bool), String>;
51
+ /// 与 `find` 等价,但直接产出 JSON 字符串,省掉 `Row` / `HashMap<String, Value>`
52
+ /// 中间表示与随之而来的二次序列化。默认实现回退到 `find`。
53
+ fn find_json(&self, table: &str, filter: &Option<HashMap<String, serde_json::Value>>,
54
+ limit: Option<usize>, offset: Option<usize>,
55
+ cursor: Option<String>,
56
+ order_by: &Option<String>, order: &Option<String>) -> Result<(String, Option<String>, bool), String> {
57
+ let (rows, cursor, has_more) = self.find(table, filter, limit, offset, cursor, order_by, order)?;
58
+ let json = serde_json::to_string(&rows).unwrap_or_else(|_| "[]".to_string());
59
+ Ok((json, cursor, has_more))
60
+ }
49
61
  fn count(&self, table: &str) -> Result<usize, String>;
50
62
  fn update_by_id(&mut self, table: &str, id: u64, data: HashMap<String, serde_json::Value>) -> Result<bool, String>;
51
63
  fn update_by_ids(&mut self, table: &str, batch: Vec<(u64, HashMap<String, serde_json::Value>)>) -> Result<usize, String>;