Skip to main content

core_query/cypher/
ast.rs

1//! Cypher subset AST. Types match the Plan 3 Task 6 interface block.
2
3use crate::filter::CmpOp;
4use core_storage::Value;
5
6/// A LIMIT or SKIP value: either an exact count or a named query parameter.
7///
8/// `$name` parameters are resolved at execution time from the params map and
9/// validated to be a non-negative integer.
10#[derive(Debug, Clone, PartialEq)]
11pub enum LimitSkip {
12    Exact(u64),
13    Param(String),
14}
15
16/// One `OPTIONAL MATCH pattern [WHERE expr]` clause.
17///
18/// If the pattern produces no rows for a given input row, the input row
19/// survives with the optional variables set to `null` (left-outer-join
20/// semantics, openCypher §10.1.3).
21///
22/// `where_expr`, when present, is applied INSIDE the optional scope:
23/// it filters candidate rows before the left-outer fallback fires.  This
24/// differs from a post-filter that would eliminate the null row entirely.
25#[derive(Debug, Clone, PartialEq)]
26pub struct OptionalClause {
27    pub patterns: Vec<Pattern>,
28    /// WHERE clause scoped to this optional match (applied before nullification).
29    pub where_expr: Option<Expr>,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct Query {
34    pub matches: Vec<Pattern>,
35    /// `OPTIONAL MATCH` clauses that follow the required matches.
36    pub optional_clauses: Vec<OptionalClause>,
37    /// Top-level WHERE filter, evaluated before UNWIND expansion.
38    pub where_expr: Option<Expr>,
39    /// Top-level UNWIND clauses (after WHERE, before post_unwind_where/WITH/RETURN).
40    pub unwinds: Vec<UnwindClause>,
41    /// Optional WHERE evaluated after UNWIND expansion (references UNWIND aliases).
42    pub post_unwind_where: Option<Expr>,
43    /// WITH pipeline stages. Each stage carries a WITH clause and optional
44    /// MATCH / UNWIND / WHERE that follow it.
45    pub stages: Vec<WithStage>,
46    pub returns: Vec<RetItem>,
47    /// `RETURN DISTINCT …` — executor hashes projected rows after `Project`.
48    pub distinct: bool,
49    pub order_by: Vec<OrderItem>,
50    pub skip: Option<LimitSkip>,
51    pub limit: Option<LimitSkip>,
52}
53
54/// One `UNWIND <expr> AS <alias>` clause.
55#[derive(Debug, Clone, PartialEq)]
56pub struct UnwindClause {
57    pub list: UnwindExpr,
58    pub alias: String,
59}
60
61/// The expression whose value is iterated in UNWIND.
62#[derive(Debug, Clone, PartialEq)]
63pub enum UnwindExpr {
64    /// Inline list literal: `[1, 2, 3]`.
65    Lit(Vec<Value>),
66    /// Property on a bound node: `n.tags`.
67    Prop { var: String, field: String },
68    /// A previously bound alias (from a prior WITH): `alias`.
69    Var(String),
70}
71
72/// One WITH stage in a pipeline:
73/// ```text
74/// WITH <items> [WHERE <expr>] [ORDER BY …] [SKIP n] [LIMIT n]
75/// [MATCH …]* [OPTIONAL MATCH …]* [UNWIND …]* [WHERE <expr>]
76/// ```
77#[derive(Debug, Clone, PartialEq)]
78pub struct WithStage {
79    /// The projected items in the WITH clause.
80    pub items: Vec<RetItem>,
81    /// Optional WHERE / HAVING filter immediately after the WITH keyword.
82    pub where_expr: Option<Expr>,
83    pub order_by: Vec<OrderItem>,
84    pub skip: Option<LimitSkip>,
85    pub limit: Option<LimitSkip>,
86    /// MATCH clauses that follow this WITH.
87    pub matches: Vec<Pattern>,
88    /// OPTIONAL MATCH clauses that follow the required MATCHes in this stage.
89    pub optional_clauses: Vec<OptionalClause>,
90    /// UNWIND clauses that follow this WITH.
91    pub unwinds: Vec<UnwindClause>,
92    /// WHERE clause that follows those MATCHes (pre-next-WITH/RETURN filter).
93    pub post_where: Option<Expr>,
94}
95
96/// Aggregate function in a RETURN clause.
97#[derive(Debug, Clone, PartialEq)]
98pub enum AggFunc {
99    Count,
100    Sum,
101    Avg,
102    Min,
103    Max,
104    /// `collect(x)` — gather each row's value of `x` into a list, skipping
105    /// nulls. Per group when grouping keys are present.
106    Collect,
107}
108
109/// Argument to an aggregate function.
110#[derive(Debug, Clone, PartialEq)]
111pub enum AggArg {
112    /// `COUNT(*)` — every matched row counts regardless of binding.
113    Star,
114    /// `COUNT(var)` — counts rows where `var` is bound (non-null).
115    Var(String),
116    /// `SUM(var.field)`, `AVG(var.field)`, etc.
117    Prop { var: String, field: String },
118}
119
120/// Hop-count range for variable-length relationship patterns (`*min..max`).
121/// Both bounds are inclusive.  `min = max` is a fixed-hop pattern.
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct HopRange {
124    pub min: u8,
125    pub max: u8,
126}
127
128#[derive(Debug, Clone, PartialEq)]
129pub struct Pattern {
130    pub start: NodePat,
131    pub chain: Vec<(RelPat, NodePat)>,
132    /// True when parsed as `MATCH shortestPath(...)`.
133    pub shortest: bool,
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub struct NodePat {
138    pub var: Option<String>,
139    pub label: Option<String>,
140    pub props: Vec<(String, Operand)>,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum RelDir {
145    Right,
146    Left,
147    Undirected,
148}
149
150#[derive(Debug, Clone, PartialEq)]
151pub struct RelPat {
152    pub var: Option<String>,
153    /// Relationship-type alternatives. Empty = any type; one = single type;
154    /// many = `[:A|:B]` alternation (match an edge of any listed type).
155    pub etypes: Vec<String>,
156    pub dir: RelDir,
157    /// `None` = single-hop (normal `Expand`).  `Some(r)` = variable-length
158    /// (`VarExpand`) with the given min/max hop bounds.
159    pub hops: Option<HopRange>,
160}
161
162#[derive(Debug, Clone, PartialEq)]
163pub enum Expr {
164    And(Box<Expr>, Box<Expr>),
165    Or(Box<Expr>, Box<Expr>),
166    Not(Box<Expr>),
167    Cmp {
168        lhs: Operand,
169        op: CmpOp,
170        rhs: Operand,
171    },
172    /// Standalone operand used as a boolean predicate.
173    ///
174    /// Enables `WHERE textMatches(n.bio, 'query')` without requiring an
175    /// explicit comparison.  Truthiness: `Bool(true)` → true, `Bool(false)`
176    /// → false, null → false, any other non-null non-false value → true.
177    Truthy(Operand),
178    /// `operand IS NULL` — true iff the operand evaluates to null.
179    IsNull(Operand),
180    /// `operand IS NOT NULL` — true iff the operand is non-null.
181    IsNotNull(Operand),
182    /// `expr IN [a, b, $p]` or `expr IN $list` (`$list` is `Value::List`).
183    In {
184        expr: Operand,
185        list: Vec<Operand>,
186    },
187}
188
189#[derive(Debug, Clone, PartialEq)]
190pub enum Operand {
191    Prop {
192        var: String,
193        field: String,
194    },
195    Lit(Value),
196    Param(String),
197    /// Bare variable reference (used in `WITH … WHERE alias > 2`).
198    Var(String),
199    /// Scalar function call: `toLower(n.name)`, `size(n.tags)`, `type(r)`, etc.
200    ///
201    /// Supported functions (case-insensitive): `toLower`, `toUpper`, `size`,
202    /// `coalesce`, `type`, `abs`, `round`, `textMatches`, `contains`,
203    /// `startsWith`, `endsWith`, `toInteger`, `toFloat`, `toString`, `decay`,
204    /// `key`.
205    /// Unknown names → named error at execution time listing the supported
206    /// set (see `SCALAR_FUNCS` in `exec.rs`).
207    FuncCall {
208        name: String,
209        args: Vec<Operand>,
210    },
211    /// Arithmetic expression inside a function argument: `abs(n.age - 27)`,
212    /// `round(n.score * 1.5)`.  Supports `+`, `-`, `*`, `/`.
213    BinArith {
214        op: ArithOp,
215        left: Box<Operand>,
216        right: Box<Operand>,
217    },
218    /// Generic `CASE WHEN <cond> THEN <value> [WHEN …] [ELSE <value>] END`.
219    /// Evaluates each branch's condition in order, returning the first matching
220    /// value; the `default` (ELSE) or null if none match.
221    Case {
222        branches: Vec<(Expr, Operand)>,
223        default: Option<Box<Operand>>,
224    },
225}
226
227/// Arithmetic operators for `Operand::BinArith`.
228#[derive(Debug, Clone, PartialEq)]
229pub enum ArithOp {
230    Add,
231    Sub,
232    Mul,
233    Div,
234}
235
236/// RETURN item value: bare variable, `var.field`, an aggregate call, a
237/// scalar function call, or an arbitrary scalar expression (arithmetic etc.).
238#[derive(Debug, Clone, PartialEq)]
239pub enum RetVal {
240    Var(String),
241    Prop {
242        var: String,
243        field: String,
244    },
245    /// Single aggregate function call.  When combined with non-aggregate items
246    /// in the same RETURN clause, the planner routes to `GroupAggregate`.
247    /// Multiple aggregate items are also supported via `GroupAggregate`.
248    Agg {
249        func: AggFunc,
250        arg: AggArg,
251    },
252    /// Scalar function call in a RETURN position, e.g. `toLower(n.name)`.
253    /// The same function set as `Operand::FuncCall` (see `SCALAR_FUNCS`).
254    FuncCall {
255        name: String,
256        args: Vec<Operand>,
257    },
258    /// Arbitrary scalar expression in a RETURN/WITH position, e.g. `n.age + 1`.
259    /// Evaluated via `resolve_operand` at execution time; null propagates.
260    ScalarExpr(Operand),
261}
262
263#[derive(Debug, Clone, PartialEq)]
264pub struct RetItem {
265    pub value: RetVal,
266    pub alias: Option<String>,
267}
268
269/// ORDER BY target. Bare identifiers that match a RETURN alias become `Alias`;
270/// otherwise they stay `Var`. `var.field` is always `Prop`.
271#[derive(Debug, Clone, PartialEq)]
272pub enum OrderTarget {
273    Alias(String),
274    Var(String),
275    Prop { var: String, field: String },
276}
277
278#[derive(Debug, Clone, PartialEq)]
279pub struct OrderItem {
280    pub target: OrderTarget,
281    pub descending: bool,
282}
283
284// ─── Write statement AST ──────────────────────────────────────────────────────
285
286/// Top-level write statement (CREATE / MATCH…SET / MATCH…DELETE / MATCH…DETACH DELETE / MERGE).
287/// Produced by `parse_write`; executed by `GraphDb::query_write`.
288#[derive(Debug, Clone, PartialEq)]
289pub enum WriteStatement {
290    Create(CreateStmt),
291    MatchSet(MatchSetStmt),
292    MatchDelete(MatchDeleteStmt),
293    MatchDeleteNode(MatchDeleteNodeStmt),
294    Merge(MergeStmt),
295}
296
297/// `CREATE (a:L {id: 'x', ...})[-[:T]->(b:L2 {id: 'y', ...})] [RETURN …]`
298///
299/// `nodes` is in encounter order. `edges` reference node vars that appear in
300/// `nodes`. Each node must have a string-valued `id` property (used as key).
301///
302/// `returns`, when `Some`, projects the created bindings as a read result.
303/// The write and the projection are committed as a single WAL batch frame.
304#[derive(Debug, Clone, PartialEq)]
305pub struct CreateStmt {
306    pub nodes: Vec<CreateNode>,
307    pub edges: Vec<CreateEdge>,
308    /// Optional RETURN clause: project created bindings after commit.
309    pub returns: Option<Vec<RetItem>>,
310}
311
312/// One node in a CREATE pattern.
313#[derive(Debug, Clone, PartialEq)]
314pub struct CreateNode {
315    /// Optional binding variable (`a` in `(a:Label {…})`).
316    pub var: Option<String>,
317    pub label: String,
318    /// Literal property pairs.  Must include a string-valued `id` field.
319    pub props: Vec<(String, core_storage::Value)>,
320}
321
322/// One edge in a CREATE pattern, referencing vars from `CreateStmt.nodes`.
323#[derive(Debug, Clone, PartialEq)]
324pub struct CreateEdge {
325    pub src_var: String,
326    pub etype: String,
327    pub dst_var: String,
328}
329
330/// `MATCH patterns [WHERE expr] SET var.field = literal [, …] [RETURN …]`
331#[derive(Debug, Clone, PartialEq)]
332pub struct MatchSetStmt {
333    pub matches: Vec<Pattern>,
334    pub where_expr: Option<Expr>,
335    pub sets: Vec<SetClause>,
336    /// Optional RETURN clause: project matched bindings from post-write state.
337    pub returns: Option<Vec<RetItem>>,
338}
339
340/// One `var.field = literal_or_param` assignment in a SET clause.
341///
342/// `value` is an `Operand` rather than a bare `Value` so that `$param`
343/// references are legal on the RHS (resolved at execution time from the
344/// query's parameter map).  Only `Operand::Lit` and `Operand::Param` are
345/// accepted by the parser; other variants produce a named parse error.
346#[derive(Debug, Clone, PartialEq)]
347pub struct SetClause {
348    pub var: String,
349    pub field: String,
350    pub value: Operand,
351}
352
353/// `MATCH patterns [WHERE expr] DELETE rel_var [, …]`
354///
355/// Each `EdgeDelete` carries the etype, src node var, and dst node var resolved
356/// from the MATCH patterns at parse time so the executor can emit a read query
357/// returning node keys without extra pattern-scanning.
358#[derive(Debug, Clone, PartialEq)]
359pub struct MatchDeleteStmt {
360    pub matches: Vec<Pattern>,
361    pub where_expr: Option<Expr>,
362    pub deletes: Vec<EdgeDelete>,
363}
364
365/// One edge variable to delete, with resolved topology.
366#[derive(Debug, Clone, PartialEq)]
367pub struct EdgeDelete {
368    pub rel_var: String,
369    pub etype: String,
370    pub src_var: String,
371    pub dst_var: String,
372}
373
374/// `MATCH patterns [WHERE expr] [DETACH] DELETE node_var [, …]`
375///
376/// When `detach` is `true` (DETACH DELETE), all incident edges are removed
377/// before the node is tombstoned (openCypher semantics).  When `false`
378/// (bare DELETE), the node must have no incident edges; the executor returns
379/// an error if any remain.
380#[derive(Debug, Clone, PartialEq)]
381pub struct MatchDeleteNodeStmt {
382    pub matches: Vec<Pattern>,
383    pub where_expr: Option<Expr>,
384    /// Node variable names whose nodes are to be deleted (resolved from MATCH patterns).
385    pub node_vars: Vec<String>,
386    /// `true` → DETACH DELETE (allowed regardless of edges).
387    /// `false` → bare DELETE (error if any edges touch the node).
388    pub detach: bool,
389}
390
391/// `MERGE (n:Label {id: 'x'}) [ON CREATE SET …] [ON MATCH SET …] [RETURN …]`
392///
393/// Exactly one property is allowed (the key). More properties → named error.
394/// `ON CREATE SET` / `ON MATCH SET` apply inside the same write batch as the
395/// insert-or-skip. `returns`, when `Some`, projects the node after commit.
396#[derive(Debug, Clone, PartialEq)]
397pub struct MergeStmt {
398    pub label: String,
399    /// The single property that identifies the node. Value must be a string.
400    pub key_field: String,
401    pub key_value: core_storage::Value,
402    /// Optional bound variable for the MERGE node (for RETURN projection).
403    pub var: Option<String>,
404    /// `ON CREATE SET` assignments, applied only when the node is inserted.
405    pub on_create: Vec<SetClause>,
406    /// `ON MATCH SET` assignments, applied only when the node already exists.
407    pub on_match: Vec<SetClause>,
408    /// Optional RETURN clause: project the node after commit.
409    pub returns: Option<Vec<RetItem>>,
410}