jsql-neo 6.5.17 → 6.5.22
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/bin/jsql +9 -2
- package/bin/yaggs-fallback.js +108 -0
- package/index.js +4 -1
- package/lib/btree.js +148 -62
- package/lib/compat/v1.js +208 -0
- package/lib/database.js +38 -17
- 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 +24 -13
- package/lib/native_client.js +60 -10
- package/lib/nedb_compat.js +2 -15
- package/lib/pg_server.js +6 -5
- package/lib/plugin.js +3 -14
- package/lib/query.js +7 -4
- package/lib/sql.js +362 -171
- package/lib/sqlite_worker.js +0 -3
- package/lib/star-nag.js +9 -20
- package/lib/table.js +42 -46
- package/lib/tui.js +28 -6
- package/lib/wasm_client.js +66 -12
- package/lib/web_ui.js +6 -10
- package/lib/xql/index.js +0 -0
- package/lib/xql/src/compiler.js +130 -36
- package/lib/xql/src/jsql.js +1 -1
- package/lib/xql/src/tokenizer.js +10 -2
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +15 -4
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +1 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +93 -10
- package/nativesrc/jsql-neo-wasm/src/lib.rs +39 -14
- package/package.json +10 -5
- package/postinstall.js +42 -18
- package/test/btree.test.js +3 -53
- package/test/bug-regression-2.test.js +299 -0
- package/test/cli-fallback.test.js +55 -0
- package/test/compat-v1.test.js +220 -0
- package/test/coverage.js +3 -3
- package/test/extra-coverage.test.js +4 -2
- package/test/issue-34-35-36.test.js +142 -0
- package/test/issue-39.test.js +160 -0
- package/test/issue-40.test.js +264 -0
- package/test/issue-g4.test.js +82 -0
- package/test/native-add-column.test.js +74 -0
- package/test/native-dup-pk.test.js +114 -0
- package/test/sql-parser.test.js +9 -1
- package/test/sql-tokenizer-literals.test.js +60 -0
- package/wasm/browser.mjs +19 -1
- package/wasm/browser_bg.mjs +150 -97
- package/wasm/jsql_neo_wasm.d.ts +5 -0
- package/wasm/jsql_neo_wasm.js +28 -0
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +1 -0
- package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
- package/test/v6.5.14-regression.test.js +0 -349
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.22** — 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/bin/jsql
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
|
|
4
|
+
// @vexify-org/yaggs 是可选依赖:缺失时(离线安装/私有源不可达/lockfile 漂移)
|
|
5
|
+
// 用内置 shim 兜底,保证 --help/--version 及核心命令仍能启动(#24)。
|
|
6
|
+
let yaggs;
|
|
7
|
+
try { yaggs = require('@vexify-org/yaggs'); }
|
|
8
|
+
catch (e) { yaggs = null; }
|
|
5
9
|
const { ModuleManager } = require('../lib/mod');
|
|
6
10
|
const Database = require('../lib/database');
|
|
7
11
|
const migrate = require('../lib/migrate');
|
|
@@ -29,7 +33,10 @@ const manager = new ModuleManager();
|
|
|
29
33
|
|
|
30
34
|
const RAW_ARGV = process.argv.slice(2);
|
|
31
35
|
|
|
32
|
-
const
|
|
36
|
+
const _pkg = require('../package.json');
|
|
37
|
+
const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
|
|
38
|
+
pkg: _pkg,
|
|
39
|
+
})
|
|
33
40
|
.usage('jsql <command> [options]')
|
|
34
41
|
.option('help', { alias: ['h'], type: 'boolean', description: 'Show help' })
|
|
35
42
|
.command('mod', 'Module management', (sub) => {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal @vexify-org/yaggs-compatible CLI shim.
|
|
5
|
+
*
|
|
6
|
+
* Used when the optional dependency @vexify-org/yaggs is not installed
|
|
7
|
+
* (offline install, private registry unreachable, lockfile drift, …).
|
|
8
|
+
* Without this shim, bin/jsql crashed on every invocation — even --help /
|
|
9
|
+
* --version — because the top-level `require('@vexify-org/yaggs')` threw.
|
|
10
|
+
*
|
|
11
|
+
* The shim mirrors the fluent API surface bin/jsql relies on:
|
|
12
|
+
* .usage(str).option(name, cfg).command(name, desc, builder, handler)
|
|
13
|
+
* .middleware(fn).run(argv)
|
|
14
|
+
*
|
|
15
|
+
* It captures every command handler registered via the fluent chain and
|
|
16
|
+
* dispatches to it with a minimal `--key value` / `--flag` argument parse.
|
|
17
|
+
* This keeps --help/--version and the core commands (serve, export, import,
|
|
18
|
+
* mod, plugin, …) working even when the optional dependency is absent.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function buildYaggsFallback(pkg) {
|
|
22
|
+
// 与 @vexify-org/yaggs 的调用形态一致:buildYaggsFallback(pkg)({ pkg }) 返回 CLI 对象。
|
|
23
|
+
return function makeCli(opts) {
|
|
24
|
+
const realPkg = (opts && opts.pkg) || pkg;
|
|
25
|
+
const commands = new Map();
|
|
26
|
+
let usageStr = '';
|
|
27
|
+
const self = {
|
|
28
|
+
usage(u) { usageStr = u; return self; },
|
|
29
|
+
option() { return self; },
|
|
30
|
+
positional() { return self; },
|
|
31
|
+
middleware() { return self; },
|
|
32
|
+
command(name, description, builder, handler) {
|
|
33
|
+
// 兼容两种签名:command(name, descObj) 或 command(name, desc, builder, handler)
|
|
34
|
+
let h = handler;
|
|
35
|
+
let desc = description;
|
|
36
|
+
if (description && typeof description === 'object') {
|
|
37
|
+
desc = description.description;
|
|
38
|
+
if (description.handler) h = description.handler;
|
|
39
|
+
}
|
|
40
|
+
commands.set(name, { desc: desc || '', handler: h, builder: builder || (() => {}) });
|
|
41
|
+
return self;
|
|
42
|
+
},
|
|
43
|
+
async run(argv) {
|
|
44
|
+
const args = (argv || []).slice();
|
|
45
|
+
const first = args[0];
|
|
46
|
+
|
|
47
|
+
if (!first || first === '--help' || first === '-h' || first === 'help') {
|
|
48
|
+
console.log(usageStr || `jsql ${realPkg.version} — embedded SQL CLI`);
|
|
49
|
+
console.log('Usage: jsql <command> [options]');
|
|
50
|
+
if (commands.size) {
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log('Commands:');
|
|
53
|
+
for (const [name, c] of commands) {
|
|
54
|
+
console.log(` ${name.padEnd(12)} ${c.desc}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log('Note: optional dependency @vexify-org/yaggs is not installed; full argument parsing is unavailable.');
|
|
59
|
+
console.log(' Install it with `npm install @vexify-org/yaggs` to restore rich argument parsing.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (first === '--version' || first === '-v' || first === 'version') {
|
|
64
|
+
console.log(realPkg.version);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const cmd = commands.get(first);
|
|
69
|
+
if (!cmd) {
|
|
70
|
+
console.error(`Unknown command: ${first}`);
|
|
71
|
+
console.error('Run `jsql --help` for the list of commands.');
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 极简参数解析:--key value / --key=value / --flag,以及位置参数。
|
|
77
|
+
const parsed = { _: [] };
|
|
78
|
+
for (let i = 1; i < args.length; i++) {
|
|
79
|
+
const a = args[i];
|
|
80
|
+
if (a.startsWith('--')) {
|
|
81
|
+
const eq = a.indexOf('=');
|
|
82
|
+
if (eq !== -1) {
|
|
83
|
+
parsed[a.slice(2, eq)] = a.slice(eq + 1);
|
|
84
|
+
} else {
|
|
85
|
+
const nxt = args[i + 1];
|
|
86
|
+
if (nxt !== undefined && !nxt.startsWith('-')) { parsed[a.slice(2)] = nxt; i++; }
|
|
87
|
+
else parsed[a.slice(2)] = true;
|
|
88
|
+
}
|
|
89
|
+
} else if (a.startsWith('-') && a.length > 1) {
|
|
90
|
+
const key = a.slice(1);
|
|
91
|
+
const nxt = args[i + 1];
|
|
92
|
+
if (nxt !== undefined && !nxt.startsWith('-')) { parsed[key] = nxt; i++; }
|
|
93
|
+
else parsed[key] = true;
|
|
94
|
+
} else {
|
|
95
|
+
parsed._.push(a);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (cmd.handler) return cmd.handler(parsed);
|
|
100
|
+
console.error(`Command '${first}' has no handler.`);
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
return self;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { buildYaggsFallback };
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JSQL-NEO v6.5.
|
|
2
|
+
* JSQL-NEO v6.5.22 — 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
|
+
};
|