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//!     ties: None,
44//! };
45//! ```
46
47use crate::ast::expr::WindowFrame;
48use crate::catalog::{IndexMetadata, TableMetadata};
49use crate::planner::aggregate_expr::AggregateExpr;
50use crate::planner::typed_expr::{Projection, SortExpr, TypedAssignment, TypedExpr};
51
52/// Function evaluated by a window operator.
53#[derive(Debug, Clone)]
54pub enum WindowFunction {
55    RowNumber,
56    Rank,
57    DenseRank,
58    PercentRank,
59    CumeDist,
60    Ntile(TypedExpr),
61    Aggregate(AggregateExpr),
62    Value(ValueWindowFunction),
63    /// Value at an offset before the current row in the whole partition.
64    Lag(OffsetWindowFunction),
65    /// Value at an offset after the current row in the whole partition.
66    Lead(OffsetWindowFunction),
67}
68
69/// Value selected from the current row's effective window frame.
70#[derive(Debug, Clone)]
71// `TypedExpr` grew with the issue #148 aggregate clauses; every variant holds
72// at least one TypedExpr, so boxing `nth` alone buys nothing structural.
73#[allow(clippy::large_enum_variant)]
74pub enum ValueWindowFunction {
75    FirstValue(TypedExpr),
76    LastValue(TypedExpr),
77    NthValue { value: TypedExpr, nth: TypedExpr },
78}
79
80/// Arguments shared by the positional `LAG` and `LEAD` window functions.
81///
82/// Offset and default expressions are evaluated against the current row. The
83/// value expression is evaluated against the addressed partition row. Unlike
84/// aggregate windows, these functions do not restrict lookup to the current
85/// aggregate frame.
86#[derive(Debug, Clone)]
87pub struct OffsetWindowFunction {
88    pub value: TypedExpr,
89    pub offset: Option<TypedExpr>,
90    pub default: Option<TypedExpr>,
91}
92
93/// A planned window expression and its partition/order specification.
94#[derive(Debug, Clone)]
95pub struct WindowExpr {
96    pub function: WindowFunction,
97    pub partition_by: Vec<TypedExpr>,
98    pub order_by: Vec<SortExpr>,
99    pub frame: Option<WindowFrame>,
100    pub result_type: crate::planner::types::ResolvedType,
101}
102
103/// JOIN type for logical and physical execution.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum JoinType {
106    Inner,
107    Left,
108    Right,
109    Full,
110    Cross,
111}
112
113/// Set operation applied to two query inputs.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum SetOperator {
116    Union,
117    Intersect,
118    Except,
119}
120
121/// Hard execution bounds for a recursive common table expression.
122///
123/// Recursive evaluation is deliberately bounded even when the SQL uses
124/// `UNION ALL`, where a repeated row is semantically significant and cannot
125/// be used as an implicit convergence signal.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct RecursiveCteLimits {
128    pub max_iterations: usize,
129    pub max_rows: usize,
130}
131
132impl Default for RecursiveCteLimits {
133    fn default() -> Self {
134        Self {
135            max_iterations: 1_000,
136            max_rows: 100_000,
137        }
138    }
139}
140
141/// FROM-clause table functions Alopex can plan (issue #151).
142///
143/// The registry is closed: a name outside this set is a planning error rather
144/// than a call into an open function namespace.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum TableFunctionKind {
147    /// `UNNEST(vector)` — one row per element.
148    Unnest,
149    /// `GENERATE_SERIES(...)` — reserved for issue #157; planning rejects it.
150    GenerateSeries,
151}
152
153impl TableFunctionKind {
154    /// Resolve a FROM-clause function name, case-insensitively.
155    pub fn from_name(name: &str) -> Option<Self> {
156        match name.to_ascii_uppercase().as_str() {
157            "UNNEST" => Some(Self::Unnest),
158            "GENERATE_SERIES" => Some(Self::GenerateSeries),
159            _ => None,
160        }
161    }
162
163    /// Canonical uppercase spelling used in messages and plan output.
164    pub fn name(self) -> &'static str {
165        match self {
166            Self::Unnest => "UNNEST",
167            Self::GenerateSeries => "GENERATE_SERIES",
168        }
169    }
170
171    /// Default relation name when the item carries no alias (PostgreSQL uses
172    /// the lowercase function name).
173    pub fn default_relation_name(self) -> &'static str {
174        match self {
175            Self::Unnest => "unnest",
176            Self::GenerateSeries => "generate_series",
177        }
178    }
179}
180
181/// Logical query plan representation.
182///
183/// This enum represents all possible logical operations that can be performed.
184/// Plans are organized into three categories:
185///
186/// 1. **Query Plans**: Read operations (Scan, Filter, Sort, Limit)
187/// 2. **DML Plans**: Data modification (Insert, Update, Delete)
188/// 3. **DDL Plans**: Schema modification (CreateTable, DropTable, CreateIndex, DropIndex)
189#[derive(Debug, Clone)]
190pub enum LogicalPlan {
191    /// Runtime configuration or statistics operation.
192    Pragma {
193        /// PRAGMA name.
194        name: String,
195        /// Optional assignment value.
196        value: Option<crate::ast::PragmaValue>,
197    },
198
199    // === Query Plans ===
200    /// Table scan operation.
201    ///
202    /// Scans all rows from a table with the specified projection.
203    /// This is typically the leaf node of query plans.
204    Scan {
205        /// Table name to scan.
206        table: String,
207        /// Columns to project (after wildcard expansion).
208        projection: Projection,
209    },
210
211    /// Inline VALUES rows evaluated once per output row.
212    Values {
213        /// Type-checked expressions for each row.
214        rows: Vec<Vec<TypedExpr>>,
215        /// Common output schema inferred column-by-column across all rows.
216        schema: Vec<crate::catalog::ColumnMetadata>,
217    },
218
219    /// Filter operation (WHERE clause).
220    ///
221    /// Filters rows from the input plan based on a predicate.
222    Filter {
223        /// Input plan to filter.
224        input: Box<LogicalPlan>,
225        /// Filter predicate (must evaluate to Boolean).
226        predicate: TypedExpr,
227    },
228
229    /// Projection boundary.
230    ///
231    /// Scan keeps the legacy single-table projection path; this node is used
232    /// when a relation-producing input such as JOIN or a derived table must be
233    /// materialized before being consumed by a parent query.
234    Project {
235        /// Input plan to project.
236        input: Box<LogicalPlan>,
237        /// Projection to apply.
238        projection: Projection,
239    },
240
241    /// JOIN operation.
242    Join {
243        /// Left input.
244        left: Box<LogicalPlan>,
245        /// Right input.
246        right: Box<LogicalPlan>,
247        /// Join type.
248        join_type: JoinType,
249        /// Optional ON condition.
250        condition: Option<TypedExpr>,
251        /// Optional USING columns.
252        using: Option<Vec<String>>,
253    },
254
255    /// LATERAL join (issue #151).
256    ///
257    /// The right input is a correlated relation: it is planned against the left
258    /// row and re-executed once per left row, so it cannot be reordered with or
259    /// hoisted above the left side the way [`LogicalPlan::Join`] can.
260    LateralJoin {
261        /// Left input, executed once.
262        left: Box<LogicalPlan>,
263        /// Correlated right input, executed once per left row with the left
264        /// row supplied as the outer row.
265        right: Box<LogicalPlan>,
266        /// `Inner`, `Left`, or `Cross`; RIGHT and FULL are rejected in planning.
267        join_type: JoinType,
268        /// Optional ON condition over the concatenated (left, right) row.
269        condition: Option<TypedExpr>,
270        /// Output schema of the right input, kept so a LEFT join can pad even
271        /// when the left input is empty.
272        right_schema: Vec<crate::catalog::ColumnMetadata>,
273    },
274
275    /// FROM-clause table function (issue #151).
276    TableFunction {
277        /// Which function this node evaluates.
278        function: TableFunctionKind,
279        /// Argument expressions, evaluated against the outer row.
280        args: Vec<TypedExpr>,
281        /// Output schema.
282        schema: Vec<crate::catalog::ColumnMetadata>,
283    },
284
285    /// Aggregate operation (GROUP BY / aggregation).
286    ///
287    /// Aggregates rows from the input plan using group keys and aggregate expressions.
288    Aggregate {
289        /// Input plan to aggregate.
290        input: Box<LogicalPlan>,
291        /// Group-by key expressions (empty for global aggregation).
292        group_keys: Vec<TypedExpr>,
293        /// Aggregate expressions to compute.
294        aggregates: Vec<AggregateExpr>,
295        /// HAVING filter applied after aggregation.
296        having: Option<TypedExpr>,
297        /// Projection to apply after aggregation.
298        projection: Projection,
299        /// Expanded GROUPING SETS masks over `group_keys` (issue #149).
300        ///
301        /// `None` keeps the pre-grouping-sets single-set behavior. Each mask
302        /// covers `group_keys` with key 0 at the most significant of the low
303        /// `group_keys.len()` bits; a 1 bit marks the key as excluded from
304        /// that grouping set (NULL placeholder in the output). When present,
305        /// the aggregate output schema gains a trailing `__grouping_id`
306        /// BIGINT column carrying the mask of the producing set.
307        grouping_sets: Option<Vec<u64>>,
308    },
309
310    /// Window operation preserving every input row and appending one result
311    /// column per window expression.
312    Window {
313        input: Box<LogicalPlan>,
314        windows: Vec<WindowExpr>,
315    },
316
317    /// UNION, INTERSECT, or EXCEPT over two projection-compatible queries.
318    SetOperation {
319        left: Box<LogicalPlan>,
320        right: Box<LogicalPlan>,
321        operator: SetOperator,
322        all: bool,
323    },
324
325    /// Materialized fixed-point evaluation for one directly self-recursive
326    /// common table expression.
327    RecursiveCte {
328        name: String,
329        anchor: Box<LogicalPlan>,
330        recursive_term: Box<LogicalPlan>,
331        union_all: bool,
332        schema: Vec<crate::catalog::ColumnMetadata>,
333        limits: RecursiveCteLimits,
334    },
335
336    /// Read the current working-table delta of an enclosing `RecursiveCte`.
337    /// The executor resolves this through an explicit per-query context.
338    RecursiveReference {
339        name: String,
340        schema: Vec<crate::catalog::ColumnMetadata>,
341    },
342
343    /// Sort operation (ORDER BY clause).
344    ///
345    /// Sorts rows from the input plan based on sort expressions.
346    Sort {
347        /// Input plan to sort.
348        input: Box<LogicalPlan>,
349        /// Sort expressions with direction.
350        order_by: Vec<SortExpr>,
351    },
352
353    /// SELECT DISTINCT ON (expr, ...) deduplication (issue #150).
354    ///
355    /// Sorts the input by the complete effective sort specification and emits
356    /// only the first row of each group of rows whose leading `key_count`
357    /// sort keys compare equal (NULL keys compare equal to NULL, D5).
358    ///
359    /// Invariants established by the planner (docs/sql-distinct-on.md):
360    /// - `order_by[..key_count]` covers every deduplicated DISTINCT ON key
361    ///   (the user's matching ORDER BY prefix plus implicit ASC NULLS LAST
362    ///   keys, D2/D3).
363    /// - `order_by[key_count..]` carries the user's ORDER BY tail followed by
364    ///   every input column as an ASC NULLS LAST tie-breaker, so the surviving
365    ///   row of each group never depends on physical input order (D4).
366    /// - The node emits rows already ordered by the effective specification,
367    ///   so no additional Sort node is planned above it (D8).
368    DistinctOn {
369        /// Input plan to deduplicate.
370        input: Box<LogicalPlan>,
371        /// Number of leading `order_by` entries that form the distinctness key.
372        key_count: usize,
373        /// Complete effective sort specification (keys, tail, tie-breakers).
374        order_by: Vec<SortExpr>,
375    },
376
377    /// Limit operation (LIMIT/OFFSET/FETCH clause).
378    ///
379    /// Limits the number of rows from the input plan. `limit` and `offset`
380    /// are concrete values resolved at plan time, so the node can be carried
381    /// by the distributed plan contract without re-evaluating expressions.
382    Limit {
383        /// Input plan to limit.
384        input: Box<LogicalPlan>,
385        /// Maximum number of rows to return.
386        limit: Option<u64>,
387        /// Number of rows to skip.
388        offset: Option<u64>,
389        /// FETCH ... WITH TIES: after `limit` rows, keep emitting rows whose
390        /// ORDER BY sort key equals the final emitted row's key (peer rows).
391        /// The keys are a copy of the `Sort` node directly beneath this
392        /// Limit; `None` means plain ONLY/LIMIT semantics.
393        ties: Option<Vec<SortExpr>>,
394    },
395
396    // === DML Plans ===
397    /// INSERT operation.
398    ///
399    /// Inserts one or more rows into a table.
400    /// When columns are omitted in the SQL statement, the Planner fills in
401    /// all columns from TableMetadata in definition order.
402    Insert {
403        /// Target table name.
404        table: String,
405        /// Column names (always populated, never empty).
406        /// If omitted in SQL, filled from TableMetadata.column_names().
407        columns: Vec<String>,
408        /// Values to insert (one Vec per row, each value corresponds to a column).
409        values: Vec<Vec<TypedExpr>>,
410    },
411
412    /// INSERT rows produced by a SELECT query.
413    InsertSelect {
414        /// Target table name.
415        table: String,
416        /// Column names (always populated, never empty).
417        columns: Vec<String>,
418        /// Query that produces one row per inserted row.
419        source: Box<LogicalPlan>,
420    },
421
422    /// UPDATE operation.
423    ///
424    /// Updates rows in a table that match an optional filter.
425    Update {
426        /// Target table name.
427        table: String,
428        /// Assignments (SET column = value).
429        assignments: Vec<TypedAssignment>,
430        /// Optional filter predicate (WHERE clause).
431        filter: Option<TypedExpr>,
432    },
433
434    /// DELETE operation.
435    ///
436    /// Deletes rows from a table that match an optional filter.
437    Delete {
438        /// Target table name.
439        table: String,
440        /// Optional filter predicate (WHERE clause).
441        filter: Option<TypedExpr>,
442    },
443
444    // === DDL Plans ===
445    /// CREATE TABLE operation.
446    ///
447    /// Creates a new table with the specified metadata.
448    CreateTable {
449        /// Table metadata (name, columns, constraints).
450        table: TableMetadata,
451        /// If true, don't error if table already exists.
452        if_not_exists: bool,
453        /// Raw WITH options to be validated during execution.
454        with_options: Vec<(String, String)>,
455    },
456
457    /// DROP TABLE operation.
458    ///
459    /// Drops an existing table.
460    DropTable {
461        /// Table name to drop.
462        name: String,
463        /// If true, don't error if table doesn't exist.
464        if_exists: bool,
465    },
466
467    /// CREATE INDEX operation.
468    ///
469    /// Creates a new index on a table column.
470    CreateIndex {
471        /// Index metadata (name, table, column, method, options).
472        index: IndexMetadata,
473        /// If true, don't error if index already exists.
474        if_not_exists: bool,
475    },
476
477    /// DROP INDEX operation.
478    ///
479    /// Drops an existing index.
480    DropIndex {
481        /// Index name to drop.
482        name: String,
483        /// If true, don't error if index doesn't exist.
484        if_exists: bool,
485    },
486}
487
488impl LogicalPlan {
489    pub fn operation_name(&self) -> &'static str {
490        match self {
491            LogicalPlan::Pragma { .. } => "PRAGMA",
492            LogicalPlan::Scan { .. }
493            | LogicalPlan::Values { .. }
494            | LogicalPlan::Filter { .. }
495            | LogicalPlan::Project { .. }
496            | LogicalPlan::Join { .. }
497            | LogicalPlan::LateralJoin { .. }
498            | LogicalPlan::TableFunction { .. }
499            | LogicalPlan::Aggregate { .. }
500            | LogicalPlan::Window { .. }
501            | LogicalPlan::SetOperation { .. }
502            | LogicalPlan::RecursiveCte { .. }
503            | LogicalPlan::RecursiveReference { .. }
504            | LogicalPlan::Sort { .. }
505            | LogicalPlan::DistinctOn { .. }
506            | LogicalPlan::Limit { .. } => "SELECT",
507            LogicalPlan::Insert { .. } => "INSERT",
508            LogicalPlan::InsertSelect { .. } => "INSERT",
509            LogicalPlan::Update { .. } => "UPDATE",
510            LogicalPlan::Delete { .. } => "DELETE",
511            LogicalPlan::CreateTable { .. } => "CREATE TABLE",
512            LogicalPlan::DropTable { .. } => "DROP TABLE",
513            LogicalPlan::CreateIndex { .. } => "CREATE INDEX",
514            LogicalPlan::DropIndex { .. } => "DROP INDEX",
515        }
516    }
517
518    /// Creates a new Scan plan.
519    pub fn scan(table: String, projection: Projection) -> Self {
520        LogicalPlan::Scan { table, projection }
521    }
522
523    /// Creates a new Filter plan.
524    pub fn filter(input: LogicalPlan, predicate: TypedExpr) -> Self {
525        LogicalPlan::Filter {
526            input: Box::new(input),
527            predicate,
528        }
529    }
530
531    /// Creates a new Project plan.
532    pub fn project(input: LogicalPlan, projection: Projection) -> Self {
533        LogicalPlan::Project {
534            input: Box::new(input),
535            projection,
536        }
537    }
538
539    /// Creates a new Join plan.
540    pub fn join(
541        left: LogicalPlan,
542        right: LogicalPlan,
543        join_type: JoinType,
544        condition: Option<TypedExpr>,
545        using: Option<Vec<String>>,
546    ) -> Self {
547        LogicalPlan::Join {
548            left: Box::new(left),
549            right: Box::new(right),
550            join_type,
551            condition,
552            using,
553        }
554    }
555
556    /// Creates a new Aggregate plan without grouping sets.
557    pub fn aggregate(
558        input: LogicalPlan,
559        group_keys: Vec<TypedExpr>,
560        aggregates: Vec<AggregateExpr>,
561        having: Option<TypedExpr>,
562        projection: Projection,
563    ) -> Self {
564        LogicalPlan::Aggregate {
565            input: Box::new(input),
566            group_keys,
567            aggregates,
568            having,
569            projection,
570            grouping_sets: None,
571        }
572    }
573
574    /// Creates a new Sort plan.
575    pub fn sort(input: LogicalPlan, order_by: Vec<SortExpr>) -> Self {
576        LogicalPlan::Sort {
577            input: Box::new(input),
578            order_by,
579        }
580    }
581
582    /// Creates a new DistinctOn plan.
583    pub fn distinct_on(input: LogicalPlan, key_count: usize, order_by: Vec<SortExpr>) -> Self {
584        LogicalPlan::DistinctOn {
585            input: Box::new(input),
586            key_count,
587            order_by,
588        }
589    }
590
591    /// Creates a new Limit plan (plain ONLY/LIMIT semantics, no ties).
592    pub fn limit(input: LogicalPlan, limit: Option<u64>, offset: Option<u64>) -> Self {
593        LogicalPlan::Limit {
594            input: Box::new(input),
595            limit,
596            offset,
597            ties: None,
598        }
599    }
600
601    /// Creates a new Insert plan.
602    pub fn insert(table: String, columns: Vec<String>, values: Vec<Vec<TypedExpr>>) -> Self {
603        LogicalPlan::Insert {
604            table,
605            columns,
606            values,
607        }
608    }
609
610    /// Creates a new Update plan.
611    pub fn update(
612        table: String,
613        assignments: Vec<TypedAssignment>,
614        filter: Option<TypedExpr>,
615    ) -> Self {
616        LogicalPlan::Update {
617            table,
618            assignments,
619            filter,
620        }
621    }
622
623    /// Creates a new Delete plan.
624    pub fn delete(table: String, filter: Option<TypedExpr>) -> Self {
625        LogicalPlan::Delete { table, filter }
626    }
627
628    /// Creates a new CreateTable plan.
629    pub fn create_table(
630        table: TableMetadata,
631        if_not_exists: bool,
632        with_options: Vec<(String, String)>,
633    ) -> Self {
634        LogicalPlan::CreateTable {
635            table,
636            if_not_exists,
637            with_options,
638        }
639    }
640
641    /// Creates a new DropTable plan.
642    pub fn drop_table(name: String, if_exists: bool) -> Self {
643        LogicalPlan::DropTable { name, if_exists }
644    }
645
646    /// Creates a new CreateIndex plan.
647    pub fn create_index(index: IndexMetadata, if_not_exists: bool) -> Self {
648        LogicalPlan::CreateIndex {
649            index,
650            if_not_exists,
651        }
652    }
653
654    /// Creates a new DropIndex plan.
655    pub fn drop_index(name: String, if_exists: bool) -> Self {
656        LogicalPlan::DropIndex { name, if_exists }
657    }
658
659    /// Returns the name of this plan variant.
660    pub fn name(&self) -> &'static str {
661        match self {
662            LogicalPlan::Pragma { .. } => "Pragma",
663            LogicalPlan::Scan { .. } => "Scan",
664            LogicalPlan::Values { .. } => "Values",
665            LogicalPlan::Filter { .. } => "Filter",
666            LogicalPlan::Project { .. } => "Project",
667            LogicalPlan::Join { .. } => "Join",
668            LogicalPlan::LateralJoin { .. } => "LateralJoin",
669            LogicalPlan::TableFunction { .. } => "TableFunction",
670            LogicalPlan::Aggregate { .. } => "Aggregate",
671            LogicalPlan::Window { .. } => "Window",
672            LogicalPlan::SetOperation { .. } => "SetOperation",
673            LogicalPlan::RecursiveCte { .. } => "RecursiveCte",
674            LogicalPlan::RecursiveReference { .. } => "RecursiveReference",
675            LogicalPlan::Sort { .. } => "Sort",
676            LogicalPlan::DistinctOn { .. } => "DistinctOn",
677            LogicalPlan::Limit { .. } => "Limit",
678            LogicalPlan::Insert { .. } => "Insert",
679            LogicalPlan::InsertSelect { .. } => "InsertSelect",
680            LogicalPlan::Update { .. } => "Update",
681            LogicalPlan::Delete { .. } => "Delete",
682            LogicalPlan::CreateTable { .. } => "CreateTable",
683            LogicalPlan::DropTable { .. } => "DropTable",
684            LogicalPlan::CreateIndex { .. } => "CreateIndex",
685            LogicalPlan::DropIndex { .. } => "DropIndex",
686        }
687    }
688
689    /// Returns true if this is a query plan (Scan, Filter, Sort, Limit).
690    pub fn is_query(&self) -> bool {
691        matches!(
692            self,
693            LogicalPlan::Scan { .. }
694                | LogicalPlan::Values { .. }
695                | LogicalPlan::Filter { .. }
696                | LogicalPlan::Project { .. }
697                | LogicalPlan::Join { .. }
698                | LogicalPlan::LateralJoin { .. }
699                | LogicalPlan::TableFunction { .. }
700                | LogicalPlan::Aggregate { .. }
701                | LogicalPlan::Window { .. }
702                | LogicalPlan::SetOperation { .. }
703                | LogicalPlan::RecursiveCte { .. }
704                | LogicalPlan::RecursiveReference { .. }
705                | LogicalPlan::Sort { .. }
706                | LogicalPlan::DistinctOn { .. }
707                | LogicalPlan::Limit { .. }
708        )
709    }
710
711    /// Returns true if this is a DML plan (Insert, Update, Delete).
712    pub fn is_dml(&self) -> bool {
713        matches!(
714            self,
715            LogicalPlan::Insert { .. }
716                | LogicalPlan::InsertSelect { .. }
717                | LogicalPlan::Update { .. }
718                | LogicalPlan::Delete { .. }
719        )
720    }
721
722    /// Returns true if this is a DDL plan (CreateTable, DropTable, CreateIndex, DropIndex).
723    pub fn is_ddl(&self) -> bool {
724        matches!(
725            self,
726            LogicalPlan::CreateTable { .. }
727                | LogicalPlan::DropTable { .. }
728                | LogicalPlan::CreateIndex { .. }
729                | LogicalPlan::DropIndex { .. }
730                | LogicalPlan::Pragma { .. }
731        )
732    }
733
734    /// Returns the input plan if this is a transformation (Filter, Aggregate, Sort, Limit).
735    pub fn input(&self) -> Option<&LogicalPlan> {
736        match self {
737            LogicalPlan::Filter { input, .. }
738            | LogicalPlan::Project { input, .. }
739            | LogicalPlan::Aggregate { input, .. }
740            | LogicalPlan::Window { input, .. }
741            | LogicalPlan::Sort { input, .. }
742            | LogicalPlan::DistinctOn { input, .. }
743            | LogicalPlan::Limit { input, .. } => Some(input),
744            LogicalPlan::Join { .. }
745            | LogicalPlan::LateralJoin { .. }
746            | LogicalPlan::TableFunction { .. }
747            | LogicalPlan::Values { .. } => None,
748            LogicalPlan::SetOperation { .. } => None,
749            LogicalPlan::RecursiveCte { .. } | LogicalPlan::RecursiveReference { .. } => None,
750            _ => None,
751        }
752    }
753
754    /// Returns the table name if this plan operates on a single table.
755    pub fn table_name(&self) -> Option<&str> {
756        match self {
757            LogicalPlan::Scan { table, .. }
758            | LogicalPlan::Insert { table, .. }
759            | LogicalPlan::InsertSelect { table, .. }
760            | LogicalPlan::Update { table, .. }
761            | LogicalPlan::Delete { table, .. } => Some(table),
762            LogicalPlan::CreateTable { table, .. } => Some(&table.name),
763            LogicalPlan::DropTable { name, .. } => Some(name),
764            LogicalPlan::CreateIndex { index, .. } => Some(&index.table),
765            LogicalPlan::DropIndex { .. } => None,
766            LogicalPlan::Pragma { .. } => None,
767            LogicalPlan::Values { .. } => None,
768            LogicalPlan::Filter { input, .. }
769            | LogicalPlan::Project { input, .. }
770            | LogicalPlan::Aggregate { input, .. }
771            | LogicalPlan::Window { input, .. }
772            | LogicalPlan::Sort { input, .. }
773            | LogicalPlan::DistinctOn { input, .. }
774            | LogicalPlan::Limit { input, .. } => input.table_name(),
775            LogicalPlan::Join { .. }
776            | LogicalPlan::LateralJoin { .. }
777            | LogicalPlan::TableFunction { .. } => None,
778            LogicalPlan::SetOperation { left, right, .. } => left
779                .table_name()
780                .filter(|name| right.table_name() == Some(*name)),
781            LogicalPlan::RecursiveCte { .. } | LogicalPlan::RecursiveReference { .. } => None,
782        }
783    }
784
785    /// Returns whether this plan tree contains a JOIN boundary.
786    ///
787    /// The normal local planner/executor continues to support JOIN.  Consumers
788    /// with a deliberately closed execution catalog (such as distributed
789    /// reads) can use this structural fact to reject it before any transport is
790    /// opened rather than trying to infer it from a table name.
791    pub fn contains_join(&self) -> bool {
792        match self {
793            LogicalPlan::Join { .. } | LogicalPlan::LateralJoin { .. } => true,
794            LogicalPlan::SetOperation { left, right, .. } => {
795                left.contains_join() || right.contains_join()
796            }
797            LogicalPlan::RecursiveCte {
798                anchor,
799                recursive_term,
800                ..
801            } => anchor.contains_join() || recursive_term.contains_join(),
802            LogicalPlan::Filter { input, .. }
803            | LogicalPlan::Project { input, .. }
804            | LogicalPlan::Aggregate { input, .. }
805            | LogicalPlan::Window { input, .. }
806            | LogicalPlan::Sort { input, .. }
807            | LogicalPlan::DistinctOn { input, .. }
808            | LogicalPlan::Limit { input, .. } => input.contains_join(),
809            _ => false,
810        }
811    }
812
813    /// Returns whether this plan tree contains a set-operation boundary.
814    pub fn contains_set_operation(&self) -> bool {
815        match self {
816            LogicalPlan::SetOperation { .. } | LogicalPlan::RecursiveCte { .. } => true,
817            LogicalPlan::Filter { input, .. }
818            | LogicalPlan::Project { input, .. }
819            | LogicalPlan::Aggregate { input, .. }
820            | LogicalPlan::Sort { input, .. }
821            | LogicalPlan::DistinctOn { input, .. }
822            | LogicalPlan::Limit { input, .. } => input.contains_set_operation(),
823            LogicalPlan::Join { left, right, .. }
824            | LogicalPlan::LateralJoin { left, right, .. } => {
825                left.contains_set_operation() || right.contains_set_operation()
826            }
827            _ => false,
828        }
829    }
830}
831
832#[cfg(test)]
833mod tests {
834    use super::*;
835    use crate::ast::expr::Literal;
836    use crate::ast::span::Span;
837    use crate::catalog::ColumnMetadata;
838    use crate::planner::typed_expr::ProjectedColumn;
839    use crate::planner::types::ResolvedType;
840
841    fn create_test_table_metadata() -> TableMetadata {
842        TableMetadata::new(
843            "users",
844            vec![
845                ColumnMetadata::new("id", ResolvedType::Integer)
846                    .with_primary_key(true)
847                    .with_not_null(true),
848                ColumnMetadata::new("name", ResolvedType::Text).with_not_null(true),
849                ColumnMetadata::new("email", ResolvedType::Text),
850            ],
851        )
852        .with_primary_key(vec!["id".to_string()])
853    }
854
855    #[test]
856    fn test_scan_plan() {
857        let plan = LogicalPlan::scan(
858            "users".to_string(),
859            Projection::All(vec![
860                "id".to_string(),
861                "name".to_string(),
862                "email".to_string(),
863            ]),
864        );
865
866        assert_eq!(plan.name(), "Scan");
867        assert!(plan.is_query());
868        assert!(!plan.is_dml());
869        assert!(!plan.is_ddl());
870        assert_eq!(plan.table_name(), Some("users"));
871        assert!(plan.input().is_none());
872    }
873
874    #[test]
875    fn test_filter_plan() {
876        let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
877        let predicate = TypedExpr::column_ref(
878            "users".to_string(),
879            "id".to_string(),
880            0,
881            ResolvedType::Integer,
882            Span::default(),
883        );
884
885        let plan = LogicalPlan::filter(scan, predicate);
886
887        assert_eq!(plan.name(), "Filter");
888        assert!(plan.is_query());
889        assert!(plan.input().is_some());
890        assert_eq!(plan.table_name(), Some("users"));
891    }
892
893    #[test]
894    fn test_sort_plan() {
895        let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
896        let sort_expr = SortExpr::asc(TypedExpr::column_ref(
897            "users".to_string(),
898            "name".to_string(),
899            1,
900            ResolvedType::Text,
901            Span::default(),
902        ));
903
904        let plan = LogicalPlan::sort(scan, vec![sort_expr]);
905
906        assert_eq!(plan.name(), "Sort");
907        assert!(plan.is_query());
908    }
909
910    #[test]
911    fn test_limit_plan() {
912        let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
913        let plan = LogicalPlan::limit(scan, Some(10), Some(5));
914
915        assert_eq!(plan.name(), "Limit");
916        assert!(plan.is_query());
917
918        if let LogicalPlan::Limit { limit, offset, .. } = &plan {
919            assert_eq!(*limit, Some(10));
920            assert_eq!(*offset, Some(5));
921        } else {
922            panic!("Expected Limit plan");
923        }
924    }
925
926    #[test]
927    fn test_nested_query_plan() {
928        // SELECT * FROM users WHERE id > 5 ORDER BY name LIMIT 10
929        let scan = LogicalPlan::scan(
930            "users".to_string(),
931            Projection::All(vec!["id".to_string(), "name".to_string()]),
932        );
933
934        let predicate = TypedExpr::literal(
935            Literal::Boolean(true),
936            ResolvedType::Boolean,
937            Span::default(),
938        );
939        let filter = LogicalPlan::filter(scan, predicate);
940
941        let sort_expr = SortExpr::asc(TypedExpr::column_ref(
942            "users".to_string(),
943            "name".to_string(),
944            1,
945            ResolvedType::Text,
946            Span::default(),
947        ));
948        let sort = LogicalPlan::sort(filter, vec![sort_expr]);
949
950        let limit = LogicalPlan::limit(sort, Some(10), None);
951
952        // Verify the plan tree
953        assert_eq!(limit.name(), "Limit");
954        assert_eq!(limit.table_name(), Some("users"));
955
956        let sort_plan = limit.input().unwrap();
957        assert_eq!(sort_plan.name(), "Sort");
958
959        let filter_plan = sort_plan.input().unwrap();
960        assert_eq!(filter_plan.name(), "Filter");
961
962        let scan_plan = filter_plan.input().unwrap();
963        assert_eq!(scan_plan.name(), "Scan");
964        assert!(scan_plan.input().is_none());
965    }
966
967    #[test]
968    fn test_insert_plan() {
969        let value1 = TypedExpr::literal(
970            Literal::Number("1".to_string()),
971            ResolvedType::Integer,
972            Span::default(),
973        );
974        let value2 = TypedExpr::literal(
975            Literal::String("Alice".to_string()),
976            ResolvedType::Text,
977            Span::default(),
978        );
979
980        let plan = LogicalPlan::insert(
981            "users".to_string(),
982            vec!["id".to_string(), "name".to_string()],
983            vec![vec![value1, value2]],
984        );
985
986        assert_eq!(plan.name(), "Insert");
987        assert!(plan.is_dml());
988        assert!(!plan.is_query());
989        assert!(!plan.is_ddl());
990        assert_eq!(plan.table_name(), Some("users"));
991
992        if let LogicalPlan::Insert {
993            table,
994            columns,
995            values,
996        } = &plan
997        {
998            assert_eq!(table, "users");
999            assert_eq!(columns, &vec!["id".to_string(), "name".to_string()]);
1000            assert_eq!(values.len(), 1);
1001            assert_eq!(values[0].len(), 2);
1002        } else {
1003            panic!("Expected Insert plan");
1004        }
1005    }
1006
1007    #[test]
1008    fn test_update_plan() {
1009        let assignment = TypedAssignment::new(
1010            "name".to_string(),
1011            1,
1012            TypedExpr::literal(
1013                Literal::String("Bob".to_string()),
1014                ResolvedType::Text,
1015                Span::default(),
1016            ),
1017        );
1018
1019        let filter = TypedExpr::literal(
1020            Literal::Boolean(true),
1021            ResolvedType::Boolean,
1022            Span::default(),
1023        );
1024
1025        let plan = LogicalPlan::update("users".to_string(), vec![assignment], Some(filter));
1026
1027        assert_eq!(plan.name(), "Update");
1028        assert!(plan.is_dml());
1029        assert_eq!(plan.table_name(), Some("users"));
1030    }
1031
1032    #[test]
1033    fn test_delete_plan() {
1034        let filter = TypedExpr::column_ref(
1035            "users".to_string(),
1036            "id".to_string(),
1037            0,
1038            ResolvedType::Integer,
1039            Span::default(),
1040        );
1041
1042        let plan = LogicalPlan::delete("users".to_string(), Some(filter));
1043
1044        assert_eq!(plan.name(), "Delete");
1045        assert!(plan.is_dml());
1046        assert_eq!(plan.table_name(), Some("users"));
1047    }
1048
1049    #[test]
1050    fn test_create_table_plan() {
1051        let table = create_test_table_metadata();
1052        let plan = LogicalPlan::create_table(table, false, vec![]);
1053
1054        assert_eq!(plan.name(), "CreateTable");
1055        assert!(plan.is_ddl());
1056        assert!(!plan.is_dml());
1057        assert!(!plan.is_query());
1058        assert_eq!(plan.table_name(), Some("users"));
1059    }
1060
1061    #[test]
1062    fn test_drop_table_plan() {
1063        let plan = LogicalPlan::drop_table("users".to_string(), true);
1064
1065        assert_eq!(plan.name(), "DropTable");
1066        assert!(plan.is_ddl());
1067        assert_eq!(plan.table_name(), Some("users"));
1068
1069        if let LogicalPlan::DropTable { name, if_exists } = &plan {
1070            assert_eq!(name, "users");
1071            assert!(*if_exists);
1072        } else {
1073            panic!("Expected DropTable plan");
1074        }
1075    }
1076
1077    #[test]
1078    fn test_create_index_plan() {
1079        let index = IndexMetadata::new(0, "idx_users_name", "users", vec!["name".into()]);
1080        let plan = LogicalPlan::create_index(index, false);
1081
1082        assert_eq!(plan.name(), "CreateIndex");
1083        assert!(plan.is_ddl());
1084        assert_eq!(plan.table_name(), Some("users"));
1085    }
1086
1087    #[test]
1088    fn test_drop_index_plan() {
1089        let plan = LogicalPlan::drop_index("idx_users_name".to_string(), false);
1090
1091        assert_eq!(plan.name(), "DropIndex");
1092        assert!(plan.is_ddl());
1093        // DropIndex doesn't have table_name directly
1094        assert!(plan.table_name().is_none());
1095    }
1096
1097    #[test]
1098    fn test_projection_columns() {
1099        let col1 = ProjectedColumn::new(TypedExpr::column_ref(
1100            "users".to_string(),
1101            "id".to_string(),
1102            0,
1103            ResolvedType::Integer,
1104            Span::default(),
1105        ));
1106        let col2 = ProjectedColumn::with_alias(
1107            TypedExpr::column_ref(
1108                "users".to_string(),
1109                "name".to_string(),
1110                1,
1111                ResolvedType::Text,
1112                Span::default(),
1113            ),
1114            "user_name".to_string(),
1115        );
1116
1117        let plan = LogicalPlan::scan("users".to_string(), Projection::Columns(vec![col1, col2]));
1118
1119        if let LogicalPlan::Scan { projection, .. } = &plan {
1120            assert_eq!(projection.len(), 2);
1121        } else {
1122            panic!("Expected Scan plan");
1123        }
1124    }
1125}