Skip to main content

oxgraph_db/
traversal.rs

1//! Graph projection traversal primitives.
2
3use std::collections::{BTreeSet, VecDeque};
4
5use oxgraph_graph::{
6    CanonicalElementIdentity, ElementPredecessors, ElementSuccessors, LocalElementIdentity,
7};
8
9use crate::{
10    DbError, ElementId,
11    projection::{GraphProjection, ProjectionElementId},
12};
13
14/// Direction used by graph projection traversal.
15///
16/// # Performance
17///
18/// Copying and comparing are `O(1)`.
19#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
20pub enum TraversalDirection {
21    /// Follow outgoing graph projection neighbors.
22    #[default]
23    Outgoing,
24    /// Follow incoming graph projection neighbors.
25    Incoming,
26    /// Follow outgoing neighbors first, then incoming neighbors.
27    Both,
28}
29
30/// Options for bounded graph projection traversal.
31///
32/// # Performance
33///
34/// Copying is `O(1)`.
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub struct TraversalOptions {
37    /// Maximum hop depth to traverse from any seed.
38    pub max_depth: usize,
39    /// Direction used to expand each frontier element.
40    pub direction: TraversalDirection,
41    /// Maximum number of rows to emit.
42    pub limit: usize,
43    /// Whether depth-0 seed elements are included in the result rows.
44    pub include_start: bool,
45}
46
47impl Default for TraversalOptions {
48    /// Creates outgoing depth-1 traversal options with an unbounded row limit.
49    ///
50    /// # Performance
51    ///
52    /// This function is `O(1)`.
53    fn default() -> Self {
54        Self {
55            max_depth: 1,
56            direction: TraversalDirection::Outgoing,
57            limit: usize::MAX,
58            include_start: false,
59        }
60    }
61}
62
63/// One graph traversal result row.
64///
65/// # Performance
66///
67/// Copying and comparing are `O(1)`.
68#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
69pub struct TraversalRow {
70    /// Canonical element discovered by traversal.
71    pub element: ElementId,
72    /// Shortest discovered hop depth from any seed.
73    pub depth: usize,
74}
75
76/// Materialized graph traversal result.
77///
78/// Rows are unique by canonical element and ordered by BFS first discovery.
79///
80/// # Performance
81///
82/// Iterating rows is `O(row count)`.
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub struct TraversalResult {
85    /// Materialized traversal rows.
86    rows: Vec<TraversalRow>,
87}
88
89impl TraversalResult {
90    /// Creates a traversal result from rows.
91    ///
92    /// # Performance
93    ///
94    /// This function is `O(1)` excluding row ownership transfer.
95    #[must_use]
96    pub(crate) const fn new(rows: Vec<TraversalRow>) -> Self {
97        Self { rows }
98    }
99
100    /// Returns result rows.
101    ///
102    /// # Performance
103    ///
104    /// This method is `O(1)`.
105    #[must_use]
106    pub fn rows(&self) -> &[TraversalRow] {
107        &self.rows
108    }
109}
110
111/// Traverses a materialized graph projection.
112pub(crate) fn traverse_graph_projection(
113    graph: &GraphProjection,
114    seeds: &[ElementId],
115    options: TraversalOptions,
116) -> Result<TraversalResult, DbError> {
117    if seeds.is_empty() || options.limit == 0 {
118        return Ok(TraversalResult::new(Vec::new()));
119    }
120
121    let mut traversal = TraversalState::new(graph, options.limit);
122    for seed in seeds {
123        let local = graph
124            .local_element_id(*seed)
125            .ok_or(DbError::UnknownElement { id: *seed })?;
126        traversal.push_seed(local, *seed, options.include_start);
127        if traversal.at_limit() {
128            return Ok(traversal.finish());
129        }
130    }
131
132    while let Some((element, depth)) = traversal.pop_frontier() {
133        if depth >= options.max_depth {
134            continue;
135        }
136        let next_depth = depth + 1;
137        let reached_limit = match options.direction {
138            TraversalDirection::Outgoing => traversal.visit_outgoing(element, next_depth),
139            TraversalDirection::Incoming => traversal.visit_incoming(element, next_depth),
140            TraversalDirection::Both => {
141                traversal.visit_outgoing(element, next_depth)
142                    || traversal.visit_incoming(element, next_depth)
143            }
144        };
145        if reached_limit {
146            return Ok(traversal.finish());
147        }
148    }
149    Ok(traversal.finish())
150}
151
152/// Mutable state for one traversal.
153struct TraversalState<'graph> {
154    /// Graph being traversed.
155    graph: &'graph GraphProjection,
156    /// Maximum emitted rows.
157    limit: usize,
158    /// Projection-local elements already discovered.
159    visited: BTreeSet<ProjectionElementId>,
160    /// FIFO BFS frontier.
161    queue: VecDeque<(ProjectionElementId, usize)>,
162    /// Materialized result rows.
163    rows: Vec<TraversalRow>,
164}
165
166impl<'graph> TraversalState<'graph> {
167    /// Creates empty traversal state.
168    const fn new(graph: &'graph GraphProjection, limit: usize) -> Self {
169        Self {
170            graph,
171            limit,
172            visited: BTreeSet::new(),
173            queue: VecDeque::new(),
174            rows: Vec::new(),
175        }
176    }
177
178    /// Adds one seed to the BFS roots.
179    fn push_seed(&mut self, local: ProjectionElementId, seed: ElementId, include_start: bool) {
180        if !self.visited.insert(local) {
181            return;
182        }
183        self.queue.push_back((local, 0));
184        if include_start {
185            self.rows.push(TraversalRow {
186                element: seed,
187                depth: 0,
188            });
189        }
190    }
191
192    /// Pops one frontier item.
193    fn pop_frontier(&mut self) -> Option<(ProjectionElementId, usize)> {
194        self.queue.pop_front()
195    }
196
197    /// Returns whether the emitted row limit has been reached.
198    const fn at_limit(&self) -> bool {
199        self.rows.len() == self.limit
200    }
201
202    /// Visits outgoing neighbors from one frontier element.
203    fn visit_outgoing(&mut self, element: ProjectionElementId, depth: usize) -> bool {
204        for neighbor in self.graph.element_successors(element) {
205            if self.push_discovered(neighbor, depth) {
206                return true;
207            }
208        }
209        false
210    }
211
212    /// Visits incoming neighbors from one frontier element.
213    fn visit_incoming(&mut self, element: ProjectionElementId, depth: usize) -> bool {
214        for neighbor in self.graph.element_predecessors(element) {
215            if self.push_discovered(neighbor, depth) {
216                return true;
217            }
218        }
219        false
220    }
221
222    /// Adds one newly discovered neighbor.
223    fn push_discovered(&mut self, neighbor: ProjectionElementId, depth: usize) -> bool {
224        if !self.visited.insert(neighbor) {
225            return false;
226        }
227        self.queue.push_back((neighbor, depth));
228        self.rows.push(TraversalRow {
229            element: self.graph.canonical_element_id(neighbor),
230            depth,
231        });
232        self.at_limit()
233    }
234
235    /// Finishes traversal into a result.
236    fn finish(self) -> TraversalResult {
237        TraversalResult::new(self.rows)
238    }
239}