Skip to main content

dbx_core/
index.rs

1//! Hash Index for fast key lookups
2//!
3//! Provides O(1) key lookup performance for indexed columns.
4
5use crate::error::{DbxError, DbxResult};
6use ahash::AHashMap;
7use dashmap::DashMap;
8use std::sync::RwLock;
9
10/// Type alias for column-level index: column_name → RwLock<value → row_ids>
11type ColumnIndex = DashMap<String, RwLock<AHashMap<Vec<u8>, Vec<usize>>>>;
12
13/// Hash Index structure
14///
15/// Maintains indexes for fast key lookups.
16/// Structure: table_name → (column_name → HashMap<value, Vec<row_id>>)
17pub struct HashIndex {
18    /// Indexes organized by table and column
19    indexes: DashMap<String, ColumnIndex>,
20}
21
22impl HashIndex {
23    /// Create a new empty hash index
24    pub fn new() -> Self {
25        Self {
26            indexes: DashMap::new(),
27        }
28    }
29
30    /// Create an index on a specific column
31    ///
32    /// # Arguments
33    ///
34    /// * `table` - Table name
35    /// * `column` - Column name to index
36    ///
37    /// # Example
38    ///
39    /// ```rust
40    /// # use dbx_core::index::HashIndex;
41    /// let index = HashIndex::new();
42    /// index.create_index("users", "id").unwrap();
43    /// ```
44    pub fn create_index(&self, table: &str, column: &str) -> DbxResult<()> {
45        let table_indexes = self.indexes.entry(table.to_string()).or_default();
46
47        if table_indexes.contains_key(column) {
48            return Err(DbxError::IndexAlreadyExists {
49                table: table.to_string(),
50                column: column.to_string(),
51            });
52        }
53
54        table_indexes.insert(column.to_string(), RwLock::new(AHashMap::new()));
55        Ok(())
56    }
57
58    /// Drop an index
59    ///
60    /// # Arguments
61    ///
62    /// * `table` - Table name
63    /// * `column` - Column name
64    pub fn drop_index(&self, table: &str, column: &str) -> DbxResult<()> {
65        if let Some(table_indexes) = self.indexes.get(table) {
66            if table_indexes.remove(column).is_none() {
67                return Err(DbxError::IndexNotFound {
68                    table: table.to_string(),
69                    column: column.to_string(),
70                });
71            }
72            Ok(())
73        } else {
74            Err(DbxError::IndexNotFound {
75                table: table.to_string(),
76                column: column.to_string(),
77            })
78        }
79    }
80
81    /// Lookup row IDs by indexed value
82    ///
83    /// # Arguments
84    ///
85    /// * `table` - Table name
86    /// * `column` - Column name
87    /// * `value` - Value to lookup
88    ///
89    /// # Returns
90    ///
91    /// Vector of row IDs that match the value
92    pub fn lookup(&self, table: &str, column: &str, value: &[u8]) -> DbxResult<Vec<usize>> {
93        if let Some(table_indexes) = self.indexes.get(table) {
94            if let Some(index) = table_indexes.get(column) {
95                let index_read = index.read().unwrap();
96                Ok(index_read.get(value).cloned().unwrap_or_default())
97            } else {
98                Err(DbxError::IndexNotFound {
99                    table: table.to_string(),
100                    column: column.to_string(),
101                })
102            }
103        } else {
104            Err(DbxError::IndexNotFound {
105                table: table.to_string(),
106                column: column.to_string(),
107            })
108        }
109    }
110
111    /// Update index on insert
112    ///
113    /// # Arguments
114    ///
115    /// * `table` - Table name
116    /// * `column` - Column name
117    /// * `value` - Value being inserted
118    /// * `row_id` - Row ID of the inserted row
119    pub fn update_on_insert(
120        &self,
121        table: &str,
122        column: &str,
123        value: &[u8],
124        row_id: usize,
125    ) -> DbxResult<()> {
126        if let Some(table_indexes) = self.indexes.get(table)
127            && let Some(index) = table_indexes.get(column)
128        {
129            let mut index_write = index.write().unwrap();
130            index_write.entry(value.to_vec()).or_default().push(row_id);
131        }
132        Ok(())
133    }
134
135    /// Update index on delete
136    ///
137    /// # Arguments
138    ///
139    /// * `table` - Table name
140    /// * `column` - Column name
141    /// * `value` - Value being deleted
142    /// * `row_id` - Row ID of the deleted row
143    pub fn update_on_delete(
144        &self,
145        table: &str,
146        column: &str,
147        value: &[u8],
148        row_id: usize,
149    ) -> DbxResult<()> {
150        if let Some(table_indexes) = self.indexes.get(table)
151            && let Some(index) = table_indexes.get(column)
152        {
153            let mut index_write = index.write().unwrap();
154            if let Some(row_ids) = index_write.get_mut(value) {
155                row_ids.retain(|&id| id != row_id);
156                if row_ids.is_empty() {
157                    index_write.remove(value);
158                }
159            }
160        }
161        Ok(())
162    }
163
164    /// Check if an index exists
165    pub fn has_index(&self, table: &str, column: &str) -> bool {
166        self.indexes
167            .get(table)
168            .map(|t| t.contains_key(column))
169            .unwrap_or(false)
170    }
171}
172
173impl Default for HashIndex {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_create_index() {
185        let index = HashIndex::new();
186        assert!(index.create_index("users", "id").is_ok());
187        assert!(index.has_index("users", "id"));
188    }
189
190    #[test]
191    fn test_create_duplicate_index() {
192        let index = HashIndex::new();
193        index.create_index("users", "id").unwrap();
194        assert!(index.create_index("users", "id").is_err());
195    }
196
197    #[test]
198    fn test_drop_index() {
199        let index = HashIndex::new();
200        index.create_index("users", "id").unwrap();
201        assert!(index.drop_index("users", "id").is_ok());
202        assert!(!index.has_index("users", "id"));
203    }
204
205    #[test]
206    fn test_drop_nonexistent_index() {
207        let index = HashIndex::new();
208        assert!(index.drop_index("users", "id").is_err());
209    }
210
211    #[test]
212    fn test_insert_and_lookup() {
213        let index = HashIndex::new();
214        index.create_index("users", "id").unwrap();
215
216        let value = b"user:123";
217        index.update_on_insert("users", "id", value, 0).unwrap();
218        index.update_on_insert("users", "id", value, 1).unwrap();
219
220        let result = index.lookup("users", "id", value).unwrap();
221        assert_eq!(result, vec![0, 1]);
222    }
223
224    #[test]
225    fn test_delete_and_lookup() {
226        let index = HashIndex::new();
227        index.create_index("users", "id").unwrap();
228
229        let value = b"user:123";
230        index.update_on_insert("users", "id", value, 0).unwrap();
231        index.update_on_insert("users", "id", value, 1).unwrap();
232
233        index.update_on_delete("users", "id", value, 0).unwrap();
234
235        let result = index.lookup("users", "id", value).unwrap();
236        assert_eq!(result, vec![1]);
237    }
238
239    #[test]
240    fn test_lookup_nonexistent() {
241        let index = HashIndex::new();
242        index.create_index("users", "id").unwrap();
243
244        let result = index.lookup("users", "id", b"nonexistent").unwrap();
245        assert!(result.is_empty());
246    }
247}