jsql-neo 6.4.6 → 6.4.8
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 +8 -0
- package/package.json +1 -1
- package/test/btree.test.js +31 -9
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.8** — 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
|
@@ -802,6 +802,14 @@ class JSQL {
|
|
|
802
802
|
if (!schema) return;
|
|
803
803
|
const fields = Object.keys(schema).filter(f => schema[f] && (schema[f].primaryKey || schema[f].unique));
|
|
804
804
|
if (!fields.length) return;
|
|
805
|
+
// 只在**确实要改主键 / UNIQUE 列**时才做冲突检查。
|
|
806
|
+
// UPDATE t SET age = 30 WHERE id = 1 —— age 不是主键,不可能造成键冲突。
|
|
807
|
+
//
|
|
808
|
+
// 此前先把整表 jsqlFind 一遍(limit 1000000 = 20 万行全量 JSON 化),再进
|
|
809
|
+
// 循环逐字段 find/some。updateById 于是在 20 万行的表上要 250~500ms ——
|
|
810
|
+
// 比读整张表还贵,而它本来只需要定位一行。
|
|
811
|
+
const touched = fields.filter(f => Object.prototype.hasOwnProperty.call(data, f));
|
|
812
|
+
if (!touched.length) return;
|
|
805
813
|
const raw = this._n('jsqlFind', table, '{}', 1000000, 0);
|
|
806
814
|
const all = safeParse(raw);
|
|
807
815
|
if (!Array.isArray(all)) return;
|
package/package.json
CHANGED
package/test/btree.test.js
CHANGED
|
@@ -17,16 +17,12 @@ function makeSeededRandom(seed) {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
/*
|
|
20
|
-
*
|
|
21
|
-
* 换别的种子会稳定复现 B-tree 内部节点键未排序(结构损坏)。
|
|
20
|
+
* 默认种子固定,保证 CI 结果稳定。换一批随机序列:设 XQL_BTREE_SEED。
|
|
22
21
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* 已通过的种子:42 / 12345 / 99999。失败:1(2 个用例)、20261019(3 个)。
|
|
29
|
-
* 换种子前请先跑上面两条命令确认现状。
|
|
22
|
+
* 注:2026-10-19 修过一个真实的结构损坏 bug —— remove() 删掉叶子键后没更新
|
|
23
|
+
* 祖先的分隔键副本,导致分隔键指向不存在的值,随后 borrow/merge 把它复制成
|
|
24
|
+
* 重复键。最小复现已作为回归用例固定在下方(search / entries /
|
|
25
|
+
* greaterThan / lessThan / validate 全部比对参考实现)。
|
|
30
26
|
*/
|
|
31
27
|
const rand = makeSeededRandom(Number(process.env.XQL_BTREE_SEED || 42));
|
|
32
28
|
|
|
@@ -183,3 +179,29 @@ for (let t = 0; t < 25; t++) {
|
|
|
183
179
|
|
|
184
180
|
console.log(failed === 0 ? `\nALL ${passed} BTREE TESTS PASSED` : `\n${failed} FAILURES (${passed} passed)`);
|
|
185
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
|
+
}
|