kmp_adapter_embedded/adapter/
projection_write.rs1use std::collections::BTreeMap;
2
3use kmp_domain::{
4 NodeProjection, NodeRelationProjection, PortError, ProjectionMutation, ProjectionWriter,
5};
6use redb::{ReadableTable, WriteTransaction};
7
8use super::serdes::{DetailRecord, NodeRecord, encode, encode_explanation};
9use super::store::{
10 ANCHORS, DETAILS, EmbeddedKernelStore, NODES, RELATIONS, RELATIONS_BY_TARGET, commit_error,
11 storage_error, table_error,
12};
13
14pub(crate) const MEMORY_ANCHOR_KIND: &str = "memory_anchor";
15
16type NodeTable<'txn> = redb::Table<'txn, &'static str, &'static [u8]>;
17type AnchorTable<'txn> = redb::Table<'txn, &'static str, ()>;
18
19fn placeholder_node(node_id: &str) -> NodeProjection {
22 NodeProjection {
23 node_id: node_id.to_string(),
24 node_kind: "placeholder".to_string(),
25 title: "[unmaterialized node]".to_string(),
26 summary: "Referenced by relation before node materialization".to_string(),
27 status: "UNMATERIALIZED".to_string(),
28 labels: vec!["placeholder".to_string()],
29 properties: BTreeMap::from([
30 ("placeholder".to_string(), "true".to_string()),
31 (
32 "placeholder_reason".to_string(),
33 "relation_materialized_before_node".to_string(),
34 ),
35 (
36 "placeholder_created_by_subject".to_string(),
37 "graph.relation.materialized".to_string(),
38 ),
39 ]),
40 provenance: None,
41 }
42}
43
44fn write_node(
45 nodes: &mut NodeTable<'_>,
46 anchors: &mut AnchorTable<'_>,
47 node: NodeProjection,
48) -> Result<(), PortError> {
49 let node_id = node.node_id.clone();
50 let is_anchor = node.node_kind == MEMORY_ANCHOR_KIND;
51 let bytes = encode("graph node", &NodeRecord::from(node))?;
52 nodes
53 .insert(node_id.as_str(), bytes.as_slice())
54 .map_err(storage_error)?;
55 if is_anchor {
56 anchors
57 .insert(node_id.as_str(), ())
58 .map_err(storage_error)?;
59 } else {
60 anchors.remove(node_id.as_str()).map_err(storage_error)?;
61 }
62 Ok(())
63}
64
65fn ensure_node(
66 nodes: &mut NodeTable<'_>,
67 anchors: &mut AnchorTable<'_>,
68 node: NodeProjection,
69) -> Result<(), PortError> {
70 if nodes
71 .get(node.node_id.as_str())
72 .map_err(storage_error)?
73 .is_some()
74 {
75 return Ok(());
76 }
77 write_node(nodes, anchors, node)
78}
79
80pub(crate) fn apply_mutations_in_transaction(
83 tx: &WriteTransaction,
84 mutations: Vec<ProjectionMutation>,
85) -> Result<u64, PortError> {
86 let mut nodes = tx.open_table(NODES).map_err(table_error)?;
87 let mut anchors = tx.open_table(ANCHORS).map_err(table_error)?;
88 let mut relations = tx.open_table(RELATIONS).map_err(table_error)?;
89 let mut relations_by_target = tx.open_table(RELATIONS_BY_TARGET).map_err(table_error)?;
90 let mut details = tx.open_table(DETAILS).map_err(table_error)?;
91
92 let mut applied = 0u64;
93 for mutation in mutations {
94 match mutation {
95 ProjectionMutation::EnsureNode(node) => {
96 ensure_node(&mut nodes, &mut anchors, node)?;
97 }
98 ProjectionMutation::UpsertNode(node) => {
99 write_node(&mut nodes, &mut anchors, node)?;
100 }
101 ProjectionMutation::UpsertNodeRelation(relation) => {
102 let NodeRelationProjection {
103 source_node_id,
104 target_node_id,
105 relation_type,
106 explanation,
107 } = *relation;
108 ensure_node(&mut nodes, &mut anchors, placeholder_node(&source_node_id))?;
109 ensure_node(&mut nodes, &mut anchors, placeholder_node(&target_node_id))?;
110 let bytes = encode_explanation(&explanation)?;
111 relations
112 .insert(
113 (
114 source_node_id.as_str(),
115 target_node_id.as_str(),
116 relation_type.as_str(),
117 ),
118 bytes.as_slice(),
119 )
120 .map_err(storage_error)?;
121 relations_by_target
122 .insert(
123 (
124 target_node_id.as_str(),
125 source_node_id.as_str(),
126 relation_type.as_str(),
127 ),
128 (),
129 )
130 .map_err(storage_error)?;
131 }
132 ProjectionMutation::UpsertNodeDetail(detail) => {
133 let node_id = detail.node_id.clone();
134 let bytes = encode("node detail", &DetailRecord::from(detail))?;
135 details
136 .insert(node_id.as_str(), bytes.as_slice())
137 .map_err(storage_error)?;
138 }
139 }
140 applied += 1;
141 }
142
143 Ok(applied)
144}
145
146impl ProjectionWriter for EmbeddedKernelStore {
147 async fn apply_mutations(&self, mutations: Vec<ProjectionMutation>) -> Result<(), PortError> {
148 if mutations.is_empty() {
149 return Ok(());
150 }
151 self.run(move |store| {
152 let tx = store.begin_write()?;
153 apply_mutations_in_transaction(&tx, mutations)?;
154 tx.commit().map_err(commit_error)
155 })
156 .await
157 }
158}