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    /// `COUNT(DISTINCT var)` / `COLLECT(DISTINCT var.field)` — the inner
119    /// argument is fed to the accumulator at most once per distinct value
120    /// within a group.
121    ///
122    /// This is what makes an N-way relation intersection expressible: a
123    /// company reached by three edge types yields three rows per talent, and
124    /// only `count(DISTINCT t)` counts the talent once. The parser rejects
125    /// `DISTINCT *` and nested `DISTINCT`, so the inner argument is always
126    /// `Star`-free and one level deep.
127    Distinct(Box<AggArg>),
128}
129
130/// Hop-count range for variable-length relationship patterns (`*min..max`).
131/// Both bounds are inclusive.  `min = max` is a fixed-hop pattern.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct HopRange {
134    pub min: u8,
135    pub max: u8,
136}
137
138#[derive(Debug, Clone, PartialEq)]
139pub struct Pattern {
140    pub start: NodePat,
141    pub chain: Vec<(RelPat, NodePat)>,
142    /// True when parsed as `MATCH shortestPath(...)`.
143    pub shortest: bool,
144}
145
146#[derive(Debug, Clone, PartialEq)]
147pub struct NodePat {
148    pub var: Option<String>,
149    pub label: Option<String>,
150    pub props: Vec<(String, Operand)>,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum RelDir {
155    Right,
156    Left,
157    Undirected,
158}
159
160#[derive(Debug, Clone, PartialEq)]
161pub struct RelPat {
162    pub var: Option<String>,
163    /// Relationship-type alternatives. Empty = any type; one = single type;
164    /// many = `[:A|:B]` alternation (match an edge of any listed type).
165    pub etypes: Vec<String>,
166    pub dir: RelDir,
167    /// `None` = single-hop (normal `Expand`).  `Some(r)` = variable-length
168    /// (`VarExpand`) with the given min/max hop bounds.
169    pub hops: Option<HopRange>,
170}
171
172#[derive(Debug, Clone, PartialEq)]
173pub enum Expr {
174    And(Box<Expr>, Box<Expr>),
175    Or(Box<Expr>, Box<Expr>),
176    Not(Box<Expr>),
177    Cmp {
178        lhs: Operand,
179        op: CmpOp,
180        rhs: Operand,
181    },
182    /// Standalone operand used as a boolean predicate.
183    ///
184    /// Enables `WHERE textMatches(n.bio, 'query')` without requiring an
185    /// explicit comparison.  Truthiness: `Bool(true)` → true, `Bool(false)`
186    /// → false, null → false, any other non-null non-false value → true.
187    Truthy(Operand),
188    /// `operand IS NULL` — true iff the operand evaluates to null.
189    IsNull(Operand),
190    /// `operand IS NOT NULL` — true iff the operand is non-null.
191    IsNotNull(Operand),
192    /// `expr IN [a, b, $p]` or `expr IN $list` (`$list` is `Value::List`).
193    In {
194        expr: Operand,
195        list: Vec<Operand>,
196    },
197}
198
199#[derive(Debug, Clone, PartialEq)]
200pub enum Operand {
201    Prop {
202        var: String,
203        field: String,
204    },
205    Lit(Value),
206    Param(String),
207    /// Bare variable reference (used in `WITH … WHERE alias > 2`).
208    Var(String),
209    /// Scalar function call: `toLower(n.name)`, `size(n.tags)`, `type(r)`, etc.
210    ///
211    /// Supported functions (case-insensitive): `toLower`, `toUpper`, `size`,
212    /// `coalesce`, `type`, `abs`, `round`, `textMatches`, `contains`,
213    /// `startsWith`, `endsWith`, `toInteger`, `toFloat`, `toString`, `decay`,
214    /// `key`.
215    /// Unknown names → named error at execution time listing the supported
216    /// set (see `SCALAR_FUNCS` in `exec.rs`).
217    FuncCall {
218        name: String,
219        args: Vec<Operand>,
220    },
221    /// Arithmetic expression inside a function argument: `abs(n.age - 27)`,
222    /// `round(n.score * 1.5)`.  Supports `+`, `-`, `*`, `/`.
223    BinArith {
224        op: ArithOp,
225        left: Box<Operand>,
226        right: Box<Operand>,
227    },
228    /// List subscript: `n.tags[0]`, `n.location[1]`, `$list[$i]`.
229    ///
230    /// Evaluates `base`, which must be a `Value::List`, and returns the
231    /// element at `index`. A negative index counts from the end
232    /// (openCypher §3.4.4). An out-of-range index, a non-list base, or a
233    /// non-integer index all evaluate to null rather than erroring, so a
234    /// subscript behaves like a missing property.
235    Index {
236        base: Box<Operand>,
237        index: Box<Operand>,
238    },
239    /// Generic `CASE WHEN <cond> THEN <value> [WHEN …] [ELSE <value>] END`.
240    /// Evaluates each branch's condition in order, returning the first matching
241    /// value; the `default` (ELSE) or null if none match.
242    Case {
243        branches: Vec<(Expr, Operand)>,
244        default: Option<Box<Operand>>,
245    },
246}
247
248/// The column name an operand gets in a RETURN or WITH item that has no
249/// `AS` alias.
250///
251/// There are three copies of the column-naming rule — the planner's
252/// duplicate-column check, the executor's projection, and core-api's
253/// write-statement RETURN — and they have to agree exactly or a `WITH` alias
254/// stops resolving. This is the one place the operand half of that rule
255/// lives.
256///
257/// A composite operand renders as a placeholder (`<arith>`, `<case>`)
258/// because it has no natural spelling; a subscript does have one, so
259/// `n.location[0]` names itself and two subscripts of the same list are two
260/// distinct columns rather than a duplicate-column error.
261pub fn operand_label(op: &Operand) -> String {
262    match op {
263        Operand::Var(v) => v.clone(),
264        Operand::Prop { var, field } => format!("{var}.{field}"),
265        Operand::Lit(_) => "<lit>".to_string(),
266        Operand::Param(p) => format!("${p}"),
267        Operand::FuncCall { name, .. } => format!("{name}(...)"),
268        Operand::BinArith { .. } => "<arith>".to_string(),
269        Operand::Case { .. } => "<case>".to_string(),
270        Operand::Index { base, index } => {
271            format!("{}[{}]", operand_label(base), subscript_label(index))
272        }
273    }
274}
275
276/// The index half of a subscript's column name. A literal integer prints as
277/// itself — `<lit>` would make every subscript of one list the same column.
278fn subscript_label(op: &Operand) -> String {
279    match op {
280        Operand::Lit(Value::Int(n)) => n.to_string(),
281        other => operand_label(other),
282    }
283}
284
285/// The column name a RETURN item takes when it has no `AS` alias, for the
286/// item kinds whose name does not depend on the planner.
287///
288/// `RetVal::Agg` is not here: an aggregate's column is computed by the
289/// planner and stored on the plan op, and the three callers disagree about
290/// what to do with it.
291pub fn ret_val_label(value: &RetVal) -> Option<String> {
292    match value {
293        RetVal::Var(v) => Some(v.clone()),
294        RetVal::Prop { var, field } => Some(format!("{var}.{field}")),
295        RetVal::FuncCall { name, args } => {
296            let arg_strs: Vec<String> = args.iter().map(operand_label).collect();
297            Some(format!("{name}({})", arg_strs.join(", ")))
298        }
299        // A subscript names itself; every other scalar expression is `<expr>`.
300        RetVal::ScalarExpr(op @ Operand::Index { .. }) => Some(operand_label(op)),
301        RetVal::ScalarExpr(_) => Some("<expr>".to_string()),
302        RetVal::Agg { .. } => None,
303    }
304}
305
306/// Arithmetic operators for `Operand::BinArith`.
307#[derive(Debug, Clone, PartialEq)]
308pub enum ArithOp {
309    Add,
310    Sub,
311    Mul,
312    Div,
313}
314
315/// RETURN item value: bare variable, `var.field`, an aggregate call, a
316/// scalar function call, or an arbitrary scalar expression (arithmetic etc.).
317#[derive(Debug, Clone, PartialEq)]
318pub enum RetVal {
319    Var(String),
320    Prop {
321        var: String,
322        field: String,
323    },
324    /// Single aggregate function call.  When combined with non-aggregate items
325    /// in the same RETURN clause, the planner routes to `GroupAggregate`.
326    /// Multiple aggregate items are also supported via `GroupAggregate`.
327    Agg {
328        func: AggFunc,
329        arg: AggArg,
330    },
331    /// Scalar function call in a RETURN position, e.g. `toLower(n.name)`.
332    /// The same function set as `Operand::FuncCall` (see `SCALAR_FUNCS`).
333    FuncCall {
334        name: String,
335        args: Vec<Operand>,
336    },
337    /// Arbitrary scalar expression in a RETURN/WITH position, e.g. `n.age + 1`.
338    /// Evaluated via `resolve_operand` at execution time; null propagates.
339    ScalarExpr(Operand),
340}
341
342#[derive(Debug, Clone, PartialEq)]
343pub struct RetItem {
344    pub value: RetVal,
345    pub alias: Option<String>,
346}
347
348/// ORDER BY target. Bare identifiers that match a RETURN alias become `Alias`;
349/// otherwise they stay `Var`. `var.field` is always `Prop`.
350#[derive(Debug, Clone, PartialEq)]
351pub enum OrderTarget {
352    Alias(String),
353    Var(String),
354    Prop { var: String, field: String },
355}
356
357#[derive(Debug, Clone, PartialEq)]
358pub struct OrderItem {
359    pub target: OrderTarget,
360    pub descending: bool,
361}
362
363// ─── Write statement AST ──────────────────────────────────────────────────────
364
365/// Top-level write statement (CREATE / MATCH…SET / MATCH…DELETE / MATCH…DETACH DELETE / MERGE).
366/// Produced by `parse_write`; executed by `GraphDb::query_write`.
367#[derive(Debug, Clone, PartialEq)]
368pub enum WriteStatement {
369    Create(CreateStmt),
370    MatchSet(MatchSetStmt),
371    MatchDelete(MatchDeleteStmt),
372    MatchDeleteNode(MatchDeleteNodeStmt),
373    Merge(MergeStmt),
374}
375
376/// `CREATE (a:L {id: 'x', ...})[-[:T]->(b:L2 {id: 'y', ...})] [RETURN …]`
377///
378/// `nodes` is in encounter order. `edges` reference node vars that appear in
379/// `nodes`. Each node must have a string-valued `id` property (used as key).
380///
381/// `returns`, when `Some`, projects the created bindings as a read result.
382/// The write and the projection are committed as a single WAL batch frame.
383#[derive(Debug, Clone, PartialEq)]
384pub struct CreateStmt {
385    pub nodes: Vec<CreateNode>,
386    pub edges: Vec<CreateEdge>,
387    /// Optional RETURN clause: project created bindings after commit.
388    pub returns: Option<Vec<RetItem>>,
389}
390
391/// One node in a CREATE pattern.
392#[derive(Debug, Clone, PartialEq)]
393pub struct CreateNode {
394    /// Optional binding variable (`a` in `(a:Label {…})`).
395    pub var: Option<String>,
396    pub label: String,
397    /// Literal property pairs.  Must include a string-valued `id` field.
398    pub props: Vec<(String, core_storage::Value)>,
399}
400
401/// One edge in a CREATE pattern, referencing vars from `CreateStmt.nodes`.
402#[derive(Debug, Clone, PartialEq)]
403pub struct CreateEdge {
404    pub src_var: String,
405    pub etype: String,
406    pub dst_var: String,
407}
408
409/// `MATCH patterns [WHERE expr] SET var.field = literal [, …] [RETURN …]`
410#[derive(Debug, Clone, PartialEq)]
411pub struct MatchSetStmt {
412    pub matches: Vec<Pattern>,
413    pub where_expr: Option<Expr>,
414    pub sets: Vec<SetClause>,
415    /// Optional RETURN clause: project matched bindings from post-write state.
416    pub returns: Option<Vec<RetItem>>,
417}
418
419/// One `var.field = literal_or_param` assignment in a SET clause.
420///
421/// `value` is an `Operand` rather than a bare `Value` so that `$param`
422/// references are legal on the RHS (resolved at execution time from the
423/// query's parameter map).  Only `Operand::Lit` and `Operand::Param` are
424/// accepted by the parser; other variants produce a named parse error.
425#[derive(Debug, Clone, PartialEq)]
426pub struct SetClause {
427    pub var: String,
428    pub field: String,
429    pub value: Operand,
430}
431
432/// `MATCH patterns [WHERE expr] DELETE rel_var [, …]`
433///
434/// Each `EdgeDelete` carries the etype, src node var, and dst node var resolved
435/// from the MATCH patterns at parse time so the executor can emit a read query
436/// returning node keys without extra pattern-scanning.
437#[derive(Debug, Clone, PartialEq)]
438pub struct MatchDeleteStmt {
439    pub matches: Vec<Pattern>,
440    pub where_expr: Option<Expr>,
441    pub deletes: Vec<EdgeDelete>,
442}
443
444/// One edge variable to delete, with resolved topology.
445#[derive(Debug, Clone, PartialEq)]
446pub struct EdgeDelete {
447    pub rel_var: String,
448    pub etype: String,
449    pub src_var: String,
450    pub dst_var: String,
451}
452
453/// `MATCH patterns [WHERE expr] [DETACH] DELETE node_var [, …]`
454///
455/// When `detach` is `true` (DETACH DELETE), all incident edges are removed
456/// before the node is tombstoned (openCypher semantics).  When `false`
457/// (bare DELETE), the node must have no incident edges; the executor returns
458/// an error if any remain.
459#[derive(Debug, Clone, PartialEq)]
460pub struct MatchDeleteNodeStmt {
461    pub matches: Vec<Pattern>,
462    pub where_expr: Option<Expr>,
463    /// Node variable names whose nodes are to be deleted (resolved from MATCH patterns).
464    pub node_vars: Vec<String>,
465    /// `true` → DETACH DELETE (allowed regardless of edges).
466    /// `false` → bare DELETE (error if any edges touch the node).
467    pub detach: bool,
468}
469
470/// `MERGE (n:Label {id: 'x'}) [ON CREATE SET …] [ON MATCH SET …] [RETURN …]`
471///
472/// Exactly one identifying property is allowed (the key). An optional `ns`
473/// names the namespace the create arm writes into; any other extra property
474/// is a named error. `ON CREATE SET` / `ON MATCH SET` apply inside the same
475/// write batch as the insert-or-skip. `returns`, when `Some`, projects the
476/// node after commit.
477#[derive(Debug, Clone, PartialEq)]
478pub struct MergeStmt {
479    pub label: String,
480    /// The single property that identifies the node. Value must be a string.
481    pub key_field: String,
482    pub key_value: core_storage::Value,
483    /// Optional `ns` named in the MERGE pattern, distinct from the identifying
484    /// key. Absent = the create arm does not name a namespace.
485    pub ns: Option<core_storage::Value>,
486    /// Optional bound variable for the MERGE node (for RETURN projection).
487    pub var: Option<String>,
488    /// `ON CREATE SET` assignments, applied only when the node is inserted.
489    pub on_create: Vec<SetClause>,
490    /// `ON MATCH SET` assignments, applied only when the node already exists.
491    pub on_match: Vec<SetClause>,
492    /// Optional RETURN clause: project the node after commit.
493    pub returns: Option<Vec<RetItem>>,
494}