jsql-neo 6.4.0-beta.15 → 6.4.0-beta.16

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.0-beta.15** — 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.0-beta.16** — 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.0-beta.15 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.0-beta.16 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/btree.js CHANGED
@@ -171,9 +171,10 @@ class BTree {
171
171
  for (let i = 0; i < node.keys.length; i++) {
172
172
  const k = node.keys[i];
173
173
  const s = k === undefined || k === null ? '' : String(k);
174
- if (s < p) continue;
175
- if (!s.startsWith(p)) return result;
176
- result.push(...node.values[i]);
174
+ // 不能假设键按字典序排序(数字键按数值序),必须检查所有键
175
+ if (s.startsWith(p)) {
176
+ result.push(...node.values[i]);
177
+ }
177
178
  }
178
179
  node = node.next;
179
180
  }
@@ -326,13 +327,14 @@ class BTree {
326
327
  const left = parent.children[i - 1];
327
328
  const right = parent.children[i];
328
329
  if (right.leaf) {
329
- // 叶子节点:分隔键是叶子的副本,用被移动的键而非旧分隔键,避免重复
330
+ // 叶子节点:移动左叶最大键到右叶最小位置
330
331
  const k = left.keys.pop();
331
332
  const v = left.values.pop();
332
333
  right.keys.unshift(k);
333
334
  right.values.unshift(v);
334
- parent.keys[i - 1] = k;
335
- parent.values[i - 1] = v;
335
+ // 父节点分隔键应为左叶新的最大值(pop 后的最后一个键)
336
+ parent.keys[i - 1] = left.keys[left.keys.length - 1];
337
+ parent.values[i - 1] = left.values[left.values.length - 1];
336
338
  } else {
337
339
  right.keys.unshift(parent.keys[i - 1]);
338
340
  right.values.unshift(parent.values[i - 1]);
package/lib/query.js CHANGED
@@ -675,6 +675,12 @@ class Query {
675
675
  for (const { field, dir } of this._orderBy) {
676
676
  const va = a[field];
677
677
  const vb = b[field];
678
+ // MySQL 语义:NULL 在 ASC 时排在最前,DESC 时排在最后
679
+ const aNull = va === null || va === undefined;
680
+ const bNull = vb === null || vb === undefined;
681
+ if (aNull && bNull) continue;
682
+ if (aNull) return dir === 'asc' ? -1 : 1;
683
+ if (bNull) return dir === 'asc' ? 1 : -1;
678
684
  if (va < vb) return dir === 'asc' ? -1 : 1;
679
685
  if (va > vb) return dir === 'asc' ? 1 : -1;
680
686
  }
package/lib/sql.js CHANGED
@@ -2908,7 +2908,7 @@ function resolveOperand(operand, row, ctx) {
2908
2908
  }
2909
2909
  // P1-19: ON DUPLICATE KEY UPDATE 表达式赋值,如 count = count + 1
2910
2910
  case 'binary': {
2911
- return evaluateExpr({ type: 'arith', op: operand.op, left: operand.left, right: operand.right }, row, ctx);
2911
+ return evaluateExpr(operand, row, ctx);
2912
2912
  }
2913
2913
  case 'func':
2914
2914
  return applyScalarFunction(operand, row, ctx);
@@ -4223,10 +4223,14 @@ function evaluateExpr(expr, row, ctx) {
4223
4223
  }
4224
4224
  case 'in': {
4225
4225
  const v = resolveOperand(expr.operand, row, ctx);
4226
+ if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
4226
4227
  if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
4227
- if (!expr.list) return false;
4228
+ if (!expr.list) return expr.not ? true : false;
4229
+ // 列表中出现 NULL 时:三值逻辑 → NULL IN (1,2,NULL) 为 NULL(UNKNOWN)
4230
+ const hasNull = expr.list.some(x => x === null || x === undefined);
4231
+ if (hasNull) return null;
4228
4232
  const r = expr.list.some(x => x === v || String(x) === String(v));
4229
- return expr.not ? !r : r; // NOT IN:此前漏了取反,导致 NOT IN 与 IN 结果相同
4233
+ return expr.not ? !r : r;
4230
4234
  }
4231
4235
  case 'like': {
4232
4236
  const v = resolveOperand(expr.operand, row, ctx);
@@ -6751,9 +6755,25 @@ class SQLExecutor {
6751
6755
 
6752
6756
  // 分组聚合
6753
6757
  if (statement.groupBy) {
6758
+ // MySQL 支持 GROUP BY alias,而 alias 指向 SELECT 列表的输出列名。
6759
+ // 解析时 groupBy[] 存的是原始字符串(如 "b"),必须先在 SELECT 别名里查找:
6760
+ // - 找到 → 取输出行里的 alias 值(因为 alias 已求出值了)
6761
+ // - 找不到 → 当列名取输入行里对应列的值(标准 SQL 行为)
6762
+ // 此前全用 resolveOperand({type:'column', name:g}) 按列名取,导致 GROUP BY b
6763
+ // 取 row['b'],而原始行里没有 b 列(b 是别名),全返回 undefined 并成一组。
6764
+ const aliases = {};
6765
+ if (statement.columns) {
6766
+ for (const c of statement.columns) {
6767
+ if (c.alias) aliases[c.alias.toLowerCase()] = true;
6768
+ }
6769
+ }
6754
6770
  const groups = new Map();
6755
6771
  for (const row of rows) {
6756
- const key = JSON.stringify(statement.groupBy.map(g => resolveOperand({ type: 'column', name: g }, row, this.ctx)));
6772
+ const key = JSON.stringify(statement.groupBy.map(g => {
6773
+ const key = String(g).toLowerCase();
6774
+ if (aliases[key] && row[key] !== undefined) return row[key];
6775
+ return resolveOperand({ type: 'column', name: g }, row, this.ctx);
6776
+ }));
6757
6777
  if (!groups.has(key)) groups.set(key, []);
6758
6778
  groups.get(key).push(row);
6759
6779
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.4.0-beta.15",
3
+ "version": "6.4.0-beta.16",
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",
@@ -0,0 +1,344 @@
1
+ // © Vexify 2026 All Rights Reserved.
2
+ /**
3
+ * Bug Hunt Tests — v6.4.0-beta.16
4
+ * Tests for bugs found during code review:
5
+ * 1. BTree _borrowFromLeft wrong parent key (leaf nodes)
6
+ * 2. Query _applyOrderBy missing NULL handling
7
+ * 3. BTree prefix incorrect early return for numeric keys
8
+ *
9
+ * node test/bug-hunt.test.js
10
+ */
11
+ const BTree = require('../lib/btree');
12
+ const Query = require('../lib/query');
13
+
14
+ let passed = 0, failed = 0;
15
+ function ok(name, cond, extra) {
16
+ if (cond) { passed++; }
17
+ else { failed++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra) : ''); }
18
+ }
19
+
20
+ // ============================================================
21
+ // Helper: Mock Table for Query tests
22
+ // ============================================================
23
+ function createMockTable(name, rows, schema = {}) {
24
+ const table = {
25
+ _name: name,
26
+ _rows: rows,
27
+ _schema: schema,
28
+ _btrees: {},
29
+ _indexes: {},
30
+ _applyFilterOptimized(rows, query) {
31
+ return rows.filter(row => {
32
+ for (const [field, condition] of Object.entries(query)) {
33
+ const value = row[field];
34
+ if (condition !== null && typeof condition === 'object' && !Array.isArray(condition)) {
35
+ for (const [op, target] of Object.entries(condition)) {
36
+ if (op === '$gt' && !(value > target)) return false;
37
+ if (op === '$gte' && !(value >= target)) return false;
38
+ if (op === '$lt' && !(value < target)) return false;
39
+ if (op === '$lte' && !(value <= target)) return false;
40
+ if (op === '$eq' && value !== target) return false;
41
+ if (op === '$ne' && value === target) return false;
42
+ if (op === '$in' && !target.includes(value)) return false;
43
+ }
44
+ } else {
45
+ if (value !== condition) return false;
46
+ }
47
+ }
48
+ return true;
49
+ });
50
+ },
51
+ _applyFilter(rows, query) { return this._applyFilterOptimized(rows, query); },
52
+ _matchRow(row, query) {
53
+ for (const [field, condition] of Object.entries(query)) {
54
+ const value = row[field];
55
+ if (condition !== null && typeof condition === 'object' && !Array.isArray(condition)) {
56
+ for (const [op, target] of Object.entries(condition)) {
57
+ if (op === '$gt' && !(value > target)) return false;
58
+ if (op === '$gte' && !(value >= target)) return false;
59
+ if (op === '$lt' && !(value < target)) return false;
60
+ if (op === '$lte' && !(value <= target)) return false;
61
+ if (op === '$eq' && value !== target) return false;
62
+ if (op === '$ne' && value === target) return false;
63
+ if (op === '$in' && !target.includes(value)) return false;
64
+ }
65
+ } else {
66
+ if (value !== condition) return false;
67
+ }
68
+ }
69
+ return true;
70
+ },
71
+ _getVisibleRows() { return this._rows; },
72
+ _db: {
73
+ _logSlowQuery() {},
74
+ _tables: {},
75
+ },
76
+ };
77
+ return table;
78
+ }
79
+
80
+ // ============================================================
81
+ // Bug 1: BTree _borrowFromLeft wrong parent key
82
+ // ============================================================
83
+ console.log('\n=== Bug 1: BTree _borrowFromLeft parent key ===');
84
+
85
+ {
86
+ const tree = new BTree(4, false);
87
+ for (let i = 0; i < 20; i++) tree.insert(i, i);
88
+
89
+ // Delete from the right side to trigger left borrows
90
+ for (let i = 19; i >= 10; i--) tree.remove(i, i);
91
+
92
+ // After deletions, search for remaining keys should work
93
+ let allFound = true;
94
+ for (let i = 0; i < 10; i++) {
95
+ const result = tree.search(i);
96
+ if (result.length === 0 || !result.includes(i)) { allFound = false; break; }
97
+ }
98
+ ok('Bug1: search after left borrows', allFound);
99
+
100
+ // Range query should return correct results
101
+ const rangeResult = tree.range(0, 9);
102
+ ok('Bug1: range [0,9] returns 10 items', rangeResult.length === 10, { len: rangeResult.length });
103
+ }
104
+
105
+ {
106
+ const tree = new BTree(4, false);
107
+ for (let i = 0; i < 50; i++) tree.insert(i, i);
108
+
109
+ // Delete every other key to force rebalancing
110
+ for (let i = 1; i < 50; i += 2) tree.remove(i, i);
111
+
112
+ // Verify all remaining keys are searchable
113
+ let allEvenFound = true;
114
+ for (let i = 0; i < 50; i += 2) {
115
+ const result = tree.search(i);
116
+ if (result.length === 0 || !result.includes(i)) { allEvenFound = false; break; }
117
+ }
118
+ ok('Bug1: even keys after heavy deletions', allEvenFound);
119
+
120
+ // Verify deleted keys are gone
121
+ let allOddGone = true;
122
+ for (let i = 1; i < 50; i += 2) {
123
+ const result = tree.search(i);
124
+ if (result.length !== 0) { allOddGone = false; break; }
125
+ }
126
+ ok('Bug1: odd keys deleted', allOddGone);
127
+
128
+ // Greater than query
129
+ const gtResult = tree.greaterThan(25);
130
+ ok('Bug1: greaterThan(25) finds keys', gtResult.length > 0);
131
+
132
+ // Less than query
133
+ const ltResult = tree.lessThan(25);
134
+ ok('Bug1: lessThan(25) finds keys', ltResult.length > 0);
135
+ }
136
+
137
+ {
138
+ const tree = new BTree(4, true);
139
+ for (let i = 0; i < 30; i++) tree.insert(i * 10, i);
140
+
141
+ // Delete from the right half to trigger left borrows
142
+ for (let i = 29; i >= 15; i--) tree.remove(i * 10, i);
143
+
144
+ // Verify remaining keys
145
+ let allFound = true;
146
+ for (let i = 0; i < 15; i++) {
147
+ const result = tree.search(i * 10);
148
+ if (result.length !== 1 || result[0] !== i) { allFound = false; break; }
149
+ }
150
+ ok('Bug1: unique index with left borrows', allFound);
151
+ }
152
+
153
+ // ============================================================
154
+ // Bug 2: Query _applyOrderBy missing NULL handling
155
+ // ============================================================
156
+ console.log('\n=== Bug 2: Query _applyOrderBy NULL handling ===');
157
+
158
+ {
159
+ const rows = [
160
+ { id: 1, name: 'Alice', age: 30 },
161
+ { id: 2, name: 'Bob', age: null },
162
+ { id: 3, name: 'Charlie', age: 25 },
163
+ { id: 4, name: 'Dave', age: null },
164
+ ];
165
+ const table = createMockTable('users', rows);
166
+ const query = new Query(table);
167
+ query.orderBy('age', 'asc');
168
+ const result = query.get();
169
+
170
+ ok('Bug2: ASC nulls first (1st)', result[0].age === null);
171
+ ok('Bug2: ASC nulls first (2nd)', result[1].age === null);
172
+ ok('Bug2: ASC 25 before 30', result[2].age === 25);
173
+ ok('Bug2: ASC 30 last', result[3].age === 30);
174
+ }
175
+
176
+ {
177
+ const rows = [
178
+ { id: 1, name: 'Alice', age: 30 },
179
+ { id: 2, name: 'Bob', age: null },
180
+ { id: 3, name: 'Charlie', age: 25 },
181
+ { id: 4, name: 'Dave', age: null },
182
+ ];
183
+ const table = createMockTable('users', rows);
184
+ const query = new Query(table);
185
+ query.orderBy('age', 'desc');
186
+ const result = query.get();
187
+
188
+ ok('Bug2: DESC nulls last (1st=30)', result[0].age === 30);
189
+ ok('Bug2: DESC nulls last (2nd=25)', result[1].age === 25);
190
+ ok('Bug2: DESC nulls last (3rd=null)', result[2].age === null);
191
+ ok('Bug2: DESC nulls last (4th=null)', result[3].age === null);
192
+ }
193
+
194
+ {
195
+ const rows = [
196
+ { id: 1, name: 'Alice', age: null },
197
+ { id: 2, name: 'Bob', age: null },
198
+ { id: 3, name: 'Charlie', age: null },
199
+ ];
200
+ const table = createMockTable('users', rows);
201
+ const query = new Query(table);
202
+ query.orderBy('age', 'asc');
203
+ const result = query.get();
204
+
205
+ ok('Bug2: all nulls ASC', result.length === 3 && result.every(r => r.age === null));
206
+ }
207
+
208
+ {
209
+ const rows = [
210
+ { id: 1, name: 'Alice', score: 85 },
211
+ { id: 2, name: 'Bob' }, // score is undefined
212
+ { id: 3, name: 'Charlie', score: null },
213
+ { id: 4, name: 'Dave', score: 90 },
214
+ ];
215
+ const table = createMockTable('users', rows);
216
+ const query = new Query(table);
217
+ query.orderBy('score', 'asc');
218
+ const result = query.get();
219
+
220
+ // Both null and undefined sort as null; stable sort preserves original order
221
+ // Bob (undefined) was at index 1, Charlie (null) at index 2
222
+ ok('Bug2: mixed null/undefined ASC (1st=null/undefined)', result[0].score == null);
223
+ ok('Bug2: mixed null/undefined ASC (2nd=null/undefined)', result[1].score == null);
224
+ ok('Bug2: mixed null/undefined ASC (3rd=85)', result[2].score === 85);
225
+ ok('Bug2: mixed null/undefined ASC (4th=90)', result[3].score === 90);
226
+ }
227
+
228
+ {
229
+ const rows = [
230
+ { id: 1, department: 'Engineering', salary: 5000 },
231
+ { id: 2, department: 'Engineering', salary: null },
232
+ { id: 3, department: 'Sales', salary: 4000 },
233
+ { id: 4, department: 'Sales', salary: null },
234
+ { id: 5, department: 'Engineering', salary: 6000 },
235
+ ];
236
+ const table = createMockTable('employees', rows);
237
+ const query = new Query(table);
238
+ query.orderBy('department', 'asc').orderBy('salary', 'asc');
239
+ const result = query.get();
240
+
241
+ ok('Bug2: multi-field orderBy', result.length === 5);
242
+ ok('Bug2: Eng null salary first', result[0].department === 'Engineering' && result[0].salary === null);
243
+ ok('Bug2: Eng 5000 second', result[1].department === 'Engineering' && result[1].salary === 5000);
244
+ ok('Bug2: Eng 6000 third', result[2].department === 'Engineering' && result[2].salary === 6000);
245
+ ok('Bug2: Sales null salary fourth', result[3].department === 'Sales' && result[3].salary === null);
246
+ ok('Bug2: Sales 4000 fifth', result[4].department === 'Sales' && result[4].salary === 4000);
247
+ }
248
+
249
+ // ============================================================
250
+ // Bug 3: BTree prefix incorrect early return for numeric keys
251
+ // ============================================================
252
+ console.log('\n=== Bug 3: BTree prefix with numeric keys ===');
253
+
254
+ {
255
+ const tree = new BTree(64, false);
256
+ const keys = [1, 2, 10, 20, 100, 200, 1000, 2000];
257
+ for (const key of keys) tree.insert(key, key);
258
+
259
+ const result = tree.prefix('1');
260
+ ok('Bug3: prefix "1" finds 1', result.includes(1));
261
+ ok('Bug3: prefix "1" finds 10', result.includes(10));
262
+ ok('Bug3: prefix "1" finds 100', result.includes(100));
263
+ ok('Bug3: prefix "1" finds 1000', result.includes(1000));
264
+ ok('Bug3: prefix "1" excludes 2', !result.includes(2));
265
+ ok('Bug3: prefix "1" excludes 20', !result.includes(20));
266
+ ok('Bug3: prefix "1" excludes 200', !result.includes(200));
267
+ ok('Bug3: prefix "1" excludes 2000', !result.includes(2000));
268
+ }
269
+
270
+ {
271
+ const tree = new BTree(64, false);
272
+ const keys = [2, 20, 200, 2000, 3, 30, 300];
273
+ for (const key of keys) tree.insert(key, key);
274
+
275
+ const result = tree.prefix('2');
276
+ ok('Bug3: prefix "2" finds 2', result.includes(2));
277
+ ok('Bug3: prefix "2" finds 20', result.includes(20));
278
+ ok('Bug3: prefix "2" finds 200', result.includes(200));
279
+ ok('Bug3: prefix "2" finds 2000', result.includes(2000));
280
+ ok('Bug3: prefix "2" excludes 3', !result.includes(3));
281
+ }
282
+
283
+ {
284
+ const tree = new BTree(64, false);
285
+ const keys = ['apple', 'application', 'apply', 'banana', 'cherry'];
286
+ for (const key of keys) tree.insert(key, key);
287
+
288
+ const result = tree.prefix('app');
289
+ ok('Bug3: string prefix finds apple', result.includes('apple'));
290
+ ok('Bug3: string prefix finds application', result.includes('application'));
291
+ ok('Bug3: string prefix finds apply', result.includes('apply'));
292
+ ok('Bug3: string prefix excludes banana', !result.includes('banana'));
293
+ }
294
+
295
+ {
296
+ const tree = new BTree(4, false);
297
+ for (let i = 1; i <= 100; i++) tree.insert(i, i);
298
+
299
+ // Prefix '1' should match: 1, 10-19, 100
300
+ const result = tree.prefix('1');
301
+ ok('Bug3: multi-leaf prefix "1" finds 1', result.includes(1));
302
+ ok('Bug3: multi-leaf prefix "1" finds 10', result.includes(10));
303
+ ok('Bug3: multi-leaf prefix "1" finds 11', result.includes(11));
304
+ ok('Bug3: multi-leaf prefix "1" finds 19', result.includes(19));
305
+ ok('Bug3: multi-leaf prefix "1" finds 100', result.includes(100));
306
+
307
+ // Prefix '9' should match: 9, 90-99
308
+ const result9 = tree.prefix('9');
309
+ ok('Bug3: multi-leaf prefix "9" finds 9', result9.includes(9));
310
+ ok('Bug3: multi-leaf prefix "9" finds 90', result9.includes(90));
311
+ ok('Bug3: multi-leaf prefix "9" finds 99', result9.includes(99));
312
+ }
313
+
314
+ {
315
+ const tree = new BTree(64, false);
316
+ tree.insert(1, 1);
317
+ tree.insert(2, 2);
318
+ tree.insert(3, 3);
319
+
320
+ const result = tree.prefix('9');
321
+ ok('Bug3: non-matching prefix returns empty', result.length === 0);
322
+ }
323
+
324
+ {
325
+ const tree = new BTree(64, false);
326
+ tree.insert('a.b', 1);
327
+ tree.insert('a*b', 2);
328
+ tree.insert('a+b', 3);
329
+ tree.insert('abc', 4);
330
+
331
+ const result = tree.prefix('a');
332
+ ok('Bug3: special chars prefix finds a.b', result.includes(1));
333
+ ok('Bug3: special chars prefix finds a*b', result.includes(2));
334
+ ok('Bug3: special chars prefix finds a+b', result.includes(3));
335
+ ok('Bug3: special chars prefix finds abc', result.includes(4));
336
+ }
337
+
338
+ // ============================================================
339
+ // Summary
340
+ // ============================================================
341
+ console.log('\n========================================');
342
+ console.log(`Results: ${passed} passed, ${failed} failed`);
343
+ if (failed > 0) process.exit(1);
344
+ else console.log('All tests passed!');