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)]
51pub struct StoreHandle {
52 data: &'static LocalKey<RefCell<DataStore>>,
53 index: &'static LocalKey<RefCell<IndexStore>>,
54}
55
56impl StoreHandle {
57 #[must_use]
59 pub const fn new(
60 data: &'static LocalKey<RefCell<DataStore>>,
61 index: &'static LocalKey<RefCell<IndexStore>>,
62 ) -> Self {
63 Self { data, index }
64 }
65
66 pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
68 self.data.with_borrow(f)
69 }
70
71 pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
73 self.data.with_borrow_mut(f)
74 }
75
76 pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
78 self.index.with_borrow(f)
79 }
80
81 pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
83 self.index.with_borrow_mut(f)
84 }
85
86 #[must_use]
88 pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
89 self.data
90 }
91
92 #[must_use]
94 pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
95 self.index
96 }
97}
98
99#[derive(Default)]
106pub struct StoreRegistry {
107 stores: HashMap<&'static str, StoreHandle>,
108}
109
110impl StoreRegistry {
111 #[must_use]
113 pub fn new() -> Self {
114 Self::default()
115 }
116
117 pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
123 self.stores.iter().map(|(k, v)| (*k, *v))
124 }
125
126 pub fn register_store(
128 &mut self,
129 name: &'static str,
130 data: &'static LocalKey<RefCell<DataStore>>,
131 index: &'static LocalKey<RefCell<IndexStore>>,
132 ) -> Result<(), InternalError> {
133 if self.stores.contains_key(name) {
134 return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
135 }
136
137 if let Some(existing_name) =
139 self.stores
140 .iter()
141 .find_map(|(existing_name, existing_handle)| {
142 (std::ptr::eq(existing_handle.data_store(), data)
143 && std::ptr::eq(existing_handle.index_store(), index))
144 .then_some(*existing_name)
145 })
146 {
147 return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
148 name: name.to_string(),
149 existing_name: existing_name.to_string(),
150 }
151 .into());
152 }
153
154 self.stores.insert(name, StoreHandle::new(data, index));
155 Ok(())
156 }
157
158 pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
160 self.stores
161 .get(path)
162 .copied()
163 .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use crate::{
170 db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
171 error::{ErrorClass, ErrorOrigin},
172 test_support::test_memory,
173 };
174 use std::{cell::RefCell, ptr};
175
176 const STORE_PATH: &str = "store_registry_tests::Store";
177 const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
178
179 thread_local! {
180 static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
181 static TEST_INDEX_STORE: RefCell<IndexStore> =
182 RefCell::new(IndexStore::init(test_memory(152)));
183 }
184
185 fn test_registry() -> StoreRegistry {
186 let mut registry = StoreRegistry::new();
187 registry
188 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
189 .expect("test store registration should succeed");
190 registry
191 }
192
193 #[test]
194 fn register_store_binds_data_and_index_handles() {
195 let registry = test_registry();
196 let handle = registry
197 .try_get_store(STORE_PATH)
198 .expect("registered store path should resolve");
199
200 assert!(
201 ptr::eq(handle.data_store(), &TEST_DATA_STORE),
202 "store handle should expose the registered data store accessor"
203 );
204 assert!(
205 ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
206 "store handle should expose the registered index store accessor"
207 );
208
209 let data_rows = handle.with_data(|store| store.len());
210 let index_rows = handle.with_index(IndexStore::len);
211 assert_eq!(data_rows, 0, "fresh test data store should be empty");
212 assert_eq!(index_rows, 0, "fresh test index store should be empty");
213 }
214
215 #[test]
216 fn missing_store_path_rejected_before_access() {
217 let registry = StoreRegistry::new();
218 let err = registry
219 .try_get_store("store_registry_tests::Missing")
220 .expect_err("missing path should fail lookup");
221
222 assert_eq!(err.class, ErrorClass::Internal);
223 assert_eq!(err.origin, ErrorOrigin::Store);
224 assert!(
225 err.message
226 .contains("store 'store_registry_tests::Missing' not found"),
227 "missing store lookup should include the missing path"
228 );
229 }
230
231 #[test]
232 fn duplicate_store_registration_is_rejected() {
233 let mut registry = StoreRegistry::new();
234 registry
235 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
236 .expect("initial store registration should succeed");
237
238 let err = registry
239 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
240 .expect_err("duplicate registration should fail");
241 assert_eq!(err.class, ErrorClass::InvariantViolation);
242 assert_eq!(err.origin, ErrorOrigin::Store);
243 assert!(
244 err.message
245 .contains("store 'store_registry_tests::Store' already registered"),
246 "duplicate registration should include the conflicting path"
247 );
248 }
249
250 #[test]
251 fn alias_store_registration_reusing_same_store_pair_is_rejected() {
252 let mut registry = StoreRegistry::new();
253 registry
254 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
255 .expect("initial store registration should succeed");
256
257 let err = registry
258 .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
259 .expect_err("alias registration reusing the same store pair should fail");
260 assert_eq!(err.class, ErrorClass::InvariantViolation);
261 assert_eq!(err.origin, ErrorOrigin::Store);
262 assert!(
263 err.message.contains(
264 "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
265 ),
266 "alias registration should include conflicting alias path"
267 );
268 assert!(
269 err.message
270 .contains("registered as 'store_registry_tests::Store'"),
271 "alias registration should include original path"
272 );
273 }
274}