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    pub fn new() -> Self {
115        Self::default()
116    }
117
118    /// Iterate registered stores.
119    pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
120        self.stores.iter().map(|(k, v)| (*k, *v))
121    }
122
123    /// Register a `Store` path to its row/index store pair.
124    pub fn register_store(
125        &mut self,
126        name: &'static str,
127        data: &'static LocalKey<RefCell<DataStore>>,
128        index: &'static LocalKey<RefCell<IndexStore>>,
129    ) -> Result<(), InternalError> {
130        if self.stores.contains_key(name) {
131            return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
132        }
133
134        self.stores.insert(name, StoreHandle::new(data, index));
135        Ok(())
136    }
137
138    /// Look up a store handle by path.
139    pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
140        self.stores
141            .get(path)
142            .copied()
143            .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use crate::{
150        db::{
151            index::IndexStore,
152            store::{DataStore, StoreRegistry},
153        },
154        error::{ErrorClass, ErrorOrigin},
155        test_support::test_memory,
156    };
157    use std::{cell::RefCell, ptr};
158
159    const STORE_PATH: &str = "store_registry_tests::Store";
160
161    thread_local! {
162        static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
163        static TEST_INDEX_STORE: RefCell<IndexStore> =
164            RefCell::new(IndexStore::init(test_memory(152)));
165    }
166
167    fn test_registry() -> StoreRegistry {
168        let mut registry = StoreRegistry::new();
169        registry
170            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
171            .expect("test store registration should succeed");
172        registry
173    }
174
175    #[test]
176    fn register_store_binds_data_and_index_handles() {
177        let registry = test_registry();
178        let handle = registry
179            .try_get_store(STORE_PATH)
180            .expect("registered store path should resolve");
181
182        assert!(
183            ptr::eq(handle.data_store(), &TEST_DATA_STORE),
184            "store handle should expose the registered data store accessor"
185        );
186        assert!(
187            ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
188            "store handle should expose the registered index store accessor"
189        );
190
191        let data_rows = handle.with_data(|store| store.len());
192        let index_rows = handle.with_index(IndexStore::len);
193        assert_eq!(data_rows, 0, "fresh test data store should be empty");
194        assert_eq!(index_rows, 0, "fresh test index store should be empty");
195    }
196
197    #[test]
198    fn missing_store_path_rejected_before_access() {
199        let registry = StoreRegistry::new();
200        let err = registry
201            .try_get_store("store_registry_tests::Missing")
202            .expect_err("missing path should fail lookup");
203
204        assert_eq!(err.class, ErrorClass::Internal);
205        assert_eq!(err.origin, ErrorOrigin::Store);
206        assert!(
207            err.message
208                .contains("store 'store_registry_tests::Missing' not found"),
209            "missing store lookup should include the missing path"
210        );
211    }
212
213    #[test]
214    fn duplicate_store_registration_is_rejected() {
215        let mut registry = StoreRegistry::new();
216        registry
217            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
218            .expect("initial store registration should succeed");
219
220        let err = registry
221            .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
222            .expect_err("duplicate registration should fail");
223        assert_eq!(err.class, ErrorClass::InvariantViolation);
224        assert_eq!(err.origin, ErrorOrigin::Store);
225        assert!(
226            err.message
227                .contains("store 'store_registry_tests::Store' already registered"),
228            "duplicate registration should include the conflicting path"
229        );
230    }
231}