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}