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