1use std::collections::{HashSet, VecDeque};
9
10use crate::error::Result;
11use crate::graph::{Edge, GraphStore, Node, NodeId};
12
13#[derive(Debug, Clone, Default)]
15pub struct NodeQuery {
16 pub label: Option<String>,
18 pub property_eq: Vec<(String, serde_json::Value)>,
20}
21
22impl NodeQuery {
23 pub fn label(label: impl Into<String>) -> Self {
25 Self {
26 label: Some(label.into()),
27 ..Self::default()
28 }
29 }
30
31 #[must_use]
33 pub fn property_eq(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
34 self.property_eq.push((key.into(), value));
35 self
36 }
37
38 pub async fn run(&self, store: &dyn GraphStore) -> Result<Vec<Node>> {
40 let label = self.label.as_deref().unwrap_or("");
41 let candidates: Vec<Node> = if label.is_empty() {
42 return Ok(Vec::new()); } else {
44 store.nodes_by_label(label).await?
45 };
46 Ok(candidates
47 .into_iter()
48 .filter(|n| {
49 self.property_eq
50 .iter()
51 .all(|(k, v)| n.properties.get(k) == Some(v))
52 })
53 .collect())
54 }
55}
56
57#[derive(Debug, Clone, Default)]
59pub struct EdgeQuery {
60 pub label: Option<String>,
62 pub direction: EdgeDirection,
64}
65
66#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
68pub enum EdgeDirection {
69 #[default]
71 Out,
72 In,
74 Either,
76}
77
78impl EdgeQuery {
79 pub fn outbound(label: impl Into<String>) -> Self {
81 Self {
82 label: Some(label.into()),
83 direction: EdgeDirection::Out,
84 }
85 }
86
87 #[must_use]
89 pub fn direction(mut self, d: EdgeDirection) -> Self {
90 self.direction = d;
91 self
92 }
93
94 pub async fn run(&self, store: &dyn GraphStore, node: &NodeId) -> Result<Vec<Edge>> {
96 let label = self.label.as_deref();
97 match self.direction {
98 EdgeDirection::Out => store.edges_from(node, label).await,
99 EdgeDirection::In => store.edges_to(node, label).await,
100 EdgeDirection::Either => {
101 let mut out = store.edges_from(node, label).await?;
102 out.extend(store.edges_to(node, label).await?);
103 Ok(out)
104 }
105 }
106 }
107}
108
109pub async fn traverse(
112 store: &dyn GraphStore,
113 start: &NodeId,
114 edge_label: Option<&str>,
115 max_depth: usize,
116) -> Result<Vec<Node>> {
117 let mut seen: HashSet<NodeId> = HashSet::new();
118 let mut queue: VecDeque<(NodeId, usize)> = VecDeque::new();
119 queue.push_back((start.clone(), 0));
120 seen.insert(start.clone());
121 let mut out = Vec::new();
122
123 while let Some((current, depth)) = queue.pop_front() {
124 if let Some(node) = store.get_node(¤t).await? {
125 out.push(node);
126 }
127 if depth >= max_depth {
128 continue;
129 }
130 let edges = store.edges_from(¤t, edge_label).await?;
131 for e in edges {
132 if seen.insert(e.to.clone()) {
133 queue.push_back((e.to.clone(), depth + 1));
134 }
135 }
136 }
137 Ok(out)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::graph::{Edge, InMemoryGraph, Node};
144 use serde_json::json;
145
146 async fn fixture() -> InMemoryGraph {
147 let g = InMemoryGraph::new();
148 g.upsert_node(
149 Node::new("pet:1", "pet")
150 .with_property("name", json!("Rex"))
151 .with_property("status", json!("available")),
152 )
153 .await
154 .unwrap();
155 g.upsert_node(
156 Node::new("pet:2", "pet")
157 .with_property("name", json!("Buddy"))
158 .with_property("status", json!("sold")),
159 )
160 .await
161 .unwrap();
162 g.upsert_node(Node::new("owner:1", "owner")).await.unwrap();
163 g.upsert_node(Node::new("owner:2", "owner")).await.unwrap();
164 g.add_edge(Edge::new("owner:1", "pet:1", "owns"))
165 .await
166 .unwrap();
167 g.add_edge(Edge::new("owner:2", "pet:2", "owns"))
168 .await
169 .unwrap();
170 g.add_edge(Edge::new("owner:1", "owner:2", "knows"))
171 .await
172 .unwrap();
173 g
174 }
175
176 #[tokio::test]
177 async fn node_query_filters_by_property() {
178 let g = fixture().await;
179 let q = NodeQuery::label("pet").property_eq("status", json!("available"));
180 let results = q.run(&g).await.unwrap();
181 assert_eq!(results.len(), 1);
182 assert_eq!(results[0].id, "pet:1");
183 }
184
185 #[tokio::test]
186 async fn edge_query_directions() {
187 let g = fixture().await;
188 let owns_out = EdgeQuery::outbound("owns")
189 .run(&g, &"owner:1".into())
190 .await
191 .unwrap();
192 assert_eq!(owns_out.len(), 1);
193
194 let either = EdgeQuery {
195 label: None,
196 direction: EdgeDirection::Either,
197 }
198 .run(&g, &"owner:1".into())
199 .await
200 .unwrap();
201 assert_eq!(either.len(), 2);
203 }
204
205 #[tokio::test]
206 async fn traverse_walks_depth() {
207 let g = fixture().await;
208 let nodes = traverse(&g, &"owner:1".into(), None, 2).await.unwrap();
209 let ids: Vec<_> = nodes.iter().map(|n| n.id.as_str()).collect();
210 assert!(ids.contains(&"owner:1"));
211 assert!(ids.contains(&"pet:1"));
212 assert!(ids.contains(&"owner:2"));
213 assert!(ids.contains(&"pet:2"));
215 }
216}