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