use crate::executor::{Record, Value};
use crate::parser::ast::RelDirection;
use cypherlite_core::NodeId;
use cypherlite_storage::StorageEngine;
pub fn execute_optional_expand(
source_records: Vec<Record>,
src_var: &str,
rel_var: Option<&str>,
target_var: &str,
rel_type_id: Option<u32>,
direction: &RelDirection,
engine: &StorageEngine,
) -> Vec<Record> {
let mut results = Vec::new();
for record in source_records {
let src_node_id = match record.get(src_var) {
Some(Value::Node(nid)) => *nid,
_ => {
let mut null_record = record;
if let Some(rv) = rel_var {
null_record.insert(rv.to_string(), Value::Null);
}
null_record.insert(target_var.to_string(), Value::Null);
results.push(null_record);
continue;
}
};
let edges = engine.get_edges_for_node(src_node_id);
let mut matched = false;
for edge in edges {
if let Some(tid) = rel_type_id {
if edge.rel_type_id != tid {
continue;
}
}
let target_node_id: Option<NodeId> = match direction {
RelDirection::Outgoing => {
if edge.start_node == src_node_id {
Some(edge.end_node)
} else {
None
}
}
RelDirection::Incoming => {
if edge.end_node == src_node_id {
Some(edge.start_node)
} else {
None
}
}
RelDirection::Undirected => {
if edge.start_node == src_node_id {
Some(edge.end_node)
} else if edge.end_node == src_node_id {
Some(edge.start_node)
} else {
None
}
}
};
if let Some(target_id) = target_node_id {
matched = true;
let mut new_record = record.clone();
if let Some(rv) = rel_var {
new_record.insert(rv.to_string(), Value::Edge(edge.edge_id));
}
new_record.insert(target_var.to_string(), Value::Node(target_id));
results.push(new_record);
}
}
if !matched {
let mut null_record = record;
if let Some(rv) = rel_var {
null_record.insert(rv.to_string(), Value::Null);
}
null_record.insert(target_var.to_string(), Value::Null);
results.push(null_record);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use crate::executor::Record;
use cypherlite_core::{DatabaseConfig, LabelRegistry, SyncMode};
use cypherlite_storage::StorageEngine;
use tempfile::tempdir;
fn test_engine(dir: &std::path::Path) -> StorageEngine {
let config = DatabaseConfig {
path: dir.join("test.cyl"),
wal_sync_mode: SyncMode::Normal,
..Default::default()
};
StorageEngine::open(config).expect("open")
}
#[test]
fn test_optional_expand_with_matches() {
let dir = tempdir().expect("tempdir");
let mut engine = test_engine(dir.path());
let knows_type = engine.get_or_create_rel_type("KNOWS");
let n1 = engine.create_node(vec![], vec![]);
let n2 = engine.create_node(vec![], vec![]);
let n3 = engine.create_node(vec![], vec![]);
engine
.create_edge(n1, n2, knows_type, vec![])
.expect("edge");
engine
.create_edge(n1, n3, knows_type, vec![])
.expect("edge");
let mut source = Record::new();
source.insert("a".to_string(), Value::Node(n1));
let results = execute_optional_expand(
vec![source],
"a",
Some("r"),
"b",
Some(knows_type),
&RelDirection::Outgoing,
&engine,
);
assert_eq!(results.len(), 2);
for r in &results {
assert!(r.contains_key("a"));
assert!(r.contains_key("r"));
assert!(r.contains_key("b"));
assert_ne!(r.get("b"), Some(&Value::Null));
}
}
#[test]
fn test_optional_expand_no_matches_produces_null() {
let dir = tempdir().expect("tempdir");
let mut engine = test_engine(dir.path());
let knows_type = engine.get_or_create_rel_type("KNOWS");
let n1 = engine.create_node(vec![], vec![]);
let mut source = Record::new();
source.insert("a".to_string(), Value::Node(n1));
let results = execute_optional_expand(
vec![source],
"a",
Some("r"),
"b",
Some(knows_type),
&RelDirection::Outgoing,
&engine,
);
assert_eq!(results.len(), 1);
assert_eq!(results[0].get("a"), Some(&Value::Node(n1)));
assert_eq!(results[0].get("b"), Some(&Value::Null));
assert_eq!(results[0].get("r"), Some(&Value::Null));
}
#[test]
fn test_optional_expand_mixed_matches() {
let dir = tempdir().expect("tempdir");
let mut engine = test_engine(dir.path());
let knows_type = engine.get_or_create_rel_type("KNOWS");
let n1 = engine.create_node(vec![], vec![]);
let n2 = engine.create_node(vec![], vec![]);
let n3 = engine.create_node(vec![], vec![]);
engine
.create_edge(n1, n3, knows_type, vec![])
.expect("edge");
let mut source1 = Record::new();
source1.insert("a".to_string(), Value::Node(n1));
let mut source2 = Record::new();
source2.insert("a".to_string(), Value::Node(n2));
let results = execute_optional_expand(
vec![source1, source2],
"a",
None,
"b",
Some(knows_type),
&RelDirection::Outgoing,
&engine,
);
assert_eq!(results.len(), 2);
assert_eq!(results[0].get("a"), Some(&Value::Node(n1)));
assert_eq!(results[0].get("b"), Some(&Value::Node(n3)));
assert_eq!(results[1].get("a"), Some(&Value::Node(n2)));
assert_eq!(results[1].get("b"), Some(&Value::Null));
}
#[test]
fn test_optional_expand_no_rel_var() {
let dir = tempdir().expect("tempdir");
let mut engine = test_engine(dir.path());
let knows_type = engine.get_or_create_rel_type("KNOWS");
let n1 = engine.create_node(vec![], vec![]);
let mut source = Record::new();
source.insert("a".to_string(), Value::Node(n1));
let results = execute_optional_expand(
vec![source],
"a",
None, "b",
Some(knows_type),
&RelDirection::Outgoing,
&engine,
);
assert_eq!(results.len(), 1);
assert_eq!(results[0].get("b"), Some(&Value::Null));
assert!(!results[0].contains_key("r"));
}
}