diff --git a/utils/__tests__/bPlusTreeLogic.test.ts b/utils/__tests__/bPlusTreeLogic.test.ts index b99a19d..a40745b 100644 --- a/utils/__tests__/bPlusTreeLogic.test.ts +++ b/utils/__tests__/bPlusTreeLogic.test.ts @@ -390,6 +390,51 @@ describe('BPlusTree', () => { }); }); + describe('链表完整性 - borrowFromRight 场景', () => { + it('借用右兄弟后叶子 next 链表指针应保持完整', () => { + const tree = new BPlusTree((r: TableRow) => r.id, { uniqueKeys: true }); + // Insert [1..7] then delete 1 to trigger borrowFromRight on the leftmost leaf + const rows = Array.from({ length: 7 }, (_, i) => ({ id: i + 1, name: `U${i + 1}`, age: 20 + i })); + rows.forEach(r => tree.insert(r)); + tree.delete(1); + + // Traverse linked list from leftmost leaf + let leaf: any = tree.root; + while (!leaf.isLeaf) leaf = leaf.children[0]; + + const keys: number[] = []; + while (leaf) { + keys.push(...leaf.keys); + leaf = leaf.next; + } + + // All remaining keys should be reachable in sorted order + expect(keys).toEqual([2, 3, 4, 5, 6, 7]); + }); + + it('大量插入删除后 next 链路应完整且有序', () => { + const tree = new BPlusTree((r: TableRow) => r.id, { uniqueKeys: true }); + const rows = Array.from({ length: 20 }, (_, i) => ({ id: i + 1, name: `U${i + 1}`, age: 20 + i })); + rows.forEach(r => tree.insert(r)); + + // Delete 10 keys spread across the tree + [2, 4, 6, 8, 10, 12, 14, 16, 18, 20].forEach(k => tree.delete(k)); + + // Traverse linked list from leftmost leaf + let leaf: any = tree.root; + while (!leaf.isLeaf) leaf = leaf.children[0]; + + const keys: number[] = []; + while (leaf) { + keys.push(...leaf.keys); + leaf = leaf.next; + } + + // Only odd ids should remain, in sorted order + expect(keys).toEqual([1, 3, 5, 7, 9, 11, 13, 15, 17, 19]); + }); + }); + describe('分隔键不变量', () => { it('删除叶子首key但不下溢时应更新父分隔键', () => { const tree = new BPlusTree((r: TableRow) => r.id, { uniqueKeys: true }); @@ -421,6 +466,16 @@ describe('BPlusTree', () => { assertSeparatorInvariants(tree.root); }); + + it('借用右兄弟后父分隔键应满足不变量', () => { + const tree = new BPlusTree((r: TableRow) => r.id, { uniqueKeys: true }); + // Insert [1..7] then delete 1 to trigger borrowFromRight + const rows = Array.from({ length: 7 }, (_, i) => ({ id: i + 1, name: `U${i + 1}`, age: 20 + i })); + rows.forEach(r => tree.insert(r)); + tree.delete(1); + + assertSeparatorInvariants(tree.root); + }); }); }); diff --git a/utils/bPlusTreeLogic.ts b/utils/bPlusTreeLogic.ts index 51121d0..de22dde 100644 --- a/utils/bPlusTreeLogic.ts +++ b/utils/bPlusTreeLogic.ts @@ -282,12 +282,19 @@ export class BPlusTree { const borrowedKey = rightSibling.keys.shift()!; const borrowedData = rightSibling.data.shift()!; - // Update parent's separator key to the new minimum key in right sibling + // After borrowing, update the separator key in parent to the new minimum key + // of the right sibling (i.e., its first remaining key). + // This maintains the B+ tree invariant: parent.keys[childIdx] == rightSibling.min if (rightSibling.keys.length > 0) { parent.keys[childIdx] = rightSibling.keys[0]; } else { - // Right sibling became empty - should trigger merge instead - console.warn('Right sibling became empty after borrow, consider merging instead'); + // This should never happen: handleUnderflow only calls borrowFromRight when + // rightSibling.keys.length > MIN_KEYS, guaranteeing at least 2 keys before borrow. + // If it does happen, the tree structure is already corrupted; log a clear error. + console.error( + '[BPlusTree] borrowFromRight: right sibling became empty after borrow. ' + + 'This indicates a bug in handleUnderflow. Tree structure may be corrupted.' + ); } node.keys.push(borrowedKey);