1use std::collections::{BTreeMap, BTreeSet};
17
18use uqa_core::{EdgeId, Value, VertexId};
19
20use crate::store::{GraphStore, GraphStoreError, GraphStoreResult};
21
22#[derive(Debug, Clone, Default)]
23pub struct LabelIndex {
24 label_to_edges: BTreeMap<String, Vec<EdgeId>>,
25 label_to_vertices: BTreeMap<String, BTreeSet<VertexId>>,
26}
27
28impl LabelIndex {
29 pub fn build<G: GraphStore>(store: &G, graph: &str) -> GraphStoreResult<Self> {
30 let mut idx = Self::default();
31 for vid in store.vertex_ids_in_graph(graph)? {
32 for eid in store.out_edge_ids(vid, graph)? {
33 let edge = store.get_edge(eid).ok_or_else(|| {
34 GraphStoreError::CorruptGraph(format!("missing indexed edge {eid}"))
35 })?;
36 idx.label_to_edges
37 .entry(edge.label.clone())
38 .or_default()
39 .push(eid);
40 let vset = idx.label_to_vertices.entry(edge.label.clone()).or_default();
41 vset.insert(edge.source_id);
42 vset.insert(edge.target_id);
43 }
44 }
45 for edges in idx.label_to_edges.values_mut() {
46 edges.sort_unstable();
47 edges.dedup();
48 }
49 Ok(idx)
50 }
51
52 pub fn edges_by_label(&self, label: &str) -> &[EdgeId] {
53 self.label_to_edges.get(label).map_or(&[], Vec::as_slice)
54 }
55
56 pub fn vertices_by_label(&self, label: &str) -> Option<&BTreeSet<VertexId>> {
57 self.label_to_vertices.get(label)
58 }
59
60 pub fn labels(&self) -> Vec<String> {
61 self.label_to_edges.keys().cloned().collect()
62 }
63
64 pub fn label_count(&self, label: &str) -> usize {
65 self.label_to_edges.get(label).map_or(0, Vec::len)
66 }
67}
68
69#[derive(Debug, Clone, Default)]
76pub struct VertexPropertyIndex {
77 property_values: BTreeMap<String, BTreeMap<Value, BTreeSet<VertexId>>>,
78}
79
80impl VertexPropertyIndex {
81 pub fn build<G: GraphStore>(
82 store: &G,
83 graph: &str,
84 properties: &[&str],
85 ) -> GraphStoreResult<Self> {
86 let property_names: BTreeSet<String> = properties
87 .iter()
88 .map(|property| (*property).to_owned())
89 .collect();
90 let mut property_values: BTreeMap<String, BTreeMap<Value, BTreeSet<VertexId>>> =
91 property_names
92 .iter()
93 .map(|property| (property.clone(), BTreeMap::new()))
94 .collect();
95
96 for vertex_id in store.vertex_ids_in_graph(graph)? {
97 let vertex = store.get_vertex(vertex_id).ok_or_else(|| {
98 GraphStoreError::CorruptGraph(format!(
99 "graph {graph:?} references missing indexed vertex {vertex_id}"
100 ))
101 })?;
102 for property in &property_names {
103 let Some(value) = vertex.properties.get(property) else {
104 continue;
105 };
106 property_values
107 .get_mut(property)
108 .expect("requested property was initialized")
109 .entry(value.clone())
110 .or_default()
111 .insert(vertex_id);
112 }
113 }
114
115 Ok(Self { property_values })
116 }
117
118 pub fn has_property(&self, property: &str) -> bool {
119 self.property_values.contains_key(property)
120 }
121
122 pub fn lookup_eq(&self, property: &str, value: &Value) -> Option<&BTreeSet<VertexId>> {
123 self.property_values.get(property)?.get(value)
124 }
125
126 pub fn property_names(&self) -> impl Iterator<Item = &str> {
127 self.property_values.keys().map(String::as_str)
128 }
129}
130
131#[derive(Debug, Clone, Default)]
136pub struct PathIndex {
137 path_pairs: BTreeMap<String, BTreeSet<(VertexId, VertexId)>>,
138}
139
140impl PathIndex {
141 pub fn build<G: GraphStore>(
142 store: &G,
143 graph: &str,
144 label_sequences: &[Vec<String>],
145 ) -> GraphStoreResult<Self> {
146 let mut idx = Self::default();
147 for seq in label_sequences {
148 let key = seq.join("/");
149 let mut pairs: BTreeSet<(VertexId, VertexId)> = BTreeSet::new();
150 for start in store.vertex_ids_in_graph(graph)? {
151 let ends = follow_path(store, graph, start, seq)?;
152 for end in ends {
153 pairs.insert((start, end));
154 }
155 }
156 idx.path_pairs.insert(key, pairs);
157 }
158 Ok(idx)
159 }
160
161 pub fn lookup(&self, label_sequence: &[String]) -> Option<&BTreeSet<(VertexId, VertexId)>> {
162 let key = label_sequence.join("/");
163 self.path_pairs.get(&key)
164 }
165
166 pub fn has_path(&self, label_sequence: &[String]) -> bool {
167 let key = label_sequence.join("/");
168 self.path_pairs.contains_key(&key)
169 }
170
171 pub fn indexed_paths(&self) -> Vec<String> {
172 self.path_pairs.keys().cloned().collect()
173 }
174}
175
176fn follow_path<G: GraphStore>(
177 store: &G,
178 graph: &str,
179 start: VertexId,
180 labels: &[String],
181) -> GraphStoreResult<BTreeSet<VertexId>> {
182 let mut current: BTreeSet<VertexId> = BTreeSet::from([start]);
183 for label in labels {
184 let mut next_set: BTreeSet<VertexId> = BTreeSet::new();
185 for vid in ¤t {
186 for eid in store.out_edge_ids(*vid, graph)? {
187 let edge = store.get_edge(eid).ok_or_else(|| {
188 GraphStoreError::CorruptGraph(format!("missing path-index edge {eid}"))
189 })?;
190 if &edge.label == label {
191 next_set.insert(edge.target_id);
192 }
193 }
194 }
195 current = next_set;
196 if current.is_empty() {
197 break;
198 }
199 }
200 Ok(current)
201}
202
203#[cfg(test)]
204mod tests {
205 use uqa_core::{Value, Vertex};
206
207 use super::VertexPropertyIndex;
208 use crate::{GraphStore, MemoryGraphStore};
209
210 #[test]
211 fn vertex_property_index_tracks_selected_values_and_missing_fields() {
212 let mut store = MemoryGraphStore::new();
213 store.create_graph("g");
214 let mut first = Vertex::new(1, "node");
215 first.properties.insert("val".into(), Value::Int(7));
216 first.properties.insert("nullable".into(), Value::Null);
217 store.add_vertex(first, "g").unwrap();
218 let mut second = Vertex::new(2, "node");
219 second.properties.insert("val".into(), Value::Float(7.0));
220 store.add_vertex(second, "g").unwrap();
221
222 let index =
223 VertexPropertyIndex::build(&store, "g", &["val", "nullable", "missing"]).unwrap();
224
225 assert!(index.has_property("val"));
226 assert!(index.has_property("missing"));
227 assert_eq!(
228 index.lookup_eq("val", &Value::Int(7)).unwrap(),
229 &std::collections::BTreeSet::from([1, 2])
230 );
231 assert_eq!(
232 index.lookup_eq("nullable", &Value::Null).unwrap(),
233 &std::collections::BTreeSet::from([1])
234 );
235 assert!(index.lookup_eq("missing", &Value::Null).is_none());
236 assert!(index.lookup_eq("not-indexed", &Value::Int(7)).is_none());
237 }
238
239 #[test]
240 fn vertex_property_index_rejects_unknown_graph() {
241 let store = MemoryGraphStore::new();
242 let error = VertexPropertyIndex::build(&store, "missing", &["val"]).unwrap_err();
243 assert!(error.to_string().contains("missing"));
244 }
245}