Skip to main content

icydb_core/db/store/
mod.rs

1mod data;
2mod data_key;
3mod row;
4mod storage_key;
5
6pub use data::*;
7pub use data_key::*;
8pub use row::*;
9pub use storage_key::*;
10
11use crate::{
12    db::index::IndexStore,
13    error::{ErrorClass, ErrorOrigin, InternalError},
14};
15use std::{cell::RefCell, collections::HashMap, thread::LocalKey};
16use thiserror::Error as ThisError;
17
18///
19/// StoreRegistryError
20///
21
22#[derive(Debug, ThisError)]
23pub enum StoreRegistryError {
24    #[error("store '{0}' not found")]
25    StoreNotFound(String),
26    #[error("store '{0}' already registered")]
27    StoreAlreadyRegistered(String),
28}
29
30impl StoreRegistryError {
31    pub(crate) const fn class(&self) -> ErrorClass {
32        match self {
33            Self::StoreNotFound(_) => ErrorClass::Internal,
34            Self::StoreAlreadyRegistered(_) => ErrorClass::InvariantViolation,
35        }
36    }
37}
38
39impl From<StoreRegistryError> for InternalError {
40    fn from(err: StoreRegistryError) -> Self {
41        Self::new(err.class(), ErrorOrigin::Store, err.to_string())
42    }
43}
44
45///
46/// StoreHandle
47///
48/// Bound pair of row and index stores for one schema `Store` path.
49///
50
51#[derive(Clone, Copy, Debug)]
52pub struct StoreHandle {
53    data: &'static LocalKey<RefCell<DataStore>>,
54    index: &'static LocalKey<RefCell<IndexStore>>,
55}
56
57impl StoreHandle {
58    /// Build a store handle from thread-local row/index stores.
59    #[must_use]
60    pub const fn new(
61        data: &'static LocalKey<RefCell<DataStore>>,
62        index: &'static LocalKey<RefCell<IndexStore>>,
63    ) -> Self {
64        Self { data, index }
65    }
66
67    /// Borrow the row store immutably.
68    pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
69        self.data.with_borrow(f)
70    }
71
72    /// Borrow the row store mutably.
73    pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
74        self.data.with_borrow_mut(f)
75    }
76
77    /// Borrow the index store immutably.
78    pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
79        self.index.with_borrow(f)
80    }
81
82    /// Borrow the index store mutably.
83    pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
84        self.index.with_borrow_mut(f)
85    }
86
87    /// Return the raw row-store accessor.
88    #[must_use]
89    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
90        self.data
91    }
92
93    /// Return the raw index-store accessor.
94    #[must_use]
95    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
96        self.index
97    }
98}
99
100///
101/// StoreRegistry
102///
103/// Thread-local registry for both row and index stores.
104///
105
106#[derive(Default)]
107pub struct StoreRegistry {
108    stores: HashMap<&'static str, StoreHandle>,
109}
110
111impl StoreRegistry {
112    /// Create an empty store registry.
113    #[must_use]
114    #[allow(clippy::new_without_default)]
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Iterate registered stores.
120    pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
121        self.stores.iter().map(|(k, v)| (*k, *v))
122    }
123
124    /// Register a `Store` path to its row/index store pair.
125    pub fn register_store(
126        &mut self,
127        name: &'static str,
128        data: &'static LocalKey<RefCell<DataStore>>,
129        index: &'static LocalKey<RefCell<IndexStore>>,
130    ) -> Result<(), InternalError> {
131        if self.stores.contains_key(name) {
132            return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
133        }
134
135        self.stores.insert(name, StoreHandle::new(data, index));
136        Ok(())
137    }
138
139    /// Look up a store handle by path.
140    pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
141        self.stores
142            .get(path)
143            .copied()
144            .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use crate::{
151        db::{
152            index::IndexStore,
153            store::{DataStore, StoreRegistry},
154        },
155        error::{ErrorClass, ErrorOrigin},
156        test_support::test_memory,
157    };
158    use std::{cell::RefCell, ptr};
159
160    const STORE_PATH: &str = "store_registry_tests::Store";
161
162    thread_local! {
163        static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
164        static TEST_INDEX_STORE: RefCell<IndexStore> =
165            RefCell::new(IndexStore::init(test_memory(152)));
166    }
167
168    fn test_registry() -> StoreRegistry {
169        let mut registry = StoreRegistry::new();
170        registry
171            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
172            .expect("test store registration should succeed");
173        registry
174    }
175
176    #[test]
177    fn register_store_binds_data_and_index_handles() {
178        let registry = test_registry();
179        let handle = registry
180            .try_get_store(STORE_PATH)
181            .expect("registered store path should resolve");
182
183        assert!(
184            ptr::eq(handle.data_store(), &TEST_DATA_STORE),
185            "store handle should expose the registered data store accessor"
186        );
187        assert!(
188            ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
189            "store handle should expose the registered index store accessor"
190        );
191
192        let data_rows = handle.with_data(|store| store.len());
193        let index_rows = handle.with_index(IndexStore::len);
194        assert_eq!(data_rows, 0, "fresh test data store should be empty");
195        assert_eq!(index_rows, 0, "fresh test index store should be empty");
196    }
197
198    #[test]
199    fn missing_store_path_rejected_before_access() {
200        let registry = StoreRegistry::new();
201        let err = registry
202            .try_get_store("store_registry_tests::Missing")
203            .expect_err("missing path should fail lookup");
204
205        assert_eq!(err.class, ErrorClass::Internal);
206        assert_eq!(err.origin, ErrorOrigin::Store);
207        assert!(
208            err.message
209                .contains("store 'store_registry_tests::Missing' not found"),
210            "missing store lookup should include the missing path"
211        );
212    }
213
214    #[test]
215    fn duplicate_store_registration_is_rejected() {
216        let mut registry = StoreRegistry::new();
217        registry
218            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
219            .expect("initial store registration should succeed");
220
221        let err = registry
222            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
223            .expect_err("duplicate registration should fail");
224        assert_eq!(err.class, ErrorClass::InvariantViolation);
225        assert_eq!(err.origin, ErrorOrigin::Store);
226        assert!(
227            err.message
228                .contains("store 'store_registry_tests::Store' already registered"),
229            "duplicate registration should include the conflicting path"
230        );
231    }
232}