Skip to main content

sqlitegraph_cli/
query.rs

1//! Simple Cypher-like query parser
2//!
3//! Supports basic patterns:
4//! - MATCH (n:Label) RETURN n.name
5//! - MATCH (n:Label {key: "value"}) RETURN n
6//! - MATCH (a)-[:REL]->(b) RETURN a, b
7
8use serde_json::Value;
9use sqlitegraph::backend::GraphBackend;
10use sqlitegraph::graph::GraphEntity;
11use sqlitegraph::snapshot::SnapshotId;
12
13#[derive(Debug)]
14pub enum Query {
15    Match {
16        pattern: Pattern,
17        returns: Vec<String>,
18    },
19}
20
21#[derive(Debug)]
22pub enum Pattern {
23    Node(NodePattern),
24    Edge(NodePattern, String, NodePattern), // from, rel_type, to
25}
26
27#[derive(Debug)]
28pub struct NodePattern {
29    pub var: String,
30    pub label: Option<String>,
31    pub props: Vec<(String, String)>,
32}
33
34impl NodePattern {
35    fn matches(&self, node: &GraphEntity) -> bool {
36        // Check label (kind)
37        if let Some(ref label) = self.label {
38            if node.kind != *label {
39                return false;
40            }
41        }
42        // Check properties
43        for (key, value) in &self.props {
44            match node.data.get(key) {
45                Some(v) if v.as_str() == Some(value) => continue,
46                _ => return false,
47            }
48        }
49        true
50    }
51}
52
53/// Parse a simple Cypher-like query
54pub fn parse(query: &str) -> anyhow::Result<Query> {
55    let query = query.trim();
56
57    if query.to_uppercase().starts_with("MATCH ") {
58        parse_match(query)
59    } else {
60        anyhow::bail!("Only MATCH queries are supported")
61    }
62}
63
64fn parse_match(query: &str) -> anyhow::Result<Query> {
65    // Remove MATCH keyword
66    let rest = query[6..].trim();
67
68    // Find RETURN clause
69    let return_pos = rest.to_uppercase().find(" RETURN ");
70    let (pattern_str, returns) = if let Some(pos) = return_pos {
71        let pattern_part = &rest[..pos];
72        let return_part = &rest[pos + 8..];
73        let returns: Vec<String> = return_part.split(',').map(|s| s.trim().to_string()).collect();
74        (pattern_part.trim(), returns)
75    } else {
76        (rest, vec!["*".to_string()])
77    };
78
79    let pattern = parse_pattern(pattern_str)?;
80
81    Ok(Query::Match { pattern, returns })
82}
83
84fn parse_pattern(s: &str) -> anyhow::Result<Pattern> {
85    let s = s.trim();
86
87    // Check for edge pattern: (a)-[:REL]->(b)
88    if s.contains("-") && s.contains("->") {
89        parse_edge_pattern(s)
90    } else if s.starts_with('(') && s.ends_with(')') {
91        // Node pattern
92        let inner = &s[1..s.len() - 1];
93        let node = parse_node(inner)?;
94        Ok(Pattern::Node(node))
95    } else {
96        anyhow::bail!("Invalid pattern syntax")
97    }
98}
99
100fn parse_edge_pattern(_s: &str) -> anyhow::Result<Pattern> {
101    anyhow::bail!("Edge patterns not yet implemented in parser")
102}
103
104fn parse_node(s: &str) -> anyhow::Result<NodePattern> {
105    let s = s.trim();
106
107    // Parse variable name
108    let var_end = s.find(|c: char| c == ':' || c == '{' || c.is_whitespace());
109    let var = if let Some(end) = var_end {
110        s[..end].trim().to_string()
111    } else {
112        s.to_string()
113    };
114
115    // Parse label if present
116    let label = if let Some(colon_pos) = s.find(':') {
117        let after_colon = &s[colon_pos + 1..];
118        let label_end = after_colon.find(|c: char| c == '{' || c.is_whitespace());
119        if let Some(end) = label_end {
120            Some(after_colon[..end].trim().to_string())
121        } else {
122            Some(after_colon.trim().to_string())
123        }
124    } else {
125        None
126    };
127
128    // Parse properties if present
129    let mut props = Vec::new();
130    if let Some(open_brace) = s.find('{') {
131        if let Some(close_brace) = s.rfind('}') {
132            let props_str = &s[open_brace + 1..close_brace];
133            // Simple key: "value" parsing
134            for part in props_str.split(',') {
135                let part = part.trim();
136                if let Some(colon_pos) = part.find(':') {
137                    let key = part[..colon_pos].trim().to_string();
138                    let value = part[colon_pos + 1..]
139                        .trim()
140                        .trim_matches('"')
141                        .trim_matches('\'')
142                        .to_string();
143                    props.push((key, value));
144                }
145            }
146        }
147    }
148
149    Ok(NodePattern { var, label, props })
150}
151
152/// Execute a query against the backend
153pub fn execute(backend: &dyn GraphBackend, query: &Query) -> anyhow::Result<Value> {
154    match query {
155        Query::Match { pattern, returns } => execute_match(backend, pattern, returns),
156    }
157}
158
159fn execute_match(
160    backend: &dyn GraphBackend,
161    pattern: &Pattern,
162    returns: &[String],
163) -> anyhow::Result<Value> {
164    match pattern {
165        Pattern::Node(node_pat) => {
166            let snapshot = SnapshotId::current();
167            let node_ids = backend.entity_ids()?;
168
169            let mut results = Vec::new();
170            for id in node_ids.iter().take(1000) {
171                // Limit to 1000 results
172                if let Ok(node) = backend.get_node(snapshot, *id) {
173                    if node_pat.matches(&node) {
174                        let mut obj = serde_json::Map::new();
175
176                        for ret in returns {
177                            if ret == "*" || *ret == node_pat.var {
178                                obj.insert(
179                                    node_pat.var.clone(),
180                                    serde_json::json!({
181                                        "id": node.id,
182                                        "kind": node.kind,
183                                        "name": node.name,
184                                        "data": node.data,
185                                    }),
186                                );
187                            } else if ret.starts_with(&format!("{}.", node_pat.var)) {
188                                let field = &ret[node_pat.var.len() + 1..];
189                                let value = match field {
190                                    "id" => serde_json::json!(node.id),
191                                    "kind" => serde_json::json!(node.kind),
192                                    "name" => serde_json::json!(node.name),
193                                    _ => node.data.get(field).cloned().unwrap_or(Value::Null),
194                                };
195                                obj.insert(ret.clone(), value);
196                            }
197                        }
198
199                        if !obj.is_empty() {
200                            results.push(Value::Object(obj));
201                        }
202                    }
203                }
204            }
205
206            Ok(serde_json::json!({
207                "results": results,
208                "count": results.len(),
209            }))
210        }
211        Pattern::Edge(_, _, _) => {
212            anyhow::bail!("Edge pattern queries not yet implemented")
213        }
214    }
215}