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