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 +1 -1
- package/index.js +1 -1
- package/lib/btree.js +8 -6
- package/lib/query.js +6 -0
- package/lib/sql.js +24 -4
- package/package.json +1 -1
- package/test/bug-hunt.test.js +344 -0
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.
|
|
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
|

|
|
9
9
|

|
package/index.js
CHANGED
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
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
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
|
-
|
|
335
|
-
parent.
|
|
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(
|
|
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;
|
|
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 =>
|
|
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
|
@@ -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!');
|