use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
fn db() -> GrafeoDB {
GrafeoDB::new_in_memory()
}
fn chain_graph() -> GrafeoDB {
let db = db();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
session
.execute("MATCH (g:Person {name: 'Gus'}) INSERT (g)-[:KNOWS]->(:Person {name: 'Vincent'})")
.unwrap();
db
}
mod empty_result_aggregation {
use super::*;
#[test]
fn count_on_empty_returns_zero() {
let db = db();
let session = db.session();
let result = session
.execute("MATCH (n:NonExistent) RETURN COUNT(*) AS cnt")
.unwrap();
assert_eq!(
result.row_count(),
1,
"Aggregate should always return one row"
);
assert_eq!(result.rows()[0][0], Value::Int64(0));
}
#[test]
fn count_star_on_empty_returns_zero() {
let db = db();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let result = session
.execute("MATCH (n:Animal) RETURN COUNT(*) AS cnt")
.unwrap();
assert_eq!(result.rows()[0][0], Value::Int64(0));
}
#[test]
fn sum_on_empty_returns_null() {
let db = db();
let session = db.session();
let result = session
.execute("MATCH (n:NonExistent) RETURN SUM(n.val) AS s")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::Null,
"SUM on empty should be NULL"
);
}
#[test]
fn avg_on_empty_returns_null() {
let db = db();
let session = db.session();
let result = session
.execute("MATCH (n:NonExistent) RETURN AVG(n.val) AS a")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::Null,
"AVG on empty should be NULL"
);
}
#[test]
fn min_on_empty_returns_null() {
let db = db();
let session = db.session();
let result = session
.execute("MATCH (n:NonExistent) RETURN MIN(n.val) AS m")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::Null,
"MIN on empty should be NULL"
);
}
#[test]
fn max_on_empty_returns_null() {
let db = db();
let session = db.session();
let result = session
.execute("MATCH (n:NonExistent) RETURN MAX(n.val) AS m")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::Null,
"MAX on empty should be NULL"
);
}
#[test]
fn collect_on_empty_returns_empty_list() {
let db = db();
let session = db.session();
let result = session
.execute("MATCH (n:NonExistent) RETURN COLLECT(n.val) AS c")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(
result.rows()[0][0],
Value::List(vec![].into()),
"COLLECT on empty should be empty list"
);
}
}
mod quantified_paths {
use super::*;
#[test]
fn variable_length_one_hop() {
let db = chain_graph();
let session = db.session();
let result = session
.execute("MATCH (a:Person {name: 'Alix'})-[:KNOWS*1..1]->(b) RETURN b.name")
.unwrap();
assert_eq!(result.row_count(), 1, "1..1 should match exactly one hop");
assert_eq!(result.rows()[0][0], Value::String("Gus".into()));
}
#[test]
fn variable_length_two_hops() {
let db = chain_graph();
let session = db.session();
let result = session
.execute("MATCH (a:Person {name: 'Alix'})-[:KNOWS*2..2]->(b) RETURN b.name")
.unwrap();
assert_eq!(result.row_count(), 1, "2..2 should reach Vincent");
assert_eq!(result.rows()[0][0], Value::String("Vincent".into()));
}
#[test]
fn variable_length_range() {
let db = chain_graph();
let session = db.session();
let result = session
.execute(
"MATCH (a:Person {name: 'Alix'})-[:KNOWS*1..2]->(b) RETURN b.name ORDER BY b.name",
)
.unwrap();
assert_eq!(
result.row_count(),
2,
"1..2 should reach both Gus and Vincent"
);
}
#[test]
fn variable_length_no_match() {
let db = chain_graph();
let session = db.session();
let result = session
.execute("MATCH (a:Person {name: 'Vincent'})-[:KNOWS*1..5]->(b) RETURN b.name")
.unwrap();
assert_eq!(result.row_count(), 0, "Vincent has no outgoing KNOWS edges");
}
#[test]
fn variable_length_star() {
let db = chain_graph();
let session = db.session();
let result = session
.execute("MATCH (a:Person {name: 'Alix'})-[:KNOWS*]->(b) RETURN b.name ORDER BY b.name")
.unwrap();
assert_eq!(result.row_count(), 2, "* should reach all reachable nodes");
}
}
mod optional_match {
use super::*;
#[test]
fn optional_match_returns_null_when_no_match() {
let db = db();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let result = session
.execute(
"MATCH (n:Person) OPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN n.name, m.name AS friend",
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(
result.rows()[0][1],
Value::Null,
"No match should return NULL"
);
}
#[test]
fn optional_match_returns_data_when_match_exists() {
let db = db();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
let result = session
.execute(
"MATCH (n:Person {name: 'Alix'}) OPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN m.name",
)
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Gus".into()));
}
#[test]
fn optional_match_mixed_results() {
let db = db();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
session
.execute("INSERT (:Person {name: 'Vincent'})")
.unwrap();
let result = session
.execute(
"MATCH (n:Person) OPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN n.name, m.name ORDER BY n.name",
)
.unwrap();
assert_eq!(result.row_count(), 3);
}
}
mod label_expressions {
use super::*;
#[test]
fn multi_label_node_matches_each_label() {
let db = db();
let session = db.session();
session
.execute("INSERT (:Person:Engineer {name: 'Alix'})")
.unwrap();
let r1 = session.execute("MATCH (n:Person) RETURN n").unwrap();
let r2 = session.execute("MATCH (n:Engineer) RETURN n").unwrap();
assert_eq!(r1.row_count(), 1);
assert_eq!(r2.row_count(), 1);
}
#[test]
fn label_disjunction() {
let db = db();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session
.execute("INSERT (:Animal {species: 'Cat'})")
.unwrap();
session.execute("INSERT (:Vehicle {type: 'Car'})").unwrap();
let result = session
.execute("MATCH (n IS Person | Animal) RETURN n")
.unwrap();
assert_eq!(
result.row_count(),
2,
"Disjunction should match Person and Animal"
);
}
#[test]
fn label_negation() {
let db = db();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session
.execute("INSERT (:Animal {species: 'Cat'})")
.unwrap();
let result = session.execute("MATCH (n IS !Person) RETURN n").unwrap();
assert_eq!(result.row_count(), 1, "Negation should exclude Person");
}
#[test]
fn label_wildcard() {
let db = db();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session
.execute("INSERT (:Animal {species: 'Cat'})")
.unwrap();
let result = session.execute("MATCH (n IS %) RETURN n").unwrap();
assert_eq!(
result.row_count(),
2,
"Wildcard should match all labeled nodes"
);
}
#[test]
fn nonexistent_label_returns_empty() {
let db = db();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let result = session.execute("MATCH (n:Unicorn) RETURN n").unwrap();
assert_eq!(
result.row_count(),
0,
"Nonexistent label should match nothing"
);
}
}
mod edge_directions {
use super::*;
#[test]
fn outgoing_edge_match() {
let db = db();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
let result = session
.execute("MATCH (a:Person {name: 'Alix'})-[:KNOWS]->(b) RETURN b.name")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Gus".into()));
}
#[test]
fn incoming_edge_match() {
let db = db();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
let result = session
.execute("MATCH (b:Person {name: 'Gus'})<-[:KNOWS]-(a) RETURN a.name")
.unwrap();
assert_eq!(result.row_count(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn undirected_edge_match() {
let db = db();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
let result = session
.execute("MATCH (a:Person {name: 'Gus'})-[:KNOWS]-(b) RETURN b.name")
.unwrap();
assert_eq!(
result.row_count(),
1,
"Undirected should match the edge from either side"
);
}
#[test]
fn wrong_direction_returns_empty() {
let db = db();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})")
.unwrap();
let result = session
.execute("MATCH (a:Person {name: 'Alix'})<-[:KNOWS]-(b) RETURN b.name")
.unwrap();
assert_eq!(result.row_count(), 0, "Wrong direction should return empty");
}
}