1use 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
57pub const FUNCTION: &str = "graph_table";
59
60const MAX_UNDIRECTED: usize = 3;
62
63#[derive(Debug, Clone, PartialEq)]
65pub struct Pattern {
66 pub graph: String,
68 pub paths: Vec<PathPattern>,
69 pub condition: Option<String>,
71 pub columns: String,
73}
74
75#[derive(Debug, Clone, PartialEq)]
77pub struct PathPattern {
78 pub var: Option<String>,
80 pub selector: Selector,
81 pub mode: Mode,
82 pub elements: Vec<Element>,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
88pub enum Selector {
89 All,
91 Shortest(u32),
94 AllShortest,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
100pub enum Mode {
101 Walk,
103 Trail,
105 Simple,
107 Acyclic,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
113pub struct Quantifier {
114 pub min: u32,
115 pub max: Option<u32>,
117}
118
119impl Quantifier {
120 pub const ONCE: Quantifier = Quantifier {
122 min: 1,
123 max: Some(1),
124 };
125}
126
127#[derive(Debug, Clone, PartialEq)]
129pub enum Element {
130 Node(Spec),
131 Edge(Spec, Direction, Option<Quantifier>),
133}
134
135#[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 Right,
147 Left,
149 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
160struct 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 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 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 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
277fn 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
295pub 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
348fn 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
394fn 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
455fn 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
489fn parse_edge(t: &mut Tokens) -> Result<Option<(Spec, Direction, Option<Quantifier>)>> {
492 let edge = match (t.peek(0), t.peek(1)) {
493 (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 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
548fn 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#[derive(Debug, Clone)]
607enum VertexSource<'g> {
608 Table(&'g VertexTable),
609 Nodes,
611}
612
613#[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
623struct Step {
625 left: String,
626 edge: String,
627 right: String,
628 direction: Direction,
629 quantifier: Option<Quantifier>,
630 path: usize,
632}
633
634pub type VarColumn = (String, String);
636
637#[derive(Debug, Clone, PartialEq)]
640pub struct Source {
641 pub var: String,
642 pub table: TableReference,
643 pub columns: Option<Vec<(String, String)>>,
645}
646
647#[derive(Debug, Clone, PartialEq)]
649pub enum Join {
650 Equal(VarColumn, VarColumn),
651 NotEqual(VarColumn, VarColumn),
653}
654
655#[derive(Debug, Clone, PartialEq)]
661pub struct Segment {
662 pub var: String,
664 pub path: Option<String>,
666 pub edges: TableReference,
668 pub source: String,
669 pub destination: String,
670 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#[derive(Debug, Clone, PartialEq)]
683pub struct Lowered {
684 pub sources: Vec<Source>,
687 pub segments: Vec<Segment>,
690 pub branches: Vec<Vec<Join>>,
693 pub conditions: Vec<String>,
695 pub columns: String,
697}
698
699pub fn lower(
702 pattern: &Pattern,
703 graph: &PropertyGraph,
704 schema_of: &dyn Fn(&TableReference) -> Result<SchemaRef>,
705) -> Result<Lowered> {
706 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 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 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 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 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 let mut searched: Vec<(usize, Option<String>, Selector, Mode)> = Vec::new(); 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 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
1105fn 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
1113fn 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
1137fn 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(); 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
1189fn 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}