Skip to main content

nodedb_graph/traversal/
shortest_path.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Bidirectional shortest paths over CSR adjacency.
4
5use std::collections::{HashMap, hash_map::Entry};
6
7#[cfg(test)]
8use super::DEFAULT_MAX_VISITED;
9use crate::csr::CsrIndex;
10use crate::overlay_delta::GraphOverlayDelta;
11use crate::path_overlay::shortest_path_overlay;
12use crate::path_params::ShortestPathParams;
13
14impl CsrIndex {
15    /// Shortest path via bidirectional BFS.
16    ///
17    /// `max_visited` caps the combined forward+backward visited set to prevent
18    /// supernode fan-out explosion. Pass [`crate::traversal::DEFAULT_MAX_VISITED`]
19    /// for the standard limit.
20    ///
21    /// `frontier_bitmap`: when `Some`, only nodes whose surrogate is present in
22    /// the bitmap are eligible for expansion. The start and end nodes are not
23    /// gated.
24    ///
25    /// `overlay`: when `Some` and non-empty, the search observes the
26    /// transaction's staged edge writes/deletes (read-your-own-writes),
27    /// including a path that must pass through a node reachable only via a
28    /// staged edge. When `None` or empty, the durable-only dense bidirectional
29    /// fast path runs unchanged.
30    pub fn shortest_path(
31        &self,
32        params: ShortestPathParams<'_>,
33        overlay: Option<&GraphOverlayDelta>,
34    ) -> Option<Vec<String>> {
35        Self::shortest_path_on(Some(self), params, overlay)
36    }
37
38    /// [`Self::shortest_path`] over an optional partition. `None` is a
39    /// tenant with no CSR partition: the search then follows only the staged
40    /// edges of `overlay`, and finds no path without them.
41    pub fn shortest_path_on(
42        partition: Option<&Self>,
43        params: ShortestPathParams<'_>,
44        overlay: Option<&GraphOverlayDelta>,
45    ) -> Option<Vec<String>> {
46        match (partition, overlay) {
47            (_, Some(ov)) if !ov.is_empty() => shortest_path_overlay(partition, params, ov),
48            (Some(csr), _) => csr.shortest_path_dense(params),
49            (None, _) => None,
50        }
51    }
52
53    /// Durable-only bidirectional BFS over the dense u32 CSR ids.
54    ///
55    /// Each step expands one forward level, then one backward level. A level
56    /// relaxes its edges in `(neighbour, frontier node)` name order, and the
57    /// search stops at the first node both sides reached. The cap is checked
58    /// before each step.
59    ///
60    /// Round `k` meets on a path of `2k - 1` edges (forward level) or `2k`
61    /// edges (backward level), so the first meeting is a shortest path, and
62    /// `max_depth.div_ceil(2)` rounds reach every path of at most `max_depth`
63    /// edges. A meeting on a longer path returns `None`.
64    fn shortest_path_dense(&self, params: ShortestPathParams<'_>) -> Option<Vec<String>> {
65        let ShortestPathParams {
66            src,
67            dst,
68            label_filter,
69            max_depth,
70            max_visited,
71            frontier_bitmap,
72        } = params;
73        let src_id = *self.node_to_id.get(src)?;
74        let dst_id = *self.node_to_id.get(dst)?;
75        if src_id == dst_id {
76            return Some(vec![src.to_string()]);
77        }
78
79        let labels = self.label_filter(label_filter);
80        // The endpoints are never gated: a bitmap that leaves them out still
81        // admits the edges that reach them.
82        let in_bitmap = |id: u32| {
83            id == src_id
84                || id == dst_id
85                || frontier_bitmap.is_none_or(|bm| {
86                    bm.contains(nodedb_types::Surrogate::new(self.node_surrogate_raw(id)))
87                })
88        };
89        let within_depth =
90            |path: Vec<String>| (path.len().saturating_sub(1) <= max_depth).then_some(path);
91        let mut fwd_parent: HashMap<u32, u32> = HashMap::new();
92        let mut bwd_parent: HashMap<u32, u32> = HashMap::new();
93        fwd_parent.insert(src_id, src_id);
94        bwd_parent.insert(dst_id, dst_id);
95
96        let mut fwd_frontier: Vec<u32> = vec![src_id];
97        let mut bwd_frontier: Vec<u32> = vec![dst_id];
98
99        for _round in 0..max_depth.div_ceil(2) {
100            if fwd_parent.len() + bwd_parent.len() >= max_visited {
101                break;
102            }
103
104            // Each level's edges are relaxed in (neighbour, frontier node)
105            // name order, so the parent a node gets, and the meeting point,
106            // do not depend on how the edges are stored. A cluster coordinator
107            // relaxes cross-shard hops in the same order.
108            let mut candidates: Vec<(u32, u32)> = Vec::new();
109            for &node in &fwd_frontier {
110                self.record_access(node);
111                for (lid, neighbor) in self.dense_iter_out(node) {
112                    if labels.keeps(lid) && in_bitmap(neighbor) {
113                        candidates.push((neighbor, node));
114                    }
115                }
116            }
117            self.sort_edges_by_name(&mut candidates);
118            let mut next_fwd = Vec::new();
119            for (neighbor, node) in candidates {
120                if let Entry::Vacant(e) = fwd_parent.entry(neighbor) {
121                    e.insert(node);
122                    next_fwd.push(neighbor);
123                }
124                if bwd_parent.contains_key(&neighbor) {
125                    return within_depth(self.reconstruct_path(neighbor, &fwd_parent, &bwd_parent));
126                }
127            }
128            fwd_frontier = next_fwd;
129
130            let mut candidates: Vec<(u32, u32)> = Vec::new();
131            for &node in &bwd_frontier {
132                self.record_access(node);
133                for (lid, neighbor) in self.dense_iter_in(node) {
134                    if labels.keeps(lid) && in_bitmap(neighbor) {
135                        candidates.push((neighbor, node));
136                    }
137                }
138            }
139            self.sort_edges_by_name(&mut candidates);
140            let mut next_bwd = Vec::new();
141            for (neighbor, node) in candidates {
142                if let Entry::Vacant(e) = bwd_parent.entry(neighbor) {
143                    e.insert(node);
144                    next_bwd.push(neighbor);
145                }
146                if fwd_parent.contains_key(&neighbor) {
147                    return within_depth(self.reconstruct_path(neighbor, &fwd_parent, &bwd_parent));
148                }
149            }
150            bwd_frontier = next_bwd;
151
152            if fwd_frontier.is_empty() && bwd_frontier.is_empty() {
153                break;
154            }
155        }
156        None
157    }
158
159    /// Order `(neighbour, frontier node)` edges by the two node names.
160    fn sort_edges_by_name(&self, edges: &mut [(u32, u32)]) {
161        edges.sort_by(|a, b| {
162            let name = |id: u32| self.node_name_checked(id);
163            (name(a.0), name(a.1)).cmp(&(name(b.0), name(b.1)))
164        });
165    }
166
167    fn reconstruct_path(
168        &self,
169        meeting: u32,
170        fwd_parent: &HashMap<u32, u32>,
171        bwd_parent: &HashMap<u32, u32>,
172    ) -> Vec<String> {
173        let mut fwd_path = Vec::new();
174        let mut current = meeting;
175        loop {
176            fwd_path.push(current);
177            let parent = fwd_parent[&current];
178            if parent == current {
179                break;
180            }
181            current = parent;
182        }
183        fwd_path.reverse();
184
185        current = bwd_parent[&meeting];
186        if current != meeting {
187            loop {
188                fwd_path.push(current);
189                let parent = bwd_parent[&current];
190                if parent == current {
191                    break;
192                }
193                current = parent;
194            }
195        }
196
197        fwd_path
198            .into_iter()
199            .map(|id| self.id_to_node[id as usize].clone())
200            .collect()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use crate::test_support::{chain_csr, long_chain_csr, test_memory};
208
209    fn path_params<'a>(
210        src: &'a str,
211        dst: &'a str,
212        label_filter: &'a [&'a str],
213        max_depth: usize,
214        frontier_bitmap: Option<&'a nodedb_types::SurrogateBitmap>,
215    ) -> ShortestPathParams<'a> {
216        ShortestPathParams {
217            src,
218            dst,
219            label_filter,
220            max_depth,
221            max_visited: DEFAULT_MAX_VISITED,
222            frontier_bitmap,
223        }
224    }
225
226    #[test]
227    fn shortest_path_direct() {
228        let csr = chain_csr();
229        let path = csr
230            .shortest_path(path_params("a", "c", &["KNOWS"], 5, None), None)
231            .unwrap();
232        assert_eq!(path, vec!["a", "b", "c"]);
233    }
234
235    #[test]
236    fn shortest_path_takes_the_smallest_named_tie() {
237        // Two paths of equal length, the `z` one stored first.
238        let mut csr = CsrIndex::new(test_memory());
239        csr.add_edge("a", "L", "z").unwrap();
240        csr.add_edge("z", "L", "d").unwrap();
241        csr.add_edge("a", "L", "b").unwrap();
242        csr.add_edge("b", "L", "d").unwrap();
243        let path = csr
244            .shortest_path(path_params("a", "d", &[], 5, None), None)
245            .unwrap();
246        assert_eq!(path, vec!["a", "b", "d"]);
247    }
248
249    #[test]
250    fn shortest_path_over_a_long_chain() {
251        let csr = long_chain_csr();
252        let path = csr
253            .shortest_path(path_params("n0", "n50", &["NEXT"], 100, None), None)
254            .unwrap();
255        assert_eq!(path.len(), 51);
256        // 50 edges: a depth of 49 finds none, 50 finds it.
257        assert!(
258            csr.shortest_path(path_params("n0", "n50", &["NEXT"], 49, None), None)
259                .is_none()
260        );
261        assert!(
262            csr.shortest_path(path_params("n0", "n50", &["NEXT"], 50, None), None)
263                .is_some()
264        );
265    }
266
267    #[test]
268    fn shortest_path_same_node() {
269        let csr = chain_csr();
270        let path = csr
271            .shortest_path(path_params("a", "a", &[], 5, None), None)
272            .unwrap();
273        assert_eq!(path, vec!["a"]);
274    }
275
276    #[test]
277    fn shortest_path_unreachable() {
278        let csr = chain_csr();
279        let path = csr.shortest_path(path_params("d", "a", &["KNOWS"], 5, None), None);
280        assert!(path.is_none());
281    }
282
283    #[test]
284    fn shortest_path_depth_limit() {
285        let csr = chain_csr();
286        let path = csr.shortest_path(path_params("a", "d", &["KNOWS"], 1, None), None);
287        assert!(path.is_none());
288    }
289
290    /// shortest_path with a bitmap that excludes the only intermediate node.
291    /// "b" is the only path from "a" to "c" via KNOWS edges; if "b" is blocked
292    /// then no path exists.
293    #[test]
294    fn shortest_path_frontier_bitmap_blocks_intermediate() {
295        use nodedb_types::{Surrogate, SurrogateBitmap};
296
297        let mut csr = chain_csr();
298        csr.set_node_surrogate("b", Surrogate::new(10));
299        csr.set_node_surrogate("c", Surrogate::new(20));
300
301        // Bitmap that does NOT contain "b".
302        let mut bm = SurrogateBitmap::new();
303        bm.insert(Surrogate::new(20)); // only "c" is in the bitmap
304
305        let path = csr.shortest_path(path_params("a", "c", &["KNOWS"], 5, Some(&bm)), None);
306        // "b" (surrogate 10) is not in the bitmap so expansion through it is
307        // blocked, making the path from "a" to "c" unreachable.
308        assert!(path.is_none());
309    }
310
311    /// A bitmap that holds neither endpoint still admits the edge between
312    /// them: the endpoints are never gated.
313    #[test]
314    fn the_frontier_bitmap_never_gates_the_endpoints() {
315        use nodedb_types::{Surrogate, SurrogateBitmap};
316
317        let mut csr = CsrIndex::new(test_memory());
318        csr.add_edge("a", "L", "c").unwrap();
319        csr.set_node_surrogate("a", Surrogate::new(1));
320        csr.set_node_surrogate("c", Surrogate::new(3));
321        let mut bm = SurrogateBitmap::new();
322        bm.insert(Surrogate::new(99));
323
324        let path = csr.shortest_path(path_params("a", "c", &[], 1, Some(&bm)), None);
325        assert_eq!(path, Some(vec!["a".to_string(), "c".to_string()]));
326    }
327
328    fn params<'a>(labels: &'a [&'a str], depth: usize) -> ShortestPathParams<'a> {
329        ShortestPathParams {
330            src: "a",
331            dst: "d",
332            label_filter: labels,
333            max_depth: depth,
334            max_visited: DEFAULT_MAX_VISITED,
335            frontier_bitmap: None,
336        }
337    }
338
339    #[test]
340    fn dense_path_matches_any_listed_label() {
341        let mut csr = CsrIndex::new(test_memory());
342        csr.add_edge("a", "FIRST", "b").unwrap();
343        csr.add_edge("b", "SECOND", "d").unwrap();
344        assert_eq!(
345            csr.shortest_path(params(&["FIRST", "SECOND"], 2), None),
346            Some(vec!["a".into(), "b".into(), "d".into()])
347        );
348        assert!(
349            csr.shortest_path(params(&["FIRST", "SECOND"], 1), None)
350                .is_none()
351        );
352        assert!(csr.shortest_path(params(&["FIRST"], 2), None).is_none());
353        assert!(csr.shortest_path(params(&["unknown"], 2), None).is_none());
354        assert!(csr.shortest_path(params(&[], 2), None).is_some());
355        assert!(
356            csr.shortest_path(params(&["unknown", "FIRST", "SECOND"], 2), None)
357                .is_some()
358        );
359        assert!(
360            csr.shortest_path(params(&["FIRST", "SECOND"], 0), None)
361                .is_none()
362        );
363    }
364
365    /// `a -FIRST-> b -SECOND-> d` plus a shortcut `a -OTHER-> d`. An unknown
366    /// label in the set must not widen it to the shortcut.
367    #[test]
368    fn an_unknown_label_in_the_set_does_not_widen_it() {
369        let mut csr = CsrIndex::new(test_memory());
370        csr.add_edge("a", "FIRST", "b").unwrap();
371        csr.add_edge("b", "SECOND", "d").unwrap();
372        csr.add_edge("a", "OTHER", "d").unwrap();
373        assert_eq!(
374            csr.shortest_path(params(&["FIRST", "SECOND", "unknown"], 2), None),
375            Some(vec!["a".into(), "b".into(), "d".into()])
376        );
377        assert!(
378            csr.shortest_path(params(&["FIRST", "unknown"], 2), None)
379                .is_none()
380        );
381        assert!(
382            csr.shortest_path(params(&["unknown", "never"], 2), None)
383                .is_none()
384        );
385    }
386
387    #[test]
388    fn overlay_path_matches_labels_without_durable_ids() {
389        let csr = CsrIndex::new(test_memory());
390        let mut overlay = GraphOverlayDelta::new();
391        overlay.stage_edge("a", "FIRST", "x");
392        overlay.stage_edge("x", "SECOND", "d");
393        assert_eq!(
394            csr.shortest_path(params(&["FIRST", "SECOND"], 2), Some(&overlay)),
395            Some(vec!["a".into(), "x".into(), "d".into()])
396        );
397        assert!(
398            csr.shortest_path(params(&["FIRST", "SECOND"], 1), Some(&overlay))
399                .is_none()
400        );
401        assert!(
402            csr.shortest_path(params(&["unknown"], 2), Some(&overlay))
403                .is_none()
404        );
405        assert!(
406            csr.shortest_path(params(&["FIRST"], 2), Some(&overlay))
407                .is_none()
408        );
409        assert!(csr.shortest_path(params(&[], 2), Some(&overlay)).is_some());
410        assert!(
411            csr.shortest_path(params(&["FIRST", "SECOND"], 0), Some(&overlay))
412                .is_none()
413        );
414    }
415
416    #[test]
417    fn mixed_labels_preserve_forward_and_backward_tombstones() {
418        let mut csr = CsrIndex::new(test_memory());
419        csr.add_edge("a", "FIRST", "b").unwrap();
420        csr.add_edge("b", "SECOND", "d").unwrap();
421        let labels = ["FIRST", "SECOND"];
422        for (src, label, dst) in [("a", "FIRST", "b"), ("b", "SECOND", "d")] {
423            let mut overlay = GraphOverlayDelta::new();
424            overlay.stage_tombstone(src, label, dst);
425            assert!(
426                csr.shortest_path(params(&labels, 2), Some(&overlay))
427                    .is_none()
428            );
429        }
430    }
431
432    #[test]
433    fn odd_length_paths_count_every_edge() {
434        let mut csr = CsrIndex::new(test_memory());
435        csr.add_edge("a", "FIRST", "b").unwrap();
436        csr.add_edge("b", "SECOND", "c").unwrap();
437        csr.add_edge("c", "FIRST", "d").unwrap();
438        let labels = ["FIRST", "SECOND"];
439        assert!(csr.shortest_path(params(&labels, 2), None).is_none());
440        assert_eq!(
441            csr.shortest_path(params(&labels, 3), None),
442            Some(vec!["a".into(), "b".into(), "c".into(), "d".into()])
443        );
444
445        let staged_csr = CsrIndex::new(test_memory());
446        let mut overlay = GraphOverlayDelta::new();
447        overlay.stage_edge("a", "FIRST", "b");
448        overlay.stage_edge("b", "SECOND", "c");
449        overlay.stage_edge("c", "FIRST", "d");
450        assert!(
451            staged_csr
452                .shortest_path(params(&labels, 2), Some(&overlay))
453                .is_none()
454        );
455        assert_eq!(
456            staged_csr.shortest_path(params(&labels, 3), Some(&overlay)),
457            Some(vec!["a".into(), "b".into(), "c".into(), "d".into()])
458        );
459    }
460}