Skip to main content

kmp_adapter_embedded/adapter/
graph_read.rs

1use 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}