Date: 2026-05-28
Time: 18:29
_delete — Recursive B-Tree Key Deletiondelete is the recursive engine behind BTree.delete(). It walks down the tree from a given node to find and remove a key from its leaf, then handles one specific structural cleanup on the way back up: removing emptied leaf nodes when they have a left sibling at depth 2. It exists to separate the tree-traversal logic from the metadata bookkeeping that the public delete() method handles (decrementing totalkeys, committing the WAL).
Preconditions:
page_num refers to a valid, allocated page in the data filedepth accurately reflects the distance from this node to the leaves (1 = leaf, 2 = parent of leaves, etc.)depth matches the subtree structure — calling with a wrong depth will misinterpret page contentsPostconditions:
next_sibling pointer is patched to maintain the linked-list scan order, and the empty page is returned to the free listInvariants preserved:
len(children) == len(keys) + 1)| Parameter | Type | Meaning |
|-----------|------|---------|
| page_num | int | Page number of the current node in the data file |
| key | str | The key to delete (must match exactly via ==) |
| depth | int | Height of the subtree rooted here. 1 = leaf, 2 = parent of leaves, etc. |
Returns a three-valued signal to the caller:
| Value | Meaning | Caller action |
|-------|---------|---------------|
| False | Key not found | Do nothing |
| True | Key deleted, leaf still has keys (or empty leaf was already cleaned up) | Propagate success |
| 'empty' | Key deleted, leaf is now empty and was not cleaned up at this level | Parent should attempt cleanup |
The public delete() method treats any truthy return as success and decrements total_keys.
Leaf case (depth == 1):
1. Read and deserialize the leaf page.
2. Binary-search for the key using bisect_left.
3. If the key isn't present, return False.
4. Remove the key and value at that index.
5. Serialize and write the modified leaf via WAL.
6. Return 'empty' if the leaf has no remaining keys, otherwise True.
Internal node case (depth > 1):
1. Read and deserialize the internal node to get separator keys and child pointers.
2. Use bisect_right to find which child subtree contains the key.
3. Recurse into that child with depth - 1.
4. Cleanup path — if the child reported 'empty', depth == 2 (this node is the direct parent of leaves), and idx > 0 (the empty leaf is not the leftmost child):
next_sibling pointer.next_sibling, splicing the empty leaf out of the linked list.ikeys[idx-1]) and the child pointer (children[idx]).True (cleanup done, don't propagate 'empty' further).5. If result == 'empty' but cleanup conditions aren't met (leftmost child, or depth > 2), return True — swallow the signal. The empty leaf stays allocated.
6. Otherwise, pass through the False or True result unchanged.
free_page).walwritepage, which logs to the WAL file *and* writes to the data file. This is not yet committed — delete() calls wal.commit() after delete returns.pm.freepage() modifies the metadata page's free-list head pointer and writes a free-list node into the freed page. This bypasses the WAL (it calls pm.writepage directly, not walwrite_page), which is a potential crash-safety gap.pm.pagesread and pm.pageswritten are incremented by all page reads/writes.No explicit error handling. If the page data is corrupt or page_num is invalid, this will raise from struct.unpack or produce garbage. The method trusts that the tree structure on disk is consistent.
Called exclusively by BTree.delete():
def delete(self, key):
root, height, total_keys, next_free, free_head = self._read_meta()
found = self._delete(root, key, height)
if found:
self._wal_write_meta(root, height, total_keys - 1, next_free, free_head)
self.wal.commit(self.pm)
return bool(found)
The caller is responsible for updating the key count and committing the WAL transaction. The bool() cast collapses 'empty' and True into True for the public API.
bisectleft, bisectright — standard library binary searchdeserializeleaf, serializeleaf — leaf page serializationdeserializeinternal, serializeinternal — internal page serializationself.pm (PageManager) — page-level I/O, free-list managementself.walwrite_page — WAL-logged page writes1. No rebalancing. This is a simplified B-tree that doesn't merge or redistribute keys on underflow. An emptied leftmost leaf (idx == 0) or an empty leaf at depth > 2 is left in place with zero keys — it wastes a page and stays in the tree permanently.
2. Only cleans up at depth 2. The depth == 2 guard means cleanup only happens when the current node is the direct parent of leaves. If a deletion empties a leaf deeper in the tree, the 'empty' signal is swallowed as True at the first internal node that can't handle it.
3. freepage bypasses WAL. When a leaf is freed during cleanup, pm.freepage() writes directly to the data file without WAL logging. If the process crashes after free_page but before wal.commit(), the free list and the internal node could be inconsistent.
4. bisectright vs bisectleft. Leaves use bisectleft (find exact match position), internals use bisectright (route to the correct child). This matches B-tree convention where a key equal to a separator routes right.