Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
724 changes: 541 additions & 183 deletions book/src/drive/count-index-group-by-examples.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,17 @@ pub fn verify_carrier_aggregate_count_proof(
proof: &Proof,
mtd: &ResponseMetadata,
limit: Option<u16>,
left_to_right: bool,
platform_version: &PlatformVersion,
provider: &dyn ContextProvider,
) -> Result<Vec<SplitCountEntry>, Error> {
let (root_hash, per_key_counts) = query
.verify_carrier_aggregate_count_proof(&proof.grovedb_proof, limit, platform_version)
.verify_carrier_aggregate_count_proof(
&proof.grovedb_proof,
limit,
left_to_right,
platform_version,
)
.map_drive_error(proof, mtd)?;

verify_tenderdash_proof(proof, mtd, &root_hash, provider)?;
Expand Down
277 changes: 245 additions & 32 deletions packages/rs-drive/benches/document_count_worst_case.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -194,19 +194,11 @@ pub fn where_clauses_from_value(
}

/// Run the system-wide where-clause validator on a structured
/// `Vec<WhereClause>`. Single source of truth for the count-endpoint
/// shape contract; called both from the legacy CBOR-decoded entry
/// [`where_clauses_from_value`] and from the dispatcher's typed
/// entry, [`Drive::execute_document_count_request`].
///
/// Despite the name, this function is **validation-only** in the
/// worktree's base — it does not re-shape the clauses (no
/// `> AND <` → `between*` merge). The "canonicalize" suffix is
/// reserved for the eventual carrier-aggregate landing where a
/// same-field range-pair merge becomes load-bearing; on the
/// current code path `WhereClause::group_clauses` only classifies,
/// and the merged form is computed lazily inside the executors
/// when an executor needs it.
/// `Vec<WhereClause>` and canonicalize same-field range pairs into
/// their `between*` form. Single source of truth for the
/// count-endpoint shape contract; called both from the legacy
/// CBOR-decoded entry [`where_clauses_from_value`] and from the
/// dispatcher's typed entry, [`Drive::execute_document_count_request`].
///
/// The validator (`WhereClause::group_clauses`) rejects:
/// - Duplicate `Equal` clauses on the same field
Expand Down Expand Up @@ -237,6 +229,21 @@ pub fn where_clauses_from_value(
/// `CountMode::GroupByRange`-with-two-ranges and routes to
/// `DocumentCountMode::RangeAggregateCarrierProof`); replicating
/// it here would be redundant.
///
/// After validation, [`merge_same_field_range_pairs`] collapses
/// `[field > A, field < B]` (and analogous pairs with `>=` / `<=`)
/// into the canonical `between*` operator that
/// [`DriveDocumentCountQuery::range_clause_to_query_item`] knows
/// how to convert into a single `QueryItem`. The regular-query
/// parser does the same merge before its grouped-triple
/// validation; for count queries we do it explicitly here so
/// callers can pass either the bounded form (e.g.
/// `[brand > A, brand < B]`) or the pre-merged form (e.g.
/// `[brand BetweenExcludeBounds [A, B]]`) and get equivalent
/// mode detection downstream. Without this merge, G8a's natural
/// wire shape (four range clauses, two per field) would slip past
/// the catch-`MultipleRangeClauses` block above and then get
/// rejected by `detect_mode`'s `range_count > 1` structural check.
pub fn validate_and_canonicalize_where_clauses(
clauses: Vec<WhereClause>,
) -> Result<Vec<WhereClause>, Error> {
Expand All @@ -245,7 +252,106 @@ pub fn validate_and_canonicalize_where_clauses(
Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(_))) => {}
Err(e) => return Err(e),
}
Ok(clauses)
merge_same_field_range_pairs(clauses)
}

/// Collapse `[field > A, field < B]` (and analogous pairs with
/// `>=` / `<=`) into a single `field between* [A, B]` clause per
/// field. Equality / In clauses pass through unchanged.
///
/// Returns an error if a field has more than two range clauses
/// (structurally meaningless — a third bound would either
/// contradict an existing one or be redundant) or if the pair
/// isn't one lower-bound + one upper-bound (e.g. two `>` on the
/// same field).
fn merge_same_field_range_pairs(clauses: Vec<WhereClause>) -> Result<Vec<WhereClause>, Error> {
use crate::query::conditions::WhereOperator::{
Between, BetweenExcludeBounds, BetweenExcludeLeft, BetweenExcludeRight, GreaterThan,
GreaterThanOrEquals, LessThan, LessThanOrEquals,
};
use std::collections::BTreeMap;

let mut by_field: BTreeMap<String, Vec<WhereClause>> = BTreeMap::new();
let mut non_range: Vec<WhereClause> = Vec::new();
for wc in clauses {
if DriveDocumentCountQuery::is_range_operator(wc.operator) {
by_field.entry(wc.field.clone()).or_default().push(wc);
} else {
non_range.push(wc);
}
}
let mut result = non_range;
for (field, mut ranges) in by_field {
match ranges.len() {
0 => {}
1 => result.push(ranges.remove(0)),
2 => {
let (mut lower, mut upper): (Option<WhereClause>, Option<WhereClause>) =
(None, None);
for r in ranges {
match r.operator {
GreaterThan | GreaterThanOrEquals => {
if lower.is_some() {
return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(
"two lower-bound range clauses on the same field cannot be \
merged; combine via `between*` or remove the redundant clause",
)));
}
lower = Some(r);
}
LessThan | LessThanOrEquals => {
if upper.is_some() {
return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(
"two upper-bound range clauses on the same field cannot be \
merged; combine via `between*` or remove the redundant clause",
)));
}
upper = Some(r);
}
_ => {
// The other range operators (Between*,
// StartsWith) are themselves bounded
// already; a second range clause on the
// same field is structurally redundant.
return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(
"cannot pair a `between*`/`startsWith` range clause with \
another range on the same field; use the pre-merged form",
)));
}
}
}
let lower = lower.ok_or(Error::Query(QuerySyntaxError::MultipleRangeClauses(
"two range clauses on the same field require one lower bound (> or >=) \
and one upper bound (< or <=)",
)))?;
let upper = upper.ok_or(Error::Query(QuerySyntaxError::MultipleRangeClauses(
"two range clauses on the same field require one lower bound (> or >=) \
and one upper bound (< or <=)",
)))?;
let merged_op = match (
lower.operator == GreaterThanOrEquals,
upper.operator == LessThanOrEquals,
) {
(true, true) => Between, // [a, b]
(false, false) => BetweenExcludeBounds, // (a, b)
(true, false) => BetweenExcludeRight, // [a, b)
(false, true) => BetweenExcludeLeft, // (a, b]
};
result.push(WhereClause {
field,
operator: merged_op,
value: dpp::platform_value::Value::Array(vec![lower.value, upper.value]),
});
}
_ => {
return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(
"more than two range clauses on the same field are not supported; a \
bounded range needs exactly one lower bound and one upper bound",
)));
}
}
}
Ok(result)
}

/// Parse the decoded `order_by` value into structured [`OrderClause`]s.
Expand Down Expand Up @@ -322,7 +428,7 @@ impl Drive {
// independent of whether the caller arrived via the CBOR-
// shaped legacy path or the v1 typed-proto path. See
// [`validate_and_canonicalize_where_clauses`]'s docstring
// for the catalog of rejections.
// for the catalog of rejections / canonicalization rules.
let where_clauses = validate_and_canonicalize_where_clauses(request.where_clauses)?;
let order_clauses = request.order_clauses;

Expand Down Expand Up @@ -567,13 +673,22 @@ impl Drive {
}
None
};
// Outer-walk direction: ascending by default (the
// grovedb invariant for serialized-key carriers), or
// descending when the caller's `order_by` first
// clause is `desc`. Carried byte-identically through
// `Query::left_to_right` so the verifier rebuilds the
// exact same `PathQuery` — same load-bearing pattern
// as the `RangeDistinctProof` arm above.
let left_to_right = order_by_ascending;
Ok(DocumentCountResponse::Proof(
self.execute_document_count_range_aggregate_carrier_proof(
contract_id,
request.document_type,
document_type_name,
where_clauses,
effective_limit,
left_to_right,
transaction,
platform_version,
)?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,15 +447,32 @@ impl DriveDocumentCountQuery<'_> {
/// Verified client-side via
/// [`grovedb::GroveDb::verify_aggregate_count_query_per_key`],
/// which returns `(RootHash, Vec<(Vec<u8>, u64)>)`.
///
/// # Arguments
/// * `left_to_right` — proof-shaping bit. Threaded into the
/// outer `Query` via `Query::new_with_direction(left_to_right)`
/// on the inner carrier path query (see
/// [`Self::carrier_aggregate_count_path_query`]). `true` walks
/// the outer range ascending and emits the per-branch `u64`s
/// in lex-ascending key order; `false` walks descending and
/// emits them in lex-descending order. The serialized
/// `PathQuery` bytes differ between the two — the verifier
/// rebuilds the path query from `(query, limit, left_to_right)`
/// on its side, so the value passed here must match what the
/// caller will pass to
/// [`Self::verify_carrier_aggregate_count_proof`] or the
/// tenderdash root check fails.
pub fn execute_carrier_aggregate_count_with_proof(
&self,
drive: &Drive,
limit: Option<u16>,
left_to_right: bool,
transaction: TransactionArg,
platform_version: &PlatformVersion,
) -> Result<Vec<u8>, Error> {
let drive_version = &platform_version.drive;
let path_query = self.carrier_aggregate_count_path_query(limit, platform_version)?;
let path_query =
self.carrier_aggregate_count_path_query(limit, left_to_right, platform_version)?;
// Same destructure pattern as the sibling aggregate / distinct
// executors. `get_proved_path_query` returns `CostContext<Result>`;
// ignoring the cost field is the same pattern those use today.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ impl Drive {
document_type_name: String,
where_clauses: Vec<WhereClause>,
limit: Option<u16>,
left_to_right: bool,
transaction: TransactionArg,
platform_version: &PlatformVersion,
) -> Result<Vec<u8>, Error> {
Expand All @@ -81,6 +82,7 @@ impl Drive {
count_query.execute_carrier_aggregate_count_with_proof(
self,
limit,
left_to_right,
transaction,
platform_version,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ impl DriveDocumentCountQuery<'_> {
pub fn carrier_aggregate_count_path_query(
&self,
limit: Option<u16>,
left_to_right: bool,
platform_version: &PlatformVersion,
) -> Result<PathQuery, Error> {
// The terminator property (last in the index) carries the
Expand Down Expand Up @@ -405,7 +406,7 @@ impl DriveDocumentCountQuery<'_> {
}
subquery_path_extension.push(terminator_prop_name.as_bytes().to_vec());

let mut outer_query = Query::new();
let mut outer_query = Query::new_with_direction(left_to_right);
match carrier {
Carrier::Pending => {
return Err(Error::Query(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ use dpp::version::PlatformVersion;
impl DriveDocumentCountQuery<'_> {
/// Verifies a **carrier** `AggregateCountOnRange` proof and
/// returns `(root_hash, per_key_counts)` — one `(in_key, u64)`
/// pair per resolved In branch in serialized lex-asc order.
/// pair per resolved In branch. Order depends on
/// `left_to_right`: `true` returns serialized lex-ascending,
/// `false` returns serialized lex-descending.
///
/// Counterpart to the prover-side
/// [`execute_carrier_aggregate_count_with_proof`](Self::execute_carrier_aggregate_count_with_proof):
Expand All @@ -24,6 +26,17 @@ impl DriveDocumentCountQuery<'_> {
///
/// # Arguments
/// * `proof` — raw grovedb proof bytes.
/// * `limit` — per-branch carrier walk cap; must match the
/// prover's `SizedQuery::limit`.
/// * `left_to_right` — proof-shaping bit. Must match the value
/// the prover passed to
/// [`Self::execute_carrier_aggregate_count_with_proof`]
/// (typically derived from the request's first
/// `order_by_clauses` entry's `ascending`). The verifier
/// constructs the outer `Query` via
/// `Query::new_with_direction(left_to_right)`; a mismatch
/// produces different `PathQuery` bytes and the tenderdash
/// root check fails.
/// * `platform_version` — selects the method version.
///
/// The `Vec<(Vec<u8>, u64)>` payload mirrors grovedb's per-key
Expand All @@ -33,6 +46,7 @@ impl DriveDocumentCountQuery<'_> {
&self,
proof: &[u8],
limit: Option<u16>,
left_to_right: bool,
platform_version: &PlatformVersion,
) -> Result<(RootHash, Vec<(Vec<u8>, u64)>), Error> {
match platform_version
Expand All @@ -42,7 +56,12 @@ impl DriveDocumentCountQuery<'_> {
.document_count
.verify_carrier_aggregate_count_proof
{
0 => self.verify_carrier_aggregate_count_proof_v0(proof, limit, platform_version),
0 => self.verify_carrier_aggregate_count_proof_v0(
proof,
limit,
left_to_right,
platform_version,
),
version => Err(Error::Drive(DriveError::UnknownVersionMismatch {
method: "DriveDocumentCountQuery::verify_carrier_aggregate_count_proof".to_string(),
known_versions: vec![0],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ impl DriveDocumentCountQuery<'_> {
&self,
proof: &[u8],
limit: Option<u16>,
left_to_right: bool,
platform_version: &PlatformVersion,
) -> Result<(RootHash, Vec<(Vec<u8>, u64)>), Error> {
let path_query = self.carrier_aggregate_count_path_query(limit, platform_version)?;
let path_query =
self.carrier_aggregate_count_path_query(limit, left_to_right, platform_version)?;
let (root_hash, entries) = GroveDb::verify_aggregate_count_query_per_key(
proof,
&path_query,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,17 @@ pub(super) fn verify_count_query(
} else {
Some(limit_to_u16_or_default(request.limit)?)
};
let left_to_right = request
.order_by_clauses
.first()
.map(|c| c.ascending)
.unwrap_or(true);
let entries = verify_carrier_aggregate_count_proof(
&count_query,
proof,
mtd,
limit_u16,
left_to_right,
platform_version,
provider,
)?;
Expand Down
Loading