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}
140
141/// WITH's HAVING-equivalent: filters on the already-projected/aggregated
142/// row (e.g. `WITH p, count(f) AS c WHERE c > 10`). Same And/Or/Not/
143/// Compare shape as `Expr`, but the comparison's LHS is a `ReturnExpr`
144/// (a WITH alias or raw expression) instead of a raw-property
145/// `PropAccess` — deliberately a separate type from `Expr` rather than a
146/// widened reuse of it, since `Expr::Compare` is what the planner pushes
147/// into pre-projection `Filter`/`Expand` nodes, and this filter
148/// fundamentally belongs *post*-projection instead (see `materialize_with`).
149#[derive(Debug, Clone)]
150pub enum WithExpr {
151 And(Box<WithExpr>, Box<WithExpr>),
152 Or(Box<WithExpr>, Box<WithExpr>),
153 Not(Box<WithExpr>),
154 Compare(ReturnExpr, CompareOp, Literal),
155}
156
157/// A `WITH` clause: projects/renames the current bindings, optionally
158/// filtered/sorted/limited at that boundary, and becomes the binding scope
159/// for whatever follows (the next `QueryPart`, or the final `Tail`).
160#[derive(Debug, Clone)]
161pub struct WithClause {
162 pub items: Vec<ReturnItem>,
163 pub where_clause: Option<WithExpr>,
164 pub order_by: Option<Vec<(ReturnExpr, SortDir)>>,
165 pub limit: Option<i64>,
166}
167
168/// One `MATCH <pattern>[, <pattern>...] [WHERE ...] [WITH ...]` segment.
169/// Comma-separated patterns within one part are spliced into a single
170/// linear `Pattern` at parse time (see `parser::splice_patterns`) — this
171/// only ever holds one already-combined `Pattern`, not several.
172#[derive(Debug, Clone)]
173pub struct QueryPart {
174 pub optional: bool,
175 pub pattern: Pattern,
176 pub where_clause: Option<Expr>,
177 pub with: Option<WithClause>,
178}
179
180#[derive(Debug, Clone)]
181pub enum Statement {
182 Create(Vec<Pattern>),
183 Match {
184 /// One or more `MATCH ... [WITH ...]` segments. The parser enforces
185 /// every part except the last has a `with` (matching real Cypher's
186 /// rule that multiple reading clauses must be separated by WITH)
187 /// and that at most one part has a `with` at all (v1 doesn't
188 /// support chaining past one WITH boundary — nothing in the target
189 /// query set needs it, and it keeps a hand-rolled parser's
190 /// untested-path surface smaller).
191 parts: Vec<QueryPart>,
192 tail: Tail,
193 /// Only meaningful for `Tail::Return`; evaluated against the
194 /// projected/aliased output row, not the raw pattern bindings —
195 /// every ORDER BY key in practice is a RETURN alias, not a bare
196 /// pattern variable.
197 order_by: Option<Vec<(ReturnExpr, SortDir)>>,
198 limit: Option<i64>,
199 },
200}