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