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 // Fewer rounds under Miri. What says the free list works is that the
713 // store is one document deep after all of them, and a store that counts
714 // up rather than reusing is two hundred deep after two hundred rounds
715 // as surely as it is ten thousand deep after ten thousand.
716 let rounds = if cfg!(miri) { 200i64 } else { 10_000 };
717 let mut g = Graph::new();
718 for year in 0..rounds {
719 g.link(1, 2, FOLLOWS, &since(year)).unwrap();
720 assert!(g.unlink(1, 2, FOLLOWS).is_some());
721 }
722 g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
723 assert_eq!(g.edges(), 1);
724 assert_eq!(g.edge_props().len(), 1);
725 }
726
727 #[test]
728 fn the_names_of_edge_properties_are_stored_once() {
729 // Fewer edges under Miri. One name for fifty edges says the same thing
730 // as one name for a thousand: the second edge is the one that would
731 // have stored the name again.
732 let edges = if cfg!(miri) { 50u64 } else { 1000 };
733 let mut g = Graph::new();
734 for dst in 2..2 + edges {
735 g.link(1, dst, FOLLOWS, &since(2026)).unwrap();
736 }
737 assert_eq!(g.edges() as u64, edges);
738 // One field name for a thousand edges, which is the whole reason edge
739 // properties are documents rather than their own store.
740 assert_eq!(g.edge_props().keys().len(), 1);
741 }
742}