Skip to content
Open
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.

256 changes: 223 additions & 33 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 @@ -3938,23 +3938,24 @@ mod range_countable_index_e2e_tests {
.expect("apply contract");
let document_type = contract.document_type_for_name("car").expect("car doctype");

// Build a where-clause `Value::Array` of one range clause:
// [["lot", ">", "b"]]. Mirrors the wire shape the abci
// handler hands to drive after CBOR-decoding.
let where_clause_value = Value::Array(vec![Value::Array(vec![
Value::Text("lot".to_string()),
Value::Text(">".to_string()),
Value::Text("b".to_string()),
])]);
// Single range clause `lot > "b"` as a typed `WhereClause`.
// The dispatcher runs the same validate-and-canonicalize
// step the CBOR-shaped path runs.
use crate::query::{WhereClause, WhereOperator};
let where_clauses = vec![WhereClause {
field: "lot".to_string(),
operator: WhereOperator::GreaterThan,
value: Value::Text("b".to_string()),
}];

let drive_config = crate::config::DriveConfig::default();
let too_large = drive_config.max_query_limit as u32 + 1;

let request = DocumentCountRequest {
contract: &contract,
document_type,
raw_where_value: where_clause_value,
raw_order_by_value: dpp::platform_value::Value::Null,
where_clauses,
order_clauses: Vec::new(),
mode: crate::query::CountMode::GroupByRange,
limit: Some(too_large),
prove: true,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -451,11 +451,13 @@ impl DriveDocumentCountQuery<'_> {
&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
96 changes: 49 additions & 47 deletions packages/rs-drive/src/query/drive_document_count_query/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,12 +501,14 @@ fn test_aggregate_count_in_fan_out_ignores_default_query_limit() {
..Default::default()
};

// Wire-shape `where` value the dispatcher CBOR-decodes: a single
// `In` clause on `age` with all 8 values.
let raw_where_value = Value::Array(vec![Value::Array(vec![
Value::Text("age".to_string()),
Value::Text("in".to_string()),
Value::Array(vec![
// Typed `In` clause on `age` with all 8 values. The dispatcher
// runs the same validate-and-canonicalize step the CBOR-shaped
// path runs (see [`validate_and_canonicalize_where_clauses`]),
// so structurally identical to the legacy fixture.
let where_clauses = vec![WhereClause {
field: "age".to_string(),
operator: WhereOperator::In,
value: Value::Array(vec![
Value::U64(30),
Value::U64(40),
Value::U64(50),
Expand All @@ -516,13 +518,13 @@ fn test_aggregate_count_in_fan_out_ignores_default_query_limit() {
Value::U64(90),
Value::U64(100),
]),
])]);
}];

let request = DocumentCountRequest {
contract: &data_contract,
document_type,
raw_where_value,
raw_order_by_value: Value::Null,
where_clauses,
order_clauses: Vec::new(),
mode: CountMode::Aggregate,
// Aggregate rejects explicit `limit` upstream; the
// dispatcher must not substitute `default_query_limit` for
Expand Down Expand Up @@ -1303,26 +1305,26 @@ fn test_compound_range_in_summed_no_proof_uses_per_in_aggregate_fanout() {
// which loops over the In values and issues
// `query_aggregate_count` per branch.
let drive_config = DriveConfig::default();
let raw_where_value = Value::Array(vec![
Value::Array(vec![
Value::Text("brand".to_string()),
Value::Text("in".to_string()),
Value::Array(vec![
let where_clauses = vec![
WhereClause {
field: "brand".to_string(),
operator: WhereOperator::In,
value: Value::Array(vec![
Value::Text("acme".to_string()),
Value::Text("contoso".to_string()),
]),
]),
Value::Array(vec![
Value::Text("color".to_string()),
Value::Text(">".to_string()),
Value::Text("blue".to_string()),
]),
]);
},
WhereClause {
field: "color".to_string(),
operator: WhereOperator::GreaterThan,
value: Value::Text("blue".to_string()),
},
];
let request = DocumentCountRequest {
contract: &data_contract,
document_type,
raw_where_value,
raw_order_by_value: Value::Null,
where_clauses,
order_clauses: Vec::new(),
mode: CountMode::Aggregate,
limit: None,
prove: false,
Expand Down Expand Up @@ -1379,24 +1381,24 @@ fn test_count_request_with_duplicate_equality_clauses_is_rejected() {
// so the answer should be 0, but a regression would return
// count("firstName = Alice") or count("firstName = Bob")
// depending on iteration order.
let raw_where_value = Value::Array(vec![
Value::Array(vec![
Value::Text("firstName".to_string()),
Value::Text("==".to_string()),
Value::Text("Alice".to_string()),
]),
Value::Array(vec![
Value::Text("firstName".to_string()),
Value::Text("==".to_string()),
Value::Text("Bob".to_string()),
]),
]);
let where_clauses = vec![
WhereClause {
field: "firstName".to_string(),
operator: WhereOperator::Equal,
value: Value::Text("Alice".to_string()),
},
WhereClause {
field: "firstName".to_string(),
operator: WhereOperator::Equal,
value: Value::Text("Bob".to_string()),
},
];
let drive_config = DriveConfig::default();
let request = DocumentCountRequest {
contract: &data_contract,
document_type,
raw_where_value,
raw_order_by_value: Value::Null,
where_clauses,
order_clauses: Vec::new(),
mode: CountMode::Aggregate,
limit: None,
prove: false,
Expand Down Expand Up @@ -1579,19 +1581,19 @@ fn test_range_distinct_proof_uses_compile_time_default_query_limit_not_operator_
..Default::default()
};

// Range clause `color > "blue"` as wire-shape (Value::Array of
// [field, op, value] tuples) — the dispatcher CBOR-decodes
// this internally into structured WhereClauses.
let raw_where_value = Value::Array(vec![Value::Array(vec![
Value::Text("color".to_string()),
Value::Text(">".to_string()),
Value::Text("blue".to_string()),
])]);
// Range clause `color > "blue"` as a typed WhereClause —
// the dispatcher runs validate-and-canonicalize internally and
// dispatches to the RangeDistinctProof path on `prove=true`.
let where_clauses = vec![WhereClause {
field: "color".to_string(),
operator: WhereOperator::GreaterThan,
value: Value::Text("blue".to_string()),
}];
let request = DocumentCountRequest {
contract: &data_contract,
document_type,
raw_where_value,
raw_order_by_value: Value::Null,
where_clauses,
order_clauses: Vec::new(),
mode: CountMode::GroupByRange,
limit: None,
prove: true,
Expand Down
90 changes: 69 additions & 21 deletions packages/rs-drive/src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -762,19 +762,6 @@ impl<'a> DriveDocumentQuery<'a> {
document_type: DocumentTypeRef<'a>,
config: &DriveConfig,
) -> Result<Self, Error> {
let limit = maybe_limit
.map_or(Some(config.default_query_limit), |limit_value| {
if limit_value == 0 || limit_value > config.default_query_limit {
None
} else {
Some(limit_value)
}
})
.ok_or(Error::Query(QuerySyntaxError::InvalidLimit(format!(
"limit greater than max limit {}",
config.max_query_limit
))))?;

let all_where_clauses: Vec<WhereClause> = match where_clause {
Value::Null => Ok(vec![]),
Value::Array(clauses) => clauses
Expand All @@ -794,10 +781,8 @@ impl<'a> DriveDocumentQuery<'a> {
))),
}?;

let internal_clauses = InternalClauses::extract_from_clauses(all_where_clauses)?;

let order_by: IndexMap<String, OrderClause> = order_by
.map_or(vec![], |id_cbor| {
let order_by_clauses: Vec<OrderClause> = order_by
.map(|id_cbor| {
if let Value::Array(clauses) = id_cbor {
clauses
.iter()
Expand All @@ -810,12 +795,75 @@ impl<'a> DriveDocumentQuery<'a> {
})
.collect()
} else {
vec![]
Vec::new()
}
})
.iter()
.map(|order_clause| Ok((order_clause.field.clone(), order_clause.to_owned())))
.collect::<Result<IndexMap<String, OrderClause>, Error>>()?;
.unwrap_or_default();

Self::from_typed_clauses(
all_where_clauses,
order_by_clauses,
maybe_limit,
start_at,
start_at_included,
block_time_ms,
contract,
document_type,
config,
)
}

/// Build a `DriveDocumentQuery` from already-structured where /
/// order_by clauses. This is the typed-input twin of
/// [`Self::from_decomposed_values`] — same downstream shape, just
/// without the `Value::Array(...)` parse step.
///
/// Used by the v1 `getDocuments` ABCI handler whose wire format
/// carries `repeated WhereClause` / `repeated OrderClause`
/// natively (no CBOR envelope). The v0 path keeps using
/// `from_decomposed_values` so its CBOR-decoded inputs flow
/// through the existing `WhereClause::from_components` parser
/// for shape validation; the typed path expects that validation
/// (or the equivalent proto→drive conversion) to have run
/// upstream.
///
/// Limit semantics mirror `from_decomposed_values`:
/// `maybe_limit = None` or `Some(0)` falls back to
/// `config.default_query_limit`; `Some(N)` with `N >
/// config.default_query_limit` is rejected as
/// `QuerySyntaxError::InvalidLimit`.
#[cfg(any(feature = "server", feature = "verify"))]
#[allow(clippy::too_many_arguments)]
pub fn from_typed_clauses(
where_clauses: Vec<WhereClause>,
order_by_clauses: Vec<OrderClause>,
maybe_limit: Option<u16>,
start_at: Option<[u8; 32]>,
start_at_included: bool,
block_time_ms: Option<u64>,
contract: &'a DataContract,
document_type: DocumentTypeRef<'a>,
config: &DriveConfig,
) -> Result<Self, Error> {
let limit = maybe_limit
.map_or(Some(config.default_query_limit), |limit_value| {
if limit_value == 0 || limit_value > config.default_query_limit {
None
} else {
Some(limit_value)
}
})
.ok_or(Error::Query(QuerySyntaxError::InvalidLimit(format!(
"limit greater than max limit {}",
config.max_query_limit
))))?;

let internal_clauses = InternalClauses::extract_from_clauses(where_clauses)?;

let order_by: IndexMap<String, OrderClause> = order_by_clauses
.into_iter()
.map(|c| (c.field.clone(), c))
.collect();

Ok(DriveDocumentQuery {
contract,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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 +43,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
Loading