Skip to main content

fabula_memory/
lib.rs

1//! A simple in-memory temporal graph implementation of [`DataSource`].
2//!
3//! Useful for testing fabula without a backing database. Not intended for
4//! production use — it's a linear scan over all edges.
5
6use fabula::datasource::{DataSource, Edge, ValueConstraint};
7use fabula::interval::Interval;
8use std::fmt;
9use std::hash::{Hash, Hasher};
10
11/// A value in the in-memory graph — can be a node reference, string, number, or boolean.
12#[derive(Debug, Clone, PartialEq, PartialOrd)]
13pub enum MemValue {
14    /// Reference to another node.
15    Node(String),
16    /// String literal.
17    Str(String),
18    /// Numeric value.
19    Num(f64),
20    /// Boolean value.
21    Bool(bool),
22}
23
24impl Hash for MemValue {
25    fn hash<H: Hasher>(&self, state: &mut H) {
26        std::mem::discriminant(self).hash(state);
27        match self {
28            MemValue::Node(s) => s.hash(state),
29            MemValue::Str(s) => s.hash(state),
30            MemValue::Num(n) => n.to_bits().hash(state),
31            MemValue::Bool(b) => b.hash(state),
32        }
33    }
34}
35
36impl fmt::Display for MemValue {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            MemValue::Node(id) => write!(f, "@{}", id),
40            MemValue::Str(s) => write!(f, "\"{}\"", s),
41            MemValue::Num(n) => write!(f, "{}", n),
42            MemValue::Bool(b) => write!(f, "{}", b),
43        }
44    }
45}
46
47/// A stored edge in the in-memory graph.
48#[derive(Debug, Clone)]
49struct StoredEdge {
50    source: String,
51    label: String,
52    target: MemValue,
53    interval: Interval<i64>,
54}
55
56/// A simple in-memory temporal graph.
57///
58/// Stores edges as `(source, label, target, interval)` tuples.
59/// Queries are linear scans — fine for testing, not for production.
60#[derive(Clone)]
61pub struct MemGraph {
62    edges: Vec<StoredEdge>,
63    current_time: i64,
64}
65
66impl MemGraph {
67    /// Create a new empty graph.
68    pub fn new() -> Self {
69        Self {
70            edges: Vec::new(),
71            current_time: 0,
72        }
73    }
74
75    /// Set the current time.
76    pub fn set_time(&mut self, t: i64) {
77        self.current_time = t;
78    }
79
80    /// Add an edge with an open-ended interval starting at `start`.
81    pub fn add_edge(&mut self, source: &str, label: &str, target: MemValue, start: i64) {
82        self.edges.push(StoredEdge {
83            source: source.to_string(),
84            label: label.to_string(),
85            target,
86            interval: Interval::open(start),
87        });
88    }
89
90    /// Add an edge with a bounded interval `[start, end)`.
91    pub fn add_edge_bounded(
92        &mut self,
93        source: &str,
94        label: &str,
95        target: MemValue,
96        start: i64,
97        end: i64,
98    ) {
99        self.edges.push(StoredEdge {
100            source: source.to_string(),
101            label: label.to_string(),
102            target,
103            interval: Interval::new(start, end),
104        });
105    }
106
107    /// Convenience: add a node-to-node edge.
108    pub fn add_ref(&mut self, source: &str, label: &str, target_node: &str, start: i64) {
109        self.add_edge(
110            source,
111            label,
112            MemValue::Node(target_node.to_string()),
113            start,
114        );
115    }
116
117    /// Convenience: add a node-to-string edge.
118    pub fn add_str(&mut self, source: &str, label: &str, value: &str, start: i64) {
119        self.add_edge(source, label, MemValue::Str(value.to_string()), start);
120    }
121
122    /// Convenience: add a node-to-number edge.
123    pub fn add_num(&mut self, source: &str, label: &str, value: f64, start: i64) {
124        self.add_edge(source, label, MemValue::Num(value), start);
125    }
126
127    /// Close an open-ended interval on the first matching edge.
128    ///
129    /// Finds the most recent open-ended edge from `source` with the given
130    /// `label` and sets its end to `at`. Returns `true` if an edge was
131    /// closed, `false` if no open-ended match was found.
132    pub fn end_edge(&mut self, source: &str, label: &str, at: i64) -> bool {
133        // Search in reverse to close the most recent matching open edge
134        for edge in self.edges.iter_mut().rev() {
135            if edge.source == source && edge.label == label && edge.interval.end.is_none() {
136                edge.interval.end = Some(at);
137                return true;
138            }
139        }
140        false
141    }
142
143    /// End the current open-ended edge (if any) and insert a new one.
144    ///
145    /// Equivalent to `end_edge(source, label, at)` followed by
146    /// `add_edge(source, label, value, at)`. Useful for updating state:
147    /// close the old value and record the new one at the same timestamp.
148    pub fn upsert_edge(&mut self, source: &str, label: &str, value: MemValue, at: i64) {
149        self.end_edge(source, label, at);
150        self.add_edge(source, label, value, at);
151    }
152
153    /// Total number of edges.
154    pub fn edge_count(&self) -> usize {
155        self.edges.len()
156    }
157}
158
159impl Default for MemGraph {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165impl DataSource for MemGraph {
166    type N = String;
167    type L = String;
168    type V = MemValue;
169    type T = i64;
170
171    fn edges_from(
172        &self,
173        node: &String,
174        label: &String,
175        at: &i64,
176    ) -> Vec<Edge<String, MemValue, i64>> {
177        self.edges
178            .iter()
179            .filter(|e| &e.source == node && &e.label == label && e.interval.covers(at))
180            .map(|e| Edge {
181                source: e.source.clone(),
182                target: e.target.clone(),
183                interval: e.interval.clone(),
184            })
185            .collect()
186    }
187
188    fn scan(
189        &self,
190        label: &String,
191        constraint: &ValueConstraint<MemValue>,
192        at: &i64,
193    ) -> Vec<Edge<String, MemValue, i64>> {
194        self.edges
195            .iter()
196            .filter(|e| &e.label == label && constraint.matches(&e.target) && e.interval.covers(at))
197            .map(|e| Edge {
198                source: e.source.clone(),
199                target: e.target.clone(),
200                interval: e.interval.clone(),
201            })
202            .collect()
203    }
204
205    fn edges_from_any_time(
206        &self,
207        node: &String,
208        label: &String,
209    ) -> Vec<Edge<String, MemValue, i64>> {
210        self.edges
211            .iter()
212            .filter(|e| &e.source == node && &e.label == label)
213            .map(|e| Edge {
214                source: e.source.clone(),
215                target: e.target.clone(),
216                interval: e.interval.clone(),
217            })
218            .collect()
219    }
220
221    fn scan_any_time(
222        &self,
223        label: &String,
224        constraint: &ValueConstraint<MemValue>,
225    ) -> Vec<Edge<String, MemValue, i64>> {
226        self.edges
227            .iter()
228            .filter(|e| &e.label == label && constraint.matches(&e.target))
229            .map(|e| Edge {
230                source: e.source.clone(),
231                target: e.target.clone(),
232                interval: e.interval.clone(),
233            })
234            .collect()
235    }
236
237    fn now(&self) -> i64 {
238        self.current_time
239    }
240
241    fn value_as_node(&self, value: &MemValue) -> Option<String> {
242        match value {
243            MemValue::Node(id) => Some(id.clone()),
244            _ => None,
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn end_edge_closes_open_interval() {
255        let mut g = MemGraph::new();
256        g.add_str("alice", "mood", "happy", 1);
257        g.set_time(5);
258
259        // Edge is visible at time 5 (open-ended)
260        assert_eq!(g.edges_from(&"alice".into(), &"mood".into(), &5).len(), 1);
261
262        // Close it at time 3
263        assert!(g.end_edge("alice", "mood", 3));
264
265        // Now it's bounded [1, 3) — visible at time 2 but not at time 3
266        assert_eq!(g.edges_from(&"alice".into(), &"mood".into(), &2).len(), 1);
267        assert_eq!(g.edges_from(&"alice".into(), &"mood".into(), &3).len(), 0);
268    }
269
270    #[test]
271    fn end_edge_returns_false_when_no_open_edge() {
272        let mut g = MemGraph::new();
273        g.add_edge_bounded("alice", "mood", MemValue::Str("happy".into()), 1, 3);
274
275        // No open-ended edge to close
276        assert!(!g.end_edge("alice", "mood", 5));
277    }
278
279    #[test]
280    fn end_edge_closes_most_recent() {
281        let mut g = MemGraph::new();
282        g.add_str("alice", "mood", "happy", 1);
283        g.add_str("alice", "mood", "sad", 5);
284
285        // Should close "sad" (most recent), not "happy"
286        assert!(g.end_edge("alice", "mood", 8));
287
288        // "happy" still open (visible at time 10)
289        let edges = g.edges_from(&"alice".into(), &"mood".into(), &10);
290        assert_eq!(edges.len(), 1);
291        assert_eq!(edges[0].target, MemValue::Str("happy".into()));
292    }
293
294    #[test]
295    fn upsert_edge_closes_and_inserts() {
296        let mut g = MemGraph::new();
297        g.add_str("alice", "mood", "happy", 1);
298        g.set_time(5);
299
300        // Upsert: close "happy", insert "sad"
301        g.upsert_edge("alice", "mood", MemValue::Str("sad".into()), 5);
302
303        // At time 3: only "happy" visible
304        let at3 = g.edges_from(&"alice".into(), &"mood".into(), &3);
305        assert_eq!(at3.len(), 1);
306        assert_eq!(at3[0].target, MemValue::Str("happy".into()));
307
308        // At time 5: only "sad" visible (happy ended at 5, sad starts at 5)
309        let at5 = g.edges_from(&"alice".into(), &"mood".into(), &5);
310        assert_eq!(at5.len(), 1);
311        assert_eq!(at5[0].target, MemValue::Str("sad".into()));
312
313        // Total edges: 2 (the closed one + the new one)
314        assert_eq!(g.edge_count(), 2);
315    }
316}