Skip to main content

kyu_executor/
execute.rs

1//! Execution driver — top-level entry point for query execution.
2
3use 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
13/// Execute a logical plan, collecting results into a `QueryResult`.
14pub 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
32/// Execute a bound statement end-to-end (plan + execute).
33pub 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        // Person table: name, age columns (matching catalog property order).
95        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        // KNOWS table: src_name, dst_name, since (simplified — no internal IDs for now).
117        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        // MATCH (a:Person)-[*1..1]->(b:Person) RETURN a.name, b.name
198        let result =
199            run_query("MATCH (a:Person)-[:KNOWS*1..1]->(b:Person) RETURN a.name, b.name").unwrap();
200        // Alice->Bob, Bob->Charlie (from mock storage)
201        assert_eq!(result.num_rows(), 2);
202        assert_eq!(result.num_columns(), 2);
203    }
204
205    #[test]
206    fn recursive_join_multi_hop() {
207        // *1..2 from Alice should reach Bob (1 hop) and Charlie (2 hops)
208        let result =
209            run_query("MATCH (a:Person)-[:KNOWS*1..2]->(b:Person) RETURN a.name, b.name").unwrap();
210        // 4 source nodes, each BFS expanding 1..2 hops:
211        // Alice: Bob(1), Charlie(2)
212        // Bob: Charlie(1)
213        // Charlie: (no outgoing in 2-row KNOWS table)
214        // Diana: (not in KNOWS src at all)
215        // But wait — KNOWS table has Alice->Bob, Bob->Charlie only.
216        // Alice: 1-hop=Bob, 2-hop=Charlie → 2 results
217        // Bob: 1-hop=Charlie → 1 result
218        // Total: 3 results
219        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}