#![cfg(feature = "algos")]
use grafeo_common::types::Value;
use grafeo_engine::GrafeoDB;
fn setup_graph() -> GrafeoDB {
let db = GrafeoDB::new_in_memory();
let alix = db.create_node(&["Person"]);
let gus = db.create_node(&["Person"]);
let harm = db.create_node(&["Person"]);
db.set_node_property(alix, "name", Value::from("Alix"));
db.set_node_property(gus, "name", Value::from("Gus"));
db.set_node_property(harm, "name", Value::from("Harm"));
db.create_edge(alix, gus, "KNOWS");
db.create_edge(gus, harm, "KNOWS");
db
}
#[test]
fn test_gql_call_pagerank() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.pagerank()").unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "node_id");
assert_eq!(result.columns[1], "score");
assert_eq!(result.row_count(), 3); }
#[test]
fn test_gql_call_pagerank_with_params() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.pagerank({damping: 0.85, max_iterations: 10})")
.unwrap();
assert_eq!(result.row_count(), 3);
let total_score: f64 = result
.rows()
.iter()
.map(|row| match &row[1] {
Value::Float64(f) => *f,
_ => 0.0,
})
.sum();
assert!(
(total_score - 1.0).abs() < 0.1,
"PageRank scores should sum to ~1.0, got {}",
total_score
);
}
#[test]
fn test_gql_call_with_yield() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.pagerank() YIELD score")
.unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.columns[0], "score");
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_gql_call_with_yield_alias() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.pagerank() YIELD node_id AS id, score AS rank")
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "id");
assert_eq!(result.columns[1], "rank");
}
#[test]
fn test_gql_call_connected_components() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.connected_components()")
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "node_id");
assert_eq!(result.columns[1], "component_id");
assert_eq!(result.row_count(), 3);
let components: Vec<&Value> = result.rows().iter().map(|r| &r[1]).collect();
assert_eq!(components[0], components[1]);
assert_eq!(components[1], components[2]);
}
#[test]
fn test_gql_call_without_namespace() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL pagerank()").unwrap();
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_gql_call_unknown_procedure() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.nonexistent()");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Unknown procedure"),
"Expected 'Unknown procedure' error, got: {}",
err
);
}
#[test]
fn test_gql_call_procedures_list() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.procedures()").unwrap();
assert_eq!(result.columns.len(), 4);
assert_eq!(result.columns[0], "name");
assert_eq!(result.columns[1], "description");
assert!(result.row_count() >= 22, "Expected at least 22 procedures");
}
#[test]
fn test_gql_call_empty_graph() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("CALL grafeo.pagerank()").unwrap();
assert_eq!(result.row_count(), 0);
}
#[test]
#[cfg(feature = "cypher")]
fn test_cypher_call_pagerank() {
let db = setup_graph();
let session = db.session();
let result = session.execute_cypher("CALL grafeo.pagerank()").unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "node_id");
assert_eq!(result.columns[1], "score");
assert_eq!(result.row_count(), 3);
}
#[test]
#[cfg(feature = "cypher")]
fn test_cypher_call_with_yield() {
let db = setup_graph();
let session = db.session();
let result = session
.execute_cypher("CALL grafeo.pagerank() YIELD score")
.unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.columns[0], "score");
}
#[test]
#[cfg(feature = "cypher")]
fn test_cypher_call_connected_components() {
let db = setup_graph();
let session = db.session();
let result = session
.execute_cypher("CALL grafeo.connected_components()")
.unwrap();
assert_eq!(result.row_count(), 3);
}
#[test]
#[cfg(feature = "sql-pgq")]
fn test_sql_pgq_call_pagerank() {
let db = setup_graph();
let session = db.session();
let result = session.execute_sql("CALL grafeo.pagerank()").unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "node_id");
assert_eq!(result.columns[1], "score");
assert_eq!(result.row_count(), 3);
}
#[test]
#[cfg(feature = "sql-pgq")]
fn test_sql_pgq_call_with_yield() {
let db = setup_graph();
let session = db.session();
let result = session
.execute_sql("CALL grafeo.pagerank() YIELD score AS rank")
.unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.columns[0], "rank");
}
#[test]
#[cfg(all(feature = "cypher", feature = "sql-pgq"))]
fn test_language_parity_pagerank() {
let db = setup_graph();
let session = db.session();
let gql_result = session.execute("CALL grafeo.pagerank()").unwrap();
let cypher_result = session.execute_cypher("CALL grafeo.pagerank()").unwrap();
let sql_result = session.execute_sql("CALL grafeo.pagerank()").unwrap();
assert_eq!(gql_result.columns, cypher_result.columns);
assert_eq!(gql_result.columns, sql_result.columns);
assert_eq!(gql_result.row_count(), cypher_result.row_count());
assert_eq!(gql_result.row_count(), sql_result.row_count());
}
#[test]
fn test_call_bfs() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.bfs(0)").unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "node_id");
assert_eq!(result.columns[1], "depth");
assert!(result.row_count() >= 1);
}
#[test]
fn test_call_clustering_coefficient() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.clustering_coefficient()")
.unwrap();
assert_eq!(result.columns[0], "node_id");
assert_eq!(result.columns[1], "coefficient");
assert_eq!(result.columns[2], "triangle_count");
assert_eq!(result.row_count(), 3);
for row in result.rows() {
if let Value::Float64(coeff) = &row[1] {
assert!(
(0.0..=1.0).contains(coeff),
"Coefficient {} out of range",
coeff
);
}
}
}
#[test]
fn test_call_degree_centrality() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.degree_centrality()").unwrap();
assert_eq!(result.columns[0], "node_id");
assert_eq!(result.columns[1], "in_degree");
assert_eq!(result.columns[2], "out_degree");
assert_eq!(result.columns[3], "total_degree");
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_call_case_insensitive() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.pagerank()");
assert!(result.is_ok());
}
#[test]
fn test_call_yield_nonexistent_column() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.pagerank() YIELD nonexistent_column");
assert!(result.is_err(), "YIELD of nonexistent column should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("not found"),
"Error should mention column not found, got: {}",
err
);
}
#[test]
fn test_call_yield_duplicate_columns() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.pagerank() YIELD score AS s1, score AS s2");
assert!(
result.is_ok(),
"YIELD same column with different aliases should work: {:?}",
result.err()
);
let result = result.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "s1");
assert_eq!(result.columns[1], "s2");
}
#[test]
fn test_call_procedures_list_has_expected_columns() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.procedures()").unwrap();
assert_eq!(result.columns[0], "name");
assert_eq!(result.columns[1], "description");
assert_eq!(result.columns[2], "parameters");
assert_eq!(result.columns[3], "output_columns");
for row in result.rows() {
if let Value::String(name) = &row[0] {
assert!(!name.is_empty(), "Procedure name should not be empty");
} else {
panic!("Procedure name should be a string");
}
}
}
#[test]
fn test_call_multiple_algorithms_on_same_graph() {
let db = setup_graph();
let session = db.session();
let pr = session.execute("CALL grafeo.pagerank()").unwrap();
let cc = session
.execute("CALL grafeo.connected_components()")
.unwrap();
let dc = session.execute("CALL grafeo.degree_centrality()").unwrap();
let bc = session
.execute("CALL grafeo.betweenness_centrality()")
.unwrap();
assert_eq!(pr.row_count(), 3);
assert_eq!(cc.row_count(), 3);
assert_eq!(dc.row_count(), 3);
assert_eq!(bc.row_count(), 3);
}
#[test]
fn test_call_bfs_with_invalid_source() {
let db = setup_graph();
let session = db.session();
let result = session.execute("CALL grafeo.bfs(999999)");
match result {
Ok(r) => assert_eq!(
r.row_count(),
0,
"BFS from invalid source should return empty"
),
Err(_) => {} }
}
#[test]
fn test_call_shortest_path_disconnected() {
let db = GrafeoDB::new_in_memory();
let a = db.create_node(&["Node"]);
let b = db.create_node(&["Node"]);
db.set_node_property(a, "name", Value::from("A"));
db.set_node_property(b, "name", Value::from("B"));
let session = db.session();
let result = session.execute(&format!(
"CALL grafeo.shortest_path({}, {})",
a.as_u64(),
b.as_u64()
));
match result {
Ok(r) => assert_eq!(r.row_count(), 0, "No path between disconnected nodes"),
Err(_) => {} }
}
#[test]
fn test_call_pagerank_single_node() {
let db = GrafeoDB::new_in_memory();
db.create_node(&["Isolated"]);
let session = db.session();
let result = session.execute("CALL grafeo.pagerank()").unwrap();
assert_eq!(result.row_count(), 1, "Single node should get PageRank");
if let Value::Float64(score) = &result.rows()[0][1] {
assert!(
(*score - 1.0).abs() < 0.01,
"Single node should have PageRank ~1.0, got {}",
score
);
}
}
#[test]
fn test_call_yield_all_then_specific() {
let db = setup_graph();
let session = db.session();
let all = session.execute("CALL grafeo.pagerank()").unwrap();
let specific = session
.execute("CALL grafeo.pagerank() YIELD score")
.unwrap();
assert_eq!(
all.columns.len(),
2,
"Without YIELD: should get all columns"
);
assert_eq!(specific.columns.len(), 1, "With YIELD: should get 1 column");
assert_eq!(
all.row_count(),
specific.row_count(),
"Row counts should match"
);
}
#[test]
fn test_gql_call_yield_where() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.pagerank() YIELD node_id, score WHERE score > 0.0")
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.row_count(), 3);
for row in result.rows() {
if let Value::Float64(score) = &row[1] {
assert!(*score > 0.0, "Expected score > 0.0, got {}", score);
}
}
}
#[test]
fn test_gql_call_yield_where_filters_rows() {
let db = setup_graph();
let session = db.session();
let all = session.execute("CALL grafeo.pagerank()").unwrap();
let max_score = all
.rows()
.iter()
.map(|r| match &r[1] {
Value::Float64(f) => *f,
_ => 0.0,
})
.fold(f64::NEG_INFINITY, f64::max);
let result = session
.execute(&format!(
"CALL grafeo.pagerank() YIELD score WHERE score > {}",
max_score
))
.unwrap();
assert_eq!(result.row_count(), 0, "No score should exceed the maximum");
}
#[test]
fn test_gql_call_yield_return() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.pagerank() YIELD node_id, score RETURN node_id, score")
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "node_id");
assert_eq!(result.columns[1], "score");
assert_eq!(result.row_count(), 3);
}
#[test]
fn test_gql_call_yield_return_with_alias() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.pagerank() YIELD node_id, score RETURN node_id AS id, score AS rank")
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0], "id");
assert_eq!(result.columns[1], "rank");
}
#[test]
fn test_gql_call_yield_return_order_by() {
let db = setup_graph();
let session = db.session();
let result = session
.execute(
"CALL grafeo.pagerank() YIELD node_id, score RETURN node_id, score ORDER BY score DESC",
)
.unwrap();
assert_eq!(result.row_count(), 3);
let scores: Vec<f64> = result
.rows()
.iter()
.map(|r| match &r[1] {
Value::Float64(f) => *f,
_ => 0.0,
})
.collect();
for i in 1..scores.len() {
assert!(
scores[i - 1] >= scores[i],
"Scores should be in DESC order: {:?}",
scores
);
}
}
#[test]
fn test_gql_call_yield_return_limit() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.pagerank() YIELD node_id, score RETURN node_id, score LIMIT 2")
.unwrap();
assert_eq!(result.row_count(), 2);
}
#[test]
fn test_gql_call_yield_where_return_order_limit() {
let db = setup_graph();
let session = db.session();
let result = session
.execute(
"CALL grafeo.pagerank() YIELD node_id, score \
WHERE score > 0.0 \
RETURN node_id, score ORDER BY score DESC LIMIT 2",
)
.unwrap();
assert!(
result.row_count() <= 2,
"LIMIT 2 should return at most 2 rows"
);
if result.row_count() == 2 {
let s0 = match &result.rows()[0][1] {
Value::Float64(f) => *f,
_ => 0.0,
};
let s1 = match &result.rows()[1][1] {
Value::Float64(f) => *f,
_ => 0.0,
};
assert!(
s0 >= s1,
"First score {} should be >= second score {}",
s0,
s1
);
}
}
#[test]
fn test_gql_call_yield_return_skip() {
let db = setup_graph();
let session = db.session();
let result = session
.execute("CALL grafeo.pagerank() YIELD node_id, score RETURN node_id, score SKIP 1")
.unwrap();
assert_eq!(result.row_count(), 2, "SKIP 1 of 3 rows should leave 2");
}
#[test]
fn test_gql_call_yield_return_order_skip_limit() {
let db = setup_graph();
let session = db.session();
let result = session
.execute(
"CALL grafeo.pagerank() YIELD node_id, score \
RETURN node_id, score ORDER BY score DESC SKIP 1 LIMIT 1",
)
.unwrap();
assert_eq!(result.row_count(), 1, "SKIP 1 + LIMIT 1 should leave 1 row");
}
#[test]
#[cfg(feature = "sql-pgq")]
fn test_sql_pgq_call_yield_where() {
let db = setup_graph();
let session = db.session();
let result = session
.execute_sql("CALL grafeo.pagerank() YIELD node_id, score WHERE score > 0.0")
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.row_count(), 3);
}
#[test]
#[cfg(feature = "sql-pgq")]
fn test_sql_pgq_call_yield_order_by_limit() {
let db = setup_graph();
let session = db.session();
let result = session
.execute_sql("CALL grafeo.pagerank() YIELD node_id, score ORDER BY score DESC LIMIT 2")
.unwrap();
assert!(result.row_count() <= 2);
if result.row_count() == 2 {
let s0 = match &result.rows()[0][1] {
Value::Float64(f) => *f,
_ => 0.0,
};
let s1 = match &result.rows()[1][1] {
Value::Float64(f) => *f,
_ => 0.0,
};
assert!(s0 >= s1, "Scores should be in DESC order");
}
}
#[test]
#[cfg(feature = "sql-pgq")]
fn test_sql_pgq_call_yield_where_order_limit() {
let db = setup_graph();
let session = db.session();
let result = session
.execute_sql(
"CALL grafeo.pagerank() YIELD node_id, score \
WHERE score > 0.0 ORDER BY score DESC LIMIT 2",
)
.unwrap();
assert!(result.row_count() <= 2);
}
#[test]
#[cfg(feature = "sql-pgq")]
fn test_sql_pgq_call_yield_where_return_skip_limit() {
let db = setup_graph();
let session = db.session();
let result = session
.execute_sql(
"CALL grafeo.pagerank() YIELD node_id, score \
WHERE score > 0.0 ORDER BY score ASC LIMIT 1",
)
.unwrap();
assert_eq!(result.row_count(), 1);
let all = session
.execute_sql("CALL grafeo.pagerank() YIELD score ORDER BY score ASC")
.unwrap();
assert_eq!(result.rows()[0][1], all.rows()[0][0]);
}
#[test]
fn test_gql_call_yield_return_order_asc() {
let db = setup_graph();
let session = db.session();
let result = session
.execute(
"CALL grafeo.pagerank() YIELD node_id, score \
RETURN node_id, score ORDER BY score ASC",
)
.unwrap();
assert_eq!(result.row_count(), 3);
let scores: Vec<f64> = result
.rows()
.iter()
.map(|r| match &r[1] {
Value::Float64(f) => *f,
_ => 0.0,
})
.collect();
for i in 1..scores.len() {
assert!(
scores[i - 1] <= scores[i],
"Scores should be in ASC order: {:?}",
scores
);
}
}
#[test]
fn test_gql_call_yield_return_distinct() {
let db = setup_graph();
let session = db.session();
let all = session
.execute(
"CALL grafeo.connected_components() YIELD component_id \
RETURN component_id",
)
.unwrap();
let distinct = session
.execute(
"CALL grafeo.connected_components() YIELD component_id \
RETURN DISTINCT component_id",
)
.unwrap();
assert!(
distinct.row_count() <= all.row_count(),
"DISTINCT ({}) should not exceed original ({})",
distinct.row_count(),
all.row_count()
);
}
#[test]
#[cfg(feature = "cypher")]
fn test_cypher_call_yield_where_return() {
let db = setup_graph();
let session = db.session();
let result = session
.execute_cypher(
"CALL grafeo.pagerank() YIELD node_id, score \
WHERE score > 0.0 \
RETURN node_id, score ORDER BY score DESC LIMIT 2",
)
.unwrap();
assert!(result.row_count() <= 2);
}