Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 37 additions & 19 deletions packages/dapi-grpc/protos/platform/v0/platform.proto
Original file line number Diff line number Diff line change
Expand Up @@ -703,12 +703,21 @@ message GetDocumentsRequest {
// before release rather than deprecated, because it invented
// non-SQL grammar for something SQL already expresses.
//
// **`HAVING` cannot yet combine with an aggregate `ORDER BY`.**
// The ranked executor reads a pre-sorted per-axis secondary and
// has no way to drop groups from the middle of that walk, so a
// request carrying both a non-empty `having` and a ranking
// `order_by` is rejected with `Unsupported` rather than served
// with one of the two silently ignored.
// **From protocol v14 a single `HAVING` clause is served as a
// bounded range read** (having-range mode): `SELECT <agg> GROUP BY
// p HAVING <agg> <op> <value> [ORDER BY <order-key> ASC|DESC]
// LIMIT n` answers from the same per-axis secondary as ranked
// mode, on an index declaring the matching ranked axis. The
// clause's aggregate must be the selected aggregate, the operator
// must describe one contiguous range (`NOT_EQUAL` / `IN` are
// rejected), and the optional `ORDER BY` picks the walk direction
// using the same order-key spelling as ranked mode: `f` for
// `SUM(f)` / `AVG(f)`, the `$count` sentinel for `COUNT(*)` —
// never an explicit `OrderClause.aggregate` target, which is
// rejected. See the supported-shape table on
// `GetDocumentsRequestV1`. On protocol v13 and earlier every
// non-empty `having` stays rejected with `Unsupported`, exactly as
// before.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
//
// The operator set mirrors `WhereOperator` minus `STARTS_WITH`
// (prefix matching has no natural meaning against a scalar
Expand Down Expand Up @@ -849,12 +858,16 @@ message GetDocumentsRequest {
// It returns `ResultData.ranked`. See `order_by` and the
// supported-shape table below.
//
// `having` is a boolean per-group predicate and is **still**
// `Unsupported` at every protocol version, ranked mode or not
// (`"HAVING clause is not yet implemented"`). It carries no ranking
// spelling: an earlier draft put cross-group ranking on the right of
// a `HAVING` (`HAVING AVG(grade) IN TOP(5)`) and that grammar was
// removed before release in favour of `ORDER BY` + `LIMIT`.
// **Having-range mode** is served from protocol v14: a single
// `having` clause whose aggregate is the selected aggregate turns
// the request into a bounded range read over the same per-axis
// secondary ranked mode walks, answered in `ResultData.ranked`.
// On protocol v13 and earlier every non-empty `having` is rejected
// (`"HAVING clause is not yet implemented"`). `having` carries no
// ranking spelling: an earlier draft put cross-group ranking on the
// right of a `HAVING` (`HAVING AVG(grade) IN TOP(5)`) and that
// grammar was removed before release in favour of `ORDER BY` +
// `LIMIT`. See the supported-shape table below.
//
// **Supported shapes** (everything else rejects with a typed
// `QuerySyntaxError::Unsupported` so callers can detect un-wired
Expand Down Expand Up @@ -883,8 +896,12 @@ message GetDocumentsRequest {
// - exactly one `group_by` property, exactly one `order_by` clause naming the select's aggregate (`f` for `SUM(f)` / `AVG(f)`, the `$count` sentinel for `COUNT(*)`), a `limit` in `1 ..= 100`, an optional `offset`, and no `where` / `having` / `start_at`, on an index declaring the matching `rankedCountable` / `rankedSummable` / `rankedAverageable` axis → ranked executor, answered in `ResultData.ranked`.
// - `DESC` is the "top n" reading (walk the axis from the largest aggregate down), `ASC` the "bottom n" reading. Worked example: `SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT 1 OFFSET 4` is the 5th-best restaurant.
//
// `select=<COUNT(*)|SUM(f)|AVG(f)>, group_by=[p], having=[<the selected aggregate> <op> <value>]` (protocol v14+) — **having-range mode**:
// - exactly one `group_by` property, exactly one `having` clause whose aggregate is the select's aggregate, an operator describing one contiguous range (`EQUAL`, `GREATER_THAN[_OR_EQUALS]`, `LESS_THAN[_OR_EQUALS]`, `BETWEEN*`; `NOT_EQUAL` / `IN` rejected), a `limit` in `1 ..= 100`, an optional `order_by` naming the same aggregate (walk direction; ascending by default), and no `where` / `offset` / `start_at` / `start_after`, on an index declaring the matching ranked axis → having-range executor, answered in `ResultData.ranked`.
// - no offset or cursor pagination: a page cut at `limit` continues only by tightening the bound past the last *distinct* aggregate value seen; a cut inside a tie (several groups sharing the boundary aggregate) cannot be continued, so size `limit` above the widest expected tie.
//
// **Rejected shapes** (return `Unsupported`):
// - any non-empty `having`, at every protocol version.
// - any non-empty `having` on protocol v13 and earlier; at v14+, any `having` shape outside having-range mode above (multiple clauses, an aggregate other than the select's, `NOT_EQUAL` / `IN`, or a carried `where` / `offset` / cursor).
// - at v14+: a ranked-shaped request carrying a `where` clause, a `start_at` / `start_after` cursor, more than one `order_by`, or an `order_by` naming anything but the selected aggregate.
// - `select=DOCUMENTS` with non-empty `group_by`.
// - `select=COUNT` with `group_by` on a field that is not constrained by an `In` or range where clause.
Expand Down Expand Up @@ -1084,12 +1101,13 @@ message GetDocumentsRequest {
// `HavingClause` / `HavingAggregate` for the operator and
// aggregate-function catalogs.
//
// **Every non-empty `having` is rejected**, at every protocol
// version, with `Unsupported("HAVING clause is not yet
// implemented")`. The wire shape ships ahead of evaluation so
// callers can construct full `HAVING COUNT(*) > 5 AND
// SUM(amount) > 100` requests in their builders, and so the
// capability can land without another version bump.
// **From protocol v14 a single clause is served** as a bounded
// range read — having-range mode; see the message-level
// supported-shape table. On v13 and earlier every non-empty
// `having` is rejected with `Unsupported("HAVING clause is not
// yet implemented")`. Multi-clause `HAVING COUNT(*) > 5 AND
// SUM(amount) > 100` requests can still be constructed on the
// wire, but stay rejected until a multi-clause evaluator lands.
//
// **`having` does not express ranking.** "The n highest-scoring
// groups" is `ORDER BY <the selected aggregate> DESC LIMIT n`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,32 @@
//! against the v1 query surface is executed: as one of the four
//! `(group_by × where)` grouped modes, or — from feature version 1 —
//! as a *ranked* request when the request orders by the aggregate it
//! selects (`GROUP BY p ORDER BY <the selected aggregate>`). Routing
//! here only asks whether the `order_by` names that aggregate; the
//! direction, the limit's bounds and the offset are drive's call and
//! are made in `detect_ranked_mode`. It also enforces the per-mode
//! `accepts_limit()` contract on the grouped path.
//! selects (`GROUP BY p ORDER BY <the selected aggregate>`), or — from
//! feature version 2 — as a *having-range* request when a grouped
//! aggregate carries exactly one `having` clause. Routing here only
//! asks which shape the request has; the direction, the bounds, the
//! limit's contract and the offset are drive's call and are made in
//! `detect_ranked_mode` / `detect_having_mode`. It also enforces the
//! per-mode `accepts_limit()` contract on the grouped path.
//!
//! The routing rules it embeds are part of the query contract clients
//! see on the wire — a change to which `(group_by × where_clauses ×
//! order_by)` shapes are accepted becomes consensus-visible because the
//! dispatcher runs on every v1 query request. Versioning it lets later
//! protocol bumps adjust the routing table without breaking older
//! nodes' replay of historical traffic, and is what keeps a
//! mixed-version network in agreement across the ranked-query
//! activation: protocol version 13 and earlier select v0, which has no
//! ranked path at all, while protocol version 14 selects v1 and answers
//! ranked queries.
//! mixed-version network in agreement across the ranked-query and
//! having-range activations: protocol version 13 and earlier select v0,
//! which has neither path, while protocol version 14 selects v2 and
//! answers both.
//!
//! Lives next to the v1 query handler (the only call site today) and
//! is dispatched via the `DriveAbciDocumentQueryHelperVersions` slot
//! in `PlatformVersion`.

mod v0;
mod v1;
mod v2;

use crate::error::query::QueryError;
use dpp::version::PlatformVersion;
Expand Down Expand Up @@ -56,6 +59,15 @@ pub(super) enum AggregateRouting {
Grouped(CountMode),
/// Ranked aggregate: execute through the ranked (top-k) surface.
Ranked,
/// Boolean-`HAVING` range: a grouped aggregate carrying exactly one
/// `having` clause, executed through
/// `Drive::execute_document_having_request` as a value-bounded range
/// read of the covering ranked index's axis secondary. The
/// feature-version-2 addition. Carries no data for the same reason
/// `Ranked` carries none: drive owns the having grammar
/// (`detect_having_mode`), and routing only decides *where* the
/// request goes.
HavingRange,
}

/// Decide how a `SELECT COUNT` / `SUM` / `AVG` request executes.
Expand Down Expand Up @@ -113,10 +125,19 @@ pub(super) fn compute_aggregate_mode_and_check_limit(
having,
function_name,
),
2 => v2::compute_aggregate_mode_and_check_limit_v2(
select,
group_by,
where_clauses,
order_by,
limit,
having,
function_name,
),
version => Err(QueryError::Drive(drive::error::Error::Drive(
drive::error::drive::DriveError::UnknownVersionMismatch {
method: "compute_aggregate_mode_and_check_limit".to_string(),
known_versions: vec![0, 1],
known_versions: vec![0, 1, 2],
received: version,
},
))),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! Feature version 2 of the aggregate routing helper: the boolean-`HAVING`
//! range activation.
//!
//! Differs from v1 in exactly one branch: a grouped aggregate carrying a
//! **single** `having` clause routes to the having-range executor
//! ([`AggregateRouting::HavingRange`]) instead of being refused. Routing
//! here only asks "is there a grouped select with exactly one having
//! clause?" — whether the clause bounds the selected aggregate, whether
//! its operator translates to a contiguous range, whether an `order_by`
//! is compatible, and the limit's bounds are all drive's call, made in
//! `detect_having_mode`; a routing-layer copy of that grammar could
//! disagree with drive's after a version bump.
//!
//! Multi-clause `having` (implicit AND) keeps the `not_yet_implemented`
//! contract: each extra clause needs a per-candidate post-check against
//! the primary that no executor performs yet. And `having` without
//! `group_by` still falls through to the v1 → v0 blanket rejection — a
//! global aggregate produces one row, and bounding it is a client-side
//! comparison, not a query.

use super::v1::compute_aggregate_mode_and_check_limit_v1;
use super::AggregateRouting;
use crate::error::query::QueryError;
use crate::query::document_query::v1::not_yet_implemented;
use drive::query::{HavingClause, OrderClause, SelectProjection, WhereClause};

#[allow(clippy::too_many_arguments)]
pub(super) fn compute_aggregate_mode_and_check_limit_v2(
select: &SelectProjection,
group_by: &[String],
where_clauses: &[WhereClause],
order_by: &[OrderClause],
limit: Option<u32>,
having: &[HavingClause],
function_name: &str,
) -> Result<AggregateRouting, QueryError> {
if !having.is_empty() && !group_by.is_empty() {
return match having {
[_single] => Ok(AggregateRouting::HavingRange),
many => Err(not_yet_implemented(&format!(
"multiple HAVING clauses (implicit AND): got {}. One clause on the \
selected {function_name} aggregate is served as a single contiguous \
range read of the covering ranked index's axis secondary; additional \
clauses would need a per-candidate post-check that is not implemented. \
Narrow to a single clause",
many.len()
))),
};
}

// No having (or no group_by, where a having still dies in v0's
// blanket rejection): identical routing to v1, including its ranked
// detection and its delegation to v0 for non-ranked shapes.
compute_aggregate_mode_and_check_limit_v1(
select,
group_by,
where_clauses,
order_by,
limit,
having,
function_name,
)
}
Loading
Loading