Skip to main content

core_storage/
edge_props.rs

1use crate::types::Value;
2use serde::{Deserialize, Serialize};
3use std::collections::{BTreeMap, BTreeSet};
4
5/// Edge-property overlay.
6///
7/// After a V8 snapshot open the base properties live in the mmap'd section 5
8/// (`ArchivedEdgeProps`).  Only post-snapshot changes land here.  Deletions of
9/// base-only edges are recorded in `tombstones` so the view layer can mask them
10/// without materialising the full base into RAM.
11#[derive(Debug, Default, Clone, Serialize, Deserialize)]
12pub struct EdgeProps {
13    map: BTreeMap<(u32, u32, u32), BTreeMap<String, Value>>,
14    /// (etype, src, dst) tuples that have been deleted from the overlay *or* the
15    /// base.  An entry here masks any archived data for the same key.
16    /// Not persisted via serde (bincode): tombstones are ephemeral in-memory
17    /// overlay state used only during encode_v8 merge; they are never written
18    /// to disk by themselves.  Skipping preserves V5–V7 bincode wire shapes.
19    #[serde(skip)]
20    tombstones: BTreeSet<(u32, u32, u32)>,
21}
22
23impl EdgeProps {
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    pub fn set(&mut self, etype: u32, src: u32, dst: u32, field: &str, value: Value) {
29        // A set un-tombstones the edge (it is being (re-)created).
30        self.tombstones.remove(&(etype, src, dst));
31        self.map
32            .entry((etype, src, dst))
33            .or_default()
34            .insert(field.to_owned(), value);
35    }
36
37    pub fn get(&self, etype: u32, src: u32, dst: u32, field: &str) -> Option<&Value> {
38        self.map.get(&(etype, src, dst))?.get(field)
39    }
40
41    /// Remove an edge's props from the overlay and record a tombstone so that
42    /// archive lookups for this key are also masked.
43    pub fn remove_edge(&mut self, etype: u32, src: u32, dst: u32) {
44        self.map.remove(&(etype, src, dst));
45        self.tombstones.insert((etype, src, dst));
46    }
47
48    /// True if `(etype, src, dst)` is tombstoned (deleted from overlay or base).
49    pub fn is_tombstoned(&self, etype: u32, src: u32, dst: u32) -> bool {
50        self.tombstones.contains(&(etype, src, dst))
51    }
52
53    /// True when this overlay has no entries and no tombstones (i.e. nothing
54    /// has changed since the last snapshot).  Used to short-circuit the
55    /// edge-props section passthrough during snapshot merging.
56    pub fn is_clean(&self) -> bool {
57        self.map.is_empty() && self.tombstones.is_empty()
58    }
59
60    /// Return all overlay entries as a sorted Vec of (etype, src, dst, &props).
61    /// Sorted by (etype, src, dst) ascending — BTreeMap iteration is already sorted.
62    pub fn sorted_entries(
63        &self,
64    ) -> Vec<(
65        u32,
66        u32,
67        u32,
68        &std::collections::BTreeMap<String, crate::types::Value>,
69    )> {
70        self.map
71            .iter()
72            .map(|((et, s, d), props)| (*et, *s, *d, props))
73            .collect()
74    }
75
76    /// Iterate tombstoned keys, used when merging with a base section.
77    pub fn tombstoned_keys(&self) -> impl Iterator<Item = (u32, u32, u32)> + '_ {
78        self.tombstones.iter().copied()
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn set_get_remove_edge_props() {
88        let mut e = EdgeProps::new();
89        e.set(0, 1, 2, "score", Value::Float(0.5));
90        e.set(0, 1, 2, "score", Value::Float(0.7)); // overwrite
91        assert_eq!(e.get(0, 1, 2, "score"), Some(&Value::Float(0.7)));
92        assert_eq!(e.get(0, 2, 1, "score"), None); // directed
93        e.remove_edge(0, 1, 2);
94        assert_eq!(e.get(0, 1, 2, "score"), None);
95    }
96
97    #[test]
98    fn tombstone_masks_base_check() {
99        let mut e = EdgeProps::new();
100        // Initially clean.
101        assert!(e.is_clean());
102        // Add an entry — no longer clean.
103        e.set(0, 1, 2, "score", Value::Float(1.0));
104        assert!(!e.is_clean());
105        // Remove it — tombstone remains, still not clean.
106        e.remove_edge(0, 1, 2);
107        assert!(!e.is_clean());
108        assert!(e.is_tombstoned(0, 1, 2));
109        assert!(!e.is_tombstoned(0, 2, 1));
110    }
111
112    #[test]
113    fn set_clears_tombstone() {
114        let mut e = EdgeProps::new();
115        e.set(0, 1, 2, "score", Value::Float(1.0));
116        e.remove_edge(0, 1, 2);
117        assert!(e.is_tombstoned(0, 1, 2));
118        // Re-set: tombstone cleared, value visible again.
119        e.set(0, 1, 2, "score", Value::Float(2.0));
120        assert!(!e.is_tombstoned(0, 1, 2));
121        assert_eq!(e.get(0, 1, 2, "score"), Some(&Value::Float(2.0)));
122    }
123}