Skip to main content

vertep_store/
memory.rs

1//! The in-memory reference implementation of both contracts.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::{Mutex, MutexGuard};
5
6use oxrdf::{Dataset, GraphName, NamedNode, Quad, Term};
7use spareval::QueryEvaluator;
8use spargebra::SparqlParser;
9
10use crate::error::{AppendError, StoreError};
11use crate::graph::{GraphCapabilities, GraphStore, QueryResults, RdfCapability};
12use crate::journal::{
13    AppendMeta, Entry, JournalCapabilities, JournalRecord, JournalStore, Position, prepare_append,
14};
15use crate::sparql::collect_results;
16
17#[derive(Default)]
18struct Inner {
19    /// The graph store contents: entry graphs, current-state graphs and anything inserted.
20    dataset: Dataset,
21    /// Stamped entries per actor, position i + 1 at index i.
22    journals: HashMap<NamedNode, Vec<Entry>>,
23    /// Every entry IRI ever appended.
24    entry_names: HashSet<NamedNode>,
25    /// The highest accepted fencing epoch per scope.
26    epochs: HashMap<NamedNode, u64>,
27}
28
29/// The in-memory reference implementation of [`JournalStore`] and [`GraphStore`] on one
30/// dataset, used by the test kit's self-test and by tests of code above the storage layer.
31///
32/// Like the tiramemsu backend, it holds the journal and the graphs in one store: an append
33/// writes the entry graphs and the state delta into the dataset under one lock. By default it
34/// enforces fencing, accepts state deltas and declares RDF 1.2; each can be switched off to
35/// exercise the other side of a capability.
36pub struct MemoryStore {
37    inner: Mutex<Inner>,
38    journal_caps: JournalCapabilities,
39    rdf: RdfCapability,
40}
41
42impl Default for MemoryStore {
43    fn default() -> Self {
44        MemoryStore::new()
45    }
46}
47
48impl std::fmt::Debug for MemoryStore {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("MemoryStore")
51            .field("journal_caps", &self.journal_caps)
52            .field("rdf", &self.rdf)
53            .finish_non_exhaustive()
54    }
55}
56
57impl MemoryStore {
58    /// An empty store with fencing, state deltas and RDF 1.2.
59    pub fn new() -> Self {
60        MemoryStore {
61            inner: Mutex::new(Inner::default()),
62            journal_caps: JournalCapabilities {
63                fencing: true,
64                state_in_append: true,
65            },
66            rdf: RdfCapability::Rdf12,
67        }
68    }
69
70    /// This store with fencing enforced or not.
71    pub fn with_fencing(mut self, on: bool) -> Self {
72        self.journal_caps.fencing = on;
73        self
74    }
75
76    /// This store with state deltas accepted or not.
77    pub fn with_state_in_append(mut self, on: bool) -> Self {
78        self.journal_caps.state_in_append = on;
79        self
80    }
81
82    /// This store declaring `rdf`. With [`RdfCapability::Rdf11`] it refuses triple terms.
83    pub fn with_rdf(mut self, rdf: RdfCapability) -> Self {
84        self.rdf = rdf;
85        self
86    }
87
88    /// A copy of every quad in the store.
89    pub fn dataset(&self) -> Dataset {
90        self.lock().dataset.clone()
91    }
92
93    fn lock(&self) -> MutexGuard<'_, Inner> {
94        // a panic while holding the lock leaves the data consistent (every write is applied
95        // after all checks), so a poisoned lock is still usable
96        self.inner
97            .lock()
98            .unwrap_or_else(std::sync::PoisonError::into_inner)
99    }
100
101    fn check_terms(&self, quads: &[Quad]) -> Result<(), StoreError> {
102        if self.rdf == RdfCapability::Rdf11
103            && quads.iter().any(|q| matches!(q.object, Term::Triple(_)))
104        {
105            return Err(StoreError::Unsupported(
106                "triple terms on a store that declares RDF 1.1".into(),
107            ));
108        }
109        Ok(())
110    }
111}
112
113impl JournalStore for MemoryStore {
114    fn capabilities(&self) -> JournalCapabilities {
115        self.journal_caps
116    }
117
118    fn append(
119        &self,
120        actor: &NamedNode,
121        expected: Position,
122        entries: &[Entry],
123        meta: &AppendMeta,
124    ) -> Result<Position, AppendError> {
125        let stamped = prepare_append(actor, expected, entries, meta, self.journal_caps)?;
126        let mut inner = self.lock();
127        // fencing first: a stale node is rejected even when its position matches
128        if let Some(token) = &meta.fencing {
129            let highest = inner.epochs.get(&token.scope).copied().unwrap_or(0);
130            if token.epoch < highest {
131                return Err(AppendError::Fenced {
132                    scope: token.scope.clone(),
133                    epoch: token.epoch,
134                    highest,
135                });
136            }
137        }
138        let actual = Position::new(inner.journals.get(actor).map_or(0, |j| j.len() as u64));
139        if actual != expected {
140            return Err(AppendError::Conflict { expected, actual });
141        }
142        if let Some(dup) = stamped
143            .iter()
144            .find(|e| inner.entry_names.contains(e.name()))
145        {
146            return Err(AppendError::Invalid(format!(
147                "entry {} is already in the store",
148                dup.name()
149            )));
150        }
151        for e in &stamped {
152            self.check_terms(&e.quads())?;
153        }
154        if let Some(delta) = &meta.state {
155            let g = GraphName::NamedNode(actor.clone());
156            let quads: Vec<Quad> = delta
157                .assert
158                .iter()
159                .map(|t| t.clone().in_graph(g.clone()))
160                .collect();
161            self.check_terms(&quads)?;
162        }
163        // every check passed: apply all writes
164        if let Some(token) = &meta.fencing {
165            inner.epochs.insert(token.scope.clone(), token.epoch);
166        }
167        for e in &stamped {
168            for q in e.quads() {
169                inner.dataset.insert(&q);
170            }
171            inner.entry_names.insert(e.name().clone());
172        }
173        if let Some(delta) = &meta.state {
174            let g = GraphName::NamedNode(actor.clone());
175            for t in &delta.retract {
176                inner.dataset.remove(&t.clone().in_graph(g.clone()));
177            }
178            for t in &delta.assert {
179                inner.dataset.insert(&t.clone().in_graph(g.clone()));
180            }
181        }
182        let journal = inner.journals.entry(actor.clone()).or_default();
183        journal.extend(stamped);
184        Ok(Position::new(journal.len() as u64))
185    }
186
187    fn read(&self, actor: &NamedNode, from: Position) -> Result<Vec<JournalRecord>, StoreError> {
188        let inner = self.lock();
189        let Some(journal) = inner.journals.get(actor) else {
190            return Ok(Vec::new());
191        };
192        let start = from.get().max(1);
193        Ok(journal
194            .iter()
195            .enumerate()
196            .skip((start - 1) as usize)
197            .map(|(i, e)| JournalRecord {
198                position: Position::new(i as u64 + 1),
199                entry: e.clone(),
200            })
201            .collect())
202    }
203
204    fn head(&self, actor: &NamedNode) -> Result<Position, StoreError> {
205        Ok(Position::new(
206            self.lock()
207                .journals
208                .get(actor)
209                .map_or(0, |j| j.len() as u64),
210        ))
211    }
212
213    fn actors(&self) -> Result<Vec<NamedNode>, StoreError> {
214        let mut actors: Vec<NamedNode> = self.lock().journals.keys().cloned().collect();
215        actors.sort();
216        Ok(actors)
217    }
218}
219
220impl GraphStore for MemoryStore {
221    fn capabilities(&self) -> GraphCapabilities {
222        GraphCapabilities::plain(self.rdf)
223    }
224
225    fn insert_quads(&self, quads: &[Quad]) -> Result<(), StoreError> {
226        self.check_terms(quads)?;
227        let mut inner = self.lock();
228        for q in quads {
229            inner.dataset.insert(q);
230        }
231        Ok(())
232    }
233
234    fn remove_quads(&self, quads: &[Quad]) -> Result<(), StoreError> {
235        let mut inner = self.lock();
236        for q in quads {
237            inner.dataset.remove(q);
238        }
239        Ok(())
240    }
241
242    fn clear_graph(&self, graph: &NamedNode) -> Result<(), StoreError> {
243        let mut inner = self.lock();
244        let doomed: Vec<Quad> = inner
245            .dataset
246            .quads_for_graph_name(graph)
247            .map(|q| q.into_owned())
248            .collect();
249        for q in &doomed {
250            inner.dataset.remove(q);
251        }
252        Ok(())
253    }
254
255    fn query(&self, sparql: &str) -> Result<QueryResults, StoreError> {
256        let query = SparqlParser::new()
257            .parse_query(sparql)
258            .map_err(|e| StoreError::invalid(e.to_string()))?;
259        let inner = self.lock();
260        let evaluator = QueryEvaluator::new();
261        let results = evaluator
262            .prepare(&query)
263            .execute(&inner.dataset)
264            .map_err(StoreError::backend)?;
265        collect_results(results)
266    }
267}