Skip to main content

uni_query/query/executor/
path_builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Path construction for graph traversals.
5//!
6//! Provides builder pattern for accumulating nodes and edges into Path objects.
7//! Used by traverse operators to construct complete paths through multi-hop traversals.
8
9use crate::types::{Edge, Node, Path, Value};
10
11/// Builder for constructing Path objects through multi-hop traversals.
12///
13/// Maintains a sequence of nodes and edges representing a path through the graph.
14/// Each hop adds an edge and a target node, building up the complete path.
15#[derive(Debug, Clone)]
16pub struct PathBuilder {
17    nodes: Vec<Node>,
18    edges: Vec<Edge>,
19}
20
21impl PathBuilder {
22    /// Create a new path starting with a source node.
23    ///
24    /// # Examples
25    ///
26    /// ```ignore
27    /// let start_node = Node { vid, label, properties };
28    /// let path = PathBuilder::new(start_node);
29    /// ```
30    pub fn new(start_node: Node) -> Self {
31        Self {
32            nodes: vec![start_node],
33            edges: Vec::new(),
34        }
35    }
36
37    /// Create from existing path (for extending in nested traversals).
38    ///
39    /// Useful when continuing a path from a previous traversal result.
40    ///
41    /// # Examples
42    ///
43    /// ```ignore
44    /// let existing_path = Path { nodes: vec![n1, n2], edges: vec![e1] };
45    /// let mut builder = PathBuilder::from_path(existing_path);
46    /// builder.add_hop(e2, n3); // Extends to (n1)-[e1]->(n2)-[e2]->(n3)
47    /// ```
48    pub fn from_path(path: Path) -> Self {
49        Self {
50            nodes: path.nodes,
51            edges: path.edges,
52        }
53    }
54
55    /// Add a hop to the path (edge + target node).
56    ///
57    /// Appends an edge and the node it leads to, extending the path by one hop.
58    ///
59    /// # Examples
60    ///
61    /// ```ignore
62    /// let mut builder = PathBuilder::new(start_node);
63    /// builder.add_hop(edge1, node2); // (start)-[edge1]->(node2)
64    /// builder.add_hop(edge2, node3); // (start)-[edge1]->(node2)-[edge2]->(node3)
65    /// ```
66    pub fn add_hop(&mut self, edge: Edge, target: Node) {
67        self.edges.push(edge);
68        self.nodes.push(target);
69    }
70
71    /// Get the last node in the path (current position).
72    ///
73    /// Returns a reference to the most recently added node, which represents
74    /// the current position in the traversal.
75    ///
76    /// # Panics
77    ///
78    /// Panics if the path has no nodes (should never happen with proper construction).
79    pub fn current_node(&self) -> &Node {
80        self.nodes.last().expect("Path must have at least one node")
81    }
82
83    /// Get path length (number of edges).
84    ///
85    /// Returns the number of hops (edges) in the path. A path with n nodes
86    /// has n-1 edges.
87    pub fn length(&self) -> usize {
88        self.edges.len()
89    }
90
91    /// Build the final Path object.
92    ///
93    /// Consumes the builder and returns the completed Path.
94    pub fn build(self) -> Path {
95        Path {
96            nodes: self.nodes,
97            edges: self.edges,
98        }
99    }
100
101    /// Build as Value::Path for row insertion.
102    ///
103    /// Convenience method that builds the Path and wraps it in Value::Path,
104    /// ready for insertion into a query result row.
105    pub fn build_value(self) -> Value {
106        Value::Path(self.build())
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use std::collections::HashMap;
114    use uni_common::core::id::{Eid, Vid};
115
116    fn make_node(vid: u64, label: &str, name: &str) -> Node {
117        let mut properties = HashMap::new();
118        properties.insert("name".to_string(), Value::String(name.to_string()));
119        Node {
120            vid: Vid::new(vid),
121            labels: vec![label.to_string()],
122            properties,
123        }
124    }
125
126    fn make_edge(eid: u64, edge_type: &str, src: u64, dst: u64) -> Edge {
127        Edge {
128            eid: Eid::new(eid),
129            edge_type: edge_type.to_string(),
130            src: Vid::new(src),
131            dst: Vid::new(dst),
132            properties: HashMap::new(),
133        }
134    }
135
136    #[test]
137    fn test_path_builder_single_hop() {
138        // Build (a)-[r]->(b)
139        let node_a = make_node(1, "Person", "Alice");
140        let node_b = make_node(2, "Person", "Bob");
141        let edge_r = make_edge(100, "KNOWS", 1, 2);
142
143        let mut builder = PathBuilder::new(node_a.clone());
144        builder.add_hop(edge_r.clone(), node_b.clone());
145
146        let path = builder.build();
147
148        assert_eq!(path.nodes.len(), 2);
149        assert_eq!(path.edges.len(), 1);
150        assert_eq!(path.nodes[0].vid, Vid::new(1));
151        assert_eq!(path.nodes[1].vid, Vid::new(2));
152        assert_eq!(path.edges[0].eid, Eid::new(100));
153    }
154
155    #[test]
156    fn test_path_builder_multi_hop() {
157        // Build (a)-[r1]->(b)-[r2]->(c)
158        let node_a = make_node(1, "Person", "Alice");
159        let node_b = make_node(2, "Person", "Bob");
160        let node_c = make_node(3, "Person", "Charlie");
161        let edge_r1 = make_edge(100, "KNOWS", 1, 2);
162        let edge_r2 = make_edge(101, "KNOWS", 2, 3);
163
164        let mut builder = PathBuilder::new(node_a.clone());
165        builder.add_hop(edge_r1.clone(), node_b.clone());
166        builder.add_hop(edge_r2.clone(), node_c.clone());
167
168        let path = builder.build();
169
170        assert_eq!(path.nodes.len(), 3);
171        assert_eq!(path.edges.len(), 2);
172        assert_eq!(path.nodes[0].vid, Vid::new(1));
173        assert_eq!(path.nodes[1].vid, Vid::new(2));
174        assert_eq!(path.nodes[2].vid, Vid::new(3));
175        assert_eq!(path.edges[0].eid, Eid::new(100));
176        assert_eq!(path.edges[1].eid, Eid::new(101));
177    }
178
179    #[test]
180    fn test_path_extension() {
181        // Start with existing path (a)-[r1]->(b), extend to (a)-[r1]->(b)-[r2]->(c)
182        let node_a = make_node(1, "Person", "Alice");
183        let node_b = make_node(2, "Person", "Bob");
184        let node_c = make_node(3, "Person", "Charlie");
185        let edge_r1 = make_edge(100, "KNOWS", 1, 2);
186        let edge_r2 = make_edge(101, "KNOWS", 2, 3);
187
188        // Create initial path
189        let initial_path = Path {
190            nodes: vec![node_a.clone(), node_b.clone()],
191            edges: vec![edge_r1.clone()],
192        };
193
194        // Extend it
195        let mut builder = PathBuilder::from_path(initial_path);
196        builder.add_hop(edge_r2.clone(), node_c.clone());
197
198        let path = builder.build();
199
200        assert_eq!(path.nodes.len(), 3);
201        assert_eq!(path.edges.len(), 2);
202    }
203
204    #[test]
205    fn test_current_node() {
206        let node_a = make_node(1, "Person", "Alice");
207        let node_b = make_node(2, "Person", "Bob");
208        let edge_r = make_edge(100, "KNOWS", 1, 2);
209
210        let mut builder = PathBuilder::new(node_a.clone());
211
212        // Current node should be Alice
213        assert_eq!(builder.current_node().vid, Vid::new(1));
214
215        builder.add_hop(edge_r, node_b.clone());
216
217        // After hop, current node should be Bob
218        assert_eq!(builder.current_node().vid, Vid::new(2));
219    }
220
221    #[test]
222    fn test_path_length() {
223        let node_a = make_node(1, "Person", "Alice");
224        let node_b = make_node(2, "Person", "Bob");
225        let node_c = make_node(3, "Person", "Charlie");
226        let edge_r1 = make_edge(100, "KNOWS", 1, 2);
227        let edge_r2 = make_edge(101, "KNOWS", 2, 3);
228
229        let mut builder = PathBuilder::new(node_a.clone());
230        assert_eq!(builder.length(), 0);
231
232        builder.add_hop(edge_r1, node_b.clone());
233        assert_eq!(builder.length(), 1);
234
235        builder.add_hop(edge_r2, node_c.clone());
236        assert_eq!(builder.length(), 2);
237    }
238
239    #[test]
240    fn test_build_value() {
241        let node_a = make_node(1, "Person", "Alice");
242        let node_b = make_node(2, "Person", "Bob");
243        let edge_r = make_edge(100, "KNOWS", 1, 2);
244
245        let mut builder = PathBuilder::new(node_a);
246        builder.add_hop(edge_r, node_b);
247
248        let value = builder.build_value();
249
250        match value {
251            Value::Path(path) => {
252                assert_eq!(path.nodes.len(), 2);
253                assert_eq!(path.edges.len(), 1);
254            }
255            _ => panic!("Expected Value::Path"),
256        }
257    }
258}