Skip to main content

ursa_engine/graph/
pattern.rs

1//! SQL/PGQ pattern matching: `GRAPH_TABLE (g MATCH … COLUMNS (…))` (#296).
2//!
3//! Ursa's parser rewrites `GRAPH_TABLE ( body )` into `graph_table('body')`;
4//! the relation planner hands the body here. [`parse`] reads it into a
5//! [`Pattern`]; [`lower`] resolves each variable to one of the graph's element
6//! tables and says what to join: a [`Lowered`] match, which the planner
7//! builds as a logical plan (scans of the element tables, equality
8//! conditions, the pattern's conditions and `COLUMNS`). DataFusion's
9//! optimizer turns the equalities into hash joins.
10//!
11//! Supported:
12//!
13//! ```text
14//! GRAPH_TABLE ( graph
15//!   MATCH path [, path …]
16//!   [WHERE condition]
17//!   COLUMNS ( expression [AS name], … ) )
18//!
19//! path       := [var =] [selector] [mode [PATH | PATHS]] node (edge node)*
20//! selector   := ANY SHORTEST | ALL SHORTEST | ANY [k] | SHORTEST k | ALL
21//! mode       := WALK | TRAIL | SIMPLE | ACYCLIC
22//! node       := ( [var] [:label | IS label] [WHERE condition] )
23//! edge       := (-[ spec ]->  |  <-[ spec ]-  |  -[ spec ]-  |  ->  |  <-  |  -) [quantifier]
24//! quantifier := * | + | ? | {n} | {m,n} | {m,} | {,n}
25//! spec       := [var] [:label | IS label] [WHERE condition]
26//! ```
27//!
28//! Conditions and `COLUMNS` are SQL expressions over the variables
29//! (`a.name`); each variable is a relation of that name. A label names an element table: by its name or one of
30//! its labels. A variable without one gets the only table it can be: from the
31//! edges next to it and their `REFERENCES`, or because the graph has one table
32//! of that kind; otherwise the pattern is refused, never guessed.
33//!
34//! Each variable reads its element table's properties: every column, or what
35//! its labels' `PROPERTIES` clauses give, under their property names. An
36//! undirected edge `-[e]-` matches both orientations of each edge (a self-loop
37//! once): the match is a `UNION ALL` with one branch per orientation, so every
38//! join stays an equality join.
39//!
40//! A quantified edge, and the edge of a path with a path variable, a selector
41//! or a mode, is not joined: it is a [`Segment`], searched on the graph index
42//! (see [`super::path_search`]), and joined to the nodes at its ends. A path
43//! with a variable, a selector or a mode is one edge between two nodes for
44//! now. Label expressions (`|`, `&`, `!`) and parenthesized path patterns are
45//! refused with a message.
46//!
47//! ## Through the index
48//!
49//! A node is *bound* when a condition on it alone limits its key to values
50//! the query names: `a.id = 7`, `a.id IN (1, 2)` or `a.id IN (SELECT …)`.
51//! From a bound node, the fixed-length edges of its path are expanded
52//! through the graph index too, one [`Segment`] of exactly one edge each,
53//! outward from it: each segment starts from the keys the one before it
54//! reached ([`Starts::After`]), so only the edges at the nodes reached are
55//! read, never the whole edge table. A segment is a relation of edges as the
56//! join's is, one row per edge row, so the rows are the same. A node whose
57//! query reads only its key, at the end of a segment whose index's nodes are
58//! that node's table's keys, is not joined to its table at all: its key is
59//! the segment's column ([`Lowered::aliases`]). Edges stay joins when no node
60//! is bound, when the query reads an edge's columns, or when
61//! [`Usage::indexed`] does not list the edge table (its index could not be
62//! kept between queries).
63
64use std::collections::{BTreeMap, BTreeSet};
65
66use datafusion::arrow::datatypes::SchemaRef;
67use datafusion::common::TableReference;
68use datafusion::error::{DataFusionError, Result};
69use datafusion::sql::sqlparser::dialect::GenericDialect;
70use datafusion::sql::sqlparser::tokenizer::{Location, Token, TokenWithSpan, Tokenizer};
71
72use super::{EdgeTable, Label, Properties, PropertyGraph, VertexTable};
73
74/// The table function Ursa's parser rewrites `GRAPH_TABLE ( … )` into.
75pub const FUNCTION: &str = "graph_table";
76
77/// Undirected edges allowed in one pattern: each doubles the query's branches.
78const MAX_UNDIRECTED: usize = 3;
79
80/// A parsed `GRAPH_TABLE` body.
81#[derive(Debug, Clone, PartialEq)]
82pub struct Pattern {
83    /// The graph's name, as written.
84    pub graph: String,
85    pub paths: Vec<PathPattern>,
86    /// The pattern's `WHERE` condition, as written.
87    pub condition: Option<String>,
88    /// The `COLUMNS` list, as written (without its parentheses).
89    pub columns: String,
90}
91
92/// One path of a `MATCH`: `[p =] [selector] [mode] (a)-[e]->…(b)`.
93#[derive(Debug, Clone, PartialEq)]
94pub struct PathPattern {
95    /// The path variable, `p` in `p = …`.
96    pub var: Option<String>,
97    pub selector: Selector,
98    pub mode: Mode,
99    /// Nodes and edges alternate, starting and ending with a node.
100    pub elements: Vec<Element>,
101}
102
103/// Which of a pair of nodes' paths a path keeps.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
105pub enum Selector {
106    /// No selector (or `ALL`): every path.
107    All,
108    /// `ANY SHORTEST`, `ANY`, `ANY k` and `SHORTEST k`: the `k` shortest
109    /// paths, the first by edge row among paths as short (see [`super::path_search`]).
110    Shortest(u32),
111    /// `ALL SHORTEST`: every path as short as the shortest.
112    AllShortest,
113}
114
115/// Which walks a path may be (SQL/PGQ's path modes).
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
117pub enum Mode {
118    /// Any walk: nodes and edges may repeat (the default).
119    Walk,
120    /// No edge twice.
121    Trail,
122    /// No node twice, except that the last may be the first.
123    Simple,
124    /// No node twice.
125    Acyclic,
126}
127
128/// An edge's quantifier: how many times it repeats, `{min,max}`.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
130pub struct Quantifier {
131    pub min: u32,
132    /// `None`: no upper bound.
133    pub max: Option<u32>,
134}
135
136impl Quantifier {
137    /// An edge without a quantifier: exactly once.
138    pub const ONCE: Quantifier = Quantifier {
139        min: 1,
140        max: Some(1),
141    };
142}
143
144/// One element of a path: nodes and edges alternate, starting with a node.
145#[derive(Debug, Clone, PartialEq)]
146pub enum Element {
147    Node(Spec),
148    /// An edge, with its quantifier if it has one.
149    Edge(Spec, Direction, Option<Quantifier>),
150}
151
152/// A node or edge: `[var] [:label] [WHERE condition]`.
153#[derive(Debug, Clone, PartialEq, Default)]
154pub struct Spec {
155    pub var: Option<String>,
156    pub label: Option<String>,
157    pub condition: Option<String>,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
161pub enum Direction {
162    /// `-[e]->`: from the node on the left to the node on the right.
163    Right,
164    /// `<-[e]-`: from the node on the right to the node on the left.
165    Left,
166    /// `-[e]-`: either way.
167    Any,
168}
169
170fn pattern_err<T>(message: impl Into<String>) -> Result<T> {
171    Err(DataFusionError::Plan(format!(
172        "in GRAPH_TABLE: {}",
173        message.into()
174    )))
175}
176
177/// The body's tokens, without whitespace and comments, with their byte ranges.
178struct Tokens<'a> {
179    text: &'a str,
180    tokens: Vec<(Token, usize, usize)>,
181    at: usize,
182}
183
184impl<'a> Tokens<'a> {
185    fn new(text: &'a str) -> Result<Self> {
186        let tokens = Tokenizer::new(&GenericDialect {}, text)
187            .tokenize_with_location()
188            .map_err(|e| DataFusionError::Plan(format!("in GRAPH_TABLE: {e}")))?;
189        let tokens = tokens
190            .into_iter()
191            .filter(|t| !matches!(t.token, Token::Whitespace(_)))
192            .map(|TokenWithSpan { token, span }| {
193                let start = offset(text, span.start).unwrap_or(text.len());
194                let end = offset(text, span.end).unwrap_or(text.len());
195                (token, start, end)
196            })
197            .filter(|(token, _, _)| *token != Token::EOF)
198            .collect();
199        Ok(Self {
200            text,
201            tokens,
202            at: 0,
203        })
204    }
205
206    fn peek(&self, n: usize) -> Option<&Token> {
207        self.tokens.get(self.at + n).map(|(t, _, _)| t)
208    }
209
210    fn next(&mut self) -> Option<Token> {
211        let token = self.tokens.get(self.at).map(|(t, _, _)| t.clone());
212        self.at += 1;
213        token
214    }
215
216    fn is_word(&self, n: usize, word: &str) -> bool {
217        matches!(self.peek(n), Some(Token::Word(w)) if w.quote_style.is_none() && w.value.eq_ignore_ascii_case(word))
218    }
219
220    fn eat(&mut self, token: &Token) -> bool {
221        if self.peek(0) == Some(token) {
222            self.at += 1;
223            true
224        } else {
225            false
226        }
227    }
228
229    /// What the next token is, for an error: "'x' (at character 12)" or "the end".
230    fn found(&self) -> String {
231        match self.tokens.get(self.at) {
232            Some((token, start, _)) => {
233                format!("'{token}' (at character {} of the pattern)", start + 1)
234            }
235            None => "the end".to_string(),
236        }
237    }
238
239    fn expect(&mut self, token: &Token, what: &str) -> Result<()> {
240        if self.eat(token) {
241            Ok(())
242        } else {
243            pattern_err(format!("expected {what}, found {}", self.found()))
244        }
245    }
246
247    /// An identifier: an unquoted word (normalized as DataFusion normalizes
248    /// identifiers: lower-cased) or a quoted one (kept).
249    fn identifier(&mut self, what: &str) -> Result<String> {
250        match self.peek(0) {
251            Some(Token::Word(w)) => {
252                let name = match w.quote_style {
253                    Some(_) => w.value.clone(),
254                    None => w.value.to_lowercase(),
255                };
256                self.at += 1;
257                Ok(name)
258            }
259            _ => pattern_err(format!("expected {what}, found {}", self.found())),
260        }
261    }
262
263    /// The text from the current token up to (not including) the first token
264    /// at bracket depth 0 for which `stop` holds; the tokens are consumed.
265    fn text_until(&mut self, stop: impl Fn(&Self, usize) -> bool) -> Option<String> {
266        let start = self.at;
267        let mut depth = 0i32;
268        while self.at < self.tokens.len() {
269            let token = &self.tokens[self.at].0;
270            if depth == 0 && stop(self, 0) {
271                break;
272            }
273            match token {
274                Token::LParen | Token::LBracket => depth += 1,
275                Token::RParen | Token::RBracket => {
276                    if depth == 0 {
277                        break;
278                    }
279                    depth -= 1;
280                }
281                _ => {}
282            }
283            self.at += 1;
284        }
285        if self.at == start {
286            return None;
287        }
288        let from = self.tokens[start].1;
289        let to = self.tokens[self.at - 1].2;
290        Some(self.text[from..to].trim().to_string())
291    }
292}
293
294/// The byte offset of a tokenizer location in `text`.
295fn offset(text: &str, at: Location) -> Option<usize> {
296    if at.line == 0 || at.column == 0 {
297        return None;
298    }
299    let mut line_start = 0;
300    for _ in 1..at.line {
301        line_start += text[line_start..].find('\n')? + 1;
302    }
303    let column = usize::try_from(at.column - 1).ok()?;
304    let line = &text[line_start..];
305    match line.char_indices().nth(column) {
306        Some((o, _)) => Some(line_start + o),
307        None if line.chars().count() == column => Some(text.len()),
308        None => None,
309    }
310}
311
312/// Parse a `GRAPH_TABLE` body: `graph MATCH … [WHERE …] COLUMNS (…)`.
313pub fn parse(body: &str) -> Result<Pattern> {
314    let mut t = Tokens::new(body)?;
315    let graph = t.identifier("the graph's name")?;
316    if !t.is_word(0, "MATCH") {
317        return pattern_err(format!(
318            "expected MATCH after the graph's name, found {}",
319            t.found()
320        ));
321    }
322    t.next();
323    let mut paths = vec![parse_path(&mut t)?];
324    while t.eat(&Token::Comma) {
325        paths.push(parse_path(&mut t)?);
326    }
327    let condition = if t.is_word(0, "WHERE") {
328        t.next();
329        let condition = t.text_until(|t, n| t.is_word(n, "COLUMNS"));
330        if condition.is_none() {
331            return pattern_err("WHERE needs a condition");
332        }
333        condition
334    } else {
335        None
336    };
337    if !t.is_word(0, "COLUMNS") {
338        if t.peek(0).is_none() {
339            return pattern_err(
340                "a GRAPH_TABLE needs COLUMNS (…), listing what to return, such as COLUMNS (a.id AS a, b.id AS b)",
341            );
342        }
343        return pattern_err(format!(
344            "expected ',', WHERE or COLUMNS after the pattern, found {}",
345            t.found()
346        ));
347    }
348    t.next();
349    t.expect(&Token::LParen, "'(' after COLUMNS")?;
350    let Some(columns) = t.text_until(|_, _| false) else {
351        return pattern_err("COLUMNS (…) lists nothing");
352    };
353    t.expect(&Token::RParen, "')' to close COLUMNS")?;
354    if t.peek(0).is_some() {
355        return pattern_err(format!("unexpected {} after COLUMNS (…)", t.found()));
356    }
357    Ok(Pattern {
358        graph,
359        paths,
360        condition,
361        columns,
362    })
363}
364
365/// A path: `[p =] [selector] [mode [PATH | PATHS]]`, then a node, then edges
366/// and nodes.
367fn parse_path(t: &mut Tokens) -> Result<PathPattern> {
368    let var = if matches!(t.peek(0), Some(Token::Word(_))) && t.peek(1) == Some(&Token::Eq) {
369        let var = t.identifier("a path variable")?;
370        t.next();
371        Some(var)
372    } else {
373        None
374    };
375    let selector = parse_selector(t)?;
376    let mode = if t.is_word(0, "WALK") {
377        Some(Mode::Walk)
378    } else if t.is_word(0, "TRAIL") {
379        Some(Mode::Trail)
380    } else if t.is_word(0, "SIMPLE") {
381        Some(Mode::Simple)
382    } else if t.is_word(0, "ACYCLIC") {
383        Some(Mode::Acyclic)
384    } else {
385        None
386    };
387    if mode.is_some() {
388        t.next();
389    }
390    if t.is_word(0, "PATH") || t.is_word(0, "PATHS") {
391        t.next();
392    }
393    if t.peek(0) == Some(&Token::LParen) && t.peek(1) == Some(&Token::LParen) {
394        return pattern_err(
395            "parenthesized path patterns, as ((a)-[e]->(b))*, are not supported yet; quantify an edge instead, as (a)-[e]->*(b)",
396        );
397    }
398    let mut elements = vec![Element::Node(parse_node(t)?)];
399    while let Some((spec, direction, quantifier)) = parse_edge(t)? {
400        elements.push(Element::Edge(spec, direction, quantifier));
401        elements.push(Element::Node(parse_node(t)?));
402    }
403    Ok(PathPattern {
404        var,
405        selector,
406        mode: mode.unwrap_or(Mode::Walk),
407        elements,
408    })
409}
410
411/// `ANY SHORTEST`, `ALL SHORTEST`, `ANY [k]`, `SHORTEST k`, `ALL`, or none.
412fn parse_selector(t: &mut Tokens) -> Result<Selector> {
413    let count = |t: &mut Tokens, what: &str| -> Result<Option<u32>> {
414        let Some(Token::Number(n, _)) = t.peek(0) else {
415            return Ok(None);
416        };
417        let k = n.parse::<u32>().ok().filter(|&k| k > 0).ok_or_else(|| {
418            DataFusionError::Plan(format!(
419                "in GRAPH_TABLE: {what} takes a whole number of paths above 0; got {n}"
420            ))
421        })?;
422        t.next();
423        Ok(Some(k))
424    };
425    if t.is_word(0, "ANY") && t.is_word(1, "SHORTEST") {
426        t.next();
427        t.next();
428        if matches!(t.peek(0), Some(Token::Number(..))) {
429            return pattern_err(
430                "ANY SHORTEST keeps one path; for several, write SHORTEST k, as SHORTEST 3",
431            );
432        }
433        return Ok(Selector::Shortest(1));
434    }
435    if t.is_word(0, "ALL") && t.is_word(1, "SHORTEST") {
436        t.next();
437        t.next();
438        return Ok(Selector::AllShortest);
439    }
440    if t.is_word(0, "ANY") {
441        t.next();
442        return Ok(Selector::Shortest(count(t, "ANY")?.unwrap_or(1)));
443    }
444    if t.is_word(0, "SHORTEST") {
445        t.next();
446        let Some(k) = count(t, "SHORTEST")? else {
447            return pattern_err(format!(
448                "SHORTEST takes a number of paths, as SHORTEST 3; found {}",
449                t.found()
450            ));
451        };
452        if t.is_word(0, "GROUP") || t.is_word(0, "GROUPS") {
453            return pattern_err(
454                "SHORTEST k GROUP is not supported yet; use SHORTEST k or ALL SHORTEST",
455            );
456        }
457        return Ok(Selector::Shortest(k));
458    }
459    if t.is_word(0, "ALL") {
460        t.next();
461    }
462    Ok(Selector::All)
463}
464
465fn parse_node(t: &mut Tokens) -> Result<Spec> {
466    t.expect(&Token::LParen, "'(' to start a node")?;
467    let spec = parse_spec(t, &Token::RParen)?;
468    t.expect(&Token::RParen, "')' to close the node")?;
469    Ok(spec)
470}
471
472/// `[var] [:label | IS label] [WHERE condition]`, up to `close`.
473fn parse_spec(t: &mut Tokens, close: &Token) -> Result<Spec> {
474    let mut spec = Spec::default();
475    if matches!(t.peek(0), Some(Token::Word(_))) && !t.is_word(0, "WHERE") && !t.is_word(0, "IS") {
476        spec.var = Some(t.identifier("a variable")?);
477    }
478    if t.eat(&Token::Colon) || (t.is_word(0, "IS") && t.next().is_some()) {
479        spec.label = Some(t.identifier("a label")?);
480        if matches!(
481            t.peek(0),
482            Some(Token::Pipe | Token::Ampersand | Token::ExclamationMark | Token::Mod)
483        ) {
484            return pattern_err(
485                "label expressions (|, &, !, %) are not supported yet; use one label (#297)",
486            );
487        }
488    }
489    if t.is_word(0, "WHERE") {
490        t.next();
491        let close = close.clone();
492        spec.condition = t.text_until(move |t, n| t.peek(n) == Some(&close));
493        if spec.condition.is_none() {
494            return pattern_err("WHERE needs a condition");
495        }
496    }
497    if t.peek(0) != Some(close) {
498        return pattern_err(format!(
499            "expected a variable, ':label', WHERE or '{close}', found {}",
500            t.found()
501        ));
502    }
503    Ok(spec)
504}
505
506/// An edge, if one is next: `-[…]->`, `<-[…]-`, `-[…]-`, `->`, `<-` or `-`,
507/// and its quantifier, if it has one.
508fn parse_edge(t: &mut Tokens) -> Result<Option<(Spec, Direction, Option<Quantifier>)>> {
509    let edge = match (t.peek(0), t.peek(1)) {
510        // Abbreviated edges.
511        (Some(Token::Arrow), _) => {
512            t.next();
513            (Spec::default(), Direction::Right)
514        }
515        (Some(Token::Lt), Some(Token::Minus)) if t.peek(2) != Some(&Token::LBracket) => {
516            t.next();
517            t.next();
518            (Spec::default(), Direction::Left)
519        }
520        (Some(Token::Minus), Some(Token::LBracket)) => {
521            t.next();
522            t.next();
523            let spec = parse_spec(t, &Token::RBracket)?;
524            t.expect(&Token::RBracket, "']' to close the edge")?;
525            let direction = match t.next() {
526                Some(Token::Arrow) => Direction::Right,
527                // `- >`, as DuckPGQ also reads it.
528                Some(Token::Minus) if t.peek(0) == Some(&Token::Gt) => {
529                    t.next();
530                    Direction::Right
531                }
532                Some(Token::Minus) => Direction::Any,
533                Some(Token::LongArrow) => {
534                    return pattern_err("expected '->' or '-' after ']', found '->>'")
535                }
536                _ => {
537                    t.at -= 1;
538                    return pattern_err(format!(
539                        "expected '->' or '-' after ']', found {}",
540                        t.found()
541                    ));
542                }
543            };
544            (spec, direction)
545        }
546        (Some(Token::Lt), Some(Token::Minus)) => {
547            t.next();
548            t.next();
549            t.expect(&Token::LBracket, "'[' after '<-'")?;
550            let spec = parse_spec(t, &Token::RBracket)?;
551            t.expect(&Token::RBracket, "']' to close the edge")?;
552            t.expect(&Token::Minus, "'-' after ']'")?;
553            (spec, Direction::Left)
554        }
555        (Some(Token::Minus), _) => {
556            t.next();
557            (Spec::default(), Direction::Any)
558        }
559        _ => return Ok(None),
560    };
561    let quantifier = parse_quantifier(t)?;
562    Ok(Some((edge.0, edge.1, quantifier)))
563}
564
565/// `*`, `+`, `?`, `{n}`, `{m,n}`, `{m,}`, `{,n}` or `{,}`, if one is next.
566fn parse_quantifier(t: &mut Tokens) -> Result<Option<Quantifier>> {
567    let q = |min, max| Ok(Some(Quantifier { min, max }));
568    match t.peek(0) {
569        Some(Token::Mul) => {
570            t.next();
571            q(0, None)
572        }
573        Some(Token::Plus) => {
574            t.next();
575            q(1, None)
576        }
577        Some(Token::Placeholder(p)) if p == "?" => {
578            t.next();
579            q(0, Some(1))
580        }
581        Some(Token::LBrace) => {
582            t.next();
583            let bound = |t: &mut Tokens| -> Result<Option<u32>> {
584                match t.peek(0) {
585                    Some(Token::Number(n, _)) => {
586                        let n = n.parse::<u32>().map_err(|_| {
587                            DataFusionError::Plan(format!(
588                                "in GRAPH_TABLE: a quantifier's bounds are whole numbers from 0; got {n}"
589                            ))
590                        })?;
591                        t.next();
592                        Ok(Some(n))
593                    }
594                    _ => Ok(None),
595                }
596            };
597            let min = bound(t)?;
598            let max = if t.eat(&Token::Comma) {
599                bound(t)?
600            } else if min.is_some() {
601                min
602            } else {
603                return pattern_err(format!(
604                    "expected a number or ',' in the quantifier, found {}",
605                    t.found()
606                ));
607            };
608            t.expect(&Token::RBrace, "'}' to close the quantifier")?;
609            let min = min.unwrap_or(0);
610            if max.is_some_and(|max| max < min) {
611                return pattern_err(format!(
612                    "the quantifier's lower bound {min} is above its upper bound {}",
613                    max.unwrap()
614                ));
615            }
616            q(min, max)
617        }
618        _ => Ok(None),
619    }
620}
621
622/// Where a vertex variable reads from.
623#[derive(Debug, Clone)]
624enum VertexSource<'g> {
625    Table(&'g VertexTable),
626    /// The graph's `nodes` table: a graph without vertex tables.
627    Nodes,
628}
629
630/// One variable of the pattern.
631#[derive(Debug, Default)]
632struct Binding<'g> {
633    is_edge: bool,
634    label: Option<String>,
635    vertex: Option<VertexSource<'g>>,
636    edge: Option<&'g EdgeTable>,
637    conditions: Vec<String>,
638}
639
640/// One edge of the pattern, with its two ends.
641struct Step {
642    left: String,
643    edge: String,
644    right: String,
645    direction: Direction,
646    quantifier: Option<Quantifier>,
647    /// The path it is in.
648    path: usize,
649}
650
651/// A column of a variable: (variable, column).
652pub type VarColumn = (String, String);
653
654/// What a variable reads: an element table (or the graph's `nodes` table),
655/// all its columns or `columns` as (column, property name).
656#[derive(Debug, Clone, PartialEq)]
657pub struct Source {
658    pub var: String,
659    pub table: TableReference,
660    /// `None`: every column, under its own name.
661    pub columns: Option<Vec<(String, String)>>,
662    /// Whether the match joins it: a node whose key is a segment's column
663    /// ([`Lowered::aliases`]) is read only for its start keys, if at all.
664    pub joined: bool,
665}
666
667/// A variable's columns, as [`Source::columns`] lists them.
668type Columns = Option<Vec<(String, String)>>;
669
670/// A condition the lowering adds.
671#[derive(Debug, Clone, PartialEq)]
672pub enum Join {
673    Equal(VarColumn, VarColumn),
674    /// Not equal: keeps a self-loop out of an undirected edge's reverse branch.
675    NotEqual(VarColumn, VarColumn),
676}
677
678/// An edge searched on the graph index rather than joined: a quantified
679/// edge, or the edge of a path with a path variable, a selector or a mode.
680/// It is a relation of paths, named `var`, with the columns of
681/// [`super::path_search`]: the path's first and last node, its length, and
682/// its nodes and edges.
683#[derive(Debug, Clone, PartialEq)]
684pub struct Segment {
685    /// The relation's name: the path variable, or an unnamed one.
686    pub var: String,
687    /// The path variable, if the path has one.
688    pub path: Option<String>,
689    /// The edge table, and its source and destination columns.
690    pub edges: TableReference,
691    pub source: String,
692    pub destination: String,
693    /// The node variables at its two ends, and their keys.
694    pub start: String,
695    pub start_key: VarColumn,
696    pub end: String,
697    pub end_key: VarColumn,
698    pub direction: Direction,
699    pub quantifier: Quantifier,
700    pub selector: Selector,
701    pub mode: Mode,
702    /// A fixed-length edge expanded through the index, not a searched path.
703    pub fixed: bool,
704    pub starts: Starts,
705    /// Whether the index's nodes are the start node's table's keys, so a
706    /// start that is not a node of the index is no node at all.
707    pub known_only: bool,
708    /// The keys the last node can have, when they are known: the values its
709    /// conditions name ([`Starts::Node`]: it is bound, see [`Usage::bound`]),
710    /// or the keys an earlier segment reached. The search's targets.
711    pub targets: Option<Starts>,
712}
713
714/// What the query reads and binds, from its conditions and `COLUMNS`: what
715/// decides which edges are expanded through the graph index (see the module
716/// docs). The default plans joins, as a pattern was planned before.
717#[derive(Debug, Clone, Default, PartialEq)]
718pub struct Usage {
719    /// Whether the rest was found: if not, every edge is joined and every
720    /// node too.
721    pub enabled: bool,
722    /// Per variable, the properties the query reads of it: `None` for all of
723    /// them (`v.*`). A variable that is not listed is not read.
724    pub reads: BTreeMap<String, Option<BTreeSet<String>>>,
725    /// Per variable with a condition on it alone that limits a property to
726    /// values the query names (`v.p = 1`, `v.p IN (…)`), those properties.
727    pub bound: BTreeMap<String, BTreeSet<String>>,
728    /// The edge tables whose index is kept between queries: the ones fixed
729    /// edges may be expanded on. Empty: every edge is joined.
730    pub indexed: BTreeSet<TableReference>,
731}
732
733/// Where a segment's start nodes come from.
734#[derive(Debug, Clone, Copy, PartialEq)]
735pub enum Starts {
736    /// The start node's keys that the conditions on it alone keep.
737    Node,
738    /// The keys an earlier segment reached: its last node's (`end`), or its
739    /// first's.
740    After { segment: usize, end: bool },
741}
742
743/// A node not joined to its table: its key is a segment's column.
744#[derive(Debug, Clone, PartialEq)]
745pub struct Alias {
746    pub var: String,
747    /// The segment column its key is.
748    pub column: VarColumn,
749}
750
751/// A pattern resolved against a graph: what to scan and join.
752#[derive(Debug, Clone, PartialEq)]
753pub struct Lowered {
754    /// One per variable, in order of appearance, except the edges searched as
755    /// segments.
756    pub sources: Vec<Source>,
757    /// The edges searched on the graph index, joined to their end nodes in
758    /// every branch. A segment's [`Starts::After`] names an earlier one.
759    pub segments: Vec<Segment>,
760    /// The nodes whose sources are not joined: their keys are segment columns.
761    pub aliases: Vec<Alias>,
762    /// One per orientation of the undirected edges: the joins of each branch
763    /// of the `UNION ALL` (one branch when every edge has a direction).
764    pub branches: Vec<Vec<Join>>,
765    /// Whether the match planned as joins would be a `UNION ALL`: it has an
766    /// undirected fixed edge, joined or not.
767    pub union: bool,
768    /// The pattern's conditions as written: in elements, then after the paths.
769    pub conditions: Vec<String>,
770    /// The `COLUMNS` list as written.
771    pub columns: String,
772}
773
774/// Resolve `pattern` against `graph`. `schema_of` gives a table's columns
775/// (for properties).
776pub fn lower(
777    pattern: &Pattern,
778    graph: &PropertyGraph,
779    schema_of: &dyn Fn(&TableReference) -> Result<SchemaRef>,
780    usage: &Usage,
781) -> Result<Lowered> {
782    // Collect the variables, naming anonymous ones.
783    let mut bindings: BTreeMap<String, Binding> = BTreeMap::new();
784    let mut order: Vec<String> = Vec::new();
785    let mut steps = Vec::new();
786    let mut anonymous = 0;
787    for (path_index, path) in pattern.paths.iter().enumerate() {
788        // (left node, direction, edge, quantifier)
789        let mut previous: Option<(String, Direction, String, Option<Quantifier>)> = None;
790        for element in &path.elements {
791            let (spec, is_edge) = match element {
792                Element::Node(spec) => (spec, false),
793                Element::Edge(spec, _, _) => (spec, true),
794            };
795            let var = match &spec.var {
796                Some(v) => v.clone(),
797                None => {
798                    anonymous += 1;
799                    format!("__ursa_{anonymous}")
800                }
801            };
802            let binding = bindings.entry(var.clone()).or_insert_with(|| {
803                order.push(var.clone());
804                Binding {
805                    is_edge,
806                    ..Binding::default()
807                }
808            });
809            if binding.is_edge != is_edge {
810                return pattern_err(format!("'{var}' is used for both a node and an edge"));
811            }
812            if is_edge
813                && (binding.label.is_some()
814                    || !binding.conditions.is_empty()
815                    || steps.iter().any(|s: &Step| s.edge == var))
816            {
817                return pattern_err(format!(
818                    "edge variable '{var}' appears twice; an edge variable binds one edge"
819                ));
820            }
821            if let Some(label) = &spec.label {
822                if binding.label.as_ref().is_some_and(|l| l != label) {
823                    return pattern_err(format!(
824                        "'{var}' has two labels, {} and {label}",
825                        binding.label.as_ref().unwrap()
826                    ));
827                }
828                binding.label = Some(label.clone());
829            }
830            binding.conditions.extend(spec.condition.clone());
831            match element {
832                Element::Node(_) => {
833                    if let Some((left, direction, edge, quantifier)) = previous.take() {
834                        steps.push(Step {
835                            left,
836                            edge,
837                            right: var.clone(),
838                            direction,
839                            quantifier,
840                            path: path_index,
841                        });
842                    }
843                    previous = Some((var, Direction::Right, String::new(), None));
844                }
845                Element::Edge(_, direction, quantifier) => {
846                    let (left, ..) = previous.take().expect("a path starts with a node");
847                    previous = Some((left, *direction, var, *quantifier));
848                }
849            }
850        }
851    }
852
853    // Labels as written.
854    for (var, b) in bindings.iter_mut() {
855        let Some(label) = &b.label else { continue };
856        if b.is_edge {
857            let tables: Vec<&EdgeTable> = graph
858                .edge_tables
859                .iter()
860                .filter(|e| e.answers_to(label))
861                .collect();
862            match tables.as_slice() {
863                [only] => b.edge = Some(only),
864                [] => return pattern_err(format!(
865                    "graph '{}' has no edge table or edge label '{label}' (for '{}'); its edge tables are {}",
866                    graph.name, shown(var), names(graph.edge_tables.iter().map(|e| e.name.as_str()))
867                )),
868                _ => return pattern_err(format!(
869                    "label '{label}' is on several edge tables; a label shared by several tables is not supported yet"
870                )),
871            }
872        } else {
873            let tables: Vec<&VertexTable> = graph
874                .vertex_tables
875                .iter()
876                .filter(|v| v.answers_to(label))
877                .collect();
878            match tables.as_slice() {
879                [only] => b.vertex = Some(VertexSource::Table(only)),
880                [] => return pattern_err(format!(
881                    "graph '{}' has no vertex table or vertex label '{label}' (for '{}'); its vertex tables are {}",
882                    graph.name, shown(var), names(graph.vertex_tables.iter().map(|v| v.name.as_str()))
883                )),
884                _ => return pattern_err(format!(
885                    "label '{label}' is on several vertex tables; a label shared by several tables is not supported yet"
886                )),
887            }
888        }
889    }
890
891    // Infer the rest from the edges' REFERENCES, until nothing changes.
892    loop {
893        let mut changed = false;
894        for step in &steps {
895            let left = vertex_table(&bindings[&step.left]);
896            let right = vertex_table(&bindings[&step.right]);
897            let edge = bindings[&step.edge].edge;
898            match edge {
899                None => {
900                    let candidates: Vec<&EdgeTable> = graph
901                        .edge_tables
902                        .iter()
903                        .filter(|e| fits(graph, e, left, right, step.direction))
904                        .collect();
905                    if let [only] = candidates.as_slice() {
906                        bindings.get_mut(&step.edge).unwrap().edge = Some(only);
907                        changed = true;
908                    }
909                }
910                Some(e) => {
911                    let (from, to) = (
912                        graph.endpoint_vertex(e, true),
913                        graph.endpoint_vertex(e, false),
914                    );
915                    let (l, r) = match step.direction {
916                        Direction::Right => (from, to),
917                        Direction::Left => (to, from),
918                        Direction::Any if from.map(|v| &v.name) == to.map(|v| &v.name) => {
919                            (from, to)
920                        }
921                        Direction::Any => (None, None),
922                    };
923                    for (var, table) in [(&step.left, l), (&step.right, r)] {
924                        let b = bindings.get_mut(var).unwrap();
925                        if b.vertex.is_none() {
926                            if let Some(table) = table {
927                                b.vertex = Some(VertexSource::Table(table));
928                                changed = true;
929                            }
930                        }
931                    }
932                }
933            }
934        }
935        if !changed {
936            break;
937        }
938    }
939
940    // What is still open: the graph's only table of its kind, or a refusal.
941    // Edges first: labelling an edge usually tells its nodes too.
942    for (var, b) in bindings.iter_mut() {
943        if b.is_edge && b.edge.is_none() {
944            match graph.edge_tables.as_slice() {
945                [only] => b.edge = Some(only),
946                [] => return pattern_err(format!("graph '{}' has no edge tables", graph.name)),
947                _ => return pattern_err(format!(
948                    "which edge table {} is cannot be told from the pattern; give it a label, as -[{}:{}]->",
949                    describe_var(var, true),
950                    if var.starts_with("__ursa_") { "" } else { var.as_str() },
951                    graph.edge_tables[0].labels()[0].name
952                )),
953            }
954        }
955    }
956    for (var, b) in bindings.iter_mut() {
957        if !b.is_edge && b.vertex.is_none() {
958            match graph.vertex_tables.as_slice() {
959                [only] => b.vertex = Some(VertexSource::Table(only)),
960                [] if graph.default_pair().is_some() => b.vertex = Some(VertexSource::Nodes),
961                [] => return pattern_err(format!(
962                    "graph '{}' has no vertex tables and several edge tables, so its nodes cannot be matched",
963                    graph.name
964                )),
965                _ => return pattern_err(format!(
966                    "which vertex table {} is cannot be told from the pattern; give it a label, as ({}:{})",
967                    describe_var(var, false),
968                    if var.starts_with("__ursa_") { "" } else { var.as_str() },
969                    graph.vertex_tables[0].labels()[0].name
970                )),
971            }
972        }
973    }
974
975    // The edges searched on the index: quantified edges, and the edge of a
976    // path with a variable, a selector or a mode.
977    let mut searched: Vec<(usize, Option<String>, Selector, Mode)> = Vec::new(); // (step, path var, …)
978    for (i, path) in pattern.paths.iter().enumerate() {
979        let edges: Vec<usize> = (0..steps.len()).filter(|&s| steps[s].path == i).collect();
980        let whole = path.var.is_some() || path.selector != Selector::All || path.mode != Mode::Walk;
981        if let Some(var) = &path.var {
982            if bindings.contains_key(var)
983                || pattern.paths[..i]
984                    .iter()
985                    .any(|p| p.var.as_ref() == Some(var))
986            {
987                return pattern_err(format!(
988                    "'{var}' names a path and another variable; give the path its own name"
989                ));
990            }
991        }
992        if whole && edges.len() > 1 {
993            return pattern_err(
994                "a path with a path variable, ANY/ALL SHORTEST or a path mode must be one edge between two nodes for now, as p = ANY SHORTEST (a)-[e]->*(b)",
995            );
996        }
997        if whole && edges.is_empty() {
998            if path.var.is_some() {
999                return pattern_err(
1000                    "a path variable needs a path with an edge, as p = (a)-[e]->*(b)",
1001                );
1002            }
1003            continue;
1004        }
1005        for s in edges {
1006            if whole {
1007                searched.push((s, path.var.clone(), path.selector, path.mode));
1008            } else if steps[s].quantifier.is_some() {
1009                searched.push((s, None, Selector::All, Mode::Walk));
1010            }
1011        }
1012    }
1013    for (s, _, selector, mode) in &searched {
1014        let step = &steps[*s];
1015        let quantifier = step.quantifier.unwrap_or(Quantifier::ONCE);
1016        let b = &bindings[&step.edge];
1017        let e = b.edge.expect("resolved");
1018        let edge = describe_var(&step.edge, true);
1019        if !b.conditions.is_empty() {
1020            return pattern_err(format!(
1021                "{edge} is quantified or in a searched path, so it stands for several edges; a condition on it is not supported yet"
1022            ));
1023        }
1024        if *selector == Selector::All && *mode == Mode::Walk && quantifier.max.is_none() {
1025            return pattern_err(format!(
1026                "{edge} has no upper bound, so its walks are endless; give it one, as {{1,3}}, keep the shortest with ANY SHORTEST, or use TRAIL, SIMPLE or ACYCLIC"
1027            ));
1028        }
1029        let ends = (
1030            graph.endpoint_vertex(e, true).map(|v| v.name.as_str()),
1031            graph.endpoint_vertex(e, false).map(|v| v.name.as_str()),
1032        );
1033        if ends.0 != ends.1
1034            && (quantifier.min != 1
1035                || quantifier.max != Some(1)
1036                || step.direction == Direction::Any)
1037        {
1038            return pattern_err(format!(
1039                "{edge} joins two vertex tables ({} and {}), so it cannot repeat, be undirected or match no edge; only a quantified edge within one vertex table can",
1040                ends.0.unwrap_or("?"),
1041                ends.1.unwrap_or("?")
1042            ));
1043        }
1044    }
1045    let is_searched = |i: usize| searched.iter().any(|(s, ..)| *s == i);
1046
1047    // Each node variable's columns, and its key's property names.
1048    let mut node_columns: BTreeMap<&str, (TableReference, Columns)> = BTreeMap::new();
1049    for var in &order {
1050        let b = &bindings[var];
1051        if b.is_edge {
1052            continue;
1053        }
1054        let read = match b.vertex.as_ref().expect("resolved") {
1055            VertexSource::Table(v) => (
1056                v.table.clone(),
1057                properties(&v.table, &v.labels(), &[v.key.as_str()], schema_of)?,
1058            ),
1059            VertexSource::Nodes => (
1060                TableReference::partial(graph.name.as_str(), super::schema::NODES),
1061                None,
1062            ),
1063        };
1064        node_columns.insert(var.as_str(), read);
1065    }
1066    let key = |var: &str| -> VarColumn {
1067        match bindings[var].vertex.as_ref().expect("resolved") {
1068            VertexSource::Table(v) => (var.to_string(), v.key.clone()),
1069            VertexSource::Nodes => (var.to_string(), super::schema::ID.to_string()),
1070        }
1071    };
1072    let key_names = |var: &str| -> Vec<String> {
1073        let (_, column) = key(var);
1074        match &node_columns[var].1 {
1075            None => vec![column],
1076            Some(columns) => columns
1077                .iter()
1078                .filter(|(c, _)| *c == column)
1079                .map(|(_, n)| n.clone())
1080                .collect(),
1081        }
1082    };
1083    // Bound: a condition on the node alone names the values of its key.
1084    let is_bound = |var: &str| {
1085        usage
1086            .bound
1087            .get(var)
1088            .is_some_and(|ps| key_names(var).iter().any(|n| ps.contains(n)))
1089    };
1090    let reads_only_its_key = |var: &str| match usage.reads.get(var) {
1091        _ if !usage.enabled => false,
1092        None => true,
1093        Some(None) => false,
1094        Some(Some(ps)) => {
1095            let names = key_names(var);
1096            ps.iter().all(|p| names.contains(p))
1097        }
1098    };
1099    // Whether the index of `e` has the keys of `var`'s table as its nodes.
1100    let known = |e: &EdgeTable, var: &str| match bindings[var].vertex.as_ref().expect("resolved") {
1101        VertexSource::Table(v) => {
1102            graph
1103                .endpoint_vertex(e, true)
1104                .is_some_and(|t| t.name == v.name)
1105                && graph
1106                    .endpoint_vertex(e, false)
1107                    .is_some_and(|t| t.name == v.name)
1108        }
1109        VertexSource::Nodes => graph.vertex_tables.is_empty() && graph.edge_tables.len() == 1,
1110    };
1111    // Whether the index of `e` keeps every edge a join of `e` to `var`'s
1112    // table keeps: it drops edges to values that are no key of its ends'
1113    // tables, so `var` must read one of them.
1114    let in_index = |e: &EdgeTable, var: &str| match (
1115        graph.endpoint_vertex(e, true),
1116        graph.endpoint_vertex(e, false),
1117    ) {
1118        (Some(s), Some(d)) => {
1119            vertex_table(&bindings[var]).is_some_and(|v| v.name == s.name || v.name == d.name)
1120        }
1121        _ => true,
1122    };
1123    let expandable = |i: usize| {
1124        let step = &steps[i];
1125        let b = &bindings[&step.edge];
1126        let e = b.edge.expect("resolved");
1127        !is_searched(i)
1128            && b.conditions.is_empty()
1129            && !usage.reads.contains_key(&step.edge)
1130            && usage.indexed.contains(&e.table)
1131            && in_index(e, &step.left)
1132            && in_index(e, &step.right)
1133    };
1134
1135    // From each bound node, the fixed edges of its path, outward, each from
1136    // the keys the one before reached; and the searched edges on the way,
1137    // from those keys too (see the module docs). (step, reversed, starts)
1138    let mut planned: Vec<(usize, bool, Starts)> = Vec::new();
1139    // Per node, where its keys come from, and the segment count when it
1140    // was reached.
1141    let mut reached: BTreeMap<String, (Starts, usize)> = BTreeMap::new();
1142    for var in &order {
1143        if !bindings[var].is_edge && is_bound(var) {
1144            reached.insert(var.clone(), (Starts::Node, 0));
1145        }
1146    }
1147    let mut done = vec![false; pattern.paths.len()];
1148    loop {
1149        let mut progress = false;
1150        for (i, finished) in done.iter_mut().enumerate() {
1151            if *finished {
1152                continue;
1153            }
1154            let path_steps: Vec<usize> = (0..steps.len()).filter(|&s| steps[s].path == i).collect();
1155            let Some(&first) = path_steps.first() else {
1156                *finished = true;
1157                continue;
1158            };
1159            let mut nodes = vec![steps[first].left.clone()];
1160            nodes.extend(path_steps.iter().map(|&s| steps[s].right.clone()));
1161            let Some(root) = nodes.iter().position(|n| reached.contains_key(n)) else {
1162                continue;
1163            };
1164            *finished = true;
1165            progress = true;
1166            for (j, &s) in path_steps.iter().enumerate().skip(root) {
1167                let Some(&(from, _)) = reached.get(&nodes[j]) else {
1168                    break;
1169                };
1170                if !is_searched(s) && !expandable(s) {
1171                    break;
1172                }
1173                planned.push((s, false, from));
1174                let segment = planned.len() - 1;
1175                reached
1176                    .entry(nodes[j + 1].clone())
1177                    .or_insert((Starts::After { segment, end: true }, planned.len()));
1178            }
1179            for j in (0..root).rev() {
1180                let s = path_steps[j];
1181                let Some(&(from, _)) = reached.get(&nodes[j + 1]) else {
1182                    break;
1183                };
1184                // A search runs from its first node: it cannot be reversed.
1185                if is_searched(s) || !expandable(s) {
1186                    break;
1187                }
1188                planned.push((s, true, from));
1189                let segment = planned.len() - 1;
1190                reached
1191                    .entry(nodes[j].clone())
1192                    .or_insert((Starts::After { segment, end: true }, planned.len()));
1193            }
1194        }
1195        if !progress {
1196            break;
1197        }
1198    }
1199    // The searched edges no bound node reached run from their first node.
1200    for (s, ..) in &searched {
1201        if !planned.iter().any(|(p, ..)| p == s) {
1202            planned.push((*s, false, Starts::Node));
1203        }
1204    }
1205    let is_expanded = |i: usize| planned.iter().any(|(s, ..)| *s == i);
1206
1207    let reverse = |d: Direction| match d {
1208        Direction::Right => Direction::Left,
1209        Direction::Left => Direction::Right,
1210        Direction::Any => Direction::Any,
1211    };
1212    let mut segments: Vec<Segment> = Vec::new();
1213    for &(s, reversed, starts) in &planned {
1214        let step = &steps[s];
1215        let e = bindings[&step.edge].edge.expect("resolved");
1216        let (start, end) = match reversed {
1217            false => (step.left.clone(), step.right.clone()),
1218            true => (step.right.clone(), step.left.clone()),
1219        };
1220        let search = searched.iter().find(|(x, ..)| *x == s);
1221        let (path, selector, mode) = match search {
1222            Some((_, path, selector, mode)) => (path.clone(), *selector, *mode),
1223            None => (None, Selector::All, Mode::Walk),
1224        };
1225        let var = match &path {
1226            Some(p) => p.clone(),
1227            None => {
1228                anonymous += 1;
1229                format!("__ursa_{anonymous}")
1230            }
1231        };
1232        // The keys the last node can have, when they are known by now.
1233        let targets = match reached.get(&end) {
1234            _ if is_bound(&end) => Some(Starts::Node),
1235            Some(&(after @ Starts::After { .. }, when)) if when <= segments.len() => Some(after),
1236            _ => None,
1237        };
1238        segments.push(Segment {
1239            var,
1240            path,
1241            edges: e.table.clone(),
1242            source: e.source.clone(),
1243            destination: e.destination.clone(),
1244            known_only: known(e, &start),
1245            start_key: key(&start),
1246            start,
1247            end_key: key(&end),
1248            end,
1249            direction: if reversed {
1250                reverse(step.direction)
1251            } else {
1252                step.direction
1253            },
1254            quantifier: step.quantifier.unwrap_or(Quantifier::ONCE),
1255            selector,
1256            mode,
1257            fixed: search.is_none(),
1258            starts,
1259            targets,
1260        });
1261    }
1262
1263    // Nodes the query reads only the key of, at the end of a segment whose
1264    // index's nodes are their table's keys: their key is that segment's column.
1265    let mut aliases: Vec<Alias> = Vec::new();
1266    for var in &order {
1267        if bindings[var].is_edge || !reads_only_its_key(var) {
1268            continue;
1269        }
1270        let at = segments
1271            .iter()
1272            .zip(&planned)
1273            .find_map(|(segment, &(s, ..))| {
1274                let e = bindings[&steps[s].edge].edge.expect("resolved");
1275                if !known(e, var) {
1276                    return None;
1277                }
1278                let column = if &segment.start == var {
1279                    super::path_search::START
1280                } else if &segment.end == var {
1281                    super::path_search::END
1282                } else {
1283                    return None;
1284                };
1285                Some((segment.var.clone(), column.to_string()))
1286            });
1287        if let Some(column) = at {
1288            aliases.push(Alias {
1289                var: var.clone(),
1290                column,
1291            });
1292        }
1293    }
1294    // Where a node's key is read: its table's key column, or a segment's.
1295    let at = |var: &str| -> VarColumn {
1296        match aliases.iter().find(|a| a.var == var) {
1297            Some(a) => a.column.clone(),
1298            None => key(var),
1299        }
1300    };
1301
1302    let undirected: Vec<usize> = steps
1303        .iter()
1304        .enumerate()
1305        .filter(|(i, s)| s.direction == Direction::Any && !is_expanded(*i))
1306        .map(|(i, _)| i)
1307        .collect();
1308    if undirected.len() > MAX_UNDIRECTED {
1309        return pattern_err(format!(
1310            "a pattern may have at most {MAX_UNDIRECTED} undirected edges (-[e]-); give some a direction"
1311        ));
1312    }
1313    let mut sources = Vec::new();
1314    for var in &order {
1315        let b = &bindings[var];
1316        if b.is_edge && planned.iter().any(|(s, ..)| &steps[*s].edge == var) {
1317            continue;
1318        }
1319        let (table, columns) = if b.is_edge {
1320            let e = b.edge.expect("resolved");
1321            let needed = [e.source.as_str(), e.destination.as_str()];
1322            (
1323                e.table.clone(),
1324                properties(&e.table, &e.labels(), &needed, schema_of)?,
1325            )
1326        } else {
1327            node_columns[var.as_str()].clone()
1328        };
1329        sources.push(Source {
1330            var: var.clone(),
1331            table,
1332            columns,
1333            joined: !aliases.iter().any(|a| &a.var == var),
1334        });
1335    }
1336
1337    let mut branches = Vec::new();
1338    for mask in 0..(1usize << undirected.len()) {
1339        let mut joins = Vec::new();
1340        for segment in &segments {
1341            let column = |name: &str| (segment.var.clone(), name.to_string());
1342            for (name, var) in [
1343                (super::path_search::START, &segment.start),
1344                (super::path_search::END, &segment.end),
1345            ] {
1346                let (a, b) = (column(name), at(var));
1347                if a != b {
1348                    joins.push(Join::Equal(a, b));
1349                }
1350            }
1351        }
1352        for (i, step) in steps.iter().enumerate() {
1353            if is_expanded(i) {
1354                continue;
1355            }
1356            let e = bindings[&step.edge].edge.expect("resolved");
1357            let src = (step.edge.clone(), e.source.clone());
1358            let dst = (step.edge.clone(), e.destination.clone());
1359            let reversed = match step.direction {
1360                Direction::Right => false,
1361                Direction::Left => true,
1362                Direction::Any => {
1363                    let bit = undirected.iter().position(|&u| u == i).unwrap();
1364                    let reversed = mask & (1 << bit) != 0;
1365                    if reversed {
1366                        // A self-loop matches once, in the forward branch.
1367                        joins.push(Join::NotEqual(src.clone(), dst.clone()));
1368                    }
1369                    reversed
1370                }
1371            };
1372            let (from, to) = if reversed {
1373                (&step.right, &step.left)
1374            } else {
1375                (&step.left, &step.right)
1376            };
1377            joins.push(Join::Equal(src, at(from)));
1378            joins.push(Join::Equal(dst, at(to)));
1379        }
1380        branches.push(joins);
1381    }
1382    let mut conditions: Vec<String> = order
1383        .iter()
1384        .flat_map(|var| bindings[var].conditions.iter().cloned())
1385        .collect();
1386    conditions.extend(pattern.condition.clone());
1387    let union = steps
1388        .iter()
1389        .enumerate()
1390        .any(|(i, s)| s.direction == Direction::Any && !is_searched(i));
1391    Ok(Lowered {
1392        sources,
1393        segments,
1394        aliases,
1395        branches,
1396        union,
1397        conditions,
1398        columns: pattern.columns.clone(),
1399    })
1400}
1401
1402/// The vertex table a variable is bound to, if it is bound to one.
1403fn vertex_table<'g>(b: &Binding<'g>) -> Option<&'g VertexTable> {
1404    match b.vertex {
1405        Some(VertexSource::Table(v)) => Some(v),
1406        _ => None,
1407    }
1408}
1409
1410/// Whether edge table `e` can join `left` to `right` in `direction`, as far
1411/// as the vertex tables already known say.
1412fn fits(
1413    graph: &PropertyGraph,
1414    e: &EdgeTable,
1415    left: Option<&VertexTable>,
1416    right: Option<&VertexTable>,
1417    direction: Direction,
1418) -> bool {
1419    let from = graph.endpoint_vertex(e, true).map(|v| v.name.as_str());
1420    let to = graph.endpoint_vertex(e, false).map(|v| v.name.as_str());
1421    let matches = |side: Option<&VertexTable>, end: Option<&str>| match (side, end) {
1422        (Some(side), Some(end)) => side.name == end,
1423        _ => true,
1424    };
1425    let forward = matches(left, from) && matches(right, to);
1426    let backward = matches(left, to) && matches(right, from);
1427    match direction {
1428        Direction::Right => forward,
1429        Direction::Left => backward,
1430        Direction::Any => forward || backward,
1431    }
1432}
1433
1434/// The columns an element table exposes, as (column, property name): `None`
1435/// when every label exposes every column under its own name; else its
1436/// labels' properties, plus the `needed` key or endpoint columns.
1437fn properties(
1438    table: &TableReference,
1439    labels: &[Label],
1440    needed: &[&str],
1441    schema_of: &dyn Fn(&TableReference) -> Result<SchemaRef>,
1442) -> Result<Option<Vec<(String, String)>>> {
1443    let all = labels
1444        .iter()
1445        .all(|l| matches!(&l.properties, Properties::AllColumns { except } if except.is_empty()));
1446    if all {
1447        return Ok(None);
1448    }
1449    let schema = schema_of(table)?;
1450    let mut items: Vec<(String, String)> = Vec::new(); // (column, property)
1451    for label in labels {
1452        let columns: Vec<(String, String)> = match &label.properties {
1453            Properties::AllColumns { except } => schema
1454                .fields()
1455                .iter()
1456                .map(|f| f.name().clone())
1457                .filter(|c| !except.contains(c))
1458                .map(|c| (c.clone(), c))
1459                .collect(),
1460            Properties::Columns(columns) => columns.clone(),
1461            Properties::None => Vec::new(),
1462        };
1463        for item in columns {
1464            if !items.iter().any(|(_, name)| name == &item.1) {
1465                items.push(item);
1466            }
1467        }
1468    }
1469    for column in needed {
1470        if !items.iter().any(|(_, name)| name == column) {
1471            items.push((column.to_string(), column.to_string()));
1472        }
1473    }
1474    Ok(Some(items))
1475}
1476
1477fn names<'a>(names: impl Iterator<Item = &'a str>) -> String {
1478    let names: Vec<&str> = names.collect();
1479    if names.is_empty() {
1480        "none".to_string()
1481    } else {
1482        names.join(", ")
1483    }
1484}
1485
1486/// A variable as a message names it.
1487fn shown(var: &str) -> String {
1488    if var.starts_with("__ursa_") {
1489        "an unnamed element".to_string()
1490    } else {
1491        var.to_string()
1492    }
1493}
1494
1495fn describe_var(var: &str, edge: bool) -> String {
1496    match (var.starts_with("__ursa_"), edge) {
1497        (true, true) => "an unnamed edge".to_string(),
1498        (true, false) => "an unnamed node".to_string(),
1499        (false, true) => format!("edge '{var}'"),
1500        (false, false) => format!("node '{var}'"),
1501    }
1502}
1503
1504#[cfg(test)]
1505mod tests {
1506    use super::*;
1507
1508    fn spec(var: Option<&str>, label: Option<&str>, condition: Option<&str>) -> Spec {
1509        Spec {
1510            var: var.map(str::to_string),
1511            label: label.map(str::to_string),
1512            condition: condition.map(str::to_string),
1513        }
1514    }
1515
1516    fn edges(path: &PathPattern) -> Vec<(Direction, Option<Quantifier>)> {
1517        path.elements
1518            .iter()
1519            .filter_map(|e| match e {
1520                Element::Edge(_, d, q) => Some((*d, *q)),
1521                _ => None,
1522            })
1523            .collect()
1524    }
1525
1526    #[test]
1527    fn parses_a_pattern() {
1528        let p = parse(
1529            "social MATCH (a:Person WHERE a.age > (1 + 2))-[k:knows WHERE k.since > 2000]->(b IS Person),
1530             (b)<-[w]-(c), (c)-(d)-[x]-(e)->(f)<-(g)
1531             WHERE a.name <> b.name COLUMNS (a.name AS a, concat(b.name, ')') AS b)",
1532        )
1533        .unwrap();
1534        assert_eq!(p.graph, "social");
1535        assert_eq!(p.condition.as_deref(), Some("a.name <> b.name"));
1536        assert_eq!(p.columns, "a.name AS a, concat(b.name, ')') AS b");
1537        assert_eq!(
1538            p.paths[0].elements,
1539            vec![
1540                Element::Node(spec(Some("a"), Some("person"), Some("a.age > (1 + 2)"))),
1541                Element::Edge(
1542                    spec(Some("k"), Some("knows"), Some("k.since > 2000")),
1543                    Direction::Right,
1544                    None
1545                ),
1546                Element::Node(spec(Some("b"), Some("person"), None)),
1547            ]
1548        );
1549        assert_eq!(
1550            p.paths[1].elements,
1551            vec![
1552                Element::Node(spec(Some("b"), None, None)),
1553                Element::Edge(spec(Some("w"), None, None), Direction::Left, None),
1554                Element::Node(spec(Some("c"), None, None)),
1555            ]
1556        );
1557        let directions: Vec<Direction> = edges(&p.paths[2]).into_iter().map(|e| e.0).collect();
1558        assert_eq!(
1559            directions,
1560            vec![
1561                Direction::Any,
1562                Direction::Any,
1563                Direction::Right,
1564                Direction::Left
1565            ]
1566        );
1567        assert_eq!(p.paths[0].selector, Selector::All);
1568        assert_eq!(p.paths[0].mode, Mode::Walk);
1569    }
1570
1571    #[test]
1572    fn parses_quantifiers_selectors_and_modes() {
1573        let q = |min, max| Some(Quantifier { min, max });
1574        for (path, expected) in [
1575            ("(a)-[e]->*(b)", (Direction::Right, q(0, None))),
1576            ("(a)-[e]-> *(b)", (Direction::Right, q(0, None))),
1577            ("(a)-[e]- > *(b)", (Direction::Right, q(0, None))),
1578            ("(a)-[e]->+(b)", (Direction::Right, q(1, None))),
1579            ("(a)<-[e]-?(b)", (Direction::Left, q(0, Some(1)))),
1580            ("(a)-[e]-*(b)", (Direction::Any, q(0, None))),
1581            ("(a)->{2}(b)", (Direction::Right, q(2, Some(2)))),
1582            ("(a)-[e]->{1,3}(b)", (Direction::Right, q(1, Some(3)))),
1583            ("(a)-[e]->{2,}(b)", (Direction::Right, q(2, None))),
1584            ("(a)-[e]->{,3}(b)", (Direction::Right, q(0, Some(3)))),
1585            ("(a)-[e]->{,}(b)", (Direction::Right, q(0, None))),
1586            ("(a)-[e]->(b)", (Direction::Right, None)),
1587        ] {
1588            let p = parse(&format!("g MATCH {path} COLUMNS (a.id)")).unwrap();
1589            assert_eq!(edges(&p.paths[0]), vec![expected], "{path}");
1590        }
1591        for (path, var, selector, mode) in [
1592            (
1593                "p = ANY SHORTEST (a)-[e]->*(b)",
1594                Some("p"),
1595                Selector::Shortest(1),
1596                Mode::Walk,
1597            ),
1598            ("ANY (a)-[e]->*(b)", None, Selector::Shortest(1), Mode::Walk),
1599            (
1600                "ANY 2 TRAIL (a)-[e]->*(b)",
1601                None,
1602                Selector::Shortest(2),
1603                Mode::Trail,
1604            ),
1605            (
1606                "q = SHORTEST 3 PATHS (a)-[e]->*(b)",
1607                Some("q"),
1608                Selector::Shortest(3),
1609                Mode::Walk,
1610            ),
1611            (
1612                "ALL SHORTEST ACYCLIC PATH (a)-[e]->*(b)",
1613                None,
1614                Selector::AllShortest,
1615                Mode::Acyclic,
1616            ),
1617            (
1618                "ALL SIMPLE (a)-[e]->{1,2}(b)",
1619                None,
1620                Selector::All,
1621                Mode::Simple,
1622            ),
1623            (
1624                "p = WALK (a)-[e]->{1,2}(b)",
1625                Some("p"),
1626                Selector::All,
1627                Mode::Walk,
1628            ),
1629        ] {
1630            let p = parse(&format!("g MATCH {path} COLUMNS (a.id)")).unwrap();
1631            let path0 = &p.paths[0];
1632            assert_eq!(
1633                (path0.var.as_deref(), path0.selector, path0.mode),
1634                (var, selector, mode),
1635                "{path}"
1636            );
1637        }
1638    }
1639
1640    #[test]
1641    fn refuses_what_is_not_supported_yet_and_reports_errors() {
1642        for (body, expected) in [
1643            (
1644                "g MATCH ANY SHORTEST 5 (a)-[e]->*(b) COLUMNS (a.id)",
1645                "for several, write SHORTEST k",
1646            ),
1647            (
1648                "g MATCH SHORTEST (a)-[e]->*(b) COLUMNS (a.id)",
1649                "SHORTEST takes a number",
1650            ),
1651            ("g MATCH SHORTEST 0 (a)-[e]->*(b) COLUMNS (a.id)", "above 0"),
1652            (
1653                "g MATCH (a)-[e]->{3,1}(b) COLUMNS (a.id)",
1654                "lower bound 3 is above its upper bound 1",
1655            ),
1656            (
1657                "g MATCH (a)-[e]->{x}(b) COLUMNS (a.id)",
1658                "expected a number or ','",
1659            ),
1660            (
1661                "g MATCH ((a)-[e]->(b))* COLUMNS (a.id)",
1662                "parenthesized path patterns",
1663            ),
1664            ("g MATCH (a:x|y) COLUMNS (a.id)", "label expressions"),
1665            ("g MATCH (a)-[e]->(b)", "a GRAPH_TABLE needs COLUMNS (…)"),
1666            (
1667                "g (a) COLUMNS (a.id)",
1668                "expected MATCH after the graph's name, found '('",
1669            ),
1670            (
1671                "g MATCH (a)-[e]=>(b) COLUMNS (a.id)",
1672                "expected '->' or '-' after ']'",
1673            ),
1674            (
1675                "g MATCH (a b c) COLUMNS (a.id)",
1676                "expected a variable, ':label', WHERE or ')'",
1677            ),
1678            ("g MATCH (a) COLUMNS (a.id) extra", "unexpected 'extra'"),
1679            ("g MATCH (a) COLUMNS ()", "COLUMNS (…) lists nothing"),
1680        ] {
1681            let message = parse(body).unwrap_err().to_string();
1682            assert!(message.contains(expected), "{body}\n{message}");
1683        }
1684    }
1685}