jsql-neo 2.0.0 → 2.1.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.
- package/README.md +514 -33
- package/index.js +2 -1
- package/lib/btree.js +2 -1
- package/lib/cache.js +328 -0
- package/lib/database.js +3 -3
- package/lib/query.js +2 -0
- package/lib/table.js +42 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,33 +1,514 @@
|
|
|
1
|
-
```
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
JJJJJJJJJJJ SSSSSSSSSSSSSSS QQQQQQQQQ LLLLLLLLLLL
|
|
5
|
-
J:::::::::J SS:::::::::::::::S QQ:::::::::QQ L:::::::::L
|
|
6
|
-
J:::::::::JS:::::SSSSSS::::::S QQ:::::::::::::QQ L:::::::::L
|
|
7
|
-
JJ:::::::JJS:::::S SSSSSSSQ:::::::QQQ:::::::QLL:::::::LL
|
|
8
|
-
J:::::J S:::::S Q::::::O Q::::::Q L:::::L nnnn nnnnnnnn eeeeeeeeeeee ooooooooooo
|
|
9
|
-
J:::::J S:::::S Q:::::O Q:::::Q L:::::L n:::nn::::::::nn ee::::::::::::ee oo:::::::::::oo
|
|
10
|
-
J:::::J S::::SSSS Q:::::O Q:::::Q L:::::L n::::::::::::::nn e::::::eeeee:::::eeo:::::::::::::::o
|
|
11
|
-
J:::::j SS::::::SSSSS Q:::::O Q:::::Q L:::::L --------------- nn:::::::::::::::ne::::::e e:::::eo:::::ooooo:::::o
|
|
12
|
-
J:::::J SSS::::::::SS Q:::::O Q:::::Q L:::::L -:::::::::::::- n:::::nnnn:::::ne:::::::eeeee::::::eo::::o o::::o
|
|
13
|
-
JJJJJJJ J:::::J SSSSSS::::S Q:::::O Q:::::Q L:::::L --------------- n::::n n::::ne:::::::::::::::::e o::::o o::::o
|
|
14
|
-
J:::::J J:::::J S:::::SQ:::::O QQQQ:::::Q L:::::L n::::n n::::ne::::::eeeeeeeeeee o::::o o::::o
|
|
15
|
-
J::::::J J::::::J S:::::SQ::::::O Q::::::::Q L:::::L LLLLLL n::::n n::::ne:::::::e o::::o o::::o
|
|
16
|
-
J:::::::JJJ:::::::J SSSSSSS S:::::SQ:::::::QQ::::::::QLL:::::::LLLLLLLLL:::::L n::::n n::::ne::::::::e o:::::ooooo:::::o
|
|
17
|
-
JJ:::::::::::::JJ S::::::SSSSSS:::::S QQ::::::::::::::Q L::::::::::::::::::::::L n::::n n::::n e::::::::eeeeeeee o:::::::::::::::o
|
|
18
|
-
JJ:::::::::JJ S:::::::::::::::SS QQ:::::::::::Q L::::::::::::::::::::::L n::::n n::::n ee:::::::::::::e oo:::::::::::oo
|
|
19
|
-
JJJJJJJJJ SSSSSSSSSSSSSSS QQQQQQQQ::::QQLLLLLLLLLLLLLLLLLLLLLLLL nnnnnn nnnnnn eeeeeeeeeeeeee ooooooooooo
|
|
20
|
-
Q:::::Q
|
|
21
|
-
QQQQQQ
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
```
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
JJJJJJJJJJJ SSSSSSSSSSSSSSS QQQQQQQQQ LLLLLLLLLLL
|
|
5
|
+
J:::::::::J SS:::::::::::::::S QQ:::::::::QQ L:::::::::L
|
|
6
|
+
J:::::::::JS:::::SSSSSS::::::S QQ:::::::::::::QQ L:::::::::L
|
|
7
|
+
JJ:::::::JJS:::::S SSSSSSSQ:::::::QQQ:::::::QLL:::::::LL
|
|
8
|
+
J:::::J S:::::S Q::::::O Q::::::Q L:::::L nnnn nnnnnnnn eeeeeeeeeeee ooooooooooo
|
|
9
|
+
J:::::J S:::::S Q:::::O Q:::::Q L:::::L n:::nn::::::::nn ee::::::::::::ee oo:::::::::::oo
|
|
10
|
+
J:::::J S::::SSSS Q:::::O Q:::::Q L:::::L n::::::::::::::nn e::::::eeeee:::::eeo:::::::::::::::o
|
|
11
|
+
J:::::j SS::::::SSSSS Q:::::O Q:::::Q L:::::L --------------- nn:::::::::::::::ne::::::e e:::::eo:::::ooooo:::::o
|
|
12
|
+
J:::::J SSS::::::::SS Q:::::O Q:::::Q L:::::L -:::::::::::::- n:::::nnnn:::::ne:::::::eeeee::::::eo::::o o::::o
|
|
13
|
+
JJJJJJJ J:::::J SSSSSS::::S Q:::::O Q:::::Q L:::::L --------------- n::::n n::::ne:::::::::::::::::e o::::o o::::o
|
|
14
|
+
J:::::J J:::::J S:::::SQ:::::O QQQQ:::::Q L:::::L n::::n n::::ne::::::eeeeeeeeeee o::::o o::::o
|
|
15
|
+
J::::::J J::::::J S:::::SQ::::::O Q::::::::Q L:::::L LLLLLL n::::n n::::ne:::::::e o::::o o::::o
|
|
16
|
+
J:::::::JJJ:::::::J SSSSSSS S:::::SQ:::::::QQ::::::::QLL:::::::LLLLLLLLL:::::L n::::n n::::ne::::::::e o:::::ooooo:::::o
|
|
17
|
+
JJ:::::::::::::JJ S::::::SSSSSS:::::S QQ::::::::::::::Q L::::::::::::::::::::::L n::::n n::::n e::::::::eeeeeeee o:::::::::::::::o
|
|
18
|
+
JJ:::::::::JJ S:::::::::::::::SS QQ:::::::::::Q L::::::::::::::::::::::L n::::n n::::n ee:::::::::::::e oo:::::::::::oo
|
|
19
|
+
JJJJJJJJJ SSSSSSSSSSSSSSS QQQQQQQQ::::QQLLLLLLLLLLLLLLLLLLLLLLLL nnnnnn nnnnnn eeeeeeeeeeeeee ooooooooooo
|
|
20
|
+
Q:::::Q
|
|
21
|
+
QQQQQQ
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
# JSQL-NEO
|
|
25
|
+
|
|
26
|
+
**Pure JavaScript Embedded Database — SQL 风格 API,零原生依赖,JSON 文件存储**
|
|
27
|
+
|
|
28
|
+
[](https://www.npmjs.com/package/jsql-neo)
|
|
29
|
+
[](https://github.com/vexify-org/jsql-neo/blob/main/LICENSE)
|
|
30
|
+
[](https://nodejs.org/)
|
|
31
|
+
|
|
32
|
+
> **v2.0.0** — B-Tree 索引、哈希 JOIN、WAL 日志、事务隔离、MySQL 风格错误码
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 快速开始
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install jsql-neo
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
const jsql = require('jsql-neo');
|
|
44
|
+
|
|
45
|
+
// 内存模式
|
|
46
|
+
const db = new jsql.Database(':memory:');
|
|
47
|
+
|
|
48
|
+
// 文件模式(带 WAL + 文件锁)
|
|
49
|
+
const db = new jsql.Database('mydb.json', {
|
|
50
|
+
wal: true,
|
|
51
|
+
fileLock: true,
|
|
52
|
+
isolationLevel: 'REPEATABLE_READ',
|
|
53
|
+
slowQueryThreshold: 100
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 创建表(自动为 primaryKey 和 unique 字段建 B-Tree 索引)
|
|
57
|
+
db.createTable('users', {
|
|
58
|
+
id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
59
|
+
name: { type: 'string', required: true, length: 50 },
|
|
60
|
+
email: { type: 'string', unique: true },
|
|
61
|
+
age: { type: 'integer', min: 0, max: 150 },
|
|
62
|
+
createdAt: { type: 'datetime', default: 'CURRENT_TIMESTAMP' }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 插入
|
|
66
|
+
db.users.insert({ name: 'Alice', email: 'alice@example.com', age: 25 });
|
|
67
|
+
|
|
68
|
+
// 主键查询(B-Tree 加速,O(log n))
|
|
69
|
+
db.users.findById(1);
|
|
70
|
+
|
|
71
|
+
// 链式查询
|
|
72
|
+
db.users.where({ age: { $gte: 18 } })
|
|
73
|
+
.orderBy('name')
|
|
74
|
+
.limit(10)
|
|
75
|
+
.get();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## v2.0.0 核心特性
|
|
81
|
+
|
|
82
|
+
### 🔴 B-Tree 索引引擎
|
|
83
|
+
|
|
84
|
+
自动为 `primaryKey` 和 `unique` 字段创建 B-Tree 索引,查询复杂度从 O(n) 降至 O(log n)。
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
// 自动 B-Tree(无需手动创建)
|
|
88
|
+
db.createTable('users', {
|
|
89
|
+
id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
90
|
+
email: { type: 'string', unique: true }
|
|
91
|
+
});
|
|
92
|
+
// → 自动为 id 和 email 字段建 B-Tree
|
|
93
|
+
|
|
94
|
+
// 手动创建 B-Tree 索引
|
|
95
|
+
db.users.createBTreeIndex('name');
|
|
96
|
+
|
|
97
|
+
// 范围查询(B-Tree 加速)
|
|
98
|
+
db.users.where({ age: { $gte: 18, $lte: 60 } }).get();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 🔴 事务隔离
|
|
102
|
+
|
|
103
|
+
支持 `READ_COMMITTED` 和 `REPEATABLE_READ` 两级隔离。
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
// REPEATABLE_READ:快照隔离
|
|
107
|
+
db.begin('REPEATABLE_READ');
|
|
108
|
+
db.users.insert({ name: 'Tx', email: 'tx@test.com' });
|
|
109
|
+
// ... 其他操作看到的是事务开始时的快照
|
|
110
|
+
db.rollback(); // 回滚所有变更
|
|
111
|
+
|
|
112
|
+
// READ_COMMITTED(默认)
|
|
113
|
+
db.begin();
|
|
114
|
+
db.users.insert({ name: 'Tx2', email: 'tx2@test.com' });
|
|
115
|
+
db.commit();
|
|
116
|
+
|
|
117
|
+
// 动态切换隔离级别
|
|
118
|
+
db.setIsolationLevel('REPEATABLE_READ');
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 🔴 文件锁 + WAL
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const db = new jsql.Database('mydb.json', {
|
|
125
|
+
wal: true, // Write-Ahead Logging,崩溃恢复
|
|
126
|
+
fileLock: true // 防止多进程同时写入
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 🟡 哈希 JOIN 优化
|
|
131
|
+
|
|
132
|
+
数据量 > 100 行时自动使用哈希 JOIN(O(n+m)),小数据量自动退化为嵌套循环。
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// JOIN 支持字符串表名(v2.0 新增)
|
|
136
|
+
db.posts.where({})
|
|
137
|
+
.join('users', 'userId', 'id', 'author')
|
|
138
|
+
.get();
|
|
139
|
+
|
|
140
|
+
// LEFT JOIN
|
|
141
|
+
db.posts.where({})
|
|
142
|
+
.leftJoin('users', 'userId', 'id', 'author')
|
|
143
|
+
.get();
|
|
144
|
+
|
|
145
|
+
// 手动指定 JOIN 策略
|
|
146
|
+
db.posts.where({})
|
|
147
|
+
.useHashJoin() // 强制哈希 JOIN
|
|
148
|
+
.join('users', 'userId', 'id', 'author')
|
|
149
|
+
.get();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 🟡 外键约束
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
db.createTable('posts', {
|
|
156
|
+
id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
157
|
+
title: { type: 'string' },
|
|
158
|
+
userId: { type: 'integer', foreignKey: {
|
|
159
|
+
table: 'users',
|
|
160
|
+
field: 'id',
|
|
161
|
+
onDelete: 'cascade' // cascade | set null | restrict
|
|
162
|
+
} }
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 删除用户时自动级联删除其帖子
|
|
166
|
+
db.users.remove({ id: 1 });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 🟡 MySQL 风格错误码
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
try {
|
|
173
|
+
db.users.insert({ email: 'alice@example.com' }); // 重复
|
|
174
|
+
} catch (e) {
|
|
175
|
+
e.code; // 1062 (ER_DUP_ENTRY)
|
|
176
|
+
e.message; // "Duplicate entry 'alice@example.com' for key 'email'"
|
|
177
|
+
e.toJSON(); // { error: true, code: 1062, message: '...', details: {...} }
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
错误码速查:
|
|
182
|
+
|
|
183
|
+
| 错误码 | 名称 | 含义 |
|
|
184
|
+
|--------|------|------|
|
|
185
|
+
| 1048 | ER_BAD_NULL_ERROR | 列不能为 NULL |
|
|
186
|
+
| 1062 | ER_DUP_ENTRY | 唯一键冲突 |
|
|
187
|
+
| 1216 | ER_NO_REFERENCED_ROW | 外键约束失败 |
|
|
188
|
+
| 1292 | ER_TRUNCATED_WRONG_VALUE | 数据类型错误 |
|
|
189
|
+
| 1406 | ER_DATA_TOO_LONG | 数据超长 |
|
|
190
|
+
| 3819 | ER_CHECK_CONSTRAINT | CHECK 约束违反 |
|
|
191
|
+
| 1568 | ER_TRANSACTION_ACTIVE | 事务已在进行中 |
|
|
192
|
+
| 1569 | ER_NO_TRANSACTION | 无活跃事务 |
|
|
193
|
+
|
|
194
|
+
### 🟡 日期类型
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
db.createTable('events', {
|
|
198
|
+
id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
199
|
+
eventDate: { type: 'date' }, // YYYY-MM-DD
|
|
200
|
+
eventTime: { type: 'datetime' }, // YYYY-MM-DD HH:MM:SS
|
|
201
|
+
timestamp: { type: 'timestamp' }, // Unix 时间戳
|
|
202
|
+
duration: { type: 'time' } // HH:MM:SS
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 🟢 慢查询日志
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
const db = new jsql.Database('mydb.json', { slowQueryThreshold: 50 });
|
|
210
|
+
// 所有 > 50ms 的查询自动记录
|
|
211
|
+
|
|
212
|
+
// 查看慢查询
|
|
213
|
+
db.getSlowQueries(20); // 最近 20 条
|
|
214
|
+
|
|
215
|
+
// 清空日志
|
|
216
|
+
db.clearSlowQueries();
|
|
217
|
+
|
|
218
|
+
// 动态调整阈值
|
|
219
|
+
db.setSlowQueryThreshold(200);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 完整 API
|
|
225
|
+
|
|
226
|
+
### Database
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
const db = new jsql.Database(filePath, options);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
233
|
+
|------|------|--------|------|
|
|
234
|
+
| `autoSave` | boolean | true | 自动保存 |
|
|
235
|
+
| `autoSaveInterval` | number | 0 | 自动保存间隔(ms) |
|
|
236
|
+
| `pretty` | boolean | true | JSON 格式化 |
|
|
237
|
+
| `encryptKey` | string | null | AES-256-CBC 加密密钥 |
|
|
238
|
+
| `versioning` | boolean | false | 版本历史 |
|
|
239
|
+
| `wal` | boolean | false | WAL 模式 |
|
|
240
|
+
| `fileLock` | boolean | false | 文件锁 |
|
|
241
|
+
| `isolationLevel` | string | 'READ_COMMITTED' | 事务隔离级别 |
|
|
242
|
+
| `slowQueryThreshold` | number | 100 | 慢查询阈值(ms),0 禁用 |
|
|
243
|
+
|
|
244
|
+
#### 表管理
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
db.createTable(name, schema) // 创建表
|
|
248
|
+
db.dropTable(name) // 删除表
|
|
249
|
+
db.hasTable(name) // 是否存在
|
|
250
|
+
db.getTables() // 获取所有表名
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### 事务
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
db.begin(isolationLevel?) // 开始事务
|
|
257
|
+
db.commit() // 提交
|
|
258
|
+
db.rollback() // 回滚
|
|
259
|
+
db.inTransaction() // 是否在事务中
|
|
260
|
+
db.getIsolationLevel() // 获取隔离级别
|
|
261
|
+
db.setIsolationLevel(level) // 设置隔离级别
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### 视图
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
db.createView('adults', db => db.users.where({ age: { $gte: 18 } }));
|
|
268
|
+
db.dropView('adults');
|
|
269
|
+
db.getViews();
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### 触发器
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
db.createTrigger('log_insert', {
|
|
276
|
+
event: 'insert',
|
|
277
|
+
table: 'users',
|
|
278
|
+
timing: 'after'
|
|
279
|
+
}, (data) => { console.log('New user:', data); });
|
|
280
|
+
|
|
281
|
+
db.dropTrigger('log_insert');
|
|
282
|
+
db.getTriggers();
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### 备份 / 恢复
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
db.backup('backup.json') // 备份到文件
|
|
289
|
+
db.restore('backup.json') // 从文件恢复
|
|
290
|
+
db.export() // 导出为 JSON 对象
|
|
291
|
+
db.import(data) // 导入 JSON 对象
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### 统计 & 监控
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
db.stats() // 数据库统计
|
|
298
|
+
db.getSlowQueries() // 慢查询日志
|
|
299
|
+
db.clearSlowQueries() // 清空慢查询日志
|
|
300
|
+
db.setSlowQueryThreshold(ms) // 设置慢查询阈值
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### 插件 & 迁移
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
db.use(plugin) // 注册插件
|
|
307
|
+
db.addMigration({ version, name, up, down }) // 注册迁移
|
|
308
|
+
db.migrate(version) // 执行迁移
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### 变更监听
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
const unsubscribe = db.onChange(event => {
|
|
315
|
+
console.log(event.type, event.table, event.data);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// 变更流(异步迭代器)
|
|
319
|
+
const stream = db.createChangeStream();
|
|
320
|
+
for await (const event of stream) { /* ... */ }
|
|
321
|
+
stream.close();
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
#### 多数据库
|
|
325
|
+
|
|
326
|
+
```javascript
|
|
327
|
+
db.attach('other', otherDb) // 附加数据库
|
|
328
|
+
db.detach('other') // 分离
|
|
329
|
+
db.getAttached() // 查看已附加的数据库
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Table
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
// 插入
|
|
336
|
+
table.insert(data) // 插入单行
|
|
337
|
+
table.insertMany([data, ...]) // 批量插入
|
|
338
|
+
table.upsert(data) // 存在则更新,否则插入
|
|
339
|
+
|
|
340
|
+
// 查询
|
|
341
|
+
table.find(query) // 条件查询
|
|
342
|
+
table.findOne(query) // 查单条
|
|
343
|
+
table.findById(id) // 主键查询(B-Tree 加速)
|
|
344
|
+
table.findAll() // 全表查询
|
|
345
|
+
table.count(query) // 计数
|
|
346
|
+
table.where(conditions) // 返回 Query 链式构建器
|
|
347
|
+
|
|
348
|
+
// 更新
|
|
349
|
+
table.update(query, updates) // 条件更新
|
|
350
|
+
table.updateById(id, updates) // 主键更新
|
|
351
|
+
|
|
352
|
+
// 删除
|
|
353
|
+
table.remove(query) // 条件删除
|
|
354
|
+
|
|
355
|
+
// 索引
|
|
356
|
+
table.createBTreeIndex(field) // 创建 B-Tree 索引
|
|
357
|
+
table.createIndex(field) // 创建 Hash 索引
|
|
358
|
+
table.dropIndex(field) // 删除索引
|
|
359
|
+
|
|
360
|
+
// Schema
|
|
361
|
+
table.addColumn(name, def) // 添加列
|
|
362
|
+
table.dropColumn(name) // 删除列
|
|
363
|
+
table.renameColumn(old, new) // 重命名列
|
|
364
|
+
|
|
365
|
+
// 钩子
|
|
366
|
+
table.on('beforeInsert', fn) // 注册钩子
|
|
367
|
+
table.off('beforeInsert', fn) // 移除钩子
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Query
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
db.users.where({ age: { $gte: 18 } }) // 条件
|
|
374
|
+
.select(['name', 'age']) // 字段选择
|
|
375
|
+
.orderBy('name') // 排序
|
|
376
|
+
.orderByDesc('age') // 降序
|
|
377
|
+
.limit(10) // 限制行数
|
|
378
|
+
.offset(20) // 偏移
|
|
379
|
+
.distinct('age') // 去重
|
|
380
|
+
.groupBy('city') // 分组
|
|
381
|
+
.having({ count: { $gt: 5 } }) // 分组过滤
|
|
382
|
+
.join('posts', 'id', 'userId') // INNER JOIN
|
|
383
|
+
.leftJoin('posts', 'id', 'userId') // LEFT JOIN
|
|
384
|
+
.rightJoin('posts', 'id', 'userId') // RIGHT JOIN
|
|
385
|
+
.union(otherQuery) // UNION
|
|
386
|
+
.window('rowNumber', 'name') // 窗口函数
|
|
387
|
+
.case([{ when: { age: { $lt: 18 } }, then: 'minor' }], 'adult')
|
|
388
|
+
.as('ageGroup') // CASE WHEN
|
|
389
|
+
.cache(30000) // 查询缓存
|
|
390
|
+
.explain() // 执行计划
|
|
391
|
+
.toSQL() // 导出 SQL 字符串
|
|
392
|
+
.useHashJoin() // 强制哈希 JOIN
|
|
393
|
+
.useNestedLoop() // 强制嵌套循环
|
|
394
|
+
|
|
395
|
+
// 执行
|
|
396
|
+
.get() // 获取结果
|
|
397
|
+
.first() // 第一条
|
|
398
|
+
.count() // 计数
|
|
399
|
+
.sum('age') // 求和
|
|
400
|
+
.avg('age') // 平均
|
|
401
|
+
.min('age') // 最小
|
|
402
|
+
.max('age') // 最大
|
|
403
|
+
.groupStats('age') // 分组统计
|
|
404
|
+
.paginate(1, 20) // 分页
|
|
405
|
+
.update({ name: 'New' }) // 条件更新
|
|
406
|
+
.remove() // 条件删除
|
|
407
|
+
.invalidateCache() // 清除缓存
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Schema 定义
|
|
411
|
+
|
|
412
|
+
```javascript
|
|
413
|
+
{
|
|
414
|
+
fieldName: {
|
|
415
|
+
type: 'integer' | 'string' | 'number' | 'boolean' | 'array' | 'object' |
|
|
416
|
+
'date' | 'datetime' | 'timestamp' | 'time' | 'any',
|
|
417
|
+
primaryKey: true,
|
|
418
|
+
autoIncrement: true,
|
|
419
|
+
required: true,
|
|
420
|
+
unique: true,
|
|
421
|
+
default: '默认值',
|
|
422
|
+
length: 50, // 字符串最大长度
|
|
423
|
+
min: 0, // 数值最小值
|
|
424
|
+
max: 150, // 数值最大值
|
|
425
|
+
check: v => v >= 18, // 自定义校验函数
|
|
426
|
+
foreignKey: {
|
|
427
|
+
table: 'users',
|
|
428
|
+
field: 'id',
|
|
429
|
+
onDelete: 'cascade', // cascade | set null | restrict
|
|
430
|
+
onUpdate: 'restrict'
|
|
431
|
+
},
|
|
432
|
+
computed: row => `prefix_${row.id}` // 计算字段
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### 操作符
|
|
438
|
+
|
|
439
|
+
| 操作符 | 说明 | 示例 |
|
|
440
|
+
|--------|------|------|
|
|
441
|
+
| `$eq` | 等于 | `{ age: { $eq: 25 } }` |
|
|
442
|
+
| `$ne` | 不等于 | `{ age: { $ne: 25 } }` |
|
|
443
|
+
| `$gt` | 大于 | `{ age: { $gt: 18 } }` |
|
|
444
|
+
| `$gte` | 大于等于 | `{ age: { $gte: 18 } }` |
|
|
445
|
+
| `$lt` | 小于 | `{ age: { $lt: 60 } }` |
|
|
446
|
+
| `$lte` | 小于等于 | `{ age: { $lte: 60 } }` |
|
|
447
|
+
| `$in` | 在列表中 | `{ status: { $in: ['a', 'b'] } }` |
|
|
448
|
+
| `$nin` | 不在列表中 | `{ status: { $nin: ['x', 'y'] } }` |
|
|
449
|
+
| `$like` | 模糊匹配 | `{ name: { $like: 'Ali' } }` |
|
|
450
|
+
| `$regex` | 正则匹配 | `{ email: { $regex: /@gmail\.com$/ } }` |
|
|
451
|
+
| `$between` | 区间 | `{ age: { $between: [18, 60] } }` |
|
|
452
|
+
| `$null` | 是否为 null | `{ deletedAt: { $null: true } }` |
|
|
453
|
+
| `$notNull` | 是否非 null | `{ name: { $notNull: true } }` |
|
|
454
|
+
| `$or` | 或 | `{ $or: [{age: 18}, {age: 21}] }` |
|
|
455
|
+
| `$and` | 且 | `{ $and: [{age: {$gte: 18}}, {age: {$lte: 60}}] }` |
|
|
456
|
+
| `$inSub` | 子查询 IN | `{ $inSub: { query: q, field: 'id' } }` |
|
|
457
|
+
| `$notInSub` | 子查询 NOT IN | `{ $notInSub: { query: q, field: 'id' } }` |
|
|
458
|
+
| `$expr` | 表达式 | `{ $expr: row => row.a > row.b }` |
|
|
459
|
+
|
|
460
|
+
### EXPLAIN 输出示例
|
|
461
|
+
|
|
462
|
+
```javascript
|
|
463
|
+
db.users.where({ id: 3 }).explain().get();
|
|
464
|
+
// {
|
|
465
|
+
// table: 'users',
|
|
466
|
+
// totalRows: 5000,
|
|
467
|
+
// filteredRows: 1,
|
|
468
|
+
// selectivity: '0.02%',
|
|
469
|
+
// hasBTree: true,
|
|
470
|
+
// indexUsed: 'id',
|
|
471
|
+
// btreeIndexes: ['id', 'email'],
|
|
472
|
+
// estimatedCost: 0.05,
|
|
473
|
+
// joins: []
|
|
474
|
+
// }
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## 内存模式 vs 文件模式
|
|
480
|
+
|
|
481
|
+
| 特性 | 内存模式 | 文件模式 |
|
|
482
|
+
|------|----------|----------|
|
|
483
|
+
| 初始化 | `new Database(':memory:')` | `new Database('db.json')` |
|
|
484
|
+
| 持久化 | 无 | 自动保存 |
|
|
485
|
+
| B-Tree 索引 | ✅ | ✅ |
|
|
486
|
+
| 事务隔离 | ✅ | ✅ |
|
|
487
|
+
| WAL | ❌(自动禁用) | ✅ |
|
|
488
|
+
| 文件锁 | ❌(自动禁用) | ✅ |
|
|
489
|
+
| 加密存储 | ❌ | ✅ |
|
|
490
|
+
| 慢查询日志 | ✅ | ✅ |
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## 与 MySQL 对比
|
|
495
|
+
|
|
496
|
+
| 特性 | JSQL-NEO v2.0 | MySQL 8.0 |
|
|
497
|
+
|------|---------------|-----------|
|
|
498
|
+
| 索引 | B-Tree(O(log n)) | B-Tree(O(log n)) |
|
|
499
|
+
| 事务隔离 | READ_COMMITTED / REPEATABLE_READ | 4 级 |
|
|
500
|
+
| 并发控制 | 文件锁 | MVCC + 锁 |
|
|
501
|
+
| 外键 | CASCADE / SET NULL / RESTRICT | ✅ |
|
|
502
|
+
| 错误码 | 25+ MySQL 兼容 | 完整 |
|
|
503
|
+
| JOIN | 哈希 / 嵌套循环 | 基于代价 |
|
|
504
|
+
| 子查询 | ✅ | ✅ |
|
|
505
|
+
| 窗口函数 | ROW_NUMBER / RANK / DENSE_RANK | 完整 |
|
|
506
|
+
| 存储引擎 | JSON 文件 | InnoDB / MyISAM |
|
|
507
|
+
| 零依赖 | ✅ | ❌ |
|
|
508
|
+
| 嵌入 Node.js | ✅ | ❌(需驱动) |
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## 许可
|
|
513
|
+
|
|
514
|
+
Apache License 2.0 — © Vexify 2026
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// © Vexify 2026 All Rights Reserved.
|
|
2
2
|
/**
|
|
3
3
|
* JSQL-NEO — Pure JavaScript Embedded Database
|
|
4
|
-
* v2.
|
|
4
|
+
* v2.1.0 — B-Tree 索引、WAL、哈希 JOIN、Redis 兼容 Cache、事务隔离
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* const jsql = require('jsql-neo');
|
|
@@ -13,6 +13,7 @@ module.exports = {
|
|
|
13
13
|
Table: require('./lib/table'),
|
|
14
14
|
Query: require('./lib/query'),
|
|
15
15
|
BTree: require('./lib/btree'),
|
|
16
|
+
Cache: require('./lib/cache'),
|
|
16
17
|
JSQL_Error: require('./lib/errors').JSQL_Error,
|
|
17
18
|
ErrorCodes: require('./lib/errors').ErrorCodes
|
|
18
19
|
};
|
package/lib/btree.js
CHANGED
|
@@ -241,8 +241,9 @@ class BTree {
|
|
|
241
241
|
return true;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
const origKey = node.keys[index];
|
|
244
245
|
this._mergeChildren(node, index);
|
|
245
|
-
return this._removeFromNode(node.children[index],
|
|
246
|
+
return this._removeFromNode(node.children[index], origKey, rowIndex);
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
_getMax(node) {
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class Cache {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this._data = new Map();
|
|
7
|
+
this._expires = new Map();
|
|
8
|
+
this._ttlTimer = null;
|
|
9
|
+
this._db = 0;
|
|
10
|
+
this._dbs = [this._data];
|
|
11
|
+
this._dbsExpires = [this._expires];
|
|
12
|
+
this._persistPath = options.path || null;
|
|
13
|
+
this._saveInterval = options.saveInterval || 0;
|
|
14
|
+
this._dirty = false;
|
|
15
|
+
if (this._persistPath) this._load();
|
|
16
|
+
if (this._saveInterval > 0) this._startAutoSave();
|
|
17
|
+
this._startTTLCleaner();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
set(key, value) { this._getData().set(String(key), String(value)); this._dirty = true; return 'OK'; }
|
|
21
|
+
get(key) {
|
|
22
|
+
const k = String(key);
|
|
23
|
+
if (this._isExpired(k)) return null;
|
|
24
|
+
const val = this._getData().get(k);
|
|
25
|
+
return val !== undefined ? val : null;
|
|
26
|
+
}
|
|
27
|
+
del(...keys) {
|
|
28
|
+
let count = 0;
|
|
29
|
+
for (const key of keys) { const k = String(key); if (this._getData().delete(k)) count++; this._getExpires().delete(k); }
|
|
30
|
+
if (count > 0) this._dirty = true;
|
|
31
|
+
return count;
|
|
32
|
+
}
|
|
33
|
+
exists(...keys) {
|
|
34
|
+
let count = 0;
|
|
35
|
+
for (const key of keys) { const k = String(key); if (!this._isExpired(k) && this._getData().has(k)) count++; }
|
|
36
|
+
return count;
|
|
37
|
+
}
|
|
38
|
+
keys(pattern = '*') {
|
|
39
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const key of this._getData().keys()) { if (!this._isExpired(key) && regex.test(key)) result.push(key); }
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
type(key) {
|
|
45
|
+
const k = String(key);
|
|
46
|
+
if (this._isExpired(k)) return 'none';
|
|
47
|
+
const val = this._getData().get(k);
|
|
48
|
+
if (val === undefined) return 'none';
|
|
49
|
+
if (typeof val === 'string') return 'string';
|
|
50
|
+
if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
|
|
51
|
+
if (val instanceof Set) return 'set';
|
|
52
|
+
return 'string';
|
|
53
|
+
}
|
|
54
|
+
expire(key, seconds) {
|
|
55
|
+
const k = String(key);
|
|
56
|
+
if (!this._getData().has(k)) return 0;
|
|
57
|
+
this._getExpires().set(k, Date.now() + seconds * 1000);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
ttl(key) {
|
|
61
|
+
const k = String(key);
|
|
62
|
+
if (!this._getData().has(k)) return -2;
|
|
63
|
+
const expiry = this._getExpires().get(k);
|
|
64
|
+
if (expiry === undefined) return -1;
|
|
65
|
+
const remaining = Math.ceil((expiry - Date.now()) / 1000);
|
|
66
|
+
return remaining > 0 ? remaining : -2;
|
|
67
|
+
}
|
|
68
|
+
persist(key) { return this._getExpires().delete(String(key)) ? 1 : 0; }
|
|
69
|
+
incr(key) { return this._incrby(key, 1); }
|
|
70
|
+
decr(key) { return this._incrby(key, -1); }
|
|
71
|
+
incrby(key, increment) { return this._incrby(key, increment); }
|
|
72
|
+
decrby(key, decrement) { return this._incrby(key, -decrement); }
|
|
73
|
+
_incrby(key, delta) {
|
|
74
|
+
const k = String(key);
|
|
75
|
+
const cur = this._getData().get(k);
|
|
76
|
+
const val = (cur !== undefined ? Number(cur) : 0) + delta;
|
|
77
|
+
if (!Number.isFinite(val)) throw new Error('value is not an integer or out of range');
|
|
78
|
+
this._getData().set(k, String(val));
|
|
79
|
+
this._dirty = true;
|
|
80
|
+
return val;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
hset(key, field, value) {
|
|
84
|
+
const k = String(key);
|
|
85
|
+
let hash = this._getData().get(k);
|
|
86
|
+
if (!hash || hash._type !== 'hash') { hash = { _type: 'hash' }; this._getData().set(k, hash); }
|
|
87
|
+
const existed = hash[field] !== undefined;
|
|
88
|
+
hash[field] = String(value);
|
|
89
|
+
this._dirty = true;
|
|
90
|
+
return existed ? 0 : 1;
|
|
91
|
+
}
|
|
92
|
+
hget(key, field) {
|
|
93
|
+
const k = String(key);
|
|
94
|
+
if (this._isExpired(k)) return null;
|
|
95
|
+
const hash = this._getData().get(k);
|
|
96
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return null;
|
|
97
|
+
return hash[field] !== undefined ? hash[field] : null;
|
|
98
|
+
}
|
|
99
|
+
hgetall(key) {
|
|
100
|
+
const k = String(key);
|
|
101
|
+
if (this._isExpired(k)) return null;
|
|
102
|
+
const hash = this._getData().get(k);
|
|
103
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return null;
|
|
104
|
+
const result = {};
|
|
105
|
+
for (const [f, v] of Object.entries(hash)) { if (f !== '_type') result[f] = v; }
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
hdel(key, ...fields) {
|
|
109
|
+
const k = String(key);
|
|
110
|
+
const hash = this._getData().get(k);
|
|
111
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return 0;
|
|
112
|
+
let count = 0;
|
|
113
|
+
for (const field of fields) { if (delete hash[field]) count++; }
|
|
114
|
+
if (count > 0) this._dirty = true;
|
|
115
|
+
return count;
|
|
116
|
+
}
|
|
117
|
+
hkeys(key) {
|
|
118
|
+
const k = String(key);
|
|
119
|
+
if (this._isExpired(k)) return [];
|
|
120
|
+
const hash = this._getData().get(k);
|
|
121
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return [];
|
|
122
|
+
return Object.keys(hash).filter(f => f !== '_type');
|
|
123
|
+
}
|
|
124
|
+
hlen(key) {
|
|
125
|
+
const k = String(key);
|
|
126
|
+
if (this._isExpired(k)) return 0;
|
|
127
|
+
const hash = this._getData().get(k);
|
|
128
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return 0;
|
|
129
|
+
return Object.keys(hash).filter(f => f !== '_type').length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lpush(key, ...values) {
|
|
133
|
+
const k = String(key);
|
|
134
|
+
let list = this._getData().get(k);
|
|
135
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
|
|
136
|
+
list.unshift(...values.map(String).reverse());
|
|
137
|
+
this._dirty = true;
|
|
138
|
+
return list.length;
|
|
139
|
+
}
|
|
140
|
+
rpush(key, ...values) {
|
|
141
|
+
const k = String(key);
|
|
142
|
+
let list = this._getData().get(k);
|
|
143
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
|
|
144
|
+
list.push(...values.map(String));
|
|
145
|
+
this._dirty = true;
|
|
146
|
+
return list.length;
|
|
147
|
+
}
|
|
148
|
+
lpop(key) {
|
|
149
|
+
const k = String(key);
|
|
150
|
+
const list = this._getData().get(k);
|
|
151
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') return null;
|
|
152
|
+
if (list.length === 0) return null;
|
|
153
|
+
this._dirty = true;
|
|
154
|
+
return list.shift();
|
|
155
|
+
}
|
|
156
|
+
rpop(key) {
|
|
157
|
+
const k = String(key);
|
|
158
|
+
const list = this._getData().get(k);
|
|
159
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') return null;
|
|
160
|
+
if (list.length === 0) return null;
|
|
161
|
+
this._dirty = true;
|
|
162
|
+
return list.pop();
|
|
163
|
+
}
|
|
164
|
+
llen(key) {
|
|
165
|
+
const k = String(key);
|
|
166
|
+
if (this._isExpired(k)) return 0;
|
|
167
|
+
const list = this._getData().get(k);
|
|
168
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') return 0;
|
|
169
|
+
return list.length;
|
|
170
|
+
}
|
|
171
|
+
lrange(key, start, stop) {
|
|
172
|
+
const k = String(key);
|
|
173
|
+
if (this._isExpired(k)) return [];
|
|
174
|
+
const list = this._getData().get(k);
|
|
175
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') return [];
|
|
176
|
+
if (start < 0) start = Math.max(list.length + start, 0);
|
|
177
|
+
if (stop < 0) stop = list.length + stop;
|
|
178
|
+
return list.slice(start, stop + 1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
sadd(key, ...members) {
|
|
182
|
+
const k = String(key);
|
|
183
|
+
let set = this._getData().get(k);
|
|
184
|
+
if (!set || !(set instanceof Set)) { set = new Set(); this._getData().set(k, set); }
|
|
185
|
+
let count = 0;
|
|
186
|
+
for (const m of members) { if (!set.has(String(m))) { set.add(String(m)); count++; } }
|
|
187
|
+
if (count > 0) this._dirty = true;
|
|
188
|
+
return count;
|
|
189
|
+
}
|
|
190
|
+
srem(key, ...members) {
|
|
191
|
+
const k = String(key);
|
|
192
|
+
const set = this._getData().get(k);
|
|
193
|
+
if (!set || !(set instanceof Set)) return 0;
|
|
194
|
+
let count = 0;
|
|
195
|
+
for (const m of members) { if (set.delete(String(m))) count++; }
|
|
196
|
+
if (count > 0) this._dirty = true;
|
|
197
|
+
return count;
|
|
198
|
+
}
|
|
199
|
+
smembers(key) {
|
|
200
|
+
const k = String(key);
|
|
201
|
+
if (this._isExpired(k)) return [];
|
|
202
|
+
const set = this._getData().get(k);
|
|
203
|
+
if (!set || !(set instanceof Set)) return [];
|
|
204
|
+
return [...set];
|
|
205
|
+
}
|
|
206
|
+
sismember(key, member) {
|
|
207
|
+
const k = String(key);
|
|
208
|
+
if (this._isExpired(k)) return 0;
|
|
209
|
+
const set = this._getData().get(k);
|
|
210
|
+
if (!set || !(set instanceof Set)) return 0;
|
|
211
|
+
return set.has(String(member)) ? 1 : 0;
|
|
212
|
+
}
|
|
213
|
+
scard(key) {
|
|
214
|
+
const k = String(key);
|
|
215
|
+
if (this._isExpired(k)) return 0;
|
|
216
|
+
const set = this._getData().get(k);
|
|
217
|
+
if (!set || !(set instanceof Set)) return 0;
|
|
218
|
+
return set.size;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
flushall() {
|
|
222
|
+
for (let i = 0; i < this._dbs.length; i++) { if (this._dbs[i]) this._dbs[i].clear(); if (this._dbsExpires[i]) this._dbsExpires[i].clear(); }
|
|
223
|
+
this._data = this._dbs[0] = new Map();
|
|
224
|
+
this._expires = this._dbsExpires[0] = new Map();
|
|
225
|
+
this._db = 0;
|
|
226
|
+
this._dirty = true;
|
|
227
|
+
return 'OK';
|
|
228
|
+
}
|
|
229
|
+
flushdb() { this._getData().clear(); this._getExpires().clear(); this._dirty = true; return 'OK'; }
|
|
230
|
+
dbsize() {
|
|
231
|
+
let count = 0;
|
|
232
|
+
for (const key of this._getData().keys()) { if (!this._isExpired(key)) count++; }
|
|
233
|
+
return count;
|
|
234
|
+
}
|
|
235
|
+
select(index) {
|
|
236
|
+
const idx = Number(index);
|
|
237
|
+
if (idx < 0 || idx > 15) throw new Error('DB index out of range');
|
|
238
|
+
this._db = idx;
|
|
239
|
+
if (!this._dbs[idx]) { this._dbs[idx] = new Map(); this._dbsExpires[idx] = new Map(); }
|
|
240
|
+
this._data = this._dbs[idx];
|
|
241
|
+
this._expires = this._dbsExpires[idx];
|
|
242
|
+
return 'OK';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
save() {
|
|
246
|
+
if (!this._persistPath) return;
|
|
247
|
+
const dump = { version: 1, databases: [] };
|
|
248
|
+
for (let i = 0; i < this._dbs.length; i++) {
|
|
249
|
+
const db = this._dbs[i];
|
|
250
|
+
if (!db || db.size === 0) continue;
|
|
251
|
+
const data = {};
|
|
252
|
+
for (const [key, val] of db) {
|
|
253
|
+
const exp = this._dbsExpires[i];
|
|
254
|
+
if (exp && exp.has(key) && exp.get(key) <= Date.now()) continue;
|
|
255
|
+
if (val instanceof Set) data[key] = { _t: 'set', _v: [...val] };
|
|
256
|
+
else if (Array.isArray(val)) data[key] = { _t: 'list', _v: val };
|
|
257
|
+
else if (typeof val === 'object' && val._type === 'hash') {
|
|
258
|
+
const clean = {}; for (const [f, v] of Object.entries(val)) { if (f !== '_type') clean[f] = v; }
|
|
259
|
+
data[key] = { _t: 'hash', _v: clean };
|
|
260
|
+
} else data[key] = { _t: 'string', _v: val };
|
|
261
|
+
}
|
|
262
|
+
const expires = {};
|
|
263
|
+
const expMap = this._dbsExpires[i];
|
|
264
|
+
if (expMap) { for (const [key, ttl] of expMap) { if (ttl > Date.now()) expires[key] = ttl; } }
|
|
265
|
+
if (Object.keys(data).length > 0 || Object.keys(expires).length > 0) dump.databases.push({ index: i, data, expires });
|
|
266
|
+
}
|
|
267
|
+
const dir = path.dirname(this._persistPath);
|
|
268
|
+
if (dir) fs.mkdirSync(dir, { recursive: true });
|
|
269
|
+
fs.writeFileSync(this._persistPath + '.tmp', JSON.stringify(dump), 'utf8');
|
|
270
|
+
fs.renameSync(this._persistPath + '.tmp', this._persistPath);
|
|
271
|
+
this._dirty = false;
|
|
272
|
+
return 'OK';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_load() {
|
|
276
|
+
try {
|
|
277
|
+
if (!fs.existsSync(this._persistPath)) return;
|
|
278
|
+
const dump = JSON.parse(fs.readFileSync(this._persistPath, 'utf8'));
|
|
279
|
+
if (dump.version !== 1) return;
|
|
280
|
+
for (const db of dump.databases || []) {
|
|
281
|
+
const idx = db.index;
|
|
282
|
+
if (!this._dbs[idx]) { this._dbs[idx] = new Map(); this._dbsExpires[idx] = new Map(); }
|
|
283
|
+
const data = this._dbs[idx], expires = this._dbsExpires[idx];
|
|
284
|
+
for (const [key, entry] of Object.entries(db.data || {})) {
|
|
285
|
+
if (entry._t === 'set') data.set(key, new Set(entry._v));
|
|
286
|
+
else if (entry._t === 'list') data.set(key, entry._v);
|
|
287
|
+
else if (entry._t === 'hash') data.set(key, { _type: 'hash', ...entry._v });
|
|
288
|
+
else data.set(key, entry._v);
|
|
289
|
+
}
|
|
290
|
+
for (const [key, ttl] of Object.entries(db.expires || {})) { if (ttl > Date.now()) expires.set(key, ttl); }
|
|
291
|
+
}
|
|
292
|
+
this._data = this._dbs[this._db] || new Map();
|
|
293
|
+
this._expires = this._dbsExpires[this._db] || new Map();
|
|
294
|
+
} catch (_) { /* ignore */ }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_startAutoSave() { this._saveTimer = setInterval(() => { if (this._dirty) this.save(); }, this._saveInterval); }
|
|
298
|
+
_startTTLCleaner() {
|
|
299
|
+
this._ttlTimer = setInterval(() => {
|
|
300
|
+
for (let i = 0; i < this._dbs.length; i++) {
|
|
301
|
+
const exp = this._dbsExpires[i]; if (!exp) continue;
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
for (const [key, ttl] of exp) { if (ttl <= now) { if (this._dbs[i]) this._dbs[i].delete(key); exp.delete(key); } }
|
|
304
|
+
}
|
|
305
|
+
}, 1000);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
close() { if (this._persistPath && this._dirty) this.save(); if (this._saveTimer) clearInterval(this._saveTimer); if (this._ttlTimer) clearInterval(this._ttlTimer); }
|
|
309
|
+
_isExpired(key) {
|
|
310
|
+
const expiry = this._getExpires().get(key);
|
|
311
|
+
if (expiry !== undefined && expiry <= Date.now()) { this._getData().delete(key); this._getExpires().delete(key); return true; }
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
_getData() { return this._data; }
|
|
315
|
+
_getExpires() { return this._expires; }
|
|
316
|
+
|
|
317
|
+
static redisPlugin(db, options = {}) {
|
|
318
|
+
const cache = new Cache(options);
|
|
319
|
+
db._cache = cache;
|
|
320
|
+
const redisMethods = ['set','get','del','exists','keys','type','expire','ttl','persist','incr','decr','incrby','decrby','hset','hget','hgetall','hdel','hkeys','hlen','lpush','rpush','lpop','rpop','llen','lrange','sadd','srem','smembers','sismember','scard','flushall','flushdb','dbsize','select'];
|
|
321
|
+
for (const m of redisMethods) {
|
|
322
|
+
db[m] = cache[m].bind(cache);
|
|
323
|
+
}
|
|
324
|
+
return cache;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = Cache;
|
package/lib/database.js
CHANGED
|
@@ -21,7 +21,7 @@ class Database {
|
|
|
21
21
|
* @param {boolean} options.versioning - 是否启用版本历史(默认 false)
|
|
22
22
|
* @param {boolean} options.wal - 是否启用 WAL 模式(默认 false,内存模式自动禁用)
|
|
23
23
|
* @param {string} options.isolationLevel - 事务隔离级别 'READ_COMMITTED' | 'REPEATABLE_READ'(默认 'READ_COMMITTED')
|
|
24
|
-
* @param {number} options.slowQueryThreshold - 慢查询阈值 ms(默认 100,0 禁用)
|
|
24
|
+
* @param {number} options.slowQueryThreshold - 慢查询阈值 ms(默认 100,0 记录全部,<0 禁用)
|
|
25
25
|
* @param {boolean} options.fileLock - 是否启用文件锁(默认 false)
|
|
26
26
|
*/
|
|
27
27
|
constructor(filePath, options = {}) {
|
|
@@ -514,8 +514,8 @@ class Database {
|
|
|
514
514
|
* 记录慢查询
|
|
515
515
|
*/
|
|
516
516
|
_logSlowQuery(sql, durationMs, rowsReturned) {
|
|
517
|
-
if (this._slowQueryThreshold
|
|
518
|
-
if (durationMs < this._slowQueryThreshold) return;
|
|
517
|
+
if (this._slowQueryThreshold < 0) return;
|
|
518
|
+
if (this._slowQueryThreshold > 0 && durationMs < this._slowQueryThreshold) return;
|
|
519
519
|
|
|
520
520
|
this._slowQueries.push({
|
|
521
521
|
sql,
|
package/lib/query.js
CHANGED
|
@@ -223,6 +223,7 @@ class Query {
|
|
|
223
223
|
// ============================================================
|
|
224
224
|
|
|
225
225
|
get() {
|
|
226
|
+
const startTime = Date.now();
|
|
226
227
|
if (this._explain) return this._getExplain();
|
|
227
228
|
|
|
228
229
|
// 缓存命中
|
|
@@ -319,6 +320,7 @@ class Query {
|
|
|
319
320
|
this._cache.timestamp = Date.now();
|
|
320
321
|
}
|
|
321
322
|
|
|
323
|
+
this._table._db._logSlowQuery(`get ${this._table._name}`, Date.now() - startTime, rows.length);
|
|
322
324
|
return rows;
|
|
323
325
|
}
|
|
324
326
|
|
package/lib/table.js
CHANGED
|
@@ -117,7 +117,37 @@ class Table {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
insertMany(items) {
|
|
120
|
-
|
|
120
|
+
if (items.length === 0) return [];
|
|
121
|
+
for (const hook of this._hooks.beforeInsert) {
|
|
122
|
+
items = items.map(data => hook({ ...data }) || data);
|
|
123
|
+
}
|
|
124
|
+
const uniqueTracker = {};
|
|
125
|
+
for (const [field, def] of Object.entries(this._schema)) {
|
|
126
|
+
if (def.unique) uniqueTracker[field] = new Set();
|
|
127
|
+
}
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const item of items) {
|
|
130
|
+
let data = this._applyDefaults(item);
|
|
131
|
+
data = this._applyComputed(data);
|
|
132
|
+
data = this._validateDataTypes(data);
|
|
133
|
+
this._validateConstraints(data, -1, uniqueTracker);
|
|
134
|
+
if (this._autoIncrementField && data[this._autoIncrementField] === undefined) {
|
|
135
|
+
this._autoIncrement++;
|
|
136
|
+
data[this._autoIncrementField] = this._autoIncrement;
|
|
137
|
+
} else if (this._autoIncrementField && data[this._autoIncrementField] > this._autoIncrement) {
|
|
138
|
+
this._autoIncrement = data[this._autoIncrementField];
|
|
139
|
+
}
|
|
140
|
+
this._rows.push({ ...data });
|
|
141
|
+
results.push(data);
|
|
142
|
+
}
|
|
143
|
+
this._rebuildAllBTrees();
|
|
144
|
+
this._dirty = true;
|
|
145
|
+
this._db._markDirty();
|
|
146
|
+
for (const hook of this._hooks.afterInsert) {
|
|
147
|
+
for (const data of results) hook(data);
|
|
148
|
+
}
|
|
149
|
+
for (const data of results) this._db._emitChange('insert', this._name, data);
|
|
150
|
+
return results;
|
|
121
151
|
}
|
|
122
152
|
|
|
123
153
|
upsert(data) {
|
|
@@ -676,7 +706,7 @@ class Table {
|
|
|
676
706
|
// 内部: 约束校验
|
|
677
707
|
// ============================================================
|
|
678
708
|
|
|
679
|
-
_validateConstraints(data, excludeIndex) {
|
|
709
|
+
_validateConstraints(data, excludeIndex, uniqueTracker) {
|
|
680
710
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
681
711
|
if (field === '_softDelete') continue;
|
|
682
712
|
|
|
@@ -687,11 +717,18 @@ class Table {
|
|
|
687
717
|
|
|
688
718
|
// UNIQUE
|
|
689
719
|
if (def.unique && data[field] !== undefined) {
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
if (this._rows[i][field] === data[field]) {
|
|
720
|
+
if (uniqueTracker && uniqueTracker[field]) {
|
|
721
|
+
if (uniqueTracker[field].has(data[field])) {
|
|
693
722
|
throw createError('ER_DUP_ENTRY', String(data[field]), field);
|
|
694
723
|
}
|
|
724
|
+
uniqueTracker[field].add(data[field]);
|
|
725
|
+
} else {
|
|
726
|
+
for (let i = 0; i < this._rows.length; i++) {
|
|
727
|
+
if (i === excludeIndex) continue;
|
|
728
|
+
if (this._rows[i][field] === data[field]) {
|
|
729
|
+
throw createError('ER_DUP_ENTRY', String(data[field]), field);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
695
732
|
}
|
|
696
733
|
}
|
|
697
734
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "JSQL-NEO — Pure JavaScript embedded database with B-Tree indexes, WAL, hash JOIN, and MySQL-style error codes",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "JSQL-NEO — Pure JavaScript embedded database with B-Tree indexes, WAL, hash JOIN, Redis-compatible Cache, and MySQL-style error codes",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"index.js",
|