Skip to main content

drive/query/drive_document_having_query/
mod.rs

1//! Types and module structure for the **boolean-`HAVING` range** document
2//! query — `SELECT <agg> GROUP BY <prop> HAVING <agg> <op> <value>
3//! [ORDER BY <agg> ASC|DESC] LIMIT n`.
4//!
5//! A having-range query answers "which groups' aggregate falls inside a
6//! value bound?" ("hashtags with more than 100 posts") in `O(log n + k)`
7//! with a proof, by range-reading the same per-axis *secondary* Merk the
8//! ranked query walks (grovedb PR #657): the secondary is keyed by
9//! `(sort_key ‖ group_key)` with an order-preserving sort-key encoding,
10//! so an inclusive numeric bound on the aggregate is a contiguous byte
11//! range in the secondary's keyspace. The same contract opt-in applies —
12//! `rankedCountable` / `rankedSummable` / `rankedAverageable` (meta schema
13//! v3 / PV14) — and a `HAVING` on an axis the index does not declare is
14//! rejected, because serving it would mean walking every group.
15//!
16//! The implementation mirrors [`super::drive_document_ranked_query`]
17//! sibling-for-sibling and reuses its axis / entry / pagination types
18//! ([`RankedAxis`], [`RankedEntry`], [`RankedEntryValue`],
19//! [`super::RankedPaginationInputs`]) and its covering-index picker —
20//! both surfaces read the same tree, so sharing the resolution logic is
21//! what keeps them provably about the same subtree:
22//! - [`mode_detection`] — request-shape validation + the versioned
23//!   `(select, group_by, having, order_by, limit)` →
24//!   [`DocumentHavingMode`] resolution, including the operator →
25//!   inclusive-bounds translation.
26//! - [`execute_range`] — the two executors on
27//!   [`DriveDocumentHavingQuery`] (no-proof read, proof generation).
28//! - [`executors`] — the `impl Drive` wrappers the dispatcher calls.
29//! - [`drive_dispatcher`] — [`DocumentHavingRequest`] /
30//!   [`DocumentHavingResponse`] and
31//!   [`crate::drive::Drive::execute_document_having_request`].
32//! - [`tests`] (cfg `server` + `test`) — unit + integration tests.
33//!
34//! ## What makes this query shape different from ranked
35//!
36//! Ranked addresses groups by **rank position** (`k` best, starting at
37//! rank `offset`); having-range addresses them by **value bound**
38//! (`aggregate ∈ [lo, hi]`). Three consequences:
39//!
40//! 1. **The bound is part of the proof contract.** Prover and verifier
41//!    build the same `Bounded` axis `PathQuery` from the request's
42//!    inclusive bounds ([`AxisRangeBounds::inclusive_bounds_i128`]),
43//!    and grovedb re-executes the proof against that traversal — so the
44//!    two sides share one bounds-to-query translation, exactly as they
45//!    share the grove path. Completeness comes from the Merk range
46//!    proof: the boundary commitments show no in-range group was
47//!    omitted.
48//! 2. **No `OFFSET`, no `start_at` — and no full pagination.** The
49//!    range primitives take a limit but no skip, and a request carrying
50//!    either knob is rejected loudly. A page cut at `limit` can only be
51//!    continued past **distinct** aggregate values, by tightening the
52//!    bound past the last value seen; a cut that lands **inside a tie**
53//!    (several groups sharing the boundary aggregate) cannot be
54//!    continued at all — moving the threshold past the tied value skips
55//!    the uncollected tied groups, and keeping it returns the same
56//!    page. Enumerating through a tie wider than [`MAX_HAVING_LIMIT`]
57//!    needs a cursor on the `(sort_key ‖ group_key)` composite
58//!    keyspace, a future capability; until then, size `limit` above the
59//!    widest tie the data can produce, or accept the cut.
60//! 3. **Entry order is axis order in the walk direction.** Ascending by
61//!    default (`ORDER BY` is optional here — the bound, not the
62//!    ordering, is the point of the query); an explicit `ORDER BY` on
63//!    the selected aggregate flips the walk. Ties break by group key in
64//!    the direction of the walk, same as ranked.
65
66#[cfg(any(feature = "server", feature = "verify"))]
67use dpp::data_contract::document_type::{DocumentTypeRef, Index};
68#[cfg(any(feature = "server", feature = "verify"))]
69use dpp::version::PlatformVersion;
70#[cfg(any(feature = "server", feature = "verify"))]
71use std::collections::BTreeMap;
72
73#[cfg(any(feature = "server", feature = "verify"))]
74use super::drive_document_ranked_query::index_picker::{
75    encode_prefix_branches, find_ranked_index_for_axis, no_covering_index_message,
76};
77#[cfg(any(feature = "server", feature = "verify"))]
78use super::drive_document_ranked_query::{
79    path::indexed_property_name_tree_path_for_index, PrefixPin, RankedAxis,
80};
81#[cfg(any(feature = "server", feature = "verify"))]
82use crate::error::drive::DriveError;
83#[cfg(any(feature = "server", feature = "verify"))]
84use crate::error::query::QuerySyntaxError;
85#[cfg(any(feature = "server", feature = "verify"))]
86use crate::error::Error;
87#[cfg(any(feature = "server", feature = "verify"))]
88use grovedb::element::indexed::{encode_avg_sort_key, encode_count_sort_key, encode_sum_sort_key};
89#[cfg(any(feature = "server", feature = "verify"))]
90use grovedb::Query;
91
92#[cfg(any(feature = "server", feature = "verify"))]
93pub mod mode_detection;
94
95// Server-side execution paths.
96#[cfg(feature = "server")]
97pub mod drive_dispatcher;
98#[cfg(feature = "server")]
99pub mod execute_range;
100#[cfg(feature = "server")]
101pub mod executors;
102
103#[cfg(feature = "server")]
104pub use drive_dispatcher::{DocumentHavingRequest, DocumentHavingResponse};
105
106#[cfg(all(feature = "server", test))]
107mod tests;
108
109/// Hard ceiling on a having-range request's `LIMIT`. Same value and same
110/// rationale as [`super::drive_document_ranked_query::MAX_RANKED_LIMIT`]:
111/// the proof commits one secondary entry per returned group, so proof
112/// bytes grow linearly in the limit, and the ceiling is a hard rejection
113/// rather than a clamp because the limit is part of the traversal the
114/// verifier re-executes: a server-side clamp would truncate the walk
115/// and fail coverage under the client's own reconstruction.
116#[cfg(any(feature = "server", feature = "verify"))]
117pub const MAX_HAVING_LIMIT: u16 = 100;
118
119/// Inclusive numeric bounds on one axis of an indexed tree — the resolved
120/// form of a `HAVING <aggregate> <operator> <value>` clause.
121///
122/// One variant per axis because the three axes have three value types
123/// (`u64` count, `i64` sum, `i128` fixed-point average) and the bound
124/// arithmetic (operator translation, successor/predecessor at exclusive
125/// bounds) must be exact in the axis's own domain. Both bounds are
126/// **inclusive**; the operator translation in
127/// [`mode_detection`] normalizes every supported operator to this form,
128/// rejecting translations that would overflow (`> MAX`) or invert
129/// (`lo > hi`) instead of serving a silently-empty range.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131#[cfg(any(feature = "server", feature = "verify"))]
132pub enum AxisRangeBounds {
133    /// `COUNT(*) ∈ [lo, hi]`.
134    Count {
135        /// Inclusive lower bound.
136        lo: u64,
137        /// Inclusive upper bound.
138        hi: u64,
139    },
140    /// `SUM(field) ∈ [lo, hi]`.
141    Sum {
142        /// Inclusive lower bound.
143        lo: i64,
144        /// Inclusive upper bound.
145        hi: i64,
146    },
147    /// `AVG(field) ∈ [lo, hi]`, in the fixed-point domain described on
148    /// [`super::drive_document_ranked_query::RANKED_AVG_SCALE`].
149    Avg {
150        /// Inclusive lower bound (fixed point).
151        lo: i128,
152        /// Inclusive upper bound (fixed point).
153        hi: i128,
154    },
155}
156
157#[cfg(any(feature = "server", feature = "verify"))]
158impl AxisRangeBounds {
159    /// The axis these bounds constrain.
160    pub fn axis(&self) -> RankedAxis {
161        match self {
162            AxisRangeBounds::Count { .. } => RankedAxis::Count,
163            AxisRangeBounds::Sum { .. } => RankedAxis::Sum,
164            AxisRangeBounds::Avg { .. } => RankedAxis::Avg,
165        }
166    }
167
168    /// The bounds as inclusive `i128` values in the axis's own domain —
169    /// the form `AxisTraversal::Bounded` carries in the unified
170    /// `PathQuery`. Count and sum widen losslessly; avg is already
171    /// `i128` fixed point.
172    pub fn inclusive_bounds_i128(&self) -> (i128, i128) {
173        match *self {
174            AxisRangeBounds::Count { lo, hi } => (lo as i128, hi as i128),
175            AxisRangeBounds::Sum { lo, hi } => (lo as i128, hi as i128),
176            AxisRangeBounds::Avg { lo, hi } => (lo, hi),
177        }
178    }
179
180    /// The bounds as a byte range over the axis secondary's keyspace:
181    /// `(inclusive_lower, exclusive_upper)`, with `None` for an upper
182    /// bound at the axis's type maximum (no representable successor —
183    /// the range is unbounded above).
184    ///
185    /// Secondary keys are `(sort_key ‖ group_key)` with order-preserving
186    /// fixed-width sort keys, so the inclusive numeric range `[lo, hi]`
187    /// is exactly the byte range `[encode(lo), encode(hi + 1))`: the
188    /// exclusive upper at the *next* sort key admits every group-key
189    /// suffix under `hi` and nothing above it. This mirrors — and must
190    /// stay identical to — the bound construction inside grovedb's
191    /// `indexed_*_range` read primitives, so the no-proof read and the
192    /// proved read answer the same question.
193    ///
194    /// The `+ 1` cannot overflow: the `hi == MAX` case returns `None`
195    /// first.
196    pub fn secondary_key_bounds(&self) -> (Vec<u8>, Option<Vec<u8>>) {
197        match *self {
198            AxisRangeBounds::Count { lo, hi } => (
199                encode_count_sort_key(lo).to_vec(),
200                (hi != u64::MAX).then(|| encode_count_sort_key(hi + 1).to_vec()),
201            ),
202            AxisRangeBounds::Sum { lo, hi } => (
203                encode_sum_sort_key(lo).to_vec(),
204                (hi != i64::MAX).then(|| encode_sum_sort_key(hi + 1).to_vec()),
205            ),
206            AxisRangeBounds::Avg { lo, hi } => (
207                encode_avg_sort_key(lo).to_vec(),
208                (hi != i128::MAX).then(|| encode_avg_sort_key(hi + 1).to_vec()),
209            ),
210        }
211    }
212
213    /// The Merk query over the axis secondary that reads exactly these
214    /// bounds, walking in the requested direction.
215    ///
216    /// This is the **prover/verifier-agreement artifact** of the having
217    /// surface: grovedb's range-proof envelope is generated against this
218    /// query and verified against the verifier's own reconstruction of
219    /// it, so both sides must build it from the same bounds through this
220    /// one function — a divergence surfaces as a failed verification,
221    /// not a wrong answer.
222    pub fn merk_query(&self, descending: bool) -> Query {
223        let (lower, upper) = self.secondary_key_bounds();
224        let mut query = Query::new_with_direction(!descending);
225        match upper {
226            Some(upper) => query.insert_range(lower..upper),
227            None => query.insert_range_from(lower..),
228        }
229        query
230    }
231}
232
233/// The resolved shape of a having-range request: the bounds (which carry
234/// the axis), the walk direction, the limit, and the `(group property,
235/// aggregate field)` pair the index picker needs.
236///
237/// Produced by [`mode_detection::detect_having_mode`]. Parallels
238/// [`super::drive_document_ranked_query::DocumentRankedMode`].
239///
240/// Not `Eq`: the prefix pins carry [`Value`]s, whose float variant
241/// keeps the type at `PartialEq`.
242#[derive(Debug, Clone, PartialEq)]
243#[cfg(any(feature = "server", feature = "verify"))]
244pub struct DocumentHavingMode {
245    /// Inclusive bounds on the aggregate, in the axis's own domain.
246    pub bounds: AxisRangeBounds,
247    /// Walk direction: `true` reads matching groups from the largest
248    /// aggregate down. Defaults to `false` (ascending) when the request
249    /// carries no `ORDER BY`.
250    pub descending: bool,
251    /// Maximum number of matching groups to return —
252    /// `1 ..= MAX_HAVING_LIMIT`, required.
253    pub limit: u16,
254    /// The single `GROUP BY` property; must be the covering ranked
255    /// index's **last** property.
256    pub group_by_property: String,
257    /// The field the aggregate applies to. Empty for `COUNT(*)`; the
258    /// index's `summable` property for `SUM` / `AVG`.
259    pub aggregate_field: String,
260    /// The `where` prefix pins — one [`PrefixPin`] per clause, exactly
261    /// one per leading property of the covering compound index, in
262    /// request order (the resolver re-orders them into index order when
263    /// it encodes the path). A pin normally carries one value (an `==`
264    /// clause); at most one pin carries several (the single permitted
265    /// branching `IN`, whose elements fan the bound out across one
266    /// prefix branch each). Empty for the single-property form.
267    pub prefix_pins: Vec<PrefixPin>,
268}
269
270/// A resolved having-range query. Shared by the prover and the verifier —
271/// both build the grove path through
272/// [`DriveDocumentHavingQuery::indexed_property_name_tree_path`] and the
273/// secondary query through [`AxisRangeBounds::merk_query`], so the two
274/// cannot drift on which subtree or which range the proof is about.
275#[derive(Debug, Clone)]
276#[cfg(any(feature = "server", feature = "verify"))]
277pub struct DriveDocumentHavingQuery<'a> {
278    /// The document type being filtered.
279    pub document_type: DocumentTypeRef<'a>,
280    /// The contract id (32 bytes). Separate from `document_type` so the
281    /// verifier can build the query without the full contract.
282    pub contract_id: [u8; 32],
283    /// The document type name — a path segment.
284    pub document_type_name: String,
285    /// The covering ranked index. Its **last** property is the `GROUP
286    /// BY` property and the final path segment; any leading properties
287    /// are pinned by [`Self::prefix_branches`].
288    pub index: &'a Index,
289    /// The prefix **branches** — one inner `Vec<Vec<u8>>` of encoded
290    /// path segments per branch, in index-property order; always at
291    /// least one branch, several exactly when the request carried a
292    /// multi-element `IN` pin. Part of the prover/verifier agreement
293    /// exactly as on the ranked surface. Produced by
294    /// [`super::drive_document_ranked_query::index_picker::encode_prefix_branches`]
295    /// — crate-private so the resolver is the only public constructor
296    /// and the encoder's invariants hold on every externally obtainable
297    /// value.
298    pub(crate) prefix_branches: Vec<Vec<Vec<u8>>>,
299    /// Inclusive bounds on the aggregate. Carry the axis; the index must
300    /// declare the matching `ranked_*` flag.
301    pub bounds: AxisRangeBounds,
302    /// `true` walks the secondary from the largest matching aggregate
303    /// down. Tie ordering is by group key in the direction of the walk,
304    /// exactly as on the ranked surface.
305    pub descending: bool,
306    /// Maximum number of matching groups to return. Fewer entries come
307    /// back when fewer groups fall inside the bounds; that is not an
308    /// error. **More matching groups than `limit` are silently cut at
309    /// `limit`** — the walk stops, and nothing marks the cut. A caller
310    /// can continue past *distinct* aggregate values by tightening the
311    /// bound, but a cut inside a **tie** cannot be continued (see the
312    /// module docs): groups tied at the boundary aggregate that fell
313    /// past the limit stay unreachable until a composite-key cursor
314    /// exists, so size the limit above the widest expected tie.
315    pub limit: u16,
316}
317
318#[cfg(any(feature = "server", feature = "verify"))]
319impl DriveDocumentHavingQuery<'_> {
320    /// The resolved prefix branches, in canonical order — one per `IN`
321    /// element (a single branch without an `IN`). Read-only: the field is
322    /// crate-private so the resolver's encoder invariants cannot be
323    /// bypassed by construction or mutation.
324    pub fn prefix_branches(&self) -> &[Vec<Vec<u8>>] {
325        &self.prefix_branches
326    }
327}
328
329#[cfg(any(feature = "server", feature = "verify"))]
330impl DriveDocumentHavingQuery<'_> {
331    /// Path of one branch's terminal property-name tree — identical to
332    /// the ranked surface's path (including the pinned-prefix segments
333    /// of a compound index), because both read the same indexed
334    /// tree(s). See
335    /// [`DriveDocumentRankedQuery::indexed_property_name_tree_path`](super::drive_document_ranked_query::DriveDocumentRankedQuery::indexed_property_name_tree_path).
336    pub fn indexed_property_name_tree_path(&self, branch: usize) -> Result<Vec<Vec<u8>>, Error> {
337        let prefix_values =
338            self.prefix_branches
339                .get(branch)
340                .ok_or(Error::Drive(DriveError::NotSupported(
341                    "ranked and having-range queries addressed a prefix branch outside the \
342                 query's resolved branch set",
343                )))?;
344        indexed_property_name_tree_path_for_index(
345            &self.contract_id,
346            &self.document_type_name,
347            self.index,
348            prefix_values,
349        )
350    }
351}
352
353/// Resolve a validated [`DocumentHavingMode`] against a document type's
354/// indexes into the executable [`DriveDocumentHavingQuery`]: pick the
355/// covering index (shared with the ranked surface — both read the same
356/// indexed tree), encode the prefix pins into prefix **branches** (one
357/// branch for all-`==` pins, one branch per element of the single
358/// permitted `IN`), and assemble the query.
359///
360/// The **one** resolution path for the having surface, mirroring
361/// [`super::drive_document_ranked_query::index_picker::resolve_ranked_query_for_mode`]:
362/// the server's executors and the SDK's proof helpers both call it, so a
363/// proof and an unproven read (and the client's verification) are about
364/// the same subtree by construction.
365///
366/// `indexes` is threaded in separately for the same lifetime reason as
367/// the ranked resolver: the returned query's `&'a Index` must outlive
368/// this frame. Callers pass `document_type.indexes()`.
369#[cfg(any(feature = "server", feature = "verify"))]
370pub fn resolve_having_query_for_mode<'a>(
371    contract_id: [u8; 32],
372    document_type: DocumentTypeRef<'a>,
373    document_type_name: String,
374    indexes: &'a BTreeMap<String, Index>,
375    mode: &DocumentHavingMode,
376    platform_version: &PlatformVersion,
377) -> Result<DriveDocumentHavingQuery<'a>, Error> {
378    let axis = mode.bounds.axis();
379    let pin_fields: Vec<String> = mode
380        .prefix_pins
381        .iter()
382        .map(|pin| pin.field.clone())
383        .collect();
384    let index = find_ranked_index_for_axis(
385        indexes,
386        &mode.group_by_property,
387        &pin_fields,
388        axis,
389        &mode.aggregate_field,
390    )
391    .ok_or_else(|| {
392        Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(
393            no_covering_index_message(
394                "having-range",
395                axis,
396                &mode.group_by_property,
397                &mode.prefix_pins,
398                &mode.aggregate_field,
399            ),
400        ))
401    })?;
402    let prefix_branches =
403        encode_prefix_branches(document_type, index, &mode.prefix_pins, platform_version)?;
404    Ok(DriveDocumentHavingQuery {
405        document_type,
406        contract_id,
407        document_type_name,
408        index,
409        prefix_branches,
410        bounds: mode.bounds,
411        descending: mode.descending,
412        limit: mode.limit,
413    })
414}