icydb_core/db/
registry.rs1use crate::{
7 db::{data::DataStore, index::IndexStore},
8 error::{ErrorClass, ErrorOrigin, InternalError},
9};
10use std::{cell::RefCell, collections::HashMap, thread::LocalKey};
11use thiserror::Error as ThisError;
12
13#[derive(Debug, ThisError)]
18#[expect(clippy::enum_variant_names)]
19pub enum StoreRegistryError {
20 #[error("store '{0}' not found")]
21 StoreNotFound(String),
22
23 #[error("store '{0}' already registered")]
24 StoreAlreadyRegistered(String),
25
26 #[error(
27 "store '{name}' reuses the same row/index store pair already registered as '{existing_name}'"
28 )]
29 StoreHandlePairAlreadyRegistered { name: String, existing_name: String },
30}
31
32impl StoreRegistryError {
33 pub(crate) const fn class(&self) -> ErrorClass {
34 match self {
35 Self::StoreNotFound(_) => ErrorClass::Internal,
36 Self::StoreAlreadyRegistered(_) | Self::StoreHandlePairAlreadyRegistered { .. } => {
37 ErrorClass::InvariantViolation
38 }
39 }
40 }
41}
42
43impl From<StoreRegistryError> for InternalError {
44 fn from(err: StoreRegistryError) -> Self {
45 Self::classified(err.class(), ErrorOrigin::Store, err.to_string())
46 }
47}
48
49#[derive(Clone, Copy, Debug)]
55pub struct StoreHandle {
56 data: &'static LocalKey<RefCell<DataStore>>,
57 index: &'static LocalKey<RefCell<IndexStore>>,
58}
59
60impl StoreHandle {
61 #[must_use]
63 pub const fn new(
64 data: &'static LocalKey<RefCell<DataStore>>,
65 index: &'static LocalKey<RefCell<IndexStore>>,
66 ) -> Self {
67 Self { data, index }
68 }
69
70 pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
72 self.data.with_borrow(f)
73 }
74
75 pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
77 self.data.with_borrow_mut(f)
78 }
79
80 pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
82 self.index.with_borrow(f)
83 }
84
85 pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
87 self.index.with_borrow_mut(f)
88 }
89
90 #[must_use]
92 pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
93 self.data
94 }
95
96 #[must_use]
98 pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
99 self.index
100 }
101}
102
103#[derive(Default)]
109pub struct StoreRegistry {
110 stores: HashMap<&'static str, StoreHandle>,
111}
112
113impl StoreRegistry {
114 #[must_use]
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
126 self.stores.iter().map(|(k, v)| (*k, *v))
127 }
128
129 pub fn register_store(
131 &mut self,
132 name: &'static str,
133 data: &'static LocalKey<RefCell<DataStore>>,
134 index: &'static LocalKey<RefCell<IndexStore>>,
135 ) -> Result<(), InternalError> {
136 if self.stores.contains_key(name) {
137 return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
138 }
139
140 if let Some(existing_name) =
142 self.stores
143 .iter()
144 .find_map(|(existing_name, existing_handle)| {
145 (std::ptr::eq(existing_handle.data_store(), data)
146 && std::ptr::eq(existing_handle.index_store(), index))
147 .then_some(*existing_name)
148 })
149 {
150 return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
151 name: name.to_string(),
152 existing_name: existing_name.to_string(),
153 }
154 .into());
155 }
156
157 self.stores.insert(name, StoreHandle::new(data, index));
158
159 Ok(())
160 }
161
162 pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
164 self.stores
165 .get(path)
166 .copied()
167 .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
168 }
169}
170
171#[cfg(test)]
176mod tests {
177 use crate::{
178 db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
179 error::{ErrorClass, ErrorOrigin},
180 testing::test_memory,
181 };
182 use std::{cell::RefCell, ptr};
183
184 const STORE_PATH: &str = "store_registry_tests::Store";
185 const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
186
187 thread_local! {
188 static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
189 static TEST_INDEX_STORE: RefCell<IndexStore> =
190 RefCell::new(IndexStore::init(test_memory(152)));
191 }
192
193 fn test_registry() -> StoreRegistry {
194 let mut registry = StoreRegistry::new();
195 registry
196 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
197 .expect("test store registration should succeed");
198 registry
199 }
200
201 #[test]
202 fn register_store_binds_data_and_index_handles() {
203 let registry = test_registry();
204 let handle = registry
205 .try_get_store(STORE_PATH)
206 .expect("registered store path should resolve");
207
208 assert!(
209 ptr::eq(handle.data_store(), &TEST_DATA_STORE),
210 "store handle should expose the registered data store accessor"
211 );
212 assert!(
213 ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
214 "store handle should expose the registered index store accessor"
215 );
216
217 let data_rows = handle.with_data(|store| store.len());
218 let index_rows = handle.with_index(IndexStore::len);
219 assert_eq!(data_rows, 0, "fresh test data store should be empty");
220 assert_eq!(index_rows, 0, "fresh test index store should be empty");
221 }
222
223 #[test]
224 fn missing_store_path_rejected_before_access() {
225 let registry = StoreRegistry::new();
226 let err = registry
227 .try_get_store("store_registry_tests::Missing")
228 .expect_err("missing path should fail lookup");
229
230 assert_eq!(err.class, ErrorClass::Internal);
231 assert_eq!(err.origin, ErrorOrigin::Store);
232 assert!(
233 err.message
234 .contains("store 'store_registry_tests::Missing' not found"),
235 "missing store lookup should include the missing path"
236 );
237 }
238
239 #[test]
240 fn duplicate_store_registration_is_rejected() {
241 let mut registry = StoreRegistry::new();
242 registry
243 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
244 .expect("initial store registration should succeed");
245
246 let err = registry
247 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
248 .expect_err("duplicate registration should fail");
249 assert_eq!(err.class, ErrorClass::InvariantViolation);
250 assert_eq!(err.origin, ErrorOrigin::Store);
251 assert!(
252 err.message
253 .contains("store 'store_registry_tests::Store' already registered"),
254 "duplicate registration should include the conflicting path"
255 );
256 }
257
258 #[test]
259 fn alias_store_registration_reusing_same_store_pair_is_rejected() {
260 let mut registry = StoreRegistry::new();
261 registry
262 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
263 .expect("initial store registration should succeed");
264
265 let err = registry
266 .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
267 .expect_err("alias registration reusing the same store pair should fail");
268 assert_eq!(err.class, ErrorClass::InvariantViolation);
269 assert_eq!(err.origin, ErrorOrigin::Store);
270 assert!(
271 err.message.contains(
272 "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
273 ),
274 "alias registration should include conflicting alias path"
275 );
276 assert!(
277 err.message
278 .contains("registered as 'store_registry_tests::Store'"),
279 "alias registration should include original path"
280 );
281 }
282}