kmp_adapter_embedded/adapter/
graph_read.rs1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
3use kmp_domain::{
4 ContextPathNeighborhood, GraphNeighborhoodReader, MemoryAboutIndexReader, NeighborhoodRequest,
5 NodeNeighborhood, NodeProjection, NodeRelationProjection, NodeRelationshipReader,
6 NodeRelationships, PortError,
7};
8
9use super::engine::{Key, ReadTx, Table};
10use super::projection_write::MEMORY_ANCHOR_KIND;
11use super::serdes::{NodeRecord, decode, decode_explanation};
12use super::store::EmbeddedKernelStore;
13
14fn load_node(tx: &dyn ReadTx, node_id: &str) -> Result<Option<NodeProjection>, PortError> {
15 match tx.get(Table::Nodes, Key::Str(node_id))? {
16 Some(raw) => Ok(Some(
17 decode::<NodeRecord>("graph node", &raw)?.into_projection()?,
18 )),
19 None => Ok(None),
20 }
21}
22
23fn outgoing_rows(tx: &dyn ReadTx, source: &str) -> Result<Vec<NodeRelationProjection>, PortError> {
24 tx.scan_str3_by_first(Table::Relations, source)?
25 .into_iter()
26 .map(|((source_node_id, target_node_id, relation_type), raw)| {
27 Ok(NodeRelationProjection {
28 source_node_id,
29 target_node_id,
30 relation_type,
31 explanation: decode_explanation(&raw)?,
32 })
33 })
34 .collect()
35}
36
37fn outgoing_targets(tx: &dyn ReadTx, source: &str) -> Result<Vec<String>, PortError> {
38 Ok(tx
39 .scan_str3_by_first(Table::Relations, source)?
40 .into_iter()
41 .map(|((_, target, _), _)| target)
42 .collect())
43}
44
45fn reachable_outward(
46 tx: &dyn ReadTx,
47 request: &NeighborhoodRequest,
48) -> Result<BTreeSet<String>, PortError> {
49 let root_node_id = request.root_node_id();
50 let mut visited = BTreeSet::from([root_node_id.to_string()]);
51 let mut reachable = BTreeSet::new();
52 let mut frontier = VecDeque::from([(root_node_id.to_string(), 0u32)]);
53
54 while let Some((node_id, hops)) = frontier.pop_front() {
55 if hops == request.depth() {
56 continue;
57 }
58 for target in outgoing_targets(tx, &node_id)? {
59 if !request.admits(&target) {
64 continue;
65 }
66 if visited.insert(target.clone()) {
67 reachable.insert(target.clone());
68 frontier.push_back((target, hops + 1));
69 }
70 }
71 }
72
73 reachable.remove(root_node_id);
74 Ok(reachable)
75}
76
77fn relations_among(
78 tx: &dyn ReadTx,
79 selected: &BTreeSet<String>,
80) -> Result<Vec<NodeRelationProjection>, PortError> {
81 let mut rows = Vec::new();
82 for source in selected {
83 for relation in outgoing_rows(tx, source)? {
84 if selected.contains(&relation.target_node_id) {
85 rows.push(relation);
86 }
87 }
88 }
89 Ok(rows)
90}
91
92fn selected_projections(
93 tx: &dyn ReadTx,
94 selected: &BTreeSet<String>,
95 root_node_id: &str,
96) -> Result<Vec<NodeProjection>, PortError> {
97 let mut projections = Vec::new();
98 for node_id in selected {
99 if node_id == root_node_id {
100 continue;
101 }
102 if let Some(projection) = load_node(tx, node_id)? {
103 projections.push(projection);
104 }
105 }
106 Ok(projections)
107}
108
109fn shortest_outward_path(
110 tx: &dyn ReadTx,
111 root_node_id: &str,
112 target_node_id: &str,
113) -> Result<Option<Vec<String>>, PortError> {
114 let mut predecessors = BTreeMap::<String, String>::new();
115 let mut visited = BTreeSet::from([root_node_id.to_string()]);
116 let mut frontier = VecDeque::from([root_node_id.to_string()]);
117
118 while let Some(node_id) = frontier.pop_front() {
119 for target in outgoing_targets(tx, &node_id)? {
120 if !visited.insert(target.clone()) {
121 continue;
122 }
123 predecessors.insert(target.clone(), node_id.clone());
124 if target == target_node_id {
125 let mut path = vec![target.clone()];
126 let mut current = target.as_str();
127 while let Some(previous) = predecessors.get(current) {
128 path.push(previous.clone());
129 current = previous;
130 }
131 path.reverse();
132 return Ok(Some(path));
133 }
134 frontier.push_back(target);
135 }
136 }
137
138 Ok(None)
139}
140
141impl GraphNeighborhoodReader for EmbeddedKernelStore {
142 async fn load_neighborhood(
143 &self,
144 root_node_id: &str,
145 depth: u32,
146 ) -> Result<Option<NodeNeighborhood>, PortError> {
147 self.load_scoped_neighborhood(&NeighborhoodRequest::new(root_node_id, depth))
148 .await
149 }
150
151 async fn load_scoped_neighborhood(
152 &self,
153 request: &NeighborhoodRequest,
154 ) -> Result<Option<NodeNeighborhood>, PortError> {
155 let request = request.clone();
156 let root_node_id = request.root_node_id().to_string();
157 self.run(move |store| {
158 let tx = store.begin_read()?;
159 let tx = tx.as_ref();
160
161 let Some(root) = load_node(tx, &root_node_id)? else {
162 return Ok(None);
163 };
164
165 let reachable = reachable_outward(tx, &request)?;
166 let relation_rows = if reachable.is_empty() {
169 Vec::new()
170 } else {
171 let mut selected = reachable.clone();
172 selected.insert(root_node_id.clone());
173 relations_among(tx, &selected)?
174 };
175
176 Ok(Some(NodeNeighborhood {
177 neighbors: selected_projections(tx, &reachable, &root_node_id)?,
178 relations: relation_rows,
179 root,
180 }))
181 })
182 .await
183 }
184
185 async fn load_context_path(
186 &self,
187 root_node_id: &str,
188 target_node_id: &str,
189 subtree_depth: u32,
190 ) -> Result<Option<ContextPathNeighborhood>, PortError> {
191 let root_node_id = root_node_id.to_string();
192 let target_node_id = target_node_id.to_string();
193 self.run(move |store| {
194 let tx = store.begin_read()?;
195 let tx = tx.as_ref();
196
197 let Some(root) = load_node(tx, &root_node_id)? else {
198 return Ok(None);
199 };
200 if load_node(tx, &target_node_id)?.is_none() {
201 return Ok(None);
202 }
203 let Some(path_node_ids) = shortest_outward_path(tx, &root_node_id, &target_node_id)?
204 else {
205 return Ok(None);
206 };
207
208 let mut selected = path_node_ids.iter().cloned().collect::<BTreeSet<_>>();
209 selected.insert(target_node_id.clone());
210 selected.extend(reachable_outward(
211 tx,
212 &NeighborhoodRequest::new(&target_node_id, subtree_depth),
213 )?);
214
215 Ok(Some(ContextPathNeighborhood {
216 neighbors: selected_projections(tx, &selected, &root_node_id)?,
217 relations: relations_among(tx, &selected)?,
218 path_node_ids,
219 root,
220 }))
221 })
222 .await
223 }
224}
225
226impl NodeRelationshipReader for EmbeddedKernelStore {
227 async fn load_node_relationships(
228 &self,
229 node_id: &str,
230 ) -> Result<Option<NodeRelationships>, PortError> {
231 let node_id = node_id.to_string();
232 self.run(move |store| {
233 let tx = store.begin_read()?;
234 let tx = tx.as_ref();
235 if load_node(tx, &node_id)?.is_none() {
236 return Ok(None);
237 }
238
239 let mut incoming = Vec::new();
240 for ((target, source, relation_type), _) in
241 tx.scan_str3_by_first(Table::RelationsByTarget, &node_id)?
242 {
243 let Some(raw) = tx.get(
244 Table::Relations,
245 Key::Str3(&source, &target, &relation_type),
246 )?
247 else {
248 return Err(PortError::InvalidState(format!(
249 "embedded store adjacency index points at missing relation \
250 `{source}` -> `{target}` ({relation_type})"
251 )));
252 };
253 incoming.push(NodeRelationProjection {
254 explanation: decode_explanation(&raw)?,
255 source_node_id: source,
256 target_node_id: target,
257 relation_type,
258 });
259 }
260
261 Ok(Some(NodeRelationships {
262 incoming,
263 outgoing: outgoing_rows(tx, &node_id)?,
264 }))
265 })
266 .await
267 }
268}
269
270impl MemoryAboutIndexReader for EmbeddedKernelStore {
271 async fn list_memory_abouts(&self) -> Result<Vec<String>, PortError> {
272 self.run(|store| {
273 let tx = store.begin_read()?;
274 Ok(tx
275 .scan_str(Table::Anchors)?
276 .into_iter()
277 .map(|(anchor, _)| anchor)
278 .collect())
279 })
280 .await
281 }
282
283 async fn list_memory_abouts_by_dimensions(
284 &self,
285 dimension_ids: &[String],
286 ) -> Result<Vec<String>, PortError> {
287 let dimension_ids = dimension_ids.to_vec();
288 self.run(move |store| {
289 let tx = store.begin_read()?;
290 let tx = tx.as_ref();
291
292 let mut abouts = BTreeSet::new();
293 for (anchor, _) in tx.scan_str(Table::Anchors)? {
294 let is_anchor = load_node(tx, &anchor)?
295 .is_some_and(|node| node.node_kind == MEMORY_ANCHOR_KIND);
296 if !is_anchor {
297 continue;
298 }
299 for relation in outgoing_rows(tx, &anchor)? {
300 if relation.relation_type != "has_dimension" {
301 continue;
302 }
303 let matches =
304 load_node(tx, &relation.target_node_id)?.is_some_and(|dimension| {
305 dimension.node_kind == "memory_dimension"
306 && dimension_ids.iter().any(|dimension_id| {
307 dimension.node_id == *dimension_id
308 || dimension
309 .node_id
310 .ends_with(&format!(":dimension:{dimension_id}"))
311 || dimension.properties.get("dimension_kind")
317 == Some(dimension_id)
318 })
319 });
320 if matches {
321 abouts.insert(anchor.clone());
322 break;
323 }
324 }
325 }
326 Ok(abouts.into_iter().collect())
327 })
328 .await
329 }
330}