use grafeo_common::types::{EdgeId, NodeId, Value};
use grafeo_engine::GrafeoDB;
#[test]
fn test_get_nonexistent_node() {
let db = GrafeoDB::new_in_memory();
assert!(db.get_node(NodeId::new(999)).is_none());
}
#[test]
fn test_get_nonexistent_edge() {
let db = GrafeoDB::new_in_memory();
assert!(db.get_edge(EdgeId::new(999)).is_none());
}
#[test]
fn test_set_property_on_nonexistent_node() {
let db = GrafeoDB::new_in_memory();
db.set_node_property(NodeId::new(999), "key", Value::Int64(1));
assert!(db.get_node(NodeId::new(999)).is_none());
}
#[test]
fn test_delete_nonexistent_node() {
let db = GrafeoDB::new_in_memory();
assert!(!db.delete_node(NodeId::new(999)));
}
#[test]
fn test_delete_nonexistent_edge() {
let db = GrafeoDB::new_in_memory();
assert!(!db.delete_edge(EdgeId::new(999)));
}
#[test]
fn test_get_labels_nonexistent_node() {
let db = GrafeoDB::new_in_memory();
assert!(db.get_node_labels(NodeId::new(999)).is_none());
}
#[test]
fn test_add_label_nonexistent_node() {
let db = GrafeoDB::new_in_memory();
assert!(!db.add_node_label(NodeId::new(999), "Label"));
}
#[test]
fn test_remove_label_nonexistent_node() {
let db = GrafeoDB::new_in_memory();
assert!(!db.remove_node_label(NodeId::new(999), "Label"));
}
#[test]
fn test_query_syntax_error() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("THIS IS NOT VALID GQL");
assert!(result.is_err(), "Invalid query should return error");
let err = result.unwrap_err();
let err_str = err.to_string();
assert!(!err_str.is_empty(), "Error message should not be empty");
}
#[test]
fn test_query_empty_string() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("");
assert!(result.is_err(), "Empty query should return error");
}
#[test]
fn test_query_unclosed_parenthesis() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n:Person RETURN n");
assert!(result.is_err(), "Unclosed parenthesis should fail");
}
#[test]
fn test_query_undefined_variable() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n:Person) RETURN x.name");
assert!(result.is_err(), "Undefined variable should fail");
}
#[test]
fn test_query_return_without_match() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("RETURN 1 + 2");
let _ = result;
}
#[test]
fn test_commit_without_begin() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
let result = session.commit();
assert!(result.is_err(), "Commit without begin should fail");
}
#[test]
fn test_rollback_without_begin() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
let result = session.rollback();
assert!(result.is_err(), "Rollback without begin should fail");
}
#[test]
fn test_double_begin_creates_nested_transaction() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
let result = session.begin_transaction();
assert!(result.is_ok(), "Double begin creates nested transaction");
session.commit().unwrap();
session.commit().unwrap();
}
#[test]
fn test_begin_after_commit_succeeds() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.commit().unwrap();
let result = session.begin_transaction();
assert!(result.is_ok(), "Begin after commit should succeed");
session.commit().unwrap();
}
#[test]
fn test_begin_after_rollback_succeeds() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.rollback().unwrap();
let result = session.begin_transaction();
assert!(result.is_ok(), "Begin after rollback should succeed");
session.commit().unwrap();
}
#[test]
fn test_query_empty_database() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n) RETURN n").unwrap();
assert_eq!(
result.row_count(),
0,
"Empty database should return no rows"
);
}
#[test]
fn test_count_on_empty_database() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n) RETURN COUNT(n) AS cnt").unwrap();
assert_eq!(
result.row_count(),
1,
"COUNT on empty db should return one row"
);
}
#[test]
fn test_validate_empty_database() {
let db = GrafeoDB::new_in_memory();
let result = db.validate();
assert!(result.is_valid(), "Empty database should be valid");
}
#[test]
fn test_schema_empty_database() {
let db = GrafeoDB::new_in_memory();
let schema = db.schema();
match schema {
grafeo_engine::SchemaInfo::Lpg(lpg) => {
assert!(lpg.labels.is_empty(), "No labels in empty db");
assert!(lpg.edge_types.is_empty(), "No edge types in empty db");
}
_ => panic!("Expected LPG schema for in-memory db"),
}
}
#[test]
fn test_info_empty_database() {
let db = GrafeoDB::new_in_memory();
let info = db.info();
assert_eq!(info.node_count, 0);
assert_eq!(info.edge_count, 0);
assert!(!info.is_persistent);
}
#[test]
fn test_property_with_null_value() {
let db = GrafeoDB::new_in_memory();
let n = db.create_node(&["Test"]);
db.set_node_property(n, "key", Value::Null);
let node = db.get_node(n).unwrap();
#[cfg(feature = "temporal")]
assert_eq!(
node.get_property("key"),
None,
"Temporal: Null acts as tombstone"
);
#[cfg(not(feature = "temporal"))]
assert_eq!(
node.get_property("key"),
Some(&Value::Null),
"Should store Null value"
);
}
#[test]
fn test_property_with_empty_string() {
let db = GrafeoDB::new_in_memory();
let n = db.create_node(&["Test"]);
db.set_node_property(n, "key", Value::String("".into()));
let node = db.get_node(n).unwrap();
assert_eq!(
node.get_property("key"),
Some(&Value::String("".into())),
"Should store empty string"
);
}
#[test]
fn test_node_with_no_labels() {
let db = GrafeoDB::new_in_memory();
let n = db.create_node(&[]);
let node = db.get_node(n);
assert!(node.is_some(), "Node with no labels should exist");
assert_eq!(db.node_count(), 1);
}
#[test]
fn test_node_with_many_labels() {
let db = GrafeoDB::new_in_memory();
let labels: Vec<&str> = (0..10)
.map(|i| match i {
0 => "A",
1 => "B",
2 => "C",
3 => "D",
4 => "E",
5 => "F",
6 => "G",
7 => "H",
8 => "I",
_ => "J",
})
.collect();
let n = db.create_node(&labels);
let stored_labels = db.get_node_labels(n).unwrap();
assert_eq!(stored_labels.len(), 10);
}
#[test]
fn test_self_referencing_edge() {
let db = GrafeoDB::new_in_memory();
let n = db.create_node(&["Node"]);
let e = db.create_edge(n, n, "SELF");
let edge = db.get_edge(e).unwrap();
assert_eq!(edge.src, n);
assert_eq!(edge.dst, n);
}
#[test]
fn test_gql_syntax_error_has_position_info() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n:Person RETURN n");
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
!err_str.contains("Internal error"),
"Parser error should NOT be an internal error, got: {}",
err_str
);
assert!(
err_str.contains("-->"),
"Parser error should include source position, got: {}",
err_str
);
}
#[test]
fn test_translator_errors_not_internal() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let test_queries = [
"MATCH (n:Person) RETURN x.name", ];
for query in &test_queries {
let result = session.execute(query);
if let Err(err) = result {
let err_str = err.to_string();
assert!(
!err_str.contains("GRAFEO-X"),
"Query '{}' should NOT produce an internal error, got: {}",
query,
err_str
);
}
}
}
#[cfg(feature = "algos")]
#[test]
fn test_gql_unknown_procedure_error_code() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("CALL grafeo.nonexistent()");
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("Unknown procedure"),
"Should say 'Unknown procedure', got: {}",
err_str
);
}
#[cfg(feature = "algos")]
#[test]
fn test_gql_yield_nonexistent_column_error() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("CALL grafeo.pagerank() YIELD nonexistent_column");
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("not found"),
"Should mention column not found, got: {}",
err_str
);
}
#[test]
#[cfg(feature = "cypher")]
fn test_cypher_pattern_comprehension_works() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute_cypher("MATCH (n) RETURN [(n)-[:KNOWS]->(m) | m.name] AS friends");
assert!(
result.is_ok(),
"Pattern comprehension should succeed, got: {:?}",
result.err()
);
}
#[test]
#[cfg(feature = "graphql")]
fn test_graphql_range_filter_end_to_end() {
let db = GrafeoDB::new_in_memory();
for i in 0..5 {
let n = db.create_node(&["Person"]);
db.set_node_property(n, "name", Value::String(format!("Person{}", i).into()));
db.set_node_property(n, "age", Value::Int64(20 + i * 10)); }
let session = db.session();
let result = session.execute_graphql(r#"{ person(age_gt: 25, age_lt: 55) { name age } }"#);
assert!(
result.is_ok(),
"GraphQL range filter should work: {:?}",
result.err()
);
let result = result.unwrap();
assert_eq!(
result.row_count(),
3,
"Should find 3 persons with 25 < age < 55, got {}",
result.row_count()
);
}
#[test]
fn test_insert_and_match_special_characters_in_properties() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Test {name: 'Hello World'})")
.unwrap();
let result = session.execute("MATCH (t:Test) RETURN t.name").unwrap();
assert_eq!(result.row_count(), 1);
}
#[test]
fn test_match_with_multiple_labels() {
let db = GrafeoDB::new_in_memory();
let n = db.create_node(&["Person", "Employee"]);
db.set_node_property(n, "name", Value::String("Alix".into()));
let session = db.session();
let result = session.execute("MATCH (n:Person) RETURN n.name").unwrap();
assert_eq!(result.row_count(), 1);
}
#[test]
fn test_in_operator_with_empty_list() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
db.create_node(&["Person"]);
let result = session
.execute("MATCH (n:Person) WHERE n.name IN [] RETURN n")
.unwrap();
assert_eq!(
result.row_count(),
0,
"IN with empty list should match nothing"
);
}
#[test]
fn test_gql_error_shows_line_and_column() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n:Person RETURN n");
assert!(result.is_err(), "Expected parse error for malformed GQL");
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("-->"),
"GQL error should show position, got: {err_str}"
);
assert!(
err_str.contains("RETURN"),
"GQL error should show source query, got: {err_str}"
);
assert!(
err_str.contains('^'),
"GQL error should show caret markers, got: {err_str}"
);
}
#[test]
fn test_gql_multiline_error_position() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let query = "MATCH (n:Person)\nWHERE n.age > 30\nRETRUN n";
let result = session.execute(query);
assert!(result.is_err(), "Expected parse error for RETURN typo");
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("--> query:3:"),
"Multiline error should show line 3, got: {err_str}"
);
}
#[test]
#[cfg(feature = "cypher")]
fn test_cypher_error_shows_position() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute_cypher("MATCH (n:Person RETURN n");
assert!(result.is_err(), "Expected parse error for malformed Cypher");
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("-->"),
"Cypher error should show position, got: {err_str}"
);
assert!(
err_str.contains("RETURN"),
"Cypher error should show source, got: {err_str}"
);
}
#[test]
#[cfg(all(feature = "sparql", feature = "triple-store"))]
fn test_sparql_error_shows_position() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute_sparql("SELECT ?s WHERE { ?s ?p ?o");
assert!(result.is_err(), "Expected parse error for malformed SPARQL");
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("-->"),
"SPARQL error should show position, got: {err_str}"
);
}
#[test]
#[cfg(feature = "sql-pgq")]
fn test_sql_pgq_error_shows_position() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute_sql("SELECT * FROM GRAPH_TABLE(g MATCH (n) COLUMNS (n.name))");
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("-->"),
"SQL/PGQ error should show position, got: {err_str}"
);
}
#[test]
fn test_whitespace_only_query() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute(" \t\n ");
assert!(result.is_err(), "whitespace-only query should error");
}
#[test]
fn test_null_byte_in_query() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n\0:Person) RETURN n");
let _ = result;
}
#[test]
fn test_deeply_nested_parentheses() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let mut query = String::from("MATCH (n:Person) WHERE ");
for _ in 0..50 {
query.push('(');
}
query.push_str("n.age > 0");
for _ in 0..50 {
query.push(')');
}
query.push_str(" RETURN n");
let _ = session.execute(&query);
}
#[test]
fn test_very_long_identifier() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let long_label = "A".repeat(10_000);
let query = format!("MATCH (n:{long_label}) RETURN n");
let _ = session.execute(&query);
}
#[test]
fn test_unclosed_string_literal() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n:Person) WHERE n.name = 'unclosed RETURN n");
assert!(result.is_err(), "unclosed string literal should error");
}
#[test]
fn test_mismatched_brackets_in_edge() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (a)-[:KNOWS->(b) RETURN a");
assert!(result.is_err(), "mismatched brackets should error");
}
#[test]
fn test_binary_garbage_input() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let garbage = String::from_utf8_lossy(&[0xFF, 0xFE, 0x00, 0x01, 0x80, 0x90]).to_string();
let result = session.execute(&garbage);
assert!(result.is_err(), "binary garbage should error");
}
#[test]
fn test_gql_error_names_unexpected_token() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n:Person) RETURNING n");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("RETURNING") || err.contains("unexpected") || err.contains("expected"),
"error should name the unexpected token or suggest expected tokens, got: {err}"
);
}
#[test]
fn test_gql_error_includes_line_and_column() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n:Person)\nWHERE n.age >\nRETURN n");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("-->"),
"multiline error should include position marker, got: {err}"
);
}
#[test]
fn test_gql_error_includes_caret_marker() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute("MATCH (n:Person) WHERE n. RETURN n");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains('^') || err.contains("-->"),
"error should include visual position markers, got: {err}"
);
}
#[test]
#[cfg(all(feature = "sparql", feature = "triple-store"))]
fn test_sparql_error_includes_position() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute_sparql("GRAB ?s WHERE { ?s ?p ?o }");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("-->") || err.contains("SELECT") || err.contains("expected"),
"SPARQL error should include position or name bad token, got: {err}"
);
}
#[test]
#[cfg(feature = "cypher")]
fn test_cypher_error_names_bad_token() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
let result = session.execute_cypher("MTCH (n) RETURN n");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("-->") || err.contains("MTCH") || err.contains("expected"),
"Cypher error should include position or name bad token, got: {err}"
);
}