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