use crate::{
db::{
cursor::IndexScanContinuationInput,
data::DataStore,
data::{DataKey, RawRow, StorageKey},
direction::Direction,
index::{
IndexState, IndexStore, RawIndexEntry, RawIndexKey, SealedStructuralIndexEntryReader,
SealedStructuralPrimaryRowReader, StructuralIndexEntryReader,
StructuralPrimaryRowReader,
},
},
error::{ErrorClass, ErrorOrigin, InternalError},
model::index::IndexModel,
types::EntityTag,
};
use std::{cell::RefCell, ops::Bound, thread::LocalKey};
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
#[expect(clippy::enum_variant_names)]
pub enum StoreRegistryError {
#[error("store '{0}' not found")]
StoreNotFound(String),
#[error("store '{0}' already registered")]
StoreAlreadyRegistered(String),
#[error(
"store '{name}' reuses the same row/index store pair already registered as '{existing_name}'"
)]
StoreHandlePairAlreadyRegistered { name: String, existing_name: String },
}
impl StoreRegistryError {
pub(crate) const fn class(&self) -> ErrorClass {
match self {
Self::StoreNotFound(_) => ErrorClass::Internal,
Self::StoreAlreadyRegistered(_) | Self::StoreHandlePairAlreadyRegistered { .. } => {
ErrorClass::InvariantViolation
}
}
}
}
impl From<StoreRegistryError> for InternalError {
fn from(err: StoreRegistryError) -> Self {
Self::classified(err.class(), ErrorOrigin::Store, err.to_string())
}
}
#[derive(Clone, Copy, Debug)]
pub struct StoreHandle {
data: &'static LocalKey<RefCell<DataStore>>,
index: &'static LocalKey<RefCell<IndexStore>>,
}
impl StoreHandle {
#[must_use]
pub const fn new(
data: &'static LocalKey<RefCell<DataStore>>,
index: &'static LocalKey<RefCell<IndexStore>>,
) -> Self {
Self { data, index }
}
pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
#[cfg(feature = "diagnostics")]
{
crate::db::physical_access::measure_physical_access_operation(|| {
self.data.with_borrow(f)
})
}
#[cfg(not(feature = "diagnostics"))]
{
self.data.with_borrow(f)
}
}
pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
self.data.with_borrow_mut(f)
}
pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
#[cfg(feature = "diagnostics")]
{
crate::db::physical_access::measure_physical_access_operation(|| {
self.index.with_borrow(f)
})
}
#[cfg(not(feature = "diagnostics"))]
{
self.index.with_borrow(f)
}
}
pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
self.index.with_borrow_mut(f)
}
#[must_use]
pub(in crate::db) fn index_state(&self) -> IndexState {
self.with_index(IndexStore::state)
}
pub(in crate::db) fn mark_index_building(&self) {
self.with_index_mut(IndexStore::mark_building);
}
pub(in crate::db) fn mark_index_ready(&self) {
self.with_index_mut(IndexStore::mark_ready);
}
#[must_use]
pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
self.data
}
#[must_use]
pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
self.index
}
}
impl StructuralPrimaryRowReader for StoreHandle {
fn read_primary_row_structural(&self, key: &DataKey) -> Result<Option<RawRow>, InternalError> {
let raw_key = key.to_raw()?;
Ok(self.with_data(|store| store.get(&raw_key)))
}
}
impl SealedStructuralPrimaryRowReader for StoreHandle {}
impl StructuralIndexEntryReader for StoreHandle {
fn read_index_entry_structural(
&self,
store: &'static LocalKey<RefCell<IndexStore>>,
key: &RawIndexKey,
) -> Result<Option<RawIndexEntry>, InternalError> {
Ok(store.with_borrow(|index_store| index_store.get(key)))
}
fn read_index_keys_in_raw_range_structural(
&self,
_entity_path: &'static str,
entity_tag: EntityTag,
store: &'static LocalKey<RefCell<IndexStore>>,
index: &IndexModel,
bounds: (&Bound<RawIndexKey>, &Bound<RawIndexKey>),
limit: usize,
) -> Result<Vec<StorageKey>, InternalError> {
let data_keys = store.with_borrow(|index_store| {
index_store.resolve_data_values_in_raw_range_limited(
entity_tag,
index,
bounds,
IndexScanContinuationInput::new(None, Direction::Asc),
limit,
None,
)
})?;
let mut out = Vec::with_capacity(data_keys.len());
for data_key in data_keys {
out.push(data_key.storage_key());
}
Ok(out)
}
}
impl SealedStructuralIndexEntryReader for StoreHandle {}
#[derive(Default)]
pub struct StoreRegistry {
stores: Vec<(&'static str, StoreHandle)>,
}
impl StoreRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
self.stores.iter().copied()
}
pub fn register_store(
&mut self,
name: &'static str,
data: &'static LocalKey<RefCell<DataStore>>,
index: &'static LocalKey<RefCell<IndexStore>>,
) -> Result<(), InternalError> {
if self
.stores
.iter()
.any(|(existing_name, _)| *existing_name == name)
{
return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
}
if let Some(existing_name) =
self.stores
.iter()
.find_map(|(existing_name, existing_handle)| {
(std::ptr::eq(existing_handle.data_store(), data)
&& std::ptr::eq(existing_handle.index_store(), index))
.then_some(*existing_name)
})
{
return Err(StoreRegistryError::StoreHandlePairAlreadyRegistered {
name: name.to_string(),
existing_name: existing_name.to_string(),
}
.into());
}
self.stores.push((name, StoreHandle::new(data, index)));
Ok(())
}
pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
self.stores
.iter()
.find_map(|(existing_path, handle)| (*existing_path == path).then_some(*handle))
.ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
}
}
#[cfg(test)]
mod tests {
use crate::{
db::{data::DataStore, index::IndexStore, registry::StoreRegistry},
error::{ErrorClass, ErrorOrigin},
testing::test_memory,
};
use std::{cell::RefCell, ptr};
const STORE_PATH: &str = "store_registry_tests::Store";
const ALIAS_STORE_PATH: &str = "store_registry_tests::StoreAlias";
thread_local! {
static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
static TEST_INDEX_STORE: RefCell<IndexStore> =
RefCell::new(IndexStore::init(test_memory(152)));
}
fn test_registry() -> StoreRegistry {
let mut registry = StoreRegistry::new();
registry
.register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
.expect("test store registration should succeed");
registry
}
#[test]
fn register_store_binds_data_and_index_handles() {
let registry = test_registry();
let handle = registry
.try_get_store(STORE_PATH)
.expect("registered store path should resolve");
assert!(
ptr::eq(handle.data_store(), &TEST_DATA_STORE),
"store handle should expose the registered data store accessor"
);
assert!(
ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
"store handle should expose the registered index store accessor"
);
let data_rows = handle.with_data(|store| store.len());
let index_rows = handle.with_index(IndexStore::len);
assert_eq!(data_rows, 0, "fresh test data store should be empty");
assert_eq!(index_rows, 0, "fresh test index store should be empty");
}
#[test]
fn missing_store_path_rejected_before_access() {
let registry = StoreRegistry::new();
let err = registry
.try_get_store("store_registry_tests::Missing")
.expect_err("missing path should fail lookup");
assert_eq!(err.class, ErrorClass::Internal);
assert_eq!(err.origin, ErrorOrigin::Store);
assert!(
err.message
.contains("store 'store_registry_tests::Missing' not found"),
"missing store lookup should include the missing path"
);
}
#[test]
fn duplicate_store_registration_is_rejected() {
let mut registry = StoreRegistry::new();
registry
.register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
.expect("initial store registration should succeed");
let err = registry
.register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
.expect_err("duplicate registration should fail");
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Store);
assert!(
err.message
.contains("store 'store_registry_tests::Store' already registered"),
"duplicate registration should include the conflicting path"
);
}
#[test]
fn alias_store_registration_reusing_same_store_pair_is_rejected() {
let mut registry = StoreRegistry::new();
registry
.register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
.expect("initial store registration should succeed");
let err = registry
.register_store(ALIAS_STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
.expect_err("alias registration reusing the same store pair should fail");
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Store);
assert!(
err.message.contains(
"store 'store_registry_tests::StoreAlias' reuses the same row/index store pair"
),
"alias registration should include conflicting alias path"
);
assert!(
err.message
.contains("registered as 'store_registry_tests::Store'"),
"alias registration should include original path"
);
}
}