icydb_core/db/
registry.rs1use crate::{
7 db::{data::DataStore, index::IndexStore},
8 error::{ErrorClass, ErrorOrigin, InternalError},
9};
10use std::{cell::RefCell, 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]
93 pub(in crate::db) fn secondary_covering_authoritative(&self) -> bool {
94 self.with_data(DataStore::secondary_covering_authoritative)
95 && self.with_index(IndexStore::secondary_covering_authoritative)
96 }
97
98 pub(in crate::db) fn mark_secondary_covering_authoritative(&self) {
101 self.with_data_mut(DataStore::mark_secondary_covering_authoritative);
102 self.with_index_mut(IndexStore::mark_secondary_covering_authoritative);
103 }
104
105 #[must_use]
108 pub(in crate::db) fn secondary_existence_witness_authoritative(&self) -> bool {
109 self.with_data(DataStore::secondary_existence_witness_authoritative)
110 && self.with_index(IndexStore::secondary_existence_witness_authoritative)
111 }
112
113 pub(in crate::db) fn mark_secondary_existence_witness_authoritative(&self) {
116 self.with_data_mut(DataStore::mark_secondary_existence_witness_authoritative);
117 self.with_index_mut(IndexStore::mark_secondary_existence_witness_authoritative);
118 }
119
120 #[must_use]
122 pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
123 self.data
124 }
125
126 #[must_use]
128 pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
129 self.index
130 }
131}
132
133#[derive(Default)]
139pub struct StoreRegistry {
140 stores: Vec<(&'static str, StoreHandle)>,
141}
142
143impl StoreRegistry {
144 #[must_use]
146 pub fn new() -> Self {
147 Self::default()
148 }
149
150 pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
156 self.stores.iter().copied()
157 }
158
159 pub fn register_store(
161 &mut self,
162 name: &'static str,
163 data: &'static LocalKey<RefCell<DataStore>>,
164 index: &'static LocalKey<RefCell<IndexStore>>,
165 ) -> Result<(), InternalError> {
166 if self
167 .stores
168 .iter()
169 .any(|(existing_name, _)| *existing_name == name)
170 {
171 return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
172 }
173
174 if let Some(existing_name) =
176 self.stores
177 .iter()
178 .find_map(|(existing_name, existing_handle)| {
179 (std::ptr::eq(existing_handle.data_store(), data)
180 && std::ptr::eq(existing_handle.index_store(), index))
181 .then_some(*existing_name)
182 })
183 {
184 return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
185 name: name.to_string(),
186 existing_name: existing_name.to_string(),
187 }
188 .into());
189 }
190
191 self.stores.push((name, StoreHandle::new(data, index)));
192
193 Ok(())
194 }
195
196 pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
198 self.stores
199 .iter()
200 .find_map(|(existing_path, handle)| (*existing_path == path).then_some(*handle))
201 .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
202 }
203}
204
205#[cfg(test)]
210mod tests {
211 use crate::{
212 db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
213 error::{ErrorClass, ErrorOrigin},
214 testing::test_memory,
215 };
216 use std::{cell::RefCell, ptr};
217
218 const STORE_PATH: &str = "store_registry_tests::Store";
219 const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
220
221 thread_local! {
222 static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
223 static TEST_INDEX_STORE: RefCell<IndexStore> =
224 RefCell::new(IndexStore::init(test_memory(152)));
225 }
226
227 fn test_registry() -> StoreRegistry {
228 let mut registry = StoreRegistry::new();
229 registry
230 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
231 .expect("test store registration should succeed");
232 registry
233 }
234
235 #[test]
236 fn register_store_binds_data_and_index_handles() {
237 let registry = test_registry();
238 let handle = registry
239 .try_get_store(STORE_PATH)
240 .expect("registered store path should resolve");
241
242 assert!(
243 ptr::eq(handle.data_store(), &TEST_DATA_STORE),
244 "store handle should expose the registered data store accessor"
245 );
246 assert!(
247 ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
248 "store handle should expose the registered index store accessor"
249 );
250
251 let data_rows = handle.with_data(|store| store.len());
252 let index_rows = handle.with_index(IndexStore::len);
253 assert_eq!(data_rows, 0, "fresh test data store should be empty");
254 assert_eq!(index_rows, 0, "fresh test index store should be empty");
255 }
256
257 #[test]
258 fn missing_store_path_rejected_before_access() {
259 let registry = StoreRegistry::new();
260 let err = registry
261 .try_get_store("store_registry_tests::Missing")
262 .expect_err("missing path should fail lookup");
263
264 assert_eq!(err.class, ErrorClass::Internal);
265 assert_eq!(err.origin, ErrorOrigin::Store);
266 assert!(
267 err.message
268 .contains("store 'store_registry_tests::Missing' not found"),
269 "missing store lookup should include the missing path"
270 );
271 }
272
273 #[test]
274 fn duplicate_store_registration_is_rejected() {
275 let mut registry = StoreRegistry::new();
276 registry
277 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
278 .expect("initial store registration should succeed");
279
280 let err = registry
281 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
282 .expect_err("duplicate registration should fail");
283 assert_eq!(err.class, ErrorClass::InvariantViolation);
284 assert_eq!(err.origin, ErrorOrigin::Store);
285 assert!(
286 err.message
287 .contains("store 'store_registry_tests::Store' already registered"),
288 "duplicate registration should include the conflicting path"
289 );
290 }
291
292 #[test]
293 fn alias_store_registration_reusing_same_store_pair_is_rejected() {
294 let mut registry = StoreRegistry::new();
295 registry
296 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
297 .expect("initial store registration should succeed");
298
299 let err = registry
300 .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
301 .expect_err("alias registration reusing the same store pair should fail");
302 assert_eq!(err.class, ErrorClass::InvariantViolation);
303 assert_eq!(err.origin, ErrorOrigin::Store);
304 assert!(
305 err.message.contains(
306 "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
307 ),
308 "alias registration should include conflicting alias path"
309 );
310 assert!(
311 err.message
312 .contains("registered as 'store_registry_tests::Store'"),
313 "alias registration should include original path"
314 );
315 }
316}