1use 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, Eq, PartialEq)]
62pub(in crate::db) struct SecondaryReadAuthoritySnapshot {
63 index_state: IndexState,
64 secondary_covering_authoritative: bool,
65 secondary_existence_witness_authoritative: bool,
66}
67
68impl SecondaryReadAuthoritySnapshot {
69 const fn new(
71 index_state: IndexState,
72 secondary_covering_authoritative: bool,
73 secondary_existence_witness_authoritative: bool,
74 ) -> Self {
75 Self {
76 index_state,
77 secondary_covering_authoritative,
78 secondary_existence_witness_authoritative,
79 }
80 }
81
82 pub(in crate::db) const fn index_state(self) -> IndexState {
84 self.index_state
85 }
86
87 pub(in crate::db) const fn index_is_valid(self) -> bool {
89 matches!(self.index_state, IndexState::Valid)
90 }
91
92 pub(in crate::db) const fn secondary_covering_authoritative(self) -> bool {
94 self.secondary_covering_authoritative
95 }
96
97 pub(in crate::db) const fn secondary_existence_witness_authoritative(self) -> bool {
99 self.secondary_existence_witness_authoritative
100 }
101}
102
103#[derive(Clone, Copy, Debug)]
109pub struct StoreHandle {
110 data: &'static LocalKey<RefCell<DataStore>>,
111 index: &'static LocalKey<RefCell<IndexStore>>,
112}
113
114impl StoreHandle {
115 #[must_use]
117 pub const fn new(
118 data: &'static LocalKey<RefCell<DataStore>>,
119 index: &'static LocalKey<RefCell<IndexStore>>,
120 ) -> Self {
121 Self { data, index }
122 }
123
124 pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
126 self.data.with_borrow(f)
127 }
128
129 pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
131 self.data.with_borrow_mut(f)
132 }
133
134 pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
136 self.index.with_borrow(f)
137 }
138
139 pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
141 self.index.with_borrow_mut(f)
142 }
143
144 #[must_use]
146 pub(in crate::db) fn index_state(&self) -> IndexState {
147 self.with_index(IndexStore::state)
148 }
149
150 #[must_use]
153 pub(in crate::db) fn index_is_valid(&self) -> bool {
154 self.with_index(IndexStore::is_valid)
155 }
156
157 pub(in crate::db) fn mark_index_building(&self) {
159 self.with_index_mut(IndexStore::mark_building);
160 }
161
162 pub(in crate::db) fn mark_index_valid(&self) {
164 self.with_index_mut(IndexStore::mark_valid);
165 }
166
167 pub(in crate::db) fn mark_index_dropping(&self) {
169 self.with_index_mut(IndexStore::mark_dropping);
170 }
171
172 #[must_use]
175 pub(in crate::db) fn secondary_covering_authoritative(&self) -> bool {
176 self.with_data(DataStore::secondary_covering_authoritative)
177 && self.with_index(IndexStore::secondary_covering_authoritative)
178 }
179
180 pub(in crate::db) fn mark_secondary_covering_authoritative(&self) {
183 self.with_data_mut(DataStore::mark_secondary_covering_authoritative);
184 self.with_index_mut(IndexStore::mark_secondary_covering_authoritative);
185 }
186
187 #[must_use]
190 pub(in crate::db) fn secondary_existence_witness_authoritative(&self) -> bool {
191 self.with_data(DataStore::secondary_existence_witness_authoritative)
192 && self.with_index(IndexStore::secondary_existence_witness_authoritative)
193 }
194
195 #[must_use]
199 pub(in crate::db) fn secondary_read_authority_snapshot(
200 &self,
201 ) -> SecondaryReadAuthoritySnapshot {
202 SecondaryReadAuthoritySnapshot::new(
203 self.index_state(),
204 self.secondary_covering_authoritative(),
205 self.secondary_existence_witness_authoritative(),
206 )
207 }
208
209 pub(in crate::db) fn mark_secondary_existence_witness_authoritative(&self) {
212 self.with_data_mut(DataStore::mark_secondary_existence_witness_authoritative);
213 self.with_index_mut(IndexStore::mark_secondary_existence_witness_authoritative);
214 }
215
216 #[must_use]
218 pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
219 self.data
220 }
221
222 #[must_use]
224 pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
225 self.index
226 }
227}
228
229#[derive(Default)]
235pub struct StoreRegistry {
236 stores: Vec<(&'static str, StoreHandle)>,
237}
238
239impl StoreRegistry {
240 #[must_use]
242 pub fn new() -> Self {
243 Self::default()
244 }
245
246 pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
252 self.stores.iter().copied()
253 }
254
255 pub fn register_store(
257 &mut self,
258 name: &'static str,
259 data: &'static LocalKey<RefCell<DataStore>>,
260 index: &'static LocalKey<RefCell<IndexStore>>,
261 ) -> Result<(), InternalError> {
262 if self
263 .stores
264 .iter()
265 .any(|(existing_name, _)| *existing_name == name)
266 {
267 return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
268 }
269
270 if let Some(existing_name) =
272 self.stores
273 .iter()
274 .find_map(|(existing_name, existing_handle)| {
275 (std::ptr::eq(existing_handle.data_store(), data)
276 && std::ptr::eq(existing_handle.index_store(), index))
277 .then_some(*existing_name)
278 })
279 {
280 return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
281 name: name.to_string(),
282 existing_name: existing_name.to_string(),
283 }
284 .into());
285 }
286
287 self.stores.push((name, StoreHandle::new(data, index)));
288
289 Ok(())
290 }
291
292 pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
294 self.stores
295 .iter()
296 .find_map(|(existing_path, handle)| (*existing_path == path).then_some(*handle))
297 .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
298 }
299}
300
301#[cfg(test)]
306mod tests {
307 use crate::{
308 db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
309 error::{ErrorClass, ErrorOrigin},
310 testing::test_memory,
311 };
312 use std::{cell::RefCell, ptr};
313
314 const STORE_PATH: &str = "store_registry_tests::Store";
315 const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
316
317 thread_local! {
318 static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
319 static TEST_INDEX_STORE: RefCell<IndexStore> =
320 RefCell::new(IndexStore::init(test_memory(152)));
321 }
322
323 fn test_registry() -> StoreRegistry {
324 let mut registry = StoreRegistry::new();
325 registry
326 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
327 .expect("test store registration should succeed");
328 registry
329 }
330
331 #[test]
332 fn register_store_binds_data_and_index_handles() {
333 let registry = test_registry();
334 let handle = registry
335 .try_get_store(STORE_PATH)
336 .expect("registered store path should resolve");
337
338 assert!(
339 ptr::eq(handle.data_store(), &TEST_DATA_STORE),
340 "store handle should expose the registered data store accessor"
341 );
342 assert!(
343 ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
344 "store handle should expose the registered index store accessor"
345 );
346
347 let data_rows = handle.with_data(|store| store.len());
348 let index_rows = handle.with_index(IndexStore::len);
349 assert_eq!(data_rows, 0, "fresh test data store should be empty");
350 assert_eq!(index_rows, 0, "fresh test index store should be empty");
351 }
352
353 #[test]
354 fn missing_store_path_rejected_before_access() {
355 let registry = StoreRegistry::new();
356 let err = registry
357 .try_get_store("store_registry_tests::Missing")
358 .expect_err("missing path should fail lookup");
359
360 assert_eq!(err.class, ErrorClass::Internal);
361 assert_eq!(err.origin, ErrorOrigin::Store);
362 assert!(
363 err.message
364 .contains("store 'store_registry_tests::Missing' not found"),
365 "missing store lookup should include the missing path"
366 );
367 }
368
369 #[test]
370 fn duplicate_store_registration_is_rejected() {
371 let mut registry = StoreRegistry::new();
372 registry
373 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
374 .expect("initial store registration should succeed");
375
376 let err = registry
377 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
378 .expect_err("duplicate registration should fail");
379 assert_eq!(err.class, ErrorClass::InvariantViolation);
380 assert_eq!(err.origin, ErrorOrigin::Store);
381 assert!(
382 err.message
383 .contains("store 'store_registry_tests::Store' already registered"),
384 "duplicate registration should include the conflicting path"
385 );
386 }
387
388 #[test]
389 fn alias_store_registration_reusing_same_store_pair_is_rejected() {
390 let mut registry = StoreRegistry::new();
391 registry
392 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
393 .expect("initial store registration should succeed");
394
395 let err = registry
396 .register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
397 .expect_err("alias registration reusing the same store pair should fail");
398 assert_eq!(err.class, ErrorClass::InvariantViolation);
399 assert_eq!(err.origin, ErrorOrigin::Store);
400 assert!(
401 err.message.contains(
402 "store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
403 ),
404 "alias registration should include conflicting alias path"
405 );
406 assert!(
407 err.message
408 .contains("registered as 'store_registry_tests::Store'"),
409 "alias registration should include original path"
410 );
411 }
412}