Skip to main content

icydb_core/db/
registry.rs

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