use cypherlite_core::{DatabaseConfig, SyncMode};
use cypherlite_query::api::CypherLite;
use cypherlite_query::executor::Value;
use tempfile::tempdir;
fn open_db(dir: &std::path::Path) -> CypherLite {
let config = DatabaseConfig {
path: dir.join("test.cyl"),
wal_sync_mode: SyncMode::Normal,
..Default::default()
};
CypherLite::open(config).expect("open db")
}
#[test]
fn test_optional_match_null_when_no_edges() {
let dir = tempdir().expect("tempdir");
let mut db = open_db(dir.path());
db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
.expect("create edge");
db.execute("CREATE (c:Person {name: 'Carol'})")
.expect("create carol");
let result = db
.execute("MATCH (a:Person) OPTIONAL MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name")
.expect("optional match");
assert_eq!(result.rows.len(), 3);
let mut found_alice_bob = false;
let mut found_null_count = 0;
for row in &result.rows {
let a_name = row.get_as::<String>("a.name");
let b_name_val = row.get("b.name");
match (a_name.as_deref(), b_name_val) {
(Some("Alice"), Some(Value::String(b))) if b == "Bob" => {
found_alice_bob = true;
}
(_, Some(Value::Null)) => {
found_null_count += 1;
}
_ => {}
}
}
assert!(
found_alice_bob,
"Should find Alice->Bob match. Rows: {:?}",
result
.rows
.iter()
.map(|r| format!("a.name={:?} b.name={:?}", r.get("a.name"), r.get("b.name")))
.collect::<Vec<_>>()
);
assert_eq!(found_null_count, 2, "Bob and Carol should have NULL b.name");
}
#[test]
fn test_optional_match_multiple_matches() {
let dir = tempdir().expect("tempdir");
let mut db = open_db(dir.path());
db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
.expect("edge1");
db.execute("MATCH (a:Person {name: 'Alice'}) CREATE (a)-[:KNOWS]->(c:Person {name: 'Carol'})")
.expect("edge2");
let result = db
.execute("MATCH (a:Person {name: 'Alice'}) OPTIONAL MATCH (a)-[:KNOWS]->(b) RETURN b.name")
.expect("optional match multi");
assert!(
result.rows.len() >= 2,
"Expected at least 2 rows, got {}",
result.rows.len()
);
let b_names: Vec<Option<String>> = result
.rows
.iter()
.map(|r| r.get_as::<String>("b.name"))
.collect();
assert!(
b_names.contains(&Some("Bob".to_string())),
"Should contain Bob"
);
assert!(
b_names.contains(&Some("Carol".to_string())),
"Should contain Carol"
);
}
#[test]
fn test_optional_match_nonexistent_rel_type() {
let dir = tempdir().expect("tempdir");
let mut db = open_db(dir.path());
db.execute("CREATE (a:Person {name: 'Alice'})")
.expect("create");
let result = db
.execute("MATCH (a:Person) OPTIONAL MATCH (a)-[:WORKS_AT]->(c) RETURN a.name, c")
.expect("optional match no rel type");
assert_eq!(result.rows.len(), 1);
let c_val = result.rows[0].get("c");
assert!(
matches!(c_val, Some(Value::Null)),
"c should be NULL when no WORKS_AT relationship exists, got {:?}",
c_val
);
}