kmp_adapter_embedded/adapter/
projection_write.rs1use std::collections::BTreeMap;
2
3use kmp_domain::{
4 NodeProjection, NodeRelationProjection, PortError, ProjectionMutation, ProjectionWriter,
5};
6
7use super::engine::{Key, Table, WriteTx};
8use super::serdes::{DetailRecord, NodeRecord, encode, encode_explanation};
9use super::store::EmbeddedKernelStore;
10
11pub(crate) const MEMORY_ANCHOR_KIND: &str = "memory_anchor";
12
13fn placeholder_node(node_id: &str) -> NodeProjection {
16 NodeProjection {
17 node_id: node_id.to_string(),
18 node_kind: "placeholder".to_string(),
19 title: "[unmaterialized node]".to_string(),
20 summary: "Referenced by relation before node materialization".to_string(),
21 status: "UNMATERIALIZED".to_string(),
22 labels: vec!["placeholder".to_string()],
23 properties: BTreeMap::from([
24 ("placeholder".to_string(), "true".to_string()),
25 (
26 "placeholder_reason".to_string(),
27 "relation_materialized_before_node".to_string(),
28 ),
29 (
30 "placeholder_created_by_subject".to_string(),
31 "graph.relation.materialized".to_string(),
32 ),
33 ]),
34 provenance: None,
35 }
36}
37
38fn write_node(tx: &mut dyn WriteTx, node: NodeProjection) -> Result<(), PortError> {
39 let node_id = node.node_id.clone();
40 let is_anchor = node.node_kind == MEMORY_ANCHOR_KIND;
41 let bytes = encode("graph node", &NodeRecord::from(node))?;
42 tx.insert(Table::Nodes, Key::Str(&node_id), &bytes)?;
43 if is_anchor {
44 tx.insert(Table::Anchors, Key::Str(&node_id), &[])
45 } else {
46 tx.remove(Table::Anchors, Key::Str(&node_id))
47 }
48}
49
50fn ensure_node(tx: &mut dyn WriteTx, node: NodeProjection) -> Result<(), PortError> {
51 if tx.get(Table::Nodes, Key::Str(&node.node_id))?.is_some() {
52 return Ok(());
53 }
54 write_node(tx, node)
55}
56
57pub(crate) fn apply_mutations_in_transaction(
60 tx: &mut dyn WriteTx,
61 mutations: Vec<ProjectionMutation>,
62) -> Result<u64, PortError> {
63 let mut applied = 0u64;
64 for mutation in mutations {
65 match mutation {
66 ProjectionMutation::EnsureNode(node) => {
67 ensure_node(tx, node)?;
68 }
69 ProjectionMutation::UpsertNode(node) => {
70 write_node(tx, node)?;
71 }
72 ProjectionMutation::UpsertNodeRelation(relation) => {
73 let NodeRelationProjection {
74 source_node_id,
75 target_node_id,
76 relation_type,
77 explanation,
78 } = *relation;
79 ensure_node(tx, placeholder_node(&source_node_id))?;
80 ensure_node(tx, placeholder_node(&target_node_id))?;
81 let bytes = encode_explanation(&explanation)?;
82 tx.insert(
83 Table::Relations,
84 Key::Str3(&source_node_id, &target_node_id, &relation_type),
85 &bytes,
86 )?;
87 tx.insert(
88 Table::RelationsByTarget,
89 Key::Str3(&target_node_id, &source_node_id, &relation_type),
90 &[],
91 )?;
92 }
93 ProjectionMutation::UpsertNodeDetail(detail) => {
94 let node_id = detail.node_id.clone();
95 let bytes = encode("node detail", &DetailRecord::from(detail))?;
96 tx.insert(Table::Details, Key::Str(&node_id), &bytes)?;
97 }
98 }
99 applied += 1;
100 }
101
102 Ok(applied)
103}
104
105impl ProjectionWriter for EmbeddedKernelStore {
106 async fn apply_mutations(&self, mutations: Vec<ProjectionMutation>) -> Result<(), PortError> {
107 if mutations.is_empty() {
108 return Ok(());
109 }
110 self.run(move |store| {
111 let mut tx = store.begin_write()?;
112 apply_mutations_in_transaction(tx.as_mut(), mutations)?;
113 tx.commit()
114 })
115 .await
116 }
117}