Skip to main content

pylon_core/parse/
ast.rs

1//
2// This source file is part of the Pylon open source project.
3//
4// Copyright (c) 2026 Jaldis B.V.
5//
6// Licensed under the MIT OR Apache-2.0 license (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     https://opensource.org/licenses/MIT
11//     https://www.apache.org/licenses/LICENSE-2.0
12//
13// Unless required by applicable law or agreed to in writing, software
14// distributed under the License is distributed on an "AS IS" BASIS,
15// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16// See the License for the specific language governing permissions and
17// limitations under the License.
18//
19
20// AST nodes for PyQL queries.
21
22// ── Statements ─────────────────────────────────────────────────────────────────
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum Stmt {
26    Select(SelectStmt),
27    Insert(InsertStmt),
28    Update(UpdateStmt),
29    Delete(DeleteStmt),
30    Group(GroupStmt),
31    /// `with alias := (stmt), ... main_stmt`
32    With(WithStmt),
33    /// `for var in iterator union body`
34    For(ForStmt),
35    /// `analyze <stmt>` — run the inner statement through Postgres's
36    /// EXPLAIN ANALYZE and report a query plan instead of the inner
37    /// statement's own result.
38    Analyze(Box<Stmt>),
39}
40
41#[derive(Debug, Clone, PartialEq)]
42pub struct GroupStmt {
43    /// The type or expression being grouped.
44    pub subject: Expr,
45    /// Optional shape restricting which fields appear in `elements`.
46    pub shape: Option<Vec<ShapeElement>>,
47    /// `USING alias := expr, ...` bindings.
48    pub using: Vec<(String, Expr)>,
49    /// `BY expr, ...` — each item is an Ident (using-alias ref) or a partial Path (.prop).
50    pub by: Vec<Expr>,
51    /// `FILTER expr` — restricts the rows that are grouped, applied before
52    /// grouping (so it decides membership, not which groups survive).
53    pub filter: Option<Expr>,
54    /// `ORDER BY ...` / `OFFSET` / `LIMIT` — these apply *within* each
55    /// group, to its elements: `group Reading by .sensor order by .taken_at
56    /// desc limit 1` is the newest reading per sensor.
57    pub order_by: Vec<SortExpr>,
58    pub offset: Option<Expr>,
59    pub limit: Option<Expr>,
60}
61
62#[derive(Debug, Clone, PartialEq)]
63pub struct WithStmt {
64    pub aliases: Vec<CteDef>,
65    pub stmt: Box<Stmt>,
66}
67
68/// One `name := (inner_stmt)` binding in a WITH block.
69#[derive(Debug, Clone, PartialEq)]
70pub struct CteDef {
71    pub name: String,
72    pub expr: Expr,
73}
74
75/// `for [optional] var in iterator union body`
76#[derive(Debug, Clone, PartialEq)]
77pub struct ForStmt {
78    pub var: String,
79    pub optional: bool,
80    pub iterator: Expr,
81    pub body: Box<Stmt>,
82}
83
84#[derive(Debug, Clone, PartialEq)]
85pub struct SelectStmt {
86    pub result: Expr,
87    pub filter: Option<Expr>,
88    pub order_by: Vec<SortExpr>,
89    pub offset: Option<Expr>,
90    pub limit: Option<Expr>,
91    /// Trailing `FOR UPDATE`/`FOR SHARE`/... row-locking clause — Postgres's
92    /// own grammar places this last, after `ORDER BY`/`LIMIT`/`OFFSET`, not
93    /// right after `WHERE`.
94    pub lock: Option<LockClause>,
95}
96
97#[derive(Debug, Clone, PartialEq)]
98pub struct LockClause {
99    pub strength: LockStrength,
100    pub wait: LockWait,
101}
102
103#[derive(Debug, Clone, PartialEq)]
104pub enum LockStrength {
105    Update,
106    NoKeyUpdate,
107    Share,
108    KeyShare,
109}
110
111#[derive(Debug, Clone, PartialEq)]
112pub enum LockWait {
113    /// No modifier — the default Postgres behavior of blocking until the
114    /// lock is available.
115    Block,
116    NoWait,
117    SkipLocked,
118}
119
120#[derive(Debug, Clone, PartialEq)]
121pub struct InsertStmt {
122    pub subject: ObjectRef,
123    pub shape: Vec<ShapeElement>,
124    pub unless_conflict: Option<UnlessConflict>,
125}
126
127#[derive(Debug, Clone, PartialEq)]
128pub struct UnlessConflict {
129    pub on: Option<Expr>,
130    pub else_: Option<Expr>,
131}
132
133#[derive(Debug, Clone, PartialEq)]
134pub struct UpdateStmt {
135    pub subject: Expr,
136    pub filter: Option<Expr>,
137    pub shape: Vec<ShapeElement>,
138}
139
140#[derive(Debug, Clone, PartialEq)]
141pub struct DeleteStmt {
142    pub subject: Expr,
143    pub filter: Option<Expr>,
144}
145
146// ── Expressions ────────────────────────────────────────────────────────────────
147
148#[derive(Debug, Clone, PartialEq)]
149pub enum Expr {
150    Path(Path),
151    Shape(Box<ShapeExpr>),
152    BinOp(Box<BinOp>),
153    UnaryOp(Box<UnaryOp>),
154    FunctionCall(FunctionCall),
155    TypeCast(Box<TypeCast>),
156    IfElse(Box<IfElse>),
157    Literal(Literal),
158    Parameter(String),
159    Tuple(Vec<Expr>),
160    NamedTuple(Vec<(String, Expr)>),
161    Array(Vec<Expr>),
162    /// A set literal: `{1, 2, 'hello'}`. Multiple values produce multiple rows.
163    Set(Vec<Expr>),
164    /// A parenthesised statement used as an expression:
165    /// `(INSERT ...)`, `(UPDATE ...)`, `(DELETE ...)`, `(SELECT ...)`.
166    SubQuery(Box<Stmt>),
167    /// Binary set union: `expr union expr` — compiles to UNION ALL.
168    Union(Box<Expr>, Box<Expr>),
169    /// Binary set difference: `expr except expr` — rows in the left operand
170    /// not present in the right, compiled to `EXCEPT`.
171    Except(Box<Expr>, Box<Expr>),
172    Intersect(Box<Expr>, Box<Expr>),
173    /// A global variable reference: `global name` or `global module::name`.
174    Global(String),
175    /// Index access: `expr[i]` (0-based).
176    Index {
177        expr: Box<Expr>,
178        index: Box<Expr>,
179    },
180    /// Slice access: `expr[lower:upper]` (0-based, either bound may be absent).
181    Slice {
182        expr: Box<Expr>,
183        lower: Option<Box<Expr>>,
184        upper: Option<Box<Expr>>,
185    },
186    /// Named tuple field access on a non-path expression: `(name := 'a', age := 1).name`.
187    FieldAccess {
188        expr: Box<Expr>,
189        field: String,
190    },
191    /// A path step applied to something that is not itself a path — the
192    /// `[is Individual]` in `(select Installation limit 1).provider[is
193    /// Individual].staff`, which is legal. `.name` on such a base is
194    /// already a `FieldAccess`; this carries the steps that have no
195    /// expression form of their own (a type intersection, a link property, a
196    /// backlink). The compiler rewrites a chain of these into an ordinary
197    /// `Path` rooted at a binding, so nothing downstream has to know about it.
198    PathStepOn {
199        expr: Box<Expr>,
200        step: Box<PathStep>,
201    },
202    /// Positional tuple element on a non-path expression: `(1, 3.14, 'red').2`.
203    TupleIndex {
204        expr: Box<Expr>,
205        index: usize,
206    },
207    /// `detached expr` — evaluate `expr` independently of the current implicit scope.
208    Detached(Box<Expr>),
209    /// `expr is TypeName` — runtime type check; returns bool.
210    TypeIs {
211        expr: Box<Expr>,
212        ty: TypeExpr,
213    },
214}
215
216// ── Paths ──────────────────────────────────────────────────────────────────────
217
218/// A traversal from a root type or property/link through zero or more steps.
219/// `partial = true` when the path starts with `.`, meaning it is relative to
220/// the current object (__subject__) rather than an absolute type reference.
221#[derive(Debug, Clone, PartialEq)]
222pub struct Path {
223    pub steps: Vec<PathStep>,
224    pub partial: bool,
225}
226
227impl Path {
228    pub fn absolute(name: impl Into<String>) -> Self {
229        Path {
230            steps: vec![PathStep::Name(name.into())],
231            partial: false,
232        }
233    }
234
235    pub fn relative(name: impl Into<String>) -> Self {
236        Path {
237            steps: vec![PathStep::Name(name.into())],
238            partial: true,
239        }
240    }
241}
242
243#[derive(Debug, Clone, PartialEq)]
244pub enum PathStep {
245    /// A property or link name: `.name`, `posts`
246    Name(String),
247    /// Type intersection filter: `[is TypeName]`
248    TypeIntersection(ObjectRef),
249    /// Link property access: `@source` in a link context
250    LinkProp(String),
251    /// Backlink traversal: `.<link_name` — objects whose `link_name` points to the current object
252    Backlink(String),
253}
254
255#[derive(Debug, Clone, PartialEq)]
256pub struct ObjectRef {
257    pub module: Option<String>,
258    pub name: String,
259}
260
261impl ObjectRef {
262    pub fn unqualified(name: impl Into<String>) -> Self {
263        ObjectRef {
264            module: None,
265            name: name.into(),
266        }
267    }
268    pub fn qualified(module: impl Into<String>, name: impl Into<String>) -> Self {
269        ObjectRef {
270            module: Some(module.into()),
271            name: name.into(),
272        }
273    }
274    /// `module::Name` when the reference carries a module. Dropping it leaves
275    /// a short name, and two modules may declare the same one.
276    pub fn qualified_name(&self) -> String {
277        match &self.module {
278            Some(module) => format!("{module}::{}", self.name),
279            None => self.name.clone(),
280        }
281    }
282}
283
284// ── Shape operators ────────────────────────────────────────────────────────────
285
286/// The assignment operator used on a shape element in UPDATE SET { ... }.
287/// Only relevant for multi-link fields; scalar/single-link fields always use Assign.
288#[derive(Debug, Clone, PartialEq)]
289pub enum ShapeOp {
290    /// `:=` — replace the entire value (clear + insert for multi-links).
291    Assign,
292    /// `+=` — append to a multi-link set.
293    Append,
294    /// `-=` — remove from a multi-link set.
295    Remove,
296}
297
298// ── Shapes ─────────────────────────────────────────────────────────────────────
299
300/// A shape expression: `Expr { element, element, ... }`.
301/// `expr = None` only in INSERT bodies where the subject is implicit.
302#[derive(Debug, Clone, PartialEq)]
303pub struct ShapeExpr {
304    pub expr: Option<Expr>,
305    pub elements: Vec<ShapeElement>,
306    /// Source byte offset of `expr`'s first token, if any — used only to
307    /// place the root `analyze` marker in the echoed query text (see
308    /// `analyze.rs`); not populated (and not needed) outside that path.
309    pub marker_offset: Option<usize>,
310}
311
312#[derive(Debug, Clone, PartialEq)]
313pub enum Splat {
314    /// `*` — expand to all scalar properties.
315    Shallow,
316    /// `**` — expand to all scalar properties and all single links (with implicit `{ id }`).
317    Deep,
318}
319
320#[derive(Debug, Clone, PartialEq)]
321pub struct ShapeElement {
322    /// Relative path being shaped, e.g. `.name` or `.posts`.
323    pub path: Path,
324    /// When set, this element is a wildcard expansion rather than a named field.
325    pub splat: Option<Splat>,
326    /// Nested shape for links: `.posts { title, body }`.
327    pub nested: Option<Vec<ShapeElement>>,
328    /// Computed override or assignment: `.total := .price * .qty` or `friends += expr`.
329    pub compexpr: Option<Expr>,
330    /// Assignment operator (only meaningful for UPDATE SET elements on multi-links).
331    pub op: ShapeOp,
332    /// Per-element link modifiers.
333    pub filter: Option<Expr>,
334    pub order_by: Vec<SortExpr>,
335    pub offset: Option<Expr>,
336    pub limit: Option<Expr>,
337    /// Source byte offset of this element's first token — used only to place
338    /// per-pointer `analyze` markers in the echoed query text (see
339    /// `analyze.rs`); not populated (and not needed) outside that path.
340    pub marker_offset: Option<usize>,
341}
342
343impl ShapeElement {
344    pub fn splat(kind: Splat) -> Self {
345        ShapeElement {
346            path: Path {
347                steps: vec![],
348                partial: true,
349            },
350            splat: Some(kind),
351            nested: None,
352            compexpr: None,
353            op: ShapeOp::Assign,
354            filter: None,
355            order_by: vec![],
356            offset: None,
357            limit: None,
358            marker_offset: None,
359        }
360    }
361}
362
363// ── Operators ──────────────────────────────────────────────────────────────────
364
365#[derive(Debug, Clone, PartialEq)]
366pub struct BinOp {
367    pub left: Expr,
368    pub op: BinOpKind,
369    pub right: Expr,
370}
371
372#[derive(Debug, Clone, PartialEq)]
373pub enum BinOpKind {
374    Add,
375    Sub,
376    Mul,
377    Div,
378    FloorDiv,
379    Mod,
380    Pow,
381    Eq,
382    Ne,
383    Lt,
384    Le,
385    Gt,
386    Ge,
387    And,
388    Or,
389    Like,
390    Ilike,
391    NotLike,
392    NotIlike,
393    In,
394    NotIn,
395    Coalesce,
396    /// `?=` / `?!=` — equality that treats an absent value as comparable
397    /// rather than unknown, which is `IS [NOT] DISTINCT FROM` in SQL.
398    CoalesceEq,
399    CoalesceNe,
400    Concat,
401}
402
403impl std::fmt::Display for BinOpKind {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        f.write_str(match self {
406            Self::Add => "+",
407            Self::Sub => "-",
408            Self::Mul => "*",
409            Self::Div => "/",
410            Self::FloorDiv => "//",
411            Self::Mod => "%",
412            Self::Pow => "^",
413            Self::Eq => "=",
414            Self::Ne => "!=",
415            Self::CoalesceEq => "?=",
416            Self::CoalesceNe => "?!=",
417            Self::Lt => "<",
418            Self::Le => "<=",
419            Self::Gt => ">",
420            Self::Ge => ">=",
421            Self::And => "and",
422            Self::Or => "or",
423            Self::Like => "like",
424            Self::Ilike => "ilike",
425            Self::NotLike => "not like",
426            Self::NotIlike => "not ilike",
427            Self::In => "in",
428            Self::NotIn => "not in",
429            Self::Coalesce => "??",
430            Self::Concat => "++",
431        })
432    }
433}
434
435#[derive(Debug, Clone, PartialEq)]
436pub struct UnaryOp {
437    pub op: UnaryOpKind,
438    pub operand: Expr,
439}
440
441#[derive(Debug, Clone, PartialEq)]
442pub enum UnaryOpKind {
443    Not,
444    Minus,
445    Exists,
446    Distinct,
447}
448
449// ── Function calls ─────────────────────────────────────────────────────────────
450
451#[derive(Debug, Clone, PartialEq)]
452pub struct FunctionCall {
453    pub module: Option<String>,
454    pub name: String,
455    pub args: Vec<Expr>,
456    pub kwargs: Vec<(String, Expr)>,
457}
458
459// ── Type cast ──────────────────────────────────────────────────────────────────
460
461#[derive(Debug, Clone, PartialEq)]
462pub struct TypeCast {
463    pub expr: Expr,
464    pub ty: TypeExpr,
465}
466
467/// A type expression appearing in a cast (`<TypeExpr>expr`) or a type-is check
468/// (`expr[is TypeExpr]`, `expr IS TypeExpr`).
469#[derive(Debug, Clone, PartialEq)]
470pub enum TypeExpr {
471    /// A named/qualified type reference: `str`, `default::Point`, `cal::local_date`.
472    Named { module: Option<String>, name: String },
473    /// A structural tuple type: `tuple<str, bool>` or `tuple<r: int16, g: int16>`.
474    /// Elements are either all-named or all-unnamed (checked at parse time) and may
475    /// nest arbitrarily (an element's own `ty` can itself be `TypeExpr::Tuple`).
476    Tuple { elements: Vec<TupleTypeElement> },
477    /// A one-dimensional array type: `array<str>`. The element type may be
478    /// anything except another array (checked at parse time) — Pylon arrays
479    /// are always one-dimensional, matching a plain Postgres `T[]` column.
480    Array { element: Box<TypeExpr> },
481}
482
483#[derive(Debug, Clone, PartialEq)]
484pub struct TupleTypeElement {
485    pub name: Option<String>,
486    pub ty: Box<TypeExpr>,
487}
488
489impl TypeExpr {
490    /// Convenience constructor for the common unqualified-name case (matches the
491    /// old plain-struct call sites before `TypeExpr` became an enum).
492    pub fn named(module: Option<String>, name: impl Into<String>) -> Self {
493        TypeExpr::Named {
494            module,
495            name: name.into(),
496        }
497    }
498
499    /// `(module, name)` for a `Named` type expr; `None` for `Tuple`/`Array` —
500    /// used by the object-type-cast / `IS` / type-intersection contexts, which
501    /// only ever mean something for a named schema type (never a structural
502    /// tuple or array).
503    pub fn as_named(&self) -> Option<(Option<&str>, &str)> {
504        match self {
505            TypeExpr::Named { module, name } => Some((module.as_deref(), name.as_str())),
506            TypeExpr::Tuple { .. } | TypeExpr::Array { .. } => None,
507        }
508    }
509}
510
511// ── If / Else ──────────────────────────────────────────────────────────────────
512
513/// PyQL `expr IF cond ELSE expr` ternary.
514#[derive(Debug, Clone, PartialEq)]
515pub struct IfElse {
516    pub if_expr: Expr,
517    pub condition: Expr,
518    pub else_expr: Expr,
519}
520
521// ── Literals ───────────────────────────────────────────────────────────────────
522
523#[derive(Debug, Clone, PartialEq)]
524pub enum Literal {
525    Str(String),
526    Int(i64),
527    Float(f64),
528    Bool(bool),
529}
530
531// ── Sort ───────────────────────────────────────────────────────────────────────
532
533#[derive(Debug, Clone, PartialEq)]
534pub struct SortExpr {
535    pub expr: Expr,
536    pub direction: SortDirection,
537    pub nones: NonesOrder,
538}
539
540#[derive(Debug, Clone, PartialEq)]
541pub enum SortDirection {
542    Asc,
543    Desc,
544}
545
546#[derive(Debug, Clone, PartialEq)]
547pub enum NonesOrder {
548    First,
549    Last,
550}