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
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
//! 预写日志(WAL)。
|
|
2
|
+
//!
|
|
3
|
+
//! 记录格式:`[len: u64 LE][crc32: u32 LE][bincode(WalEntry)]`,头部与载荷一次性写出,
|
|
4
|
+
//! 每条记录带 CRC 校验。重放时遇到截断或校验失败的记录即停止,把它视为「未完整落盘」,
|
|
5
|
+
//! 这样即使进程在写一半时被杀,也不会把半截数据当成有效记录。
|
|
6
|
+
|
|
1
7
|
use serde::{Deserialize, Serialize};
|
|
2
|
-
use std::io::{Read, Write};
|
|
8
|
+
use std::io::{Read, Seek, SeekFrom, Write};
|
|
3
9
|
|
|
4
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
10
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
11
|
+
#[repr(u8)]
|
|
5
12
|
pub enum WalOp {
|
|
6
|
-
CreateTable,
|
|
7
|
-
DropTable,
|
|
8
|
-
Insert,
|
|
9
|
-
Update,
|
|
10
|
-
Remove,
|
|
13
|
+
CreateTable = 0,
|
|
14
|
+
DropTable = 1,
|
|
15
|
+
Insert = 2,
|
|
16
|
+
Update = 3,
|
|
17
|
+
Remove = 4,
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
@@ -17,40 +24,133 @@ pub struct WalEntry {
|
|
|
17
24
|
pub data: Vec<u8>,
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
/// 记录头长度:len(u64) + crc32(u32)
|
|
28
|
+
const HEADER_LEN: usize = 12;
|
|
29
|
+
/// 单条载荷上限,避免损坏的长度字段触发超大内存分配
|
|
30
|
+
const MAX_PAYLOAD: usize = 64 * 1024 * 1024;
|
|
31
|
+
|
|
32
|
+
fn io_err<E: std::fmt::Display>(e: E) -> std::io::Error {
|
|
33
|
+
std::io::Error::new(std::io::ErrorKind::Other, e.to_string())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// 顺序重放,回调所有 `tx_id > after_tx` 的完整记录。
|
|
37
|
+
/// 返回成功解析的字节数(尾部残缺部分不计入)。
|
|
38
|
+
fn replay<R: Read>(reader: &mut R, after_tx: u64, mut on_entry: impl FnMut(WalEntry)) -> std::io::Result<u64> {
|
|
39
|
+
let mut valid = 0u64;
|
|
40
|
+
let mut header = [0u8; HEADER_LEN];
|
|
41
|
+
loop {
|
|
42
|
+
if reader.read_exact(&mut header).is_err() {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
let len = u64::from_le_bytes(header[..8].try_into().unwrap()) as usize;
|
|
46
|
+
let crc = u32::from_le_bytes(header[8..].try_into().unwrap());
|
|
47
|
+
if len == 0 || len > MAX_PAYLOAD {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
let mut body = vec![0u8; len];
|
|
51
|
+
if reader.read_exact(&mut body).is_err() {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if crc32fast::hash(&body) != crc {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
match bincode::deserialize::<WalEntry>(&body) {
|
|
58
|
+
Ok(entry) => {
|
|
59
|
+
valid += (HEADER_LEN + len) as u64;
|
|
60
|
+
if entry.tx_id > after_tx {
|
|
61
|
+
on_entry(entry);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
Err(_) => break,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
Ok(valid)
|
|
68
|
+
}
|
|
69
|
+
|
|
20
70
|
pub struct WalWriter {
|
|
21
|
-
|
|
22
|
-
file: Option<std::fs::File>,
|
|
71
|
+
file: std::fs::File,
|
|
23
72
|
next_tx: u64,
|
|
73
|
+
pending: u32,
|
|
74
|
+
sync_interval: u32,
|
|
75
|
+
buf: Vec<u8>,
|
|
24
76
|
}
|
|
25
77
|
|
|
26
78
|
impl WalWriter {
|
|
27
79
|
pub fn open(path: &str) -> std::io::Result<Self> {
|
|
28
|
-
let file = std::fs::OpenOptions::new()
|
|
80
|
+
let mut file = std::fs::OpenOptions::new()
|
|
29
81
|
.create(true)
|
|
82
|
+
.read(true)
|
|
30
83
|
.append(true)
|
|
31
84
|
.open(path)?;
|
|
32
|
-
|
|
85
|
+
// 从已有文件恢复 next_tx:否则重启后 tx_id 会从 1 重新开始,
|
|
86
|
+
// 与历史记录撞号,`replay_after` 的增量语义就废了。
|
|
87
|
+
let mut max_tx = 0u64;
|
|
88
|
+
replay(&mut file, 0, |e| {
|
|
89
|
+
if e.tx_id > max_tx {
|
|
90
|
+
max_tx = e.tx_id;
|
|
91
|
+
}
|
|
92
|
+
})?;
|
|
93
|
+
file.seek(SeekFrom::End(0))?;
|
|
94
|
+
Ok(Self {
|
|
95
|
+
file,
|
|
96
|
+
next_tx: max_tx + 1,
|
|
97
|
+
pending: 0,
|
|
98
|
+
sync_interval: 1,
|
|
99
|
+
buf: Vec::with_capacity(256),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// 每写多少条记录 fsync 一次。1 = 每条都落盘(默认,最安全);调大换取吞吐。
|
|
104
|
+
pub fn set_sync_interval(&mut self, n: u32) {
|
|
105
|
+
self.sync_interval = n.max(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub fn next_tx(&self) -> u64 {
|
|
109
|
+
self.next_tx
|
|
33
110
|
}
|
|
34
111
|
|
|
35
|
-
#[allow(dead_code)]
|
|
36
112
|
pub fn append(&mut self, op: WalOp, data: Vec<u8>) -> std::io::Result<u64> {
|
|
37
113
|
let tx_id = self.next_tx;
|
|
114
|
+
let body = bincode::serialize(&WalEntry { tx_id, op, data }).map_err(io_err)?;
|
|
115
|
+
if body.len() > MAX_PAYLOAD {
|
|
116
|
+
return Err(std::io::Error::new(
|
|
117
|
+
std::io::ErrorKind::InvalidInput,
|
|
118
|
+
"wal record too large",
|
|
119
|
+
));
|
|
120
|
+
}
|
|
121
|
+
// 头 + 载荷一次 write_all,避免两次写之间被打断留下半截头部
|
|
122
|
+
self.buf.clear();
|
|
123
|
+
self.buf.extend_from_slice(&(body.len() as u64).to_le_bytes());
|
|
124
|
+
self.buf.extend_from_slice(&crc32fast::hash(&body).to_le_bytes());
|
|
125
|
+
self.buf.extend_from_slice(&body);
|
|
126
|
+
self.file.write_all(&self.buf)?;
|
|
127
|
+
|
|
38
128
|
self.next_tx += 1;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let len = buf.len() as u64;
|
|
43
|
-
file.write_all(&len.to_le_bytes())?;
|
|
44
|
-
file.write_all(&buf)?;
|
|
45
|
-
file.flush()?;
|
|
129
|
+
self.pending += 1;
|
|
130
|
+
if self.pending >= self.sync_interval {
|
|
131
|
+
self.sync()?;
|
|
46
132
|
}
|
|
47
133
|
Ok(tx_id)
|
|
48
134
|
}
|
|
49
135
|
|
|
136
|
+
/// 真正落盘。`File::flush` 是空操作,WAL 必须走 `sync_data` 才算持久化。
|
|
137
|
+
pub fn sync(&mut self) -> std::io::Result<()> {
|
|
138
|
+
self.file.sync_data()?;
|
|
139
|
+
self.pending = 0;
|
|
140
|
+
Ok(())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// `sync` 的别名
|
|
50
144
|
pub fn flush(&mut self) -> std::io::Result<()> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
145
|
+
self.sync()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// 清空日志(快照落盘后调用),避免 WAL 无限增长
|
|
149
|
+
pub fn truncate(&mut self) -> std::io::Result<()> {
|
|
150
|
+
self.file.set_len(0)?;
|
|
151
|
+
self.file.seek(SeekFrom::Start(0))?;
|
|
152
|
+
self.file.sync_data()?;
|
|
153
|
+
self.pending = 0;
|
|
54
154
|
Ok(())
|
|
55
155
|
}
|
|
56
156
|
}
|
|
@@ -64,23 +164,136 @@ impl WalReader {
|
|
|
64
164
|
Self { path: path.to_string() }
|
|
65
165
|
}
|
|
66
166
|
|
|
67
|
-
|
|
167
|
+
/// 增量重放:只返回 `tx_id > after_tx` 的记录。
|
|
168
|
+
pub fn replay_after(&self, after_tx: u64) -> std::io::Result<Vec<WalEntry>> {
|
|
68
169
|
let file = match std::fs::File::open(&self.path) {
|
|
69
170
|
Ok(f) => f,
|
|
70
171
|
Err(_) => return Ok(Vec::new()),
|
|
71
172
|
};
|
|
72
173
|
let mut reader = std::io::BufReader::new(file);
|
|
73
174
|
let mut entries = Vec::new();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
175
|
+
replay(&mut reader, after_tx, |e| entries.push(e))?;
|
|
176
|
+
Ok(entries)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// 重放全部记录
|
|
180
|
+
pub fn replay_all(&self) -> std::io::Result<Vec<WalEntry>> {
|
|
181
|
+
self.replay_after(0)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[cfg(test)]
|
|
186
|
+
mod tests {
|
|
187
|
+
use super::*;
|
|
188
|
+
|
|
189
|
+
fn tmp_path(tag: &str) -> String {
|
|
190
|
+
let mut p = std::env::temp_dir();
|
|
191
|
+
p.push(format!("jsql-wal-{}-{}.log", tag, std::process::id()));
|
|
192
|
+
let s = p.to_string_lossy().to_string();
|
|
193
|
+
let _ = std::fs::remove_file(&s);
|
|
194
|
+
s
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[test]
|
|
198
|
+
fn append_replay_roundtrip() {
|
|
199
|
+
let path = tmp_path("rt");
|
|
200
|
+
{
|
|
201
|
+
let mut w = WalWriter::open(&path).unwrap();
|
|
202
|
+
for i in 0..5u8 {
|
|
203
|
+
assert_eq!(w.append(WalOp::Insert, vec![i]).unwrap(), i as u64 + 1);
|
|
82
204
|
}
|
|
205
|
+
w.sync().unwrap();
|
|
83
206
|
}
|
|
84
|
-
|
|
207
|
+
let entries = WalReader::open(&path).replay_all().unwrap();
|
|
208
|
+
assert_eq!(entries.len(), 5);
|
|
209
|
+
assert_eq!(entries[3].tx_id, 4);
|
|
210
|
+
assert_eq!(entries[3].op, WalOp::Insert);
|
|
211
|
+
assert_eq!(entries[3].data, vec![3u8]);
|
|
212
|
+
let _ = std::fs::remove_file(&path);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn replay_after_is_incremental() {
|
|
217
|
+
let path = tmp_path("inc");
|
|
218
|
+
{
|
|
219
|
+
let mut w = WalWriter::open(&path).unwrap();
|
|
220
|
+
for i in 0..5u8 {
|
|
221
|
+
w.append(WalOp::Update, vec![i]).unwrap();
|
|
222
|
+
}
|
|
223
|
+
w.sync().unwrap();
|
|
224
|
+
}
|
|
225
|
+
let tail = WalReader::open(&path).replay_after(3).unwrap();
|
|
226
|
+
assert_eq!(tail.len(), 2);
|
|
227
|
+
assert!(tail.iter().all(|e| e.tx_id > 3));
|
|
228
|
+
let _ = std::fs::remove_file(&path);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[test]
|
|
232
|
+
fn next_tx_is_recovered_after_reopen() {
|
|
233
|
+
let path = tmp_path("recover");
|
|
234
|
+
{
|
|
235
|
+
let mut w = WalWriter::open(&path).unwrap();
|
|
236
|
+
w.append(WalOp::Insert, vec![1]).unwrap();
|
|
237
|
+
w.append(WalOp::Insert, vec![2]).unwrap();
|
|
238
|
+
w.sync().unwrap();
|
|
239
|
+
}
|
|
240
|
+
let mut w = WalWriter::open(&path).unwrap();
|
|
241
|
+
assert_eq!(w.next_tx(), 3, "tx_id 必须接着历史最大值,不能回退到 1");
|
|
242
|
+
assert_eq!(w.append(WalOp::Remove, vec![3]).unwrap(), 3);
|
|
243
|
+
assert_eq!(WalReader::open(&path).replay_all().unwrap().len(), 3);
|
|
244
|
+
let _ = std::fs::remove_file(&path);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn truncated_tail_is_dropped() {
|
|
249
|
+
let path = tmp_path("torn");
|
|
250
|
+
{
|
|
251
|
+
let mut w = WalWriter::open(&path).unwrap();
|
|
252
|
+
w.append(WalOp::Insert, vec![7]).unwrap();
|
|
253
|
+
w.append(WalOp::Insert, vec![8]).unwrap();
|
|
254
|
+
w.sync().unwrap();
|
|
255
|
+
}
|
|
256
|
+
// 模拟写一半被杀:尾部只剩半截垃圾
|
|
257
|
+
{
|
|
258
|
+
use std::io::Write as _;
|
|
259
|
+
let mut f = std::fs::OpenOptions::new().append(true).open(&path).unwrap();
|
|
260
|
+
f.write_all(&[9u8, 9, 9]).unwrap();
|
|
261
|
+
f.sync_all().unwrap();
|
|
262
|
+
}
|
|
263
|
+
assert_eq!(WalReader::open(&path).replay_all().unwrap().len(), 2);
|
|
264
|
+
let _ = std::fs::remove_file(&path);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn crc_mismatch_stops_replay() {
|
|
269
|
+
let path = tmp_path("crc");
|
|
270
|
+
{
|
|
271
|
+
let mut w = WalWriter::open(&path).unwrap();
|
|
272
|
+
w.append(WalOp::Insert, vec![1]).unwrap();
|
|
273
|
+
w.append(WalOp::Insert, vec![2]).unwrap();
|
|
274
|
+
w.sync().unwrap();
|
|
275
|
+
}
|
|
276
|
+
let len = std::fs::metadata(&path).unwrap().len();
|
|
277
|
+
{
|
|
278
|
+
use std::io::{Seek, SeekFrom, Write as _};
|
|
279
|
+
let mut f = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
|
|
280
|
+
f.seek(SeekFrom::Start(len - 1)).unwrap();
|
|
281
|
+
f.write_all(&[0xFF]).unwrap();
|
|
282
|
+
f.sync_all().unwrap();
|
|
283
|
+
}
|
|
284
|
+
assert_eq!(WalReader::open(&path).replay_all().unwrap().len(), 1, "校验失败记录应丢弃");
|
|
285
|
+
let _ = std::fs::remove_file(&path);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[test]
|
|
289
|
+
fn truncate_resets_log_but_keeps_tx_monotonic() {
|
|
290
|
+
let path = tmp_path("trunc");
|
|
291
|
+
let mut w = WalWriter::open(&path).unwrap();
|
|
292
|
+
w.append(WalOp::Insert, vec![1]).unwrap();
|
|
293
|
+
w.truncate().unwrap();
|
|
294
|
+
assert_eq!(WalReader::open(&path).replay_all().unwrap().len(), 0);
|
|
295
|
+
// tx_id 不能回落:否则消费方持有的水位线(replay_after(T))会看不到新记录
|
|
296
|
+
assert_eq!(w.append(WalOp::DropTable, vec![]).unwrap(), 2);
|
|
297
|
+
let _ = std::fs::remove_file(&path);
|
|
85
298
|
}
|
|
86
299
|
}
|
|
@@ -205,12 +205,13 @@ dependencies = [
|
|
|
205
205
|
|
|
206
206
|
[[package]]
|
|
207
207
|
name = "jsql-neo-core"
|
|
208
|
-
version = "
|
|
208
|
+
version = "5.6.0"
|
|
209
209
|
dependencies = [
|
|
210
210
|
"bincode",
|
|
211
211
|
"chrono",
|
|
212
212
|
"crc32fast",
|
|
213
213
|
"itoa",
|
|
214
|
+
"regex-lite",
|
|
214
215
|
"rustc-hash",
|
|
215
216
|
"ryu",
|
|
216
217
|
"serde",
|
|
@@ -219,7 +220,7 @@ dependencies = [
|
|
|
219
220
|
|
|
220
221
|
[[package]]
|
|
221
222
|
name = "jsql-neo-native"
|
|
222
|
-
version = "
|
|
223
|
+
version = "5.6.0"
|
|
223
224
|
dependencies = [
|
|
224
225
|
"jsql-neo-core",
|
|
225
226
|
"napi",
|
|
@@ -375,6 +376,12 @@ dependencies = [
|
|
|
375
376
|
"regex-syntax",
|
|
376
377
|
]
|
|
377
378
|
|
|
379
|
+
[[package]]
|
|
380
|
+
name = "regex-lite"
|
|
381
|
+
version = "0.1.9"
|
|
382
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
383
|
+
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
|
|
384
|
+
|
|
378
385
|
[[package]]
|
|
379
386
|
name = "regex-syntax"
|
|
380
387
|
version = "0.8.11"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "jsql-neo-native"
|
|
3
|
-
version = "
|
|
3
|
+
version = "6.0.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
|
|
6
6
|
[lib]
|
|
@@ -14,3 +14,11 @@ serde_json = "1"
|
|
|
14
14
|
|
|
15
15
|
[build-dependencies]
|
|
16
16
|
napi-build = "2"
|
|
17
|
+
|
|
18
|
+
# 体积更小 + 运行更快:全图 LTO 去掉跨 crate 死代码,单 codegen-unit 换取更好的内联与优化,
|
|
19
|
+
# strip 去掉符号表(不影响 napi 动态导出符号与 dlopen 加载)。
|
|
20
|
+
[profile.release]
|
|
21
|
+
opt-level = 3
|
|
22
|
+
lto = "fat"
|
|
23
|
+
codegen-units = 1
|
|
24
|
+
strip = "symbols"
|
|
@@ -111,6 +111,18 @@ pub fn jsql_create_table(name: String, schema_json: String) -> String {
|
|
|
111
111
|
})
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
#[napi]
|
|
115
|
+
pub fn jsql_add_column(table: String, name: String, fs_json: String) -> String {
|
|
116
|
+
let fs: FieldSchema = match serde_json::from_str(&fs_json) {
|
|
117
|
+
Ok(f) => f,
|
|
118
|
+
Err(e) => return format!(r#"{{"ok":false,"error":"invalid field schema: {}"}}"#, e),
|
|
119
|
+
};
|
|
120
|
+
with_engine_mut(|eng| match eng.add_column(&table, &name, fs) {
|
|
121
|
+
Ok(()) => r#"{"ok":true}"#.to_string(),
|
|
122
|
+
Err(e) => format!(r#"{{"ok":false,"error":"{}"}}"#, e),
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
#[napi]
|
|
115
127
|
pub fn jsql_drop_table(name: String) -> String {
|
|
116
128
|
with_engine_mut(|eng| match eng.drop_table(&name) {
|
|
@@ -152,8 +164,8 @@ pub fn jsql_find(table: String, filter_json: String, limit: i32, offset: i32) ->
|
|
|
152
164
|
with_engine_ro(|eng| {
|
|
153
165
|
let filter: Option<HashMap<String, serde_json::Value>> =
|
|
154
166
|
if filter_json.is_empty() { None } else { serde_json::from_str(&filter_json).ok() };
|
|
155
|
-
match eng.
|
|
156
|
-
Ok((
|
|
167
|
+
match eng.find_json(&table, &filter, Some(limit as usize), Some(offset as usize), None, &None, &None) {
|
|
168
|
+
Ok((json, _, _)) => json,
|
|
157
169
|
Err(e) => format!(r#"{{"error":"{}"}}"#, e),
|
|
158
170
|
}
|
|
159
171
|
})
|
|
@@ -171,13 +171,14 @@ dependencies = [
|
|
|
171
171
|
|
|
172
172
|
[[package]]
|
|
173
173
|
name = "jsql-neo-core"
|
|
174
|
-
version = "
|
|
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",
|
|
@@ -187,7 +188,7 @@ dependencies = [
|
|
|
187
188
|
|
|
188
189
|
[[package]]
|
|
189
190
|
name = "jsql-neo-wasm"
|
|
190
|
-
version = "
|
|
191
|
+
version = "6.0.0"
|
|
191
192
|
dependencies = [
|
|
192
193
|
"js-sys",
|
|
193
194
|
"jsql-neo-core",
|
|
@@ -252,6 +253,12 @@ dependencies = [
|
|
|
252
253
|
"proc-macro2",
|
|
253
254
|
]
|
|
254
255
|
|
|
256
|
+
[[package]]
|
|
257
|
+
name = "regex-lite"
|
|
258
|
+
version = "0.1.9"
|
|
259
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
260
|
+
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
|
|
261
|
+
|
|
255
262
|
[[package]]
|
|
256
263
|
name = "rustc-hash"
|
|
257
264
|
version = "2.1.3"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "jsql-neo-wasm"
|
|
3
|
-
version = "
|
|
3
|
+
version = "6.0.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
|
|
6
6
|
[lib]
|
|
@@ -13,3 +13,10 @@ serde_json = "1"
|
|
|
13
13
|
|
|
14
14
|
[dependencies.js-sys]
|
|
15
15
|
version = "0.3"
|
|
16
|
+
|
|
17
|
+
# wasm 产物:LTO + 单 codegen-unit 显著减小体积,strip 去掉符号表
|
|
18
|
+
[profile.release]
|
|
19
|
+
opt-level = 3
|
|
20
|
+
lto = "fat"
|
|
21
|
+
codegen-units = 1
|
|
22
|
+
strip = "symbols"
|
|
@@ -34,6 +34,22 @@ pub fn jsql_create_table(name: &str, schema_json: &str) -> String {
|
|
|
34
34
|
})
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/// 运行期加列:扩展表结构并为已有行回填默认值。
|
|
38
|
+
/// fs_json 为单个 FieldSchema 的 JSON,例如 {"type":"integer","nullable":true,"default":0}
|
|
39
|
+
#[wasm_bindgen]
|
|
40
|
+
pub fn jsql_add_column(table: &str, name: &str, fs_json: &str) -> String {
|
|
41
|
+
ENGINE.with(|eng| {
|
|
42
|
+
let fs: FieldSchema = match serde_json::from_str(fs_json) {
|
|
43
|
+
Ok(f) => f,
|
|
44
|
+
Err(e) => return format!(r#"{{"ok":false,"error":"invalid field schema: {}"}}"#, e),
|
|
45
|
+
};
|
|
46
|
+
match eng.borrow_mut().add_column(table, name, fs) {
|
|
47
|
+
Ok(()) => r#"{"ok":true}"#.to_string(),
|
|
48
|
+
Err(e) => format!(r#"{{"ok":false,"error":"{}"}}"#, e),
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
#[wasm_bindgen]
|
|
38
54
|
pub fn jsql_drop_table(name: &str) -> String {
|
|
39
55
|
ENGINE.with(|eng| match eng.borrow_mut().drop_table(name) {
|
|
@@ -328,8 +344,8 @@ pub fn jsql_find(table: &str, filter_json: &str, limit: usize, offset: usize) ->
|
|
|
328
344
|
ENGINE.with(|eng| {
|
|
329
345
|
let filter: Option<std::collections::HashMap<String, serde_json::Value>> =
|
|
330
346
|
if filter_json.is_empty() { None } else { serde_json::from_str(filter_json).ok() };
|
|
331
|
-
match eng.borrow().
|
|
332
|
-
Ok((
|
|
347
|
+
match eng.borrow().find_json(table, &filter, Some(limit), Some(offset), None, &None, &None) {
|
|
348
|
+
Ok((json, _, _)) => json,
|
|
333
349
|
Err(e) => format!(r#"{{"error":"{}"}}"#, e),
|
|
334
350
|
}
|
|
335
351
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0-beta2",
|
|
4
4
|
"description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -45,11 +45,12 @@
|
|
|
45
45
|
],
|
|
46
46
|
"scripts": {
|
|
47
47
|
"postinstall": "node postinstall.js",
|
|
48
|
-
"test": "node test/smoke.js",
|
|
48
|
+
"test": "node test/smoke.js && node test/sql-parser.test.js",
|
|
49
|
+
"test:parser": "node test/sql-parser.test.js",
|
|
49
50
|
"test:btree": "node test/btree.test.js",
|
|
50
51
|
"test:native": "node test/native.test.js",
|
|
51
52
|
"test:regress": "node test/regress-5.1.0.js",
|
|
52
|
-
"test:all": "node test/smoke.js && node test/native.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js",
|
|
53
|
+
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/native.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js",
|
|
53
54
|
"test:coverage": "c8 --all --include='lib/**' --exclude='lib/mysql_server.js' --exclude='lib/native_client.js' --exclude='lib/wasm_client.js' --exclude='lib/mysql_compat.js' --exclude='lib/nedb_compat.js' --exclude='lib/plugin.js' --reporter=text --reporter=lcov node test/coverage.js",
|
|
54
55
|
"test:orms": "node examples/orms/run-all.js"
|
|
55
56
|
},
|