Skip to main content

drive/query/
canonicalize.rs

1//! Shared where-clause validation + canonicalization for the aggregate
2//! query surfaces (count / sum / average / joint count-and-sum) and
3//! their SDK proof verifiers.
4//!
5//! Lives outside the per-surface dispatcher modules because the shape
6//! contract must be identical on every route: the server dispatchers
7//! canonicalize before mode detection, and the proof verifiers must run
8//! the very same canonicalization before *their* mode detection or a
9//! proof the server produced for the canonical shape is rejected
10//! client-side (the count dispatcher promises callers "the bounded form
11//! and the pre-merged form get equivalent mode detection" — that promise
12//! only holds if verifiers canonicalize too).
13
14use crate::error::query::QuerySyntaxError;
15use crate::error::Error;
16use crate::query::drive_document_count_query::DriveDocumentCountQuery;
17use crate::query::WhereClause;
18use dpp::version::PlatformVersion;
19
20/// Run the system-wide where-clause validator on a structured
21/// `Vec<WhereClause>` and canonicalize same-field range pairs into
22/// their `between*` form. Single source of truth for the aggregate
23/// shape contract; called by the count / sum / average / joint
24/// dispatchers, the legacy CBOR-decoded count entry, and the SDK
25/// count / sum / average proof verifiers.
26///
27/// The validator (`WhereClause::group_clauses`) rejects:
28/// - Duplicate `Equal` clauses on the same field
29///   (`DuplicateNonGroupableClauseSameField`).
30/// - Multiple `In` clauses (`MultipleInClauses`) — rejected here: the
31///   shared grammar accepts them for protocol version 14+ document
32///   queries, but the aggregate surfaces do not.
33/// - Multiple non-groupable range clauses (`MultipleRangeClauses`).
34/// - Equality + `In` on the same field, range + equality/In on the
35///   same field (`DuplicateNonGroupableClauseSameField` /
36///   `InvalidWhereClauseComponents`).
37///
38/// Without this validation, downstream
39/// [`DriveDocumentCountQuery::find_countable_index_for_where_clauses`]
40/// collapses repeated fields into a `BTreeSet` and
41/// [`DriveDocumentCountQuery::point_lookup_count_path_query`]
42/// resolves each index property with a single `.find(...)` — both
43/// of which silently pick the first clause on a duplicated field
44/// and return a count for an arbitrarily reduced query rather than
45/// rejecting the malformed request.
46///
47/// **Exception**: `MultipleRangeClauses` is intentionally tolerated
48/// here. The regular-query parser rejects two ranges on different
49/// fields wholesale (its callers expect
50/// `(equal_clauses, in_clause, range_clause)` triples), but the
51/// count-query path accepts the carrier-aggregate shape
52/// (`outer_range + inner_ACOR_range` on different fields, e.g.
53/// G8). Structural validation for that shape lives in
54/// [`DriveDocumentCountQuery::detect_mode`] (which knows about
55/// `CountMode::GroupByRange`-with-two-ranges and routes to
56/// `DocumentCountMode::RangeAggregateCarrierProof`); replicating
57/// it here would be redundant.
58///
59/// After validation, [`merge_same_field_range_pairs`] collapses
60/// `[field > A, field < B]` (and analogous pairs with `>=` / `<=`)
61/// into the canonical `between*` operator that
62/// [`DriveDocumentCountQuery::range_clause_to_query_item`] knows
63/// how to convert into a single `QueryItem`. The regular-query
64/// parser does the same merge before its grouped-triple
65/// validation; for aggregate queries we do it explicitly here so
66/// callers can pass either the bounded form (e.g.
67/// `[brand > A, brand < B]`) or the pre-merged form (e.g.
68/// `[brand BetweenExcludeBounds [A, B]]`) and get equivalent
69/// mode detection downstream. Without this merge, G8a's natural
70/// wire shape (four range clauses, two per field) would slip past
71/// the catch-`MultipleRangeClauses` block above and then get
72/// rejected by `detect_mode`'s `range_count > 1` structural check.
73pub fn validate_and_canonicalize_where_clauses(
74    clauses: Vec<WhereClause>,
75    platform_version: &PlatformVersion,
76) -> Result<Vec<WhereClause>, Error> {
77    match WhereClause::group_clauses(&clauses, platform_version) {
78        // Multiple `In` clauses are a document-query-only shape (protocol
79        // version 14+); the aggregate surfaces keep rejecting them since
80        // their mode detection and index pickers assume a single `In`.
81        Ok((_, _, in_clauses)) if in_clauses.len() > 1 => {
82            return Err(Error::Query(QuerySyntaxError::MultipleInClauses(
83                "aggregate queries support at most one in clause",
84            )));
85        }
86        Ok(_) => {}
87        Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(_))) => {}
88        Err(e) => return Err(e),
89    }
90    merge_same_field_range_pairs(clauses)
91}
92
93/// Collapse `[field > A, field < B]` (and analogous pairs with
94/// `>=` / `<=`) into a single `field between* [A, B]` clause per
95/// field. Equality / In clauses pass through unchanged.
96///
97/// Returns an error if a field has more than two range clauses
98/// (structurally meaningless — a third bound would either
99/// contradict an existing one or be redundant) or if the pair
100/// isn't one lower-bound + one upper-bound (e.g. two `>` on the
101/// same field).
102fn merge_same_field_range_pairs(clauses: Vec<WhereClause>) -> Result<Vec<WhereClause>, Error> {
103    use crate::query::conditions::WhereOperator::{
104        Between, BetweenExcludeBounds, BetweenExcludeLeft, BetweenExcludeRight, GreaterThan,
105        GreaterThanOrEquals, LessThan, LessThanOrEquals,
106    };
107    use std::collections::BTreeMap;
108
109    let mut by_field: BTreeMap<String, Vec<WhereClause>> = BTreeMap::new();
110    let mut non_range: Vec<WhereClause> = Vec::new();
111    for wc in clauses {
112        if DriveDocumentCountQuery::is_range_operator(wc.operator) {
113            by_field.entry(wc.field.clone()).or_default().push(wc);
114        } else {
115            non_range.push(wc);
116        }
117    }
118    let mut result = non_range;
119    for (field, mut ranges) in by_field {
120        match ranges.len() {
121            0 => {}
122            1 => result.push(ranges.remove(0)),
123            2 => {
124                let (mut lower, mut upper): (Option<WhereClause>, Option<WhereClause>) =
125                    (None, None);
126                for r in ranges {
127                    match r.operator {
128                        GreaterThan | GreaterThanOrEquals => {
129                            if lower.is_some() {
130                                return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(
131                                    "two lower-bound range clauses on the same field cannot be \
132                                     merged; combine via `between*` or remove the redundant clause",
133                                )));
134                            }
135                            lower = Some(r);
136                        }
137                        LessThan | LessThanOrEquals => {
138                            if upper.is_some() {
139                                return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(
140                                    "two upper-bound range clauses on the same field cannot be \
141                                     merged; combine via `between*` or remove the redundant clause",
142                                )));
143                            }
144                            upper = Some(r);
145                        }
146                        _ => {
147                            // The other range operators (Between*,
148                            // StartsWith) are themselves bounded
149                            // already; a second range clause on the
150                            // same field is structurally redundant.
151                            return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(
152                                "cannot pair a `between*`/`startsWith` range clause with \
153                                 another range on the same field; use the pre-merged form",
154                            )));
155                        }
156                    }
157                }
158                let lower = lower.ok_or(Error::Query(QuerySyntaxError::MultipleRangeClauses(
159                    "two range clauses on the same field require one lower bound (> or >=) \
160                     and one upper bound (< or <=)",
161                )))?;
162                let upper = upper.ok_or(Error::Query(QuerySyntaxError::MultipleRangeClauses(
163                    "two range clauses on the same field require one lower bound (> or >=) \
164                     and one upper bound (< or <=)",
165                )))?;
166                let merged_op = match (
167                    lower.operator == GreaterThanOrEquals,
168                    upper.operator == LessThanOrEquals,
169                ) {
170                    (true, true) => Between,                // [a, b]
171                    (false, false) => BetweenExcludeBounds, // (a, b)
172                    (true, false) => BetweenExcludeRight,   // [a, b)
173                    (false, true) => BetweenExcludeLeft,    // (a, b]
174                };
175                result.push(WhereClause {
176                    field,
177                    operator: merged_op,
178                    value: dpp::platform_value::Value::Array(vec![lower.value, upper.value]),
179                });
180            }
181            _ => {
182                return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(
183                    "more than two range clauses on the same field are not supported; a \
184                     bounded range needs exactly one lower bound and one upper bound",
185                )));
186            }
187        }
188    }
189    Ok(result)
190}