Skip to main content

cynos_query/planner/
query_planner.rs

1//! Unified query planner with ExecutionContext support.
2//!
3//! This module provides a unified entry point for query planning that handles
4//! both logical and physical plan optimizations with proper ExecutionContext support.
5//!
6//! ## Architecture
7//!
8//! The query planning pipeline consists of:
9//!
10//! 1. **Logical Optimization** - Context-free transformations:
11//!    - NotSimplification
12//!    - AndPredicatePass
13//!    - CrossProductPass
14//!    - ImplicitJoinsPass
15//!    - OuterJoinSimplification
16//!    - PredicatePushdown
17//!    - JoinReorder
18//!
19//! 2. **Context-Aware Logical Optimization** - Requires ExecutionContext:
20//!    - IndexSelection (converts Filter+Scan to IndexScan/IndexGet)
21//!
22//! 3. **Physical Plan Conversion** - Converts logical to physical plan
23//!
24//! 4. **Physical Optimization** - Context-aware physical transformations:
25//!    - TopNPushdown (converts Sort+Limit to TopN)
26//!    - OrderByIndexPass (leverages indexes for sorting)
27//!    - LimitSkipByIndexPass (pushes limit/offset to IndexScan)
28//!
29//! ## Usage
30//!
31//! ```ignore
32//! let ctx = build_execution_context(&cache, "users");
33//! let planner = QueryPlanner::new(ctx);
34//! let physical_plan = planner.plan(logical_plan);
35//! ```
36
37use 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
47/// Unified query planner that handles the complete optimization pipeline.
48///
49/// Unlike the basic `Optimizer`, `QueryPlanner` supports `ExecutionContext`
50/// throughout the entire pipeline, enabling context-aware optimizations
51/// for both logical and physical plans.
52pub struct QueryPlanner {
53    ctx: ExecutionContext,
54    /// Logical optimization passes (context-free)
55    logical_passes: Vec<Box<dyn OptimizerPass>>,
56}
57
58impl QueryPlanner {
59    /// Creates a new QueryPlanner with the given execution context.
60    ///
61    /// The planner is initialized with default optimization passes:
62    /// - Logical: NotSimplification, AndPredicatePass, CrossProductPass,
63    ///   ImplicitJoinsPass, OuterJoinSimplification, PredicatePushdown, JoinReorder
64    /// - Context-aware logical: IndexSelection
65    /// - Physical: TopNPushdown, OrderByIndexPass, LimitSkipByIndexPass
66    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    /// Creates a QueryPlanner with custom logical passes.
82    ///
83    /// Context-aware passes (IndexSelection, OrderByIndexPass, etc.) are
84    /// still applied automatically using the provided context.
85    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    /// Returns a reference to the execution context.
93    pub fn context(&self) -> &ExecutionContext {
94        &self.ctx
95    }
96
97    /// Plans a logical query into an optimized physical plan.
98    ///
99    /// This is the main entry point that runs the complete optimization pipeline:
100    /// 1. Apply context-free logical optimizations
101    /// 2. Apply context-aware logical optimizations (IndexSelection)
102    /// 3. Convert to physical plan
103    /// 4. Apply physical optimizations (TopNPushdown, OrderByIndexPass, LimitSkipByIndexPass)
104    pub fn plan(&self, plan: LogicalPlan) -> PhysicalPlan {
105        // Phase 1: Context-free logical optimizations
106        let mut logical = plan;
107        for pass in &self.logical_passes {
108            logical = pass.optimize(logical);
109        }
110
111        // Phase 2: Context-aware logical optimizations
112        let index_selection = IndexSelection::with_context(self.ctx.clone());
113        logical = index_selection.optimize(logical);
114
115        // Phase 3: Convert to physical plan
116        let mut physical = self.logical_to_physical(logical);
117
118        // Phase 4: Physical optimizations
119        // TopNPushdown: Sort + Limit -> TopN
120        physical = TopNPushdown::new().optimize(physical);
121
122        // OrderByIndexPass: leverage indexes for sorting (needs context)
123        physical = OrderByIndexPass::new(&self.ctx).optimize(physical);
124
125        // LimitSkipByIndexPass: push limit/offset to IndexScan (needs context)
126        physical = LimitSkipByIndexPass::new(&self.ctx).optimize(physical);
127
128        physical
129    }
130
131    /// Optimizes only the logical plan without converting to physical.
132    ///
133    /// Useful for debugging or when you need to inspect the optimized logical plan.
134    pub fn optimize_logical(&self, plan: LogicalPlan) -> LogicalPlan {
135        let mut logical = plan;
136
137        // Context-free passes
138        for pass in &self.logical_passes {
139            logical = pass.optimize(logical);
140        }
141
142        // Context-aware passes
143        let index_selection = IndexSelection::with_context(self.ctx.clone());
144        logical = index_selection.optimize(logical);
145
146        logical
147    }
148
149    /// Converts a logical plan to physical and applies physical optimizations.
150    ///
151    /// Assumes the logical plan has already been optimized.
152    pub fn to_physical(&self, plan: LogicalPlan) -> PhysicalPlan {
153        let mut physical = self.logical_to_physical(plan);
154
155        // Physical optimizations
156        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    /// Converts a logical plan to a physical plan without optimizations.
164    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        // Filter: id = 42
371        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        // Should use IndexGet
379        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        // Sort by id ASC
388        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        // Should use IndexScan instead of Sort
396        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        // Sort by id DESC + Limit 10
405        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        // Should become IndexScan with limit and reverse
417        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        // Should convert to IndexGet
443        assert!(matches!(optimized, LogicalPlan::IndexGet { .. }));
444    }
445}