Skip to main content

reddb_server/storage/query/parser/
graph.rs

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