use crate::datatypes::values::Value;
use crate::graph::schema::InternedKey;
use petgraph::graph::{EdgeIndex, NodeIndex};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Pattern {
pub elements: Vec<PatternElement>,
}
#[derive(Debug, Clone)]
pub enum PatternElement {
Node(NodePattern),
Edge(EdgePattern),
}
#[derive(Debug, Clone)]
pub struct NodePattern {
pub variable: Option<String>,
pub node_type: Option<String>,
pub extra_labels: Vec<String>,
pub properties: Option<HashMap<String, PropertyMatcher>>,
}
#[derive(Debug, Clone)]
pub struct EdgePattern {
pub variable: Option<String>,
pub connection_type: Option<String>,
pub connection_types: Option<Vec<String>>,
pub direction: EdgeDirection,
pub properties: Option<HashMap<String, PropertyMatcher>>,
pub var_length: Option<(usize, usize)>,
pub needs_path_info: bool,
pub skip_target_type_check: bool,
pub edge_filter: Option<RelEdgeFilter>,
}
#[derive(Debug, Clone)]
pub struct RelEdgeFilter {
pub predicate: RelEdgePredicate,
pub anchor: AnchorSide,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnchorSide {
Source,
Target,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum RelEdgePredicate {
True,
False,
TypeIn(Vec<InternedKey>),
Property {
prop: String,
op: PropOp,
value: Value,
},
StartNodeIsPeer,
EndNodeIsPeer,
StartNodeIs(NodeIndex),
EndNodeIs(NodeIndex),
And(Vec<RelEdgePredicate>),
Or(Vec<RelEdgePredicate>),
Not(Box<RelEdgePredicate>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropOp {
Eq,
Ne,
Gt,
Ge,
Lt,
Le,
StartsWith,
Contains,
EndsWith,
}
impl RelEdgePredicate {
#[inline]
pub fn eval(
&self,
connection_type: InternedKey,
peer_is_start: bool,
edge_source: NodeIndex,
edge_target: NodeIndex,
get_prop: &impl Fn(&str) -> Option<Value>,
) -> bool {
self.eval_nullable(
connection_type,
peer_is_start,
edge_source,
edge_target,
get_prop,
) == Some(true)
}
#[inline]
fn eval_nullable(
&self,
connection_type: InternedKey,
peer_is_start: bool,
edge_source: NodeIndex,
edge_target: NodeIndex,
get_prop: &impl Fn(&str) -> Option<Value>,
) -> Option<bool> {
match self {
RelEdgePredicate::True => Some(true),
RelEdgePredicate::False => Some(false),
RelEdgePredicate::TypeIn(types) => Some(types.contains(&connection_type)),
RelEdgePredicate::Property { prop, op, value } => match get_prop(prop) {
Some(Value::Null) | None => None,
Some(v) => Some(match op {
PropOp::Eq => crate::graph::core::filtering::values_equal(&v, value),
PropOp::Ne => !crate::graph::core::filtering::values_equal(&v, value),
PropOp::Gt => matches!(
crate::graph::core::filtering::compare_values(&v, value),
Some(std::cmp::Ordering::Greater)
),
PropOp::Ge => matches!(
crate::graph::core::filtering::compare_values(&v, value),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
),
PropOp::Lt => matches!(
crate::graph::core::filtering::compare_values(&v, value),
Some(std::cmp::Ordering::Less)
),
PropOp::Le => matches!(
crate::graph::core::filtering::compare_values(&v, value),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
),
PropOp::StartsWith => matches!(
(&v, value),
(Value::String(text), Value::String(prefix)) if text.starts_with(prefix)
),
PropOp::Contains => matches!(
(&v, value),
(Value::String(text), Value::String(needle)) if text.contains(needle)
),
PropOp::EndsWith => matches!(
(&v, value),
(Value::String(text), Value::String(suffix)) if text.ends_with(suffix)
),
}),
},
RelEdgePredicate::StartNodeIsPeer => Some(peer_is_start),
RelEdgePredicate::EndNodeIsPeer => Some(!peer_is_start),
RelEdgePredicate::StartNodeIs(idx) => Some(edge_source == *idx),
RelEdgePredicate::EndNodeIs(idx) => Some(edge_target == *idx),
RelEdgePredicate::And(items) => {
let mut saw_unknown = false;
for predicate in items {
match predicate.eval_nullable(
connection_type,
peer_is_start,
edge_source,
edge_target,
get_prop,
) {
Some(false) => return Some(false),
None => saw_unknown = true,
Some(true) => {}
}
}
if saw_unknown {
None
} else {
Some(true)
}
}
RelEdgePredicate::Or(items) => {
let mut saw_unknown = false;
for predicate in items {
match predicate.eval_nullable(
connection_type,
peer_is_start,
edge_source,
edge_target,
get_prop,
) {
Some(true) => return Some(true),
None => saw_unknown = true,
Some(false) => {}
}
}
if saw_unknown {
None
} else {
Some(false)
}
}
RelEdgePredicate::Not(inner) => inner
.eval_nullable(
connection_type,
peer_is_start,
edge_source,
edge_target,
get_prop,
)
.map(|value| !value),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EdgeDirection {
Outgoing, Incoming, Both, }
#[derive(Debug, Clone)]
pub enum PropertyMatcher {
Equals(Value),
EqualsParam(String),
EqualsVar(String),
EqualsNodeProp {
var: String,
prop: String,
},
In(Vec<Value>),
GreaterThan(Value),
GreaterOrEqual(Value),
LessThan(Value),
LessOrEqual(Value),
Range {
lower: Value,
lower_inclusive: bool,
upper: Value,
upper_inclusive: bool,
},
StartsWith(String),
Contains(String),
EndsWith(String),
}
#[derive(Debug, Clone)]
pub struct PatternMatch {
pub bindings: Vec<(String, MatchBinding)>,
#[doc(hidden)]
pub exact_path: Option<Box<(NodeIndex, Vec<PathHop>)>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PathHop {
pub node: NodeIndex,
pub edge: EdgeIndex,
pub connection_type: InternedKey,
}
#[derive(Debug, Clone)]
pub enum MatchBinding {
Node {
index: NodeIndex,
node_type: String,
title: String,
id: Value,
properties: HashMap<String, Value>,
},
NodeRef(NodeIndex),
Edge {
source: NodeIndex,
target: NodeIndex,
edge_index: EdgeIndex,
connection_type: InternedKey,
},
VariableLengthPath {
source: NodeIndex,
target: NodeIndex,
hops: usize,
path: Vec<PathHop>,
},
}
#[cfg(test)]
mod tests {
use super::*;
fn property(op: PropOp) -> RelEdgePredicate {
RelEdgePredicate::Property {
prop: "tag".to_string(),
op,
value: Value::String("foo".to_string()),
}
}
fn eval_with(predicate: &RelEdgePredicate, get_prop: &impl Fn(&str) -> Option<Value>) -> bool {
predicate.eval(
InternedKey::from_str("R"),
true,
NodeIndex::new(0),
NodeIndex::new(1),
get_prop,
)
}
#[test]
fn relationship_not_preserves_missing_property_unknown() {
for op in [PropOp::Eq, PropOp::Ne] {
let negated = RelEdgePredicate::Not(Box::new(property(op)));
assert!(!eval_with(&negated, &|_| None));
assert!(!eval_with(&negated, &|_| Some(Value::Null)));
}
}
#[test]
fn relationship_boolean_composition_uses_kleene_logic() {
assert!(!eval_with(
&RelEdgePredicate::And(vec![RelEdgePredicate::False, property(PropOp::Eq),]),
&|_| None,
));
assert!(eval_with(
&RelEdgePredicate::Or(vec![RelEdgePredicate::True, property(PropOp::Eq),]),
&|_| None,
));
assert!(!eval_with(
&RelEdgePredicate::Not(Box::new(RelEdgePredicate::Or(vec![
RelEdgePredicate::False,
property(PropOp::Eq),
]))),
&|_| None,
));
assert!(!eval_with(
&RelEdgePredicate::And(vec![RelEdgePredicate::False, property(PropOp::Eq),]),
&|_| Some(Value::Null)
));
}
#[test]
fn relationship_text_predicates_match_strings_and_reject_nulls() {
for (op, text) in [
(PropOp::StartsWith, "foobar"),
(PropOp::Contains, "xfooy"),
(PropOp::EndsWith, "barfoo"),
] {
let predicate = property(op);
assert!(eval_with(&predicate, &|_| Some(Value::String(
text.to_string()
))));
assert!(!eval_with(&predicate, &|_| None));
assert!(!eval_with(&predicate, &|_| Some(Value::Null)));
assert!(!eval_with(&predicate, &|_| Some(Value::Int64(7))));
}
}
}