Divide a full leaf where its two halves' bytes are closest, not in the middle of its cells, with the incoming cell weighed among them. Make UPDATE handle a row that no longer fits: it stays in its leaf when the room its old row leaves is enough, and otherwise splits the leaf as an insert of the new row would.
In the previous stage a leaf became full when its bytes ran out, but how it split was left open: any division that fit was right, halving the count included. With rows of one size that is a fine answer. With rows of very different sizes it is not.
Take a leaf that holds three rows of 1000 bytes and thirty-seven of 21. Halving by count puts the three long rows and seventeen short ones on the left, 3517 bytes, and twenty short rows on the right, 580. The left leaf is full again after a couple of inserts, and splits again; the right one sits nearly empty. A B+Tree splits so that both halves have room to grow. Room is measured in bytes now, so the split has to be too.
This stage also finishes what variable rows do to UPDATE and DELETE. A row that grows may no longer fit in its leaf; a row that is deleted leaves room another row can take. Both follow from one idea: a leaf's free space is the only thing that decides whether a cell fits.
A cell weighs its row, 4 bytes of key and 4 of slot. Line the cells up in key order, the new one in its place, and cut where the two halves weigh closest to the same:
The rule has three parts.
Nothing else about the split changes. The page, link, parent and allocation rules are the Core Engine's: the root's halves go to two new pages, any other leaf keeps its page for the left half and the right half goes to one new page, and the separator is the right leaf's first key. Only where the cut goes is new.
Dividing by count left the Core Engine's tree fixed by the order of the inserts. Dividing by bytes does the same for variable rows: given the rows and the order they arrive in, which keys each leaf holds follows, whatever the internal nodes do above them. The tests use this. They replay hundreds of statements on a model of the leaves and expect btree structure to show exactly those leaves, left to right. A split that is almost by bytes, or by bytes without the new cell, drifts from the model after a few splits.
An UPDATE builds the new row and checks it before touching the tree, as before. What happens next depends only on bytes:
btree find may report a new page and cell.Checking whether the new row fits before taking the old one out asks for room the leaf already has, and splits a leaf that did not need it.
DELETE is unchanged: the cell's slot goes, and its bytes are free at once. What this stage checks is that free really means usable. After deleting a row from a full leaf, a new row as large as the room freed goes in without a split, even though the bytes it takes back sit in the middle of the record area, behind other records. The slotted page compacts when the gap between the slots and the records is too small but the free space is not; the leaf does the same.
No leaf is merged and no page is freed. A leaf can end up empty, and a separator can point at keys that are gone: both are valid, as in the Core Engine.
The split of the Core Engine, with a different answer to where.
Build the new row, check it, then put it back where its key belongs.
Nothing new in DELETE; what is new is that the room it frees is used.
Nothing new is printed. The lines the tests check are those of the previous stages; the full definition is on the output contract page, sections 9, 10 and 17.
| Input | Output |
|---|---|
btree dump [page], a leaf | [BTREE] Page <p>: type=LEAF num_cells=<n> free_space=<f>, then per cell [BTREE] Cell <i>: key=<k> (<row size> bytes) |
btree dump [page], an internal node | [BTREE] Page <p>: type=INTERNAL num_keys=<n> right_child=<page>, then per key [BTREE] Key <i>: key=<k> (child_page=<page>) |
btree structure | [BTREE] Tree depth: <d>, then one line per node: [BTREE] INTERNAL (page=<p> keys=[...]) and [BTREE] LEAF (page=<p> cells=<n> keys=[...]) |
btree find <key> | [BTREE] Find key=<k>: FOUND (page=<p> cell=<i>), then the Row: line |
What the tests pin down is which keys land where:
row size + 8 at once, and the next cell that fits uses it without a split.No earlier case is retired: the previous stage allowed any division that fit, and none of its cases depended on which.
The file holds the rows of split-by-bytes: three rows of 1000 bytes, keys 1 to 3, then keys 4 to 39 with 21 bytes each. The leaf has 12 bytes left. Open it again, look, and insert key 40:
Welcome to droid!>btree structure[BTREE] Tree depth: 1[BTREE] LEAF (page=0 cells=39 keys=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39])>insert into users (id, name, email) values (40, 'dan', 'd@e');Insert statement executed successfully (1 rows)>btree structure[BTREE] Tree depth: 2[BTREE] INTERNAL (page=0 keys=[3])[BTREE] LEAF (page=1 cells=2 keys=[1,2])[BTREE] LEAF (page=2 cells=38 keys=[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40])>btree dump 0[BTREE] Page 0: type=INTERNAL num_keys=1 right_child=2[BTREE] Key 0: key=3 (child_page=1)>btree dump 1[BTREE] Page 1: type=LEAF num_cells=2 free_space=2064[BTREE] Cell 0: key=1 (1000 bytes)[BTREE] Cell 1: key=2 (1000 bytes)>btree find 40[BTREE] Find key=40: FOUND (page=2 cell=37)[BTREE] Row: id=40 name='dan' email='d@e'>delete from users where id = 2;Delete statement executed successfully (1 rows)[EXEC] rows_examined=1>btree dump 1[BTREE] Page 1: type=LEAF num_cells=1 free_space=3072[BTREE] Cell 0: key=1 (1000 bytes)>.exitexiting.. good bye!
Things to notice. Key 40 is 21 bytes, and it moved 38 cells to a new page: the split weighs every cell, and two 1000-byte rows balance thirty-eight short ones. Page 1 holds two cells and has 2064 bytes free; page 2, with 38, has 1999. Deleting row 2 gave back its 1000 bytes and 8 more, and left the separator 3 where it was.
row size + 8. Weighing only the rows moves the cut when the halves have different numbers of cells.The page above is the same for every language. These are the only language-specific hints.
std.mem.copyForwards writes them into the halves.