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 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.5** — 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.5 — 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;
@@ -5,7 +5,33 @@ const {
5
5
  createHookRegistry, applyPlugin, buildPluginContext, registerHook,
6
6
  runHooks, getPluginApis
7
7
  } = require('./plugin');
8
- const native = require(path.join(__dirname, '..', 'native', 'jsql-neo-native.node'));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.4.5",
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",
@@ -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(Math.random() * (i + 1)); [seq[i], seq[j]] = [seq[j], seq[i]]; }
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(Math.random() * 40);
117
- if (Math.random() < 0.55) {
118
- const r = Math.floor(Math.random() * 5);
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(Math.random() * 5);
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
- if (validate(bt).length) okAll = false;
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
+ }