Skip to main content

powdb_query/
plan.rs

1use crate::ast::{
2    AggFunc, AggregateMode, AlterAction, Assignment, Expr, GroupKey, JoinKind, Literal, WindowFunc,
3};
4use powdb_storage::stored_json_path::StoredJsonPathV1;
5
6/// A column definition carried by `PlanNode::CreateTable`. Replaces the
7/// old `(name, type_name, required)` tuple so the `unique` modifier can
8/// flow from the parser through to the executor's DDL arm.
9#[derive(Debug, Clone)]
10pub struct CreateField {
11    pub name: String,
12    pub type_name: String,
13    pub required: bool,
14    pub unique: bool,
15    /// Literal default applied when an insert omits this column.
16    pub default: Option<Literal>,
17    /// `true` when declared `auto` — integer column auto-assigned from a
18    /// per-table sequence when an insert omits it.
19    pub auto: bool,
20}
21
22/// Physical plan nodes — what the executor actually runs.
23#[derive(Debug, Clone)]
24pub enum PlanNode {
25    SeqScan {
26        table: String,
27    },
28    /// Mission E1.2: sequential scan that renames output columns to
29    /// `alias.field`. Used exclusively as the leaves of a join plan so
30    /// downstream `NestedLoopJoin` + `Filter` + `Project` nodes can resolve
31    /// `Expr::QualifiedField` lookups by direct column-name match. Kept
32    /// separate from `SeqScan` so the single-table fast paths (which match
33    /// on `PlanNode::SeqScan { .. }` in many places) stay untouched.
34    AliasScan {
35        table: String,
36        alias: String,
37    },
38    IndexScan {
39        table: String,
40        column: String,
41        key: Expr,
42    },
43    /// B+tree range scan: returns rows where the indexed column falls within
44    /// the given bounds. Generated by the planner when it detects inequality
45    /// predicates (>, >=, <, <=, BETWEEN) on an indexed column. The executor
46    /// falls back to SeqScan+Filter if no index exists on the column.
47    RangeScan {
48        table: String,
49        column: String,
50        /// Lower bound: (expr, inclusive). None = unbounded below.
51        start: Option<(Expr, bool)>,
52        /// Upper bound: (expr, inclusive). None = unbounded above.
53        end: Option<(Expr, bool)>,
54    },
55    /// Speculative equality lookup through an expression index. The planner is
56    /// catalog-pure; the executor resolves `path` at runtime and reconstructs
57    /// the equivalent path predicate if no matching index exists.
58    ExprIndexScan {
59        table: String,
60        path: StoredJsonPathV1,
61        key: Expr,
62    },
63    /// Speculative bounded lookup through an expression index. `path` is the
64    /// persisted table-local identity, never a runtime catalog identifier.
65    ExprRangeScan {
66        table: String,
67        path: StoredJsonPathV1,
68        start: Option<(Expr, bool)>,
69        end: Option<(Expr, bool)>,
70    },
71    /// Exact single-path ORDER BY + LIMIT/OFFSET candidate. Missing or
72    /// incompatible runtime indexes fall back to a normal scan/sort/slice.
73    OrderedExprIndexScan {
74        table: String,
75        path: StoredJsonPathV1,
76        descending: bool,
77        limit: Expr,
78        offset: Option<Expr>,
79    },
80    Filter {
81        input: Box<PlanNode>,
82        predicate: Expr,
83    },
84    Project {
85        input: Box<PlanNode>,
86        fields: Vec<ProjectField>,
87    },
88    /// Language-lab slice: projection with one or more nested sub-query
89    /// fields. `input` is the parent pipeline (AliasScan-based so qualified
90    /// references resolve by column name); each nested field is assembled by
91    /// a single hash-build pass over its child table, O(parent + child).
92    /// Kept separate from `Project` so the single-table fast paths and the
93    /// plan cache never see this shape.
94    NestedProject {
95        input: Box<PlanNode>,
96        fields: Vec<NestedProjectField>,
97    },
98    Sort {
99        input: Box<PlanNode>,
100        keys: Vec<SortKey>,
101    },
102    Limit {
103        input: Box<PlanNode>,
104        count: Expr,
105    },
106    Offset {
107        input: Box<PlanNode>,
108        count: Expr,
109    },
110    Aggregate {
111        input: Box<PlanNode>,
112        function: AggFunc,
113        argument: Option<Expr>,
114        mode: AggregateMode,
115        /// Source alias whose physical row identity controls symmetric
116        /// de-duplication. `None` means raw aggregation semantics.
117        provenance_alias: Option<String>,
118    },
119    /// Mission E1.2: nested-loop join. Correctness-first implementation —
120    /// O(L × R) scan for every join. E1.3 will add a hash-join fast path
121    /// for equijoins (the common case). The executor handles `Inner`,
122    /// `Cross`, and `LeftOuter`; `RightOuter` is rewritten by the planner
123    /// into a `LeftOuter` with swapped inputs.
124    NestedLoopJoin {
125        left: Box<PlanNode>,
126        right: Box<PlanNode>,
127        /// Join predicate. `None` for `Cross` joins (emit every pair).
128        on: Option<Expr>,
129        kind: JoinKind,
130    },
131    Distinct {
132        input: Box<PlanNode>,
133    },
134    /// Mission E2b: grouped aggregation. Output columns are
135    /// `keys ++ [agg.output_name for agg in aggregates]`. The optional
136    /// `having` predicate is evaluated against each output row *after*
137    /// aggregation — it can reference both key columns and aggregate
138    /// output names (the planner rewrites `FunctionCall` nodes in the
139    /// HAVING expression into `Field("__agg_N")` references).
140    GroupBy {
141        input: Box<PlanNode>,
142        keys: Vec<GroupKey>,
143        aggregates: Vec<GroupAgg>,
144        having: Option<Expr>,
145    },
146    AlterTable {
147        table: String,
148        action: AlterAction,
149    },
150    DropTable {
151        name: String,
152        /// `drop if exists` — a missing table is a no-op instead of an error.
153        if_exists: bool,
154    },
155    Insert {
156        table: String,
157        /// One assignment-block per row to insert. Always at least one.
158        rows: Vec<Vec<Assignment>>,
159        /// `true` when `returning` was requested — executor returns the
160        /// inserted rows instead of a modified-count.
161        returning: bool,
162    },
163    /// UPSERT: probe index on `key_column` — if miss, insert; if hit, update.
164    Upsert {
165        table: String,
166        key_column: String,
167        assignments: Vec<Assignment>,
168        on_conflict: Vec<Assignment>,
169    },
170    Update {
171        input: Box<PlanNode>,
172        table: String,
173        assignments: Vec<Assignment>,
174        /// `true` when `returning` was requested — executor returns the
175        /// post-update rows instead of a modified-count.
176        returning: bool,
177    },
178    Delete {
179        input: Box<PlanNode>,
180        table: String,
181        /// `true` when `returning` was requested — executor returns the
182        /// pre-delete rows instead of a modified-count.
183        returning: bool,
184    },
185    CreateTable {
186        name: String,
187        fields: Vec<CreateField>,
188        /// `type X if not exists { ... }` — re-declaring an existing type is
189        /// a no-op instead of an error.
190        if_not_exists: bool,
191    },
192    /// `schema` — list every type (table) in the catalog. Read-only; reads
193    /// live catalog state at execution time, so a cached plan is never stale.
194    ListTypes,
195    /// `describe <Type>` — the columns and indexes of one type. Read-only.
196    Describe {
197        table: String,
198    },
199    /// Create a materialized view: execute query, store results, register.
200    CreateView {
201        name: String,
202        query_text: String,
203    },
204    /// Explicitly refresh a materialized view.
205    RefreshView {
206        name: String,
207    },
208    /// Drop a materialized view (backing table + registry entry).
209    DropView {
210        name: String,
211        /// `drop view if exists` — a missing view is a no-op instead of an error.
212        if_exists: bool,
213    },
214    /// Window function computation layer.
215    Window {
216        input: Box<PlanNode>,
217        windows: Vec<WindowDef>,
218    },
219    /// `UNION [ALL]`: execute both sides, concatenate (ALL) or deduplicate.
220    Union {
221        left: Box<PlanNode>,
222        right: Box<PlanNode>,
223        all: bool,
224    },
225    /// EXPLAIN: format the inner plan tree as a text result without executing.
226    Explain {
227        input: Box<PlanNode>,
228    },
229    Begin,
230    Commit,
231    Rollback,
232}
233
234#[derive(Debug, Clone)]
235pub struct ProjectField {
236    pub alias: Option<String>,
237    pub expr: Expr,
238}
239
240/// One output column of a `PlanNode::NestedProject`.
241#[derive(Debug, Clone)]
242pub enum NestedProjectField {
243    /// A scalar field, evaluated against the parent row like `Project`.
244    Plain(ProjectField),
245    /// A nested sub-query field, emitted as a PJ1 JSON array of objects.
246    Nested(Box<NestedProjection>),
247}
248
249/// The resolved form of one nested sub-query projection field. Correlation
250/// columns are pre-split by the planner: at the top level `parent_key` is
251/// the qualified parent column name (`u.id`) as produced by an `AliasScan`;
252/// for a nested-in-nested field it is the bare column name of the enclosing
253/// child table. `child_key` is always the bare child column name.
254#[derive(Debug, Clone)]
255pub struct NestedProjection {
256    /// Output column name (the projection field's alias).
257    pub name: String,
258    pub table: String,
259    /// The child alias from the source text, kept for EXPLAIN and errors.
260    pub alias: String,
261    /// The enclosing scope's alias, kept for EXPLAIN (the executor
262    /// correlates through `parent_key` alone).
263    pub parent_alias: String,
264    pub child_key: String,
265    pub parent_key: String,
266    /// Residual filter conditions beyond the correlation predicate,
267    /// rewritten by the planner to reference bare child columns.
268    pub residual: Option<Expr>,
269    /// Per-parent ordering: `(bare child column, descending)` keys applied
270    /// to each parent's child bucket before truncation.
271    pub order: Vec<(String, bool)>,
272    /// Per-parent truncation bounds, applied after ordering. Must evaluate
273    /// to non-negative integer literals (like top-level limit/offset).
274    pub limit: Option<Expr>,
275    pub offset: Option<Expr>,
276    /// Source wrote `offset` before `limit`; the plan cache refuses this
277    /// form (see `plan_cache::nested_projection_defeats_cache`).
278    pub offset_before_limit: bool,
279    pub fields: Vec<NestedField>,
280}
281
282/// One entry in a nested projection's output object.
283#[derive(Debug, Clone)]
284pub enum NestedField {
285    /// A scalar child column emitted under `key`.
286    Scalar { key: String, column: String },
287    /// A deeper nested sub-query, correlated to this child table by its
288    /// (bare) `parent_key` column.
289    Nested(Box<NestedProjection>),
290}
291
292impl NestedProjection {
293    /// Visit this projection's child table and every deeper nested child
294    /// table. Used for dirty-view escalation and auto-refresh.
295    pub fn visit_tables(&self, visit: &mut dyn FnMut(&str)) {
296        visit(&self.table);
297        for field in &self.fields {
298            if let NestedField::Nested(inner) = field {
299                inner.visit_tables(visit);
300            }
301        }
302    }
303}
304
305#[derive(Debug, Clone)]
306pub struct SortKey {
307    pub expr: Expr,
308    pub descending: bool,
309}
310
311/// One aggregate computation inside a `PlanNode::GroupBy`.
312#[derive(Debug, Clone)]
313pub struct GroupAgg {
314    pub function: AggFunc,
315    pub argument: Expr,
316    pub mode: AggregateMode,
317    /// Source alias whose physical row identity controls symmetric
318    /// de-duplication. `None` means raw aggregation semantics.
319    pub provenance_alias: Option<String>,
320    /// Synthetic output column name (`__agg_0`, `__agg_1`, …).
321    pub output_name: String,
322}
323
324/// One window function definition inside a `PlanNode::Window`.
325#[derive(Debug, Clone)]
326pub struct WindowDef {
327    pub function: WindowFunc,
328    pub args: Vec<Expr>,
329    pub mode: AggregateMode,
330    pub partition_by: Vec<Expr>,
331    pub order_by: Vec<SortKey>,
332    pub output_name: String,
333}