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