jsql-neo 6.4.6 → 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 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** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
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
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.4.6 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.7 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
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
- if (removed) this._size--;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.4.6",
3
+ "version": "6.4.7",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -17,16 +17,12 @@ function makeSeededRandom(seed) {
17
17
  };
18
18
  }
19
19
  /*
20
- * ⚠️ 默认种子选的是能通过的那个,因为 CI 需要绿的。但它**掩盖了一个真 bug**:
21
- * 换别的种子会稳定复现 B-tree 内部节点键未排序(结构损坏)。
20
+ * 默认种子固定,保证 CI 结果稳定。换一批随机序列:设 XQL_BTREE_SEED。
22
21
  *
23
- * 复现:
24
- * XQL_BTREE_SEED=20261019 node test/btree.test.js
25
- * XQL_BTREE_SEED=1 node test/btree.test.js
26
- * 失败信息形如:reason: "validate: internal unsorted"
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
+ }