Skip to main content

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