Skip to main content

reddb_rql/parser/
graph.rs

1//! Graph query parsing (MATCH pattern)
2
3use super::error::ParseError;
4use super::Parser;
5use crate::ast::{
6    CompareOp, EdgeDirection, EdgePattern, FieldRef, GraphPattern, GraphQuery, NodePattern,
7    Projection, PropertyFilter, QueryExpr,
8};
9use crate::lexer::Token;
10
11impl<'a> Parser<'a> {
12    /// Parse MATCH ... RETURN query
13    pub fn parse_match_query(&mut self) -> Result<QueryExpr, ParseError> {
14        self.expect(Token::Match)?;
15
16        let pattern = self.parse_graph_pattern()?;
17
18        let filter = if self.consume(&Token::Where)? {
19            Some(self.parse_filter()?)
20        } else {
21            None
22        };
23
24        self.expect(Token::Return)?;
25        let return_ = self.parse_return_list()?;
26        let limit = self.parse_match_limit()?;
27
28        Ok(QueryExpr::Graph(GraphQuery {
29            alias: None,
30            pattern,
31            filter,
32            return_,
33            limit,
34        }))
35    }
36
37    fn parse_match_limit(&mut self) -> Result<Option<u64>, ParseError> {
38        if !self.consume(&Token::Limit)? && !self.consume_ident_ci("LIMIT")? {
39            return Ok(None);
40        }
41
42        let pos = self.position();
43        if matches!(self.current.token, Token::Minus | Token::Dash) {
44            return Err(ParseError::value_out_of_range(
45                "MATCH LIMIT",
46                "must be a non-negative integer",
47                pos,
48            ));
49        }
50
51        let raw = self.parse_integer()?;
52        if raw < 0 {
53            return Err(ParseError::value_out_of_range(
54                "MATCH LIMIT",
55                "must be a non-negative integer",
56                pos,
57            ));
58        }
59        Ok(Some(raw as u64))
60    }
61
62    /// Parse graph pattern: (a)-[r]->(b)
63    pub fn parse_graph_pattern(&mut self) -> Result<GraphPattern, ParseError> {
64        let mut pattern = GraphPattern::new();
65
66        // Parse first node
67        let first_node = self.parse_node_pattern()?;
68        pattern.nodes.push(first_node);
69
70        // Parse chain of edges and nodes
71        while self.peek() == &Token::Dash || self.peek() == &Token::ArrowLeft {
72            let (edge, next_node) =
73                self.parse_edge_and_node(pattern.nodes.last().unwrap().alias.clone())?;
74            pattern.edges.push(edge);
75            pattern.nodes.push(next_node);
76        }
77
78        Ok(pattern)
79    }
80
81    /// Parse node pattern: (alias:Type {props})
82    pub fn parse_node_pattern(&mut self) -> Result<NodePattern, ParseError> {
83        self.expect(Token::LParen)?;
84
85        let alias = self.expect_ident()?;
86
87        // Label filter is a free-form string; resolution against the
88        // graph's `LabelRegistry` happens at execution time, not here.
89        let node_label = if self.consume(&Token::Colon)? {
90            let label = self.expect_ident_or_keyword()?;
91            Some(self.parse_node_label(&label)?)
92        } else {
93            None
94        };
95
96        let properties = if self.consume(&Token::LBrace)? {
97            self.parse_property_filters()?
98        } else {
99            Vec::new()
100        };
101
102        self.expect(Token::RParen)?;
103
104        Ok(NodePattern {
105            alias,
106            node_label,
107            properties,
108        })
109    }
110
111    /// Parse edge and next node: -[r:TYPE*min..max]->(b)
112    fn parse_edge_and_node(
113        &mut self,
114        from_alias: String,
115    ) -> Result<(EdgePattern, NodePattern), ParseError> {
116        // Determine direction
117        let incoming = self.consume(&Token::ArrowLeft)?;
118        if !incoming {
119            self.expect(Token::Dash)?;
120        }
121
122        // Parse edge pattern
123        self.expect(Token::LBracket)?;
124
125        let alias = if let Token::Ident(name) = self.peek() {
126            let name = name.clone();
127            self.advance()?;
128            Some(name)
129        } else {
130            None
131        };
132
133        let edge_label = if self.consume(&Token::Colon)? {
134            let label = self.expect_ident_or_keyword()?;
135            Some(self.parse_edge_label(&label)?)
136        } else {
137            None
138        };
139
140        // Variable length: *min..max
141        let (min_hops, max_hops) = if self.consume(&Token::Star)? {
142            if let Token::Integer(_) = self.peek() {
143                let min = self.parse_integer()? as u32;
144                if self.consume(&Token::DotDot)? {
145                    let max = self.parse_integer()? as u32;
146                    (min, max)
147                } else {
148                    (min, min)
149                }
150            } else {
151                (1, u32::MAX) // * means any length
152            }
153        } else {
154            (1, 1) // Default: exactly 1 hop
155        };
156
157        self.expect(Token::RBracket)?;
158
159        // Determine final direction
160        let direction = if incoming {
161            self.expect(Token::Dash)?;
162            EdgeDirection::Incoming
163        } else if self.consume(&Token::Arrow)? {
164            EdgeDirection::Outgoing
165        } else {
166            self.expect(Token::Dash)?;
167            EdgeDirection::Both
168        };
169
170        // Parse next node
171        let next_node = self.parse_node_pattern()?;
172
173        let edge = EdgePattern {
174            alias,
175            from: from_alias,
176            to: next_node.alias.clone(),
177            edge_label,
178            direction,
179            min_hops,
180            max_hops,
181        };
182
183        Ok((edge, next_node))
184    }
185
186    /// Parse property filters in braces: {name: 'value', age: 25}
187    pub fn parse_property_filters(&mut self) -> Result<Vec<PropertyFilter>, ParseError> {
188        let mut filters = Vec::new();
189
190        loop {
191            let name = self.expect_ident()?;
192            self.expect(Token::Colon)?;
193            let value = self.parse_value()?;
194
195            filters.push(PropertyFilter {
196                name,
197                op: CompareOp::Eq,
198                value,
199            });
200
201            if !self.consume(&Token::Comma)? {
202                break;
203            }
204        }
205
206        self.expect(Token::RBrace)?;
207        Ok(filters)
208    }
209
210    /// Parse RETURN list
211    pub fn parse_return_list(&mut self) -> Result<Vec<Projection>, ParseError> {
212        let mut projections = Vec::new();
213        loop {
214            let proj = self.parse_graph_projection()?;
215            projections.push(proj);
216
217            if !self.consume(&Token::Comma)? {
218                break;
219            }
220        }
221        Ok(projections)
222    }
223
224    /// Parse a graph projection (can be node alias, node.property, etc.)
225    fn parse_graph_projection(&mut self) -> Result<Projection, ParseError> {
226        let first = self.expect_ident()?;
227
228        let field = if self.consume(&Token::Dot)? {
229            let prop = self.expect_ident()?;
230            FieldRef::NodeProperty {
231                alias: first,
232                property: prop,
233            }
234        } else {
235            // Just the alias, refers to the whole node
236            FieldRef::NodeId { alias: first }
237        };
238
239        let alias = if self.consume(&Token::As)? {
240            Some(self.expect_ident()?)
241        } else {
242            None
243        };
244
245        Ok(Projection::Field(field, alias))
246    }
247
248    /// Normalize a parsed node-type token to its label-string form. The
249    /// pentest-flavoured aliases (`VULN`, `TECH`, `CERT`) are kept so
250    /// existing query strings continue to parse, but the result is just
251    /// the canonical lowercase label and is no longer constrained to a
252    /// closed enum.
253    pub fn parse_node_label(&self, name: &str) -> Result<String, ParseError> {
254        let canonical = match name.to_uppercase().as_str() {
255            "HOST" => "host",
256            "SERVICE" => "service",
257            "CREDENTIAL" => "credential",
258            "VULNERABILITY" | "VULN" => "vulnerability",
259            "ENDPOINT" => "endpoint",
260            "TECHNOLOGY" | "TECH" => "technology",
261            "USER" => "user",
262            "DOMAIN" => "domain",
263            "CERTIFICATE" | "CERT" => "certificate",
264            // Forward unknown labels verbatim (lowercased) — the registry
265            // resolves them at execution time, or a later validation
266            // pass can reject them.
267            other => return Ok(other.to_lowercase()),
268        };
269        Ok(canonical.to_string())
270    }
271
272    /// Edge label counterpart to [`parse_node_label`].
273    pub fn parse_edge_label(&self, name: &str) -> Result<String, ParseError> {
274        let canonical = match name.to_uppercase().as_str() {
275            "HAS_SERVICE" => "has_service",
276            "HAS_ENDPOINT" => "has_endpoint",
277            "USES_TECH" | "USES_TECHNOLOGY" => "uses_tech",
278            "AUTH_ACCESS" | "AUTH" => "auth_access",
279            "AFFECTED_BY" => "affected_by",
280            "CONTAINS" => "contains",
281            "CONNECTS_TO" | "CONNECTS" => "connects_to",
282            "RELATED_TO" | "RELATED" => "related_to",
283            "HAS_USER" => "has_user",
284            "HAS_CERT" | "HAS_CERTIFICATE" => "has_cert",
285            other => return Ok(other.to_lowercase()),
286        };
287        Ok(canonical.to_string())
288    }
289}