Skip to main content

icydb_core/db/
registry.rs

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