jsql-neo 6.5.16 → 6.5.20
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 +4 -4
- package/index.js +4 -1
- package/lib/btree.js +148 -62
- package/lib/compat/v1.js +208 -0
- package/lib/database.js +21 -16
- package/lib/errors.js +16 -4
- package/lib/migrate.js +2 -6
- package/lib/mongo_server.js +3 -20
- package/lib/mysql_compat.js +1 -1
- package/lib/mysql_server.js +2 -5
- package/lib/native_client.js +22 -2
- package/lib/nedb_compat.js +2 -15
- package/lib/pg_server.js +1 -4
- package/lib/plugin.js +3 -14
- package/lib/sql.js +190 -112
- package/lib/sqlite_worker.js +0 -3
- package/lib/star-nag.js +9 -20
- package/lib/table.js +29 -41
- package/lib/wasm_client.js +4 -2
- package/lib/web_ui.js +6 -10
- package/lib/xql/index.js +0 -0
- package/lib/xql/src/compiler.js +8 -18
- package/lib/xql/src/jsql.js +1 -1
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +1 -1
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +1 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +29 -6
- package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
- package/package.json +8 -5
- package/test/btree.test.js +3 -53
- package/test/bug-regression-2.test.js +299 -0
- package/test/compat-v1.test.js +220 -0
- package/test/coverage.js +3 -3
- package/test/extra-coverage.test.js +7 -4
- package/test/native-dup-pk.test.js +114 -0
- package/test/sql-tokenizer-literals.test.js +60 -0
- package/test/v6.5.14-regression.test.js +0 -323
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.5.
|
|
6
|
+
> **v6.5.20** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
|
@@ -47,10 +47,10 @@ irm jsql.vexify.top/setup | iex
|
|
|
47
47
|
npm install jsql-neo
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
**JSQL-NEO** is a Rust-powered embedded database with **
|
|
50
|
+
**JSQL-NEO** is a Rust-powered embedded database with a **single runtime dependency** (`@vexify-org/yaggs`, used only by the `jsql` CLI argument parser) — no native libs to compile, no system deps to install. It runs in Node.js, CLI, Web UI, and the browser (WASM).
|
|
51
51
|
|
|
52
52
|
- **Four wire protocols on one port** — `jsql serve` accepts MySQL, PostgreSQL, MongoDB and Redis clients; `mysql2`, `pg`, `mongodb` and `ioredis` connect directly
|
|
53
|
-
- **
|
|
53
|
+
- **Nearly dependency-free** — only `@vexify-org/yaggs` (CLI arg parsing); no native libs to compile, no system deps to install
|
|
54
54
|
- **Three engines, same API** — `native` (Rust N-API), `wasm` (same Rust core), `js` (pure JS)
|
|
55
55
|
- **O(1) primary-key lookups and updates** — hash index, no full-table scan; unindexed filters run in Rust
|
|
56
56
|
|
|
@@ -80,7 +80,7 @@ JSQL-NEO: MySQL + PostgreSQL + MongoDB + Redis + SQL
|
|
|
80
80
|
| **Redis** | `ioredis`, `redis-cli` — all data types + TTL |
|
|
81
81
|
| **One Port** | Protocol sniffing routes all four clients to the same data |
|
|
82
82
|
| **Zero-Dep TUI** | `jsql tui` — line editing, history, Tab, CJK tables |
|
|
83
|
-
| **Web UI** |
|
|
83
|
+
| **Web UI** | Management console + HTTP API with no frontend framework |
|
|
84
84
|
| **WASM** | Full engine in browser, no server needed |
|
|
85
85
|
|
|
86
86
|
---
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JSQL-NEO v6.5.
|
|
2
|
+
* JSQL-NEO v6.5.20 — Rust-Powered Embedded Database (WASM + HTTP)
|
|
3
3
|
*
|
|
4
4
|
* @example
|
|
5
5
|
* const jsql = require('jsql-neo');
|
|
@@ -131,6 +131,9 @@ function injectMySQLCompat(mod) {
|
|
|
131
131
|
|
|
132
132
|
module.exports = {
|
|
133
133
|
JSQL: WasmClient.JSQL,
|
|
134
|
+
// jsql 1.0.0 怀旧兼容层(同步 createTable/dropTable + db.<表名> 属性)
|
|
135
|
+
// 用法:const { Database } = require('jsql-neo/compat/v1');
|
|
136
|
+
compatV1: require('./lib/compat/v1'),
|
|
134
137
|
NativeJSQL: NativeClient.JSQL,
|
|
135
138
|
Database: require('./lib/database'),
|
|
136
139
|
Table: require('./lib/table'),
|
package/lib/btree.js
CHANGED
|
@@ -20,10 +20,28 @@ class BTree {
|
|
|
20
20
|
* @param {boolean} unique - 是否唯一索引
|
|
21
21
|
*/
|
|
22
22
|
constructor(order = 64, unique = false) {
|
|
23
|
-
|
|
23
|
+
if (typeof order !== 'number' || !Number.isFinite(order)) {
|
|
24
|
+
throw new Error(`BTree order must be a finite number (got ${order})`);
|
|
25
|
+
}
|
|
26
|
+
// order=3 属于 2-3 树,删除时的 merge 需要「三个 1-key 子节点 + 分隔键 = 3 keys」
|
|
27
|
+
// 塞进一个最多 2 keys 的节点,B-tree 的标准 borrow/merge 在数学上做不到,
|
|
28
|
+
// 必须换一整套 2-3 树删除算法(或者用 2-3-4 树即 order=4 等价替身)。
|
|
29
|
+
// 这里把 order<4 归一化成 4:多路树的所有查询语义(search/range/entries/prefix)
|
|
30
|
+
// 与 order 无关,唯一区别是节点最多存 3 个 key 而不是 2 个 —— 只会更平衡。
|
|
31
|
+
//
|
|
32
|
+
// 以前不归一化时 _minKeys = floor(order/2)-1 = 0,允许空节点;_splitChild 在
|
|
33
|
+
// 只剩 2 个 key 的节点上算出的分隔键下标又超出了右半边界,于是右子节点为空、
|
|
34
|
+
// 父节点出现重复分隔键 —— 插入 19,7,12 后根节点 keys 变成 [19,19],
|
|
35
|
+
// 之后混合增删会静默丢索引条目。
|
|
36
|
+
this._requestedOrder = order;
|
|
37
|
+
this._order = Math.max(4, Math.trunc(order));
|
|
24
38
|
this._unique = unique;
|
|
25
39
|
this._root = new BTreeNode();
|
|
26
40
|
this._size = 0;
|
|
41
|
+
// 必须用归一化后的 this._order 计算下界,否则 order=3 会算出 _minKeys=0
|
|
42
|
+
// (允许空节点),删除时 merge/borrow 判定全错,最后留下 64 条幽灵条目。
|
|
43
|
+
this._minKeys = Math.floor(this._order / 2) - 1;
|
|
44
|
+
this._minDegree = Math.ceil(this._order / 2);
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
get size() { return this._size; }
|
|
@@ -91,22 +109,28 @@ class BTree {
|
|
|
91
109
|
const newChild = new BTreeNode(child.leaf);
|
|
92
110
|
const mid = Math.floor((this._order - 1) / 2);
|
|
93
111
|
|
|
94
|
-
|
|
95
|
-
|
|
112
|
+
// 分隔键下标:必须保证左、右两侧都至少留 1 个 key。
|
|
113
|
+
// 节点满时 keys.length === order-1,右半从 sepIdx+1 开始,
|
|
114
|
+
// 所以 sepIdx <= order-3。取 min(mid, order-3):order>=4 时 mid <= order-3 恒成立
|
|
115
|
+
// (order=4 → 1, order=5 → 2, order=6 → 2, order=64 → 31),行为与标准 B-tree 一致。
|
|
116
|
+
const sepIdx = Math.max(0, Math.min(mid, this._order - 3));
|
|
117
|
+
|
|
118
|
+
const midKey = child.keys[sepIdx];
|
|
119
|
+
const midVal = child.values[sepIdx];
|
|
96
120
|
|
|
97
121
|
if (child.leaf) {
|
|
98
122
|
// 叶子:右半移到新节点;分隔键保留在左叶(它是真实数据键),父节点只存副本
|
|
99
|
-
newChild.keys = child.keys.splice(
|
|
100
|
-
newChild.values = child.values.splice(
|
|
123
|
+
newChild.keys = child.keys.splice(sepIdx + 1);
|
|
124
|
+
newChild.values = child.values.splice(sepIdx + 1);
|
|
101
125
|
newChild.next = child.next;
|
|
102
126
|
child.next = newChild;
|
|
103
127
|
} else {
|
|
104
128
|
// 内部:分隔键提升到父节点并从子节点移除,保持 children = keys + 1
|
|
105
|
-
newChild.keys = child.keys.splice(
|
|
106
|
-
newChild.values = child.values.splice(
|
|
107
|
-
child.keys.splice(
|
|
108
|
-
child.values.splice(
|
|
109
|
-
newChild.children = child.children.splice(
|
|
129
|
+
newChild.keys = child.keys.splice(sepIdx + 1);
|
|
130
|
+
newChild.values = child.values.splice(sepIdx + 1);
|
|
131
|
+
child.keys.splice(sepIdx, 1);
|
|
132
|
+
child.values.splice(sepIdx, 1);
|
|
133
|
+
newChild.children = child.children.splice(sepIdx + 1);
|
|
110
134
|
}
|
|
111
135
|
|
|
112
136
|
parent.keys.splice(index, 0, midKey);
|
|
@@ -271,81 +295,122 @@ class BTree {
|
|
|
271
295
|
|
|
272
296
|
remove(key, rowIndex) {
|
|
273
297
|
if (!this._root) return false;
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.
|
|
282
|
-
|
|
298
|
+
const removed = this._removeFromNode(this._root, key, rowIndex);
|
|
299
|
+
// 删掉整个键(而非仅删某个 rowIndex)后,叶子的键域可能缩小,
|
|
300
|
+
// 沿途的分隔键副本可能已指向不存在的键 —— 自顶向下重建一次。
|
|
301
|
+
if (removed) {
|
|
302
|
+
this._size--;
|
|
303
|
+
if (!this._root.leaf) this._fixSeparators(this._root);
|
|
304
|
+
}
|
|
305
|
+
if (this._root.keys.length === 0 && !this._root.leaf && this._root.children.length > 0) {
|
|
306
|
+
this._root = this._root.children[0];
|
|
307
|
+
}
|
|
308
|
+
return removed;
|
|
283
309
|
}
|
|
284
310
|
|
|
285
311
|
_removeFromNode(node, key, rowIndex) {
|
|
286
312
|
if (node.leaf) {
|
|
287
313
|
const i = this._lowerBound(node.keys, key);
|
|
288
|
-
if (i >= node.keys.length || node.keys[i] !== key) return
|
|
289
|
-
|
|
290
|
-
if (pos === -1) return { found: false };
|
|
314
|
+
if (i >= node.keys.length || node.keys[i] !== key) return false;
|
|
315
|
+
if (!node.values[i].includes(rowIndex)) return false;
|
|
291
316
|
const vals = node.values[i].filter(v => v !== rowIndex);
|
|
292
317
|
if (vals.length === 0) {
|
|
293
318
|
node.keys.splice(i, 1);
|
|
294
319
|
node.values.splice(i, 1);
|
|
295
|
-
return
|
|
320
|
+
return true;
|
|
296
321
|
}
|
|
297
322
|
node.values[i] = vals;
|
|
298
|
-
return
|
|
323
|
+
return false;
|
|
299
324
|
}
|
|
300
325
|
|
|
301
|
-
|
|
326
|
+
let i = this._route(node, key);
|
|
327
|
+
|
|
328
|
+
// 下降前确保子节点有足够键,避免欠满子节点
|
|
302
329
|
const child = node.children[i];
|
|
303
|
-
if (
|
|
304
|
-
|
|
330
|
+
if (child.keys.length <= this._minKeys) {
|
|
331
|
+
i = this._rebalanceChild(node, i);
|
|
332
|
+
}
|
|
333
|
+
return this._removeFromNode(node.children[i], key, rowIndex);
|
|
305
334
|
}
|
|
306
335
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
336
|
+
_rebalanceChild(parent, i) {
|
|
337
|
+
if (i > 0 && parent.children[i - 1].keys.length > this._minKeys) {
|
|
338
|
+
this._borrowFromLeft(parent, i);
|
|
339
|
+
return i;
|
|
340
|
+
}
|
|
341
|
+
if (i + 1 < parent.children.length && parent.children[i + 1].keys.length > this._minKeys) {
|
|
342
|
+
this._borrowFromRight(parent, i);
|
|
343
|
+
return i;
|
|
344
|
+
}
|
|
345
|
+
if (i > 0) {
|
|
346
|
+
this._mergeChildren(parent, i - 1);
|
|
347
|
+
return i - 1;
|
|
348
|
+
}
|
|
349
|
+
this._mergeChildren(parent, i);
|
|
350
|
+
return i;
|
|
316
351
|
}
|
|
317
352
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
353
|
+
_borrowFromLeft(parent, i) {
|
|
354
|
+
const left = parent.children[i - 1];
|
|
355
|
+
const right = parent.children[i];
|
|
356
|
+
if (right.leaf) {
|
|
357
|
+
// 叶子节点:移动左叶最大键到右叶最小位置
|
|
358
|
+
const k = left.keys.pop();
|
|
359
|
+
const v = left.values.pop();
|
|
360
|
+
right.keys.unshift(k);
|
|
361
|
+
right.values.unshift(v);
|
|
362
|
+
// 父节点分隔键应为左叶新的最大值(pop 后的最后一个键)
|
|
363
|
+
parent.keys[i - 1] = left.keys[left.keys.length - 1];
|
|
364
|
+
parent.values[i - 1] = left.values[left.values.length - 1];
|
|
365
|
+
} else {
|
|
366
|
+
right.keys.unshift(parent.keys[i - 1]);
|
|
367
|
+
right.values.unshift(parent.values[i - 1]);
|
|
368
|
+
parent.keys[i - 1] = left.keys.pop();
|
|
369
|
+
parent.values[i - 1] = left.values.pop();
|
|
370
|
+
right.children.unshift(left.children.pop());
|
|
324
371
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_borrowFromRight(parent, i) {
|
|
375
|
+
const left = parent.children[i];
|
|
376
|
+
const right = parent.children[i + 1];
|
|
377
|
+
if (left.leaf) {
|
|
378
|
+
// 叶子节点:用被移动的键而非旧分隔键,避免重复
|
|
379
|
+
const k = right.keys.shift();
|
|
380
|
+
const v = right.values.shift();
|
|
381
|
+
left.keys.push(k);
|
|
382
|
+
left.values.push(v);
|
|
383
|
+
parent.keys[i] = k;
|
|
384
|
+
parent.values[i] = v;
|
|
385
|
+
} else {
|
|
386
|
+
left.keys.push(parent.keys[i]);
|
|
387
|
+
left.values.push(parent.values[i]);
|
|
388
|
+
parent.keys[i] = right.keys.shift();
|
|
389
|
+
parent.values[i] = right.values.shift();
|
|
390
|
+
left.children.push(right.children.shift());
|
|
334
391
|
}
|
|
335
|
-
return node;
|
|
336
392
|
}
|
|
337
393
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
394
|
+
/**
|
|
395
|
+
* 修复内部节点的分隔键:keys[i] 必须是一个**确实存在于 child[i] 里**的键。
|
|
396
|
+
*
|
|
397
|
+
* 背景:分隔键的约定是「左子树的最大键」(叶子上存真实数据,父节点存副本)。
|
|
398
|
+
* remove() 删掉叶子上的某个键后,如果那个键正好是父节点的分隔键副本,
|
|
399
|
+
* 分隔键就会指向一个已经不存在的值 —— 树还能路由对,但内部键不再严格递增,
|
|
400
|
+
* 下一次 borrow/merge 就会把它复制成重复键,直接破坏结构。
|
|
401
|
+
*
|
|
402
|
+
* 最小复现(BTree(4)):
|
|
403
|
+
* +28:1 +24:4 +0:0 +14:2 -24:4 +25:4 +24:2 +28:1 -26:2
|
|
404
|
+
* 前 8 步都正常,第 9 步把 keys=[24,25] 变成 [24,24]。
|
|
405
|
+
*/
|
|
406
|
+
_fixSeparators(node) {
|
|
407
|
+
if (node.leaf) return;
|
|
408
|
+
for (let i = 0; i < node.keys.length; i++) {
|
|
409
|
+
const m = this._getMax(node.children[i]);
|
|
410
|
+
node.keys[i] = m.key;
|
|
411
|
+
node.values[i] = m.value;
|
|
347
412
|
}
|
|
348
|
-
|
|
413
|
+
for (const c of node.children) this._fixSeparators(c);
|
|
349
414
|
}
|
|
350
415
|
|
|
351
416
|
_getMax(node) {
|
|
@@ -354,6 +419,27 @@ class BTree {
|
|
|
354
419
|
return { key: node.keys[i], value: node.values[i] };
|
|
355
420
|
}
|
|
356
421
|
|
|
422
|
+
_mergeChildren(parent, index) {
|
|
423
|
+
const left = parent.children[index];
|
|
424
|
+
const right = parent.children[index + 1];
|
|
425
|
+
const midKey = parent.keys[index];
|
|
426
|
+
const midVal = parent.values[index];
|
|
427
|
+
|
|
428
|
+
// 叶子节点:分隔键是叶子的副本(已在 left 或 right 中),不能重复压入
|
|
429
|
+
if (!left.leaf) {
|
|
430
|
+
left.keys.push(midKey);
|
|
431
|
+
left.values.push(midVal);
|
|
432
|
+
}
|
|
433
|
+
for (const __e of right.keys) left.keys.push(__e);
|
|
434
|
+
for (const __e of right.values) left.values.push(__e);
|
|
435
|
+
if (!left.leaf) for (const __e of right.children) left.children.push(__e);
|
|
436
|
+
if (left.leaf) left.next = right.next;
|
|
437
|
+
|
|
438
|
+
parent.keys.splice(index, 1);
|
|
439
|
+
parent.values.splice(index, 1);
|
|
440
|
+
parent.children.splice(index + 1, 1);
|
|
441
|
+
}
|
|
442
|
+
|
|
357
443
|
// ============================================================
|
|
358
444
|
// 遍历
|
|
359
445
|
// ============================================================
|
package/lib/compat/v1.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
/**
|
|
3
|
+
* jsql 1.0.0 兼容层(怀旧 API)
|
|
4
|
+
*
|
|
5
|
+
* 1.0.0 的公开 API 是**全同步**的:
|
|
6
|
+
*
|
|
7
|
+
* const jsql = require('jsql-neo');
|
|
8
|
+
* const db = new jsql.Database('mydb.json');
|
|
9
|
+
* db.createTable('users', {
|
|
10
|
+
* id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
11
|
+
* name: { type: 'string', required: true },
|
|
12
|
+
* age: { type: 'integer' }
|
|
13
|
+
* });
|
|
14
|
+
* db.users.insert({ name: 'Alice', age: 25 });
|
|
15
|
+
* db.users.where({ age: { $gte: 18 } }).orderBy('age', 'desc').limit(10).get();
|
|
16
|
+
*
|
|
17
|
+
* v6 引擎本身已经把 Table / Query 的全部方法做成同步的,只有
|
|
18
|
+
* `Database.createTable()` / `Database.dropTable()` 变成了 async。
|
|
19
|
+
* 这层只做两件事:
|
|
20
|
+
*
|
|
21
|
+
* 1. 同步版 createTable/dropTable(内部走 _createTableSync/_dropTableSync);
|
|
22
|
+
* 2. 把 `db.<表名>` 的取值属性镜像到包装对象上,保证老代码直接可用。
|
|
23
|
+
*
|
|
24
|
+
* 除了这两个方法,其余行为完全等同 v6:拿到的 Table 就是 lib/table.js 的
|
|
25
|
+
* Table,查询链就是 lib/query.js 的 Query,不存在第二套实现。
|
|
26
|
+
*
|
|
27
|
+
* 注意:这是兼容层,不是"降级"。它不会关闭 v6 的任何能力,
|
|
28
|
+
* 需要 async API 时通过 `db.raw` 拿到底层 Database 即可。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const Database = require('../database');
|
|
32
|
+
const Table = require('../table');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 把 1.0.0 的简写 schema('string' / 'integer' 等)解析成对象。
|
|
36
|
+
* 与 Database#createTable 里的解析逻辑保持一致,避免两处语义漂移。
|
|
37
|
+
*/
|
|
38
|
+
function parseShorthand(schema) {
|
|
39
|
+
if (typeof schema !== 'string') return schema || {};
|
|
40
|
+
const parsed = {};
|
|
41
|
+
for (const seg of schema.split(',')) {
|
|
42
|
+
const trimmed = seg.trim();
|
|
43
|
+
if (!trimmed) continue;
|
|
44
|
+
const parts = trimmed.split(/\s+/);
|
|
45
|
+
const field = parts.shift();
|
|
46
|
+
parsed[field] = parts.join(' ').toLowerCase().trim() || 'string';
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 深拷贝(优先 structuredClone,退化到 JSON) */
|
|
52
|
+
function deepClone(value) {
|
|
53
|
+
try {
|
|
54
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
55
|
+
} catch (e) { /* 含函数/类实例时退化 */ }
|
|
56
|
+
return JSON.parse(JSON.stringify(value));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从行数据推断 schema(仅用于 import 时表还不存在的场景)。
|
|
61
|
+
* 1.0.0 的 export() 只吐出 rows,不带 schema,所以必须能反推。
|
|
62
|
+
*/
|
|
63
|
+
function inferSchema(rows) {
|
|
64
|
+
const schema = {};
|
|
65
|
+
if (!Array.isArray(rows) || rows.length === 0) return schema;
|
|
66
|
+
const sample = rows[0];
|
|
67
|
+
for (const [field, value] of Object.entries(sample)) {
|
|
68
|
+
let type = 'string';
|
|
69
|
+
if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'float';
|
|
70
|
+
else if (typeof value === 'boolean') type = 'boolean';
|
|
71
|
+
else if (Array.isArray(value)) type = 'array';
|
|
72
|
+
else if (value !== null && typeof value === 'object') type = 'object';
|
|
73
|
+
schema[field] = { type };
|
|
74
|
+
}
|
|
75
|
+
// 1.0.0 的 export() 不带 schema,自增主键信息会丢。
|
|
76
|
+
// 常见约定:`id` 且整型唯一 —— 还原成 primaryKey + autoIncrement,
|
|
77
|
+
// 否则 import 后继续 insert 会得到 id=undefined(老代码会当场炸)。
|
|
78
|
+
if (schema.id && schema.id.type === 'integer') {
|
|
79
|
+
const ids = rows.map(r => r.id);
|
|
80
|
+
const unique = new Set(ids).size === ids.length && ids.every(v => typeof v === 'number');
|
|
81
|
+
if (unique) {
|
|
82
|
+
schema.id.primaryKey = true;
|
|
83
|
+
schema.id.autoIncrement = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return schema;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class V1Database {
|
|
90
|
+
/**
|
|
91
|
+
* @param {string|null} [filePath] 文件路径;null / ':memory:' 为内存模式
|
|
92
|
+
* @param {object} [options] 透传给 v6 Database
|
|
93
|
+
*/
|
|
94
|
+
constructor(filePath, options = {}) {
|
|
95
|
+
this._db = new Database(filePath, options);
|
|
96
|
+
// 文件模式导入或目录模式加载时,表已经存在 —— 同样要把
|
|
97
|
+
// db.<表名> 属性镜像到包装对象上,否则老代码 `db.users` 会 undefined。
|
|
98
|
+
for (const name of this._db.getTables()) this._bindTable(name);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 底层 v6 Database(需要 async / 高级能力时用) */
|
|
102
|
+
get raw() {
|
|
103
|
+
return this._db;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_bindTable(name) {
|
|
107
|
+
if (typeof name !== 'string' || !name) return;
|
|
108
|
+
// 内部字段绝不覆盖(表名叫 _db 时不能把引擎句柄删掉)
|
|
109
|
+
if (name === '_db') return;
|
|
110
|
+
// 表名撞上类方法(如叫 "save")时不覆盖方法,保持 1.0.0 的边界行为。
|
|
111
|
+
if (Object.prototype.hasOwnProperty.call(V1Database.prototype, name)) return;
|
|
112
|
+
if (Object.prototype.hasOwnProperty.call(this, name)) {
|
|
113
|
+
delete this[name];
|
|
114
|
+
}
|
|
115
|
+
Object.defineProperty(this, name, {
|
|
116
|
+
get: () => this._db[name],
|
|
117
|
+
enumerable: true,
|
|
118
|
+
configurable: true
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================
|
|
123
|
+
// 表管理(1.0.0 语义:同步)
|
|
124
|
+
// ============================================================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 同步建表。
|
|
128
|
+
* @param {string} name
|
|
129
|
+
* @param {object|string} schema
|
|
130
|
+
* @returns {Table}
|
|
131
|
+
*/
|
|
132
|
+
createTable(name, schema) {
|
|
133
|
+
const table = this._db._createTableSync(name, parseShorthand(schema));
|
|
134
|
+
this._bindTable(name);
|
|
135
|
+
return table;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 同步删表(表不存在时静默返回,符合 1.0.0 的宽松行为)。
|
|
140
|
+
* @param {string} name
|
|
141
|
+
*/
|
|
142
|
+
dropTable(name) {
|
|
143
|
+
if (this._db.hasTable(name)) this._db._dropTableSync(name);
|
|
144
|
+
if (name !== '_db' && Object.prototype.hasOwnProperty.call(this, name)) delete this[name];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
hasTable(name) {
|
|
148
|
+
return this._db.hasTable(name);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getTable(name) {
|
|
152
|
+
return this._db[name];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getTables() {
|
|
156
|
+
return this._db.getTables();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================
|
|
160
|
+
// 持久化(与 v6 一致,均为同步)
|
|
161
|
+
// ============================================================
|
|
162
|
+
|
|
163
|
+
save() {
|
|
164
|
+
return this._db.save();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
close() {
|
|
168
|
+
return this._db.close();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export() {
|
|
172
|
+
const data = this._db.export();
|
|
173
|
+
// 1.0.0 的 export() 是快照:调用方会存起来 / 发出去 / 过一会儿再 import。
|
|
174
|
+
// v6 的 Table#toJSON() 直接返回 this._rows,原样透传的话后续 insert/update
|
|
175
|
+
// 会同步改写刚导出的"快照"(import 到新库后两个库还共享同一个数组)。
|
|
176
|
+
// 这里做一次深拷贝,保持导出结果的不可变语义。
|
|
177
|
+
const snapshot = {};
|
|
178
|
+
for (const [name, rows] of Object.entries(data)) {
|
|
179
|
+
snapshot[name] = Array.isArray(rows) ? deepClone(rows) : rows;
|
|
180
|
+
}
|
|
181
|
+
return snapshot;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 导入数据。1.0.0 的 export() 只保留 rows,表不存在时按首行推断 schema 建表,
|
|
186
|
+
* 保证 export → import 往返可用。
|
|
187
|
+
*/
|
|
188
|
+
import(data) {
|
|
189
|
+
if (!data || typeof data !== 'object') return this;
|
|
190
|
+
for (const [name, rows] of Object.entries(data)) {
|
|
191
|
+
if (!this._db.hasTable(name)) {
|
|
192
|
+
this.createTable(name, inferSchema(rows));
|
|
193
|
+
}
|
|
194
|
+
// 再拷贝一次:避免导入方后续修改自己的数据时串改库内行
|
|
195
|
+
this._db[name]._loadRows(Array.isArray(rows) ? deepClone(rows) : []);
|
|
196
|
+
}
|
|
197
|
+
this._db._markDirtyLegacy && this._db._markDirtyLegacy();
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
Database: V1Database,
|
|
204
|
+
Table,
|
|
205
|
+
// 便于测试 / 高级用法
|
|
206
|
+
V1Database,
|
|
207
|
+
_inferSchema: inferSchema
|
|
208
|
+
};
|
package/lib/database.js
CHANGED
|
@@ -1344,11 +1344,15 @@ class Database {
|
|
|
1344
1344
|
startedAt: Date.now()
|
|
1345
1345
|
};
|
|
1346
1346
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1347
|
+
if (level === 'REPEATABLE_READ') {
|
|
1348
|
+
// 快照:保存当前所有数据(深拷贝,保证嵌套对象可回滚)
|
|
1349
|
+
this._transaction.snapshot = this._captureTxSnapshot();
|
|
1350
|
+
} else {
|
|
1351
|
+
// READ_COMMITTED:只保存 autoIncrement
|
|
1352
|
+
for (const [name, table] of Object.entries(this._tables)) {
|
|
1353
|
+
this._transaction.autoIncrements[name] = table._autoIncrement;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1352
1356
|
|
|
1353
1357
|
this._savepointStack = [];
|
|
1354
1358
|
this._walWrite({ op: 'begin', level });
|
|
@@ -1370,6 +1374,12 @@ class Database {
|
|
|
1370
1374
|
|
|
1371
1375
|
if (this._transaction.snapshot) {
|
|
1372
1376
|
this._restoreTxSnapshot(this._transaction.snapshot);
|
|
1377
|
+
} else {
|
|
1378
|
+
// READ_COMMITTED:没有行快照,只能退回自增值
|
|
1379
|
+
for (const [name, table] of Object.entries(this._tables)) {
|
|
1380
|
+
const v = this._transaction.autoIncrements[name];
|
|
1381
|
+
if (v !== undefined) table._autoIncrement = v;
|
|
1382
|
+
}
|
|
1373
1383
|
}
|
|
1374
1384
|
|
|
1375
1385
|
this._walWrite({ op: 'rollback', at: this._transaction.startedAt });
|
|
@@ -1847,8 +1857,8 @@ class Database {
|
|
|
1847
1857
|
let rows = table._applyFilterOptimized(table._getVisibleRows(), filter || {});
|
|
1848
1858
|
if (limit > 0) rows = rows.slice(offset, offset + limit);
|
|
1849
1859
|
else if (offset > 0) rows = rows.slice(offset);
|
|
1850
|
-
// P1-11:
|
|
1851
|
-
rows = rows.map(r =>
|
|
1860
|
+
// P1-11: 返回浅拷贝,防止调用方修改影响内部数据
|
|
1861
|
+
rows = rows.map(r => ({ ...r }));
|
|
1852
1862
|
this._touchTable(tableName);
|
|
1853
1863
|
await this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
|
|
1854
1864
|
return rows;
|
|
@@ -1880,8 +1890,8 @@ class Database {
|
|
|
1880
1890
|
const row = this._resolveId(table, id);
|
|
1881
1891
|
// P1-4: 软删除行对普通读取不可见
|
|
1882
1892
|
if (row && row._deleted) return null;
|
|
1883
|
-
// P1-11:
|
|
1884
|
-
const result = row ?
|
|
1893
|
+
// P1-11: 返回浅拷贝,防止调用方修改影响内部数据
|
|
1894
|
+
const result = row ? { ...row } : null;
|
|
1885
1895
|
this._touchTable(tableName);
|
|
1886
1896
|
await this._runHooks('afterFind', [tableName, { id }, result]);
|
|
1887
1897
|
return result;
|
|
@@ -1892,7 +1902,7 @@ class Database {
|
|
|
1892
1902
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1893
1903
|
if (!(await this._runHooks('beforeFind', [tableName, { ids }]))) return null;
|
|
1894
1904
|
// P1-4: 过滤掉软删除行;P1-11: 返回浅拷贝
|
|
1895
|
-
const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r =>
|
|
1905
|
+
const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r => ({ ...r }));
|
|
1896
1906
|
this._touchTable(tableName);
|
|
1897
1907
|
await this._runHooks('afterFind', [tableName, { ids }, rows]);
|
|
1898
1908
|
return rows;
|
|
@@ -1938,9 +1948,6 @@ class Database {
|
|
|
1938
1948
|
const clash = table._rows.find(r => r !== row && r[field] === nv);
|
|
1939
1949
|
if (clash) throw createError('ER_DUP_ENTRY', `Duplicate entry '${nv}' for key '${field}'`);
|
|
1940
1950
|
}
|
|
1941
|
-
// 主键/必填列的 NULL 校验:updateById 此前只做类型校验与重复检查,
|
|
1942
|
-
// `UPDATE t SET id = NULL` 会被静默写入(主键行变成 NULL 主键)。
|
|
1943
|
-
table._validateConstraints({ ...row, ...validated }, table._rows.indexOf(row));
|
|
1944
1951
|
Object.assign(row, validated);
|
|
1945
1952
|
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...validated } });
|
|
1946
1953
|
this._emit('update', { table: tableName, id, data: validated });
|
|
@@ -1959,9 +1966,7 @@ class Database {
|
|
|
1959
1966
|
for (const [id, data] of pairs) {
|
|
1960
1967
|
const row = this._resolveId(table, id);
|
|
1961
1968
|
if (row) {
|
|
1962
|
-
|
|
1963
|
-
table._validateConstraints({ ...row, ...validated }, table._rows.indexOf(row));
|
|
1964
|
-
Object.assign(row, validated);
|
|
1969
|
+
Object.assign(row, data);
|
|
1965
1970
|
count++;
|
|
1966
1971
|
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...data } });
|
|
1967
1972
|
}
|
package/lib/errors.js
CHANGED
|
@@ -81,10 +81,22 @@ function createError(codeKey, ...args) {
|
|
|
81
81
|
return new JSQL_Error('UNKNOWN', `Unknown error: ${codeKey}`, { codeKey, args });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
// 单次扫描替换:占位符按模板顺序消费,参数内容原样写入。
|
|
85
|
+
//
|
|
86
|
+
// 旧实现是 `for (args) msg = msg.replace('%s', () => String(arg))`,
|
|
87
|
+
// 每一轮都在**上一轮的结果**上再找第一个 '%s' —— 参数里自带的 '%s'
|
|
88
|
+
// 会被当成下一个占位符吃掉,且顺序错位:
|
|
89
|
+
// createError('ER_BAD_FIELD_ERROR', 'X%sY', 'Z')
|
|
90
|
+
// → "Unknown column 'XZY' in '%s'"(X%sY 被改写成 XZY)
|
|
91
|
+
// 正确结果是 "Unknown column 'X%sY' in 'Z'"。
|
|
92
|
+
let i = 0;
|
|
93
|
+
let msg = template.msg.replace(/%s/g, () => (i < args.length ? String(args[i++]) : '%s'));
|
|
94
|
+
if (args.length > 0 && !/%s/.test(template.msg)) {
|
|
95
|
+
// 模板里没有占位符(如 ER_NO_TRANSACTION)时,参数不能被静默丢弃:
|
|
96
|
+
// 追加在后面,至少保留调用方给出的上下文。
|
|
97
|
+
const rest = args.map(a => String(a)).join(' ');
|
|
98
|
+
msg = rest ? `${msg} (${rest})` : msg;
|
|
99
|
+
}
|
|
88
100
|
|
|
89
101
|
return new JSQL_Error(template.code, msg, { args });
|
|
90
102
|
}
|
package/lib/migrate.js
CHANGED
|
@@ -30,12 +30,8 @@ function normalizeSchema(schema) {
|
|
|
30
30
|
if (d.type === 'varchar' || d.type === 'text' || d.type === 'char') d.type = 'string';
|
|
31
31
|
if (d.type === 'double' || d.type === 'real' || d.type === 'decimal' || d.type === 'numeric') d.type = 'float';
|
|
32
32
|
if (d.type === 'bool') d.type = 'boolean';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (d.maxLength !== undefined) {
|
|
36
|
-
if (d.length === undefined) d.length = d.maxLength;
|
|
37
|
-
delete d.maxLength;
|
|
38
|
-
}
|
|
33
|
+
delete d.length;
|
|
34
|
+
if (d.maxLength) { d.length = d.maxLength; delete d.maxLength; }
|
|
39
35
|
out[name] = d;
|
|
40
36
|
}
|
|
41
37
|
return out;
|