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::{
8        data::DataStore,
9        index::{IndexState, IndexStore},
10    },
11    error::{ErrorClass, ErrorOrigin, InternalError},
12};
13use std::{cell::RefCell, thread::LocalKey};
14use thiserror::Error as ThisError;
15
16///
17/// StoreRegistryError
18///
19
20#[derive(Debug, ThisError)]
21#[expect(clippy::enum_variant_names)]
22pub enum StoreRegistryError {
23    #[error("store '{0}' not found")]
24    StoreNotFound(String),
25
26    #[error("store '{0}' already registered")]
27    StoreAlreadyRegistered(String),
28
29    #[error(
30        "store '{name}' reuses the same row/index store pair already registered as '{existing_name}'"
31    )]
32    StoreHandlePairAlreadyRegistered { name: String, existing_name: String },
33}
34
35impl StoreRegistryError {
36    pub(crate) const fn class(&self) -> ErrorClass {
37        match self {
38            Self::StoreNotFound(_) => ErrorClass::Internal,
39            Self::StoreAlreadyRegistered(_) | Self::StoreHandlePairAlreadyRegistered { .. } => {
40                ErrorClass::InvariantViolation
41            }
42        }
43    }
44}
45
46impl From<StoreRegistryError> for InternalError {
47    fn from(err: StoreRegistryError) -> Self {
48        Self::classified(err.class(), ErrorOrigin::Store, err.to_string())
49    }
50}
51
52///
53/// StoreHandle
54/// Bound pair of row and index stores for one schema `Store` path.
55///
56
57#[derive(Clone, Copy, Debug)]
58pub struct StoreHandle {
59    data: &'static LocalKey<RefCell<DataStore>>,
60    index: &'static LocalKey<RefCell<IndexStore>>,
61}
62
63impl StoreHandle {
64    /// Build a store handle from thread-local row/index stores.
65    #[must_use]
66    pub const fn new(
67        data: &'static LocalKey<RefCell<DataStore>>,
68        index: &'static LocalKey<RefCell<IndexStore>>,
69    ) -> Self {
70        Self { data, index }
71    }
72
73    /// Borrow the row store immutably.
74    pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
75        self.data.with_borrow(f)
76    }
77
78    /// Borrow the row store mutably.
79    pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
80        self.data.with_borrow_mut(f)
81    }
82
83    /// Borrow the index store immutably.
84    pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
85        self.index.with_borrow(f)
86    }
87
88    /// Borrow the index store mutably.
89    pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
90        self.index.with_borrow_mut(f)
91    }
92
93    /// Return the explicit lifecycle state of the bound index store.
94    #[must_use]
95    pub(in crate::db) fn index_state(&self) -> IndexState {
96        self.with_index(IndexStore::state)
97    }
98
99    /// Return whether the bound index store is currently valid for probe-free
100    /// covering authority.
101    #[must_use]
102    pub(in crate::db) fn index_is_valid(&self) -> bool {
103        self.with_index(IndexStore::is_valid)
104    }
105
106    /// Mark the bound index store as Building.
107    pub(in crate::db) fn mark_index_building(&self) {
108        self.with_index_mut(IndexStore::mark_building);
109    }
110
111    /// Mark the bound index store as Valid.
112    pub(in crate::db) fn mark_index_valid(&self) {
113        self.with_index_mut(IndexStore::mark_valid);
114    }
115
116    /// Mark the bound index store as Dropping.
117    pub(in crate::db) fn mark_index_dropping(&self) {
118        self.with_index_mut(IndexStore::mark_dropping);
119    }
120
121    /// Return whether this store pair currently carries a synchronized
122    /// secondary covering-authority witness.
123    #[must_use]
124    pub(in crate::db) fn secondary_covering_authoritative(&self) -> bool {
125        self.with_data(DataStore::secondary_covering_authoritative)
126            && self.with_index(IndexStore::secondary_covering_authoritative)
127    }
128
129    /// Mark this row/index store pair as synchronized for witness-backed
130    /// secondary covering after successful commit or recovery.
131    pub(in crate::db) fn mark_secondary_covering_authoritative(&self) {
132        self.with_data_mut(DataStore::mark_secondary_covering_authoritative);
133        self.with_index_mut(IndexStore::mark_secondary_covering_authoritative);
134    }
135
136    /// Return whether this store pair currently carries one explicit
137    /// storage-owned secondary existence witness contract.
138    #[must_use]
139    pub(in crate::db) fn secondary_existence_witness_authoritative(&self) -> bool {
140        self.with_data(DataStore::secondary_existence_witness_authoritative)
141            && self.with_index(IndexStore::secondary_existence_witness_authoritative)
142    }
143
144    /// Mark this row/index store pair as synchronized for one explicit
145    /// storage-owned secondary existence witness contract.
146    pub(in crate::db) fn mark_secondary_existence_witness_authoritative(&self) {
147        self.with_data_mut(DataStore::mark_secondary_existence_witness_authoritative);
148        self.with_index_mut(IndexStore::mark_secondary_existence_witness_authoritative);
149    }
150
151    /// Return the raw row-store accessor.
152    #[must_use]
153    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
154        self.data
155    }
156
157    /// Return the raw index-store accessor.
158    #[must_use]
159    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
160        self.index
161    }
162}
163
164///
165/// StoreRegistry
166/// Thread-local registry for both row and index stores.
167///
168
169#[derive(Default)]
170pub struct StoreRegistry {
171    stores: Vec<(&'static str, StoreHandle)>,
172}
173
174impl StoreRegistry {
175    /// Create an empty store registry.
176    #[must_use]
177    pub fn new() -> Self {
178        Self::default()
179    }
180
181    /// Iterate registered stores.
182    ///
183    /// Iteration order follows registration order. Semantic result ordering
184    /// must still not depend on this iteration order; callers that need
185    /// deterministic ordering must sort by store path.
186    pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
187        self.stores.iter().copied()
188    }
189
190    /// Register a `Store` path to its row/index store pair.
191    pub fn register_store(
192        &mut self,
193        name: &'static str,
194        data: &'static LocalKey<RefCell<DataStore>>,
195        index: &'static LocalKey<RefCell<IndexStore>>,
196    ) -> Result<(), InternalError> {
197        if self
198            .stores
199            .iter()
200            .any(|(existing_name, _)| *existing_name == name)
201        {
202            return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
203        }
204
205        // Keep one canonical logical store name per physical row/index store pair.
206        if let Some(existing_name) =
207            self.stores
208                .iter()
209                .find_map(|(existing_name, existing_handle)| {
210                    (std::ptr::eq(existing_handle.data_store(), data)
211                        && std::ptr::eq(existing_handle.index_store(), index))
212                    .then_some(*existing_name)
213                })
214        {
215            return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
216                name: name.to_string(),
217                existing_name: existing_name.to_string(),
218            }
219            .into());
220        }
221
222        self.stores.push((name, StoreHandle::new(data, index)));
223
224        Ok(())
225    }
226
227    /// Look up a store handle by path.
228    pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
229        self.stores
230            .iter()
231            .find_map(|(existing_path, handle)| (*existing_path == path).then_some(*handle))
232            .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
233    }
234}
235
236///
237/// TESTS
238///
239
240#[cfg(test)]
241mod tests {
242    use crate::{
243        db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
244        error::{ErrorClass, ErrorOrigin},
245        testing::test_memory,
246    };
247    use std::{cell::RefCell, ptr};
248
249    const STORE_PATH: &str = "store_registry_tests::Store";
250    const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
251
252    thread_local! {
253        static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
254        static TEST_INDEX_STORE: RefCell<IndexStore> =
255            RefCell::new(IndexStore::init(test_memory(152)));
256    }
257
258    fn test_registry() -> StoreRegistry {
259        let mut registry = StoreRegistry::new();
260        registry
261            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
262            .expect("test store registration should succeed");
263        registry
264    }
265
266    #[test]
267    fn register_store_binds_data_and_index_handles() {
268        let registry = test_registry();
269        let handle = registry
270            .try_get_store(STORE_PATH)
271            .expect("registered store path should resolve");
272
273        assert!(
274            ptr::eq(handle.data_store(), &TEST_DATA_STORE),
275            "store handle should expose the registered data store accessor"
276        );
277        assert!(
278            ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
279            "store handle should expose the registered index store accessor"
280        );
281
282        let data_rows = handle.with_data(|store| store.len());
283        let index_rows = handle.with_index(IndexStore::len);
284        assert_eq!(data_rows, 0, "fresh test data store should be empty");
285        assert_eq!(index_rows, 0, "fresh test index store should be empty");
286    }
287
288    #[test]
289    fn missing_store_path_rejected_before_access() {
290        let registry = StoreRegistry::new();
291        let err = registry
292            .try_get_store("store_registry_tests::Missing")
293            .expect_err("missing path should fail lookup");
294
295        assert_eq!(err.class, ErrorClass::Internal);
296        assert_eq!(err.origin, ErrorOrigin::Store);
297        assert!(
298            err.message
299                .contains("store 'store_registry_tests::Missing' not found"),
300            "missing store lookup should include the missing path"
301        );
302    }
303
304    #[test]
305    fn duplicate_store_registration_is_rejected() {
306        let mut registry = StoreRegistry::new();
307        registry
308            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
309            .expect("initial store registration should succeed");
310
311        let err = registry
312            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
313            .expect_err("duplicate registration should fail");
314        assert_eq!(err.class, ErrorClass::InvariantViolation);
315        assert_eq!(err.origin, ErrorOrigin::Store);
316        assert!(
317            err.message
318                .contains("store 'store_registry_tests::Store' already registered"),
319            "duplicate registration should include the conflicting path"
320        );
321    }
322
323    #[test]
324    fn alias_store_registration_reusing_same_store_pair_is_rejected() {
325        let mut registry = StoreRegistry::new();
326        registry
327            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
328            .expect("initial store registration should succeed");
329
330        let err = registry
331            .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
332            .expect_err("alias registration reusing the same store pair should fail");
333        assert_eq!(err.class, ErrorClass::InvariantViolation);
334        assert_eq!(err.origin, ErrorOrigin::Store);
335        assert!(
336            err.message.contains(
337                "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
338            ),
339            "alias registration should include conflicting alias path"
340        );
341        assert!(
342            err.message
343                .contains("registered as 'store_registry_tests::Store'"),
344            "alias registration should include original path"
345        );
346    }
347}