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