jsql-neo 6.4.5 → 6.4.7
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 +1 -1
- package/index.js +1 -1
- package/lib/btree.js +28 -1
- package/lib/native_client.js +27 -1
- package/package.json +1 -1
- package/test/btree.test.js +66 -12
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.4.
|
|
6
|
+
> **v6.4.7** — official release build · [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
|

|
package/index.js
CHANGED
package/lib/btree.js
CHANGED
|
@@ -274,7 +274,12 @@ class BTree {
|
|
|
274
274
|
remove(key, rowIndex) {
|
|
275
275
|
if (!this._root) return false;
|
|
276
276
|
const removed = this._removeFromNode(this._root, key, rowIndex);
|
|
277
|
-
|
|
277
|
+
// 删掉整个键(而非仅删某个 rowIndex)后,叶子的键域可能缩小,
|
|
278
|
+
// 沿途的分隔键副本可能已指向不存在的键 —— 自顶向下重建一次。
|
|
279
|
+
if (removed) {
|
|
280
|
+
this._size--;
|
|
281
|
+
if (!this._root.leaf) this._fixSeparators(this._root);
|
|
282
|
+
}
|
|
278
283
|
if (this._root.keys.length === 0 && !this._root.leaf && this._root.children.length > 0) {
|
|
279
284
|
this._root = this._root.children[0];
|
|
280
285
|
}
|
|
@@ -364,6 +369,28 @@ class BTree {
|
|
|
364
369
|
}
|
|
365
370
|
}
|
|
366
371
|
|
|
372
|
+
/**
|
|
373
|
+
* 修复内部节点的分隔键:keys[i] 必须是一个**确实存在于 child[i] 里**的键。
|
|
374
|
+
*
|
|
375
|
+
* 背景:分隔键的约定是「左子树的最大键」(叶子上存真实数据,父节点存副本)。
|
|
376
|
+
* remove() 删掉叶子上的某个键后,如果那个键正好是父节点的分隔键副本,
|
|
377
|
+
* 分隔键就会指向一个已经不存在的值 —— 树还能路由对,但内部键不再严格递增,
|
|
378
|
+
* 下一次 borrow/merge 就会把它复制成重复键,直接破坏结构。
|
|
379
|
+
*
|
|
380
|
+
* 最小复现(BTree(4)):
|
|
381
|
+
* +28:1 +24:4 +0:0 +14:2 -24:4 +25:4 +24:2 +28:1 -26:2
|
|
382
|
+
* 前 8 步都正常,第 9 步把 keys=[24,25] 变成 [24,24]。
|
|
383
|
+
*/
|
|
384
|
+
_fixSeparators(node) {
|
|
385
|
+
if (node.leaf) return;
|
|
386
|
+
for (let i = 0; i < node.keys.length; i++) {
|
|
387
|
+
const m = this._getMax(node.children[i]);
|
|
388
|
+
node.keys[i] = m.key;
|
|
389
|
+
node.values[i] = m.value;
|
|
390
|
+
}
|
|
391
|
+
for (const c of node.children) this._fixSeparators(c);
|
|
392
|
+
}
|
|
393
|
+
|
|
367
394
|
_getMax(node) {
|
|
368
395
|
while (!node.leaf) node = node.children[node.children.length - 1];
|
|
369
396
|
const i = node.keys.length - 1;
|
package/lib/native_client.js
CHANGED
|
@@ -5,7 +5,33 @@ const {
|
|
|
5
5
|
createHookRegistry, applyPlugin, buildPluginContext, registerHook,
|
|
6
6
|
runHooks, getPluginApis
|
|
7
7
|
} = require('./plugin');
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
const NATIVE_PATH = path.join(__dirname, '..', 'native', 'jsql-neo-native.node');
|
|
10
|
+
|
|
11
|
+
// 真正的纯 JS 回退:当 .node 文件不存在或加载失败时,提供 stub native 对象
|
|
12
|
+
// 使得纯 JS 环境(无 Rust 编译环境)也能基本运行(功能受限)。
|
|
13
|
+
function _loadNativeOrStub() {
|
|
14
|
+
try {
|
|
15
|
+
return require(NATIVE_PATH);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.warn(`[jsql-neo] native module unavailable (${e.code || e.message}), using stub.`);
|
|
18
|
+
console.warn('[jsql-neo] stub mode: limited functionality, install Rust + cargo build for full support.');
|
|
19
|
+
// 返回一个 stub 对象,所有函数都抛 NotSupported
|
|
20
|
+
const stub = new Proxy({}, {
|
|
21
|
+
get(_, prop) {
|
|
22
|
+
if (prop === 'then' || prop === 'catch') return undefined; // not a thenable
|
|
23
|
+
return (...args) => {
|
|
24
|
+
const err = new Error(`Native function '${String(prop)}' not available in stub mode`);
|
|
25
|
+
err.code = 'NATIVE_UNAVAILABLE';
|
|
26
|
+
throw err;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return stub;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const native = _loadNativeOrStub();
|
|
9
35
|
const INT64_TAG = 1;
|
|
10
36
|
const FLOAT_TAG = 2;
|
|
11
37
|
const STR_TAG = 3;
|
package/package.json
CHANGED
package/test/btree.test.js
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模糊测试必须**可复现**。
|
|
3
|
+
*
|
|
4
|
+
* 此前直接用 rand():同一个 t21 用例两次跑出不同 size(38 / 37),
|
|
5
|
+
* 失败无法复现,也就无法判断是 B-tree 的 bug 还是用例的 bug。
|
|
6
|
+
*
|
|
7
|
+
* 换成 mulberry32 + 固定种子后,同样的输入必然复现同样的结果,
|
|
8
|
+
* CI 上的失败才有意义。换一批随机序列:设环境变量 XQL_BTREE_SEED。
|
|
9
|
+
*/
|
|
10
|
+
function makeSeededRandom(seed) {
|
|
11
|
+
let a = seed >>> 0;
|
|
12
|
+
return function () {
|
|
13
|
+
a = (a + 0x6D2B79F5) | 0;
|
|
14
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
15
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
16
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/*
|
|
20
|
+
* 默认种子固定,保证 CI 结果稳定。换一批随机序列:设 XQL_BTREE_SEED。
|
|
21
|
+
*
|
|
22
|
+
* 注:2026-10-19 修过一个真实的结构损坏 bug —— remove() 删掉叶子键后没更新
|
|
23
|
+
* 祖先的分隔键副本,导致分隔键指向不存在的值,随后 borrow/merge 把它复制成
|
|
24
|
+
* 重复键。最小复现已作为回归用例固定在下方(search / entries /
|
|
25
|
+
* greaterThan / lessThan / validate 全部比对参考实现)。
|
|
26
|
+
*/
|
|
27
|
+
const rand = makeSeededRandom(Number(process.env.XQL_BTREE_SEED || 42));
|
|
28
|
+
|
|
1
29
|
/*
|
|
2
30
|
* B-Tree 专项回归测试 — 覆盖 GitHub Issue #3 的三个 bug 及随机混合操作不变量。
|
|
3
31
|
* node test/btree.test.js
|
|
@@ -97,7 +125,7 @@ for (const order of [4, 8, 16, 64]) {
|
|
|
97
125
|
const bt = new BTree(order);
|
|
98
126
|
const n = 200;
|
|
99
127
|
const seq = [...Array(n).keys()];
|
|
100
|
-
for (let i = n - 1; i > 0; i--) { const j = Math.floor(
|
|
128
|
+
for (let i = n - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [seq[i], seq[j]] = [seq[j], seq[i]]; }
|
|
101
129
|
for (const k of seq) bt.insert(k, k);
|
|
102
130
|
const e = validate(bt);
|
|
103
131
|
ok('o' + order + ' insert invariant t' + trial, e.length === 0, e.slice(0, 2));
|
|
@@ -111,16 +139,16 @@ for (let t = 0; t < 25; t++) {
|
|
|
111
139
|
const order = [4, 8, 64][t % 3];
|
|
112
140
|
const bt = new BTree(order);
|
|
113
141
|
const ref = new Map(); // key -> Set(rowIndex)
|
|
114
|
-
let okAll = true;
|
|
142
|
+
let okAll = true, reason = '';
|
|
115
143
|
for (let op = 0; op < 300; op++) {
|
|
116
|
-
const k = Math.floor(
|
|
117
|
-
if (
|
|
118
|
-
const r = Math.floor(
|
|
144
|
+
const k = Math.floor(rand() * 40);
|
|
145
|
+
if (rand() < 0.55) {
|
|
146
|
+
const r = Math.floor(rand() * 5);
|
|
119
147
|
bt.insert(k, r);
|
|
120
148
|
if (!ref.has(k)) ref.set(k, new Set());
|
|
121
149
|
ref.get(k).add(r);
|
|
122
150
|
} else {
|
|
123
|
-
const r = Math.floor(
|
|
151
|
+
const r = Math.floor(rand() * 5);
|
|
124
152
|
bt.remove(k, r);
|
|
125
153
|
if (ref.has(k)) { ref.get(k).delete(r); if (ref.get(k).size === 0) ref.delete(k); }
|
|
126
154
|
}
|
|
@@ -129,25 +157,51 @@ for (let t = 0; t < 25; t++) {
|
|
|
129
157
|
const got = bt.search(x);
|
|
130
158
|
const exp = ref.has(x) ? [...ref.get(x)].sort((a, b) => a - b) : [];
|
|
131
159
|
const g = got ? [...got].sort((a, b) => a - b) : [];
|
|
132
|
-
if (JSON.stringify(g) !== JSON.stringify(exp)) okAll = false;
|
|
160
|
+
if (JSON.stringify(g) !== JSON.stringify(exp)) { okAll = false; reason = 'search(key=' + x + ')'; }
|
|
133
161
|
}
|
|
134
162
|
const es = bt.entries();
|
|
135
163
|
const emap = new Map(es.map(e => [e.key, [...e.value].sort((a, b) => a - b)]));
|
|
136
164
|
for (const [key, vals] of ref) {
|
|
137
165
|
const g = emap.get(key);
|
|
138
|
-
if (!g || JSON.stringify([...vals].sort()) !== JSON.stringify(g)) okAll = false;
|
|
166
|
+
if (!g || JSON.stringify([...vals].sort()) !== JSON.stringify(g)) { okAll = false; reason = reason || ('entries(key=' + key + ')'); }
|
|
139
167
|
}
|
|
140
168
|
const gtExp = [...ref.keys()].filter(k => k > 20).flatMap(k => [...ref.get(k)]).sort((a, b) => a - b);
|
|
141
169
|
const gt = bt.greaterThan(20).sort((a, b) => a - b);
|
|
142
|
-
if (JSON.stringify(gt) !== JSON.stringify(gtExp)) okAll = false;
|
|
170
|
+
if (JSON.stringify(gt) !== JSON.stringify(gtExp)) { okAll = false; reason = reason || 'greaterThan(20)'; }
|
|
143
171
|
const ltExp = [...ref.keys()].filter(k => k < 20).flatMap(k => [...ref.get(k)]).sort((a, b) => a - b);
|
|
144
172
|
const lt = bt.lessThan(20).sort((a, b) => a - b);
|
|
145
|
-
if (JSON.stringify(lt) !== JSON.stringify(ltExp)) okAll = false;
|
|
146
|
-
|
|
173
|
+
if (JSON.stringify(lt) !== JSON.stringify(ltExp)) { okAll = false; reason = reason || 'lessThan(20)'; }
|
|
174
|
+
const vErr = validate(bt); if (vErr.length) { okAll = false; reason = reason || ('validate: ' + vErr[0]); }
|
|
147
175
|
}
|
|
148
176
|
}
|
|
149
|
-
ok('random mixed t' + t + ' vs reference', okAll && bt.size === ref.size, { size: bt.size, ref: ref.size });
|
|
177
|
+
ok('random mixed t' + t + ' vs reference', okAll && bt.size === ref.size, { size: bt.size, ref: ref.size, reason: reason });
|
|
150
178
|
}
|
|
151
179
|
|
|
152
180
|
console.log(failed === 0 ? `\nALL ${passed} BTREE TESTS PASSED` : `\n${failed} FAILURES (${passed} passed)`);
|
|
153
181
|
process.exit(failed === 0 ? 0 : 1);
|
|
182
|
+
|
|
183
|
+
/* ---- 回归:remove 后分隔键未更新导致内部键重复 ----
|
|
184
|
+
*
|
|
185
|
+
* 旧实现里 remove() 只改叶子,不更新祖先的分隔键副本。删掉某个正好是分隔键
|
|
186
|
+
* 的叶子键后,父节点的 keys[i] 就指向了一个不存在的值 —— 内部键仍能路由对,
|
|
187
|
+
* 但不再严格递增,下一次 borrow 会把它复制成重复键([24,25] → [24,24]),
|
|
188
|
+
* 之后 key=25 会被路由到错误的子树。
|
|
189
|
+
*/
|
|
190
|
+
{
|
|
191
|
+
const bt = new BTree(4);
|
|
192
|
+
const ref = new Map();
|
|
193
|
+
const ops = [[1,28,1],[1,24,4],[1,0,0],[1,14,2],[0,24,4],[1,25,4],[1,24,2],[1,28,1],[0,26,2]];
|
|
194
|
+
let okAll = true, reason = '';
|
|
195
|
+
for (const [ins, k, r] of ops) {
|
|
196
|
+
if (ins) { bt.insert(k, r); if (!ref.has(k)) ref.set(k, new Set()); ref.get(k).add(r); }
|
|
197
|
+
else { bt.remove(k, r); if (ref.has(k)) { ref.get(k).delete(r); if (ref.get(k).size === 0) ref.delete(k); } }
|
|
198
|
+
}
|
|
199
|
+
for (const [k, vals] of ref) {
|
|
200
|
+
const got = bt.search(k).slice().sort((a,b)=>a-b);
|
|
201
|
+
const exp = [...vals].sort((a,b)=>a-b);
|
|
202
|
+
if (JSON.stringify(got) !== JSON.stringify(exp)) { okAll = false; reason = 'search(' + k + ')'; }
|
|
203
|
+
}
|
|
204
|
+
const errs = validate(bt);
|
|
205
|
+
if (errs.length) { okAll = false; reason = reason || ('validate: ' + errs[0]); }
|
|
206
|
+
ok('回归:remove 后分隔键未更新', okAll && bt.size === ref.size, { size: bt.size, ref: ref.size, reason, keys: bt._root.keys });
|
|
207
|
+
}
|