1use crate::context::ExecutionContext;
38use crate::optimizer::{
39 AndPredicatePass, CrossProductPass, ImplicitJoinsPass, IndexSelection, JoinReorder,
40 LimitSkipByIndexPass, NotSimplification, OptimizerPass, OrderByIndexPass,
41 OuterJoinSimplification, PredicatePushdown, TopNPushdown,
42};
43use crate::planner::{LogicalPlan, PhysicalPlan};
44use alloc::boxed::Box;
45use alloc::vec::Vec;
46
47pub struct QueryPlanner {
53 ctx: ExecutionContext,
54 logical_passes: Vec<Box<dyn OptimizerPass>>,
56}
57
58impl QueryPlanner {
59 pub fn new(ctx: ExecutionContext) -> Self {
67 Self {
68 ctx,
69 logical_passes: alloc::vec![
70 Box::new(NotSimplification),
71 Box::new(AndPredicatePass),
72 Box::new(CrossProductPass),
73 Box::new(ImplicitJoinsPass),
74 Box::new(OuterJoinSimplification),
75 Box::new(PredicatePushdown),
76 Box::new(JoinReorder::new()),
77 ],
78 }
79 }
80
81 pub fn with_logical_passes(ctx: ExecutionContext, passes: Vec<Box<dyn OptimizerPass>>) -> Self {
86 Self {
87 ctx,
88 logical_passes: passes,
89 }
90 }
91
92 pub fn context(&self) -> &ExecutionContext {
94 &self.ctx
95 }
96
97 pub fn plan(&self, plan: LogicalPlan) -> PhysicalPlan {
105 let mut logical = plan;
107 for pass in &self.logical_passes {
108 logical = pass.optimize(logical);
109 }
110
111 let index_selection = IndexSelection::with_context(self.ctx.clone());
113 logical = index_selection.optimize(logical);
114
115 let mut physical = self.logical_to_physical(logical);
117
118 physical = TopNPushdown::new().optimize(physical);
121
122 physical = OrderByIndexPass::new(&self.ctx).optimize(physical);
124
125 physical = LimitSkipByIndexPass::new(&self.ctx).optimize(physical);
127
128 physical
129 }
130
131 pub fn optimize_logical(&self, plan: LogicalPlan) -> LogicalPlan {
135 let mut logical = plan;
136
137 for pass in &self.logical_passes {
139 logical = pass.optimize(logical);
140 }
141
142 let index_selection = IndexSelection::with_context(self.ctx.clone());
144 logical = index_selection.optimize(logical);
145
146 logical
147 }
148
149 pub fn to_physical(&self, plan: LogicalPlan) -> PhysicalPlan {
153 let mut physical = self.logical_to_physical(plan);
154
155 physical = TopNPushdown::new().optimize(physical);
157 physical = OrderByIndexPass::new(&self.ctx).optimize(physical);
158 physical = LimitSkipByIndexPass::new(&self.ctx).optimize(physical);
159
160 physical
161 }
162
163 fn logical_to_physical(&self, plan: LogicalPlan) -> PhysicalPlan {
165 use crate::planner::JoinAlgorithm;
166
167 match plan {
168 LogicalPlan::Scan { table } => PhysicalPlan::table_scan(table),
169
170 LogicalPlan::IndexScan {
171 table,
172 index,
173 range_start,
174 range_end,
175 include_start,
176 include_end,
177 } => PhysicalPlan::IndexScan {
178 table,
179 index,
180 range_start,
181 range_end,
182 include_start,
183 include_end,
184 limit: None,
185 offset: None,
186 reverse: false,
187 },
188
189 LogicalPlan::IndexGet { table, index, key } => {
190 PhysicalPlan::index_get(table, index, key)
191 }
192
193 LogicalPlan::IndexInGet { table, index, keys } => {
194 PhysicalPlan::index_in_get(table, index, keys)
195 }
196
197 LogicalPlan::GinIndexScan {
198 table,
199 index,
200 column: _,
201 column_index: _,
202 path,
203 value,
204 query_type,
205 } => {
206 let key: alloc::string::String = path.trim_start_matches("$.").into();
207 let value_str = value.map(|v| match v {
208 cynos_core::Value::String(s) => s,
209 cynos_core::Value::Int32(i) => alloc::format!("{}", i),
210 cynos_core::Value::Int64(i) => alloc::format!("{}", i),
211 cynos_core::Value::Float64(f) => alloc::format!("{}", f),
212 cynos_core::Value::Boolean(b) => alloc::format!("{}", b),
213 _ => alloc::format!("{:?}", v),
214 });
215 PhysicalPlan::gin_index_scan(table, index, key, value_str, query_type)
216 }
217
218 LogicalPlan::GinIndexScanMulti {
219 table,
220 index,
221 column: _,
222 pairs,
223 } => {
224 let string_pairs: Vec<(alloc::string::String, alloc::string::String)> = pairs
225 .into_iter()
226 .map(|(path, value)| {
227 let key: alloc::string::String = path.trim_start_matches("$.").into();
228 let value_str = match value {
229 cynos_core::Value::String(s) => s,
230 cynos_core::Value::Int32(i) => alloc::format!("{}", i),
231 cynos_core::Value::Int64(i) => alloc::format!("{}", i),
232 cynos_core::Value::Float64(f) => alloc::format!("{}", f),
233 cynos_core::Value::Boolean(b) => alloc::format!("{}", b),
234 _ => alloc::format!("{:?}", value),
235 };
236 (key, value_str)
237 })
238 .collect();
239 PhysicalPlan::gin_index_scan_multi(table, index, string_pairs)
240 }
241
242 LogicalPlan::Filter { input, predicate } => {
243 let input_physical = self.logical_to_physical(*input);
244 PhysicalPlan::filter(input_physical, predicate)
245 }
246
247 LogicalPlan::Project { input, columns } => {
248 let input_physical = self.logical_to_physical(*input);
249 PhysicalPlan::project(input_physical, columns)
250 }
251
252 LogicalPlan::Join {
253 left,
254 right,
255 condition,
256 join_type,
257 } => {
258 let left_physical = self.logical_to_physical(*left);
259 let right_physical = self.logical_to_physical(*right);
260 let algorithm = self.choose_join_algorithm(&condition);
261
262 match algorithm {
263 JoinAlgorithm::Hash => {
264 PhysicalPlan::hash_join(left_physical, right_physical, condition, join_type)
265 }
266 JoinAlgorithm::SortMerge => PhysicalPlan::sort_merge_join(
267 left_physical,
268 right_physical,
269 condition,
270 join_type,
271 ),
272 JoinAlgorithm::NestedLoop | JoinAlgorithm::IndexNestedLoop => {
273 PhysicalPlan::nested_loop_join(
274 left_physical,
275 right_physical,
276 condition,
277 join_type,
278 )
279 }
280 }
281 }
282
283 LogicalPlan::Aggregate {
284 input,
285 group_by,
286 aggregates,
287 } => {
288 let input_physical = self.logical_to_physical(*input);
289 PhysicalPlan::hash_aggregate(input_physical, group_by, aggregates)
290 }
291
292 LogicalPlan::Sort { input, order_by } => {
293 let input_physical = self.logical_to_physical(*input);
294 PhysicalPlan::sort(input_physical, order_by)
295 }
296
297 LogicalPlan::Limit {
298 input,
299 limit,
300 offset,
301 } => {
302 let input_physical = self.logical_to_physical(*input);
303 PhysicalPlan::limit(input_physical, limit, offset)
304 }
305
306 LogicalPlan::CrossProduct { left, right } => {
307 let left_physical = self.logical_to_physical(*left);
308 let right_physical = self.logical_to_physical(*right);
309 PhysicalPlan::CrossProduct {
310 left: Box::new(left_physical),
311 right: Box::new(right_physical),
312 }
313 }
314
315 LogicalPlan::Union { .. } => PhysicalPlan::Empty,
316
317 LogicalPlan::Empty => PhysicalPlan::Empty,
318 }
319 }
320
321 fn choose_join_algorithm(&self, condition: &crate::ast::Expr) -> crate::planner::JoinAlgorithm {
322 if condition.is_equi_join() {
323 return crate::planner::JoinAlgorithm::Hash;
324 }
325 if condition.is_range_join() {
326 return crate::planner::JoinAlgorithm::NestedLoop;
327 }
328 crate::planner::JoinAlgorithm::NestedLoop
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::ast::{Expr, SortOrder};
336 use crate::context::{IndexInfo, TableStats};
337
338 fn create_test_context() -> ExecutionContext {
339 let mut ctx = ExecutionContext::new();
340 ctx.register_table(
341 "users",
342 TableStats {
343 row_count: 1000,
344 is_sorted: false,
345 indexes: alloc::vec![
346 IndexInfo::new("idx_id", alloc::vec!["id".into()], true),
347 IndexInfo::new("idx_name", alloc::vec!["name".into()], false),
348 ],
349 },
350 );
351 ctx
352 }
353
354 #[test]
355 fn test_query_planner_basic() {
356 let ctx = create_test_context();
357 let planner = QueryPlanner::new(ctx);
358
359 let plan = LogicalPlan::scan("users");
360 let physical = planner.plan(plan);
361
362 assert!(matches!(physical, PhysicalPlan::TableScan { .. }));
363 }
364
365 #[test]
366 fn test_query_planner_index_selection() {
367 let ctx = create_test_context();
368 let planner = QueryPlanner::new(ctx);
369
370 let plan = LogicalPlan::filter(
372 LogicalPlan::scan("users"),
373 Expr::eq(Expr::column("users", "id", 0), Expr::literal(42i64)),
374 );
375
376 let physical = planner.plan(plan);
377
378 assert!(matches!(physical, PhysicalPlan::IndexGet { .. }));
380 }
381
382 #[test]
383 fn test_query_planner_order_by_index() {
384 let ctx = create_test_context();
385 let planner = QueryPlanner::new(ctx);
386
387 let plan = LogicalPlan::Sort {
389 input: Box::new(LogicalPlan::scan("users")),
390 order_by: alloc::vec![(Expr::column("users", "id", 0), SortOrder::Asc)],
391 };
392
393 let physical = planner.plan(plan);
394
395 assert!(matches!(physical, PhysicalPlan::IndexScan { .. }));
397 }
398
399 #[test]
400 fn test_query_planner_topn_pushdown() {
401 let ctx = create_test_context();
402 let planner = QueryPlanner::new(ctx);
403
404 let plan = LogicalPlan::Limit {
406 input: Box::new(LogicalPlan::Sort {
407 input: Box::new(LogicalPlan::scan("users")),
408 order_by: alloc::vec![(Expr::column("users", "id", 0), SortOrder::Desc)],
409 }),
410 limit: 10,
411 offset: 0,
412 };
413
414 let physical = planner.plan(plan);
415
416 match physical {
418 PhysicalPlan::IndexScan {
419 limit,
420 reverse,
421 ..
422 } => {
423 assert_eq!(limit, Some(10));
424 assert!(reverse);
425 }
426 _ => panic!("Expected IndexScan, got {:?}", physical),
427 }
428 }
429
430 #[test]
431 fn test_query_planner_optimize_logical() {
432 let ctx = create_test_context();
433 let planner = QueryPlanner::new(ctx);
434
435 let plan = LogicalPlan::filter(
436 LogicalPlan::scan("users"),
437 Expr::eq(Expr::column("users", "id", 0), Expr::literal(42i64)),
438 );
439
440 let optimized = planner.optimize_logical(plan);
441
442 assert!(matches!(optimized, LogicalPlan::IndexGet { .. }));
444 }
445}