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    // JSON
323    JsonType, // json_type(expr) — 'null'|'string'|'number'|'bool'|'object'|'array', Empty when missing
324}
325
326/// Target type for CAST expressions.
327#[derive(Debug, Clone, Copy, PartialEq)]
328pub enum CastType {
329    Int,
330    Float,
331    Str,
332    Bool,
333    DateTime,
334    Uuid,
335    Bytes,
336}
337
338/// A single step in a JSON `->` path expression.
339///
340/// This is the query-crate's OWNED path-segment type (distinct from
341/// [`powdb_storage::pj1::PathSeg`], which borrows `&str` keys). Segments are
342/// STRUCTURAL: they are part of the query shape, never literal slots, so the
343/// plan cache hashes them into the canonical token stream and neither counts
344/// nor substitutes them (see `plan_cache::count_expr` / `substitute_expr`).
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub enum PathSeg {
347    /// Object member access: `->author` or `->"weird key!"`.
348    Key(String),
349    /// Array element access: `->0`.
350    Index(u32),
351}
352
353/// Expressions.
354#[derive(Debug, Clone, PartialEq)]
355pub enum Expr {
356    Field(String),
357    /// A table-qualified field reference: `table.field` or `alias.field`.
358    /// Used by join queries to disambiguate columns that appear in multiple
359    /// sources. The single-table read path never emits this variant, so
360    /// existing fast paths keep matching `Expr::Field` unchanged.
361    QualifiedField {
362        qualifier: String,
363        field: String,
364    },
365    Literal(Literal),
366    Param(String),
367    BinaryOp(Box<Expr>, BinOp, Box<Expr>),
368    UnaryOp(UnaryOp, Box<Expr>),
369    FunctionCall(AggFunc, Box<Expr>),
370    /// Scalar (non-aggregate) function call.
371    ScalarFunc(ScalarFn, Vec<Expr>),
372    Coalesce(Box<Expr>, Box<Expr>),
373    /// `expr in (val1, val2, ...)` or `expr not in (val1, val2, ...)`
374    InList {
375        expr: Box<Expr>,
376        list: Vec<Expr>,
377        negated: bool,
378    },
379    /// `expr [not] in (subquery)` — the subquery is a full QueryExpr
380    /// that produces a single column.
381    InSubquery {
382        expr: Box<Expr>,
383        subquery: Box<QueryExpr>,
384        negated: bool,
385    },
386    /// `[not] exists (subquery)` — the subquery is a full QueryExpr.
387    /// Currently uncorrelated only: the executor runs the subquery once
388    /// before the scan loop and replaces this node with a Bool literal.
389    ExistsSubquery {
390        subquery: Box<QueryExpr>,
391        negated: bool,
392    },
393    /// CASE WHEN ... THEN ... [ELSE ...] END
394    Case {
395        whens: Vec<(Box<Expr>, Box<Expr>)>,
396        else_expr: Option<Box<Expr>>,
397    },
398    /// Window function: `func(args) over (partition ... order ...)`
399    Window {
400        function: WindowFunc,
401        args: Vec<Expr>,
402        partition_by: Vec<String>,
403        order_by: Vec<OrderKey>,
404    },
405    /// Type cast: `cast(expr, "int")` or `cast(expr, "str")` etc.
406    Cast(Box<Expr>, CastType),
407    /// A runtime-materialized literal carrying a concrete Value. Produced only
408    /// during subquery/correlated substitution (post-planning) for values that
409    /// have no Literal form (NULL, datetime, uuid, bytes); never emitted by the
410    /// parser/canonicalizer.
411    ValueLit(Value),
412    /// The `null` literal — produces `Value::Empty`.
413    Null,
414    /// A JSON path access: `base->seg->seg...`. `base` is restricted at parse
415    /// time to `Field`, `QualifiedField`, or (nested) `JsonPath`. Evaluating it
416    /// walks the base `Value::Json` document and scalarizes the addressed node
417    /// (see `eval_expr`); the `segments` are structural (see [`PathSeg`]).
418    JsonPath {
419        base: Box<Expr>,
420        segments: Vec<PathSeg>,
421    },
422}
423
424#[derive(Debug, Clone, PartialEq)]
425pub enum Literal {
426    Int(i64),
427    Float(f64),
428    String(String),
429    Bool(bool),
430}
431
432/// A bound value supplied for a `$N` placeholder in
433/// [`crate::parser::parse_with_params`].
434///
435/// Unlike [`Literal`], this carries a `Null` variant so a parameter can
436/// bind PowQL `null` (substituted as `Token::Null`, not a string). Values
437/// are turned into literal *tokens* before parsing, so an injection-shaped
438/// string is inert data — it can never change the query's shape.
439#[derive(Debug, Clone, PartialEq)]
440pub enum ParamValue {
441    Null,
442    Int(i64),
443    Float(f64),
444    Bool(bool),
445    Str(String),
446}
447
448#[derive(Debug, Clone, Copy, PartialEq)]
449pub enum BinOp {
450    Eq,
451    Neq,
452    Lt,
453    Gt,
454    Lte,
455    Gte,
456    And,
457    Or,
458    Add,
459    Sub,
460    Mul,
461    Div,
462    Like,
463}
464
465#[derive(Debug, Clone, Copy, PartialEq)]
466pub enum UnaryOp {
467    Not,
468    Exists,
469    NotExists,
470    IsNull,
471    IsNotNull,
472}