Skip to main content

prolly/prolly/secondary_index/
async_snapshot.rs

1use std::collections::BTreeMap;
2use std::ops::ControlFlow;
3
4use super::super::error::Error;
5use super::super::manifest::AsyncManifestStore;
6use super::super::read::{EntryRef, ScanOutcome};
7use super::super::store::AsyncStore;
8use super::super::tree::Tree;
9use super::super::versioned_map::MapVersionId;
10use super::super::AsyncProlly;
11use super::async_coordinator::{find_snapshot, AsyncIndexedMap};
12use super::budget::{BudgetCounter, Deadline, QueryBudget};
13use super::definition::IndexProjection;
14use super::publication::AsyncIndexedStore;
15use super::snapshot::{
16    physical_bounds, IndexedSourceRecord, LogicalBounds, ProjectedIndexEntry, SecondaryIndexCursor,
17    SecondaryIndexDirection, SecondaryIndexMatch, SecondaryIndexMatchRef, SecondaryIndexPage,
18    SnapshotContext,
19};
20use super::state::{
21    CollectionIndexPolicy, IndexDescriptor, IndexedCollectionState, IndexedSnapshotId,
22    IndexedSnapshotManifest, IndexedSnapshotRecord,
23};
24use super::storage::{decode_physical_index_key, physical_index_key, IndexValue};
25
26/// Immutable async view pinned to one canonical collection-state root.
27pub struct AsyncIndexedSnapshot<'a, S: AsyncIndexedStore> {
28    id: IndexedSnapshotId,
29    state_tree: Tree,
30    state_version: MapVersionId,
31    source_tree: Tree,
32    source_version: MapVersionId,
33    indexes: BTreeMap<Vec<u8>, AsyncSecondaryIndexSnapshot<'a, S>>,
34}
35
36impl<'a, S: AsyncIndexedStore> AsyncIndexedSnapshot<'a, S> {
37    /// Content-addressed canonical snapshot identifier.
38    pub fn id(&self) -> &IndexedSnapshotId {
39        &self.id
40    }
41
42    /// Pinned source version.
43    pub fn source_version(&self) -> &MapVersionId {
44        &self.source_version
45    }
46
47    /// Pinned collection-state version.
48    pub fn state_version(&self) -> &MapVersionId {
49        &self.state_version
50    }
51
52    /// Pinned source tree.
53    pub fn source_tree(&self) -> &Tree {
54        &self.source_tree
55    }
56
57    /// Pinned canonical state tree.
58    pub fn state_tree(&self) -> &Tree {
59        &self.state_tree
60    }
61
62    /// Select one active index from this exact snapshot.
63    pub fn index(
64        &self,
65        name: impl AsRef<[u8]>,
66    ) -> Result<&AsyncSecondaryIndexSnapshot<'a, S>, Error> {
67        self.indexes
68            .get(name.as_ref())
69            .ok_or_else(|| Error::IndexUnavailableAtVersion {
70                name: name.as_ref().to_vec(),
71                source_version: self.source_version.clone(),
72            })
73    }
74
75    /// Iterate all indexes selected by this exact snapshot.
76    pub fn indexes(&self) -> impl ExactSizeIterator<Item = &AsyncSecondaryIndexSnapshot<'a, S>> {
77        self.indexes.values()
78    }
79}
80
81/// One immutable secondary-index tree selected by an async snapshot.
82pub struct AsyncSecondaryIndexSnapshot<'a, S: AsyncIndexedStore> {
83    prolly: &'a AsyncProlly<S>,
84    snapshot_id: SnapshotContext,
85    descriptor: IndexDescriptor,
86    selected: super::state::IndexSnapshotRef,
87    source_tree: Tree,
88    index_tree: Tree,
89    max_projection_bytes: usize,
90}
91
92/// Finite-budget async query session over one immutable index tree.
93pub struct AsyncSecondaryIndexQuery<'query, 'engine, S: AsyncIndexedStore> {
94    index: &'query AsyncSecondaryIndexSnapshot<'engine, S>,
95    budget: QueryBudget,
96}
97
98impl<'query, 'engine, S> AsyncSecondaryIndexQuery<'query, 'engine, S>
99where
100    S: AsyncIndexedStore + Clone,
101    <S as AsyncStore>::Error: Send + Sync,
102    <S as AsyncManifestStore>::Error: Send + Sync,
103{
104    pub async fn exact_page(
105        &self,
106        term: &[u8],
107        cursor: Option<&SecondaryIndexCursor>,
108        limit: usize,
109    ) -> Result<SecondaryIndexPage, Error> {
110        self.index
111            .page(
112                LogicalBounds::Exact(term.to_vec()),
113                SecondaryIndexDirection::Forward,
114                cursor,
115                limit,
116                &self.budget,
117            )
118            .await
119    }
120
121    pub async fn prefix_page(
122        &self,
123        prefix: &[u8],
124        cursor: Option<&SecondaryIndexCursor>,
125        limit: usize,
126    ) -> Result<SecondaryIndexPage, Error> {
127        self.index
128            .page(
129                LogicalBounds::Prefix(prefix.to_vec()),
130                SecondaryIndexDirection::Forward,
131                cursor,
132                limit,
133                &self.budget,
134            )
135            .await
136    }
137
138    pub async fn range_page(
139        &self,
140        start: &[u8],
141        end: Option<&[u8]>,
142        cursor: Option<&SecondaryIndexCursor>,
143        limit: usize,
144    ) -> Result<SecondaryIndexPage, Error> {
145        self.index
146            .page(
147                LogicalBounds::Range(start.to_vec(), end.map(ToOwned::to_owned)),
148                SecondaryIndexDirection::Forward,
149                cursor,
150                limit,
151                &self.budget,
152            )
153            .await
154    }
155
156    pub async fn exact_reverse_page(
157        &self,
158        term: &[u8],
159        cursor: Option<&SecondaryIndexCursor>,
160        limit: usize,
161    ) -> Result<SecondaryIndexPage, Error> {
162        self.index
163            .page(
164                LogicalBounds::Exact(term.to_vec()),
165                SecondaryIndexDirection::Reverse,
166                cursor,
167                limit,
168                &self.budget,
169            )
170            .await
171    }
172
173    pub async fn prefix_reverse_page(
174        &self,
175        prefix: &[u8],
176        cursor: Option<&SecondaryIndexCursor>,
177        limit: usize,
178    ) -> Result<SecondaryIndexPage, Error> {
179        self.index
180            .page(
181                LogicalBounds::Prefix(prefix.to_vec()),
182                SecondaryIndexDirection::Reverse,
183                cursor,
184                limit,
185                &self.budget,
186            )
187            .await
188    }
189
190    pub async fn range_reverse_page(
191        &self,
192        start: &[u8],
193        end: Option<&[u8]>,
194        cursor: Option<&SecondaryIndexCursor>,
195        limit: usize,
196    ) -> Result<SecondaryIndexPage, Error> {
197        self.index
198            .page(
199                LogicalBounds::Range(start.to_vec(), end.map(ToOwned::to_owned)),
200                SecondaryIndexDirection::Reverse,
201                cursor,
202                limit,
203                &self.budget,
204            )
205            .await
206    }
207
208    pub async fn records(&self, term: &[u8]) -> Result<Vec<IndexedSourceRecord>, Error> {
209        self.index.records_with_budget(term, &self.budget).await
210    }
211}
212
213impl<'a, S> AsyncSecondaryIndexSnapshot<'a, S>
214where
215    S: AsyncIndexedStore + Clone,
216    <S as AsyncStore>::Error: Send + Sync,
217    <S as AsyncManifestStore>::Error: Send + Sync,
218{
219    /// Create a finite-budget query session.
220    pub fn query(&self, budget: QueryBudget) -> Result<AsyncSecondaryIndexQuery<'_, 'a, S>, Error> {
221        budget.validate()?;
222        Ok(AsyncSecondaryIndexQuery {
223            index: self,
224            budget,
225        })
226    }
227
228    pub fn name(&self) -> &[u8] {
229        &self.descriptor.name
230    }
231
232    pub fn descriptor(&self) -> &IndexDescriptor {
233        &self.descriptor
234    }
235
236    pub fn snapshot_ref(&self) -> &super::state::IndexSnapshotRef {
237        &self.selected
238    }
239
240    pub fn tree(&self) -> &Tree {
241        &self.index_tree
242    }
243
244    pub async fn exact(&self, term: &[u8]) -> Result<Vec<SecondaryIndexMatch>, Error> {
245        self.collect_page(
246            self.exact_page(term, None, QueryBudget::default().max_returned_entries)
247                .await?,
248        )
249    }
250
251    pub async fn prefix(&self, prefix: &[u8]) -> Result<Vec<SecondaryIndexMatch>, Error> {
252        self.collect_page(
253            self.prefix_page(prefix, None, QueryBudget::default().max_returned_entries)
254                .await?,
255        )
256    }
257
258    pub async fn range(
259        &self,
260        start_term: &[u8],
261        end_term: Option<&[u8]>,
262    ) -> Result<Vec<SecondaryIndexMatch>, Error> {
263        self.collect_page(
264            self.range_page(
265                start_term,
266                end_term,
267                None,
268                QueryBudget::default().max_returned_entries,
269            )
270            .await?,
271        )
272    }
273
274    pub async fn primary_keys(&self, term: &[u8]) -> Result<Vec<Vec<u8>>, Error> {
275        Ok(self
276            .exact(term)
277            .await?
278            .into_iter()
279            .map(|matched| matched.primary_key)
280            .collect())
281    }
282
283    pub async fn projected(&self, term: &[u8]) -> Result<Vec<ProjectedIndexEntry>, Error> {
284        Ok(self
285            .exact(term)
286            .await?
287            .into_iter()
288            .map(|matched| (matched.primary_key, matched.projection))
289            .collect())
290    }
291
292    /// Resolve matching primary keys with one native ordered async batch read.
293    pub async fn records(&self, term: &[u8]) -> Result<Vec<IndexedSourceRecord>, Error> {
294        self.records_with_budget(term, &QueryBudget::default())
295            .await
296    }
297
298    async fn records_with_budget(
299        &self,
300        term: &[u8],
301        budget: &QueryBudget,
302    ) -> Result<Vec<IndexedSourceRecord>, Error> {
303        budget.validate()?;
304        let matches = self
305            .page(
306                LogicalBounds::Exact(term.to_vec()),
307                SecondaryIndexDirection::Forward,
308                None,
309                budget.max_returned_entries,
310                budget,
311            )
312            .await?;
313        let matches = self.collect_page(matches)?;
314        if matches.len() > budget.max_source_fetches {
315            return Err(Error::IndexResourceLimitExceeded {
316                resource: "query_source_fetches",
317                limit: budget.max_source_fetches,
318                actual: matches.len(),
319            });
320        }
321        let keys = matches
322            .iter()
323            .map(|matched| matched.primary_key.as_slice())
324            .collect::<Vec<_>>();
325        let values = self.prolly.get_many(&self.source_tree, &keys).await?;
326        let counter = BudgetCounter::new();
327        let mut returned_bytes = 0usize;
328        let mut accounted_memory = 0usize;
329        let mut records = Vec::with_capacity(matches.len());
330        for (matched, value) in matches.into_iter().zip(values) {
331            let value = value.ok_or_else(|| Error::IndexSnapshotMismatch {
332                name: self.descriptor.name.clone(),
333                source_version: self.snapshot_id.source_version.clone(),
334                reason: format!(
335                    "index references missing source primary key {:?}",
336                    matched.primary_key
337                ),
338            })?;
339            let retained = matched.primary_key.len().checked_add(value.len()).ok_or(
340                Error::IndexResourceLimitExceeded {
341                    resource: "query_returned_bytes",
342                    limit: budget.max_returned_bytes,
343                    actual: usize::MAX,
344                },
345            )?;
346            counter.charge(
347                "query_returned_bytes",
348                &mut returned_bytes,
349                retained,
350                budget.max_returned_bytes,
351            )?;
352            counter.charge(
353                "query_accounted_memory_bytes",
354                &mut accounted_memory,
355                retained,
356                budget.max_accounted_memory_bytes,
357            )?;
358            counter.check_elapsed("query_elapsed_millis", budget.max_elapsed)?;
359            records.push((matched.primary_key, value));
360        }
361        Ok(records)
362    }
363
364    pub async fn scan_exact(
365        &self,
366        term: &[u8],
367        mut visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>),
368    ) -> Result<u64, Error> {
369        Ok(self
370            .scan_exact_until(term, |row| {
371                visit(row);
372                ControlFlow::<()>::Continue(())
373            })
374            .await?
375            .visited)
376    }
377
378    pub async fn scan_exact_until<B>(
379        &self,
380        term: &[u8],
381        visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>) -> ControlFlow<B>,
382    ) -> Result<ScanOutcome<B>, Error> {
383        self.scan_matches_until(
384            LogicalBounds::Exact(term.to_vec()),
385            SecondaryIndexDirection::Forward,
386            visit,
387        )
388        .await
389    }
390
391    pub async fn scan_prefix(
392        &self,
393        prefix: &[u8],
394        mut visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>),
395    ) -> Result<u64, Error> {
396        Ok(self
397            .scan_prefix_until(prefix, |row| {
398                visit(row);
399                ControlFlow::<()>::Continue(())
400            })
401            .await?
402            .visited)
403    }
404
405    pub async fn scan_prefix_until<B>(
406        &self,
407        prefix: &[u8],
408        visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>) -> ControlFlow<B>,
409    ) -> Result<ScanOutcome<B>, Error> {
410        self.scan_matches_until(
411            LogicalBounds::Prefix(prefix.to_vec()),
412            SecondaryIndexDirection::Forward,
413            visit,
414        )
415        .await
416    }
417
418    pub async fn scan_range(
419        &self,
420        start: &[u8],
421        end: Option<&[u8]>,
422        mut visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>),
423    ) -> Result<u64, Error> {
424        Ok(self
425            .scan_range_until(start, end, |row| {
426                visit(row);
427                ControlFlow::<()>::Continue(())
428            })
429            .await?
430            .visited)
431    }
432
433    pub async fn scan_range_until<B>(
434        &self,
435        start: &[u8],
436        end: Option<&[u8]>,
437        visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>) -> ControlFlow<B>,
438    ) -> Result<ScanOutcome<B>, Error> {
439        self.scan_matches_until(
440            LogicalBounds::Range(start.to_vec(), end.map(ToOwned::to_owned)),
441            SecondaryIndexDirection::Forward,
442            visit,
443        )
444        .await
445    }
446
447    pub async fn scan_exact_reverse(
448        &self,
449        term: &[u8],
450        mut visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>),
451    ) -> Result<u64, Error> {
452        Ok(self
453            .scan_exact_reverse_until(term, |row| {
454                visit(row);
455                ControlFlow::<()>::Continue(())
456            })
457            .await?
458            .visited)
459    }
460
461    pub async fn scan_exact_reverse_until<B>(
462        &self,
463        term: &[u8],
464        visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>) -> ControlFlow<B>,
465    ) -> Result<ScanOutcome<B>, Error> {
466        self.scan_matches_until(
467            LogicalBounds::Exact(term.to_vec()),
468            SecondaryIndexDirection::Reverse,
469            visit,
470        )
471        .await
472    }
473
474    pub async fn scan_prefix_reverse(
475        &self,
476        prefix: &[u8],
477        mut visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>),
478    ) -> Result<u64, Error> {
479        Ok(self
480            .scan_prefix_reverse_until(prefix, |row| {
481                visit(row);
482                ControlFlow::<()>::Continue(())
483            })
484            .await?
485            .visited)
486    }
487
488    pub async fn scan_prefix_reverse_until<B>(
489        &self,
490        prefix: &[u8],
491        visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>) -> ControlFlow<B>,
492    ) -> Result<ScanOutcome<B>, Error> {
493        self.scan_matches_until(
494            LogicalBounds::Prefix(prefix.to_vec()),
495            SecondaryIndexDirection::Reverse,
496            visit,
497        )
498        .await
499    }
500
501    pub async fn scan_range_reverse(
502        &self,
503        start: &[u8],
504        end: Option<&[u8]>,
505        mut visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>),
506    ) -> Result<u64, Error> {
507        Ok(self
508            .scan_range_reverse_until(start, end, |row| {
509                visit(row);
510                ControlFlow::<()>::Continue(())
511            })
512            .await?
513            .visited)
514    }
515
516    pub async fn scan_range_reverse_until<B>(
517        &self,
518        start: &[u8],
519        end: Option<&[u8]>,
520        visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>) -> ControlFlow<B>,
521    ) -> Result<ScanOutcome<B>, Error> {
522        self.scan_matches_until(
523            LogicalBounds::Range(start.to_vec(), end.map(ToOwned::to_owned)),
524            SecondaryIndexDirection::Reverse,
525            visit,
526        )
527        .await
528    }
529
530    async fn scan_matches_until<B>(
531        &self,
532        logical: LogicalBounds,
533        direction: SecondaryIndexDirection,
534        mut visit: impl for<'row> FnMut(SecondaryIndexMatchRef<'row>) -> ControlFlow<B>,
535    ) -> Result<ScanOutcome<B>, Error> {
536        let budget = QueryBudget::default();
537        budget.validate()?;
538        let started = Deadline::new();
539        let mut scanned = 0usize;
540        let mut returned = 0usize;
541        let mut returned_bytes = 0usize;
542        let bounds = physical_bounds(&logical)?;
543        let mut handle = |entry: EntryRef<'_>| {
544            scanned = scanned.saturating_add(1);
545            returned = returned.saturating_add(1);
546            returned_bytes = returned_bytes
547                .saturating_add(entry.key().len())
548                .saturating_add(entry.value().len());
549            if scanned > budget.max_scanned_entries
550                || returned > budget.max_returned_entries
551                || returned_bytes > budget.max_returned_bytes
552                || returned_bytes > budget.max_accounted_memory_bytes
553                || started.exceeded(budget.max_elapsed)
554            {
555                return ControlFlow::Break(Err(Error::IndexResourceLimitExceeded {
556                    resource: "query_scan_budget",
557                    limit: budget.max_scanned_entries.min(budget.max_returned_entries),
558                    actual: scanned.max(returned),
559                }));
560            }
561            match self.decode_match(entry.key(), entry.value()) {
562                Ok(matched) => match visit(SecondaryIndexMatchRef {
563                    term: &matched.term,
564                    primary_key: &matched.primary_key,
565                    projection: matched.projection.as_deref(),
566                }) {
567                    ControlFlow::Continue(()) => ControlFlow::Continue(()),
568                    ControlFlow::Break(value) => ControlFlow::Break(Ok(value)),
569                },
570                Err(error) => ControlFlow::Break(Err(error)),
571            }
572        };
573        let outcome = match direction {
574            SecondaryIndexDirection::Reverse => {
575                self.prolly
576                    .scan_range_reverse_until(
577                        &self.index_tree,
578                        &bounds.start,
579                        bounds.end.as_deref(),
580                        &mut handle,
581                    )
582                    .await?
583            }
584            SecondaryIndexDirection::Forward => {
585                self.prolly
586                    .scan_range_until(
587                        &self.index_tree,
588                        &bounds.start,
589                        bounds.end.as_deref(),
590                        &mut handle,
591                    )
592                    .await?
593            }
594        };
595        match outcome.break_value {
596            Some(Ok(value)) => Ok(ScanOutcome::stopped(outcome.visited, value)),
597            Some(Err(error)) => Err(error),
598            None => Ok(ScanOutcome::complete(outcome.visited)),
599        }
600    }
601
602    pub async fn exact_page(
603        &self,
604        term: &[u8],
605        cursor: Option<&SecondaryIndexCursor>,
606        limit: usize,
607    ) -> Result<SecondaryIndexPage, Error> {
608        self.page(
609            LogicalBounds::Exact(term.to_vec()),
610            SecondaryIndexDirection::Forward,
611            cursor,
612            limit,
613            &QueryBudget::default(),
614        )
615        .await
616    }
617
618    pub async fn exact_reverse_page(
619        &self,
620        term: &[u8],
621        cursor: Option<&SecondaryIndexCursor>,
622        limit: usize,
623    ) -> Result<SecondaryIndexPage, Error> {
624        self.page(
625            LogicalBounds::Exact(term.to_vec()),
626            SecondaryIndexDirection::Reverse,
627            cursor,
628            limit,
629            &QueryBudget::default(),
630        )
631        .await
632    }
633
634    pub async fn prefix_page(
635        &self,
636        prefix: &[u8],
637        cursor: Option<&SecondaryIndexCursor>,
638        limit: usize,
639    ) -> Result<SecondaryIndexPage, Error> {
640        self.page(
641            LogicalBounds::Prefix(prefix.to_vec()),
642            SecondaryIndexDirection::Forward,
643            cursor,
644            limit,
645            &QueryBudget::default(),
646        )
647        .await
648    }
649
650    pub async fn prefix_reverse_page(
651        &self,
652        prefix: &[u8],
653        cursor: Option<&SecondaryIndexCursor>,
654        limit: usize,
655    ) -> Result<SecondaryIndexPage, Error> {
656        self.page(
657            LogicalBounds::Prefix(prefix.to_vec()),
658            SecondaryIndexDirection::Reverse,
659            cursor,
660            limit,
661            &QueryBudget::default(),
662        )
663        .await
664    }
665
666    pub async fn range_page(
667        &self,
668        start: &[u8],
669        end: Option<&[u8]>,
670        cursor: Option<&SecondaryIndexCursor>,
671        limit: usize,
672    ) -> Result<SecondaryIndexPage, Error> {
673        self.page(
674            LogicalBounds::Range(start.to_vec(), end.map(ToOwned::to_owned)),
675            SecondaryIndexDirection::Forward,
676            cursor,
677            limit,
678            &QueryBudget::default(),
679        )
680        .await
681    }
682
683    pub async fn range_reverse_page(
684        &self,
685        start: &[u8],
686        end: Option<&[u8]>,
687        cursor: Option<&SecondaryIndexCursor>,
688        limit: usize,
689    ) -> Result<SecondaryIndexPage, Error> {
690        self.page(
691            LogicalBounds::Range(start.to_vec(), end.map(ToOwned::to_owned)),
692            SecondaryIndexDirection::Reverse,
693            cursor,
694            limit,
695            &QueryBudget::default(),
696        )
697        .await
698    }
699
700    /// Construct a snapshot-bound continuation cursor from the logical term
701    /// and primary key carried by a DynamoDB-style `ExclusiveStartKey`.
702    pub fn exact_cursor_after(
703        &self,
704        query_term: &[u8],
705        term: &[u8],
706        primary_key: &[u8],
707        direction: SecondaryIndexDirection,
708    ) -> Result<SecondaryIndexCursor, Error> {
709        self.cursor_after_logical(
710            LogicalBounds::Exact(query_term.to_vec()),
711            term,
712            primary_key,
713            direction,
714        )
715    }
716
717    pub fn prefix_cursor_after(
718        &self,
719        query_prefix: &[u8],
720        term: &[u8],
721        primary_key: &[u8],
722        direction: SecondaryIndexDirection,
723    ) -> Result<SecondaryIndexCursor, Error> {
724        self.cursor_after_logical(
725            LogicalBounds::Prefix(query_prefix.to_vec()),
726            term,
727            primary_key,
728            direction,
729        )
730    }
731
732    pub fn range_cursor_after(
733        &self,
734        start: &[u8],
735        end: Option<&[u8]>,
736        term: &[u8],
737        primary_key: &[u8],
738        direction: SecondaryIndexDirection,
739    ) -> Result<SecondaryIndexCursor, Error> {
740        self.cursor_after_logical(
741            LogicalBounds::Range(start.to_vec(), end.map(ToOwned::to_owned)),
742            term,
743            primary_key,
744            direction,
745        )
746    }
747
748    fn cursor_after_logical(
749        &self,
750        logical: LogicalBounds,
751        term: &[u8],
752        primary_key: &[u8],
753        direction: SecondaryIndexDirection,
754    ) -> Result<SecondaryIndexCursor, Error> {
755        let cursor = SecondaryIndexCursor {
756            snapshot: self.snapshot_id.snapshot.clone(),
757            source_version: self.snapshot_id.source_version.clone(),
758            state_version: self.snapshot_id.state_version.clone(),
759            index_name: self.descriptor.name.clone(),
760            index_version: MapVersionId::for_tree(&self.selected.tree)?,
761            definition_fingerprint: self.descriptor.fingerprint.clone(),
762            direction,
763            bounds: logical.clone(),
764            raw_key: Some(physical_index_key(term, primary_key)?),
765        };
766        self.validate_cursor(&cursor, &logical, direction)?;
767        Ok(cursor)
768    }
769
770    async fn page(
771        &self,
772        logical: LogicalBounds,
773        direction: SecondaryIndexDirection,
774        cursor: Option<&SecondaryIndexCursor>,
775        limit: usize,
776        budget: &QueryBudget,
777    ) -> Result<SecondaryIndexPage, Error> {
778        budget.validate()?;
779        let counter = BudgetCounter::new();
780        let max_page_entries = budget.max_page_entries.min(budget.max_returned_entries);
781        if limit > max_page_entries {
782            return Err(Error::IndexResourceLimitExceeded {
783                resource: "query_page_entries",
784                limit: max_page_entries,
785                actual: limit,
786            });
787        }
788        if let Some(cursor) = cursor {
789            self.validate_cursor(cursor, &logical, direction)?;
790        }
791        if limit == 0 {
792            let next_cursor = cursor.cloned().or_else(|| {
793                Some(SecondaryIndexCursor {
794                    snapshot: self.snapshot_id.snapshot.clone(),
795                    source_version: self.snapshot_id.source_version.clone(),
796                    state_version: self.snapshot_id.state_version.clone(),
797                    index_name: self.descriptor.name.clone(),
798                    index_version: MapVersionId::for_tree(&self.selected.tree)
799                        .expect("validated index tree"),
800                    definition_fingerprint: self.descriptor.fingerprint.clone(),
801                    direction,
802                    bounds: logical,
803                    raw_key: None,
804                })
805            });
806            return Ok(SecondaryIndexPage {
807                matches: Vec::new(),
808                next_cursor,
809            });
810        }
811        let bounds = physical_bounds(&logical)?;
812        let mut matches = Vec::with_capacity(limit);
813        let mut returned_bytes = 0usize;
814        let mut accounted_memory = 0usize;
815        let mut scanned = 0usize;
816        let mut has_more = false;
817        let mut raw_key = None;
818        let after = cursor.and_then(|cursor| cursor.raw_key.as_deref());
819        let mut handle = |entry: EntryRef<'_>| {
820            if direction == SecondaryIndexDirection::Forward
821                && after.is_some_and(|after| entry.key() <= after)
822            {
823                return ControlFlow::Continue(());
824            }
825            scanned = scanned.saturating_add(1);
826            if scanned > budget.max_scanned_entries {
827                return ControlFlow::Break(Err(Error::IndexResourceLimitExceeded {
828                    resource: "query_scanned_entries",
829                    limit: budget.max_scanned_entries,
830                    actual: scanned,
831                }));
832            }
833            if matches.len() == limit {
834                has_more = true;
835                return ControlFlow::Break(Ok(()));
836            }
837            let matched = match self.decode_match(entry.key(), entry.value()) {
838                Ok(matched) => matched,
839                Err(error) => return ControlFlow::Break(Err(error)),
840            };
841            let retained = matched
842                .term
843                .len()
844                .checked_add(matched.primary_key.len())
845                .and_then(|bytes| {
846                    bytes.checked_add(matched.projection.as_ref().map_or(0, Vec::len))
847                })
848                .ok_or(Error::IndexResourceLimitExceeded {
849                    resource: "query_returned_bytes",
850                    limit: budget.max_returned_bytes,
851                    actual: usize::MAX,
852                });
853            let retained = match retained {
854                Ok(retained) => retained,
855                Err(error) => return ControlFlow::Break(Err(error)),
856            };
857            if let Err(error) = counter
858                .charge(
859                    "query_returned_bytes",
860                    &mut returned_bytes,
861                    retained,
862                    budget.max_returned_bytes,
863                )
864                .and_then(|_| {
865                    counter.charge(
866                        "query_accounted_memory_bytes",
867                        &mut accounted_memory,
868                        retained,
869                        budget.max_accounted_memory_bytes,
870                    )
871                })
872                .and_then(|_| counter.check_elapsed("query_elapsed_millis", budget.max_elapsed))
873            {
874                return ControlFlow::Break(Err(error));
875            }
876            raw_key = Some(entry.key().to_vec());
877            matches.push(matched);
878            ControlFlow::Continue(())
879        };
880        let outcome: ScanOutcome<Result<(), Error>> = match direction {
881            SecondaryIndexDirection::Forward => {
882                let start = after.unwrap_or(&bounds.start);
883                self.prolly
884                    .scan_range_until(&self.index_tree, start, bounds.end.as_deref(), &mut handle)
885                    .await?
886            }
887            SecondaryIndexDirection::Reverse => {
888                let end = after.or(bounds.end.as_deref());
889                self.prolly
890                    .scan_range_reverse_until(&self.index_tree, &bounds.start, end, &mut handle)
891                    .await?
892            }
893        };
894        if let Some(Err(error)) = outcome.break_value {
895            return Err(error);
896        }
897        let next_cursor = has_more.then(|| SecondaryIndexCursor {
898            snapshot: self.snapshot_id.snapshot.clone(),
899            source_version: self.snapshot_id.source_version.clone(),
900            state_version: self.snapshot_id.state_version.clone(),
901            index_name: self.descriptor.name.clone(),
902            index_version: MapVersionId::for_tree(&self.selected.tree)
903                .expect("validated index tree"),
904            definition_fingerprint: self.descriptor.fingerprint.clone(),
905            direction,
906            bounds: logical,
907            raw_key,
908        });
909        Ok(SecondaryIndexPage {
910            matches,
911            next_cursor,
912        })
913    }
914
915    fn validate_cursor(
916        &self,
917        cursor: &SecondaryIndexCursor,
918        bounds: &LogicalBounds,
919        direction: SecondaryIndexDirection,
920    ) -> Result<(), Error> {
921        let index_version =
922            MapVersionId::for_tree(&self.selected.tree).expect("validated index tree");
923        let valid = cursor.snapshot == self.snapshot_id.snapshot
924            && cursor.source_version == self.snapshot_id.source_version
925            && cursor.state_version == self.snapshot_id.state_version
926            && cursor.index_name == self.descriptor.name
927            && cursor.index_version == index_version
928            && cursor.definition_fingerprint == self.descriptor.fingerprint
929            && cursor.direction == direction
930            && &cursor.bounds == bounds;
931        let physical_key_valid = match cursor.raw_key.as_deref() {
932            None => true,
933            Some(raw_key) => {
934                let physical = physical_bounds(bounds)?;
935                raw_key >= physical.start.as_slice()
936                    && physical.end.as_deref().is_none_or(|end| raw_key < end)
937                    && decode_physical_index_key(raw_key).is_ok()
938            }
939        };
940        if valid && physical_key_valid {
941            return Ok(());
942        }
943        Err(Error::IndexCursorVersionMismatch {
944            expected: format!(
945                "source={}, state={}, index={}, direction={direction:?}, bounds={bounds:?}",
946                self.snapshot_id.source_version, self.snapshot_id.state_version, index_version
947            ),
948            actual: format!(
949                "source={}, state={}, index={}, direction={:?}, bounds={:?}",
950                cursor.source_version,
951                cursor.state_version,
952                cursor.index_version,
953                cursor.direction,
954                cursor.bounds
955            ),
956        })
957    }
958
959    fn collect_page(&self, page: SecondaryIndexPage) -> Result<Vec<SecondaryIndexMatch>, Error> {
960        if page.next_cursor.is_some() {
961            let limit = QueryBudget::default().max_returned_entries;
962            return Err(Error::IndexResourceLimitExceeded {
963                resource: "query_returned_entries",
964                limit,
965                actual: limit.saturating_add(1),
966            });
967        }
968        Ok(page.matches)
969    }
970
971    fn decode_match(&self, key: &[u8], value: &[u8]) -> Result<SecondaryIndexMatch, Error> {
972        let decoded = decode_physical_index_key(key)?;
973        let stored = IndexValue::from_bytes(value, self.max_projection_bytes)?;
974        let projection = match (self.descriptor.projection, stored) {
975            (IndexProjection::KeysOnly, IndexValue::KeysOnly) => None,
976            (IndexProjection::Include, IndexValue::Included(bytes))
977            | (IndexProjection::All, IndexValue::FullSource(bytes)) => Some(bytes),
978            _ => {
979                return Err(Error::IndexSnapshotMismatch {
980                    name: self.descriptor.name.clone(),
981                    source_version: self.snapshot_id.source_version.clone(),
982                    reason: "stored projection value does not match its descriptor".to_string(),
983                })
984            }
985        };
986        Ok(SecondaryIndexMatch {
987            term: decoded.term,
988            primary_key: decoded.primary_key,
989            projection,
990        })
991    }
992}
993
994impl<'a, S> AsyncIndexedMap<'a, S>
995where
996    S: AsyncIndexedStore + Clone,
997    <S as AsyncStore>::Error: Send + Sync,
998    <S as AsyncManifestStore>::Error: Send + Sync,
999{
1000    /// Pin the current canonical collection state and every tree it names.
1001    pub async fn snapshot(&self) -> Result<AsyncIndexedSnapshot<'a, S>, Error> {
1002        let loaded = self.load_state().await?;
1003        let record = loaded.state.head_snapshot()?.clone();
1004        let record_id = loaded.state.head.clone();
1005        self.resolve_snapshot(loaded.tree, loaded.state, record_id, record)
1006    }
1007
1008    /// Reopen the retained snapshot containing `source_version`.
1009    pub async fn snapshot_at(
1010        &self,
1011        source_version: &MapVersionId,
1012    ) -> Result<AsyncIndexedSnapshot<'a, S>, Error> {
1013        let loaded = self.load_state().await?;
1014        let record = find_snapshot(&loaded.state, source_version)?.clone();
1015        let record_id = record.id()?;
1016        self.resolve_snapshot(loaded.tree, loaded.state, record_id, record)
1017    }
1018
1019    /// Reopen one exact retained content-addressed snapshot.
1020    pub async fn snapshot_by_id(
1021        &self,
1022        id: &IndexedSnapshotId,
1023    ) -> Result<AsyncIndexedSnapshot<'a, S>, Error> {
1024        let loaded = self.load_state().await?;
1025        let record = loaded.state.snapshots.get(id).cloned().ok_or_else(|| {
1026            Error::InvalidVersionedMap(format!(
1027                "indexed snapshot {:?} is not retained",
1028                id.as_cid()
1029            ))
1030        })?;
1031        self.resolve_snapshot(loaded.tree, loaded.state, id.clone(), record)
1032    }
1033
1034    /// Resolve an immutable snapshot from a self-contained historical manifest.
1035    ///
1036    /// `manifest_tree` is the immutable tree that durably carried the manifest;
1037    /// its content identity is bound into secondary-index cursor context. The
1038    /// method performs no mutable-state lookup and rejects a manifest owned by
1039    /// another indexed map.
1040    pub fn snapshot_from_manifest(
1041        &self,
1042        manifest_tree: Tree,
1043        manifest: IndexedSnapshotManifest,
1044    ) -> Result<AsyncIndexedSnapshot<'a, S>, Error> {
1045        manifest.validate()?;
1046        if manifest.record.source_map_id != self.source_map_id {
1047            return Err(Error::InvalidVersionedMap(
1048                "indexed snapshot manifest belongs to another source".to_string(),
1049            ));
1050        }
1051        let active = manifest
1052            .record
1053            .indexes
1054            .iter()
1055            .map(|index| (index.name.clone(), index.descriptor_fingerprint.clone()))
1056            .collect();
1057        let descriptors = manifest
1058            .descriptors
1059            .into_iter()
1060            .map(|descriptor| {
1061                (
1062                    (descriptor.name.clone(), descriptor.fingerprint.clone()),
1063                    descriptor,
1064                )
1065            })
1066            .collect();
1067        let record_id = manifest.snapshot_id;
1068        let record = manifest.record;
1069        let state = IndexedCollectionState {
1070            source_map_id: self.source_map_id.clone(),
1071            policy: CollectionIndexPolicy::default(),
1072            head: record_id.clone(),
1073            snapshots: BTreeMap::from([(record_id.clone(), record.clone())]),
1074            descriptors,
1075            active,
1076            retired: Default::default(),
1077            pins: BTreeMap::new(),
1078        };
1079        state.validate_closure()?;
1080        self.resolve_snapshot(manifest_tree, state, record_id, record)
1081    }
1082
1083    fn resolve_snapshot(
1084        &self,
1085        state_tree: Tree,
1086        state: super::state::IndexedCollectionState,
1087        record_id: IndexedSnapshotId,
1088        record: IndexedSnapshotRecord,
1089    ) -> Result<AsyncIndexedSnapshot<'a, S>, Error> {
1090        let source_version = MapVersionId::for_tree(&record.source.tree)?;
1091        let state_version = MapVersionId::for_tree(&state_tree)?;
1092        let snapshot_id = SnapshotContext {
1093            snapshot: record_id,
1094            source_version: source_version.clone(),
1095            state_version: state_version.clone(),
1096        };
1097        let mut indexes = BTreeMap::new();
1098        for selected in record.indexes {
1099            let descriptor = state
1100                .descriptors
1101                .get(&(
1102                    selected.name.clone(),
1103                    selected.descriptor_fingerprint.clone(),
1104                ))
1105                .cloned()
1106                .ok_or_else(|| Error::IndexSnapshotMismatch {
1107                    name: selected.name.clone(),
1108                    source_version: source_version.clone(),
1109                    reason: "canonical descriptor is missing".to_string(),
1110                })?;
1111            let max_projection_bytes = match descriptor.projection {
1112                IndexProjection::KeysOnly => 0,
1113                IndexProjection::Include => descriptor.limits.max_projection_bytes,
1114                IndexProjection::All => descriptor.limits.max_all_value_bytes,
1115            };
1116            indexes.insert(
1117                selected.name.clone(),
1118                AsyncSecondaryIndexSnapshot {
1119                    prolly: self.prolly,
1120                    snapshot_id: snapshot_id.clone(),
1121                    descriptor,
1122                    source_tree: record.source.tree.clone(),
1123                    index_tree: selected.tree.clone(),
1124                    max_projection_bytes,
1125                    selected,
1126                },
1127            );
1128        }
1129        Ok(AsyncIndexedSnapshot {
1130            id: snapshot_id.snapshot,
1131            state_tree,
1132            state_version,
1133            source_tree: record.source.tree,
1134            source_version,
1135            indexes,
1136        })
1137    }
1138}