use petgraph::Graph;
use petgraph_decypher::{
CypherEdge, CypherError, CypherNode, CypherProperties, CypherValue, MatchStrategy, NodeData,
Parameters, PetgraphCypher, QueryResult, ResultValue, Row, build_graph_from_cypher,
build_graph_from_cypher_typed,
};
use std::collections::HashMap;
fn collect_rows<N, E>(result: QueryResult<'_, N, E>) -> Vec<Row> {
result.into_iter().collect()
}
#[derive(Debug, Clone, PartialEq)]
struct CustomNode {
labels: Vec<String>,
properties: HashMap<String, CypherValue>,
}
impl CypherProperties for CustomNode {
fn get(&self, name: &str) -> Option<&CypherValue> {
self.properties.get(name)
}
fn properties(&self) -> HashMap<String, CypherValue> {
self.properties.clone()
}
}
impl CypherNode for CustomNode {
fn has_label(&self, label: &str) -> bool {
self.labels.iter().any(|node_label| node_label == label)
}
fn labels(&self) -> Vec<String> {
self.labels.clone()
}
fn from_cypher(
_variable: Option<String>,
labels: Vec<String>,
properties: HashMap<String, CypherValue>,
) -> Self {
Self { labels, properties }
}
}
#[derive(Debug, Clone, PartialEq)]
struct CustomEdge {
rel_type: Option<String>,
properties: HashMap<String, CypherValue>,
}
impl CypherProperties for CustomEdge {
fn get(&self, name: &str) -> Option<&CypherValue> {
self.properties.get(name)
}
fn properties(&self) -> HashMap<String, CypherValue> {
self.properties.clone()
}
}
impl CypherEdge for CustomEdge {
fn has_rel_type(&self, rel_type: &str) -> bool {
self.rel_type.as_deref() == Some(rel_type)
}
fn rel_type(&self) -> Option<&str> {
self.rel_type.as_deref()
}
fn from_cypher(
_variable: Option<String>,
rel_type: Option<String>,
properties: HashMap<String, CypherValue>,
) -> Self {
Self {
rel_type,
properties,
}
}
}
struct CompatibilityWrapper {
graph: Graph<NodeData, petgraph_decypher::EdgeData>,
}
impl PetgraphCypher for CompatibilityWrapper {
fn cypher(&self, query: &str) -> Result<QueryResult<'_>, CypherError> {
self.graph.cypher(query)
}
fn cypher_mut(&mut self, query: &str) -> Result<(), CypherError> {
self.graph.cypher_mut(query)
}
fn cypher_with_strategy(
&self,
query: &str,
strategy: MatchStrategy,
) -> Result<QueryResult<'_>, CypherError> {
self.graph.cypher_with_strategy(query, strategy)
}
}
impl petgraph_decypher::query::PetgraphCypherRead<NodeData, petgraph_decypher::EdgeData>
for CompatibilityWrapper
{
fn cypher(
&self,
query: &str,
) -> Result<QueryResult<'_, NodeData, petgraph_decypher::EdgeData>, CypherError> {
petgraph_decypher::query::PetgraphCypherRead::cypher(&self.graph, query)
}
fn cypher_with_strategy(
&self,
query: &str,
strategy: MatchStrategy,
) -> Result<QueryResult<'_, NodeData, petgraph_decypher::EdgeData>, CypherError> {
petgraph_decypher::query::PetgraphCypherRead::cypher_with_strategy(
&self.graph,
query,
strategy,
)
}
}
#[test]
fn query_match_all_nodes() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g.cypher("MATCH (n) RETURN n.name AS name").unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 2);
}
#[test]
fn query_match_all_nodes_with_custom_weights() {
let g = build_graph_from_cypher_typed::<CustomNode, CustomEdge>(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
let result = petgraph_decypher::query::PetgraphCypherRead::cypher(
&g,
"MATCH (n:Person) RETURN n.name AS name",
)
.unwrap();
assert_eq!(result.columns(), &["name".to_string()]);
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 2);
let mut names = rows
.iter()
.map(|row| match row.values.get("name").unwrap() {
ResultValue::Scalar(CypherValue::String(name)) => name.clone(),
_ => panic!("expected string value for name field"),
})
.collect::<Vec<_>>();
names.sort();
assert_eq!(names, vec!["Alice".to_string(), "Bob".to_string()]);
}
#[test]
fn query_match_relationship_with_custom_weights() {
let g = build_graph_from_cypher_typed::<CustomNode, CustomEdge>(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS {since: 2020}]->(b:Person {name: "Bob"})"#,
)
.unwrap();
let result = petgraph_decypher::query::PetgraphCypherRead::cypher(
&g,
"MATCH (a)-[r:KNOWS]->(b) RETURN r.since AS since, b.name AS name",
)
.unwrap();
assert_eq!(result.columns(), &["since".to_string(), "name".to_string()]);
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::Integer(since)) = rows[0].values.get("since").unwrap() {
assert_eq!(*since, 2020);
} else {
panic!("expected integer value for since field");
}
if let ResultValue::Scalar(CypherValue::String(name)) = rows[0].values.get("name").unwrap() {
assert_eq!(name, "Bob");
} else {
panic!("expected string value for name field");
}
}
#[test]
fn query_match_with_label_filter() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Dog {name: "Rex"})"#,
)
.unwrap();
let result = g.cypher("MATCH (n:Person) RETURN n.name AS name").unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::String(name)) = rows[0].values.get("name").unwrap() {
assert_eq!(name, "Alice");
} else {
panic!("expected string value");
}
}
#[test]
fn query_match_relationship() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher("MATCH (a)-[r:KNOWS]->(b) RETURN a.name AS a_name, b.name AS b_name")
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_match_no_matching_edges() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g.cypher("MATCH (a)-[r:HATES]->(b) RETURN a.name").unwrap();
let rows: Vec<_> = collect_rows(result);
assert!(rows.is_empty());
}
#[test]
fn query_where_filter() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})
CREATE (c:Person {name: "Charlie"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.name = "Alice" RETURN n.name AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::String(name)) = rows[0].values.get("name").unwrap() {
assert_eq!(name, "Alice");
} else {
panic!("expected string value");
}
}
#[test]
fn query_where_with_and() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice", age: 30})
CREATE (b:Person {name: "Bob", age: 25})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.name = "Alice" AND n.age = 30 RETURN n.name AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_where_with_parameters() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice", age: 30})
CREATE (b:Person {name: "Bob", age: 25})"#,
)
.unwrap();
let mut params = Parameters::new();
params.insert("name".into(), CypherValue::String("Alice".into()));
params.insert("min_age".into(), CypherValue::Integer(30));
let result = g
.cypher_params(
"MATCH (n:Person) WHERE n.name = $name AND n.age >= $min_age RETURN n.name AS name",
¶ms,
)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0].values.get("name"),
Some(&ResultValue::Scalar(CypherValue::String("Alice".into())))
);
}
#[test]
fn query_where_no_match() {
let g = build_graph_from_cypher(r#"CREATE (a:Person {name: "Alice"})"#).unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.name = "Nobody" RETURN n.name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert!(rows.is_empty());
}
#[test]
fn query_match_two_hop_path() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})-[:KNOWS]->(c:Person {name: "Charlie"})"#,
)
.unwrap();
let result = g
.cypher("MATCH (a)-[:KNOWS]->(b)-[:KNOWS]->(c) RETURN a.name AS a_name, c.name AS c_name")
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_match_chain_partial() {
let g = build_graph_from_cypher(r#"CREATE (a)-[:E]->(b)-[:E]->(c)-[:E]->(d)"#).unwrap();
let result = g.cypher("MATCH (a)-[:E]->(b)-[:E]->(c) RETURN a").unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 2);
}
#[test]
fn query_return_property() {
let g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice", age: 30})"#).unwrap();
let result = g
.cypher("MATCH (n) RETURN n.name AS name, n.age AS age")
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_return_wildcard() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g.cypher("MATCH (a)-[r]->(b) RETURN *").unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
assert!(rows[0].values.contains_key("a"));
assert!(rows[0].values.contains_key("r"));
assert!(rows[0].values.contains_key("b"));
}
#[test]
fn query_columns() {
let g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap();
let result = g
.cypher("MATCH (n) RETURN n.name AS name, n.age AS age")
.unwrap();
assert_eq!(result.columns(), &["name".to_string(), "age".to_string()]);
}
#[test]
fn query_empty_graph() {
let g: Graph<petgraph_decypher::NodeData, petgraph_decypher::EdgeData> = Graph::new();
let result = g.cypher("MATCH (n) RETURN n").unwrap();
let rows: Vec<_> = collect_rows(result);
assert!(rows.is_empty());
}
#[test]
fn query_mut_create_single_node() {
let mut g: Graph<petgraph_decypher::NodeData, petgraph_decypher::EdgeData> = Graph::new();
g.cypher_mut(r#"CREATE (n:Person {name: "Alice"})"#)
.unwrap();
assert_eq!(g.node_count(), 1);
}
#[test]
fn query_mut_create_two_nodes_with_edge() {
let mut g: Graph<petgraph_decypher::NodeData, petgraph_decypher::EdgeData> = Graph::new();
g.cypher_mut(r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#)
.unwrap();
assert_eq!(g.node_count(), 2);
assert_eq!(g.edge_count(), 1);
}
#[test]
fn query_mut_create_multiple_clauses() {
let mut g: Graph<petgraph_decypher::NodeData, petgraph_decypher::EdgeData> = Graph::new();
g.cypher_mut(r#"CREATE (a:Person {name: "Alice"})"#)
.unwrap();
g.cypher_mut(r#"CREATE (b:Person {name: "Bob"})"#).unwrap();
assert_eq!(g.node_count(), 2);
}
#[test]
fn query_mut_delete_node() {
let mut g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
g.cypher_mut(r#"MATCH (n) WHERE n.name = "Alice" DETACH DELETE n"#)
.unwrap();
assert_eq!(g.node_count(), 1);
assert_eq!(g.edge_count(), 0);
}
#[test]
fn query_mut_detach_delete() {
let mut g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})-[:KNOWS]->(c:Person {name: "Charlie"})"#,
)
.unwrap();
g.cypher_mut(r#"MATCH (n) WHERE n.name = "Bob" DETACH DELETE n"#)
.unwrap();
assert_eq!(g.node_count(), 2);
assert_eq!(g.edge_count(), 0);
}
#[test]
fn query_mut_merge_creates_if_not_found() {
let mut g: Graph<petgraph_decypher::NodeData, petgraph_decypher::EdgeData> = Graph::new();
g.cypher_mut(r#"MERGE (n:Person {name: "Alice"})"#).unwrap();
assert_eq!(g.node_count(), 1);
}
#[test]
fn query_mut_merge_does_not_duplicate() {
let mut g = build_graph_from_cypher(r#"CREATE (a:Person {name: "Alice"})"#).unwrap();
g.cypher_mut(r#"MERGE (n:Person)"#).unwrap();
assert_eq!(g.node_count(), 1);
}
#[test]
fn query_cypher_rejects_create() {
let g: Graph<petgraph_decypher::NodeData, petgraph_decypher::EdgeData> = Graph::new();
let result = g.cypher("CREATE (n:Person) RETURN n");
assert!(result.is_err());
}
#[test]
fn query_cypher_rejects_delete() {
let g: Graph<petgraph_decypher::NodeData, petgraph_decypher::EdgeData> = Graph::new();
let result = g.cypher("MATCH (n) DELETE n");
assert!(result.is_err());
}
#[test]
fn query_cypher_mut_rejects_return_clause() {
let mut g: Graph<petgraph_decypher::NodeData, petgraph_decypher::EdgeData> = Graph::new();
let result = g.cypher_mut("CREATE (n:Person {name: \"Alice\"}) RETURN n");
assert!(result.is_err());
}
#[test]
fn query_with_strategy_backtrack() {
use petgraph_decypher::MatchStrategy;
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher_with_strategy(
"MATCH (a)-[:KNOWS]->(b) RETURN a.name",
MatchStrategy::Backtrack,
)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_with_strategy_fast() {
use petgraph_decypher::MatchStrategy;
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher_with_strategy("MATCH (a)-[:KNOWS]->(b) RETURN a.name", MatchStrategy::Fast)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_parameter_trait_defaults_are_source_compatible() {
let wrapper = CompatibilityWrapper {
graph: build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap(),
};
let empty_params = Parameters::new();
let result = wrapper
.cypher_params("MATCH (n:Person) RETURN n.name AS name", &empty_params)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
let mut params = Parameters::new();
params.insert("name".into(), CypherValue::String("Alice".into()));
let err = match wrapper.cypher_params("MATCH (n:Person) RETURN n.name AS name", ¶ms) {
Ok(_) => panic!("expected unsupported error for non-empty parameter map"),
Err(err) => err,
};
assert_eq!(
err,
CypherError::Unsupported(
"parameterized queries are not supported by this PetgraphCypher implementor".into()
)
);
}
#[test]
fn query_match_multiple_patterns_cartesian() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher("MATCH (a:Person), (b:Person) RETURN a.name, b.name")
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 4);
}
#[test]
fn query_where_lt_gt() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice", age: 30})
CREATE (b:Person {name: "Bob", age: 25})
CREATE (c:Person {name: "Charlie", age: 35})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.age > 25 RETURN n.name AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 2);
}
#[test]
fn query_where_not_eq() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.name <> "Alice" RETURN n.name AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::String(name)) = rows[0].values.get("name").unwrap() {
assert_eq!(name, "Bob");
} else {
panic!("expected string value");
}
}
#[test]
fn query_where_le_ge() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {age: 30})
CREATE (b:Person {age: 25})
CREATE (c:Person {age: 35})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.age >= 25 AND n.age <= 30 RETURN n"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 2);
}
#[test]
fn query_where_starts_with() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.name STARTS WITH "Al" RETURN n.name AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_where_ends_with() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.name ENDS WITH "ce" RETURN n.name AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_where_contains() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.name CONTAINS "li" RETURN n.name AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_where_is_null() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {age: 25})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.name IS NULL RETURN n"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_where_is_not_null() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {age: 25})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE n.name IS NOT NULL RETURN n.name AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_where_not() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) WHERE NOT n.name = "Alice" RETURN n.name AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::String(name)) = rows[0].values.get("name").unwrap() {
assert_eq!(name, "Bob");
} else {
panic!("expected string value");
}
}
#[test]
fn query_order_by_asc() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice", age: 30})
CREATE (b:Person {name: "Bob", age: 25})
CREATE (c:Person {name: "Charlie", age: 35})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN n.name AS name ORDER BY n.age"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 3);
if let ResultValue::Scalar(CypherValue::String(name)) = rows[0].values.get("name").unwrap() {
assert_eq!(name, "Bob");
} else {
panic!("expected string value");
}
}
#[test]
fn query_order_by_desc() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice", age: 30})
CREATE (b:Person {name: "Bob", age: 25})
CREATE (c:Person {name: "Charlie", age: 35})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN n.name AS name ORDER BY n.age DESC"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 3);
if let ResultValue::Scalar(CypherValue::String(name)) = rows[0].values.get("name").unwrap() {
assert_eq!(name, "Charlie");
} else {
panic!("expected string value");
}
}
#[test]
fn query_limit() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})
CREATE (c:Person {name: "Charlie"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN n.name AS name LIMIT 2"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 2);
}
#[test]
fn query_skip() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})
CREATE (c:Person {name: "Charlie"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN n.name AS name SKIP 1"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 2);
}
#[test]
fn query_skip_and_limit() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})
CREATE (b:Person {name: "Bob"})
CREATE (c:Person {name: "Charlie"})
CREATE (d:Person {name: "David"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN n.name AS name SKIP 1 LIMIT 2"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 2);
}
#[test]
fn query_distinct() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {age: 30})
CREATE (b:Person {age: 30})
CREATE (c:Person {age: 25})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN DISTINCT n.age AS age"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 2);
}
#[test]
fn query_function_tostring() {
let g = build_graph_from_cypher(r#"CREATE (n:Person {age: 30})"#).unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN toString(n.age) AS s"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::String(s)) = rows[0].values.get("s").unwrap() {
assert_eq!(s, "30");
} else {
panic!("expected string value");
}
}
#[test]
fn query_function_to_upper() {
let g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN toUpper(n.name) AS name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::String(name)) = rows[0].values.get("name").unwrap() {
assert_eq!(name, "ALICE");
} else {
panic!("expected string value");
}
}
#[test]
fn query_function_size() {
let g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN size(n.name) AS len"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::Integer(len)) = rows[0].values.get("len").unwrap() {
assert_eq!(*len, 5);
} else {
panic!("expected integer value");
}
}
#[test]
fn query_return_parameter_value() {
let g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap();
let mut params = Parameters::new();
params.insert("requested_name".into(), CypherValue::String("Alice".into()));
let result = g
.cypher_params(
r#"MATCH (n:Person) RETURN $requested_name AS name LIMIT 1"#,
¶ms,
)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0].values.get("name"),
Some(&ResultValue::Scalar(CypherValue::String("Alice".into())))
);
}
#[test]
fn query_function_argument_parameter() {
let g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap();
let mut params = Parameters::new();
params.insert("name".into(), CypherValue::String("Alice".into()));
let result = g
.cypher_params(
r#"MATCH (n:Person) RETURN toUpper($name) AS upper LIMIT 1"#,
¶ms,
)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0].values.get("upper"),
Some(&ResultValue::Scalar(CypherValue::String("ALICE".into())))
);
}
#[test]
fn query_missing_parameter_returns_error() {
let g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap();
let params = Parameters::new();
let err = match g.cypher_params("MATCH (n:Person) WHERE n.name = $name RETURN n", ¶ms) {
Ok(_) => panic!("expected missing parameter error"),
Err(err) => err,
};
assert_eq!(
err,
CypherError::InvalidQuery("missing query parameter: $name".into())
);
}
#[test]
fn query_optional_match() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
let result = g
.cypher(r#"MATCH (a:Person {name: "Alice"}) OPTIONAL MATCH (a)-[:HATES]->(b) RETURN a.name AS a_name, b.name AS b_name"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
}
#[test]
fn query_mut_set_property() {
let mut g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap();
g.cypher_mut(r#"MATCH (n:Person) SET n.age = 30"#).unwrap();
let result = g.cypher(r#"MATCH (n:Person) RETURN n.age AS age"#).unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::Integer(age)) = rows[0].values.get("age").unwrap() {
assert_eq!(*age, 30);
} else {
panic!("expected integer value");
}
}
#[test]
fn query_mut_rejects_parameters_with_clear_error() {
let mut g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap();
let err = g
.cypher_mut(r#"MATCH (n:Person) WHERE n.name = $name SET n.seen = true"#)
.unwrap_err();
assert_eq!(
err,
CypherError::Unsupported(
"query parameters are not supported in cypher_mut(): $name".into()
)
);
}
#[test]
fn query_mut_remove_property() {
let mut g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice", age: 30})"#).unwrap();
g.cypher_mut(r#"MATCH (n:Person) REMOVE n.age"#).unwrap();
let result = g.cypher(r#"MATCH (n:Person) RETURN n.age AS age"#).unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::Null) = rows[0].values.get("age").unwrap() {
} else {
panic!("expected null value");
}
}
#[test]
fn query_mut_merge_on_create() {
let mut g: Graph<petgraph_decypher::NodeData, petgraph_decypher::EdgeData> = Graph::new();
g.cypher_mut(r#"MERGE (n:Person {name: "Alice"}) ON CREATE SET n.created = true"#)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN n.created AS created"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::Boolean(created)) =
rows[0].values.get("created").unwrap()
{
assert!(created);
} else {
panic!("expected boolean value");
}
}
#[test]
fn query_mut_merge_on_match() {
let mut g = build_graph_from_cypher(r#"CREATE (n:Person {name: "Alice"})"#).unwrap();
g.cypher_mut(r#"MERGE (n:Person {name: "Alice"}) ON MATCH SET n.seen = true"#)
.unwrap();
let result = g
.cypher(r#"MATCH (n:Person) RETURN n.seen AS seen"#)
.unwrap();
let rows: Vec<_> = collect_rows(result);
assert_eq!(rows.len(), 1);
if let ResultValue::Scalar(CypherValue::Boolean(seen)) = rows[0].values.get("seen").unwrap() {
assert!(seen);
} else {
panic!("expected boolean value");
}
}