Skip to main content

powdb_query/
plan.rs

1use crate::ast::{AggFunc, AlterAction, Assignment, Expr, GroupKey, JoinKind, Literal, WindowFunc};
2
3/// A column definition carried by `PlanNode::CreateTable`. Replaces the
4/// old `(name, type_name, required)` tuple so the `unique` modifier can
5/// flow from the parser through to the executor's DDL arm.
6#[derive(Debug, Clone)]
7pub struct CreateField {
8    pub name: String,
9    pub type_name: String,
10    pub required: bool,
11    pub unique: bool,
12    /// Literal default applied when an insert omits this column.
13    pub default: Option<Literal>,
14    /// `true` when declared `auto` — integer column auto-assigned from a
15    /// per-table sequence when an insert omits it.
16    pub auto: bool,
17}
18
19/// Physical plan nodes — what the executor actually runs.
20#[derive(Debug, Clone)]
21pub enum PlanNode {
22    SeqScan {
23        table: String,
24    },
25    /// Mission E1.2: sequential scan that renames output columns to
26    /// `alias.field`. Used exclusively as the leaves of a join plan so
27    /// downstream `NestedLoopJoin` + `Filter` + `Project` nodes can resolve
28    /// `Expr::QualifiedField` lookups by direct column-name match. Kept
29    /// separate from `SeqScan` so the single-table fast paths (which match
30    /// on `PlanNode::SeqScan { .. }` in many places) stay untouched.
31    AliasScan {
32        table: String,
33        alias: String,
34    },
35    IndexScan {
36        table: String,
37        column: String,
38        key: Expr,
39    },
40    /// B+tree range scan: returns rows where the indexed column falls within
41    /// the given bounds. Generated by the planner when it detects inequality
42    /// predicates (>, >=, <, <=, BETWEEN) on an indexed column. The executor
43    /// falls back to SeqScan+Filter if no index exists on the column.
44    RangeScan {
45        table: String,
46        column: String,
47        /// Lower bound: (expr, inclusive). None = unbounded below.
48        start: Option<(Expr, bool)>,
49        /// Upper bound: (expr, inclusive). None = unbounded above.
50        end: Option<(Expr, bool)>,
51    },
52    Filter {
53        input: Box<PlanNode>,
54        predicate: Expr,
55    },
56    Project {
57        input: Box<PlanNode>,
58        fields: Vec<ProjectField>,
59    },
60    Sort {
61        input: Box<PlanNode>,
62        keys: Vec<SortKey>,
63    },
64    Limit {
65        input: Box<PlanNode>,
66        count: Expr,
67    },
68    Offset {
69        input: Box<PlanNode>,
70        count: Expr,
71    },
72    Aggregate {
73        input: Box<PlanNode>,
74        function: AggFunc,
75        field: Option<String>,
76    },
77    /// Mission E1.2: nested-loop join. Correctness-first implementation —
78    /// O(L × R) scan for every join. E1.3 will add a hash-join fast path
79    /// for equijoins (the common case). The executor handles `Inner`,
80    /// `Cross`, and `LeftOuter`; `RightOuter` is rewritten by the planner
81    /// into a `LeftOuter` with swapped inputs.
82    NestedLoopJoin {
83        left: Box<PlanNode>,
84        right: Box<PlanNode>,
85        /// Join predicate. `None` for `Cross` joins (emit every pair).
86        on: Option<Expr>,
87        kind: JoinKind,
88    },
89    Distinct {
90        input: Box<PlanNode>,
91    },
92    /// Mission E2b: grouped aggregation. Output columns are
93    /// `keys ++ [agg.output_name for agg in aggregates]`. The optional
94    /// `having` predicate is evaluated against each output row *after*
95    /// aggregation — it can reference both key columns and aggregate
96    /// output names (the planner rewrites `FunctionCall` nodes in the
97    /// HAVING expression into `Field("__agg_N")` references).
98    GroupBy {
99        input: Box<PlanNode>,
100        keys: Vec<GroupKey>,
101        aggregates: Vec<GroupAgg>,
102        having: Option<Expr>,
103    },
104    AlterTable {
105        table: String,
106        action: AlterAction,
107    },
108    DropTable {
109        name: String,
110        /// `drop if exists` — a missing table is a no-op instead of an error.
111        if_exists: bool,
112    },
113    Insert {
114        table: String,
115        /// One assignment-block per row to insert. Always at least one.
116        rows: Vec<Vec<Assignment>>,
117        /// `true` when `returning` was requested — executor returns the
118        /// inserted rows instead of a modified-count.
119        returning: bool,
120    },
121    /// UPSERT: probe index on `key_column` — if miss, insert; if hit, update.
122    Upsert {
123        table: String,
124        key_column: String,
125        assignments: Vec<Assignment>,
126        on_conflict: Vec<Assignment>,
127    },
128    Update {
129        input: Box<PlanNode>,
130        table: String,
131        assignments: Vec<Assignment>,
132        /// `true` when `returning` was requested — executor returns the
133        /// post-update rows instead of a modified-count.
134        returning: bool,
135    },
136    Delete {
137        input: Box<PlanNode>,
138        table: String,
139        /// `true` when `returning` was requested — executor returns the
140        /// pre-delete rows instead of a modified-count.
141        returning: bool,
142    },
143    CreateTable {
144        name: String,
145        fields: Vec<CreateField>,
146        /// `type X if not exists { ... }` — re-declaring an existing type is
147        /// a no-op instead of an error.
148        if_not_exists: bool,
149    },
150    /// `schema` — list every type (table) in the catalog. Read-only; reads
151    /// live catalog state at execution time, so a cached plan is never stale.
152    ListTypes,
153    /// `describe <Type>` — the columns and indexes of one type. Read-only.
154    Describe {
155        table: String,
156    },
157    /// Create a materialized view: execute query, store results, register.
158    CreateView {
159        name: String,
160        query_text: String,
161    },
162    /// Explicitly refresh a materialized view.
163    RefreshView {
164        name: String,
165    },
166    /// Drop a materialized view (backing table + registry entry).
167    DropView {
168        name: String,
169        /// `drop view if exists` — a missing view is a no-op instead of an error.
170        if_exists: bool,
171    },
172    /// Window function computation layer.
173    Window {
174        input: Box<PlanNode>,
175        windows: Vec<WindowDef>,
176    },
177    /// `UNION [ALL]`: execute both sides, concatenate (ALL) or deduplicate.
178    Union {
179        left: Box<PlanNode>,
180        right: Box<PlanNode>,
181        all: bool,
182    },
183    /// EXPLAIN: format the inner plan tree as a text result without executing.
184    Explain {
185        input: Box<PlanNode>,
186    },
187    Begin,
188    Commit,
189    Rollback,
190}
191
192#[derive(Debug, Clone)]
193pub struct ProjectField {
194    pub alias: Option<String>,
195    pub expr: Expr,
196}
197
198#[derive(Debug, Clone)]
199pub struct SortKey {
200    pub field: String,
201    pub descending: bool,
202}
203
204/// One aggregate computation inside a `PlanNode::GroupBy`.
205#[derive(Debug, Clone)]
206pub struct GroupAgg {
207    pub function: AggFunc,
208    /// Source column name to aggregate over.
209    pub field: String,
210    /// Synthetic output column name (`__agg_0`, `__agg_1`, …).
211    pub output_name: String,
212}
213
214/// One window function definition inside a `PlanNode::Window`.
215#[derive(Debug, Clone)]
216pub struct WindowDef {
217    pub function: WindowFunc,
218    pub args: Vec<Expr>,
219    pub partition_by: Vec<String>,
220    pub order_by: Vec<SortKey>,
221    pub output_name: String,
222}