cypherlite_query/executor/operators/
optional_expand.rs1use crate::executor::{Record, Value};
8use crate::parser::ast::RelDirection;
9use cypherlite_core::NodeId;
10use cypherlite_storage::StorageEngine;
11
12pub fn execute_optional_expand(
16 source_records: Vec<Record>,
17 src_var: &str,
18 rel_var: Option<&str>,
19 target_var: &str,
20 rel_type_id: Option<u32>,
21 direction: &RelDirection,
22 engine: &StorageEngine,
23) -> Vec<Record> {
24 let mut results = Vec::new();
25
26 for record in source_records {
27 let src_node_id = match record.get(src_var) {
28 Some(Value::Node(nid)) => *nid,
29 _ => {
30 let mut null_record = record;
32 if let Some(rv) = rel_var {
33 null_record.insert(rv.to_string(), Value::Null);
34 }
35 null_record.insert(target_var.to_string(), Value::Null);
36 results.push(null_record);
37 continue;
38 }
39 };
40
41 let edges = engine.get_edges_for_node(src_node_id);
42 let mut matched = false;
43
44 for edge in edges {
45 if let Some(tid) = rel_type_id {
47 if edge.rel_type_id != tid {
48 continue;
49 }
50 }
51
52 let target_node_id: Option<NodeId> = match direction {
54 RelDirection::Outgoing => {
55 if edge.start_node == src_node_id {
56 Some(edge.end_node)
57 } else {
58 None
59 }
60 }
61 RelDirection::Incoming => {
62 if edge.end_node == src_node_id {
63 Some(edge.start_node)
64 } else {
65 None
66 }
67 }
68 RelDirection::Undirected => {
69 if edge.start_node == src_node_id {
70 Some(edge.end_node)
71 } else if edge.end_node == src_node_id {
72 Some(edge.start_node)
73 } else {
74 None
75 }
76 }
77 };
78
79 if let Some(target_id) = target_node_id {
80 matched = true;
81 let mut new_record = record.clone();
82 if let Some(rv) = rel_var {
83 new_record.insert(rv.to_string(), Value::Edge(edge.edge_id));
84 }
85 new_record.insert(target_var.to_string(), Value::Node(target_id));
86 results.push(new_record);
87 }
88 }
89
90 if !matched {
92 let mut null_record = record;
93 if let Some(rv) = rel_var {
94 null_record.insert(rv.to_string(), Value::Null);
95 }
96 null_record.insert(target_var.to_string(), Value::Null);
97 results.push(null_record);
98 }
99 }
100
101 results
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::executor::Record;
108 use cypherlite_core::{DatabaseConfig, LabelRegistry, SyncMode};
109 use cypherlite_storage::StorageEngine;
110 use tempfile::tempdir;
111
112 fn test_engine(dir: &std::path::Path) -> StorageEngine {
113 let config = DatabaseConfig {
114 path: dir.join("test.cyl"),
115 wal_sync_mode: SyncMode::Normal,
116 ..Default::default()
117 };
118 StorageEngine::open(config).expect("open")
119 }
120
121 #[test]
123 fn test_optional_expand_with_matches() {
124 let dir = tempdir().expect("tempdir");
125 let mut engine = test_engine(dir.path());
126
127 let knows_type = engine.get_or_create_rel_type("KNOWS");
128 let n1 = engine.create_node(vec![], vec![]);
129 let n2 = engine.create_node(vec![], vec![]);
130 let n3 = engine.create_node(vec![], vec![]);
131
132 engine
133 .create_edge(n1, n2, knows_type, vec![])
134 .expect("edge");
135 engine
136 .create_edge(n1, n3, knows_type, vec![])
137 .expect("edge");
138
139 let mut source = Record::new();
140 source.insert("a".to_string(), Value::Node(n1));
141
142 let results = execute_optional_expand(
143 vec![source],
144 "a",
145 Some("r"),
146 "b",
147 Some(knows_type),
148 &RelDirection::Outgoing,
149 &engine,
150 );
151
152 assert_eq!(results.len(), 2);
154 for r in &results {
155 assert!(r.contains_key("a"));
156 assert!(r.contains_key("r"));
157 assert!(r.contains_key("b"));
158 assert_ne!(r.get("b"), Some(&Value::Null));
159 }
160 }
161
162 #[test]
164 fn test_optional_expand_no_matches_produces_null() {
165 let dir = tempdir().expect("tempdir");
166 let mut engine = test_engine(dir.path());
167
168 let knows_type = engine.get_or_create_rel_type("KNOWS");
169 let n1 = engine.create_node(vec![], vec![]);
171
172 let mut source = Record::new();
173 source.insert("a".to_string(), Value::Node(n1));
174
175 let results = execute_optional_expand(
176 vec![source],
177 "a",
178 Some("r"),
179 "b",
180 Some(knows_type),
181 &RelDirection::Outgoing,
182 &engine,
183 );
184
185 assert_eq!(results.len(), 1);
187 assert_eq!(results[0].get("a"), Some(&Value::Node(n1)));
188 assert_eq!(results[0].get("b"), Some(&Value::Null));
189 assert_eq!(results[0].get("r"), Some(&Value::Null));
190 }
191
192 #[test]
194 fn test_optional_expand_mixed_matches() {
195 let dir = tempdir().expect("tempdir");
196 let mut engine = test_engine(dir.path());
197
198 let knows_type = engine.get_or_create_rel_type("KNOWS");
199 let n1 = engine.create_node(vec![], vec![]);
200 let n2 = engine.create_node(vec![], vec![]);
201 let n3 = engine.create_node(vec![], vec![]);
202
203 engine
205 .create_edge(n1, n3, knows_type, vec![])
206 .expect("edge");
207
208 let mut source1 = Record::new();
209 source1.insert("a".to_string(), Value::Node(n1));
210 let mut source2 = Record::new();
211 source2.insert("a".to_string(), Value::Node(n2));
212
213 let results = execute_optional_expand(
214 vec![source1, source2],
215 "a",
216 None,
217 "b",
218 Some(knows_type),
219 &RelDirection::Outgoing,
220 &engine,
221 );
222
223 assert_eq!(results.len(), 2);
225
226 assert_eq!(results[0].get("a"), Some(&Value::Node(n1)));
228 assert_eq!(results[0].get("b"), Some(&Value::Node(n3)));
229
230 assert_eq!(results[1].get("a"), Some(&Value::Node(n2)));
232 assert_eq!(results[1].get("b"), Some(&Value::Null));
233 }
234
235 #[test]
237 fn test_optional_expand_no_rel_var() {
238 let dir = tempdir().expect("tempdir");
239 let mut engine = test_engine(dir.path());
240
241 let knows_type = engine.get_or_create_rel_type("KNOWS");
242 let n1 = engine.create_node(vec![], vec![]);
243
244 let mut source = Record::new();
245 source.insert("a".to_string(), Value::Node(n1));
246
247 let results = execute_optional_expand(
248 vec![source],
249 "a",
250 None, "b",
252 Some(knows_type),
253 &RelDirection::Outgoing,
254 &engine,
255 );
256
257 assert_eq!(results.len(), 1);
258 assert_eq!(results[0].get("b"), Some(&Value::Null));
259 assert!(!results[0].contains_key("r"));
261 }
262}