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