Skip to main content

drive/query/drive_document_ranked_query/
index_picker.rs

1//! Covering-index picker for the ranked query, plus the shared
2//! prefix-value encoding.
3//!
4//! Pure functions on the document type's index map plus the
5//! `(group property, equality pins, axis, aggregate field)` tuple
6//! [`super::mode_detection`] resolved. No Drive, no proof — the server
7//! and the SDK verifier both call these so they land on the same index
8//! (and therefore the same grove path) for the same request.
9
10use super::{DocumentRankedMode, DriveDocumentRankedQuery, PrefixPin, RankedAxis};
11use crate::error::query::QuerySyntaxError;
12use crate::error::Error;
13use crate::query::{index_admissible_for_resolved_time_range, ResolvedTimeRange};
14use dpp::data_contract::document_type::methods::DocumentTypeV0Methods;
15use dpp::data_contract::document_type::{DocumentTypeRef, Index};
16use dpp::version::PlatformVersion;
17use std::collections::BTreeMap;
18
19/// Find the index that can serve `axis` ranking grouped by
20/// `group_by_property` with the given equality pins, aggregating
21/// `aggregate_field`.
22///
23/// An index qualifies when **all** of:
24///
25/// - one of its **ranked levels** for `axis` is `group_by_property`,
26///   with exactly one pin per property before it. For the boolean axes
27///   the ranked level is the index's **last** property; a prefix-level
28///   `rankedCountable: { at }` hosts a Count secondary at the `at`
29///   property as well, ranking its values by whole-subtree count — an
30///   index may declare both, and the (group property, pin count) pair
31///   singles out which level a request addresses. At a prefix level the
32///   properties *after* `at` never appear in the request at all (they
33///   are interior to the subtrees being counted);
34/// - every property **before** the ranked level is pinned: each appears
35///   (by name) among `equality_pin_fields`. Lengths matching plus the
36///   pins being distinct (enforced upstream by
37///   [`super::mode_detection::prefix_pins_from_where_clauses`]) makes
38///   this set equality, so no pin is left over either;
39/// - it declares the ranking keyword for `axis`
40///   ([`RankedAxis::required_index_keyword`]);
41/// - for [`RankedAxis::Sum`] / [`RankedAxis::Avg`], its `summable`
42///   property is exactly `aggregate_field`. Both axes are derived from
43///   the same running sum the index maintains (`Avg` is that sum over the
44///   group's count), so summing a *different* field than the one the
45///   index accumulates would silently answer about the wrong property.
46///   A prefix-level Count ranking excludes both (the grammar rejects the
47///   combination), so those arms never see an `at` index;
48/// - it is admissible for the request's resolved time-range selections
49///   ([`index_admissible_for_resolved_time_range`]): a request whose
50///   leading pin was produced by `IN_TIME_RANGE` resolution may only be
51///   served by the index bucketing that field with exactly that grid,
52///   and a raw request never by a bucketed index — either mismatch
53///   would be a validly-proven wrong answer. The resolved bucket-start
54///   pin then descends the grid-qualified first level like any other
55///   leading-property pin, into that window's own per-prefix secondary.
56///
57/// With no pins this degenerates to the original single-property rule —
58/// or, for an index ranked at its FIRST property, to the global group
59/// ranking ("top hashtags by total likes" with nothing pinned). A
60/// partial pin (some but not all leading properties) matches nothing —
61/// the per-prefix secondary lives under one value tree per leading
62/// property, so there is no subtree an unpinned prefix could address —
63/// and callers turn the `None` into a loud
64/// [`crate::error::query::QuerySyntaxError`] naming what is missing.
65///
66/// Returns `None` when nothing qualifies; callers turn that into
67/// [`crate::error::query::QuerySyntaxError::WhereClauseOnNonIndexedProperty`]
68/// with a message naming the missing keyword.
69///
70/// At most one index can qualify for a given `(group property, pins,
71/// axis, field)` tuple — rs-dpp rejects two indexes over the same
72/// property set on one document type — so "first match wins" is not a
73/// tie-break in practice. Should that ever change, the `BTreeMap`
74/// iteration order (index name, ascending) keeps the choice
75/// deterministic, which is what prover/verifier agreement actually
76/// requires: both sides run this same function over the same contract
77/// and must land on the same grove path.
78///
79/// Note that axis availability is decided from the index's `ranked_*`
80/// flags, **not** from the element variant the write path laid down: a
81/// `rankedCountable` index that also declares `rangeSummable` is stored
82/// as a `ProvableCountProvableSumIndexedTree` carrying only the Count
83/// axis, so the element variant alone would over-report what is rankable.
84pub fn find_ranked_index_for_axis<'b>(
85    indexes: &'b BTreeMap<String, Index>,
86    group_by_property: &str,
87    equality_pin_fields: &[String],
88    axis: RankedAxis,
89    aggregate_field: &str,
90    resolved_time_ranges: &[ResolvedTimeRange],
91) -> Option<&'b Index> {
92    indexes.values().find(|index| {
93        // Bucketed and raw indexes are never interchangeable, and one
94        // grid's index never serves another grid's resolution — the same
95        // provenance rule every other aggregate picker applies. This is
96        // what keeps a raw request off bucketed indexes AND routes a
97        // resolved request to exactly the grid it was resolved against.
98        if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) {
99            return false;
100        }
101        // The positions whose levels host this axis's secondaries — for
102        // the Count axis every `at` level plus the terminal when the
103        // boolean is on (any subset of an index's levels may rank); empty
104        // when the index does not declare the axis (or aggregates a
105        // different field than requested).
106        let candidate_positions: Vec<usize> = match axis {
107            RankedAxis::Count => index
108                .ranked_countable_at
109                .iter()
110                .filter_map(|at| index.properties.iter().position(|p| &p.name == at))
111                .chain(
112                    index
113                        .ranked_countable
114                        .then(|| index.properties.len().checked_sub(1))
115                        .flatten(),
116                )
117                .collect(),
118            RankedAxis::Sum => (index.ranked_summable
119                && index.summable.as_deref() == Some(aggregate_field))
120            .then(|| index.properties.len().checked_sub(1))
121            .flatten()
122            .into_iter()
123            .collect(),
124            RankedAxis::Avg => (index.ranked_averageable
125                && index.summable.as_deref() == Some(aggregate_field))
126            .then(|| index.properties.len().checked_sub(1))
127            .flatten()
128            .into_iter()
129            .collect(),
130        };
131        // A candidate matches when its property is the grouping property
132        // and every property before it is pinned exactly once (length
133        // equality + distinct pins ⇒ set equality). At most one candidate
134        // can match a given request: the two levels are distinct
135        // positions, and the pin count singles one out.
136        candidate_positions.into_iter().any(|ranked_position| {
137            let Some(ranked_property) = index.properties.get(ranked_position) else {
138                return false;
139            };
140            let leading = &index.properties[..ranked_position];
141            ranked_property.name == group_by_property
142                && leading.len() == equality_pin_fields.len()
143                && leading
144                    .iter()
145                    .all(|property| equality_pin_fields.iter().any(|f| f == &property.name))
146        })
147    })
148}
149
150/// [`find_ranked_index_for_axis`] driven straight from a resolved
151/// [`DocumentRankedMode`] — the shape every caller actually has.
152pub fn find_ranked_index_for_mode<'b>(
153    indexes: &'b BTreeMap<String, Index>,
154    mode: &DocumentRankedMode,
155    resolved_time_ranges: &[ResolvedTimeRange],
156) -> Option<&'b Index> {
157    let pin_fields: Vec<String> = mode
158        .prefix_pins
159        .iter()
160        .map(|pin| pin.field.clone())
161        .collect();
162    find_ranked_index_for_axis(
163        indexes,
164        &mode.group_by_property,
165        &pin_fields,
166        mode.axis,
167        &mode.aggregate_field,
168        resolved_time_ranges,
169    )
170}
171
172/// Resolve a validated [`DocumentRankedMode`] against a document type's
173/// indexes into the executable [`DriveDocumentRankedQuery`]: pick the
174/// covering index, encode the prefix pins into prefix **branches** (one
175/// branch for all-`==` pins, one branch per element of the single
176/// permitted `IN`), and assemble the query.
177///
178/// This is the **one** resolution path — the server's executors and the
179/// SDK's proof helpers both call it, which is what guarantees a proof
180/// and an unproven read (and the client's verification) are about the
181/// same subtree.
182///
183/// `indexes` is threaded in separately rather than read off
184/// `document_type` here because
185/// [`DocumentTypeV0Getters::indexes`](dpp::data_contract::document_type::accessors::DocumentTypeV0Getters::indexes)
186/// borrows its receiver — taking the map from the caller lets the
187/// returned query's `&'a Index` outlive this frame. Callers pass
188/// `document_type.indexes()`.
189///
190/// The main failure is "no index covers this", reported with the exact
191/// contract keyword (and, for pinned requests, the exact index shape)
192/// the request needs, so the caller can act on it without reading the
193/// schema spec.
194pub fn resolve_ranked_query_for_mode<'a>(
195    contract_id: [u8; 32],
196    document_type: DocumentTypeRef<'a>,
197    document_type_name: String,
198    indexes: &'a BTreeMap<String, Index>,
199    mode: &DocumentRankedMode,
200    resolved_time_ranges: &[ResolvedTimeRange],
201    platform_version: &PlatformVersion,
202) -> Result<DriveDocumentRankedQuery<'a>, Error> {
203    let index =
204        find_ranked_index_for_mode(indexes, mode, resolved_time_ranges).ok_or_else(|| {
205            Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
206                no_covering_index_message(
207                    "ranked",
208                    mode.axis,
209                    &mode.group_by_property,
210                    &mode.prefix_pins,
211                    &mode.aggregate_field,
212                ),
213            ))
214        })?;
215    let prefix_branches =
216        encode_prefix_branches(document_type, index, &mode.prefix_pins, platform_version)?;
217    Ok(DriveDocumentRankedQuery {
218        document_type,
219        contract_id,
220        document_type_name,
221        index,
222        prefix_branches,
223        axis: mode.axis,
224        descending: mode.descending,
225        k: mode.k,
226        offset: mode.offset,
227    })
228}
229
230/// The "no index covers this request" rejection text, shared by the
231/// ranked and having-range resolutions (and the SDK's mirrors of them)
232/// so a rejected request reads identically everywhere. Names the exact
233/// index the request needs: property list (pins first, in request
234/// order, then the grouping property), ranking keyword, and `summable`
235/// field where applicable.
236pub fn no_covering_index_message(
237    surface: &str,
238    axis: RankedAxis,
239    group_by_property: &str,
240    prefix_pins: &[PrefixPin],
241    aggregate_field: &str,
242) -> String {
243    let pin_fields = || {
244        prefix_pins
245            .iter()
246            .map(|pin| pin.field.as_str())
247            .collect::<Vec<_>>()
248            .join(", ")
249    };
250    let index_shape = if prefix_pins.is_empty() {
251        format!("a single-property index on `{group_by_property}`")
252    } else {
253        format!(
254            "a compound index on [{}, {group_by_property}] (every leading property pinned \
255             by an equality or `IN` `where` clause, the trailing property grouped over)",
256            pin_fields()
257        )
258    };
259    format!(
260        "no ranked index covers `group_by = [{group_by_property}]`{} on the {axis:?} axis \
261         for this {surface} query: the document type needs {index_shape} declaring `{}`{}",
262        if prefix_pins.is_empty() {
263            String::new()
264        } else {
265            format!(" with pins on [{}]", pin_fields())
266        },
267        axis.required_index_keyword(),
268        if aggregate_field.is_empty() {
269            String::new()
270        } else {
271            format!(" with `summable: \"{aggregate_field}\"`")
272        }
273    )
274}
275
276/// Encode the resolved prefix pins into **branches** — one
277/// `Vec<Vec<u8>>` of prefix path segments per branch, in index-property
278/// order (the same order and encoding the write path used to key those
279/// prefix value trees). A request with only `==` pins yields exactly
280/// one branch; the (at most one) `IN` pin yields one branch per
281/// element.
282///
283/// This is part of the prover/verifier agreement: server executors and
284/// the SDK's proof helpers both come through here, so a pinned value
285/// can only ever name one subtree — and a branch *set* only ever one
286/// ordered subtree list — identically on both sides. Branch order is
287/// canonical: ascending by encoded segment bytes, independent of the
288/// caller's element order (which also makes `null`, the empty segment,
289/// sort first deterministically).
290///
291/// `index` must have been picked by [`find_ranked_index_for_axis`]
292/// against these same pins — every leading property is then guaranteed
293/// a pin. A value the property's type cannot encode is a caller error
294/// naming the property; two `IN` elements that encode to the same
295/// segment (two spellings of one value) are one branch and are rejected
296/// as a duplicate rather than walked twice.
297pub fn encode_prefix_branches(
298    document_type: DocumentTypeRef,
299    index: &Index,
300    prefix_pins: &[PrefixPin],
301    platform_version: &PlatformVersion,
302) -> Result<Vec<Vec<Vec<u8>>>, Error> {
303    // The pinnable properties end at the ranked level the pin count
304    // addresses (an index may host secondaries at both its `at` property
305    // and its terminal — the pin count singles one out; properties past
306    // it are interior to the counted subtrees and never pinned).
307    let (leading, _) = super::path::ranked_level_split(index, prefix_pins.len())?;
308    // Enforced BEFORE any encoding: the ceiling bounds every downstream
309    // cost (encode, sort, clone, walk, proof size), so an oversized pin
310    // must not buy that work first. The post-product branch count check
311    // below stays as a backstop.
312    if prefix_pins
313        .iter()
314        .any(|pin| pin.values.len() > super::MAX_PREFIX_IN_BRANCHES)
315    {
316        return Err(Error::Query(
317            QuerySyntaxError::InvalidWhereClauseComponents(
318                "an `IN` prefix pin fans out into more branches than the ranked surface serves \
319             — narrow the element list or issue several requests",
320            ),
321        ));
322    }
323    let per_property: Vec<Vec<Vec<u8>>> = leading
324        .iter()
325        .map(|property| {
326            let pin = prefix_pins
327                .iter()
328                .find(|pin| pin.field == property.name)
329                .ok_or_else(|| {
330                    Error::Query(QuerySyntaxError::InvalidWhereClauseComponents(
331                        "internal resolution mismatch: the picked compound ranked index has \
332                         a leading property with no pin — the index picker and the prefix \
333                         encoder disagreed on the pins",
334                    ))
335                })?;
336            let mut encoded = pin
337                .values
338                .iter()
339                .map(|value| {
340                    // A null pin addresses the subtree the write walkers
341                    // create for an **absent** value: they encode it as
342                    // `get_raw_for_document_type(..).unwrap_or_default()`
343                    // — an empty path segment — for user and system
344                    // properties alike. Null must short-circuit here
345                    // because the system-property encoders (`$updatedAt`,
346                    // `$creatorId`, …) reject null before any encoding
347                    // happens, which would make the stored empty-segment
348                    // prefix unaddressable.
349                    if value.is_null() {
350                        return Ok(Vec::new());
351                    }
352                    document_type
353                        .serialize_value_for_key(&property.name, value, platform_version)
354                        .map_err(|e| {
355                            Error::Query(QuerySyntaxError::InvalidParameter(format!(
356                                "the pin on `{}` does not encode as that property's \
357                                 index key: {e}",
358                                property.name
359                            )))
360                        })
361                })
362                .collect::<Result<Vec<_>, Error>>()?;
363            if encoded.len() > 1 {
364                encoded.sort();
365                if encoded.windows(2).any(|pair| pair[0] == pair[1]) {
366                    return Err(Error::Query(
367                        QuerySyntaxError::InvalidWhereClauseComponents(
368                            "an `IN` pin's elements encode to the same index key: two \
369                             spellings of one value are one prefix branch — deduplicate \
370                             the element list",
371                        ),
372                    ));
373                }
374            }
375            Ok(encoded)
376        })
377        .collect::<Result<Vec<_>, Error>>()?;
378
379    // Defense in depth at the shared choke point: the grammar enforces
380    // both invariants upstream, but this function is `pub` and the
381    // prover/verifier agreement hangs off it, so a mis-built pin set
382    // must fail here rather than collapse to zero branches (a
383    // downstream panic) or fan out into an unbounded cartesian product
384    // (which would also break the one-varying-position assumption
385    // `in_key` and the merge order rely on).
386    if per_property.iter().any(|candidates| candidates.is_empty()) {
387        return Err(Error::Query(
388            QuerySyntaxError::InvalidWhereClauseComponents(
389                "internal resolution mismatch: a prefix pin carries no values",
390            ),
391        ));
392    }
393    if per_property
394        .iter()
395        .filter(|candidates| candidates.len() > 1)
396        .count()
397        > 1
398    {
399        return Err(Error::Query(
400            QuerySyntaxError::InvalidWhereClauseComponents(
401                "internal resolution mismatch: more than one branching pin — the grammar \
402             admits at most one `IN` across the prefix properties",
403            ),
404        ));
405    }
406
407    // A single `null` pin encodes as the empty path segment; the branched
408    // proof grammar (`PathQuery::new_branched_axis`) cannot address an
409    // empty segment in the shared prefix or suffix, so a null `==` pin
410    // combined with an `IN` would serve the unproved read and fail the
411    // prove — the exact proved/unproved divergence this surface forbids.
412    // Rejected for any non-branching pin position, conservatively: issue
413    // one request per `IN` element to combine null pins with multiple
414    // prefixes. `null` as an ELEMENT of the `IN` itself stays legal — it
415    // is a branch key, which the envelope addresses and authenticates
416    // like any other.
417    let has_branching_pin = per_property.iter().any(|candidates| candidates.len() > 1);
418    if has_branching_pin
419        && per_property
420            .iter()
421            .any(|candidates| candidates.len() == 1 && candidates[0].is_empty())
422    {
423        return Err(Error::Query(
424            QuerySyntaxError::InvalidWhereClauseComponents(
425                "an `IN` prefix pin cannot be combined with a `null` pin: null addresses the \
426             absent-value prefix through an empty path segment, which the branched proof \
427             cannot express — issue one request per `IN` element instead",
428            ),
429        ));
430    }
431
432    // The grammar admits at most one multi-value pin, so this product
433    // is |IN| branches (or exactly one), already in canonical order
434    // because the only varying position was sorted above.
435    let mut branches: Vec<Vec<Vec<u8>>> = vec![Vec::with_capacity(leading.len())];
436    for candidates in per_property {
437        branches = branches
438            .into_iter()
439            .flat_map(|prefix| {
440                candidates.iter().map(move |segment| {
441                    let mut branch = prefix.clone();
442                    branch.push(segment.clone());
443                    branch
444                })
445            })
446            .collect();
447    }
448    // The documented hard ceiling on branch fan-out, enforced at the
449    // shared choke point too: this function is `pub`, and everything
450    // downstream (encoding, sorting, per-branch walks, proof size) is
451    // linear in the branch count.
452    if branches.len() > super::MAX_PREFIX_IN_BRANCHES {
453        return Err(Error::Query(
454            QuerySyntaxError::InvalidWhereClauseComponents(
455                "an `IN` prefix pin fans out into more branches than the ranked surface serves \
456             — narrow the element list or issue several requests",
457            ),
458        ));
459    }
460    Ok(branches)
461}