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.
- package/README.md +198 -14
- package/index.d.ts +29 -0
- package/index.js +12 -2
- package/lib/ast.js +214 -0
- package/lib/database.js +26 -60
- package/lib/mongo_server.js +1 -1
- package/lib/native_client.js +43 -66
- package/lib/plugin.js +227 -3
- package/lib/plugins/audit.js +37 -0
- package/lib/plugins/index.js +72 -0
- package/lib/plugins/logger.js +35 -0
- package/lib/plugins/metrics.js +38 -0
- package/lib/plugins/timestamps.js +43 -0
- package/lib/plugins/validation.js +128 -0
- package/lib/sql.js +1212 -84
- package/lib/wasm_client.js +41 -57
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +8 -1
- package/nativesrc/jsql-neo-core/Cargo.toml +3 -1
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +50 -13
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +46 -23
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +13 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +338 -42
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +245 -32
- package/nativesrc/jsql-neo-native/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-native/Cargo.toml +9 -1
- package/nativesrc/jsql-neo-native/src/lib.rs +14 -2
- package/nativesrc/jsql-neo-wasm/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-wasm/Cargo.toml +8 -1
- package/nativesrc/jsql-neo-wasm/src/lib.rs +18 -2
- package/package.json +4 -3
- package/test/sql-parser.test.js +450 -0
- package/wasm/browser.mjs +12 -0
- package/wasm/browser_bg.mjs +269 -248
- package/wasm/jsql_neo_wasm.d.ts +6 -0
- package/wasm/jsql_neo_wasm.js +248 -127
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +22 -22
- package/wasm/package.json +1 -1
package/lib/wasm_client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 = "
|
|
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.
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
.
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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>;
|