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