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}