1use super::*;
2
3impl Graph {
4 #[instrument(skip(self, props), fields(src = %src, dst = %dst, etype = %etype))]
10 pub fn add_edge(
11 &self,
12 src: NodeId,
13 dst: NodeId,
14 etype: &str,
15 props: &impl Serialize,
16 ) -> Result<EdgeId, Error> {
17 let _guard = self._write_lock.lock();
18 let mut wtxn = self.storage.env.write_txn()?;
19 let edge_id = self.add_edge_impl(&mut wtxn, src, dst, etype, props)?;
20 wtxn.commit()?;
21 self.csr_cache.record_added_edge(src, dst);
22 self.edge_columns.record_touched(edge_id);
23 self.maybe_spawn_rebuild();
24 Ok(edge_id)
25 }
26
27 pub(super) fn add_edge_impl(
28 &self,
29 wtxn: &mut heed::RwTxn,
30 src: NodeId,
31 dst: NodeId,
32 etype: &str,
33 props: &impl Serialize,
34 ) -> Result<EdgeId, Error> {
35 let type_id = get_or_create_type(&self.storage, wtxn, etype)?;
36 let edge_id = alloc_edge_id(&self.storage, wtxn)?;
37 let encoded_props = props::encode(props)?;
38
39 self.write_edge_index_entries(wtxn, edge_id, type_id, etype, &encoded_props)?;
41
42 let record = EdgeRecord {
43 src,
44 dst,
45 edge_type: type_id,
46 props: encoded_props,
47 };
48 self.storage
49 .edges
50 .put(wtxn, &edge_id, &props::encode(&record)?)?;
51 self.storage
52 .type_idx
53 .put(wtxn, &composite_key(type_id, edge_id), &())?;
54
55 self.append_adj(wtxn, src, dst, type_id, edge_id, true)?;
56 self.append_adj(wtxn, dst, src, type_id, edge_id, false)?;
57
58 adjust_type_count(&self.storage, wtxn, type_id, 1)?;
59
60 Ok(edge_id)
61 }
62
63 pub fn update_edge(&self, id: EdgeId, props: &impl serde::Serialize) -> Result<(), Error> {
65 let _guard = self._write_lock.lock();
66 let mut wtxn = self.storage.env.write_txn()?;
67 let existing = self
68 .storage
69 .edges
70 .get(&wtxn, &id)?
71 .ok_or(Error::EdgeNotFound(id))?;
72 let record: EdgeRecord = crate::storage::props::decode(existing)?;
73 let etype = self
74 .type_name_impl(&wtxn, record.edge_type)?
75 .ok_or(Error::Corrupt("edge type name missing"))?;
76
77 self.delete_edge_index_entries(&mut wtxn, id, &record)?;
82 let encoded_props = crate::storage::props::encode(props)?;
83 self.write_edge_index_entries(&mut wtxn, id, record.edge_type, &etype, &encoded_props)?;
84
85 let new_record = EdgeRecord {
86 src: record.src,
87 dst: record.dst,
88 edge_type: record.edge_type,
89 props: encoded_props,
90 };
91 self.storage
92 .edges
93 .put(&mut wtxn, &id, &crate::storage::props::encode(&new_record)?)?;
94 wtxn.commit()?;
95 self.edge_columns.record_touched(id);
96 Ok(())
97 }
98
99 pub fn get_edge(&self, id: EdgeId) -> Result<Option<EdgeRecord>, Error> {
101 let rtxn = self.storage.env.read_txn()?;
102 self.get_edge_impl(&rtxn, id)
103 }
104
105 pub(super) fn get_edge_impl(
106 &self,
107 txn: &heed::RoTxn,
108 id: EdgeId,
109 ) -> Result<Option<EdgeRecord>, Error> {
110 match self.storage.edges.get(txn, &id)? {
111 Some(bytes) => Ok(Some(props::decode(bytes)?)),
112 None => Ok(None),
113 }
114 }
115
116 #[instrument(skip(self))]
118 pub fn delete_edge(&self, id: EdgeId) -> Result<(), Error> {
119 let _guard = self._write_lock.lock();
120 let mut wtxn = self.storage.env.write_txn()?;
121 let endpoints = self.delete_edge_impl(&mut wtxn, id)?;
122 wtxn.commit()?;
123 if let Some((src, dst)) = endpoints {
124 self.csr_cache.record_removed_edge(src, dst);
125 self.edge_columns.record_force_full();
127 }
128 self.maybe_spawn_rebuild();
129 Ok(())
130 }
131
132 pub(crate) fn delete_edge_impl(
136 &self,
137 wtxn: &mut heed::RwTxn,
138 id: EdgeId,
139 ) -> Result<Option<(NodeId, NodeId)>, Error> {
140 let record: EdgeRecord = match self.get_edge_impl(wtxn, id)? {
141 Some(rec) => rec,
142 None => return Ok(None),
143 };
144
145 self.delete_edge_index_entries(wtxn, id, &record)?;
147
148 self.storage.edges.delete(wtxn, &id)?;
150
151 self.storage
153 .type_idx
154 .delete(wtxn, &composite_key(record.edge_type, id))?;
155
156 adjust_type_count(&self.storage, wtxn, record.edge_type, -1)?;
158
159 let out_entry = AdjEntry {
161 edge_type: record.edge_type,
162 other: record.dst,
163 edge_id: id,
164 };
165 self.storage
166 .out_adj
167 .delete_one_duplicate(wtxn, &record.src, out_entry.as_bytes())?;
168
169 let in_entry = AdjEntry {
171 edge_type: record.edge_type,
172 other: record.src,
173 edge_id: id,
174 };
175 self.storage
176 .in_adj
177 .delete_one_duplicate(wtxn, &record.dst, in_entry.as_bytes())?;
178
179 Ok(Some((record.src, record.dst)))
180 }
181
182 pub fn out_neighbors(&self, node: NodeId) -> Result<Vec<NeighborEntry>, Error> {
196 let rtxn = self.storage.env.read_txn()?;
197 self.out_neighbors_impl(&rtxn, node)
198 }
199
200 pub(super) fn out_neighbors_impl(
201 &self,
202 rtxn: &heed::RoTxn,
203 node: NodeId,
204 ) -> Result<Vec<NeighborEntry>, Error> {
205 self.adj_entries_impl(rtxn, node, true)
206 }
207
208 pub fn in_neighbors(&self, node: NodeId) -> Result<Vec<NeighborEntry>, Error> {
210 let rtxn = self.storage.env.read_txn()?;
211 self.in_neighbors_impl(&rtxn, node)
212 }
213
214 pub(super) fn in_neighbors_impl(
215 &self,
216 rtxn: &heed::RoTxn,
217 node: NodeId,
218 ) -> Result<Vec<NeighborEntry>, Error> {
219 self.adj_entries_impl(rtxn, node, false)
220 }
221
222 pub fn node_has_relationships(&self, node: NodeId) -> Result<bool, Error> {
228 let rtxn = self.storage.env.read_txn()?;
229 if !self.adj_entries_impl(&rtxn, node, true)?.is_empty() {
230 return Ok(true);
231 }
232 Ok(!self.adj_entries_impl(&rtxn, node, false)?.is_empty())
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use tempfile::TempDir;
239
240 use super::*;
241
242 fn open_tmp() -> (TempDir, Graph) {
243 let dir = TempDir::new().unwrap();
244 let g = Graph::open(dir.path(), 1).unwrap();
245 (dir, g)
246 }
247
248 #[test]
252 fn out_neighbors_reflects_edge_added_after_snapshot() {
253 let (_dir, g) = open_tmp();
254 let a = g.add_node("N", &()).unwrap();
255 let b = g.add_node("N", &()).unwrap();
256
257 g.rebuild_csr().unwrap();
259 assert!(g.out_neighbors(a).unwrap().is_empty());
260
261 let eid = g.add_edge(a, b, "E", &()).unwrap();
262
263 let out = g.out_neighbors(a).unwrap();
264 assert_eq!(out.len(), 1, "new edge must be visible despite stale CSR");
265 assert_eq!(out[0].edge, eid);
266 assert_eq!(out[0].node, b);
267 }
268
269 #[test]
273 fn out_neighbors_reflects_edge_deleted_after_snapshot() {
274 let (_dir, g) = open_tmp();
275 let a = g.add_node("N", &()).unwrap();
276 let b = g.add_node("N", &()).unwrap();
277 let eid = g.add_edge(a, b, "E", &()).unwrap();
278
279 g.rebuild_csr().unwrap();
280 assert_eq!(g.out_neighbors(a).unwrap().len(), 1);
281
282 g.delete_edge(eid).unwrap();
283
284 assert!(
285 g.out_neighbors(a).unwrap().is_empty(),
286 "deleted edge must not appear, even though CSR still holds it"
287 );
288 }
289
290 #[test]
295 fn out_and_in_neighbors_agree_after_snapshot() {
296 let (_dir, g) = open_tmp();
297 let a = g.add_node("N", &()).unwrap();
298 let b = g.add_node("N", &()).unwrap();
299 g.rebuild_csr().unwrap();
300
301 let eid = g.add_edge(a, b, "E", &()).unwrap();
302
303 let out = g.out_neighbors(a).unwrap();
304 let inc = g.in_neighbors(b).unwrap();
305 assert_eq!(out.len(), 1);
306 assert_eq!(inc.len(), 1);
307 assert_eq!(out[0].edge, eid);
308 assert_eq!(inc[0].edge, eid);
309 }
310
311 #[test]
314 fn write_txn_out_neighbors_sees_uncommitted_edge() {
315 let (_dir, g) = open_tmp();
316 let a = g.add_node("N", &()).unwrap();
317 let b = g.add_node("N", &()).unwrap();
318 g.rebuild_csr().unwrap();
320
321 g.update(|txn| {
322 let eid = txn.add_edge(a, b, "E", &())?;
323 let out = txn.out_neighbors(a)?;
324 assert_eq!(out.len(), 1, "uncommitted edge must be visible in-txn");
325 assert_eq!(out[0].edge, eid);
326 Ok(())
327 })
328 .unwrap();
329 }
330
331 #[test]
334 fn update_edge_replaces_props() {
335 let (_dir, g) = open_tmp();
336 let a = g.add_node("N", &()).unwrap();
337 let b = g.add_node("N", &()).unwrap();
338 let eid = g.add_edge(a, b, "E", &serde_json::json!({"w": 1})).unwrap();
339
340 g.update_edge(eid, &serde_json::json!({"w": 2})).unwrap();
341
342 let rec = g.get_edge(eid).unwrap().expect("edge must still exist");
343 assert_eq!(rec.src, a);
344 assert_eq!(rec.dst, b);
345 let props: serde_json::Value = rmp_serde::from_slice(&rec.props).unwrap();
346 assert_eq!(props["w"], serde_json::json!(2));
347 }
348
349 #[test]
350 fn update_edge_missing_edge_errors() {
351 let (_dir, g) = open_tmp();
352 let err = g
353 .update_edge(999, &serde_json::json!({"w": 1}))
354 .unwrap_err();
355 assert!(matches!(err, Error::EdgeNotFound(999)));
356 }
357
358 #[test]
361 fn node_has_relationships_reflects_adjacency() {
362 let (_dir, g) = open_tmp();
363 let a = g.add_node("N", &()).unwrap();
364 let b = g.add_node("N", &()).unwrap();
365 assert!(!g.node_has_relationships(a).unwrap());
366 assert!(!g.node_has_relationships(b).unwrap());
367
368 let eid = g.add_edge(a, b, "E", &()).unwrap();
369 assert!(g.node_has_relationships(a).unwrap(), "out edge counts");
370 assert!(g.node_has_relationships(b).unwrap(), "in edge counts");
371
372 g.delete_edge(eid).unwrap();
373 assert!(!g.node_has_relationships(a).unwrap());
374 assert!(!g.node_has_relationships(b).unwrap());
375 }
376}