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, 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 whether this store pair currently carries a synchronized
91    /// secondary covering-authority witness.
92    #[must_use]
93    pub(in crate::db) fn secondary_covering_authoritative(&self) -> bool {
94        self.with_data(DataStore::secondary_covering_authoritative)
95            && self.with_index(IndexStore::secondary_covering_authoritative)
96    }
97
98    /// Mark this row/index store pair as synchronized for witness-backed
99    /// secondary covering after successful commit or recovery.
100    pub(in crate::db) fn mark_secondary_covering_authoritative(&self) {
101        self.with_data_mut(DataStore::mark_secondary_covering_authoritative);
102        self.with_index_mut(IndexStore::mark_secondary_covering_authoritative);
103    }
104
105    /// Return whether this store pair currently carries one explicit
106    /// storage-owned secondary existence witness contract.
107    #[must_use]
108    pub(in crate::db) fn secondary_existence_witness_authoritative(&self) -> bool {
109        self.with_data(DataStore::secondary_existence_witness_authoritative)
110            && self.with_index(IndexStore::secondary_existence_witness_authoritative)
111    }
112
113    /// Mark this row/index store pair as synchronized for one explicit
114    /// storage-owned secondary existence witness contract.
115    pub(in crate::db) fn mark_secondary_existence_witness_authoritative(&self) {
116        self.with_data_mut(DataStore::mark_secondary_existence_witness_authoritative);
117        self.with_index_mut(IndexStore::mark_secondary_existence_witness_authoritative);
118    }
119
120    /// Return the raw row-store accessor.
121    #[must_use]
122    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
123        self.data
124    }
125
126    /// Return the raw index-store accessor.
127    #[must_use]
128    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
129        self.index
130    }
131}
132
133///
134/// StoreRegistry
135/// Thread-local registry for both row and index stores.
136///
137
138#[derive(Default)]
139pub struct StoreRegistry {
140    stores: Vec<(&'static str, StoreHandle)>,
141}
142
143impl StoreRegistry {
144    /// Create an empty store registry.
145    #[must_use]
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Iterate registered stores.
151    ///
152    /// Iteration order follows registration order. Semantic result ordering
153    /// must still not depend on this iteration order; callers that need
154    /// deterministic ordering must sort by store path.
155    pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
156        self.stores.iter().copied()
157    }
158
159    /// Register a `Store` path to its row/index store pair.
160    pub fn register_store(
161        &mut self,
162        name: &'static str,
163        data: &'static LocalKey<RefCell<DataStore>>,
164        index: &'static LocalKey<RefCell<IndexStore>>,
165    ) -> Result<(), InternalError> {
166        if self
167            .stores
168            .iter()
169            .any(|(existing_name, _)| *existing_name == name)
170        {
171            return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
172        }
173
174        // Keep one canonical logical store name per physical row/index store pair.
175        if let Some(existing_name) =
176            self.stores
177                .iter()
178                .find_map(|(existing_name, existing_handle)| {
179                    (std::ptr::eq(existing_handle.data_store(), data)
180                        && std::ptr::eq(existing_handle.index_store(), index))
181                    .then_some(*existing_name)
182                })
183        {
184            return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
185                name: name.to_string(),
186                existing_name: existing_name.to_string(),
187            }
188            .into());
189        }
190
191        self.stores.push((name, StoreHandle::new(data, index)));
192
193        Ok(())
194    }
195
196    /// Look up a store handle by path.
197    pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
198        self.stores
199            .iter()
200            .find_map(|(existing_path, handle)| (*existing_path == path).then_some(*handle))
201            .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
202    }
203}
204
205///
206/// TESTS
207///
208
209#[cfg(test)]
210mod tests {
211    use crate::{
212        db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
213        error::{ErrorClass, ErrorOrigin},
214        testing::test_memory,
215    };
216    use std::{cell::RefCell, ptr};
217
218    const STORE_PATH: &str = "store_registry_tests::Store";
219    const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
220
221    thread_local! {
222        static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
223        static TEST_INDEX_STORE: RefCell<IndexStore> =
224            RefCell::new(IndexStore::init(test_memory(152)));
225    }
226
227    fn test_registry() -> StoreRegistry {
228        let mut registry = StoreRegistry::new();
229        registry
230            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
231            .expect("test store registration should succeed");
232        registry
233    }
234
235    #[test]
236    fn register_store_binds_data_and_index_handles() {
237        let registry = test_registry();
238        let handle = registry
239            .try_get_store(STORE_PATH)
240            .expect("registered store path should resolve");
241
242        assert!(
243            ptr::eq(handle.data_store(), &TEST_DATA_STORE),
244            "store handle should expose the registered data store accessor"
245        );
246        assert!(
247            ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
248            "store handle should expose the registered index store accessor"
249        );
250
251        let data_rows = handle.with_data(|store| store.len());
252        let index_rows = handle.with_index(IndexStore::len);
253        assert_eq!(data_rows, 0, "fresh test data store should be empty");
254        assert_eq!(index_rows, 0, "fresh test index store should be empty");
255    }
256
257    #[test]
258    fn missing_store_path_rejected_before_access() {
259        let registry = StoreRegistry::new();
260        let err = registry
261            .try_get_store("store_registry_tests::Missing")
262            .expect_err("missing path should fail lookup");
263
264        assert_eq!(err.class, ErrorClass::Internal);
265        assert_eq!(err.origin, ErrorOrigin::Store);
266        assert!(
267            err.message
268                .contains("store 'store_registry_tests::Missing' not found"),
269            "missing store lookup should include the missing path"
270        );
271    }
272
273    #[test]
274    fn duplicate_store_registration_is_rejected() {
275        let mut registry = StoreRegistry::new();
276        registry
277            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
278            .expect("initial store registration should succeed");
279
280        let err = registry
281            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
282            .expect_err("duplicate registration should fail");
283        assert_eq!(err.class, ErrorClass::InvariantViolation);
284        assert_eq!(err.origin, ErrorOrigin::Store);
285        assert!(
286            err.message
287                .contains("store 'store_registry_tests::Store' already registered"),
288            "duplicate registration should include the conflicting path"
289        );
290    }
291
292    #[test]
293    fn alias_store_registration_reusing_same_store_pair_is_rejected() {
294        let mut registry = StoreRegistry::new();
295        registry
296            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
297            .expect("initial store registration should succeed");
298
299        let err = registry
300            .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
301            .expect_err("alias registration reusing the same store pair should fail");
302        assert_eq!(err.class, ErrorClass::InvariantViolation);
303        assert_eq!(err.origin, ErrorOrigin::Store);
304        assert!(
305            err.message.contains(
306                "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
307            ),
308            "alias registration should include conflicting alias path"
309        );
310        assert!(
311            err.message
312                .contains("registered as 'store_registry_tests::Store'"),
313            "alias registration should include original path"
314        );
315    }
316}