icydb_core/db/
registry.rs1use crate::{
7 db::{
8 data::DataStore,
9 index::{IndexState, IndexStore},
10 },
11 error::{ErrorClass, ErrorOrigin, InternalError},
12};
13use std::{cell::RefCell, thread::LocalKey};
14use thiserror::Error as ThisError;
15
16#[derive(Debug, ThisError)]
21#[expect(clippy::enum_variant_names)]
22pub enum StoreRegistryError {
23 #[error("store '{0}' not found")]
24 StoreNotFound(String),
25
26 #[error("store '{0}' already registered")]
27 StoreAlreadyRegistered(String),
28
29 #[error(
30 "store '{name}' reuses the same row/index store pair already registered as '{existing_name}'"
31 )]
32 StoreHandlePairAlreadyRegistered { name: String, existing_name: String },
33}
34
35impl StoreRegistryError {
36 pub(crate) const fn class(&self) -> ErrorClass {
37 match self {
38 Self::StoreNotFound(_) => ErrorClass::Internal,
39 Self::StoreAlreadyRegistered(_) | Self::StoreHandlePairAlreadyRegistered { .. } => {
40 ErrorClass::InvariantViolation
41 }
42 }
43 }
44}
45
46impl From<StoreRegistryError> for InternalError {
47 fn from(err: StoreRegistryError) -> Self {
48 Self::classified(err.class(), ErrorOrigin::Store, err.to_string())
49 }
50}
51
52#[derive(Clone, Copy, Debug)]
58pub struct StoreHandle {
59 data: &'static LocalKey<RefCell<DataStore>>,
60 index: &'static LocalKey<RefCell<IndexStore>>,
61}
62
63impl StoreHandle {
64 #[must_use]
66 pub const fn new(
67 data: &'static LocalKey<RefCell<DataStore>>,
68 index: &'static LocalKey<RefCell<IndexStore>>,
69 ) -> Self {
70 Self { data, index }
71 }
72
73 pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
75 self.data.with_borrow(f)
76 }
77
78 pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
80 self.data.with_borrow_mut(f)
81 }
82
83 pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
85 self.index.with_borrow(f)
86 }
87
88 pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
90 self.index.with_borrow_mut(f)
91 }
92
93 #[must_use]
95 pub(in crate::db) fn index_state(&self) -> IndexState {
96 self.with_index(IndexStore::state)
97 }
98
99 #[must_use]
102 pub(in crate::db) fn index_is_valid(&self) -> bool {
103 self.with_index(IndexStore::is_valid)
104 }
105
106 pub(in crate::db) fn mark_index_building(&self) {
108 self.with_index_mut(IndexStore::mark_building);
109 }
110
111 pub(in crate::db) fn mark_index_valid(&self) {
113 self.with_index_mut(IndexStore::mark_valid);
114 }
115
116 pub(in crate::db) fn mark_index_dropping(&self) {
118 self.with_index_mut(IndexStore::mark_dropping);
119 }
120
121 #[must_use]
124 pub(in crate::db) fn secondary_covering_authoritative(&self) -> bool {
125 self.with_data(DataStore::secondary_covering_authoritative)
126 && self.with_index(IndexStore::secondary_covering_authoritative)
127 }
128
129 pub(in crate::db) fn mark_secondary_covering_authoritative(&self) {
132 self.with_data_mut(DataStore::mark_secondary_covering_authoritative);
133 self.with_index_mut(IndexStore::mark_secondary_covering_authoritative);
134 }
135
136 #[must_use]
139 pub(in crate::db) fn secondary_existence_witness_authoritative(&self) -> bool {
140 self.with_data(DataStore::secondary_existence_witness_authoritative)
141 && self.with_index(IndexStore::secondary_existence_witness_authoritative)
142 }
143
144 pub(in crate::db) fn mark_secondary_existence_witness_authoritative(&self) {
147 self.with_data_mut(DataStore::mark_secondary_existence_witness_authoritative);
148 self.with_index_mut(IndexStore::mark_secondary_existence_witness_authoritative);
149 }
150
151 #[must_use]
153 pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
154 self.data
155 }
156
157 #[must_use]
159 pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
160 self.index
161 }
162}
163
164#[derive(Default)]
170pub struct StoreRegistry {
171 stores: Vec<(&'static str, StoreHandle)>,
172}
173
174impl StoreRegistry {
175 #[must_use]
177 pub fn new() -> Self {
178 Self::default()
179 }
180
181 pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
187 self.stores.iter().copied()
188 }
189
190 pub fn register_store(
192 &mut self,
193 name: &'static str,
194 data: &'static LocalKey<RefCell<DataStore>>,
195 index: &'static LocalKey<RefCell<IndexStore>>,
196 ) -> Result<(), InternalError> {
197 if self
198 .stores
199 .iter()
200 .any(|(existing_name, _)| *existing_name == name)
201 {
202 return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
203 }
204
205 if let Some(existing_name) =
207 self.stores
208 .iter()
209 .find_map(|(existing_name, existing_handle)| {
210 (std::ptr::eq(existing_handle.data_store(), data)
211 && std::ptr::eq(existing_handle.index_store(), index))
212 .then_some(*existing_name)
213 })
214 {
215 return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
216 name: name.to_string(),
217 existing_name: existing_name.to_string(),
218 }
219 .into());
220 }
221
222 self.stores.push((name, StoreHandle::new(data, index)));
223
224 Ok(())
225 }
226
227 pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
229 self.stores
230 .iter()
231 .find_map(|(existing_path, handle)| (*existing_path == path).then_some(*handle))
232 .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
233 }
234}
235
236#[cfg(test)]
241mod tests {
242 use crate::{
243 db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
244 error::{ErrorClass, ErrorOrigin},
245 testing::test_memory,
246 };
247 use std::{cell::RefCell, ptr};
248
249 const STORE_PATH: &str = "store_registry_tests::Store";
250 const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
251
252 thread_local! {
253 static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
254 static TEST_INDEX_STORE: RefCell<IndexStore> =
255 RefCell::new(IndexStore::init(test_memory(152)));
256 }
257
258 fn test_registry() -> StoreRegistry {
259 let mut registry = StoreRegistry::new();
260 registry
261 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
262 .expect("test store registration should succeed");
263 registry
264 }
265
266 #[test]
267 fn register_store_binds_data_and_index_handles() {
268 let registry = test_registry();
269 let handle = registry
270 .try_get_store(STORE_PATH)
271 .expect("registered store path should resolve");
272
273 assert!(
274 ptr::eq(handle.data_store(), &TEST_DATA_STORE),
275 "store handle should expose the registered data store accessor"
276 );
277 assert!(
278 ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
279 "store handle should expose the registered index store accessor"
280 );
281
282 let data_rows = handle.with_data(|store| store.len());
283 let index_rows = handle.with_index(IndexStore::len);
284 assert_eq!(data_rows, 0, "fresh test data store should be empty");
285 assert_eq!(index_rows, 0, "fresh test index store should be empty");
286 }
287
288 #[test]
289 fn missing_store_path_rejected_before_access() {
290 let registry = StoreRegistry::new();
291 let err = registry
292 .try_get_store("store_registry_tests::Missing")
293 .expect_err("missing path should fail lookup");
294
295 assert_eq!(err.class, ErrorClass::Internal);
296 assert_eq!(err.origin, ErrorOrigin::Store);
297 assert!(
298 err.message
299 .contains("store 'store_registry_tests::Missing' not found"),
300 "missing store lookup should include the missing path"
301 );
302 }
303
304 #[test]
305 fn duplicate_store_registration_is_rejected() {
306 let mut registry = StoreRegistry::new();
307 registry
308 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
309 .expect("initial store registration should succeed");
310
311 let err = registry
312 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
313 .expect_err("duplicate registration should fail");
314 assert_eq!(err.class, ErrorClass::InvariantViolation);
315 assert_eq!(err.origin, ErrorOrigin::Store);
316 assert!(
317 err.message
318 .contains("store 'store_registry_tests::Store' already registered"),
319 "duplicate registration should include the conflicting path"
320 );
321 }
322
323 #[test]
324 fn alias_store_registration_reusing_same_store_pair_is_rejected() {
325 let mut registry = StoreRegistry::new();
326 registry
327 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
328 .expect("initial store registration should succeed");
329
330 let err = registry
331 .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
332 .expect_err("alias registration reusing the same store pair should fail");
333 assert_eq!(err.class, ErrorClass::InvariantViolation);
334 assert_eq!(err.origin, ErrorOrigin::Store);
335 assert!(
336 err.message.contains(
337 "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
338 ),
339 "alias registration should include conflicting alias path"
340 );
341 assert!(
342 err.message
343 .contains("registered as 'store_registry_tests::Store'"),
344 "alias registration should include original path"
345 );
346 }
347}