yo_graph/props.rs
1//! The properties of nodes and of edges, which are documents (`11` section 3).
2//!
3//! A property graph is an adjacency structure and a pile of key value maps, and
4//! the second half is the part that engines get wrong. Neo4j gives every
5//! property its own store and pays a pointer chase per read. FalkorDB keeps a
6//! matrix of attribute vectors. Both are a second data model built to hold what
7//! the document model already holds, and both then need their own indexes,
8//! their own encoding and their own answer to nested values.
9//!
10//! There is a document model here, so a node's properties are a document and an
11//! edge's properties are a document. That is not a saving in lines of code, it
12//! is what makes `#[yo(index)]` on a node's field mean the same thing as
13//! `#[yo(index)]` on a document's field, and it is why a graph gets path
14//! indexes, key interning and nested values without any of the three being
15//! written twice.
16//!
17//! ```
18//! use yo_doc::Builder;
19//! use yo_graph::Props;
20//!
21//! let mut b = Builder::new();
22//! b.begin_object()?;
23//! b.key(b"name")?;
24//! b.text("ada")?;
25//! b.end_object()?;
26//! let doc = b.finish()?.to_vec();
27//!
28//! let mut people = Props::new();
29//! people.put(41_920, &doc)?;
30//! let got = people.get(41_920).expect("stored");
31//! assert_eq!(got.get(b"name").and_then(|n| n.as_text()), Some("ada"));
32//! # Ok::<(), yo_common::Error>(())
33//! ```
34//!
35//! # Interning is worth more here than anywhere else
36//!
37//! A document collection repeats its field names on every document, which is
38//! what key interning is for. A graph repeats them harder: an edge property map
39//! is two or three fields and there are ten to a hundred times as many edges as
40//! nodes, so the names are most of what an edge property store weighs. Two byte
41//! ids against a `weight` and a `since` on fifty million edges is the difference
42//! between the properties fitting beside the adjacency and not.
43//!
44//! # Why the id is bytes
45//!
46//! [`Docs`] is keyed by bytes because that is what a document collection is
47//! keyed by, and a node id here is a `u64`, so it becomes eight bytes. They are
48//! big endian, which costs a byte swap that no lookup notices and buys the one
49//! thing byte order can buy: if this ever grows an ordered scan, the keys sort
50//! the way the numbers do.
51
52use yo_common::Result;
53use yo_doc::{Doc, Docs, IndexKind, Key, Keys, Value};
54
55/// The properties of a set of nodes, or of a set of edges.
56///
57/// One of these holds documents under integer ids, with whatever path indexes
58/// the caller declared. A [`crate::Graph`] has two: one keyed by node id and one
59/// keyed by edge slot.
60#[derive(Debug, Default)]
61pub struct Props {
62 docs: Docs,
63}
64
65/// A node id or an edge slot, as the bytes a document collection is keyed by.
66///
67/// Big endian, so that the keys sort the way the numbers do. Eight bytes for
68/// both, because an edge slot is a `u32` and widening it here means the two
69/// stores are the same shape and a later move to `u64` slots is not a format
70/// change.
71#[must_use]
72pub fn id_key(id: u64) -> [u8; 8] {
73 id.to_be_bytes()
74}
75
76impl Props {
77 /// An empty store.
78 pub fn new() -> Props {
79 Props { docs: Docs::new() }
80 }
81
82 /// Stores `doc` under `id`, replacing whatever was there.
83 ///
84 /// Answers whether this was new, which is what a caller counting nodes
85 /// wants and what an overwrite is not.
86 ///
87 /// # Errors
88 ///
89 /// Whatever [`Docs::put_bytes`] answers: the document is malformed, or a
90 /// value that an index covers is too long to be an index key. In the second
91 /// case nothing is stored, so a document is never in the collection and out
92 /// of its own indexes.
93 pub fn put(&mut self, id: u64, doc: &[u8]) -> Result<bool> {
94 self.docs.put_bytes(&id_key(id), doc)
95 }
96
97 /// The same from a value that is already read.
98 ///
99 /// # Errors
100 ///
101 /// The same as [`Props::put`].
102 pub fn put_value(&mut self, id: u64, value: Value<'_>) -> Result<bool> {
103 self.docs.put(&id_key(id), value)
104 }
105
106 /// What is stored under `id`.
107 #[must_use]
108 pub fn get(&self, id: u64) -> Option<Doc<'_>> {
109 self.docs.get(&id_key(id))
110 }
111
112 /// The stored bytes, for a caller that is about to write them somewhere
113 /// rather than read them.
114 #[must_use]
115 pub fn bytes(&self, id: u64) -> Option<&[u8]> {
116 self.docs.bytes(&id_key(id))
117 }
118
119 /// Whether anything is stored under `id`.
120 #[must_use]
121 pub fn contains(&self, id: u64) -> bool {
122 self.docs.contains(&id_key(id))
123 }
124
125 /// Takes out what is under `id`, and says whether there was anything.
126 pub fn remove(&mut self, id: u64) -> bool {
127 self.docs.remove(&id_key(id))
128 }
129
130 /// How many ids have properties.
131 ///
132 /// Not how many nodes the graph has. A node with no properties is a node,
133 /// and it is not in here.
134 #[must_use]
135 pub fn len(&self) -> usize {
136 self.docs.len()
137 }
138
139 /// Whether nothing has properties.
140 #[must_use]
141 pub fn is_empty(&self) -> bool {
142 self.docs.is_empty()
143 }
144
145 /// Declares an index over `path`, and backfills it over what is already
146 /// stored.
147 ///
148 /// # Errors
149 ///
150 /// Whatever [`Docs::create_index_bytes`] answers: the path does not parse,
151 /// it is already indexed, or a document already here has a value under it
152 /// that cannot be an index key.
153 pub fn create_index(&mut self, path: &str, kind: IndexKind) -> Result<()> {
154 self.docs.create_index_bytes(path.as_bytes(), kind)
155 }
156
157 /// Drops the index over `path`, and says whether there was one.
158 pub fn drop_index(&mut self, path: &str) -> bool {
159 self.docs.drop_index(path)
160 }
161
162 /// Calls `f` for every id whose document has `key` under `path`, and
163 /// answers how many that was.
164 ///
165 /// # Errors
166 ///
167 /// [`yo_common::Code::Invalid`] if `path` is not indexed, because a query
168 /// that would have to scan the whole store is a mistake rather than a slow
169 /// answer.
170 pub fn find(&self, path: &str, key: &Key, mut f: impl FnMut(u64, Doc<'_>)) -> Result<usize> {
171 self.docs.find(path, key, |id, doc| {
172 if let Some(id) = read_key(id) {
173 f(id, doc);
174 }
175 })
176 }
177
178 /// How many ids have `key` under `path`, without reading any of them.
179 ///
180 /// # Errors
181 ///
182 /// The same as [`Props::find`].
183 pub fn count(&self, path: &str, key: &Key) -> Result<usize> {
184 self.docs.count(path, key)
185 }
186
187 /// Every id with properties, and what they are.
188 pub fn iter(&self) -> impl Iterator<Item = (u64, Doc<'_>)> {
189 self.docs
190 .iter()
191 .filter_map(|(id, doc)| read_key(id).map(|id| (id, doc)))
192 }
193
194 /// The key table, which is where the field names went.
195 #[must_use]
196 pub fn keys(&self) -> &Keys {
197 self.docs.keys()
198 }
199
200 /// Takes everything out.
201 pub fn clear(&mut self) {
202 self.docs.clear();
203 }
204
205 /// What this store weighs.
206 #[must_use]
207 pub fn memory_bytes(&self) -> usize {
208 self.docs.memory_bytes()
209 }
210
211 /// The collection underneath, for the range and scan operations that are
212 /// the document model's rather than the graph's.
213 #[must_use]
214 pub fn docs(&self) -> &Docs {
215 &self.docs
216 }
217}
218
219/// The id a key stands for, or `None` if it is not one this store wrote.
220///
221/// It cannot be anything else today, since every write goes through
222/// [`Props::put`]. It is checked rather than asserted because the alternative is
223/// a panic in a callback that a caller has no way to guard against, and a key of
224/// the wrong length means the collection was handed to something that is not
225/// this.
226fn read_key(k: &[u8]) -> Option<u64> {
227 let raw: [u8; 8] = k.try_into().ok()?;
228 Some(u64::from_be_bytes(raw))
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use yo_doc::Builder;
235
236 fn person(name: &str, city: &str, age: i64) -> Vec<u8> {
237 let mut b = Builder::new();
238 b.begin_object().unwrap();
239 b.key(b"age").unwrap();
240 b.int(age).unwrap();
241 b.key(b"city").unwrap();
242 b.text(city).unwrap();
243 b.key(b"name").unwrap();
244 b.text(name).unwrap();
245 b.end_object().unwrap();
246 b.finish().unwrap().to_vec()
247 }
248
249 #[test]
250 fn a_node_keeps_its_properties() {
251 let mut p = Props::new();
252 assert!(p.put(1, &person("ada", "london", 36)).unwrap());
253 assert!(
254 !p.put(1, &person("ada", "turin", 37)).unwrap(),
255 "an overwrite is not new"
256 );
257 assert_eq!(p.len(), 1);
258 let got = p.get(1).expect("stored");
259 assert_eq!(got.get(b"city").and_then(|c| c.as_text()), Some("turin"));
260 assert_eq!(got.get(b"age").and_then(|a| a.as_int()), Some(37));
261 }
262
263 #[test]
264 fn an_id_that_was_never_written_has_nothing() {
265 let mut p = Props::new();
266 p.put(1, &person("ada", "london", 36)).unwrap();
267 assert!(p.get(2).is_none());
268 assert!(!p.contains(2));
269 assert!(!p.remove(2));
270 assert!(p.remove(1));
271 assert!(p.is_empty());
272 }
273
274 #[test]
275 fn the_whole_range_of_a_u64_id_works() {
276 // Zero, the top of a u32 either side, and the top of a u64. A key that
277 // was truncated to four bytes or read at the wrong width would collide
278 // two of these.
279 let ids = [
280 0u64,
281 1,
282 u64::from(u32::MAX) - 1,
283 u64::from(u32::MAX),
284 u64::from(u32::MAX) + 1,
285 u64::MAX,
286 ];
287 let mut p = Props::new();
288 for (i, id) in ids.into_iter().enumerate() {
289 assert!(
290 p.put(id, &person(&format!("n{i}"), "here", i as i64))
291 .unwrap()
292 );
293 }
294 assert_eq!(p.len(), ids.len());
295 for (i, id) in ids.into_iter().enumerate() {
296 let got = p.get(id).unwrap_or_else(|| panic!("{id} is missing"));
297 assert_eq!(
298 got.get(b"name").and_then(|n| n.as_text()),
299 Some(&*format!("n{i}"))
300 );
301 }
302 }
303
304 #[test]
305 fn an_index_finds_nodes_by_a_property() {
306 let mut p = Props::new();
307 p.create_index("$.city", IndexKind::Equality).unwrap();
308 p.put(1, &person("ada", "london", 36)).unwrap();
309 p.put(2, &person("grace", "london", 45)).unwrap();
310 p.put(3, &person("edsger", "austin", 51)).unwrap();
311
312 let mut found = Vec::new();
313 let n = p
314 .find("$.city", &Key::text("london"), |id, _| found.push(id))
315 .unwrap();
316 assert_eq!(n, 2);
317 found.sort_unstable();
318 assert_eq!(found, vec![1, 2]);
319 assert_eq!(p.count("$.city", &Key::text("austin")).unwrap(), 1);
320 }
321
322 #[test]
323 fn an_index_declared_after_the_fact_backfills() {
324 let mut p = Props::new();
325 p.put(1, &person("ada", "london", 36)).unwrap();
326 p.put(2, &person("grace", "london", 45)).unwrap();
327 p.create_index("$.city", IndexKind::Equality).unwrap();
328 assert_eq!(p.count("$.city", &Key::text("london")).unwrap(), 2);
329
330 // And a removal takes the node out of the index, not only out of the
331 // store, which is the failure that shows up as a query answering ids
332 // that are not there any more.
333 assert!(p.remove(1));
334 assert_eq!(p.count("$.city", &Key::text("london")).unwrap(), 1);
335 }
336
337 #[test]
338 fn the_field_names_are_stored_once() {
339 let mut p = Props::new();
340 for id in 0..100u64 {
341 p.put(id, &person("someone", "london", id as i64)).unwrap();
342 }
343 // Three names, whatever the document count is. That is the whole point
344 // of interning and it is worth an assertion rather than a comment.
345 assert_eq!(p.keys().len(), 3);
346 assert!(p.get(7).expect("stored").value().is_interned());
347 }
348
349 #[test]
350 fn iterating_gives_back_the_ids_that_went_in() {
351 let mut p = Props::new();
352 for id in [5u64, 9, 41_920] {
353 p.put(id, &person("someone", "london", 1)).unwrap();
354 }
355 let mut ids: Vec<u64> = p.iter().map(|(id, _)| id).collect();
356 ids.sort_unstable();
357 assert_eq!(ids, vec![5, 9, 41_920]);
358 }
359}