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}