Function: free_page in b-tree-storage-engine/btree.py

Date: 2026-05-29

Time: 07:50

PageManager.free_page

Purpose

freepage returns a disk page to a singly-linked free list so it can be reused by future allocatepage calls. Without it, deleted B-tree nodes would leave permanent holes in the data file — the file would grow monotonically even under heavy delete workloads. This is the deallocation half of the page allocator; allocate_page is the allocation half.

Contract

Preconditions:

Postconditions:

Invariant preserved: The free list is a singly-linked list threaded through the pages themselves, terminated by NOSIBLING (0xFFFFFFFF). After this call, freehead → pagenum → oldfreehead → ... → NOSIBLING.

Parameters

| Parameter | Type | Description |

|-----------|------|-------------|

| self | PageManager | The page manager with an open data file |

| page_num | int | The page number to free. Must be a valid, unreferenced page. No bounds checking is performed. |

Return Value

None. All effects are through side effects on disk.

Algorithm

1. Read current metadata — fetches (root, height, totalkeys, nextfree, free_head) from page 0.

2. Build a free-list node — constructs a minimal page payload:

3. Write the free-list node to pagenum — overwrites whatever was there. The data is padded to pagesize by write_page.

4. Update metadata — writes the same root, height, totalkeys, and nextfree, but replaces freehead with pagenum. This makes the freed page the new head of the list.

The key insight: the free list is intrusive — each free page stores its own "next" pointer in its body, so no separate data structure is needed.

Side Effects

Error Handling

None. If the file I/O fails, the underlying write/seek calls raise OSError and the free list is left in a partially-updated state. There is no rollback or atomicity guarantee at this level.

Usage Patterns

The only caller in this codebase is BTree._delete at line ~247, which frees an empty leaf after merging its sibling pointer:


self.pm.free_page(child_page)

The caller must:

1. First unlink the page from the tree structure (update parent's children list, fix sibling pointers).

2. Then call free_page.

3. The corresponding allocatepage method checks freehead != NO_SIBLING before allocating from the high-water mark, so freed pages are reused LIFO (most-recently-freed page is allocated first).

Dependencies

Assumptions Not Enforced

1. No double-free detection — freeing the same page twice creates a cycle in the free list, causing allocate_page to return the same page number repeatedly, silently corrupting the tree.

2. No bounds checkingpage_num could be 0 (the metadata page) or beyond the file's extent; both would corrupt state.

3. No crash atomicity — the writepage and writemeta are not atomic together. A crash between them leaks the page.

4. Free-list node is typed as INTERNAL with 0 keysallocate_page doesn't validate the page type when popping from the free list, so this works, but nothing prevents confusing a free-list node with a real empty internal node during debugging or recovery.

Beliefs