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)]
13#[expect(clippy::enum_variant_names)]
14pub enum StoreRegistryError {
15    #[error("store '{0}' not found")]
16    StoreNotFound(String),
17
18    #[error("store '{0}' already registered")]
19    StoreAlreadyRegistered(String),
20
21    #[error(
22        "store '{name}' reuses the same row/index store pair already registered as '{existing_name}'"
23    )]
24    StoreHandlePairAlreadyRegistered { name: String, existing_name: String },
25}
26
27impl StoreRegistryError {
28    pub(crate) const fn class(&self) -> ErrorClass {
29        match self {
30            Self::StoreNotFound(_) => ErrorClass::Internal,
31            Self::StoreAlreadyRegistered(_) | Self::StoreHandlePairAlreadyRegistered { .. } => {
32                ErrorClass::InvariantViolation
33            }
34        }
35    }
36}
37
38impl From<StoreRegistryError> for InternalError {
39    fn from(err: StoreRegistryError) -> Self {
40        Self::classified(err.class(), ErrorOrigin::Store, err.to_string())
41    }
42}
43
44///
45/// StoreHandle
46///
47/// Bound pair of row and index stores for one schema `Store` path.
48///
49
50#[derive(Clone, Copy, Debug)]
51pub struct StoreHandle {
52    data: &'static LocalKey<RefCell<DataStore>>,
53    index: &'static LocalKey<RefCell<IndexStore>>,
54}
55
56impl StoreHandle {
57    /// Build a store handle from thread-local row/index stores.
58    #[must_use]
59    pub const fn new(
60        data: &'static LocalKey<RefCell<DataStore>>,
61        index: &'static LocalKey<RefCell<IndexStore>>,
62    ) -> Self {
63        Self { data, index }
64    }
65
66    /// Borrow the row store immutably.
67    pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
68        self.data.with_borrow(f)
69    }
70
71    /// Borrow the row store mutably.
72    pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
73        self.data.with_borrow_mut(f)
74    }
75
76    /// Borrow the index store immutably.
77    pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
78        self.index.with_borrow(f)
79    }
80
81    /// Borrow the index store mutably.
82    pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
83        self.index.with_borrow_mut(f)
84    }
85
86    /// Return the raw row-store accessor.
87    #[must_use]
88    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
89        self.data
90    }
91
92    /// Return the raw index-store accessor.
93    #[must_use]
94    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
95        self.index
96    }
97}
98
99///
100/// StoreRegistry
101///
102/// Thread-local registry for both row and index stores.
103///
104
105#[derive(Default)]
106pub struct StoreRegistry {
107    stores: HashMap<&'static str, StoreHandle>,
108}
109
110impl StoreRegistry {
111    /// Create an empty store registry.
112    #[must_use]
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    /// Iterate registered stores.
118    ///
119    /// Iteration order is intentionally unspecified because the registry uses a
120    /// `HashMap`. Semantic result ordering must never depend on this iteration
121    /// order; callers that need deterministic ordering must sort by store path.
122    pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
123        self.stores.iter().map(|(k, v)| (*k, *v))
124    }
125
126    /// Register a `Store` path to its row/index store pair.
127    pub fn register_store(
128        &mut self,
129        name: &'static str,
130        data: &'static LocalKey<RefCell<DataStore>>,
131        index: &'static LocalKey<RefCell<IndexStore>>,
132    ) -> Result<(), InternalError> {
133        if self.stores.contains_key(name) {
134            return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
135        }
136
137        // Keep one canonical logical store name per physical row/index store pair.
138        if let Some(existing_name) =
139            self.stores
140                .iter()
141                .find_map(|(existing_name, existing_handle)| {
142                    (std::ptr::eq(existing_handle.data_store(), data)
143                        && std::ptr::eq(existing_handle.index_store(), index))
144                    .then_some(*existing_name)
145                })
146        {
147            return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
148                name: name.to_string(),
149                existing_name: existing_name.to_string(),
150            }
151            .into());
152        }
153
154        self.stores.insert(name, StoreHandle::new(data, index));
155        Ok(())
156    }
157
158    /// Look up a store handle by path.
159    pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
160        self.stores
161            .get(path)
162            .copied()
163            .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use crate::{
170        db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
171        error::{ErrorClass, ErrorOrigin},
172        test_support::test_memory,
173    };
174    use std::{cell::RefCell, ptr};
175
176    const STORE_PATH: &str = "store_registry_tests::Store";
177    const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
178
179    thread_local! {
180        static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
181        static TEST_INDEX_STORE: RefCell<IndexStore> =
182            RefCell::new(IndexStore::init(test_memory(152)));
183    }
184
185    fn test_registry() -> StoreRegistry {
186        let mut registry = StoreRegistry::new();
187        registry
188            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
189            .expect("test store registration should succeed");
190        registry
191    }
192
193    #[test]
194    fn register_store_binds_data_and_index_handles() {
195        let registry = test_registry();
196        let handle = registry
197            .try_get_store(STORE_PATH)
198            .expect("registered store path should resolve");
199
200        assert!(
201            ptr::eq(handle.data_store(), &TEST_DATA_STORE),
202            "store handle should expose the registered data store accessor"
203        );
204        assert!(
205            ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
206            "store handle should expose the registered index store accessor"
207        );
208
209        let data_rows = handle.with_data(|store| store.len());
210        let index_rows = handle.with_index(IndexStore::len);
211        assert_eq!(data_rows, 0, "fresh test data store should be empty");
212        assert_eq!(index_rows, 0, "fresh test index store should be empty");
213    }
214
215    #[test]
216    fn missing_store_path_rejected_before_access() {
217        let registry = StoreRegistry::new();
218        let err = registry
219            .try_get_store("store_registry_tests::Missing")
220            .expect_err("missing path should fail lookup");
221
222        assert_eq!(err.class, ErrorClass::Internal);
223        assert_eq!(err.origin, ErrorOrigin::Store);
224        assert!(
225            err.message
226                .contains("store 'store_registry_tests::Missing' not found"),
227            "missing store lookup should include the missing path"
228        );
229    }
230
231    #[test]
232    fn duplicate_store_registration_is_rejected() {
233        let mut registry = StoreRegistry::new();
234        registry
235            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
236            .expect("initial store registration should succeed");
237
238        let err = registry
239            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
240            .expect_err("duplicate registration should fail");
241        assert_eq!(err.class, ErrorClass::InvariantViolation);
242        assert_eq!(err.origin, ErrorOrigin::Store);
243        assert!(
244            err.message
245                .contains("store 'store_registry_tests::Store' already registered"),
246            "duplicate registration should include the conflicting path"
247        );
248    }
249
250    #[test]
251    fn alias_store_registration_reusing_same_store_pair_is_rejected() {
252        let mut registry = StoreRegistry::new();
253        registry
254            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
255            .expect("initial store registration should succeed");
256
257        let err = registry
258            .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
259            .expect_err("alias registration reusing the same store pair should fail");
260        assert_eq!(err.class, ErrorClass::InvariantViolation);
261        assert_eq!(err.origin, ErrorOrigin::Store);
262        assert!(
263            err.message.contains(
264                "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
265            ),
266            "alias registration should include conflicting alias path"
267        );
268        assert!(
269            err.message
270                .contains("registered as 'store_registry_tests::Store'"),
271            "alias registration should include original path"
272        );
273    }
274}