Proofs: B^ε-tree
These are the human proofs of B^ε-tree, at full length.
Searches in the B^ε-tree return the newest value and updates keep the invariants full
This proof shows: correctness, termination.
We use the pseudocode of the page, with , and . The range of the root is all keys, and the range of the child of is the part of the range of between the pivots and , closed on the left and open on the right, with and . For a key and a node whose range holds , define the state of at :
- if is a leaf, is present with value when the pair is in , and absent otherwise;
- if is internal and has a message on , the state is that of the message: present with for
PUT(v), absent forDEL; - otherwise, the state of at the child of whose range holds .
Let be the state of after all updates so far, in the abstract dictionary. We prove that between operations:
- (E1) the pivots of each node are increasing and inside its range, all leaves are at one depth, each internal node has between 2 and children, and each leaf has at most keys, all in its range;
- (E2) each message in is on a key in the range of ;
- (E4) every buffer has at most messages;
- (S) for every key , the state of at the root is .
Property (E3) of the page is what makes (S) survive a flush; we use it in the form of the definition of state, which looks at the highest message first.
Base. The tree is one empty leaf, and every key is absent in the dictionary and at the root.
GET. Lines L1 to L5 follow the definition of state: at each internal node, line L3 returns the state of the message on if there is one, and line L4 goes to the child whose range holds , which exists and is unique by (E1). At the leaf, line L5 returns the state there. So GET returns the state of at the root, which is by (S). It ends because the height is finite, and it changes nothing.
UPSERT, line L6. If the root is a leaf, applying the message makes present with or absent, which is the new ; other keys keep their state. The leaf stays sorted. It may have keys, which line L8 handles.
UPSERT, line L7. The message goes into the root buffer and replaces any older message on . Now the state of at the root is that of the new message, which is the new . Other keys are not affected. Since the range of the root is all keys, (E2) holds. The buffer may have messages, and FLUSH is called.
FLUSH(x). We show: if, when FLUSH is called, (E1) and (E2) hold except that may have more than messages and the states at of all keys are given, then when it returns, has at most messages, every buffer below has at most , every child of satisfies (E1) but itself may have more than children, and the state of every key at is unchanged.
Lines L10 and L11. Let be the chosen child and the set of messages on keys in the range of , all moved to . For a key with a message in : before the move, its state at was given by that message. After the move, has no message on , so the state at is the state at . If is a leaf, line L11 applied the message to in the leaf, so the state at is that of the message. If is internal, the message is now in , having replaced any older message on there, so again the state at is that of the message. For a key with no message in , nothing on its path changed. So the state of every key at is unchanged. The moved messages are on keys in the range of , so (E2) holds for .
Line L12. If now has more than messages, the recursive call FLUSH keeps the states at , so also at , and returns with the buffers of and below at most .
Line L13. If has more than keys or more than children, SPLIT cuts it.
SPLIT (line L14). Let be a leaf with keys , , and . The left half keeps , the right half gets , and becomes a pivot of the parent, between the pivots around . Since and all keys of are in its range, the new pivot is strictly inside the range of , so the pivots of the parent stay increasing, and each key is in the range of its new leaf. If is internal with children, , the left half keeps the children and their pivots, the right half the others, and the pivot between them goes up. Each message of goes to the half whose range holds its key. Then each key has, in the half that holds it, the same message and the same subtree as before, so its state is unchanged. Each half has between 2 and children if , and the repeat in line L14 splits halves that are still too big, which happens a finite number of times since each split makes the halves smaller. The new nodes are at the depth of , so all leaves stay at one depth. The parent gains children, which is allowed during FLUSH.
Line L9. The loop ends: each pass moves a nonempty group of messages out of , since , and nothing is added to during the loop. When it ends, .
UPSERT, line L8. After line L6 or L7, only the root may be too big: every other node satisfies (E1) by the claim on FLUSH. While the root is too big, line L8 puts a new root with one child above it and splits that child, as in line L14. The new root has an empty buffer and its range is all keys, so all states are kept; the depth of all leaves grows by one at once; and after the split, every child of the new root satisfies (E1), because SPLIT repeats on each half that is still too big. The new root has at least 2 children. If it has more than , the loop runs again with it. Each round adds a level above nodes of at most children and replaces a root of children by one of about or fewer, so the loop ends, and then (E1) holds.
Deletes and the rebuild. A delete is an update with the message DEL, covered above. The rebuild computes, for each key, its state at the root, by resolving each buffer over the content of its subtree from the bottom up, which is the definition of state; it then builds a tree with empty buffers from the present keys, with leaves of at most keys and nodes of 2 to children. So (S) and (E1) to (E4) hold after it.
Termination. GET follows one path. FLUSH recurses only into children of , so the depth of the recursion is at most the height, and each loop ends as shown. SPLIT recurses only on smaller nodes. So every operation ends.
Hence every GET returns the value of the newest update of its key, or "absent", and all invariants hold between operations.