Skip to main content

cypherlite_query/executor/operators/
with.rs

1// WithOp: intermediate projection for WITH clause (scope reset)
2
3use crate::executor::eval::eval;
4use crate::executor::{ExecutionError, Params, Record, ScalarFnLookup};
5use crate::parser::ast::{Expression, ReturnItem};
6use cypherlite_storage::StorageEngine;
7
8/// Execute WITH projection: evaluate each ReturnItem and build new records
9/// with only the projected columns. This is similar to ProjectOp but serves
10/// as an intermediate step rather than final output.
11pub fn execute_with(
12    source_records: Vec<Record>,
13    items: &[ReturnItem],
14    engine: &StorageEngine,
15    params: &Params,
16    scalar_fns: &dyn ScalarFnLookup,
17) -> Result<Vec<Record>, ExecutionError> {
18    let mut results = Vec::new();
19
20    for record in &source_records {
21        let mut projected = Record::new();
22
23        for item in items {
24            let value = eval(&item.expr, record, engine, params, scalar_fns)?;
25            let column_name = match &item.alias {
26                Some(alias) => alias.clone(),
27                None => expr_display_name(&item.expr),
28            };
29            projected.insert(column_name, value);
30        }
31
32        results.push(projected);
33    }
34
35    Ok(results)
36}
37
38/// Generate a display name for an expression (used when no alias is provided).
39fn expr_display_name(expr: &Expression) -> String {
40    match expr {
41        Expression::Variable(name) => name.clone(),
42        Expression::Property(inner, prop) => {
43            format!("{}.{}", expr_display_name(inner), prop)
44        }
45        Expression::CountStar => "count(*)".to_string(),
46        _ => "expr".to_string(),
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::executor::Value;
54    use crate::parser::ast::*;
55    use cypherlite_core::{DatabaseConfig, LabelRegistry, SyncMode};
56    use cypherlite_storage::StorageEngine;
57    use tempfile::tempdir;
58
59    fn test_engine(dir: &std::path::Path) -> StorageEngine {
60        let config = DatabaseConfig {
61            path: dir.join("test.cyl"),
62            wal_sync_mode: SyncMode::Normal,
63            ..Default::default()
64        };
65        StorageEngine::open(config).expect("open")
66    }
67
68    // TASK-062: WithOp projects only specified columns
69    #[test]
70    fn test_with_projects_specified_columns() {
71        let dir = tempdir().expect("tempdir");
72        let engine = test_engine(dir.path());
73
74        let mut record = Record::new();
75        record.insert("x".to_string(), Value::Int64(1));
76        record.insert("y".to_string(), Value::Int64(2));
77        record.insert("z".to_string(), Value::Int64(3));
78
79        // WITH x -- only 'x' survives
80        let items = vec![ReturnItem {
81            expr: Expression::Variable("x".to_string()),
82            alias: None,
83        }];
84
85        let params = Params::new();
86        let result = execute_with(vec![record], &items, &engine, &params, &());
87        let records = result.expect("should succeed");
88        assert_eq!(records.len(), 1);
89        assert_eq!(records[0].get("x"), Some(&Value::Int64(1)));
90        assert!(!records[0].contains_key("y"));
91        assert!(!records[0].contains_key("z"));
92    }
93
94    // TASK-062: WITH with alias
95    #[test]
96    fn test_with_alias_renames_column() {
97        let dir = tempdir().expect("tempdir");
98        let mut engine = test_engine(dir.path());
99
100        let name_key = engine.get_or_create_prop_key("name");
101        let nid = engine.create_node(
102            vec![],
103            vec![(
104                name_key,
105                cypherlite_core::PropertyValue::String("Alice".into()),
106            )],
107        );
108
109        let mut record = Record::new();
110        record.insert("n".to_string(), Value::Node(nid));
111
112        // WITH n.name AS person_name
113        let items = vec![ReturnItem {
114            expr: Expression::Property(
115                Box::new(Expression::Variable("n".to_string())),
116                "name".to_string(),
117            ),
118            alias: Some("person_name".to_string()),
119        }];
120
121        let params = Params::new();
122        let result = execute_with(vec![record], &items, &engine, &params, &());
123        let records = result.expect("should succeed");
124        assert_eq!(records.len(), 1);
125        assert_eq!(
126            records[0].get("person_name"),
127            Some(&Value::String("Alice".into()))
128        );
129        assert!(!records[0].contains_key("n"));
130    }
131
132    // TASK-062: WITH multiple items
133    #[test]
134    fn test_with_multiple_items() {
135        let dir = tempdir().expect("tempdir");
136        let engine = test_engine(dir.path());
137
138        let mut record = Record::new();
139        record.insert("a".to_string(), Value::Int64(10));
140        record.insert("b".to_string(), Value::Int64(20));
141        record.insert("c".to_string(), Value::Int64(30));
142
143        // WITH a, b
144        let items = vec![
145            ReturnItem {
146                expr: Expression::Variable("a".to_string()),
147                alias: None,
148            },
149            ReturnItem {
150                expr: Expression::Variable("b".to_string()),
151                alias: None,
152            },
153        ];
154
155        let params = Params::new();
156        let result = execute_with(vec![record], &items, &engine, &params, &());
157        let records = result.expect("should succeed");
158        assert_eq!(records.len(), 1);
159        assert_eq!(records[0].get("a"), Some(&Value::Int64(10)));
160        assert_eq!(records[0].get("b"), Some(&Value::Int64(20)));
161        assert!(!records[0].contains_key("c"));
162    }
163
164    // TASK-064: WITH DISTINCT deduplication (tested at higher level via executor dispatch)
165    #[test]
166    fn test_with_produces_duplicates_for_distinct_to_handle() {
167        let dir = tempdir().expect("tempdir");
168        let engine = test_engine(dir.path());
169
170        let mut r1 = Record::new();
171        r1.insert("x".to_string(), Value::String("A".into()));
172        r1.insert("y".to_string(), Value::Int64(1));
173        let mut r2 = Record::new();
174        r2.insert("x".to_string(), Value::String("A".into()));
175        r2.insert("y".to_string(), Value::Int64(2));
176
177        // WITH x -- both records have x="A", so after projection they are duplicates
178        let items = vec![ReturnItem {
179            expr: Expression::Variable("x".to_string()),
180            alias: None,
181        }];
182
183        let params = Params::new();
184        let result = execute_with(vec![r1, r2], &items, &engine, &params, &());
185        let records = result.expect("should succeed");
186        // Without DISTINCT, duplicates are preserved
187        assert_eq!(records.len(), 2);
188        assert_eq!(records[0].get("x"), Some(&Value::String("A".into())));
189        assert_eq!(records[1].get("x"), Some(&Value::String("A".into())));
190    }
191
192    // TASK-062: empty input produces empty output
193    #[test]
194    fn test_with_empty_input() {
195        let dir = tempdir().expect("tempdir");
196        let engine = test_engine(dir.path());
197
198        let items = vec![ReturnItem {
199            expr: Expression::Variable("x".to_string()),
200            alias: None,
201        }];
202
203        let params = Params::new();
204        let result = execute_with(vec![], &items, &engine, &params, &());
205        let records = result.expect("should succeed");
206        assert!(records.is_empty());
207    }
208}