use super::super::ast::*;
use super::super::tokenizer::{keyword_name_token, reserved_literal_name_token, CypherToken};
use super::{describe_with_hint, describe_with_hint_opt, CypherParser};
impl CypherParser {
pub(super) fn parse_match_clause(&mut self, optional: bool) -> Result<Clause, String> {
self.expect(&CypherToken::Match)?;
let mut path_assignments = Vec::new();
let patterns = if self.is_path_assignment() {
let path_var = self.consume_identifier()?;
self.expect(&CypherToken::Equals)?;
let is_all_shortest = self.is_all_shortest_paths_call();
let is_shortest = is_all_shortest || self.is_shortest_path_call();
if is_shortest {
self.advance(); self.expect(&CypherToken::LParen)?;
}
let patterns = self.parse_match_patterns()?;
if is_shortest {
self.expect(&CypherToken::RParen)?;
}
path_assignments.push(PathAssignment {
variable: path_var,
pattern_index: 0,
is_shortest_path: is_shortest,
all_shortest: is_all_shortest,
});
patterns
} else {
self.parse_match_patterns()?
};
let where_clause = if optional && self.check(&CypherToken::Where) {
self.advance(); Some(WhereClause {
predicate: self.parse_predicate()?,
})
} else {
None
};
let clause = MatchClause {
patterns,
path_assignments,
limit_hint: None,
distinct_node_hint: None,
where_clause,
node_anchors: Vec::new(),
};
if optional {
Ok(Clause::OptionalMatch(clause))
} else {
Ok(Clause::Match(clause))
}
}
pub(super) fn is_path_assignment(&self) -> bool {
matches!(self.peek(), Some(CypherToken::Identifier(_)))
&& self.peek_at(1) == Some(&CypherToken::Equals)
}
pub(super) fn is_shortest_path_call(&self) -> bool {
if let Some(CypherToken::Identifier(name)) = self.peek() {
name.eq_ignore_ascii_case("shortestPath")
&& self.peek_at(1) == Some(&CypherToken::LParen)
} else {
false
}
}
pub(super) fn is_all_shortest_paths_call(&self) -> bool {
if let Some(CypherToken::Identifier(name)) = self.peek() {
name.eq_ignore_ascii_case("allShortestPaths")
&& self.peek_at(1) == Some(&CypherToken::LParen)
} else {
false
}
}
pub(super) fn consume_identifier(&mut self) -> Result<String, String> {
match self.advance() {
Some(CypherToken::Identifier(s)) => Ok(s.clone()),
other => Err(format!(
"Expected identifier, got {}",
describe_with_hint_opt(other)
)),
}
}
pub(super) fn parse_match_patterns(
&mut self,
) -> Result<Vec<crate::graph::core::pattern_matching::Pattern>, String> {
let mut patterns = Vec::new();
loop {
let pattern_str = self.extract_pattern_string()?;
if pattern_str.is_empty() {
return Err("Expected a pattern in MATCH clause".to_string());
}
let pattern = crate::graph::core::pattern_matching::parse_pattern(&pattern_str)
.map_err(|e| format!("Pattern parse error: {}", e))?;
patterns.push(pattern);
if self.check(&CypherToken::Comma) {
self.advance();
} else {
break;
}
}
Ok(patterns)
}
pub(super) fn parse_exists_patterns(
&mut self,
) -> Result<
(
Vec<crate::graph::core::pattern_matching::Pattern>,
Vec<usize>,
),
String,
> {
self.parse_pattern_subquery_patterns(&CypherToken::RBrace)
}
pub(super) fn parse_pattern_subquery_patterns(
&mut self,
end_token: &CypherToken,
) -> Result<
(
Vec<crate::graph::core::pattern_matching::Pattern>,
Vec<usize>,
),
String,
> {
let mut patterns = Vec::new();
let mut groups: Vec<usize> = Vec::new();
let mut group = 0usize;
loop {
let pattern_str = self.extract_pattern_subquery_string(end_token)?;
if pattern_str.is_empty() {
if patterns.is_empty() {
return Err("Expected a pattern inside EXISTS { }".to_string());
}
break;
}
let pattern = crate::graph::core::pattern_matching::parse_pattern(&pattern_str)
.map_err(|e| format!("Pattern parse error in EXISTS: {}", e))?;
patterns.push(pattern);
groups.push(group);
if self.check(&CypherToken::Comma) {
self.advance();
} else if self.check(&CypherToken::Match) {
group += 1;
} else {
break;
}
}
Ok((patterns, groups))
}
pub(super) fn quote_identifier(s: &str) -> String {
if s.contains(' ')
|| s.contains('-')
|| s.contains('/')
|| s.contains('.')
|| s.contains('(')
|| s.contains(')')
|| s.contains('`')
|| crate::graph::core::pattern_matching::parser::bare_word_needs_quoting(s)
{
backtick_quote(s)
} else {
s.to_string()
}
}
pub(super) fn extract_pattern_subquery_string(
&mut self,
end_token: &CypherToken,
) -> Result<String, String> {
if self.check(&CypherToken::Match) {
self.advance();
}
let mut parts = Vec::new();
let mut paren_depth = 0i32;
let mut bracket_depth = 0i32;
let mut brace_depth = 0i32;
let mut prev: Option<CypherToken> = None;
while self.has_tokens() {
if paren_depth == 0 && bracket_depth == 0 && self.check(end_token) {
break;
}
if paren_depth == 0 && bracket_depth == 0 && self.check(&CypherToken::Comma) {
break;
}
if paren_depth == 0 && bracket_depth == 0 && self.check(&CypherToken::Where) {
break;
}
if paren_depth == 0 && bracket_depth == 0 && self.check(&CypherToken::Match) {
break;
}
let token = self.advance().unwrap().clone();
match &token {
CypherToken::LParen => {
paren_depth += 1;
parts.push("(".to_string());
}
CypherToken::RParen => {
paren_depth -= 1;
parts.push(")".to_string());
}
CypherToken::LBracket => {
bracket_depth += 1;
parts.push("[".to_string());
}
CypherToken::RBracket => {
bracket_depth -= 1;
parts.push("]".to_string());
}
CypherToken::LBrace => {
brace_depth += 1;
parts.push("{".to_string());
}
CypherToken::RBrace => {
brace_depth -= 1;
parts.push("}".to_string());
}
CypherToken::Colon => parts.push(":".to_string()),
CypherToken::Comma => parts.push(",".to_string()),
CypherToken::Dash => parts.push("-".to_string()),
CypherToken::GreaterThan => parts.push(">".to_string()),
CypherToken::LessThan => parts.push("<".to_string()),
CypherToken::Star => parts.push("*".to_string()),
CypherToken::DotDot => parts.push("..".to_string()),
CypherToken::Dot => parts.push(".".to_string()),
CypherToken::Identifier(s) => parts.push(Self::quote_identifier(s)),
CypherToken::StringLit(s) => {
let escaped = s.replace('\\', "\\\\").replace('\'', "\\'");
parts.push(format!("'{}'", escaped));
}
CypherToken::IntLit(n) => parts.push(n.to_string()),
CypherToken::FloatLit(f) => parts.push(f.to_string()),
tok @ (CypherToken::True | CypherToken::False | CypherToken::Null)
if at_name_position(prev.as_ref(), self.peek(), brace_depth) =>
{
let name = self
.keyword_lexeme_at(self.pos - 1)
.unwrap_or_else(|| reserved_literal_name_token(tok).unwrap());
parts.push(backtick_quote(name));
}
CypherToken::True => parts.push("true".to_string()),
CypherToken::False => parts.push("false".to_string()),
CypherToken::Parameter(name) => parts.push(format!("${}", name)),
tok if keyword_name_token(tok).is_some() => {
let name = self
.keyword_lexeme_at(self.pos - 1)
.unwrap_or_else(|| keyword_name_token(tok).unwrap());
parts.push(backtick_quote(name));
}
_ => {
return Err(format!(
"Unexpected token in EXISTS pattern: {}",
describe_with_hint(&token)
));
}
}
prev = Some(token);
}
Ok(parts.join(" "))
}
pub(super) fn extract_pattern_string(&mut self) -> Result<String, String> {
let mut parts = Vec::new();
let mut paren_depth = 0i32;
let mut bracket_depth = 0i32;
let mut brace_depth = 0i32;
let mut prev: Option<CypherToken> = None;
while self.has_tokens() {
if paren_depth == 0 && bracket_depth == 0 && self.at_clause_boundary() {
break;
}
if paren_depth == 0 && bracket_depth == 0 && self.check(&CypherToken::Comma) {
break;
}
if paren_depth == 0
&& bracket_depth == 0
&& matches!(
self.peek(),
Some(CypherToken::And)
| Some(CypherToken::Or)
| Some(CypherToken::Xor)
| Some(CypherToken::As)
| Some(CypherToken::Then)
| Some(CypherToken::Else)
| Some(CypherToken::End)
| Some(CypherToken::Pipe)
)
{
break;
}
if paren_depth == 0 && self.check(&CypherToken::RParen) {
break;
}
if paren_depth == 0 && bracket_depth == 0 && self.check(&CypherToken::RBrace) {
break;
}
let token = self.advance().unwrap().clone();
match &token {
CypherToken::LParen => {
paren_depth += 1;
parts.push("(".to_string());
}
CypherToken::RParen => {
paren_depth -= 1;
parts.push(")".to_string());
}
CypherToken::LBracket => {
bracket_depth += 1;
parts.push("[".to_string());
}
CypherToken::RBracket => {
bracket_depth -= 1;
parts.push("]".to_string());
}
CypherToken::LBrace => {
brace_depth += 1;
parts.push("{".to_string());
}
CypherToken::RBrace => {
brace_depth -= 1;
parts.push("}".to_string());
}
CypherToken::Colon => parts.push(":".to_string()),
CypherToken::Comma => parts.push(",".to_string()),
CypherToken::Dash => parts.push("-".to_string()),
CypherToken::GreaterThan => parts.push(">".to_string()),
CypherToken::LessThan => parts.push("<".to_string()),
CypherToken::Star => parts.push("*".to_string()),
CypherToken::DotDot => parts.push("..".to_string()),
CypherToken::Dot => parts.push(".".to_string()),
CypherToken::Pipe => parts.push("|".to_string()),
CypherToken::Identifier(s) => parts.push(Self::quote_identifier(s)),
CypherToken::StringLit(s) => {
let escaped = s.replace('\\', "\\\\").replace('\'', "\\'");
parts.push(format!("'{}'", escaped));
}
CypherToken::IntLit(n) => parts.push(n.to_string()),
CypherToken::FloatLit(f) => parts.push(f.to_string()),
tok @ (CypherToken::True | CypherToken::False | CypherToken::Null)
if at_name_position(prev.as_ref(), self.peek(), brace_depth) =>
{
let name = self
.keyword_lexeme_at(self.pos - 1)
.unwrap_or_else(|| reserved_literal_name_token(tok).unwrap());
parts.push(backtick_quote(name));
}
CypherToken::True => parts.push("true".to_string()),
CypherToken::False => parts.push("false".to_string()),
CypherToken::Parameter(name) => {
parts.push(format!("${}", name));
}
tok if keyword_name_token(tok).is_some() => {
let name = self
.keyword_lexeme_at(self.pos - 1)
.unwrap_or_else(|| keyword_name_token(tok).unwrap());
parts.push(backtick_quote(name));
}
_ => {
return Err(format!(
"Unexpected token in MATCH pattern: {}",
describe_with_hint(&token)
));
}
}
prev = Some(token);
}
Ok(parts.join(""))
}
}
fn at_name_position(
prev: Option<&CypherToken>,
next: Option<&CypherToken>,
brace_depth: i32,
) -> bool {
if brace_depth > 0 {
matches!(next, Some(CypherToken::Colon))
} else {
matches!(prev, Some(CypherToken::Colon) | Some(CypherToken::Pipe))
}
}
pub(super) fn backtick_quote(name: &str) -> String {
let mut out = String::with_capacity(name.len() + 2);
out.push('`');
for ch in name.chars() {
if ch == '`' {
out.push('`');
}
out.push(ch);
}
out.push('`');
out
}