Keep a list of free pages in the file: its head and length in two new header fields, and in each free page the number of the next. pager free puts a page at the head; every allocation takes the head before it makes the file longer. Show the list with db freelist.
Every page you have ever allocated is still in the file. Nothing gives one back: a page used for a while and then abandoned keeps its place, and the next allocation makes the file longer instead of using it. With what you have built so far that rarely matters, because nothing abandons a page. The next stages do. DROP TABLE throws away a whole tree, merging leaves on DELETE empties pages, and overflow chains come and go with the rows that own them. Without a way to reuse those pages, the file only ever grows, however small the data in it gets.
So before anything frees pages, there has to be a place for freed pages to go. Real databases call it the free list. It is the simplest structure that works: the free pages chained together, and the start of the chain written down somewhere you can always find it.
The header gains two fields after the root: the first free page, the head, and how many pages are on the list. A file with no free pages has zeros there, which is exactly what every header written before this stage already holds, so old files need no conversion.
A free page is a page of type 4. It keeps the number of the next free page at bytes 10 to 13, the same place a B+Tree node keeps its link, and nothing else: every other byte up to the trailer is zero, and the trailer is written as for any page.
Follow the chain from the header and you visit every free page once:
| Where | Says |
|---|---|
| header | head is page 4, 2 pages on the list |
| page 4 | type 4, next is page 2 |
| page 2 | type 4, next is 0: the end |
Freeing a page writes it as a free page whose next is the current head, then makes it the head and adds one to the count. Allocating takes the head: its next becomes the head, the count goes down by one, and the page is zeroed before anyone sees it. The last page freed is the first one reused, which is why only the head ever matters: nobody searches the list.
Both operations change two pages, the header and the free page, and both stay in the buffer pool like any other change until they are written. The list survives a restart because it is in those pages, not in memory.
SQLite keeps its free list the same way, a head and a count in the database header, except that its free pages are mostly "leaf" pages listed inside "trunk" pages, so one trunk read finds hundreds of free pages at once. PostgreSQL has no file-wide list; each table keeps a free space map of how full its own pages are.
Freeing puts the page at the head of the list.
Every allocation, by pager alloc or by a statement that needs a page, asks the list first.
db freelist walks the chain.
The full definition is on the output contract page, section 26.
| Input | Output |
|---|---|
pager free <n> | [PAGER] Free: page <n> |
db freelist | [DB] Free list: pages=<k> chain=[<p1>,<p2>,...], from the head; pages=0 chain=[] when empty |
pager free of a page never allocated | [ERROR:00601] <message>, checked first |
pager free 0 | [ERROR:01204] <message> |
pager free of a page already on the list | [ERROR:01203] <message> |
pager alloc with a non-empty list | [PAGER] Alloc: page <n> (4096 bytes, zeroed), n the old head |
db header prints the line of section 18, unchanged. btree dump of a free page is 00703 and a page command on it 00606; pager get, read and write work on it as on any page.
On a file of five pages, the header and pages 1 to 4:
$droid --db pages.dbWelcome to droid!>db freelist[DB] Free list: pages=0 chain=[]>pager free 1[PAGER] Free: page 1>pager free 3[PAGER] Free: page 3>db freelist[DB] Free list: pages=2 chain=[3,1]>pager free 3[ERROR:01203] Page 3 is already free>pager alloc[PAGER] Alloc: page 3 (4096 bytes, zeroed)>.exitexiting.. good bye!
$droid --db pages.dbWelcome to droid!>db freelist[DB] Free list: pages=1 chain=[1]>pager alloc[PAGER] Alloc: page 1 (4096 bytes, zeroed)>pager alloc[PAGER] Alloc: page 5 (4096 bytes, zeroed)>.exitexiting.. good bye!
Things to notice. Page 3 was freed last and came back first. The allocation did not make the file longer: it took page 3 instead of making page 5. The next session found page 1 still on the list, because the list is in the file, and only once it was empty did an allocation make page 5.
pager alloc, a leaf split, a new table's root. One code path that still appends to the file is enough to waste pages.00601 before anything else, so pager free 0 on an empty file is 00601, not 01204.The page above is the same for every language. These are the only language-specific hints.
memset the page to zero on allocation, then set nothing else: the caller decides what the page becomes.u32::from_le_bytes over a four-byte slice reads a header field, and to_le_bytes writes one. fill(0) zeroes a reused page.std.mem.readInt and std.mem.writeInt with .little read and write the fields; @memset zeroes a page.