Skip to main content

nodedb_graph/csr/index/
restore.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Exact edge writes, and the reversal primitives a rollback of a graph
4//! write uses.
5//!
6//! A rollback puts the index back to what it held before the write: the
7//! edge's presence and weight, each node surrogate the write rebound, and
8//! each node or node label the write interned. Interning appends, so a
9//! rollback withdraws the newest entry first and refuses any other.
10
11use super::types::CsrIndex;
12use crate::GraphError;
13use crate::csr::rebuild::journal::{CsrWriteOp, OpOutcome};
14
15impl CsrIndex {
16    /// Weight of the live `(src, label, dst)` edge in `collection`, `None`
17    /// when that edge is not live.
18    pub fn edge_weight_in_collection(
19        &self,
20        src: &str,
21        label: &str,
22        dst: &str,
23        collection: &str,
24    ) -> Option<f64> {
25        let src_id = *self.node_to_id.get(src)?;
26        let dst_id = *self.node_to_id.get(dst)?;
27        let label_id = *self.label_to_id.get(label)?;
28        let coll_id = *self.collection_to_id.get(collection)?;
29        let idx = src_id as usize;
30
31        if let (Some(edges), Some(colls)) = (
32            self.buffer_out.get(idx),
33            self.buffer_out_collections.get(idx),
34        ) {
35            let position = edges
36                .iter()
37                .zip(colls.iter())
38                .position(|(&(l, d), &c)| l == label_id && d == dst_id && c == coll_id);
39            if let Some(k) = position {
40                let weight = if self.has_weights {
41                    self.buffer_out_weights
42                        .get(idx)
43                        .and_then(|weights| weights.get(k))
44                        .copied()
45                        .unwrap_or(1.0)
46                } else {
47                    1.0
48                };
49                return Some(weight);
50            }
51        }
52
53        if self
54            .deleted_edges
55            .contains(&(src_id, label_id, dst_id, coll_id))
56            || idx + 1 >= self.out_offsets.len()
57        {
58            return None;
59        }
60        let start = self.out_offsets[idx] as usize;
61        let end = self.out_offsets[idx + 1] as usize;
62        (start..end)
63            .find(|&i| {
64                self.out_labels[i] == label_id
65                    && self.out_targets[i] == dst_id
66                    && self.out_collections.get(i).copied().unwrap_or(0) == coll_id
67            })
68            .map(|i| self.out_edge_weight(i))
69    }
70
71    /// Make the `(src, label, dst)` edge in `collection` live with `weight`.
72    ///
73    /// A live edge with another weight takes the new one. Returns the weight
74    /// the edge had while live before, `None` when it was not live, so a
75    /// rollback can put it back.
76    pub fn put_edge_in_collection(
77        &mut self,
78        src: &str,
79        label: &str,
80        dst: &str,
81        collection: &str,
82        weight: f64,
83    ) -> Result<Option<f64>, GraphError> {
84        let result = self.apply_put_edge(src, label, dst, collection, weight);
85        self.journal_record(
86            || CsrWriteOp::PutEdge {
87                src: src.to_string(),
88                label: label.to_string(),
89                dst: dst.to_string(),
90                collection: collection.to_string(),
91                weight,
92            },
93            OpOutcome::of(&result),
94        );
95        result
96    }
97
98    /// The edge put itself, unjournaled.
99    pub(crate) fn apply_put_edge(
100        &mut self,
101        src: &str,
102        label: &str,
103        dst: &str,
104        collection: &str,
105        weight: f64,
106    ) -> Result<Option<f64>, GraphError> {
107        let prior = self.edge_weight_in_collection(src, label, dst, collection);
108        match prior {
109            Some(current) if current == weight => return Ok(prior),
110            Some(_) => self.apply_remove_edge(src, label, dst, collection),
111            None => {}
112        }
113        let src_id = self.ensure_node(src)?;
114        let dst_id = self.ensure_node(dst)?;
115        let label_id = self.ensure_label(label)?;
116        let coll_id = self.ensure_collection(collection);
117        if weight != 1.0 && !self.has_weights {
118            self.enable_weights();
119        }
120        // A deleted dense copy stays deleted: the live copy is the buffer one.
121        // With no dense copy, a deletion mark names this edge only, so it goes.
122        if !self.dense_has_edge(src_id, label_id, dst_id, coll_id) {
123            self.deleted_edges
124                .remove(&(src_id, label_id, dst_id, coll_id));
125        }
126        self.buffer_out[src_id as usize].push((label_id, dst_id));
127        self.buffer_in[dst_id as usize].push((label_id, src_id));
128        self.buffer_out_collections[src_id as usize].push(coll_id);
129        self.buffer_in_collections[dst_id as usize].push(coll_id);
130        if self.has_weights {
131            self.buffer_out_weights[src_id as usize].push(weight);
132            self.buffer_in_weights[dst_id as usize].push(weight);
133        }
134        Ok(prior)
135    }
136
137    /// Put the edge back to `prior`: live with that weight, or absent when
138    /// `prior` is `None`.
139    pub fn restore_edge_in_collection(
140        &mut self,
141        src: &str,
142        label: &str,
143        dst: &str,
144        collection: &str,
145        prior: Option<f64>,
146    ) -> Result<(), GraphError> {
147        match prior {
148            Some(weight) => self
149                .put_edge_in_collection(src, label, dst, collection, weight)
150                .map(drop),
151            None => {
152                self.remove_edge_in_collection(src, label, dst, collection);
153                Ok(())
154            }
155        }
156    }
157
158    /// Put `node`'s surrogate back to `prior`, `0` for none.
159    pub fn restore_node_surrogate(&mut self, node: &str, prior: u32) {
160        self.apply_restore_node_surrogate(node, prior);
161        self.journal_record(
162            || CsrWriteOp::RestoreNodeSurrogate {
163                node: node.to_string(),
164                prior,
165            },
166            OpOutcome::Applied,
167        );
168    }
169
170    /// The surrogate restore itself, unjournaled.
171    pub(crate) fn apply_restore_node_surrogate(&mut self, node: &str, prior: u32) {
172        let Some(&id) = self.node_to_id.get(node) else {
173            return;
174        };
175        let Some(slot) = self.node_surrogates.get_mut(id as usize) else {
176            return;
177        };
178        let current = *slot;
179        if current == prior {
180            return;
181        }
182        *slot = prior;
183        if current != 0 && self.surrogate_to_local.get(&current) == Some(&id) {
184            self.surrogate_to_local.remove(&current);
185        }
186        if prior != 0 {
187            self.surrogate_to_local.insert(prior, id);
188        }
189    }
190
191    /// Whether the node label `label` is interned.
192    pub fn has_node_label_name(&self, label: &str) -> bool {
193        self.node_label_to_id.contains_key(label)
194    }
195
196    /// Withdraw `node`, which a rolled-back write interned.
197    ///
198    /// An absent node is a no-op. The node must be the newest one, with no
199    /// edge and no label: withdrawing any other would renumber or orphan live
200    /// state, so that is refused.
201    pub fn withdraw_newest_node(&mut self, node: &str) -> Result<(), GraphError> {
202        let result = self.apply_withdraw_newest_node(node);
203        self.journal_record(
204            || CsrWriteOp::WithdrawNewestNode {
205                node: node.to_string(),
206            },
207            OpOutcome::of(&result),
208        );
209        result
210    }
211
212    /// The node withdraw itself, unjournaled.
213    pub(crate) fn apply_withdraw_newest_node(&mut self, node: &str) -> Result<(), GraphError> {
214        let Some(&id) = self.node_to_id.get(node) else {
215            return Ok(());
216        };
217        let idx = id as usize;
218        let newest = idx + 1 == self.id_to_node.len();
219        let no_buffered_edge = self.buffer_out.get(idx).is_none_or(Vec::is_empty)
220            && self.buffer_in.get(idx).is_none_or(Vec::is_empty);
221        let empty_range = |offsets: &[u32]| match (offsets.get(idx), offsets.get(idx + 1)) {
222            (Some(start), Some(end)) => start == end,
223            _ => true,
224        };
225        let no_dense_edge =
226            empty_range(self.out_offsets.as_slice()) && empty_range(self.in_offsets.as_slice());
227        let unlabeled = self.node_label_bits.get(idx).copied().unwrap_or(0) == 0;
228        if !(newest && no_buffered_edge && no_dense_edge && unlabeled) {
229            return Err(GraphError::WithdrawRefused {
230                kind: "node",
231                name: node.to_string(),
232            });
233        }
234
235        self.node_to_id.remove(node);
236        self.id_to_node.pop();
237        // Offsets hold one entry more than there are nodes.
238        if self.out_offsets.len() > idx + 1 {
239            self.out_offsets.pop();
240        }
241        if self.in_offsets.len() > idx + 1 {
242            self.in_offsets.pop();
243        }
244        self.buffer_out.truncate(idx);
245        self.buffer_in.truncate(idx);
246        self.buffer_out_weights.truncate(idx);
247        self.buffer_in_weights.truncate(idx);
248        self.buffer_out_collections.truncate(idx);
249        self.buffer_in_collections.truncate(idx);
250        self.node_label_bits.truncate(idx);
251        self.node_surrogates.truncate(idx);
252        self.surrogate_to_local.retain(|_, local| *local != id);
253        // The id goes back to the pool: no deletion mark may name it.
254        self.deleted_edges
255            .retain(|&(src, _, dst, _)| src != id && dst != id);
256        self.access_counts.truncate(idx);
257        Ok(())
258    }
259
260    /// Withdraw the node label `label`, which a rolled-back write interned.
261    ///
262    /// An absent label is a no-op. The label must be the newest one and no
263    /// node may carry it, or the withdraw is refused.
264    pub fn withdraw_newest_node_label(&mut self, label: &str) -> Result<(), GraphError> {
265        let result = self.apply_withdraw_newest_node_label(label);
266        self.journal_record(
267            || CsrWriteOp::WithdrawNewestNodeLabel {
268                label: label.to_string(),
269            },
270            OpOutcome::of(&result),
271        );
272        result
273    }
274
275    /// The node-label withdraw itself, unjournaled.
276    pub(crate) fn apply_withdraw_newest_node_label(
277        &mut self,
278        label: &str,
279    ) -> Result<(), GraphError> {
280        let Some(&id) = self.node_label_to_id.get(label) else {
281            return Ok(());
282        };
283        let newest = usize::from(id) + 1 == self.node_label_names.len();
284        let bit = 1u64 << id;
285        let carried = self.node_label_bits.iter().any(|bits| bits & bit != 0);
286        if !newest || carried {
287            return Err(GraphError::WithdrawRefused {
288                kind: "node label",
289                name: label.to_string(),
290            });
291        }
292        self.node_label_to_id.remove(label);
293        self.node_label_names.pop();
294        Ok(())
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use crate::csr::index::types::Direction;
302    use crate::test_support::test_memory;
303
304    #[test]
305    fn put_edge_replaces_the_weight_of_a_live_edge_and_reports_the_old_one() {
306        let mut csr = CsrIndex::new(test_memory());
307        assert_eq!(
308            csr.put_edge_in_collection("a", "L", "b", "c", 2.5)
309                .expect("first put"),
310            None
311        );
312        assert_eq!(
313            csr.put_edge_in_collection("a", "L", "b", "c", 9.0)
314                .expect("second put"),
315            Some(2.5)
316        );
317        assert_eq!(csr.edge_weight_in_collection("a", "L", "b", "c"), Some(9.0));
318        assert_eq!(csr.neighbors("a", &[], Direction::Out).len(), 1);
319    }
320
321    #[test]
322    fn put_edge_replaces_the_weight_of_a_compacted_edge() {
323        let mut csr = CsrIndex::new(test_memory());
324        csr.put_edge_in_collection("a", "L", "b", "c", 2.5)
325            .expect("put");
326        csr.compact().expect("compact");
327        assert_eq!(
328            csr.put_edge_in_collection("a", "L", "b", "c", 9.0)
329                .expect("reweigh"),
330            Some(2.5)
331        );
332        assert_eq!(csr.edge_weight_in_collection("a", "L", "b", "c"), Some(9.0));
333        assert_eq!(csr.neighbors("a", &[], Direction::Out).len(), 1);
334        csr.compact().expect("compact again");
335        assert_eq!(csr.edge_weight_in_collection("a", "L", "b", "c"), Some(9.0));
336        assert_eq!(csr.neighbors("a", &[], Direction::Out).len(), 1);
337    }
338
339    #[test]
340    fn restoring_an_edge_brings_back_a_deleted_compacted_edge() {
341        let mut csr = CsrIndex::new(test_memory());
342        csr.put_edge_in_collection("a", "L", "b", "c", 1.0)
343            .expect("put");
344        csr.compact().expect("compact");
345        csr.remove_edge_in_collection("a", "L", "b", "c");
346        assert_eq!(csr.edge_weight_in_collection("a", "L", "b", "c"), None);
347
348        csr.restore_edge_in_collection("a", "L", "b", "c", Some(1.0))
349            .expect("restore");
350        assert_eq!(csr.edge_weight_in_collection("a", "L", "b", "c"), Some(1.0));
351        assert_eq!(csr.neighbors("a", &[], Direction::Out).len(), 1);
352    }
353
354    #[test]
355    fn re_adding_a_deleted_compacted_edge_brings_it_back() {
356        let mut csr = CsrIndex::new(test_memory());
357        csr.add_edge_in_collection("a", "L", "b", "c")
358            .expect("edge");
359        csr.compact().expect("compact");
360        csr.remove_edge_in_collection("a", "L", "b", "c");
361        csr.add_edge_in_collection("a", "L", "b", "c")
362            .expect("re-add");
363        assert_eq!(csr.neighbors("a", &[], Direction::Out).len(), 1);
364    }
365
366    #[test]
367    fn withdrawing_the_newest_isolated_node_removes_it() {
368        let mut csr = CsrIndex::new(test_memory());
369        csr.add_edge("a", "L", "b").expect("edge");
370        csr.put_edge_in_collection("b", "L", "z", "c", 1.0)
371            .expect("edge to z");
372        csr.remove_edge_in_collection("b", "L", "z", "c");
373        csr.set_node_surrogate("z", nodedb_types::Surrogate::new(42));
374
375        csr.withdraw_newest_node("z").expect("withdraw");
376
377        assert!(!csr.contains_node("z"));
378        assert_eq!(csr.node_count(), 2);
379        assert_eq!(
380            csr.node_id_for_surrogate(nodedb_types::Surrogate::new(42)),
381            None
382        );
383        csr.add_edge("a", "L", "y")
384            .expect("a later node takes the freed id");
385        csr.compact().expect("compact");
386        assert_eq!(csr.neighbors("a", &[], Direction::Out).len(), 2);
387    }
388
389    #[test]
390    fn withdrawing_a_node_that_is_not_the_newest_is_refused() {
391        let mut csr = CsrIndex::new(test_memory());
392        csr.add_node_label("x", "Person").expect("label x");
393        csr.remove_node_label("x", "Person");
394        csr.add_edge("a", "L", "b").expect("edge");
395        assert!(matches!(
396            csr.withdraw_newest_node("x"),
397            Err(GraphError::WithdrawRefused { .. })
398        ));
399        assert!(csr.contains_node("x"));
400    }
401
402    #[test]
403    fn withdrawing_a_node_with_an_edge_is_refused() {
404        let mut csr = CsrIndex::new(test_memory());
405        csr.add_edge("a", "L", "b").expect("edge");
406        assert!(matches!(
407            csr.withdraw_newest_node("b"),
408            Err(GraphError::WithdrawRefused { .. })
409        ));
410    }
411
412    #[test]
413    fn withdrawing_the_newest_unused_node_label_frees_its_slot() {
414        let mut csr = CsrIndex::new(test_memory());
415        csr.add_node_label("x", "Person").expect("label");
416        csr.remove_node_label("x", "Person");
417        csr.withdraw_newest_node_label("Person").expect("withdraw");
418        assert!(!csr.has_node_label_name("Person"));
419    }
420
421    #[test]
422    fn withdrawing_a_carried_node_label_is_refused() {
423        let mut csr = CsrIndex::new(test_memory());
424        csr.add_node_label("x", "Person").expect("label");
425        assert!(matches!(
426            csr.withdraw_newest_node_label("Person"),
427            Err(GraphError::WithdrawRefused { .. })
428        ));
429    }
430
431    #[test]
432    fn restoring_a_surrogate_unbinds_the_one_a_write_set() {
433        let mut csr = CsrIndex::new(test_memory());
434        csr.add_edge("a", "L", "b").expect("edge");
435        csr.set_node_surrogate("a", nodedb_types::Surrogate::new(5));
436        csr.restore_node_surrogate("a", 0);
437        assert_eq!(csr.node_surrogate("a"), None);
438        assert_eq!(
439            csr.node_id_for_surrogate(nodedb_types::Surrogate::new(5)),
440            None
441        );
442    }
443}