Skip to main content

drive/query/drive_document_ranked_query/
mod.rs

1//! Types and module structure for the **ranked** (top-k / bottom-k)
2//! document query —
3//! `SELECT <agg> GROUP BY <prop> ORDER BY <agg> DESC LIMIT n OFFSET m`.
4//!
5//! A ranked query answers "which `n` groups score highest (or lowest) on
6//! an aggregate, starting from rank `m`?" in `O(log n + k)` with a proof,
7//! by reading grovedb's per-axis *secondary* Merk of an indexed tree
8//! (grovedb PR #657). The contract opts in per index via
9//! `rankedCountable` / `rankedSummable` / `rankedAverageable` (meta
10//! schema v3 / PV14); the write path keeps the secondaries in sync. See
11//! [`crate::drive::document::ranked_index_tree_type`] for the storage
12//! layout this query reads.
13//!
14//! The implementation is split across siblings, mirroring
15//! [`super::drive_document_count_query`]:
16//! - [`mode_detection`] — request-shape validation + the versioned
17//!   [`mode_detection::detect_ranked_mode`] that resolves
18//!   `(select, group_by, order_by, limit, offset)` into a
19//!   [`DocumentRankedMode`].
20//! - [`index_picker`] — [`index_picker::find_ranked_index_for_axis`],
21//!   the covering-index picker for a `(group_by property, axis,
22//!   aggregate field)` triple.
23//! - [`path`] — the load-bearing prover/verifier-agreement path builder
24//!   ([`DriveDocumentRankedQuery::indexed_property_name_tree_path`]).
25//! - [`execute_top_k`] — the two executors on
26//!   [`DriveDocumentRankedQuery`] (no-proof read, proof generation).
27//! - [`executors`] — the `impl Drive` wrappers the dispatcher calls.
28//! - [`drive_dispatcher`] — [`DocumentRankedRequest`] /
29//!   [`DocumentRankedResponse`] and
30//!   [`crate::drive::Drive::execute_document_ranked_request`].
31//! - [`tests`] (cfg `server` + `test`) — unit + integration tests.
32//!
33//! ## What makes this query shape different
34//!
35//! Every other aggregate query in this crate walks *value trees* under a
36//! property-name tree and aggregates what it finds. A ranked query never
37//! touches the value trees at all: the answer lives pre-sorted in the
38//! secondary Merk, keyed by `(sort_key ‖ group_key)`. Three consequences
39//! shape the API:
40//!
41//! 1. **`where` clauses are equality pins on a compound prefix — or
42//!    absent.** A single-property ranked index has no prefix to narrow,
43//!    so its requests carry no `where`. A compound ranked index
44//!    `[p1, …, pn]` maintains one secondary **per prefix value**
45//!    (per-prefix semantics: each terminal `pn` property-name tree,
46//!    inside the `[p1, …, pn-1]` value trees, is its own indexed tree
47//!    — grovedb creates and populates it in the same document batch),
48//!    so a request must pin every leading property with an equality
49//!    clause to name which prefix's secondary the walk reads. A `where`
50//!    on the grouped (terminal) property itself would ask for a
51//!    *filtered* ranking, which no secondary can express — it is sorted
52//!    by aggregate, not by group key — and is rejected rather than
53//!    silently ignored, as is any non-equality prefix clause except one
54//!    `IN`: exactly one leading pin may carry 2..=[`MAX_PREFIX_IN_BRANCHES`]
55//!    distinct elements (a single-element `IN` normalizes to the
56//!    equality pin), read as one walk per element and merged by
57//!    `(aggregate, encoded pin, group key)`, proved in a single branched
58//!    `PathQuery` envelope with per-element authenticated absence. A
59//!    `null` pin cannot combine with an `IN` (null addresses its prefix
60//!    through an empty path segment the branched proof cannot express),
61//!    and `OFFSET` is rejected together with `IN`.
62//! 2. **`limit` is mandatory, `offset` is depth-bounded, `start_at` is
63//!    refused.**
64//!    `limit` is the `k` of the walk and the ranked surface has no
65//!    server default for it, so it must be supplied. `offset` is the
66//!    rank the page starts at and is unbounded above: grovedb counts
67//!    the skipped region from the subtree aggregates rather than
68//!    walking it entry by entry, so both executors are `O(log n + k)`
69//!    *regardless of offset* and a large offset is not a cost lever on
70//!    either. Only the proved result additionally attests the count.
71//!    So the offset needs no ceiling. `start_at` / `start_after` name a document id,
72//!    which does not appear anywhere in an aggregate-ordered keyspace.
73//! 3. **Entry order IS the ranking order.** The executor returns entries
74//!    in the order grovedb walked the secondary; callers must not
75//!    re-sort. Ties are broken by group key — see
76//!    [`DriveDocumentRankedQuery::descending`].
77
78#[cfg(any(feature = "server", feature = "verify"))]
79use crate::error::query::QuerySyntaxError;
80#[cfg(any(feature = "server", feature = "verify"))]
81use crate::error::Error;
82#[cfg(any(feature = "server", feature = "verify"))]
83use crate::query::drive_document_count_query::counter_sum_as_document_count;
84#[cfg(any(feature = "server", feature = "verify"))]
85use dpp::data_contract::document_type::{DocumentTypeRef, Index};
86#[cfg(any(feature = "server", feature = "verify"))]
87use dpp::platform_value::Value;
88
89/// The fixed-point scale grovedb's Avg axis sorts by:
90/// `avg_fixed_point = floor(sum * RANKED_AVG_SCALE / count)` with
91/// euclidean (toward -∞) division.
92///
93/// Re-exported from grovedb rather than re-declared so the two can never
94/// drift — the encoded sort keys in storage are produced with grovedb's
95/// constant, and a platform-side copy that fell out of step would silently
96/// mis-scale every average the client renders.
97#[cfg(any(feature = "server", feature = "verify"))]
98pub use grovedb::element::indexed::AVG_FIXED_POINT_SCALE as RANKED_AVG_SCALE;
99
100#[cfg(any(feature = "server", feature = "verify"))]
101pub(crate) mod branches;
102#[cfg(any(feature = "server", feature = "verify"))]
103pub mod index_picker;
104#[cfg(any(feature = "server", feature = "verify"))]
105pub mod mode_detection;
106#[cfg(any(feature = "server", feature = "verify"))]
107pub mod path;
108
109// Server-side execution paths.
110#[cfg(feature = "server")]
111pub mod drive_dispatcher;
112#[cfg(feature = "server")]
113pub mod execute_top_k;
114#[cfg(feature = "server")]
115pub mod executors;
116
117#[cfg(feature = "server")]
118pub use drive_dispatcher::{DocumentRankedRequest, DocumentRankedResponse};
119
120#[cfg(all(feature = "server", test))]
121mod tests;
122
123/// Hard ceiling on `k` (the request's `LIMIT`).
124///
125/// The ranked proof commits one secondary entry per returned group, so
126/// proof bytes grow linearly in `k`. 100 keeps the worst case in the same
127/// order of magnitude as the other aggregate proof surfaces (compare
128/// [`super::conditions::WhereClause::in_values`]'s 100-value cap on `In`
129/// fan-out) and matches the `In` bound callers already design against.
130///
131/// This is a **hard** ceiling, not a clamp: a request with `limit > 100`
132/// is rejected with
133/// [`crate::error::query::QuerySyntaxError::InvalidLimit`] rather than
134/// silently truncated. Truncation would be especially treacherous here
135/// because `k` is part of the traversal the client reconstructs for
136/// [`grovedb::GroveDb::verify_path_query`] — a server-side clamp would
137/// produce a page the client's reconstruction did not ask for.
138///
139/// There is deliberately **no companion ceiling on `OFFSET`**; see the
140/// module docs and [`DriveDocumentRankedQuery::offset`].
141#[cfg(any(feature = "server", feature = "verify"))]
142pub const MAX_RANKED_LIMIT: u16 = 100;
143
144/// Hard ceiling on the element count of the (at most one) `IN` prefix
145/// pin — the number of prefix **branches** one ranked / having-range
146/// request may fan out into.
147///
148/// Each element is one full secondary walk and one proof branch, each
149/// carrying up to `limit` committed entries plus boundary commitments,
150/// so worst-case proof size is `MAX_PREFIX_IN_BRANCHES ×
151/// MAX_RANKED_LIMIT` entries (≈100–150 KB at the ceiling). A hard
152/// rejection rather than a clamp, for the same reason as the limit: the
153/// branch set is bound into the branched proof envelope and re-checked
154/// by the verifier.
155#[cfg(any(feature = "server", feature = "verify"))]
156pub const MAX_PREFIX_IN_BRANCHES: usize = 10;
157
158/// The `ORDER BY` field name that means "the group's `COUNT(*)`".
159///
160/// `COUNT(*)` has no field to name, so the ranked grammar needs some
161/// token for "order by the thing the select projects". `$count` is
162/// chosen because the leading `$` is DPP's **system-property
163/// namespace** (`$id`, `$ownerId`, `$revision`, `$createdAt`, …): a
164/// document schema cannot declare a property whose name starts with
165/// `$`, so the sentinel is guaranteed not to collide with any real
166/// property a contract author could write, now or in any future
167/// contract. That is the whole reason it is spelled with a sigil rather
168/// than as `"count"` — a bare `count` would silently hijack ordering
169/// for any schema that happens to have a `count` column.
170#[cfg(any(feature = "server", feature = "verify"))]
171pub const RANKED_COUNT_ORDER_KEY: &str = "$count";
172
173/// Which per-group aggregate the groups are ranked by.
174///
175/// Maps 1:1 onto [`grovedb::element::IndexAxis`], the axis tag stored in
176/// an indexed tree's TLV and rebuilt into the `PathQuery` a verifier
177/// re-executes proofs against. Kept as a
178/// separate drive-side type (rather than re-exporting grovedb's) so the
179/// query surface's error messages and validation can talk about
180/// `rankedCountable` / `rankedSummable` / `rankedAverageable` — contract
181/// grammar the storage layer knows nothing about.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183#[cfg(any(feature = "server", feature = "verify"))]
184pub enum RankedAxis {
185    /// Rank by the number of documents in each group. Requires the
186    /// index to declare `rankedCountable`.
187    Count,
188    /// Rank by the running sum of the index's `summable` property across
189    /// each group. Requires `rankedSummable`.
190    Sum,
191    /// Rank by each group's average of the index's `summable` property,
192    /// as the fixed-point value described on [`RANKED_AVG_SCALE`].
193    /// Requires `rankedAverageable`.
194    Avg,
195}
196
197#[cfg(any(feature = "server", feature = "verify"))]
198impl From<RankedAxis> for grovedb::element::IndexAxis {
199    fn from(axis: RankedAxis) -> Self {
200        match axis {
201            RankedAxis::Count => grovedb::element::IndexAxis::Count,
202            RankedAxis::Sum => grovedb::element::IndexAxis::Sum,
203            RankedAxis::Avg => grovedb::element::IndexAxis::Avg,
204        }
205    }
206}
207
208#[cfg(any(feature = "server", feature = "verify"))]
209impl RankedAxis {
210    /// The contract-grammar keyword an index must declare to be rankable
211    /// on this axis. Used in error messages so a rejected query names the
212    /// exact schema key the contract author has to add.
213    pub fn required_index_keyword(self) -> &'static str {
214        match self {
215            RankedAxis::Count => "rankedCountable",
216            RankedAxis::Sum => "rankedSummable",
217            RankedAxis::Avg => "rankedAverageable",
218        }
219    }
220}
221
222/// The aggregate value carried by one ranked entry. Mirrors grovedb's
223/// [`grovedb::operations::proof::indexed_axis::AxisEntries`] variants
224/// exactly, one scalar at a time, so a `Vec<RankedEntry>` and an
225/// `AxisEntries` carry the same information with the same types.
226///
227/// The variant is redundant with the request's [`RankedAxis`] by
228/// construction; carrying it per entry means a decoded response is
229/// self-describing (no need to thread the request alongside it to know
230/// how to interpret the number), and lets both the executor and the
231/// verifier fail loudly if grovedb ever hands back an axis's entries
232/// under a different axis's request.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234#[cfg(any(feature = "server", feature = "verify"))]
235pub enum RankedEntryValue {
236    /// Document count in the group ([`RankedAxis::Count`]).
237    Count(u64),
238    /// Running sum over the group ([`RankedAxis::Sum`]).
239    Sum(i64),
240    /// Fixed-point average over the group ([`RankedAxis::Avg`]);
241    /// divide by [`RANKED_AVG_SCALE`] for the real value, or use
242    /// [`Self::as_f64`].
243    AvgFixedPoint(i128),
244}
245
246#[cfg(any(feature = "server", feature = "verify"))]
247impl RankedEntryValue {
248    /// The axis this value came from.
249    pub fn axis(self) -> RankedAxis {
250        match self {
251            RankedEntryValue::Count(_) => RankedAxis::Count,
252            RankedEntryValue::Sum(_) => RankedAxis::Sum,
253            RankedEntryValue::AvgFixedPoint(_) => RankedAxis::Avg,
254        }
255    }
256
257    /// The value as an `f64`, with the Avg variant scaled down by
258    /// [`RANKED_AVG_SCALE`].
259    ///
260    /// Lossy for large counts / sums (beyond 2^53) and for averages —
261    /// this is a display helper. Consensus-relevant comparisons must use
262    /// the exact integer variants; two groups whose fixed-point averages
263    /// differ can round to the same `f64`.
264    pub fn as_f64(self) -> f64 {
265        match self {
266            RankedEntryValue::Count(count) => count as f64,
267            RankedEntryValue::Sum(sum) => sum as f64,
268            RankedEntryValue::AvgFixedPoint(avg) => (avg as f64) / (RANKED_AVG_SCALE as f64),
269        }
270    }
271}
272
273/// One group in a ranked result: the group's index key plus its aggregate.
274///
275/// `key` is the **raw index-key bytes of the grouping property's value** —
276/// the same bytes that name the group's value tree under the indexed
277/// property-name tree (for a `string` property, its UTF-8 bytes). Callers
278/// that want the original typed value decode it with the document type's
279/// key deserialization; the query layer deliberately hands back bytes so
280/// prover and verifier agree without a DPP round-trip.
281#[derive(Debug, Clone, PartialEq, Eq)]
282#[cfg(any(feature = "server", feature = "verify"))]
283pub struct RankedEntry {
284    /// Raw index-key bytes of the grouped property value.
285    pub key: Vec<u8>,
286    /// The group's aggregate on the requested axis.
287    pub value: RankedEntryValue,
288    /// The branch this entry came from, on an `IN`-pinned request: the
289    /// encoded index-key segment of the `IN` position's pinned value
290    /// (empty bytes for the `null` branch). `None` on single-branch
291    /// responses — the same group key can appear under two prefixes, so
292    /// only a merged page needs the discriminator. See
293    /// [`branches::branch_in_key`].
294    pub in_key: Option<Vec<u8>>,
295}
296
297/// A resolved ranked query. Shared by the prover and the verifier — both
298/// build the grove path through
299/// [`DriveDocumentRankedQuery::indexed_property_name_tree_path`], so the
300/// two cannot drift on which subtree the proof is about.
301///
302/// Construction is normally left to
303/// [`crate::drive::Drive::execute_document_ranked_request`] (server) or to
304/// the SDK's proof helpers (client); both go through
305/// [`index_picker::find_ranked_index_for_axis`] to resolve `index`.
306#[derive(Debug, Clone)]
307#[cfg(any(feature = "server", feature = "verify"))]
308pub struct DriveDocumentRankedQuery<'a> {
309    /// The document type being ranked.
310    pub document_type: DocumentTypeRef<'a>,
311    /// The contract id (32 bytes). Separate from `document_type` so the
312    /// verifier can build the query without the full contract.
313    pub contract_id: [u8; 32],
314    /// The document type name — a path segment.
315    pub document_type_name: String,
316    /// The covering ranked index. Its **last** property is the `GROUP
317    /// BY` property and the final path segment; any leading properties
318    /// are pinned by [`Self::prefix_branches`].
319    pub index: &'a Index,
320    /// The prefix **branches** — one inner `Vec<Vec<u8>>` of encoded
321    /// index-key path segments per branch, each in index-property
322    /// order. Always at least one branch; a single-property index or an
323    /// all-`==` pinned request has exactly one (possibly empty) branch,
324    /// and the (at most one) `IN` pin contributes one branch per
325    /// element, in canonical encoded-ascending order. Together with
326    /// `index` these determine the grove path(s), so the branch set is
327    /// as much a part of the prover/verifier agreement as the path
328    /// builder itself. Produced by
329    /// [`index_picker::encode_prefix_branches`] from the request's
330    /// `where` pins — crate-private so the resolver is the only public
331    /// constructor and the encoder's invariants (nonempty, canonical
332    /// order, distinct keys, one varying position, the fan-out ceiling)
333    /// hold on every externally obtainable value.
334    pub(crate) prefix_branches: Vec<Vec<Vec<u8>>>,
335    /// Which aggregate the groups are ranked by, as requested and as the
336    /// entries are presented. The walked secondary is [`Self::read_axis`]'s,
337    /// covered by `index`'s matching `ranked_*` flag.
338    pub axis: RankedAxis,
339    /// `true` walks the secondary from the largest aggregate down
340    /// (`ORDER BY <agg> DESC`); `false` walks from the smallest up
341    /// (`ORDER BY <agg> ASC`).
342    ///
343    /// **Tie ordering.** The secondary's keys are `(sort_key ‖
344    /// group_key)`, and the walk is a plain directional scan of that
345    /// keyspace — so groups with equal aggregates come back in group-key
346    /// order *in the direction of the walk*: ascending group key when
347    /// `descending == false`, and **descending group key when
348    /// `descending == true`**. The reversal is a property of the scan,
349    /// not a separate tie-break rule; it is pinned by the
350    /// `ties_break_by_group_key_in_the_walk_direction` test.
351    pub descending: bool,
352    /// How many groups to return — the request's `LIMIT`.
353    /// `1 ..= MAX_RANKED_LIMIT`, validated in [`mode_detection`]. Fewer
354    /// entries come back when the index has fewer groups than
355    /// `offset + k`; that is not an error.
356    pub k: u16,
357    /// How many ranks to skip before the returned page — the request's
358    /// `OFFSET`. `0` for an unpaginated ranking.
359    ///
360    /// Unbounded above (any `u32`), on purpose. grovedb skips by
361    /// counting rather than walking — descending the secondary on each
362    /// subtree's aggregate count (`HashWithCount` /
363    /// `HashWithCountAndSum`) and collapsing any subtree that fits
364    /// inside the remaining offset — so work and proof size stay
365    /// `O(log n + k)` **at any offset**, and an offset of 4 and an
366    /// offset of four billion cost the same order of work, the deeper
367    /// one in fact slightly less. Both executors go through that
368    /// descent, the unproved one without building a proof, so there is
369    /// no denial-of-service lever to cap on either path and capping
370    /// would only stop honest deep pagination.
371    ///
372    /// An offset past the end of the secondary is a provable answer, not
373    /// an error: the page comes back empty and
374    /// [`RankedPage::skipped`] is the secondary's entire population.
375    pub offset: u32,
376}
377
378/// The axis whose secondary a ranked or having read of `index` walks for a
379/// request on `axis`: the requested one, except that a document count
380/// (`Count`) over a `summableOffCountIndex` index walks the Sum secondary.
381/// Such an index's counters each count one group in its count trees and add
382/// their group's documents to its sums, so its sums are its document counts
383/// (`document_count_of_element`).
384///
385/// Unversioned, so every protocol version reaches it: it departs from the
386/// plain axis only on a `summableOffCountIndex` index, which only
387/// meta-schema v3 (protocol version 14) admits.
388#[cfg(any(feature = "server", feature = "verify"))]
389pub fn read_axis_for(axis: RankedAxis, index: &Index) -> RankedAxis {
390    if axis == RankedAxis::Count && index.is_summable_off_count_index() {
391        RankedAxis::Sum
392    } else {
393        axis
394    }
395}
396
397/// Entries read on [`read_axis_for`]'s axis, presented on the requested
398/// `axis`: document counts read from sums come back as counts. A no-op
399/// whenever the read axis is the requested one.
400#[cfg(any(feature = "server", feature = "verify"))]
401pub fn present_entries_on_axis(axis: RankedAxis, entries: Vec<RankedEntry>) -> Vec<RankedEntry> {
402    if axis != RankedAxis::Count {
403        return entries;
404    }
405    entries
406        .into_iter()
407        .map(|entry| match entry.value {
408            RankedEntryValue::Sum(sum) => RankedEntry {
409                value: RankedEntryValue::Count(counter_sum_as_document_count(sum)),
410                ..entry
411            },
412            _ => entry,
413        })
414        .collect()
415}
416
417#[cfg(any(feature = "server", feature = "verify"))]
418impl DriveDocumentRankedQuery<'_> {
419    /// The axis whose secondary this query walks ([`read_axis_for`]); its
420    /// entries are presented on [`Self::axis`].
421    pub fn read_axis(&self) -> RankedAxis {
422        read_axis_for(self.axis, self.index)
423    }
424
425    /// The resolved prefix branches, in canonical order — one per `IN`
426    /// element (a single branch without an `IN`). Read-only: the field is
427    /// crate-private so the resolver's encoder invariants cannot be
428    /// bypassed by construction or mutation.
429    pub fn prefix_branches(&self) -> &[Vec<Vec<u8>>] {
430        &self.prefix_branches
431    }
432
433    /// Reject the one cross-field combination the request grammar
434    /// forbids but public construction can still express: a
435    /// multi-branch (`IN`) query carrying a non-zero `offset`.
436    /// Rank-skip is attested from ONE secondary's counted commitments;
437    /// applied independently per branch it would page each branch
438    /// separately, merge the independently skipped pages, and report
439    /// `skipped: 0` — and the verifier, reconstructing the same
440    /// malformed per-branch traversal, would not reject it. Enforced at
441    /// every execution, proving and verification boundary, because
442    /// `offset` is a public field and the mode-detection grammar check
443    /// can be bypassed by building a mode or mutating a resolved query
444    /// directly.
445    pub(crate) fn reject_offset_with_branches(&self) -> Result<(), crate::error::Error> {
446        if self.prefix_branches.len() > 1 && self.offset != 0 {
447            return Err(Error::Query(QuerySyntaxError::InvalidLimit(
448                "`OFFSET` cannot combine with an `IN` prefix pin: rank-skip is attested \
449                     from one secondary's counted commitments, and an `IN` merges several \
450                     secondaries with no counted structure over the union. Page one prefix \
451                     at a time (`==` pin + `OFFSET`), or drop the offset."
452                    .to_string(),
453            )));
454        }
455        Ok(())
456    }
457}
458
459/// A page of a ranked result: the entries, plus how many ranks were
460/// actually skipped to reach them.
461///
462/// `skipped` is what turns a page into a *ranking*: entry `i` of
463/// `entries` is the group at rank `skipped + i` (0-based). Without it a
464/// caller that asked for `OFFSET 4 LIMIT 1` would receive one entry and
465/// have to trust the server that it really is the 5th-best group.
466#[derive(Debug, Clone, PartialEq, Eq)]
467#[cfg(any(feature = "server", feature = "verify"))]
468pub struct RankedPage {
469    /// Number of secondary entries skipped before this page.
470    ///
471    /// Both paths report the same quantity, and it is never an echo of
472    /// the request: grovedb's counted descent tracks how far the skip
473    /// actually got, so this equals the requested offset when the skip
474    /// succeeded and the secondary's whole population when the walk ran
475    /// out of groups first (in which case `entries` is empty).
476    ///
477    /// What differs between the paths is the warrant. On the **proved**
478    /// path the value is cryptographically attested — independently
479    /// re-derived by the verifier from the counted subtree commitments
480    /// in the proof bytes — so a verifying client uses its own
481    /// reconstruction rather than trusting the server's. On the
482    /// **unproven** read it is the node's unverified claim, exactly like
483    /// the entries beside it: equal to the attested value on an honest
484    /// node, with nothing forcing a node to be honest.
485    ///
486    /// One nuance worth knowing on the unproven path: the population is
487    /// read from the secondary's root aggregate, while grovedb's
488    /// per-node payload check only fires on nodes the descent visits. In
489    /// a *corrupt* secondary whose count violation lies outside the
490    /// visited region, this value can therefore disagree with the true
491    /// row count where the proved path's would not. On any valid
492    /// secondary the two are identical by construction.
493    pub skipped: u64,
494    /// The groups on this page, **in ranking order**. Never longer than
495    /// the query's `k`.
496    pub entries: Vec<RankedEntry>,
497}
498
499/// The pagination knobs a ranked request carries, bundled so the
500/// versioned validator reads them in one place.
501///
502/// `limit` is required (it is the ranking's `k`), `offset` is optional
503/// and defaults to `0`, and `start_at` is refused outright — a cursor
504/// names a document id, and document ids do not appear in a keyspace
505/// sorted by aggregate. `has_start_at` is a bare `bool` because the
506/// value is never used; only its presence is an error.
507#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
508#[cfg(any(feature = "server", feature = "verify"))]
509pub struct RankedPaginationInputs {
510    /// The request's `limit`. Required in ranked mode.
511    pub limit: Option<u32>,
512    /// The request's `offset`, if it set one. `None` means rank 0.
513    pub offset: Option<u32>,
514    /// Whether the request carried a `start_at` / `start_after` cursor.
515    pub has_start_at: bool,
516}
517
518/// The resolved shape of a ranked request: which axis, which direction,
519/// how many groups, and the `(group property, aggregate field)` pair the
520/// index picker needs.
521///
522/// Produced by [`mode_detection::detect_ranked_mode`] from the caller's
523/// `(select, group_by, order_by, limit, offset)` inputs. Parallels
524/// [`super::drive_document_count_query::DocumentCountMode`] in role —
525/// the versioned classification of a request — but carries data rather
526/// than being a bare discriminant, because the ranked surface has exactly
527/// one executor pair (no-proof / proof) and all of its variation is in
528/// these values.
529///
530/// Not `Eq`: the prefix pins carry [`Value`]s, whose float variant
531/// keeps the type at `PartialEq`.
532#[derive(Debug, Clone, PartialEq)]
533#[cfg(any(feature = "server", feature = "verify"))]
534pub struct DocumentRankedMode {
535    /// The ranking axis, from the `SELECT` function.
536    pub axis: RankedAxis,
537    /// Walk direction: `ORDER BY … DESC` ⇒ `true`, `ASC` ⇒ `false`.
538    pub descending: bool,
539    /// Number of groups requested — the `LIMIT`, `1 ..= MAX_RANKED_LIMIT`.
540    pub k: u16,
541    /// Ranks to skip — the `OFFSET`, `0` when unset.
542    pub offset: u32,
543    /// The single `GROUP BY` property; must be the covering ranked
544    /// index's **last** property.
545    pub group_by_property: String,
546    /// The field the aggregate applies to. Empty for
547    /// [`RankedAxis::Count`] (`COUNT(*)`); the index's `summable`
548    /// property for [`RankedAxis::Sum`] / [`RankedAxis::Avg`].
549    pub aggregate_field: String,
550    /// The `where` prefix pins — one [`PrefixPin`] per clause, exactly
551    /// one per leading property of the covering compound index, in
552    /// whatever order the request supplied them (the resolver re-orders
553    /// them into index-property order when it encodes the path). A pin
554    /// normally carries one value (an `==` clause); at most one carries
555    /// several (the single permitted branching `IN`). Empty for the
556    /// single-property form. Shape-validated only: the index-aware
557    /// checks (does a compound index exist whose leading properties
558    /// these pin?) live in [`index_picker`].
559    pub prefix_pins: Vec<PrefixPin>,
560}
561
562/// One pinned leading property of the covering compound index.
563///
564/// An `==` clause pins exactly one value; the (at most one) `IN` clause
565/// pins several, each element selecting its own prefix **branch** — the
566/// executors walk one secondary per branch and merge deterministically.
567/// A single-element `IN` is normalized to an equality pin at grammar
568/// time, so `values.len() > 1` is exactly "this is the branching pin".
569#[derive(Debug, Clone, PartialEq)]
570pub struct PrefixPin {
571    /// The pinned property's name.
572    pub field: String,
573    /// The pinned value(s); never empty.
574    pub values: Vec<Value>,
575}