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}