1use std::collections::BTreeMap;
23
24use cvx_core::StorageBackend;
25use cvx_core::error::StorageError;
26use cvx_core::types::TemporalPoint;
27use parking_lot::RwLock;
28
29type StoreKey = (u64, u32, i64);
36
37pub struct InMemoryStore {
42 data: RwLock<BTreeMap<StoreKey, TemporalPoint>>,
43}
44
45impl InMemoryStore {
46 pub fn new() -> Self {
48 Self {
49 data: RwLock::new(BTreeMap::new()),
50 }
51 }
52
53 pub fn len(&self) -> usize {
55 self.data.read().len()
56 }
57
58 pub fn is_empty(&self) -> bool {
60 self.data.read().is_empty()
61 }
62}
63
64impl Default for InMemoryStore {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl StorageBackend for InMemoryStore {
71 fn get(
72 &self,
73 entity_id: u64,
74 space_id: u32,
75 timestamp: i64,
76 ) -> Result<Option<TemporalPoint>, StorageError> {
77 let data = self.data.read();
78 Ok(data.get(&(entity_id, space_id, timestamp)).cloned())
79 }
80
81 fn put(&self, space_id: u32, point: &TemporalPoint) -> Result<(), StorageError> {
82 let key = (point.entity_id(), space_id, point.timestamp());
83 let mut data = self.data.write();
84 data.insert(key, point.clone());
85 Ok(())
86 }
87
88 fn range(
89 &self,
90 entity_id: u64,
91 space_id: u32,
92 start: i64,
93 end: i64,
94 ) -> Result<Vec<TemporalPoint>, StorageError> {
95 let data = self.data.read();
96 let range_start = (entity_id, space_id, start);
97 let range_end = (entity_id, space_id, end);
98 let points: Vec<TemporalPoint> = data
99 .range(range_start..=range_end)
100 .map(|(_, v)| v.clone())
101 .collect();
102 Ok(points)
103 }
104
105 fn delete(&self, entity_id: u64, space_id: u32, timestamp: i64) -> Result<(), StorageError> {
106 let mut data = self.data.write();
107 data.remove(&(entity_id, space_id, timestamp));
108 Ok(())
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 fn sample_point(entity_id: u64, timestamp: i64) -> TemporalPoint {
117 TemporalPoint::new(entity_id, timestamp, vec![0.1, 0.2, 0.3])
118 }
119
120 #[test]
121 fn put_and_get() {
122 let store = InMemoryStore::new();
123 let p = sample_point(1, 1000);
124 store.put(0, &p).unwrap();
125
126 let result = store.get(1, 0, 1000).unwrap();
127 assert_eq!(result, Some(p));
128 }
129
130 #[test]
131 fn get_nonexistent_returns_none() {
132 let store = InMemoryStore::new();
133 assert_eq!(store.get(999, 0, 0).unwrap(), None);
134 }
135
136 #[test]
137 fn put_overwrites() {
138 let store = InMemoryStore::new();
139 let p1 = TemporalPoint::new(1, 1000, vec![1.0]);
140 let p2 = TemporalPoint::new(1, 1000, vec![2.0]);
141 store.put(0, &p1).unwrap();
142 store.put(0, &p2).unwrap();
143
144 let result = store.get(1, 0, 1000).unwrap().unwrap();
145 assert_eq!(result.vector(), &[2.0]);
146 }
147
148 #[test]
149 fn delete_removes_point() {
150 let store = InMemoryStore::new();
151 store.put(0, &sample_point(1, 1000)).unwrap();
152 assert_eq!(store.len(), 1);
153
154 store.delete(1, 0, 1000).unwrap();
155 assert_eq!(store.len(), 0);
156 assert_eq!(store.get(1, 0, 1000).unwrap(), None);
157 }
158
159 #[test]
160 fn delete_nonexistent_is_noop() {
161 let store = InMemoryStore::new();
162 store.delete(999, 0, 0).unwrap(); }
164
165 #[test]
166 fn range_returns_ordered_subset() {
167 let store = InMemoryStore::new();
168 for ts in [100, 200, 300, 400, 500] {
169 store.put(0, &sample_point(1, ts)).unwrap();
170 }
171
172 let results = store.range(1, 0, 200, 400).unwrap();
173 assert_eq!(results.len(), 3);
174 assert_eq!(results[0].timestamp(), 200);
175 assert_eq!(results[1].timestamp(), 300);
176 assert_eq!(results[2].timestamp(), 400);
177 }
178
179 #[test]
180 fn range_empty_window() {
181 let store = InMemoryStore::new();
182 store.put(0, &sample_point(1, 100)).unwrap();
183
184 let results = store.range(1, 0, 200, 300).unwrap();
185 assert!(results.is_empty());
186 }
187
188 #[test]
189 fn range_does_not_cross_entities() {
190 let store = InMemoryStore::new();
191 store.put(0, &sample_point(1, 100)).unwrap();
192 store.put(0, &sample_point(2, 100)).unwrap();
193
194 let results = store.range(1, 0, 0, i64::MAX).unwrap();
195 assert_eq!(results.len(), 1);
196 assert_eq!(results[0].entity_id(), 1);
197 }
198
199 #[test]
200 fn range_does_not_cross_spaces() {
201 let store = InMemoryStore::new();
202 store.put(0, &sample_point(1, 100)).unwrap();
203 store.put(1, &sample_point(1, 100)).unwrap();
204
205 let results = store.range(1, 0, 0, i64::MAX).unwrap();
206 assert_eq!(results.len(), 1);
207 }
208
209 #[test]
210 fn insert_100k_and_retrieve() {
211 let store = InMemoryStore::new();
212 for i in 0..100_000u64 {
213 let p = TemporalPoint::new(i / 100, (i % 100) as i64 * 1000, vec![i as f32; 8]);
214 store.put(0, &p).unwrap();
215 }
216 assert_eq!(store.len(), 100_000);
217
218 let results = store.range(42, 0, 0, 100_000).unwrap();
220 assert_eq!(results.len(), 100); for window in results.windows(2) {
224 assert!(window[0].timestamp() < window[1].timestamp());
225 }
226 }
227
228 #[test]
229 fn negative_timestamps_work() {
230 let store = InMemoryStore::new();
231 store.put(0, &sample_point(1, -5000)).unwrap();
232 store.put(0, &sample_point(1, -1000)).unwrap();
233 store.put(0, &sample_point(1, 0)).unwrap();
234 store.put(0, &sample_point(1, 1000)).unwrap();
235
236 let results = store.range(1, 0, -3000, 0).unwrap();
237 assert_eq!(results.len(), 2); assert_eq!(results[0].timestamp(), -1000);
239 assert_eq!(results[1].timestamp(), 0);
240 }
241
242 #[test]
243 fn len_and_is_empty() {
244 let store = InMemoryStore::new();
245 assert!(store.is_empty());
246 assert_eq!(store.len(), 0);
247
248 store.put(0, &sample_point(1, 100)).unwrap();
249 assert!(!store.is_empty());
250 assert_eq!(store.len(), 1);
251 }
252}