reddb_server/storage/unified/store/
secondary_index.rs1use super::*;
8use reddb_file::native_resume::SecondaryIndexResume;
9
10pub use reddb_engine::storage::secondary_index_read::SECONDARY_KEY_BYTES_MAX;
12pub type SecondaryIndexRead =
13 reddb_engine::storage::secondary_index_read::SecondaryIndexRead<Pager>;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct SecondaryIndexIdentity {
17 pub codec_version: u32,
18 pub definition: [u8; 32],
19}
20
21pub trait SecondaryIndexProjector: Send + Sync {
26 fn project(
27 &self,
28 entity: &UnifiedEntity,
29 key: &mut [u8; SECONDARY_KEY_BYTES_MAX],
30 ) -> Result<Option<usize>, StoreError>;
31}
32
33#[derive(Clone)]
34pub(super) struct SecondaryIndexSpec {
35 pub(super) identity: SecondaryIndexIdentity,
36 pub(super) projector: Arc<dyn SecondaryIndexProjector>,
37}
38
39pub(super) type SecondaryIndexSpecs = HashMap<String, BTreeMap<String, SecondaryIndexSpec>>;
40
41#[derive(Clone)]
42pub(super) struct PublishedSecondaryIndex {
43 pub identity: SecondaryIndexIdentity,
44 pub tree: Arc<BTree>,
45}
46
47impl PublishedSecondaryIndex {
48 pub(super) fn resume(&self) -> SecondaryIndexResume {
49 SecondaryIndexResume {
50 root_page: self.tree.root_page_id(),
51 codec_version: self.identity.codec_version,
52 definition: self.identity.definition,
53 }
54 }
55}
56
57impl PublishedKeyIndex {
58 pub(super) fn trees(&self) -> impl Iterator<Item = &Arc<BTree>> {
59 std::iter::once(&self.tree).chain(self.secondary.values().map(|index| &index.tree))
60 }
61}
62
63impl UnifiedStore {
64 pub fn register_secondary_index(
68 &self,
69 collection: &str,
70 name: &str,
71 identity: SecondaryIndexIdentity,
72 projector: Arc<dyn SecondaryIndexProjector>,
73 ) -> Result<(), StoreError> {
74 let collection = Self::canonical_collection(collection);
75 if name.is_empty() || name.len() > 1024 || identity.codec_version == 0 {
76 return Err(StoreError::Serialization(
77 "invalid secondary index identity".into(),
78 ));
79 }
80 let _publication = self.vault_publication_lock.lock();
81 if self.config.resident_memory_bytes_max.is_none() || self.commit.is_none() {
82 return Err(StoreError::Serialization(
83 "secondary trees require disk residency and durable page ownership".into(),
84 ));
85 }
86 if !self.resident.collections.read().contains_key(collection) {
87 return Err(StoreError::CollectionNotFound(collection.into()));
88 }
89 let mut specs = self.secondary_index_specs.write();
90 let indices = specs.entry(collection.to_owned()).or_default();
91 if !indices.contains_key(name) && indices.len() >= 4096 {
92 return Err(StoreError::Serialization(
93 "too many secondary indexes".into(),
94 ));
95 }
96 indices.insert(
97 name.to_owned(),
98 SecondaryIndexSpec {
99 identity,
100 projector,
101 },
102 );
103 Ok(())
104 }
105
106 pub(crate) fn checkpoint_missing_secondary_indices(&self) -> Result<(), StoreError> {
109 let missing = {
110 let _publication = self.vault_publication_lock.lock();
111 let specs = self.secondary_index_specs.read();
112 specs.iter().any(|(collection, indices)| {
113 indices.iter().any(|(name, spec)| {
114 self.secondary_index(collection, name, &spec.identity)
115 .is_none()
116 })
117 })
118 };
119 if missing {
120 self.persist()?;
121 }
122 Ok(())
123 }
124
125 pub fn secondary_index(
129 &self,
130 collection: &str,
131 name: &str,
132 identity: &SecondaryIndexIdentity,
133 ) -> Option<SecondaryIndexRead> {
134 let collection = Self::canonical_collection(collection);
135 let _publication = self.vault_publication_lock.lock();
136 let trees = self.btree_indices.read();
137 let data = trees.get(collection)?;
138 let keys = self.key_indices.read();
139 let generation = keys.get(collection)?;
140 if !generation.source.ptr_eq(&Arc::downgrade(data)) {
141 return None;
142 }
143 let index = generation.secondary.get(name)?;
144 (index.identity == *identity)
145 .then(|| SecondaryIndexRead::with_source(Arc::clone(&index.tree), Arc::clone(data)))
146 }
147
148 pub fn unregister_secondary_index(&self, collection: &str, name: &str) {
151 let collection = Self::canonical_collection(collection);
152 let _publication = self.vault_publication_lock.lock();
153 if let Some(specs) = self.secondary_index_specs.write().get_mut(collection) {
154 specs.remove(name);
155 }
156 if let Some(index) = self
157 .key_indices
158 .write()
159 .get_mut(collection)
160 .and_then(|generation| generation.secondary.remove(name))
161 {
162 self.retire_btree(index.tree);
163 self.resident
164 .paged_registry_dirty
165 .store(true, Ordering::Release);
166 }
167 }
168
169 pub(super) fn checkpoint_secondary_indices(
170 &self,
171 collection: &str,
172 data: &Arc<BTree>,
173 previous: Option<&PublishedKeyIndex>,
174 pager: &Arc<Pager>,
175 limit_bytes: usize,
176 ) -> Result<BTreeMap<String, PublishedSecondaryIndex>, StoreError> {
177 use reddb_engine::storage::collection_read::{CollectionCursor, PagedCollection};
178 let same_generation = previous.filter(|index| index.source.ptr_eq(&Arc::downgrade(data)));
179 let mut next = same_generation
180 .map(|index| index.secondary.clone())
181 .unwrap_or_default();
182 let specs = self.secondary_index_specs.read();
183 let Some(specs) = specs.get(collection) else {
184 return Ok(next);
185 };
186 let mut builds = Vec::new();
187 for (name, spec) in specs {
188 if next
189 .get(name)
190 .is_some_and(|index| index.identity == spec.identity)
191 {
192 continue;
193 }
194 builds.push((name, spec, Arc::new(BTree::new(Arc::clone(pager)))));
195 }
196 if builds.is_empty() {
197 return Ok(next);
198 }
199 let source = PagedCollection::new(Arc::clone(data));
200 let mut cursor = source.scan()?;
201 let mut key = [0; SECONDARY_KEY_BYTES_MAX];
202 while let Some(record) = cursor.next_record(limit_bytes)? {
203 let (entity, _) = record.decode_with_budget(self.format_version(), limit_bytes)?;
204 for (_, spec, tree) in &builds {
205 let Some(length) = spec.projector.project(&entity, &mut key)? else {
206 continue;
207 };
208 if length > key.len() {
209 return Err(StoreError::Serialization(
210 "secondary key exceeds its bounded buffer".into(),
211 ));
212 }
213 let mut posting = [0; SECONDARY_KEY_BYTES_MAX + 8];
214 posting[..length].copy_from_slice(&key[..length]);
215 posting[length..length + 8].copy_from_slice(&entity.id.raw().to_be_bytes());
216 tree.insert(&posting[..length + 8], &[])
217 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
218 }
219 }
220 for (name, spec, tree) in builds {
221 next.insert(
222 name.clone(),
223 PublishedSecondaryIndex {
224 identity: spec.identity.clone(),
225 tree,
226 },
227 );
228 }
229 Ok(next)
230 }
231}
232
233#[cfg(test)]
234mod tests;