1use 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
74pub const FUNCTION: &str = "graph_table";
76
77const MAX_UNDIRECTED: usize = 3;
79
80#[derive(Debug, Clone, PartialEq)]
82pub struct Pattern {
83 pub graph: String,
85 pub paths: Vec<PathPattern>,
86 pub condition: Option<String>,
88 pub columns: String,
90}
91
92#[derive(Debug, Clone, PartialEq)]
94pub struct PathPattern {
95 pub var: Option<String>,
97 pub selector: Selector,
98 pub mode: Mode,
99 pub elements: Vec<Element>,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
105pub enum Selector {
106 All,
108 Shortest(u32),
111 AllShortest,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
117pub enum Mode {
118 Walk,
120 Trail,
122 Simple,
124 Acyclic,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
130pub struct Quantifier {
131 pub min: u32,
132 pub max: Option<u32>,
134}
135
136impl Quantifier {
137 pub const ONCE: Quantifier = Quantifier {
139 min: 1,
140 max: Some(1),
141 };
142}
143
144#[derive(Debug, Clone, PartialEq)]
146pub enum Element {
147 Node(Spec),
148 Edge(Spec, Direction, Option<Quantifier>),
150}
151
152#[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 Right,
164 Left,
166 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
177struct 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 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 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 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
294fn 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
312pub 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
365fn 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
411fn 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
472fn 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
506fn parse_edge(t: &mut Tokens) -> Result<Option<(Spec, Direction, Option<Quantifier>)>> {
509 let edge = match (t.peek(0), t.peek(1)) {
510 (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 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
565fn 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#[derive(Debug, Clone)]
624enum VertexSource<'g> {
625 Table(&'g VertexTable),
626 Nodes,
628}
629
630#[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
640struct Step {
642 left: String,
643 edge: String,
644 right: String,
645 direction: Direction,
646 quantifier: Option<Quantifier>,
647 path: usize,
649}
650
651pub type VarColumn = (String, String);
653
654#[derive(Debug, Clone, PartialEq)]
657pub struct Source {
658 pub var: String,
659 pub table: TableReference,
660 pub columns: Option<Vec<(String, String)>>,
662 pub joined: bool,
665}
666
667type Columns = Option<Vec<(String, String)>>;
669
670#[derive(Debug, Clone, PartialEq)]
672pub enum Join {
673 Equal(VarColumn, VarColumn),
674 NotEqual(VarColumn, VarColumn),
676}
677
678#[derive(Debug, Clone, PartialEq)]
684pub struct Segment {
685 pub var: String,
687 pub path: Option<String>,
689 pub edges: TableReference,
691 pub source: String,
692 pub destination: String,
693 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 pub fixed: bool,
704 pub starts: Starts,
705 pub known_only: bool,
708 pub targets: Option<Starts>,
712}
713
714#[derive(Debug, Clone, Default, PartialEq)]
718pub struct Usage {
719 pub enabled: bool,
722 pub reads: BTreeMap<String, Option<BTreeSet<String>>>,
725 pub bound: BTreeMap<String, BTreeSet<String>>,
728 pub indexed: BTreeSet<TableReference>,
731}
732
733#[derive(Debug, Clone, Copy, PartialEq)]
735pub enum Starts {
736 Node,
738 After { segment: usize, end: bool },
741}
742
743#[derive(Debug, Clone, PartialEq)]
745pub struct Alias {
746 pub var: String,
747 pub column: VarColumn,
749}
750
751#[derive(Debug, Clone, PartialEq)]
753pub struct Lowered {
754 pub sources: Vec<Source>,
757 pub segments: Vec<Segment>,
760 pub aliases: Vec<Alias>,
762 pub branches: Vec<Vec<Join>>,
765 pub union: bool,
768 pub conditions: Vec<String>,
770 pub columns: String,
772}
773
774pub fn lower(
777 pattern: &Pattern,
778 graph: &PropertyGraph,
779 schema_of: &dyn Fn(&TableReference) -> Result<SchemaRef>,
780 usage: &Usage,
781) -> Result<Lowered> {
782 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 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 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 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 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 let mut searched: Vec<(usize, Option<String>, Selector, Mode)> = Vec::new(); 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 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 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 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 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 let mut planned: Vec<(usize, bool, Starts)> = Vec::new();
1139 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 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 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 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 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 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 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
1402fn 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
1410fn 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
1434fn 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(); 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
1486fn 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}