diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd0bebd..4f6655f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,5 +13,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - AST for the currently parsed subset of PartiQL - Tracking of locations in source text for ASTs and Errors - Conformance tests via test generation from [partiql-tests](https://github.com/partiql/partiql-tests/) +- An experimental (pending [#15](https://github.com/partiql/partiql-docs/issues/15)) embedding of a subset of + the [GPML (Graph Pattern Matching Language)](https://arxiv.org/abs/2112.06217) graph query into the `FROM` clause, + supporting. The use within the grammar is based on the assumption of a new graph data type being added to the + specification of data types within PartiQL, and should be considered experimental until the semantics of the graph + data type are specified. + - basic and abbreviated node and edge patterns (section 4.1 of the GPML paper) + - concatenated path patterns (section 4.2 of the GPML paper) + - path variables (section 4.2 of the GPML paper) + - graph patterns (i.e., comma separated path patterns) (section 4.3 of the GPML paper) + - parenthesized patterns (section 4.4 of the GPML paper) + - path quantifiers (section 4.4 of the GPML paper) + - restrictors and selector (section 5.1 of the GPML paper) + - pre-filters and post-filters (section 5.2 of the GPML paper) diff --git a/partiql-ast/src/ast.rs b/partiql-ast/src/ast.rs index 810b1b32..71b6fe3b 100644 --- a/partiql-ast/src/ast.rs +++ b/partiql-ast/src/ast.rs @@ -12,6 +12,7 @@ use rust_decimal::Decimal as RustDecimal; use serde::{Deserialize, Serialize}; use std::fmt; use std::fmt::Display; +use std::num::NonZeroU32; use std::ops::Range; /// Provides the required methods for AstNode conversations. @@ -235,6 +236,13 @@ pub type CallAst = AstBytePos; pub type CaseAst = AstBytePos; pub type FromClauseAst = AstBytePos; pub type FromLetAst = AstBytePos; +pub type GraphMatchAst = AstBytePos; +pub type GraphMatchExprAst = AstBytePos; +pub type GraphMatchEdgeAst = AstBytePos; +pub type GraphMatchNodeAst = AstBytePos; +pub type GraphMatchPatternAst = AstBytePos; +pub type GraphMatchPatternPartAst = AstBytePos; +pub type GraphMatchQuantifierAst = AstBytePos; pub type GroupByExprAst = AstBytePos; pub type GroupKeyAst = AstBytePos; pub type InAst = AstBytePos; @@ -655,6 +663,9 @@ pub enum FromClause { FromLet(FromLetAst), /// JOIN \[INNER | LEFT | RIGHT | FULL\] ON Join(JoinAst), + + /// MATCH + GraphMatch(GraphMatchAst), } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] @@ -699,6 +710,142 @@ pub enum JoinKind { Cross, } +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GraphMatch { + pub expr: Box, + pub graph_expr: Box, +} + +/// The direction of an edge +/// | Orientation | Edge pattern | Abbreviation | +/// |---------------------------+--------------+--------------| +/// | Pointing left | <−[ spec ]− | <− | +/// | Undirected | ~[ spec ]~ | ~ | +/// | Pointing right | −[ spec ]−> | −> | +/// | Left or undirected | <~[ spec ]~ | <~ | +/// | Undirected or right | ~[ spec ]~> | ~> | +/// | Left or right | <−[ spec ]−> | <−> | +/// | Left, undirected or right | −[ spec ]− | − | +/// +/// Fig. 5. Table of edge patterns: +/// https://arxiv.org/abs/2112.06217 +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub enum GraphMatchDirection { + Left, + Undirected, + Right, + LeftOrUndirected, + UndirectedOrRight, + LeftOrRight, + LeftOrUndirectedOrRight, +} + +/// A part of a graph pattern +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub enum GraphMatchPatternPart { + /// A single node in a graph pattern. + Node(GraphMatchNodeAst), + + /// A single edge in a graph pattern. + Edge(GraphMatchEdgeAst), + + /// A sub-pattern. + Pattern(GraphMatchPatternAst), +} + +/// A quantifier for graph edges or patterns. (e.g., the `{2,5}` in `MATCH (x)->{2,5}(y)`) +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GraphMatchQuantifier { + pub lower: u32, + pub upper: Option, +} + +/// A path restrictor +/// | Keyword | Description +/// |----------------+-------------- +/// | TRAIL | No repeated edges. +/// | ACYCLIC | No repeated nodes. +/// | SIMPLE | No repeated nodes, except that the first and last nodes may be the same. +/// +/// Fig. 7. Table of restrictors: +/// https://arxiv.org/abs/2112.06217 +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub enum GraphMatchRestrictor { + Trail, + Acyclic, + Simple, +} + +/// A single node in a graph pattern. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GraphMatchNode { + /// an optional node pre-filter, e.g.: `WHERE c.name='Alarm'` in `MATCH (c WHERE c.name='Alarm')` + pub prefilter: Option>, + /// the optional element variable of the node match, e.g.: `x` in `MATCH (x)` + pub variable: Option, + /// the optional label(s) to match for the node, e.g.: `Entity` in `MATCH (x:Entity)` + pub label: Option>, +} + +/// A single edge in a graph pattern. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GraphMatchEdge { + /// edge direction + pub direction: GraphMatchDirection, + /// an optional quantifier for the edge match + pub quantifier: Option, + /// an optional edge pre-filter, e.g.: `WHERE t.capacity>100` in `MATCH −[t:hasSupply WHERE t.capacity>100]−>` + pub prefilter: Option>, + /// the optional element variable of the edge match, e.g.: `t` in `MATCH −[t]−>` + pub variable: Option, + /// the optional label(s) to match for the edge. e.g.: `Target` in `MATCH −[t:Target]−>` + pub label: Option>, +} + +/// A single graph match pattern. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GraphMatchPattern { + pub restrictor: Option, + /// an optional quantifier for the entire pattern match + pub quantifier: Option, + /// an optional pattern pre-filter, e.g.: `WHERE a.name=b.name` in `MATCH [(a)->(b) WHERE a.name=b.name]` + pub prefilter: Option>, + /// the optional element variable of the pattern, e.g.: `p` in `MATCH p = (a) −[t]−> (b)` + pub variable: Option, + /// the ordered pattern parts + pub parts: Vec, +} + +/// A path selector +/// | Keyword +/// |------------------ +/// | ANY SHORTEST +/// | ALL SHORTEST +/// | ANY +/// | ANY k +/// | SHORTEST k +/// | SHORTEST k GROUP +/// +/// Fig. 8. Table of restrictors: +/// https://arxiv.org/abs/2112.06217 +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub enum GraphMatchSelector { + AnyShortest, + AllShortest, + Any, + AnyK(NonZeroU32), + ShortestK(NonZeroU32), + ShortestKGroup(NonZeroU32), +} + +/// A graph match clause as defined in GPML +/// See https://arxiv.org/abs/2112.06217 +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GraphMatchExpr { + pub selector: Option, + pub patterns: Vec, +} + /// A generic pair of expressions. Used in the `pub struct`, `searched_case` /// and `simple_case` expr variants above. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] diff --git a/partiql-parser/benches/bench_parse.rs b/partiql-parser/benches/bench_parse.rs index 40a5daaf..cdb4d221 100644 --- a/partiql-parser/benches/bench_parse.rs +++ b/partiql-parser/benches/bench_parse.rs @@ -34,6 +34,20 @@ const Q_COMPLEX_FEXPR: &str = r#" AS deltas FROM SOURCE_VIEW_DELTA_FULL_TRANSACTIONS delta_full_transactions "#; +const Q_COMPLEX_MATCH: &str = r#" + SELECT ( + SELECT numRec, data + FROM + (deltaGraph MATCH (t) -[:hasChange]-> (dt), (dt) -[:checkPointedBy]-> (t1)), + ( + SELECT foo(u.id), bar(review), rindex + FROM delta.data as u CROSS JOIN UNPIVOT u.reviews as review AT rindex + ) as data, + delta.numRec as numRec + ) + AS deltas FROM SOURCE_VIEW_DELTA_FULL_TRANSACTIONS delta_full_transactions + "#; + fn parse_bench(c: &mut Criterion) { fn parse(text: &str) -> ParserResult { Parser::default().parse(text) @@ -45,6 +59,9 @@ fn parse_bench(c: &mut Criterion) { c.bench_function("parse-complex-fexpr", |b| { b.iter(|| parse(black_box(Q_COMPLEX_FEXPR))) }); + c.bench_function("parse-complex-match", |b| { + b.iter(|| parse(black_box(Q_COMPLEX_MATCH))) + }); } criterion_group! { diff --git a/partiql-parser/src/lexer.rs b/partiql-parser/src/lexer.rs index 91ef62a9..6083434b 100644 --- a/partiql-parser/src/lexer.rs +++ b/partiql-parser/src/lexer.rs @@ -465,6 +465,8 @@ pub enum Token<'input> { Caret, #[token(".")] Period, + #[token("~")] + Tilde, #[token("||")] DblPipe, @@ -510,10 +512,14 @@ pub enum Token<'input> { // Keywords #[regex("(?i:All)")] All, + #[regex("(?i:Acyclic)")] + Acyclic, #[regex("(?i:Asc)")] Asc, #[regex("(?i:And)")] And, + #[regex("(?i:Any)")] + Any, #[regex("(?i:As)")] As, #[regex("(?i:At)")] @@ -572,6 +578,8 @@ pub enum Token<'input> { Like, #[regex("(?i:Limit)")] Limit, + #[regex("(?i:Match)")] + Match, #[regex("(?i:Missing)")] Missing, #[regex("(?i:Natural)")] @@ -602,8 +610,14 @@ pub enum Token<'input> { Right, #[regex("(?i:Select)")] Select, + #[regex("(?i:Simple)")] + Simple, + #[regex("(?i:Shortest)")] + Shortest, #[regex("(?i:Then)")] Then, + #[regex("(?i:Trail)")] + Trail, #[regex("(?i:True)")] True, #[regex("(?i:Union)")] @@ -628,9 +642,11 @@ impl<'input> Token<'input> { pub fn is_keyword(&self) -> bool { matches!( self, - Token::All + Token::Acyclic + | Token::All | Token::Asc | Token::And + | Token::Any | Token::As | Token::At | Token::Between @@ -656,6 +672,7 @@ impl<'input> Token<'input> { | Token::Left | Token::Like | Token::Limit + | Token::Match | Token::Missing | Token::Natural | Token::Not @@ -671,7 +688,10 @@ impl<'input> Token<'input> { | Token::Preserve | Token::Right | Token::Select + | Token::Simple + | Token::Shortest | Token::Then + | Token::Trail | Token::Union | Token::Unpivot | Token::Using @@ -717,6 +737,7 @@ impl<'input> fmt::Display for Token<'input> { Token::Slash => write!(f, "/"), Token::Caret => write!(f, "^"), Token::Period => write!(f, "."), + Token::Tilde => write!(f, "~"), Token::DblPipe => write!(f, "||"), Token::UnquotedIdent(id) => write!(f, "<{}:UNQUOTED_IDENT>", id), Token::QuotedIdent(id) => write!(f, "<{}:QUOTED_IDENT>", id), @@ -729,9 +750,11 @@ impl<'input> fmt::Display for Token<'input> { Token::EmbeddedIonQuote => write!(f, ""), Token::Ion(txt) => write!(f, "<{}:ION>", txt), - Token::All + Token::Acyclic + | Token::All | Token::Asc | Token::And + | Token::Any | Token::As | Token::At | Token::Between @@ -761,6 +784,7 @@ impl<'input> fmt::Display for Token<'input> { | Token::Left | Token::Like | Token::Limit + | Token::Match | Token::Missing | Token::Natural | Token::Not @@ -776,7 +800,10 @@ impl<'input> fmt::Display for Token<'input> { | Token::Preserve | Token::Right | Token::Select + | Token::Simple + | Token::Shortest | Token::Then + | Token::Trail | Token::True | Token::Union | Token::Unpivot @@ -811,7 +838,8 @@ mod tests { "WiTH Where Value uSiNg Unpivot UNION True Select right Preserve pivoT Outer Order Or \ On Offset Nulls Null Not Natural Missing Limit Like Left Lateral Last Join \ Intersect Is Inner In Having Group From For Full First False Except Escape Desc \ - Cross By Between At As And Asc All Values Case When Then Else End"; + Cross By Between At As And Asc All Values Case When Then Else End Match Any Shortest \ + Trail Acyclic Simple"; let symbols = symbols.split(' ').chain(primitives.split(' ')); let keywords = keywords.split(' '); @@ -833,7 +861,7 @@ mod tests { "", "IN", "", "HAVING", "", "GROUP", "FROM", "FOR", "FULL", "FIRST", "FALSE", "EXCEPT", "ESCAPE", "DESC", "CROSS", "BY", "BETWEEN", "AT", "AS", "AND", "ASC", "ALL", "VALUES", - "CASE", "WHEN", "THEN", "ELSE", "END", + "CASE", "WHEN", "THEN", "ELSE", "END", "MATCH", "ANY", "SHORTEST", "TRAIL", "ACYCLIC", "SIMPLE", ]; let displayed = toks .into_iter() diff --git a/partiql-parser/src/parse/mod.rs b/partiql-parser/src/parse/mod.rs index 10c237f5..1bcfea22 100644 --- a/partiql-parser/src/parse/mod.rs +++ b/partiql-parser/src/parse/mod.rs @@ -573,6 +573,133 @@ mod tests { } } + mod graph { + use super::*; + + #[test] + fn no_labels() { + parse!(r#"SELECT 1 FROM my_graph MATCH ()"#); + parse!(r#"SELECT 1 FROM my_graph MATCH () WHERE contains_value('1')"#); + parse!(r#"SELECT x.info AS info FROM my_graph MATCH (x) WHERE x.name LIKE 'foo'"#); + //parse!(r#"SELECT 1 FROM g MATCH -[]-> "#); + } + + #[test] + fn labelled_nodes() { + parse!(r#"SELECT x AS target FROM my_graph MATCH (x:Label) WHERE x.has_data = true"#); + } + + #[test] + fn edges() { + parse!(r#"SELECT a,b FROM g MATCH (a:A) -[e:E]-> (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) -> (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) ~[e:E]~ (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) ~ (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) <-[e:E]- (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) <- (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) ~[e:E]~> (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) ~> (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) <~[e:E]~ (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) <~ (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) <-[e:E]-> (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) <-> (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) -[e:E]- (b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A) - (b:B)"#); + } + + #[test] + fn quantifiers() { + parse!(r#"SELECT a,b FROM g MATCH (a:A)-[:edge]->*(b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A)<-[:edge]-+(b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A)~[:edge]~{5,}(b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A)-[e:edge]-{2,6}(b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A)->*(b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A)<-+(b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A)~{5,}(b:B)"#); + parse!(r#"SELECT a,b FROM g MATCH (a:A)-{2,6}(b:B)"#); + } + + #[test] + fn patterns() { + parse!( + r#"SELECT the_a.name AS src, the_b.name AS dest FROM my_graph MATCH (the_a:a) -[the_y:y]-> (the_b:b) WHERE the_y.score > 10"# + ); + parse!(r#""SELECT a,b FROM g MATCH (a)-[:has]->()-[:contains]->(b)""#); + parse!(r#"SELECT a,b FROM (g MATCH (a) -[:has]-> (x), (x)-[:contains]->(b))"#); + } + + #[test] + fn path_var() { + parse!(r#"SELECT a,b FROM g MATCH p = (a:A) -[e:E]-> (b:B)"#); + } + + #[test] + fn paranthesized() { + parse!(r#"SELECT a,b FROM g MATCH [(a:A)-[e:Edge]->(b:A) WHERE a.owner=b.owner]{2,5}"#); + parse!(r#"SELECT a,b FROM g MATCH pathVar = (a:A)[()-[e:Edge]->()]{1,3}(b:B)"#); + + // brackets + parse!(r#"SELECT a,b FROM g MATCH pathVar = (a:A)[-[e:Edge]->]*(b:B)"#); + // parens + parse!(r#"SELECT a,b FROM g MATCH pathVar = (a:A)(-[e:Edge]->)*(b:B)"#); + } + + #[test] + fn filters() { + parse!( + r#"SELECT u as banCandidate FROM g MATCH (p:Post Where p.isFlagged = true) <-[:createdPost]- (u:User WHERE u.isBanned = false AND u.karma < 20) -[:createdComment]->(c:Comment WHERE c.isFlagged = true) WHERE p.title LIKE '%considered harmful%'"# + ); + } + + #[test] + fn restrictors() { + parse!( + r#"SELECT p FROM g MATCH TRAIL p = (a WHERE a.owner='Dave') -[t:Transfer]-> * (b WHERE b.owner='Aretha')"# + ); + parse!( + r#"SELECT p FROM g MATCH SIMPLE p = (a WHERE a.owner='Dave') -[t:Transfer]-> * (b WHERE b.owner='Aretha')"# + ); + parse!( + r#"SELECT p FROM g MATCH ACYCLIC p = (a WHERE a.owner='Dave') -[t:Transfer]-> * (b WHERE b.owner='Aretha')"# + ); + } + + #[test] + fn selectors() { + parse!( + r#"SELECT p FROM g MATCH ANY SHORTEST p = (a WHERE a.owner='Dave') -[t:Transfer]-> * (b WHERE b.owner='Aretha')"# + ); + parse!( + r#"SELECT p FROM g MATCH ALL SHORTEST p = (a WHERE a.owner='Dave') -[t:Transfer]-> * (b WHERE b.owner='Aretha')"# + ); + parse!( + r#"SELECT p FROM g MATCH ANY p = (a WHERE a.owner='Dave') -[t:Transfer]-> * (b WHERE b.owner='Aretha')"# + ); + parse!( + r#"SELECT p FROM g MATCH ANY 5 p = (a WHERE a.owner='Dave') -[t:Transfer]-> * (b WHERE b.owner='Aretha')"# + ); + parse!( + r#"SELECT p FROM g MATCH SHORTEST 5 p = (a WHERE a.owner='Dave') -[t:Transfer]-> * (b WHERE b.owner='Aretha')"# + ); + parse!( + r#"SELECT p FROM g MATCH SHORTEST 5 GROUP p = (a WHERE a.owner='Dave') -[t:Transfer]-> * (b WHERE b.owner='Aretha')"# + ); + } + + #[test] + fn match_and_join() { + parse!( + r#"SELECT a,b,c, t1.x as x, t2.y as y FROM (graph MATCH (a) -> (b), (a) -> (c)), table1 as t1, table2 as t2"# + ); + } + + #[test] + fn etc() { + parse!("SELECT * FROM g MATCH ALL SHORTEST [ (x)-[e]->*(y) ]"); + parse!("SELECT * FROM g MATCH ALL SHORTEST [ TRAIL (x)-[e]->*(y) ]"); + } + } + mod errors { use super::*; use crate::error::{LexError, UnexpectedToken, UnexpectedTokenData}; diff --git a/partiql-parser/src/parse/partiql.lalrpop b/partiql-parser/src/parse/partiql.lalrpop index 3075a8a5..3bef83cd 100644 --- a/partiql-parser/src/parse/partiql.lalrpop +++ b/partiql-parser/src/parse/partiql.lalrpop @@ -238,6 +238,7 @@ TableReference: ast::FromClauseAst = { TableNonJoin: ast::FromClauseAst = { => ast::FromClause::FromLet( t ).ast(lo..hi), => ast::FromClause::FromLet( t ).ast(lo..hi), + => ast::FromClause::GraphMatch( t ).ast(lo..hi), } #[inline] @@ -262,7 +263,6 @@ TableBaseReference: ast::FromLetAst = { } } -#[inline] TableUnpivot: ast::FromLetAst = { "UNPIVOT" => { ast::FromLet { @@ -275,6 +275,200 @@ TableUnpivot: ast::FromLetAst = { } } +TableMatch: ast::GraphMatchAst = { + "MATCH" => { + let graph_expr = Box::new(ast::GraphMatchExpr{selector, patterns: vec![patterns]}.ast(lo..hi)); + ast::GraphMatch{ expr, graph_expr }.ast(lo..hi) + }, + "(" "MATCH" > ")" => { + let graph_expr = Box::new(ast::GraphMatchExpr{selector, patterns}.ast(lo..hi)); + ast::GraphMatch{ expr, graph_expr }.ast(lo..hi) + }, +} + +#[inline] +MatchPatternSelector: ast::GraphMatchSelector = { + "ANY" "SHORTEST" => ast::GraphMatchSelector::AnyShortest, + "ALL" "SHORTEST" => ast::GraphMatchSelector::AllShortest, + "ANY" => { + // TODO handle bad number parse + k.map(|n| ast::GraphMatchSelector::AnyK(n.parse().unwrap())).unwrap_or(ast::GraphMatchSelector::Any) + }, + "SHORTEST" => { + // TODO handle bad number parse + ast::GraphMatchSelector::ShortestK(k.parse().unwrap()) + }, + "SHORTEST" "GROUP" => { + // TODO handle bad number parse + ast::GraphMatchSelector::ShortestKGroup(k.parse().unwrap()) + } +} + +MatchPattern: ast::GraphMatchPatternAst = { + => { + ast::GraphMatchPattern{ + restrictor, + quantifier: None, + prefilter: None, + variable, + parts, + }.ast(lo..hi) + }, + "(" ")" => { + ast::GraphMatchPattern{ + quantifier, + prefilter, + ..pattern.node + }.ast(lo..hi) + }, + "[" "]" => { + ast::GraphMatchPattern{ + quantifier, + prefilter, + ..pattern.node + }.ast(lo..hi) + }, +} + +MatchPatternNested: ast::GraphMatchPatternAst = { + => { + ast::GraphMatchPattern{ + restrictor, + quantifier: None, + prefilter: None, + variable, + parts, + }.ast(lo..hi) + } +} + +#[inline] +MatchPatternRestrictor: ast::GraphMatchRestrictor = { + "TRAIL" => ast::GraphMatchRestrictor::Trail, + "ACYCLIC" => ast::GraphMatchRestrictor::Acyclic, + "SIMPLE" => ast::GraphMatchRestrictor::Simple, +} + +#[inline] +MatchPatternParts: Vec = { + => { + let node = ast::GraphMatchPatternPart::Node(n); + std::iter::once(node).chain(parts.into_iter().flatten()).collect() + } +} + +#[inline] +MatchPatternPartsNested: Vec = { + , + , + => vec![ast::GraphMatchPatternPart::Edge(e)], +} + +MatchPatternPartContinue: Vec = { + => vec![ast::GraphMatchPatternPart::Edge(e),ast::GraphMatchPatternPart::Node(n)], + => vec![ast::GraphMatchPatternPart::Pattern(p),ast::GraphMatchPatternPart::Node(n)], +} + +MatchPatternPartParen: ast::GraphMatchPatternAst = { + "(" ")" => { + ast::GraphMatchPattern { + prefilter, + quantifier, + ..pattern.node + }.ast(lo..hi) + }, + "[" "]" => { + ast::GraphMatchPattern { + prefilter, + quantifier, + ..pattern.node + }.ast(lo..hi) + }, +} + +MatchPatternPartNode: ast::GraphMatchNodeAst = { + "(" ")" => { + ast::GraphMatchNode { + prefilter, + variable, + label, + }.ast(lo..hi) + }, +} + +#[inline] +MatchPatternQuantifier: ast::GraphMatchQuantifierAst = { + "+" => ast::GraphMatchQuantifier{ lower:0, upper:None }.ast(lo..hi), + "*" => ast::GraphMatchQuantifier{ lower:1, upper:None }.ast(lo..hi), + "{" "," "}" => { + // TODO error on invalid literal + ast::GraphMatchQuantifier{ lower: lower.parse().unwrap(), upper: upper.map(|n| n.parse().unwrap()) }.ast(lo..hi) + }, +} + +MatchPatternPartEdge: ast::GraphMatchEdgeAst = { + => ast::GraphMatchEdge{ quantifier, ..spec}.ast(lo..hi), + => ast::GraphMatchEdge{ quantifier, ..spec}.ast(lo..hi), +} + +MatchPatternPartEdgeWSpec: ast::GraphMatchEdge = { + "-" "-" ">" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::Right, ..spec}, + "~" "~" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::Undirected, ..spec}, + "<" "-" "-" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::Left, ..spec}, + "~" "~" ">" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::UndirectedOrRight, ..spec}, + "<" "~" "~" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::LeftOrUndirected, ..spec}, + "<" "-" "-" ">" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::LeftOrRight, ..spec}, + "-" "-" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::LeftOrUndirectedOrRight, ..spec}, +} + +MatchPatternPartEdgeSpec: ast::GraphMatchEdge = { + "[" "]" => { + ast::GraphMatchEdge { + direction: ast::GraphMatchDirection::Undirected, + quantifier: None, + prefilter, + variable, + label, + } + } +} + +MatchPatternPartEdgeAbbr: ast::GraphMatchEdge = { + "-" ">" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::Right, quantifier: None, prefilter: None, variable: None, label: Default::default() }, + "~" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::Undirected, quantifier: None, prefilter: None, variable: None, label: Default::default() }, + "<" "-" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::Left, quantifier: None, prefilter: None, variable: None, label: Default::default() }, + "~" ">" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::UndirectedOrRight, quantifier: None, prefilter: None, variable: None, label: Default::default() }, + "<" "~" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::LeftOrUndirected, quantifier: None, prefilter: None, variable: None, label: Default::default() }, + "<" "-" ">" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::LeftOrRight, quantifier: None, prefilter: None, variable: None, label: Default::default() }, + "-" => ast::GraphMatchEdge{ direction: ast::GraphMatchDirection::LeftOrUndirectedOrRight, quantifier: None, prefilter: None, variable: None, label: Default::default() }, +} + +#[inline] +MatchPatternPartName: ast::SymbolPrimitive = { + => { + ast::SymbolPrimitive { + value: name.to_owned(), + case: Some(ast::CaseSensitivity::CaseSensitive) + } + } +} + +#[inline] // TODO conjunction/disjunction/negation +MatchPatternPartLabel: Vec = { + ":" => vec![l] +} + +#[inline] +MatchPatternPartPrefilter: Box = { + "WHERE" +} + +#[inline] +MatchPatternPathVariable: ast::SymbolPrimitive = { + "=" +} + + TableJoined: ast::FromClauseAst = { , , @@ -1210,6 +1404,7 @@ extern { "==" => lexer::Token::EqualEqual, "!=" => lexer::Token::BangEqual, "<>" => lexer::Token::LessGreater, + "~" => lexer::Token::Tilde, "<" => lexer::Token::LessThan, ">" => lexer::Token::GreaterThan, @@ -1229,9 +1424,11 @@ extern { "Ion" => lexer::Token::Ion(<&'input str>), // Keywords + "ACYCLIC" => lexer::Token::Acyclic, "ALL" => lexer::Token::All, "ASC" => lexer::Token::Asc, "AND" => lexer::Token::And, + "ANY" => lexer::Token::Any, "AS" => lexer::Token::As, "AT" => lexer::Token::At, "BETWEEN" => lexer::Token::Between, @@ -1261,6 +1458,7 @@ extern { "LEFT" => lexer::Token::Left, "LIKE" => lexer::Token::Like, "LIMIT" => lexer::Token::Limit, + "MATCH" => lexer::Token::Match, "MISSING" => lexer::Token::Missing, "NATURAL" => lexer::Token::Natural, "NOT" => lexer::Token::Not, @@ -1276,7 +1474,10 @@ extern { "PRESERVE" => lexer::Token::Preserve, "RIGHT" => lexer::Token::Right, "SELECT" => lexer::Token::Select, + "SIMPLE" => lexer::Token::Simple, + "SHORTEST" => lexer::Token::Shortest, "THEN" => lexer::Token::Then, + "TRAIL" => lexer::Token::Trail, "TRUE" => lexer::Token::True, "UNION" => lexer::Token::Union, "UNPIVOT" => lexer::Token::Unpivot,