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 /// Unknown names → named error at execution time listing the supported
205 /// set (see `SCALAR_FUNCS` in `exec.rs`).
206 FuncCall {
207 name: String,
208 args: Vec<Operand>,
209 },
210 /// Arithmetic expression inside a function argument: `abs(n.age - 27)`,
211 /// `round(n.score * 1.5)`. Supports `+`, `-`, `*`, `/`.
212 BinArith {
213 op: ArithOp,
214 left: Box<Operand>,
215 right: Box<Operand>,
216 },
217 /// Generic `CASE WHEN <cond> THEN <value> [WHEN …] [ELSE <value>] END`.
218 /// Evaluates each branch's condition in order, returning the first matching
219 /// value; the `default` (ELSE) or null if none match.
220 Case {
221 branches: Vec<(Expr, Operand)>,
222 default: Option<Box<Operand>>,
223 },
224}
225
226/// Arithmetic operators for `Operand::BinArith`.
227#[derive(Debug, Clone, PartialEq)]
228pub enum ArithOp {
229 Add,
230 Sub,
231 Mul,
232 Div,
233}
234
235/// RETURN item value: bare variable, `var.field`, an aggregate call, a
236/// scalar function call, or an arbitrary scalar expression (arithmetic etc.).
237#[derive(Debug, Clone, PartialEq)]
238pub enum RetVal {
239 Var(String),
240 Prop {
241 var: String,
242 field: String,
243 },
244 /// Single aggregate function call. When combined with non-aggregate items
245 /// in the same RETURN clause, the planner routes to `GroupAggregate`.
246 /// Multiple aggregate items are also supported via `GroupAggregate`.
247 Agg {
248 func: AggFunc,
249 arg: AggArg,
250 },
251 /// Scalar function call in a RETURN position, e.g. `toLower(n.name)`.
252 /// The same function set as `Operand::FuncCall` (see `SCALAR_FUNCS`).
253 FuncCall {
254 name: String,
255 args: Vec<Operand>,
256 },
257 /// Arbitrary scalar expression in a RETURN/WITH position, e.g. `n.age + 1`.
258 /// Evaluated via `resolve_operand` at execution time; null propagates.
259 ScalarExpr(Operand),
260}
261
262#[derive(Debug, Clone, PartialEq)]
263pub struct RetItem {
264 pub value: RetVal,
265 pub alias: Option<String>,
266}
267
268/// ORDER BY target. Bare identifiers that match a RETURN alias become `Alias`;
269/// otherwise they stay `Var`. `var.field` is always `Prop`.
270#[derive(Debug, Clone, PartialEq)]
271pub enum OrderTarget {
272 Alias(String),
273 Var(String),
274 Prop { var: String, field: String },
275}
276
277#[derive(Debug, Clone, PartialEq)]
278pub struct OrderItem {
279 pub target: OrderTarget,
280 pub descending: bool,
281}
282
283// ─── Write statement AST ──────────────────────────────────────────────────────
284
285/// Top-level write statement (CREATE / MATCH…SET / MATCH…DELETE / MATCH…DETACH DELETE / MERGE).
286/// Produced by `parse_write`; executed by `GraphDb::query_write`.
287#[derive(Debug, Clone, PartialEq)]
288pub enum WriteStatement {
289 Create(CreateStmt),
290 MatchSet(MatchSetStmt),
291 MatchDelete(MatchDeleteStmt),
292 MatchDeleteNode(MatchDeleteNodeStmt),
293 Merge(MergeStmt),
294}
295
296/// `CREATE (a:L {id: 'x', ...})[-[:T]->(b:L2 {id: 'y', ...})] [RETURN …]`
297///
298/// `nodes` is in encounter order. `edges` reference node vars that appear in
299/// `nodes`. Each node must have a string-valued `id` property (used as key).
300///
301/// `returns`, when `Some`, projects the created bindings as a read result.
302/// The write and the projection are committed as a single WAL batch frame.
303#[derive(Debug, Clone, PartialEq)]
304pub struct CreateStmt {
305 pub nodes: Vec<CreateNode>,
306 pub edges: Vec<CreateEdge>,
307 /// Optional RETURN clause: project created bindings after commit.
308 pub returns: Option<Vec<RetItem>>,
309}
310
311/// One node in a CREATE pattern.
312#[derive(Debug, Clone, PartialEq)]
313pub struct CreateNode {
314 /// Optional binding variable (`a` in `(a:Label {…})`).
315 pub var: Option<String>,
316 pub label: String,
317 /// Literal property pairs. Must include a string-valued `id` field.
318 pub props: Vec<(String, core_storage::Value)>,
319}
320
321/// One edge in a CREATE pattern, referencing vars from `CreateStmt.nodes`.
322#[derive(Debug, Clone, PartialEq)]
323pub struct CreateEdge {
324 pub src_var: String,
325 pub etype: String,
326 pub dst_var: String,
327}
328
329/// `MATCH patterns [WHERE expr] SET var.field = literal [, …] [RETURN …]`
330#[derive(Debug, Clone, PartialEq)]
331pub struct MatchSetStmt {
332 pub matches: Vec<Pattern>,
333 pub where_expr: Option<Expr>,
334 pub sets: Vec<SetClause>,
335 /// Optional RETURN clause: project matched bindings from post-write state.
336 pub returns: Option<Vec<RetItem>>,
337}
338
339/// One `var.field = literal_or_param` assignment in a SET clause.
340///
341/// `value` is an `Operand` rather than a bare `Value` so that `$param`
342/// references are legal on the RHS (resolved at execution time from the
343/// query's parameter map). Only `Operand::Lit` and `Operand::Param` are
344/// accepted by the parser; other variants produce a named parse error.
345#[derive(Debug, Clone, PartialEq)]
346pub struct SetClause {
347 pub var: String,
348 pub field: String,
349 pub value: Operand,
350}
351
352/// `MATCH patterns [WHERE expr] DELETE rel_var [, …]`
353///
354/// Each `EdgeDelete` carries the etype, src node var, and dst node var resolved
355/// from the MATCH patterns at parse time so the executor can emit a read query
356/// returning node keys without extra pattern-scanning.
357#[derive(Debug, Clone, PartialEq)]
358pub struct MatchDeleteStmt {
359 pub matches: Vec<Pattern>,
360 pub where_expr: Option<Expr>,
361 pub deletes: Vec<EdgeDelete>,
362}
363
364/// One edge variable to delete, with resolved topology.
365#[derive(Debug, Clone, PartialEq)]
366pub struct EdgeDelete {
367 pub rel_var: String,
368 pub etype: String,
369 pub src_var: String,
370 pub dst_var: String,
371}
372
373/// `MATCH patterns [WHERE expr] [DETACH] DELETE node_var [, …]`
374///
375/// When `detach` is `true` (DETACH DELETE), all incident edges are removed
376/// before the node is tombstoned (openCypher semantics). When `false`
377/// (bare DELETE), the node must have no incident edges; the executor returns
378/// an error if any remain.
379#[derive(Debug, Clone, PartialEq)]
380pub struct MatchDeleteNodeStmt {
381 pub matches: Vec<Pattern>,
382 pub where_expr: Option<Expr>,
383 /// Node variable names whose nodes are to be deleted (resolved from MATCH patterns).
384 pub node_vars: Vec<String>,
385 /// `true` → DETACH DELETE (allowed regardless of edges).
386 /// `false` → bare DELETE (error if any edges touch the node).
387 pub detach: bool,
388}
389
390/// `MERGE (n:Label {id: 'x'}) [ON CREATE SET …] [ON MATCH SET …] [RETURN …]`
391///
392/// Exactly one property is allowed (the key). More properties → named error.
393/// `ON CREATE SET` / `ON MATCH SET` apply inside the same write batch as the
394/// insert-or-skip. `returns`, when `Some`, projects the node after commit.
395#[derive(Debug, Clone, PartialEq)]
396pub struct MergeStmt {
397 pub label: String,
398 /// The single property that identifies the node. Value must be a string.
399 pub key_field: String,
400 pub key_value: core_storage::Value,
401 /// Optional bound variable for the MERGE node (for RETURN projection).
402 pub var: Option<String>,
403 /// `ON CREATE SET` assignments, applied only when the node is inserted.
404 pub on_create: Vec<SetClause>,
405 /// `ON MATCH SET` assignments, applied only when the node already exists.
406 pub on_match: Vec<SetClause>,
407 /// Optional RETURN clause: project the node after commit.
408 pub returns: Option<Vec<RetItem>>,
409}