Skip to main content

powdb_query/
plan.rs

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