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