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)]
13pub enum StoreRegistryError {
14 #[error("store '{0}' not found")]
15 StoreNotFound(String),
16
17 #[error("store '{0}' already registered")]
18 StoreAlreadyRegistered(String),
19}
20
21impl StoreRegistryError {
22 pub(crate) const fn class(&self) -> ErrorClass {
23 match self {
24 Self::StoreNotFound(_) => ErrorClass::Internal,
25 Self::StoreAlreadyRegistered(_) => ErrorClass::InvariantViolation,
26 }
27 }
28}
29
30impl From<StoreRegistryError> for InternalError {
31 fn from(err: StoreRegistryError) -> Self {
32 Self::classified(err.class(), ErrorOrigin::Store, err.to_string())
33 }
34}
35
36#[derive(Clone, Copy, Debug)]
43pub struct StoreHandle {
44 data: &'static LocalKey<RefCell<DataStore>>,
45 index: &'static LocalKey<RefCell<IndexStore>>,
46}
47
48impl StoreHandle {
49 #[must_use]
51 pub const fn new(
52 data: &'static LocalKey<RefCell<DataStore>>,
53 index: &'static LocalKey<RefCell<IndexStore>>,
54 ) -> Self {
55 Self { data, index }
56 }
57
58 pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
60 self.data.with_borrow(f)
61 }
62
63 pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
65 self.data.with_borrow_mut(f)
66 }
67
68 pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
70 self.index.with_borrow(f)
71 }
72
73 pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
75 self.index.with_borrow_mut(f)
76 }
77
78 #[must_use]
80 pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
81 self.data
82 }
83
84 #[must_use]
86 pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
87 self.index
88 }
89}
90
91#[derive(Default)]
98pub struct StoreRegistry {
99 stores: HashMap<&'static str, StoreHandle>,
100}
101
102impl StoreRegistry {
103 #[must_use]
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
111 self.stores.iter().map(|(k, v)| (*k, *v))
112 }
113
114 pub fn register_store(
116 &mut self,
117 name: &'static str,
118 data: &'static LocalKey<RefCell<DataStore>>,
119 index: &'static LocalKey<RefCell<IndexStore>>,
120 ) -> Result<(), InternalError> {
121 if self.stores.contains_key(name) {
122 return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
123 }
124
125 self.stores.insert(name, StoreHandle::new(data, index));
126 Ok(())
127 }
128
129 pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
131 self.stores
132 .get(path)
133 .copied()
134 .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use crate::{
141 db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
142 error::{ErrorClass, ErrorOrigin},
143 test_support::test_memory,
144 };
145 use std::{cell::RefCell, ptr};
146
147 const STORE_PATH: &str = "store_registry_tests::Store";
148
149 thread_local! {
150 static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
151 static TEST_INDEX_STORE: RefCell<IndexStore> =
152 RefCell::new(IndexStore::init(test_memory(152)));
153 }
154
155 fn test_registry() -> StoreRegistry {
156 let mut registry = StoreRegistry::new();
157 registry
158 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
159 .expect("test store registration should succeed");
160 registry
161 }
162
163 #[test]
164 fn register_store_binds_data_and_index_handles() {
165 let registry = test_registry();
166 let handle = registry
167 .try_get_store(STORE_PATH)
168 .expect("registered store path should resolve");
169
170 assert!(
171 ptr::eq(handle.data_store(), &TEST_DATA_STORE),
172 "store handle should expose the registered data store accessor"
173 );
174 assert!(
175 ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
176 "store handle should expose the registered index store accessor"
177 );
178
179 let data_rows = handle.with_data(|store| store.len());
180 let index_rows = handle.with_index(IndexStore::len);
181 assert_eq!(data_rows, 0, "fresh test data store should be empty");
182 assert_eq!(index_rows, 0, "fresh test index store should be empty");
183 }
184
185 #[test]
186 fn missing_store_path_rejected_before_access() {
187 let registry = StoreRegistry::new();
188 let err = registry
189 .try_get_store("store_registry_tests::Missing")
190 .expect_err("missing path should fail lookup");
191
192 assert_eq!(err.class, ErrorClass::Internal);
193 assert_eq!(err.origin, ErrorOrigin::Store);
194 assert!(
195 err.message
196 .contains("store 'store_registry_tests::Missing' not found"),
197 "missing store lookup should include the missing path"
198 );
199 }
200
201 #[test]
202 fn duplicate_store_registration_is_rejected() {
203 let mut registry = StoreRegistry::new();
204 registry
205 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
206 .expect("initial store registration should succeed");
207
208 let err = registry
209 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
210 .expect_err("duplicate registration should fail");
211 assert_eq!(err.class, ErrorClass::InvariantViolation);
212 assert_eq!(err.origin, ErrorOrigin::Store);
213 assert!(
214 err.message
215 .contains("store 'store_registry_tests::Store' already registered"),
216 "duplicate registration should include the conflicting path"
217 );
218 }
219}