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 Sort {
89 input: Box<PlanNode>,
90 keys: Vec<SortKey>,
91 },
92 Limit {
93 input: Box<PlanNode>,
94 count: Expr,
95 },
96 Offset {
97 input: Box<PlanNode>,
98 count: Expr,
99 },
100 Aggregate {
101 input: Box<PlanNode>,
102 function: AggFunc,
103 argument: Option<Expr>,
104 mode: AggregateMode,
105 /// Source alias whose physical row identity controls symmetric
106 /// de-duplication. `None` means raw aggregation semantics.
107 provenance_alias: Option<String>,
108 },
109 /// Mission E1.2: nested-loop join. Correctness-first implementation —
110 /// O(L × R) scan for every join. E1.3 will add a hash-join fast path
111 /// for equijoins (the common case). The executor handles `Inner`,
112 /// `Cross`, and `LeftOuter`; `RightOuter` is rewritten by the planner
113 /// into a `LeftOuter` with swapped inputs.
114 NestedLoopJoin {
115 left: Box<PlanNode>,
116 right: Box<PlanNode>,
117 /// Join predicate. `None` for `Cross` joins (emit every pair).
118 on: Option<Expr>,
119 kind: JoinKind,
120 },
121 Distinct {
122 input: Box<PlanNode>,
123 },
124 /// Mission E2b: grouped aggregation. Output columns are
125 /// `keys ++ [agg.output_name for agg in aggregates]`. The optional
126 /// `having` predicate is evaluated against each output row *after*
127 /// aggregation — it can reference both key columns and aggregate
128 /// output names (the planner rewrites `FunctionCall` nodes in the
129 /// HAVING expression into `Field("__agg_N")` references).
130 GroupBy {
131 input: Box<PlanNode>,
132 keys: Vec<GroupKey>,
133 aggregates: Vec<GroupAgg>,
134 having: Option<Expr>,
135 },
136 AlterTable {
137 table: String,
138 action: AlterAction,
139 },
140 DropTable {
141 name: String,
142 /// `drop if exists` — a missing table is a no-op instead of an error.
143 if_exists: bool,
144 },
145 Insert {
146 table: String,
147 /// One assignment-block per row to insert. Always at least one.
148 rows: Vec<Vec<Assignment>>,
149 /// `true` when `returning` was requested — executor returns the
150 /// inserted rows instead of a modified-count.
151 returning: bool,
152 },
153 /// UPSERT: probe index on `key_column` — if miss, insert; if hit, update.
154 Upsert {
155 table: String,
156 key_column: String,
157 assignments: Vec<Assignment>,
158 on_conflict: Vec<Assignment>,
159 },
160 Update {
161 input: Box<PlanNode>,
162 table: String,
163 assignments: Vec<Assignment>,
164 /// `true` when `returning` was requested — executor returns the
165 /// post-update rows instead of a modified-count.
166 returning: bool,
167 },
168 Delete {
169 input: Box<PlanNode>,
170 table: String,
171 /// `true` when `returning` was requested — executor returns the
172 /// pre-delete rows instead of a modified-count.
173 returning: bool,
174 },
175 CreateTable {
176 name: String,
177 fields: Vec<CreateField>,
178 /// `type X if not exists { ... }` — re-declaring an existing type is
179 /// a no-op instead of an error.
180 if_not_exists: bool,
181 },
182 /// `schema` — list every type (table) in the catalog. Read-only; reads
183 /// live catalog state at execution time, so a cached plan is never stale.
184 ListTypes,
185 /// `describe <Type>` — the columns and indexes of one type. Read-only.
186 Describe {
187 table: String,
188 },
189 /// Create a materialized view: execute query, store results, register.
190 CreateView {
191 name: String,
192 query_text: String,
193 },
194 /// Explicitly refresh a materialized view.
195 RefreshView {
196 name: String,
197 },
198 /// Drop a materialized view (backing table + registry entry).
199 DropView {
200 name: String,
201 /// `drop view if exists` — a missing view is a no-op instead of an error.
202 if_exists: bool,
203 },
204 /// Window function computation layer.
205 Window {
206 input: Box<PlanNode>,
207 windows: Vec<WindowDef>,
208 },
209 /// `UNION [ALL]`: execute both sides, concatenate (ALL) or deduplicate.
210 Union {
211 left: Box<PlanNode>,
212 right: Box<PlanNode>,
213 all: bool,
214 },
215 /// EXPLAIN: format the inner plan tree as a text result without executing.
216 Explain {
217 input: Box<PlanNode>,
218 },
219 Begin,
220 Commit,
221 Rollback,
222}
223
224#[derive(Debug, Clone)]
225pub struct ProjectField {
226 pub alias: Option<String>,
227 pub expr: Expr,
228}
229
230#[derive(Debug, Clone)]
231pub struct SortKey {
232 pub expr: Expr,
233 pub descending: bool,
234}
235
236/// One aggregate computation inside a `PlanNode::GroupBy`.
237#[derive(Debug, Clone)]
238pub struct GroupAgg {
239 pub function: AggFunc,
240 pub argument: Expr,
241 pub mode: AggregateMode,
242 /// Source alias whose physical row identity controls symmetric
243 /// de-duplication. `None` means raw aggregation semantics.
244 pub provenance_alias: Option<String>,
245 /// Synthetic output column name (`__agg_0`, `__agg_1`, …).
246 pub output_name: String,
247}
248
249/// One window function definition inside a `PlanNode::Window`.
250#[derive(Debug, Clone)]
251pub struct WindowDef {
252 pub function: WindowFunc,
253 pub args: Vec<Expr>,
254 pub mode: AggregateMode,
255 pub partition_by: Vec<Expr>,
256 pub order_by: Vec<SortKey>,
257 pub output_name: String,
258}