Skip to main content

icydb_core/db/
registry.rs

1use crate::{
2    db::{data::DataStore, index::IndexStore},
3    error::{ErrorClass, ErrorOrigin, InternalError},
4};
5use std::{cell::RefCell, collections::HashMap, thread::LocalKey};
6use thiserror::Error as ThisError;
7
8///
9/// StoreRegistryError
10///
11
12#[derive(Debug, ThisError)]
13pub enum StoreRegistryError {
14    #[error("store '{0}' not found")]
15    StoreNotFound(String),
16
17    #[error("store '{0}' already registered")]
18    StoreAlreadyRegistered(String),
19}
20
21impl StoreRegistryError {
22    pub(crate) const fn class(&self) -> ErrorClass {
23        match self {
24            Self::StoreNotFound(_) => ErrorClass::Internal,
25            Self::StoreAlreadyRegistered(_) => ErrorClass::InvariantViolation,
26        }
27    }
28}
29
30impl From<StoreRegistryError> for InternalError {
31    fn from(err: StoreRegistryError) -> Self {
32        Self::classified(err.class(), ErrorOrigin::Store, err.to_string())
33    }
34}
35
36///
37/// StoreHandle
38///
39/// Bound pair of row and index stores for one schema `Store` path.
40///
41
42#[derive(Clone, Copy, Debug)]
43pub struct StoreHandle {
44    data: &'static LocalKey<RefCell<DataStore>>,
45    index: &'static LocalKey<RefCell<IndexStore>>,
46}
47
48impl StoreHandle {
49    /// Build a store handle from thread-local row/index stores.
50    #[must_use]
51    pub const fn new(
52        data: &'static LocalKey<RefCell<DataStore>>,
53        index: &'static LocalKey<RefCell<IndexStore>>,
54    ) -> Self {
55        Self { data, index }
56    }
57
58    /// Borrow the row store immutably.
59    pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
60        self.data.with_borrow(f)
61    }
62
63    /// Borrow the row store mutably.
64    pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
65        self.data.with_borrow_mut(f)
66    }
67
68    /// Borrow the index store immutably.
69    pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
70        self.index.with_borrow(f)
71    }
72
73    /// Borrow the index store mutably.
74    pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
75        self.index.with_borrow_mut(f)
76    }
77
78    /// Return the raw row-store accessor.
79    #[must_use]
80    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
81        self.data
82    }
83
84    /// Return the raw index-store accessor.
85    #[must_use]
86    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
87        self.index
88    }
89}
90
91///
92/// StoreRegistry
93///
94/// Thread-local registry for both row and index stores.
95///
96
97#[derive(Default)]
98pub struct StoreRegistry {
99    stores: HashMap<&'static str, StoreHandle>,
100}
101
102impl StoreRegistry {
103    /// Create an empty store registry.
104    #[must_use]
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Iterate registered stores.
110    pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
111        self.stores.iter().map(|(k, v)| (*k, *v))
112    }
113
114    /// Register a `Store` path to its row/index store pair.
115    pub fn register_store(
116        &mut self,
117        name: &'static str,
118        data: &'static LocalKey<RefCell<DataStore>>,
119        index: &'static LocalKey<RefCell<IndexStore>>,
120    ) -> Result<(), InternalError> {
121        if self.stores.contains_key(name) {
122            return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
123        }
124
125        self.stores.insert(name, StoreHandle::new(data, index));
126        Ok(())
127    }
128
129    /// Look up a store handle by path.
130    pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
131        self.stores
132            .get(path)
133            .copied()
134            .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use crate::{
141        db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
142        error::{ErrorClass, ErrorOrigin},
143        test_support::test_memory,
144    };
145    use std::{cell::RefCell, ptr};
146
147    const STORE_PATH: &str = "store_registry_tests::Store";
148
149    thread_local! {
150        static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
151        static TEST_INDEX_STORE: RefCell<IndexStore> =
152            RefCell::new(IndexStore::init(test_memory(152)));
153    }
154
155    fn test_registry() -> StoreRegistry {
156        let mut registry = StoreRegistry::new();
157        registry
158            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
159            .expect("test store registration should succeed");
160        registry
161    }
162
163    #[test]
164    fn register_store_binds_data_and_index_handles() {
165        let registry = test_registry();
166        let handle = registry
167            .try_get_store(STORE_PATH)
168            .expect("registered store path should resolve");
169
170        assert!(
171            ptr::eq(handle.data_store(), &TEST_DATA_STORE),
172            "store handle should expose the registered data store accessor"
173        );
174        assert!(
175            ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
176            "store handle should expose the registered index store accessor"
177        );
178
179        let data_rows = handle.with_data(|store| store.len());
180        let index_rows = handle.with_index(IndexStore::len);
181        assert_eq!(data_rows, 0, "fresh test data store should be empty");
182        assert_eq!(index_rows, 0, "fresh test index store should be empty");
183    }
184
185    #[test]
186    fn missing_store_path_rejected_before_access() {
187        let registry = StoreRegistry::new();
188        let err = registry
189            .try_get_store("store_registry_tests::Missing")
190            .expect_err("missing path should fail lookup");
191
192        assert_eq!(err.class, ErrorClass::Internal);
193        assert_eq!(err.origin, ErrorOrigin::Store);
194        assert!(
195            err.message
196                .contains("store 'store_registry_tests::Missing' not found"),
197            "missing store lookup should include the missing path"
198        );
199    }
200
201    #[test]
202    fn duplicate_store_registration_is_rejected() {
203        let mut registry = StoreRegistry::new();
204        registry
205            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
206            .expect("initial store registration should succeed");
207
208        let err = registry
209            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
210            .expect_err("duplicate registration should fail");
211        assert_eq!(err.class, ErrorClass::InvariantViolation);
212        assert_eq!(err.origin, ErrorOrigin::Store);
213        assert!(
214            err.message
215                .contains("store 'store_registry_tests::Store' already registered"),
216            "duplicate registration should include the conflicting path"
217        );
218    }
219}