Skip to main content

velesdb_core/velesql/parser/
match_patterns.rs

1//! Graph pattern parsing for MATCH clauses (node, relationship, path patterns).
2//!
3//! Extracted from `match_clause.rs` to comply with the 500 NLOC file limit.
4//! These functions parse the graph pattern elements of a MATCH query:
5//! node patterns `(n:Label {props})`, relationship patterns `-[r:TYPE*1..3]->`,
6//! and composite path patterns linking them together.
7
8use crate::velesql::error::ParseError;
9use crate::velesql::graph_pattern::{Direction, GraphPattern, NodePattern, RelationshipPattern};
10
11use super::match_clause::split_with_braces;
12
13/// Parses a single node pattern.
14///
15/// # Errors
16///
17/// Returns [`ParseError`] when delimiters are invalid or properties cannot be parsed.
18pub fn parse_node_pattern(input: &str) -> Result<NodePattern, ParseError> {
19    let input = input.trim();
20    validate_node_delimiters(input)?;
21    let inner = input[1..input.len() - 1].trim();
22    if inner.is_empty() {
23        return Ok(NodePattern::new());
24    }
25    let mut node = NodePattern::new();
26    let (main_part, properties) = split_with_braces(inner, input, "node pattern")?;
27    node.properties = properties;
28    apply_alias_and_labels(main_part, &mut node);
29    Ok(node)
30}
31
32/// Validates that a node pattern string starts with `(` and ends with `)`.
33fn validate_node_delimiters(input: &str) -> Result<(), ParseError> {
34    if !input.starts_with('(') {
35        return Err(ParseError::syntax(
36            0,
37            input,
38            "Node pattern must start with '('",
39        ));
40    }
41    if !input.ends_with(')') {
42        return Err(ParseError::syntax(input.len(), input, "Expected ')'"));
43    }
44    Ok(())
45}
46
47/// Extracts alias, labels, and optional collection from a node identifier.
48///
49/// Supports:
50/// - `n:Person` -> alias=n, labels=\[Person\]
51/// - `n:Person:Author` -> alias=n, labels=\[Person, Author\]
52/// - `n:Person@products` -> alias=n, labels=\[Person\], collection=products
53/// - `:Product@catalog` -> labels=\[Product\], collection=catalog
54fn apply_alias_and_labels(main_part: &str, node: &mut NodePattern) {
55    if main_part.is_empty() {
56        return;
57    }
58
59    let (part_without_coll, collection) = extract_collection_annotation(main_part);
60
61    let parts: Vec<&str> = part_without_coll.split(':').collect();
62    if !parts[0].trim().is_empty() {
63        node.alias = Some(parts[0].trim().to_string());
64    }
65    for label in &parts[1..] {
66        let trimmed = label.trim();
67        if !trimmed.is_empty() {
68            node.labels.push(trimmed.to_string());
69        }
70    }
71
72    node.collection = collection;
73}
74
75/// Extracts `@collection` annotation from a node identifier string.
76///
77/// Returns `(identifier_without_annotation, Some(collection_name))` if found,
78/// or `(original, None)` if no `@` is present.
79fn extract_collection_annotation(input: &str) -> (&str, Option<String>) {
80    if let Some(at_pos) = input.rfind('@') {
81        let before = &input[..at_pos];
82        let after = input[at_pos + 1..].trim();
83        if !after.is_empty() {
84            return (before, Some(after.to_string()));
85        }
86    }
87    (input, None)
88}
89
90/// Parses a relationship pattern.
91///
92/// # Errors
93///
94/// Returns [`ParseError`] when direction/brackets are malformed or relationship
95/// details cannot be parsed.
96pub fn parse_relationship_pattern(input: &str) -> Result<RelationshipPattern, ParseError> {
97    let input = input.trim();
98    let (direction, is, ie) = detect_direction_and_brackets(input)?;
99    let mut rel = RelationshipPattern::new(direction);
100
101    validate_bracket_matching(input)?;
102
103    if input.contains('[') && input.contains(']') {
104        parse_bracket_contents(input, is, ie, &mut rel)?;
105    }
106    Ok(rel)
107}
108
109/// Detects relationship direction and returns bracket positions.
110fn detect_direction_and_brackets(input: &str) -> Result<(Direction, usize, usize), ParseError> {
111    if input.starts_with("<-") && input.ends_with('-') {
112        Ok((
113            Direction::Incoming,
114            input.find('[').unwrap_or(2),
115            input.rfind(']').unwrap_or(input.len() - 1),
116        ))
117    } else if input.starts_with('-') && input.ends_with("->") {
118        Ok((
119            Direction::Outgoing,
120            input.find('[').unwrap_or(1),
121            input.rfind(']').unwrap_or(input.len() - 2),
122        ))
123    } else if input.starts_with('-') && input.ends_with('-') {
124        Ok((
125            Direction::Both,
126            input.find('[').unwrap_or(1),
127            input.rfind(']').unwrap_or(input.len() - 1),
128        ))
129    } else {
130        Err(ParseError::syntax(
131            0,
132            input,
133            "Invalid relationship direction",
134        ))
135    }
136}
137
138/// Validates that brackets are matched (both present or both absent).
139fn validate_bracket_matching(input: &str) -> Result<(), ParseError> {
140    let has_open = input.contains('[');
141    let has_close = input.contains(']');
142    if has_open != has_close {
143        return Err(ParseError::syntax(
144            0,
145            input,
146            if has_open {
147                "Missing closing ']' in relationship pattern"
148            } else {
149                "Missing opening '[' in relationship pattern"
150            },
151        ));
152    }
153    Ok(())
154}
155
156/// Parses the contents between brackets in a relationship pattern.
157fn parse_bracket_contents(
158    input: &str,
159    is: usize,
160    ie: usize,
161    rel: &mut RelationshipPattern,
162) -> Result<(), ParseError> {
163    if ie <= is {
164        return Err(ParseError::syntax(
165            is,
166            input,
167            "Mismatched brackets in relationship pattern",
168        ));
169    }
170    let inner = input[is + 1..ie].trim();
171    if inner.is_empty() {
172        return Ok(());
173    }
174    if let Some(sp) = inner.find('*') {
175        if let Some((s, e)) = parse_range(&inner[sp + 1..]) {
176            rel.range = Some((s, e));
177        }
178        parse_rel_details(inner[..sp].trim(), rel)?;
179    } else {
180        parse_rel_details(inner, rel)?;
181    }
182    Ok(())
183}
184
185fn parse_rel_details(input: &str, rel: &mut RelationshipPattern) -> Result<(), ParseError> {
186    if input.is_empty() {
187        return Ok(());
188    }
189    let (main_part, props) = split_with_braces(input, input, "relationship properties")?;
190    rel.properties = props;
191    if let Some(stripped) = main_part.strip_prefix(':') {
192        parse_rel_types(stripped, rel);
193    } else if let Some(cp) = main_part.find(':') {
194        rel.alias = Some(main_part[..cp].trim().to_string());
195        parse_rel_types(&main_part[cp + 1..], rel);
196    } else if !main_part.is_empty() {
197        rel.alias = Some(main_part.to_string());
198    }
199    Ok(())
200}
201
202fn parse_rel_types(input: &str, rel: &mut RelationshipPattern) {
203    for t in input.split('|') {
204        if !t.trim().is_empty() {
205            rel.types.push(t.trim().to_string());
206        }
207    }
208}
209
210/// Parses variable-length range after `*`.
211fn parse_range(input: &str) -> Option<(u32, u32)> {
212    let input = input.trim();
213    if input.is_empty() {
214        return Some((1, u32::MAX));
215    }
216    if let Some(d) = input.find("..") {
217        Some((
218            input[..d].trim().parse().unwrap_or(1),
219            input[d + 2..].trim().parse().unwrap_or(u32::MAX),
220        ))
221    } else {
222        input.parse::<u32>().ok().map(|n| (n, n))
223    }
224}
225
226/// Parses a comma-separated list of graph patterns from the MATCH clause body.
227pub(super) fn parse_pattern_list(input: &str) -> Result<Vec<GraphPattern>, ParseError> {
228    let (name, ps) = if let Some(eq) = input.find('=') {
229        let b = input[..eq].trim();
230        if b.chars().all(|c| c.is_alphanumeric() || c == '_') {
231            (Some(b.to_string()), input[eq + 1..].trim())
232        } else {
233            (None, input)
234        }
235    } else {
236        (None, input)
237    };
238    let mut pattern = parse_path_pattern(ps)?;
239    pattern.name = name;
240    Ok(vec![pattern])
241}
242
243fn parse_path_pattern(input: &str) -> Result<GraphPattern, ParseError> {
244    let mut nodes = Vec::new();
245    let mut rels = Vec::new();
246    let mut pos = 0;
247    let input = input.trim();
248    while pos < input.len() {
249        let Some(s) = input[pos..].find('(') else {
250            break;
251        };
252        let abs = pos + s;
253        let end = find_matching_paren(input, abs)?;
254        nodes.push(parse_node_pattern(&input[abs..=end])?);
255        pos = end + 1;
256        pos += parse_trailing_relationship(&input[pos.min(input.len())..], &mut rels)?;
257    }
258    Ok(GraphPattern {
259        name: None,
260        nodes,
261        relationships: rels,
262    })
263}
264
265/// Parses a relationship pattern that may follow a node, returning how many bytes to advance.
266fn parse_trailing_relationship(
267    rem: &str,
268    rels: &mut Vec<RelationshipPattern>,
269) -> Result<usize, ParseError> {
270    if !(rem.starts_with('-') || rem.starts_with('<')) {
271        return Ok(0);
272    }
273    let Some(np) = rem.find('(') else {
274        return Ok(0);
275    };
276    rels.push(parse_relationship_pattern(&rem[..np])?);
277    Ok(np)
278}
279
280fn find_matching_paren(input: &str, start: usize) -> Result<usize, ParseError> {
281    let mut d = 0;
282    for (i, c) in input[start..].char_indices() {
283        match c {
284            '(' => d += 1,
285            ')' => {
286                d -= 1;
287                if d == 0 {
288                    return Ok(start + i);
289                }
290            }
291            _ => {}
292        }
293    }
294    Err(ParseError::syntax(start, input, "Expected ')'"))
295}