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            }
166        }
167    }
168
169    /// Diamond + shortcut:
170    ///   A -KNOWS-> B -KNOWS-> D
171    ///   A -KNOWS-> C -KNOWS-> D
172    ///   A -LIKES-> D
173    struct Diamond {
174        fx: Fx,
175        a: u32,
176        b: u32,
177        c: u32,
178        d: u32,
179        knows: u32,
180        likes: u32,
181    }
182
183    fn diamond() -> Diamond {
184        let mut fx = Fx::new();
185        let a = fx.add("Person", "a");
186        let b = fx.add("Person", "b");
187        let c = fx.add("Person", "c");
188        let d = fx.add("Person", "d");
189        let knows = fx.syms.intern("KNOWS");
190        let likes = fx.syms.intern("LIKES");
191        fx.topo.add_edge(knows, a, b);
192        fx.topo.add_edge(knows, a, c);
193        fx.topo.add_edge(knows, b, d);
194        fx.topo.add_edge(knows, c, d);
195        fx.topo.add_edge(likes, a, d);
196        Diamond {
197            fx,
198            a,
199            b,
200            c,
201            d,
202            knows,
203            likes,
204        }
205    }
206
207    fn e(etype: u32, src: u32, dst: u32) -> EdgeRef {
208        EdgeRef { etype, src, dst }
209    }
210
211    #[test]
212    fn expand_etype_then_neighbor_order_and_both_is_out_then_in() {
213        let g = diamond();
214        let v = g.fx.view();
215        assert_eq!(
216            expand(&v, g.a, None, Dir::Out),
217            vec![
218                e(g.knows, g.a, g.b),
219                e(g.knows, g.a, g.c),
220                e(g.likes, g.a, g.d)
221            ]
222        );
223        // caller etype order is ignored; result is still etype asc
224        assert_eq!(
225            expand(&v, g.a, Some(&[g.likes, g.knows]), Dir::Out),
226            vec![
227                e(g.knows, g.a, g.b),
228                e(g.knows, g.a, g.c),
229                e(g.likes, g.a, g.d)
230            ]
231        );
232        assert_eq!(
233            expand(&v, g.d, Some(&[g.knows]), Dir::In),
234            vec![e(g.knows, g.b, g.d), e(g.knows, g.c, g.d)]
235        );
236        assert_eq!(
237            expand(&v, g.b, Some(&[g.knows]), Dir::Both),
238            vec![e(g.knows, g.b, g.d), e(g.knows, g.a, g.b)]
239        );
240        // The same directed triple is visible as Out from src and In from dst.
241        assert!(expand(&v, g.a, Some(&[g.knows]), Dir::Out).contains(&e(g.knows, g.a, g.b)));
242        assert_eq!(
243            expand(&v, g.b, Some(&[g.knows]), Dir::In),
244            vec![e(g.knows, g.a, g.b)]
245        );
246    }
247
248    #[test]
249    fn expand_none_uses_topo_etypes_not_interned_symbols() {
250        let mut fx = Fx::new();
251        // Labels/fields interned between etypes: symbol space ≠ topology etypes.
252        let a = fx.add("Person", "a");
253        let b = fx.add("Person", "b");
254        let knows = fx.syms.intern("KNOWS");
255        let _age = fx.syms.intern("age");
256        let _company = fx.syms.intern("Company");
257        let likes = fx.syms.intern("LIKES");
258        fx.topo.add_edge(knows, a, b);
259        fx.topo.add_edge(likes, a, b);
260        assert!(fx.syms.get("Person").unwrap() < knows);
261        assert!(knows < fx.syms.get("age").unwrap());
262        assert!(fx.syms.get("age").unwrap() < likes);
263        let v = fx.view();
264        assert_eq!(
265            expand(&v, a, None, Dir::Out),
266            vec![e(knows, a, b), e(likes, a, b)]
267        );
268    }
269
270    #[test]
271    fn expand_dedupes_only_identical_triples() {
272        let mut g = diamond();
273        g.fx.topo.add_edge(g.knows, g.a, g.a); // self-loop: Out and In are the same triple
274        let v = g.fx.view();
275        let both = expand(&v, g.a, Some(&[g.knows, g.knows]), Dir::Both);
276        let self_loop_hits = both.iter().filter(|x| *x == &e(g.knows, g.a, g.a)).count();
277        assert_eq!(self_loop_hits, 1);
278        assert!(both.contains(&e(g.knows, g.a, g.b)));
279        assert!(both.contains(&e(g.knows, g.a, g.c)));
280    }
281
282    #[test]
283    fn neighborhood_depth0_empty() {
284        let g = diamond();
285        let v = g.fx.view();
286        let n = neighborhood(&v, g.a, 0, None, Dir::Out);
287        assert!(n.nodes.is_empty());
288        assert!(n.edges.is_empty());
289    }
290
291    #[test]
292    fn neighborhood_depth1_vs_depth2_and_first_seen() {
293        let g = diamond();
294        let v = g.fx.view();
295
296        let d1 = neighborhood(&v, g.a, 1, Some(&[g.knows]), Dir::Out);
297        assert_eq!(d1.nodes, vec![(g.b, 1), (g.c, 1)]);
298        assert_eq!(d1.edges, vec![e(g.knows, g.a, g.b), e(g.knows, g.a, g.c)]);
299
300        let d2 = neighborhood(&v, g.a, 2, Some(&[g.knows]), Dir::Out);
301        assert_eq!(d2.nodes, vec![(g.b, 1), (g.c, 1), (g.d, 2)]);
302        // every traversed edge, including both diamond legs to D
303        assert_eq!(
304            d2.edges,
305            vec![
306                e(g.knows, g.a, g.b),
307                e(g.knows, g.a, g.c),
308                e(g.knows, g.b, g.d),
309                e(g.knows, g.c, g.d),
310            ]
311        );
312
313        // LIKES shortcut: D is first seen at depth 1, not 2
314        let all = neighborhood(&v, g.a, 2, None, Dir::Out);
315        assert_eq!(all.nodes, vec![(g.b, 1), (g.c, 1), (g.d, 1)]);
316        assert_eq!(
317            all.edges,
318            vec![
319                e(g.knows, g.a, g.b),
320                e(g.knows, g.a, g.c),
321                e(g.knows, g.b, g.d),
322                e(g.knows, g.c, g.d),
323                e(g.likes, g.a, g.d),
324            ]
325        );
326    }
327
328    #[test]
329    fn neighborhood_dir_in_out_both_and_etype_filter() {
330        let g = diamond();
331        let v = g.fx.view();
332
333        let inn = neighborhood(&v, g.d, 1, None, Dir::In);
334        assert_eq!(inn.nodes, vec![(g.b, 1), (g.c, 1), (g.a, 1)]);
335        assert_eq!(
336            inn.edges,
337            vec![
338                e(g.knows, g.b, g.d),
339                e(g.knows, g.c, g.d),
340                e(g.likes, g.a, g.d),
341            ]
342        );
343
344        let likes_only = neighborhood(&v, g.a, 2, Some(&[g.likes]), Dir::Out);
345        assert_eq!(likes_only.nodes, vec![(g.d, 1)]);
346        assert_eq!(likes_only.edges, vec![e(g.likes, g.a, g.d)]);
347
348        let both = neighborhood(&v, g.b, 1, Some(&[g.knows]), Dir::Both);
349        assert_eq!(both.nodes, vec![(g.d, 1), (g.a, 1)]);
350        assert_eq!(both.edges, vec![e(g.knows, g.a, g.b), e(g.knows, g.b, g.d)]);
351    }
352
353    #[test]
354    fn neighborhood_is_deterministic() {
355        let g = diamond();
356        let v = g.fx.view();
357        let x = neighborhood(&v, g.a, 2, None, Dir::Both);
358        let y = neighborhood(&v, g.a, 2, None, Dir::Both);
359        assert_eq!(x, y);
360        assert_eq!(
361            expand(&v, g.a, None, Dir::Both),
362            expand(&v, g.a, None, Dir::Both)
363        );
364    }
365}