Skip to main content

uqa_graph/cypher/
ast.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Cypher AST. Mirrors the openCypher subset implemented by the
8//! UQA Cypher subset: `MATCH`, `OPTIONAL MATCH`, `CREATE`, `MERGE`,
9//! `SET`, `DELETE`, `DETACH DELETE`, `RETURN`, `WITH`, `WHERE`,
10//! `ORDER BY`, `SKIP`, `LIMIT`, `UNWIND`.
11
12use std::collections::BTreeMap;
13
14use uqa_core::Value;
15
16// -- Expressions ----------------------------------------------------------
17
18#[derive(Debug, Clone, PartialEq)]
19pub struct PropertyAccess {
20    pub variable: String,
21    pub keys: Vec<String>,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct Parameter {
26    pub name: String,
27}
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct Literal {
31    pub value: Value,
32}
33
34#[derive(Debug, Clone, PartialEq)]
35pub struct Variable {
36    pub name: String,
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct FunctionCall {
41    pub name: String,
42    pub args: Vec<CypherExpr>,
43    pub distinct: bool,
44}
45
46#[derive(Debug, Clone, PartialEq)]
47pub struct BinaryOp {
48    pub op: String,
49    pub left: Box<CypherExpr>,
50    pub right: Box<CypherExpr>,
51}
52
53#[derive(Debug, Clone, PartialEq)]
54pub struct UnaryOp {
55    pub op: String,
56    pub operand: Box<CypherExpr>,
57}
58
59#[derive(Debug, Clone, PartialEq)]
60pub struct ListIndex {
61    pub expr: Box<CypherExpr>,
62    pub index: Box<CypherExpr>,
63}
64
65/// `expr[start..end]` slice; either bound may be omitted. Slices are
66/// end-exclusive and support negative offsets.
67#[derive(Debug, Clone, PartialEq)]
68pub struct ListSlice {
69    pub expr: Box<CypherExpr>,
70    pub start: Option<Box<CypherExpr>>,
71    pub end: Option<Box<CypherExpr>>,
72}
73
74/// `[variable IN list WHERE filter | map]` list comprehension.
75#[derive(Debug, Clone, PartialEq)]
76pub struct ListComprehension {
77    pub variable: String,
78    pub list_expr: Box<CypherExpr>,
79    pub filter: Option<Box<CypherExpr>>,
80    pub map_expr: Option<Box<CypherExpr>>,
81}
82
83#[derive(Debug, Clone, PartialEq)]
84pub struct InList {
85    pub expr: Box<CypherExpr>,
86    pub list_expr: Box<CypherExpr>,
87}
88
89#[derive(Debug, Clone, PartialEq)]
90pub struct IsNull {
91    pub expr: Box<CypherExpr>,
92}
93
94#[derive(Debug, Clone, PartialEq)]
95pub struct IsNotNull {
96    pub expr: Box<CypherExpr>,
97}
98
99#[derive(Debug, Clone, PartialEq)]
100pub struct CaseExpr {
101    pub operand: Option<Box<CypherExpr>>,
102    pub whens: Vec<(CypherExpr, CypherExpr)>,
103    pub else_expr: Option<Box<CypherExpr>>,
104}
105
106#[derive(Debug, Clone, PartialEq)]
107pub struct ListLiteral {
108    pub elements: Vec<CypherExpr>,
109}
110
111#[derive(Debug, Clone, PartialEq)]
112pub struct MapLiteral {
113    pub pairs: Vec<(String, CypherExpr)>,
114}
115
116/// Top-level expression node.
117#[derive(Debug, Clone, PartialEq)]
118pub enum CypherExpr {
119    PropertyAccess(PropertyAccess),
120    Parameter(Parameter),
121    Literal(Literal),
122    Variable(Variable),
123    FunctionCall(FunctionCall),
124    BinaryOp(BinaryOp),
125    UnaryOp(UnaryOp),
126    ListIndex(ListIndex),
127    ListSlice(ListSlice),
128    ListComprehension(ListComprehension),
129    InList(InList),
130    IsNull(IsNull),
131    IsNotNull(IsNotNull),
132    CaseExpr(CaseExpr),
133    ListLiteral(ListLiteral),
134    MapLiteral(MapLiteral),
135    /// `exists((a)-[:R]->(b))` pattern predicate.
136    ExistsPattern(PathPattern),
137}
138
139// -- Patterns ------------------------------------------------------------
140
141#[derive(Debug, Clone, PartialEq)]
142pub struct NodePattern {
143    pub variable: Option<String>,
144    pub labels: Vec<String>,
145    pub properties: Option<BTreeMap<String, CypherExpr>>,
146}
147
148/// Direction of a relationship pattern.
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum RelDirection {
151    /// `-[...]->`
152    Right,
153    /// `<-[...]-`
154    Left,
155    /// `-[...]-`
156    Both,
157}
158
159#[derive(Debug, Clone, PartialEq)]
160pub struct RelPattern {
161    pub variable: Option<String>,
162    pub types: Vec<String>,
163    pub properties: Option<BTreeMap<String, CypherExpr>>,
164    pub direction: RelDirection,
165    /// `None` = exactly 1; `Some` = variable-length lower bound
166    /// (`*<min>..<max>`).
167    pub min_hops: Option<u32>,
168    /// `None` = unbounded (or exactly 1 if `min_hops` is also `None`).
169    pub max_hops: Option<u32>,
170}
171
172/// One element in a path: a node or a relationship.
173#[derive(Debug, Clone, PartialEq)]
174pub enum PathElement {
175    Node(NodePattern),
176    Rel(RelPattern),
177}
178
179#[derive(Debug, Clone, PartialEq)]
180pub struct PathPattern {
181    /// Path variable when the pattern is `p = (...)-[...]-(...)`.
182    pub variable: Option<String>,
183    pub elements: Vec<PathElement>,
184}
185
186// -- Clauses --------------------------------------------------------------
187
188#[derive(Debug, Clone, PartialEq)]
189pub struct MatchClause {
190    pub patterns: Vec<PathPattern>,
191    pub r#where: Option<CypherExpr>,
192    pub optional: bool,
193}
194
195#[derive(Debug, Clone, PartialEq)]
196pub struct CreateClause {
197    pub patterns: Vec<PathPattern>,
198}
199
200#[derive(Debug, Clone, PartialEq)]
201pub struct MergeClause {
202    pub pattern: PathPattern,
203    pub on_create_set: Option<Vec<SetItem>>,
204    pub on_match_set: Option<Vec<SetItem>>,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum SetOperator {
209    /// `=`
210    Assign,
211    /// `+=`
212    Update,
213}
214
215#[derive(Debug, Clone, PartialEq)]
216pub struct SetItem {
217    pub target: CypherExpr,
218    pub value: CypherExpr,
219    pub operator: SetOperator,
220}
221
222#[derive(Debug, Clone, PartialEq)]
223pub struct SetClause {
224    pub items: Vec<SetItem>,
225}
226
227#[derive(Debug, Clone, PartialEq)]
228pub struct DeleteClause {
229    pub expressions: Vec<CypherExpr>,
230    pub detach: bool,
231}
232
233#[derive(Debug, Clone, PartialEq)]
234pub struct ReturnItem {
235    pub expr: CypherExpr,
236    pub alias: Option<String>,
237}
238
239#[derive(Debug, Clone, PartialEq)]
240pub struct OrderByItem {
241    pub expr: CypherExpr,
242    pub ascending: bool,
243}
244
245#[derive(Debug, Clone, PartialEq)]
246pub struct ReturnClause {
247    pub items: Vec<ReturnItem>,
248    pub distinct: bool,
249    pub order_by: Option<Vec<OrderByItem>>,
250    pub skip: Option<CypherExpr>,
251    pub limit: Option<CypherExpr>,
252}
253
254#[derive(Debug, Clone, PartialEq)]
255pub struct WithClause {
256    pub items: Vec<ReturnItem>,
257    pub distinct: bool,
258    pub order_by: Option<Vec<OrderByItem>>,
259    pub skip: Option<CypherExpr>,
260    pub limit: Option<CypherExpr>,
261    pub r#where: Option<CypherExpr>,
262}
263
264#[derive(Debug, Clone, PartialEq)]
265pub struct UnwindClause {
266    pub expr: CypherExpr,
267    pub variable: String,
268}
269
270#[derive(Debug, Clone, PartialEq)]
271pub enum CypherClause {
272    Match(MatchClause),
273    Create(CreateClause),
274    Merge(MergeClause),
275    Set(SetClause),
276    Delete(DeleteClause),
277    Return(ReturnClause),
278    With(WithClause),
279    Unwind(UnwindClause),
280}
281
282#[derive(Debug, Clone, PartialEq)]
283pub struct CypherQuery {
284    pub clauses: Vec<CypherClause>,
285}
286
287impl CypherQuery {
288    /// Whether execution can change graph data.
289    #[must_use]
290    pub fn mutates_graph(&self) -> bool {
291        self.clauses.iter().any(|clause| {
292            matches!(
293                clause,
294                CypherClause::Create(_)
295                    | CypherClause::Merge(_)
296                    | CypherClause::Set(_)
297                    | CypherClause::Delete(_)
298            )
299        })
300    }
301}