Skip to main content

core_query/
traverse.rs

1use crate::view::GraphView;
2use core_storage::Direction;
3use std::collections::{BTreeSet, VecDeque};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Dir {
7    Out,
8    In,
9    Both,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
13pub struct EdgeRef {
14    pub etype: u32,
15    pub src: u32,
16    pub dst: u32,
17}
18
19/// Typed 1-hop expansion. `etypes=None` → all edge types in the graph (sorted).
20/// Deterministic: etype asc, then neighbor asc; `Dir::Both` = Out then In.
21/// Dedupe only identical `EdgeRef` triples — Out and In of a pair are distinct.
22pub fn expand(view: &GraphView, id: u32, etypes: Option<&[u32]>, dir: Dir) -> Vec<EdgeRef> {
23    let types: Vec<u32> = match etypes {
24        Some(ts) => {
25            let mut v = ts.to_vec();
26            v.sort_unstable();
27            v.dedup();
28            v
29        }
30        None => view.topo.etypes().collect(),
31    };
32    let mut out = Vec::new();
33    let mut seen = BTreeSet::new();
34    for etype in types {
35        if matches!(dir, Dir::Out | Dir::Both) {
36            for &dst in view.topo.neighbors(etype, Direction::Out, id).as_ref() {
37                push_unique(
38                    &mut out,
39                    &mut seen,
40                    EdgeRef {
41                        etype,
42                        src: id,
43                        dst,
44                    },
45                );
46            }
47        }
48        if matches!(dir, Dir::In | Dir::Both) {
49            for &src in view.topo.neighbors(etype, Direction::In, id).as_ref() {
50                push_unique(
51                    &mut out,
52                    &mut seen,
53                    EdgeRef {
54                        etype,
55                        src,
56                        dst: id,
57                    },
58                );
59            }
60        }
61    }
62    out
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub struct Neighborhood {
67    pub nodes: Vec<(u32, u32)>, // (node id, depth), BFS order, start excluded, first-seen depth
68    pub edges: Vec<EdgeRef>,    // every edge traversed to reach a node (deduped, sorted)
69}
70
71pub fn neighborhood(
72    view: &GraphView,
73    start: u32,
74    depth: u32,
75    etypes: Option<&[u32]>,
76    dir: Dir,
77) -> Neighborhood {
78    let mut visited = BTreeSet::new();
79    visited.insert(start);
80    let mut queue = VecDeque::new();
81    queue.push_back((start, 0u32));
82    let mut nodes = Vec::new();
83    let mut edges = BTreeSet::new();
84
85    while let Some((id, d)) = queue.pop_front() {
86        if d >= depth {
87            continue;
88        }
89        for e in expand(view, id, etypes, dir) {
90            let nbr = if e.src == id { e.dst } else { e.src };
91            // Respect the view's node-visibility mask.  When `view` carries a
92            // mask (role-token path), hidden nodes are skipped entirely — they
93            // are not added to the result *and* not used as traversal
94            // intermediaries.  With no mask `view.visible` always returns true,
95            // so this branch is a no-op for the unmasked case.
96            if !view.visible(nbr) {
97                continue;
98            }
99            edges.insert(e);
100            if visited.insert(nbr) {
101                let nd = d + 1;
102                nodes.push((nbr, nd));
103                queue.push_back((nbr, nd));
104            }
105        }
106    }
107
108    Neighborhood {
109        nodes,
110        edges: edges.into_iter().collect(),
111    }
112}
113
114fn push_unique(out: &mut Vec<EdgeRef>, seen: &mut BTreeSet<EdgeRef>, e: EdgeRef) {
115    if seen.insert(e) {
116        out.push(e);
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::{expand, neighborhood, Dir, EdgeRef};
123    use crate::view::GraphView;
124    use core_storage::v8::seam::{ColumnsView, EdgePropsView, TopologyView};
125    use core_storage::{ColumnStore, EdgeProps, IdMap, Interner, Topology};
126
127    struct Fx {
128        ids: IdMap,
129        syms: Interner,
130        labels: Vec<u32>,
131        props: ColumnStore,
132        topo: Topology,
133        eprops: EdgeProps,
134    }
135
136    impl Fx {
137        fn new() -> Self {
138            Fx {
139                ids: IdMap::new(),
140                syms: Interner::new(),
141                labels: vec![],
142                props: ColumnStore::new(),
143                topo: Topology::new(),
144                eprops: EdgeProps::new(),
145            }
146        }
147
148        fn add(&mut self, label: &str, key: &str) -> u32 {
149            let id = self.ids.get_or_insert(key);
150            let sym = self.syms.intern(label);
151            self.labels.resize(id as usize + 1, u32::MAX);
152            self.labels[id as usize] = sym;
153            id
154        }
155
156        fn view(&self) -> GraphView<'_> {
157            GraphView {
158                ids: &self.ids,
159                syms: &self.syms,
160                labels: &self.labels,
161                props: ColumnsView::owned(&self.props),
162                topo: TopologyView::owned(&self.topo),
163                edge_props: EdgePropsView::owned(&self.eprops),
164                mask: None,
165                prop_index: None,
166                layer: None,
167            }
168        }
169    }
170
171    /// Diamond + shortcut:
172    ///   A -KNOWS-> B -KNOWS-> D
173    ///   A -KNOWS-> C -KNOWS-> D
174    ///   A -LIKES-> D
175    struct Diamond {
176        fx: Fx,
177        a: u32,
178        b: u32,
179        c: u32,
180        d: u32,
181        knows: u32,
182        likes: u32,
183    }
184
185    fn diamond() -> Diamond {
186        let mut fx = Fx::new();
187        let a = fx.add("Person", "a");
188        let b = fx.add("Person", "b");
189        let c = fx.add("Person", "c");
190        let d = fx.add("Person", "d");
191        let knows = fx.syms.intern("KNOWS");
192        let likes = fx.syms.intern("LIKES");
193        fx.topo.add_edge(knows, a, b);
194        fx.topo.add_edge(knows, a, c);
195        fx.topo.add_edge(knows, b, d);
196        fx.topo.add_edge(knows, c, d);
197        fx.topo.add_edge(likes, a, d);
198        Diamond {
199            fx,
200            a,
201            b,
202            c,
203            d,
204            knows,
205            likes,
206        }
207    }
208
209    fn e(etype: u32, src: u32, dst: u32) -> EdgeRef {
210        EdgeRef { etype, src, dst }
211    }
212
213    #[test]
214    fn expand_etype_then_neighbor_order_and_both_is_out_then_in() {
215        let g = diamond();
216        let v = g.fx.view();
217        assert_eq!(
218            expand(&v, g.a, None, Dir::Out),
219            vec![
220                e(g.knows, g.a, g.b),
221                e(g.knows, g.a, g.c),
222                e(g.likes, g.a, g.d)
223            ]
224        );
225        // caller etype order is ignored; result is still etype asc
226        assert_eq!(
227            expand(&v, g.a, Some(&[g.likes, g.knows]), Dir::Out),
228            vec![
229                e(g.knows, g.a, g.b),
230                e(g.knows, g.a, g.c),
231                e(g.likes, g.a, g.d)
232            ]
233        );
234        assert_eq!(
235            expand(&v, g.d, Some(&[g.knows]), Dir::In),
236            vec![e(g.knows, g.b, g.d), e(g.knows, g.c, g.d)]
237        );
238        assert_eq!(
239            expand(&v, g.b, Some(&[g.knows]), Dir::Both),
240            vec![e(g.knows, g.b, g.d), e(g.knows, g.a, g.b)]
241        );
242        // The same directed triple is visible as Out from src and In from dst.
243        assert!(expand(&v, g.a, Some(&[g.knows]), Dir::Out).contains(&e(g.knows, g.a, g.b)));
244        assert_eq!(
245            expand(&v, g.b, Some(&[g.knows]), Dir::In),
246            vec![e(g.knows, g.a, g.b)]
247        );
248    }
249
250    #[test]
251    fn expand_none_uses_topo_etypes_not_interned_symbols() {
252        let mut fx = Fx::new();
253        // Labels/fields interned between etypes: symbol space ≠ topology etypes.
254        let a = fx.add("Person", "a");
255        let b = fx.add("Person", "b");
256        let knows = fx.syms.intern("KNOWS");
257        let _age = fx.syms.intern("age");
258        let _company = fx.syms.intern("Company");
259        let likes = fx.syms.intern("LIKES");
260        fx.topo.add_edge(knows, a, b);
261        fx.topo.add_edge(likes, a, b);
262        assert!(fx.syms.get("Person").unwrap() < knows);
263        assert!(knows < fx.syms.get("age").unwrap());
264        assert!(fx.syms.get("age").unwrap() < likes);
265        let v = fx.view();
266        assert_eq!(
267            expand(&v, a, None, Dir::Out),
268            vec![e(knows, a, b), e(likes, a, b)]
269        );
270    }
271
272    #[test]
273    fn expand_dedupes_only_identical_triples() {
274        let mut g = diamond();
275        g.fx.topo.add_edge(g.knows, g.a, g.a); // self-loop: Out and In are the same triple
276        let v = g.fx.view();
277        let both = expand(&v, g.a, Some(&[g.knows, g.knows]), Dir::Both);
278        let self_loop_hits = both.iter().filter(|x| *x == &e(g.knows, g.a, g.a)).count();
279        assert_eq!(self_loop_hits, 1);
280        assert!(both.contains(&e(g.knows, g.a, g.b)));
281        assert!(both.contains(&e(g.knows, g.a, g.c)));
282    }
283
284    #[test]
285    fn neighborhood_depth0_empty() {
286        let g = diamond();
287        let v = g.fx.view();
288        let n = neighborhood(&v, g.a, 0, None, Dir::Out);
289        assert!(n.nodes.is_empty());
290        assert!(n.edges.is_empty());
291    }
292
293    #[test]
294    fn neighborhood_depth1_vs_depth2_and_first_seen() {
295        let g = diamond();
296        let v = g.fx.view();
297
298        let d1 = neighborhood(&v, g.a, 1, Some(&[g.knows]), Dir::Out);
299        assert_eq!(d1.nodes, vec![(g.b, 1), (g.c, 1)]);
300        assert_eq!(d1.edges, vec![e(g.knows, g.a, g.b), e(g.knows, g.a, g.c)]);
301
302        let d2 = neighborhood(&v, g.a, 2, Some(&[g.knows]), Dir::Out);
303        assert_eq!(d2.nodes, vec![(g.b, 1), (g.c, 1), (g.d, 2)]);
304        // every traversed edge, including both diamond legs to D
305        assert_eq!(
306            d2.edges,
307            vec![
308                e(g.knows, g.a, g.b),
309                e(g.knows, g.a, g.c),
310                e(g.knows, g.b, g.d),
311                e(g.knows, g.c, g.d),
312            ]
313        );
314
315        // LIKES shortcut: D is first seen at depth 1, not 2
316        let all = neighborhood(&v, g.a, 2, None, Dir::Out);
317        assert_eq!(all.nodes, vec![(g.b, 1), (g.c, 1), (g.d, 1)]);
318        assert_eq!(
319            all.edges,
320            vec![
321                e(g.knows, g.a, g.b),
322                e(g.knows, g.a, g.c),
323                e(g.knows, g.b, g.d),
324                e(g.knows, g.c, g.d),
325                e(g.likes, g.a, g.d),
326            ]
327        );
328    }
329
330    #[test]
331    fn neighborhood_dir_in_out_both_and_etype_filter() {
332        let g = diamond();
333        let v = g.fx.view();
334
335        let inn = neighborhood(&v, g.d, 1, None, Dir::In);
336        assert_eq!(inn.nodes, vec![(g.b, 1), (g.c, 1), (g.a, 1)]);
337        assert_eq!(
338            inn.edges,
339            vec![
340                e(g.knows, g.b, g.d),
341                e(g.knows, g.c, g.d),
342                e(g.likes, g.a, g.d),
343            ]
344        );
345
346        let likes_only = neighborhood(&v, g.a, 2, Some(&[g.likes]), Dir::Out);
347        assert_eq!(likes_only.nodes, vec![(g.d, 1)]);
348        assert_eq!(likes_only.edges, vec![e(g.likes, g.a, g.d)]);
349
350        let both = neighborhood(&v, g.b, 1, Some(&[g.knows]), Dir::Both);
351        assert_eq!(both.nodes, vec![(g.d, 1), (g.a, 1)]);
352        assert_eq!(both.edges, vec![e(g.knows, g.a, g.b), e(g.knows, g.b, g.d)]);
353    }
354
355    #[test]
356    fn neighborhood_is_deterministic() {
357        let g = diamond();
358        let v = g.fx.view();
359        let x = neighborhood(&v, g.a, 2, None, Dir::Both);
360        let y = neighborhood(&v, g.a, 2, None, Dir::Both);
361        assert_eq!(x, y);
362        assert_eq!(
363            expand(&v, g.a, None, Dir::Both),
364            expand(&v, g.a, None, Dir::Both)
365        );
366    }
367}