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::RecordNodeCard(card) => {
81 super::node_card::project(tx, &card)?;
82 }
83 ProjectionMutation::EnsureNode(node) => {
84 ensure_node(tx, node)?;
85 }
86 ProjectionMutation::UpsertNode(node) => {
87 write_node(tx, node)?;
88 }
89 ProjectionMutation::UpdateNodeStatus { node_id, status } => {
90 update_node_status(tx, &node_id, status)?;
91 }
92 ProjectionMutation::UpsertNodeRelation(relation) => {
93 let NodeRelationProjection {
94 source_node_id,
95 target_node_id,
96 relation_type,
97 explanation,
98 } = *relation;
99 ensure_node(tx, placeholder_node(&source_node_id))?;
100 ensure_node(tx, placeholder_node(&target_node_id))?;
101 let bytes = encode_explanation(&explanation)?;
102 tx.insert(
103 Table::Relations,
104 Key::Str3(&source_node_id, &target_node_id, &relation_type),
105 &bytes,
106 )?;
107 tx.insert(
108 Table::RelationsByTarget,
109 Key::Str3(&target_node_id, &source_node_id, &relation_type),
110 &[],
111 )?;
112 }
113 ProjectionMutation::RemoveNodeRelation {
114 source_node_id,
115 target_node_id,
116 relation_type,
117 } => {
118 tx.remove(
119 Table::Relations,
120 Key::Str3(&source_node_id, &target_node_id, &relation_type),
121 )?;
122 tx.remove(
123 Table::RelationsByTarget,
124 Key::Str3(&target_node_id, &source_node_id, &relation_type),
125 )?;
126 }
127 ProjectionMutation::UpsertNodeDetail(detail) => {
128 let node_id = detail.node_id.clone();
129 let bytes = encode("node detail", &DetailRecord::from(detail.clone()))?;
130 let header = DetailHeaderRecord::describe(&detail, &bytes);
133 tx.insert(Table::Details, Key::Str(&node_id), &bytes)?;
134 detail_header::write(tx, &header)?;
135 }
136 }
137 applied += 1;
138 }
139
140 Ok(applied)
141}
142
143impl ProjectionWriter for EmbeddedKernelStore {
144 async fn apply_mutations(&self, mutations: Vec<ProjectionMutation>) -> Result<(), PortError> {
145 if mutations.is_empty() {
146 return Ok(());
147 }
148 self.run(move |store| {
149 let mut tx = store.begin_write()?;
150 apply_mutations_in_transaction(tx.as_mut(), mutations)?;
151 tx.commit()
152 })
153 .await
154 }
155}