use super::ast::*;
use super::tokenizer::{keyword_name_token, token_to_keyword_name, CypherToken};
#[cfg(test)]
use crate::datatypes::values::Value;
use crate::error::KgError;
pub mod clauses;
pub mod expression;
pub mod match_pattern;
pub mod predicate;
pub struct CypherParser {
tokens: Vec<CypherToken>,
pos: usize,
depth: usize,
keyword_lexemes: std::collections::HashMap<usize, String>,
}
const MAX_EXPRESSION_DEPTH: usize = 512;
impl CypherParser {
pub fn with_keyword_lexemes(
tokens: Vec<CypherToken>,
keyword_lexemes: Vec<(usize, String)>,
) -> Self {
CypherParser {
tokens,
pos: 0,
depth: 0,
keyword_lexemes: keyword_lexemes.into_iter().collect(),
}
}
pub(super) fn keyword_lexeme_at(&self, idx: usize) -> Option<&str> {
self.keyword_lexemes.get(&idx).map(String::as_str)
}
pub(super) fn descend<T>(
&mut self,
f: impl FnOnce(&mut Self) -> Result<T, String>,
) -> Result<T, String> {
if self.depth >= MAX_EXPRESSION_DEPTH {
return Err(format!(
"Expression nesting exceeds {} levels; simplify the query",
MAX_EXPRESSION_DEPTH
));
}
self.depth += 1;
let result = f(self);
self.depth -= 1;
result
}
pub(super) fn peek(&self) -> Option<&CypherToken> {
self.tokens.get(self.pos)
}
pub(super) fn peek_at(&self, offset: usize) -> Option<&CypherToken> {
self.tokens.get(self.pos + offset)
}
pub(super) fn advance(&mut self) -> Option<&CypherToken> {
let token = self.tokens.get(self.pos);
if token.is_some() {
self.pos += 1;
}
token
}
pub(super) fn expect(&mut self, expected: &CypherToken) -> Result<(), String> {
match self.peek() {
Some(t) if t == expected => {
self.advance();
Ok(())
}
Some(t) => Err(format!("Expected {:?}, found {:?}", expected, t)),
None => Err(format!("Expected {:?}, but reached end of query", expected)),
}
}
pub(super) fn has_tokens(&self) -> bool {
self.pos < self.tokens.len()
}
pub(super) fn check(&self, token: &CypherToken) -> bool {
self.peek() == Some(token)
}
pub(super) fn try_consume_alias_name(&mut self) -> Result<String, String> {
match self.advance().cloned() {
Some(CypherToken::Identifier(name)) => Ok(name),
Some(ref token) => token_to_keyword_name(token)
.map(|canonical| {
self.keyword_lexeme_at(self.pos - 1)
.map(str::to_string)
.unwrap_or(canonical)
})
.ok_or_else(|| format!("Expected alias name after AS, got {:?}", token)),
None => Err("Expected alias name after AS".to_string()),
}
}
pub(super) fn expect_name(&mut self, context: &str) -> Result<String, String> {
match self.advance().cloned() {
Some(CypherToken::Identifier(name)) => Ok(name),
Some(ref token) => keyword_name_token(token)
.map(|canonical| {
self.keyword_lexeme_at(self.pos - 1)
.map(str::to_string)
.unwrap_or_else(|| canonical.to_string())
})
.ok_or_else(|| format!("Expected {}, got {:?}", context, token)),
None => Err(format!("Expected {}", context)),
}
}
pub(super) fn at_clause_boundary(&self) -> bool {
match self.peek() {
Some(CypherToken::Where)
| Some(CypherToken::Return)
| Some(CypherToken::With)
| Some(CypherToken::Limit)
| Some(CypherToken::Skip)
| Some(CypherToken::Unwind)
| Some(CypherToken::Union)
| Some(CypherToken::Intersect)
| Some(CypherToken::Except)
| Some(CypherToken::Create)
| Some(CypherToken::Set)
| Some(CypherToken::Delete)
| Some(CypherToken::Detach)
| Some(CypherToken::Merge)
| Some(CypherToken::Remove)
| Some(CypherToken::Foreach)
| Some(CypherToken::On)
| Some(CypherToken::Call)
| Some(CypherToken::Yield)
| Some(CypherToken::Having) => true,
Some(CypherToken::Match) => true,
Some(CypherToken::Optional) => {
self.peek_at(1) == Some(&CypherToken::Match)
}
Some(CypherToken::Order) => {
self.peek_at(1) == Some(&CypherToken::By)
}
None => true,
_ => false,
}
}
pub fn parse_query(&mut self) -> Result<CypherQuery, String> {
let mut explain = false;
let mut profile = false;
if self.check(&CypherToken::Explain) {
self.advance();
explain = true;
} else if self.check(&CypherToken::Profile) {
self.advance();
profile = true;
}
let (clauses, output_format) = self.parse_clause_sequence(false)?;
if clauses.is_empty() {
return Err("Empty query".to_string());
}
Ok(CypherQuery {
clauses,
explain,
profile,
output_format,
optimizer_tags: Vec::new(),
})
}
pub(super) fn parse_clause_sequence(
&mut self,
end_at_rbrace: bool,
) -> Result<(Vec<Clause>, OutputFormat), String> {
let mut clauses = Vec::new();
while self.has_tokens() {
if end_at_rbrace && self.check(&CypherToken::RBrace) {
break;
}
if self.check(&CypherToken::Semicolon) {
self.advance();
continue;
}
match self.peek() {
Some(CypherToken::Match) => {
clauses.push(self.parse_match_clause(false)?);
}
Some(CypherToken::Optional) => {
if self.peek_at(1) == Some(&CypherToken::Match) {
self.advance(); clauses.push(self.parse_match_clause(true)?);
} else {
return Err("Expected MATCH after OPTIONAL".to_string());
}
}
Some(CypherToken::Where) => {
clauses.push(self.parse_where_clause()?);
}
Some(CypherToken::Return) => {
clauses.push(self.parse_return_clause()?);
}
Some(CypherToken::With) => {
clauses.push(self.parse_with_clause()?);
}
Some(CypherToken::Order) => {
clauses.push(self.parse_order_by_clause()?);
}
Some(CypherToken::Limit) => {
clauses.push(self.parse_limit_clause()?);
}
Some(CypherToken::Skip) => {
clauses.push(self.parse_skip_clause()?);
}
Some(CypherToken::Unwind) => {
clauses.push(self.parse_unwind_clause()?);
}
Some(CypherToken::Union)
| Some(CypherToken::Intersect)
| Some(CypherToken::Except)
if end_at_rbrace =>
{
return Err(
"UNION / INTERSECT / EXCEPT inside a CALL { } subquery is not supported \
in this version"
.to_string(),
);
}
Some(CypherToken::Union) => {
clauses.push(self.parse_union_clause()?);
}
Some(CypherToken::Intersect) => {
clauses.push(self.parse_intersect_clause()?);
}
Some(CypherToken::Except) => {
clauses.push(self.parse_except_clause()?);
}
Some(CypherToken::Create) => {
clauses.push(self.parse_create_clause()?);
}
Some(CypherToken::Set) => {
clauses.push(self.parse_set_clause()?);
}
Some(CypherToken::Delete) | Some(CypherToken::Detach) => {
clauses.push(self.parse_delete_clause()?);
}
Some(CypherToken::Remove) => {
clauses.push(self.parse_remove_clause()?);
}
Some(CypherToken::Merge) => {
clauses.push(self.parse_merge_clause()?);
}
Some(CypherToken::Call) => {
clauses.push(self.parse_call_clause()?);
}
Some(CypherToken::Foreach) => {
clauses.push(self.parse_foreach_clause()?);
}
Some(CypherToken::Identifier(s)) if s.eq_ignore_ascii_case("FORMAT") => {
if end_at_rbrace {
return Err(
"FORMAT is not allowed inside a CALL { } subquery body".to_string()
);
}
self.advance(); match self.peek() {
Some(CypherToken::Identifier(fmt)) if fmt.eq_ignore_ascii_case("CSV") => {
self.advance(); return Ok((clauses, OutputFormat::Csv));
}
other => {
return Err(format!(
"Expected format name after FORMAT (supported: CSV), got {:?}",
other
));
}
}
}
Some(t) => {
return Err(format!("Unexpected token at start of clause: {:?}", t));
}
None => break,
}
}
Ok((clauses, OutputFormat::Default))
}
}
pub fn parse_cypher(input: &str) -> Result<CypherQuery, KgError> {
let positioned =
super::tokenizer::tokenize_cypher_with_positions(input).map_err(|tokenizer_err| {
KgError::CypherSyntax {
message: tokenizer_err,
line: None,
col: None,
}
})?;
let keyword_lexemes = positioned.keyword_lexemes;
let (tokens, positions): (Vec<_>, Vec<_>) = positioned.tokens.into_iter().unzip();
let mut parser = CypherParser::with_keyword_lexemes(tokens, keyword_lexemes);
match parser.parse_query() {
Ok(q) => Ok(q),
Err(e) => {
let char_offset = positions
.get(parser.pos)
.copied()
.unwrap_or_else(|| input.chars().count());
let (line, col) = char_offset_to_line_col(input, char_offset);
let message = format_parse_error_message(input, &e, line, col);
Err(KgError::CypherSyntax {
message,
line: Some(line),
col: Some(col),
})
}
}
}
fn char_offset_to_line_col(input: &str, target_char: usize) -> (usize, usize) {
let mut line = 1usize;
let mut col = 1usize;
for (idx, ch) in input.chars().enumerate() {
if idx == target_char {
return (line, col);
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
fn intent_level_rewrite(_input: &str, _err: &str) -> Option<String> {
None
}
fn format_parse_error_message(input: &str, err: &str, line: usize, col: usize) -> String {
let intent = intent_level_rewrite(input, err);
let lines: Vec<&str> = input.lines().collect();
let excerpt = if line >= 1 && line <= lines.len() {
let src_line = lines[line - 1];
let caret_col = col.saturating_sub(1).min(src_line.len());
let caret = format!("{:width$}^", "", width = caret_col);
format!("\n {}\n {}", src_line, caret)
} else {
String::new()
};
let body = intent.as_deref().unwrap_or(err);
format!("{}{}", body, excerpt)
}
#[cfg(test)]
#[path = "parser_tests.rs"]
mod parser_tests;