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
74            .split(',')
75            .map(|s| s.trim().to_string())
76            .collect();
77        (pattern_part.trim(), returns)
78    } else {
79        (rest, vec!["*".to_string()])
80    };
81
82    let pattern = parse_pattern(pattern_str)?;
83
84    Ok(Query::Match { pattern, returns })
85}
86
87fn parse_pattern(s: &str) -> anyhow::Result<Pattern> {
88    let s = s.trim();
89
90    // Check for edge pattern: (a)-[:REL]->(b)
91    if s.contains("-") && s.contains("->") {
92        parse_edge_pattern(s)
93    } else if s.starts_with('(') && s.ends_with(')') {
94        // Node pattern
95        let inner = &s[1..s.len() - 1];
96        let node = parse_node(inner)?;
97        Ok(Pattern::Node(node))
98    } else {
99        anyhow::bail!("Invalid pattern syntax")
100    }
101}
102
103fn parse_edge_pattern(_s: &str) -> anyhow::Result<Pattern> {
104    anyhow::bail!("Edge patterns not yet implemented in parser")
105}
106
107fn parse_node(s: &str) -> anyhow::Result<NodePattern> {
108    let s = s.trim();
109
110    // Parse variable name
111    let var_end = s.find(|c: char| c == ':' || c == '{' || c.is_whitespace());
112    let var = if let Some(end) = var_end {
113        s[..end].trim().to_string()
114    } else {
115        s.to_string()
116    };
117
118    // Parse label if present
119    let label = if let Some(colon_pos) = s.find(':') {
120        let after_colon = &s[colon_pos + 1..];
121        let label_end = after_colon.find(|c: char| c == '{' || c.is_whitespace());
122        if let Some(end) = label_end {
123            Some(after_colon[..end].trim().to_string())
124        } else {
125            Some(after_colon.trim().to_string())
126        }
127    } else {
128        None
129    };
130
131    // Parse properties if present
132    let mut props = Vec::new();
133    if let Some(open_brace) = s.find('{') {
134        if let Some(close_brace) = s.rfind('}') {
135            let props_str = &s[open_brace + 1..close_brace];
136            // Simple key: "value" parsing
137            for part in props_str.split(',') {
138                let part = part.trim();
139                if let Some(colon_pos) = part.find(':') {
140                    let key = part[..colon_pos].trim().to_string();
141                    let value = part[colon_pos + 1..]
142                        .trim()
143                        .trim_matches('"')
144                        .trim_matches('\'')
145                        .to_string();
146                    props.push((key, value));
147                }
148            }
149        }
150    }
151
152    Ok(NodePattern { var, label, props })
153}
154
155/// Execute a query against the backend
156pub fn execute(backend: &dyn GraphBackend, query: &Query) -> anyhow::Result<Value> {
157    match query {
158        Query::Match { pattern, returns } => execute_match(backend, pattern, returns),
159    }
160}
161
162fn execute_match(
163    backend: &dyn GraphBackend,
164    pattern: &Pattern,
165    returns: &[String],
166) -> anyhow::Result<Value> {
167    match pattern {
168        Pattern::Node(node_pat) => {
169            let snapshot = SnapshotId::current();
170            let node_ids = backend.entity_ids()?;
171
172            let mut results = Vec::new();
173            for id in node_ids.iter().take(1000) {
174                // Limit to 1000 results
175                if let Ok(node) = backend.get_node(snapshot, *id) {
176                    if node_pat.matches(&node) {
177                        let mut obj = serde_json::Map::new();
178
179                        for ret in returns {
180                            if ret == "*" || *ret == node_pat.var {
181                                obj.insert(
182                                    node_pat.var.clone(),
183                                    serde_json::json!({
184                                        "id": node.id,
185                                        "kind": node.kind,
186                                        "name": node.name,
187                                        "data": node.data,
188                                    }),
189                                );
190                            } else if ret.starts_with(&format!("{}.", node_pat.var)) {
191                                let field = &ret[node_pat.var.len() + 1..];
192                                let value = match field {
193                                    "id" => serde_json::json!(node.id),
194                                    "kind" => serde_json::json!(node.kind),
195                                    "name" => serde_json::json!(node.name),
196                                    _ => node.data.get(field).cloned().unwrap_or(Value::Null),
197                                };
198                                obj.insert(ret.clone(), value);
199                            }
200                        }
201
202                        if !obj.is_empty() {
203                            results.push(Value::Object(obj));
204                        }
205                    }
206                }
207            }
208
209            Ok(serde_json::json!({
210                "results": results,
211                "count": results.len(),
212            }))
213        }
214        Pattern::Edge(_, _, _) => {
215            anyhow::bail!("Edge pattern queries not yet implemented")
216        }
217    }
218}