Skip to main content

ursa_engine/graph/
path_search.rs

1//! Searching paths on the graph index: SQL/PGQ's quantified edges
2//! (`-[e]->{1,3}`, `-[e]->*`), path variables, selectors (`ANY SHORTEST`,
3//! `ALL SHORTEST`, `SHORTEST k`) and path modes (`TRAIL`, `SIMPLE`,
4//! `ACYCLIC`) (#297).
5//!
6//! A fixed-length pattern is planned as joins (see [`super::pattern`]). A
7//! quantified edge is not: it is a [`PathSearchNode`], a relation of paths
8//! that runs a breadth-first search on the edge table's graph index, the
9//! index `hop` and `shortest_path` use, and joins to the nodes at its two
10//! ends like any edge. Its rows are one per path:
11//!
12//! | column        | what                                                  |
13//! |---------------|-------------------------------------------------------|
14//! | `__start`     | the key of the path's first node                      |
15//! | `__end`       | the key of its last node                              |
16//! | `__length`    | its number of edges (`path_length(p)`)                |
17//! | `__vertices`  | its nodes' keys, first to last (`vertices(p)`)        |
18//! | `__edges`     | its edges' rows in the edge table, from 0 (`edges(p)`) |
19//! | `__element_id`| nodes and edges alternating (`element_id(p)`)         |
20//!
21//! The last three are computed only when the query uses them.
22//!
23//! A fixed-length edge next to a bound node is searched too, as one edge
24//! (`{1,1}`) from the nodes already reached (see [`super::pattern`]): each of
25//! its rows is one edge, in edge-row order, so the search's rows are the
26//! join's rows.
27//!
28//! ## Targets
29//!
30//! A search may be given its targets: the keys the last node can have, when
31//! a condition on it alone says so (`b.id = 7`, `b.id IN (…)`). It keeps only
32//! the paths that end at one, and a search with a selector stops as soon as
33//! every target has all its paths: `ANY SHORTEST` from one node to another
34//! looks no further than the target.
35//!
36//! ## Which paths
37//!
38//! The search runs from each start node: every node of the left end's
39//! table that its conditions keep. A path of `n` edges is in the result when
40//! `min <= n <= max` and the path mode allows it. Without a selector, every
41//! such path is (a walk needs an upper bound, or it would be endless). With
42//! one, per pair of first and last node:
43//!
44//! - `ANY SHORTEST` (and `ANY`): one shortest path;
45//! - `SHORTEST k` (and `ANY k`): the `k` shortest paths;
46//! - `ALL SHORTEST`: every path as short as the shortest.
47//!
48//! Among paths of one length, a path comes before another when its first
49//! edge has a lower row in the edge table, or the same first edge and a
50//! lower second, and so on: `ANY SHORTEST` keeps the first. The search finds
51//! paths in that order, so the result does not depend on the run or the
52//! thread count. (An undirected edge is followed either way; a self-loop
53//! once.)
54//!
55//! ## How
56//!
57//! The search is breadth-first over (node, edges so far, up to `min`), so a
58//! shortest path of at least `min` edges is found even when it has to come
59//! back to a node it passed. Under `WALK`, a selector lets it stop extending
60//! a state once it has its `k` paths (or, for `ALL SHORTEST`, the paths as
61//! short as the first): any longer prefix makes a longer or later path. That
62//! holds for the other modes too when the lower bound is 0 or 1, the
63//! selector is `ANY SHORTEST` or `ALL SHORTEST` and the path is not an
64//! undirected `TRAIL`: a shortest walk between two nodes repeats no node, and
65//! one back to its start is a cycle. Otherwise the search lists every path
66//! the mode allows, up to [`MAX_PATHS`].
67
68use 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
88/// The path's first node.
89pub const START: &str = "__start";
90/// Its last node.
91pub const END: &str = "__end";
92/// Its number of edges.
93pub const LENGTH: &str = "__length";
94/// Its nodes, first to last.
95pub const VERTICES: &str = "__vertices";
96/// Its edges' rows in the edge table.
97pub const EDGES: &str = "__edges";
98/// Its nodes and edges, alternating.
99pub const ELEMENTS: &str = "__element_id";
100
101/// The path functions, and the column each reads.
102pub const FUNCTIONS: &[(&str, &str)] = &[
103    ("path_length", LENGTH),
104    ("vertices", VERTICES),
105    ("edges", EDGES),
106    ("element_id", ELEMENTS),
107];
108
109/// The most paths one search lists before it stops with an error.
110pub const MAX_PATHS: usize = 10_000_000;
111
112/// Rows per output batch.
113const BATCH_ROWS: usize = 8192;
114
115/// What a search looks for.
116#[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/// Which path columns a node computes besides its ends and length.
153#[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
160/// The schema of a search's rows, for node ids of `id_type`.
161pub 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/// A search on a graph's index, as a logical plan node. Its inputs:
181///
182/// 1. the edge table's rows, projected to the endpoints (the plan the index
183///    is built from, as for a traversal);
184/// 2. the start nodes: one column of keys, each once.
185#[derive(Debug, Clone)]
186pub struct PathSearchNode {
187    pub graph: String,
188    pub search: Search,
189    pub columns: PathColumns,
190    pub input: Arc<LogicalPlan>,
191    /// The keys of the vertex tables the edges connect, one column, when they
192    /// have them: the graph's nodes, as an algorithm's (#311).
193    pub vertices: Option<Arc<LogicalPlan>>,
194    pub starts: Arc<LogicalPlan>,
195    /// The keys the paths may end at, one column, if they are known (see the
196    /// module docs).
197    pub targets: Option<Arc<LogicalPlan>>,
198    /// Whether every node the starts can name is a node of the index (the
199    /// start node's table is the one the index's nodes are the keys of): a
200    /// start that is not one is no node, so it has no path, not even the
201    /// path of no edges.
202    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    /// Every input is read whole: the endpoints, the vertex key, the one
357    /// start column and the one target column.
358    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    /// A filter on the paths cannot move into the edges or the starts.
372    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/// One path prefix the search holds: its last node, the entry it extends,
382/// the edge that does, and its length.
383#[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
393/// Per search state, (node, edges so far up to `min`): how many prefixes
394/// reached it, and the length of the first.
395enum 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
446/// The edges a step can take from `u`, in edge-row order, as (next node,
447/// edge row).
448fn 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            // A self-loop is in both lists: it is taken once, forward.
462            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
481/// Whether a prefix ending at entry `at` may take the edge `edge` to `w`
482/// under `mode`.
483fn 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; // closed: it cannot go on
503            }
504            w == start && chain().all(|e| e.node != w || e.parent == ROOT)
505                || chain().all(|e| e.node != w)
506        }
507    }
508}
509
510/// Collects a search's rows into batches.
511struct 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    /// The starts, then every node of the index, as user ids: what the
524    /// node references index.
525    values: ArrayRef,
526    /// The edge table's row of an edge of the index.
527    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    /// The index into `values` of dense node `d`.
561    fn dense(&self, d: u32) -> u32 {
562        (self.starts.len() as u32) + d
563    }
564
565    /// A path from start row `start`: its nodes (as `values` indexes) and
566    /// edges, first to last.
567    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
668/// The targets of a search: the dense nodes its paths may end at, each with
669/// a slot, and the target keys that are no node of the index.
670struct 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
700/// What one search is doing, apart from its rows.
701struct 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    /// Paths listed so far.
710    total: usize,
711    /// Prefixes extended so far.
712    expanded: usize,
713    /// The targets' slots, when a pruned search has targets: their paths are
714    /// known once pushed (see [`Reach`]).
715    slots: Option<&'a HashMap<u32, u32>>,
716    reach: Reach,
717}
718
719/// How far a pruned search with targets has reached them, counting the
720/// prefixes it pushed. A pruned search lists every prefix it pushes to a
721/// target (at `min` edges or more), in the order it pushed them, so once each
722/// target has its `k` (`SHORTEST k`), or its first (`ALL SHORTEST`), no
723/// prefix needs extending any more: for `ALL SHORTEST`, none at the
724/// longest of those lengths or beyond.
725#[derive(Default)]
726struct Reach {
727    /// Per slot, prefixes pushed.
728    pushed: Vec<u32>,
729    touched: Vec<u32>,
730    /// Targets without all their paths yet.
731    unfilled: usize,
732    /// The longest length at which a target was first reached.
733    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    /// Whether a prefix of `len` edges still needs extending.
749    fn extends(&self, len: u32, selector: Selector) -> bool {
750        self.unfilled > 0 || (selector == Selector::AllShortest && len < self.longest)
751    }
752}
753
754impl Walker<'_> {
755    /// Push the prefixes that extend entry `at` by one edge.
756    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            // A pruned search lists this prefix: a target may have its paths.
793            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/// Run `search` from each of `starts` (keys of the graph's id type, each
824/// once) on the index `topology`/`ids`; the rows of [`schema`], and how many
825/// path prefixes the search extended. `edge_row` gives the edge table's row
826/// of an edge of the index (see [`super::GraphIndex::edge_row`]). With
827/// `targets`, only the paths that end at one are kept, and a search with a
828/// selector stops once every target has its paths (see the module docs).
829/// With `known_only`, a start that is no node of the index has no path.
830#[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    // Per last node, for a search that lists every path: (paths kept, length of the first).
869    let mut targets: HashMap<u32, (u32, u32)> = HashMap::new();
870    // Per target slot, for this start: (paths kept, length of the first).
871    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            // A node with no edges: only the path of no edges.
880            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; // no target is a node: no path ends at one
894        }
895        for slot in touched.drain(..) {
896            found[slot as usize] = (0, 0);
897        }
898        walker.reach.reset(found.len());
899        // Targets not yet done, and (ALL SHORTEST) the length past which
900        // none has another path once every one is found.
901        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            // The target slot of the entry's node, when the search has targets.
924            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                // ACYCLIC: a path back to its start is a cycle.
941                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; // every target has its paths among those pushed
988            }
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    /// The index of edges `(src, dst)`, in row order.
1004    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    /// Each path as (vertices, edges).
1033    /// A path: its vertices and its edges.
1034    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    /// Each path as (vertices, edges), searched toward `targets`, and how
1041    /// many prefixes the search extended.
1042    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    /// A search toward targets keeps the paths a search without them finds
1091    /// to those targets, in the same order, extending fewer prefixes.
1092    #[test]
1093    fn targets_keep_the_same_paths_and_stop_early() {
1094        // A chain 0 → 1 → … → 9 with a fan out of every node, and shortcuts.
1095        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    // DuckPGQ's test graph: Daniel 0, Tavneet 1, Gabor 2, Peter 3, David 4.
1141    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        // From 0, a shortest walk of one edge or more back to 0 is 0→3→0.
1175        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        // ACYCLIC drops it.
1188        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        // Every walk of 1 or 2 edges from Peter: 3→0, then 0→1, 0→2, 0→3.
1205        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        // Without a bound under ACYCLIC: every simple path, finitely many.
1212        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        // 3→0, 3→0→1, 3→0→2 and 3→0→1→2.
1224        assert_eq!(acyclic.len(), 4);
1225        // SHORTEST 2 from 0 to 3: 0→3 (edge 2), then 0→1→3 (0, 5) before 0→2→3 (1, 6).
1226        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        // ALL SHORTEST from 0 to 3 is the one edge; to 2: 0→2 alone.
1237        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        // A start with no edges has its path of no edges.
1262        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        // Undirected, a self-loop is followed once.
1269        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        // An undirected trail back to the start cannot reuse its edge: 1-2-1
1276        // takes the two parallel edges.
1277        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        // Left: against the edges.
1292        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        // SIMPLE: back to the start is allowed, once.
1307        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}