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