reddb_rql/parser/
graph.rs1use 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 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 pub fn parse_graph_pattern(&mut self) -> Result<GraphPattern, ParseError> {
64 let mut pattern = GraphPattern::new();
65
66 let first_node = self.parse_node_pattern()?;
68 pattern.nodes.push(first_node);
69
70 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 pub fn parse_node_pattern(&mut self) -> Result<NodePattern, ParseError> {
83 self.expect(Token::LParen)?;
84
85 let alias = self.expect_ident()?;
86
87 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 fn parse_edge_and_node(
113 &mut self,
114 from_alias: String,
115 ) -> Result<(EdgePattern, NodePattern), ParseError> {
116 let incoming = self.consume(&Token::ArrowLeft)?;
118 if !incoming {
119 self.expect(Token::Dash)?;
120 }
121
122 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 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) }
153 } else {
154 (1, 1) };
156
157 self.expect(Token::RBracket)?;
158
159 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 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 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 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 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 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 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 other => return Ok(other.to_lowercase()),
268 };
269 Ok(canonical.to_string())
270 }
271
272 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}