Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Keys

Dialog keeps its facts in a sorted tree, and a sorted tree can only find things by the order of their keys. A fact gets looked up in three different ways, though. An app shows one grocery item, and needs every fact about item:1. It shows the whole list, and needs every demo.grocery/name. It asks which items are tagged "breakfast", and needs to start from a value.

No single sort order serves all three. So Dialog writes every fact three times, each time with its parts in a different order:

EAV tag 00 item:1 demo.grocery/name "Oat milk" find by entity AEV tag 01 demo.grocery/name item:1 "Oat milk" find by attribute VAE tag 02 "Oat milk" demo.grocery/name item:1 find by value

Each copy is called an index. The first byte of the key, the tag, says which index it belongs to. All the indexes live side by side in one tree, and the tag keeps them apart:

TagIndexParts in orderUsed to find
00EAVentity, attribute, valueeverything about a thing
01AEVattribute, entity, valueeverything with an attribute
02VAEvalue, attribute, entitythings that have a value
03Historyversion, entity, attribute, valuethe log of changes
04Assethashassets, in trees written before assets were recorded as facts
05Coverageversion, entity, attribute, value hashwhat a change removed

The first three hold the facts themselves. The last three are Dialog’s own bookkeeping, and later chapters come back to them. Nothing is added under tag 04 any more. A stored asset is recorded by an ordinary fact instead (see Blocks and Storage), and the entries older trees hold there are still read.

A key, byte by byte

Here is the EAV key for the grocery name of item 1 is “Oat milk”:

00 i 69 t 74 e 65 m 6d : 3a 1 31 00 d 64 e 65 m 6d o 6f . 2e g 67 r 72 o 6f c 63 e 65 r 72 y 79 / 2f n 6e a 61 m 6d e 65 00 03 O 4f a 61 t 74 20 m 6d i 69 l 6c k 6b 00 tag entity attribute type value

The key starts with the tag, 00. Then each part follows, written so that comparing the raw bytes gives the same answer as comparing the parts themselves:

  • Strings (the entity URI, the attribute, and text values) are written as their UTF-8 bytes followed by a 00 terminator. Any 00 inside the string is written as 00 FF, so the terminator is never ambiguous.
  • The type byte of the value comes right before the value. It is the value’s type number from the Facts chapter; 03 is text.
  • Numbers all carry the type byte 09, and are written as decimals, in the key encoding SQLite4 designed. The first byte says the number’s sign and how big it is. The digits follow, two decimal digits to a byte, and the last byte is marked, so a number ends itself and needs no terminator. A negative number has every byte after the first inverted. Bigger numbers sort later, whether they are whole or not, and a number has exactly one encoding: the quantity 2 is the two bytes 18 04, and so are 2.0 and +2.
  • Booleans are one byte, 00 for false and 01 for true.

The terminator is what makes prefixes sort first. item:1 ends with 00, and item:10 continues with 30 (the character 0) at the same position. Since 00 is smaller, every key for item:1 comes before every key for item:10, just as the strings would. Nothing about item 1 ever lands in the middle of item 10.

Why no field starts with FF. A terminator followed by FF would read back as an escaped zero. So no part of a key that follows a terminated string may begin with the byte FF, and the range bounds Dialog builds for scans use FE as their filler byte instead. The spill marker below starts with 01 for the same reason.

The VAE key holds the same bytes in a different order. The tag is 02, and the value comes first, led by its type:

02 03 O 4f a 61 t 74 20 m 6d i 69 l 6c k 6b 00 d 64 e 65 m 6d o 6f . 2e g 67 r 72 o 6f c 63 e 65 r 72 y 79 / 2f n 6e a 61 m 6d e 65 00 i 69 t 74 e 65 m 6d : 3a 1 31 00 tag type value attribute entity

Sorted, the list tells its own story

Put Alice’s keys in byte order and each index turns into a set of neat runs. Part of the AEV index is elided:

value type byte: 02 Boolean 03 String 00 item:1 demo.grocery/done 02 false everything about item:1 00 item:1 demo.grocery/name 03 "Oat milk" 00 item:2 demo.grocery/done 02 false everything about item:2 00 item:2 demo.grocery/name 03 "Eggs" 00 item:2 demo.grocery/tag 03 "breakfast" 01 demo.grocery/done item:1 02 false every item's done flag 01 demo.grocery/done item:2 02 false 01 demo.grocery/name item:1 03 "Oat milk" … 02 02 false demo.grocery/done item:1 every item not yet done 02 02 false demo.grocery/done item:2 02 03 "Eggs" demo.grocery/name item:2 then every string, in order 02 03 "Oat milk" demo.grocery/name item:1 02 03 "breakfast" demo.grocery/tag item:2

Every fact about item:2 is one contiguous run in EAV. Every demo.grocery/done is one run in AEV. In VAE the type byte comes right after the tag, so every Boolean sorts before every string: Boolean is type 02 and text is type 03. Among the strings, "Eggs" and "Oat milk" come before "breakfast", because uppercase letters have smaller byte values than lowercase ones.

A query is mostly a matter of picking the index whose runs match the question, then reading one run.

The value lives in the key

The value is part of every key, not stored beside it. That means a lookup by value in VAE finds the entity without reading anything else.

It also means a fact has exactly one place in each index. If Alice and Bob both assert that the eggs are done, both assertions build the same key, so each index holds one entry for that fact, not two.

That entry has a small payload next to its key. The payload holds no part of the fact, only its temporal information: the versions that asserted it, here both Alice’s and Bob’s, and the fact’s optional cause (see Commits and History).

Big values spill

A key holding a whole photo would make a terrible key. So every tree sets a limit on how long a value in a key may be, and a value whose encoded form is longer than that limit spills. The value’s place in the key keeps only the encoding of a short prefix of it, and the key ends with a 01 byte and the 32-byte BLAKE3 hash of the whole value. In EAV and AEV that tail comes right after the value; in VAE it comes after the entity, the key’s last part. The value itself is stored on its own, in the asset store, named by that hash:

EAV key of a spilled value 00 item:3 demo.grocery/note 03 its prefix, 00 01 blake3(value) sorts by its prefix spilled 32 bytes the whole value stored on its own, found by its hash

The prefix keeps a spilled value sorting by how it starts, so a range scan over strings still finds it in about the right neighborhood. The hash keeps two different long values from sharing a key. A reader tells the two shapes apart by what is left after the key’s last part: nothing for an inline value, exactly 33 bytes for a spilled one.

The limit and the length of the prefix are not constants of Dialog. They are settings of the tree, and every node carries them in its header, as the Nodes chapter shows. Unless a tree says otherwise, the limit is 4,096 bytes and the prefix is the value’s first 64 bytes. A reader that guessed them wrong would build different keys and miss facts, so they are among the few settings a reader must understand.

Before 0.3. Releases before version 0.3.0 kept natural numbers, integers and floats in three separate bands of the index, each under its own type byte, so 2 and 2.0 were different keys. A tree written that way can still be read, but not added to: a commit on it fails with NeedsMigration, and Branch::migrate rewrites the branch into the layout this chapter describes.

Implementations. Keys are built by build_key in dialog-artifacts/src/key/varkey.rs. The string encoding is in artifacts/ordkey.rs and artifacts/ordvalue.rs, and the number encoding is the dialog-number crate. Spilling is decided by EncodedValue::new in key.rs.