Skip to main content

powdb_query/
ast.rs

1use powdb_storage::types::Value;
2
3/// Top-level PowQL statement.
4#[derive(Debug, Clone, PartialEq)]
5pub enum Statement {
6    Query(QueryExpr),
7    Insert(InsertExpr),
8    UpdateQuery(UpdateExpr),
9    DeleteQuery(DeleteExpr),
10    CreateType(CreateTypeExpr),
11    AlterTable(AlterTableExpr),
12    DropTable(DropTableExpr),
13    CreateView(CreateViewExpr),
14    RefreshView(RefreshViewExpr),
15    DropView(DropViewExpr),
16    Union(UnionExpr),
17    Upsert(UpsertExpr),
18    Explain(Box<Statement>),
19    Begin,
20    Commit,
21    Rollback,
22    /// `schema` — introspection: list every type (table) in the catalog.
23    ListTypes,
24    /// `describe <Type>` / `schema <Type>` — introspection: the columns and
25    /// indexes of one type.
26    Describe(String),
27}
28
29/// `alter User add column status: str` / `alter User drop column status`
30#[derive(Debug, Clone, PartialEq)]
31pub struct AlterTableExpr {
32    pub table: String,
33    pub action: AlterAction,
34}
35
36/// An individual ALTER TABLE action.
37#[derive(Debug, Clone, PartialEq)]
38pub enum AlterAction {
39    AddColumn {
40        name: String,
41        type_name: String,
42        required: bool,
43    },
44    DropColumn {
45        name: String,
46        /// `drop column if exists` — a missing column is a no-op instead of
47        /// an error.
48        if_exists: bool,
49    },
50    /// `alter <Table> add index [if not exists] .<column>` — creates a
51    /// B+Tree index on `column`. No-op if the index already exists.
52    AddIndex {
53        column: String,
54        /// Parsed for symmetry with the other DDL; `add index` is already
55        /// idempotent, so this does not change behavior.
56        if_not_exists: bool,
57    },
58    /// `alter <Table> add unique [if not exists] .<column>` — creates a
59    /// UNIQUE B+Tree index on `column`. Scans existing data first and fails
60    /// if any duplicate (non-null) value is present. Without `if not exists`
61    /// it errors when the column is already indexed (no in-place upgrade);
62    /// with it, an existing index is a no-op.
63    AddUnique { column: String, if_not_exists: bool },
64}
65
66/// `drop [if exists] User`
67#[derive(Debug, Clone, PartialEq)]
68pub struct DropTableExpr {
69    pub table: String,
70    /// `drop if exists` — a missing table is a no-op instead of an error.
71    pub if_exists: bool,
72}
73
74/// `create [materialized] view ActiveUsers as User filter .active = true`
75#[derive(Debug, Clone, PartialEq)]
76pub struct CreateViewExpr {
77    pub name: String,
78    pub query: QueryExpr,
79    /// The original source query text, stored for re-execution on refresh.
80    pub query_text: String,
81}
82
83/// `refresh ActiveUsers`
84#[derive(Debug, Clone, PartialEq)]
85pub struct RefreshViewExpr {
86    pub name: String,
87}
88
89/// `drop view [if exists] ActiveUsers`
90#[derive(Debug, Clone, PartialEq)]
91pub struct DropViewExpr {
92    pub name: String,
93    /// `drop view if exists` — a missing view is a no-op instead of an error.
94    pub if_exists: bool,
95}
96
97/// `User filter .age > 30 union User filter .status = "vip"`
98#[derive(Debug, Clone, PartialEq)]
99pub struct UnionExpr {
100    pub left: Box<Statement>,
101    pub right: Box<Statement>,
102    /// `true` for `union all` (keep duplicates), `false` for `union` (deduplicate).
103    pub all: bool,
104}
105
106/// A query expression: Type [join ...]* [filter ...] [order ...] [limit ...] [{ projection }]
107#[derive(Debug, Clone, PartialEq)]
108pub struct QueryExpr {
109    pub source: String,
110    /// Optional alias for the primary source (e.g. `User as u`). Used to
111    /// disambiguate qualified column references in join queries. `None` for
112    /// single-table queries.
113    pub alias: Option<String>,
114    /// Zero or more join clauses chained to the primary source. For a
115    /// single-table query this is always empty so existing code paths are
116    /// untouched.
117    pub joins: Vec<JoinClause>,
118    pub filter: Option<Expr>,
119    pub order: Option<OrderClause>,
120    pub limit: Option<Expr>,
121    pub offset: Option<Expr>,
122    pub projection: Option<Vec<ProjectionField>>,
123    pub aggregation: Option<AggregateExpr>,
124    pub distinct: bool,
125    pub group_by: Option<GroupByClause>,
126}
127
128/// GROUP BY clause: `group .field1, alias.field2 [having <expr>]`.
129#[derive(Debug, Clone, PartialEq)]
130pub struct GroupByClause {
131    pub keys: Vec<GroupKey>,
132    pub having: Option<Expr>,
133}
134
135/// A single GROUP BY key.
136///
137/// `Unqualified` covers the classic single-table form `group .status`, whose
138/// field name is resolved against the input columns by an exact match first
139/// and a unique `.field` suffix match second (so it also works over a join
140/// whose output columns are named `alias.field`).
141///
142/// `Qualified` covers the join form `group u.status`; it resolves exactly to
143/// the `alias.field` output column and never suffix-matches.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum GroupKey {
146    Unqualified(String),
147    Qualified { alias: String, field: String },
148}
149
150impl GroupKey {
151    /// Name of the output column this key produces. Unqualified keys keep
152    /// their bare field name; qualified keys are emitted as `alias.field` so
153    /// HAVING and downstream projections can reference them consistently.
154    pub fn output_name(&self) -> String {
155        match self {
156            GroupKey::Unqualified(field) => field.clone(),
157            GroupKey::Qualified { alias, field } => format!("{alias}.{field}"),
158        }
159    }
160}
161
162/// A join clause appended to a query's primary source.
163///
164/// Example syntax (Mission E1.1 parser accepts this; executor still errors):
165///   `User as u inner join Order as o on u.id = o.user_id filter o.total > 100`
166#[derive(Debug, Clone, PartialEq)]
167pub struct JoinClause {
168    pub kind: JoinKind,
169    pub source: String,
170    pub alias: Option<String>,
171    /// `on <expr>` — required for every kind except `Cross`.
172    pub on: Option<Expr>,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum JoinKind {
177    Inner,
178    LeftOuter,
179    RightOuter,
180    Cross,
181}
182
183#[derive(Debug, Clone, PartialEq)]
184pub struct ProjectionField {
185    pub alias: Option<String>,
186    pub expr: Expr,
187}
188
189#[derive(Debug, Clone, PartialEq)]
190pub struct OrderClause {
191    pub keys: Vec<OrderKey>,
192}
193
194#[derive(Debug, Clone, PartialEq)]
195pub struct OrderKey {
196    pub field: String,
197    pub descending: bool,
198}
199
200#[derive(Debug, Clone, PartialEq)]
201pub struct InsertExpr {
202    pub target: String,
203    /// One assignment-block per row. Always contains at least one row;
204    /// `insert T { .. }` yields one, `insert T { .. }, { .. }` yields many.
205    pub rows: Vec<Vec<Assignment>>,
206    /// `true` when the statement ends with `returning` — the executor returns
207    /// the inserted rows (all columns) instead of a modified-count.
208    pub returning: bool,
209}
210
211#[derive(Debug, Clone, PartialEq)]
212pub struct UpdateExpr {
213    pub source: String,
214    pub filter: Option<Expr>,
215    pub assignments: Vec<Assignment>,
216    /// `true` when the statement ends with `returning` — the executor returns
217    /// the post-update rows (all columns) instead of a modified-count.
218    pub returning: bool,
219}
220
221#[derive(Debug, Clone, PartialEq)]
222pub struct DeleteExpr {
223    pub source: String,
224    pub filter: Option<Expr>,
225    /// `true` when the statement ends with `returning` — the executor returns
226    /// the pre-delete rows (all columns) instead of a modified-count.
227    pub returning: bool,
228}
229
230#[derive(Debug, Clone, PartialEq)]
231pub struct Assignment {
232    pub field: String,
233    pub value: Expr,
234}
235
236#[derive(Debug, Clone, PartialEq)]
237/// `upsert User on .id { id := 1, name := "Alice" } [on conflict { name := "Alice" }]`
238pub struct UpsertExpr {
239    pub target: String,
240    pub key_column: String,
241    pub assignments: Vec<Assignment>,
242    /// Assignments to apply on conflict. If empty, all non-key assignments
243    /// from `assignments` are used as the update set.
244    pub on_conflict: Vec<Assignment>,
245}
246
247#[derive(Debug, Clone, PartialEq)]
248pub struct CreateTypeExpr {
249    pub name: String,
250    pub fields: Vec<FieldDef>,
251    /// `type X if not exists { ... }` — re-declaring an existing type is a
252    /// no-op instead of an error.
253    pub if_not_exists: bool,
254}
255
256#[derive(Debug, Clone, PartialEq)]
257pub struct FieldDef {
258    pub name: String,
259    pub type_name: String,
260    pub required: bool,
261    /// `true` when declared with the `unique` modifier — auto-creates a
262    /// unique B+Tree index on this column at table-create time.
263    pub unique: bool,
264    /// Literal default applied when an insert omits this column. `None` means
265    /// the column has no default (omitting it yields the empty set / null).
266    pub default: Option<Literal>,
267    /// `true` when declared `auto` — an integer column whose value is assigned
268    /// from a monotonic per-table sequence when an insert omits it.
269    pub auto: bool,
270}
271
272#[derive(Debug, Clone, PartialEq)]
273pub struct AggregateExpr {
274    pub function: AggFunc,
275    pub field: Option<String>,
276}
277
278#[derive(Debug, Clone, Copy, PartialEq)]
279pub enum AggFunc {
280    Count,
281    CountDistinct,
282    Avg,
283    Sum,
284    Min,
285    Max,
286}
287
288/// Window function identifier.
289#[derive(Debug, Clone, Copy, PartialEq)]
290pub enum WindowFunc {
291    RowNumber,
292    Rank,
293    DenseRank,
294    Sum,
295    Avg,
296    Count,
297    Min,
298    Max,
299}
300
301/// Scalar (non-aggregate) function — operates on single values.
302#[derive(Debug, Clone, Copy, PartialEq)]
303pub enum ScalarFn {
304    Upper,
305    Lower,
306    Length,
307    Trim,
308    Substring, // substring(expr, start, len) — 1-indexed
309    Concat,    // concat(expr, expr, ...) — variadic
310    // Math
311    Abs,
312    Round, // round(expr) or round(expr, decimals)
313    Ceil,
314    Floor,
315    Sqrt,
316    Pow, // pow(base, exponent)
317    // Date/time
318    Now,      // now() — returns current unix timestamp in microseconds
319    Extract,  // extract("year"|"month"|..., datetime_expr)
320    DateAdd,  // date_add(datetime_expr, amount, "unit")
321    DateDiff, // date_diff(dt1, dt2, "unit")
322}
323
324/// Target type for CAST expressions.
325#[derive(Debug, Clone, Copy, PartialEq)]
326pub enum CastType {
327    Int,
328    Float,
329    Str,
330    Bool,
331    DateTime,
332    Uuid,
333    Bytes,
334}
335
336/// Expressions.
337#[derive(Debug, Clone, PartialEq)]
338pub enum Expr {
339    Field(String),
340    /// A table-qualified field reference: `table.field` or `alias.field`.
341    /// Used by join queries to disambiguate columns that appear in multiple
342    /// sources. The single-table read path never emits this variant, so
343    /// existing fast paths keep matching `Expr::Field` unchanged.
344    QualifiedField {
345        qualifier: String,
346        field: String,
347    },
348    Literal(Literal),
349    Param(String),
350    BinaryOp(Box<Expr>, BinOp, Box<Expr>),
351    UnaryOp(UnaryOp, Box<Expr>),
352    FunctionCall(AggFunc, Box<Expr>),
353    /// Scalar (non-aggregate) function call.
354    ScalarFunc(ScalarFn, Vec<Expr>),
355    Coalesce(Box<Expr>, Box<Expr>),
356    /// `expr in (val1, val2, ...)` or `expr not in (val1, val2, ...)`
357    InList {
358        expr: Box<Expr>,
359        list: Vec<Expr>,
360        negated: bool,
361    },
362    /// `expr [not] in (subquery)` — the subquery is a full QueryExpr
363    /// that produces a single column.
364    InSubquery {
365        expr: Box<Expr>,
366        subquery: Box<QueryExpr>,
367        negated: bool,
368    },
369    /// `[not] exists (subquery)` — the subquery is a full QueryExpr.
370    /// Currently uncorrelated only: the executor runs the subquery once
371    /// before the scan loop and replaces this node with a Bool literal.
372    ExistsSubquery {
373        subquery: Box<QueryExpr>,
374        negated: bool,
375    },
376    /// CASE WHEN ... THEN ... [ELSE ...] END
377    Case {
378        whens: Vec<(Box<Expr>, Box<Expr>)>,
379        else_expr: Option<Box<Expr>>,
380    },
381    /// Window function: `func(args) over (partition ... order ...)`
382    Window {
383        function: WindowFunc,
384        args: Vec<Expr>,
385        partition_by: Vec<String>,
386        order_by: Vec<OrderKey>,
387    },
388    /// Type cast: `cast(expr, "int")` or `cast(expr, "str")` etc.
389    Cast(Box<Expr>, CastType),
390    /// A runtime-materialized literal carrying a concrete Value. Produced only
391    /// during subquery/correlated substitution (post-planning) for values that
392    /// have no Literal form (NULL, datetime, uuid, bytes); never emitted by the
393    /// parser/canonicalizer.
394    ValueLit(Value),
395    /// The `null` literal — produces `Value::Empty`.
396    Null,
397}
398
399#[derive(Debug, Clone, PartialEq)]
400pub enum Literal {
401    Int(i64),
402    Float(f64),
403    String(String),
404    Bool(bool),
405}
406
407/// A bound value supplied for a `$N` placeholder in
408/// [`crate::parser::parse_with_params`].
409///
410/// Unlike [`Literal`], this carries a `Null` variant so a parameter can
411/// bind PowQL `null` (substituted as `Token::Null`, not a string). Values
412/// are turned into literal *tokens* before parsing, so an injection-shaped
413/// string is inert data — it can never change the query's shape.
414#[derive(Debug, Clone, PartialEq)]
415pub enum ParamValue {
416    Null,
417    Int(i64),
418    Float(f64),
419    Bool(bool),
420    Str(String),
421}
422
423#[derive(Debug, Clone, Copy, PartialEq)]
424pub enum BinOp {
425    Eq,
426    Neq,
427    Lt,
428    Gt,
429    Lte,
430    Gte,
431    And,
432    Or,
433    Add,
434    Sub,
435    Mul,
436    Div,
437    Like,
438}
439
440#[derive(Debug, Clone, Copy, PartialEq)]
441pub enum UnaryOp {
442    Not,
443    Exists,
444    NotExists,
445    IsNull,
446    IsNotNull,
447}