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}