velesdb_core/collection/core/graph_api.rs
1//! Graph API methods for Collection (EPIC-015 US-001).
2//!
3//! Exposes Knowledge Graph operations on Collection for use by
4//! Tauri plugin, REST API, and other consumers.
5
6use crate::collection::graph::GraphEdge;
7use crate::collection::types::Collection;
8use crate::error::Result;
9
10/// Traversal result for graph operations.
11#[derive(Debug, Clone)]
12pub struct TraversalResult {
13 /// Target node ID reached.
14 pub target_id: u64,
15 /// Depth of traversal.
16 pub depth: u32,
17 /// Path taken (node IDs).
18 pub path: Vec<u64>,
19}
20
21impl Collection {
22 /// Adds an edge to the collection's knowledge graph.
23 ///
24 /// # Arguments
25 ///
26 /// * `edge` - The edge to add (id, source, target, label, properties)
27 ///
28 /// # Errors
29 ///
30 /// Returns `Error::EdgeExists` if an edge with the same ID already exists.
31 ///
32 /// # Example
33 ///
34 /// ```rust,ignore
35 /// use velesdb_core::collection::graph::GraphEdge;
36 ///
37 /// let edge = GraphEdge::new(1, 100, 200, "KNOWS")?;
38 /// collection.add_edge(edge)?;
39 /// ```
40 pub fn add_edge(&self, edge: GraphEdge) -> Result<()> {
41 self.edge_store.write().add_edge(edge)
42 }
43
44 /// Gets all edges from the collection's knowledge graph.
45 ///
46 /// Note: This iterates through all stored edges. For large graphs,
47 /// consider using `get_edges_by_label` or `get_outgoing_edges` for
48 /// more targeted queries.
49 ///
50 /// # Returns
51 ///
52 /// Vector of all edges in the graph (cloned).
53 #[must_use]
54 pub fn get_all_edges(&self) -> Vec<GraphEdge> {
55 let store = self.edge_store.read();
56 store.all_edges().into_iter().cloned().collect()
57 }
58
59 /// Gets edges filtered by label.
60 ///
61 /// # Arguments
62 ///
63 /// * `label` - The edge label (relationship type) to filter by
64 ///
65 /// # Returns
66 ///
67 /// Vector of edges with the specified label (cloned).
68 #[must_use]
69 pub fn get_edges_by_label(&self, label: &str) -> Vec<GraphEdge> {
70 self.edge_store
71 .read()
72 .get_edges_by_label(label)
73 .into_iter()
74 .cloned()
75 .collect()
76 }
77
78 /// Gets outgoing edges from a specific node.
79 ///
80 /// # Arguments
81 ///
82 /// * `node_id` - The source node ID
83 ///
84 /// # Returns
85 ///
86 /// Vector of edges originating from the specified node (cloned).
87 #[must_use]
88 pub fn get_outgoing_edges(&self, node_id: u64) -> Vec<GraphEdge> {
89 self.edge_store
90 .read()
91 .get_outgoing(node_id)
92 .into_iter()
93 .cloned()
94 .collect()
95 }
96
97 /// Gets incoming edges to a specific node.
98 ///
99 /// # Arguments
100 ///
101 /// * `node_id` - The target node ID
102 ///
103 /// # Returns
104 ///
105 /// Vector of edges pointing to the specified node (cloned).
106 #[must_use]
107 pub fn get_incoming_edges(&self, node_id: u64) -> Vec<GraphEdge> {
108 self.edge_store
109 .read()
110 .get_incoming(node_id)
111 .into_iter()
112 .cloned()
113 .collect()
114 }
115
116 /// Traverses the graph using BFS from a source node.
117 ///
118 /// # Arguments
119 ///
120 /// * `source` - Starting node ID
121 /// * `max_depth` - Maximum traversal depth
122 /// * `rel_types` - Optional filter by relationship types
123 /// * `limit` - Maximum number of results
124 ///
125 /// # Returns
126 ///
127 /// Vector of traversal results with target nodes and paths.
128 ///
129 /// # Errors
130 ///
131 /// Returns an error if traversal fails.
132 pub fn traverse_bfs(
133 &self,
134 source: u64,
135 max_depth: u32,
136 rel_types: Option<&[&str]>,
137 limit: usize,
138 ) -> Result<Vec<TraversalResult>> {
139 use std::collections::{HashSet, VecDeque};
140
141 let store = self.edge_store.read();
142 let mut visited = HashSet::new();
143 let mut queue = VecDeque::new();
144 let mut results = Vec::new();
145
146 visited.insert(source);
147 queue.push_back((source, 0u32, vec![source]));
148
149 while let Some((node, depth, path)) = queue.pop_front() {
150 if results.len() >= limit {
151 break;
152 }
153
154 if depth >= max_depth {
155 continue;
156 }
157
158 for edge in store.get_outgoing(node) {
159 // Filter by relationship type if specified
160 if let Some(types) = rel_types {
161 if !types.contains(&edge.label()) {
162 continue;
163 }
164 }
165
166 let target = edge.target();
167 if !visited.contains(&target) {
168 visited.insert(target);
169 let mut new_path = path.clone();
170 new_path.push(target);
171
172 results.push(TraversalResult {
173 target_id: target,
174 depth: depth + 1,
175 path: new_path.clone(),
176 });
177
178 if results.len() < limit {
179 queue.push_back((target, depth + 1, new_path));
180 }
181 }
182 }
183 }
184
185 Ok(results)
186 }
187
188 /// Traverses the graph using DFS from a source node.
189 ///
190 /// # Arguments
191 ///
192 /// * `source` - Starting node ID
193 /// * `max_depth` - Maximum traversal depth
194 /// * `rel_types` - Optional filter by relationship types
195 /// * `limit` - Maximum number of results
196 ///
197 /// # Returns
198 ///
199 /// Vector of traversal results with target nodes and paths.
200 ///
201 /// # Errors
202 ///
203 /// Returns an error if traversal fails.
204 pub fn traverse_dfs(
205 &self,
206 source: u64,
207 max_depth: u32,
208 rel_types: Option<&[&str]>,
209 limit: usize,
210 ) -> Result<Vec<TraversalResult>> {
211 use std::collections::HashSet;
212
213 let store = self.edge_store.read();
214 let mut visited = HashSet::new();
215 let mut stack = Vec::new();
216 let mut results = Vec::new();
217
218 visited.insert(source);
219 stack.push((source, 0u32, vec![source]));
220
221 while let Some((node, depth, path)) = stack.pop() {
222 if results.len() >= limit {
223 break;
224 }
225
226 if depth >= max_depth {
227 continue;
228 }
229
230 for edge in store.get_outgoing(node) {
231 // Filter by relationship type if specified
232 if let Some(types) = rel_types {
233 if !types.contains(&edge.label()) {
234 continue;
235 }
236 }
237
238 let target = edge.target();
239 if !visited.contains(&target) {
240 visited.insert(target);
241 let mut new_path = path.clone();
242 new_path.push(target);
243
244 results.push(TraversalResult {
245 target_id: target,
246 depth: depth + 1,
247 path: new_path.clone(),
248 });
249
250 if results.len() < limit {
251 stack.push((target, depth + 1, new_path));
252 }
253 }
254 }
255 }
256
257 Ok(results)
258 }
259
260 /// Gets the in-degree and out-degree of a node.
261 ///
262 /// # Arguments
263 ///
264 /// * `node_id` - The node ID
265 ///
266 /// # Returns
267 ///
268 /// Tuple of (`in_degree`, `out_degree`).
269 #[must_use]
270 pub fn get_node_degree(&self, node_id: u64) -> (usize, usize) {
271 let store = self.edge_store.read();
272 let in_degree = store.get_incoming(node_id).len();
273 let out_degree = store.get_outgoing(node_id).len();
274 (in_degree, out_degree)
275 }
276
277 /// Removes an edge from the graph by ID.
278 ///
279 /// # Arguments
280 ///
281 /// * `edge_id` - The edge ID to remove
282 ///
283 /// # Returns
284 ///
285 /// `true` if the edge existed and was removed, `false` if it didn't exist.
286 #[must_use]
287 pub fn remove_edge(&self, edge_id: u64) -> bool {
288 let mut store = self.edge_store.write();
289 if store.contains_edge(edge_id) {
290 store.remove_edge(edge_id);
291 true
292 } else {
293 false
294 }
295 }
296
297 /// Returns the total number of edges in the graph.
298 #[must_use]
299 pub fn edge_count(&self) -> usize {
300 let store = self.edge_store.read();
301 store.len()
302 }
303}