Skip to main content

graphforge_ast/
ast.rs

1//! Syntax-faithful, span-rich AST for openCypher queries.
2//!
3//! Produced by `graphforge-cypher`; consumed by `graphforge-ir` (the binder). No other crate
4//! should depend on this module directly.
5//!
6//! Every public type is `Clone + Debug + PartialEq + Serialize + Deserialize`
7//! so the differential test harness can round-trip ASTs through JSON and
8//! compare parser outputs.
9//!
10//! Struct fields carry `span: Span` to locate source positions — doc comments
11//! on these repetitive span fields are omitted intentionally.
12#![allow(missing_docs)]
13
14use graphforge_core::Span;
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17
18// ---------------------------------------------------------------------------
19// Top-level query
20// ---------------------------------------------------------------------------
21
22/// openCypher dialect version.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
24pub enum DialectVersion {
25    /// openCypher 9 (the baseline target for GraphForge).
26    #[default]
27    OpenCypher9,
28}
29
30/// A fully parsed Cypher query.
31///
32/// At the skeleton stage `clauses` may be empty; the real parser populates it.
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct AstQuery {
35    /// Cypher dialect in use.
36    pub dialect: DialectVersion,
37    /// Top-level clauses in source order.
38    pub clauses: Vec<AstClause>,
39    /// Span covering the full query source.
40    pub span: Span,
41}
42
43// ---------------------------------------------------------------------------
44// Clause variants
45// ---------------------------------------------------------------------------
46
47/// A top-level clause in a Cypher query.
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49#[non_exhaustive]
50pub enum AstClause {
51    Match(MatchClause),
52    OptionalMatch(MatchClause),
53    Where(WhereClause),
54    With(WithClause),
55    Return(ReturnClause),
56    Create(CreateClause),
57    Merge(MergeClause),
58    Set(SetClause),
59    Remove(RemoveClause),
60    Delete(DeleteClause),
61    Unwind(UnwindClause),
62    Call(CallClause),
63    Union(UnionClause),
64}
65
66impl AstClause {
67    /// Span of the clause keyword plus its body.
68    #[must_use]
69    pub fn span(&self) -> Span {
70        match self {
71            Self::Match(c) | Self::OptionalMatch(c) => c.span,
72            Self::Where(c) => c.span,
73            Self::With(c) => c.span,
74            Self::Return(c) => c.span,
75            Self::Create(c) => c.span,
76            Self::Merge(c) => c.span,
77            Self::Set(c) => c.span,
78            Self::Remove(c) => c.span,
79            Self::Delete(c) => c.span,
80            Self::Unwind(c) => c.span,
81            Self::Call(c) => c.span,
82            Self::Union(c) => c.span,
83        }
84    }
85}
86
87// ---------------------------------------------------------------------------
88// MATCH
89// ---------------------------------------------------------------------------
90
91/// A `MATCH` or `OPTIONAL MATCH` clause.
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93pub struct MatchClause {
94    /// The path patterns being matched.
95    pub patterns: Vec<PathPattern>,
96    /// Optional inline `WHERE` predicate.
97    pub where_clause: Option<WhereClause>,
98    pub span: Span,
99}
100
101// ---------------------------------------------------------------------------
102// WHERE
103// ---------------------------------------------------------------------------
104
105/// A `WHERE` clause (also used inline in MATCH/WITH).
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107pub struct WhereClause {
108    pub predicate: Expr,
109    pub span: Span,
110}
111
112// ---------------------------------------------------------------------------
113// WITH
114// ---------------------------------------------------------------------------
115
116/// A `WITH` clause (pipeline stage with optional ORDER/SKIP/LIMIT/WHERE).
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct WithClause {
119    pub distinct: bool,
120    pub items: Vec<ReturnItem>,
121    pub order_by: Option<OrderByClause>,
122    pub skip: Option<Expr>,
123    pub limit: Option<Expr>,
124    pub where_clause: Option<WhereClause>,
125    pub span: Span,
126}
127
128// ---------------------------------------------------------------------------
129// RETURN
130// ---------------------------------------------------------------------------
131
132/// A `RETURN` clause.
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134pub struct ReturnClause {
135    pub distinct: bool,
136    pub items: Vec<ReturnItem>,
137    pub order_by: Option<OrderByClause>,
138    pub skip: Option<Expr>,
139    pub limit: Option<Expr>,
140    pub span: Span,
141}
142
143/// A single item in a `RETURN` or `WITH` projection.
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct ReturnItem {
146    pub expr: Expr,
147    /// Optional `AS alias` name.
148    pub alias: Option<String>,
149    /// The verbatim source text of `expr` (no `AS` part), captured by the parser
150    /// for an un-aliased item. openCypher names an un-aliased projection column by
151    /// the expression as written (`n.prop`, `count(*)`, `a.x IS NULL`); the binder
152    /// uses this as the default `RETURN` column name when there is no `alias`.
153    /// `None` for `RETURN *` and for items built outside the parser.
154    #[serde(default)]
155    pub display: Option<String>,
156    pub span: Span,
157}
158
159// ---------------------------------------------------------------------------
160// ORDER BY
161// ---------------------------------------------------------------------------
162
163/// An `ORDER BY` sub-clause carrying one or more sort keys.
164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct OrderByClause {
166    pub items: Vec<SortItem>,
167    pub span: Span,
168}
169
170/// A single sort key.
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub struct SortItem {
173    pub expr: Expr,
174    pub order: SortOrder,
175    pub span: Span,
176}
177
178/// Sort direction.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
180pub enum SortOrder {
181    #[default]
182    Ascending,
183    Descending,
184}
185
186// ---------------------------------------------------------------------------
187// CREATE / MERGE
188// ---------------------------------------------------------------------------
189
190/// A `CREATE` clause.
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct CreateClause {
193    pub patterns: Vec<PathPattern>,
194    pub span: Span,
195}
196
197/// A `MERGE` clause (with optional ON CREATE / ON MATCH SET actions).
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub struct MergeClause {
200    pub pattern: PathPattern,
201    pub on_create: Vec<SetItem>,
202    pub on_match: Vec<SetItem>,
203    pub span: Span,
204}
205
206// ---------------------------------------------------------------------------
207// SET / REMOVE / DELETE
208// ---------------------------------------------------------------------------
209
210/// A `SET` clause.
211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
212pub struct SetClause {
213    pub items: Vec<SetItem>,
214    pub span: Span,
215}
216
217/// A single item inside a SET clause.
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219#[non_exhaustive]
220pub enum SetItem {
221    /// `n.prop = expr`
222    Property {
223        target: PropertyAccess,
224        value: Expr,
225        span: Span,
226    },
227    /// `n += {map}` (property merge)
228    PropertyMerge { var: String, map: Expr, span: Span },
229    /// `n = {map}` (property replace)
230    PropertyReplace { var: String, map: Expr, span: Span },
231    /// `n:Label` (add label)
232    Label {
233        var: String,
234        labels: Vec<String>,
235        span: Span,
236    },
237}
238
239/// A `REMOVE` clause.
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241pub struct RemoveClause {
242    pub items: Vec<RemoveItem>,
243    pub span: Span,
244}
245
246/// A single item inside a `REMOVE` clause.
247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
248#[non_exhaustive]
249pub enum RemoveItem {
250    /// `n.prop`
251    Property(PropertyAccess, Span),
252    /// `n:Label`
253    Label {
254        var: String,
255        labels: Vec<String>,
256        span: Span,
257    },
258}
259
260/// A `DELETE` or `DETACH DELETE` clause.
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
262pub struct DeleteClause {
263    pub detach: bool,
264    pub exprs: Vec<Expr>,
265    pub span: Span,
266}
267
268// ---------------------------------------------------------------------------
269// UNWIND
270// ---------------------------------------------------------------------------
271
272/// An `UNWIND` clause.
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274pub struct UnwindClause {
275    pub expr: Expr,
276    pub alias: String,
277    pub span: Span,
278}
279
280// ---------------------------------------------------------------------------
281// CALL
282// ---------------------------------------------------------------------------
283
284/// A `CALL procedure YIELD ...` clause.
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286pub struct CallClause {
287    pub procedure: Vec<String>,
288    pub args: Vec<Expr>,
289    /// Whether the call included an explicit parenthesized argument list.
290    #[serde(default)]
291    pub args_explicit: bool,
292    pub yield_items: Vec<ReturnItem>,
293    pub span: Span,
294}
295
296// ---------------------------------------------------------------------------
297// UNION
298// ---------------------------------------------------------------------------
299
300/// A `UNION` or `UNION ALL` clause joining two query halves.
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302pub struct UnionClause {
303    pub all: bool,
304    pub span: Span,
305}
306
307// ---------------------------------------------------------------------------
308// Path patterns
309// ---------------------------------------------------------------------------
310
311/// A path pattern: one or more alternating node/relationship patterns.
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
313pub struct PathPattern {
314    /// Optional `variable = ...` path binding.
315    pub var: Option<String>,
316    /// Elements: must start and end with a `NodePattern`, with
317    /// `RelPattern` in between.
318    pub elements: Vec<PathElement>,
319    pub span: Span,
320}
321
322/// One element in a path pattern.
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
324pub enum PathElement {
325    Node(NodePattern),
326    Rel(RelPattern),
327}
328
329/// A node pattern: `(var:Label {props})`.
330#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
331pub struct NodePattern {
332    /// Optional variable binding.
333    pub var: Option<String>,
334    /// Label constraints.
335    pub labels: Vec<String>,
336    /// Property constraints (map literal).
337    pub properties: Option<Expr>,
338    pub span: Span,
339}
340
341/// A relationship pattern: `-[var:TYPE*min..max {props}]->`.
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
343pub struct RelPattern {
344    pub var: Option<String>,
345    /// Relationship type constraints.
346    pub types: Vec<String>,
347    pub direction: Direction,
348    /// Minimum hops for variable-length patterns (`None` = exactly 1).
349    pub min_hops: Option<u32>,
350    /// Maximum hops (`None` = unbounded or exactly 1 if `min_hops` is also None).
351    pub max_hops: Option<u32>,
352    pub properties: Option<Expr>,
353    pub span: Span,
354}
355
356/// Edge direction in a relationship pattern.
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
358pub enum Direction {
359    /// `-[r]->` — outgoing.
360    Out,
361    /// `<-[r]-` — incoming.
362    In,
363    /// `-[r]-` — undirected.
364    Undirected,
365}
366
367// ---------------------------------------------------------------------------
368// Expressions
369// ---------------------------------------------------------------------------
370
371/// A Cypher expression.
372///
373/// `Expr` is recursive: `Box<Expr>` is used wherever a child expression is
374/// needed to keep the enum `Sized`.
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376#[non_exhaustive]
377pub enum Expr {
378    /// An integer, float, string, boolean, or null literal.
379    Literal(Literal),
380    /// A variable reference: `n`.
381    Var(VarRef),
382    /// A property access: `n.prop`.
383    Property(PropertyAccess),
384    /// A binary operation: `left op right`.
385    BinaryOp(BinaryOp),
386    /// A unary operation: `op expr` (e.g. `NOT x`, `-x`).
387    UnaryOp(UnaryOp),
388    /// A function call: `name(args)` or `name(DISTINCT arg)`.
389    FunctionCall(FunctionCall),
390    /// A list literal: `[1, 2, 3]`.
391    List(ListLiteral),
392    /// A map literal: `{key: expr}`.
393    Map(MapLiteral),
394    /// A Cypher parameter: `$name`.
395    Param(ParamRef),
396    /// A `CASE` expression.
397    Case(CaseExpr),
398    /// A list comprehension: `[x IN list WHERE pred | expr]`.
399    ListComprehension(ListComprehension),
400    /// A quantifier predicate: `all/any/none/single(x IN list WHERE pred)`.
401    Quantifier(Quantifier),
402    /// A pattern comprehension: `[(n)-[r]->(m) | r.weight]`.
403    PatternComprehension(PatternComprehension),
404    /// A pattern predicate: `(n)-[:KNOWS]->(m)`.
405    PatternPredicate(PatternPredicate),
406    /// A simple or full existential subquery: `exists { (n)-->(m) }` or
407    /// `exists { MATCH (n)-->(m) RETURN true }`.
408    ExistentialSubquery(ExistentialSubquery),
409    /// A label predicate: `n:Person`.
410    LabelPredicate(LabelPredicate),
411    /// `IS NULL` / `IS NOT NULL`.
412    IsNull {
413        expr: Box<Expr>,
414        negated: bool,
415        span: Span,
416    },
417    /// `x IN list` / `x NOT IN list`.
418    InList {
419        expr: Box<Expr>,
420        list: Box<Expr>,
421        negated: bool,
422        span: Span,
423    },
424    /// `x STARTS WITH y` / `x ENDS WITH y` / `x CONTAINS y`.
425    StringOp {
426        expr: Box<Expr>,
427        op: StringOpKind,
428        pattern: Box<Expr>,
429        span: Span,
430    },
431    /// `x =~ pattern` — regular expression match.
432    RegexMatch {
433        expr: Box<Expr>,
434        pattern: Box<Expr>,
435        span: Span,
436    },
437    /// A subexpression wrapped in parentheses (preserved for span accuracy).
438    Parenthesized { inner: Box<Expr>, span: Span },
439}
440
441impl Expr {
442    /// Return the span of this expression.
443    #[must_use]
444    pub fn span(&self) -> Span {
445        match self {
446            Self::Literal(l) => l.span(),
447            Self::Var(v) => v.span,
448            Self::Property(p) => p.span,
449            Self::BinaryOp(b) => b.span,
450            Self::UnaryOp(u) => u.span,
451            Self::FunctionCall(f) => f.span,
452            Self::List(l) => l.span,
453            Self::Map(m) => m.span,
454            Self::Param(p) => p.span,
455            Self::Case(c) => c.span,
456            Self::ListComprehension(lc) => lc.span,
457            Self::Quantifier(q) => q.span,
458            Self::PatternComprehension(pc) => pc.span,
459            Self::PatternPredicate(pp) => pp.span,
460            Self::ExistentialSubquery(es) => es.span,
461            Self::LabelPredicate(lp) => lp.span,
462            Self::IsNull { span, .. }
463            | Self::InList { span, .. }
464            | Self::StringOp { span, .. }
465            | Self::RegexMatch { span, .. }
466            | Self::Parenthesized { span, .. } => *span,
467        }
468    }
469}
470
471// ---------------------------------------------------------------------------
472// Literal
473// ---------------------------------------------------------------------------
474
475/// A scalar literal value: integer, float, string, boolean, or null.
476#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
477pub enum Literal {
478    Int(i64, Span),
479    Float(f64, Span),
480    Str(String, Span),
481    Bool(bool, Span),
482    Null(Span),
483}
484
485impl Literal {
486    #[must_use]
487    pub fn span(&self) -> Span {
488        match self {
489            Self::Int(_, s)
490            | Self::Float(_, s)
491            | Self::Str(_, s)
492            | Self::Bool(_, s)
493            | Self::Null(s) => *s,
494        }
495    }
496}
497
498// ---------------------------------------------------------------------------
499// Leaf expression types
500// ---------------------------------------------------------------------------
501
502/// A variable reference: `n`.
503#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
504pub struct VarRef {
505    pub name: String,
506    pub span: Span,
507}
508
509/// A property access: `n.prop`.
510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
511pub struct PropertyAccess {
512    pub object: Box<Expr>,
513    pub key: String,
514    pub span: Span,
515}
516
517/// A query parameter reference: `$name`.
518#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
519pub struct ParamRef {
520    pub name: String,
521    pub span: Span,
522}
523
524/// A label predicate: `n:Person` used as a boolean expression.
525#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
526pub struct LabelPredicate {
527    pub var: String,
528    pub labels: Vec<String>,
529    pub span: Span,
530}
531
532// ---------------------------------------------------------------------------
533// Compound expression types
534// ---------------------------------------------------------------------------
535
536/// A binary infix expression: `left op right`.
537#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538pub struct BinaryOp {
539    pub op: BinaryOpKind,
540    pub left: Box<Expr>,
541    pub right: Box<Expr>,
542    pub span: Span,
543}
544
545/// All binary operators in openCypher.
546#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
547pub enum BinaryOpKind {
548    // Comparison
549    Eq,
550    Neq,
551    Lt,
552    Lte,
553    Gt,
554    Gte,
555    // Logical
556    And,
557    Or,
558    Xor,
559    // Arithmetic
560    Add,
561    Sub,
562    Mul,
563    Div,
564    Mod,
565    Pow,
566    // String
567    Concat,
568}
569
570/// A unary prefix expression: `NOT expr` or `-expr`.
571#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
572pub struct UnaryOp {
573    pub op: UnaryOpKind,
574    pub expr: Box<Expr>,
575    pub span: Span,
576}
577
578/// The operator in a unary expression.
579#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
580pub enum UnaryOpKind {
581    Not,
582    Neg,
583}
584
585/// A function call: `name(args)`, `name(DISTINCT arg)`, or `name(*)`.
586#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
587pub struct FunctionCall {
588    /// Namespace-qualified name, e.g. `["apoc", "coll", "sum"]`.
589    pub name: Vec<String>,
590    pub distinct: bool,
591    /// `true` when called as `f(*)` — `args` is empty in this case.
592    pub star: bool,
593    pub args: Vec<Expr>,
594    pub span: Span,
595}
596
597/// A list literal: `[1, 2, 3]`.
598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
599pub struct ListLiteral {
600    pub elements: Vec<Expr>,
601    pub span: Span,
602}
603
604/// A map literal: `{key: expr, …}`.
605#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
606pub struct MapLiteral {
607    pub entries: HashMap<String, Expr>,
608    /// Exact source span for each parsed key token. Synthetic and legacy
609    /// deserialized maps may omit entries here.
610    #[serde(default)]
611    pub key_spans: HashMap<String, Span>,
612    pub span: Span,
613}
614
615// ---------------------------------------------------------------------------
616// CASE expression
617// ---------------------------------------------------------------------------
618
619/// A `CASE` expression — simple (`CASE expr WHEN … THEN …`) or searched (`CASE WHEN … THEN …`).
620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
621pub struct CaseExpr {
622    /// `CASE expr WHEN ...` — the subject is `Some`; simple `CASE WHEN` is `None`.
623    pub subject: Option<Box<Expr>>,
624    pub when_clauses: Vec<WhenClause>,
625    pub else_expr: Option<Box<Expr>>,
626    pub span: Span,
627}
628
629/// A single `WHEN condition THEN result` arm inside a `CASE` expression.
630#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
631pub struct WhenClause {
632    pub condition: Expr,
633    pub result: Expr,
634    pub span: Span,
635}
636
637// ---------------------------------------------------------------------------
638// List / pattern comprehensions
639// ---------------------------------------------------------------------------
640
641/// A list comprehension: `[x IN list WHERE pred | projection]`.
642#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
643pub struct ListComprehension {
644    pub var: String,
645    pub list: Box<Expr>,
646    pub filter: Option<Box<Expr>>,
647    pub projection: Option<Box<Expr>>,
648    pub span: Span,
649}
650
651/// Which quantifier a [`Quantifier`] predicate applies.
652#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
653pub enum QuantifierKind {
654    /// `all(x IN list WHERE pred)` — true iff every element satisfies `pred`.
655    All,
656    /// `any(x IN list WHERE pred)` — true iff some element satisfies `pred`.
657    Any,
658    /// `none(x IN list WHERE pred)` — true iff no element satisfies `pred`.
659    None,
660    /// `single(x IN list WHERE pred)` — true iff exactly one element satisfies `pred`.
661    Single,
662}
663
664/// A quantifier predicate: `all/any/none/single(var IN list WHERE pred)`.
665#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
666pub struct Quantifier {
667    pub kind: QuantifierKind,
668    pub var: String,
669    pub list: Box<Expr>,
670    pub predicate: Box<Expr>,
671    pub span: Span,
672}
673
674/// A pattern comprehension: `[(n)-[r]->(m) WHERE pred | r.weight]`.
675#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
676pub struct PatternComprehension {
677    pub var: Option<String>,
678    pub pattern: PathPattern,
679    pub filter: Option<Box<Expr>>,
680    pub projection: Box<Expr>,
681    pub span: Span,
682}
683
684/// A pattern predicate used as a boolean expression, e.g. `(n)-->()`.
685#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
686pub struct PatternPredicate {
687    pub pattern: PathPattern,
688    pub span: Span,
689}
690
691/// A simple or full existential subquery.
692#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
693pub struct ExistentialSubquery {
694    pub body: ExistentialSubqueryBody,
695    pub span: Span,
696}
697
698#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
699pub enum ExistentialSubqueryBody {
700    Simple {
701        pattern: PathPattern,
702        filter: Option<Box<Expr>>,
703    },
704    Full(Box<AstQuery>),
705}
706
707// ---------------------------------------------------------------------------
708// String operations
709// ---------------------------------------------------------------------------
710
711/// The operation in a `STARTS WITH` / `ENDS WITH` / `CONTAINS` string predicate.
712#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
713pub enum StringOpKind {
714    StartsWith,
715    EndsWith,
716    Contains,
717}