Skip to main content

icydb_core/db/registry/
registry.rs

1//! Module: db::registry::registry
2//! Responsibility: generated store path registration and lookup.
3//! Does not own: store handle capability definitions or storage mutation behavior.
4//! Boundary: validates registration invariants before runtime store lookup.
5
6use crate::{
7    db::{
8        data::DataStore,
9        index::IndexStore,
10        journal::JournalTailStore,
11        registry::{
12            StoreAllocationIdentities, StoreHandle, StoreRegistryError,
13            StoreRuntimeStorageCapabilities, StoreRuntimeStorageMode,
14        },
15        schema::SchemaStore,
16    },
17    error::InternalError,
18};
19use std::{cell::RefCell, thread::LocalKey};
20
21///
22/// StoreRegistry
23///
24/// StoreRegistry owns the generated mapping from schema `Store` paths to their
25/// row, index, and schema store handles.
26/// It validates registration invariants once at generated wiring time and then
27/// serves cheap immutable lookups during runtime operations.
28///
29
30#[derive(Default)]
31pub struct StoreRegistry {
32    stores: Vec<(&'static str, StoreHandle)>,
33}
34
35impl StoreRegistry {
36    /// Create an empty store registry.
37    #[must_use]
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Iterate registered stores.
43    ///
44    /// Iteration order follows registration order. Semantic result ordering
45    /// must still not depend on this iteration order; callers that need
46    /// deterministic ordering must sort by store path.
47    pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
48        self.stores.iter().copied()
49    }
50
51    /// Register a `Store` path to its row/index/schema store triplet with an
52    /// explicit allocation identity decision.
53    ///
54    /// Heap stores must pass [`StoreAllocationIdentities::absent`] explicitly
55    /// because allocation identities are intentionally unavailable.
56    /// Each row, index and schema handle must belong to only one store path.
57    pub fn register_store(
58        &mut self,
59        name: &'static str,
60        data: &'static LocalKey<RefCell<DataStore>>,
61        index: &'static LocalKey<RefCell<IndexStore>>,
62        schema: &'static LocalKey<RefCell<SchemaStore>>,
63        allocations: StoreAllocationIdentities,
64        capabilities: StoreRuntimeStorageCapabilities,
65    ) -> Result<(), InternalError> {
66        self.validate_register_store_shape(name, data, index, schema, allocations, capabilities)?;
67        if capabilities.storage_mode() != StoreRuntimeStorageMode::Heap {
68            return Err(StoreRegistryError::StoreAllocationCapabilityMismatch.into());
69        }
70
71        self.stores.push((
72            name,
73            StoreHandle::new(data, index, schema, allocations, capabilities),
74        ));
75
76        Ok(())
77    }
78
79    /// Register one journaled store with its journal-tail storage handle.
80    /// Each row, index, schema and journal handle must belong to only one store path.
81    #[expect(
82        clippy::too_many_arguments,
83        reason = "generated journaled registration adds one journal-tail handle to the existing store triplet"
84    )]
85    pub fn register_journaled_store(
86        &mut self,
87        name: &'static str,
88        data: &'static LocalKey<RefCell<DataStore>>,
89        index: &'static LocalKey<RefCell<IndexStore>>,
90        schema: &'static LocalKey<RefCell<SchemaStore>>,
91        journal: &'static LocalKey<RefCell<JournalTailStore>>,
92        allocations: StoreAllocationIdentities,
93        capabilities: StoreRuntimeStorageCapabilities,
94    ) -> Result<(), InternalError> {
95        self.validate_register_store_shape(name, data, index, schema, allocations, capabilities)?;
96        if capabilities.storage_mode() != StoreRuntimeStorageMode::Journaled
97            || allocations.journal().is_none()
98        {
99            return Err(StoreRegistryError::StoreAllocationCapabilityMismatch.into());
100        }
101        if self.stores.iter().any(|(_, existing)| {
102            existing
103                .journal_tail_store()
104                .is_some_and(|existing_journal| std::ptr::eq(existing_journal, journal))
105        }) {
106            return Err(StoreRegistryError::StoreHandleAlreadyRegistered.into());
107        }
108
109        self.stores.push((
110            name,
111            StoreHandle::new_journaled(data, index, schema, journal, allocations, capabilities),
112        ));
113
114        Ok(())
115    }
116
117    /// Look up a store handle by path.
118    pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
119        self.stores
120            .iter()
121            .find_map(|(existing_path, handle)| (*existing_path == path).then_some(*handle))
122            .ok_or_else(|| StoreRegistryError::StoreNotFound.into())
123    }
124
125    fn validate_register_store_shape(
126        &self,
127        name: &'static str,
128        data: &'static LocalKey<RefCell<DataStore>>,
129        index: &'static LocalKey<RefCell<IndexStore>>,
130        schema: &'static LocalKey<RefCell<SchemaStore>>,
131        allocations: StoreAllocationIdentities,
132        capabilities: StoreRuntimeStorageCapabilities,
133    ) -> Result<(), InternalError> {
134        if self
135            .stores
136            .iter()
137            .any(|(existing_name, _)| *existing_name == name)
138        {
139            return Err(StoreRegistryError::StoreAlreadyRegistered.into());
140        }
141
142        // Even one shared role crosses logical store ownership: accepted roots
143        // and entity tags belong to their registered store, not to an alias.
144        if self.stores.iter().any(|(_, existing_handle)| {
145            std::ptr::eq(existing_handle.data_store(), data)
146                || std::ptr::eq(existing_handle.index_store(), index)
147                || std::ptr::eq(existing_handle.schema_store(), schema)
148        }) {
149            return Err(StoreRegistryError::StoreHandleAlreadyRegistered.into());
150        }
151
152        if allocations.allocation_identity_capability() != Some(capabilities.allocation_identity())
153            || !allocations.matches_storage_capabilities(capabilities)
154        {
155            return Err(StoreRegistryError::StoreAllocationCapabilityMismatch.into());
156        }
157
158        Ok(())
159    }
160}