csm_memory/
singularity_ext.rs1use crate::singularity::Singularity;
4use csm_core_lib::error::{MemoryError, Result};
5use csm_core_lib::hyperdim::HVec10240;
6use tracing::instrument;
7
8impl Singularity {
9 #[instrument(skip(self, ns), fields(ids_count = ids.len()))]
11 pub fn bundle_concepts_strict(&self, ns: &str, ids: &[String]) -> Result<HVec10240> {
12 let ns_state = self
13 .get_namespace(ns)
14 .ok_or_else(|| MemoryError::NotFound {
15 entity: "Namespace".to_string(),
16 id: ns.to_string(),
17 })?;
18 let mut vectors = Vec::with_capacity(ids.len());
19 for id in ids {
20 match ns_state.concepts.get(id) {
21 Some(concept) => vectors.push(concept.vector),
22 None => {
23 return Err(MemoryError::NotFound {
24 entity: "Concept".to_string(),
25 id: id.clone(),
26 });
27 }
28 }
29 }
30
31 if vectors.is_empty() {
32 return Err(MemoryError::InvalidInput {
33 field: "ids".to_string(),
34 reason: "Empty concept list for bundling".to_string(),
35 });
36 }
37
38 HVec10240::bundle(&vectors)
39 }
40
41 pub fn update_metadata(
42 &mut self,
43 ns: &str,
44 id: &str,
45 metadata: std::collections::HashMap<String, serde_json::Value>,
46 ) -> Result<()> {
47 let ns_state = self.ensure_namespace(ns)?;
48 if let Some(concept) = ns_state.concepts.get_mut(id) {
49 concept.metadata = metadata;
50 concept.modified_at = crate::singularity::unix_now_secs();
51 self.invalidate_cache(ns);
52 Ok(())
53 } else {
54 Err(MemoryError::NotFound {
55 entity: "Concept".to_string(),
56 id: id.to_string(),
57 })
58 }
59 }
60
61 pub fn clear_associations(&mut self, ns: &str, id: &str) -> Result<()> {
62 let ns_state = self.ensure_namespace(ns)?;
63 if let Some(neighbors) = ns_state.associations.get_mut(id) {
64 neighbors.clear();
65 }
66 Ok(())
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
73 use super::*;
74 use crate::singularity::{ConceptBuilder, Singularity, SingularityConfig};
75 use csm_core_lib::error::MemoryError;
76 use std::collections::HashMap;
77
78 const NS: &str = "_default";
79
80 #[test]
81 fn test_bundle_concepts_strict_success() -> csm_core_lib::error::Result<()> {
82 let mut singularity = Singularity::with_config(SingularityConfig::default());
83 let vec1 = HVec10240::random();
84 let vec2 = HVec10240::random();
85
86 let c1 = ConceptBuilder::new("c1").with_vector(vec1).build().unwrap();
87 let c2 = ConceptBuilder::new("c2").with_vector(vec2).build().unwrap();
88
89 singularity.inject(NS, c1)?;
90 singularity.inject(NS, c2)?;
91
92 let result = singularity.bundle_concepts_strict(NS, &["c1".to_string(), "c2".to_string()]);
93 assert!(result.is_ok());
94 Ok(())
95 }
96
97 #[test]
98 fn test_bundle_concepts_strict_missing_id() -> csm_core_lib::error::Result<()> {
99 let mut singularity = Singularity::with_config(SingularityConfig::default());
100 let vec1 = HVec10240::random();
101
102 let c1 = ConceptBuilder::new("c1").with_vector(vec1).build()?;
103 singularity.inject(NS, c1)?;
104
105 let result =
106 singularity.bundle_concepts_strict(NS, &["c1".to_string(), "missing_id".to_string()]);
107
108 match result {
109 Err(MemoryError::NotFound { entity, id }) => {
110 assert_eq!(entity, "Concept");
111 assert_eq!(id, "missing_id");
112 }
113 _ => panic!("Expected NotFound error, got {result:?}"),
114 }
115 Ok(())
116 }
117
118 #[test]
119 fn test_update_metadata_not_found() {
120 let mut sing = Singularity::<HVec10240>::new(SingularityConfig::default());
121 let metadata = HashMap::new();
122
123 let result = sing.update_metadata(NS, "non-existent-id", metadata);
124
125 match result {
126 Err(MemoryError::NotFound { entity, id }) => {
127 assert_eq!(entity, "Concept");
128 assert_eq!(id, "non-existent-id");
129 }
130 _ => panic!("Expected MemoryError::NotFound, got {result:?}"),
131 }
132 }
133
134 #[test]
135 fn test_update_metadata_success() -> csm_core_lib::error::Result<()> {
136 let mut sing = Singularity::<HVec10240>::new(SingularityConfig::default());
137 let concept = ConceptBuilder::new("test-id")
138 .with_metadata("original", serde_json::Value::Bool(true))
139 .build()
140 .expect("Failed to build concept");
141
142 sing.inject(NS, concept)?;
143
144 let mut new_metadata = HashMap::new();
145 new_metadata.insert("updated".to_string(), serde_json::Value::Bool(true));
146
147 let time_before = crate::singularity::unix_now_secs();
148
149 let result = sing.update_metadata(NS, "test-id", new_metadata.clone());
150 assert!(result.is_ok());
151
152 let updated_concept = sing
153 .get(NS, "test-id")
154 .ok_or_else(|| MemoryError::NotFound {
155 entity: "Concept".to_string(),
156 id: "test-id".to_string(),
157 })?;
158 assert_eq!(updated_concept.metadata, new_metadata);
159 assert!(updated_concept.modified_at >= time_before);
160 Ok(())
161 }
162
163 #[test]
164 fn ensure_namespace_bruteforce_ok() {
165 let mut sing = Singularity::<HVec10240>::new(SingularityConfig::default());
166 assert!(sing.ensure_namespace(NS).is_ok());
167 assert!(sing.get_namespace(NS).is_some());
168 }
169
170 #[cfg(feature = "ann-hnsw")]
171 #[test]
172 fn inject_invalid_hnsw_returns_invalid_input_not_panic() {
173 use crate::index::IndexBackend;
174 let mut sing = Singularity::<HVec10240>::with_config_and_backend(
175 SingularityConfig::default(),
176 IndexBackend::Hnsw {
177 m: 0,
178 ef_construction: 200,
179 ef_search: 50,
180 },
181 );
182 let concept = ConceptBuilder::new("bad-backend")
183 .build()
184 .expect("concept build");
185 match sing.inject(NS, concept) {
186 Err(MemoryError::InvalidInput { field, .. }) => assert_eq!(field, "m"),
187 other => panic!("expected InvalidInput, got {other:?}"),
188 }
189 match sing.ensure_namespace(NS) {
190 Err(MemoryError::InvalidInput { field, .. }) => assert_eq!(field, "m"),
191 other => panic!("expected InvalidInput from ensure_namespace, got {other:?}"),
192 }
193 }
194
195 #[cfg(feature = "ann-lsh")]
196 #[test]
197 fn inject_invalid_lsh_returns_invalid_input_not_panic() {
198 use crate::index::IndexBackend;
199 let mut sing = Singularity::<HVec10240>::with_config_and_backend(
200 SingularityConfig::default(),
201 IndexBackend::Lsh {
202 num_tables: 0,
203 hash_bits: 8,
204 },
205 );
206 let concept = ConceptBuilder::new("bad-lsh")
207 .build()
208 .expect("concept build");
209 match sing.inject(NS, concept) {
210 Err(MemoryError::InvalidInput { field, .. }) => assert_eq!(field, "num_tables"),
211 other => panic!("expected InvalidInput, got {other:?}"),
212 }
213 }
214}