cypherlite_query/executor/operators/
unwind.rs1use crate::executor::eval::eval;
4use crate::executor::{ExecutionError, Params, Record, ScalarFnLookup, Value};
5use crate::parser::ast::Expression;
6use cypherlite_storage::StorageEngine;
7
8pub fn execute_unwind(
13 source_records: Vec<Record>,
14 expr: &Expression,
15 variable: &str,
16 engine: &StorageEngine,
17 params: &Params,
18 scalar_fns: &dyn ScalarFnLookup,
19) -> Result<Vec<Record>, ExecutionError> {
20 let mut results = Vec::new();
21
22 for record in &source_records {
23 let value = eval(expr, record, engine, params, scalar_fns)?;
24 match value {
25 Value::List(elements) => {
26 for element in elements {
27 let mut new_record = record.clone();
28 new_record.insert(variable.to_string(), element);
29 results.push(new_record);
30 }
31 }
32 Value::Null => {
33 }
35 _ => {
36 return Err(ExecutionError {
37 message: format!("UNWIND expected a list or null, got {:?}", value),
38 });
39 }
40 }
41 }
42
43 Ok(results)
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use crate::executor::Value;
50 use crate::parser::ast::*;
51 use cypherlite_core::{DatabaseConfig, SyncMode};
52 use cypherlite_storage::StorageEngine;
53 use tempfile::tempdir;
54
55 fn test_engine(dir: &std::path::Path) -> StorageEngine {
56 let config = DatabaseConfig {
57 path: dir.join("test.cyl"),
58 wal_sync_mode: SyncMode::Normal,
59 ..Default::default()
60 };
61 StorageEngine::open(config).expect("open")
62 }
63
64 #[test]
66 fn test_unwind_list_produces_rows() {
67 let dir = tempdir().expect("tempdir");
68 let engine = test_engine(dir.path());
69
70 let mut record = Record::new();
71 record.insert("data".to_string(), Value::Int64(42));
72
73 let expr = Expression::ListLiteral(vec![
74 Expression::Literal(Literal::Integer(1)),
75 Expression::Literal(Literal::Integer(2)),
76 Expression::Literal(Literal::Integer(3)),
77 ]);
78
79 let params = Params::new();
80 let result = execute_unwind(vec![record], &expr, "x", &engine, ¶ms, &());
81 let records = result.expect("should succeed");
82
83 assert_eq!(records.len(), 3);
84 assert_eq!(records[0].get("x"), Some(&Value::Int64(1)));
85 assert_eq!(records[1].get("x"), Some(&Value::Int64(2)));
86 assert_eq!(records[2].get("x"), Some(&Value::Int64(3)));
87 assert_eq!(records[0].get("data"), Some(&Value::Int64(42)));
89 assert_eq!(records[1].get("data"), Some(&Value::Int64(42)));
90 assert_eq!(records[2].get("data"), Some(&Value::Int64(42)));
91 }
92
93 #[test]
95 fn test_unwind_multiple_source_records() {
96 let dir = tempdir().expect("tempdir");
97 let engine = test_engine(dir.path());
98
99 let mut r1 = Record::new();
100 r1.insert("name".to_string(), Value::String("Alice".into()));
101
102 let mut r2 = Record::new();
103 r2.insert("name".to_string(), Value::String("Bob".into()));
104
105 let expr = Expression::ListLiteral(vec![
106 Expression::Literal(Literal::Integer(1)),
107 Expression::Literal(Literal::Integer(2)),
108 ]);
109
110 let params = Params::new();
111 let result = execute_unwind(vec![r1, r2], &expr, "x", &engine, ¶ms, &());
112 let records = result.expect("should succeed");
113
114 assert_eq!(records.len(), 4);
116 assert_eq!(records[0].get("name"), Some(&Value::String("Alice".into())));
117 assert_eq!(records[0].get("x"), Some(&Value::Int64(1)));
118 assert_eq!(records[1].get("name"), Some(&Value::String("Alice".into())));
119 assert_eq!(records[1].get("x"), Some(&Value::Int64(2)));
120 assert_eq!(records[2].get("name"), Some(&Value::String("Bob".into())));
121 assert_eq!(records[2].get("x"), Some(&Value::Int64(1)));
122 assert_eq!(records[3].get("name"), Some(&Value::String("Bob".into())));
123 assert_eq!(records[3].get("x"), Some(&Value::Int64(2)));
124 }
125
126 #[test]
128 fn test_unwind_empty_list_produces_zero_rows() {
129 let dir = tempdir().expect("tempdir");
130 let engine = test_engine(dir.path());
131
132 let mut record = Record::new();
133 record.insert("data".to_string(), Value::Int64(42));
134
135 let expr = Expression::ListLiteral(vec![]);
136
137 let params = Params::new();
138 let result = execute_unwind(vec![record], &expr, "x", &engine, ¶ms, &());
139 let records = result.expect("should succeed");
140
141 assert!(records.is_empty());
142 }
143
144 #[test]
146 fn test_unwind_null_produces_zero_rows() {
147 let dir = tempdir().expect("tempdir");
148 let engine = test_engine(dir.path());
149
150 let mut record = Record::new();
151 record.insert("data".to_string(), Value::Int64(42));
152
153 let expr = Expression::Literal(Literal::Null);
154
155 let params = Params::new();
156 let result = execute_unwind(vec![record], &expr, "x", &engine, ¶ms, &());
157 let records = result.expect("should succeed");
158
159 assert!(records.is_empty());
160 }
161
162 #[test]
164 fn test_unwind_non_list_returns_error() {
165 let dir = tempdir().expect("tempdir");
166 let engine = test_engine(dir.path());
167
168 let record = Record::new();
169 let expr = Expression::Literal(Literal::Integer(42));
170
171 let params = Params::new();
172 let result = execute_unwind(vec![record], &expr, "x", &engine, ¶ms, &());
173
174 assert!(result.is_err());
175 let err = result.unwrap_err();
176 assert!(
177 err.message.contains("expected a list or null"),
178 "expected list error, got: {}",
179 err.message
180 );
181 }
182
183 #[test]
185 fn test_unwind_string_returns_error() {
186 let dir = tempdir().expect("tempdir");
187 let engine = test_engine(dir.path());
188
189 let record = Record::new();
190 let expr = Expression::Literal(Literal::String("not a list".into()));
191
192 let params = Params::new();
193 let result = execute_unwind(vec![record], &expr, "x", &engine, ¶ms, &());
194
195 assert!(result.is_err());
196 }
197
198 #[test]
200 fn test_unwind_empty_source() {
201 let dir = tempdir().expect("tempdir");
202 let engine = test_engine(dir.path());
203
204 let expr = Expression::ListLiteral(vec![Expression::Literal(Literal::Integer(1))]);
205
206 let params = Params::new();
207 let result = execute_unwind(vec![], &expr, "x", &engine, ¶ms, &());
208 let records = result.expect("should succeed");
209
210 assert!(records.is_empty());
211 }
212
213 #[test]
215 fn test_unwind_variable_reference() {
216 let dir = tempdir().expect("tempdir");
217 let engine = test_engine(dir.path());
218
219 let mut record = Record::new();
220 record.insert(
221 "items".to_string(),
222 Value::List(vec![Value::String("a".into()), Value::String("b".into())]),
223 );
224
225 let expr = Expression::Variable("items".to_string());
226
227 let params = Params::new();
228 let result = execute_unwind(vec![record], &expr, "x", &engine, ¶ms, &());
229 let records = result.expect("should succeed");
230
231 assert_eq!(records.len(), 2);
232 assert_eq!(records[0].get("x"), Some(&Value::String("a".into())));
233 assert_eq!(records[1].get("x"), Some(&Value::String("b".into())));
234 }
235}