Skip to main content

grafeo_core/index/
hash.rs

1//! Hash index for O(1) point lookups.
2//!
3//! Use this when you need to find entities by exact key - like looking up
4//! a user by their unique username or finding a node by a primary key.
5
6use grafeo_common::types::NodeId;
7use grafeo_common::utils::hash::FxHashMap;
8use parking_lot::RwLock;
9use std::hash::Hash;
10
11/// A thread-safe hash index for O(1) key lookups.
12///
13/// Backed by FxHashMap with an RwLock - multiple readers, single writer.
14/// Best for exact-match queries on unique keys.
15///
16/// # Example
17///
18/// ```
19/// use grafeo_core::index::HashIndex;
20/// use grafeo_common::types::NodeId;
21///
22/// let index: HashIndex<String, NodeId> = HashIndex::new();
23/// index.insert("alice".to_string(), NodeId::new(1));
24/// index.insert("bob".to_string(), NodeId::new(2));
25///
26/// assert_eq!(index.get(&"alice".to_string()), Some(NodeId::new(1)));
27/// ```
28pub struct HashIndex<K: Hash + Eq, V: Copy> {
29    /// The underlying hash map.
30    map: RwLock<FxHashMap<K, V>>,
31}
32
33impl<K: Hash + Eq, V: Copy> HashIndex<K, V> {
34    /// Creates a new empty hash index.
35    #[must_use]
36    pub fn new() -> Self {
37        Self {
38            map: RwLock::new(FxHashMap::default()),
39        }
40    }
41
42    /// Creates a new hash index with the given capacity.
43    #[must_use]
44    pub fn with_capacity(capacity: usize) -> Self {
45        Self {
46            map: RwLock::new(FxHashMap::with_capacity_and_hasher(
47                capacity,
48                Default::default(),
49            )),
50        }
51    }
52
53    /// Inserts a key-value pair into the index.
54    ///
55    /// Returns the previous value if the key was already present.
56    pub fn insert(&self, key: K, value: V) -> Option<V> {
57        self.map.write().insert(key, value)
58    }
59
60    /// Gets the value for a key.
61    pub fn get(&self, key: &K) -> Option<V> {
62        self.map.read().get(key).copied()
63    }
64
65    /// Removes a key from the index.
66    ///
67    /// Returns the value if the key was present.
68    pub fn remove(&self, key: &K) -> Option<V> {
69        self.map.write().remove(key)
70    }
71
72    /// Checks if a key exists in the index.
73    pub fn contains(&self, key: &K) -> bool {
74        self.map.read().contains_key(key)
75    }
76
77    /// Returns the number of entries in the index.
78    pub fn len(&self) -> usize {
79        self.map.read().len()
80    }
81
82    /// Returns true if the index is empty.
83    pub fn is_empty(&self) -> bool {
84        self.map.read().is_empty()
85    }
86
87    /// Clears all entries from the index.
88    pub fn clear(&self) {
89        self.map.write().clear();
90    }
91}
92
93impl<K: Hash + Eq, V: Copy> Default for HashIndex<K, V> {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99/// A hash index from string keys to NodeIds.
100pub type StringKeyIndex = HashIndex<String, NodeId>;
101
102/// A hash index from NodeIds to NodeIds.
103pub type NodeIdIndex = HashIndex<NodeId, NodeId>;
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_hash_index_basic() {
111        let index: HashIndex<u64, NodeId> = HashIndex::new();
112
113        index.insert(1, NodeId::new(100));
114        index.insert(2, NodeId::new(200));
115
116        assert_eq!(index.get(&1), Some(NodeId::new(100)));
117        assert_eq!(index.get(&2), Some(NodeId::new(200)));
118        assert_eq!(index.get(&3), None);
119    }
120
121    #[test]
122    fn test_hash_index_update() {
123        let index: HashIndex<u64, NodeId> = HashIndex::new();
124
125        index.insert(1, NodeId::new(100));
126        let old = index.insert(1, NodeId::new(200));
127
128        assert_eq!(old, Some(NodeId::new(100)));
129        assert_eq!(index.get(&1), Some(NodeId::new(200)));
130    }
131
132    #[test]
133    fn test_hash_index_remove() {
134        let index: HashIndex<u64, NodeId> = HashIndex::new();
135
136        index.insert(1, NodeId::new(100));
137        assert!(index.contains(&1));
138
139        let removed = index.remove(&1);
140        assert_eq!(removed, Some(NodeId::new(100)));
141        assert!(!index.contains(&1));
142    }
143
144    #[test]
145    fn test_hash_index_len() {
146        let index: HashIndex<u64, NodeId> = HashIndex::new();
147
148        assert!(index.is_empty());
149        assert_eq!(index.len(), 0);
150
151        index.insert(1, NodeId::new(100));
152        index.insert(2, NodeId::new(200));
153
154        assert!(!index.is_empty());
155        assert_eq!(index.len(), 2);
156    }
157
158    #[test]
159    fn test_hash_index_clear() {
160        let index: HashIndex<u64, NodeId> = HashIndex::new();
161
162        index.insert(1, NodeId::new(100));
163        index.insert(2, NodeId::new(200));
164
165        index.clear();
166
167        assert!(index.is_empty());
168        assert_eq!(index.get(&1), None);
169    }
170
171    #[test]
172    fn test_string_key_index() {
173        let index: StringKeyIndex = HashIndex::new();
174
175        index.insert("alice".to_string(), NodeId::new(1));
176        index.insert("bob".to_string(), NodeId::new(2));
177
178        assert_eq!(index.get(&"alice".to_string()), Some(NodeId::new(1)));
179        assert_eq!(index.get(&"bob".to_string()), Some(NodeId::new(2)));
180    }
181}