Skip to main content

cypherlite_query/parser/
pattern.rs

1// Pattern parsing: node patterns, relationship patterns, path chains
2
3use super::ast::*;
4use super::{ParseError, Parser};
5use crate::lexer::Token;
6
7impl<'a> Parser<'a> {
8    /// Parse a full pattern: `node (rel node)* (, node (rel node)*)*`
9    pub fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
10        let mut chains = Vec::new();
11        chains.push(self.parse_pattern_chain()?);
12        while self.eat(&Token::Comma) {
13            chains.push(self.parse_pattern_chain()?);
14        }
15        Ok(Pattern { chains })
16    }
17
18    /// Parse a single pattern chain: `node (rel node)*`
19    fn parse_pattern_chain(&mut self) -> Result<PatternChain, ParseError> {
20        let mut elements = Vec::new();
21        elements.push(PatternElement::Node(self.parse_node_pattern()?));
22
23        loop {
24            match self.peek() {
25                // Outgoing or undirected: starts with `-`
26                Some(Token::Minus) => {
27                    let rel = self.parse_relationship_from_minus()?;
28                    elements.push(PatternElement::Relationship(rel));
29                    elements.push(PatternElement::Node(self.parse_node_pattern()?));
30                }
31                // Incoming: starts with `<-`
32                Some(Token::ArrowLeft) => {
33                    let rel = self.parse_relationship_incoming()?;
34                    elements.push(PatternElement::Relationship(rel));
35                    elements.push(PatternElement::Node(self.parse_node_pattern()?));
36                }
37                // Undirected without brackets: `--`
38                Some(Token::DoubleDash) => {
39                    self.advance(); // consume --
40                    elements.push(PatternElement::Relationship(RelationshipPattern {
41                        variable: None,
42                        rel_types: Vec::new(),
43                        direction: RelDirection::Undirected,
44                        properties: None,
45                        min_hops: None,
46                        max_hops: None,
47                    }));
48                    elements.push(PatternElement::Node(self.parse_node_pattern()?));
49                }
50                _ => break,
51            }
52        }
53
54        Ok(PatternChain { elements })
55    }
56
57    /// Parse a node pattern: `(variable:Label:Label2 {prop: value, ...})`
58    /// All parts are optional except the parentheses.
59    pub fn parse_node_pattern(&mut self) -> Result<NodePattern, ParseError> {
60        self.expect(&Token::LParen)?;
61
62        let mut variable = None;
63        let mut labels = Vec::new();
64        let mut properties = None;
65
66        // Optional variable name (identifier not followed by colon could still be
67        // a variable, or identifier followed by colon means variable + label).
68        if let Some(Token::Ident(_) | Token::BacktickIdent(_)) = self.peek() {
69            variable = Some(self.expect_ident()?);
70        }
71
72        // Optional labels (one or more `:Label`)
73        while self.eat(&Token::Colon) {
74            labels.push(self.expect_ident()?);
75        }
76
77        // Optional properties `{key: value, ...}`
78        if self.check(&Token::LBrace) {
79            properties = Some(self.parse_map_literal()?);
80        }
81
82        self.expect(&Token::RParen)?;
83
84        Ok(NodePattern {
85            variable,
86            labels,
87            properties,
88        })
89    }
90
91    /// Parse a map literal: `{key: value, key: value, ...}`
92    pub fn parse_map_literal(&mut self) -> Result<MapLiteral, ParseError> {
93        self.expect(&Token::LBrace)?;
94        let mut entries = Vec::new();
95
96        if !self.check(&Token::RBrace) {
97            let key = self.expect_ident()?;
98            self.expect(&Token::Colon)?;
99            let value = self.parse_expression()?;
100            entries.push((key, value));
101
102            while self.eat(&Token::Comma) {
103                let key = self.expect_ident()?;
104                self.expect(&Token::Colon)?;
105                let value = self.parse_expression()?;
106                entries.push((key, value));
107            }
108        }
109
110        self.expect(&Token::RBrace)?;
111        Ok(entries)
112    }
113
114    /// Parse relationship starting with `-` (outgoing `-[...]->`  or undirected `-[...]-`).
115    fn parse_relationship_from_minus(&mut self) -> Result<RelationshipPattern, ParseError> {
116        self.expect(&Token::Minus)?;
117
118        // Must have `[` for bracket content
119        self.expect(&Token::LBracket)?;
120
121        // Check for variable-length path `*` before or after content
122        let content = self.parse_relationship_content()?;
123
124        self.expect(&Token::RBracket)?;
125
126        // Determine direction: `->` means outgoing, `-` means undirected
127        let direction = if self.eat(&Token::ArrowRight) {
128            RelDirection::Outgoing
129        } else if self.eat(&Token::Minus) {
130            RelDirection::Undirected
131        } else {
132            return Err(self.error("expected -> or - after relationship bracket"));
133        };
134
135        Ok(RelationshipPattern {
136            variable: content.variable,
137            rel_types: content.rel_types,
138            direction,
139            properties: content.properties,
140            min_hops: content.min_hops,
141            max_hops: content.max_hops,
142        })
143    }
144
145    /// Parse incoming relationship: `<-[...]-`
146    fn parse_relationship_incoming(&mut self) -> Result<RelationshipPattern, ParseError> {
147        // Consume `<-`
148        self.expect(&Token::ArrowLeft)?;
149
150        self.expect(&Token::LBracket)?;
151        let content = self.parse_relationship_content()?;
152        self.expect(&Token::RBracket)?;
153
154        // Must end with `-`
155        self.expect(&Token::Minus)?;
156
157        Ok(RelationshipPattern {
158            variable: content.variable,
159            rel_types: content.rel_types,
160            direction: RelDirection::Incoming,
161            properties: content.properties,
162            min_hops: content.min_hops,
163            max_hops: content.max_hops,
164        })
165    }
166
167    /// Parse content inside relationship brackets: `[variable :TYPE | TYPE2 *N..M {props}]`
168    fn parse_relationship_content(&mut self) -> Result<RelContentResult, ParseError> {
169        let mut variable = None;
170        let mut rel_types = Vec::new();
171        let mut properties = None;
172        let mut min_hops = None;
173        let mut max_hops = None;
174
175        // Check for bare star: [*...] (no variable, no type)
176        if self.check(&Token::Star) {
177            let (mn, mx) = self.parse_var_length_spec()?;
178            min_hops = Some(mn);
179            max_hops = mx;
180
181            // After star spec, optional properties then done
182            if self.check(&Token::LBrace) {
183                properties = Some(self.parse_map_literal()?);
184            }
185            return Ok(RelContentResult {
186                variable,
187                rel_types,
188                properties,
189                min_hops,
190                max_hops,
191            });
192        }
193
194        // Optional variable
195        if let Some(Token::Ident(_) | Token::BacktickIdent(_)) = self.peek() {
196            variable = Some(self.expect_ident()?);
197        }
198
199        // Optional variable-length path star after variable: [r*...]
200        if self.check(&Token::Star) {
201            let (mn, mx) = self.parse_var_length_spec()?;
202            min_hops = Some(mn);
203            max_hops = mx;
204
205            if self.check(&Token::LBrace) {
206                properties = Some(self.parse_map_literal()?);
207            }
208            return Ok(RelContentResult {
209                variable,
210                rel_types,
211                properties,
212                min_hops,
213                max_hops,
214            });
215        }
216
217        // Optional relationship types: `:TYPE` or `:TYPE|TYPE2`
218        if self.eat(&Token::Colon) {
219            rel_types.push(self.expect_ident()?);
220            while self.eat(&Token::Pipe) {
221                rel_types.push(self.expect_ident()?);
222            }
223        }
224
225        // Optional variable-length path star after types: [:TYPE*...]
226        if self.check(&Token::Star) {
227            let (mn, mx) = self.parse_var_length_spec()?;
228            min_hops = Some(mn);
229            max_hops = mx;
230        }
231
232        // Optional properties
233        if self.check(&Token::LBrace) {
234            properties = Some(self.parse_map_literal()?);
235        }
236
237        Ok(RelContentResult {
238            variable,
239            rel_types,
240            properties,
241            min_hops,
242            max_hops,
243        })
244    }
245
246    /// Parse variable-length spec after `*`: `*`, `*N`, `*N..M`, `*N..`, `*..M`
247    /// Returns (min_hops, max_hops).
248    fn parse_var_length_spec(&mut self) -> Result<(u32, Option<u32>), ParseError> {
249        self.expect(&Token::Star)?;
250
251        // Check for DoubleDot immediately: *..M
252        if self.eat(&Token::DoubleDot) {
253            // *..M form
254            if let Some(Token::Integer(n)) = self.peek() {
255                let max = Self::int_to_u32(*n, self)?;
256                self.advance();
257                return Ok((1, Some(max)));
258            }
259            // *.. alone (unbounded from 1)
260            return Ok((1, None));
261        }
262
263        // Check for integer: *N or *N..M or *N..
264        if let Some(Token::Integer(n)) = self.peek() {
265            let first = Self::int_to_u32(*n, self)?;
266            self.advance();
267
268            // Check for DoubleDot: *N..M or *N..
269            if self.eat(&Token::DoubleDot) {
270                if let Some(Token::Integer(m)) = self.peek() {
271                    let second = Self::int_to_u32(*m, self)?;
272                    self.advance();
273                    return Ok((first, Some(second)));
274                }
275                // *N.. (open end)
276                return Ok((first, None));
277            }
278
279            // *N (exact hop)
280            return Ok((first, Some(first)));
281        }
282
283        // Just * alone (unbounded)
284        Ok((1, None))
285    }
286
287    /// Convert i64 to u32 for hop counts, returning error if negative.
288    fn int_to_u32(n: i64, parser: &Self) -> Result<u32, ParseError> {
289        if n < 0 {
290            return Err(parser.error("hop count must be non-negative"));
291        }
292        Ok(n as u32)
293    }
294}
295
296/// Intermediate result for relationship bracket content.
297struct RelContentResult {
298    variable: Option<String>,
299    rel_types: Vec<String>,
300    properties: Option<MapLiteral>,
301    min_hops: Option<u32>,
302    max_hops: Option<u32>,
303}
304
305// ---------------------------------------------------------------------------
306// Tests
307// ---------------------------------------------------------------------------
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use crate::lexer::lex;
313
314    /// Helper: parse a pattern from a string.
315    fn parse_pattern_str(input: &str) -> Result<Pattern, ParseError> {
316        let tokens = lex(input).expect("lexing should succeed");
317        let mut parser = Parser::new(&tokens, input);
318        parser.parse_pattern()
319    }
320
321    /// Helper: parse a node pattern from a string.
322    fn parse_node(input: &str) -> Result<NodePattern, ParseError> {
323        let tokens = lex(input).expect("lexing should succeed");
324        let mut parser = Parser::new(&tokens, input);
325        parser.parse_node_pattern()
326    }
327
328    // -- TASK-027: Pattern parser tests --
329
330    // Single node: (n:Person)
331    #[test]
332    fn pattern_single_node_with_label() {
333        let node = parse_node("(n:Person)").expect("should parse");
334        assert_eq!(
335            node,
336            NodePattern {
337                variable: Some("n".to_string()),
338                labels: vec!["Person".to_string()],
339                properties: None,
340            }
341        );
342    }
343
344    // Node with no label: (n)
345    #[test]
346    fn pattern_node_no_label() {
347        let node = parse_node("(n)").expect("should parse");
348        assert_eq!(
349            node,
350            NodePattern {
351                variable: Some("n".to_string()),
352                labels: vec![],
353                properties: None,
354            }
355        );
356    }
357
358    // Empty node: ()
359    #[test]
360    fn pattern_empty_node() {
361        let node = parse_node("()").expect("should parse");
362        assert_eq!(
363            node,
364            NodePattern {
365                variable: None,
366                labels: vec![],
367                properties: None,
368            }
369        );
370    }
371
372    // Node with properties: (n:Person {name: 'Alice'})
373    #[test]
374    fn pattern_node_with_properties() {
375        let node = parse_node("(n:Person {name: 'Alice'})").expect("should parse");
376        assert_eq!(
377            node,
378            NodePattern {
379                variable: Some("n".to_string()),
380                labels: vec!["Person".to_string()],
381                properties: Some(vec![(
382                    "name".to_string(),
383                    Expression::Literal(Literal::String("Alice".to_string())),
384                )]),
385            }
386        );
387    }
388
389    // Node with multiple labels: (n:Person:Employee)
390    #[test]
391    fn pattern_node_multiple_labels() {
392        let node = parse_node("(n:Person:Employee)").expect("should parse");
393        assert_eq!(
394            node,
395            NodePattern {
396                variable: Some("n".to_string()),
397                labels: vec!["Person".to_string(), "Employee".to_string()],
398                properties: None,
399            }
400        );
401    }
402
403    // Node with multiple properties
404    #[test]
405    fn pattern_node_multiple_properties() {
406        let node = parse_node("(n:Person {name: 'Alice', age: 30})").expect("should parse");
407        assert_eq!(
408            node,
409            NodePattern {
410                variable: Some("n".to_string()),
411                labels: vec!["Person".to_string()],
412                properties: Some(vec![
413                    (
414                        "name".to_string(),
415                        Expression::Literal(Literal::String("Alice".to_string())),
416                    ),
417                    ("age".to_string(), Expression::Literal(Literal::Integer(30)),),
418                ]),
419            }
420        );
421    }
422
423    // Label without variable: (:Person)
424    #[test]
425    fn pattern_node_label_no_variable() {
426        let node = parse_node("(:Person)").expect("should parse");
427        assert_eq!(
428            node,
429            NodePattern {
430                variable: None,
431                labels: vec!["Person".to_string()],
432                properties: None,
433            }
434        );
435    }
436
437    // Outgoing relationship: (a)-[:KNOWS]->(b)
438    #[test]
439    fn pattern_outgoing_relationship() {
440        let pattern = parse_pattern_str("(a)-[:KNOWS]->(b)").expect("should parse");
441        assert_eq!(pattern.chains.len(), 1);
442        let chain = &pattern.chains[0];
443        assert_eq!(chain.elements.len(), 3);
444
445        assert_eq!(
446            chain.elements[0],
447            PatternElement::Node(NodePattern {
448                variable: Some("a".to_string()),
449                labels: vec![],
450                properties: None,
451            })
452        );
453        assert_eq!(
454            chain.elements[1],
455            PatternElement::Relationship(RelationshipPattern {
456                variable: None,
457                rel_types: vec!["KNOWS".to_string()],
458                direction: RelDirection::Outgoing,
459                properties: None,
460                min_hops: None,
461                max_hops: None,
462            })
463        );
464        assert_eq!(
465            chain.elements[2],
466            PatternElement::Node(NodePattern {
467                variable: Some("b".to_string()),
468                labels: vec![],
469                properties: None,
470            })
471        );
472    }
473
474    // Incoming: (a)<-[:KNOWS]-(b)
475    #[test]
476    fn pattern_incoming_relationship() {
477        let pattern = parse_pattern_str("(a)<-[:KNOWS]-(b)").expect("should parse");
478        let chain = &pattern.chains[0];
479        assert_eq!(
480            chain.elements[1],
481            PatternElement::Relationship(RelationshipPattern {
482                variable: None,
483                rel_types: vec!["KNOWS".to_string()],
484                direction: RelDirection::Incoming,
485                properties: None,
486                min_hops: None,
487                max_hops: None,
488            })
489        );
490    }
491
492    // Undirected with brackets: (a)-[:KNOWS]-(b)
493    #[test]
494    fn pattern_undirected_relationship() {
495        let pattern = parse_pattern_str("(a)-[:KNOWS]-(b)").expect("should parse");
496        let chain = &pattern.chains[0];
497        assert_eq!(
498            chain.elements[1],
499            PatternElement::Relationship(RelationshipPattern {
500                variable: None,
501                rel_types: vec!["KNOWS".to_string()],
502                direction: RelDirection::Undirected,
503                properties: None,
504                min_hops: None,
505                max_hops: None,
506            })
507        );
508    }
509
510    // Undirected without brackets: (a)--(b)
511    #[test]
512    fn pattern_undirected_no_brackets() {
513        let pattern = parse_pattern_str("(a)--(b)").expect("should parse");
514        let chain = &pattern.chains[0];
515        assert_eq!(
516            chain.elements[1],
517            PatternElement::Relationship(RelationshipPattern {
518                variable: None,
519                rel_types: vec![],
520                direction: RelDirection::Undirected,
521                properties: None,
522                min_hops: None,
523                max_hops: None,
524            })
525        );
526    }
527
528    // Relationship with variable: (a)-[r:KNOWS]->(b)
529    #[test]
530    fn pattern_relationship_with_variable() {
531        let pattern = parse_pattern_str("(a)-[r:KNOWS]->(b)").expect("should parse");
532        let chain = &pattern.chains[0];
533        assert_eq!(
534            chain.elements[1],
535            PatternElement::Relationship(RelationshipPattern {
536                variable: Some("r".to_string()),
537                rel_types: vec!["KNOWS".to_string()],
538                direction: RelDirection::Outgoing,
539                properties: None,
540                min_hops: None,
541                max_hops: None,
542            })
543        );
544    }
545
546    // Relationship with properties: (a)-[r:KNOWS {since: 2020}]->(b)
547    #[test]
548    fn pattern_relationship_with_properties() {
549        let pattern = parse_pattern_str("(a)-[r:KNOWS {since: 2020}]->(b)").expect("should parse");
550        let chain = &pattern.chains[0];
551        assert_eq!(
552            chain.elements[1],
553            PatternElement::Relationship(RelationshipPattern {
554                variable: Some("r".to_string()),
555                rel_types: vec!["KNOWS".to_string()],
556                direction: RelDirection::Outgoing,
557                properties: Some(vec![(
558                    "since".to_string(),
559                    Expression::Literal(Literal::Integer(2020)),
560                )]),
561                min_hops: None,
562                max_hops: None,
563            })
564        );
565    }
566
567    // Multiple relationship types: (a)-[:KNOWS|LIKES]->(b)
568    #[test]
569    fn pattern_multiple_rel_types() {
570        let pattern = parse_pattern_str("(a)-[:KNOWS|LIKES]->(b)").expect("should parse");
571        let chain = &pattern.chains[0];
572        assert_eq!(
573            chain.elements[1],
574            PatternElement::Relationship(RelationshipPattern {
575                variable: None,
576                rel_types: vec!["KNOWS".to_string(), "LIKES".to_string()],
577                direction: RelDirection::Outgoing,
578                properties: None,
579                min_hops: None,
580                max_hops: None,
581            })
582        );
583    }
584
585    // Multi-hop: (a)-[:KNOWS]->(b)-[:KNOWS]->(c)
586    #[test]
587    fn pattern_multi_hop() {
588        let pattern = parse_pattern_str("(a)-[:KNOWS]->(b)-[:KNOWS]->(c)").expect("should parse");
589        let chain = &pattern.chains[0];
590        assert_eq!(chain.elements.len(), 5);
591        // a, KNOWS->, b, KNOWS->, c
592        assert!(matches!(&chain.elements[0], PatternElement::Node(_)));
593        assert!(matches!(
594            &chain.elements[1],
595            PatternElement::Relationship(_)
596        ));
597        assert!(matches!(&chain.elements[2], PatternElement::Node(_)));
598        assert!(matches!(
599            &chain.elements[3],
600            PatternElement::Relationship(_)
601        ));
602        assert!(matches!(&chain.elements[4], PatternElement::Node(_)));
603    }
604
605    // Multiple chains: (a)-[:KNOWS]->(b), (c)-[:LIKES]->(d)
606    #[test]
607    fn pattern_multiple_chains() {
608        let pattern =
609            parse_pattern_str("(a)-[:KNOWS]->(b), (c)-[:LIKES]->(d)").expect("should parse");
610        assert_eq!(pattern.chains.len(), 2);
611        assert_eq!(pattern.chains[0].elements.len(), 3);
612        assert_eq!(pattern.chains[1].elements.len(), 3);
613    }
614
615    // -- TASK-102: Variable-length path parsing --
616
617    // [*] -> min=1, max=None (unbounded)
618    #[test]
619    fn pattern_var_length_star_only() {
620        let pattern = parse_pattern_str("(a)-[*]->(b)").expect("should parse");
621        let chain = &pattern.chains[0];
622        if let PatternElement::Relationship(rel) = &chain.elements[1] {
623            assert_eq!(rel.min_hops, Some(1));
624            assert_eq!(rel.max_hops, None);
625        } else {
626            panic!("expected relationship");
627        }
628    }
629
630    // [*N] -> min=N, max=N (exact hop)
631    #[test]
632    fn pattern_var_length_exact_hop() {
633        let pattern = parse_pattern_str("(a)-[*3]->(b)").expect("should parse");
634        let chain = &pattern.chains[0];
635        if let PatternElement::Relationship(rel) = &chain.elements[1] {
636            assert_eq!(rel.min_hops, Some(3));
637            assert_eq!(rel.max_hops, Some(3));
638        } else {
639            panic!("expected relationship");
640        }
641    }
642
643    // [*N..M] -> min=N, max=M
644    #[test]
645    fn pattern_var_length_range() {
646        let pattern = parse_pattern_str("(a)-[*1..3]->(b)").expect("should parse");
647        let chain = &pattern.chains[0];
648        if let PatternElement::Relationship(rel) = &chain.elements[1] {
649            assert_eq!(rel.min_hops, Some(1));
650            assert_eq!(rel.max_hops, Some(3));
651        } else {
652            panic!("expected relationship");
653        }
654    }
655
656    // [*N..] -> min=N, max=None
657    #[test]
658    fn pattern_var_length_open_end() {
659        let pattern = parse_pattern_str("(a)-[*2..]->(b)").expect("should parse");
660        let chain = &pattern.chains[0];
661        if let PatternElement::Relationship(rel) = &chain.elements[1] {
662            assert_eq!(rel.min_hops, Some(2));
663            assert_eq!(rel.max_hops, None);
664        } else {
665            panic!("expected relationship");
666        }
667    }
668
669    // [*..M] -> min=1, max=M
670    #[test]
671    fn pattern_var_length_open_start() {
672        let pattern = parse_pattern_str("(a)-[*..5]->(b)").expect("should parse");
673        let chain = &pattern.chains[0];
674        if let PatternElement::Relationship(rel) = &chain.elements[1] {
675            assert_eq!(rel.min_hops, Some(1));
676            assert_eq!(rel.max_hops, Some(5));
677        } else {
678            panic!("expected relationship");
679        }
680    }
681
682    // [:TYPE*N..M] -> typed + bounded
683    #[test]
684    fn pattern_var_length_typed_bounded() {
685        let pattern = parse_pattern_str("(a)-[:KNOWS*2..4]->(b)").expect("should parse");
686        let chain = &pattern.chains[0];
687        if let PatternElement::Relationship(rel) = &chain.elements[1] {
688            assert_eq!(rel.rel_types, vec!["KNOWS".to_string()]);
689            assert_eq!(rel.min_hops, Some(2));
690            assert_eq!(rel.max_hops, Some(4));
691        } else {
692            panic!("expected relationship");
693        }
694    }
695
696    // [r:TYPE*2..5] -> variable + typed + bounded
697    #[test]
698    fn pattern_var_length_variable_typed_bounded() {
699        let pattern = parse_pattern_str("(a)-[r:KNOWS*2..5]->(b)").expect("should parse");
700        let chain = &pattern.chains[0];
701        if let PatternElement::Relationship(rel) = &chain.elements[1] {
702            assert_eq!(rel.variable, Some("r".to_string()));
703            assert_eq!(rel.rel_types, vec!["KNOWS".to_string()]);
704            assert_eq!(rel.min_hops, Some(2));
705            assert_eq!(rel.max_hops, Some(5));
706        } else {
707            panic!("expected relationship");
708        }
709    }
710
711    // Incoming variable-length: (a)<-[*1..3]-(b)
712    #[test]
713    fn pattern_var_length_incoming() {
714        let pattern = parse_pattern_str("(a)<-[*1..3]-(b)").expect("should parse");
715        let chain = &pattern.chains[0];
716        if let PatternElement::Relationship(rel) = &chain.elements[1] {
717            assert_eq!(rel.direction, RelDirection::Incoming);
718            assert_eq!(rel.min_hops, Some(1));
719            assert_eq!(rel.max_hops, Some(3));
720        } else {
721            panic!("expected relationship");
722        }
723    }
724
725    // [*0..1] zero-length path
726    #[test]
727    fn pattern_var_length_zero() {
728        let pattern = parse_pattern_str("(a)-[*0..1]->(b)").expect("should parse");
729        let chain = &pattern.chains[0];
730        if let PatternElement::Relationship(rel) = &chain.elements[1] {
731            assert_eq!(rel.min_hops, Some(0));
732            assert_eq!(rel.max_hops, Some(1));
733        } else {
734            panic!("expected relationship");
735        }
736    }
737
738    // Variable-length with variable only (no type): [r*2..5]
739    #[test]
740    fn pattern_var_length_variable_no_type() {
741        let pattern = parse_pattern_str("(a)-[r*2..5]->(b)").expect("should parse");
742        let chain = &pattern.chains[0];
743        if let PatternElement::Relationship(rel) = &chain.elements[1] {
744            assert_eq!(rel.variable, Some("r".to_string()));
745            assert_eq!(rel.rel_types, Vec::<String>::new());
746            assert_eq!(rel.min_hops, Some(2));
747            assert_eq!(rel.max_hops, Some(5));
748        } else {
749            panic!("expected relationship");
750        }
751    }
752
753    // Regular rel (no *) still has None/None
754    #[test]
755    fn pattern_regular_rel_no_hops() {
756        let pattern = parse_pattern_str("(a)-[:KNOWS]->(b)").expect("should parse");
757        let chain = &pattern.chains[0];
758        if let PatternElement::Relationship(rel) = &chain.elements[1] {
759            assert_eq!(rel.min_hops, None);
760            assert_eq!(rel.max_hops, None);
761        } else {
762            panic!("expected relationship");
763        }
764    }
765
766    // Relationship with only variable, no type: (a)-[r]->(b)
767    #[test]
768    fn pattern_rel_variable_only() {
769        let pattern = parse_pattern_str("(a)-[r]->(b)").expect("should parse");
770        let chain = &pattern.chains[0];
771        assert_eq!(
772            chain.elements[1],
773            PatternElement::Relationship(RelationshipPattern {
774                variable: Some("r".to_string()),
775                rel_types: vec![],
776                direction: RelDirection::Outgoing,
777                properties: None,
778                min_hops: None,
779                max_hops: None,
780            })
781        );
782    }
783
784    // Empty relationship brackets: (a)-[]->(b)
785    #[test]
786    fn pattern_empty_rel_brackets() {
787        let pattern = parse_pattern_str("(a)-[]->(b)").expect("should parse");
788        let chain = &pattern.chains[0];
789        assert_eq!(
790            chain.elements[1],
791            PatternElement::Relationship(RelationshipPattern {
792                variable: None,
793                rel_types: vec![],
794                direction: RelDirection::Outgoing,
795                properties: None,
796                min_hops: None,
797                max_hops: None,
798            })
799        );
800    }
801}