marsdb_query/ast.rs
1#[derive(Debug, Clone, PartialEq)]
2pub enum Literal {
3 Int(i64),
4 Float(f64),
5 String(String),
6 Bool(bool),
7 Null,
8 /// `$name` placeholder — resolved to a concrete `Literal` by
9 /// `params::substitute_params` before execution, never seen by the
10 /// executor.
11 Param(String),
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct PropAccess {
16 pub var: String,
17 pub prop: String,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum CompareOp {
22 Eq,
23 Ne,
24 Lt,
25 Le,
26 Gt,
27 Ge,
28 /// String-only predicates (`a.name STARTS WITH 'x'`, etc.) — anything
29 /// but a `String`/`String` operand pair compares `false`, same as every
30 /// other type-mismatched `CompareOp` already does in `compare()`.
31 StartsWith,
32 EndsWith,
33 Contains,
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub enum Expr {
38 And(Box<Expr>, Box<Expr>),
39 Or(Box<Expr>, Box<Expr>),
40 Not(Box<Expr>),
41 Compare(PropAccess, CompareOp, Literal),
42 /// `a.id = b.id` / `x.val < y.val` — a property compared against
43 /// *another* property, not a constant. Never eligible for the
44 /// planner's index-seek fusion (that only matches `Compare`'s
45 /// literal-RHS shape), always evaluated as a generic post-scan filter.
46 PropCompare(PropAccess, CompareOp, PropAccess),
47 /// `n.prop IS NULL` — unlike `Compare`, this is always a definite
48 /// `true`/`false`, never "unknown" (that's the whole point of the
49 /// check). `IS NOT NULL` parses to `Not(IsNull(..))`, reusing the
50 /// existing `Not` variant rather than a fourth boolean-op variant.
51 IsNull(PropAccess),
52 /// Does the node bound to `var` have label `label` among its (possibly
53 /// multiple) labels? Synthesized by the planner for the 2nd+ label in a
54 /// multi-label pattern like `(n:Post:Message)`, *and* user-typed
55 /// directly in a `WHERE` (`WHERE a:A`, `WHERE a:A:B` desugars to an
56 /// `And` chain of one `HasLabel` per label).
57 HasLabel(String, String),
58 /// Do these two row bindings refer to the same node/edge? Synthesized
59 /// by the planner when a pattern's hop variable is a "bound-node
60 /// repetition" — the same variable already bound earlier reappearing
61 /// mid-pattern (e.g. IS7's `p`, bound by an earlier MATCH, reappearing
62 /// as the endpoint of an OPTIONAL MATCH pattern: `(a)-[r:KNOWS]-(p)`
63 /// must mean "KNOWS *this* `p`", not "KNOWS anyone"). Also user-typed
64 /// directly in a `WHERE` (`WHERE a = b`; `WHERE a <> b` desugars to
65 /// `Not(VarEq(a, b))`) — real Cypher's node/relationship identity
66 /// comparison, distinct from comparing two of their *properties*
67 /// (`PropCompare`) or two arbitrary values (`WithExpr::Compare`,
68 /// post-projection only).
69 VarEq(String, String),
70 /// `WHERE toInteger(n.id) = 1`, `WHERE r.weight * 2 > n.threshold`,
71 /// ... -- any comparison whose operand isn't the narrower
72 /// `prop_access`/`literal` shape `Compare`/`PropCompare` cover
73 /// (a function call, arithmetic, a bare variable compared to
74 /// something, ...). Same operand type `WithExpr::Compare` uses
75 /// (`ReturnExpr`, built from the shared `add_expr` grammar rule), but
76 /// this variant keeps pattern-level `Expr`'s own pre-projection
77 /// evaluation context (`Executor::eval_expr`, against the raw
78 /// `BindingRow`, not a post-projection value map) -- never eligible
79 /// for the planner's index-seek fusion, always a generic post-scan
80 /// filter, same as `PropCompare`.
81 GeneralCompare(ReturnExpr, CompareOp, ReturnExpr),
82 /// `WHERE r IS NULL` (a whole bound variable, e.g. checking an
83 /// `OPTIONAL MATCH` miss) or `WHERE toInteger(n.id) IS NULL` -- unlike
84 /// `IsNull`, the operand isn't restricted to a bare `prop_access`.
85 /// Mirrors `WithExpr::IsNull` exactly, just evaluated pre-projection.
86 GeneralIsNull(ReturnExpr),
87 /// A boolean-valued expression used directly as a predicate with no
88 /// comparison operator at all -- `WHERE single(x IN list WHERE x = 2)
89 /// OR all(x IN list WHERE x = 2)`, `WHERE n.flag`, `WHERE NOT
90 /// exists(n.prop)`. Three-valued (`Null` is "unknown", same as every
91 /// other `Expr` leaf), evaluated via `value_to_bool3`. Mirrors
92 /// `WithExpr::Bare` exactly, just evaluated pre-projection.
93 GeneralBare(ReturnExpr),
94 /// `WHERE (n)-[:REL]->(m)` etc (TCK's Pattern1 "Pattern predicate") --
95 /// existential: true iff at least one real match of `Pattern` exists
96 /// against the graph, with every named endpoint already bound in the
97 /// current row held fixed to that binding rather than searched freely
98 /// (a pattern predicate never introduces a new variable -- real
99 /// Cypher's `UndefinedVariable`, checked at compile time by
100 /// `semantic::validate_pattern_predicate`). Evaluated by
101 /// `Executor::eval_expr` via the same `build_match_plan` "already-
102 /// bound var -> Seed" mechanism `eval_merge`'s own "try as an
103 /// ordinary MATCH first" half already uses.
104 Pattern(Pattern),
105 /// `WHERE exists { (n)-->(m) WHERE n.prop = m.prop }` (TCK's
106 /// ExistentialSubquery1, the "simple" form) -- unlike `Pattern`
107 /// above, `pattern` here *can* introduce brand-new variables (`m`
108 /// above; real Cypher allows that inside an `exists {}` block, unlike
109 /// a bare pattern predicate), and carries its own inline `where?`
110 /// clause (the grammar's `patternWhere` rule, shared with `MATCH`'s
111 /// own pattern -- `where_clause` is threaded into `build_match_plan`
112 /// directly, same as an ordinary `MATCH ... WHERE ...`, rather than
113 /// evaluated as a separate post-filter step). Existence check only
114 /// (`Some(1)`-limited, same as `Pattern`) -- see `ExistsSubquery` below
115 /// for the "full" `exists { MATCH ... RETURN ... }` subquery form.
116 Exists {
117 pattern: Box<Pattern>,
118 where_clause: Option<Box<Expr>>,
119 },
120 /// `WHERE exists { MATCH ... RETURN ... }` (TCK's
121 /// ExistentialSubquery2/3) -- runs an arbitrary read-only
122 /// `Statement::Match` correlated against the current row (its already-
123 /// bound variables seed the nested statement's own scope, same
124 /// already-bound-var -> Seed mechanism `Exists`/`Pattern` above use),
125 /// true iff it produces at least one output row. Unlike `Exists`
126 /// above, the nested statement can carry its own aggregation/multiple
127 /// clauses/nested `exists {}` -- `semantic::validate_statement`
128 /// rejects any mutating clause inside it at compile time (real
129 /// Cypher's `InvalidClauseComposition`, TCK's ExistentialSubquery2
130 /// `[3]`).
131 ExistsSubquery(Box<Statement>),
132 /// Real Cypher's edge-isomorphism rule (`VarEq`'s own docs), extended
133 /// to a variable-length hop: `edge_var`'s bound edge must not be among
134 /// the edges an *earlier* variable-length hop in the same pattern
135 /// already traversed for this row (`edge_set_var`, a `Binding::Path`
136 /// segment `planner::build_match_plan` always threads through a
137 /// `VarExpand` for exactly this purpose -- see `LogicalPlan::
138 /// VarExpand::exclude_edge_var`'s own docs). Unlike `VarEq` (one edge
139 /// vs one edge), this checks one edge against a *set* -- each row can
140 /// carry a different set, one per distinct earlier traversal.
141 /// Synthesized by the planner only; no surface syntax constructs this
142 /// directly.
143 EdgeNotInSet {
144 edge_var: String,
145 edge_set_var: String,
146 },
147}
148
149#[derive(Debug, Clone, PartialEq)]
150pub enum ReturnExpr {
151 Var(String),
152 Prop(PropAccess),
153 /// `<expr>.prop` where `<expr>` is anything other than a bare variable
154 /// (`startNode(r).id`, `head(nodes(p)).name`, `{a: 1}.a`) — `Prop`
155 /// only covers the flat `var.prop` case (matching pest's own
156 /// `prop_access` rule, a dedicated `identifier DOT identifier`
157 /// production). TCK's Graph6 [4]/[8], Map1 [3], Merge5 [11].
158 PropOf(Box<ReturnExpr>, String),
159 Lit(Literal),
160 Call {
161 name: String,
162 args: Vec<ReturnExpr>,
163 distinct: bool,
164 },
165 /// `count(*)` — its own variant, not `Call` with a magic `"*"`-sentinel
166 /// argument, so evaluation physically cannot mishandle it as an
167 /// ordinary function call (no args to evaluate, no DISTINCT target —
168 /// it counts rows, not values).
169 CountStar,
170 /// `CASE <test> WHEN <value> THEN <result> ... [ELSE <else>] END`
171 /// (simple form, `test: Some`) or `CASE WHEN <bool_expr> THEN <result>
172 /// ... [ELSE <else>] END` (searched form, `test: None` -- each `WHEN`
173 /// carries its own full condition instead of a value compared against
174 /// `test`).
175 Case {
176 test: Option<Box<ReturnExpr>>,
177 whens: Vec<(ReturnExpr, ReturnExpr)>,
178 else_: Option<Box<ReturnExpr>>,
179 },
180 /// `lhs op rhs` — `+ - * / %`, real precedence (`*`/`/`/`%` bind
181 /// tighter than `+`/`-`), usable anywhere a `ReturnExpr` is (RETURN/
182 /// WITH items, `CASE` branches, function args, `ORDER BY` keys).
183 /// Deliberately not threaded into pattern-level `WHERE` (`Expr`) or
184 /// `WITH ... WHERE` (`WithExpr`)'s comparison operands in this pass --
185 /// both currently take a bare `PropAccess`/`Literal` on each side, and
186 /// widening that is a separate, larger change to the planner's
187 /// pre-projection `Filter` pushdown.
188 Arith(Box<ReturnExpr>, ArithOp, Box<ReturnExpr>),
189 /// `-x` — general unary negation (`-n.prop`, `-(1 + 2)`, `-f()`, ...),
190 /// distinct from a negative numeric *literal* (`-3` is still just
191 /// `Lit(Int(-3))`, parsed directly by `int_literal`/`float_literal`'s
192 /// own optional leading `-` — see `cypher.pest`'s `unary_minus_expr`
193 /// docs for why that path is deliberately left untouched, rather than
194 /// this variant subsuming it, to avoid losing the planner's index-seek
195 /// fusion for `MATCH (n {x: -5})`-shaped literal patterns). Binds
196 /// tighter than every other arithmetic operator, including `^`.
197 Neg(Box<ReturnExpr>),
198 /// `[a, b, c]` — a general expression list, not `UnwindSource::List`'s
199 /// literal-only cousin (that one's deliberately scoped to right after
200 /// `UNWIND`; this one is a real `ReturnExpr`, usable anywhere one is).
201 ListLit(Vec<ReturnExpr>),
202 /// `list[index]` — a negative index counts from the end
203 /// (`list[-1]` is the last element); out of bounds either way is
204 /// `Null`, not an error (matches real Cypher).
205 Index(Box<ReturnExpr>, Box<ReturnExpr>),
206 /// `list[start..end]` — either bound omitted means "from/to the edge
207 /// of the list". Same negative-counts-from-end rule as `Index`, but
208 /// out-of-range bounds clamp instead of nulling out, and a start at or
209 /// past the (clamped) end yields `[]` rather than erroring.
210 Slice(
211 Box<ReturnExpr>,
212 Option<Box<ReturnExpr>>,
213 Option<Box<ReturnExpr>>,
214 ),
215 /// `[x IN <source> WHERE <cond> | <project>]` — `WHERE`/`| project` are
216 /// each independently optional (`[x IN list]` is a legal no-op
217 /// identity-filter comprehension). `where_clause` is a `ReturnExpr`
218 /// (not pattern-level `Expr`) for the same reason `UnwindClause`'s own
219 /// filter used to reuse `WithExpr` — `var` is very often a bare
220 /// scalar/node/edge, not something `Expr::Compare`'s
221 /// `prop_access`-only LHS can express. Now that boolean logic/
222 /// comparisons are real `ReturnExpr` variants (`And`/`Or`/`Not`/
223 /// `Compare`), this is the wider type `WithExpr` used to be, letting a
224 /// bare `WHERE x`/`WHERE true` parse (previously rejected — `WithExpr`
225 /// only ever wrapped a `Compare`, never a standalone boolean value).
226 ListComp {
227 var: String,
228 source: Box<ReturnExpr>,
229 where_clause: Option<Box<ReturnExpr>>,
230 project: Option<Box<ReturnExpr>>,
231 },
232 /// `ALL(x IN list WHERE cond)` / `ANY(...)` / `NONE(...)` / `SINGLE(...)`
233 /// — shares `ListComp`'s "one bound variable over a list, optionally
234 /// filtered" shape (no `project` half; a quantifier always yields a
235 /// `Bool`, never a projected list). `where_clause` absent means "every
236 /// element's own truthiness", same convention `CASE`'s subject-less
237 /// `WHEN` branch already uses (`matches!(v, Literal(Bool(true)))`).
238 Quantifier {
239 kind: QuantifierKind,
240 var: String,
241 source: Box<ReturnExpr>,
242 where_clause: Option<Box<ReturnExpr>>,
243 },
244 /// `{a: 1, b: 2 + 1}` — a general expression map. `NodePattern`/
245 /// `RelPattern`'s own `props` reuse this same `ReturnExpr` value type
246 /// (not a separate `Literal`-only map) for the identical `{...}`
247 /// pattern syntax — a `CREATE`/`MERGE` prop value can be any
248 /// expression too (`{date: date({year: 1984, ...})}`), evaluated
249 /// against the row already bound so far (`Executor::
250 /// eval_props_to_values`). `MATCH`/`MERGE`'s own inline pattern props
251 /// specifically are further restricted back down to plain literals at
252 /// plan-build time (`planner::require_literal_pattern_prop`) — a
253 /// computed value there doesn't make sense before any row exists to
254 /// evaluate it against, matching real Cypher's own restriction.
255 MapLit(Vec<(String, ReturnExpr)>),
256 /// `lhs AND/OR/XOR rhs`, `NOT rhs` — real three-valued logic (`Null`
257 /// propagates per Cypher's truth tables, see `and3`/`or3`/`xor3` in
258 /// executor.rs), evaluating to `Value::Literal(Bool(_))` or
259 /// `Value::Null`, not `Option<bool>` the way pattern-level `Expr`/
260 /// `WithExpr` do — a `ReturnExpr` always evaluates to one `Value`,
261 /// there's no separate "unbound" state to fold in beyond `Null`
262 /// itself. A non-bool, non-null operand is a real error (`1 AND
263 /// true`), not silently coerced.
264 And(Box<ReturnExpr>, Box<ReturnExpr>),
265 Or(Box<ReturnExpr>, Box<ReturnExpr>),
266 Xor(Box<ReturnExpr>, Box<ReturnExpr>),
267 Not(Box<ReturnExpr>),
268 /// `lhs op rhs` — a single comparison between two arbitrary
269 /// expressions (both operands can be a variable/property/arithmetic
270 /// expression, same as `WithExpr::Compare`'s two `ReturnExpr`
271 /// operands). A chain (`1 < x < 10`) parses into nested `And`s of
272 /// each adjacent pair (`(1 < x) AND (x < 10)`), same as real Cypher's
273 /// own chained-comparison semantics — not a separate AST shape.
274 /// `WithExpr::Compare` doesn't chain this way (its own grammar level
275 /// doesn't recurse into itself), just a single comparison per node.
276 Compare(Box<ReturnExpr>, CompareOp, Box<ReturnExpr>),
277 /// `x IS NULL` — always a definite `true`/`false`, never "unknown"
278 /// (that's the whole point of the check). `IS NOT NULL` parses to
279 /// `Not(IsNull(..))`, reusing the existing `Not` variant.
280 IsNull(Box<ReturnExpr>),
281 /// `x IN list` — real Cypher's list membership test, three-valued
282 /// like `=` (`null IN [1]` and `1 IN [null]` are both "unknown", not
283 /// `false`): a definite element match wins outright even past a later
284 /// `null` element, no match with at least one `null` element compared
285 /// is "unknown", no match and no `null` anywhere is a definite
286 /// `false`. Binds *tighter* than a surrounding comparison, same
287 /// precedence tier as `IsNull` (`a = b IN list` is `a = (b IN
288 /// list)`) — see `compare_expr`'s grammar comment.
289 In(Box<ReturnExpr>, Box<ReturnExpr>),
290 /// `(n:Foo)`/`(n:Foo:Bar)` used as a boolean expression — `true` iff
291 /// the bound node has every listed label, `false` otherwise (a
292 /// definite bool, not three-valued — same as `Expr::HasLabel`, the
293 /// pattern-position sibling this mirrors, but reachable directly from
294 /// `RETURN`/`WITH`/`WHERE` instead of only ever being synthesized by
295 /// the planner for a multi-label pattern token).
296 HasLabel(String, Vec<String>),
297 /// `(n)-[]->()` etc used directly as a boolean expression -- existential
298 /// pattern-predicate syntax, same shape as `Expr::Pattern` (the
299 /// pattern-level sibling this mirrors), but reachable from generic
300 /// expression position (parsed via the same `atom` alternative
301 /// anything else is). In practice only ever meaningful inside `WHERE`
302 /// -- `return_expr_to_expr` folds it into `Expr::Pattern` there before
303 /// it ever reaches `RETURN`/`WITH` position or the executor. Reaching
304 /// `RETURN (n)-->()`/a RETURN item/property value/etc directly (no
305 /// `Expr`-folding step in between) is a real error, not evaluated --
306 /// existence can't be checked without a bound row to check it against,
307 /// which only `WHERE`'s evaluation context has.
308 PatternPredicate(Pattern),
309 /// `[p = (n)-->() | p]` / `[(n)-[:T]->(b) | b.name]` -- unlike
310 /// `PatternPredicate` (existence-only, folded into `Expr::Pattern`
311 /// before it ever reaches the executor), this enumerates *every*
312 /// match of `pattern` (each held-fixed against already-bound named
313 /// endpoints, same as `Expr::Pattern`/`PatternPredicate`) and projects
314 /// `projection` against each match's own bindings -- which include
315 /// any *new* node/relationship variables the pattern introduces
316 /// (e.g. `b`/`r` above; real Cypher allows that here, unlike a
317 /// pattern predicate) -- collecting the results into a `Value::List`.
318 /// `where_clause` is pattern-level `Expr` (not `ReturnExpr`), reusing
319 /// the exact same evaluation `Executor::eval_expr`'s `Expr::Pattern`
320 /// arm already does for a pattern's own `WHERE` -- the grammar's
321 /// `patternComprehension` rule shares its `where?` production with
322 /// ordinary `MATCH`, not with `ListComp`'s post-projection
323 /// `WithExpr`-shaped filter.
324 PatternComprehension {
325 path_var: Option<String>,
326 pattern: Box<Pattern>,
327 where_clause: Option<Box<Expr>>,
328 projection: Box<ReturnExpr>,
329 },
330 /// `exists { (n)-->(m) WHERE ... }` reached from general expression
331 /// position -- same "only meaningful inside WHERE" story as
332 /// `PatternPredicate` (`return_expr_to_expr` folds it into
333 /// `Expr::Exists` there before it ever reaches the executor;
334 /// reaching `eval_return_expr` directly is a real error).
335 ExistsPattern {
336 pattern: Box<Pattern>,
337 where_clause: Option<Box<Expr>>,
338 },
339 /// `exists { MATCH ... RETURN ... }` reached from general expression
340 /// position -- the "full" nested-subquery form of `exists {}`
341 /// (`ExistsPattern`'s complement, TCK's ExistentialSubquery2/3), an
342 /// arbitrary read-only `Statement::Match` (any number of MATCH/UNWIND/
343 /// WITH clauses, its own aggregation/WHERE, ending in a RETURN) run
344 /// correlated against whatever's already bound in the enclosing row --
345 /// same "only meaningful inside WHERE" story as `ExistsPattern`
346 /// (`return_expr_to_expr` folds it into `Expr::ExistsSubquery` there
347 /// before it ever reaches the executor; reaching `eval_return_expr`
348 /// directly is a real error). The inner statement's own RETURN items
349 /// are never actually projected out -- only whether it produces at
350 /// least one row matters, same as `ExistsPattern`.
351 ExistsSubquery(Box<Statement>),
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355pub enum QuantifierKind {
356 All,
357 Any,
358 None,
359 Single,
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub enum ArithOp {
364 Add,
365 Sub,
366 Mul,
367 Div,
368 Mod,
369 /// `a ^ b` -- always produces a `Float`, even for two `Int` operands
370 /// (real Cypher's own rule; unlike every other `ArithOp`, there's no
371 /// Int/Int-stays-Int case). Right-associative and binds tighter than
372 /// `*`/`/`/`%` but looser than unary minus (`-3 ^ 2` is `(-3) ^ 2`,
373 /// not `-(3 ^ 2)`) -- see `cypher.pest`'s `pow_expr`/`unary_minus_expr`.
374 Pow,
375}
376
377/// Case-insensitive aggregate-function recognition, shared by `parser.rs`
378/// (DISTINCT-validity check) and `executor.rs` (grouping classification —
379/// a RETURN/WITH item list "has an aggregate" iff any item's top-level
380/// expression is `CountStar` or a `Call` whose name passes this check).
381pub fn is_aggregate_name(name: &str) -> bool {
382 matches!(
383 name.to_ascii_lowercase().as_str(),
384 "count" | "sum" | "avg" | "min" | "max" | "collect" | "percentilecont" | "percentiledisc"
385 )
386}
387
388/// `percentileCont`/`percentileDisc` are the only aggregates that take a
389/// second argument (the percentile, `0.0..=1.0`) alongside the value being
390/// aggregated -- every other name in `is_aggregate_name` takes exactly one.
391pub fn is_percentile_name(name: &str) -> bool {
392 matches!(
393 name.to_ascii_lowercase().as_str(),
394 "percentilecont" | "percentiledisc"
395 )
396}
397
398#[derive(Debug, Clone, PartialEq)]
399pub struct ReturnItem {
400 pub expr: ReturnExpr,
401 pub alias: Option<String>,
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
405pub enum SortDir {
406 Asc,
407 Desc,
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411pub enum RelDirection {
412 /// (a)-[..]->(b)
413 Right,
414 /// (a)<-[..]-(b)
415 Left,
416 /// (a)-[..]-(b) — matches either direction.
417 Either,
418}
419
420#[derive(Debug, Clone, PartialEq)]
421pub struct NodePattern {
422 pub var: Option<String>,
423 pub labels: Vec<String>,
424 /// `ReturnExpr`, not `Literal` — a CREATE prop value can be any
425 /// expression (`{date: date({year: 1984, ...})}`, `{x: 1 + 2}`), not
426 /// just a literal; see `cypher.pest`'s `map_expr` docs and
427 /// `Executor::eval_props_to_values`, which evaluates each one against
428 /// the row already bound so far in the same CREATE.
429 pub props: Vec<(String, ReturnExpr)>,
430 /// Whether an inline `{...}` map token was actually written, even an
431 /// empty one (`(n {})`) -- `props` alone can't distinguish that from
432 /// no map token at all (`(n)`), both giving an empty `Vec`, but real
433 /// Cypher's `VariableAlreadyBound` check cares about the distinction:
434 /// `MATCH (n) CREATE (n {})` is still "imposing a new predicate" on
435 /// an already-bound node even though the map is empty (TCK's Create1
436 /// `[19]`), while `MATCH (n) CREATE (n)-->()` (no map token at all)
437 /// is fine.
438 pub has_explicit_props: bool,
439}
440
441#[derive(Debug, Clone, PartialEq)]
442pub struct RelPattern {
443 pub var: Option<String>,
444 /// `[:A]` -- one element; `[:A|B]`/`[:A|:B]` (real Cypher accepts
445 /// either separator form) -- more than one, matched if the edge's
446 /// type is ANY of them (TCK's Match2 [6]/Match3 [8], Pattern1 [13]).
447 /// Empty means untyped (`[]`/`[r]`, any type matches).
448 pub rel_types: Vec<String>,
449 pub props: Vec<(String, ReturnExpr)>,
450 pub direction: RelDirection,
451 /// `[:TYPE*min..max]` — `None` means a fixed single hop (existing
452 /// behavior). `max: None` means unbounded, capped at a safety depth by
453 /// the executor.
454 pub hop_range: Option<(u32, Option<u32>)>,
455 /// Set only by `executor::name_pattern_for_path` on a variable-length
456 /// hop, when assembling a named-path capture (`p = (a)-[*1..3]->(b)`,
457 /// TCK's Quantifier1-4 `[8]`/`[9]`) -- asks the planner's `VarExpand`
458 /// to also expose its own internally-traversed edge/node sequence (in
459 /// order) for `executor::assemble_path` to splice into the whole
460 /// pattern's path, via a fresh internal binding name that `var` gets
461 /// overwritten to hold (see `name_pattern_for_path`'s own docs). The
462 /// user's own real relationship-list variable, if this hop had one
463 /// (`p = (a)-[r*1..3]->(b)`, TCK's Match9 `[9]`), is preserved
464 /// separately in `rel_list_var` below rather than lost to that
465 /// overwrite.
466 pub capture_path_segment: bool,
467 /// The user's own `[r:TYPE*1..3]` relationship-list variable name, for
468 /// a hop that has `capture_path_segment` set -- `None` for an
469 /// anonymous hop (`p = (a)-[*1..3]->(b)`, nothing to bind). For a
470 /// var-length hop *without* named-path capture, `var` itself already
471 /// holds this directly (this field stays `None` in that case; the
472 /// planner reads whichever of the two applies). Two separate fields
473 /// rather than reusing `var` for both, since `capture_path_segment`
474 /// already needs `var` for its own internal bookkeeping name.
475 pub rel_list_var: Option<String>,
476}
477
478/// A linear chain: node, (rel, node)*.
479#[derive(Debug, Clone, PartialEq)]
480pub struct Pattern {
481 pub start: NodePattern,
482 pub hops: Vec<(RelPattern, NodePattern)>,
483}
484
485#[derive(Debug, Clone, PartialEq)]
486pub enum Tail {
487 /// `distinct`: `RETURN DISTINCT ...` -- a result-set-level dedup of the
488 /// whole projected row, applied after projection (and after grouping,
489 /// for an aggregating RETURN) -- not the same knob as `DISTINCT` inside
490 /// an aggregate call (`count(DISTINCT x)`), which only affects that one
491 /// aggregate's own accumulation.
492 Return(Vec<ReturnItem>, bool),
493 /// `RETURN *` (`distinct`: `RETURN DISTINCT *`) -- every currently-
494 /// bound variable, alphabetically. Can't be resolved into a concrete
495 /// `Return(Vec<ReturnItem>, _)` at parse time (no scope exists yet);
496 /// resolved independently wherever the real bound-variable-name set
497 /// is already on hand (`execute_match`'s own `carried_vars` in
498 /// `executor.rs`, `Scope`'s keys in `semantic.rs`) rather than via a
499 /// separate AST-mutation pass, avoiding a `&mut Statement` ripple
500 /// through `Executor::execute`'s public signature. `MATCH ()
501 /// RETURN *` (nothing bound at all) is a compile-time
502 /// `NoVariablesInScope` error, not an empty projection.
503 ReturnStar(bool),
504 /// Every mutating tail variant's trailing `Option<ReturnTail>` is real
505 /// Cypher: `MATCH (n) SET n.x = 1 RETURN n`, `MATCH (n) DELETE n RETURN
506 /// count(n)`, etc — see `ReturnTail`'s docs for why it's `None` (the
507 /// pre-existing terminal-mutation shape) vs `Some` (this statement's
508 /// final clause is actually this RETURN, projected off whatever the
509 /// mutation left in scope).
510 /// Each target is any expression, not just a bare variable — real
511 /// Cypher allows `DELETE list[0]`/`DELETE map.key`/`DELETE aPath`
512 /// (deletes every node/edge in the path). Evaluated per row
513 /// (`executor::materialize_delete`); `Value::Null` is a documented
514 /// no-op, anything that isn't a node/relationship/path is a real
515 /// `QueryError::Type`.
516 Delete(Vec<ReturnExpr>, Option<ReturnTail>),
517 DetachDelete(Vec<ReturnExpr>, Option<ReturnTail>),
518 Set(Vec<SetItem>, Option<ReturnTail>),
519 Remove(Vec<RemoveItem>, Option<ReturnTail>),
520 /// `MATCH ... CREATE ...` — same pattern syntax as `Statement::Create`,
521 /// but runs once per row already bound by the preceding MATCH/WITH: a
522 /// node pattern token whose variable is already bound in that row
523 /// reuses the existing node instead of creating a new one. This is
524 /// the only way to add an edge between two nodes that already exist —
525 /// `Statement::Create` alone can't (every node token it sees is
526 /// always fresh).
527 Create(Vec<Pattern>, Option<ReturnTail>),
528}
529
530/// A `RETURN` trailing a mutating `Tail` (`SET`/`DELETE`/`DETACH DELETE`/
531/// `REMOVE`/`MATCH ... CREATE`) in the same statement, e.g. `MATCH (n) SET
532/// n.prop = 1 RETURN n`. Same two fields `Tail::Return` itself carries
533/// (`items`, `distinct`) — wrapped in its own type so every mutating `Tail`
534/// variant can carry one `Option<ReturnTail>` instead of repeating a
535/// `(Vec<ReturnItem>, bool)` tuple five times. Arbitrary multi-clause
536/// chaining (`SET ... DELETE ... RETURN`, a mutating clause followed by
537/// `WITH` before the final `RETURN`) isn't supported yet — this only covers
538/// exactly one mutating clause directly followed by exactly one `RETURN`,
539/// which is the shape the real TCK scenarios for SET/DELETE/REMOVE
540/// overwhelmingly use.
541#[derive(Debug, Clone, PartialEq)]
542pub struct ReturnTail {
543 pub items: Vec<ReturnItem>,
544 pub distinct: bool,
545}
546
547#[derive(Debug, Clone, PartialEq)]
548pub enum SetItem {
549 /// `SET n.prop = <expr>` — the value is any `ReturnExpr` (arithmetic,
550 /// a property read, a function call, ...), not just a literal, same
551 /// as `CREATE`'s inline `{...}` prop values already are. Evaluating
552 /// to `Value::Null` removes the property (matches real Cypher; see
553 /// `executor::apply_set_item`'s docs), not storing a literal null.
554 Prop(PropAccess, ReturnExpr),
555 /// `SET n:A:B` — adds each label to the node's label set (idempotent,
556 /// not an error if already present).
557 Labels(String, Vec<String>),
558 /// `SET n = {...}` (`merge: false`, replaces every existing property)
559 /// or `SET n += {...}` (`merge: true`, only overrides/adds/removes
560 /// -- a `null` value -- the map's own listed keys, everything else on
561 /// `n` stays as-is). `value` must evaluate to a `Value::Map` — TCK's
562 /// Set4/Set5.
563 MapAssign {
564 var: String,
565 value: ReturnExpr,
566 merge: bool,
567 },
568}
569
570#[derive(Debug, Clone, PartialEq)]
571pub enum RemoveItem {
572 Prop(PropAccess),
573 /// `REMOVE n:A:B` — removes each label from the node's label set (not
574 /// an error if it wasn't there).
575 Labels(String, Vec<String>),
576}
577
578/// WITH's HAVING-equivalent: filters on the already-projected/aggregated
579/// row (e.g. `WITH p, count(f) AS c WHERE c > 10`, or `WITH a, b WHERE
580/// a = b`). Same And/Or/Not/Compare shape as `Expr`, but both comparison
581/// operands are a `ReturnExpr` (a WITH alias or raw expression, so either
582/// side can be a bound variable/property, not just the LHS against a
583/// constant) instead of a raw-property `PropAccess`/`Literal` pair —
584/// deliberately a separate type from `Expr` rather than a widened reuse
585/// of it, since `Expr::Compare` is what the planner pushes into
586/// pre-projection `Filter`/`Expand` nodes, and this filter fundamentally
587/// belongs *post*-projection instead (see `materialize_with`).
588#[derive(Debug, Clone, PartialEq)]
589pub enum WithExpr {
590 And(Box<WithExpr>, Box<WithExpr>),
591 Or(Box<WithExpr>, Box<WithExpr>),
592 Not(Box<WithExpr>),
593 Compare(ReturnExpr, CompareOp, ReturnExpr),
594 /// `x IS NULL` -- `x` is any `add_expr`, not just a property access
595 /// (`WHERE r IS NULL`, checking an OPTIONAL MATCH miss on a whole
596 /// bound var). `IS NOT NULL` parses to `Not(IsNull(..))`, same
597 /// convention as pattern-level `Expr::IsNull`.
598 IsNull(ReturnExpr),
599 /// A boolean-valued expression used directly as a predicate with no
600 /// comparison operator at all -- `WHERE single(x IN list WHERE x = 2)
601 /// OR all(x IN list WHERE x = 2)`, `WHERE n.flag`, `WHERE
602 /// exists(n.prop)`. Three-valued (`Null` is "unknown"), evaluated via
603 /// `value_to_bool3` (real Cypher: a non-boolean value here is a type
604 /// error, not silently coerced).
605 Bare(ReturnExpr),
606}
607
608/// A `WITH` clause: projects/renames the current bindings, optionally
609/// filtered/sorted/limited at that boundary, and becomes the binding scope
610/// for whatever follows (the next `QueryPart`, or the final `Tail`).
611#[derive(Debug, Clone, PartialEq)]
612pub struct WithClause {
613 pub items: Vec<ReturnItem>,
614 /// `WITH *` (optionally followed by more items, `WITH *, x AS y`) --
615 /// every currently-bound variable, alphabetically, same convention
616 /// `Tail::ReturnStar`'s own `return_star_items` already established
617 /// for `RETURN *`. Can't be resolved into concrete `items` at parse
618 /// time (no scope exists yet) -- resolved independently wherever the
619 /// real bound-variable-name set entering this WITH is already on
620 /// hand (`executor::apply_with_or_carry`'s own `carried_vars`,
621 /// `semantic::project_with`'s `input: &Scope`, `explain.rs`'s own
622 /// `carried_vars`), mirroring `ReturnStar`'s "resolve at each call
623 /// site" approach rather than a separate whole-AST-mutation pass.
624 /// When both `star` and `items` are present, star-expanded names
625 /// come first (real Cypher has no TCK-tested requirement either way
626 /// for this combination, but this ordering is the common convention).
627 pub star: bool,
628 /// `WITH DISTINCT ...` -- dedups the projected rows, same as `RETURN
629 /// DISTINCT` (`Tail::Return`'s own `distinct` flag), applied right
630 /// after projection/aggregation, before `where_clause` (matching
631 /// `WHERE`'s own "only sees the projected/aggregated names" rule for
632 /// an aggregating `WITH` -- `DISTINCT` puts `WITH` in that same
633 /// post-projection-only regime).
634 pub distinct: bool,
635 pub where_clause: Option<WithExpr>,
636 pub order_by: Option<Vec<(ReturnExpr, SortDir)>>,
637 /// Always applied *after* `order_by` (real Cypher's own rule — skip N,
638 /// then take the following `limit`, against the sorted sequence when
639 /// one exists), regardless of which field a caller happens to read
640 /// first.
641 ///
642 /// Any expression, not just a literal integer — `SKIP $n`, `SKIP
643 /// toInteger(rand()*9)` (TCK's `ReturnSkipLimit1 [2]`/`[3]`) are real
644 /// Cypher. Evaluated exactly once against an empty row (no pattern
645 /// variable can be in scope here) — see
646 /// `executor::resolve_skip_limit`.
647 pub skip: Option<ReturnExpr>,
648 pub limit: Option<ReturnExpr>,
649}
650
651/// One `MATCH <pattern>[, <pattern>...] [WHERE ...] [WITH ...]` segment.
652/// Comma-separated patterns that continue each other (a later pattern's
653/// start is the previous one's last-introduced variable) are spliced into
654/// a single linear `Pattern` at parse time (see
655/// `parser::group_into_linear_patterns`) — this only ever holds one
656/// already-combined `Pattern`, not several. A genuine disjoint cross join
657/// (`MATCH (a:A), (b:B)`) instead becomes *multiple* `QueryPart`s, one per
658/// disjoint group (see `parser::parse_match_part`'s docs).
659///
660/// `path_var` is `Some` for `p = (a)-->(b)` / `p = shortestPath(...)` —
661/// capturing the whole matched path, not just its endpoints. General
662/// named-path capture (`shortest_path: false`) is limited to fixed-hop
663/// patterns — `pattern` must contain no variable-length (`*`) hop, parser-
664/// enforced, since reconstructing a path over `VarExpand`'s BFS would need
665/// the same parent-pointer tracking `shortestPath()` already has, but
666/// generalized, which isn't worth it for the narrow payoff. `shortest_path
667/// : true` is the opposite: `pattern` must be exactly one variable-length
668/// hop (`shortestPath((a)-[:TYPE*..N]-(b))`), and both endpoints must
669/// already be bound by a preceding clause (see `executor::eval_shortest_
670/// path`'s docs for why unbound endpoints aren't supported in v1).
671#[derive(Debug, Clone, PartialEq)]
672pub struct QueryPart {
673 pub optional: bool,
674 pub path_var: Option<String>,
675 pub shortest_path: bool,
676 pub pattern: Pattern,
677 pub where_clause: Option<Expr>,
678 pub with: Option<WithClause>,
679}
680
681/// `UNWIND <source> AS <var> [WHERE ...] [WITH ...]` — fans a list out into
682/// one row per element, cross-joined against whatever rows already exist
683/// (same "row-vector-in, row-vector-out, no graph traversal" shape as a
684/// `WithClause`, not a graph-traversal `LogicalPlan` node — see
685/// `executor::eval_unwind`). Its own `where_clause` (rather than requiring
686/// a `WITH` right after it just to filter) is what makes `UNWIND [1,2,3]
687/// AS x WHERE x > 2` — or `WITH ... collect(m) AS ms UNWIND ms AS m2
688/// WHERE m2.x > 1` — work within the one-`WITH`-per-statement cap (see
689/// `QueryClause`'s docs). Deliberately typed as `WithExpr`, not the
690/// pattern-level `Expr`: an unwound variable is very often a bare scalar
691/// (`x > 2`), which `Expr::Compare`'s always-`PropAccess` LHS structurally
692/// cannot express (only `x.prop > 2` is) — `WithExpr::Compare`'s
693/// `ReturnExpr` LHS covers both.
694#[derive(Debug, Clone, PartialEq)]
695pub struct UnwindClause {
696 pub source: UnwindSource,
697 pub var: String,
698 pub where_clause: Option<WithExpr>,
699 pub with: Option<WithClause>,
700}
701
702/// Where an `UNWIND`'s list comes from — any expression (`range(0, 2)`,
703/// `n.tags`, a bound `collect()` result, an inline `[1, 2, 3]`, ...),
704/// evaluated per input row and required to produce a `Value::List`
705/// (`executor::eval_unwind`). Element bindings restore graph identity via
706/// the same `value_to_binding_restore` regardless of where the list came
707/// from — there's no `PropertyValue::List` yet, so a `$param` bound
708/// directly to a list still isn't reachable here on its own (every
709/// `$param` is a single scalar); a `$param` used *inside* an inline list
710/// literal (`[1, 2, $p]`) still works, since each element substitutes
711/// independently.
712#[derive(Debug, Clone, PartialEq)]
713pub struct UnwindSource(pub ReturnExpr);
714
715/// `MERGE <pattern> [ON CREATE SET ...] [ON MATCH SET ...] [WITH ...]` —
716/// match-or-create: try the pattern as an ordinary MATCH first (reusing
717/// `build_match_plan`/`eval_plan` — this already does the right "search
718/// the *connected* sub-pattern, not each node in isolation" thing for a
719/// hop pattern, since `Expand` only follows real edges and `Filter` only
720/// keeps matches against the target's own constraints); if that finds
721/// nothing, create exactly one new pattern instance (reusing
722/// `resolve_or_create_node`, the same "reuse if the token's var is
723/// already bound in the row" logic `Tail::Create` uses). `pattern.hops`
724/// is capped at one relationship by the parser — whole-pattern atomicity
725/// across multiple simultaneously-unbound hops isn't attempted in v1, see
726/// `executor::eval_merge`'s docs for why.
727#[derive(Debug, Clone, PartialEq)]
728pub struct MergeClause {
729 pub pattern: Pattern,
730 /// `MERGE p = (a)-[:R]->(b)` -- captures the whole matched-or-created
731 /// pattern as a path, same as ordinary `MATCH`'s own named-path
732 /// capture (`Pattern::path_var`) but on `MergeClause` directly, since
733 /// `Pattern` itself has no notion of a MERGE-vs-MATCH distinction.
734 /// `MergeClause::pattern` caps at one relationship hop (see this
735 /// struct's own construction site), so assembling the path is just
736 /// "the start node, plus the one hop's edge and node if present" --
737 /// no BFS/parent-pointer tracking needed the way a general
738 /// variable-length pattern's path capture would (TCK's Merge1 [13],
739 /// Merge5 [10]).
740 pub path_var: Option<String>,
741 pub on_create: Vec<SetItem>,
742 pub on_match: Vec<SetItem>,
743 pub with: Option<WithClause>,
744}
745
746/// `CALL proc.name(args) [YIELD ...]` -- both the in-query reading-clause
747/// form (`QueryClause::Call`, `args` always `Some` per the grammar's own
748/// `queryCallSt : CALL invocationName parenExpressionChain (YIELD
749/// yieldItems)?`, parens mandatory) and the standalone top-level form
750/// (`Statement::StandaloneCall`, whose `standaloneCall` rule's own
751/// `parenExpressionChain?` is optional -- `args: None` is that implicit-
752/// argument shape, `CALL proc` with no parens at all, where each declared
753/// input instead resolves from a same-named `$param`, TCK's Call1
754/// `[2]`/`[11]`, Call2 `[3]`).
755#[derive(Debug, Clone, PartialEq)]
756pub struct CallClause {
757 pub name: String,
758 pub args: Option<Vec<ReturnExpr>>,
759 /// A trailing `WITH` (TCK's Call6 `[1]`: `CALL ... YIELD label WITH
760 /// count(*) AS c CALL ... YIELD label RETURN *`) -- same "glued onto
761 /// the preceding reading clause while walking `multiPartQ`'s children"
762 /// mechanism `QueryPart`/`UnwindClause`/`MergeClause` already use for
763 /// their own trailing `with`. Always `None` on a `Statement::
764 /// StandaloneCall` (never read there -- a standalone call is always
765 /// the whole statement, nothing can follow it).
766 pub with: Option<WithClause>,
767 /// `None` -- no `YIELD` written at all. For the in-query form this
768 /// means every output is discarded, nothing bound into scope (TCK's
769 /// Call1 `[12]`: referencing an un-yielded output afterward is
770 /// `UndefinedVariable`); for the standalone form it instead means
771 /// "auto-yield every output," same as `Some(CallYield::Star)` would,
772 /// since a standalone `CALL` *is* the whole query (TCK's Call1 `[5]`,
773 /// Call2 `[2]`) -- `executor::eval_standalone_call` is what actually
774 /// applies that standalone-only distinction; this AST shape alone
775 /// can't tell the two apart.
776 pub yield_items: Option<CallYield>,
777}
778
779#[derive(Debug, Clone, PartialEq)]
780pub enum CallYield {
781 /// `YIELD *` -- every declared output, bound under its own name.
782 Star,
783 /// `YIELD a, b AS c, ...` -- explicit output name (optionally
784 /// renamed), plus `yieldItems`' own optional trailing `WHERE` (only
785 /// this variant's grammar production carries one -- `YIELD *` has no
786 /// `where?` of its own).
787 Items(Vec<(String, Option<String>)>, Option<Box<Expr>>),
788}
789
790/// One reading clause in a `MATCH`/`UNWIND`/`MERGE` sequence. `Match` is
791/// today's `MATCH`/`OPTIONAL MATCH ... [WHERE] [WITH]` segment; `Unwind`
792/// fans out a list; `Merge` matches-or-creates. All three can optionally
793/// end in a `WITH` — see `Statement::Match`'s docs for the WITH-
794/// separation/one-WITH-total rules this enum's variants are validated
795/// against.
796#[derive(Debug, Clone, PartialEq)]
797pub enum QueryClause {
798 Match(QueryPart),
799 Unwind(UnwindClause),
800 Merge(MergeClause),
801 /// A statement-leading `WITH` -- no pattern to match, just projects/
802 /// aliases values (`WITH [1,2,3] AS list ...`). Distinct from the
803 /// trailing `with: Option<WithClause>` every other clause kind
804 /// already carries (that one follows a real pattern match; this one
805 /// has nothing preceding it at all).
806 With(WithClause),
807 /// `SET ...` immediately followed by `WITH` -- continues the query
808 /// past the mutation instead of only ever allowing one trailing
809 /// `RETURN` (the pre-existing `Tail::Set`'s own `ReturnTail`).
810 /// Doesn't itself change any row's bindings, only the underlying
811 /// graph -- `execute_match`'s clause loop applies each item per row
812 /// and passes `current_rows` through unchanged, same as `Merge`
813 /// already does for its own non-binding side effects.
814 Set(Vec<SetItem>),
815 /// `DELETE`/`DETACH DELETE ...` immediately followed by `WITH` -- same
816 /// reasoning as `Set` above (TCK's Delete6 "Persistence of delete
817 /// clause side effects"). `detach`: whether `DETACH` was present
818 /// (mirrors `Tail::Delete` vs `Tail::DetachDelete`'s own split).
819 Delete {
820 items: Vec<ReturnExpr>,
821 detach: bool,
822 },
823 /// `REMOVE ...` immediately followed by `WITH` -- same reasoning as
824 /// `Set` above (TCK's Remove3 "Persistence of remove clause side
825 /// effects").
826 Remove(Vec<RemoveItem>),
827 /// `CREATE ...` immediately followed by `WITH` -- same positive-
828 /// lookahead reasoning as `Set` above, but unlike `Set`/`Delete`/
829 /// `Remove`, CREATE *does* change row bindings (each pattern's
830 /// fresh/reused node-and-relationship vars) -- `execute_match`'s
831 /// clause loop reuses `materialize_create` (the exact same function
832 /// `Tail::Create`/`Executor::execute_create` already call) and
833 /// extends `carried_vars` with every pattern's own vars, same as
834 /// `Merge`'s own binding-changing clause already does.
835 Create(Vec<Pattern>),
836 /// `CALL proc.name(args) [YIELD ...]` used as a reading clause --
837 /// `CallClause::args` is always `Some` here (the grammar's own
838 /// `queryCallSt` requires parens), see `CallClause`'s own docs.
839 Call(CallClause),
840}
841
842#[derive(Debug, Clone, PartialEq)]
843pub enum Statement {
844 Create(Vec<Pattern>),
845 /// `CREATE INDEX ON :Label(prop)`, optionally `UNIQUE`.
846 CreateIndex {
847 label: String,
848 prop: String,
849 unique: bool,
850 },
851 /// `EXPLAIN <statement>` — describes the plan `<statement>` would run
852 /// (scan vs seek, pushdown applied) without executing any of it. Never
853 /// nests (the parser only ever wraps a `create_index_stmt`/
854 /// `create_stmt`/`match_stmt`, not another `explain_stmt`).
855 Explain(Box<Statement>),
856 Match {
857 /// One or more `MATCH`/`UNWIND`/`MERGE ... [WITH ...]` clauses. The
858 /// parser enforces every `Match` clause except the statement's
859 /// last has a `with` before the next `Match` clause (matching real
860 /// Cypher's rule that multiple reading clauses must be separated
861 /// by WITH) — `Unwind`/`Merge` clauses are exempt from this
862 /// specific check (they share one binding scope the same way
863 /// `OPTIONAL MATCH` already does, real Cypher needs no WITH around
864 /// a bare UNWIND/MERGE either) — and that at most one clause (of
865 /// any kind) has a `with` at all across the whole statement (v1
866 /// doesn't support chaining past one WITH boundary — nothing in
867 /// the target query set needs it, and it keeps a hand-rolled
868 /// parser's untested-path surface smaller).
869 clauses: Vec<QueryClause>,
870 /// `None` only when a `MERGE` clause is present with nothing after
871 /// it (`MERGE (n:Label)` alone, no `RETURN`/etc — a pure write,
872 /// same as standalone `CREATE`). The parser rejects a missing tail
873 /// otherwise (`MATCH (n)` alone is almost certainly a mistake, not
874 /// a deliberate no-op).
875 tail: Option<Tail>,
876 /// Only meaningful for `Tail::Return`; evaluated against the
877 /// projected/aliased output row, not the raw pattern bindings —
878 /// every ORDER BY key in practice is a RETURN alias, not a bare
879 /// pattern variable.
880 order_by: Option<Vec<(ReturnExpr, SortDir)>>,
881 /// Applied after `order_by`, before `limit` — same convention as
882 /// `WithClause::skip`. Boxed (clippy's `large_enum_variant`) --
883 /// `Statement::Match` would otherwise be far larger than
884 /// `Statement`'s other variants just for this rarely-non-`None`
885 /// field.
886 skip: Option<Box<ReturnExpr>>,
887 limit: Option<Box<ReturnExpr>>,
888 },
889 /// `<match_stmt> UNION [ALL] <match_stmt> (UNION [ALL] <match_stmt>)*`
890 /// — each `parts` entry is itself a `Statement::Match`, own scope, no
891 /// bindings shared across parts (real Cypher: a UNION member can't see
892 /// a preceding member's variables). `all` applies uniformly to the
893 /// whole statement — real Cypher rejects mixing bare `UNION` and
894 /// `UNION ALL` in one statement (a semantic check, `parser::
895 /// parse_union_stmt`, since it's only checkable once every part's own
896 /// `UNION`/`UNION ALL` keyword is in hand). `false` = dedup the
897 /// combined rows (`UNION`'s default); `true` = keep every row
898 /// (`UNION ALL`).
899 Union {
900 parts: Vec<Statement>,
901 all: bool,
902 },
903 /// A bare `CALL proc.name(args) [YIELD ...]` with nothing else in the
904 /// statement (the grammar's own `standaloneCall`, a top-level
905 /// alternative alongside `regularQuery` -- never wrapped in
906 /// `Statement::Match`, since there's no pattern to match at all). See
907 /// `CallClause`'s own docs for why `args`/`yield_items` mean something
908 /// subtly different here than in `QueryClause::Call`. Boxed (clippy's
909 /// `large_enum_variant`) -- `CallClause` is far bigger than
910 /// `Statement`'s other variants just for this rarely-taken one.
911 StandaloneCall(Box<CallClause>),
912}