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}
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}