cvx_storage/memory/
mod.rs

1//! In-memory storage backend.
2//!
3//! A non-persistent [`StorageBackend`] implementation using a `BTreeMap`
4//! for ordered key access. Suitable for development, testing, and small datasets.
5//!
6//! Thread-safe via [`parking_lot::RwLock`].
7//!
8//! # Example
9//!
10//! ```
11//! use cvx_core::{StorageBackend, TemporalPoint};
12//! use cvx_storage::memory::InMemoryStore;
13//!
14//! let store = InMemoryStore::new();
15//! let point = TemporalPoint::new(42, 1000, vec![0.1, 0.2, 0.3]);
16//! store.put(0, &point).unwrap();
17//!
18//! let retrieved = store.get(42, 0, 1000).unwrap();
19//! assert_eq!(retrieved.as_ref(), Some(&point));
20//! ```
21
22use std::collections::BTreeMap;
23
24use cvx_core::StorageBackend;
25use cvx_core::error::StorageError;
26use cvx_core::types::TemporalPoint;
27use parking_lot::RwLock;
28
29/// Composite key for the in-memory store: `(entity_id, space_id, timestamp)`.
30///
31/// `BTreeMap` ordering on this tuple gives us:
32/// - Prefix scan by `(entity_id,)` → all spaces and timestamps
33/// - Prefix scan by `(entity_id, space_id)` → all timestamps in a space
34/// - Range scan by `(entity_id, space_id, t1..=t2)` → time window
35type StoreKey = (u64, u32, i64);
36
37/// Non-persistent in-memory storage using a sorted `BTreeMap`.
38///
39/// Thread-safe: multiple readers or one writer via [`RwLock`].
40/// All data is lost when the process exits.
41pub struct InMemoryStore {
42    data: RwLock<BTreeMap<StoreKey, TemporalPoint>>,
43}
44
45impl InMemoryStore {
46    /// Create a new empty in-memory store.
47    pub fn new() -> Self {
48        Self {
49            data: RwLock::new(BTreeMap::new()),
50        }
51    }
52
53    /// Number of points currently stored.
54    pub fn len(&self) -> usize {
55        self.data.read().len()
56    }
57
58    /// Whether the store is empty.
59    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(); // should not panic
163    }
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        // Retrieve specific entity
219        let results = store.range(42, 0, 0, 100_000).unwrap();
220        assert_eq!(results.len(), 100); // entity 42 has 100 points (i/100 == 42)
221
222        // Verify ordering
223        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); // -1000 and 0
238        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}