Skip to main content

cynos_query/planner/
logical.rs

1//! Logical query plan definitions.
2
3use crate::ast::JoinType;
4use crate::ast::{AggregateFunc, Expr, SortOrder};
5use alloc::boxed::Box;
6use alloc::string::String;
7use alloc::vec::Vec;
8use cynos_core::Value;
9
10/// Logical query plan node.
11#[derive(Clone, Debug)]
12pub enum LogicalPlan {
13    /// Table scan.
14    Scan { table: String },
15
16    /// Index scan with a key range.
17    IndexScan {
18        table: String,
19        index: String,
20        range_start: Option<Value>,
21        range_end: Option<Value>,
22        include_start: bool,
23        include_end: bool,
24    },
25
26    /// Index point lookup.
27    IndexGet {
28        table: String,
29        index: String,
30        key: Value,
31    },
32
33    /// Index multi-point lookup (for IN queries).
34    /// Performs multiple index lookups and unions the results.
35    IndexInGet {
36        table: String,
37        index: String,
38        keys: Vec<Value>,
39    },
40
41    /// GIN index scan for JSONB queries.
42    GinIndexScan {
43        table: String,
44        index: String,
45        /// The JSONB column being queried.
46        column: String,
47        /// The column index in the table schema.
48        column_index: usize,
49        /// The JSON path being queried (e.g., "$.city").
50        path: String,
51        /// The value to match (for equality queries).
52        value: Option<Value>,
53        /// Query type: "eq", "contains", or "exists".
54        query_type: String,
55    },
56
57    /// GIN index scan for multiple JSONB predicates (AND combination).
58    /// More efficient than multiple single GIN scans followed by intersection.
59    GinIndexScanMulti {
60        table: String,
61        index: String,
62        /// The JSONB column being queried.
63        column: String,
64        /// Multiple (path, value) pairs to match (all must match - AND semantics).
65        pairs: Vec<(String, Value)>,
66    },
67
68    /// Filter (WHERE clause).
69    Filter {
70        input: Box<LogicalPlan>,
71        predicate: Expr,
72    },
73
74    /// Projection (SELECT columns).
75    Project {
76        input: Box<LogicalPlan>,
77        columns: Vec<Expr>,
78    },
79
80    /// Join two relations.
81    Join {
82        left: Box<LogicalPlan>,
83        right: Box<LogicalPlan>,
84        condition: Expr,
85        join_type: JoinType,
86    },
87
88    /// Aggregation (GROUP BY).
89    Aggregate {
90        input: Box<LogicalPlan>,
91        group_by: Vec<Expr>,
92        aggregates: Vec<(AggregateFunc, Expr)>,
93    },
94
95    /// Sort (ORDER BY).
96    Sort {
97        input: Box<LogicalPlan>,
98        order_by: Vec<(Expr, SortOrder)>,
99    },
100
101    /// Limit and offset.
102    Limit {
103        input: Box<LogicalPlan>,
104        limit: usize,
105        offset: usize,
106    },
107
108    /// Cross product (cartesian join).
109    CrossProduct {
110        left: Box<LogicalPlan>,
111        right: Box<LogicalPlan>,
112    },
113
114    /// Union of two relations.
115    Union {
116        left: Box<LogicalPlan>,
117        right: Box<LogicalPlan>,
118        all: bool,
119    },
120
121    /// Empty relation.
122    Empty,
123}
124
125impl LogicalPlan {
126    /// Creates a table scan plan.
127    pub fn scan(table: impl Into<String>) -> Self {
128        LogicalPlan::Scan {
129            table: table.into(),
130        }
131    }
132
133    /// Creates a filter plan.
134    pub fn filter(input: LogicalPlan, predicate: Expr) -> Self {
135        LogicalPlan::Filter {
136            input: Box::new(input),
137            predicate,
138        }
139    }
140
141    /// Creates a projection plan.
142    pub fn project(input: LogicalPlan, columns: Vec<Expr>) -> Self {
143        LogicalPlan::Project {
144            input: Box::new(input),
145            columns,
146        }
147    }
148
149    /// Creates a join plan.
150    pub fn join(
151        left: LogicalPlan,
152        right: LogicalPlan,
153        condition: Expr,
154        join_type: JoinType,
155    ) -> Self {
156        LogicalPlan::Join {
157            left: Box::new(left),
158            right: Box::new(right),
159            condition,
160            join_type,
161        }
162    }
163
164    /// Creates an inner join plan.
165    pub fn inner_join(left: LogicalPlan, right: LogicalPlan, condition: Expr) -> Self {
166        Self::join(left, right, condition, JoinType::Inner)
167    }
168
169    /// Creates a left outer join plan.
170    pub fn left_join(left: LogicalPlan, right: LogicalPlan, condition: Expr) -> Self {
171        Self::join(left, right, condition, JoinType::LeftOuter)
172    }
173
174    /// Creates an aggregation plan.
175    pub fn aggregate(
176        input: LogicalPlan,
177        group_by: Vec<Expr>,
178        aggregates: Vec<(AggregateFunc, Expr)>,
179    ) -> Self {
180        LogicalPlan::Aggregate {
181            input: Box::new(input),
182            group_by,
183            aggregates,
184        }
185    }
186
187    /// Creates a sort plan.
188    pub fn sort(input: LogicalPlan, order_by: Vec<(Expr, SortOrder)>) -> Self {
189        LogicalPlan::Sort {
190            input: Box::new(input),
191            order_by,
192        }
193    }
194
195    /// Creates a limit plan.
196    pub fn limit(input: LogicalPlan, limit: usize, offset: usize) -> Self {
197        LogicalPlan::Limit {
198            input: Box::new(input),
199            limit,
200            offset,
201        }
202    }
203
204    /// Creates a cross product plan.
205    pub fn cross_product(left: LogicalPlan, right: LogicalPlan) -> Self {
206        LogicalPlan::CrossProduct {
207            left: Box::new(left),
208            right: Box::new(right),
209        }
210    }
211
212    /// Returns the input plan(s) of this node.
213    pub fn inputs(&self) -> Vec<&LogicalPlan> {
214        match self {
215            LogicalPlan::Scan { .. }
216            | LogicalPlan::IndexScan { .. }
217            | LogicalPlan::IndexGet { .. }
218            | LogicalPlan::IndexInGet { .. }
219            | LogicalPlan::GinIndexScan { .. }
220            | LogicalPlan::GinIndexScanMulti { .. }
221            | LogicalPlan::Empty => alloc::vec![],
222            LogicalPlan::Filter { input, .. }
223            | LogicalPlan::Project { input, .. }
224            | LogicalPlan::Aggregate { input, .. }
225            | LogicalPlan::Sort { input, .. }
226            | LogicalPlan::Limit { input, .. } => alloc::vec![input.as_ref()],
227            LogicalPlan::Join { left, right, .. }
228            | LogicalPlan::CrossProduct { left, right }
229            | LogicalPlan::Union { left, right, .. } => alloc::vec![left.as_ref(), right.as_ref()],
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::ast::Expr;
238
239    #[test]
240    fn test_logical_plan_builders() {
241        let scan = LogicalPlan::scan("users");
242        assert!(matches!(scan, LogicalPlan::Scan { table } if table == "users"));
243
244        let filter = LogicalPlan::filter(
245            LogicalPlan::scan("users"),
246            Expr::eq(Expr::column("users", "id", 0), Expr::literal(1i64)),
247        );
248        assert!(matches!(filter, LogicalPlan::Filter { .. }));
249
250        let project = LogicalPlan::project(
251            LogicalPlan::scan("users"),
252            alloc::vec![Expr::column("users", "name", 1)],
253        );
254        assert!(matches!(project, LogicalPlan::Project { .. }));
255    }
256
257    #[test]
258    fn test_logical_plan_inputs() {
259        let scan = LogicalPlan::scan("users");
260        assert!(scan.inputs().is_empty());
261
262        let filter = LogicalPlan::filter(
263            LogicalPlan::scan("users"),
264            Expr::eq(Expr::column("users", "id", 0), Expr::literal(1i64)),
265        );
266        assert_eq!(filter.inputs().len(), 1);
267
268        let join = LogicalPlan::inner_join(
269            LogicalPlan::scan("a"),
270            LogicalPlan::scan("b"),
271            Expr::eq(Expr::column("a", "id", 0), Expr::column("b", "a_id", 0)),
272        );
273        assert_eq!(join.inputs().len(), 2);
274    }
275}