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