Skip to main content

kmp_adapter_embedded/adapter/
graph_read.rs

1use 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            // A dimension the caller did not ask for is not descended into,
60            // and everything hanging from it is thereby never loaded. Nothing
61            // else is refused: the narrowing is on the axis, not on the
62            // contents.
63            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            // Mirrors the Neo4j neighborhood query: an empty neighborhood
167            // reports no relations, even for self-referential root edges.
168            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                                        // A selection names dimensions by kind
312                                        // (`incident`) as readily as by id
313                                        // (`incident:north-outage`); the filter
314                                        // that follows reads kinds, so the
315                                        // index that picks the abouts must too.
316                                        || 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}