#[cfg(test)]
#[allow(clippy::module_inception)]
mod tests {
use super::super::lexer::TokenKind;
use super::super::*;
use crate::{gql::Gql, Graph};
use serde_json::json;
use std::cell::RefCell;
fn setup_test_graph() -> crate::Result<RefCell<Graph>> {
let mut graph = Graph::new()?;
let _alice_id = graph.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Alice")),
("age".to_string(), json!(30)),
("city".to_string(), json!("SF")),
]
.into(),
)?;
let _bob_id = graph.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Bob")),
("age".to_string(), json!(25)),
("city".to_string(), json!("NYC")),
]
.into(),
)?;
let _company_id = graph.create_node(
[
("type".to_string(), json!("Company")),
("name".to_string(), json!("TechCorp")),
("industry".to_string(), json!("Tech")),
]
.into(),
)?;
graph.create_relationship(
1,
2,
"KNOWS".to_string(),
[("since".to_string(), json!("2020"))].into(),
)?;
graph.create_relationship(
1,
3,
"WORKS_FOR".to_string(),
[("role".to_string(), json!("Engineer"))].into(),
)?;
Ok(RefCell::new(graph))
}
#[test]
fn test_lexer_basic_tokens() {
let mut lexer = Lexer::new("MATCH (n) RETURN n");
let tokens = lexer.tokenize().unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Match));
assert!(matches!(tokens[1].kind, TokenKind::LeftParen));
assert!(matches!(tokens[2].kind, TokenKind::Identifier(_)));
assert!(matches!(tokens[3].kind, TokenKind::RightParen));
assert!(matches!(tokens[4].kind, TokenKind::Return));
assert!(matches!(tokens[5].kind, TokenKind::Identifier(_)));
assert!(matches!(tokens[6].kind, TokenKind::Eof));
}
#[test]
fn test_lexer_string_literals() {
let mut lexer = Lexer::new("\"hello\" 'world'");
let tokens = lexer.tokenize().unwrap();
assert!(matches!(tokens[0].kind, TokenKind::String(ref s) if s == "hello"));
assert!(matches!(tokens[1].kind, TokenKind::String(ref s) if s == "world"));
}
#[test]
fn test_lexer_numbers() {
let mut lexer = Lexer::new("42 3.14");
let tokens = lexer.tokenize().unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Integer(42)));
#[allow(clippy::approx_constant)]
{
assert!(
matches!(tokens[1].kind, TokenKind::Float(f) if (f - 3.14).abs() < f64::EPSILON)
);
}
}
#[test]
fn test_lexer_operators() {
let mut lexer = Lexer::new("= == <> < > <= >= -> + - * /");
let tokens = lexer.tokenize().unwrap();
let expected = [
TokenKind::Assign,
TokenKind::Equal,
TokenKind::NotEqual,
TokenKind::LessThan,
TokenKind::GreaterThan,
TokenKind::LessEqual,
TokenKind::GreaterEqual,
TokenKind::Arrow,
TokenKind::Plus,
TokenKind::Minus,
TokenKind::Multiply,
TokenKind::Divide,
];
for (i, expected_kind) in expected.iter().enumerate() {
assert_eq!(
std::mem::discriminant(&tokens[i].kind),
std::mem::discriminant(expected_kind)
);
}
}
#[test]
fn test_lexer_keywords() {
let mut lexer = Lexer::new("MATCH WHERE RETURN AND OR NOT");
let tokens = lexer.tokenize().unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Match));
assert!(matches!(tokens[1].kind, TokenKind::Where));
assert!(matches!(tokens[2].kind, TokenKind::Return));
assert!(matches!(tokens[3].kind, TokenKind::And));
assert!(matches!(tokens[4].kind, TokenKind::Or));
assert!(matches!(tokens[5].kind, TokenKind::Not));
}
#[test]
fn test_parser_simple_match() {
let mut lexer = Lexer::new("MATCH (n) RETURN n");
let tokens = lexer.tokenize().unwrap();
let mut parser = Parser::new(tokens);
let ast = parser.parse().unwrap();
if let GqlStatement::LinearQuery(query) = ast {
assert_eq!(query.clauses.len(), 2);
assert!(matches!(query.clauses[0], QueryClause::Match(_)));
assert!(matches!(query.clauses[1], QueryClause::Return(_)));
} else {
panic!("Expected LinearQuery");
}
}
#[test]
fn test_parser_node_with_label() {
let mut lexer = Lexer::new("MATCH (p:Person) RETURN p");
let tokens = lexer.tokenize().unwrap();
let mut parser = Parser::new(tokens);
let ast = parser.parse().unwrap();
if let GqlStatement::LinearQuery(query) = ast {
if let QueryClause::Match(match_clause) = &query.clauses[0] {
if let GraphPattern::Node(node) = &match_clause.patterns[0] {
assert_eq!(node.variable, Some("p".to_string()));
assert_eq!(node.labels, vec!["Person".to_string()]);
} else {
panic!("Expected node pattern");
}
} else {
panic!("Expected match clause");
}
} else {
panic!("Expected LinearQuery");
}
}
#[test]
fn test_parser_where_clause() {
let mut lexer = Lexer::new("MATCH (p:Person) WHERE p.age > 30 RETURN p");
let tokens = lexer.tokenize().unwrap();
let mut parser = Parser::new(tokens);
let ast = parser.parse().unwrap();
if let GqlStatement::LinearQuery(query) = ast {
assert_eq!(query.clauses.len(), 3);
assert!(matches!(query.clauses[0], QueryClause::Match(_)));
assert!(matches!(query.clauses[1], QueryClause::Filter(_)));
assert!(matches!(query.clauses[2], QueryClause::Return(_)));
} else {
panic!("Expected LinearQuery");
}
}
#[test]
fn test_parser_graph_query() {
let mut lexer = Lexer::new("GRAPH myGraph MATCH (n) RETURN n");
let tokens = lexer.tokenize().unwrap();
let mut parser = Parser::new(tokens);
let ast = parser.parse().unwrap();
if let GqlStatement::GraphQuery { graph_name, query } = ast {
assert_eq!(graph_name, "myGraph");
assert_eq!(query.clauses.len(), 2);
} else {
panic!("Expected GraphQuery");
}
}
#[test]
fn test_executor_simple_match() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql.execute("MATCH (p:Person) RETURN p.name").unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.columns[0], "p.name");
assert!(result.row_count() > 0);
}
#[test]
fn test_executor_property_access() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql
.execute("MATCH (p:Person) RETURN p.name, p.age")
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "p.name");
assert_eq!(result.columns[1], "p.age");
}
#[test]
fn test_executor_filtering() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql
.execute("MATCH (p:Person) WHERE p.age > 25 RETURN p.name")
.unwrap();
assert_eq!(result.columns.len(), 1);
assert!(result.row_count() <= 1);
}
#[test]
fn test_expression_literal_creation() {
let int_expr = Expression::literal(json!(42));
assert!(matches!(int_expr, Expression::Literal(_)));
let str_expr = Expression::literal(json!("hello"));
assert!(matches!(str_expr, Expression::Literal(_)));
let bool_expr = Expression::literal(json!(true));
assert!(matches!(bool_expr, Expression::Literal(_)));
}
#[test]
fn test_expression_construction() {
let var_expr = Expression::variable("x".to_string());
assert!(matches!(var_expr, Expression::Variable(_)));
let prop_expr =
Expression::property(Expression::variable("node".to_string()), "name".to_string());
assert!(matches!(prop_expr, Expression::Property { .. }));
let binary_expr = Expression::binary(
Expression::literal(json!(1)),
BinaryOperator::Add,
Expression::literal(json!(2)),
);
assert!(matches!(binary_expr, Expression::Binary { .. }));
}
#[test]
fn test_error_handling() {
let mut lexer = Lexer::new("@invalid");
let result = lexer.tokenize();
assert!(result.is_err());
let mut lexer = Lexer::new("MATCH (");
let tokens = lexer.tokenize().unwrap();
let mut parser = Parser::new(tokens);
let result = parser.parse();
assert!(result.is_err());
let graph = RefCell::new(Graph::new().unwrap());
let gql = Gql::new(&graph);
let result = gql.execute("MATCH (p:NonExistent) RETURN p.unknown");
assert!(result.is_ok());
}
#[test]
fn test_complex_patterns() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql.execute("MATCH (p1:Person)-[:KNOWS]->(p2:Person) RETURN p1.name, p2.name");
assert!(result.is_ok());
let result = gql.execute("MATCH (p:Person), (c:Company) RETURN p.name, c.name");
assert!(result.is_ok());
}
#[test]
fn test_multi_hop_traversal() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql.execute("MATCH (p1:Person)-[:KNOWS]->(p2:Person)-[:WORKS_FOR]->(c:Company) RETURN p1.name, c.name");
assert!(result.is_ok());
let result = result.unwrap();
println!("Multi-hop result: {result:?}");
}
#[test]
fn test_path_pattern_execution() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result =
gql.execute("MATCH (p:Person)-[:KNOWS]->(friend:Person) RETURN p.name, friend.name");
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "p.name");
assert_eq!(result.columns[1], "friend.name");
println!("Path pattern result: {result:?}");
}
#[test]
fn test_path_pattern_with_relationship_filtering() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result =
gql.execute("MATCH (p:Person)-[:WORKS_FOR]->(c:Company) RETURN p.name, c.name");
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.columns.len(), 2);
println!("Work relationship result: {result:?}");
}
#[test]
fn test_longer_path_pattern() {
let mut graph = Graph::new().unwrap();
let _a_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Alice")),
]
.into(),
)
.unwrap();
let _b_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Bob")),
]
.into(),
)
.unwrap();
let _c_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Charlie")),
]
.into(),
)
.unwrap();
let _d_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("David")),
]
.into(),
)
.unwrap();
graph
.create_relationship(1, 2, "KNOWS".to_string(), [].into())
.unwrap();
graph
.create_relationship(2, 3, "KNOWS".to_string(), [].into())
.unwrap();
graph
.create_relationship(3, 4, "KNOWS".to_string(), [].into())
.unwrap();
let graph = RefCell::new(graph);
let gql = Gql::new(&graph);
let result = gql.execute("MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)-[:KNOWS]->(d:Person) RETURN a.name, d.name");
assert!(result.is_ok());
let result = result.unwrap();
println!("3-hop path result: {result:?}");
assert_eq!(result.columns.len(), 2);
}
#[test]
fn test_complex_multi_hop_scenarios() {
let mut graph = Graph::new().unwrap();
let _alice_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Alice")),
("city".to_string(), json!("SF")),
]
.into(),
)
.unwrap();
let _bob_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Bob")),
("city".to_string(), json!("NYC")),
]
.into(),
)
.unwrap();
let _charlie_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Charlie")),
("city".to_string(), json!("LA")),
]
.into(),
)
.unwrap();
let _techcorp_id = graph
.create_node(
[
("type".to_string(), json!("Company")),
("name".to_string(), json!("TechCorp")),
("industry".to_string(), json!("Tech")),
]
.into(),
)
.unwrap();
let _startup_id = graph
.create_node(
[
("type".to_string(), json!("Company")),
("name".to_string(), json!("StartupCo")),
("industry".to_string(), json!("AI")),
]
.into(),
)
.unwrap();
graph
.create_relationship(1, 2, "KNOWS".to_string(), [].into())
.unwrap();
graph
.create_relationship(2, 3, "KNOWS".to_string(), [].into())
.unwrap();
graph
.create_relationship(1, 4, "WORKS_FOR".to_string(), [].into())
.unwrap();
graph
.create_relationship(3, 5, "WORKS_FOR".to_string(), [].into())
.unwrap();
graph
.create_relationship(2, 4, "WORKS_FOR".to_string(), [].into())
.unwrap();
let graph = RefCell::new(graph);
let gql = Gql::new(&graph);
let result = gql.execute(
"MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) RETURN a.name, c.name",
);
assert!(result.is_ok());
let result = result.unwrap();
println!("Friend of friend: {result:?}");
assert_eq!(result.columns.len(), 2);
let result = gql.execute("MATCH (p1:Person)-[:KNOWS]->(p2:Person)-[:WORKS_FOR]->(c:Company) RETURN p1.name, c.name");
assert!(result.is_ok());
let result = result.unwrap();
println!("Friend's company: {result:?}");
let result = gql.execute("MATCH (p1:Person)-[:WORKS_FOR]->(c:Company)<-[:WORKS_FOR]-(p2:Person) RETURN p1.name, p2.name, c.name");
assert!(result.is_ok());
let result = result.unwrap();
println!("Same company colleagues: {result:?}");
let result = gql.execute("MATCH (p:Person)-[:KNOWS]->(friend:Person) WHERE friend.city = 'NYC' RETURN p.name, friend.name");
assert!(result.is_ok());
let result = result.unwrap();
println!("Friends in NYC: {result:?}");
assert_eq!(result.columns.len(), 2);
assert!(
!result.rows.is_empty(),
"Should find Alice -> Bob where Bob is in NYC"
);
let result = gql.execute("MATCH (p:Person)-[:WORKS_FOR]->(c:Company) WHERE c.industry = 'Tech' RETURN p.name, c.name");
assert!(result.is_ok());
let result = result.unwrap();
println!("Tech workers: {result:?}");
}
#[test]
fn test_gql_integration() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let queries = vec![
"MATCH (n) RETURN n",
"MATCH (p:Person) RETURN p.name",
"MATCH (p:Person) WHERE p.age > 20 RETURN p.name, p.age",
"MATCH (p:Person)-[:KNOWS]->(f) RETURN p.name, f.name",
"MATCH (p:Person)-[:WORKS_FOR]->(c:Company) RETURN p.name, c.name",
];
for query in queries {
let result = gql.execute(query);
assert!(result.is_ok(), "Query failed: {query}");
let result = result.unwrap();
assert!(
!result.columns.is_empty(),
"No columns returned for: {query}"
);
}
}
#[test]
fn test_variable_length_patterns() {
let mut graph = Graph::new().unwrap();
let _a_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Alice")),
]
.into(),
)
.unwrap();
let _b_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Bob")),
]
.into(),
)
.unwrap();
let _c_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Charlie")),
]
.into(),
)
.unwrap();
let _d_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("David")),
]
.into(),
)
.unwrap();
let _e_id = graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Eve")),
]
.into(),
)
.unwrap();
graph
.create_relationship(1, 2, "KNOWS".to_string(), [].into())
.unwrap();
graph
.create_relationship(2, 3, "KNOWS".to_string(), [].into())
.unwrap();
graph
.create_relationship(3, 4, "KNOWS".to_string(), [].into())
.unwrap();
graph
.create_relationship(4, 5, "KNOWS".to_string(), [].into())
.unwrap();
let graph = RefCell::new(graph);
let gql = Gql::new(&graph);
let simple_result = gql.execute("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name");
assert!(simple_result.is_ok());
let all_nodes = gql.execute("MATCH (n) RETURN n.name");
assert!(all_nodes.is_ok());
assert_eq!(all_nodes.unwrap().row_count(), 5);
let result = gql.execute("MATCH (a)-[:KNOWS*2]->(b) RETURN a.name, b.name");
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.row_count(), 3, "Should find exactly 3 2-hop paths");
let result = gql.execute("MATCH (a)-[:KNOWS*1..3]->(b) RETURN a.name, b.name");
assert!(result.is_ok());
let result = result.unwrap();
assert!(
result.row_count() >= 4,
"Should find multiple 1-3 hop paths"
);
let result = gql.execute("MATCH (a)-[:KNOWS*]->(b) RETURN a.name, b.name");
assert!(result.is_ok());
let result = result.unwrap();
assert!(
result.row_count() >= 4,
"Should find multiple variable length paths"
);
}
#[test]
fn test_query_result_structure() {
let mut result = QueryResult::new();
result.add_column("name".to_string());
result.add_column("age".to_string());
result.add_row(vec![
QueryValue::String("Alice".to_string()),
QueryValue::Integer(30),
]);
result.add_row(vec![
QueryValue::String("Bob".to_string()),
QueryValue::Integer(25),
]);
assert_eq!(result.columns.len(), 2);
assert_eq!(result.row_count(), 2);
assert!(!result.is_empty());
assert_eq!(result.columns[0], "name");
assert_eq!(result.columns[1], "age");
if let QueryValue::String(name) = &result.rows[0][0] {
assert_eq!(name, "Alice");
} else {
panic!("Expected string value");
}
}
#[test]
fn test_order_by_functionality() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name ASC")
.unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.columns[0], "p.name");
assert!(result.row_count() >= 2);
if let (QueryValue::String(first), QueryValue::String(second)) =
(&result.rows[0][0], &result.rows[1][0])
{
assert!(
first <= second,
"Results should be sorted in ascending order"
);
}
let result = gql
.execute("MATCH (p:Person) RETURN p.name, p.age ORDER BY p.age DESC")
.unwrap();
assert_eq!(result.columns.len(), 2);
let result =
gql.execute("MATCH (p:Person) RETURN p.name, p.age ORDER BY p.age ASC, p.name DESC");
assert!(result.is_ok());
}
#[test]
fn test_limit_functionality() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql
.execute("MATCH (p:Person) RETURN p.name LIMIT 1")
.unwrap();
assert_eq!(result.row_count(), 1);
let result = gql
.execute("MATCH (p:Person) RETURN p.name LIMIT 100")
.unwrap();
assert!(result.row_count() <= 100);
let result = gql
.execute("MATCH (p:Person) RETURN p.name LIMIT 0")
.unwrap();
assert_eq!(result.row_count(), 0);
}
#[test]
fn test_offset_functionality() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let all_results = gql
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name")
.unwrap();
let total_rows = all_results.row_count();
if total_rows > 1 {
let result = gql
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name OFFSET 1")
.unwrap();
assert_eq!(result.row_count(), total_rows - 1);
let result = gql
.execute("MATCH (p:Person) RETURN p.name OFFSET 100")
.unwrap();
assert_eq!(result.row_count(), 0);
}
let result = gql
.execute("MATCH (p:Person) RETURN p.name OFFSET 0")
.unwrap();
assert_eq!(result.row_count(), total_rows);
}
#[test]
fn test_combined_order_limit_offset() {
let mut graph = Graph::new().unwrap();
for i in 1..=5 {
graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!(format!("Person{}", i))),
("age".to_string(), json!(20 + i)),
("score".to_string(), json!(100 - i * 10)),
]
.into(),
)
.unwrap();
}
let graph = RefCell::new(graph);
let gql = Gql::new(&graph);
let result = gql
.execute("MATCH (p:Person) RETURN p.name, p.age ORDER BY p.age ASC LIMIT 2")
.unwrap();
assert_eq!(result.row_count(), 2);
let result = gql
.execute("MATCH (p:Person) RETURN p.name, p.age ORDER BY p.age ASC OFFSET 2")
.unwrap();
assert_eq!(result.row_count(), 3);
let result = gql
.execute("MATCH (p:Person) RETURN p.name, p.age ORDER BY p.age ASC LIMIT 2 OFFSET 1")
.unwrap();
assert_eq!(result.row_count(), 2);
let asc_result = gql
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name ASC")
.unwrap();
let desc_result = gql
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name DESC")
.unwrap();
assert_eq!(asc_result.row_count(), desc_result.row_count());
if asc_result.row_count() > 1 {
assert_eq!(
asc_result.rows[0][0],
desc_result.rows[desc_result.row_count() - 1][0]
);
}
}
#[test]
fn test_order_by_with_path_patterns() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql.execute(
"MATCH (p:Person)-[:KNOWS]->(f:Person) RETURN p.name, f.name ORDER BY p.name, f.name"
).unwrap();
assert_eq!(result.columns.len(), 2);
println!("Path pattern with ORDER BY: {result:?}");
let result = gql.execute(
"MATCH (p:Person)-[:WORKS_FOR]->(c:Company) RETURN p.name, c.name ORDER BY c.name LIMIT 1"
).unwrap();
assert!(result.row_count() <= 1);
println!("Limited path pattern: {result:?}");
}
#[test]
fn test_aggregation_functions() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql.execute("MATCH (p:Person) RETURN COUNT(*)").unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.columns[0], "COUNT(*)");
assert_eq!(result.row_count(), 1);
if let QueryValue::Integer(count) = &result.rows[0][0] {
assert!(*count >= 2, "Should count at least Alice and Bob");
}
let result = gql
.execute("MATCH (p:Person) RETURN COUNT(p.name)")
.unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.row_count(), 1);
if let QueryValue::Integer(count) = &result.rows[0][0] {
assert!(*count >= 2, "Should count non-null names");
}
let result = gql.execute("MATCH (p:Person) RETURN AVG(p.age)").unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.row_count(), 1);
match &result.rows[0][0] {
QueryValue::Float(avg) => assert!(*avg > 0.0),
QueryValue::Integer(avg) => assert!(*avg > 0),
_ => panic!("Expected numeric average"),
}
let result = gql
.execute("MATCH (p:Person) RETURN MIN(p.age), MAX(p.age)")
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.row_count(), 1);
let result = gql.execute("MATCH (p:Person) RETURN SUM(p.age)").unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.row_count(), 1);
match &result.rows[0][0] {
QueryValue::Integer(sum) => assert!(*sum > 0),
QueryValue::Float(sum) => assert!(*sum > 0.0),
_ => panic!("Expected numeric sum"),
}
println!("Aggregation test results: {result:?}");
}
#[test]
fn test_aggregation_with_where_clause() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql
.execute("MATCH (p:Person) WHERE p.age > 25 RETURN COUNT(p)")
.unwrap();
assert_eq!(result.row_count(), 1);
if let QueryValue::Integer(count) = &result.rows[0][0] {
assert!(*count >= 1, "Should count at least Alice (age 30)");
}
let result = gql
.execute("MATCH (p:Person)-[:KNOWS]->(f:Person) RETURN COUNT(*)")
.unwrap();
assert_eq!(result.row_count(), 1);
println!("Relationship count: {result:?}");
}
#[test]
fn test_aggregation_with_path_patterns() {
let graph = setup_test_graph().unwrap();
let gql = Gql::new(&graph);
let result = gql
.execute("MATCH (p:Person)-[:WORKS_FOR]->(c:Company) RETURN COUNT(p), COUNT(c)")
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.row_count(), 1);
println!("Path pattern aggregation: {result:?}");
let result = gql
.execute("MATCH (p:Person)-[:KNOWS]->(friend:Person) RETURN COUNT(friend)")
.unwrap();
assert_eq!(result.row_count(), 1);
}
#[test]
fn test_complex_aggregation_scenario() {
let mut graph = Graph::new().unwrap();
for i in 1..=5 {
graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!(format!("Person{}", i))),
("age".to_string(), json!(20 + i * 5)),
("salary".to_string(), json!(30000 + i * 10000)),
]
.into(),
)
.unwrap();
}
let graph = RefCell::new(graph);
let gql = Gql::new(&graph);
let result = gql.execute("MATCH (p:Person) RETURN COUNT(p), AVG(p.age), SUM(p.salary), MIN(p.age), MAX(p.salary)").unwrap();
assert_eq!(result.columns.len(), 5);
assert_eq!(result.row_count(), 1);
let row = &result.rows[0];
if let QueryValue::Integer(count) = &row[0] {
assert_eq!(*count, 5);
}
match &row[1] {
QueryValue::Float(avg) => assert_eq!(*avg, 35.0),
QueryValue::Integer(avg) => assert_eq!(*avg, 35),
_ => panic!("Expected numeric average"),
}
match &row[2] {
QueryValue::Integer(sum) => assert_eq!(*sum, 300000),
QueryValue::Float(sum) => assert_eq!(*sum, 300000.0),
_ => panic!("Expected numeric sum"),
}
match &row[3] {
QueryValue::Integer(min_age) => assert_eq!(*min_age, 25),
_ => panic!("Expected integer minimum age"),
}
match &row[4] {
QueryValue::Integer(max_salary) => assert_eq!(*max_salary, 80000),
_ => panic!("Expected integer maximum salary"),
}
println!("Complex aggregation results: {result:?}");
}
#[test]
fn test_aggregation_with_nulls() {
let mut graph = Graph::new().unwrap();
graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Alice")),
("age".to_string(), json!(30)),
]
.into(),
)
.unwrap();
graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Bob")),
]
.into(),
)
.unwrap();
graph
.create_node(
[
("type".to_string(), json!("Person")),
("name".to_string(), json!("Charlie")),
("age".to_string(), json!(25)),
]
.into(),
)
.unwrap();
let graph = RefCell::new(graph);
let gql = Gql::new(&graph);
let result = gql.execute("MATCH (p:Person) RETURN COUNT(*)").unwrap();
if let QueryValue::Integer(count) = &result.rows[0][0] {
assert_eq!(*count, 3, "COUNT(*) should count all 3 nodes");
}
let result = gql.execute("MATCH (p:Person) RETURN COUNT(p.age)").unwrap();
if let QueryValue::Integer(count) = &result.rows[0][0] {
assert_eq!(*count, 2, "COUNT(p.age) should count only 2 non-null ages");
}
let result = gql.execute("MATCH (p:Person) RETURN AVG(p.age)").unwrap();
if let QueryValue::Float(avg) = &result.rows[0][0] {
assert_eq!(*avg, 27.5, "AVG should ignore null values");
}
}
}