icydb_core/db/store/
mod.rs1mod data;
2mod data_key;
3mod row;
4mod storage_key;
5
6pub use data::*;
7pub use data_key::*;
8pub use row::*;
9pub use storage_key::*;
10
11use crate::{
12 db::index::IndexStore,
13 error::{ErrorClass, ErrorOrigin, InternalError},
14};
15use std::{cell::RefCell, collections::HashMap, thread::LocalKey};
16use thiserror::Error as ThisError;
17
18#[derive(Debug, ThisError)]
23pub enum StoreRegistryError {
24 #[error("store '{0}' not found")]
25 StoreNotFound(String),
26 #[error("store '{0}' already registered")]
27 StoreAlreadyRegistered(String),
28}
29
30impl StoreRegistryError {
31 pub(crate) const fn class(&self) -> ErrorClass {
32 match self {
33 Self::StoreNotFound(_) => ErrorClass::Internal,
34 Self::StoreAlreadyRegistered(_) => ErrorClass::InvariantViolation,
35 }
36 }
37}
38
39impl From<StoreRegistryError> for InternalError {
40 fn from(err: StoreRegistryError) -> Self {
41 Self::new(err.class(), ErrorOrigin::Store, err.to_string())
42 }
43}
44
45#[derive(Clone, Copy, Debug)]
52pub struct StoreHandle {
53 data: &'static LocalKey<RefCell<DataStore>>,
54 index: &'static LocalKey<RefCell<IndexStore>>,
55}
56
57impl StoreHandle {
58 #[must_use]
60 pub const fn new(
61 data: &'static LocalKey<RefCell<DataStore>>,
62 index: &'static LocalKey<RefCell<IndexStore>>,
63 ) -> Self {
64 Self { data, index }
65 }
66
67 pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
69 self.data.with_borrow(f)
70 }
71
72 pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
74 self.data.with_borrow_mut(f)
75 }
76
77 pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
79 self.index.with_borrow(f)
80 }
81
82 pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
84 self.index.with_borrow_mut(f)
85 }
86
87 #[must_use]
89 pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
90 self.data
91 }
92
93 #[must_use]
95 pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
96 self.index
97 }
98}
99
100#[derive(Default)]
107pub struct StoreRegistry {
108 stores: HashMap<&'static str, StoreHandle>,
109}
110
111impl StoreRegistry {
112 #[must_use]
114 #[allow(clippy::new_without_default)]
115 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn iter(&self) -> impl Iterator<Item = (&'static str, StoreHandle)> {
121 self.stores.iter().map(|(k, v)| (*k, *v))
122 }
123
124 pub fn register_store(
126 &mut self,
127 name: &'static str,
128 data: &'static LocalKey<RefCell<DataStore>>,
129 index: &'static LocalKey<RefCell<IndexStore>>,
130 ) -> Result<(), InternalError> {
131 if self.stores.contains_key(name) {
132 return Err(StoreRegistryError::StoreAlreadyRegistered(name.to_string()).into());
133 }
134
135 self.stores.insert(name, StoreHandle::new(data, index));
136 Ok(())
137 }
138
139 pub fn try_get_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
141 self.stores
142 .get(path)
143 .copied()
144 .ok_or_else(|| StoreRegistryError::StoreNotFound(path.to_string()).into())
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use crate::{
151 db::{
152 index::IndexStore,
153 store::{DataStore, StoreRegistry},
154 },
155 error::{ErrorClass, ErrorOrigin},
156 test_support::test_memory,
157 };
158 use std::{cell::RefCell, ptr};
159
160 const STORE_PATH: &str = "store_registry_tests::Store";
161
162 thread_local! {
163 static TEST_DATA_STORE: RefCell<DataStore> = RefCell::new(DataStore::init(test_memory(151)));
164 static TEST_INDEX_STORE: RefCell<IndexStore> =
165 RefCell::new(IndexStore::init(test_memory(152)));
166 }
167
168 fn test_registry() -> StoreRegistry {
169 let mut registry = StoreRegistry::new();
170 registry
171 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
172 .expect("test store registration should succeed");
173 registry
174 }
175
176 #[test]
177 fn register_store_binds_data_and_index_handles() {
178 let registry = test_registry();
179 let handle = registry
180 .try_get_store(STORE_PATH)
181 .expect("registered store path should resolve");
182
183 assert!(
184 ptr::eq(handle.data_store(), &TEST_DATA_STORE),
185 "store handle should expose the registered data store accessor"
186 );
187 assert!(
188 ptr::eq(handle.index_store(), &TEST_INDEX_STORE),
189 "store handle should expose the registered index store accessor"
190 );
191
192 let data_rows = handle.with_data(|store| store.len());
193 let index_rows = handle.with_index(IndexStore::len);
194 assert_eq!(data_rows, 0, "fresh test data store should be empty");
195 assert_eq!(index_rows, 0, "fresh test index store should be empty");
196 }
197
198 #[test]
199 fn missing_store_path_rejected_before_access() {
200 let registry = StoreRegistry::new();
201 let err = registry
202 .try_get_store("store_registry_tests::Missing")
203 .expect_err("missing path should fail lookup");
204
205 assert_eq!(err.class, ErrorClass::Internal);
206 assert_eq!(err.origin, ErrorOrigin::Store);
207 assert!(
208 err.message
209 .contains("store 'store_registry_tests::Missing' not found"),
210 "missing store lookup should include the missing path"
211 );
212 }
213
214 #[test]
215 fn duplicate_store_registration_is_rejected() {
216 let mut registry = StoreRegistry::new();
217 registry
218 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
219 .expect("initial store registration should succeed");
220
221 let err = registry
222 .register_store(STORE_PATH, &TEST_DATA_STORE, &TEST_INDEX_STORE)
223 .expect_err("duplicate registration should fail");
224 assert_eq!(err.class, ErrorClass::InvariantViolation);
225 assert_eq!(err.origin, ErrorOrigin::Store);
226 assert!(
227 err.message
228 .contains("store 'store_registry_tests::Store' already registered"),
229 "duplicate registration should include the conflicting path"
230 );
231 }
232}