Skip to main content

cypherlite_query/executor/operators/
project.rs

1// ProjectOp: evaluates RETURN expressions, applies column aliases
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/// Project specific expressions from source records.
9/// Each ReturnItem is evaluated and given a column name (alias or expression text).
10pub fn execute_project(
11    source_records: Vec<Record>,
12    items: &[ReturnItem],
13    engine: &StorageEngine,
14    params: &Params,
15    scalar_fns: &dyn ScalarFnLookup,
16) -> Result<Vec<Record>, ExecutionError> {
17    let mut results = Vec::new();
18
19    for record in &source_records {
20        let mut projected = Record::new();
21
22        for item in items {
23            let value = eval(&item.expr, record, engine, params, scalar_fns)?;
24            let column_name = match &item.alias {
25                Some(alias) => alias.clone(),
26                None => expr_display_name(&item.expr),
27            };
28            projected.insert(column_name, value);
29        }
30
31        results.push(projected);
32    }
33
34    Ok(results)
35}
36
37/// Generate a display name for an expression (used when no alias is provided).
38///
39/// For property access (e.g., `n.name`), returns "n.name" instead of "expr".
40fn expr_display_name(expr: &Expression) -> String {
41    match expr {
42        Expression::Variable(name) => name.clone(),
43        Expression::Property(inner, prop) => {
44            format!("{}.{}", expr_display_name(inner), prop)
45        }
46        Expression::CountStar => "count(*)".to_string(),
47        _ => "expr".to_string(),
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use crate::executor::Value;
55    use crate::parser::ast::*;
56    use cypherlite_core::{DatabaseConfig, LabelRegistry, NodeId, SyncMode};
57    use cypherlite_storage::StorageEngine;
58    use tempfile::tempdir;
59
60    fn test_engine(dir: &std::path::Path) -> StorageEngine {
61        let config = DatabaseConfig {
62            path: dir.join("test.cyl"),
63            wal_sync_mode: SyncMode::Normal,
64            ..Default::default()
65        };
66        StorageEngine::open(config).expect("open")
67    }
68
69    // EXEC-T004: ProjectOp column rename (RETURN AS alias)
70    #[test]
71    fn test_project_with_alias() {
72        let dir = tempdir().expect("tempdir");
73        let mut engine = test_engine(dir.path());
74
75        let name_key = engine.get_or_create_prop_key("name");
76        let nid = engine.create_node(
77            vec![],
78            vec![(
79                name_key,
80                cypherlite_core::PropertyValue::String("Alice".into()),
81            )],
82        );
83
84        let mut record = Record::new();
85        record.insert("n".to_string(), Value::Node(nid));
86
87        let items = vec![ReturnItem {
88            expr: Expression::Property(
89                Box::new(Expression::Variable("n".to_string())),
90                "name".to_string(),
91            ),
92            alias: Some("person_name".to_string()),
93        }];
94
95        let params = Params::new();
96        let result = execute_project(vec![record], &items, &engine, &params, &());
97        let records = result.expect("should succeed");
98        assert_eq!(records.len(), 1);
99        assert_eq!(
100            records[0].get("person_name"),
101            Some(&Value::String("Alice".into()))
102        );
103    }
104
105    #[test]
106    fn test_project_without_alias_uses_variable_name() {
107        let dir = tempdir().expect("tempdir");
108        let engine = test_engine(dir.path());
109
110        let mut record = Record::new();
111        record.insert("n".to_string(), Value::Node(NodeId(1)));
112
113        let items = vec![ReturnItem {
114            expr: Expression::Variable("n".to_string()),
115            alias: None,
116        }];
117
118        let params = Params::new();
119        let result = execute_project(vec![record], &items, &engine, &params, &());
120        let records = result.expect("should succeed");
121        assert_eq!(records.len(), 1);
122        assert!(records[0].contains_key("n"));
123    }
124
125    #[test]
126    fn test_project_multiple_columns() {
127        let dir = tempdir().expect("tempdir");
128        let engine = test_engine(dir.path());
129
130        let mut record = Record::new();
131        record.insert("x".to_string(), Value::Int64(1));
132        record.insert("y".to_string(), Value::Int64(2));
133
134        let items = vec![
135            ReturnItem {
136                expr: Expression::Variable("x".to_string()),
137                alias: None,
138            },
139            ReturnItem {
140                expr: Expression::Variable("y".to_string()),
141                alias: Some("val".to_string()),
142            },
143        ];
144
145        let params = Params::new();
146        let result = execute_project(vec![record], &items, &engine, &params, &());
147        let records = result.expect("should succeed");
148        assert_eq!(records.len(), 1);
149        assert_eq!(records[0].get("x"), Some(&Value::Int64(1)));
150        assert_eq!(records[0].get("val"), Some(&Value::Int64(2)));
151    }
152}