(smollerGraph)
  • Episode 09
  • 38 min

Property graph storage and WHERE in C++

Properties for smollerGraph's nodes and relationships, in a chain of records on disk, loaded from facts, asked about with WHERE, and shown in the inspector.

24 October 2026 · Watch on YouTube

We give smollerGraph's nodes and relationships properties, kept as in Neo4j's record format: a third file of 32-byte records, each with a key, a value and the next property's id, so a node's properties form a chain, like its relationships. The facts we load get properties in braces, read by our Cypher parser, and Cypher gets WHERE, with each filter planned as early as it can run, and RETURN of any property. The UI's inspector shows a node's properties as they're kept on disk, and we check what they cost a walk.

The code

Each step on screen is a tag in the database's repository. The code at the end of the episode is ep009/end.

  1. 3:07ep009/step-1Property recordsBrowseChanges
  2. 10:34ep009/step-2Facts with propertiesBrowseChanges
  3. 18:04ep009/step-3WHEREBrowseChanges
  4. 30:38ep009/step-4Properties in the UIBrowseChanges

Transcript

Intro

0:00 Hello and welcome back to episode nine, where we're going to give smollerGraph's nodes and relationships properties, such as a weight or a diet, and ask about a node's in Cypher, with WHERE. Let's begin with a quick look at where we're going to be by the end of the episode.

0:15 By the end, the page is going to answer questions about properties. This one asks which animals eat something under a kilo, with WHERE, and returns its weight too. The red fox and the tawny owl both eat the field mouse, at about twenty-five grams. And clicking the fox opens its record, with each of its properties in a record of its own on disk, in a chain.

0:38 On the website of the series, you can find more information about the project. The code is open source on GitHub, tagged at each step of each episode, so that you can follow along, and each episode has a page here, with its chapters and its code.

Last time

0:53 Now, before we get to today's work, let's quickly go over what we did last time. We put it on a VPS I rent, a server with Docker on it, and deploy dot sh sent our committed code to it over SSH, where Docker Compose built it and ran it. Caddy serves the page, and passes the API's requests on to sg serve, so the page answers from the server's address. But a node in our graph is a name and its relationships, with no other facts about it, so there isn't much on that server worth keeping yet. Today, our nodes and relationships get properties, such as a weight or a diet.

Today

1:27 Today, we go from the bytes on disk up to the query, as we've done before. Properties get a file of records of their own, and each node and relationship points at the first of a chain of them. The facts we load get properties in braces, written as in Cypher. Then Cypher gets WHERE, and RETURN gets a node's other properties as well as its name. And the page's inspector shows a node's properties as they're kept on disk. All right, let's get started with how Neo4j and Kùzu keep their properties.

Where properties go

1:58 In episode one, we saw that Neo4j and Kùzu go about keeping properties in two different ways. Neo4j's record format gives a node the id of its first property record, and each property record points at the next, so a node's properties form a chain, like its relationships. Kùzu keeps each property in a column of its own, so a query that compares weights reads the weights, and skips the rest. Our nodes and relationships are records already, which point at their chains, so we're going to follow Neo4j, and add a chain of property records to each of them.

Property records

2:32 Here's how the red fox is going to look on disk. Its record keeps its name and its first relationship, and gains the id of its first property. That id takes four of the bytes the name had, so a record stays thirty-two bytes, and a name can be twenty-three bytes long. The id leads to a property record, with a key, diet, and a value, the string omnivore, then the id of the next property, its legs, and then its weight, at the end of the chain. The relationships keep their chain, and a relationship gets a first property of its own, for facts about the relationship, such as where it came from.

3:07 In graph dot h, we include variant, the standard library's type that holds one of a few types at a time, for a value that's a number or a string. A property id is a thirty-two-bit number, like the ids of nodes and relationships. The end of a property chain is all ones, as it is for relationships, so the comment over the two of them covers both now. The node record gives four bytes of its name, which is twenty-four bytes now, to the id of its first property, and its comment says so. The relationship record gives four bytes of its type, down to sixteen, and gets a first property after its next relationship. Value type is an enum class with a byte underneath, as it's going to sit in the record, and a value is an integer or a string. The property record has a key of up to eleven bytes and its zero, the value's type, fifteen bytes for the value, and the id of the next property. Its comment says how they fit together: a property points at the next property of the node or relationship it belongs to, so their properties form a chain too. A whole number takes the first eight bytes of the value, and a string runs to its zero. Twelve, one, fifteen and four make thirty-two, which a static assert checks, as it does for the other two records, so a page holds a hundred and twenty-eight of each. Thirty-two divides a page of four kibibytes with nothing left, so no bytes at the end of a page go to waste, in any of the three files. Then Value, a variant of a sixty-four-bit integer and a string, which is what the graph hands out, as its comment says. A property is a key and a value, and Value of reads the value a property record holds, back out of its bytes. The graph's comment says it keeps three files of records now. K max key is the longest key that fits, worked out from the record's field with size of, as the name and the type are, less one for the zero at the end. Eleven bytes is room for a key such as weight_g. The comment over it covers the two limits: the longest key and string that fit in a property's record. K max string is the string's, fourteen bytes, which is room for a word such as omnivore, and that's what our animals need. Flush writes the properties' pages too, and Add relationship returns the new relationship's id now, so whoever adds one can set its properties. Set property sets a property of a node, and set relationship property sets one of a relationship, replacing a value the key had. The comment says how long a key and a string can be, and that a node's name isn't a key to set, as it's in the record already. Find property returns a node's property, if it has one, and its comment adds that the name is a property too, so a query can ask for it like any other. That way, a query doesn't have to know which property lives in the node's record, and which in the chain. Properties and relationship properties list the ones in the chain, without the name, in the order they were first set, for whoever wants all of them at once. The property accessor reads a property record by its id, as node and relationship do, for code that wants the record as it's kept. The page counts add the properties' file, so sg load counts its pages too. In the private part, Set sets a key in a chain, given the chain's first id by reference, so it can change which record comes first. Its comment says that a key the chain doesn't have yet gets a record of its own, at the front. Chain reads a chain into a list, and the properties' record file goes between the relationships and the names. In graph dot c c, Encode goes in the unnamed namespace, and puts a value in a property record. Get if asks the variant for a pointer to its number, which is null when it holds a string. For a number, the type is integer, the value's bytes are zeroed, and mem copy puts the number's eight bytes at the front. Otherwise, the type is string, and Copy field, from episode two, copies the string in with its zero. Zeroing the value first means the bytes after a number are zeros, and not left over from an earlier value, which keeps the file easy to read with xxd. The comment over Encode says why the type goes in too: so the value can be read back. Value of does that, outside the namespace: an integer's eight bytes are copied out into a number, and a string is read up to its zero. The constructor opens a temporary file for the properties, between the relationships and the names, and Open opens the folder's properties file, so a folder holds four files now, with the index's. As before, each file opens if the one before it did, and the first error is the one that's returned. Add node starts a node with no property, and so does Add relationship, which returns the new relationship's id, the new head of the node's chain. Set property reads the node's record, sets the key in its chain, and writes the record back, as the first property's id may have changed. Set relationship property does that for a relationship's record. The two share Set, which works on whichever chain it's given. Set walks the chain from its first id, reading each record through the buffer pool, as Neighbours does for a node's relationships. When a record has the key, its value is replaced where it is, and written back, so setting a key twice doesn't make two records. Otherwise, a new record gets the key and the value, points at the old first property, and becomes the first, once the walk reaches the end without a match. The comment says that's how a relationship joins its chain too. So a new key costs a walk down the chain, then two writes, one for the new property and one for its owner. Find property reads the node's record, and for the key name, it returns the name from the record, so the name is a property to a query, without a record of its own. Otherwise, it walks the chain until a key matches, and returns its value, or nullopt when the node doesn't have it. Reading a property is a walk down a short chain, a record at a time, much like following a node's relationships. Properties and relationship properties both read a chain from their record's first property. Chain walks it from the first id, collecting each key and value, which Value of reads out of each record, then reverses the list. Its comment says why: the chain is newest first, and callers get them oldest first, in the order they were first set. The name index's comment says its names can be longer than a node's now. In graph test dot c c, keeps properties in a chain adds the red fox, and gives it a weight of five thousand grams, then a diet, omnivore, then a new weight, six thousand. The comment says what that checks: setting a key again replaces its value, in the record it already has, so there's no second weight further down the chain. Find property gives the new weight, the name from the record, and no value for legs, which the fox doesn't have. There are two properties, not three, with the weight first, as it was set first. Then a relationship, the fox eating the rabbit, gets its source, Wikipedia, as in episode one, and relationship properties has to give it back. And keeps its records in a folder gives the rabbit four legs before the graph closes. It checks they're there once the graph opens again, so the properties file is written to the folder, and read back from it. And the inspector's test sees the node record's new bytes, which end in all ones, for no first property.

10:20 Let's build it, with the property records, and run all of the tests, from episode one on, the new test among them. All thirty-eight of them pass, and the old ones still find their nodes and walk their chains, with four bytes less for each name.

Facts with properties

10:35 Our facts are written as Cypher patterns, so their properties are going to be written as Cypher writes them, in braces, and the parser from episode seven can read them. In parser dot h, we include the graph, for Value and Property. Parse properties reads properties in braces, such as legs four and diet omnivore, as its comment says, with a string or a whole number for each key. In parser dot c c, we include cstdint, for a sixty-four-bit integer. In the parser class, parse properties alone is a second way in, next to parse query. It reads the properties, then checks that there's no token left after them, and its comment says what it's for: properties on their own, as the facts we load have them. Parse query stays the way in for a whole query, and in the next step, it reads its braces this way too. Then parse properties reads an opening brace, and in a do while loop, a key, which has to be a word, a colon, and a value. Each property goes on the list, for as long as a comma follows it. Last comes the closing brace, and over it, its line of grammar: braces around a key, a colon and a value, with commas between them. A value can't be a word or a list, which keeps our subset small, and the facts we load don't need either. Parse value takes a string as it is. Otherwise, from chars reads a number into a sixty-four-bit integer, as we did for hops. A token that isn't a number, or a number too big for it, fails, saying it expected a string or a whole number. Its line of grammar says a value is a string or a number. At the bottom, the parse properties function lexes the text, hands the tokens to a parser, and returns the properties, or the error, as Parse does for a query. In facts dot h, a node on its own, such as the rabbit, can be a fact now, and nodes and relationships can have properties in braces, as the comment says. In facts dot c c, we include utility, for move, and vector, then Abseil's status or, and our parser. Loading facts leans on the parser now, as the braces are Cypher, and a second reader for them would mean two sets of rules to keep in step. In an unnamed namespace, a part is a node or a relationship type in a fact, with the properties in braces after it, as its comment says, such as the red fox with four legs. Its text is a string view, a view into the line, so the name isn't copied until the graph keeps it. Read part reads one into part, which it fills in. Its comment says it returns false when it can't, and that the braces are written as in Cypher, so our parser reads them. The text and the properties have to fit in their records. Read part starts the part from the whole text and no properties, and when the text has a space and an opening brace, what comes from the brace on goes to parse properties. The text before the brace is the name or the type. A part that's empty, longer than max, or holds a bracket isn't one we can keep, as a bracket in a name would make the line hard to split, so it returns false. Then each property has to fit in a property record. Get if gives the value's string, when it is one. A key can be up to k max key long, a string up to k max string, and no key can be called name. The comment inside says why: a node's name is in its record, so it isn't a property to set. When all of that holds, Read part returns true, with the part filled in. Add with properties adds a node, and sets each of its properties on it. In load facts, a line still has to start and end with a bracket, or it's an error straight away. Then text is a view of the line without its first and last characters, which is what's inside the first and the last bracket, as its comment says. Then it looks for the relationship's two ends, as before. The comment says what a fact can be: a node on its own, or a node, a relationship and a node. With no relationship in it, there's one part to read, a node. With both ends of a relationship, in order, there are three: two nodes, and a type between them, read one after the other until one of them fails. If the parts weren't read, the line is the error, as before. Then the first node is added, with its properties, and for a node on its own, that's the line done. Otherwise, the second node is added, then the relationship between them, and its properties are set on the id that Add relationship returns. In graph test dot c c, reads properties in braces loads two facts. The first is the fox on its own, with its legs and its diet, and the second is the fox eating the rabbit, with a source on the relationship and legs on the rabbit. Then it checks three of them: the fox's diet, the rabbit's legs, and the source on relationship zero, the first one added. Each check reads the value back through the graph, from the records the loader wrote. Stops at a line that is not a fact checks three lines now, in a loop, with a fresh graph for each. Each line's error has to be invalid argument, with the line as its message, so sg load can say which line it couldn't read. The comment lists the three: a line that isn't a pattern, a value that's a word and not a string or a number, and a name set as a property. And the animals get properties, at the top of their file. Each of the four animals has a line of its own, with a rough weight in grams for an adult, its legs and its diet, which the comment at the top says. The fox weighs about six kilos, the rabbit two, the owl half a kilo, and the field mouse twenty-five grams. The fox and the field mouse eat plants and animals, the rabbit eats plants, and the owl eats animals. The fox eating the rabbit gets its source, Wikipedia, as it had in Neo4j in episode one.

Properties on disk

16:15 Let's build it, with properties in the facts this time, and run the tests, which check the facts' braces now. All thirty-nine of them pass, the old ones and the new one. We load the animals, with their properties this time, and the folder has a fourth file, properties, two pages long: a header, and a page of thirteen records. So let's look at that page with xxd. Dash s skips the header's four thousand and ninety-six bytes, and dash l shows the first hundred and twenty-eight, four records. Record zero is the fox's weight, with its key, weight_g, padded to twelve bytes, then a zero byte for an integer, and seventy and seventeen in hex, which is six thousand, with its lowest byte first. Its next property is all ones, as it's the end of the chain. Record one is the fox's legs, four, which points back at record zero. Record two is its diet, with a one for a string, and then omnivore, and its next property is one, so the fox's chain goes from its diet to its legs and its weight, newest first. The fourth record is the rabbit's weight, which ends the rabbit's chain, as it was set first.

WHERE

17:21 Now for the query side, and a plan for a query with a WHERE, which asks what the two-legged animals eat, when it weighs under a kilo. The start has no name, so the plan scans each node into slot a. The WHERE says a's legs have to be two, and that filter can run as soon as a has a slot, before the expand, so the nodes it drops aren't expanded at all. Then the expand follows EATS to b, and the filter on b's weight comes after it, as b has no slot before then. And last, the names in slots a and b come back, a row for each match. Real planners move filters as early as they can go too, and call it pushing a predicate down.

18:05 A comparison needs an equals sign, and less than or equal and the like, which are two characters each. In lexer dot h, the comment on a symbol says it can be a comparison now, such as less than or equal. In lexer dot c c, before the one-character symbols, Lex looks at the next two characters. Less than or equal, greater than or equal, and less than greater than, which is Cypher's not equal, each become a symbol, as the comment says: two characters that make one comparison. Then equals joins the one-character symbols, for a comparison that's a single character. Its new test, reads comparisons, lexes a WHERE with greater than or equal, AND, and not equal. Lex returns the tokens, and Describe writes them as one string, as in the other test. Each comparison has to be a token of its own, and not two. In parser dot h, a node pattern has properties now, in place of a name, as the name is a property like the others. The comment says its variable and its properties are both optional. A property lookup is a variable's property, such as a dot name, or b dot weight_g. A comparison is what WHERE holds: a property, an operator, which is one of the six its comment lists, and a value. Its operator is kept as the text of its symbol, which is how a plan is going to show it. The example in the query's comment gets a WHERE, and the query gets a list of comparisons, where, which are joined by AND, so a row has to pass them all. Returns becomes a list of lookups, the properties returned, in order. With those three, a query says what to match, which rows to keep, and what to give back. In parser dot c c, we include algorithm, for find, and iterator, for begin and end, which work on a plain array as they do on a vector. K operators is an array of six string views, which point at text the program holds already, without copying it. C++ works out the array's length from its six values. Its comment says what they are: the operators a comparison in WHERE can have, which are Cypher's six written as symbols. Find, from algorithm, is going to look a symbol up in it. The grammar over parse query changes, as its first line ends after the pattern now. A WHERE comes next, of comparisons joined by AND, and RETURN lists lookups now. In parse query, WHERE is optional, as it is in Cypher. When it's there, a do while loop reads a comparison, with parse comparison, and pushes it onto the query's where. It reads another after each AND, until there's no AND left. Then returns reads lookups, in place of variables, each of them a variable and a key. The old code that read a name in braces goes, and the grammar over parse node says a node has optional properties now, in place of a name. Parse node reads a node's properties with parse properties, from the facts, when there's an opening brace. So a node can match any property now, with its name among them. Parse comparison reads a lookup first, the property to compare. Then comes an operator, which has to be a symbol that find comes across in k operators, and anything else fails, saying it expected a comparison. Then comes the value, read with parse value, as the facts' values are. Parse lookup reads a variable, a dot and a key, each of which has to be there, and its grammar is variable dot key. And Expect word goes, as no part of the grammar expects the word name any more. In parser test dot c c, reads a pattern finds the start's name among its properties now, as a string value, and the third node has no properties. It checks the returned lookups too, the second of which is c and name. Reads where and properties parses a start with two legs, which is a number, and a WHERE with a weight under a thousand and a diet that isn't herbivore. Both comparisons are on b, joined by AND, and the start's legs are a property in braces. The start's legs come back as a number, two, as the lexer reads digits as a number token. It checks what the WHERE holds: two entries, the first one's key and operator, and the second one's value, a string. And it checks the second returned lookup, b's weight, which is a property other than the name. The last test, says what it expected, gets a fourth error: a WHERE with the word is in place of an operator, which fails at the word is, twenty-three characters in.

Filters and values

22:29 In plan dot h, we include optional, for a value that may be missing. A filter compares a property now: its comment says it keeps the rows whose node in slot from has a property, named text, that compares with value as op says. Op and value are new fields, a filter's operator, equals by default, and the value it compares with, as their comment says. Giving them defaults keeps the other steps' lines short, as a seek or an expand has no use for them. The plan gets the keys of the properties it returns, next to their slots, and the comment over the two says so. A slot and a key together say which property of which node goes in a column. Plan query's comment adds the variables a WHERE compares to its errors, as a WHERE can name a variable that the pattern doesn't have. A cell is a value a query returns, together with the node it's a property of, and its comment says what a node without the property gives: Cypher's null, which is no value. So the cell's node is an id, for the page to fill in the node a value came from, and the value is optional. A result's rows are rows of cells, and Value text writes a value as sg query prints it, a number in digits, and a string as it is. In plan dot c c, we include compare, for C++20's three-way comparison, cstdint, for a sixty-four-bit integer, and variant, for get if. In the unnamed namespace, Add filter goes, as filters come from more places now, and Literal writes a value as Cypher does, as its comment says. A string goes in quotes, with str cat, and a number goes through Value text, for EXPLAIN, so a plan reads the way the query was written. Name of returns the name a node of the pattern gives, if it gives one, which the index can find, as its comment says. It looks through the node's properties for the key name, with a string for its value. A name that's a number can't be in the index, so a start like that is scanned. Add filters adds the filters on a node of the pattern. It takes the whole query, for its WHERE, then the node, its slot, whether the plan found the node with a seek, and the plan to add to. Each of the node's properties becomes an equals filter, on its key and its value. After a seek, the name has been found already, so the name is skipped. A node without a variable can't be named in a WHERE, so for those, that's all. Then, for a node with a variable, each comparison in WHERE on that variable becomes a filter, with its key, its operator and its value. The comment over it says when that happens: once the node has a slot. The filters come as early as they can, so the rows they drop are expanded no further. Compare decides whether a property compares with a value as the operator says, and it's where Cypher's rules for a missing value come in. When there's no property, Compare returns false, as null keeps no row. When its value holds another type than the value it's compared with, the two aren't equal, so not equals is true, and the rest are false. Otherwise, C++20's three-way comparison, the operator that looks like a spaceship, compares the two in one go. Here both variants hold one type, so it compares their values, and gives an ordering, less, equal or greater, which each operator checks against zero. The comment over Compare says the rule: a missing property is Cypher's null, which no comparison keeps, and values of different types, such as a string and a number, aren't equal, and have no order. Value text, outside the namespace, writes a number with str cat, and returns a string as it is, for EXPLAIN and for sg query to print. In plan query, each comparison in WHERE has to name a variable of the pattern, which none of checks, or it's an error, as it is for a returned variable that isn't there. The start's name comes from Name of, and when there is one, the plan starts with a seek, as before. Then come the start's filters, with seek telling Add filters whether the seek has matched the name already. After each node's expands come its filters, so a filter on b comes after the expand that reaches b, and before any expand that leaves it. And each returned lookup finds the slot of its variable, which the lookup holds now, for the error message too, then adds its key to the plan's keys, and its column is the variable, a dot and the key. In Run, a filter finds the row's property with Find property, and keeps the row when Compare says so. A filter on the name reads the node's record and no more, and any other property walks the node's chain. So a filter on a name is the cheapest, and a scan with a filter on another property reads the chain of each node in the graph. After the last step, the result is made from what's left. The set that DISTINCT uses to remember what it's seen holds lists of values now, in place of lists of node ids, and the lines that copied each row's node ids go. Then each row makes a cell for each column, with the node from the column's slot, and that node's property, found with the column's key. The values go in a list of their own too, which is what DISTINCT compares. The comment says DISTINCT keeps the first of the rows that return equal values now, as two nodes can share a diet. In Explain, a filter is written as the slot's property, the operator and the literal, the way a WHERE is. Add filters is where a WHERE moves ahead of the expands, as its filters go in as soon as their node has a slot. In plan test dot c c, the meadow's filter, in the test from episode seven, reads p dot name equals Meadow now, as EXPLAIN writes each filter as a comparison. The meadow's test is the one that scans, as its start has no name, and its filter on p comes after the expand that reaches p. Filters a node as soon as it has a slot plans a two-legged start, with a WHERE on both of its nodes. The query matches a start with two legs, along EATS, to b, and returns b's name. B's comparison is written first, so the order of the filters has to come from the planner, and not from the query. The plan scans, and filters a's legs and then a's weight before the expand, and b's diet after it. Refuses variables it cannot plan adds a WHERE on a missing variable, and the Answers helper goes through the cells of each row, writing a cell's value with Value text, or null when it has none. The run test's animals get weights and diets, on lines of their own: the red fox, the rabbit, the field mouse and the tawny owl. Returns distinct rows asks for the diets of the animals that eat, omnivore, herbivore and carnivore, once each. Its comment says why that's a test of values: the fox and the field mouse are both omnivores. And filters and returns properties asks who eats something under a thousand grams. That's the fox and the owl, with the field mouse. Then what the rabbit eats is the clover, with a null weight, as the comment says the clover has none. A missing property comes back as null in a RETURN, where a filter on it would have dropped the row. In main dot c c, the table's rows are rows of cells now. Sg query prints each cell's value with Value text, or null when there isn't one. It takes a reference to the value first, so clang-tidy can see that it's checked before it's read. In api dot c c, the JSON for the page takes the node from each cell, so the page goes on showing names as it did.

29:53 Let's build it, with WHERE in the parser and the planner, and run the tests, the four new ones among them. All forty-three of them pass, from the lexer's to the planner's. With the animals loaded, let's ask what the red fox eats, and how much each of those weighs. The rabbit weighs about two thousand grams, and the field mouse twenty-five. With EXPLAIN in front, the query from our plan asks what two-legged animals eat when it weighs under a kilo. It filters on the legs straight after the scan, before the expand, as we drew it. Then we run it, without EXPLAIN, and it's the tawny owl, which eats the field mouse. And the mammals' diets, with DISTINCT, come back once each, omnivore and herbivore.

Properties in the UI

30:39 Last comes the page, starting with what sg serve sends it. In api dot h, Node JSON's comment grows, as the inspector is going to show more of the record. It adds each relationship's properties, and the node's chain of properties, newest first, each with where it's kept. And Query JSON's comment says it answers with each value and the id of its node, in place of a name, as a column can hold any property now. In api dot c c, we include variant. Value JSON writes a value as JSON, as its comment says. A missing one is null, and a number goes in as it is, which JavaScript reads without losing precision up to about nine quadrillion, far above any weight in grams we're going to keep. A string goes through Json string, in quotes. Properties JSON writes a list of properties as a JSON object, with a field for each key. An object suits a relationship's few properties, which the page goes through key by key, and writes in braces. In Node JSON, each relationship in the chain gets its properties, as an object, between its type and the node it goes to. Relationship properties reads them from the relationship's own chain. Then, after the relationships, the JSON goes on with an array for the node's properties. A loop walks the node's chain of properties, from its first property, the way the relationships' loop walks theirs, so the two lists sit side by side in the JSON. For each record, it reads the record through the graph's property accessor, and puts a comma before each record but the first. Then it writes the record's id, its key, its value through Value of, and its page and offset, through Where, from episode four. The comment over the loop says what it writes: the properties' chain, newest first too, each record with where it's kept. In Query JSON, each cell is its node's id and its value, through Value JSON. In api test dot c c, shows where a record and its chains are kept gives the fox's meal a source, and the fox four legs. The record's bytes end with the fox's first property, one, as the relationship's source is property zero, which the comment explains. The relationships carry their properties, and the node's properties list its legs, at offset thirty-two. Answers with rows or a plan returns the rabbit's name and legs, and the fox's legs, which are null, as the comment says the fox has no legs property here. In index dot html, the inspector gets a heading and a list for the properties, newest first, above the relationships, as a node's own facts read best before what it's connected to. Both lists get a class, so the script can tell them apart. In graph dot js, the comment over the click handler gets longer, as the inspector shows more than it did in episode four, where it showed the record, its bytes and its relationships. Clicking a node shows its record as sg keeps it on disk: where it is, its bytes, and its chain of properties, which is the new part of the comment. The server sends that chain now, newest first, with each record's page and offset. Then comes its chain of relationships, which light up in the graph too, as before. Then two helpers come before inspect, for writing values. Literal writes a value as Cypher does, as its comment says: a string in quotes, and a number as it is. Braces writes a relationship's properties in braces, the way a pattern does, or nothing when it has none, which its comment says too. Object entries turns the object into a list of keys with their values, and each of them becomes the key, a colon and the value. In inspect, the properties list gets a line for each property record, with its key and its value, then its id, page and offset, as the relationships have. The relationships list is selected by its class now, and each relationship's line gets its properties in braces after its type. And in the results table, each cell shows its value, or null. And the README says what the page shows now.

34:46 Back in the browser, here's the page at the end of the episode, with the animals laid out as before. Clicking the red fox opens its record, and under its bytes come its properties, newest first. Its diet is property two, then its legs and its weight, at offsets sixty-four, thirty-two and zero of page one. In its relationships, the one where it eats the rabbit has its source, Wikipedia, in braces. The tawny owl's chain starts at property eleven, its diet, as its line came last of the four animals. And let's ask who lives in the forest, with their weights and their diets. It's the fox, at six thousand grams, which eats plants and animals, and the owl, at five hundred grams, which eats animals.

What it costs a walk

35:29 Our records are still thirty-two bytes, so a walk should cost what it did, but let's measure it. On last episode's code, we load a million relationships from episode three, and time two-hop walks from a thousand people. The median walk takes about six point five microseconds.

35:46 Then on this episode's code, where the folder has a properties file, which is empty, as these people have no properties. The walks read the node and relationship records they read before, and the median takes about six point zero microseconds, close to what it was, as the walk does just the work it did before.

What's missing

36:03 Our properties leave a fair amount out, and some of it is worth knowing about. A value is a whole number or a string that fits in fifteen bytes with its zero, so there are no decimals yet, and Neo4j keeps a longer string in records of its own, chained from the property. Nodes have no labels yet, such as Animal or Plant, which a Cypher pattern uses to say what kind of node it wants. A query that starts from a property scans each node, as our index holds names alone, and is used for a name in braces and nowhere else. And properties come from the facts we load, as our Cypher can't set them yet, nor read a relationship's. A graph that's written to needs to survive a crash part way through a write, which is what a write-ahead log is for. Next time, nodes get labels, for a much bigger graph, from Wikidata.

Wrap-up

36:54 So, today smollerGraph got properties, from the bytes on disk up to Cypher. A node or a relationship points at a chain of property records, in a file of their own, with a key and a value in each thirty-two-byte record. The facts we load have properties in braces, which our Cypher parser reads. Cypher has WHERE now, with each filter planned as soon as its node has a slot, and RETURN gives any property of a node. And the page shows a node's chain of properties, with where each is kept on disk, next to its relationships.

37:29 This is going to be an evolving project, so I'd love to hear from you. Let me know in the comments which properties you'd give the animals, and if you'd like to follow along, subscribe to the channel. I hope you find this useful, and thanks for watching.