use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchQuery {
pub clauses: Vec<MatchClause>,
pub where_predicates: Vec<WherePredicate>,
pub return_columns: Vec<ReturnColumn>,
pub limit: Option<usize>,
pub order_by: Vec<OrderByColumn>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchClause {
pub patterns: Vec<PatternChain>,
pub optional: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternChain {
pub triples: Vec<PatternTriple>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternTriple {
pub src: NodeBinding,
pub edge: EdgeBinding,
pub dst: NodeBinding,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeBinding {
pub name: Option<String>,
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeBinding {
pub name: Option<String>,
pub edge_type: Option<String>,
pub direction: EdgeDirection,
pub min_hops: usize,
pub max_hops: usize,
}
impl EdgeBinding {
pub fn is_variable_length(&self) -> bool {
self.min_hops != self.max_hops || self.min_hops > 1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EdgeDirection {
Right,
Left,
Both,
}
impl EdgeDirection {
pub fn to_csr_direction(self) -> crate::engine::graph::edge_store::Direction {
match self {
Self::Right => crate::engine::graph::edge_store::Direction::Out,
Self::Left => crate::engine::graph::edge_store::Direction::In,
Self::Both => crate::engine::graph::edge_store::Direction::Both,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WherePredicate {
Equals {
binding: String,
field: String,
value: String,
},
Comparison {
binding: String,
field: String,
op: ComparisonOp,
value: String,
},
NotExists { sub_pattern: MatchClause },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComparisonOp {
Eq,
Neq,
Lt,
Lte,
Gt,
Gte,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReturnColumn {
pub expr: String,
pub alias: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderByColumn {
pub expr: String,
pub ascending: bool,
}
impl MatchQuery {
pub fn bound_node_names(&self) -> Vec<String> {
let mut names = Vec::new();
for clause in &self.clauses {
for chain in &clause.patterns {
for triple in &chain.triples {
if let Some(ref n) = triple.src.name
&& !names.contains(n)
{
names.push(n.clone());
}
if let Some(ref n) = triple.dst.name
&& !names.contains(n)
{
names.push(n.clone());
}
}
}
}
names
}
pub fn bound_edge_names(&self) -> Vec<String> {
let mut names = Vec::new();
for clause in &self.clauses {
for chain in &clause.patterns {
for triple in &chain.triples {
if let Some(ref n) = triple.edge.name
&& !names.contains(n)
{
names.push(n.clone());
}
}
}
}
names
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edge_binding_fixed_length() {
let e = EdgeBinding {
name: None,
edge_type: Some("KNOWS".into()),
direction: EdgeDirection::Right,
min_hops: 1,
max_hops: 1,
};
assert!(!e.is_variable_length());
}
#[test]
fn edge_binding_variable_length() {
let e = EdgeBinding {
name: None,
edge_type: Some("KNOWS".into()),
direction: EdgeDirection::Right,
min_hops: 1,
max_hops: 3,
};
assert!(e.is_variable_length());
}
#[test]
fn bound_names_extraction() {
let query = MatchQuery {
clauses: vec![MatchClause {
patterns: vec![PatternChain {
triples: vec![PatternTriple {
src: NodeBinding {
name: Some("a".into()),
label: Some("Person".into()),
},
edge: EdgeBinding {
name: Some("r".into()),
edge_type: Some("KNOWS".into()),
direction: EdgeDirection::Right,
min_hops: 1,
max_hops: 1,
},
dst: NodeBinding {
name: Some("b".into()),
label: Some("Person".into()),
},
}],
}],
optional: false,
}],
where_predicates: vec![],
return_columns: vec![],
limit: None,
order_by: vec![],
};
assert_eq!(query.bound_node_names(), vec!["a", "b"]);
assert_eq!(query.bound_edge_names(), vec!["r"]);
}
#[test]
fn edge_direction_to_csr() {
assert_eq!(
EdgeDirection::Right.to_csr_direction(),
crate::engine::graph::edge_store::Direction::Out
);
assert_eq!(
EdgeDirection::Left.to_csr_direction(),
crate::engine::graph::edge_store::Direction::In
);
}
#[test]
fn serde_roundtrip() {
let query = MatchQuery {
clauses: vec![MatchClause {
patterns: vec![PatternChain {
triples: vec![PatternTriple {
src: NodeBinding {
name: Some("a".into()),
label: None,
},
edge: EdgeBinding {
name: None,
edge_type: Some("LIKES".into()),
direction: EdgeDirection::Both,
min_hops: 1,
max_hops: 3,
},
dst: NodeBinding {
name: Some("b".into()),
label: None,
},
}],
}],
optional: true,
}],
where_predicates: vec![],
return_columns: vec![ReturnColumn {
expr: "a".into(),
alias: None,
}],
limit: Some(10),
order_by: vec![],
};
let json = serde_json::to_string(&query).unwrap();
let parsed: MatchQuery = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.clauses.len(), 1);
assert!(parsed.clauses[0].optional);
assert_eq!(parsed.limit, Some(10));
}
}