Skip to main content

yo_graph/
graph.rs

1//! A property graph: the adjacency plane with a document behind every node and
2//! every edge (`11` section 3).
3//!
4//! [`crate::Adjacency`] is the structure and [`crate::Props`] is what hangs off
5//! it. This is the two of them together, plus the one thing neither of them can
6//! own on its own: which edge slot an edge got.
7//!
8//! ```
9//! use yo_doc::Builder;
10//! use yo_graph::{Dir, Graph};
11//!
12//! const FOLLOWS: u32 = 1;
13//!
14//! fn doc(f: impl FnOnce(&mut Builder) -> yo_common::Result<()>) -> Vec<u8> {
15//!     let mut b = Builder::new();
16//!     f(&mut b).unwrap();
17//!     b.finish().unwrap().to_vec()
18//! }
19//!
20//! let mut g = Graph::new();
21//! g.put_node(1, &doc(|b| { b.begin_object()?; b.key(b"name")?; b.text("ada")?; b.end_object() }))?;
22//! g.put_node(2, &doc(|b| { b.begin_object()?; b.key(b"name")?; b.text("grace")?; b.end_object() }))?;
23//! let e = g.link(1, 2, FOLLOWS, &doc(|b| { b.begin_object()?; b.key(b"since")?; b.int(2026)?; b.end_object() }))?;
24//!
25//! assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), [2]);
26//! assert_eq!(g.node(2).and_then(|n| n.get(b"name").and_then(|v| v.as_text())), Some("grace"));
27//! assert_eq!(g.edge(e).and_then(|n| n.get(b"since").and_then(|v| v.as_int())), Some(2026));
28//! # Ok::<(), yo_common::Error>(())
29//! ```
30//!
31//! # A node is its properties
32//!
33//! The adjacency plane has no node table. A node is a run of neighbours, so a
34//! node with no edges is not in it at all, and asking whether a node exists is
35//! not a question it can answer.
36//!
37//! So the node property store is the node table. [`Graph::put_node`] with an
38//! empty object is how an isolated node exists, [`Graph::has_node`] is a lookup
39//! in it, and [`Graph::nodes`] is its count. That is one structure doing two
40//! jobs rather than two structures that can disagree about which nodes there
41//! are, and the empty object it costs is four bytes.
42//!
43//! # Edge slots
44//!
45//! An edge's properties are keyed by a slot, and the slot is what the adjacency
46//! plane carries beside each neighbour. [`Graph::link`] hands one out and
47//! [`Graph::unlink`] gives it back, through a free list, because an edge store
48//! that only ever counts up turns a graph that churns into a store that grows
49//! forever.
50//!
51//! A slot is reused only after the properties under it are gone, which is what
52//! stops a new edge from inheriting an old edge's fields. That ordering is the
53//! whole of why the free list is here rather than in [`crate::Adjacency`]: the
54//! plane does not know there is a property store, and a free list that hands
55//! out a slot whose document is still there would be worse than no free list.
56//!
57//! # Parallel edges
58//!
59//! Linking the same pair twice under the same label leaves two edges, because
60//! that is what [`crate::Adjacency::link`] does and what a property graph means
61//! by a multigraph. Each gets its own slot and so its own properties, which is
62//! the point: two `RATED` edges between the same person and the same film with
63//! different scores and different dates is the case, not the corner case.
64
65use yo_common::{Code, Error, Result};
66use yo_doc::{Doc, IndexKind, Key};
67
68use crate::{Adjacency, Dir, Props};
69
70/// An empty object, which is what an isolated node's properties are.
71///
72/// A container header with a count of zero, in the flags a `Builder` writes an
73/// empty object with. It is four bytes written out rather than a `Builder` run
74/// because it is a constant and building it would allocate once per node that
75/// has no properties, and a test below holds it against what a `Builder`
76/// actually produces so it cannot drift.
77const EMPTY_OBJECT: [u8; 4] = EMPTY_OBJECT_HEAD.to_le_bytes();
78
79/// Tag 7 is a container, bit 4 is sorted and bit 5 carries offsets, which is
80/// what every object this version writes has, and the count is in the top three
81/// bytes. `yo_format::doc_flags` names the same bits.
82const EMPTY_OBJECT_HEAD: u32 = 7 | (1 << 4) | (1 << 5);
83
84/// What to pass as the properties of a node or an edge that has none.
85///
86/// An edge still needs a document, because the property store is keyed by the
87/// edge slot and a slot with nothing under it would be a slot that later reads
88/// have to guard against. This is the four bytes that document is, so a caller
89/// with nothing to say does not have to run a `Builder` to say it.
90pub const NO_PROPS: &[u8] = &EMPTY_OBJECT;
91
92/// A property graph.
93#[derive(Debug, Default)]
94pub struct Graph {
95    adj: Adjacency,
96    nodes: Props,
97    edges: Props,
98    /// The next slot never handed out.
99    next: u32,
100    /// Slots handed back, whose properties are already gone.
101    free: Vec<u32>,
102    /// Every label that has an edge, in order. Small, because a schema has
103    /// tens of edge types and not thousands, and a linear structure beats a
104    /// hash at that size.
105    labels: Vec<u32>,
106}
107
108impl Graph {
109    /// An empty graph that indexes both directions.
110    #[must_use]
111    pub fn new() -> Graph {
112        Graph {
113            adj: Adjacency::new(),
114            ..Graph::default()
115        }
116    }
117
118    /// An empty graph that indexes outgoing edges only.
119    ///
120    /// Half the adjacency memory, and [`Graph::neighbours`] in [`Dir::In`]
121    /// answers nothing. [`Graph::remove_node`] cannot find the edges that point
122    /// at a node either, so removing a node from one of these leaves the edges
123    /// into it in place, and it says so by refusing.
124    #[must_use]
125    pub fn out_only() -> Graph {
126        Graph {
127            adj: Adjacency::out_only(),
128            ..Graph::default()
129        }
130    }
131
132    /// Stores `props` under node `id`, replacing whatever was there, and
133    /// answers whether the node is new.
134    ///
135    /// # Errors
136    ///
137    /// Whatever [`Props::put`] answers: the document is malformed, or a value
138    /// an index covers cannot be an index key.
139    pub fn put_node(&mut self, id: u64, props: &[u8]) -> Result<bool> {
140        self.nodes.put(id, props)
141    }
142
143    /// Makes sure node `id` exists, with no properties if it did not.
144    ///
145    /// # Errors
146    ///
147    /// Only if a declared index refuses an empty object, which cannot happen,
148    /// and is returned rather than swallowed for the same reason every other
149    /// write here returns.
150    pub fn add_node(&mut self, id: u64) -> Result<bool> {
151        if self.nodes.contains(id) {
152            return Ok(false);
153        }
154        self.nodes.put(id, &EMPTY_OBJECT)
155    }
156
157    /// Node `id`'s properties.
158    #[must_use]
159    pub fn node(&self, id: u64) -> Option<Doc<'_>> {
160        self.nodes.get(id)
161    }
162
163    /// Whether the graph has node `id`.
164    #[must_use]
165    pub fn has_node(&self, id: u64) -> bool {
166        self.nodes.contains(id)
167    }
168
169    /// How many nodes the graph has.
170    #[must_use]
171    pub fn nodes(&self) -> usize {
172        self.nodes.len()
173    }
174
175    /// How many edges the graph has, counting a parallel edge as its own.
176    #[must_use]
177    pub fn edges(&self) -> usize {
178        self.adj.edges()
179    }
180
181    /// Every label with an edge under it, in order.
182    #[must_use]
183    pub fn labels(&self) -> &[u32] {
184        &self.labels
185    }
186
187    /// Adds an edge from `src` to `dst` under `label`, with `props` behind it,
188    /// and answers the slot the properties are under.
189    ///
190    /// Both ends are added as nodes if they were not there, because an edge
191    /// whose endpoints are not nodes is a dangling reference that every later
192    /// read has to guard against.
193    ///
194    /// # Errors
195    ///
196    /// [`Code::Full`] when every edge slot is taken, which is four billion live
197    /// edges. Otherwise whatever [`Props::put`] answers about `props`.
198    pub fn link(&mut self, src: u64, dst: u64, label: u32, props: &[u8]) -> Result<u32> {
199        self.add_node(src)?;
200        self.add_node(dst)?;
201        let slot = self.take_slot()?;
202        // The properties go in first. If the document is refused, the slot goes
203        // back and the adjacency plane never heard about the edge, so a failed
204        // link leaves nothing behind.
205        if let Err(e) = self.edges.put(u64::from(slot), props) {
206            self.free.push(slot);
207            return Err(e);
208        }
209        self.adj.link(src, dst, label, slot);
210        if let Err(at) = self.labels.binary_search(&label) {
211            self.labels.insert(at, label);
212        }
213        Ok(slot)
214    }
215
216    /// Removes one edge from `src` to `dst` under `label`, and answers the slot
217    /// it was under.
218    ///
219    /// With parallel edges it removes one of them and which one is whatever the
220    /// run's order left, the same as [`crate::Adjacency::unlink`].
221    pub fn unlink(&mut self, src: u64, dst: u64, label: u32) -> Option<u32> {
222        let slot = self.adj.unlink(src, dst, label)?;
223        self.release(slot);
224        Some(slot)
225    }
226
227    /// Edge `slot`'s properties.
228    #[must_use]
229    pub fn edge(&self, slot: u32) -> Option<Doc<'_>> {
230        self.edges.get(u64::from(slot))
231    }
232
233    /// Replaces edge `slot`'s properties.
234    ///
235    /// # Errors
236    ///
237    /// [`Code::NotFound`] if no edge is under that slot, so that a caller that
238    /// held a slot across a removal is told rather than quietly creating
239    /// properties for an edge that is gone. Otherwise whatever [`Props::put`]
240    /// answers.
241    pub fn put_edge(&mut self, slot: u32, props: &[u8]) -> Result<()> {
242        if !self.edges.contains(u64::from(slot)) {
243            return Err(Error::new(Code::NotFound, "no edge is under that slot")
244                .with_detail(format!("slot={slot}")));
245        }
246        self.edges.put(u64::from(slot), props)?;
247        Ok(())
248    }
249
250    /// Takes node `id` out, along with every edge at either end of it.
251    ///
252    /// Answers whether the node was there.
253    ///
254    /// # Errors
255    ///
256    /// [`Code::Unsupported`] on a graph built by [`Graph::out_only`] when the
257    /// node has any outgoing edge, because the edges pointing at it cannot be
258    /// found and removing it would leave them pointing at nothing. A node with
259    /// no edges at all is removed either way.
260    pub fn remove_node(&mut self, id: u64) -> Result<bool> {
261        if !self.nodes.contains(id) {
262            return Ok(false);
263        }
264        if !self.adj.indexes_incoming() {
265            let any = self
266                .labels
267                .iter()
268                .any(|&l| !self.adj.neighbours(id, l, Dir::Out).is_empty());
269            if any {
270                return Err(Error::new(
271                    Code::Unsupported,
272                    "this graph does not index incoming edges, so a node with edges cannot be removed",
273                )
274                .with_detail(format!("node={id}")));
275            }
276        }
277        // The labels are copied because unlinking borrows the graph, and there
278        // are tens of them.
279        let labels = self.labels.clone();
280        for label in labels {
281            // Out first, then in. Each end is snapshotted before anything is
282            // removed, because unlinking moves the last entry of a run into the
283            // hole it made and a walk over a run being edited would skip
284            // whatever moved.
285            let out: Vec<u64> = self.adj.neighbours(id, label, Dir::Out).to_vec();
286            for dst in out {
287                if let Some(slot) = self.adj.unlink(id, dst, label) {
288                    self.release(slot);
289                }
290            }
291            let into: Vec<u64> = self.adj.neighbours(id, label, Dir::In).to_vec();
292            for src in into {
293                if let Some(slot) = self.adj.unlink(src, id, label) {
294                    self.release(slot);
295                }
296            }
297        }
298        Ok(self.nodes.remove(id))
299    }
300
301    /// The neighbours of `node` under `label` in `dir`, in one contiguous run.
302    #[must_use]
303    pub fn neighbours(&self, node: u64, label: u32, dir: Dir) -> &[u64] {
304        self.adj.neighbours(node, label, dir)
305    }
306
307    /// The edge slots beside those neighbours, in the same order.
308    #[must_use]
309    pub fn edge_slots(&self, node: u64, label: u32, dir: Dir) -> &[u32] {
310        self.adj.edge_slots(node, label, dir)
311    }
312
313    /// The neighbours of `node` under `label` in `dir`, each with the slot of
314    /// the edge that got there.
315    pub fn hop(&self, node: u64, label: u32, dir: Dir) -> impl Iterator<Item = (u64, u32)> {
316        self.adj
317            .neighbours(node, label, dir)
318            .iter()
319            .copied()
320            .zip(self.adj.edge_slots(node, label, dir).iter().copied())
321    }
322
323    /// How many edges `node` has under `label` in `dir`.
324    #[must_use]
325    pub fn degree(&self, node: u64, label: u32, dir: Dir) -> usize {
326        self.adj.degree(node, label, dir)
327    }
328
329    /// Warms the run `node` is about to be walked through.
330    pub fn prefetch(&self, node: u64, label: u32, dir: Dir) {
331        self.adj.prefetch(node, label, dir);
332    }
333
334    /// Declares an index over a path into node properties, and backfills it.
335    ///
336    /// # Errors
337    ///
338    /// The same as [`Props::create_index`].
339    pub fn index_nodes(&mut self, path: &str, kind: IndexKind) -> Result<()> {
340        self.nodes.create_index(path, kind)
341    }
342
343    /// Declares an index over a path into edge properties, and backfills it.
344    ///
345    /// # Errors
346    ///
347    /// The same as [`Props::create_index`].
348    pub fn index_edges(&mut self, path: &str, kind: IndexKind) -> Result<()> {
349        self.edges.create_index(path, kind)
350    }
351
352    /// Calls `f` for every node whose properties have `key` under `path`.
353    ///
354    /// # Errors
355    ///
356    /// The same as [`Props::find`]: the path has to be indexed.
357    pub fn find_nodes(&self, path: &str, key: &Key, f: impl FnMut(u64, Doc<'_>)) -> Result<usize> {
358        self.nodes.find(path, key, f)
359    }
360
361    /// How many nodes have `key` under `path`, without reading any of them.
362    ///
363    /// # Errors
364    ///
365    /// The same as [`Graph::find_nodes`].
366    pub fn count_nodes(&self, path: &str, key: &Key) -> Result<usize> {
367        self.nodes.count(path, key)
368    }
369
370    /// How many edges have `key` under `path`, without reading any of them.
371    ///
372    /// # Errors
373    ///
374    /// The same as [`Graph::find_edges`].
375    pub fn count_edges(&self, path: &str, key: &Key) -> Result<usize> {
376        self.edges.count(path, key)
377    }
378
379    /// Calls `f` for every edge slot whose properties have `key` under `path`.
380    ///
381    /// # Errors
382    ///
383    /// The same as [`Props::find`].
384    pub fn find_edges(
385        &self,
386        path: &str,
387        key: &Key,
388        mut f: impl FnMut(u32, Doc<'_>),
389    ) -> Result<usize> {
390        self.edges.find(path, key, |slot, doc| {
391            // A slot is a u32 that was widened on the way in, so this cannot
392            // truncate, and a store handed something else is not this store.
393            if let Ok(slot) = u32::try_from(slot) {
394                f(slot, doc);
395            }
396        })
397    }
398
399    /// The node properties, for the document operations that are the document
400    /// model's rather than the graph's.
401    #[must_use]
402    pub fn node_props(&self) -> &Props {
403        &self.nodes
404    }
405
406    /// The edge properties, likewise.
407    #[must_use]
408    pub fn edge_props(&self) -> &Props {
409        &self.edges
410    }
411
412    /// The adjacency plane underneath.
413    #[must_use]
414    pub fn adjacency(&self) -> &Adjacency {
415        &self.adj
416    }
417
418    /// What the whole graph weighs.
419    #[must_use]
420    pub fn memory_bytes(&self) -> usize {
421        self.adj.bytes()
422            + self.nodes.memory_bytes()
423            + self.edges.memory_bytes()
424            + self.free.capacity() * size_of::<u32>()
425            + self.labels.capacity() * size_of::<u32>()
426    }
427
428    /// A slot to put an edge's properties under.
429    fn take_slot(&mut self) -> Result<u32> {
430        if let Some(slot) = self.free.pop() {
431            return Ok(slot);
432        }
433        if self.next == u32::MAX {
434            return Err(Error::new(Code::Full, "this graph has no edge slots left"));
435        }
436        let slot = self.next;
437        self.next += 1;
438        Ok(slot)
439    }
440
441    /// Gives a slot back, after its properties are gone.
442    fn release(&mut self, slot: u32) {
443        self.edges.remove(u64::from(slot));
444        self.free.push(slot);
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use yo_doc::{Builder, Value};
452
453    const FOLLOWS: u32 = 1;
454    const BLOCKS: u32 = 2;
455
456    fn doc(f: impl FnOnce(&mut Builder) -> Result<()>) -> Vec<u8> {
457        let mut b = Builder::new();
458        f(&mut b).expect("built");
459        b.finish().expect("finished").to_vec()
460    }
461
462    fn named(name: &str) -> Vec<u8> {
463        doc(|b| {
464            b.begin_object()?;
465            b.key(b"name")?;
466            b.text(name)?;
467            b.end_object()
468        })
469    }
470
471    fn since(year: i64) -> Vec<u8> {
472        doc(|b| {
473            b.begin_object()?;
474            b.key(b"since")?;
475            b.int(year)?;
476            b.end_object()
477        })
478    }
479
480    #[test]
481    fn the_empty_object_constant_is_an_empty_object() {
482        // It is written as four bytes rather than built, so this is the check
483        // that those four bytes are the ones a builder would have produced.
484        let built = doc(|b| {
485            b.begin_object()?;
486            b.end_object()
487        });
488        assert_eq!(&EMPTY_OBJECT[..], &built[..]);
489        let v = Value::new(&EMPTY_OBJECT).expect("readable");
490        assert!(v.validate());
491        assert!(v.is_empty());
492    }
493
494    #[test]
495    fn a_node_exists_once_it_has_properties() {
496        let mut g = Graph::new();
497        assert!(!g.has_node(1));
498        assert!(g.add_node(1).unwrap());
499        assert!(!g.add_node(1).unwrap(), "adding twice is not two nodes");
500        assert!(g.has_node(1));
501        assert_eq!(g.nodes(), 1);
502        assert_eq!(g.edges(), 0);
503    }
504
505    #[test]
506    fn linking_creates_the_endpoints() {
507        let mut g = Graph::new();
508        let e = g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
509        assert!(g.has_node(1) && g.has_node(2));
510        assert_eq!(g.nodes(), 2);
511        assert_eq!(g.edges(), 1);
512        assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), [2]);
513        assert_eq!(g.neighbours(2, FOLLOWS, Dir::In), [1]);
514        assert_eq!(
515            g.edge(e)
516                .and_then(|d| d.get(b"since"))
517                .and_then(|v| v.as_int()),
518            Some(2026)
519        );
520        assert_eq!(g.labels(), [FOLLOWS]);
521    }
522
523    #[test]
524    fn a_hop_gives_the_neighbour_and_the_edge_together() {
525        let mut g = Graph::new();
526        g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
527        g.link(1, 3, FOLLOWS, &since(2025)).unwrap();
528        let mut seen: Vec<(u64, i64)> = g
529            .hop(1, FOLLOWS, Dir::Out)
530            .map(|(n, slot)| {
531                let year = g
532                    .edge(slot)
533                    .and_then(|d| d.get(b"since"))
534                    .and_then(|v| v.as_int())
535                    .expect("an edge has its year");
536                (n, year)
537            })
538            .collect();
539        seen.sort_unstable();
540        assert_eq!(seen, vec![(2, 2024), (3, 2025)]);
541    }
542
543    #[test]
544    fn parallel_edges_each_keep_their_own_properties() {
545        let mut g = Graph::new();
546        let a = g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
547        let b = g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
548        assert_ne!(a, b);
549        assert_eq!(g.edges(), 2);
550        assert_eq!(g.degree(1, FOLLOWS, Dir::Out), 2);
551        assert_eq!(
552            g.edge(a)
553                .and_then(|d| d.get(b"since"))
554                .and_then(|v| v.as_int()),
555            Some(2024)
556        );
557        assert_eq!(
558            g.edge(b)
559                .and_then(|d| d.get(b"since"))
560                .and_then(|v| v.as_int()),
561            Some(2026)
562        );
563    }
564
565    #[test]
566    fn a_freed_slot_is_reused_without_its_old_properties() {
567        let mut g = Graph::new();
568        let a = g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
569        assert_eq!(g.unlink(1, 2, FOLLOWS), Some(a));
570        assert!(g.edge(a).is_none(), "an unlinked edge keeps nothing");
571
572        // The same slot comes back, and the point of the test is that what is
573        // under it is the new edge's document and not the old one's.
574        let b = g.link(3, 4, FOLLOWS, &since(2026)).unwrap();
575        assert_eq!(a, b, "a freed slot is handed out again");
576        assert_eq!(
577            g.edge(b)
578                .and_then(|d| d.get(b"since"))
579                .and_then(|v| v.as_int()),
580            Some(2026)
581        );
582    }
583
584    #[test]
585    fn removing_a_node_takes_its_edges_at_both_ends() {
586        let mut g = Graph::new();
587        g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
588        g.link(3, 2, FOLLOWS, &since(2025)).unwrap();
589        g.link(2, 4, BLOCKS, &since(2026)).unwrap();
590        g.link(1, 3, FOLLOWS, &since(2023)).unwrap();
591        assert_eq!(g.edges(), 4);
592
593        assert!(g.remove_node(2).unwrap());
594        assert!(!g.has_node(2));
595        // The three edges that touched node 2 are gone and the one that did not
596        // is still there.
597        assert_eq!(g.edges(), 1);
598        assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), [3]);
599        assert!(g.neighbours(3, FOLLOWS, Dir::Out).is_empty());
600        assert!(g.neighbours(4, BLOCKS, Dir::In).is_empty());
601        // Their properties went with them, and the surviving edge kept its
602        // own, which is the half of this that a store that cleared everything
603        // would also pass.
604        assert_eq!(g.edge_props().len(), 1);
605        let left = g.edge_slots(1, FOLLOWS, Dir::Out)[0];
606        assert_eq!(
607            g.edge(left)
608                .and_then(|d| d.get(b"since"))
609                .and_then(|v| v.as_int()),
610            Some(2023)
611        );
612    }
613
614    #[test]
615    fn removing_a_node_with_parallel_edges_takes_all_of_them() {
616        // The case a snapshot of the run guards against: unlinking moves the
617        // last entry into the hole, so a walk over a live run would skip one.
618        let mut g = Graph::new();
619        for year in 2020..2030 {
620            g.link(1, 2, FOLLOWS, &since(year)).unwrap();
621        }
622        assert_eq!(g.edges(), 10);
623        assert!(g.remove_node(2).unwrap());
624        assert_eq!(g.edges(), 0);
625        assert!(g.neighbours(1, FOLLOWS, Dir::Out).is_empty());
626        assert!(g.edge_props().is_empty());
627    }
628
629    #[test]
630    fn removing_a_node_that_is_not_there_says_so() {
631        let mut g = Graph::new();
632        g.add_node(1).unwrap();
633        assert!(!g.remove_node(2).unwrap());
634        assert!(g.remove_node(1).unwrap());
635        assert_eq!(g.nodes(), 0);
636    }
637
638    #[test]
639    fn an_out_only_graph_refuses_to_remove_a_linked_node() {
640        let mut g = Graph::out_only();
641        g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
642        // Node 1 has an outgoing edge and no way to find what points at it.
643        assert!(g.remove_node(1).is_err());
644        // Node 2 has no outgoing edge, so there is nothing to leave dangling
645        // at this end, and it goes.
646        assert!(g.remove_node(2).unwrap());
647    }
648
649    #[test]
650    fn an_edge_slot_that_is_gone_refuses_a_write() {
651        let mut g = Graph::new();
652        let e = g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
653        assert!(g.put_edge(e, &since(2025)).is_ok());
654        g.unlink(1, 2, FOLLOWS);
655        assert!(
656            g.put_edge(e, &since(2026)).is_err(),
657            "a slot held across a removal is not an edge"
658        );
659    }
660
661    #[test]
662    fn nodes_are_found_by_an_indexed_property() {
663        let mut g = Graph::new();
664        g.index_nodes("$.name", IndexKind::Equality).unwrap();
665        g.put_node(1, &named("ada")).unwrap();
666        g.put_node(2, &named("grace")).unwrap();
667        g.put_node(3, &named("ada")).unwrap();
668
669        let mut found = Vec::new();
670        let n = g
671            .find_nodes("$.name", &Key::text("ada"), |id, _| found.push(id))
672            .unwrap();
673        assert_eq!(n, 2);
674        found.sort_unstable();
675        assert_eq!(found, vec![1, 3]);
676    }
677
678    #[test]
679    fn edges_are_found_by_an_indexed_property() {
680        let mut g = Graph::new();
681        g.index_edges("$.since", IndexKind::Equality).unwrap();
682        let a = g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
683        g.link(1, 3, FOLLOWS, &since(2024)).unwrap();
684        let c = g.link(2, 3, BLOCKS, &since(2026)).unwrap();
685
686        let mut found = Vec::new();
687        let n = g
688            .find_edges("$.since", &Key::int(2026), |slot, _| found.push(slot))
689            .unwrap();
690        assert_eq!(n, 2);
691        found.sort_unstable();
692        let mut want = vec![a, c];
693        want.sort_unstable();
694        assert_eq!(found, want);
695    }
696
697    #[test]
698    fn labels_are_the_ones_with_edges_in_order() {
699        let mut g = Graph::new();
700        g.link(1, 2, BLOCKS, &since(2026)).unwrap();
701        g.link(1, 3, FOLLOWS, &since(2026)).unwrap();
702        g.link(1, 4, FOLLOWS, &since(2026)).unwrap();
703        assert_eq!(g.labels(), [FOLLOWS, BLOCKS]);
704    }
705
706    #[test]
707    fn a_graph_that_churns_does_not_grow_forever() {
708        // The failure the free list is for. Ten thousand edges added and
709        // removed one at a time, with only one ever live, so a slot allocator
710        // that only counted up would leave an edge store ten thousand
711        // documents deep holding one edge.
712        let mut g = Graph::new();
713        for year in 0..10_000i64 {
714            g.link(1, 2, FOLLOWS, &since(year)).unwrap();
715            assert!(g.unlink(1, 2, FOLLOWS).is_some());
716        }
717        g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
718        assert_eq!(g.edges(), 1);
719        assert_eq!(g.edge_props().len(), 1);
720    }
721
722    #[test]
723    fn the_names_of_edge_properties_are_stored_once() {
724        let mut g = Graph::new();
725        for dst in 2..1002u64 {
726            g.link(1, dst, FOLLOWS, &since(2026)).unwrap();
727        }
728        assert_eq!(g.edges(), 1000);
729        // One field name for a thousand edges, which is the whole reason edge
730        // properties are documents rather than their own store.
731        assert_eq!(g.edge_props().keys().len(), 1);
732    }
733}