1use arrow::array::{Array, RecordBatch, StringArray};
9use std::collections::{HashMap, HashSet, VecDeque};
10
11#[derive(Debug, Clone)]
16pub struct EdgeSchema {
17 pub source_col: usize,
19 pub target_col: usize,
21 pub predicate_col: Option<usize>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Direction {
28 Forward,
30 Reverse,
32}
33
34pub fn build_adjacency(
39 edges: &RecordBatch,
40 schema: &EdgeSchema,
41 direction: Direction,
42 predicate_filter: Option<&str>,
43) -> HashMap<String, Vec<String>> {
44 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
45
46 if edges.num_rows() == 0 {
47 return adj;
48 }
49
50 let Some(sources) = edges
51 .column(schema.source_col)
52 .as_any()
53 .downcast_ref::<StringArray>()
54 else {
55 return adj;
56 };
57 let Some(targets) = edges
58 .column(schema.target_col)
59 .as_any()
60 .downcast_ref::<StringArray>()
61 else {
62 return adj;
63 };
64
65 let predicates = schema
66 .predicate_col
67 .and_then(|col| edges.column(col).as_any().downcast_ref::<StringArray>());
68
69 for i in 0..edges.num_rows() {
70 if let (Some(filter), Some(pred_col)) = (predicate_filter, predicates)
72 && (pred_col.is_null(i) || pred_col.value(i) != filter)
73 {
74 continue;
75 }
76
77 if sources.is_null(i) || targets.is_null(i) {
78 continue;
79 }
80
81 match direction {
82 Direction::Forward => {
83 adj.entry(sources.value(i).to_string())
84 .or_default()
85 .push(targets.value(i).to_string());
86 }
87 Direction::Reverse => {
88 adj.entry(targets.value(i).to_string())
89 .or_default()
90 .push(sources.value(i).to_string());
91 }
92 }
93 }
94
95 adj
96}
97
98#[derive(Debug, Clone)]
100pub struct TraversalNode {
101 pub id: String,
102 pub depth: usize,
103}
104
105pub fn bfs(
110 start_id: &str,
111 edges: &RecordBatch,
112 schema: &EdgeSchema,
113 direction: Direction,
114 predicate_filter: Option<&str>,
115 max_depth: usize,
116) -> Vec<TraversalNode> {
117 let adj = build_adjacency(edges, schema, direction, predicate_filter);
118
119 let mut visited: HashSet<String> = HashSet::new();
120 visited.insert(start_id.to_string());
121 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
122 queue.push_back((start_id.to_string(), 0));
123 let mut result: Vec<TraversalNode> = Vec::new();
124
125 while let Some((current, depth)) = queue.pop_front() {
126 if depth >= max_depth {
127 continue;
128 }
129 if let Some(neighbors) = adj.get(¤t) {
130 for neighbor in neighbors {
131 if visited.insert(neighbor.clone()) {
132 result.push(TraversalNode {
133 id: neighbor.clone(),
134 depth: depth + 1,
135 });
136 queue.push_back((neighbor.clone(), depth + 1));
137 }
138 }
139 }
140 }
141
142 result
143}
144
145pub fn build_adjacency_from_list(
151 batch: &RecordBatch,
152 id_col: usize,
153 list_col: usize,
154 direction: Direction,
155) -> HashMap<String, Vec<String>> {
156 use arrow::array::ListArray;
157
158 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
159
160 if batch.num_rows() == 0 {
161 return adj;
162 }
163
164 let Some(ids) = batch.column(id_col).as_any().downcast_ref::<StringArray>() else {
165 return adj;
166 };
167 let Some(lists) = batch.column(list_col).as_any().downcast_ref::<ListArray>() else {
168 return adj;
169 };
170
171 for i in 0..batch.num_rows() {
172 if lists.is_null(i) {
173 continue;
174 }
175 let values = lists.value(i);
176 let Some(str_arr) = values.as_any().downcast_ref::<StringArray>() else {
177 continue;
178 };
179
180 let id = ids.value(i);
181 for j in 0..str_arr.len() {
182 if str_arr.is_null(j) {
183 continue;
184 }
185 let dep = str_arr.value(j);
186 match direction {
187 Direction::Forward => {
188 adj.entry(id.to_string()).or_default().push(dep.to_string());
189 }
190 Direction::Reverse => {
191 adj.entry(dep.to_string()).or_default().push(id.to_string());
192 }
193 }
194 }
195 }
196
197 adj
198}
199
200pub fn bfs_with_adjacency(
205 start_id: &str,
206 adj: &HashMap<String, Vec<String>>,
207 max_depth: usize,
208) -> Vec<TraversalNode> {
209 let mut visited: HashSet<String> = HashSet::new();
210 visited.insert(start_id.to_string());
211 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
212 queue.push_back((start_id.to_string(), 0));
213 let mut result: Vec<TraversalNode> = Vec::new();
214
215 while let Some((current, depth)) = queue.pop_front() {
216 if depth >= max_depth {
217 continue;
218 }
219 if let Some(neighbors) = adj.get(¤t) {
220 for neighbor in neighbors {
221 if visited.insert(neighbor.clone()) {
222 result.push(TraversalNode {
223 id: neighbor.clone(),
224 depth: depth + 1,
225 });
226 queue.push_back((neighbor.clone(), depth + 1));
227 }
228 }
229 }
230 }
231
232 result
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use arrow::array::{ListBuilder, StringBuilder};
239 use arrow::datatypes::{DataType, Field, Schema};
240 use std::sync::Arc;
241
242 fn make_edges(triples: &[(&str, &str, &str)]) -> RecordBatch {
244 let schema = Arc::new(Schema::new(vec![
245 Field::new("source", DataType::Utf8, false),
246 Field::new("target", DataType::Utf8, false),
247 Field::new("predicate", DataType::Utf8, false),
248 ]));
249 let sources: Vec<&str> = triples.iter().map(|(s, _, _)| *s).collect();
250 let targets: Vec<&str> = triples.iter().map(|(_, t, _)| *t).collect();
251 let preds: Vec<&str> = triples.iter().map(|(_, _, p)| *p).collect();
252
253 RecordBatch::try_new(
254 schema,
255 vec![
256 Arc::new(StringArray::from(sources)),
257 Arc::new(StringArray::from(targets)),
258 Arc::new(StringArray::from(preds)),
259 ],
260 )
261 .expect("build edges batch")
262 }
263
264 fn edge_schema() -> EdgeSchema {
265 EdgeSchema {
266 source_col: 0,
267 target_col: 1,
268 predicate_col: Some(2),
269 }
270 }
271
272 #[test]
273 fn test_build_adjacency_forward() {
274 let edges = make_edges(&[
275 ("A", "B", "calls"),
276 ("A", "C", "calls"),
277 ("B", "C", "calls"),
278 ]);
279 let adj = build_adjacency(&edges, &edge_schema(), Direction::Forward, Some("calls"));
280
281 assert_eq!(adj.get("A").unwrap().len(), 2);
282 assert_eq!(adj.get("B").unwrap().len(), 1);
283 assert!(adj.get("C").is_none());
284 }
285
286 #[test]
287 fn test_build_adjacency_reverse() {
288 let edges = make_edges(&[
289 ("A", "B", "calls"),
290 ("A", "C", "calls"),
291 ("B", "C", "calls"),
292 ]);
293 let adj = build_adjacency(&edges, &edge_schema(), Direction::Reverse, Some("calls"));
294
295 assert!(adj.get("A").is_none()); assert_eq!(adj.get("B").unwrap(), &["A"]);
297 assert_eq!(adj.get("C").unwrap().len(), 2); }
299
300 #[test]
301 fn test_build_adjacency_predicate_filter() {
302 let edges = make_edges(&[
303 ("A", "B", "calls"),
304 ("A", "C", "tests"),
305 ("B", "C", "calls"),
306 ]);
307 let adj = build_adjacency(&edges, &edge_schema(), Direction::Forward, Some("calls"));
308
309 assert_eq!(adj.get("A").unwrap(), &["B"]); }
311
312 #[test]
313 fn test_build_adjacency_no_filter() {
314 let edges = make_edges(&[
315 ("A", "B", "calls"),
316 ("A", "C", "tests"),
317 ("B", "C", "calls"),
318 ]);
319 let adj = build_adjacency(&edges, &edge_schema(), Direction::Forward, None);
320
321 assert_eq!(adj.get("A").unwrap().len(), 2); }
323
324 #[test]
325 fn test_bfs_depth_1() {
326 let edges = make_edges(&[("A", "B", "dep"), ("A", "C", "dep"), ("B", "D", "dep")]);
327 let result = bfs(
328 "A",
329 &edges,
330 &edge_schema(),
331 Direction::Forward,
332 Some("dep"),
333 1,
334 );
335
336 assert_eq!(result.len(), 2); assert!(result.iter().all(|n| n.depth == 1));
338 }
339
340 #[test]
341 fn test_bfs_depth_2() {
342 let edges = make_edges(&[("A", "B", "dep"), ("B", "C", "dep"), ("C", "D", "dep")]);
343 let result = bfs(
344 "A",
345 &edges,
346 &edge_schema(),
347 Direction::Forward,
348 Some("dep"),
349 2,
350 );
351
352 assert_eq!(result.len(), 2); assert_eq!(result[0].id, "B");
354 assert_eq!(result[0].depth, 1);
355 assert_eq!(result[1].id, "C");
356 assert_eq!(result[1].depth, 2);
357 }
358
359 #[test]
360 fn test_bfs_depth_0() {
361 let edges = make_edges(&[("A", "B", "dep")]);
362 let result = bfs(
363 "A",
364 &edges,
365 &edge_schema(),
366 Direction::Forward,
367 Some("dep"),
368 0,
369 );
370 assert!(result.is_empty());
371 }
372
373 #[test]
374 fn test_bfs_reverse() {
375 let edges = make_edges(&[("A", "C", "dep"), ("B", "C", "dep")]);
376 let result = bfs(
377 "C",
378 &edges,
379 &edge_schema(),
380 Direction::Reverse,
381 Some("dep"),
382 1,
383 );
384
385 assert_eq!(result.len(), 2); }
387
388 #[test]
389 fn test_bfs_cycle_safe() {
390 let edges = make_edges(&[("A", "B", "dep"), ("B", "A", "dep")]);
391 let result = bfs(
392 "A",
393 &edges,
394 &edge_schema(),
395 Direction::Forward,
396 Some("dep"),
397 10,
398 );
399
400 assert_eq!(result.len(), 1); }
402
403 #[test]
404 fn test_bfs_empty_edges() {
405 let edges = make_edges(&[]);
406 let result = bfs("A", &edges, &edge_schema(), Direction::Forward, None, 5);
407 assert!(result.is_empty());
408 }
409
410 #[test]
411 fn test_build_adjacency_from_list() {
412 let schema = Arc::new(Schema::new(vec![
414 Field::new("id", DataType::Utf8, false),
415 Field::new(
416 "depends_on",
417 DataType::List(Arc::new(Field::new("item", DataType::Utf8, true))),
418 false,
419 ),
420 ]));
421
422 let ids = StringArray::from(vec!["A", "B", "C"]);
423 let mut list_builder = ListBuilder::new(StringBuilder::new());
424 list_builder.values().append_value("B");
426 list_builder.values().append_value("C");
427 list_builder.append(true);
428 list_builder.values().append_value("C");
430 list_builder.append(true);
431 list_builder.append(true);
433
434 let batch =
435 RecordBatch::try_new(schema, vec![Arc::new(ids), Arc::new(list_builder.finish())])
436 .unwrap();
437
438 let adj = build_adjacency_from_list(&batch, 0, 1, Direction::Forward);
439 assert_eq!(adj.get("A").unwrap().len(), 2);
440 assert_eq!(adj.get("B").unwrap().len(), 1);
441 assert!(adj.get("C").is_none()); let adj_rev = build_adjacency_from_list(&batch, 0, 1, Direction::Reverse);
445 assert!(adj_rev.get("A").is_none()); assert_eq!(adj_rev.get("B").unwrap(), &["A"]);
447 assert_eq!(adj_rev.get("C").unwrap().len(), 2); }
449
450 #[test]
451 fn test_bfs_with_adjacency() {
452 let mut adj = HashMap::new();
453 adj.insert("A".to_string(), vec!["B".to_string(), "C".to_string()]);
454 adj.insert("B".to_string(), vec!["D".to_string()]);
455
456 let result = bfs_with_adjacency("A", &adj, 2);
457 assert_eq!(result.len(), 3); }
459}