kmp_adapter_embedded/adapter/
graph_read.rs1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
3use kmp_domain::{
4 ContextPathNeighborhood, GraphNeighborhoodReader, NeighborhoodRequest, NodeNeighborhood,
5 NodeProjection, NodeRelationProjection, NodeRelationshipReader, NodeRelationships, PortError,
6};
7
8use super::engine::{Key, ReadTx, Table};
9use super::outward_neighborhood::OutwardNeighborhoodRead;
10use super::serdes::{NodeRecord, decode, decode_explanation};
11use super::store::EmbeddedKernelStore;
12#[path = "node_admission_header.rs"]
13mod node_admission_header;
14
15fn load_node(tx: &dyn ReadTx, node_id: &str) -> Result<Option<NodeProjection>, PortError> {
16 match tx.get(Table::Nodes, Key::Str(node_id))? {
17 Some(raw) => Ok(Some(
18 decode::<NodeRecord>("graph node", &raw)?.into_projection()?,
19 )),
20 None => Ok(None),
21 }
22}
23
24fn outgoing_rows(tx: &dyn ReadTx, source: &str) -> Result<Vec<NodeRelationProjection>, PortError> {
25 tx.scan_str3_by_first(Table::Relations, source)?
26 .into_iter()
27 .map(|((source_node_id, target_node_id, relation_type), raw)| {
28 Ok(NodeRelationProjection {
29 source_node_id,
30 target_node_id,
31 relation_type,
32 explanation: decode_explanation(&raw)?,
33 })
34 })
35 .collect()
36}
37
38fn outgoing_targets(tx: &dyn ReadTx, source: &str) -> Result<Vec<String>, PortError> {
39 Ok(tx
40 .scan_str3_by_first(Table::Relations, source)?
41 .into_iter()
42 .map(|((_, target, _), _)| target)
43 .collect())
44}
45
46fn selected_projections(
47 tx: &dyn ReadTx,
48 selected: &BTreeSet<String>,
49 root_node_id: &str,
50) -> Result<Vec<NodeProjection>, PortError> {
51 let mut projections = Vec::new();
52 for node_id in selected {
53 if node_id == root_node_id {
54 continue;
55 }
56 if let Some(projection) = load_node(tx, node_id)? {
57 projections.push(projection);
58 }
59 }
60 Ok(projections)
61}
62
63fn shortest_outward_path(
64 tx: &dyn ReadTx,
65 root_node_id: &str,
66 target_node_id: &str,
67) -> Result<Option<Vec<String>>, PortError> {
68 let mut predecessors = BTreeMap::<String, String>::new();
69 let mut visited = BTreeSet::from([root_node_id.to_string()]);
70 let mut frontier = VecDeque::from([root_node_id.to_string()]);
71
72 while let Some(node_id) = frontier.pop_front() {
73 for target in outgoing_targets(tx, &node_id)? {
74 if !visited.insert(target.clone()) {
75 continue;
76 }
77 predecessors.insert(target.clone(), node_id.clone());
78 if target == target_node_id {
79 let mut path = vec![target.clone()];
80 let mut current = target.as_str();
81 while let Some(previous) = predecessors.get(current) {
82 path.push(previous.clone());
83 current = previous;
84 }
85 path.reverse();
86 return Ok(Some(path));
87 }
88 frontier.push_back(target);
89 }
90 }
91
92 Ok(None)
93}
94
95impl EmbeddedKernelStore {
96 async fn read_catalogue_neighborhood(
97 &self,
98 request: &NeighborhoodRequest,
99 headers: bool,
100 ) -> Result<Option<NodeNeighborhood>, PortError> {
101 let request = request.clone();
102 self.run(move |store| {
103 let tx = store.begin_read()?;
104 let tx = tx.as_ref();
105 let read = |id: &str| {
106 if headers {
107 node_admission_header::read(tx, id)
108 } else {
109 load_node(tx, id)
110 }
111 };
112 let Some(root) = read(request.root_node_id())? else {
113 return Ok(None);
114 };
115 let (reachable, relations) = OutwardNeighborhoodRead::new(tx, &request).catalogue()?;
116 let mut neighbors = Vec::with_capacity(reachable.len());
117 for id in reachable {
118 if let Some(node) = read(&id)? {
119 neighbors.push(node);
120 }
121 }
122 Ok(Some(NodeNeighborhood {
123 root,
124 neighbors,
125 relations,
126 }))
127 })
128 .await
129 }
130}
131
132impl GraphNeighborhoodReader for EmbeddedKernelStore {
133 async fn graph_read_revision(
134 &self,
135 ) -> Result<Option<kmp_domain::GraphReadRevision>, PortError> {
136 Ok(self.read_revision())
137 }
138
139 async fn load_nodes_batch(
140 &self,
141 node_ids: Vec<String>,
142 ) -> Result<Vec<Option<NodeProjection>>, PortError> {
143 self.run(move |store| {
144 let tx = store.begin_read()?;
145 node_ids
146 .iter()
147 .map(|id| load_node(tx.as_ref(), id))
148 .collect()
149 })
150 .await
151 }
152
153 async fn load_bounded_trace(
154 &self,
155 request: &kmp_domain::TraceSearchRequest,
156 ) -> Result<kmp_domain::TraceSearchResult, PortError> {
157 let request = request.clone();
158 self.run(move |store| {
159 let tx = store.begin_read()?;
160 kmp_domain::bounded_trace_search(
161 &super::trace_snapshot::TraceSnapshot(tx.as_ref()),
162 &request,
163 )
164 })
165 .await
166 }
167
168 async fn load_memory_nodes(
169 &self,
170 request: &kmp_domain::MemoryNodesRequest,
171 ) -> Result<kmp_domain::MemoryNodesResult, PortError> {
172 let request = request.clone();
173 self.run(move |store| {
174 let pinned = store.pin_snapshot()?;
175 let revision = pinned.read_revision().ok_or_else(|| {
176 PortError::Conflict(
177 "store changed while pinning the node batch; retry the original read".into(),
178 )
179 })?;
180 if request
181 .expect_snapshot
182 .as_ref()
183 .is_some_and(|expected| expected != &revision)
184 {
185 return Err(PortError::Conflict(
186 "node batch snapshot changed; discard previous batches and restart the focus"
187 .into(),
188 ));
189 }
190 let tx = pinned.begin_read()?;
191 let mut result = kmp_domain::read_memory_nodes(
192 &super::trace_snapshot::TraceSnapshot(tx.as_ref()),
193 &request,
194 )?;
195 result.snapshot = Some(revision);
196 Ok(result)
197 })
198 .await
199 }
200
201 async fn load_evidence_paths(
202 &self,
203 request: &kmp_domain::EvidencePathRequest,
204 ) -> Result<kmp_domain::EvidencePathResult, PortError> {
205 let request = request.clone();
206 self.run(move |store| {
207 let tx = store.begin_read()?;
208 kmp_domain::search_evidence_paths(
209 &super::trace_snapshot::TraceSnapshot(tx.as_ref()),
210 &request,
211 )
212 })
213 .await
214 }
215
216 async fn load_neighborhood(
217 &self,
218 root_node_id: &str,
219 depth: u32,
220 ) -> Result<Option<NodeNeighborhood>, PortError> {
221 self.load_scoped_neighborhood(&NeighborhoodRequest::new(root_node_id, depth))
222 .await
223 }
224
225 async fn load_scoped_neighborhood(
226 &self,
227 request: &NeighborhoodRequest,
228 ) -> Result<Option<NodeNeighborhood>, PortError> {
229 self.read_catalogue_neighborhood(request, false).await
230 }
231
232 async fn load_neighborhood_headers(
233 &self,
234 request: &NeighborhoodRequest,
235 ) -> Result<Option<NodeNeighborhood>, PortError> {
236 self.read_catalogue_neighborhood(request, true).await
237 }
238
239 async fn load_context_path(
240 &self,
241 root_node_id: &str,
242 target_node_id: &str,
243 subtree_depth: u32,
244 ) -> Result<Option<ContextPathNeighborhood>, PortError> {
245 let root_node_id = root_node_id.to_string();
246 let target_node_id = target_node_id.to_string();
247 self.run(move |store| {
248 let tx = store.begin_read()?;
249 let tx = tx.as_ref();
250
251 let Some(root) = load_node(tx, &root_node_id)? else {
252 return Ok(None);
253 };
254 if load_node(tx, &target_node_id)?.is_none() {
255 return Ok(None);
256 }
257 let Some(path_node_ids) = shortest_outward_path(tx, &root_node_id, &target_node_id)?
258 else {
259 return Ok(None);
260 };
261
262 let subtree = NeighborhoodRequest::new(&target_node_id, subtree_depth);
263 let (selected, relations) = OutwardNeighborhoodRead::new(tx, &subtree)
264 .extending(path_node_ids.iter().cloned().collect())?;
265
266 Ok(Some(ContextPathNeighborhood {
267 neighbors: selected_projections(tx, &selected, &root_node_id)?,
268 relations,
269 path_node_ids,
270 root,
271 }))
272 })
273 .await
274 }
275}
276
277impl NodeRelationshipReader for EmbeddedKernelStore {
278 async fn load_node_relationships(
279 &self,
280 node_id: &str,
281 ) -> Result<Option<NodeRelationships>, PortError> {
282 let node_id = node_id.to_string();
283 self.run(move |store| {
284 let tx = store.begin_read()?;
285 let tx = tx.as_ref();
286 if load_node(tx, &node_id)?.is_none() {
287 return Ok(None);
288 }
289
290 let mut incoming = Vec::new();
291 for ((target, source, relation_type), _) in
292 tx.scan_str3_by_first(Table::RelationsByTarget, &node_id)?
293 {
294 let Some(raw) = tx.get(
295 Table::Relations,
296 Key::Str3(&source, &target, &relation_type),
297 )?
298 else {
299 return Err(PortError::InvalidState(format!(
300 "embedded store adjacency index points at missing relation \
301 `{source}` -> `{target}` ({relation_type})"
302 )));
303 };
304 incoming.push(NodeRelationProjection {
305 explanation: decode_explanation(&raw)?,
306 source_node_id: source,
307 target_node_id: target,
308 relation_type,
309 });
310 }
311
312 Ok(Some(NodeRelationships {
313 incoming,
314 outgoing: outgoing_rows(tx, &node_id)?,
315 }))
316 })
317 .await
318 }
319}