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