1use std::collections::{HashMap, HashSet};
69use std::fmt;
70use std::hash::{Hash, Hasher};
71use std::sync::Arc;
72
73use datafusion::arrow::array::{
74 Array, ArrayBuilder, ArrayRef, Int64Array, Int64Builder, ListArray, UInt32Array,
75};
76use datafusion::arrow::buffer::OffsetBuffer;
77use datafusion::arrow::compute::{concat, take};
78use datafusion::arrow::datatypes::{DataType, Field, Schema, SchemaRef};
79use datafusion::arrow::record_batch::RecordBatch;
80use datafusion::common::{DFSchema, DFSchemaRef, ScalarValue};
81use datafusion::error::{DataFusionError, Result};
82use datafusion::logical_expr::{Expr, LogicalPlan, UserDefinedLogicalNodeCore};
83use ursa_core::{IdMap, Topology};
84
85use super::node::{check_vertices, endpoint_id_type};
86use super::pattern::{Direction, Mode, Quantifier, Selector};
87
88pub const START: &str = "__start";
90pub const END: &str = "__end";
92pub const LENGTH: &str = "__length";
94pub const VERTICES: &str = "__vertices";
96pub const EDGES: &str = "__edges";
98pub const ELEMENTS: &str = "__element_id";
100
101pub const FUNCTIONS: &[(&str, &str)] = &[
103 ("path_length", LENGTH),
104 ("vertices", VERTICES),
105 ("edges", EDGES),
106 ("element_id", ELEMENTS),
107];
108
109pub const MAX_PATHS: usize = 10_000_000;
111
112const BATCH_ROWS: usize = 8192;
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
117pub struct Search {
118 pub direction: Direction,
119 pub quantifier: Quantifier,
120 pub selector: Selector,
121 pub mode: Mode,
122}
123
124impl fmt::Display for Search {
125 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
126 match self.selector {
127 Selector::All => {}
128 Selector::Shortest(1) => write!(f, "ANY SHORTEST ")?,
129 Selector::Shortest(k) => write!(f, "SHORTEST {k} ")?,
130 Selector::AllShortest => write!(f, "ALL SHORTEST ")?,
131 }
132 let mode = match self.mode {
133 Mode::Walk => "WALK",
134 Mode::Trail => "TRAIL",
135 Mode::Simple => "SIMPLE",
136 Mode::Acyclic => "ACYCLIC",
137 };
138 let arrow = match self.direction {
139 Direction::Right => "-[]->",
140 Direction::Left => "<-[]-",
141 Direction::Any => "-[]-",
142 };
143 let max = self
144 .quantifier
145 .max
146 .map(|m| m.to_string())
147 .unwrap_or_default();
148 write!(f, "{mode} {arrow}{{{},{max}}}", self.quantifier.min)
149 }
150}
151
152#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd)]
154pub struct PathColumns {
155 pub vertices: bool,
156 pub edges: bool,
157 pub elements: bool,
158}
159
160pub fn schema(id_type: &DataType, columns: PathColumns) -> Schema {
162 let list = |t: DataType| DataType::List(Arc::new(Field::new_list_field(t, false)));
163 let mut fields = vec![
164 Field::new(START, id_type.clone(), false),
165 Field::new(END, id_type.clone(), false),
166 Field::new(LENGTH, DataType::Int64, false),
167 ];
168 if columns.vertices {
169 fields.push(Field::new(VERTICES, list(id_type.clone()), false));
170 }
171 if columns.edges {
172 fields.push(Field::new(EDGES, list(DataType::Int64), false));
173 }
174 if columns.elements {
175 fields.push(Field::new(ELEMENTS, list(DataType::Int64), false));
176 }
177 Schema::new(fields)
178}
179
180#[derive(Debug, Clone)]
186pub struct PathSearchNode {
187 pub graph: String,
188 pub search: Search,
189 pub columns: PathColumns,
190 pub input: Arc<LogicalPlan>,
191 pub vertices: Option<Arc<LogicalPlan>>,
194 pub starts: Arc<LogicalPlan>,
195 pub targets: Option<Arc<LogicalPlan>>,
198 pub known_only: bool,
203 pub source: Expr,
204 pub destination: Expr,
205 pub id_type: DataType,
206 schema: DFSchemaRef,
207}
208
209impl PathSearchNode {
210 #[allow(clippy::too_many_arguments)]
211 pub fn try_new(
212 graph: String,
213 search: Search,
214 columns: PathColumns,
215 input: Arc<LogicalPlan>,
216 vertices: Option<Arc<LogicalPlan>>,
217 starts: Arc<LogicalPlan>,
218 targets: Option<Arc<LogicalPlan>>,
219 known_only: bool,
220 source: Expr,
221 destination: Expr,
222 ) -> Result<Self> {
223 let id_type = endpoint_id_type(&graph, &input, &source, &destination)?;
224 check_vertices(&graph, vertices.as_deref())?;
225 if columns.elements && id_type != DataType::Int64 {
226 return Err(DataFusionError::Plan(format!(
227 "in GRAPH_TABLE: element_id(p) lists node keys and edge rows together, so it needs integer node keys; graph '{graph}' has {id_type} keys: use vertices(p) and edges(p)"
228 )));
229 }
230 let schema = Arc::new(DFSchema::try_from(schema(&id_type, columns))?);
231 Ok(Self {
232 graph,
233 search,
234 columns,
235 input,
236 vertices,
237 starts,
238 targets,
239 known_only,
240 source,
241 destination,
242 id_type,
243 schema,
244 })
245 }
246
247 pub(crate) fn endpoint_indices(&self) -> Result<(usize, usize)> {
248 let index = |e: &Expr| -> Result<usize> {
249 match e {
250 Expr::Column(c) => self.input.schema().index_of_column(c),
251 other => Err(DataFusionError::Internal(format!(
252 "a path search's endpoint must be a column; got {other}"
253 ))),
254 }
255 };
256 Ok((index(&self.source)?, index(&self.destination)?))
257 }
258}
259
260impl PartialEq for PathSearchNode {
261 fn eq(&self, other: &Self) -> bool {
262 self.graph == other.graph
263 && self.search == other.search
264 && self.columns == other.columns
265 && self.input == other.input
266 && self.vertices == other.vertices
267 && self.starts == other.starts
268 && self.targets == other.targets
269 && self.known_only == other.known_only
270 && self.source == other.source
271 && self.destination == other.destination
272 }
273}
274
275impl Eq for PathSearchNode {}
276
277impl Hash for PathSearchNode {
278 fn hash<H: Hasher>(&self, state: &mut H) {
279 self.graph.hash(state);
280 self.search.hash(state);
281 self.columns.hash(state);
282 self.input.hash(state);
283 self.vertices.hash(state);
284 self.starts.hash(state);
285 self.targets.hash(state);
286 self.known_only.hash(state);
287 }
288}
289
290impl PartialOrd for PathSearchNode {
291 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
292 (&self.graph, &self.search, &self.columns)
293 .partial_cmp(&(&other.graph, &other.search, &other.columns))
294 .filter(|o| o.is_ne())
295 }
296}
297
298impl UserDefinedLogicalNodeCore for PathSearchNode {
299 fn name(&self) -> &str {
300 "PathSearch"
301 }
302
303 fn inputs(&self) -> Vec<&LogicalPlan> {
304 let mut inputs = vec![self.input.as_ref()];
305 inputs.extend(self.vertices.as_deref());
306 inputs.push(&self.starts);
307 inputs.extend(self.targets.as_deref());
308 inputs
309 }
310
311 fn schema(&self) -> &DFSchemaRef {
312 &self.schema
313 }
314
315 fn expressions(&self) -> Vec<Expr> {
316 vec![]
317 }
318
319 fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
320 write!(
321 f,
322 "PathSearch: graph={}, search={}{}",
323 self.graph,
324 self.search,
325 if self.targets.is_some() {
326 ", targets"
327 } else {
328 ""
329 }
330 )
331 }
332
333 fn with_exprs_and_inputs(&self, _exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> Result<Self> {
334 let expected =
335 2 + usize::from(self.vertices.is_some()) + usize::from(self.targets.is_some());
336 if inputs.len() != expected {
337 return Err(DataFusionError::Internal(format!(
338 "PathSearch has {expected} inputs; got {}",
339 inputs.len()
340 )));
341 }
342 let mut inputs = inputs.into_iter().map(Arc::new);
343 let input = inputs.next().expect("checked above");
344 let vertices = self.vertices.as_ref().and_then(|_| inputs.next());
345 let starts = inputs.next().expect("checked above");
346 let targets = self.targets.as_ref().and_then(|_| inputs.next());
347 Ok(Self {
348 input,
349 vertices,
350 starts,
351 targets,
352 ..self.clone()
353 })
354 }
355
356 fn necessary_children_exprs(&self, _output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
359 let (source, destination) = self.endpoint_indices().ok()?;
360 let mut children = vec![vec![source, destination]];
361 if self.vertices.is_some() {
362 children.push(vec![0]);
363 }
364 children.push(vec![0]);
365 if self.targets.is_some() {
366 children.push(vec![0]);
367 }
368 Some(children)
369 }
370
371 fn prevent_predicate_push_down_columns(&self) -> std::collections::HashSet<String> {
373 self.schema
374 .fields()
375 .iter()
376 .map(|f| f.name().clone())
377 .collect()
378 }
379}
380
381#[derive(Debug, Clone, Copy)]
384struct Entry {
385 node: u32,
386 parent: u32,
387 edge: u32,
388 len: u32,
389}
390
391const ROOT: u32 = u32::MAX;
392
393enum States {
396 Dense {
397 seen: Vec<(u32, u32)>,
398 touched: Vec<usize>,
399 width: usize,
400 },
401 Sparse(HashMap<(u32, u32), (u32, u32)>),
402}
403
404impl States {
405 fn new(n_nodes: usize, min: u32) -> Self {
406 let width = min as usize + 1;
407 match n_nodes.checked_mul(width) {
408 Some(n) if n <= 1 << 26 => States::Dense {
409 seen: vec![(0, 0); n],
410 touched: Vec::new(),
411 width,
412 },
413 _ => States::Sparse(HashMap::new()),
414 }
415 }
416
417 fn get(&mut self, node: u32, c: u32) -> &mut (u32, u32) {
418 match self {
419 States::Dense {
420 seen,
421 touched,
422 width,
423 } => {
424 let i = node as usize * *width + c as usize;
425 if seen[i].0 == 0 {
426 touched.push(i);
427 }
428 &mut seen[i]
429 }
430 States::Sparse(map) => map.entry((node, c)).or_insert((0, 0)),
431 }
432 }
433
434 fn clear(&mut self) {
435 match self {
436 States::Dense { seen, touched, .. } => {
437 for i in touched.drain(..) {
438 seen[i] = (0, 0);
439 }
440 }
441 States::Sparse(map) => map.clear(),
442 }
443 }
444}
445
446fn steps(topology: &Topology, direction: Direction, u: u32, out: &mut Vec<(u32, u32)>) {
449 out.clear();
450 fn pairs(a: &ursa_core::Adjacency, u: u32) -> impl Iterator<Item = (u32, u32)> + '_ {
451 a.neighbors(u)
452 .iter()
453 .copied()
454 .zip(a.edge_ids(u).iter().copied())
455 }
456 match direction {
457 Direction::Right => out.extend(pairs(topology.out(), u)),
458 Direction::Left => out.extend(pairs(topology.incoming(), u)),
459 Direction::Any => {
460 let forward: Vec<(u32, u32)> = pairs(topology.out(), u).collect();
461 let backward: Vec<(u32, u32)> = pairs(topology.incoming(), u)
463 .filter(|&(w, _)| w != u)
464 .collect();
465 let (mut i, mut j) = (0, 0);
466 while i < forward.len() || j < backward.len() {
467 let take_forward =
468 j == backward.len() || (i < forward.len() && forward[i].1 < backward[j].1);
469 if take_forward {
470 out.push(forward[i]);
471 i += 1;
472 } else {
473 out.push(backward[j]);
474 j += 1;
475 }
476 }
477 }
478 }
479}
480
481fn allowed(entries: &[Entry], at: u32, w: u32, edge: u32, mode: Mode, start: u32) -> bool {
484 let chain = || {
485 let mut i = at;
486 std::iter::from_fn(move || {
487 if i == ROOT {
488 return None;
489 }
490 let e = entries[i as usize];
491 i = e.parent;
492 Some(e)
493 })
494 };
495 match mode {
496 Mode::Walk => true,
497 Mode::Trail => chain().all(|e| e.parent == ROOT || e.edge != edge),
498 Mode::Acyclic => chain().all(|e| e.node != w),
499 Mode::Simple => {
500 let last = entries[at as usize];
501 if last.len > 0 && last.node == start {
502 return false; }
504 w == start && chain().all(|e| e.node != w || e.parent == ROOT)
505 || chain().all(|e| e.node != w)
506 }
507 }
508}
509
510struct Rows<'a> {
512 starts: &'a ArrayRef,
513 columns: PathColumns,
514 schema: SchemaRef,
515 start_rows: Vec<u32>,
516 ends: Vec<u32>,
517 lengths: Vec<i64>,
518 vertex_offsets: Vec<i32>,
519 vertex_values: Vec<u32>,
520 edge_offsets: Vec<i32>,
521 edge_values: Vec<i64>,
522 batches: Vec<RecordBatch>,
523 values: ArrayRef,
526 edge_row: &'a dyn Fn(u32) -> u64,
528}
529
530impl<'a> Rows<'a> {
531 fn new(
532 ids: &'a IdMap,
533 starts: &'a ArrayRef,
534 columns: PathColumns,
535 schema: SchemaRef,
536 edge_row: &'a dyn Fn(u32) -> u64,
537 ) -> Result<Self> {
538 let values = if ids.is_empty() {
539 Arc::clone(starts)
540 } else {
541 concat(&[starts.as_ref(), ids.user_id_array().as_ref()])?
542 };
543 Ok(Self {
544 starts,
545 columns,
546 schema,
547 start_rows: Vec::new(),
548 ends: Vec::new(),
549 lengths: Vec::new(),
550 vertex_offsets: vec![0],
551 vertex_values: Vec::new(),
552 edge_offsets: vec![0],
553 edge_values: Vec::new(),
554 batches: Vec::new(),
555 values,
556 edge_row,
557 })
558 }
559
560 fn dense(&self, d: u32) -> u32 {
562 (self.starts.len() as u32) + d
563 }
564
565 fn push(&mut self, start: u32, nodes: &[u32], edges: &[u32]) -> Result<()> {
568 self.start_rows.push(start);
569 self.ends.push(*nodes.last().expect("a path has a node"));
570 self.lengths.push(edges.len() as i64);
571 if self.columns.vertices || self.columns.elements {
572 self.vertex_values.extend_from_slice(nodes);
573 self.vertex_offsets.push(self.vertex_values.len() as i32);
574 }
575 if self.columns.edges || self.columns.elements {
576 let edge_row = self.edge_row;
577 self.edge_values
578 .extend(edges.iter().map(|&e| edge_row(e) as i64));
579 self.edge_offsets.push(self.edge_values.len() as i32);
580 }
581 if self.start_rows.len() >= BATCH_ROWS {
582 self.flush()?;
583 }
584 Ok(())
585 }
586
587 fn flush(&mut self) -> Result<()> {
588 if self.start_rows.is_empty() {
589 return Ok(());
590 }
591 let start = take(
592 self.starts.as_ref(),
593 &UInt32Array::from(std::mem::take(&mut self.start_rows)),
594 None,
595 )?;
596 let end = take(
597 self.values.as_ref(),
598 &UInt32Array::from(std::mem::take(&mut self.ends)),
599 None,
600 )?;
601 let mut columns: Vec<ArrayRef> = vec![
602 start,
603 end,
604 Arc::new(Int64Array::from(std::mem::take(&mut self.lengths))),
605 ];
606 let vertex_offsets = std::mem::replace(&mut self.vertex_offsets, vec![0]);
607 let vertex_values = take(
608 self.values.as_ref(),
609 &UInt32Array::from(std::mem::take(&mut self.vertex_values)),
610 None,
611 )?;
612 let edge_offsets = std::mem::replace(&mut self.edge_offsets, vec![0]);
613 let edge_values = std::mem::take(&mut self.edge_values);
614 let field = |i: usize| match self.schema.field(i).data_type() {
615 DataType::List(f) => Arc::clone(f),
616 _ => unreachable!("a list column"),
617 };
618 let mut i = 3;
619 if self.columns.vertices {
620 columns.push(Arc::new(ListArray::try_new(
621 field(i),
622 OffsetBuffer::new(vertex_offsets.clone().into()),
623 Arc::clone(&vertex_values),
624 None,
625 )?));
626 i += 1;
627 }
628 if self.columns.edges {
629 columns.push(Arc::new(ListArray::try_new(
630 field(i),
631 OffsetBuffer::new(edge_offsets.clone().into()),
632 Arc::new(Int64Array::from(edge_values.clone())),
633 None,
634 )?));
635 i += 1;
636 }
637 if self.columns.elements {
638 let keys = vertex_values
639 .as_any()
640 .downcast_ref::<Int64Array>()
641 .ok_or_else(|| DataFusionError::Internal("element_id needs Int64 keys".into()))?;
642 let mut values = Int64Builder::with_capacity(keys.len() + edge_values.len());
643 let mut offsets = vec![0i32];
644 for p in 0..vertex_offsets.len() - 1 {
645 let (v0, v1) = (vertex_offsets[p] as usize, vertex_offsets[p + 1] as usize);
646 let e0 = edge_offsets[p] as usize;
647 for k in v0..v1 {
648 values.append_value(keys.value(k));
649 if k + 1 < v1 {
650 values.append_value(edge_values[e0 + (k - v0)]);
651 }
652 }
653 offsets.push(values.len() as i32);
654 }
655 columns.push(Arc::new(ListArray::try_new(
656 field(i),
657 OffsetBuffer::new(offsets.into()),
658 Arc::new(values.finish()),
659 None,
660 )?));
661 }
662 self.batches
663 .push(RecordBatch::try_new(Arc::clone(&self.schema), columns)?);
664 Ok(())
665 }
666}
667
668struct Goals {
671 slots: HashMap<u32, u32>,
672 unknown: HashSet<ScalarValue>,
673}
674
675impl Goals {
676 fn new(ids: &IdMap, targets: &ArrayRef) -> Result<Self> {
677 let dense = ids
678 .dense_from_array(targets.as_ref())
679 .map_err(|e| DataFusionError::Execution(format!("in GRAPH_TABLE: {e}")))?;
680 let mut slots = HashMap::new();
681 let mut unknown = HashSet::new();
682 for (row, d) in dense.iter().enumerate() {
683 if targets.is_null(row) {
684 continue;
685 }
686 match d {
687 Some(d) => {
688 let next = slots.len() as u32;
689 slots.entry(*d).or_insert(next);
690 }
691 None => {
692 unknown.insert(ScalarValue::try_from_array(targets.as_ref(), row)?);
693 }
694 }
695 }
696 Ok(Self { slots, unknown })
697 }
698}
699
700struct Walker<'a> {
702 topology: &'a Topology,
703 search: &'a Search,
704 restricted: bool,
705 pruned: bool,
706 states: States,
707 entries: Vec<Entry>,
708 next: Vec<(u32, u32)>,
709 total: usize,
711 expanded: usize,
713 slots: Option<&'a HashMap<u32, u32>>,
716 reach: Reach,
717}
718
719#[derive(Default)]
726struct Reach {
727 pushed: Vec<u32>,
729 touched: Vec<u32>,
730 unfilled: usize,
732 longest: u32,
734}
735
736impl Reach {
737 fn reset(&mut self, n: usize) {
738 if self.pushed.len() != n {
739 self.pushed = vec![0; n];
740 }
741 for slot in self.touched.drain(..) {
742 self.pushed[slot as usize] = 0;
743 }
744 self.unfilled = n;
745 self.longest = 0;
746 }
747
748 fn extends(&self, len: u32, selector: Selector) -> bool {
750 self.unfilled > 0 || (selector == Selector::AllShortest && len < self.longest)
751 }
752}
753
754impl Walker<'_> {
755 fn extend(&mut self, at: u32, start: u32) -> Result<()> {
757 let entry = self.entries[at as usize];
758 let min = self.search.quantifier.min;
759 self.expanded += 1;
760 steps(
761 self.topology,
762 self.search.direction,
763 entry.node,
764 &mut self.next,
765 );
766 for &(w, edge) in self.next.iter() {
767 if self.restricted && !allowed(&self.entries, at, w, edge, self.search.mode, start) {
768 continue;
769 }
770 let len = entry.len + 1;
771 if self.pruned {
772 let state = self.states.get(w, len.min(min));
773 let accept = match self.search.selector {
774 Selector::All => true,
775 Selector::Shortest(k) => state.0 < k,
776 Selector::AllShortest => state.0 == 0 || state.1 == len,
777 };
778 if !accept {
779 continue;
780 }
781 if state.0 == 0 {
782 state.1 = len;
783 }
784 state.0 += 1;
785 }
786 self.entries.push(Entry {
787 node: w,
788 parent: at,
789 edge,
790 len,
791 });
792 let listed = len >= min && !(self.search.mode == Mode::Acyclic && w == start);
794 if let (true, true, Some(&slot)) =
795 (self.pruned, listed, self.slots.and_then(|s| s.get(&w)))
796 {
797 let reach = &mut self.reach;
798 let pushed = &mut reach.pushed[slot as usize];
799 if *pushed == 0 {
800 reach.touched.push(slot);
801 reach.longest = reach.longest.max(len);
802 }
803 *pushed += 1;
804 let filled = match self.search.selector {
805 Selector::All => false,
806 Selector::Shortest(k) => *pushed == k,
807 Selector::AllShortest => *pushed == 1,
808 };
809 if filled {
810 reach.unfilled -= 1;
811 }
812 }
813 if self.entries.len() + self.total > MAX_PATHS {
814 return Err(DataFusionError::Execution(format!(
815 "in GRAPH_TABLE: the path search reached {MAX_PATHS} paths; give the quantified edge a lower upper bound, or keep fewer paths with ANY SHORTEST"
816 )));
817 }
818 }
819 Ok(())
820 }
821}
822
823#[allow(clippy::too_many_arguments)]
831pub fn run(
832 topology: &Topology,
833 ids: &IdMap,
834 edge_row: &dyn Fn(u32) -> u64,
835 search: &Search,
836 starts: &ArrayRef,
837 targets: Option<&ArrayRef>,
838 known_only: bool,
839 columns: PathColumns,
840 schema: SchemaRef,
841) -> Result<(Vec<RecordBatch>, usize)> {
842 let dense = ids
843 .dense_from_array(starts.as_ref())
844 .map_err(|e| DataFusionError::Execution(format!("in GRAPH_TABLE: {e}")))?;
845 let goals = targets.map(|t| Goals::new(ids, t)).transpose()?;
846 let mut rows = Rows::new(ids, starts, columns, schema, edge_row)?;
847 let Quantifier { min, max } = search.quantifier;
848 let restricted = search.mode != Mode::Walk;
849 let pruned = !restricted
850 || (matches!(
851 search.selector,
852 Selector::Shortest(1) | Selector::AllShortest
853 ) && min <= 1
854 && !(search.mode == Mode::Trail && search.direction == Direction::Any));
855 let mut walker = Walker {
856 topology,
857 search,
858 restricted,
859 pruned,
860 states: States::new(topology.n_nodes(), min),
861 entries: Vec::new(),
862 next: Vec::new(),
863 total: 0,
864 expanded: 0,
865 slots: goals.as_ref().map(|g| &g.slots),
866 reach: Reach::default(),
867 };
868 let mut targets: HashMap<u32, (u32, u32)> = HashMap::new();
870 let mut found: Vec<(u32, u32)> = vec![(0, 0); goals.as_ref().map_or(0, |g| g.slots.len())];
872 let mut touched: Vec<u32> = Vec::new();
873 let (mut nodes, mut edges) = (Vec::new(), Vec::new());
874 for (row, start) in dense.iter().enumerate() {
875 if starts.is_null(row) {
876 continue;
877 }
878 let Some(start) = *start else {
879 let ends_at_a_target = match &goals {
881 None => true,
882 Some(g) => g
883 .unknown
884 .contains(&ScalarValue::try_from_array(starts.as_ref(), row)?),
885 };
886 if min == 0 && !known_only && ends_at_a_target {
887 rows.push(row as u32, &[row as u32], &[])?;
888 }
889 continue;
890 };
891 let row = row as u32;
892 if goals.as_ref().is_some_and(|g| g.slots.is_empty()) {
893 continue; }
895 for slot in touched.drain(..) {
896 found[slot as usize] = (0, 0);
897 }
898 walker.reach.reset(found.len());
899 let mut remaining = found.len();
902 let mut longest = 0u32;
903 let mut stop_after: Option<u32> = None;
904 walker.states.clear();
905 targets.clear();
906 walker.entries.clear();
907 walker.entries.push(Entry {
908 node: start,
909 parent: ROOT,
910 edge: 0,
911 len: 0,
912 });
913 *walker.states.get(start, 0) = (1, 0);
914 let mut head = 0;
915 'search: while head < walker.entries.len() {
916 let at = head as u32;
917 let entry = walker.entries[head];
918 head += 1;
919 if stop_after.is_some_and(|len| entry.len > len) {
920 break;
921 }
922 let c = entry.len.min(min);
923 let slot = goals.as_ref().map(|g| g.slots.get(&entry.node).copied());
925 if c == min && slot != Some(None) {
926 let keep = if pruned {
927 true
928 } else {
929 let t = targets.entry(entry.node).or_insert((0, entry.len));
930 let keep = match search.selector {
931 Selector::All => true,
932 Selector::Shortest(k) => t.0 < k,
933 Selector::AllShortest => t.1 == entry.len,
934 };
935 if keep {
936 t.0 += 1;
937 }
938 keep
939 };
940 let closed = search.mode == Mode::Acyclic && entry.len > 0 && entry.node == start;
942 if keep && !closed {
943 nodes.clear();
944 edges.clear();
945 let mut i = at;
946 while i != ROOT {
947 let e = walker.entries[i as usize];
948 nodes.push(rows.dense(e.node));
949 if e.parent != ROOT {
950 edges.push(e.edge);
951 }
952 i = e.parent;
953 }
954 nodes.reverse();
955 edges.reverse();
956 rows.push(row, &nodes, &edges)?;
957 walker.total += 1;
958 if let Some(Some(slot)) = slot {
959 let f = &mut found[slot as usize];
960 if f.0 == 0 {
961 touched.push(slot);
962 f.1 = entry.len;
963 }
964 f.0 += 1;
965 let done = match search.selector {
966 Selector::All => false,
967 Selector::Shortest(k) => f.0 == k,
968 Selector::AllShortest => f.0 == 1,
969 };
970 if done {
971 remaining -= 1;
972 longest = longest.max(f.1);
973 if remaining == 0 {
974 match search.selector {
975 Selector::AllShortest => stop_after = Some(longest),
976 _ => break 'search,
977 }
978 }
979 }
980 }
981 }
982 }
983 if max.is_some_and(|max| entry.len >= max) {
984 continue;
985 }
986 if pruned && goals.is_some() && !walker.reach.extends(entry.len, search.selector) {
987 continue; }
989 walker.extend(at, start)?;
990 }
991 }
992 rows.flush()?;
993 Ok((rows.batches, walker.expanded))
994}
995
996#[cfg(test)]
997mod tests {
998 use datafusion::arrow::array::{AsArray, Int64Array};
999 use datafusion::arrow::datatypes::Int64Type;
1000
1001 use super::*;
1002
1003 fn index(edges: &[(i64, i64)]) -> (Topology, IdMap) {
1005 let src: ArrayRef = Arc::new(Int64Array::from(
1006 edges.iter().map(|e| e.0).collect::<Vec<_>>(),
1007 ));
1008 let dst: ArrayRef = Arc::new(Int64Array::from(
1009 edges.iter().map(|e| e.1).collect::<Vec<_>>(),
1010 ));
1011 let mut interner = ursa_core::EdgeInterner::with_capacity(edges.len());
1012 interner.push(src.as_ref(), dst.as_ref()).unwrap();
1013 let (ids, s, d) = interner.finish();
1014 (Topology::build(ids.len(), s, d), ids)
1015 }
1016
1017 fn search(
1018 direction: Direction,
1019 min: u32,
1020 max: Option<u32>,
1021 selector: Selector,
1022 mode: Mode,
1023 ) -> Search {
1024 Search {
1025 direction,
1026 quantifier: Quantifier { min, max },
1027 selector,
1028 mode,
1029 }
1030 }
1031
1032 type Path = (Vec<i64>, Vec<i64>);
1035
1036 fn paths(edges: &[(i64, i64)], starts: &[i64], s: Search) -> Vec<Path> {
1037 targeted(edges, starts, None, s).0
1038 }
1039
1040 fn targeted(
1043 edges: &[(i64, i64)],
1044 starts: &[i64],
1045 targets: Option<&[i64]>,
1046 s: Search,
1047 ) -> (Vec<Path>, usize) {
1048 let (topology, ids) = index(edges);
1049 let targets: Option<ArrayRef> =
1050 targets.map(|t| Arc::new(Int64Array::from(t.to_vec())) as ArrayRef);
1051 let columns = PathColumns {
1052 vertices: true,
1053 edges: true,
1054 elements: true,
1055 };
1056 let schema = Arc::new(super::schema(&DataType::Int64, columns));
1057 let starts: ArrayRef = Arc::new(Int64Array::from(starts.to_vec()));
1058 let (batches, expanded) = run(
1059 &topology,
1060 &ids,
1061 &|e| u64::from(e),
1062 &s,
1063 &starts,
1064 targets.as_ref(),
1065 false,
1066 columns,
1067 schema,
1068 )
1069 .unwrap();
1070 let mut out = Vec::new();
1071 for b in batches {
1072 let v = b.column(3).as_list::<i32>();
1073 let e = b.column(4).as_list::<i32>();
1074 let el = b.column(5).as_list::<i32>();
1075 for r in 0..b.num_rows() {
1076 let vs: Vec<i64> = v.value(r).as_primitive::<Int64Type>().values().to_vec();
1077 let es: Vec<i64> = e.value(r).as_primitive::<Int64Type>().values().to_vec();
1078 let els: Vec<i64> = el.value(r).as_primitive::<Int64Type>().values().to_vec();
1079 assert_eq!(els.len(), vs.len() + es.len());
1080 assert_eq!(
1081 b.column(2).as_primitive::<Int64Type>().value(r),
1082 es.len() as i64
1083 );
1084 out.push((vs, es));
1085 }
1086 }
1087 (out, expanded)
1088 }
1089
1090 #[test]
1093 fn targets_keep_the_same_paths_and_stop_early() {
1094 let mut edges: Vec<(i64, i64)> = (0..9).map(|i| (i, i + 1)).collect();
1096 for i in 0..10 {
1097 edges.extend((0..4).map(|j| (i, 100 + 10 * i + j)));
1098 }
1099 edges.extend([(0, 5), (2, 7), (5, 5), (7, 2), (3, 1)]);
1100 let graphs: [&[(i64, i64)]; 2] = [KNOW, &edges];
1101 let mut stopped_early = false;
1102 for edges in graphs {
1103 for direction in [Direction::Right, Direction::Left, Direction::Any] {
1104 for selector in [
1105 Selector::All,
1106 Selector::Shortest(1),
1107 Selector::Shortest(2),
1108 Selector::AllShortest,
1109 ] {
1110 for mode in [Mode::Walk, Mode::Trail, Mode::Simple, Mode::Acyclic] {
1111 for (min, max) in [(0, Some(3)), (1, None), (2, Some(4)), (1, Some(1))] {
1112 if selector == Selector::All && mode == Mode::Walk && max.is_none() {
1113 continue;
1114 }
1115 let s = search(direction, min, max, selector, mode);
1116 for (starts, targets) in [
1117 (&[0][..], &[3][..]),
1118 (&[0, 4], &[0, 3, 9]),
1119 (&[1, 2], &[2, 77]),
1120 (&[3], &[7, 5, 1]),
1121 ] {
1122 let (all, n_all) = targeted(edges, starts, None, s);
1123 let kept: Vec<_> = all
1124 .into_iter()
1125 .filter(|(v, _)| targets.contains(v.last().unwrap()))
1126 .collect();
1127 let (found, n) = targeted(edges, starts, Some(targets), s);
1128 assert_eq!(found, kept, "{s} from {starts:?} to {targets:?}");
1129 assert!(n <= n_all, "{s}");
1130 stopped_early |= n < n_all;
1131 }
1132 }
1133 }
1134 }
1135 }
1136 }
1137 assert!(stopped_early);
1138 }
1139
1140 const KNOW: &[(i64, i64)] = &[
1142 (0, 1),
1143 (0, 2),
1144 (0, 3),
1145 (3, 0),
1146 (1, 2),
1147 (1, 3),
1148 (2, 3),
1149 (4, 3),
1150 ];
1151
1152 #[test]
1153 fn any_shortest_takes_the_first_edges_and_can_return_to_the_start() {
1154 let found = paths(
1155 KNOW,
1156 &[4],
1157 search(
1158 Direction::Right,
1159 1,
1160 Some(3),
1161 Selector::Shortest(1),
1162 Mode::Walk,
1163 ),
1164 );
1165 assert_eq!(
1166 found,
1167 vec![
1168 (vec![4, 3], vec![7]),
1169 (vec![4, 3, 0], vec![7, 3]),
1170 (vec![4, 3, 0, 1], vec![7, 3, 0]),
1171 (vec![4, 3, 0, 2], vec![7, 3, 1]),
1172 ]
1173 );
1174 let found = paths(
1176 KNOW,
1177 &[0],
1178 search(
1179 Direction::Right,
1180 1,
1181 Some(3),
1182 Selector::Shortest(1),
1183 Mode::Walk,
1184 ),
1185 );
1186 assert!(found.contains(&(vec![0, 3, 0], vec![2, 3])));
1187 let found = paths(
1189 KNOW,
1190 &[0],
1191 search(
1192 Direction::Right,
1193 1,
1194 Some(3),
1195 Selector::Shortest(1),
1196 Mode::Acyclic,
1197 ),
1198 );
1199 assert!(found.iter().all(|(v, _)| v.last() != Some(&0)));
1200 }
1201
1202 #[test]
1203 fn walks_trails_and_counts() {
1204 let found = paths(
1206 KNOW,
1207 &[3],
1208 search(Direction::Right, 1, Some(2), Selector::All, Mode::Walk),
1209 );
1210 assert_eq!(found.len(), 4);
1211 let acyclic = paths(
1213 KNOW,
1214 &[3],
1215 search(Direction::Right, 1, None, Selector::All, Mode::Acyclic),
1216 );
1217 assert!(acyclic.iter().all(|(v, _)| {
1218 let mut s = v.clone();
1219 s.sort();
1220 s.dedup();
1221 s.len() == v.len()
1222 }));
1223 assert_eq!(acyclic.len(), 4);
1225 let found = paths(
1227 KNOW,
1228 &[0],
1229 search(Direction::Right, 1, None, Selector::Shortest(2), Mode::Walk),
1230 );
1231 let to3: Vec<_> = found.iter().filter(|(v, _)| v.last() == Some(&3)).collect();
1232 assert_eq!(
1233 to3,
1234 vec![&(vec![0, 3], vec![2]), &(vec![0, 1, 3], vec![0, 5])]
1235 );
1236 let found = paths(
1238 KNOW,
1239 &[1],
1240 search(Direction::Right, 1, None, Selector::AllShortest, Mode::Walk),
1241 );
1242 let to0: Vec<_> = found.iter().filter(|(v, _)| v.last() == Some(&0)).collect();
1243 assert_eq!(to0, vec![&(vec![1, 3, 0], vec![5, 3])]);
1244 let found = paths(
1245 KNOW,
1246 &[4],
1247 search(
1248 Direction::Right,
1249 3,
1250 Some(3),
1251 Selector::AllShortest,
1252 Mode::Walk,
1253 ),
1254 );
1255 let to2: Vec<_> = found.iter().filter(|(v, _)| v.last() == Some(&2)).collect();
1256 assert_eq!(to2, vec![&(vec![4, 3, 0, 2], vec![7, 3, 1])]);
1257 }
1258
1259 #[test]
1260 fn zero_length_undirected_and_trails() {
1261 let found = paths(
1263 KNOW,
1264 &[9, 0],
1265 search(Direction::Right, 0, Some(0), Selector::All, Mode::Walk),
1266 );
1267 assert_eq!(found, vec![(vec![9], vec![]), (vec![0], vec![])]);
1268 let found = paths(
1270 &[(1, 1), (1, 2)],
1271 &[1],
1272 search(Direction::Any, 1, Some(1), Selector::All, Mode::Walk),
1273 );
1274 assert_eq!(found, vec![(vec![1, 1], vec![0]), (vec![1, 2], vec![1])]);
1275 let found = paths(
1278 &[(1, 2), (2, 1)],
1279 &[1],
1280 search(Direction::Any, 1, Some(2), Selector::All, Mode::Trail),
1281 );
1282 assert_eq!(
1283 found,
1284 vec![
1285 (vec![1, 2], vec![0]),
1286 (vec![1, 2], vec![1]),
1287 (vec![1, 2, 1], vec![0, 1]),
1288 (vec![1, 2, 1], vec![1, 0]),
1289 ]
1290 );
1291 let found = paths(
1293 KNOW,
1294 &[3],
1295 search(Direction::Left, 1, Some(1), Selector::All, Mode::Walk),
1296 );
1297 assert_eq!(
1298 found,
1299 vec![
1300 (vec![3, 0], vec![2]),
1301 (vec![3, 1], vec![5]),
1302 (vec![3, 2], vec![6]),
1303 (vec![3, 4], vec![7])
1304 ]
1305 );
1306 let found = paths(
1308 KNOW,
1309 &[0],
1310 search(Direction::Right, 2, Some(2), Selector::All, Mode::Simple),
1311 );
1312 assert!(found.contains(&(vec![0, 3, 0], vec![2, 3])));
1313 }
1314}