drive/query/drive_document_sum_query/mode_detection/mod.rs
1//! Versioned mode detection for the sum-query dispatcher.
2//!
3//! `detect_sum_mode` classifies a [`DocumentSumRequest`] into one of
4//! the [`DocumentSumMode`] variants by inspecting the
5//! `(where × SumMode × prove)` triple. The result picks the
6//! executor the dispatcher routes to.
7//!
8//! Versioned because the routing table is a consensus-relevant
9//! contract on the query surface — a future protocol version that
10//! adds or relaxes shapes (e.g. a new "GroupByRange + In + prove"
11//! mapping) has to land behind a method-version bump so older
12//! nodes replaying historical traffic keep dispatching the way
13//! the chain originally saw.
14//!
15//! Two public surfaces, both routed through the same method-version
16//! slot to guarantee server + SDK pick the same executor:
17//!
18//! - [`detect_sum_mode`] (server-side) — takes a full
19//! [`DocumentSumRequest`] and additionally cross-validates the
20//! request's `sum_property` against the doctype's
21//! `documents_summable`. Server-only because
22//! [`DocumentSumRequest`] is `#[cfg(feature = "server")]`.
23//!
24//! - [`detect_sum_mode_from_inputs`] — takes the minimal
25//! `(where_clauses, mode, prove)` tuple the routing table
26//! actually needs, callable from both server and SDK (`server`
27//! OR `verify` features). Mirrors count's
28//! [`crate::query::drive_document_count_query::DriveDocumentCountQuery::detect_mode_versioned`]
29//! so the SDK's verify path picks the same executor the prover
30//! used — without this, the SDK had to reconstruct routing from
31//! ad-hoc `(has_range, has_in, distinct_mode)` booleans, which
32//! could disagree with the v0 routing table on edge cases (e.g.
33//! `group_by = [in_field]` with a co-present range clause routes
34//! to the carrier shape server-side but the heuristic would have
35//! sent it to an aggregate verifier).
36
37#[cfg(any(feature = "server", feature = "verify"))]
38mod v0;
39
40use crate::error::query::QuerySyntaxError;
41use crate::error::Error;
42#[cfg(feature = "server")]
43use crate::query::drive_document_sum_query::DocumentSumRequest;
44#[cfg(any(feature = "server", feature = "verify"))]
45use crate::query::drive_document_sum_query::{DocumentSumMode, SumMode};
46#[cfg(any(feature = "server", feature = "verify"))]
47use crate::query::WhereClause;
48use dpp::version::PlatformVersion;
49
50/// Server-side wrapper around [`detect_sum_mode_from_inputs`] that
51/// adds the doctype cross-validation (`sum_property` must agree
52/// with the doctype's `documents_summable`). Server-only because
53/// [`DocumentSumRequest`] is gated behind the `server` feature.
54///
55/// Routes through
56/// `platform_version.drive.methods.document.query.detect_sum_mode`.
57#[cfg(feature = "server")]
58pub fn detect_sum_mode(
59 request: &DocumentSumRequest,
60 platform_version: &PlatformVersion,
61) -> Result<DocumentSumMode, Error> {
62 use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters;
63
64 // Doctype cross-validation. Kept here (server-side only)
65 // rather than in `detect_sum_mode_from_inputs` because the SDK
66 // doesn't carry the constructed `DocumentSumRequest`'s implied
67 // contract-level invariants — the SDK's index picker
68 // (`find_summable_index_for_where_clauses`) enforces the same
69 // matching constraint at index-resolution time.
70 if let Some(doctype_sum) = request.document_type.documents_summable() {
71 if doctype_sum != request.sum_property {
72 return Err(Error::Drive(crate::error::drive::DriveError::NotSupported(
73 "request `sum_property` doesn't match the document type's \
74 `documents_summable`. Sum trees aggregate `i64` per merk node; \
75 mixing property names would produce a meaningless aggregation. \
76 Define a separate index whose `summable: \"<the other name>\"` \
77 covers the alternate aggregation surface.",
78 )));
79 }
80 }
81
82 detect_sum_mode_from_inputs(
83 &request.where_clauses,
84 request.mode,
85 request.prove,
86 platform_version,
87 )
88}
89
90/// Same routing decision as [`detect_sum_mode`] but takes the
91/// minimal `(where_clauses, mode, prove)` tuple — callable from
92/// SDK verify paths that don't have a `DocumentSumRequest`.
93///
94/// Skips the `sum_property ↔ documents_summable` cross-validation
95/// that the server does — that invariant is enforced for the SDK by
96/// the index picker, which only returns indexes whose `summable`
97/// declaration matches the request's select field. Match-any
98/// (mismatched) requests fail at index picking with a clear error;
99/// they never reach this function with a wrong combination.
100///
101/// Routes through the same method-version slot as
102/// [`detect_sum_mode`] so server + SDK can never drift.
103#[cfg(any(feature = "server", feature = "verify"))]
104pub fn detect_sum_mode_from_inputs(
105 where_clauses: &[WhereClause],
106 mode: SumMode,
107 prove: bool,
108 platform_version: &PlatformVersion,
109) -> Result<DocumentSumMode, Error> {
110 match platform_version
111 .drive
112 .methods
113 .document
114 .query
115 .detect_sum_mode
116 {
117 0 => v0::detect_sum_mode_v0_from_inputs(where_clauses, mode, prove),
118 version => Err(Error::Query(QuerySyntaxError::Unsupported(format!(
119 "detect_sum_mode: unknown method version {version}; only 0 is supported"
120 )))),
121 }
122}