cypherlite_query/executor/operators/
project.rs1use crate::executor::eval::eval;
4use crate::executor::{ExecutionError, Params, Record, ScalarFnLookup};
5use crate::parser::ast::{Expression, ReturnItem};
6use cypherlite_storage::StorageEngine;
7
8pub 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
37fn 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 #[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, ¶ms, &());
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, ¶ms, &());
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, ¶ms, &());
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}