rushdb_query/planned.rs
1//! Index candidates for an executor that checks every predicate itself.
2//!
3//! The SQL label scan filters rows with `DataFusion`, but it reads
4//! candidates the way records reads do: from the index plan when an index
5//! answers a condition, from a label walk otherwise, and it is refused the
6//! same way when a filter without a usable index would check more records
7//! than the unindexed-walk limit allows (QRY-3: none by default).
8
9use rushdb_lmpg::Lmpg;
10
11use crate::planner::OwnerSet;
12use crate::{
13 check_walk_budget, index_candidates_with_budget, working_set_budget, Expr, QueryError,
14 QuerySnapshot, SearchRequest,
15};
16
17/// The index plan's candidate ordinals, ascending. A superset of the
18/// records matching the conditions it was planned for, or exactly them.
19pub struct PlannedCandidates {
20 ords: roaring::treemap::IntoIter,
21 exact: bool,
22}
23
24impl PlannedCandidates {
25 /// Every candidate is a live record matching every condition: none
26 /// needs checking.
27 #[must_use]
28 pub fn exact(&self) -> bool {
29 self.exact
30 }
31}
32
33impl Iterator for PlannedCandidates {
34 type Item = u64;
35
36 fn next(&mut self) -> Option<u64> {
37 self.ords.next()
38 }
39}
40
41/// Candidates for records of `labels` matching every one of `conditions`.
42///
43/// `conditions` are the conjuncts of the caller's filter that it can state
44/// as native predicates; an empty slice is a filter none of whose
45/// conditions can be stated. `Ok(None)` means no index answers them and
46/// the caller walks the labels' records.
47///
48/// # Errors
49///
50/// Returns [`QueryError::UnindexedWalk`] when no index answers and the walk
51/// would check more records than the handle's unindexed-walk limit,
52/// [`QueryError::ResourceLimit`] when the candidate set exceeds
53/// `query-memory-max`, [`QueryError::Cancelled`] when the probe requests a
54/// stop, and storage read errors.
55pub fn planned_candidates(
56 lmpg: &Lmpg,
57 snap: &QuerySnapshot,
58 labels: &[String],
59 conditions: &[Expr],
60 is_cancelled: &dyn Fn() -> bool,
61) -> Result<Option<PlannedCandidates>, QueryError> {
62 let req = SearchRequest {
63 labels: labels.to_vec(),
64 where_: Some(Expr::And(conditions.to_vec())),
65 ..SearchRequest::default()
66 };
67 check_walk_budget(lmpg, snap, &req)?;
68 let is_cancelled: &dyn Fn() -> bool = &crate::cancel_probe(lmpg, is_cancelled);
69 let plan =
70 index_candidates_with_budget(lmpg, snap, &req, working_set_budget(lmpg), is_cancelled)?;
71 Ok(plan.map(|plan| {
72 let ords: OwnerSet = plan.ords;
73 PlannedCandidates {
74 ords: ords.into_iter(),
75 exact: plan.exact,
76 }
77 }))
78}