Skip to main content

marsdb_graph/
store.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3
4use marsdb_storage::{ReadableMultimapTable, ReadableTable, StorageEngine, WriteTransaction};
5
6use crate::encode::{decode, encode, EdgeRecord, NodeRecord};
7use crate::error::GraphError;
8use crate::id::next_id;
9use crate::labels::{intern_label, lookup_label_id, resolve_label};
10use crate::model::{AdjEntry, Direction, Edge, EdgeId, Node, NodeId, PropertyValue};
11
12pub struct GraphStore {
13    storage: StorageEngine,
14}
15
16impl GraphStore {
17    pub fn open_file(path: impl AsRef<Path>) -> Result<Self, GraphError> {
18        Ok(Self {
19            storage: StorageEngine::open_file(path)?,
20        })
21    }
22
23    pub fn open_memory() -> Result<Self, GraphError> {
24        Ok(Self {
25            storage: StorageEngine::open_memory()?,
26        })
27    }
28
29    /// Open a write transaction spanning multiple graph operations. Callers
30    /// (e.g. the query executor) drive an entire Cypher statement through
31    /// the `*_in_txn` methods below using this one transaction, then call
32    /// `write_txn.commit()` themselves — this is the crash-safety boundary
33    /// from the plan: one statement = one transaction, not one transaction
34    /// per individual node/edge write.
35    ///
36    /// v1 uses a write transaction even for pure-read statements (rather
37    /// than a separate read-only path) to keep one code path and guarantee
38    /// every statement — reads included — sees one consistent snapshot.
39    /// Trade-off: this serializes concurrent readers behind redb's
40    /// single-writer lock instead of allowing true concurrent reads; a
41    /// read-only transaction path is the natural follow-up if read
42    /// concurrency becomes a bottleneck.
43    pub fn begin_write(&self) -> Result<WriteTransaction, GraphError> {
44        Ok(self.storage.begin_write()?)
45    }
46
47    /// Commit a transaction obtained from [`begin_write`](Self::begin_write).
48    pub fn commit(write_txn: WriteTransaction) -> Result<(), GraphError> {
49        write_txn.commit()?;
50        Ok(())
51    }
52
53    /// Abort (roll back) a transaction obtained from
54    /// [`begin_write`](Self::begin_write), discarding any writes made
55    /// through it.
56    pub fn abort(write_txn: WriteTransaction) -> Result<(), GraphError> {
57        write_txn.abort()?;
58        Ok(())
59    }
60
61    pub fn create_node(
62        &self,
63        label: &str,
64        props: BTreeMap<String, PropertyValue>,
65    ) -> Result<NodeId, GraphError> {
66        let write_txn = self.begin_write()?;
67        let id = Self::create_node_in_txn(&write_txn, label, props)?;
68        write_txn.commit()?;
69        Ok(id)
70    }
71
72    pub fn create_node_in_txn(
73        write_txn: &WriteTransaction,
74        label: &str,
75        props: BTreeMap<String, PropertyValue>,
76    ) -> Result<NodeId, GraphError> {
77        let label_id = intern_label(write_txn, label)?;
78        let id = next_id(write_txn, "next_node_id")?;
79        let record = NodeRecord { label_id, props };
80        let bytes = encode(&record)?;
81        let mut nodes = write_txn.open_table(marsdb_storage::tables::NODES)?;
82        nodes.insert(id, bytes.as_slice())?;
83        Ok(NodeId(id))
84    }
85
86    pub fn get_node(&self, id: NodeId) -> Result<Option<Node>, GraphError> {
87        let write_txn = self.begin_write()?;
88        let node = Self::get_node_in_txn(&write_txn, id)?;
89        write_txn.abort()?;
90        Ok(node)
91    }
92
93    pub fn get_node_in_txn(write_txn: &WriteTransaction, id: NodeId) -> Result<Option<Node>, GraphError> {
94        let record: Option<NodeRecord> = {
95            let nodes = write_txn.open_table(marsdb_storage::tables::NODES)?;
96            let found = match nodes.get(id.0)? {
97                Some(guard) => Some(decode(guard.value())?),
98                None => None,
99            };
100            found
101        };
102        let Some(record) = record else { return Ok(None) };
103        let label = resolve_label(write_txn, record.label_id)?;
104        Ok(Some(Node {
105            id,
106            label,
107            props: record.props,
108        }))
109    }
110
111    pub fn create_edge(
112        &self,
113        label: &str,
114        src: NodeId,
115        dst: NodeId,
116        props: BTreeMap<String, PropertyValue>,
117    ) -> Result<EdgeId, GraphError> {
118        let write_txn = self.begin_write()?;
119        let id = Self::create_edge_in_txn(&write_txn, label, src, dst, props)?;
120        write_txn.commit()?;
121        Ok(id)
122    }
123
124    pub fn create_edge_in_txn(
125        write_txn: &WriteTransaction,
126        label: &str,
127        src: NodeId,
128        dst: NodeId,
129        props: BTreeMap<String, PropertyValue>,
130    ) -> Result<EdgeId, GraphError> {
131        let label_id = intern_label(write_txn, label)?;
132        let id = next_id(write_txn, "next_edge_id")?;
133        let record = EdgeRecord {
134            label_id,
135            src: src.0,
136            dst: dst.0,
137            props,
138        };
139        let bytes = encode(&record)?;
140        let mut edges = write_txn.open_table(marsdb_storage::tables::EDGES)?;
141        edges.insert(id, bytes.as_slice())?;
142
143        let out_entry = AdjEntry {
144            edge_id: EdgeId(id),
145            other: dst,
146            label_id,
147        }
148        .encode();
149        let in_entry = AdjEntry {
150            edge_id: EdgeId(id),
151            other: src,
152            label_id,
153        }
154        .encode();
155        let mut adj_out = write_txn.open_multimap_table(marsdb_storage::tables::ADJ_OUT)?;
156        adj_out.insert(src.0, out_entry.as_slice())?;
157        let mut adj_in = write_txn.open_multimap_table(marsdb_storage::tables::ADJ_IN)?;
158        adj_in.insert(dst.0, in_entry.as_slice())?;
159        Ok(EdgeId(id))
160    }
161
162    pub fn get_edge(&self, id: EdgeId) -> Result<Option<Edge>, GraphError> {
163        let write_txn = self.begin_write()?;
164        let edge = Self::get_edge_in_txn(&write_txn, id)?;
165        write_txn.abort()?;
166        Ok(edge)
167    }
168
169    pub fn get_edge_in_txn(write_txn: &WriteTransaction, id: EdgeId) -> Result<Option<Edge>, GraphError> {
170        let record: Option<EdgeRecord> = {
171            let edges = write_txn.open_table(marsdb_storage::tables::EDGES)?;
172            let found = match edges.get(id.0)? {
173                Some(guard) => Some(decode(guard.value())?),
174                None => None,
175            };
176            found
177        };
178        let Some(record) = record else { return Ok(None) };
179        let label = resolve_label(write_txn, record.label_id)?;
180        Ok(Some(Edge {
181            id,
182            label,
183            src: NodeId(record.src),
184            dst: NodeId(record.dst),
185            props: record.props,
186        }))
187    }
188
189    /// Neighbors of `node` in `dir`, optionally filtered by edge label.
190    /// Reads directly from the adjacency multimap without touching `edges`.
191    pub fn neighbors(
192        &self,
193        node: NodeId,
194        dir: Direction,
195        label_filter: Option<&str>,
196    ) -> Result<Vec<AdjEntry>, GraphError> {
197        let write_txn = self.begin_write()?;
198        let result = Self::neighbors_in_txn(&write_txn, node, dir, label_filter)?;
199        write_txn.abort()?;
200        Ok(result)
201    }
202
203    pub fn neighbors_in_txn(
204        write_txn: &WriteTransaction,
205        node: NodeId,
206        dir: Direction,
207        label_filter: Option<&str>,
208    ) -> Result<Vec<AdjEntry>, GraphError> {
209        let label_id_filter = match label_filter {
210            Some(l) => match lookup_label_id(write_txn, l)? {
211                Some(id) => Some(id),
212                None => return Ok(Vec::new()),
213            },
214            None => None,
215        };
216        let mut result = Vec::new();
217        let table_def = match dir {
218            Direction::Out => marsdb_storage::tables::ADJ_OUT,
219            Direction::In => marsdb_storage::tables::ADJ_IN,
220        };
221        let table = write_txn.open_multimap_table(table_def)?;
222        for item in table.get(node.0)? {
223            let entry = AdjEntry::decode(item?.value());
224            if label_id_filter.is_none_or(|lid| lid == entry.label_id) {
225                result.push(entry);
226            }
227        }
228        Ok(result)
229    }
230
231    pub fn delete_edge(&self, id: EdgeId) -> Result<bool, GraphError> {
232        let write_txn = self.begin_write()?;
233        let removed = Self::delete_edge_in_txn(&write_txn, id)?;
234        write_txn.commit()?;
235        Ok(removed)
236    }
237
238    pub fn delete_edge_in_txn(write_txn: &WriteTransaction, id: EdgeId) -> Result<bool, GraphError> {
239        let record_bytes: Option<Vec<u8>> = {
240            let mut edges = write_txn.open_table(marsdb_storage::tables::EDGES)?;
241            let removed = edges.remove(id.0)?.map(|guard| guard.value().to_vec());
242            removed
243        };
244        let Some(record_bytes) = record_bytes else {
245            return Ok(false);
246        };
247        let record: EdgeRecord = decode(&record_bytes)?;
248        let out_entry = AdjEntry {
249            edge_id: id,
250            other: NodeId(record.dst),
251            label_id: record.label_id,
252        }
253        .encode();
254        let in_entry = AdjEntry {
255            edge_id: id,
256            other: NodeId(record.src),
257            label_id: record.label_id,
258        }
259        .encode();
260        {
261            let mut adj_out = write_txn.open_multimap_table(marsdb_storage::tables::ADJ_OUT)?;
262            adj_out.remove(record.src, out_entry.as_slice())?;
263        }
264        {
265            let mut adj_in = write_txn.open_multimap_table(marsdb_storage::tables::ADJ_IN)?;
266            adj_in.remove(record.dst, in_entry.as_slice())?;
267        }
268        Ok(true)
269    }
270
271    /// Delete a node. If `detach` is false and the node has incident edges,
272    /// returns `GraphError::NodeHasEdges` instead of deleting anything.
273    pub fn delete_node(&self, id: NodeId, detach: bool) -> Result<bool, GraphError> {
274        let write_txn = self.begin_write()?;
275        let existed = Self::delete_node_in_txn(&write_txn, id, detach)?;
276        write_txn.commit()?;
277        Ok(existed)
278    }
279
280    pub fn delete_node_in_txn(write_txn: &WriteTransaction, id: NodeId, detach: bool) -> Result<bool, GraphError> {
281        let mut incident: Vec<EdgeId> = Vec::new();
282        {
283            let adj_out = write_txn.open_multimap_table(marsdb_storage::tables::ADJ_OUT)?;
284            for item in adj_out.get(id.0)? {
285                incident.push(AdjEntry::decode(item?.value()).edge_id);
286            }
287            let adj_in = write_txn.open_multimap_table(marsdb_storage::tables::ADJ_IN)?;
288            for item in adj_in.get(id.0)? {
289                incident.push(AdjEntry::decode(item?.value()).edge_id);
290            }
291        }
292        if !incident.is_empty() && !detach {
293            return Err(GraphError::NodeHasEdges(id));
294        }
295        for edge_id in incident {
296            Self::delete_edge_in_txn(write_txn, edge_id)?;
297        }
298        let mut nodes = write_txn.open_table(marsdb_storage::tables::NODES)?;
299        let existed = nodes.remove(id.0)?.is_some();
300        Ok(existed)
301    }
302
303    pub fn set_node_prop(&self, id: NodeId, key: &str, value: PropertyValue) -> Result<bool, GraphError> {
304        let write_txn = self.begin_write()?;
305        let updated = Self::set_node_prop_in_txn(&write_txn, id, key, value)?;
306        if updated {
307            write_txn.commit()?;
308        } else {
309            write_txn.abort()?;
310        }
311        Ok(updated)
312    }
313
314    pub fn set_node_prop_in_txn(
315        write_txn: &WriteTransaction,
316        id: NodeId,
317        key: &str,
318        value: PropertyValue,
319    ) -> Result<bool, GraphError> {
320        let bytes_opt: Option<Vec<u8>> = {
321            let nodes = write_txn.open_table(marsdb_storage::tables::NODES)?;
322            let found = nodes.get(id.0)?.map(|g| g.value().to_vec());
323            found
324        };
325        let Some(bytes) = bytes_opt else { return Ok(false) };
326        let mut record: NodeRecord = decode(&bytes)?;
327        record.props.insert(key.to_string(), value);
328        let new_bytes = encode(&record)?;
329        let mut nodes = write_txn.open_table(marsdb_storage::tables::NODES)?;
330        nodes.insert(id.0, new_bytes.as_slice())?;
331        Ok(true)
332    }
333
334    pub fn set_edge_prop(&self, id: EdgeId, key: &str, value: PropertyValue) -> Result<bool, GraphError> {
335        let write_txn = self.begin_write()?;
336        let updated = Self::set_edge_prop_in_txn(&write_txn, id, key, value)?;
337        if updated {
338            write_txn.commit()?;
339        } else {
340            write_txn.abort()?;
341        }
342        Ok(updated)
343    }
344
345    pub fn set_edge_prop_in_txn(
346        write_txn: &WriteTransaction,
347        id: EdgeId,
348        key: &str,
349        value: PropertyValue,
350    ) -> Result<bool, GraphError> {
351        let bytes_opt: Option<Vec<u8>> = {
352            let edges = write_txn.open_table(marsdb_storage::tables::EDGES)?;
353            let found = edges.get(id.0)?.map(|g| g.value().to_vec());
354            found
355        };
356        let Some(bytes) = bytes_opt else { return Ok(false) };
357        let mut record: EdgeRecord = decode(&bytes)?;
358        record.props.insert(key.to_string(), value);
359        let new_bytes = encode(&record)?;
360        let mut edges = write_txn.open_table(marsdb_storage::tables::EDGES)?;
361        edges.insert(id.0, new_bytes.as_slice())?;
362        Ok(true)
363    }
364
365    /// Full scan of all nodes, optionally filtered by label. v1 has no
366    /// secondary index on label, so this is a linear scan of the table.
367    pub fn all_nodes(&self, label_filter: Option<&str>) -> Result<Vec<Node>, GraphError> {
368        let write_txn = self.begin_write()?;
369        let result = Self::all_nodes_in_txn(&write_txn, label_filter)?;
370        write_txn.abort()?;
371        Ok(result)
372    }
373
374    pub fn all_nodes_in_txn(write_txn: &WriteTransaction, label_filter: Option<&str>) -> Result<Vec<Node>, GraphError> {
375        let label_id_filter = match label_filter {
376            Some(l) => match lookup_label_id(write_txn, l)? {
377                Some(id) => Some(id),
378                None => return Ok(Vec::new()),
379            },
380            None => None,
381        };
382        let mut result = Vec::new();
383        let nodes = write_txn.open_table(marsdb_storage::tables::NODES)?;
384        for item in nodes.iter()? {
385            let (key, value) = item?;
386            let record: NodeRecord = decode(value.value())?;
387            if label_id_filter.is_none_or(|lid| lid == record.label_id) {
388                let label = resolve_label(write_txn, record.label_id)?;
389                result.push(Node {
390                    id: NodeId(key.value()),
391                    label,
392                    props: record.props,
393                });
394            }
395        }
396        Ok(result)
397    }
398}