Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions utils/__tests__/bPlusTreeLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
});
});
});

Expand Down
13 changes: 10 additions & 3 deletions utils/bPlusTreeLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down