Skip to main content

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}