Skip to main content

horon_engine/
storage.rs

1//! storage.rs - Hierarchical Storage Implementation
2//!
3//! High-performance storage backend using Hyperbolic Tree Tensors (HTT).
4//! Provides hierarchical data storage with efficient path-based access
5//! patterns and O(1) lookup operations.
6//!
7//! ## Features
8//!
9//! - Path-based hierarchical storage structure
10//! - Automatic directory creation
11//! - Content-type support
12//! - Metadata attachment
13//! - Efficient subtree listing
14//! - Spatial proximity queries via hyperbolic geometry
15
16use std::collections::HashMap;
17use std::ops::Range;
18use std::sync::Arc;
19use g_math::fixed_point::FixedPoint;
20use log::trace;
21use super::tree_tensor::{HyperbolicTreeTensor, HTTConfig, SharedHTT, IntegrationError, IntegrationResult};
22use super::config::HTTStorageConfig;
23
24/// HTT Storage Backend implementation.
25///
26/// Uses Hyperbolic Tree Tensors for efficient hierarchical data storage
27/// with O(1) operations and spatial queries.
28pub struct HTTStorage {
29    /// The shared HTT instance
30    htt: SharedHTT,
31    /// Configuration
32    config: HTTStorageConfig,
33}
34
35impl HTTStorage {
36    /// Create a new HTT storage instance.
37    pub fn new(config: HTTStorageConfig) -> Self {
38        let mut htt_config = HTTConfig::new(
39            config.dimension,
40            config.max_memory_nodes,
41            config.cache_size,
42        );
43        if config.tau > FixedPoint::from_int(0) {
44            htt_config = htt_config.with_tau(config.tau);
45        }
46
47        let htt = Arc::new(HyperbolicTreeTensor::new(htt_config));
48
49        // Initialize with a root node. Inserting "/" into a fresh tree is
50        // infallible under a correct build; a failure here means a broken
51        // invariant (e.g. wrong fixed-point profile), not a recoverable
52        // runtime condition. Fail loudly rather than hand back a store with
53        // no root — every path operation assumes the root exists.
54        htt.insert("/", vec![], Some("application/x-directory".to_string()))
55            .expect("failed to initialize HTT root node ('/')");
56
57        Self { htt, config }
58    }
59
60    /// Get the shared HTT instance.
61    pub fn shared_htt(&self) -> &SharedHTT {
62        &self.htt
63    }
64
65    /// Store data without geometric embedding (data + semantic only).
66    ///
67    /// Much faster than `store()` — skips Sarkar embedding. Use for bulk
68    /// loading when spatial queries are not needed (semantic queries still work).
69    pub fn store_data_only(&self, key: &str, value: &[u8], content_type: Option<String>) -> IntegrationResult<()> {
70        let normalized_key = Self::normalize_key(key);
71
72        let missing_ancestors = Self::find_missing_ancestors(&self.htt, &normalized_key);
73        for ancestor in &missing_ancestors {
74            if !self.htt.exists(ancestor) {
75                match self.htt.insert_data_only(ancestor, vec![], Some("application/x-directory".to_string())) {
76                    Ok(()) => {},
77                    Err(IntegrationError::AlreadyExists(_)) => {},
78                    Err(e) => return Err(e),
79                }
80            }
81        }
82
83        if self.htt.exists(&normalized_key) {
84            self.htt.update_value(&normalized_key, value.to_vec())?;
85        } else {
86            match self.htt.insert_data_only(&normalized_key, value.to_vec(), content_type) {
87                Ok(()) => {},
88                Err(IntegrationError::AlreadyExists(_)) => {
89                    self.htt.update_value(&normalized_key, value.to_vec())?;
90                },
91                Err(e) => return Err(e),
92            }
93        }
94
95        Ok(())
96    }
97
98    /// Store data by key.
99    ///
100    /// Batch-creates missing ancestor directories under a single write lock
101    /// before inserting the target node. This avoids the previous recursive
102    /// approach which acquired/released the lock once per ancestor.
103    pub fn store(&self, key: &str, value: &[u8], content_type: Option<String>) -> IntegrationResult<()> {
104        trace!("HTTStorage::store - key: {}, size: {} bytes", key, value.len());
105
106        let normalized_key = Self::normalize_key(key);
107
108        // Collect missing ancestors (cheap DashMap existence checks)
109        let missing_ancestors = Self::find_missing_ancestors(&self.htt, &normalized_key);
110
111        // Create missing ancestors root-to-leaf (each parent exists before its child).
112        // Ignore AlreadyExists — another thread may have created the same ancestor
113        // concurrently, which is correct behavior (TOCTOU between exists() and insert()).
114        for ancestor in &missing_ancestors {
115            if !self.htt.exists(ancestor) {
116                match self.htt.insert(
117                    ancestor,
118                    vec![],
119                    Some("application/x-directory".to_string()),
120                ) {
121                    Ok(()) => {},
122                    Err(IntegrationError::AlreadyExists(_)) => {},
123                    Err(e) => return Err(e),
124                }
125            }
126        }
127
128        // Insert or update the target node.
129        // Same TOCTOU guard: if another thread inserted between our exists() check
130        // and our insert(), fall through to update.
131        if self.htt.exists(&normalized_key) {
132            self.htt.update_value(&normalized_key, value.to_vec())?;
133        } else {
134            match self.htt.insert(&normalized_key, value.to_vec(), content_type) {
135                Ok(()) => {},
136                Err(IntegrationError::AlreadyExists(_)) => {
137                    self.htt.update_value(&normalized_key, value.to_vec())?;
138                },
139                Err(e) => return Err(e),
140            }
141        }
142
143        Ok(())
144    }
145
146    /// Store data with an explicit child_index for deterministic Sarkar reconstruction.
147    ///
148    /// Same as `store()` but passes child_index through so the node gets the same
149    /// geometric position regardless of insertion order. Used during snapshot replay.
150    pub fn store_positioned(&self, key: &str, value: &[u8], content_type: Option<String>, child_index: u32) -> IntegrationResult<()> {
151        let normalized_key = Self::normalize_key(key);
152
153        let missing_ancestors = Self::find_missing_ancestors(&self.htt, &normalized_key);
154        for ancestor in &missing_ancestors {
155            if !self.htt.exists(ancestor) {
156                match self.htt.insert(
157                    ancestor,
158                    vec![],
159                    Some("application/x-directory".to_string()),
160                ) {
161                    Ok(()) => {},
162                    Err(IntegrationError::AlreadyExists(_)) => {},
163                    Err(e) => return Err(e),
164                }
165            }
166        }
167
168        if self.htt.exists(&normalized_key) {
169            self.htt.update_value(&normalized_key, value.to_vec())?;
170        } else {
171            match self.htt.insert_positioned(&normalized_key, value.to_vec(), content_type, child_index) {
172                Ok(()) => {},
173                Err(IntegrationError::AlreadyExists(_)) => {
174                    self.htt.update_value(&normalized_key, value.to_vec())?;
175                },
176                Err(e) => return Err(e),
177            }
178        }
179
180        Ok(())
181    }
182
183    /// Retrieve data by key.
184    pub fn retrieve(&self, key: &str) -> IntegrationResult<Vec<u8>> {
185        trace!("HTTStorage::retrieve - key: {}", key);
186
187        let normalized_key = Self::normalize_key(key);
188        let node = self.htt.get(&normalized_key)?;
189        Ok(node.value().to_vec())
190    }
191
192    /// Delete data by key.
193    pub fn delete(&self, key: &str) -> IntegrationResult<()> {
194        trace!("HTTStorage::delete - key: {}", key);
195
196        let normalized_key = Self::normalize_key(key);
197
198        if normalized_key == "/" {
199            return Err(IntegrationError::ValidationFailed(
200                "Cannot delete root node".to_string(),
201            ));
202        }
203
204        self.htt.delete(&normalized_key)
205    }
206
207    /// List keys with a prefix.
208    pub fn list(&self, prefix: &str) -> IntegrationResult<Vec<String>> {
209        trace!("HTTStorage::list - prefix: {}", prefix);
210
211        let normalized_prefix = Self::normalize_key(prefix);
212        self.htt.list_subtree(&normalized_prefix)
213    }
214
215    /// Check if a key exists.
216    pub fn exists(&self, key: &str) -> bool {
217        let normalized_key = Self::normalize_key(key);
218        self.htt.exists(&normalized_key)
219    }
220
221    /// Get metadata for a key.
222    pub fn get_metadata(&self, key: &str) -> IntegrationResult<HashMap<String, String>> {
223        trace!("HTTStorage::get_metadata - key: {}", key);
224
225        let normalized_key = Self::normalize_key(key);
226        let node = self.htt.get(&normalized_key)?;
227        let meta = node.metadata();
228
229        let mut result = meta.metadata.clone();
230        result.insert("key".to_string(), meta.key.clone());
231        result.insert("size".to_string(), node.value().len().to_string());
232        result.insert("created_at".to_string(), meta.created_at.to_string());
233        result.insert("updated_at".to_string(), meta.updated_at.to_string());
234
235        if let Some(ref ct) = meta.content_type {
236            result.insert("content_type".to_string(), ct.clone());
237        }
238
239        Ok(result)
240    }
241
242    /// Set metadata for a key.
243    pub fn set_metadata(&self, key: &str, meta_key: &str, meta_value: &str) -> IntegrationResult<()> {
244        trace!("HTTStorage::set_metadata - key: {}, meta_key: {}", key, meta_key);
245
246        let normalized_key = Self::normalize_key(key);
247        self.htt.set_node_metadata(&normalized_key, meta_key, meta_value)
248    }
249
250    /// Set semantic coordinates for a key (raw Q64.64 bytes, 16 bytes per dimension).
251    pub fn set_semantic(&self, key: &str, coords: Vec<u8>) -> IntegrationResult<()> {
252        trace!("HTTStorage::set_semantic - key: {}, bytes: {}", key, coords.len());
253
254        let normalized_key = Self::normalize_key(key);
255        self.htt.set_semantic(&normalized_key, coords)
256    }
257
258    /// Get semantic coordinates for a key (raw Q64.64 bytes).
259    pub fn get_semantic(&self, key: &str) -> IntegrationResult<Vec<u8>> {
260        trace!("HTTStorage::get_semantic - key: {}", key);
261
262        let normalized_key = Self::normalize_key(key);
263        self.htt.get_semantic(&normalized_key)
264    }
265
266    /// The hyperbolic (Poincaré) position of a stored key.
267    pub fn position(&self, key: &str) -> IntegrationResult<crate::hyperbolic_geometry::HyperbolicPoint> {
268        let normalized_key = Self::normalize_key(key);
269        self.htt.position(&normalized_key)
270    }
271
272    /// Upgrade a data-only key to a full geometric embedding (embed-on-demand — see
273    /// [`crate::tree_tensor::HyperbolicTreeTensor::embed_existing`]).
274    /// Returns whether this call performed the upgrade.
275    pub fn embed_existing(&self, key: &str) -> IntegrationResult<bool> {
276        let normalized_key = Self::normalize_key(key);
277        self.htt.embed_existing(&normalized_key)
278    }
279
280    /// Monotone counter of semantic-relevant mutations (see
281    /// [`crate::tensor_network::HyperbolicTensorNetwork::semantic_epoch`]).
282    pub fn semantic_epoch(&self) -> u64 {
283        self.htt.tensor_network().semantic_epoch()
284    }
285
286    /// Find the k nearest stored keys to the given key's position in hyperbolic space.
287    /// Returns paths sorted by ascending hyperbolic distance.
288    pub fn find_nearest(&self, path: &str, k: usize) -> IntegrationResult<Vec<String>> {
289        let normalized = Self::normalize_key(path);
290        let results = self.htt.find_nearest(&normalized, k)?;
291        Ok(results.into_iter().map(|(p, _dist)| p).collect())
292    }
293
294    /// Find all keys within hyperbolic radius of the given key.
295    pub fn find_in_radius(&self, path: &str, radius: FixedPoint) -> IntegrationResult<Vec<String>> {
296        let normalized = Self::normalize_key(path);
297        let results = self.htt.find_in_radius(&normalized, radius)?;
298        Ok(results.into_iter().map(|(p, _dist)| p).collect())
299    }
300
301    // -----------------------------------------------------------------------
302    // Semantic dimensional distance queries
303    // -----------------------------------------------------------------------
304
305    /// Find the k nearest nodes by Euclidean distance across a dimensional slice.
306    ///
307    /// `query_coords`: raw Q64.64 bytes for the query point.
308    /// `k`: number of results.
309    /// `dim_range`: which dimensions to compare.
310    ///
311    /// Returns paths sorted by distance ascending.
312    pub fn nearest_semantic(
313        &self,
314        query_coords: &[u8],
315        k: usize,
316        dim_range: &Range<usize>,
317    ) -> IntegrationResult<Vec<(String, FixedPoint)>> {
318        self.htt.nearest_semantic(query_coords, k, dim_range)
319    }
320
321    /// Find the k nearest nodes to an existing node by semantic dimensional distance.
322    /// The queried node is excluded from results.
323    pub fn neighbors_semantic(
324        &self,
325        path: &str,
326        k: usize,
327        dim_range: &Range<usize>,
328    ) -> IntegrationResult<Vec<(String, FixedPoint)>> {
329        let normalized = Self::normalize_key(path);
330        self.htt.neighbors_semantic(&normalized, k, dim_range)
331    }
332
333    /// Find the nearest stored node to an arbitrary point in the Poincaré disk.
334    ///
335    /// The power-diagram grid supplies candidates in O(1); the answer is
336    /// then decided by hyperbolic distance against the VP-tree's candidate
337    /// as well, so the result is the true nearest node.
338    ///
339    /// **Complexity**: O(log n). The grid alone cannot decide the query —
340    /// it holds one owner per tile, and Sarkar placement drives power cells
341    /// below tile size within a few levels, so a grid hit may name a node
342    /// that is not nearest.
343    /// Coordinates are in f32 (user-facing boundary); converted internally to FixedPoint.
344    /// Returns (path, hyperbolic_distance_as_FixedPoint).
345    pub fn nearest_neighbor_point(&self, coords: &[FixedPoint]) -> IntegrationResult<(String, FixedPoint)> {
346        self.validate_query_coords(coords)?;
347        let query = super::hyperbolic_geometry::HyperbolicPoint::from_slice(coords);
348        self.htt.nearest_neighbor_point(&query)
349    }
350
351    /// Find the k nearest stored nodes to an arbitrary point in the Poincaré disk.
352    ///
353    /// Returns `(path, hyperbolic_distance)` sorted by ascending distance.
354    pub fn nearest_neighbor_point_k(&self, coords: &[FixedPoint], k: usize) -> IntegrationResult<Vec<(String, FixedPoint)>> {
355        self.validate_query_coords(coords)?;
356        let query = super::hyperbolic_geometry::HyperbolicPoint::from_slice(coords);
357        self.htt.nearest_neighbor_point_k(&query, k)
358    }
359
360    /// Reject query coordinates whose length doesn't match the configured
361    /// embedding dimension — the geometry kernels assert on mismatched
362    /// dimensions, and a panic must not be reachable from user input.
363    fn validate_query_coords(&self, coords: &[FixedPoint]) -> IntegrationResult<()> {
364        if coords.len() != self.config.dimension {
365            return Err(IntegrationError::ValidationFailed(format!(
366                "query has {} coordinates but the store dimension is {}",
367                coords.len(),
368                self.config.dimension
369            )));
370        }
371        Ok(())
372    }
373
374    /// Collect all missing ancestor paths between `path` and the nearest existing
375    /// ancestor, returned in root-to-leaf order for sequential creation.
376    fn find_missing_ancestors(htt: &HyperbolicTreeTensor, path: &str) -> Vec<String> {
377        let mut missing = Vec::new();
378        let mut current = path.to_string();
379
380        loop {
381            let parent = match current.rfind('/') {
382                Some(index) if index > 0 => current[0..index].to_string(),
383                Some(0) if current != "/" => "/".to_string(),
384                _ => break,
385            };
386
387            if parent == current {
388                break;
389            }
390
391            if htt.exists(&parent) {
392                break;
393            }
394
395            missing.push(parent.clone());
396            current = parent;
397        }
398
399        missing.reverse(); // root-to-leaf order
400        missing
401    }
402
403    /// Total number of nodes in the tree, including the root node.
404    pub fn node_count(&self) -> usize {
405        self.htt.node_count()
406    }
407
408    /// Get storage statistics.
409    pub fn stats(&self) -> HashMap<String, String> {
410        let mut stats = HashMap::new();
411
412        for (key, value) in self.htt.stats() {
413            stats.insert(format!("htt.{}", key), value);
414        }
415        stats.insert("node_count".to_string(), self.htt.node_count().to_string());
416
417        stats.insert("dimension".to_string(), self.config.dimension.to_string());
418        stats.insert(
419            "max_memory_nodes".to_string(),
420            self.config.max_memory_nodes.to_string(),
421        );
422        stats.insert("cache_size".to_string(), self.config.cache_size.to_string());
423
424        stats
425    }
426
427    /// Normalize a key to have a leading '/'.
428    fn normalize_key(key: &str) -> String {
429        if !key.starts_with('/') {
430            format!("/{}", key)
431        } else {
432            key.to_string()
433        }
434    }
435}
436
437#[cfg(test)]
438mod tests {
439
440/// Exact fixed-point coordinates from decimal literals.
441fn fp(vals: &[f64]) -> Vec<g_math::fixed_point::FixedPoint> {
442    vals.iter().map(|&v| g_math::fixed_point::FixedPoint::from_f64(v)).collect()
443}
444
445    use super::*;
446
447    #[test]
448    fn test_storage_creation() {
449        let config = HTTStorageConfig::default();
450        let storage = HTTStorage::new(config);
451        assert!(storage.exists("/"));
452    }
453
454    #[test]
455    fn test_storage_operations() {
456        let config = HTTStorageConfig::default();
457        let storage = HTTStorage::new(config);
458
459        // Store data
460        storage.store("/test", b"test data", None).unwrap();
461        assert!(storage.exists("/test"));
462
463        // Retrieve data
464        let data = storage.retrieve("/test").unwrap();
465        assert_eq!(data, b"test data");
466
467        // Update data
468        storage.store("/test", b"updated data", None).unwrap();
469        let updated = storage.retrieve("/test").unwrap();
470        assert_eq!(updated, b"updated data");
471
472        // Create nested path
473        storage.store("/parent/child", b"child data", None).unwrap();
474        assert!(storage.exists("/parent"));
475
476        // List with prefix
477        let keys = storage.list("/").unwrap();
478        assert!(keys.contains(&"/test".to_string()));
479        assert!(keys.contains(&"/parent".to_string()));
480        assert!(keys.contains(&"/parent/child".to_string()));
481
482        // Delete
483        storage.delete("/test").unwrap();
484        assert!(!storage.exists("/test"));
485
486        // Metadata
487        storage
488            .set_metadata("/parent", "description", "A parent directory")
489            .unwrap();
490        let metadata = storage.get_metadata("/parent").unwrap();
491        assert_eq!(
492            metadata.get("description"),
493            Some(&"A parent directory".to_string())
494        );
495    }
496
497    #[test]
498    fn test_find_nearest_api() {
499        let config = HTTStorageConfig::default();
500        let storage = HTTStorage::new(config);
501
502        storage.store("/a", b"a", None).unwrap();
503        storage.store("/b", b"b", None).unwrap();
504        storage.store("/c", b"c", None).unwrap();
505
506        let nearest = storage.find_nearest("/a", 2).unwrap();
507        assert!(!nearest.is_empty());
508        assert!(nearest.len() <= 2);
509        // Should not include /a itself
510        assert!(!nearest.contains(&"/a".to_string()));
511    }
512
513    #[test]
514    fn test_find_in_radius_api() {
515        use g_math::fixed_point::FixedPoint;
516
517        let config = HTTStorageConfig::default();
518        let storage = HTTStorage::new(config);
519
520        storage.store("/x", b"x", None).unwrap();
521        storage.store("/y", b"y", None).unwrap();
522
523        let large_radius = FixedPoint::from_int(10);
524        let results = storage.find_in_radius("/x", large_radius).unwrap();
525        // Should find at least /y and / (root)
526        assert!(!results.is_empty());
527    }
528
529    #[test]
530    fn test_storage_stats() {
531        let config = HTTStorageConfig::default();
532        let storage = HTTStorage::new(config);
533
534        storage.store("/test1", b"data1", None).unwrap();
535        storage.store("/test2", b"data2", None).unwrap();
536
537        let stats = storage.stats();
538        assert!(stats.contains_key("node_count"));
539        assert!(stats.contains_key("dimension"));
540    }
541
542    #[test]
543    fn test_nearest_neighbor_point_api() {
544        let config = HTTStorageConfig::default();
545        let storage = HTTStorage::new(config);
546
547        storage.store("/a", b"a", None).unwrap();
548        storage.store("/b", b"b", None).unwrap();
549        storage.store("/c", b"c", None).unwrap();
550
551        // Query at origin should return the root (which is at the origin)
552        let (path, dist) = storage.nearest_neighbor_point(&fp(&[0.0, 0.0, 0.0, 0.0])).unwrap();
553        assert_eq!(path, "/", "Query at origin should find root");
554        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(10);
555        assert!(dist < tolerance, "Distance to root at origin should be small");
556    }
557}