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}
29
30#[derive(Debug, Clone)]
31pub enum Expr {
32 And(Box<Expr>, Box<Expr>),
33 Or(Box<Expr>, Box<Expr>),
34 Not(Box<Expr>),
35 Compare(PropAccess, CompareOp, Literal),
36 /// Does the node bound to `var` have label `label` among its (possibly
37 /// multiple) labels? Synthesized by the planner for the 2nd+ label in a
38 /// multi-label pattern like `(n:Post:Message)` — never user-typed.
39 HasLabel(String, String),
40 /// Do these two row bindings refer to the same node/edge? Synthesized
41 /// by the planner when a pattern's hop variable is a "bound-node
42 /// repetition" — the same variable already bound earlier reappearing
43 /// mid-pattern (e.g. IS7's `p`, bound by an earlier MATCH, reappearing
44 /// as the endpoint of an OPTIONAL MATCH pattern: `(a)-[r:KNOWS]-(p)`
45 /// must mean "KNOWS *this* `p`", not "KNOWS anyone"). Never user-typed.
46 VarEq(String, String),
47}
48
49#[derive(Debug, Clone)]
50pub enum ReturnExpr {
51 Var(String),
52 Prop(PropAccess),
53 Lit(Literal),
54 Call {
55 name: String,
56 args: Vec<ReturnExpr>,
57 distinct: bool,
58 },
59 /// `count(*)` — its own variant, not `Call` with a magic `"*"`-sentinel
60 /// argument, so evaluation physically cannot mishandle it as an
61 /// ordinary function call (no args to evaluate, no DISTINCT target —
62 /// it counts rows, not values).
63 CountStar,
64 /// Simple/value `CASE`: `CASE <test> WHEN <value> THEN <result> ... [ELSE
65 /// <else>] END`. `test` is `Some` for every form the parser currently
66 /// produces; kept `Option` so a future searched-`CASE` (`CASE WHEN
67 /// <bool_expr> THEN ...`) can reuse this variant without another type
68 /// change.
69 Case {
70 test: Option<Box<ReturnExpr>>,
71 whens: Vec<(ReturnExpr, ReturnExpr)>,
72 else_: Option<Box<ReturnExpr>>,
73 },
74}
75
76/// Case-insensitive aggregate-function recognition, shared by `parser.rs`
77/// (DISTINCT-validity check) and `executor.rs` (grouping classification —
78/// a RETURN/WITH item list "has an aggregate" iff any item's top-level
79/// expression is `CountStar` or a `Call` whose name passes this check).
80pub fn is_aggregate_name(name: &str) -> bool {
81 matches!(name.to_ascii_lowercase().as_str(), "count" | "sum" | "avg" | "min" | "max" | "collect")
82}
83
84#[derive(Debug, Clone)]
85pub struct ReturnItem {
86 pub expr: ReturnExpr,
87 pub alias: Option<String>,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum SortDir {
92 Asc,
93 Desc,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum RelDirection {
98 /// (a)-[..]->(b)
99 Right,
100 /// (a)<-[..]-(b)
101 Left,
102 /// (a)-[..]-(b) — matches either direction.
103 Either,
104}
105
106#[derive(Debug, Clone)]
107pub struct NodePattern {
108 pub var: Option<String>,
109 pub labels: Vec<String>,
110 pub props: Vec<(String, Literal)>,
111}
112
113#[derive(Debug, Clone)]
114pub struct RelPattern {
115 pub var: Option<String>,
116 pub rel_type: Option<String>,
117 #[allow(dead_code)] // property-map on relationships parsed but not filtered on in v1
118 pub props: Vec<(String, Literal)>,
119 pub direction: RelDirection,
120 /// `[:TYPE*min..max]` — `None` means a fixed single hop (existing
121 /// behavior). `max: None` means unbounded, capped at a safety depth by
122 /// the executor.
123 pub hop_range: Option<(u32, Option<u32>)>,
124}
125
126/// A linear chain: node, (rel, node)*.
127#[derive(Debug, Clone)]
128pub struct Pattern {
129 pub start: NodePattern,
130 pub hops: Vec<(RelPattern, NodePattern)>,
131}
132
133#[derive(Debug, Clone)]
134pub enum Tail {
135 Return(Vec<ReturnItem>),
136 Delete(Vec<String>),
137 DetachDelete(Vec<String>),
138 Set(Vec<(PropAccess, Literal)>),
139 /// `MATCH ... CREATE ...` — same pattern syntax as `Statement::Create`,
140 /// but runs once per row already bound by the preceding MATCH/WITH: a
141 /// node pattern token whose variable is already bound in that row
142 /// reuses the existing node instead of creating a new one. This is
143 /// the only way to add an edge between two nodes that already exist —
144 /// `Statement::Create` alone can't (every node token it sees is
145 /// always fresh).
146 Create(Vec<Pattern>),
147}
148
149/// WITH's HAVING-equivalent: filters on the already-projected/aggregated
150/// row (e.g. `WITH p, count(f) AS c WHERE c > 10`). Same And/Or/Not/
151/// Compare shape as `Expr`, but the comparison's LHS is a `ReturnExpr`
152/// (a WITH alias or raw expression) instead of a raw-property
153/// `PropAccess` — deliberately a separate type from `Expr` rather than a
154/// widened reuse of it, since `Expr::Compare` is what the planner pushes
155/// into pre-projection `Filter`/`Expand` nodes, and this filter
156/// fundamentally belongs *post*-projection instead (see `materialize_with`).
157#[derive(Debug, Clone)]
158pub enum WithExpr {
159 And(Box<WithExpr>, Box<WithExpr>),
160 Or(Box<WithExpr>, Box<WithExpr>),
161 Not(Box<WithExpr>),
162 Compare(ReturnExpr, CompareOp, Literal),
163}
164
165/// A `WITH` clause: projects/renames the current bindings, optionally
166/// filtered/sorted/limited at that boundary, and becomes the binding scope
167/// for whatever follows (the next `QueryPart`, or the final `Tail`).
168#[derive(Debug, Clone)]
169pub struct WithClause {
170 pub items: Vec<ReturnItem>,
171 pub where_clause: Option<WithExpr>,
172 pub order_by: Option<Vec<(ReturnExpr, SortDir)>>,
173 pub limit: Option<i64>,
174}
175
176/// One `MATCH <pattern>[, <pattern>...] [WHERE ...] [WITH ...]` segment.
177/// Comma-separated patterns within one part are spliced into a single
178/// linear `Pattern` at parse time (see `parser::splice_patterns`) — this
179/// only ever holds one already-combined `Pattern`, not several.
180///
181/// `path_var` is `Some` for `p = (a)-->(b)` / `p = shortestPath(...)` —
182/// capturing the whole matched path, not just its endpoints. General
183/// named-path capture (`shortest_path: false`) is limited to fixed-hop
184/// patterns — `pattern` must contain no variable-length (`*`) hop, parser-
185/// enforced, since reconstructing a path over `VarExpand`'s BFS would need
186/// the same parent-pointer tracking `shortestPath()` already has, but
187/// generalized, which isn't worth it for the narrow payoff. `shortest_path
188/// : true` is the opposite: `pattern` must be exactly one variable-length
189/// hop (`shortestPath((a)-[:TYPE*..N]-(b))`), and both endpoints must
190/// already be bound by a preceding clause (see `executor::eval_shortest_
191/// path`'s docs for why unbound endpoints aren't supported in v1).
192#[derive(Debug, Clone)]
193pub struct QueryPart {
194 pub optional: bool,
195 pub path_var: Option<String>,
196 pub shortest_path: bool,
197 pub pattern: Pattern,
198 pub where_clause: Option<Expr>,
199 pub with: Option<WithClause>,
200}
201
202/// `UNWIND <source> AS <var> [WHERE ...] [WITH ...]` — fans a list out into
203/// one row per element, cross-joined against whatever rows already exist
204/// (same "row-vector-in, row-vector-out, no graph traversal" shape as a
205/// `WithClause`, not a graph-traversal `LogicalPlan` node — see
206/// `executor::eval_unwind`). Its own `where_clause` (rather than requiring
207/// a `WITH` right after it just to filter) is what makes `UNWIND [1,2,3]
208/// AS x WHERE x > 2` — or `WITH ... collect(m) AS ms UNWIND ms AS m2
209/// WHERE m2.x > 1` — work within the one-`WITH`-per-statement cap (see
210/// `QueryClause`'s docs). Deliberately typed as `WithExpr`, not the
211/// pattern-level `Expr`: an unwound variable is very often a bare scalar
212/// (`x > 2`), which `Expr::Compare`'s always-`PropAccess` LHS structurally
213/// cannot express (only `x.prop > 2` is) — `WithExpr::Compare`'s
214/// `ReturnExpr` LHS covers both.
215#[derive(Debug, Clone)]
216pub struct UnwindClause {
217 pub source: UnwindSource,
218 pub var: String,
219 pub where_clause: Option<WithExpr>,
220 pub with: Option<WithClause>,
221}
222
223/// Where an `UNWIND`'s list comes from. `Var` restores graph identity per
224/// element when the list came from `collect()`-ing nodes/edges (see
225/// `executor::value_to_binding_restore`) — there's no `PropertyValue::List`
226/// yet, so a `$param`-supplied list isn't reachable here; only a
227/// previously-bound `collect()` result or an inline Cypher-text list.
228#[derive(Debug, Clone)]
229pub enum UnwindSource {
230 Var(String),
231 List(Vec<Literal>),
232}
233
234/// `MERGE <pattern> [ON CREATE SET ...] [ON MATCH SET ...] [WITH ...]` —
235/// match-or-create: try the pattern as an ordinary MATCH first (reusing
236/// `build_match_plan`/`eval_plan` — this already does the right "search
237/// the *connected* sub-pattern, not each node in isolation" thing for a
238/// hop pattern, since `Expand` only follows real edges and `Filter` only
239/// keeps matches against the target's own constraints); if that finds
240/// nothing, create exactly one new pattern instance (reusing
241/// `resolve_or_create_node`, the same "reuse if the token's var is
242/// already bound in the row" logic `Tail::Create` uses). `pattern.hops`
243/// is capped at one relationship by the parser — whole-pattern atomicity
244/// across multiple simultaneously-unbound hops isn't attempted in v1, see
245/// `executor::eval_merge`'s docs for why.
246#[derive(Debug, Clone)]
247pub struct MergeClause {
248 pub pattern: Pattern,
249 pub on_create: Vec<(PropAccess, Literal)>,
250 pub on_match: Vec<(PropAccess, Literal)>,
251 pub with: Option<WithClause>,
252}
253
254/// One reading clause in a `MATCH`/`UNWIND`/`MERGE` sequence. `Match` is
255/// today's `MATCH`/`OPTIONAL MATCH ... [WHERE] [WITH]` segment; `Unwind`
256/// fans out a list; `Merge` matches-or-creates. All three can optionally
257/// end in a `WITH` — see `Statement::Match`'s docs for the WITH-
258/// separation/one-WITH-total rules this enum's variants are validated
259/// against.
260#[derive(Debug, Clone)]
261pub enum QueryClause {
262 Match(QueryPart),
263 Unwind(UnwindClause),
264 Merge(MergeClause),
265}
266
267#[derive(Debug, Clone)]
268pub enum Statement {
269 Create(Vec<Pattern>),
270 Match {
271 /// One or more `MATCH`/`UNWIND`/`MERGE ... [WITH ...]` clauses. The
272 /// parser enforces every `Match` clause except the statement's
273 /// last has a `with` before the next `Match` clause (matching real
274 /// Cypher's rule that multiple reading clauses must be separated
275 /// by WITH) — `Unwind`/`Merge` clauses are exempt from this
276 /// specific check (they share one binding scope the same way
277 /// `OPTIONAL MATCH` already does, real Cypher needs no WITH around
278 /// a bare UNWIND/MERGE either) — and that at most one clause (of
279 /// any kind) has a `with` at all across the whole statement (v1
280 /// doesn't support chaining past one WITH boundary — nothing in
281 /// the target query set needs it, and it keeps a hand-rolled
282 /// parser's untested-path surface smaller).
283 clauses: Vec<QueryClause>,
284 /// `None` only when a `MERGE` clause is present with nothing after
285 /// it (`MERGE (n:Label)` alone, no `RETURN`/etc — a pure write,
286 /// same as standalone `CREATE`). The parser rejects a missing tail
287 /// otherwise (`MATCH (n)` alone is almost certainly a mistake, not
288 /// a deliberate no-op).
289 tail: Option<Tail>,
290 /// Only meaningful for `Tail::Return`; evaluated against the
291 /// projected/aliased output row, not the raw pattern bindings —
292 /// every ORDER BY key in practice is a RETURN alias, not a bare
293 /// pattern variable.
294 order_by: Option<Vec<(ReturnExpr, SortDir)>>,
295 limit: Option<i64>,
296 },
297}