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//! ## Which paths
24//!
25//! The search runs from each start node: every node of the left end's
26//! table that its conditions keep. A path of `n` edges is in the result when
27//! `min <= n <= max` and the path mode allows it. Without a selector, every
28//! such path is (a walk needs an upper bound, or it would be endless). With
29//! one, per pair of first and last node:
30//!
31//! - `ANY SHORTEST` (and `ANY`): one shortest path;
32//! - `SHORTEST k` (and `ANY k`): the `k` shortest paths;
33//! - `ALL SHORTEST`: every path as short as the shortest.
34//!
35//! Among paths of one length, a path comes before another when its first
36//! edge has a lower row in the edge table, or the same first edge and a
37//! lower second, and so on: `ANY SHORTEST` keeps the first. The search finds
38//! paths in that order, so the result does not depend on the run or the
39//! thread count. (An undirected edge is followed either way; a self-loop
40//! once.)
41//!
42//! ## How
43//!
44//! The search is breadth-first over (node, edges so far, up to `min`), so a
45//! shortest path of at least `min` edges is found even when it has to come
46//! back to a node it passed. Under `WALK`, a selector lets it stop extending
47//! a state once it has its `k` paths (or, for `ALL SHORTEST`, the paths as
48//! short as the first): any longer prefix makes a longer or later path. That
49//! holds for the other modes too when the lower bound is 0 or 1, the
50//! selector is `ANY SHORTEST` or `ALL SHORTEST` and the path is not an
51//! undirected `TRAIL`: a shortest walk between two nodes repeats no node, and
52//! one back to its start is a cycle. Otherwise the search lists every path
53//! the mode allows, up to [`MAX_PATHS`].
54
55use std::collections::HashMap;
56use std::fmt;
57use std::hash::{Hash, Hasher};
58use std::sync::Arc;
59
60use datafusion::arrow::array::{
61    Array, ArrayBuilder, ArrayRef, Int64Array, Int64Builder, ListArray, UInt32Array,
62};
63use datafusion::arrow::buffer::OffsetBuffer;
64use datafusion::arrow::compute::{concat, take};
65use datafusion::arrow::datatypes::{DataType, Field, Schema, SchemaRef};
66use datafusion::arrow::record_batch::RecordBatch;
67use datafusion::common::{DFSchema, DFSchemaRef};
68use datafusion::error::{DataFusionError, Result};
69use datafusion::logical_expr::{Expr, LogicalPlan, UserDefinedLogicalNodeCore};
70use ursa_core::{IdMap, Topology};
71
72use super::node::{check_vertices, endpoint_id_type};
73use super::pattern::{Direction, Mode, Quantifier, Selector};
74
75/// The path's first node.
76pub const START: &str = "__start";
77/// Its last node.
78pub const END: &str = "__end";
79/// Its number of edges.
80pub const LENGTH: &str = "__length";
81/// Its nodes, first to last.
82pub const VERTICES: &str = "__vertices";
83/// Its edges' rows in the edge table.
84pub const EDGES: &str = "__edges";
85/// Its nodes and edges, alternating.
86pub const ELEMENTS: &str = "__element_id";
87
88/// The path functions, and the column each reads.
89pub const FUNCTIONS: &[(&str, &str)] = &[
90    ("path_length", LENGTH),
91    ("vertices", VERTICES),
92    ("edges", EDGES),
93    ("element_id", ELEMENTS),
94];
95
96/// The most paths one search lists before it stops with an error.
97pub const MAX_PATHS: usize = 10_000_000;
98
99/// Rows per output batch.
100const BATCH_ROWS: usize = 8192;
101
102/// What a search looks for.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
104pub struct Search {
105    pub direction: Direction,
106    pub quantifier: Quantifier,
107    pub selector: Selector,
108    pub mode: Mode,
109}
110
111impl fmt::Display for Search {
112    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
113        match self.selector {
114            Selector::All => {}
115            Selector::Shortest(1) => write!(f, "ANY SHORTEST ")?,
116            Selector::Shortest(k) => write!(f, "SHORTEST {k} ")?,
117            Selector::AllShortest => write!(f, "ALL SHORTEST ")?,
118        }
119        let mode = match self.mode {
120            Mode::Walk => "WALK",
121            Mode::Trail => "TRAIL",
122            Mode::Simple => "SIMPLE",
123            Mode::Acyclic => "ACYCLIC",
124        };
125        let arrow = match self.direction {
126            Direction::Right => "-[]->",
127            Direction::Left => "<-[]-",
128            Direction::Any => "-[]-",
129        };
130        let max = self
131            .quantifier
132            .max
133            .map(|m| m.to_string())
134            .unwrap_or_default();
135        write!(f, "{mode} {arrow}{{{},{max}}}", self.quantifier.min)
136    }
137}
138
139/// Which path columns a node computes besides its ends and length.
140#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd)]
141pub struct PathColumns {
142    pub vertices: bool,
143    pub edges: bool,
144    pub elements: bool,
145}
146
147/// The schema of a search's rows, for node ids of `id_type`.
148pub fn schema(id_type: &DataType, columns: PathColumns) -> Schema {
149    let list = |t: DataType| DataType::List(Arc::new(Field::new_list_field(t, false)));
150    let mut fields = vec![
151        Field::new(START, id_type.clone(), false),
152        Field::new(END, id_type.clone(), false),
153        Field::new(LENGTH, DataType::Int64, false),
154    ];
155    if columns.vertices {
156        fields.push(Field::new(VERTICES, list(id_type.clone()), false));
157    }
158    if columns.edges {
159        fields.push(Field::new(EDGES, list(DataType::Int64), false));
160    }
161    if columns.elements {
162        fields.push(Field::new(ELEMENTS, list(DataType::Int64), false));
163    }
164    Schema::new(fields)
165}
166
167/// A search on a graph's index, as a logical plan node. Its inputs:
168///
169/// 1. the edge table's rows, projected to the endpoints (the plan the index
170///    is built from, as for a traversal);
171/// 2. the start nodes: one column of keys, each once.
172#[derive(Debug, Clone)]
173pub struct PathSearchNode {
174    pub graph: String,
175    pub search: Search,
176    pub columns: PathColumns,
177    pub input: Arc<LogicalPlan>,
178    /// The keys of the vertex tables the edges connect, one column, when they
179    /// have them: the graph's nodes, as an algorithm's (#311).
180    pub vertices: Option<Arc<LogicalPlan>>,
181    pub starts: Arc<LogicalPlan>,
182    pub source: Expr,
183    pub destination: Expr,
184    pub id_type: DataType,
185    schema: DFSchemaRef,
186}
187
188impl PathSearchNode {
189    #[allow(clippy::too_many_arguments)]
190    pub fn try_new(
191        graph: String,
192        search: Search,
193        columns: PathColumns,
194        input: Arc<LogicalPlan>,
195        vertices: Option<Arc<LogicalPlan>>,
196        starts: Arc<LogicalPlan>,
197        source: Expr,
198        destination: Expr,
199    ) -> Result<Self> {
200        let id_type = endpoint_id_type(&graph, &input, &source, &destination)?;
201        check_vertices(&graph, vertices.as_deref())?;
202        if columns.elements && id_type != DataType::Int64 {
203            return Err(DataFusionError::Plan(format!(
204                "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)"
205            )));
206        }
207        let schema = Arc::new(DFSchema::try_from(schema(&id_type, columns))?);
208        Ok(Self {
209            graph,
210            search,
211            columns,
212            input,
213            vertices,
214            starts,
215            source,
216            destination,
217            id_type,
218            schema,
219        })
220    }
221
222    pub(crate) fn endpoint_indices(&self) -> Result<(usize, usize)> {
223        let index = |e: &Expr| -> Result<usize> {
224            match e {
225                Expr::Column(c) => self.input.schema().index_of_column(c),
226                other => Err(DataFusionError::Internal(format!(
227                    "a path search's endpoint must be a column; got {other}"
228                ))),
229            }
230        };
231        Ok((index(&self.source)?, index(&self.destination)?))
232    }
233}
234
235impl PartialEq for PathSearchNode {
236    fn eq(&self, other: &Self) -> bool {
237        self.graph == other.graph
238            && self.search == other.search
239            && self.columns == other.columns
240            && self.input == other.input
241            && self.vertices == other.vertices
242            && self.starts == other.starts
243            && self.source == other.source
244            && self.destination == other.destination
245    }
246}
247
248impl Eq for PathSearchNode {}
249
250impl Hash for PathSearchNode {
251    fn hash<H: Hasher>(&self, state: &mut H) {
252        self.graph.hash(state);
253        self.search.hash(state);
254        self.columns.hash(state);
255        self.input.hash(state);
256        self.vertices.hash(state);
257        self.starts.hash(state);
258    }
259}
260
261impl PartialOrd for PathSearchNode {
262    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
263        (&self.graph, &self.search, &self.columns)
264            .partial_cmp(&(&other.graph, &other.search, &other.columns))
265            .filter(|o| o.is_ne())
266    }
267}
268
269impl UserDefinedLogicalNodeCore for PathSearchNode {
270    fn name(&self) -> &str {
271        "PathSearch"
272    }
273
274    fn inputs(&self) -> Vec<&LogicalPlan> {
275        let mut inputs = vec![self.input.as_ref()];
276        inputs.extend(self.vertices.as_deref());
277        inputs.push(&self.starts);
278        inputs
279    }
280
281    fn schema(&self) -> &DFSchemaRef {
282        &self.schema
283    }
284
285    fn expressions(&self) -> Vec<Expr> {
286        vec![]
287    }
288
289    fn fmt_for_explain(&self, f: &mut fmt::Formatter) -> fmt::Result {
290        write!(
291            f,
292            "PathSearch: graph={}, search={}",
293            self.graph, self.search
294        )
295    }
296
297    fn with_exprs_and_inputs(&self, _exprs: Vec<Expr>, inputs: Vec<LogicalPlan>) -> Result<Self> {
298        let expected = 2 + usize::from(self.vertices.is_some());
299        if inputs.len() != expected {
300            return Err(DataFusionError::Internal(format!(
301                "PathSearch has {expected} inputs; got {}",
302                inputs.len()
303            )));
304        }
305        let mut inputs = inputs.into_iter().map(Arc::new);
306        let input = inputs.next().expect("checked above");
307        let vertices = self.vertices.as_ref().and_then(|_| inputs.next());
308        let starts = inputs.next().expect("checked above");
309        Ok(Self {
310            input,
311            vertices,
312            starts,
313            ..self.clone()
314        })
315    }
316
317    /// Every input is read whole: the endpoints, the vertex key, and the one
318    /// start column.
319    fn necessary_children_exprs(&self, _output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
320        let (source, destination) = self.endpoint_indices().ok()?;
321        let mut children = vec![vec![source, destination]];
322        if self.vertices.is_some() {
323            children.push(vec![0]);
324        }
325        children.push(vec![0]);
326        Some(children)
327    }
328
329    /// A filter on the paths cannot move into the edges or the starts.
330    fn prevent_predicate_push_down_columns(&self) -> std::collections::HashSet<String> {
331        self.schema
332            .fields()
333            .iter()
334            .map(|f| f.name().clone())
335            .collect()
336    }
337}
338
339/// One path prefix the search holds: its last node, the entry it extends,
340/// the edge that does, and its length.
341#[derive(Debug, Clone, Copy)]
342struct Entry {
343    node: u32,
344    parent: u32,
345    edge: u32,
346    len: u32,
347}
348
349const ROOT: u32 = u32::MAX;
350
351/// Per search state, (node, edges so far up to `min`): how many prefixes
352/// reached it, and the length of the first.
353enum States {
354    Dense {
355        seen: Vec<(u32, u32)>,
356        touched: Vec<usize>,
357        width: usize,
358    },
359    Sparse(HashMap<(u32, u32), (u32, u32)>),
360}
361
362impl States {
363    fn new(n_nodes: usize, min: u32) -> Self {
364        let width = min as usize + 1;
365        match n_nodes.checked_mul(width) {
366            Some(n) if n <= 1 << 26 => States::Dense {
367                seen: vec![(0, 0); n],
368                touched: Vec::new(),
369                width,
370            },
371            _ => States::Sparse(HashMap::new()),
372        }
373    }
374
375    fn get(&mut self, node: u32, c: u32) -> &mut (u32, u32) {
376        match self {
377            States::Dense {
378                seen,
379                touched,
380                width,
381            } => {
382                let i = node as usize * *width + c as usize;
383                if seen[i].0 == 0 {
384                    touched.push(i);
385                }
386                &mut seen[i]
387            }
388            States::Sparse(map) => map.entry((node, c)).or_insert((0, 0)),
389        }
390    }
391
392    fn clear(&mut self) {
393        match self {
394            States::Dense { seen, touched, .. } => {
395                for i in touched.drain(..) {
396                    seen[i] = (0, 0);
397                }
398            }
399            States::Sparse(map) => map.clear(),
400        }
401    }
402}
403
404/// The edges a step can take from `u`, in edge-row order, as (next node,
405/// edge row).
406fn steps(topology: &Topology, direction: Direction, u: u32, out: &mut Vec<(u32, u32)>) {
407    out.clear();
408    fn pairs(a: &ursa_core::Adjacency, u: u32) -> impl Iterator<Item = (u32, u32)> + '_ {
409        a.neighbors(u)
410            .iter()
411            .copied()
412            .zip(a.edge_ids(u).iter().copied())
413    }
414    match direction {
415        Direction::Right => out.extend(pairs(topology.out(), u)),
416        Direction::Left => out.extend(pairs(topology.incoming(), u)),
417        Direction::Any => {
418            let forward: Vec<(u32, u32)> = pairs(topology.out(), u).collect();
419            // A self-loop is in both lists: it is taken once, forward.
420            let backward: Vec<(u32, u32)> = pairs(topology.incoming(), u)
421                .filter(|&(w, _)| w != u)
422                .collect();
423            let (mut i, mut j) = (0, 0);
424            while i < forward.len() || j < backward.len() {
425                let take_forward =
426                    j == backward.len() || (i < forward.len() && forward[i].1 < backward[j].1);
427                if take_forward {
428                    out.push(forward[i]);
429                    i += 1;
430                } else {
431                    out.push(backward[j]);
432                    j += 1;
433                }
434            }
435        }
436    }
437}
438
439/// Whether a prefix ending at entry `at` may take the edge `edge` to `w`
440/// under `mode`.
441fn allowed(entries: &[Entry], at: u32, w: u32, edge: u32, mode: Mode, start: u32) -> bool {
442    let chain = || {
443        let mut i = at;
444        std::iter::from_fn(move || {
445            if i == ROOT {
446                return None;
447            }
448            let e = entries[i as usize];
449            i = e.parent;
450            Some(e)
451        })
452    };
453    match mode {
454        Mode::Walk => true,
455        Mode::Trail => chain().all(|e| e.parent == ROOT || e.edge != edge),
456        Mode::Acyclic => chain().all(|e| e.node != w),
457        Mode::Simple => {
458            let last = entries[at as usize];
459            if last.len > 0 && last.node == start {
460                return false; // closed: it cannot go on
461            }
462            w == start && chain().all(|e| e.node != w || e.parent == ROOT)
463                || chain().all(|e| e.node != w)
464        }
465    }
466}
467
468/// Collects a search's rows into batches.
469struct Rows<'a> {
470    starts: &'a ArrayRef,
471    columns: PathColumns,
472    schema: SchemaRef,
473    start_rows: Vec<u32>,
474    ends: Vec<u32>,
475    lengths: Vec<i64>,
476    vertex_offsets: Vec<i32>,
477    vertex_values: Vec<u32>,
478    edge_offsets: Vec<i32>,
479    edge_values: Vec<i64>,
480    batches: Vec<RecordBatch>,
481    /// The starts, then every node of the index, as user ids: what the
482    /// node references index.
483    values: ArrayRef,
484    /// The edge table's row of an edge of the index.
485    edge_row: &'a dyn Fn(u32) -> u64,
486}
487
488impl<'a> Rows<'a> {
489    fn new(
490        ids: &'a IdMap,
491        starts: &'a ArrayRef,
492        columns: PathColumns,
493        schema: SchemaRef,
494        edge_row: &'a dyn Fn(u32) -> u64,
495    ) -> Result<Self> {
496        let values = if ids.is_empty() {
497            Arc::clone(starts)
498        } else {
499            concat(&[starts.as_ref(), ids.user_id_array().as_ref()])?
500        };
501        Ok(Self {
502            starts,
503            columns,
504            schema,
505            start_rows: Vec::new(),
506            ends: Vec::new(),
507            lengths: Vec::new(),
508            vertex_offsets: vec![0],
509            vertex_values: Vec::new(),
510            edge_offsets: vec![0],
511            edge_values: Vec::new(),
512            batches: Vec::new(),
513            values,
514            edge_row,
515        })
516    }
517
518    /// The index into `values` of dense node `d`.
519    fn dense(&self, d: u32) -> u32 {
520        (self.starts.len() as u32) + d
521    }
522
523    /// A path from start row `start`: its nodes (as `values` indexes) and
524    /// edges, first to last.
525    fn push(&mut self, start: u32, nodes: &[u32], edges: &[u32]) -> Result<()> {
526        self.start_rows.push(start);
527        self.ends.push(*nodes.last().expect("a path has a node"));
528        self.lengths.push(edges.len() as i64);
529        if self.columns.vertices || self.columns.elements {
530            self.vertex_values.extend_from_slice(nodes);
531            self.vertex_offsets.push(self.vertex_values.len() as i32);
532        }
533        if self.columns.edges || self.columns.elements {
534            let edge_row = self.edge_row;
535            self.edge_values
536                .extend(edges.iter().map(|&e| edge_row(e) as i64));
537            self.edge_offsets.push(self.edge_values.len() as i32);
538        }
539        if self.start_rows.len() >= BATCH_ROWS {
540            self.flush()?;
541        }
542        Ok(())
543    }
544
545    fn flush(&mut self) -> Result<()> {
546        if self.start_rows.is_empty() {
547            return Ok(());
548        }
549        let start = take(
550            self.starts.as_ref(),
551            &UInt32Array::from(std::mem::take(&mut self.start_rows)),
552            None,
553        )?;
554        let end = take(
555            self.values.as_ref(),
556            &UInt32Array::from(std::mem::take(&mut self.ends)),
557            None,
558        )?;
559        let mut columns: Vec<ArrayRef> = vec![
560            start,
561            end,
562            Arc::new(Int64Array::from(std::mem::take(&mut self.lengths))),
563        ];
564        let vertex_offsets = std::mem::replace(&mut self.vertex_offsets, vec![0]);
565        let vertex_values = take(
566            self.values.as_ref(),
567            &UInt32Array::from(std::mem::take(&mut self.vertex_values)),
568            None,
569        )?;
570        let edge_offsets = std::mem::replace(&mut self.edge_offsets, vec![0]);
571        let edge_values = std::mem::take(&mut self.edge_values);
572        let field = |i: usize| match self.schema.field(i).data_type() {
573            DataType::List(f) => Arc::clone(f),
574            _ => unreachable!("a list column"),
575        };
576        let mut i = 3;
577        if self.columns.vertices {
578            columns.push(Arc::new(ListArray::try_new(
579                field(i),
580                OffsetBuffer::new(vertex_offsets.clone().into()),
581                Arc::clone(&vertex_values),
582                None,
583            )?));
584            i += 1;
585        }
586        if self.columns.edges {
587            columns.push(Arc::new(ListArray::try_new(
588                field(i),
589                OffsetBuffer::new(edge_offsets.clone().into()),
590                Arc::new(Int64Array::from(edge_values.clone())),
591                None,
592            )?));
593            i += 1;
594        }
595        if self.columns.elements {
596            let keys = vertex_values
597                .as_any()
598                .downcast_ref::<Int64Array>()
599                .ok_or_else(|| DataFusionError::Internal("element_id needs Int64 keys".into()))?;
600            let mut values = Int64Builder::with_capacity(keys.len() + edge_values.len());
601            let mut offsets = vec![0i32];
602            for p in 0..vertex_offsets.len() - 1 {
603                let (v0, v1) = (vertex_offsets[p] as usize, vertex_offsets[p + 1] as usize);
604                let e0 = edge_offsets[p] as usize;
605                for k in v0..v1 {
606                    values.append_value(keys.value(k));
607                    if k + 1 < v1 {
608                        values.append_value(edge_values[e0 + (k - v0)]);
609                    }
610                }
611                offsets.push(values.len() as i32);
612            }
613            columns.push(Arc::new(ListArray::try_new(
614                field(i),
615                OffsetBuffer::new(offsets.into()),
616                Arc::new(values.finish()),
617                None,
618            )?));
619        }
620        self.batches
621            .push(RecordBatch::try_new(Arc::clone(&self.schema), columns)?);
622        Ok(())
623    }
624}
625
626/// Run `search` from each of `starts` (keys of the graph's id type, each
627/// once) on the index `topology`/`ids`; the rows of [`schema`]. `edge_row`
628/// gives the edge table's row of an edge of the index (see
629/// [`super::GraphIndex::edge_row`]).
630pub fn run(
631    topology: &Topology,
632    ids: &IdMap,
633    edge_row: &dyn Fn(u32) -> u64,
634    search: &Search,
635    starts: &ArrayRef,
636    columns: PathColumns,
637    schema: SchemaRef,
638) -> Result<Vec<RecordBatch>> {
639    let dense = ids
640        .dense_from_array(starts.as_ref())
641        .map_err(|e| DataFusionError::Execution(format!("in GRAPH_TABLE: {e}")))?;
642    let mut rows = Rows::new(ids, starts, columns, schema, edge_row)?;
643    let Quantifier { min, max } = search.quantifier;
644    let restricted = search.mode != Mode::Walk;
645    let pruned = !restricted
646        || (matches!(
647            search.selector,
648            Selector::Shortest(1) | Selector::AllShortest
649        ) && min <= 1
650            && !(search.mode == Mode::Trail && search.direction == Direction::Any));
651    let mut states = States::new(topology.n_nodes(), min);
652    // Per last node, for a search that lists every path: (paths kept, length of the first).
653    let mut targets: HashMap<u32, (u32, u32)> = HashMap::new();
654    let mut entries: Vec<Entry> = Vec::new();
655    let mut next = Vec::new();
656    let (mut nodes, mut edges) = (Vec::new(), Vec::new());
657    let mut total = 0usize;
658    for (row, start) in dense.iter().enumerate() {
659        if starts.is_null(row) {
660            continue;
661        }
662        let row = row as u32;
663        let Some(start) = *start else {
664            // A node with no edges: only the path of no edges.
665            if min == 0 {
666                rows.push(row, &[row], &[])?;
667            }
668            continue;
669        };
670        states.clear();
671        targets.clear();
672        entries.clear();
673        entries.push(Entry {
674            node: start,
675            parent: ROOT,
676            edge: 0,
677            len: 0,
678        });
679        *states.get(start, 0) = (1, 0);
680        let mut head = 0;
681        while head < entries.len() {
682            let at = head as u32;
683            let entry = entries[head];
684            head += 1;
685            let c = entry.len.min(min);
686            if c == min {
687                let keep = if pruned {
688                    true
689                } else {
690                    let t = targets.entry(entry.node).or_insert((0, entry.len));
691                    let keep = match search.selector {
692                        Selector::All => true,
693                        Selector::Shortest(k) => t.0 < k,
694                        Selector::AllShortest => t.1 == entry.len,
695                    };
696                    if keep {
697                        t.0 += 1;
698                    }
699                    keep
700                };
701                // ACYCLIC: a path back to its start is a cycle.
702                let closed = search.mode == Mode::Acyclic && entry.len > 0 && entry.node == start;
703                if keep && !closed {
704                    nodes.clear();
705                    edges.clear();
706                    let mut i = at;
707                    while i != ROOT {
708                        let e = entries[i as usize];
709                        nodes.push(rows.dense(e.node));
710                        if e.parent != ROOT {
711                            edges.push(e.edge);
712                        }
713                        i = e.parent;
714                    }
715                    nodes.reverse();
716                    edges.reverse();
717                    rows.push(row, &nodes, &edges)?;
718                    total += 1;
719                }
720            }
721            if max.is_some_and(|max| entry.len >= max) {
722                continue;
723            }
724            steps(topology, search.direction, entry.node, &mut next);
725            for &(w, edge) in &next {
726                if restricted && !allowed(&entries, at, w, edge, search.mode, start) {
727                    continue;
728                }
729                let len = entry.len + 1;
730                if pruned {
731                    let state = states.get(w, len.min(min));
732                    let accept = match search.selector {
733                        Selector::All => true,
734                        Selector::Shortest(k) => state.0 < k,
735                        Selector::AllShortest => state.0 == 0 || state.1 == len,
736                    };
737                    if !accept {
738                        continue;
739                    }
740                    if state.0 == 0 {
741                        state.1 = len;
742                    }
743                    state.0 += 1;
744                }
745                entries.push(Entry {
746                    node: w,
747                    parent: at,
748                    edge,
749                    len,
750                });
751                if entries.len() + total > MAX_PATHS {
752                    return Err(DataFusionError::Execution(format!(
753                        "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"
754                    )));
755                }
756            }
757        }
758    }
759    rows.flush()?;
760    Ok(rows.batches)
761}
762
763#[cfg(test)]
764mod tests {
765    use datafusion::arrow::array::{AsArray, Int64Array};
766    use datafusion::arrow::datatypes::Int64Type;
767
768    use super::*;
769
770    /// The index of edges `(src, dst)`, in row order.
771    fn index(edges: &[(i64, i64)]) -> (Topology, IdMap) {
772        let src: ArrayRef = Arc::new(Int64Array::from(
773            edges.iter().map(|e| e.0).collect::<Vec<_>>(),
774        ));
775        let dst: ArrayRef = Arc::new(Int64Array::from(
776            edges.iter().map(|e| e.1).collect::<Vec<_>>(),
777        ));
778        let mut interner = ursa_core::EdgeInterner::with_capacity(edges.len());
779        interner.push(src.as_ref(), dst.as_ref()).unwrap();
780        let (ids, s, d) = interner.finish();
781        (Topology::build(ids.len(), s, d), ids)
782    }
783
784    fn search(
785        direction: Direction,
786        min: u32,
787        max: Option<u32>,
788        selector: Selector,
789        mode: Mode,
790    ) -> Search {
791        Search {
792            direction,
793            quantifier: Quantifier { min, max },
794            selector,
795            mode,
796        }
797    }
798
799    /// Each path as (vertices, edges).
800    fn paths(edges: &[(i64, i64)], starts: &[i64], s: Search) -> Vec<(Vec<i64>, Vec<i64>)> {
801        let (topology, ids) = index(edges);
802        let columns = PathColumns {
803            vertices: true,
804            edges: true,
805            elements: true,
806        };
807        let schema = Arc::new(super::schema(&DataType::Int64, columns));
808        let starts: ArrayRef = Arc::new(Int64Array::from(starts.to_vec()));
809        let batches = run(
810            &topology,
811            &ids,
812            &|e| u64::from(e),
813            &s,
814            &starts,
815            columns,
816            schema,
817        )
818        .unwrap();
819        let mut out = Vec::new();
820        for b in batches {
821            let v = b.column(3).as_list::<i32>();
822            let e = b.column(4).as_list::<i32>();
823            let el = b.column(5).as_list::<i32>();
824            for r in 0..b.num_rows() {
825                let vs: Vec<i64> = v.value(r).as_primitive::<Int64Type>().values().to_vec();
826                let es: Vec<i64> = e.value(r).as_primitive::<Int64Type>().values().to_vec();
827                let els: Vec<i64> = el.value(r).as_primitive::<Int64Type>().values().to_vec();
828                assert_eq!(els.len(), vs.len() + es.len());
829                assert_eq!(
830                    b.column(2).as_primitive::<Int64Type>().value(r),
831                    es.len() as i64
832                );
833                out.push((vs, es));
834            }
835        }
836        out
837    }
838
839    // DuckPGQ's test graph: Daniel 0, Tavneet 1, Gabor 2, Peter 3, David 4.
840    const KNOW: &[(i64, i64)] = &[
841        (0, 1),
842        (0, 2),
843        (0, 3),
844        (3, 0),
845        (1, 2),
846        (1, 3),
847        (2, 3),
848        (4, 3),
849    ];
850
851    #[test]
852    fn any_shortest_takes_the_first_edges_and_can_return_to_the_start() {
853        let found = paths(
854            KNOW,
855            &[4],
856            search(
857                Direction::Right,
858                1,
859                Some(3),
860                Selector::Shortest(1),
861                Mode::Walk,
862            ),
863        );
864        assert_eq!(
865            found,
866            vec![
867                (vec![4, 3], vec![7]),
868                (vec![4, 3, 0], vec![7, 3]),
869                (vec![4, 3, 0, 1], vec![7, 3, 0]),
870                (vec![4, 3, 0, 2], vec![7, 3, 1]),
871            ]
872        );
873        // From 0, a shortest walk of one edge or more back to 0 is 0→3→0.
874        let found = paths(
875            KNOW,
876            &[0],
877            search(
878                Direction::Right,
879                1,
880                Some(3),
881                Selector::Shortest(1),
882                Mode::Walk,
883            ),
884        );
885        assert!(found.contains(&(vec![0, 3, 0], vec![2, 3])));
886        // ACYCLIC drops it.
887        let found = paths(
888            KNOW,
889            &[0],
890            search(
891                Direction::Right,
892                1,
893                Some(3),
894                Selector::Shortest(1),
895                Mode::Acyclic,
896            ),
897        );
898        assert!(found.iter().all(|(v, _)| v.last() != Some(&0)));
899    }
900
901    #[test]
902    fn walks_trails_and_counts() {
903        // Every walk of 1 or 2 edges from Peter: 3→0, then 0→1, 0→2, 0→3.
904        let found = paths(
905            KNOW,
906            &[3],
907            search(Direction::Right, 1, Some(2), Selector::All, Mode::Walk),
908        );
909        assert_eq!(found.len(), 4);
910        // Without a bound under ACYCLIC: every simple path, finitely many.
911        let acyclic = paths(
912            KNOW,
913            &[3],
914            search(Direction::Right, 1, None, Selector::All, Mode::Acyclic),
915        );
916        assert!(acyclic.iter().all(|(v, _)| {
917            let mut s = v.clone();
918            s.sort();
919            s.dedup();
920            s.len() == v.len()
921        }));
922        // 3→0, 3→0→1, 3→0→2 and 3→0→1→2.
923        assert_eq!(acyclic.len(), 4);
924        // SHORTEST 2 from 0 to 3: 0→3 (edge 2), then 0→1→3 (0, 5) before 0→2→3 (1, 6).
925        let found = paths(
926            KNOW,
927            &[0],
928            search(Direction::Right, 1, None, Selector::Shortest(2), Mode::Walk),
929        );
930        let to3: Vec<_> = found.iter().filter(|(v, _)| v.last() == Some(&3)).collect();
931        assert_eq!(
932            to3,
933            vec![&(vec![0, 3], vec![2]), &(vec![0, 1, 3], vec![0, 5])]
934        );
935        // ALL SHORTEST from 0 to 3 is the one edge; to 2: 0→2 alone.
936        let found = paths(
937            KNOW,
938            &[1],
939            search(Direction::Right, 1, None, Selector::AllShortest, Mode::Walk),
940        );
941        let to0: Vec<_> = found.iter().filter(|(v, _)| v.last() == Some(&0)).collect();
942        assert_eq!(to0, vec![&(vec![1, 3, 0], vec![5, 3])]);
943        let found = paths(
944            KNOW,
945            &[4],
946            search(
947                Direction::Right,
948                3,
949                Some(3),
950                Selector::AllShortest,
951                Mode::Walk,
952            ),
953        );
954        let to2: Vec<_> = found.iter().filter(|(v, _)| v.last() == Some(&2)).collect();
955        assert_eq!(to2, vec![&(vec![4, 3, 0, 2], vec![7, 3, 1])]);
956    }
957
958    #[test]
959    fn zero_length_undirected_and_trails() {
960        // A start with no edges has its path of no edges.
961        let found = paths(
962            KNOW,
963            &[9, 0],
964            search(Direction::Right, 0, Some(0), Selector::All, Mode::Walk),
965        );
966        assert_eq!(found, vec![(vec![9], vec![]), (vec![0], vec![])]);
967        // Undirected, a self-loop is followed once.
968        let found = paths(
969            &[(1, 1), (1, 2)],
970            &[1],
971            search(Direction::Any, 1, Some(1), Selector::All, Mode::Walk),
972        );
973        assert_eq!(found, vec![(vec![1, 1], vec![0]), (vec![1, 2], vec![1])]);
974        // An undirected trail back to the start cannot reuse its edge: 1-2-1
975        // takes the two parallel edges.
976        let found = paths(
977            &[(1, 2), (2, 1)],
978            &[1],
979            search(Direction::Any, 1, Some(2), Selector::All, Mode::Trail),
980        );
981        assert_eq!(
982            found,
983            vec![
984                (vec![1, 2], vec![0]),
985                (vec![1, 2], vec![1]),
986                (vec![1, 2, 1], vec![0, 1]),
987                (vec![1, 2, 1], vec![1, 0]),
988            ]
989        );
990        // Left: against the edges.
991        let found = paths(
992            KNOW,
993            &[3],
994            search(Direction::Left, 1, Some(1), Selector::All, Mode::Walk),
995        );
996        assert_eq!(
997            found,
998            vec![
999                (vec![3, 0], vec![2]),
1000                (vec![3, 1], vec![5]),
1001                (vec![3, 2], vec![6]),
1002                (vec![3, 4], vec![7])
1003            ]
1004        );
1005        // SIMPLE: back to the start is allowed, once.
1006        let found = paths(
1007            KNOW,
1008            &[0],
1009            search(Direction::Right, 2, Some(2), Selector::All, Mode::Simple),
1010        );
1011        assert!(found.contains(&(vec![0, 3, 0], vec![2, 3])));
1012    }
1013}