1use crate::{
7 db::{
8 data::DataStore,
9 data::{DataKey, RawRow, StorageKey},
10 direction::Direction,
11 index::{
12 IndexState, IndexStore, RawIndexEntry, RawIndexKey, SealedStructuralIndexEntryReader,
13 SealedStructuralPrimaryRowReader, StructuralIndexEntryReader,
14 StructuralPrimaryRowReader,
15 },
16 },
17 error::{ErrorClass, ErrorOrigin, InternalError},
18 model::index::IndexModel,
19 types::EntityTag,
20};
21use std::{cell::RefCell, ops::Bound, thread::LocalKey};
22use thiserror::Error as ThisError;
23
24#[derive(Debug, ThisError)]
29#[expect(clippy::enum_variant_names)]
30pub enum StoreRegistryError {
31 #[error("store '{0}' not found")]
32 StoreNotFound(String),
33
34 #[error("store '{0}' already registered")]
35 StoreAlreadyRegistered(String),
36
37 #[error(
38 "store '{name}' reuses the same row/index store pair already registered as '{existing_name}'"
39 )]
40 StoreHandlePairAlreadyRegistered { name: String, existing_name: String },
41}
42
43impl StoreRegistryError {
44 pub(crate) const fn class(&self) -> ErrorClass {
45 match self {
46 Self::StoreNotFound(_) => ErrorClass::Internal,
47 Self::StoreAlreadyRegistered(_) | Self::StoreHandlePairAlreadyRegistered { .. } => {
48 ErrorClass::InvariantViolation
49 }
50 }
51 }
52}
53
54impl From<StoreRegistryError> for InternalError {
55 fn from(err: StoreRegistryError) -> Self {
56 Self::classified(err.class(), ErrorOrigin::Store, err.to_string())
57 }
58}
59
60#[derive(Clone, Copy, Debug)]
66pub struct StoreHandle {
67 data: &'static LocalKey<RefCell<DataStore>>,
68 index: &'static LocalKey<RefCell<IndexStore>>,
69}
70
71impl StoreHandle {
72 #[must_use]
74 pub const fn new(
75 data: &'static LocalKey<RefCell<DataStore>>,
76 index: &'static LocalKey<RefCell<IndexStore>>,
77 ) -> Self {
78 Self { data, index }
79 }
80
81 pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
83 #[cfg(feature = "diagnostics")]
84 {
85 crate::db::physical_access::measure_physical_access_operation(|| {
86 self.data.with_borrow(f)
87 })
88 }
89
90 #[cfg(not(feature = "diagnostics"))]
91 {
92 self.data.with_borrow(f)
93 }
94 }
95
96 pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
98 self.data.with_borrow_mut(f)
99 }
100
101 pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
103 #[cfg(feature = "diagnostics")]
104 {
105 crate::db::physical_access::measure_physical_access_operation(|| {
106 self.index.with_borrow(f)
107 })
108 }
109
110 #[cfg(not(feature = "diagnostics"))]
111 {
112 self.index.with_borrow(f)
113 }
114 }
115
116 pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
118 self.index.with_borrow_mut(f)
119 }
120
121 #[must_use]
123 pub(in crate::db) fn index_state(&self) -> IndexState {
124 self.with_index(IndexStore::state)
125 }
126
127 pub(in crate::db) fn mark_index_building(&self) {
129 self.with_index_mut(IndexStore::mark_building);
130 }
131
132 pub(in crate::db) fn mark_index_ready(&self) {
134 self.with_index_mut(IndexStore::mark_ready);
135 }
136
137 #[must_use]
139 pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
140 self.data
141 }
142
143 #[must_use]
145 pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
146 self.index
147 }
148}
149
150impl StructuralPrimaryRowReader for StoreHandle {
151 fn read_primary_row_structural(&self, key: &DataKey) -> Result<Option<RawRow>, InternalError> {
152 let raw_key = key.to_raw()?;
153
154 Ok(self.with_data(|store| store.get(&raw_key)))
155 }
156}
157
158impl SealedStructuralPrimaryRowReader for StoreHandle {}
159
160impl StructuralIndexEntryReader for StoreHandle {
161 fn read_index_entry_structural(
162 &self,
163 store: &'static LocalKey<RefCell<IndexStore>>,
164 key: &RawIndexKey,
165 ) -> Result<Option<RawIndexEntry>, InternalError> {
166 Ok(store.with_borrow(|index_store| index_store.get(key)))
167 }
168
169 fn read_index_keys_in_raw_range_structural(
170 &self,
171 _entity_path: &'static str,
172 _entity_tag: EntityTag,
173 store: &'static LocalKey<RefCell<IndexStore>>,
174 index: &IndexModel,
175 bounds: (&Bound<RawIndexKey>, &Bound<RawIndexKey>),
176 limit: usize,
177 ) -> Result<Vec<StorageKey>, InternalError> {
178 let mut out = Vec::with_capacity(limit.min(32));
179 store.with_borrow(|index_store| {
180 index_store.visit_raw_entries_in_range(bounds, Direction::Asc, |_, raw_entry| {
181 push_index_entry_storage_keys(index, raw_entry, &mut out, limit)
182 })
183 })?;
184
185 Ok(out)
186 }
187}
188
189impl SealedStructuralIndexEntryReader for StoreHandle {}
190
191fn push_index_entry_storage_keys(
194 index: &IndexModel,
195 raw_entry: &RawIndexEntry,
196 out: &mut Vec<StorageKey>,
197 limit: usize,
198) -> Result<bool, InternalError> {
199 raw_entry.push_membership_storage_keys_limited(
200 index.is_unique(),
201 out,
202 limit,
203 |err| {
204 InternalError::index_plan_index_corruption(format!(
205 "index corrupted: ({}) -> {}",
206 index.fields().join(", "),
207 err
208 ))
209 },
210 InternalError::unique_index_entry_single_key_required,
211 )
212}
213
214#[derive(Default)]
220pub struct StoreRegistry {
221 stores: Vec<(&'static str, StoreHandle)>,
222}
223
224impl StoreRegistry {
225 #[must_use]
227 pub fn new() -> Self {
228 Self::default()
229 }
230
231 pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
237 self.stores.iter().copied()
238 }
239
240 pub fn register_store(
242 &mut self,
243 name: &'static str,
244 data: &'static LocalKey<RefCell<DataStore>>,
245 index: &'static LocalKey<RefCell<IndexStore>>,
246 ) -> Result<(), InternalError> {
247 if self
248 .stores
249 .iter()
250 .any(|(existing_name, _)| *existing_name == name)
251 {
252 return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
253 }
254
255 if let Some(existing_name) =
257 self.stores
258 .iter()
259 .find_map(|(existing_name, existing_handle)| {
260 (std::ptr::eq(existing_handle.data_store(), data)
261 && std::ptr::eq(existing_handle.index_store(), index))
262 .then_some(*existing_name)
263 })
264 {
265 return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
266 name: name.to_string(),
267 existing_name: existing_name.to_string(),
268 }
269 .into());
270 }
271
272 self.stores.push((name, StoreHandle::new(data, index)));
273
274 Ok(())
275 }
276
277 pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
279 self.stores
280 .iter()
281 .find_map(|(existing_path, handle)| (*existing_path == path).then_some(*handle))
282 .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
283 }
284}
285
286#[cfg(test)]
291mod tests {
292 use crate::{
293 db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
294 error::{ErrorClass, ErrorOrigin},
295 testing::test_memory,
296 };
297 use std::{cell::RefCell, ptr};
298
299 const STORE_PATH: &str = "store_registry_tests::Store";
300 const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
301
302 thread_local! {
303 static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
304 static TEST_INDEX_STORE: RefCell<IndexStore> =
305 RefCell::new(IndexStore::init(test_memory(152)));
306 }
307
308 fn test_registry() -> StoreRegistry {
309 let mut registry = StoreRegistry::new();
310 registry
311 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
312 .expect("test store registration should succeed");
313 registry
314 }
315
316 #[test]
317 fn register_store_binds_data_and_index_handles() {
318 let registry = test_registry();
319 let handle = registry
320 .try_get_store(STORE_PATH)
321 .expect("registered store path should resolve");
322
323 assert!(
324 ptr::eq(handle.data_store(), &TEST_DATA_STORE),
325 "store handle should expose the registered data store accessor"
326 );
327 assert!(
328 ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
329 "store handle should expose the registered index store accessor"
330 );
331
332 let data_rows = handle.with_data(DataStore::len);
333 let index_rows = handle.with_index(IndexStore::len);
334 assert_eq!(data_rows, 0, "fresh test data store should be empty");
335 assert_eq!(index_rows, 0, "fresh test index store should be empty");
336 }
337
338 #[test]
339 fn missing_store_path_rejected_before_access() {
340 let registry = StoreRegistry::new();
341 let err = registry
342 .try_get_store("store_registry_tests::Missing")
343 .expect_err("missing path should fail lookup");
344
345 assert_eq!(err.class, ErrorClass::Internal);
346 assert_eq!(err.origin, ErrorOrigin::Store);
347 assert!(
348 err.message
349 .contains("store 'store_registry_tests::Missing' not found"),
350 "missing store lookup should include the missing path"
351 );
352 }
353
354 #[test]
355 fn duplicate_store_registration_is_rejected() {
356 let mut registry = StoreRegistry::new();
357 registry
358 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
359 .expect("initial store registration should succeed");
360
361 let err = registry
362 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
363 .expect_err("duplicate registration should fail");
364 assert_eq!(err.class, ErrorClass::InvariantViolation);
365 assert_eq!(err.origin, ErrorOrigin::Store);
366 assert!(
367 err.message
368 .contains("store 'store_registry_tests::Store' already registered"),
369 "duplicate registration should include the conflicting path"
370 );
371 }
372
373 #[test]
374 fn alias_store_registration_reusing_same_store_pair_is_rejected() {
375 let mut registry = StoreRegistry::new();
376 registry
377 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
378 .expect("initial store registration should succeed");
379
380 let err = registry
381 .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
382 .expect_err("alias registration reusing the same store pair should fail");
383 assert_eq!(err.class, ErrorClass::InvariantViolation);
384 assert_eq!(err.origin, ErrorOrigin::Store);
385 assert!(
386 err.message.contains(
387 "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
388 ),
389 "alias registration should include conflicting alias path"
390 );
391 assert!(
392 err.message
393 .contains("registered as 'store_registry_tests::Store'"),
394 "alias registration should include original path"
395 );
396 }
397}