diff --git a/packages/dash-platform-queries/src/documents/aggregate_limit.rs b/packages/dash-platform-queries/src/documents/aggregate_limit.rs new file mode 100644 index 00000000000..af55ea36cca --- /dev/null +++ b/packages/dash-platform-queries/src/documents/aggregate_limit.rs @@ -0,0 +1,267 @@ +//! Shared limit cap for the aggregate (COUNT / SUM / AVG) proof +//! verifiers. +//! +//! Server counterpart: the prove-path arms of drive's aggregate +//! dispatchers (`rs-drive`'s +//! `query/drive_document_{count,sum,average}_query/drive_dispatcher.rs`) +//! refuse any request whose limit exceeds +//! `drive_config.max_query_limit` with +//! `QuerySyntaxError::InvalidLimit` *before* producing proof bytes. +//! The verifier mirrors that gate against the compile-time +//! [`DEFAULT_MAX_QUERY_LIMIT`] — `max_query_limit`'s config default +//! (100); the SDK cannot see an operator's runtime tuning, and proof +//! bytes never depend on it — so a request the server would refuse +//! can never reach a proof primitive. Without the cap, an untrusted +//! transport could pair a server-invalid request (limit 101..=65535 +//! fits the wire's `u32` and even a `u16`) with a genuine proof +//! produced for a different, server-permitted query. +//! +//! The `0` sentinel ("no limit set on the wire": V0's `limit: 0`, +//! V1's `limit: None`) is translated per walk shape, mirroring the +//! server dispatchers exactly: +//! - distinct walk (`RangeDistinctProof`): `0` → +//! [`DEFAULT_QUERY_LIMIT`], because the server applies +//! `limit.unwrap_or(DEFAULT_QUERY_LIMIT)` before building the +//! path query; +//! - carrier walk (`RangeAggregateCarrierProof`), SUM / AVG: `0` → +//! `None`, because those servers keep an unset limit as an +//! unbounded outer walk; +//! - carrier walk, COUNT: shape-dependent — see +//! [`ServerCappedLimit::count_carrier_walk_limit`], whose +//! range-outer arm enforces the stricter compile-time cap the +//! COUNT dispatcher applies. + +use drive::config::{DEFAULT_MAX_QUERY_LIMIT, DEFAULT_QUERY_LIMIT}; + +// The distinct-walk fallback (`DEFAULT_QUERY_LIMIT`, mirroring the +// server's `limit.unwrap_or(DEFAULT_QUERY_LIMIT)`) must itself pass +// the cap, exactly as the server checks its own fallback against +// `max_query_limit`. Both are 100 today with no compile-time link; +// this pin makes a future divergence a build error here instead of +// a silent parity break. +const _: () = assert!(DEFAULT_QUERY_LIMIT <= DEFAULT_MAX_QUERY_LIMIT); + +/// A request limit that has passed the server's aggregate +/// prove-path cap. Constructing one via [`check_within_server_cap`] +/// is the only way to reach the walk-shape converters, so the +/// "cap check runs first" ordering — what makes their narrowing +/// casts exact — is enforced by the type system rather than a +/// `debug_assert!` that vanishes from release builds. +#[derive(Debug)] +pub(crate) struct ServerCappedLimit(u32); + +/// Reject a limit the server's aggregate prove paths would refuse +/// with `InvalidLimit`. Run this before any proof or +/// context-provider machinery; the returned [`ServerCappedLimit`] +/// carries the proof that it passed. `0` (unset sentinel) always +/// passes — its meaning is resolved per walk shape. +pub(crate) fn check_within_server_cap( + limit: u32, + surface: &str, +) -> Result { + if limit > u32::from(DEFAULT_MAX_QUERY_LIMIT) { + return Err(drive_proof_verifier::Error::RequestError { + error: format!( + "limit {limit} exceeds the server's max_query_limit {DEFAULT_MAX_QUERY_LIMIT} \ + on the prove path ({surface}); the server refuses such requests with \ + InvalidLimit before producing proof bytes, so no proved response can \ + belong to this request" + ), + }); + } + Ok(ServerCappedLimit(limit)) +} + +impl ServerCappedLimit { + /// Distinct-walk (`RangeDistinctProof`) limit: `0` falls back + /// to [`DEFAULT_QUERY_LIMIT`], mirroring the server's + /// `limit.unwrap_or(DEFAULT_QUERY_LIMIT)`. + pub(crate) fn distinct_walk_limit(&self) -> u16 { + if self.0 == 0 { + DEFAULT_QUERY_LIMIT + } else { + self.0 as u16 + } + } + + /// Carrier-walk (`RangeAggregateCarrierProof`) limit for SUM / + /// AVG: `0` stays `None` (unbounded outer walk), mirroring + /// those servers keeping an unset request limit as `None`. + /// COUNT must use [`Self::count_carrier_walk_limit`] instead — + /// its dispatcher applies shape-dependent rules this converter + /// does not know about. + pub(crate) fn carrier_walk_limit(&self) -> Option { + if self.0 == 0 { + None + } else { + Some(self.0 as u16) + } + } + + /// COUNT carrier-walk (`RangeAggregateCarrierProof`) limit, + /// mirroring the two shape-dependent rules of the COUNT + /// dispatcher's carrier arm (`drive_document_count_query/ + /// drive_dispatcher.rs`) exactly — both change the + /// proof-sensitive `SizedQuery::limit` bytes, so a mismatch + /// either rejects honest proofs or verifies request/proof + /// pairings no server produced: + /// + /// - **Range-outer carrier (G8)** — `GROUP BY` one range field + /// with two range clauses on distinct fields + /// (`has_outer_range`): the server lowers an unset limit to + /// the compile-time cap + /// [`MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`] and refuses + /// explicit limits above it with `InvalidLimit` before + /// producing proof bytes. + /// - **In-outer carrier (G7)**: the `In` array already bounds + /// the walk. An unset limit stays `None`; the server refuses + /// every explicit limit here, so the verifier must too. + /// + /// [`MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT`]: + /// drive::query::drive_document_count_query::MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT + pub(crate) fn count_carrier_walk_limit( + &self, + has_outer_range: bool, + ) -> Result, drive_proof_verifier::Error> { + use drive::query::drive_document_count_query::MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT; + + if has_outer_range { + if self.0 == 0 { + return Ok(Some(MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT)); + } + if self.0 > u32::from(MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT) { + return Err(drive_proof_verifier::Error::RequestError { + error: format!( + "limit {} exceeds the carrier-aggregate range-outer cap \ + {MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT} (COUNT); the server \ + refuses such requests with InvalidLimit before producing proof \ + bytes, so no proved response can belong to this request", + self.0 + ), + }); + } + Ok(Some(self.0 as u16)) + } else { + if self.0 != 0 { + return Err(drive_proof_verifier::Error::RequestError { + error: format!( + "limit {} on a carrier-aggregate In-outer COUNT; the server refuses \ + every explicit limit here (the In array bounds the walk), so no \ + proved response can belong to this request", + self.0 + ), + }); + } + Ok(None) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The cap is inclusive at `DEFAULT_MAX_QUERY_LIMIT`, exclusive + /// one past it, and the `0` sentinel always passes (its meaning + /// is resolved per walk shape) — the same acceptance set as the + /// server dispatchers' `> max_query_limit` rejection. + #[test] + fn cap_boundaries() { + assert!(check_within_server_cap(0, "TEST").is_ok()); + assert!(check_within_server_cap(1, "TEST").is_ok()); + assert!(check_within_server_cap(u32::from(DEFAULT_MAX_QUERY_LIMIT), "TEST").is_ok()); + + let error = check_within_server_cap(u32::from(DEFAULT_MAX_QUERY_LIMIT) + 1, "TEST") + .expect_err("one past the cap must be rejected"); + assert!( + error + .to_string() + .contains("exceeds the server's max_query_limit"), + "unexpected error: {error}" + ); + } + + fn capped(limit: u32) -> ServerCappedLimit { + check_within_server_cap(limit, "TEST").expect("limit within cap") + } + + /// Distinct walks translate the `0` sentinel to + /// `DEFAULT_QUERY_LIMIT`, mirroring the server's + /// `limit.unwrap_or(DEFAULT_QUERY_LIMIT)`; in-range values pass + /// through untouched. + #[test] + fn distinct_walk_sentinel_translation() { + assert_eq!(capped(0).distinct_walk_limit(), DEFAULT_QUERY_LIMIT); + assert_eq!(capped(1).distinct_walk_limit(), 1); + assert_eq!( + capped(u32::from(DEFAULT_MAX_QUERY_LIMIT)).distinct_walk_limit(), + DEFAULT_MAX_QUERY_LIMIT + ); + } + + /// SUM / AVG carrier walks keep the `0` sentinel as `None` + /// (unbounded outer walk), mirroring those servers keeping an + /// unset request limit as `None`; in-range values pass through + /// untouched. + #[test] + fn carrier_walk_sentinel_translation() { + assert_eq!(capped(0).carrier_walk_limit(), None); + assert_eq!(capped(1).carrier_walk_limit(), Some(1)); + assert_eq!( + capped(u32::from(DEFAULT_MAX_QUERY_LIMIT)).carrier_walk_limit(), + Some(DEFAULT_MAX_QUERY_LIMIT) + ); + } + + /// COUNT range-outer (G8) carriers mirror the COUNT + /// dispatcher: `0` lowers to the compile-time carrier cap, + /// explicit limits pass through up to that cap inclusively, + /// and anything above it is rejected even though it fits the + /// shared 100-item cap. + #[test] + fn count_carrier_range_outer_translation() { + use drive::query::drive_document_count_query::MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT; + let cap = MAX_CARRIER_AGGREGATE_OUTER_RANGE_LIMIT; + + assert_eq!( + capped(0).count_carrier_walk_limit(true).unwrap(), + Some(cap), + "unset must lower to the carrier cap, not None" + ); + assert_eq!(capped(1).count_carrier_walk_limit(true).unwrap(), Some(1)); + assert_eq!( + capped(u32::from(cap)) + .count_carrier_walk_limit(true) + .unwrap(), + Some(cap) + ); + + let error = capped(u32::from(cap) + 1) + .count_carrier_walk_limit(true) + .expect_err("one past the carrier cap must be rejected"); + assert!( + error + .to_string() + .contains("exceeds the carrier-aggregate range-outer cap"), + "unexpected error: {error}" + ); + } + + /// COUNT In-outer (G7) carriers mirror the COUNT dispatcher: + /// `0` stays `None` (the In array bounds the walk) and every + /// explicit limit is rejected. + #[test] + fn count_carrier_in_outer_translation() { + assert_eq!(capped(0).count_carrier_walk_limit(false).unwrap(), None); + + let error = capped(1) + .count_carrier_walk_limit(false) + .expect_err("explicit In-outer limits must be rejected"); + assert!( + error + .to_string() + .contains("carrier-aggregate In-outer COUNT"), + "unexpected error: {error}" + ); + } +} diff --git a/packages/dash-platform-queries/src/documents/average_proof_helpers.rs b/packages/dash-platform-queries/src/documents/average_proof_helpers.rs index 5ab4e9bf77a..b38364bbc9b 100644 --- a/packages/dash-platform-queries/src/documents/average_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/average_proof_helpers.rs @@ -101,6 +101,10 @@ pub(super) fn verify_average_query( platform_version: &PlatformVersion, provider: &dyn ContextProvider, ) -> Result<(Option>, ResponseMetadata, Proof), drive_proof_verifier::Error> { + // First gate: a limit above the server's cap is refused on every + // server route with `InvalidLimit`, so no proof can belong to + // such a request — reject before any proof or provider machinery. + let capped_limit = super::aggregate_limit::check_within_server_cap(request.limit, "AVG")?; let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -234,18 +238,11 @@ pub(super) fn verify_average_query( )) } DocumentSumMode::RangeDistinctProof => { - let limit_u16 = if request.limit == 0 { - drive::config::DEFAULT_QUERY_LIMIT - } else { - u16::try_from(request.limit).map_err(|_| { - drive_proof_verifier::Error::RequestError { - error: format!( - "limit {} exceeds u16::MAX for distinct AVG proof", - request.limit - ), - } - })? - }; + // `0` falls back to the compile-time + // `DEFAULT_QUERY_LIMIT` the server's dispatcher reads, + // matching SUM's distinct arm. Cap already enforced at + // the top of this function. + let limit_u16 = capped_limit.distinct_walk_limit(); let left_to_right = request .order_by_clauses .first() @@ -263,18 +260,10 @@ pub(super) fn verify_average_query( Ok((Some(entries), mtd.clone(), proof.clone())) } DocumentSumMode::RangeAggregateCarrierProof => { - let limit_u16 = if request.limit == 0 { - None - } else { - Some(u16::try_from(request.limit).map_err(|_| { - drive_proof_verifier::Error::RequestError { - error: format!( - "limit {} exceeds u16::MAX for carrier-aggregate AVG proof", - request.limit - ), - } - })?) - }; + // `0` stays `None` (unbounded outer walk), mirroring the + // server's carrier arm. Cap already enforced at the top + // of this function. + let limit_u16 = capped_limit.carrier_walk_limit(); let left_to_right = request .order_by_clauses .first() diff --git a/packages/dash-platform-queries/src/documents/count_proof_helpers.rs b/packages/dash-platform-queries/src/documents/count_proof_helpers.rs index e19e9074f53..c681ae925d1 100644 --- a/packages/dash-platform-queries/src/documents/count_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/count_proof_helpers.rs @@ -62,35 +62,6 @@ pub(super) fn assert_select_is_count( Ok(()) } -/// Translate the SDK's `u32`-with-`0`-sentinel limit into the -/// `u16` the proof verifier wants to rebuild the prover's path -/// query. -/// -/// `0` falls back to [`drive::config::DEFAULT_QUERY_LIMIT`] — the -/// same compile-time constant the server's prove-distinct -/// dispatcher reads (NOT the operator-tunable -/// `drive_config.default_query_limit`, which the SDK can't see). -/// With both sides anchored to the shared constant the path-query -/// bytes match byte-for-byte across operators, so merk-root -/// recomputation succeeds regardless of any operator's tuning. -/// -/// Non-zero values must fit in `u16` since the wire's -/// `optional uint32` is wider than the verifier's path-query -/// representation. We `try_from` rather than truncate so a caller -/// passing `limit > u16::MAX` fails loudly at the SDK boundary -/// rather than silently producing a mismatched path query. -fn limit_to_u16_or_default(limit: u32) -> Result { - if limit == 0 { - return Ok(drive::config::DEFAULT_QUERY_LIMIT); - } - u16::try_from(limit).map_err(|_| drive_proof_verifier::Error::RequestError { - error: format!( - "limit {} exceeds u16::MAX; the prove-distinct path query cannot represent it", - limit - ), - }) -} - /// Verify a count-shape proof and return per-branch entries. /// /// Single source of truth for the count-proof dispatch. Picks @@ -143,6 +114,10 @@ pub(super) fn verify_count_query( platform_version: &PlatformVersion, provider: &dyn ContextProvider, ) -> Result<(Option>, ResponseMetadata, Proof), drive_proof_verifier::Error> { + // First gate: a limit above the server's cap is refused on every + // server route with `InvalidLimit`, so no proof can belong to + // such a request — reject before any proof or provider machinery. + let capped_limit = super::aggregate_limit::check_within_server_cap(request.limit, "COUNT")?; let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -271,7 +246,13 @@ pub(super) fn verify_count_query( )) } DocumentCountMode::RangeDistinctProof => { - let limit_u16 = limit_to_u16_or_default(request.limit)?; + // `0` falls back to the compile-time + // `DEFAULT_QUERY_LIMIT` the server's prove-distinct + // dispatcher reads (NOT the operator-tunable runtime + // value, which the SDK can't see), so the path-query + // bytes match byte-for-byte across operators. Cap + // already enforced at the top of this function. + let limit_u16 = capped_limit.distinct_walk_limit(); let left_to_right = request .order_by_clauses .first() @@ -290,17 +271,21 @@ pub(super) fn verify_count_query( } DocumentCountMode::RangeAggregateCarrierProof => { // Carrier-ACOR (grovedb #663) — one verified `u64` per - // present In branch. `limit` cap on the per-branch - // walk follows the same `validate-don't-clamp` - // contract the distinct path uses; pass through what - // the caller asked for (with the `0` → default - // sentinel) so the path-query bytes match the - // server's exactly. - let limit_u16 = if request.limit == 0 { - None - } else { - Some(limit_to_u16_or_default(request.limit)?) - }; + // outer branch. `limit` on the outer walk follows the + // same `validate-don't-clamp` contract the distinct + // path uses, but the COUNT dispatcher's rules are + // shape-dependent: the range-outer (G8) shape lowers an + // unset limit to the compile-time carrier cap and + // refuses explicit limits above it, while the In-outer + // (G7) shape keeps `None` and refuses every explicit + // limit. Mirror the server's own shape test. + let has_outer_range = request + .where_clauses + .iter() + .filter(|wc| DriveDocumentCountQuery::is_range_operator(wc.operator)) + .count() + == 2; + let limit_u16 = capped_limit.count_carrier_walk_limit(has_outer_range)?; let left_to_right = request .order_by_clauses .first() diff --git a/packages/dash-platform-queries/src/documents/mod.rs b/packages/dash-platform-queries/src/documents/mod.rs index 65cde6af086..fe81d09f98e 100644 --- a/packages/dash-platform-queries/src/documents/mod.rs +++ b/packages/dash-platform-queries/src/documents/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod aggregate_limit; pub(crate) mod average_proof_helpers; pub(crate) mod count_proof_helpers; pub mod document_average; diff --git a/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs b/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs index fadfd00cfd8..212214d1ee4 100644 --- a/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs @@ -94,6 +94,10 @@ pub(super) fn verify_sum_query( platform_version: &PlatformVersion, provider: &dyn ContextProvider, ) -> Result<(Option>, ResponseMetadata, Proof), drive_proof_verifier::Error> { + // First gate: a limit above the server's cap is refused on every + // server route with `InvalidLimit`, so no proof can belong to + // such a request — reject before any proof or provider machinery. + let capped_limit = super::aggregate_limit::check_within_server_cap(request.limit, "SUM")?; let document_type = request .data_contract .document_type_for_name(&request.document_type_name) @@ -232,20 +236,10 @@ pub(super) fn verify_sum_query( // path — the server rejects over-max // (`max_query_limit`) requests with a typed // `InvalidLimit` error before producing proof bytes, - // so the SDK never sees a clamped value to - // un-clamp. - let limit_u16 = if request.limit == 0 { - drive::config::DEFAULT_QUERY_LIMIT - } else { - u16::try_from(request.limit).map_err(|_| { - drive_proof_verifier::Error::RequestError { - error: format!( - "limit {} exceeds u16::MAX for distinct SUM proof", - request.limit - ), - } - })? - }; + // and the cap check at the top of this function + // mirrors that rejection, so the SDK never sees a + // clamped value to un-clamp. + let limit_u16 = capped_limit.distinct_walk_limit(); let left_to_right = request .order_by_clauses .first() @@ -263,18 +257,10 @@ pub(super) fn verify_sum_query( Ok((Some(entries), mtd.clone(), proof.clone())) } DocumentSumMode::RangeAggregateCarrierProof => { - let limit_u16 = if request.limit == 0 { - None - } else { - Some(u16::try_from(request.limit).map_err(|_| { - drive_proof_verifier::Error::RequestError { - error: format!( - "limit {} exceeds u16::MAX for carrier-aggregate SUM proof", - request.limit - ), - } - })?) - }; + // `0` stays `None` (unbounded outer walk), mirroring the + // server's carrier arm. Cap already enforced at the top + // of this function. + let limit_u16 = capped_limit.carrier_walk_limit(); let left_to_right = request .order_by_clauses .first() diff --git a/packages/dash-platform-queries/tests/aggregate_limit_parity.rs b/packages/dash-platform-queries/tests/aggregate_limit_parity.rs new file mode 100644 index 00000000000..016ca5e4e8e --- /dev/null +++ b/packages/dash-platform-queries/tests/aggregate_limit_parity.rs @@ -0,0 +1,105 @@ +//! The aggregate verify paths (COUNT / SUM / AVG) must refuse every +//! request limit the server's aggregate dispatchers refuse — before +//! any proof or context-provider machinery runs. See +//! `src/documents/aggregate_limit.rs` for the server counterpart. + +use std::sync::Arc; + +use dapi_grpc::platform::v0::GetDocumentsResponse; +use dash_context_provider::{ContextProvider, ContextProviderError}; +use dash_platform_queries::documents::document_query::DocumentQuery; +use dpp::dashcore::Network; +use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; +use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; +use dpp::tests::fixtures::get_data_contract_fixture; +use dpp::version::PlatformVersion; +use drive::query::SelectProjection; +use drive_proof_verifier::{DocumentAverage, DocumentCount, DocumentSum, FromProof}; + +fn test_contract() -> Arc { + let platform_version = PlatformVersion::latest(); + Arc::new( + get_data_contract_fixture(None, 0, platform_version.protocol_version).data_contract_owned(), + ) +} + +fn documents_query() -> DocumentQuery { + DocumentQuery::new(test_contract(), "niceDocument").expect("document type exists") +} + +/// Fails the test if proof verification is reached at all. +struct NeverCalledProvider; + +impl ContextProvider for NeverCalledProvider { + fn get_data_contract( + &self, + _id: &Identifier, + _platform_version: &PlatformVersion, + ) -> Result>, ContextProviderError> { + panic!("request must be rejected before proof verification starts") + } + + fn get_token_configuration( + &self, + _token_id: &Identifier, + ) -> Result, ContextProviderError> { + panic!("request must be rejected before proof verification starts") + } + + fn get_quorum_public_key( + &self, + _quorum_type: u32, + _quorum_hash: [u8; 32], + _core_chain_locked_height: u32, + ) -> Result<[u8; 48], ContextProviderError> { + panic!("request must be rejected before proof verification starts") + } + + fn get_platform_activation_height(&self) -> Result { + panic!("request must be rejected before proof verification starts") + } +} + +/// The aggregate verify paths (COUNT / SUM / AVG) share the same +/// server-side limit cap — drive's aggregate dispatchers refuse +/// over-`max_query_limit` requests with `InvalidLimit` before +/// producing proof bytes — so their verifiers must refuse the +/// same requests before any proof machinery. Exercised through +/// all three `FromProof` entry points, so dropping the shared +/// `aggregate_limit::check_within_server_cap` gate from any one +/// of them fails this test. The panicking provider pins that the +/// rejection precedes all proof machinery, and the asserted +/// message pins that the limit gate (not the missing proof in +/// the default response) is what fired. +#[test] +fn rejects_aggregate_limit_above_server_cap() { + fn assert_over_cap_rejected(select: SelectProjection, surface: &str) + where + T: FromProof + + std::fmt::Debug, + { + for limit in [101u32, 65_535, u32::MAX] { + let query = documents_query() + .with_select(select.clone()) + .with_limit(limit); + let error = T::maybe_from_proof_with_metadata( + query, + GetDocumentsResponse::default(), + Network::Testnet, + PlatformVersion::latest(), + &NeverCalledProvider, + ) + .expect_err("an over-cap limit on an aggregate verify path must be rejected"); + assert!( + error + .to_string() + .contains("exceeds the server's max_query_limit 100"), + "unexpected error for {surface} limit {limit}: {error}" + ); + } + } + + assert_over_cap_rejected::(SelectProjection::count_star(), "COUNT"); + assert_over_cap_rejected::(SelectProjection::sum("age"), "SUM"); + assert_over_cap_rejected::(SelectProjection::avg("age"), "AVG"); +}