Skip to main content

alopex_sql/planner/
logical_plan.rs

1//! Logical plan representation for query execution.
2//!
3//! This module defines [`LogicalPlan`], which represents the logical structure
4//! of a query after parsing and semantic analysis. The logical plan is used
5//! by the executor to produce query results.
6//!
7//! # Plan Structure
8//!
9//! Logical plans form a tree structure where:
10//! - Leaf nodes are typically scans or DDL operations
11//! - Internal nodes represent transformations (filter, sort, limit)
12//! - DML operations (insert, update, delete) are also represented
13//!
14//! # Examples
15//!
16//! ```
17//! use alopex_sql::planner::logical_plan::LogicalPlan;
18//! use alopex_sql::planner::{Projection, TypedExpr, TypedExprKind, SortExpr};
19//! use alopex_sql::planner::types::ResolvedType;
20//! use alopex_sql::Span;
21//!
22//! // SELECT * FROM users ORDER BY name LIMIT 10
23//! let scan = LogicalPlan::Scan {
24//!     table: "users".to_string(),
25//!     projection: Projection::All(vec!["id".to_string(), "name".to_string()]),
26//! };
27//!
28//! let sort = LogicalPlan::Sort {
29//!     input: Box::new(scan),
30//!     order_by: vec![SortExpr::asc(TypedExpr::column_ref(
31//!         "users".to_string(),
32//!         "name".to_string(),
33//!         1,
34//!         ResolvedType::Text,
35//!         Span::default(),
36//!     ))],
37//! };
38//!
39//! let limit = LogicalPlan::Limit {
40//!     input: Box::new(sort),
41//!     limit: Some(10),
42//!     offset: None,
43//! };
44//! ```
45
46use crate::ast::expr::WindowFrame;
47use crate::catalog::{IndexMetadata, TableMetadata};
48use crate::planner::aggregate_expr::AggregateExpr;
49use crate::planner::typed_expr::{Projection, SortExpr, TypedAssignment, TypedExpr};
50
51/// Function evaluated by a window operator.
52#[derive(Debug, Clone)]
53pub enum WindowFunction {
54    RowNumber,
55    Rank,
56    DenseRank,
57    Aggregate(AggregateExpr),
58    /// Value at an offset before the current row in the whole partition.
59    Lag(OffsetWindowFunction),
60    /// Value at an offset after the current row in the whole partition.
61    Lead(OffsetWindowFunction),
62}
63
64/// Arguments shared by the positional `LAG` and `LEAD` window functions.
65///
66/// Offset and default expressions are evaluated against the current row. The
67/// value expression is evaluated against the addressed partition row. Unlike
68/// aggregate windows, these functions do not restrict lookup to the current
69/// aggregate frame.
70#[derive(Debug, Clone)]
71pub struct OffsetWindowFunction {
72    pub value: TypedExpr,
73    pub offset: Option<TypedExpr>,
74    pub default: Option<TypedExpr>,
75}
76
77/// A planned window expression and its partition/order specification.
78#[derive(Debug, Clone)]
79pub struct WindowExpr {
80    pub function: WindowFunction,
81    pub partition_by: Vec<TypedExpr>,
82    pub order_by: Vec<SortExpr>,
83    pub frame: Option<WindowFrame>,
84    pub result_type: crate::planner::types::ResolvedType,
85}
86
87/// JOIN type for logical and physical execution.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum JoinType {
90    Inner,
91    Left,
92    Right,
93    Full,
94    Cross,
95}
96
97/// Set operation applied to two query inputs.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum SetOperator {
100    Union,
101    Intersect,
102    Except,
103}
104
105/// Hard execution bounds for a recursive common table expression.
106///
107/// Recursive evaluation is deliberately bounded even when the SQL uses
108/// `UNION ALL`, where a repeated row is semantically significant and cannot
109/// be used as an implicit convergence signal.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct RecursiveCteLimits {
112    pub max_iterations: usize,
113    pub max_rows: usize,
114}
115
116impl Default for RecursiveCteLimits {
117    fn default() -> Self {
118        Self {
119            max_iterations: 1_000,
120            max_rows: 100_000,
121        }
122    }
123}
124
125/// Logical query plan representation.
126///
127/// This enum represents all possible logical operations that can be performed.
128/// Plans are organized into three categories:
129///
130/// 1. **Query Plans**: Read operations (Scan, Filter, Sort, Limit)
131/// 2. **DML Plans**: Data modification (Insert, Update, Delete)
132/// 3. **DDL Plans**: Schema modification (CreateTable, DropTable, CreateIndex, DropIndex)
133#[derive(Debug, Clone)]
134pub enum LogicalPlan {
135    /// Runtime configuration or statistics operation.
136    Pragma {
137        /// PRAGMA name.
138        name: String,
139        /// Optional assignment value.
140        value: Option<crate::ast::PragmaValue>,
141    },
142
143    // === Query Plans ===
144    /// Table scan operation.
145    ///
146    /// Scans all rows from a table with the specified projection.
147    /// This is typically the leaf node of query plans.
148    Scan {
149        /// Table name to scan.
150        table: String,
151        /// Columns to project (after wildcard expansion).
152        projection: Projection,
153    },
154
155    /// Filter operation (WHERE clause).
156    ///
157    /// Filters rows from the input plan based on a predicate.
158    Filter {
159        /// Input plan to filter.
160        input: Box<LogicalPlan>,
161        /// Filter predicate (must evaluate to Boolean).
162        predicate: TypedExpr,
163    },
164
165    /// Projection boundary.
166    ///
167    /// Scan keeps the legacy single-table projection path; this node is used
168    /// when a relation-producing input such as JOIN or a derived table must be
169    /// materialized before being consumed by a parent query.
170    Project {
171        /// Input plan to project.
172        input: Box<LogicalPlan>,
173        /// Projection to apply.
174        projection: Projection,
175    },
176
177    /// JOIN operation.
178    Join {
179        /// Left input.
180        left: Box<LogicalPlan>,
181        /// Right input.
182        right: Box<LogicalPlan>,
183        /// Join type.
184        join_type: JoinType,
185        /// Optional ON condition.
186        condition: Option<TypedExpr>,
187        /// Optional USING columns.
188        using: Option<Vec<String>>,
189    },
190
191    /// Aggregate operation (GROUP BY / aggregation).
192    ///
193    /// Aggregates rows from the input plan using group keys and aggregate expressions.
194    Aggregate {
195        /// Input plan to aggregate.
196        input: Box<LogicalPlan>,
197        /// Group-by key expressions (empty for global aggregation).
198        group_keys: Vec<TypedExpr>,
199        /// Aggregate expressions to compute.
200        aggregates: Vec<AggregateExpr>,
201        /// HAVING filter applied after aggregation.
202        having: Option<TypedExpr>,
203        /// Projection to apply after aggregation.
204        projection: Projection,
205    },
206
207    /// Window operation preserving every input row and appending one result
208    /// column per window expression.
209    Window {
210        input: Box<LogicalPlan>,
211        windows: Vec<WindowExpr>,
212    },
213
214    /// UNION, INTERSECT, or EXCEPT over two projection-compatible queries.
215    SetOperation {
216        left: Box<LogicalPlan>,
217        right: Box<LogicalPlan>,
218        operator: SetOperator,
219        all: bool,
220    },
221
222    /// Materialized fixed-point evaluation for one directly self-recursive
223    /// common table expression.
224    RecursiveCte {
225        name: String,
226        anchor: Box<LogicalPlan>,
227        recursive_term: Box<LogicalPlan>,
228        union_all: bool,
229        schema: Vec<crate::catalog::ColumnMetadata>,
230        limits: RecursiveCteLimits,
231    },
232
233    /// Read the current working-table delta of an enclosing `RecursiveCte`.
234    /// The executor resolves this through an explicit per-query context.
235    RecursiveReference {
236        name: String,
237        schema: Vec<crate::catalog::ColumnMetadata>,
238    },
239
240    /// Sort operation (ORDER BY clause).
241    ///
242    /// Sorts rows from the input plan based on sort expressions.
243    Sort {
244        /// Input plan to sort.
245        input: Box<LogicalPlan>,
246        /// Sort expressions with direction.
247        order_by: Vec<SortExpr>,
248    },
249
250    /// Limit operation (LIMIT/OFFSET clause).
251    ///
252    /// Limits the number of rows from the input plan.
253    Limit {
254        /// Input plan to limit.
255        input: Box<LogicalPlan>,
256        /// Maximum number of rows to return.
257        limit: Option<u64>,
258        /// Number of rows to skip.
259        offset: Option<u64>,
260    },
261
262    // === DML Plans ===
263    /// INSERT operation.
264    ///
265    /// Inserts one or more rows into a table.
266    /// When columns are omitted in the SQL statement, the Planner fills in
267    /// all columns from TableMetadata in definition order.
268    Insert {
269        /// Target table name.
270        table: String,
271        /// Column names (always populated, never empty).
272        /// If omitted in SQL, filled from TableMetadata.column_names().
273        columns: Vec<String>,
274        /// Values to insert (one Vec per row, each value corresponds to a column).
275        values: Vec<Vec<TypedExpr>>,
276    },
277
278    /// INSERT rows produced by a SELECT query.
279    InsertSelect {
280        /// Target table name.
281        table: String,
282        /// Column names (always populated, never empty).
283        columns: Vec<String>,
284        /// Query that produces one row per inserted row.
285        source: Box<LogicalPlan>,
286    },
287
288    /// UPDATE operation.
289    ///
290    /// Updates rows in a table that match an optional filter.
291    Update {
292        /// Target table name.
293        table: String,
294        /// Assignments (SET column = value).
295        assignments: Vec<TypedAssignment>,
296        /// Optional filter predicate (WHERE clause).
297        filter: Option<TypedExpr>,
298    },
299
300    /// DELETE operation.
301    ///
302    /// Deletes rows from a table that match an optional filter.
303    Delete {
304        /// Target table name.
305        table: String,
306        /// Optional filter predicate (WHERE clause).
307        filter: Option<TypedExpr>,
308    },
309
310    // === DDL Plans ===
311    /// CREATE TABLE operation.
312    ///
313    /// Creates a new table with the specified metadata.
314    CreateTable {
315        /// Table metadata (name, columns, constraints).
316        table: TableMetadata,
317        /// If true, don't error if table already exists.
318        if_not_exists: bool,
319        /// Raw WITH options to be validated during execution.
320        with_options: Vec<(String, String)>,
321    },
322
323    /// DROP TABLE operation.
324    ///
325    /// Drops an existing table.
326    DropTable {
327        /// Table name to drop.
328        name: String,
329        /// If true, don't error if table doesn't exist.
330        if_exists: bool,
331    },
332
333    /// CREATE INDEX operation.
334    ///
335    /// Creates a new index on a table column.
336    CreateIndex {
337        /// Index metadata (name, table, column, method, options).
338        index: IndexMetadata,
339        /// If true, don't error if index already exists.
340        if_not_exists: bool,
341    },
342
343    /// DROP INDEX operation.
344    ///
345    /// Drops an existing index.
346    DropIndex {
347        /// Index name to drop.
348        name: String,
349        /// If true, don't error if index doesn't exist.
350        if_exists: bool,
351    },
352}
353
354impl LogicalPlan {
355    pub fn operation_name(&self) -> &'static str {
356        match self {
357            LogicalPlan::Pragma { .. } => "PRAGMA",
358            LogicalPlan::Scan { .. }
359            | LogicalPlan::Filter { .. }
360            | LogicalPlan::Project { .. }
361            | LogicalPlan::Join { .. }
362            | LogicalPlan::Aggregate { .. }
363            | LogicalPlan::Window { .. }
364            | LogicalPlan::SetOperation { .. }
365            | LogicalPlan::RecursiveCte { .. }
366            | LogicalPlan::RecursiveReference { .. }
367            | LogicalPlan::Sort { .. }
368            | LogicalPlan::Limit { .. } => "SELECT",
369            LogicalPlan::Insert { .. } => "INSERT",
370            LogicalPlan::InsertSelect { .. } => "INSERT",
371            LogicalPlan::Update { .. } => "UPDATE",
372            LogicalPlan::Delete { .. } => "DELETE",
373            LogicalPlan::CreateTable { .. } => "CREATE TABLE",
374            LogicalPlan::DropTable { .. } => "DROP TABLE",
375            LogicalPlan::CreateIndex { .. } => "CREATE INDEX",
376            LogicalPlan::DropIndex { .. } => "DROP INDEX",
377        }
378    }
379
380    /// Creates a new Scan plan.
381    pub fn scan(table: String, projection: Projection) -> Self {
382        LogicalPlan::Scan { table, projection }
383    }
384
385    /// Creates a new Filter plan.
386    pub fn filter(input: LogicalPlan, predicate: TypedExpr) -> Self {
387        LogicalPlan::Filter {
388            input: Box::new(input),
389            predicate,
390        }
391    }
392
393    /// Creates a new Project plan.
394    pub fn project(input: LogicalPlan, projection: Projection) -> Self {
395        LogicalPlan::Project {
396            input: Box::new(input),
397            projection,
398        }
399    }
400
401    /// Creates a new Join plan.
402    pub fn join(
403        left: LogicalPlan,
404        right: LogicalPlan,
405        join_type: JoinType,
406        condition: Option<TypedExpr>,
407        using: Option<Vec<String>>,
408    ) -> Self {
409        LogicalPlan::Join {
410            left: Box::new(left),
411            right: Box::new(right),
412            join_type,
413            condition,
414            using,
415        }
416    }
417
418    /// Creates a new Aggregate plan.
419    pub fn aggregate(
420        input: LogicalPlan,
421        group_keys: Vec<TypedExpr>,
422        aggregates: Vec<AggregateExpr>,
423        having: Option<TypedExpr>,
424        projection: Projection,
425    ) -> Self {
426        LogicalPlan::Aggregate {
427            input: Box::new(input),
428            group_keys,
429            aggregates,
430            having,
431            projection,
432        }
433    }
434
435    /// Creates a new Sort plan.
436    pub fn sort(input: LogicalPlan, order_by: Vec<SortExpr>) -> Self {
437        LogicalPlan::Sort {
438            input: Box::new(input),
439            order_by,
440        }
441    }
442
443    /// Creates a new Limit plan.
444    pub fn limit(input: LogicalPlan, limit: Option<u64>, offset: Option<u64>) -> Self {
445        LogicalPlan::Limit {
446            input: Box::new(input),
447            limit,
448            offset,
449        }
450    }
451
452    /// Creates a new Insert plan.
453    pub fn insert(table: String, columns: Vec<String>, values: Vec<Vec<TypedExpr>>) -> Self {
454        LogicalPlan::Insert {
455            table,
456            columns,
457            values,
458        }
459    }
460
461    /// Creates a new Update plan.
462    pub fn update(
463        table: String,
464        assignments: Vec<TypedAssignment>,
465        filter: Option<TypedExpr>,
466    ) -> Self {
467        LogicalPlan::Update {
468            table,
469            assignments,
470            filter,
471        }
472    }
473
474    /// Creates a new Delete plan.
475    pub fn delete(table: String, filter: Option<TypedExpr>) -> Self {
476        LogicalPlan::Delete { table, filter }
477    }
478
479    /// Creates a new CreateTable plan.
480    pub fn create_table(
481        table: TableMetadata,
482        if_not_exists: bool,
483        with_options: Vec<(String, String)>,
484    ) -> Self {
485        LogicalPlan::CreateTable {
486            table,
487            if_not_exists,
488            with_options,
489        }
490    }
491
492    /// Creates a new DropTable plan.
493    pub fn drop_table(name: String, if_exists: bool) -> Self {
494        LogicalPlan::DropTable { name, if_exists }
495    }
496
497    /// Creates a new CreateIndex plan.
498    pub fn create_index(index: IndexMetadata, if_not_exists: bool) -> Self {
499        LogicalPlan::CreateIndex {
500            index,
501            if_not_exists,
502        }
503    }
504
505    /// Creates a new DropIndex plan.
506    pub fn drop_index(name: String, if_exists: bool) -> Self {
507        LogicalPlan::DropIndex { name, if_exists }
508    }
509
510    /// Returns the name of this plan variant.
511    pub fn name(&self) -> &'static str {
512        match self {
513            LogicalPlan::Pragma { .. } => "Pragma",
514            LogicalPlan::Scan { .. } => "Scan",
515            LogicalPlan::Filter { .. } => "Filter",
516            LogicalPlan::Project { .. } => "Project",
517            LogicalPlan::Join { .. } => "Join",
518            LogicalPlan::Aggregate { .. } => "Aggregate",
519            LogicalPlan::Window { .. } => "Window",
520            LogicalPlan::SetOperation { .. } => "SetOperation",
521            LogicalPlan::RecursiveCte { .. } => "RecursiveCte",
522            LogicalPlan::RecursiveReference { .. } => "RecursiveReference",
523            LogicalPlan::Sort { .. } => "Sort",
524            LogicalPlan::Limit { .. } => "Limit",
525            LogicalPlan::Insert { .. } => "Insert",
526            LogicalPlan::InsertSelect { .. } => "InsertSelect",
527            LogicalPlan::Update { .. } => "Update",
528            LogicalPlan::Delete { .. } => "Delete",
529            LogicalPlan::CreateTable { .. } => "CreateTable",
530            LogicalPlan::DropTable { .. } => "DropTable",
531            LogicalPlan::CreateIndex { .. } => "CreateIndex",
532            LogicalPlan::DropIndex { .. } => "DropIndex",
533        }
534    }
535
536    /// Returns true if this is a query plan (Scan, Filter, Sort, Limit).
537    pub fn is_query(&self) -> bool {
538        matches!(
539            self,
540            LogicalPlan::Scan { .. }
541                | LogicalPlan::Filter { .. }
542                | LogicalPlan::Project { .. }
543                | LogicalPlan::Join { .. }
544                | LogicalPlan::Aggregate { .. }
545                | LogicalPlan::Window { .. }
546                | LogicalPlan::SetOperation { .. }
547                | LogicalPlan::RecursiveCte { .. }
548                | LogicalPlan::RecursiveReference { .. }
549                | LogicalPlan::Sort { .. }
550                | LogicalPlan::Limit { .. }
551        )
552    }
553
554    /// Returns true if this is a DML plan (Insert, Update, Delete).
555    pub fn is_dml(&self) -> bool {
556        matches!(
557            self,
558            LogicalPlan::Insert { .. }
559                | LogicalPlan::InsertSelect { .. }
560                | LogicalPlan::Update { .. }
561                | LogicalPlan::Delete { .. }
562        )
563    }
564
565    /// Returns true if this is a DDL plan (CreateTable, DropTable, CreateIndex, DropIndex).
566    pub fn is_ddl(&self) -> bool {
567        matches!(
568            self,
569            LogicalPlan::CreateTable { .. }
570                | LogicalPlan::DropTable { .. }
571                | LogicalPlan::CreateIndex { .. }
572                | LogicalPlan::DropIndex { .. }
573                | LogicalPlan::Pragma { .. }
574        )
575    }
576
577    /// Returns the input plan if this is a transformation (Filter, Aggregate, Sort, Limit).
578    pub fn input(&self) -> Option<&LogicalPlan> {
579        match self {
580            LogicalPlan::Filter { input, .. }
581            | LogicalPlan::Project { input, .. }
582            | LogicalPlan::Aggregate { input, .. }
583            | LogicalPlan::Window { input, .. }
584            | LogicalPlan::Sort { input, .. }
585            | LogicalPlan::Limit { input, .. } => Some(input),
586            LogicalPlan::Join { .. } => None,
587            LogicalPlan::SetOperation { .. } => None,
588            LogicalPlan::RecursiveCte { .. } | LogicalPlan::RecursiveReference { .. } => None,
589            _ => None,
590        }
591    }
592
593    /// Returns the table name if this plan operates on a single table.
594    pub fn table_name(&self) -> Option<&str> {
595        match self {
596            LogicalPlan::Scan { table, .. }
597            | LogicalPlan::Insert { table, .. }
598            | LogicalPlan::InsertSelect { table, .. }
599            | LogicalPlan::Update { table, .. }
600            | LogicalPlan::Delete { table, .. } => Some(table),
601            LogicalPlan::CreateTable { table, .. } => Some(&table.name),
602            LogicalPlan::DropTable { name, .. } => Some(name),
603            LogicalPlan::CreateIndex { index, .. } => Some(&index.table),
604            LogicalPlan::DropIndex { .. } => None,
605            LogicalPlan::Pragma { .. } => None,
606            LogicalPlan::Filter { input, .. }
607            | LogicalPlan::Project { input, .. }
608            | LogicalPlan::Aggregate { input, .. }
609            | LogicalPlan::Window { input, .. }
610            | LogicalPlan::Sort { input, .. }
611            | LogicalPlan::Limit { input, .. } => input.table_name(),
612            LogicalPlan::Join { .. } => None,
613            LogicalPlan::SetOperation { left, right, .. } => left
614                .table_name()
615                .filter(|name| right.table_name() == Some(*name)),
616            LogicalPlan::RecursiveCte { .. } | LogicalPlan::RecursiveReference { .. } => None,
617        }
618    }
619
620    /// Returns whether this plan tree contains a JOIN boundary.
621    ///
622    /// The normal local planner/executor continues to support JOIN.  Consumers
623    /// with a deliberately closed execution catalog (such as distributed
624    /// reads) can use this structural fact to reject it before any transport is
625    /// opened rather than trying to infer it from a table name.
626    pub fn contains_join(&self) -> bool {
627        match self {
628            LogicalPlan::Join { .. } => true,
629            LogicalPlan::SetOperation { left, right, .. } => {
630                left.contains_join() || right.contains_join()
631            }
632            LogicalPlan::RecursiveCte {
633                anchor,
634                recursive_term,
635                ..
636            } => anchor.contains_join() || recursive_term.contains_join(),
637            LogicalPlan::Filter { input, .. }
638            | LogicalPlan::Project { input, .. }
639            | LogicalPlan::Aggregate { input, .. }
640            | LogicalPlan::Window { input, .. }
641            | LogicalPlan::Sort { input, .. }
642            | LogicalPlan::Limit { input, .. } => input.contains_join(),
643            _ => false,
644        }
645    }
646
647    /// Returns whether this plan tree contains a set-operation boundary.
648    pub fn contains_set_operation(&self) -> bool {
649        match self {
650            LogicalPlan::SetOperation { .. } | LogicalPlan::RecursiveCte { .. } => true,
651            LogicalPlan::Filter { input, .. }
652            | LogicalPlan::Project { input, .. }
653            | LogicalPlan::Aggregate { input, .. }
654            | LogicalPlan::Sort { input, .. }
655            | LogicalPlan::Limit { input, .. } => input.contains_set_operation(),
656            LogicalPlan::Join { left, right, .. } => {
657                left.contains_set_operation() || right.contains_set_operation()
658            }
659            _ => false,
660        }
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667    use crate::ast::expr::Literal;
668    use crate::ast::span::Span;
669    use crate::catalog::ColumnMetadata;
670    use crate::planner::typed_expr::ProjectedColumn;
671    use crate::planner::types::ResolvedType;
672
673    fn create_test_table_metadata() -> TableMetadata {
674        TableMetadata::new(
675            "users",
676            vec![
677                ColumnMetadata::new("id", ResolvedType::Integer)
678                    .with_primary_key(true)
679                    .with_not_null(true),
680                ColumnMetadata::new("name", ResolvedType::Text).with_not_null(true),
681                ColumnMetadata::new("email", ResolvedType::Text),
682            ],
683        )
684        .with_primary_key(vec!["id".to_string()])
685    }
686
687    #[test]
688    fn test_scan_plan() {
689        let plan = LogicalPlan::scan(
690            "users".to_string(),
691            Projection::All(vec![
692                "id".to_string(),
693                "name".to_string(),
694                "email".to_string(),
695            ]),
696        );
697
698        assert_eq!(plan.name(), "Scan");
699        assert!(plan.is_query());
700        assert!(!plan.is_dml());
701        assert!(!plan.is_ddl());
702        assert_eq!(plan.table_name(), Some("users"));
703        assert!(plan.input().is_none());
704    }
705
706    #[test]
707    fn test_filter_plan() {
708        let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
709        let predicate = TypedExpr::column_ref(
710            "users".to_string(),
711            "id".to_string(),
712            0,
713            ResolvedType::Integer,
714            Span::default(),
715        );
716
717        let plan = LogicalPlan::filter(scan, predicate);
718
719        assert_eq!(plan.name(), "Filter");
720        assert!(plan.is_query());
721        assert!(plan.input().is_some());
722        assert_eq!(plan.table_name(), Some("users"));
723    }
724
725    #[test]
726    fn test_sort_plan() {
727        let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
728        let sort_expr = SortExpr::asc(TypedExpr::column_ref(
729            "users".to_string(),
730            "name".to_string(),
731            1,
732            ResolvedType::Text,
733            Span::default(),
734        ));
735
736        let plan = LogicalPlan::sort(scan, vec![sort_expr]);
737
738        assert_eq!(plan.name(), "Sort");
739        assert!(plan.is_query());
740    }
741
742    #[test]
743    fn test_limit_plan() {
744        let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
745        let plan = LogicalPlan::limit(scan, Some(10), Some(5));
746
747        assert_eq!(plan.name(), "Limit");
748        assert!(plan.is_query());
749
750        if let LogicalPlan::Limit { limit, offset, .. } = &plan {
751            assert_eq!(*limit, Some(10));
752            assert_eq!(*offset, Some(5));
753        } else {
754            panic!("Expected Limit plan");
755        }
756    }
757
758    #[test]
759    fn test_nested_query_plan() {
760        // SELECT * FROM users WHERE id > 5 ORDER BY name LIMIT 10
761        let scan = LogicalPlan::scan(
762            "users".to_string(),
763            Projection::All(vec!["id".to_string(), "name".to_string()]),
764        );
765
766        let predicate = TypedExpr::literal(
767            Literal::Boolean(true),
768            ResolvedType::Boolean,
769            Span::default(),
770        );
771        let filter = LogicalPlan::filter(scan, predicate);
772
773        let sort_expr = SortExpr::asc(TypedExpr::column_ref(
774            "users".to_string(),
775            "name".to_string(),
776            1,
777            ResolvedType::Text,
778            Span::default(),
779        ));
780        let sort = LogicalPlan::sort(filter, vec![sort_expr]);
781
782        let limit = LogicalPlan::limit(sort, Some(10), None);
783
784        // Verify the plan tree
785        assert_eq!(limit.name(), "Limit");
786        assert_eq!(limit.table_name(), Some("users"));
787
788        let sort_plan = limit.input().unwrap();
789        assert_eq!(sort_plan.name(), "Sort");
790
791        let filter_plan = sort_plan.input().unwrap();
792        assert_eq!(filter_plan.name(), "Filter");
793
794        let scan_plan = filter_plan.input().unwrap();
795        assert_eq!(scan_plan.name(), "Scan");
796        assert!(scan_plan.input().is_none());
797    }
798
799    #[test]
800    fn test_insert_plan() {
801        let value1 = TypedExpr::literal(
802            Literal::Number("1".to_string()),
803            ResolvedType::Integer,
804            Span::default(),
805        );
806        let value2 = TypedExpr::literal(
807            Literal::String("Alice".to_string()),
808            ResolvedType::Text,
809            Span::default(),
810        );
811
812        let plan = LogicalPlan::insert(
813            "users".to_string(),
814            vec!["id".to_string(), "name".to_string()],
815            vec![vec![value1, value2]],
816        );
817
818        assert_eq!(plan.name(), "Insert");
819        assert!(plan.is_dml());
820        assert!(!plan.is_query());
821        assert!(!plan.is_ddl());
822        assert_eq!(plan.table_name(), Some("users"));
823
824        if let LogicalPlan::Insert {
825            table,
826            columns,
827            values,
828        } = &plan
829        {
830            assert_eq!(table, "users");
831            assert_eq!(columns, &vec!["id".to_string(), "name".to_string()]);
832            assert_eq!(values.len(), 1);
833            assert_eq!(values[0].len(), 2);
834        } else {
835            panic!("Expected Insert plan");
836        }
837    }
838
839    #[test]
840    fn test_update_plan() {
841        let assignment = TypedAssignment::new(
842            "name".to_string(),
843            1,
844            TypedExpr::literal(
845                Literal::String("Bob".to_string()),
846                ResolvedType::Text,
847                Span::default(),
848            ),
849        );
850
851        let filter = TypedExpr::literal(
852            Literal::Boolean(true),
853            ResolvedType::Boolean,
854            Span::default(),
855        );
856
857        let plan = LogicalPlan::update("users".to_string(), vec![assignment], Some(filter));
858
859        assert_eq!(plan.name(), "Update");
860        assert!(plan.is_dml());
861        assert_eq!(plan.table_name(), Some("users"));
862    }
863
864    #[test]
865    fn test_delete_plan() {
866        let filter = TypedExpr::column_ref(
867            "users".to_string(),
868            "id".to_string(),
869            0,
870            ResolvedType::Integer,
871            Span::default(),
872        );
873
874        let plan = LogicalPlan::delete("users".to_string(), Some(filter));
875
876        assert_eq!(plan.name(), "Delete");
877        assert!(plan.is_dml());
878        assert_eq!(plan.table_name(), Some("users"));
879    }
880
881    #[test]
882    fn test_create_table_plan() {
883        let table = create_test_table_metadata();
884        let plan = LogicalPlan::create_table(table, false, vec![]);
885
886        assert_eq!(plan.name(), "CreateTable");
887        assert!(plan.is_ddl());
888        assert!(!plan.is_dml());
889        assert!(!plan.is_query());
890        assert_eq!(plan.table_name(), Some("users"));
891    }
892
893    #[test]
894    fn test_drop_table_plan() {
895        let plan = LogicalPlan::drop_table("users".to_string(), true);
896
897        assert_eq!(plan.name(), "DropTable");
898        assert!(plan.is_ddl());
899        assert_eq!(plan.table_name(), Some("users"));
900
901        if let LogicalPlan::DropTable { name, if_exists } = &plan {
902            assert_eq!(name, "users");
903            assert!(*if_exists);
904        } else {
905            panic!("Expected DropTable plan");
906        }
907    }
908
909    #[test]
910    fn test_create_index_plan() {
911        let index = IndexMetadata::new(0, "idx_users_name", "users", vec!["name".into()]);
912        let plan = LogicalPlan::create_index(index, false);
913
914        assert_eq!(plan.name(), "CreateIndex");
915        assert!(plan.is_ddl());
916        assert_eq!(plan.table_name(), Some("users"));
917    }
918
919    #[test]
920    fn test_drop_index_plan() {
921        let plan = LogicalPlan::drop_index("idx_users_name".to_string(), false);
922
923        assert_eq!(plan.name(), "DropIndex");
924        assert!(plan.is_ddl());
925        // DropIndex doesn't have table_name directly
926        assert!(plan.table_name().is_none());
927    }
928
929    #[test]
930    fn test_projection_columns() {
931        let col1 = ProjectedColumn::new(TypedExpr::column_ref(
932            "users".to_string(),
933            "id".to_string(),
934            0,
935            ResolvedType::Integer,
936            Span::default(),
937        ));
938        let col2 = ProjectedColumn::with_alias(
939            TypedExpr::column_ref(
940                "users".to_string(),
941                "name".to_string(),
942                1,
943                ResolvedType::Text,
944                Span::default(),
945            ),
946            "user_name".to_string(),
947        );
948
949        let plan = LogicalPlan::scan("users".to_string(), Projection::Columns(vec![col1, col2]));
950
951        if let LogicalPlan::Scan { projection, .. } = &plan {
952            assert_eq!(projection.len(), 2);
953        } else {
954            panic!("Expected Scan plan");
955        }
956    }
957}