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    /// `schema links`: list every declared entity link. Read-only; reads
211    /// live catalog state at execution time, so a cached plan is never stale.
212    ListLinks,
213    /// Create a materialized view: execute query, store results, register.
214    CreateView {
215        name: String,
216        query_text: String,
217    },
218    /// Explicitly refresh a materialized view.
219    RefreshView {
220        name: String,
221    },
222    /// Drop a materialized view (backing table + registry entry).
223    DropView {
224        name: String,
225        /// `drop view if exists` — a missing view is a no-op instead of an error.
226        if_exists: bool,
227    },
228    /// Window function computation layer.
229    Window {
230        input: Box<PlanNode>,
231        windows: Vec<WindowDef>,
232    },
233    /// `UNION [ALL]`: execute both sides, concatenate (ALL) or deduplicate.
234    Union {
235        left: Box<PlanNode>,
236        right: Box<PlanNode>,
237        all: bool,
238    },
239    /// EXPLAIN: format the inner plan tree as a text result without executing.
240    Explain {
241        input: Box<PlanNode>,
242    },
243    Begin,
244    Commit,
245    Rollback,
246}
247
248#[derive(Debug, Clone)]
249pub struct ProjectField {
250    pub alias: Option<String>,
251    pub expr: Expr,
252}
253
254/// One output column of a `PlanNode::NestedProject`.
255#[derive(Debug, Clone)]
256pub enum NestedProjectField {
257    /// A scalar field, evaluated against the parent row like `Project`.
258    Plain(ProjectField),
259    /// A nested sub-query field, emitted as a PJ1 JSON array of objects.
260    Nested(Box<NestedProjection>),
261    /// A scalar link traversal field (`o.user.name`), emitted as the
262    /// traversed target column's value (or empty when the FK is NULL or
263    /// dangling at any hop).
264    Link(Box<ScalarLinkField>),
265}
266
267/// A scalar link traversal projection field. Like `via_link` on
268/// [`NestedProjection`], the planner cannot resolve link names (it never
269/// touches the catalog), so `resolved` is `None` out of the planner and is
270/// filled in by the executor before assembly.
271#[derive(Debug, Clone)]
272pub struct ScalarLinkField {
273    /// Output column name (the field alias, or the dotted source spelling).
274    pub name: String,
275    /// The outer scan alias the path starts from (`o`).
276    pub outer_alias: String,
277    /// Declared to-one link names in traversal order (`["user", "company"]`).
278    pub links: Vec<String>,
279    /// The column read on the final target (`name`).
280    pub column: String,
281    /// Filled by the executor's catalog resolution; `None` out of the planner.
282    pub resolved: Option<ScalarLinkResolved>,
283}
284
285/// The catalog-resolved form of a scalar link path.
286#[derive(Debug, Clone)]
287pub struct ScalarLinkResolved {
288    /// The FK column on the parent scan, alias-qualified as the parent's
289    /// `AliasScan` emits it (`o.user_id`).
290    pub first_fk: String,
291    /// One hop per link name, in traversal order.
292    pub hops: Vec<ScalarLinkHop>,
293}
294
295/// One resolved hop of a scalar link path: look the incoming FK value up by
296/// `key_col` in `table`, then read `out_col` (the next hop's FK column, or the
297/// final target column on the last hop).
298#[derive(Debug, Clone)]
299pub struct ScalarLinkHop {
300    pub table: String,
301    pub key_col: String,
302    pub out_col: String,
303}
304
305/// The resolved form of one nested sub-query projection field. Correlation
306/// columns are pre-split by the planner: at the top level `parent_key` is
307/// the qualified parent column name (`u.id`) as produced by an `AliasScan`;
308/// for a nested-in-nested field it is the bare column name of the enclosing
309/// child table. `child_key` is always the bare child column name.
310#[derive(Debug, Clone)]
311pub struct NestedProjection {
312    /// Output column name (the projection field's alias).
313    pub name: String,
314    pub table: String,
315    /// Set when this level came from a block link traversal
316    /// (`orders: u.orders { ... }`). While set, `table`/`child_key`/
317    /// `parent_key` are placeholders; the executor resolves them from the
318    /// persistent catalog before assembly (like `RangeScan` late lowering)
319    /// and clears this. The planner never touches the catalog, so it cannot
320    /// resolve it.
321    pub via_link: Option<ViaLink>,
322    /// The child alias from the source text, kept for EXPLAIN and errors.
323    pub alias: String,
324    /// The enclosing scope's alias, kept for EXPLAIN (the executor
325    /// correlates through `parent_key` alone).
326    pub parent_alias: String,
327    pub child_key: String,
328    pub parent_key: String,
329    /// Residual filter conditions beyond the correlation predicate,
330    /// rewritten by the planner to reference bare child columns.
331    pub residual: Option<Expr>,
332    /// Per-parent ordering: `(bare child column, descending)` keys applied
333    /// to each parent's child bucket before truncation.
334    pub order: Vec<(String, bool)>,
335    /// Per-parent truncation bounds, applied after ordering. Must evaluate
336    /// to non-negative integer literals (like top-level limit/offset).
337    pub limit: Option<Expr>,
338    pub offset: Option<Expr>,
339    /// Source wrote `offset` before `limit`; the plan cache refuses this
340    /// form (see `plan_cache::nested_projection_defeats_cache`).
341    pub offset_before_limit: bool,
342    pub fields: Vec<NestedField>,
343}
344
345/// One entry in a nested projection's output object.
346#[derive(Debug, Clone)]
347pub enum NestedField {
348    /// A scalar child column emitted under `key`.
349    Scalar { key: String, column: String },
350    /// A deeper nested sub-query, correlated to this child table by its
351    /// (bare) `parent_key` column.
352    Nested(Box<NestedProjection>),
353}
354
355impl NestedProjection {
356    /// Visit this projection's child table and every deeper nested child
357    /// table. Used for dirty-view escalation and auto-refresh.
358    pub fn visit_tables(&self, visit: &mut dyn FnMut(&str)) {
359        visit(&self.table);
360        for field in &self.fields {
361            if let NestedField::Nested(inner) = field {
362                inner.visit_tables(visit);
363            }
364        }
365    }
366}
367
368#[derive(Debug, Clone)]
369pub struct SortKey {
370    pub expr: Expr,
371    pub descending: bool,
372}
373
374/// One aggregate computation inside a `PlanNode::GroupBy`.
375#[derive(Debug, Clone)]
376pub struct GroupAgg {
377    pub function: AggFunc,
378    pub argument: Expr,
379    pub mode: AggregateMode,
380    /// Source alias whose physical row identity controls symmetric
381    /// de-duplication. `None` means raw aggregation semantics.
382    pub provenance_alias: Option<String>,
383    /// Synthetic output column name (`__agg_0`, `__agg_1`, …).
384    pub output_name: String,
385}
386
387/// One window function definition inside a `PlanNode::Window`.
388#[derive(Debug, Clone)]
389pub struct WindowDef {
390    pub function: WindowFunc,
391    pub args: Vec<Expr>,
392    pub mode: AggregateMode,
393    pub partition_by: Vec<Expr>,
394    pub order_by: Vec<SortKey>,
395    pub output_name: String,
396}