Skip to main content

sz_orm_graph/
cypher_parser.rs

1//! Cypher 子集递归下降解析器
2//!
3//! 支持语法子集:
4//! 1. `MATCH (n:Label) RETURN n`
5//! 2. `MATCH (n:Label {prop: $param}) RETURN n`
6//! 3. `MATCH (a:L1)-[r:RelType]->(b:L2) RETURN a, r, b`
7//! 4. `MATCH (n) WHERE n.prop = $param RETURN n`
8//! 5. `MATCH (n:Label) RETURN count(n)`
9
10use crate::error::GraphError;
11use std::collections::HashMap;
12
13#[derive(Debug, Clone)]
14#[non_exhaustive]
15pub enum ParsedQuery {
16    MatchNode {
17        alias: String,
18        label: Option<String>,
19        where_clause: Option<WhereClause>,
20        return_items: Vec<ReturnItem>,
21    },
22    MatchRelationship {
23        from: NodePattern,
24        rel: RelPattern,
25        to: NodePattern,
26        return_items: Vec<ReturnItem>,
27    },
28    CreateNode {
29        alias: String,
30        label: String,
31        properties: Vec<(String, String)>,
32    },
33    MergeNode {
34        alias: String,
35        label: String,
36        properties: Vec<(String, String)>,
37    },
38    Delete {
39        alias: String,
40    },
41    Set {
42        alias: String,
43        prop: String,
44        param_name: String,
45    },
46}
47
48#[derive(Debug, Clone)]
49pub struct NodePattern {
50    pub alias: String,
51    pub label: Option<String>,
52}
53
54#[derive(Debug, Clone)]
55pub struct RelPattern {
56    pub alias: String,
57    pub rel_type: Option<String>,
58}
59
60#[derive(Debug, Clone)]
61pub struct WhereClause {
62    pub alias: String,
63    pub prop: String,
64    pub param_name: String,
65}
66
67#[derive(Debug, Clone)]
68pub enum ReturnItem {
69    Node(String),
70    Relationship(String),
71    Count(String),
72}
73
74pub struct CypherSubsetParser;
75
76impl CypherSubsetParser {
77    pub fn parse(
78        cypher: &str,
79        _params: &HashMap<String, serde_json::Value>,
80    ) -> Result<ParsedQuery, GraphError> {
81        let cypher = cypher.trim();
82
83        if cypher.is_empty() {
84            return Err(GraphError::QueryError("empty query".into()));
85        }
86
87        let upper = cypher.to_uppercase();
88
89        if upper.contains("SELECT") {
90            return Err(GraphError::SqlNotSupported(
91                "SQL passthrough is not supported in graph query".into(),
92            ));
93        }
94
95        if upper.starts_with("CREATE") {
96            let after = cypher[6..].trim_start();
97            return Self::parse_create(after);
98        }
99        if upper.starts_with("MERGE") {
100            let after = cypher[5..].trim_start();
101            return Self::parse_merge(after);
102        }
103        if upper.starts_with("DELETE") {
104            let after = cypher[6..].trim_start();
105            return Self::parse_delete(after);
106        }
107        if upper.starts_with("SET") {
108            let after = cypher[3..].trim_start();
109            return Self::parse_set(after);
110        }
111
112        if !upper.starts_with("MATCH") {
113            return Err(GraphError::QueryError(format!(
114                "unsupported syntax: query must start with MATCH/CREATE/MERGE/DELETE/SET, got: {}",
115                &cypher[..cypher.len().min(20)]
116            )));
117        }
118
119        let after_match = cypher[5..].trim_start();
120
121        if let Some(arrow_pos) = after_match.find("]->") {
122            return Self::parse_relationship(after_match, arrow_pos);
123        }
124
125        Self::parse_node(after_match)
126    }
127
128    fn parse_node(rest: &str) -> Result<ParsedQuery, GraphError> {
129        let (node_pattern, after_node) = Self::parse_node_pattern(rest)?;
130        let mut where_clause = None;
131        let mut after_where = after_node;
132
133        let after_trimmed = after_node.trim_start();
134        if after_trimmed.to_uppercase().starts_with("WHERE") {
135            let where_rest = after_trimmed[5..].trim_start();
136            let (wc, rest) = Self::parse_where(where_rest)?;
137            where_clause = Some(wc);
138            after_where = rest;
139        }
140
141        let return_rest = after_where.trim_start();
142        let return_items = Self::parse_return(return_rest)?;
143
144        Ok(ParsedQuery::MatchNode {
145            alias: node_pattern.alias,
146            label: node_pattern.label,
147            where_clause,
148            return_items,
149        })
150    }
151
152    fn parse_relationship(rest: &str, _arrow_pos: usize) -> Result<ParsedQuery, GraphError> {
153        let (from, after_from) = Self::parse_node_pattern(rest)?;
154
155        let after_from_trimmed = after_from.trim_start();
156        if !after_from_trimmed.starts_with('-') {
157            return Err(GraphError::QueryError(
158                "expected '-' after node pattern in relationship".into(),
159            ));
160        }
161
162        let bracket_start = after_from_trimmed.find('[').ok_or_else(|| {
163            GraphError::QueryError("expected '[' for relationship pattern".into())
164        })?;
165        let bracket_end = after_from_trimmed.find(']').ok_or_else(|| {
166            GraphError::QueryError("expected ']' for relationship pattern".into())
167        })?;
168
169        let rel_content = &after_from_trimmed[bracket_start + 1..bracket_end];
170        let rel_pattern = Self::parse_rel_pattern(rel_content)?;
171
172        let after_bracket = &after_from_trimmed[bracket_end + 1..];
173        let after_arrow = after_bracket.trim_start_matches(['-', '>']).trim_start();
174
175        let (to, after_to) = Self::parse_node_pattern(after_arrow)?;
176
177        let return_rest = after_to.trim_start();
178        let return_items = Self::parse_return(return_rest)?;
179
180        Ok(ParsedQuery::MatchRelationship {
181            from,
182            rel: rel_pattern,
183            to,
184            return_items,
185        })
186    }
187
188    fn parse_node_pattern(rest: &str) -> Result<(NodePattern, &str), GraphError> {
189        let rest = rest.trim_start();
190        if !rest.starts_with('(') {
191            return Err(GraphError::QueryError(
192                "expected '(' for node pattern".into(),
193            ));
194        }
195
196        let close_pos = rest
197            .find(')')
198            .ok_or_else(|| GraphError::QueryError("expected ')' for node pattern".into()))?;
199
200        let content = &rest[1..close_pos];
201        let after = &rest[close_pos + 1..];
202
203        let (alias, label) = if let Some(colon_pos) = content.find(':') {
204            let alias = content[..colon_pos].trim().to_string();
205            let label_part = &content[colon_pos + 1..];
206            let label = if let Some(brace_pos) = label_part.find('{') {
207                label_part[..brace_pos].trim().to_string()
208            } else {
209                label_part.trim().to_string()
210            };
211            (alias, Some(label))
212        } else {
213            (content.trim().to_string(), None)
214        };
215
216        if alias.is_empty() {
217            return Err(GraphError::QueryError(
218                "node pattern alias cannot be empty".into(),
219            ));
220        }
221
222        Ok((NodePattern { alias, label }, after))
223    }
224
225    fn parse_rel_pattern(content: &str) -> Result<RelPattern, GraphError> {
226        let content = content.trim();
227        let (alias, rel_type) = if let Some(colon_pos) = content.find(':') {
228            let alias = content[..colon_pos].trim().to_string();
229            let rel_type = content[colon_pos + 1..].trim().to_string();
230            (alias, Some(rel_type))
231        } else {
232            (content.to_string(), None)
233        };
234
235        Ok(RelPattern { alias, rel_type })
236    }
237
238    fn parse_where(rest: &str) -> Result<(WhereClause, &str), GraphError> {
239        let eq_pos = rest
240            .find('=')
241            .ok_or_else(|| GraphError::QueryError("WHERE clause must contain '='".into()))?;
242
243        let left = rest[..eq_pos].trim();
244        let right_start = rest[eq_pos + 1..].trim_start();
245
246        let param_match = right_start.find('$').ok_or_else(|| {
247            GraphError::QueryError("WHERE clause must use $param parameter".into())
248        })?;
249
250        let param_rest = &right_start[param_match + 1..];
251        let param_end = param_rest
252            .find(|c: char| c.is_whitespace())
253            .unwrap_or(param_rest.len());
254        let param_name = param_rest[..param_end].to_string();
255
256        let after_param = &param_rest[param_end..];
257
258        let dot_pos = left.find('.').ok_or_else(|| {
259            GraphError::QueryError("WHERE clause must use alias.prop format".into())
260        })?;
261
262        let alias = left[..dot_pos].trim().to_string();
263        let prop = left[dot_pos + 1..].trim().to_string();
264
265        Ok((
266            WhereClause {
267                alias,
268                prop,
269                param_name,
270            },
271            after_param,
272        ))
273    }
274
275    fn parse_return(rest: &str) -> Result<Vec<ReturnItem>, GraphError> {
276        let rest = rest.trim_start();
277        let upper = rest.to_uppercase();
278        if !upper.starts_with("RETURN") {
279            return Err(GraphError::QueryError("expected RETURN clause".into()));
280        }
281
282        let return_rest = rest[6..].trim();
283        let mut items = Vec::new();
284
285        for part in return_rest.split(',') {
286            let part = part.trim();
287            if part.is_empty() {
288                continue;
289            }
290            if part.to_uppercase().starts_with("COUNT(") {
291                let inner_start = part.find('(').unwrap();
292                let inner_end = part.find(')').unwrap();
293                let alias = part[inner_start + 1..inner_end].trim().to_string();
294                items.push(ReturnItem::Count(alias));
295            } else {
296                items.push(ReturnItem::Node(part.to_string()));
297            }
298        }
299
300        if items.is_empty() {
301            return Err(GraphError::QueryError(
302                "RETURN clause must have at least one item".into(),
303            ));
304        }
305
306        Ok(items)
307    }
308
309    fn parse_create(rest: &str) -> Result<ParsedQuery, GraphError> {
310        let (node_pattern, properties) = Self::parse_node_with_properties(rest)?;
311        let label = node_pattern.label.ok_or_else(|| {
312            GraphError::QueryError(
313                "CREATE requires a node label, e.g. CREATE (n:Label {...})".into(),
314            )
315        })?;
316        Ok(ParsedQuery::CreateNode {
317            alias: node_pattern.alias,
318            label,
319            properties,
320        })
321    }
322
323    fn parse_merge(rest: &str) -> Result<ParsedQuery, GraphError> {
324        let (node_pattern, properties) = Self::parse_node_with_properties(rest)?;
325        let label = node_pattern.label.ok_or_else(|| {
326            GraphError::QueryError("MERGE requires a node label, e.g. MERGE (n:Label {...})".into())
327        })?;
328        Ok(ParsedQuery::MergeNode {
329            alias: node_pattern.alias,
330            label,
331            properties,
332        })
333    }
334
335    fn parse_delete(rest: &str) -> Result<ParsedQuery, GraphError> {
336        let rest = rest.trim();
337        if rest.is_empty() {
338            return Err(GraphError::QueryError(
339                "DELETE requires an alias, e.g. DELETE n".into(),
340            ));
341        }
342        let alias = rest
343            .split(|c: char| c.is_whitespace())
344            .next()
345            .unwrap_or("")
346            .to_string();
347        if alias.is_empty() {
348            return Err(GraphError::QueryError(
349                "DELETE alias cannot be empty".into(),
350            ));
351        }
352        Ok(ParsedQuery::Delete { alias })
353    }
354
355    fn parse_set(rest: &str) -> Result<ParsedQuery, GraphError> {
356        let rest = rest.trim();
357        let eq_pos = rest.find('=').ok_or_else(|| {
358            GraphError::QueryError("SET clause must contain '=', e.g. SET n.prop = $param".into())
359        })?;
360
361        let left = rest[..eq_pos].trim();
362        let right = rest[eq_pos + 1..].trim();
363
364        if !right.starts_with('$') {
365            return Err(GraphError::ParameterizationError(
366                "SET clause must use $param parameter, literal values are not allowed".into(),
367            ));
368        }
369        let param_name = right[1..].trim().to_string();
370        if param_name.is_empty() {
371            return Err(GraphError::QueryError(
372                "SET clause param name cannot be empty".into(),
373            ));
374        }
375
376        let dot_pos = left.find('.').ok_or_else(|| {
377            GraphError::QueryError("SET clause must use alias.prop format".into())
378        })?;
379        let alias = left[..dot_pos].trim().to_string();
380        let prop = left[dot_pos + 1..].trim().to_string();
381
382        if alias.is_empty() || prop.is_empty() {
383            return Err(GraphError::QueryError(
384                "SET clause alias and prop cannot be empty".into(),
385            ));
386        }
387
388        Ok(ParsedQuery::Set {
389            alias,
390            prop,
391            param_name,
392        })
393    }
394
395    fn parse_properties(content: &str) -> Result<Vec<(String, String)>, GraphError> {
396        let content = content.trim();
397        if content.is_empty() {
398            return Ok(Vec::new());
399        }
400        if !content.starts_with('{') || !content.ends_with('}') {
401            return Err(GraphError::QueryError(
402                "properties must be enclosed in {}, e.g. {prop: $param}".into(),
403            ));
404        }
405        let inner = &content[1..content.len() - 1];
406        let mut props = Vec::new();
407        for part in inner.split(',') {
408            let part = part.trim();
409            if part.is_empty() {
410                continue;
411            }
412            let colon_pos = part.find(':').ok_or_else(|| {
413                GraphError::QueryError("property must be prop: $param format".into())
414            })?;
415            let prop_name = part[..colon_pos].trim().to_string();
416            let param_part = part[colon_pos + 1..].trim();
417            if !param_part.starts_with('$') {
418                return Err(GraphError::ParameterizationError(format!(
419                    "property {} must use $param parameter, literal values are not allowed",
420                    prop_name
421                )));
422            }
423            let param_name = param_part[1..].trim().to_string();
424            if prop_name.is_empty() || param_name.is_empty() {
425                return Err(GraphError::QueryError(
426                    "property name and param name cannot be empty".into(),
427                ));
428            }
429            props.push((prop_name, param_name));
430        }
431        Ok(props)
432    }
433
434    fn parse_node_with_properties(
435        rest: &str,
436    ) -> Result<(NodePattern, Vec<(String, String)>), GraphError> {
437        let rest = rest.trim_start();
438        if !rest.starts_with('(') {
439            return Err(GraphError::QueryError(
440                "expected '(' for node pattern, e.g. (alias:Label {prop: $param})".into(),
441            ));
442        }
443        let close_pos = rest
444            .find(')')
445            .ok_or_else(|| GraphError::QueryError("expected ')' for node pattern".into()))?;
446
447        let content = &rest[1..close_pos];
448        let (alias, label, properties) = if let Some(brace_pos) = content.find('{') {
449            let before_brace = &content[..brace_pos];
450            let props_str = &content[brace_pos..];
451            let props = Self::parse_properties(props_str)?;
452            let colon_pos = before_brace
453                .find(':')
454                .ok_or_else(|| GraphError::QueryError("node pattern requires :Label".into()))?;
455            let alias = before_brace[..colon_pos].trim().to_string();
456            let label = before_brace[colon_pos + 1..].trim().to_string();
457            if alias.is_empty() || label.is_empty() {
458                return Err(GraphError::QueryError(
459                    "alias and label cannot be empty in node pattern".into(),
460                ));
461            }
462            (alias, Some(label), props)
463        } else {
464            let colon_pos = content
465                .find(':')
466                .ok_or_else(|| GraphError::QueryError("node pattern requires :Label".into()))?;
467            let alias = content[..colon_pos].trim().to_string();
468            let label = content[colon_pos + 1..].trim().to_string();
469            if alias.is_empty() || label.is_empty() {
470                return Err(GraphError::QueryError(
471                    "alias and label cannot be empty in node pattern".into(),
472                ));
473            }
474            (alias, Some(label), Vec::new())
475        };
476
477        Ok((NodePattern { alias, label }, properties))
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    fn empty_params() -> HashMap<String, serde_json::Value> {
486        HashMap::new()
487    }
488
489    #[test]
490    fn test_parse_match_node_label() {
491        let q = CypherSubsetParser::parse("MATCH (n:Person) RETURN n", &empty_params()).unwrap();
492        match q {
493            ParsedQuery::MatchNode { alias, label, .. } => {
494                assert_eq!(alias, "n");
495                assert_eq!(label.as_deref(), Some("Person"));
496            }
497            _ => panic!("expected MatchNode"),
498        }
499    }
500
501    #[test]
502    fn test_parse_match_node_where_param() {
503        let q = CypherSubsetParser::parse(
504            "MATCH (n:Person) WHERE n.name = $name RETURN n",
505            &empty_params(),
506        )
507        .unwrap();
508        match q {
509            ParsedQuery::MatchNode { where_clause, .. } => {
510                let wc = where_clause.unwrap();
511                assert_eq!(wc.alias, "n");
512                assert_eq!(wc.prop, "name");
513                assert_eq!(wc.param_name, "name");
514            }
515            _ => panic!("expected MatchNode"),
516        }
517    }
518
519    #[test]
520    fn test_parse_match_relationship() {
521        let q = CypherSubsetParser::parse(
522            "MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a, r, b",
523            &empty_params(),
524        )
525        .unwrap();
526        match q {
527            ParsedQuery::MatchRelationship { from, rel, to, .. } => {
528                assert_eq!(from.alias, "a");
529                assert_eq!(from.label.as_deref(), Some("Person"));
530                assert_eq!(rel.rel_type.as_deref(), Some("KNOWS"));
531                assert_eq!(to.alias, "b");
532            }
533            _ => panic!("expected MatchRelationship"),
534        }
535    }
536
537    #[test]
538    fn test_parse_count_aggregation() {
539        let q =
540            CypherSubsetParser::parse("MATCH (n:Person) RETURN count(n)", &empty_params()).unwrap();
541        match q {
542            ParsedQuery::MatchNode { return_items, .. } => {
543                assert!(matches!(return_items[0], ReturnItem::Count(_)));
544            }
545            _ => panic!("expected MatchNode"),
546        }
547    }
548
549    #[test]
550    fn test_parse_create_node() {
551        let q =
552            CypherSubsetParser::parse("CREATE (n:Person {name: $name})", &empty_params()).unwrap();
553        match q {
554            ParsedQuery::CreateNode {
555                alias,
556                label,
557                properties,
558            } => {
559                assert_eq!(alias, "n");
560                assert_eq!(label, "Person");
561                assert_eq!(properties, vec![("name".to_string(), "name".to_string())]);
562            }
563            _ => panic!("expected CreateNode"),
564        }
565    }
566
567    #[test]
568    fn test_parse_merge_node() {
569        let q =
570            CypherSubsetParser::parse("MERGE (n:Person {name: $name, age: $age})", &empty_params())
571                .unwrap();
572        match q {
573            ParsedQuery::MergeNode {
574                alias,
575                label,
576                properties,
577            } => {
578                assert_eq!(alias, "n");
579                assert_eq!(label, "Person");
580                assert_eq!(properties.len(), 2);
581            }
582            _ => panic!("expected MergeNode"),
583        }
584    }
585
586    #[test]
587    fn test_parse_delete() {
588        let q = CypherSubsetParser::parse("DELETE n", &empty_params()).unwrap();
589        match q {
590            ParsedQuery::Delete { alias } => {
591                assert_eq!(alias, "n");
592            }
593            _ => panic!("expected Delete"),
594        }
595    }
596
597    #[test]
598    fn test_parse_set() {
599        let q = CypherSubsetParser::parse("SET n.name = $name", &empty_params()).unwrap();
600        match q {
601            ParsedQuery::Set {
602                alias,
603                prop,
604                param_name,
605            } => {
606                assert_eq!(alias, "n");
607                assert_eq!(prop, "name");
608                assert_eq!(param_name, "name");
609            }
610            _ => panic!("expected Set"),
611        }
612    }
613
614    #[test]
615    fn test_parse_set_reject_literal() {
616        let result = CypherSubsetParser::parse("SET n.name = \"Alice\"", &empty_params());
617        assert!(result.is_err());
618        assert!(matches!(
619            result.unwrap_err(),
620            GraphError::ParameterizationError(_)
621        ));
622    }
623
624    #[test]
625    fn test_parse_create_reject_literal() {
626        let result =
627            CypherSubsetParser::parse("CREATE (n:Person {name: \"Alice\"})", &empty_params());
628        assert!(result.is_err());
629        assert!(matches!(
630            result.unwrap_err(),
631            GraphError::ParameterizationError(_)
632        ));
633    }
634
635    #[test]
636    fn test_parse_reject_delete() {
637        let result = CypherSubsetParser::parse("MATCH (n) DELETE n", &empty_params());
638        assert!(result.is_err());
639    }
640
641    #[test]
642    fn test_parse_reject_sql() {
643        let result = CypherSubsetParser::parse("SELECT * FROM users", &empty_params());
644        assert!(result.is_err());
645        assert!(matches!(
646            result.unwrap_err(),
647            GraphError::SqlNotSupported(_)
648        ));
649    }
650
651    #[test]
652    fn test_parse_reject_no_match() {
653        let result = CypherSubsetParser::parse("RETURN n", &empty_params());
654        assert!(result.is_err());
655    }
656}