use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RowCount {
Literal(u64),
Parameter(String),
}
impl RowCount {
pub fn value(&self) -> crate::error::Result<u64> {
match self {
Self::Literal(value) => Ok(*value),
Self::Parameter(name) => Err(crate::error::GraphError::PlanError {
message: format!("Unresolved pagination parameter: ${name}"),
location: snafu::Location::new(file!(), line!(), column!()),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CypherQuery {
pub reading_clauses: Vec<ReadingClause>,
pub where_clause: Option<WhereClause>,
pub with_clause: Option<WithClause>,
pub post_with_reading_clauses: Vec<ReadingClause>,
pub post_with_where_clause: Option<WhereClause>,
#[serde(default)]
pub further_with_stages: Vec<WithStage>,
pub return_clause: ReturnClause,
pub limit: Option<RowCount>,
pub order_by: Option<OrderByClause>,
pub skip: Option<RowCount>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub union_branches: Vec<UnionBranch>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UnionBranch {
pub all: bool,
pub query: CypherQuery,
}
impl CypherQuery {
pub(crate) fn all_reading_clauses(&self) -> impl Iterator<Item = &ReadingClause> {
self.reading_clauses
.iter()
.chain(&self.post_with_reading_clauses)
.chain(
self.further_with_stages
.iter()
.flat_map(|stage| &stage.reading_clauses),
)
}
pub(crate) fn for_each_query(&self, visit: &mut dyn FnMut(&Self)) {
visit(self);
for clause in self.all_reading_clauses() {
if let ReadingClause::Call(query) = clause {
query.for_each_query(visit);
}
}
for branch in &self.union_branches {
branch.query.for_each_query(visit);
}
}
pub fn get_node_labels(&self) -> Vec<String> {
let mut labels = Vec::new();
for clause in self.all_reading_clauses() {
if let ReadingClause::Call(inner) = clause {
for label in inner.get_node_labels() {
if !labels.contains(&label) {
labels.push(label);
}
}
}
if let ReadingClause::Match(match_clause) = clause {
for pattern in &match_clause.patterns {
match pattern {
GraphPattern::Node(node) => {
for label in &node.labels {
if !labels.contains(label) {
labels.push(label.clone());
}
}
}
GraphPattern::Path(path) => {
for label in &path.start_node.labels {
if !labels.contains(label) {
labels.push(label.clone());
}
}
for segment in &path.segments {
for label in &segment.end_node.labels {
if !labels.contains(label) {
labels.push(label.clone());
}
}
}
}
}
}
}
}
for branch in &self.union_branches {
for label in branch.query.get_node_labels() {
if !labels.contains(&label) {
labels.push(label);
}
}
}
labels
}
pub fn get_relationship_types(&self) -> Vec<String> {
let mut types = Vec::new();
for clause in self.all_reading_clauses() {
if let ReadingClause::Call(inner) = clause {
for kind in inner.get_relationship_types() {
if !types.contains(&kind) {
types.push(kind);
}
}
}
if let ReadingClause::Match(match_clause) = clause {
for pattern in &match_clause.patterns {
self.collect_relationship_types_from_pattern(pattern, &mut types);
}
}
}
for branch in &self.union_branches {
for rel_type in branch.query.get_relationship_types() {
if !types.contains(&rel_type) {
types.push(rel_type);
}
}
}
types
}
fn collect_relationship_types_from_pattern(
&self,
pattern: &GraphPattern,
types: &mut Vec<String>,
) {
if let GraphPattern::Path(path) = pattern {
for segment in &path.segments {
for rel_type in &segment.relationship.types {
if !types.contains(rel_type) {
types.push(rel_type.clone());
}
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ReadingClause {
Match(MatchClause),
Unwind(UnwindClause),
Call(Box<CypherQuery>),
}
impl CypherQuery {
pub(crate) fn call_union_imports(&self) -> crate::error::Result<Vec<String>> {
let mut names = self.call_imports()?;
for branch in &self.union_branches {
names.extend(branch.query.call_union_imports()?);
}
names.sort();
names.dedup();
Ok(names)
}
pub(crate) fn call_imports(&self) -> crate::error::Result<Vec<String>> {
let Some(clause) = self
.with_clause
.as_ref()
.filter(|_| self.reading_clauses.is_empty())
else {
return Ok(vec![]);
};
if clause.distinct
|| clause.order_by.is_some()
|| clause.limit.is_some()
|| clause.where_clause.is_some()
{
return Err(crate::error::GraphError::PlanError {
message: "CALL importing WITH must contain only unaliased variable names; use a second WITH for expressions or filters".into(),
location: snafu::Location::new(file!(), line!(), column!()),
});
}
clause.items.iter().map(|item| match (&item.expression, &item.alias) {
(ValueExpression::Variable(name), None) => Ok(name.to_lowercase()),
_ => Err(crate::error::GraphError::PlanError {
message: "CALL importing WITH must contain only unaliased variable names; use a second WITH for expressions".into(),
location: snafu::Location::new(file!(), line!(), column!()),
}),
}).collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MatchClause {
pub patterns: Vec<GraphPattern>,
#[serde(default)]
pub optional: bool,
#[serde(default)]
pub where_clause: Option<WhereClause>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UnwindClause {
pub expression: ValueExpression,
pub alias: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GraphPattern {
Node(NodePattern),
Path(PathPattern),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NodePattern {
pub variable: Option<String>,
pub labels: Vec<String>,
pub properties: HashMap<String, PropertyValue>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PathPattern {
#[serde(default)]
pub variable: Option<String>,
pub start_node: NodePattern,
pub segments: Vec<PathSegment>,
#[serde(default)]
pub shortest: Option<ShortestPathSpec>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ShortestPathSpec {
pub all: bool,
pub variable: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PathSegment {
pub relationship: RelationshipPattern,
pub end_node: NodePattern,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RelationshipPattern {
pub variable: Option<String>,
pub types: Vec<String>,
pub direction: RelationshipDirection,
pub properties: HashMap<String, PropertyValue>,
pub length: Option<LengthRange>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum RelationshipDirection {
Outgoing,
Incoming,
Undirected,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LengthRange {
pub min: Option<u32>,
pub max: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PropertyValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Null,
Parameter(String),
Property(PropertyRef),
Variable(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PropertyRef {
pub variable: String,
pub property: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WhereClause {
pub expression: BooleanExpression,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BooleanExpression {
Comparison {
left: ValueExpression,
operator: ComparisonOperator,
right: ValueExpression,
},
And(Box<BooleanExpression>, Box<BooleanExpression>),
Or(Box<BooleanExpression>, Box<BooleanExpression>),
Not(Box<BooleanExpression>),
Exists(PropertyRef),
PatternExists {
patterns: Vec<GraphPattern>,
where_clause: Option<Box<BooleanExpression>>,
},
In {
expression: ValueExpression,
list: Vec<ValueExpression>,
},
Member {
expression: ValueExpression,
list: ValueExpression,
},
Like {
expression: ValueExpression,
pattern: String,
},
ILike {
expression: ValueExpression,
pattern: String,
},
Contains {
expression: ValueExpression,
substring: String,
},
StartsWith {
expression: ValueExpression,
prefix: String,
},
EndsWith {
expression: ValueExpression,
suffix: String,
},
IsNull(ValueExpression),
IsNotNull(ValueExpression),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub enum DistanceMetric {
L2,
#[default]
Cosine,
Dot,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ComparisonOperator {
Equal,
NotEqual,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ValueExpression {
Variable(String),
Property(PropertyRef),
Literal(PropertyValue),
Case {
whens: Vec<(BooleanExpression, ValueExpression)>,
otherwise: Option<Box<ValueExpression>>,
},
ScalarFunction {
name: String,
args: Vec<ValueExpression>,
},
AggregateFunction {
name: String,
args: Vec<ValueExpression>,
distinct: bool,
},
Arithmetic {
left: Box<ValueExpression>,
operator: ArithmeticOperator,
right: Box<ValueExpression>,
},
VectorDistance {
left: Box<ValueExpression>,
right: Box<ValueExpression>,
metric: DistanceMetric,
},
VectorSimilarity {
left: Box<ValueExpression>,
right: Box<ValueExpression>,
metric: DistanceMetric,
},
Parameter(String),
Boolean(Box<BooleanExpression>),
Map(Vec<(String, ValueExpression)>),
Field {
base: Box<ValueExpression>,
field: String,
},
PatternComprehension {
pattern: Box<PathPattern>,
filter: Option<Box<BooleanExpression>>,
map: Box<ValueExpression>,
},
ListComprehension {
variable: String,
list: Box<ValueExpression>,
filter: Option<Box<BooleanExpression>>,
map: Option<Box<ValueExpression>>,
},
LocalVariable(String),
Reduce {
accumulator: String,
initial: Box<ValueExpression>,
variable: String,
list: Box<ValueExpression>,
expression: Box<ValueExpression>,
},
ListPredicate {
kind: String,
variable: String,
list: Box<ValueExpression>,
predicate: Box<ValueExpression>,
},
VectorLiteral(Vec<f32>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum FunctionType {
Aggregate,
Scalar,
Unknown,
}
pub fn classify_function(name: &str) -> FunctionType {
match name.to_lowercase().as_str() {
"count" | "sum" | "avg" | "min" | "max" | "collect" => FunctionType::Aggregate,
"tolower" | "lower" | "toupper" | "upper" | "coalesce" | "date_part" | "to_epoch_ms"
| "length" | "nodes" | "size" | "range" | "__index" | "__list" | "tostring" | "head"
| "tointeger" | "tofloat" | "date" | "datetime" | "duration.inseconds" => {
FunctionType::Scalar
}
_ => FunctionType::Unknown,
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ArithmeticOperator {
Add,
Subtract,
Multiply,
Divide,
Modulo,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WithClause {
#[serde(default)]
pub distinct: bool,
pub items: Vec<ReturnItem>,
pub order_by: Option<OrderByClause>,
pub limit: Option<RowCount>,
#[serde(default)]
pub where_clause: Option<WhereClause>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WithStage {
pub with_clause: WithClause,
pub reading_clauses: Vec<ReadingClause>,
pub where_clause: Option<WhereClause>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReturnClause {
pub distinct: bool,
pub items: Vec<ReturnItem>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReturnItem {
pub expression: ValueExpression,
pub alias: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OrderByClause {
pub items: Vec<OrderByItem>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OrderByItem {
pub expression: ValueExpression,
pub direction: SortDirection,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SortDirection {
Ascending,
Descending,
}
impl NodePattern {
pub fn new(variable: Option<String>) -> Self {
Self {
variable,
labels: Vec::new(),
properties: HashMap::new(),
}
}
pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
self.labels.push(label.into());
self
}
pub fn with_property<S: Into<String>>(mut self, key: S, value: PropertyValue) -> Self {
self.properties.insert(key.into(), value);
self
}
}
impl RelationshipPattern {
pub fn new(direction: RelationshipDirection) -> Self {
Self {
variable: None,
types: Vec::new(),
direction,
properties: HashMap::new(),
length: None,
}
}
pub fn with_variable<S: Into<String>>(mut self, variable: S) -> Self {
self.variable = Some(variable.into());
self
}
pub fn with_type<S: Into<String>>(mut self, rel_type: S) -> Self {
self.types.push(rel_type.into());
self
}
pub fn with_property<S: Into<String>>(mut self, key: S, value: PropertyValue) -> Self {
self.properties.insert(key.into(), value);
self
}
}
impl PropertyRef {
pub fn new<S: Into<String>>(variable: S, property: S) -> Self {
Self {
variable: variable.into(),
property: property.into(),
}
}
}
pub fn transform_value(expr: &mut ValueExpression, f: &mut dyn FnMut(&mut ValueExpression)) {
match expr {
ValueExpression::ScalarFunction { args, .. }
| ValueExpression::AggregateFunction { args, .. } => {
args.iter_mut().for_each(|a| transform_value(a, f))
}
ValueExpression::Arithmetic { left, right, .. }
| ValueExpression::VectorDistance { left, right, .. }
| ValueExpression::VectorSimilarity { left, right, .. } => {
transform_value(left, f);
transform_value(right, f);
}
ValueExpression::Case { whens, otherwise } => {
for (condition, then) in whens {
transform_boolean_values(condition, f);
transform_value(then, f);
}
if let Some(e) = otherwise {
transform_value(e, f);
}
}
ValueExpression::Boolean(b) => transform_boolean_values(b, f),
ValueExpression::Map(entries) => {
entries.iter_mut().for_each(|(_, v)| transform_value(v, f))
}
ValueExpression::Field { base, .. } => transform_value(base, f),
ValueExpression::ListComprehension {
list, filter, map, ..
} => {
transform_value(list, f);
if let Some(b) = filter {
transform_boolean_values(b, f);
}
if let Some(m) = map {
transform_value(m, f);
}
}
ValueExpression::PatternComprehension { filter, map, .. } => {
if let Some(b) = filter {
transform_boolean_values(b, f);
}
transform_value(map, f);
}
ValueExpression::Reduce {
initial,
list,
expression,
..
} => {
transform_value(initial, f);
transform_value(list, f);
transform_value(expression, f);
}
ValueExpression::ListPredicate {
list, predicate, ..
} => {
transform_value(list, f);
transform_value(predicate, f);
}
ValueExpression::Variable(_)
| ValueExpression::LocalVariable(_)
| ValueExpression::Property(_)
| ValueExpression::Literal(_)
| ValueExpression::Parameter(_)
| ValueExpression::VectorLiteral(_) => {}
}
f(expr);
}
pub(crate) fn mark_locals(expr: &mut ValueExpression, names: &[&str]) {
transform_value(expr, &mut |value| match value {
ValueExpression::Variable(name) if names.iter().any(|n| n.eq_ignore_ascii_case(name)) => {
*value = ValueExpression::LocalVariable(name.to_lowercase());
}
ValueExpression::Property(p)
if names.iter().any(|n| n.eq_ignore_ascii_case(&p.variable)) =>
{
*value = ValueExpression::Field {
base: Box::new(ValueExpression::LocalVariable(p.variable.to_lowercase())),
field: p.property.clone(),
};
}
_ => {}
});
}
pub(crate) fn needs_relational_evaluation(expr: &ValueExpression) -> bool {
fn boolean_has_pattern(expr: &BooleanExpression) -> bool {
match expr {
BooleanExpression::PatternExists { .. } => true,
BooleanExpression::And(a, b) | BooleanExpression::Or(a, b) => {
boolean_has_pattern(a) || boolean_has_pattern(b)
}
BooleanExpression::Not(b) => boolean_has_pattern(b),
_ => false,
}
}
let mut found = false;
transform_value(&mut expr.clone(), &mut |value| {
found |= match value {
ValueExpression::PatternComprehension { .. }
| ValueExpression::ListComprehension { .. } => true,
ValueExpression::Boolean(b) => boolean_has_pattern(b),
ValueExpression::Case { whens, .. } => {
whens.iter().any(|(b, _)| boolean_has_pattern(b))
}
_ => false,
};
});
found
}
pub fn transform_boolean_values(
expr: &mut BooleanExpression,
f: &mut dyn FnMut(&mut ValueExpression),
) {
match expr {
BooleanExpression::Comparison { left, right, .. } => {
transform_value(left, f);
transform_value(right, f);
}
BooleanExpression::And(a, b) | BooleanExpression::Or(a, b) => {
transform_boolean_values(a, f);
transform_boolean_values(b, f);
}
BooleanExpression::Not(inner) => transform_boolean_values(inner, f),
BooleanExpression::In { expression, list } => {
transform_value(expression, f);
list.iter_mut().for_each(|v| transform_value(v, f));
}
BooleanExpression::Member { expression, list } => {
transform_value(expression, f);
transform_value(list, f);
}
BooleanExpression::Like { expression, .. }
| BooleanExpression::ILike { expression, .. }
| BooleanExpression::Contains { expression, .. }
| BooleanExpression::StartsWith { expression, .. }
| BooleanExpression::EndsWith { expression, .. }
| BooleanExpression::IsNull(expression)
| BooleanExpression::IsNotNull(expression) => transform_value(expression, f),
BooleanExpression::Exists(_) | BooleanExpression::PatternExists { .. } => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_pattern_creation() {
let node = NodePattern::new(Some("n".to_string()))
.with_label("Person")
.with_property("name", PropertyValue::String("John".to_string()));
assert_eq!(node.variable, Some("n".to_string()));
assert_eq!(node.labels, vec!["Person"]);
assert_eq!(node.properties.len(), 1);
}
#[test]
fn test_relationship_pattern_creation() {
let rel = RelationshipPattern::new(RelationshipDirection::Outgoing)
.with_variable("r")
.with_type("KNOWS");
assert_eq!(rel.variable, Some("r".to_string()));
assert_eq!(rel.types, vec!["KNOWS"]);
assert_eq!(rel.direction, RelationshipDirection::Outgoing);
}
#[test]
fn test_property_ref() {
let prop_ref = PropertyRef::new("n", "name");
assert_eq!(prop_ref.variable, "n");
assert_eq!(prop_ref.property, "name");
}
}