use crate::datatypes::values::Value;
use crate::graph::core::membership::MembershipSet;
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 alt_labels: Option<Vec<String>>,
pub properties: Option<HashMap<String, PropertyMatcher>>,
pub label_params: Vec<ParamLabel>,
}
impl NodePattern {
pub fn label_alternatives(&self) -> &[String] {
match &self.alt_labels {
Some(alts) => alts.as_slice(),
None => self.node_type.as_slice(),
}
}
pub fn multi_label_constrained(&self) -> bool {
!self.extra_labels.is_empty() || self.alt_labels.is_some()
}
}
#[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 var_length_max_written: bool,
pub needs_path_info: bool,
pub skip_target_type_check: bool,
pub edge_filter: Option<RelEdgeFilter>,
pub type_params: Vec<ParamLabel>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParamLabel {
pub slot: usize,
pub param: String,
}
impl ParamLabel {
pub fn placeholder(param: &str) -> String {
format!("${param}")
}
}
impl EdgePattern {
pub fn conn_filter(&self) -> ConnTypeFilter {
match (&self.connection_types, &self.connection_type) {
(Some(types), _) if !types.is_empty() => {
let mut keys: Vec<InternedKey> = Vec::with_capacity(types.len());
for ty in types {
let key = InternedKey::from_str(ty);
if !keys.contains(&key) {
keys.push(key);
}
}
match keys.len() {
1 => ConnTypeFilter::One(keys[0]),
_ => ConnTypeFilter::AnyOf(keys),
}
}
(_, Some(ty)) => ConnTypeFilter::One(InternedKey::from_str(ty)),
_ => ConnTypeFilter::Any,
}
}
}
#[derive(Debug, Clone)]
pub enum ConnTypeFilter {
Any,
One(InternedKey),
AnyOf(Vec<InternedKey>),
}
impl ConnTypeFilter {
#[inline]
pub fn hint(&self) -> Option<InternedKey> {
match self {
ConnTypeFilter::One(key) => Some(*key),
_ => None,
}
}
#[inline]
pub fn accepts(&self, key: InternedKey) -> bool {
match self {
ConnTypeFilter::Any => true,
ConnTypeFilter::One(want) => key == *want,
ConnTypeFilter::AnyOf(keys) => keys.contains(&key),
}
}
#[inline]
pub fn is_any(&self) -> bool {
matches!(self, ConnTypeFilter::Any)
}
pub fn try_fold_counts<E>(
&self,
mut f: impl FnMut(Option<InternedKey>) -> Result<usize, E>,
) -> Result<usize, E> {
match self {
ConnTypeFilter::Any => f(None),
ConnTypeFilter::One(key) => f(Some(*key)),
ConnTypeFilter::AnyOf(keys) => {
let mut total = 0usize;
for key in keys {
total = total.saturating_add(f(Some(*key))?);
}
Ok(total)
}
}
}
}
#[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 => {
return crate::graph::core::filtering::predicate_values_equal(&v, value)
}
PropOp::Ne => {
return crate::graph::core::filtering::predicate_values_equal(&v, value)
.map(|v| !v)
}
PropOp::Gt => {
return crate::graph::core::filtering::ordering_matches(
&v,
value,
|ordering| ordering == std::cmp::Ordering::Greater,
)
}
PropOp::Ge => {
return crate::graph::core::filtering::ordering_matches(
&v,
value,
|ordering| ordering != std::cmp::Ordering::Less,
)
}
PropOp::Lt => {
return crate::graph::core::filtering::ordering_matches(
&v,
value,
|ordering| ordering == std::cmp::Ordering::Less,
)
}
PropOp::Le => {
return crate::graph::core::filtering::ordering_matches(
&v,
value,
|ordering| ordering != std::cmp::Ordering::Greater,
)
}
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(edge_source == edge_target || peer_is_start),
RelEdgePredicate::EndNodeIsPeer => Some(edge_source == edge_target || !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,
},
EqualsExpr(Box<crate::graph::languages::cypher::ast::Expression>),
In(MembershipSet),
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_nested_null_survives_negation() {
for op in [PropOp::Eq, PropOp::Ne] {
let predicate = RelEdgePredicate::Property {
prop: "tag".into(),
op,
value: Value::List(vec![Value::Int64(1)]),
};
let read = |_: &str| Some(Value::List(vec![Value::Null]));
assert_eq!(
predicate.eval_nullable(
InternedKey::from_str("R"),
true,
NodeIndex::new(0),
NodeIndex::new(1),
&read
),
None
);
assert!(!eval_with(&predicate, &read));
assert!(!eval_with(
&RelEdgePredicate::Not(Box::new(predicate)),
&read
));
}
}
#[test]
fn self_loop_peer_is_both_relationship_endpoints() {
for predicate in [
RelEdgePredicate::StartNodeIsPeer,
RelEdgePredicate::EndNodeIsPeer,
] {
for peer_is_start in [false, true] {
assert_eq!(
predicate.eval_nullable(
InternedKey::from_str("R"),
peer_is_start,
NodeIndex::new(0),
NodeIndex::new(0),
&|_| None
),
Some(true)
);
}
}
}
#[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_ordering_without_a_rule_is_null() {
for op in [PropOp::Gt, PropOp::Ge, PropOp::Lt, PropOp::Le] {
for (stored, literal) in [
(Value::String("foo".to_string()), Value::Int64(1)),
(Value::Int64(1), Value::String("foo".to_string())),
(Value::Boolean(true), Value::Int64(1)),
(Value::List(vec![Value::Int64(1)]), Value::Int64(2)),
] {
let predicate = RelEdgePredicate::Property {
prop: "w".to_string(),
op,
value: literal,
};
let read = |_: &str| Some(stored.clone());
assert_eq!(
predicate.eval_nullable(
InternedKey::from_str("R"),
true,
NodeIndex::new(0),
NodeIndex::new(1),
&read
),
None,
"{stored:?} {op:?} must be null"
);
assert!(!eval_with(&predicate, &read));
assert!(!eval_with(
&RelEdgePredicate::Not(Box::new(predicate)),
&read
));
}
}
}
#[test]
fn relationship_ordering_against_nan_is_false_not_null() {
for op in [PropOp::Gt, PropOp::Ge, PropOp::Lt, PropOp::Le] {
let predicate = RelEdgePredicate::Property {
prop: "w".to_string(),
op,
value: Value::Int64(1),
};
let read = |_: &str| Some(Value::Float64(f64::NAN));
assert_eq!(
predicate.eval_nullable(
InternedKey::from_str("R"),
true,
NodeIndex::new(0),
NodeIndex::new(1),
&read
),
Some(false),
"NaN {op:?} 1 must be false"
);
assert!(eval_with(
&RelEdgePredicate::Not(Box::new(predicate)),
&read
));
}
}
#[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))));
}
}
}