Skip to main content

prolly/prolly/secondary_index/
definition.rs

1use serde::{Deserialize, Serialize};
2use std::collections::{btree_map::Entry, BTreeMap};
3use std::fmt;
4use std::sync::Arc;
5
6use super::super::error::Error;
7
8/// Bytes stored beside a matching primary key in the physical index tree.
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum IndexProjection {
11    /// Store only the physical `(term, primary_key)` key.
12    #[default]
13    KeysOnly,
14    /// Store extractor-supplied deterministic projection bytes.
15    Include,
16    /// Store the complete raw source value.
17    All,
18}
19
20/// One logical index emission from one source record.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct SecondaryIndexEntry {
23    /// Logical term used for exact, prefix, and range lookup.
24    pub term: Vec<u8>,
25    /// Application projection bytes. Present only for [`IndexProjection::Include`].
26    pub projection: Option<Vec<u8>>,
27}
28
29/// Callback-scoped index emission used by allocation-reusing extractors.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct SecondaryIndexEntryRef<'a> {
32    pub term: &'a [u8],
33    pub projection: Option<&'a [u8]>,
34}
35
36/// Extractor that emits borrowed terms/projections into a synchronous sink.
37pub trait StreamingSecondaryIndexExtractor: Send + Sync {
38    fn extract(
39        &self,
40        primary_key: &[u8],
41        source_value: &[u8],
42        emit: &mut dyn FnMut(SecondaryIndexEntryRef<'_>) -> Result<(), SecondaryIndexError>,
43    ) -> Result<(), SecondaryIndexError>;
44}
45
46impl SecondaryIndexEntry {
47    /// Emit a term without application projection bytes.
48    pub fn term(term: impl AsRef<[u8]>) -> Self {
49        Self {
50            term: term.as_ref().to_vec(),
51            projection: None,
52        }
53    }
54
55    /// Emit a term and deterministic application projection bytes.
56    pub fn included(term: impl AsRef<[u8]>, projection: impl AsRef<[u8]>) -> Self {
57        Self {
58            term: term.as_ref().to_vec(),
59            projection: Some(projection.as_ref().to_vec()),
60        }
61    }
62}
63
64/// Error returned by an application index extractor.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct SecondaryIndexError {
67    reason: String,
68}
69
70impl SecondaryIndexError {
71    /// Create an extractor failure with a stable application-facing reason.
72    pub fn new(reason: impl Into<String>) -> Self {
73        Self {
74            reason: reason.into(),
75        }
76    }
77
78    /// Borrow the failure reason.
79    pub fn reason(&self) -> &str {
80        &self.reason
81    }
82}
83
84impl fmt::Display for SecondaryIndexError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(&self.reason)
87    }
88}
89
90impl std::error::Error for SecondaryIndexError {}
91
92/// Deterministic runtime callback that derives zero or more index emissions.
93pub trait SecondaryIndexExtractor: Send + Sync + 'static {
94    /// Derive emissions for one source record.
95    fn extract(
96        &self,
97        primary_key: &[u8],
98        source_value: &[u8],
99    ) -> Result<Vec<SecondaryIndexEntry>, SecondaryIndexError>;
100}
101
102impl<F> SecondaryIndexExtractor for F
103where
104    F: Fn(&[u8], &[u8]) -> Result<Vec<SecondaryIndexEntry>, SecondaryIndexError>
105        + Send
106        + Sync
107        + 'static,
108{
109    fn extract(
110        &self,
111        primary_key: &[u8],
112        source_value: &[u8],
113    ) -> Result<Vec<SecondaryIndexEntry>, SecondaryIndexError> {
114        self(primary_key, source_value)
115    }
116}
117
118struct TermsExtractor<F>(F);
119
120impl<F> SecondaryIndexExtractor for TermsExtractor<F>
121where
122    F: Fn(&[u8], &[u8]) -> Result<Vec<Vec<u8>>, SecondaryIndexError> + Send + Sync + 'static,
123{
124    fn extract(
125        &self,
126        primary_key: &[u8],
127        source_value: &[u8],
128    ) -> Result<Vec<SecondaryIndexEntry>, SecondaryIndexError> {
129        (self.0)(primary_key, source_value)
130            .map(|terms| terms.into_iter().map(SecondaryIndexEntry::term).collect())
131    }
132}
133
134/// Resource bounds applied before index-derived buffers are published.
135#[derive(Clone, Debug, PartialEq, Eq)]
136pub struct SecondaryIndexLimits {
137    pub max_term_bytes: usize,
138    pub max_projection_bytes: usize,
139    pub max_all_value_bytes: usize,
140    pub max_terms_per_record: usize,
141    pub max_projected_bytes_per_record: usize,
142    pub max_derived_mutations_per_transaction: usize,
143    pub max_projected_bytes_per_transaction: usize,
144    pub max_indexes: usize,
145    pub build_page_size: usize,
146    pub max_temporary_sort_bytes: usize,
147    pub max_bundle_nodes: usize,
148    pub max_bundle_bytes: usize,
149    pub max_verification_entries: usize,
150    pub max_write_retries: usize,
151    pub max_build_retries: usize,
152}
153
154impl Default for SecondaryIndexLimits {
155    fn default() -> Self {
156        Self {
157            max_term_bytes: 4 * 1024,
158            max_projection_bytes: 64 * 1024,
159            max_all_value_bytes: 1024 * 1024,
160            max_terms_per_record: 1024,
161            max_projected_bytes_per_record: 1024 * 1024,
162            max_derived_mutations_per_transaction: 100_000,
163            max_projected_bytes_per_transaction: 64 * 1024 * 1024,
164            max_indexes: 32,
165            build_page_size: 4096,
166            max_temporary_sort_bytes: 256 * 1024 * 1024,
167            max_bundle_nodes: 1_000_000,
168            max_bundle_bytes: 1024 * 1024 * 1024,
169            max_verification_entries: 10_000_000,
170            max_write_retries: 8,
171            max_build_retries: 8,
172        }
173    }
174}
175
176impl SecondaryIndexLimits {
177    fn validate(&self) -> Result<(), Error> {
178        let values = [
179            ("max_term_bytes", self.max_term_bytes),
180            ("max_projection_bytes", self.max_projection_bytes),
181            ("max_all_value_bytes", self.max_all_value_bytes),
182            ("max_terms_per_record", self.max_terms_per_record),
183            (
184                "max_projected_bytes_per_record",
185                self.max_projected_bytes_per_record,
186            ),
187            (
188                "max_derived_mutations_per_transaction",
189                self.max_derived_mutations_per_transaction,
190            ),
191            (
192                "max_projected_bytes_per_transaction",
193                self.max_projected_bytes_per_transaction,
194            ),
195            ("max_indexes", self.max_indexes),
196            ("build_page_size", self.build_page_size),
197            ("max_temporary_sort_bytes", self.max_temporary_sort_bytes),
198            ("max_bundle_nodes", self.max_bundle_nodes),
199            ("max_bundle_bytes", self.max_bundle_bytes),
200            ("max_verification_entries", self.max_verification_entries),
201            ("max_write_retries", self.max_write_retries),
202            ("max_build_retries", self.max_build_retries),
203        ];
204        if let Some((field, _)) = values.into_iter().find(|(_, value)| *value == 0) {
205            return Err(Error::InvalidIndexDefinition {
206                reason: format!("{field} must be greater than zero"),
207            });
208        }
209        Ok(())
210    }
211}
212
213/// One immutable runtime index definition.
214#[derive(Clone)]
215pub struct SecondaryIndex {
216    name: Vec<u8>,
217    generation: u64,
218    extractor_id: String,
219    projection: IndexProjection,
220    limits: SecondaryIndexLimits,
221    extractor: Arc<dyn SecondaryIndexExtractor>,
222}
223
224impl fmt::Debug for SecondaryIndex {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        f.debug_struct("SecondaryIndex")
227            .field("name", &self.name)
228            .field("generation", &self.generation)
229            .field("extractor_id", &self.extractor_id)
230            .field("projection", &self.projection)
231            .field("limits", &self.limits)
232            .finish_non_exhaustive()
233    }
234}
235
236impl SecondaryIndex {
237    /// Define a non-unique keys-only index from a zero-or-more term callback.
238    pub fn non_unique<N, I, F>(
239        name: N,
240        generation: u64,
241        extractor_id: I,
242        extractor: F,
243    ) -> Result<Self, Error>
244    where
245        N: AsRef<[u8]>,
246        I: Into<String>,
247        F: Fn(&[u8], &[u8]) -> Result<Vec<Vec<u8>>, SecondaryIndexError> + Send + Sync + 'static,
248    {
249        Self::builder(name, generation, extractor_id)
250            .projection(IndexProjection::KeysOnly)
251            .extract_terms(extractor)
252    }
253
254    /// Start a general runtime definition builder.
255    pub fn builder<N, I>(name: N, generation: u64, extractor_id: I) -> SecondaryIndexBuilder
256    where
257        N: AsRef<[u8]>,
258        I: Into<String>,
259    {
260        SecondaryIndexBuilder {
261            name: name.as_ref().to_vec(),
262            generation,
263            extractor_id: extractor_id.into(),
264            projection: IndexProjection::KeysOnly,
265            limits: SecondaryIndexLimits::default(),
266        }
267    }
268
269    /// Stable arbitrary-byte index name.
270    pub fn name(&self) -> &[u8] {
271        &self.name
272    }
273    /// Monotonically increasing semantic definition generation.
274    pub fn generation(&self) -> u64 {
275        self.generation
276    }
277    /// Application-controlled identity for deterministic extractor semantics.
278    pub fn extractor_id(&self) -> &str {
279        &self.extractor_id
280    }
281    /// Projection mode for physical index values.
282    pub fn projection(&self) -> IndexProjection {
283        self.projection
284    }
285    /// Resource bounds attached to this definition.
286    pub fn limits(&self) -> &SecondaryIndexLimits {
287        &self.limits
288    }
289
290    /// Run and validate the deterministic extractor for one record.
291    pub fn extract(
292        &self,
293        primary_key: &[u8],
294        source_value: &[u8],
295    ) -> Result<Vec<SecondaryIndexEntry>, Error> {
296        if self.projection == IndexProjection::All
297            && source_value.len() > self.limits.max_all_value_bytes
298        {
299            return Err(Error::IndexResourceLimitExceeded {
300                resource: "all_source_value_bytes",
301                limit: self.limits.max_all_value_bytes,
302                actual: source_value.len(),
303            });
304        }
305        let entries = self
306            .extractor
307            .extract(primary_key, source_value)
308            .map_err(|error| Error::IndexExtractionFailed {
309                name: self.name.clone(),
310                primary_key: primary_key.to_vec(),
311                reason: error.to_string(),
312            })?;
313        self.validate_entries(primary_key, entries)
314    }
315
316    fn validate_entries(
317        &self,
318        primary_key: &[u8],
319        entries: Vec<SecondaryIndexEntry>,
320    ) -> Result<Vec<SecondaryIndexEntry>, Error> {
321        if entries.len() > self.limits.max_terms_per_record {
322            return Err(Error::IndexResourceLimitExceeded {
323                resource: "terms_per_record",
324                limit: self.limits.max_terms_per_record,
325                actual: entries.len(),
326            });
327        }
328        let mut canonical = BTreeMap::<Vec<u8>, Option<Vec<u8>>>::new();
329        let mut projected_bytes = 0usize;
330        for entry in entries {
331            if entry.term.len() > self.limits.max_term_bytes {
332                return Err(Error::IndexResourceLimitExceeded {
333                    resource: "term_bytes",
334                    limit: self.limits.max_term_bytes,
335                    actual: entry.term.len(),
336                });
337            }
338            let projection_matches = match self.projection {
339                IndexProjection::KeysOnly | IndexProjection::All => entry.projection.is_none(),
340                IndexProjection::Include => entry.projection.is_some(),
341            };
342            if !projection_matches {
343                return Err(Error::IndexProjectionMismatch {
344                    name: self.name.clone(),
345                    mode: self.projection,
346                    primary_key: primary_key.to_vec(),
347                });
348            }
349            if let Some(projection) = &entry.projection {
350                if projection.len() > self.limits.max_projection_bytes {
351                    return Err(Error::IndexResourceLimitExceeded {
352                        resource: "projection_bytes",
353                        limit: self.limits.max_projection_bytes,
354                        actual: projection.len(),
355                    });
356                }
357                projected_bytes = projected_bytes.saturating_add(projection.len());
358            }
359            match canonical.entry(entry.term) {
360                Entry::Vacant(slot) => {
361                    slot.insert(entry.projection);
362                }
363                Entry::Occupied(slot) if slot.get() == &entry.projection => {}
364                Entry::Occupied(slot) => {
365                    return Err(Error::ConflictingIndexProjection {
366                        name: self.name.clone(),
367                        primary_key: primary_key.to_vec(),
368                        term: slot.key().clone(),
369                    });
370                }
371            }
372        }
373        if projected_bytes > self.limits.max_projected_bytes_per_record {
374            return Err(Error::IndexResourceLimitExceeded {
375                resource: "projected_bytes_per_record",
376                limit: self.limits.max_projected_bytes_per_record,
377                actual: projected_bytes,
378            });
379        }
380        Ok(canonical
381            .into_iter()
382            .map(|(term, projection)| SecondaryIndexEntry { term, projection })
383            .collect())
384    }
385}
386
387/// Builder for a general projection-aware runtime definition.
388#[derive(Clone, Debug)]
389pub struct SecondaryIndexBuilder {
390    name: Vec<u8>,
391    generation: u64,
392    extractor_id: String,
393    projection: IndexProjection,
394    limits: SecondaryIndexLimits,
395}
396
397impl SecondaryIndexBuilder {
398    /// Select the persisted projection mode.
399    pub fn projection(mut self, projection: IndexProjection) -> Self {
400        self.projection = projection;
401        self
402    }
403    /// Override all default resource bounds.
404    pub fn limits(mut self, limits: SecondaryIndexLimits) -> Self {
405        self.limits = limits;
406        self
407    }
408
409    /// Finish with a projection-aware entry callback.
410    pub fn extract<F>(self, extractor: F) -> Result<SecondaryIndex, Error>
411    where
412        F: Fn(&[u8], &[u8]) -> Result<Vec<SecondaryIndexEntry>, SecondaryIndexError>
413            + Send
414            + Sync
415            + 'static,
416    {
417        self.finish(Arc::new(extractor))
418    }
419
420    /// Finish with a term-only callback for `KeysOnly` or `All` projection.
421    pub fn extract_terms<F>(self, extractor: F) -> Result<SecondaryIndex, Error>
422    where
423        F: Fn(&[u8], &[u8]) -> Result<Vec<Vec<u8>>, SecondaryIndexError> + Send + Sync + 'static,
424    {
425        if self.projection == IndexProjection::Include {
426            return Err(Error::InvalidIndexDefinition {
427                reason: "Include projection requires an entry extractor".to_string(),
428            });
429        }
430        self.finish(Arc::new(TermsExtractor(extractor)))
431    }
432
433    fn finish(self, extractor: Arc<dyn SecondaryIndexExtractor>) -> Result<SecondaryIndex, Error> {
434        if self.name.is_empty() {
435            return Err(Error::InvalidIndexDefinition {
436                reason: "index name must not be empty".to_string(),
437            });
438        }
439        if self.generation == 0 {
440            return Err(Error::InvalidIndexDefinition {
441                reason: "index generation must be greater than zero".to_string(),
442            });
443        }
444        if self.extractor_id.is_empty() {
445            return Err(Error::InvalidIndexDefinition {
446                reason: "extractor ID must not be empty".to_string(),
447            });
448        }
449        self.limits.validate()?;
450        Ok(SecondaryIndex {
451            name: self.name,
452            generation: self.generation,
453            extractor_id: self.extractor_id,
454            projection: self.projection,
455            limits: self.limits,
456            extractor,
457        })
458    }
459}
460
461/// Deterministically ordered runtime definitions supplied when opening an indexed map.
462#[derive(Clone, Debug, Default)]
463pub struct SecondaryIndexRegistry {
464    indexes: BTreeMap<Vec<u8>, SecondaryIndex>,
465    historical: BTreeMap<Vec<u8>, Vec<SecondaryIndex>>,
466}
467
468impl SecondaryIndexRegistry {
469    /// Create an empty registry.
470    pub fn new() -> Self {
471        Self::default()
472    }
473
474    /// Register one definition, rejecting duplicate logical names.
475    pub fn register(mut self, index: SecondaryIndex) -> Result<Self, Error> {
476        if self.indexes.contains_key(index.name()) {
477            return Err(Error::InvalidIndexDefinition {
478                reason: format!("duplicate index name {:?}", index.name()),
479            });
480        }
481        self.indexes.insert(index.name.clone(), index);
482        Ok(self)
483    }
484
485    /// Replace the active runtime definition while retaining the previous
486    /// generation for exact historical snapshot reopening.
487    pub fn replace(mut self, index: SecondaryIndex) -> Result<Self, Error> {
488        let name = index.name.clone();
489        let current = self
490            .indexes
491            .remove(&name)
492            .ok_or_else(|| Error::InvalidIndexDefinition {
493                reason: format!("cannot replace unregistered index name {:?}", name),
494            })?;
495        if index.generation() <= current.generation() {
496            self.indexes.insert(name, current);
497            return Err(Error::InvalidIndexDefinition {
498                reason: "replacement generation must be strictly greater than active generation"
499                    .to_string(),
500            });
501        }
502        self.historical
503            .entry(name.clone())
504            .or_default()
505            .push(current);
506        self.indexes.insert(name, index);
507        Ok(self)
508    }
509
510    /// Borrow a definition by its exact raw name.
511    pub fn get(&self, name: &[u8]) -> Option<&SecondaryIndex> {
512        self.indexes.get(name)
513    }
514    /// Iterate definitions in raw-name order.
515    pub fn iter(&self) -> impl ExactSizeIterator<Item = &SecondaryIndex> {
516        self.indexes.values()
517    }
518
519    pub(crate) fn definitions_for_name(&self, name: &[u8]) -> Vec<SecondaryIndex> {
520        self.indexes
521            .get(name)
522            .into_iter()
523            .chain(
524                self.historical
525                    .get(name)
526                    .into_iter()
527                    .flat_map(|definitions| definitions.iter()),
528            )
529            .cloned()
530            .collect()
531    }
532    /// Number of definitions in the registry.
533    pub fn len(&self) -> usize {
534        self.indexes.len()
535    }
536    /// Whether the registry contains no definitions.
537    pub fn is_empty(&self) -> bool {
538        self.indexes.is_empty()
539    }
540}