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/// SecondaryReadAuthoritySnapshot
54///
55/// Immutable authority snapshot for one store-backed secondary read.
56/// This keeps index lifecycle truth and synchronized witness bits together at
57/// the registry boundary so executor authority resolution can consume one
58/// stable input instead of reaching back into the live store handle.
59///
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub(in crate::db) struct SecondaryReadAuthoritySnapshot {
63    index_state: IndexState,
64    secondary_covering_authoritative: bool,
65    secondary_existence_witness_authoritative: bool,
66}
67
68impl SecondaryReadAuthoritySnapshot {
69    // Build one immutable authority snapshot from the current store state.
70    const fn new(
71        index_state: IndexState,
72        secondary_covering_authoritative: bool,
73        secondary_existence_witness_authoritative: bool,
74    ) -> Self {
75        Self {
76            index_state,
77            secondary_covering_authoritative,
78            secondary_existence_witness_authoritative,
79        }
80    }
81
82    // Return the explicit lifecycle state captured for this secondary read.
83    pub(in crate::db) const fn index_state(self) -> IndexState {
84        self.index_state
85    }
86
87    // Return whether this captured index state is probe-free eligible.
88    pub(in crate::db) const fn index_is_valid(self) -> bool {
89        matches!(self.index_state, IndexState::Valid)
90    }
91
92    // Return whether the stronger synchronized pair witness was captured.
93    pub(in crate::db) const fn secondary_covering_authoritative(self) -> bool {
94        self.secondary_covering_authoritative
95    }
96
97    // Return whether the narrower storage existence witness was captured.
98    pub(in crate::db) const fn secondary_existence_witness_authoritative(self) -> bool {
99        self.secondary_existence_witness_authoritative
100    }
101}
102
103///
104/// StoreHandle
105/// Bound pair of row and index stores for one schema `Store` path.
106///
107
108#[derive(Clone, Copy, Debug)]
109pub struct StoreHandle {
110    data: &'static LocalKey<RefCell<DataStore>>,
111    index: &'static LocalKey<RefCell<IndexStore>>,
112}
113
114impl StoreHandle {
115    /// Build a store handle from thread-local row/index stores.
116    #[must_use]
117    pub const fn new(
118        data: &'static LocalKey<RefCell<DataStore>>,
119        index: &'static LocalKey<RefCell<IndexStore>>,
120    ) -> Self {
121        Self { data, index }
122    }
123
124    /// Borrow the row store immutably.
125    pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
126        self.data.with_borrow(f)
127    }
128
129    /// Borrow the row store mutably.
130    pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
131        self.data.with_borrow_mut(f)
132    }
133
134    /// Borrow the index store immutably.
135    pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
136        self.index.with_borrow(f)
137    }
138
139    /// Borrow the index store mutably.
140    pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
141        self.index.with_borrow_mut(f)
142    }
143
144    /// Return the explicit lifecycle state of the bound index store.
145    #[must_use]
146    pub(in crate::db) fn index_state(&self) -> IndexState {
147        self.with_index(IndexStore::state)
148    }
149
150    /// Return whether the bound index store is currently valid for probe-free
151    /// covering authority.
152    #[must_use]
153    pub(in crate::db) fn index_is_valid(&self) -> bool {
154        self.with_index(IndexStore::is_valid)
155    }
156
157    /// Mark the bound index store as Building.
158    pub(in crate::db) fn mark_index_building(&self) {
159        self.with_index_mut(IndexStore::mark_building);
160    }
161
162    /// Mark the bound index store as Valid.
163    pub(in crate::db) fn mark_index_valid(&self) {
164        self.with_index_mut(IndexStore::mark_valid);
165    }
166
167    /// Mark the bound index store as Dropping.
168    pub(in crate::db) fn mark_index_dropping(&self) {
169        self.with_index_mut(IndexStore::mark_dropping);
170    }
171
172    /// Return whether this store pair currently carries a synchronized
173    /// secondary covering-authority witness.
174    #[must_use]
175    pub(in crate::db) fn secondary_covering_authoritative(&self) -> bool {
176        self.with_data(DataStore::secondary_covering_authoritative)
177            && self.with_index(IndexStore::secondary_covering_authoritative)
178    }
179
180    /// Mark this row/index store pair as synchronized for witness-backed
181    /// secondary covering after successful commit or recovery.
182    pub(in crate::db) fn mark_secondary_covering_authoritative(&self) {
183        self.with_data_mut(DataStore::mark_secondary_covering_authoritative);
184        self.with_index_mut(IndexStore::mark_secondary_covering_authoritative);
185    }
186
187    /// Return whether this store pair currently carries one explicit
188    /// storage-owned secondary existence witness contract.
189    #[must_use]
190    pub(in crate::db) fn secondary_existence_witness_authoritative(&self) -> bool {
191        self.with_data(DataStore::secondary_existence_witness_authoritative)
192            && self.with_index(IndexStore::secondary_existence_witness_authoritative)
193    }
194
195    /// Capture one immutable authority snapshot for a single secondary read
196    /// resolution pass. This keeps lifecycle truth at the registry boundary
197    /// instead of letting deeper executor code rediscover it from `StoreHandle`.
198    #[must_use]
199    pub(in crate::db) fn secondary_read_authority_snapshot(
200        &self,
201    ) -> SecondaryReadAuthoritySnapshot {
202        SecondaryReadAuthoritySnapshot::new(
203            self.index_state(),
204            self.secondary_covering_authoritative(),
205            self.secondary_existence_witness_authoritative(),
206        )
207    }
208
209    /// Mark this row/index store pair as synchronized for one explicit
210    /// storage-owned secondary existence witness contract.
211    pub(in crate::db) fn mark_secondary_existence_witness_authoritative(&self) {
212        self.with_data_mut(DataStore::mark_secondary_existence_witness_authoritative);
213        self.with_index_mut(IndexStore::mark_secondary_existence_witness_authoritative);
214    }
215
216    /// Return the raw row-store accessor.
217    #[must_use]
218    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
219        self.data
220    }
221
222    /// Return the raw index-store accessor.
223    #[must_use]
224    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
225        self.index
226    }
227}
228
229///
230/// StoreRegistry
231/// Thread-local registry for both row and index stores.
232///
233
234#[derive(Default)]
235pub struct StoreRegistry {
236    stores: Vec<(&'static str, StoreHandle)>,
237}
238
239impl StoreRegistry {
240    /// Create an empty store registry.
241    #[must_use]
242    pub fn new() -> Self {
243        Self::default()
244    }
245
246    /// Iterate registered stores.
247    ///
248    /// Iteration order follows registration order. Semantic result ordering
249    /// must still not depend on this iteration order; callers that need
250    /// deterministic ordering must sort by store path.
251    pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
252        self.stores.iter().copied()
253    }
254
255    /// Register a `Store` path to its row/index store pair.
256    pub fn register_store(
257        &mut self,
258        name: &'static str,
259        data: &'static LocalKey<RefCell<DataStore>>,
260        index: &'static LocalKey<RefCell<IndexStore>>,
261    ) -> Result<(), InternalError> {
262        if self
263            .stores
264            .iter()
265            .any(|(existing_name, _)| *existing_name == name)
266        {
267            return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
268        }
269
270        // Keep one canonical logical store name per physical row/index store pair.
271        if let Some(existing_name) =
272            self.stores
273                .iter()
274                .find_map(|(existing_name, existing_handle)| {
275                    (std::ptr::eq(existing_handle.data_store(), data)
276                        && std::ptr::eq(existing_handle.index_store(), index))
277                    .then_some(*existing_name)
278                })
279        {
280            return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
281                name: name.to_string(),
282                existing_name: existing_name.to_string(),
283            }
284            .into());
285        }
286
287        self.stores.push((name, StoreHandle::new(data, index)));
288
289        Ok(())
290    }
291
292    /// Look up a store handle by path.
293    pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
294        self.stores
295            .iter()
296            .find_map(|(existing_path, handle)| (*existing_path == path).then_some(*handle))
297            .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
298    }
299}
300
301///
302/// TESTS
303///
304
305#[cfg(test)]
306mod tests {
307    use crate::{
308        db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
309        error::{ErrorClass, ErrorOrigin},
310        testing::test_memory,
311    };
312    use std::{cell::RefCell, ptr};
313
314    const STORE_PATH: &str = "store_registry_tests::Store";
315    const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
316
317    thread_local! {
318        static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
319        static TEST_INDEX_STORE: RefCell<IndexStore> =
320            RefCell::new(IndexStore::init(test_memory(152)));
321    }
322
323    fn test_registry() -> StoreRegistry {
324        let mut registry = StoreRegistry::new();
325        registry
326            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
327            .expect("test store registration should succeed");
328        registry
329    }
330
331    #[test]
332    fn register_store_binds_data_and_index_handles() {
333        let registry = test_registry();
334        let handle = registry
335            .try_get_store(STORE_PATH)
336            .expect("registered store path should resolve");
337
338        assert!(
339            ptr::eq(handle.data_store(), &TEST_DATA_STORE),
340            "store handle should expose the registered data store accessor"
341        );
342        assert!(
343            ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
344            "store handle should expose the registered index store accessor"
345        );
346
347        let data_rows = handle.with_data(|store| store.len());
348        let index_rows = handle.with_index(IndexStore::len);
349        assert_eq!(data_rows, 0, "fresh test data store should be empty");
350        assert_eq!(index_rows, 0, "fresh test index store should be empty");
351    }
352
353    #[test]
354    fn missing_store_path_rejected_before_access() {
355        let registry = StoreRegistry::new();
356        let err = registry
357            .try_get_store("store_registry_tests::Missing")
358            .expect_err("missing path should fail lookup");
359
360        assert_eq!(err.class, ErrorClass::Internal);
361        assert_eq!(err.origin, ErrorOrigin::Store);
362        assert!(
363            err.message
364                .contains("store 'store_registry_tests::Missing' not found"),
365            "missing store lookup should include the missing path"
366        );
367    }
368
369    #[test]
370    fn duplicate_store_registration_is_rejected() {
371        let mut registry = StoreRegistry::new();
372        registry
373            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
374            .expect("initial store registration should succeed");
375
376        let err = registry
377            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
378            .expect_err("duplicate registration should fail");
379        assert_eq!(err.class, ErrorClass::InvariantViolation);
380        assert_eq!(err.origin, ErrorOrigin::Store);
381        assert!(
382            err.message
383                .contains("store 'store_registry_tests::Store' already registered"),
384            "duplicate registration should include the conflicting path"
385        );
386    }
387
388    #[test]
389    fn alias_store_registration_reusing_same_store_pair_is_rejected() {
390        let mut registry = StoreRegistry::new();
391        registry
392            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
393            .expect("initial store registration should succeed");
394
395        let err = registry
396            .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
397            .expect_err("alias registration reusing the same store pair should fail");
398        assert_eq!(err.class, ErrorClass::InvariantViolation);
399        assert_eq!(err.origin, ErrorOrigin::Store);
400        assert!(
401            err.message.contains(
402                "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
403            ),
404            "alias registration should include conflicting alias path"
405        );
406        assert!(
407            err.message
408                .contains("registered as 'store_registry_tests::Store'"),
409            "alias registration should include original path"
410        );
411    }
412}