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