1use kyu_binder::BoundStatement;
4use kyu_common::{KyuError, KyuResult};
5use kyu_planner::LogicalPlan;
6use kyu_types::LogicalType;
7use smol_str::SmolStr;
8
9use crate::context::ExecutionContext;
10use crate::mapper::map_plan;
11use crate::result::QueryResult;
12
13pub fn execute(
15 plan: &LogicalPlan,
16 output_schema: &[(SmolStr, LogicalType)],
17 ctx: &ExecutionContext<'_>,
18) -> KyuResult<QueryResult> {
19 let mut physical = map_plan(plan)?;
20
21 let column_names: Vec<SmolStr> = output_schema.iter().map(|(n, _)| n.clone()).collect();
22 let column_types: Vec<LogicalType> = output_schema.iter().map(|(_, t)| t.clone()).collect();
23 let mut result = QueryResult::new(column_names, column_types);
24
25 while let Some(chunk) = physical.next(ctx)? {
26 result.push_chunk(&chunk);
27 }
28
29 Ok(result)
30}
31
32pub fn execute_statement(
34 stmt: &BoundStatement,
35 ctx: &ExecutionContext<'_>,
36) -> KyuResult<QueryResult> {
37 match stmt {
38 BoundStatement::Query(query) => {
39 let plan = kyu_planner::build_query_plan(query, &ctx.catalog)?;
40 execute(&plan, &query.output_schema, ctx)
41 }
42 _ => Err(KyuError::NotImplemented(
43 "non-query execution not yet supported".into(),
44 )),
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51 use crate::context::MockStorage;
52 use kyu_binder::Binder;
53 use kyu_catalog::{CatalogContent, NodeTableEntry, Property, RelTableEntry};
54 use kyu_common::id::{PropertyId, TableId};
55 use kyu_expression::FunctionRegistry;
56 use kyu_types::TypedValue;
57
58 fn make_catalog() -> CatalogContent {
59 let mut catalog = CatalogContent::new();
60 catalog
61 .add_node_table(NodeTableEntry {
62 table_id: TableId(0),
63 name: SmolStr::new("Person"),
64 properties: vec![
65 Property::new(PropertyId(0), "name", LogicalType::String, true),
66 Property::new(PropertyId(1), "age", LogicalType::Int64, false),
67 ],
68 primary_key_idx: 0,
69 num_rows: 0,
70 comment: None,
71 })
72 .unwrap();
73 catalog
74 .add_rel_table(RelTableEntry {
75 table_id: TableId(1),
76 name: SmolStr::new("KNOWS"),
77 from_table_id: TableId(0),
78 to_table_id: TableId(0),
79 properties: vec![Property::new(
80 PropertyId(2),
81 "since",
82 LogicalType::Int64,
83 false,
84 )],
85 num_rows: 0,
86 comment: None,
87 })
88 .unwrap();
89 catalog
90 }
91
92 fn make_storage() -> MockStorage {
93 let mut storage = MockStorage::new();
94 storage.insert_table(
96 TableId(0),
97 vec![
98 vec![
99 TypedValue::String(SmolStr::new("Alice")),
100 TypedValue::Int64(25),
101 ],
102 vec![
103 TypedValue::String(SmolStr::new("Bob")),
104 TypedValue::Int64(30),
105 ],
106 vec![
107 TypedValue::String(SmolStr::new("Charlie")),
108 TypedValue::Int64(35),
109 ],
110 vec![
111 TypedValue::String(SmolStr::new("Diana")),
112 TypedValue::Int64(28),
113 ],
114 ],
115 );
116 storage.insert_table(
118 TableId(1),
119 vec![
120 vec![
121 TypedValue::String(SmolStr::new("Alice")),
122 TypedValue::String(SmolStr::new("Bob")),
123 TypedValue::Int64(2020),
124 ],
125 vec![
126 TypedValue::String(SmolStr::new("Bob")),
127 TypedValue::String(SmolStr::new("Charlie")),
128 TypedValue::Int64(2021),
129 ],
130 ],
131 );
132 storage
133 }
134
135 fn run_query(cypher: &str) -> KyuResult<QueryResult> {
136 let catalog = make_catalog();
137 let storage = make_storage();
138 let ctx = ExecutionContext::new(catalog.clone(), &storage);
139
140 let parse_result = kyu_parser::parse(cypher);
141 let stmt = parse_result
142 .ast
143 .ok_or_else(|| KyuError::Binder(format!("parse failed: {:?}", parse_result.errors)))?;
144 let mut binder = Binder::new(catalog, FunctionRegistry::with_builtins());
145 let bound = binder.bind(&stmt)?;
146 execute_statement(&bound, &ctx)
147 }
148
149 #[test]
150 fn return_literal() {
151 let result = run_query("RETURN 1 AS x").unwrap();
152 assert_eq!(result.num_rows(), 1);
153 assert_eq!(result.row(0), vec![TypedValue::Int64(1)]);
154 }
155
156 #[test]
157 fn return_arithmetic() {
158 let result = run_query("RETURN 1 + 2 AS sum").unwrap();
159 assert_eq!(result.num_rows(), 1);
160 assert_eq!(result.row(0), vec![TypedValue::Int64(3)]);
161 }
162
163 #[test]
164 fn return_multiple_columns() {
165 let result = run_query("RETURN 'hello' AS greeting, 42 AS answer").unwrap();
166 assert_eq!(result.num_rows(), 1);
167 assert_eq!(result.num_columns(), 2);
168 assert_eq!(result.row(0)[0], TypedValue::String(SmolStr::new("hello")));
169 assert_eq!(result.row(0)[1], TypedValue::Int64(42));
170 }
171
172 #[test]
173 fn return_null_is_null() {
174 let result = run_query("RETURN null IS NULL AS t").unwrap();
175 assert_eq!(result.num_rows(), 1);
176 assert_eq!(result.row(0), vec![TypedValue::Bool(true)]);
177 }
178
179 #[test]
180 fn return_case_expression() {
181 let result = run_query("RETURN CASE WHEN true THEN 'yes' ELSE 'no' END AS v").unwrap();
182 assert_eq!(result.num_rows(), 1);
183 assert_eq!(result.row(0), vec![TypedValue::String(SmolStr::new("yes"))]);
184 }
185
186 #[test]
187 fn unwind_list() {
188 let result = run_query("UNWIND [1, 2, 3] AS x RETURN x").unwrap();
189 assert_eq!(result.num_rows(), 3);
190 assert_eq!(result.row(0), vec![TypedValue::Int64(1)]);
191 assert_eq!(result.row(1), vec![TypedValue::Int64(2)]);
192 assert_eq!(result.row(2), vec![TypedValue::Int64(3)]);
193 }
194
195 #[test]
196 fn recursive_join_1_hop() {
197 let result =
199 run_query("MATCH (a:Person)-[:KNOWS*1..1]->(b:Person) RETURN a.name, b.name").unwrap();
200 assert_eq!(result.num_rows(), 2);
202 assert_eq!(result.num_columns(), 2);
203 }
204
205 #[test]
206 fn recursive_join_multi_hop() {
207 let result =
209 run_query("MATCH (a:Person)-[:KNOWS*1..2]->(b:Person) RETURN a.name, b.name").unwrap();
210 assert_eq!(result.num_rows(), 3);
220 }
221
222 #[test]
223 fn recursive_join_count() {
224 let result =
225 run_query("MATCH (a:Person)-[:KNOWS*1..1]->(b:Person) RETURN count(*) AS cnt").unwrap();
226 assert_eq!(result.num_rows(), 1);
227 assert_eq!(result.row(0), vec![TypedValue::Int64(2)]);
228 }
229}