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