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