Skip to main content

drive/query/
having.rs

1//! `HAVING` clause types for the v1 `getDocuments` aggregate surface.
2//!
3//! `HAVING` is a **boolean predicate evaluated per group**, exactly as
4//! in SQL. It differs from `WHERE` in one structural way the type
5//! system needs to reflect: the left operand is a per-group aggregate
6//! (`COUNT(*)`, `SUM(field)`, `AVG(field)`) rather than a raw row
7//! field. The right operand is always a concrete value (`> 5`,
8//! `BETWEEN 5 AND 10`, `IN (5, 10, 15)`).
9//!
10//! **Ranking does not live here.** "Which groups score highest on the
11//! selected aggregate" is spelled with SQL's own ordering surface —
12//! `ORDER BY <selected aggregate> [ASC|DESC] LIMIT n OFFSET m` — and is
13//! resolved by
14//! [`crate::query::drive_document_ranked_query::mode_detection`]. An
15//! earlier iteration of this module carried `TOP(n)` / `BOTTOM(n)` /
16//! `MIN` / `MAX` right-operands; they were removed because they
17//! duplicated `ORDER BY … LIMIT` with a second, non-SQL grammar that
18//! could not express an offset.
19//!
20//! The operator set matches [`crate::query::WhereOperator`] minus
21//! `STARTS_WITH` (prefix matching has no meaning on a scalar
22//! aggregate result, even one that's a string): scalar comparison,
23//! `IN`, and all four `BETWEEN*` variants all carry through.
24//!
25//! Multi-clause HAVING (`HAVING COUNT(*) > 5 AND SUM(amount) > 100`)
26//! is expressed by repeating [`HavingClause`] at the request
27//! level — implicit AND, same shape as multiple `where_clauses`
28//! entries.
29//!
30//! These types are shared between the wire-decoding layer
31//! (`rs-drive-abci/src/query/document_query/v1/conversions.rs`)
32//! and the SDK's request builder
33//! (`rs-sdk/src/platform/documents/document_query.rs`) so the
34//! drive-side struct is the single source of truth for the shape.
35//!
36//! **What executes (protocol version 14+)**: a grouped aggregate
37//! carrying exactly one clause that bounds the aggregate the select
38//! projects, with a contiguous-range operator (`=`, `>`, `>=`, `<`,
39//! `<=`, the four `BETWEEN*` variants). It is served as a
40//! value-bounded range read of the covering ranked index's axis
41//! secondary — see `drive_document_having_query::mode_detection` for
42//! the versioned grammar. Everything else the types can express
43//! remains rejected with `QuerySyntaxError`: multiple clauses
44//! (implicit AND would need a per-candidate post-check no executor
45//! performs), a clause on a different aggregate than the select's,
46//! the non-contiguous operators (`!=`, `IN`), and `having` without
47//! `group_by`. Protocol version 13 and earlier reject every
48//! non-empty `having`, so mixed-version networks agree across the
49//! upgrade.
50
51use dpp::platform_value::Value;
52#[cfg(feature = "serde")]
53use serde::{Deserialize, Serialize};
54
55/// Aggregate function applied to a group on the left side of a
56/// [`HavingClause`]. These are the per-group aggregates whose
57/// result is the scalar / numeric value the right-side operand
58/// compares against.
59#[derive(Copy, Clone, Debug, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
61pub enum HavingAggregateFunction {
62    /// `COUNT(*)` when [`HavingAggregate::field`] is empty,
63    /// otherwise `COUNT(field)`.
64    Count,
65    /// `SUM(field)`. Numeric field required.
66    Sum,
67    /// `AVG(field)`. Numeric field required; result is `f64`.
68    Avg,
69}
70
71/// Aggregate operand for the left side of a [`HavingClause`]. See
72/// [`HavingAggregateFunction`] for the per-function `field`
73/// requirements (empty only for `COUNT(*)`).
74#[derive(Clone, Debug, PartialEq, Eq)]
75#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
76pub struct HavingAggregate {
77    /// The aggregate function applied to the group.
78    pub function: HavingAggregateFunction,
79    /// The field the aggregate is applied to. Empty only when
80    /// `function == Count` (to express `COUNT(*)`).
81    pub field: String,
82}
83
84/// Right-side operand of a [`HavingClause`]: a concrete value
85/// (literal scalar, or list-shaped operand for `BETWEEN*` / `IN`).
86///
87/// Kept as a single-variant enum rather than collapsed into a bare
88/// `Value` field on [`HavingClause`] because the wire models the
89/// right operand as a `oneof`: an enum is what a `oneof` decodes
90/// into, and a future right-operand kind (a correlated subquery, a
91/// reference to another aggregate) lands as a variant here instead
92/// of reshaping every consumer's field access.
93#[derive(Clone, Debug, PartialEq)]
94#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
95pub enum HavingRightOperand {
96    /// Concrete value: scalar for `=` / `!=` / `<` / `<=` / `>` /
97    /// `>=`; 2-element list `[lower, upper]` for `Between*`;
98    /// list of candidates for `In`.
99    Value(Value),
100}
101
102/// Comparison operator for a [`HavingClause`]. Mirrors
103/// [`crate::query::WhereOperator`] minus `STARTS_WITH` (prefix
104/// matching has no natural meaning against a scalar aggregate
105/// result, even a string-typed one). `BETWEEN*` operand semantics
106/// match `WhereOperator`: a 2-element list `[lower, upper]`; `IN`
107/// expects a list of candidate values.
108#[derive(Copy, Clone, Debug, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
110pub enum HavingOperator {
111    /// `aggregate = value`.
112    Equal,
113    /// `aggregate != value`.
114    NotEqual,
115    /// `aggregate > value`.
116    GreaterThan,
117    /// `aggregate >= value`.
118    GreaterThanOrEquals,
119    /// `aggregate < value`.
120    LessThan,
121    /// `aggregate <= value`.
122    LessThanOrEquals,
123    /// `aggregate BETWEEN lower AND upper` (inclusive on both
124    /// ends). `value` must be a 2-element list `[lower, upper]`.
125    Between,
126    /// `aggregate > lower AND aggregate < upper` (exclusive on
127    /// both ends). `value` shape same as `Between`.
128    BetweenExcludeBounds,
129    /// `aggregate > lower AND aggregate <= upper` (exclusive on
130    /// the left bound only). `value` shape same as `Between`.
131    BetweenExcludeLeft,
132    /// `aggregate >= lower AND aggregate < upper` (exclusive on
133    /// the right bound only). `value` shape same as `Between`.
134    BetweenExcludeRight,
135    /// `aggregate IN (v1, v2, …)`. `value` must be a list of
136    /// candidate values matching the aggregate's result type.
137    In,
138}
139
140/// Single `HAVING <aggregate> <op> <right>` clause.
141///
142/// Multiple [`HavingClause`] entries in the request-level
143/// `repeated HavingClause having` field are combined with implicit
144/// `AND` — same semantics as multiple `where_clauses` entries.
145/// `HAVING COUNT(*) > 5 AND SUM(amount) > 100` is two clauses, not
146/// a tree; the wire has no dedicated `AND` node because the
147/// repeated field already expresses it. Future `OR` capability
148/// would land as an additional wire shape (e.g. a `HavingGroup`
149/// message with a logical-op tag) rather than overloading this
150/// type.
151#[derive(Clone, Debug, PartialEq)]
152#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
153pub struct HavingClause {
154    /// Left-side per-group aggregate operand.
155    pub aggregate: HavingAggregate,
156    /// Comparison operator.
157    pub operator: HavingOperator,
158    /// Right-side operand. See [`HavingRightOperand`] for the
159    /// shape contract.
160    pub right: HavingRightOperand,
161}