Skip to content

Fix borrowFromRight: replace silent warn with error log + add linked-list/separator invariant tests#1

Draft
Copilot wants to merge 2 commits into
mainfrom
copilot/fix-borrow-from-right-bug
Draft

Fix borrowFromRight: replace silent warn with error log + add linked-list/separator invariant tests#1
Copilot wants to merge 2 commits into
mainfrom
copilot/fix-borrow-from-right-bug

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 4, 2026

borrowFromRight silently swallowed a corrupting edge case via console.warn, leaving parent separator keys stale. Three new tests cover borrowFromRight-triggered scenarios that were previously untested.

Changes

utils/bPlusTreeLogic.ts

  • borrowFromRight leaf branch: Replace console.warn('Right sibling became empty...') with console.error(...) carrying a clear diagnostic message. The if branch gains a comment explaining the B+ tree invariant being maintained (parent.keys[childIdx] == rightSibling.min).
// Before
} else {
  console.warn('Right sibling became empty after borrow, consider merging instead');
}

// After
} else {
  // This should never happen: handleUnderflow only calls borrowFromRight when
  // rightSibling.keys.length > MIN_KEYS, guaranteeing at least 2 keys before borrow.
  console.error(
    '[BPlusTree] borrowFromRight: right sibling became empty after borrow. ' +
    'This indicates a bug in handleUnderflow. Tree structure may be corrupted.'
  );
}

utils/__tests__/bPlusTreeLogic.test.ts

Three new test cases targeting borrowFromRight scenarios:

  • Leaf next chain intact after borrow — inserts [1..7], deletes 1 (triggers borrowFromRight on leftmost leaf), traverses next pointers and asserts all of [2..7] are reachable in order.
  • Bulk insert/delete next chain integrity — inserts 20 rows, deletes 10 even keys, asserts the leaf linked list contains exactly the 10 odd keys in sorted order.
  • Separator invariant after borrow — same triggering scenario, calls assertSeparatorInvariants to confirm all parent separator keys satisfy the B+ tree invariant post-borrow.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • registry.npmmirror.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/_temp/ghcca-node/node/bin/npm install (dns block)
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/_temp/ghcca-node/node/bin/npm install --prefer-offline (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

问题描述

需要修复两个 P0 级别的 Bug:


Bug 1:borrowFromRight 叶子节点分隔键更新错误

文件utils/bPlusTreeLogic.tsborrowFromRight 方法(约第 276-307 行)

当前错误代码(叶子节点分支,第 280-294 行):

if (node.isLeaf) {
  // Borrow first key from right sibling
  const borrowedKey = rightSibling.keys.shift()!;
  const borrowedData = rightSibling.data.shift()!;

  // Update parent's separator key to the new minimum key in right sibling
  if (rightSibling.keys.length > 0) {
    parent.keys[childIdx] = rightSibling.keys[0];  // ❌ 错误:rightSibling.keys[0] 是借用后右兄弟的新首key,但分隔键应该是 borrowedKey
  } else {
    console.warn('Right sibling became empty after borrow, consider merging instead');
  }

  node.keys.push(borrowedKey);
  node.data.push(borrowedData);
}

根因分析

B+树叶子节点借用右兄弟的语义是:

  • 从右兄弟取走 borrowedKey,该 key 放入当前节点
  • 父节点的分隔键 parent.keys[childIdx] 的语义是"右子树的最小 key"
  • 借用后,右兄弟的最小 key 变成了 rightSibling.keys[0](即 shift() 之后的新首位),不是 borrowedKey
  • 所以父分隔键应该更新为 rightSibling.keys[0](借用后右兄弟的新最小 key)

当前代码在 rightSibling.keys.length > 0 时,使用 rightSibling.keys[0](即借用后的新首 key)这部分逻辑实际上是正确的,但存在以下问题:

  1. 如果借用后 rightSibling.keys.length === 0(即右兄弟借出后变为空),当前代码仅打印警告而没有处理,会导致父分隔键没有更新,从而破坏 B+树的查找路由不变量
  2. 需要添加防御性处理:若右兄弟借出后变为空(理论上不应发生,因为 handleUnderflow 中确保 keys.length > MIN_KEYS 才借用),需抛出明确错误而不是静默失败

修复方案

if (node.isLeaf) {
  // Borrow first key from right sibling
  const borrowedKey = rightSibling.keys.shift()!;
  const borrowedData = rightSibling.data.shift()!;

  // After borrowing, the separator key in parent must be updated 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 {
    // 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);
  node.data.push(borrowedData);
}

Bug 2:TreeVisualizer 叶子节点链路未使用真实 next 指针(已在当前代码中修复,但需验证并补充测试

文件components/TreeVisualizer.tsxuseMemo 中叶子链路构建逻辑(约第 74-82 行)

当前代码

const leaves = treeData.leaves();
const leavesByName = new Map(leaves.map(l => [l.data.name, l]));
const leafLinksArr: Array<{ source: any; target: any }> = [];
for (const leaf of leaves) {
  const nextId = leaf.data.attributes?.nextId;
  if (nextId && leavesByName.has(nextId)) {
    leafLinksArr.push({ source: leaf, target: leavesByName.get(nextId)! });
  }
}

检查结论:当前代码已经通过 nextId(即 Leaf-${node.next.id})来跟踪真实的 next 指针,而非通过 D3 叶��顺序推断。这部分逻辑正确,与代码审查报告中的"问题 P8"描述不符(报告描述的旧写法已被修复)。

需要做的事

  1. 确认 convertToHierarchy 函数中 nextId 的传递是正确的(已确认:nextId: node.next ? \Leaf-${node.next.id}` : null`)
  2. 在测试文件中补充验证叶子 next 链表指针完整性的测试用例(扩展现有的链表完整性测试),确保复杂删除场景下 next 指针不会断裂

需要在测试文件中补充的测试

文件utils/__tests__/bPlusTreeLogic.test.ts

在"链表完整性"测试组中,新增以下测试用例:

  1. 借用右兄弟后链表指针保持完整:构造一个会触发 borrowFromRight 的场景,删除后验证叶子 next 指针链完整(即从最左叶子遍历 next 能访问到所有 key,且有序)

  2. 大量插入删除后的 next 链表验证:插入 20 条数据后随机删除 10 条,验证剩余数据的 next 链路完整且有序(补充现有"借用/合并后应满足分隔键不变量"测试)

  3. 借用右兄弟后父分隔键满足不变量:构造触发 borrowFromRight 的场景,删除后调用 assertSeparatorInvariants 验证

具体触发 borrowFromRight 的最小构造方式(ORDER=4,MIN_KEYS=1):

// 插入 [1,2,3,4,5,6,7] 后删除 1,触发左侧叶子下溢,右兄弟有 2+ keys,触发 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); // 触发对右兄弟借用

总结:需要修改的文件

文件 修改内容
utils/bPlusTreeLogic.ts borrowFromRight 叶子分支:将静默警告改为明确错误日志,注释说明分隔键更新语义
utils/__tests__/bPlusTreeLogic.test.ts 补充 3 个新测试用例,覆盖 borrowFromRight 触发场景下的链表完整性和分隔键不变量

注意:components/TreeVisualizer.tsx 中叶子链路逻辑当前已正确,无需修改

This pull request was created from Copilot chat.


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

…rowFromRight test cases

Co-authored-by: 4iKZ <145829986+4iKZ@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix update of separator key in borrowFromRight method Fix borrowFromRight: replace silent warn with error log + add linked-list/separator invariant tests Mar 4, 2026
Copilot finished work on behalf of 4iKZ March 4, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants