termivar_scanner/knowledge/
relations.rs1use termivar_core::{
4 EntityId, Evidence, KnowledgeEntity, KnowledgeRelation, RelationId, RelationKind,
5};
6
7use crate::knowledge::{
8 bump_subject_revision, collect_indexed, identity_conflict, index, KnowledgeBase,
9 KnowledgeBaseError, KnowledgeRecordKind, KnowledgeWrite,
10};
11
12pub const MAX_KNOWLEDGE_RELATION_ID_BYTES: usize = 512;
14pub const MAX_KNOWLEDGE_RELATION_ENTITY_ID_BYTES: usize = 2_048;
16pub const MAX_KNOWLEDGE_RELATION_KIND_BYTES: usize = 256;
18pub const MAX_KNOWLEDGE_RELATION_EVIDENCE_IDS: usize = 32;
20pub const MAX_KNOWLEDGE_RELATION_EVIDENCE_ID_BYTES: usize = 512;
22
23impl KnowledgeBase {
24 pub fn insert_evidence_with_relation(
32 &self,
33 evidence: Evidence,
34 relation: KnowledgeRelation,
35 ) -> Result<(KnowledgeWrite, KnowledgeWrite), KnowledgeBaseError> {
36 validate_relation_bounds(&relation)?;
37 let evidence_id = evidence.id().clone();
38 let relation_id = relation.id().clone();
39 if relation.evidence_ids().len() != 1 || !relation.evidence_ids().contains(&evidence_id) {
40 return Err(KnowledgeBaseError::RelationEvidenceMismatch {
41 relation_id: relation_id.to_string(),
42 evidence_id: evidence_id.to_string(),
43 });
44 }
45 if relation.from() != evidence.subject() {
46 return Err(KnowledgeBaseError::RelationSubjectMismatch {
47 relation_id: relation_id.to_string(),
48 evidence_subject: evidence.subject().to_string(),
49 relation_from: relation.from().to_string(),
50 });
51 }
52
53 let evidence_subject = evidence.subject().clone();
54 let evidence_predicate = evidence.predicate().clone();
55 let relation_from = relation.from().clone();
56 let relation_to = relation.to().clone();
57 let mut state = self.write_state();
58
59 let evidence_write = match state.evidence.get(&evidence_id) {
60 Some(existing) if existing == &evidence => KnowledgeWrite::Unchanged,
61 Some(_) => {
62 return Err(identity_conflict(
63 KnowledgeRecordKind::Evidence,
64 &evidence_id,
65 ));
66 },
67 None => KnowledgeWrite::Inserted,
68 };
69 let relation_write = match state.relations.get(&relation_id) {
70 Some(existing) if existing == &relation => KnowledgeWrite::Unchanged,
71 Some(existing)
72 if existing.from() == relation.from()
73 && existing.to() == relation.to()
74 && existing.kind() == relation.kind() =>
75 {
76 KnowledgeWrite::Updated
77 },
78 Some(_) => {
79 return Err(identity_conflict(
80 KnowledgeRecordKind::Relation,
81 &relation_id,
82 ));
83 },
84 None => KnowledgeWrite::Inserted,
85 };
86
87 if evidence_write == KnowledgeWrite::Inserted {
88 state.evidence.insert(evidence_id.clone(), evidence);
89 bump_subject_revision(&mut state, &evidence_subject);
90 index(
91 &mut state.evidence_by_subject,
92 evidence_subject,
93 evidence_id.clone(),
94 );
95 index(
96 &mut state.evidence_by_predicate,
97 evidence_predicate,
98 evidence_id,
99 );
100 }
101 if relation_write != KnowledgeWrite::Unchanged {
102 state.relations.insert(relation_id.clone(), relation);
103 if relation_write == KnowledgeWrite::Inserted {
104 index(
105 &mut state.relations_from,
106 relation_from,
107 relation_id.clone(),
108 );
109 index(&mut state.relations_to, relation_to, relation_id);
110 }
111 }
112
113 Ok((evidence_write, relation_write))
114 }
115
116 pub fn insert_entity(
118 &self,
119 entity: KnowledgeEntity,
120 ) -> Result<KnowledgeWrite, KnowledgeBaseError> {
121 let id = entity.id().clone();
122 let mut state = self.write_state();
123
124 if let Some(existing) = state.entities.get(&id) {
125 return if existing == &entity {
126 Ok(KnowledgeWrite::Unchanged)
127 } else {
128 Err(identity_conflict(KnowledgeRecordKind::Entity, &id))
129 };
130 }
131
132 state.entities.insert(id, entity);
133 Ok(KnowledgeWrite::Inserted)
134 }
135
136 pub fn upsert_relation(
142 &self,
143 relation: KnowledgeRelation,
144 ) -> Result<KnowledgeWrite, KnowledgeBaseError> {
145 validate_relation_bounds(&relation)?;
146 let id = relation.id().clone();
147 let from = relation.from().clone();
148 let to = relation.to().clone();
149 let mut state = self.write_state();
150
151 if let Some(existing) = state.relations.get(&id) {
152 if existing == &relation {
153 return Ok(KnowledgeWrite::Unchanged);
154 }
155 if existing.from() != relation.from()
156 || existing.to() != relation.to()
157 || existing.kind() != relation.kind()
158 {
159 return Err(identity_conflict(KnowledgeRecordKind::Relation, &id));
160 }
161 state.relations.insert(id, relation);
162 return Ok(KnowledgeWrite::Updated);
163 }
164
165 state.relations.insert(id.clone(), relation);
166 index(&mut state.relations_from, from, id.clone());
167 index(&mut state.relations_to, to, id);
168 Ok(KnowledgeWrite::Inserted)
169 }
170
171 pub fn entity(&self, id: &EntityId) -> Option<KnowledgeEntity> {
173 self.read_state().entities.get(id).cloned()
174 }
175
176 pub fn relation(&self, id: &RelationId) -> Option<KnowledgeRelation> {
178 self.read_state().relations.get(id).cloned()
179 }
180
181 pub fn relations_from(&self, entity_id: &EntityId) -> Vec<KnowledgeRelation> {
183 let state = self.read_state();
184 collect_indexed(state.relations_from.get(entity_id), &state.relations)
185 }
186
187 pub fn relations_to(&self, entity_id: &EntityId) -> Vec<KnowledgeRelation> {
189 let state = self.read_state();
190 collect_indexed(state.relations_to.get(entity_id), &state.relations)
191 }
192
193 pub fn relations_to_page(
199 &self,
200 entity_id: &EntityId,
201 after_exclusive: Option<&RelationId>,
202 limit: usize,
203 ) -> Vec<KnowledgeRelation> {
204 self.relations_to_page_with_more(entity_id, after_exclusive, limit)
205 .0
206 }
207
208 pub(crate) fn relations_to_page_with_more(
213 &self,
214 entity_id: &EntityId,
215 after_exclusive: Option<&RelationId>,
216 limit: usize,
217 ) -> (Vec<KnowledgeRelation>, bool) {
218 if limit == 0 {
219 return (Vec::new(), false);
220 }
221
222 let state = self.read_state();
223 let Some(ids) = state.relations_to.get(entity_id) else {
224 return (Vec::new(), false);
225 };
226 let lower_bound = after_exclusive
227 .cloned()
228 .map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
229 let mut ids = ids.range((lower_bound, std::ops::Bound::Unbounded));
230 let relations = ids
231 .by_ref()
232 .take(limit)
233 .filter_map(|id| state.relations.get(id).cloned())
234 .collect();
235 let has_more = ids.next().is_some();
236 (relations, has_more)
237 }
238}
239
240fn validate_relation_bounds(relation: &KnowledgeRelation) -> Result<(), KnowledgeBaseError> {
241 validate_relation_limit(
242 "id",
243 relation.id().as_str().len(),
244 MAX_KNOWLEDGE_RELATION_ID_BYTES,
245 )?;
246 validate_relation_limit(
247 "from",
248 relation.from().as_str().len(),
249 MAX_KNOWLEDGE_RELATION_ENTITY_ID_BYTES,
250 )?;
251 validate_relation_limit(
252 "to",
253 relation.to().as_str().len(),
254 MAX_KNOWLEDGE_RELATION_ENTITY_ID_BYTES,
255 )?;
256 if let RelationKind::Custom(kind) = relation.kind() {
257 validate_relation_limit("kind", kind.len(), MAX_KNOWLEDGE_RELATION_KIND_BYTES)?;
258 }
259 validate_relation_limit(
260 "evidence_ids",
261 relation.evidence_ids().len(),
262 MAX_KNOWLEDGE_RELATION_EVIDENCE_IDS,
263 )?;
264 for evidence_id in relation.evidence_ids() {
265 validate_relation_limit(
266 "evidence_id",
267 evidence_id.as_str().len(),
268 MAX_KNOWLEDGE_RELATION_EVIDENCE_ID_BYTES,
269 )?;
270 }
271 Ok(())
272}
273
274fn validate_relation_limit(
275 field: &'static str,
276 actual: usize,
277 maximum: usize,
278) -> Result<(), KnowledgeBaseError> {
279 if actual > maximum {
280 return Err(KnowledgeBaseError::RelationLimitExceeded {
281 field,
282 actual,
283 maximum,
284 });
285 }
286 Ok(())
287}