Skip to main content

rushdb_query/
matches.rs

1//! The records-page pipeline: plan a page (the predicate planner, then the
2//! order planner on its numbers), pick its candidates, and keep the
3//! matches the page needs, in query order.
4
5use rushdb_lmpg::reconstruct;
6use rushdb_lmpg::Lmpg;
7use serde_json::{Map, Value as Json};
8
9use crate::budget::WorkingSet;
10use crate::planner::index_candidates_with_budget;
11use crate::point_fields::{self, PointFields};
12use crate::{
13    check_walk_budget, eval_expr_ctx, normalize, order, planner, related_exists, scan,
14    sort_ordered_rows, working_set_budget, EvalCtx, Expr, QueryError, QuerySnapshot, SearchRequest,
15};
16
17/// The matches a search keeps, and how many there are in all.
18pub(crate) struct Matches {
19    /// Matches in query order, from position `offset` of the full order:
20    /// the first `skip + limit`, or (read from the far end) the page itself.
21    pub(crate) retained: Vec<u64>,
22    /// Position of `retained[0]` among every match in query order.
23    pub(crate) offset: usize,
24    /// Every match, retained or not.
25    pub(crate) total: u64,
26}
27
28/// The matches of `req` in query order, enough for the page
29/// `skip..skip + limit` (`limit` `usize::MAX`: every match), and how many
30/// there are in all.
31///
32/// The page is read predicate-first (every candidate of the filter, the
33/// best `skip + limit` kept) unless the order planner ([`order::plan_order`])
34/// finds a source already in the requested order that reaches the page in
35/// fewer reads. Such a walk gives up when it passes more rows than
36/// predicate-first reads, and the page is read predicate-first, so a wrong
37/// estimate costs at most twice the plain read.
38///
39/// The working set (the candidate owner set, retained ordinals, ordering
40/// rows and the record being evaluated) is charged against
41/// `query-memory-max`.
42pub(crate) fn run_matches(
43    lmpg: &Lmpg,
44    req: &SearchRequest,
45    is_cancelled: &dyn Fn() -> bool,
46    snap: &QuerySnapshot,
47    skip: usize,
48    limit: usize,
49    known_total: Option<u64>,
50) -> Result<Matches, QueryError> {
51    check_walk_budget(lmpg, snap, req)?;
52    let page = (skip, limit);
53    if let Some(matches) = run_pass(lmpg, req, is_cancelled, snap, page, known_total, true)? {
54        return Ok(matches);
55    }
56    Ok(
57        run_pass(lmpg, req, is_cancelled, snap, page, known_total, false)?
58            .expect("a predicate-first pass always completes"),
59    )
60}
61
62/// One [`run_matches`] pass; `None` when its order-first walk (allowed by
63/// `order_first`) gave up.
64pub(crate) fn run_pass(
65    lmpg: &Lmpg,
66    req: &SearchRequest,
67    is_cancelled: &dyn Fn() -> bool,
68    snap: &QuerySnapshot,
69    (skip, limit): (usize, usize),
70    known_total: Option<u64>,
71    order_first: bool,
72) -> Result<Option<Matches>, QueryError> {
73    let mut working = WorkingSet {
74        limit: working_set_budget(lmpg).map(|bytes| u64::try_from(bytes).unwrap_or(u64::MAX)),
75        charged: 0,
76    };
77    let CandidateSource {
78        phases,
79        exact,
80        known,
81        predicate,
82        member_of,
83        plan,
84    } = candidate_source(
85        lmpg,
86        req,
87        is_cancelled,
88        snap,
89        (skip, limit),
90        known_total,
91        order_first,
92        &mut working,
93    )?;
94    let mirror = plan.as_ref().is_some_and(order::OrderPlan::mirror);
95    let budget = plan.as_ref().map(|plan| plan.predicate_first_reads);
96    let ctx = EvalCtx::at(lmpg, snap.clone(), Some(is_cancelled));
97    let mut session = reconstruct::ReconstructSession::new(snap, lmpg.tenant);
98    let (order_fields, predicate_fields) =
99        point_reads(snap, lmpg.tenant, req, &phases, predicate.as_ref())?;
100    let mut kept = Kept::new(mirror, known, skip, limit);
101    let mut total: u64 = 0;
102    let mut examined: u64 = 0;
103    for Phase {
104        mut candidates,
105        sorted,
106    } in phases
107    {
108        if known.is_some() && kept.full() {
109            break;
110        }
111        let mut ordered = Sorted::new(kept.keep - kept.count.min(kept.keep));
112        while let Some(ord) = candidates.next(is_cancelled)? {
113            if is_cancelled() {
114                return Err(QueryError::Cancelled);
115            }
116            examined += 1;
117            if budget.is_some_and(|budget| examined + candidates.passed() > budget) {
118                return Ok(None);
119            }
120            if member_of.as_ref().is_some_and(|set| !set.contains(ord)) {
121                continue;
122            }
123            if !exact
124                && !is_match(
125                    &ctx,
126                    req,
127                    predicate.as_ref(),
128                    predicate_fields.as_ref(),
129                    &mut session,
130                    &mut working,
131                    ord,
132                )?
133            {
134                continue;
135            }
136            total += 1;
137            if sorted {
138                let row = read_row(order_fields.as_ref(), &mut session, snap, lmpg.tenant, ord)?;
139                ordered.push(ord, row, req, &mut working)?;
140            } else if !kept.push(ord, &mut working)? && known.is_some() {
141                break;
142            }
143        }
144        if candidates.gave_up() {
145            return Ok(None);
146        }
147        examined += candidates.passed();
148        if sorted {
149            for ord in ordered.finish(req) {
150                kept.count += 1;
151                kept.retained.push_back(ord);
152            }
153        }
154    }
155    let (retained, offset) = if mirror {
156        (kept.retained.into_iter().rev().collect(), skip)
157    } else {
158        (kept.retained.into(), 0)
159    };
160    Ok(Some(Matches {
161        retained,
162        offset,
163        total: known.unwrap_or(total),
164    }))
165}
166
167/// The point reads of a pass: the order fields (when a phase is sorted) and
168/// the fields the residual predicate reads; `None` where the whole record
169/// is needed.
170fn point_reads(
171    snap: &QuerySnapshot,
172    tenant: rushdb_types::ids::TenantId,
173    req: &SearchRequest,
174    phases: &[Phase<'_>],
175    predicate: Option<&Expr>,
176) -> Result<(Option<PointFields>, Option<PointFields>), QueryError> {
177    let order_fields = if phases.iter().any(|phase| phase.sorted) {
178        PointFields::resolve_in(
179            snap,
180            tenant,
181            &req.labels,
182            req.order_by.iter().map(|(f, _)| f.as_str()),
183        )?
184    } else {
185        None
186    };
187    let predicate_fields = match predicate {
188        Some(expr) => PointFields::for_predicate(snap, tenant, &req.labels, expr)?,
189        None => None,
190    };
191    Ok((order_fields, predicate_fields))
192}
193
194/// Matches kept as they come, in query (or far-end) order.
195struct Kept {
196    retained: std::collections::VecDeque<u64>,
197    count: usize,
198    /// Matches wanted in walk order.
199    keep: usize,
200    /// From the far end, only the page's rows (this many) are held.
201    page: Option<usize>,
202}
203
204impl Kept {
205    /// Matches to keep in walk order: from the far end (`mirror`), the rows
206    /// up to the page `skip..skip + limit`, holding only the page's; else
207    /// the first `skip + limit`.
208    fn new(mirror: bool, known: Option<u64>, skip: usize, limit: usize) -> Self {
209        let keep = if mirror {
210            usize::try_from(known.unwrap_or(0))
211                .unwrap_or(usize::MAX)
212                .saturating_sub(skip)
213        } else {
214            skip.saturating_add(limit)
215        };
216        Self {
217            retained: std::collections::VecDeque::new(),
218            count: 0,
219            keep,
220            page: mirror.then_some(limit),
221        }
222    }
223
224    fn full(&self) -> bool {
225        self.count >= self.keep
226    }
227
228    /// Keep `ord` if more are wanted; `false` when the page was full.
229    fn push(&mut self, ord: u64, working: &mut WorkingSet) -> Result<bool, QueryError> {
230        if self.full() {
231            return Ok(false);
232        }
233        self.count += 1;
234        self.retained.push_back(ord);
235        if self.page.is_some_and(|page| self.retained.len() > page) {
236            self.retained.pop_front();
237        } else {
238            working.charge(8, true)?;
239        }
240        Ok(true)
241    }
242}
243
244/// A sorted phase's best rows `(ordinal, order fields)`, sorted and cut back
245/// to what the page still needs whenever they double, so memory follows the
246/// page, not the matches.
247struct Sorted {
248    rows: Vec<OrderRow>,
249    bytes: Vec<u64>,
250    wanted: usize,
251}
252
253impl Sorted {
254    fn new(wanted: usize) -> Self {
255        Self {
256            rows: Vec::new(),
257            bytes: Vec::new(),
258            wanted,
259        }
260    }
261
262    fn push(
263        &mut self,
264        ord: u64,
265        row: Map<String, Json>,
266        req: &SearchRequest,
267        working: &mut WorkingSet,
268    ) -> Result<(), QueryError> {
269        let bytes =
270            serde_json::to_vec(&row).map_or(0, |v| u64::try_from(v.len()).unwrap_or(u64::MAX));
271        working.charge(bytes, true)?;
272        self.rows.push((ord, row));
273        self.bytes.push(bytes);
274        if self.rows.len() >= self.wanted.saturating_mul(2).max(64) {
275            working.charged = working.charged.saturating_sub(cut_ordered(
276                &mut self.rows,
277                &mut self.bytes,
278                req,
279                self.wanted,
280            ));
281        }
282        Ok(())
283    }
284
285    /// The best `wanted` ordinals, in query order.
286    fn finish(mut self, req: &SearchRequest) -> impl Iterator<Item = u64> {
287        cut_ordered(&mut self.rows, &mut self.bytes, req, self.wanted);
288        self.rows.into_iter().map(|(ord, _)| ord)
289    }
290}
291
292/// Whether candidate `ord` matches: `predicate` holds on it and every
293/// relationship branch binds. Candidates are live records (a label's
294/// members, the tenant's record headers, or the index plan's owners, all
295/// written and deleted in the commit that writes and deletes the record),
296/// so the record header is not read again here.
297pub(crate) fn is_match(
298    ctx: &EvalCtx<'_>,
299    req: &SearchRequest,
300    predicate: Option<&Expr>,
301    fields: Option<&PointFields>,
302    session: &mut reconstruct::ReconstructSession<'_, QuerySnapshot>,
303    working: &mut WorkingSet,
304    ord: u64,
305) -> Result<bool, QueryError> {
306    let (snap, tenant) = (&ctx.snap, ctx.lmpg.tenant);
307    if let Some(expr) = predicate {
308        let obj = read_row(fields, session, snap, tenant, ord)?;
309        let bytes =
310            serde_json::to_vec(&obj).map_or(0, |v| u64::try_from(v.len()).unwrap_or(u64::MAX));
311        working.charge(bytes, false)?;
312        if !eval_expr_ctx(ctx, expr, &obj, ord)? {
313            return Ok(false);
314        }
315    }
316    for spec in &req.related {
317        if !related_exists(snap, tenant, ord, spec)? {
318            return Ok(false);
319        }
320    }
321    Ok(true)
322}
323
324/// The properties `predicate` reads of record `ord`, decoded through
325/// `reader` (which may meter the bytes): the predicate's own fields by
326/// name, or the whole record when it needs it (`$id`, a traversal, an
327/// array-typed property). A record the predicate cannot see is empty.
328pub(crate) fn filtered_row<R: rushdb_lmpg::reader::TryRead>(
329    reader: &R,
330    snap: &QuerySnapshot,
331    tenant: rushdb_types::ids::TenantId,
332    predicate: &Expr,
333    ord: u64,
334) -> Result<Map<String, Json>, QueryError> {
335    let mut session = reconstruct::ReconstructSession::new(reader, tenant);
336    let mut names = std::collections::BTreeSet::new();
337    let record = if point_fields::predicate_fields(predicate, &mut names) {
338        let selected: std::collections::HashSet<String> = names
339            .iter()
340            .flat_map(|name| [name.clone(), normalize(name)])
341            .collect();
342        session.reconstruct_record_selected(ord, &selected)?
343    } else {
344        session.reconstruct_record(ord)?
345    };
346    let _ = snap;
347    Ok(match record {
348        Some(Json::Object(map)) => map,
349        _ => Map::new(),
350    })
351}
352
353/// The properties `fields` names, by point reads, or the rebuilt record.
354pub(crate) fn read_row(
355    fields: Option<&PointFields>,
356    session: &mut reconstruct::ReconstructSession<'_, QuerySnapshot>,
357    snap: &QuerySnapshot,
358    tenant: rushdb_types::ids::TenantId,
359    ord: u64,
360) -> Result<Map<String, Json>, QueryError> {
361    match fields {
362        Some(fields) => fields.row(snap, tenant, ord),
363        None => Ok(session
364            .reconstruct_record(ord)?
365            .and_then(|record| match record {
366                Json::Object(map) => Some(map),
367                _ => None,
368            })
369            .unwrap_or_default()),
370    }
371}
372
373/// A page's plan: the predicate planner's owner set, what is known before
374/// reading, and the order-first plan if one reads the page in fewer rows.
375pub(crate) struct PagePlan {
376    index_plan: Option<planner::IndexPlan>,
377    exact: bool,
378    known: Option<u64>,
379    predicate: Option<Expr>,
380    order: Option<order::OrderPlan>,
381}
382
383/// Plan the page `skip..skip + limit` of `req`: the predicate planner first,
384/// then, when `order_first` is allowed, the order planner on its numbers.
385pub(crate) fn plan_page(
386    lmpg: &Lmpg,
387    req: &SearchRequest,
388    is_cancelled: &dyn Fn() -> bool,
389    snap: &QuerySnapshot,
390    (skip, limit): (usize, usize),
391    known_total: Option<u64>,
392    order_first: bool,
393) -> Result<PagePlan, QueryError> {
394    let index_plan =
395        index_candidates_with_budget(lmpg, snap, req, working_set_budget(lmpg), is_cancelled)?;
396    let (exact, known, predicate, candidates) = if let Some(plan) = &index_plan {
397        let known = known_total
398            .or(plan.known_total)
399            .or_else(|| plan.exact.then(|| plan.ords.len()));
400        (plan.exact, known, plan.residual.clone(), plan.ords.len())
401    } else {
402        let exact = req.where_.is_none() && req.related.is_empty();
403        let records = walk_total(lmpg, snap, &req.labels)?;
404        let known = known_total.or_else(|| exact.then_some(records));
405        (exact, known, req.where_.clone(), records)
406    };
407    // An owner set cut to the page (the bounded `$exists` plan) holds only
408    // the first matches: an order-first walk would check membership in it.
409    let whole_set = index_plan
410        .as_ref()
411        .is_none_or(|plan| plan.known_total.is_none());
412    let order = if order_first && whole_set {
413        order::plan_order(
414            snap.as_ref(),
415            lmpg.tenant,
416            &order::PageShape {
417                labels: &req.labels,
418                order_by: &req.order_by,
419                skip,
420                limit,
421                matches: known,
422                candidates,
423            },
424        )?
425    } else {
426        None
427    };
428    Ok(PagePlan {
429        index_plan,
430        exact,
431        known,
432        predicate,
433        order,
434    })
435}
436
437/// How a page ordered by a single key is read when a source already in
438/// that order wins over reading every candidate (`describe.explain`).
439#[derive(Clone, Debug, PartialEq, Eq)]
440pub struct OrderExplain {
441    /// `storage` (record creation order), `id` or `property`.
442    pub source: &'static str,
443    /// The page is read from the far end of the order and reversed.
444    pub reverse_walk: bool,
445    /// Estimated rows the ordered walk reads.
446    pub estimated_reads: u64,
447    /// Rows reading every candidate would take.
448    pub predicate_first_reads: u64,
449}
450
451/// The order-first plan of the page `skip..skip + limit` of `req`, if one
452/// wins; it does not read the page.
453///
454/// # Errors
455/// Returns storage read errors from the planners' counters.
456pub fn explain_order(
457    lmpg: &Lmpg,
458    req: &SearchRequest,
459    snap: &QuerySnapshot,
460    skip: usize,
461    limit: usize,
462) -> Result<Option<OrderExplain>, QueryError> {
463    let plan = plan_page(lmpg, req, &|| false, snap, (skip, limit), None, true)?;
464    Ok(plan.order.map(|order| OrderExplain {
465        source: order.source(),
466        reverse_walk: order.mirror(),
467        estimated_reads: order.order_first_reads,
468        predicate_first_reads: order.predicate_first_reads,
469    }))
470}
471
472/// One stretch of a search's candidates.
473pub(crate) struct Phase<'v> {
474    candidates: scan::Candidates<'v>,
475    /// The candidates are not in the requested order: their best rows are
476    /// kept by reading and sorting their order fields. Otherwise they are
477    /// (or no order is requested) and are kept as they come.
478    sorted: bool,
479}
480
481/// Where a search's candidates come from and what is left to check.
482pub(crate) struct CandidateSource<'v> {
483    /// The candidates, one stretch after another.
484    phases: Vec<Phase<'v>>,
485    /// Every candidate is a live match: neither the predicate nor the
486    /// record's existence is checked per candidate.
487    exact: bool,
488    /// The total, when known without counting.
489    known: Option<u64>,
490    /// What is left to check per candidate: the conjuncts the owner set does
491    /// not answer, or the whole `where` on a walk.
492    predicate: Option<Expr>,
493    /// The index plan's owner set, checked per candidate when the candidates
494    /// come from an order-first walk instead of the set.
495    member_of: Option<planner::OwnerSet>,
496    /// The order-first plan the phases follow, if one won.
497    plan: Option<order::OrderPlan>,
498}
499
500/// The candidates of a search: the index plan's owner set (charged to
501/// `working`) or a walk of the tenant's or labels' records, unless
502/// `order_first` is allowed and an order-first plan reads the page in fewer
503/// rows ([`order::plan_order`]).
504#[expect(
505    clippy::too_many_arguments,
506    reason = "the pass's inputs, each used once to choose its source"
507)]
508pub(crate) fn candidate_source<'v>(
509    lmpg: &Lmpg,
510    req: &SearchRequest,
511    is_cancelled: &dyn Fn() -> bool,
512    snap: &'v QuerySnapshot,
513    (skip, limit): (usize, usize),
514    known_total: Option<u64>,
515    order_first: bool,
516    working: &mut WorkingSet,
517) -> Result<CandidateSource<'v>, QueryError> {
518    let PagePlan {
519        index_plan,
520        exact,
521        known,
522        predicate,
523        order: plan,
524    } = plan_page(
525        lmpg,
526        req,
527        is_cancelled,
528        snap,
529        (skip, limit),
530        known_total,
531        order_first,
532    )?;
533    if let Some(plan) = &index_plan {
534        working.charge(
535            u64::try_from(plan.ords.serialized_size()).unwrap_or(u64::MAX),
536            true,
537        )?;
538    }
539    let in_order = |candidates| Phase {
540        candidates,
541        sorted: false,
542    };
543    let Some(plan) = plan else {
544        let candidates = match index_plan {
545            Some(plan) => scan::Candidates::from_set(plan.ords),
546            None => scan::Candidates::walk(snap, lmpg.tenant, &req.labels)?,
547        };
548        return Ok(CandidateSource {
549            phases: vec![Phase {
550                candidates,
551                sorted: !req.order_by.is_empty(),
552            }],
553            exact,
554            known,
555            predicate,
556            member_of: None,
557            plan: None,
558        });
559    };
560    let (phases, member_of) = match &plan.path {
561        order::OrderPath::StorageMirror => {
562            let candidates = match index_plan {
563                Some(plan) => scan::Candidates::from_set_descending(plan.ords),
564                None => scan::Candidates::walk_ordered(snap, lmpg.tenant, &req.labels, true)?,
565            };
566            (vec![in_order(candidates)], None)
567        }
568        order::OrderPath::Id {
569            walk_descending, ..
570        } => (
571            vec![in_order(scan::Candidates::by_id(
572                snap,
573                lmpg.tenant,
574                &req.labels,
575                *walk_descending,
576            )?)],
577            index_plan.map(|plan| plan.ords),
578        ),
579        order::OrderPath::Property {
580            descending,
581            pids,
582            holding,
583            scope,
584            missing,
585            index,
586        } => {
587            let phases = property_phases(
588                snap,
589                lmpg.tenant,
590                *descending,
591                (pids, holding, scope),
592                *missing,
593                index.as_ref(),
594            )?;
595            (phases, index_plan.map(|plan| plan.ords))
596        }
597    };
598    let mut phases = phases;
599    for phase in &mut phases {
600        // Past the rows predicate-first reads, the walk is not worth it.
601        phase.candidates.give_up_after(plan.predicate_first_reads);
602    }
603    Ok(CandidateSource {
604        phases,
605        exact,
606        known,
607        predicate,
608        member_of,
609        plan: Some(plan),
610    })
611}
612
613/// One ordering row: a match's ordinal and the properties it is ordered by.
614pub(crate) type OrderRow = (u64, Map<String, Json>);
615
616/// Sort the ordering buffer and keep its best `keep` rows; returns the bytes
617/// of the rows dropped.
618pub(crate) fn cut_ordered(
619    ordered: &mut Vec<OrderRow>,
620    bytes: &mut Vec<u64>,
621    req: &SearchRequest,
622    keep: usize,
623) -> u64 {
624    let mut rows: Vec<(OrderRow, u64)> = ordered.drain(..).zip(bytes.drain(..)).collect();
625    // Candidates arrive in ascending ordinal order and the sort is stable,
626    // so ties keep that order across cuts, exactly as one sort of every
627    // match would.
628    sort_ordered_rows(&mut rows, req);
629    let freed = rows.iter().skip(keep).map(|(_, b)| *b).sum();
630    rows.truncate(keep);
631    for (row, b) in rows {
632        ordered.push(row);
633        bytes.push(b);
634    }
635    freed
636}
637
638/// Live records of the tenant (`labels` empty) or of `labels`, from counters.
639pub(crate) fn walk_total(
640    lmpg: &Lmpg,
641    snap: &QuerySnapshot,
642    labels: &[String],
643) -> Result<u64, QueryError> {
644    if labels.is_empty() {
645        return Ok(rushdb_lmpg::derived::tenant_usage(snap.as_ref(), lmpg.tenant)?.records);
646    }
647    label_total(snap.as_ref(), lmpg.tenant, labels)
648}
649
650/// Live records of `labels`, from their counters.
651pub(crate) fn label_total<R: rushdb_lmpg::reader::TryRead + ?Sized>(
652    snap: &R,
653    tenant: rushdb_types::ids::TenantId,
654    labels: &[String],
655) -> Result<u64, QueryError> {
656    let distinct: std::collections::BTreeSet<&String> = labels.iter().collect();
657    let mut total = 0u64;
658    for label in distinct {
659        total = total.saturating_add(rushdb_lmpg::derived::label_count(snap, tenant, label)?);
660    }
661    Ok(total)
662}
663
664/// The phases of a property order: the records missing the property (first
665/// ascending, last descending) and its holders, read and sorted, or (with
666/// an index order) its nulls and its indexed values, each already in order.
667fn property_phases<'v>(
668    snap: &'v QuerySnapshot,
669    tenant: rushdb_types::ids::TenantId,
670    descending: bool,
671    (pids, holding, scope): (&[u64], &std::collections::BTreeSet<String>, &[String]),
672    missing: u64,
673    index: Option<&order::IndexOrder>,
674) -> Result<Vec<Phase<'v>>, QueryError> {
675    let in_order = |candidates| Phase {
676        candidates,
677        sorted: false,
678    };
679    let missing = in_order(scan::Candidates::missing(
680        snap,
681        tenant,
682        scope,
683        holding.clone(),
684        pids.to_vec(),
685        missing,
686    )?);
687    let presence = |nullness: &[u8]| {
688        let mut prefixes = Vec::new();
689        for label in holding {
690            for &pid in pids {
691                for &nullness in nullness {
692                    prefixes.push(rushdb_types::keys::presence_nullness_prefix(
693                        tenant,
694                        rushdb_types::ids::OwnerKind::Record,
695                        label,
696                        pid,
697                        nullness,
698                    ));
699                }
700            }
701        }
702        scan::Candidates::presence(snap, &prefixes)
703    };
704    // Missing values sort first ascending and last descending.
705    let phases = match index {
706        None => {
707            let holders = Phase {
708                candidates: presence(&[
709                    rushdb_types::keys::NULLNESS_VALUE,
710                    rushdb_types::keys::NULLNESS_NULL,
711                ])?,
712                sorted: true,
713            };
714            if descending {
715                vec![holders, missing]
716            } else {
717                vec![missing, holders]
718            }
719        }
720        Some(index) => {
721            // Nulls sort before numbers and after strings; the
722            // index walks the values themselves in order.
723            let nulls = in_order(presence(&[rushdb_types::keys::NULLNESS_NULL])?);
724            let values = in_order(scan::Candidates::index_order(
725                snap, tenant, &index.ids, descending,
726            )?);
727            let mut phases = if index.numeric {
728                vec![missing, nulls, values]
729            } else {
730                vec![missing, values, nulls]
731            };
732            if descending {
733                phases.reverse();
734            }
735            phases
736        }
737    };
738    Ok(phases)
739}