Give a page the layout of a B+Tree leaf, a 14-byte header and then 64-byte cells of key and row; create the root leaf in page 0 the first time the tree is needed; make INSERT check the statement, serialize the row and store it as a new cell; and show the leaf with btree dump.
You can turn a row into 60 bytes, and you can keep pages in a file across restarts. Nothing connects the two yet: an INSERT has nowhere to go. This stage gives it a place. Page 0 becomes a leaf node, the bottom level of the B+Tree the next stages grow, and INSERT puts each row into it as a cell.
A leaf is the smallest tree there is: one node, no children. It is still worth building as a tree node from the start, because everything that comes later works on the same page layout. The next stage keeps the cells sorted and finds keys by binary search. The stage after it splits a full leaf in two and adds the first internal node above them. None of that changes how a leaf stores its cells, which is what you build now.
Until now a page was 4096 bytes with no meaning. A node page has a structure: a header of 14 bytes that says what kind of node the page is and how full it is, then the cells, one after the other.
| Bytes | Field | In a leaf |
|---|---|---|
| 0 | node type | 1 for a leaf. (2 is the internal node of a later stage; 0 is never a node, so a page of zeros is not mistaken for an empty leaf.) |
| 1 | is root | 1 for page 0 |
| 2–5 | parent page | 0 for the root. The first leaf with a parent appears when a leaf splits |
| 6–9 | count | the number of cells |
| 10–13 | next leaf | 0: there is no other leaf yet |
| from 14 | cells | 64 bytes each: the 4-byte key, then the 60-byte row |
Every number is an unsigned integer with its least significant byte first, like id in the row format. The key is the row's id, stored once more in front of the row. That looks redundant, and it is what lets a tree compare keys without knowing anything about rows.
This is page 0 after inserting (2, 'bo', 'b@e') and then (5, 'dan', 'd@e'): the header, and the first of the two cells.
The root of the tree is always page 0. That is a rule, not an accident: every search starts at the root, and a fixed page number means the program never has to look up where the root is.
Page 0 does not exist in a new file, and opening the file does not create it. The root is created the first time a command needs the tree: btree dump or an INSERT. Before that, pager status still reports total_pages=0, exactly as in the pager stage.
The rule that matters most is the other half: the root is created only when the file has no pages. A file with pages already has its root, with its cells in it. A program that writes an empty leaf into page 0 at every start works perfectly in one session and empties the table at every restart.
An INSERT goes through every layer you have built. The parser gives you the table, the columns and the values. Then come the checks this stage adds: the table must be users (00401), each column must be id, name or email (00402), and id must be there (00406). A column that is left out is an empty string. The row format's checks follow, as in the row-serialization stage: 00403 for a value of the wrong type, 00404 for an id out of range, 00405 for a string of more than 27 bytes. Last comes the leaf's own check, below: it must have room for one more cell.
Only then does anything change. The row is serialized, the cell goes in at the position the count gives, the count grows by one, and the program prints Insert statement executed successfully. Every check comes before the page changes, which is what makes "a failed statement changes nothing" easy to keep true.
In this stage a new cell simply goes after the last one, so the cells are in the order the rows arrived. The next stage keeps them sorted by key.
A leaf holds at most 8 cells in this tutorial. That is a test-mode number, chosen so a full leaf takes eight inserts and a test that fills one runs in an instant.
The 9th INSERT into a full leaf is error 00702, and it changes nothing: the leaf still holds 8 cells and the row is not stored anywhere. That is a temporary answer. From btree-leaf-split on the leaf splits instead, and 00702 is no longer what a 9th row gets.
Every btree command and every SQL statement starts here.
Every check happens before the first byte of the page changes.
In this stage a new cell always goes after the last one.
These are the exact lines the tests check. The full definition is on the output contract page, sections 3, 6, 9 and 10, including the page layout above.
| Input | Output |
|---|---|
insert into users (...) values (...); | Insert statement executed successfully |
btree dump or btree dump <page>, a leaf | [BTREE] Page <page>: type=LEAF num_cells=<n>, then one line per cell, cell 0 first: [BTREE] Cell <i>: key=<k> (60 bytes) |
an INSERT into a table other than users | [ERROR:00401] <message> |
an INSERT naming a column users does not have | [ERROR:00402] <message> |
| a value of the wrong type, an id out of range, a string over 27 bytes | [ERROR:00403], [ERROR:00404], [ERROR:00405] |
an INSERT without id | [ERROR:00406] <message> |
| an INSERT into a leaf with 8 cells | [ERROR:00702] <message> |
btree dump of a page that was never allocated | [ERROR:00601] <message> |
btree dump with a page number that is not digits | [ERROR:00104] <message> |
There are three spaces between [BTREE] and Cell. The page in the file is part of the contract too: the header and cells laid out as in the table above, so the tests read the file to check a row really is where the layout says.
$droid --db users.dbWelcome to droid!>pager status[PAGER] Status: total_pages=0 cached=0>insert into users (id, name, email) values (1, 'ada', 'ada@example.com');Insert statement executed successfully>insert into users (id) values (2);Insert statement executed successfully>insert into people (id) values (3);[ERROR:00401] Unknown table: people>insert into users (id, name, email) values (4, 'abcdefghijklmnopqrstuvwxyz01', 'x');[ERROR:00405] name is 28 bytes, at most 27 fit>btree dump[BTREE] Page 0: type=LEAF num_cells=2[BTREE] Cell 0: key=1 (60 bytes)[BTREE] Cell 1: key=2 (60 bytes)>pager status[PAGER] Status: total_pages=1 cached=1>.exitexiting.. good bye!$droid --db users.dbWelcome to droid!>pager status[PAGER] Status: total_pages=1 cached=0>btree dump 0[BTREE] Page 0: type=LEAF num_cells=2[BTREE] Cell 0: key=1 (60 bytes)[BTREE] Cell 1: key=2 (60 bytes)>.exitexiting.. good bye!
The first INSERT created page 0; before it, there were no pages. The two failed INSERTs left no trace in the leaf. The second session found one page in the file, read nothing until btree dump needed page 0, and found both rows still there. The error messages are yours to word; the tests check only the codes.
num_cells=0. Create the root only when the file has no pages.total_pages=1 before anything has used the tree. Create it when a command first needs it.i starts after the 14-byte header, 64 bytes after cell i − 1.id.id in the row format.The page above is the same for every language. These are the only language-specific hints.
memcpy into and out of a fixed-width integer, and convert the byte order if your machine is not little-endian.u32::from_le_bytes and to_le_bytes over slices of the page read and write each field. u32::try_from on the parsed id rejects values past 4294967295.std.mem.readInt(u32, page[6..10], .little) and std.mem.writeInt read and write a field at its offset in the given byte order. An extern struct laid over the page has C layout, padding included, so reading each field at its offset is the simpler path.