Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
f5e39e3
feat(grovedb,query): allow AggregateCountOnRange as carrier subquery
QuantumExplorer May 14, 2026
73ccbc2
fix(grovedb,query): address review feedback on carrier ACOR
QuantumExplorer May 14, 2026
ae7962d
test(grovedb,query): boost carrier ACOR patch coverage
QuantumExplorer May 14, 2026
f31fcb2
refactor(grovedb): reject V0 envelopes for AggregateCountOnRange enti…
QuantumExplorer May 14, 2026
4219045
fix(grovedb): support right-to-left direction in carrier ACOR verifier
QuantumExplorer May 14, 2026
d8ddda5
docs(grovedb): tone down carrier ACOR direction comment
QuantumExplorer May 14, 2026
e1dddd5
refactor(grovedb,query): drop "ACOR" abbreviation for forward-compat
QuantumExplorer May 14, 2026
066c774
refactor(grovedb): split aggregate_count proof module into submodules
QuantumExplorer May 14, 2026
19739e5
fix(grovedb): canonical decoding for aggregate-count proofs
QuantumExplorer May 14, 2026
f1c365a
test(grovedb,query): pin nested-carrier rejection
QuantumExplorer May 14, 2026
7b083f5
test(grovedb): pin SQL-style A=1, B>4, COUNT(C>4) carrier query
QuantumExplorer May 14, 2026
0dbeff4
fix(grovedb): emit lower-layer for empty count tree under carrier ACOR
QuantumExplorer May 14, 2026
2bbf7d1
fix(grovedb): tighten query_aggregate_count to leaf-only validation
QuantumExplorer May 14, 2026
fc4e09e
fix(grovedb): surface InvalidProof instead of panicking on classifica…
QuantumExplorer May 14, 2026
d1cd151
feat(grovedb): add no-proof query_aggregate_count_per_key entry point
QuantumExplorer May 14, 2026
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
877 changes: 877 additions & 0 deletions grovedb-query/src/aggregate_count.rs

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions grovedb-query/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
/// Error types for query operations.
pub mod error;

/// `AggregateCountOnRange` (ACOR) construction and validation. The
/// shape, detection, and leaf/carrier rule sets live here; the proof
/// generation and verification live in the `grovedb` crate.
mod aggregate_count;

/// Aggregate sum query for sum-up-to style queries.
pub mod aggregate_sum_query;

Expand Down
372 changes: 0 additions & 372 deletions grovedb-query/src/query.rs

Large diffs are not rendered by default.

177 changes: 169 additions & 8 deletions grovedb/src/operations/get/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,10 +668,16 @@ where {
/// skips proof generation, serialization, and verification entirely.
///
/// `path_query` must satisfy
/// [`PathQuery::validate_aggregate_count_on_range`] — a single
/// `AggregateCountOnRange(_)` item, no subqueries, no pagination, and an
/// inner range that isn't `Key`, `RangeFull`, or another
/// `AggregateCountOnRange`. Any other shape is rejected up front with
/// [`PathQuery::validate_leaf_aggregate_count_on_range`] — strictly the
/// **leaf** shape: a single `AggregateCountOnRange(_)` item, no
/// subqueries, no pagination, and an inner range that isn't `Key`,
/// `RangeFull`, or another `AggregateCountOnRange`. Carrier-shape
/// queries (outer `Keys` + `AggregateCountOnRange` subquery) are
/// rejected here because this entry point returns one `u64` and has
/// no way to surface per-outer-key counts; use
/// [`Self::prove_query`] +
/// [`Self::verify_aggregate_count_query_per_key`](GroveDb::verify_aggregate_count_query_per_key)
/// for those. Any other shape is rejected up front with
/// `Error::InvalidQuery` before any merk reads happen.
///
/// The subtree at `path_query.path` must be a `ProvableCountTree` or
Expand Down Expand Up @@ -701,12 +707,14 @@ where {

let mut cost = OperationCost::default();

// Up-front shape validation: same gate the prover and verifier use.
// Catches malformed ACOR queries (illegal inner range, ACOR-hidden-in-
// subquery, pagination, etc.) before any storage reads.
// Up-front shape validation. Strictly the leaf shape — this
// entry point returns a single `u64` and has no way to surface
// per-outer-key carrier results. Catches malformed leaf
// aggregate-count queries (illegal inner range, pagination,
// etc.) AND carrier-shape queries before any storage reads.
let inner_range = cost_return_on_error_no_add!(
cost,
path_query.validate_aggregate_count_on_range().cloned()
path_query.validate_leaf_aggregate_count_on_range().cloned()
);

let tx = TxRef::new(&self.db, transaction);
Expand Down Expand Up @@ -735,6 +743,159 @@ where {
Ok(count).wrap_with_cost(cost)
}

/// Executes an `AggregateCountOnRange` query in either the **leaf** or
/// **carrier** shape without generating a proof, returning one
/// `(outer_key, count)` pair per matched outer key.
///
/// This is the no-proof counterpart of
/// [`GroveDb::verify_aggregate_count_query_per_key`]: it performs the
/// same merk-level boundary walks the per-key verifier reconstructs
/// from a proof but skips proof generation, encoding, decoding, and
/// chain verification entirely.
///
/// For a **leaf** query the returned vector contains exactly one
/// entry whose key is an empty byte string and whose count is the
/// same `u64` [`Self::query_aggregate_count`] would have returned.
/// This matches the per-key verifier's leaf behavior, so callers
/// that always handle `Vec<(Vec<u8>, u64)>` don't need to branch on
/// the shape.
///
/// For a **carrier** query the outer items must be `Key(_)` /
/// `Range*(_)` and the `default_subquery_branch.subquery` must
/// validate as a leaf `AggregateCountOnRange`. The optional
/// `subquery_path` is followed exactly (single-key step per element)
/// before the count walk. The returned vector has one entry per
/// matched outer key in query-direction order (ascending lex when
/// `left_to_right = true`, descending otherwise). Outer-key
/// candidates that don't exist contribute no entry; outer-key
/// candidates whose leaf subtree is empty contribute `(key, 0)`.
///
/// `path_query` must satisfy
/// [`PathQuery::validate_aggregate_count_on_range`] in either
/// shape. Pagination is rejected. Each leaf subtree the walk
/// terminates in must be a `ProvableCountTree` or
/// `ProvableCountSumTree` — the merk-level walk rejects any other
/// tree type.
///
/// The returned counts are **not** independently verifiable —
/// callers are trusting their own merk read path. For verifiable
/// counts, use [`Self::prove_query`] +
/// [`GroveDb::verify_aggregate_count_query_per_key`].
pub fn query_aggregate_count_per_key(
&self,
path_query: &PathQuery,
transaction: TransactionArg,
grove_version: &GroveVersion,
) -> CostResult<Vec<(Vec<u8>, u64)>, Error> {
check_grovedb_v0_with_cost!(
"query_aggregate_count_per_key",
grove_version
.grovedb_versions
.operations
.query
.query_aggregate_count_on_range
);

let mut cost = OperationCost::default();

// Up-front shape validation: accept both leaf and carrier shapes.
// We classify by what the top-level query owns: a direct
// `AggregateCountOnRange` item means leaf; otherwise the
// dispatcher already confirmed a valid carrier subquery exists.
let inner_range = cost_return_on_error_no_add!(
cost,
path_query.validate_aggregate_count_on_range().cloned()
);

if path_query.query.query.aggregate_count_on_range().is_some() {
// Leaf shape: delegate to the existing single-`u64` entry
// point and wrap as a one-entry vector with an empty key.
let count = cost_return_on_error!(
&mut cost,
self.query_aggregate_count(path_query, transaction, grove_version)
);
return Ok(vec![(Vec::new(), count)]).wrap_with_cost(cost);
}

// Carrier shape: enumerate matched outer keys at the carrier
// subtree, then per match navigate `subquery_path` and run the
// merk-level count walk on the leaf.
let q = &path_query.query.query;
let outer_items = q.items.clone();
let subquery_path = q
.default_subquery_branch
.subquery_path
.clone()
.unwrap_or_default();
let left_to_right = q.left_to_right;

// Build a "shallow" path query that enumerates the carrier's
// outer items at `path_query.path` without descending into the
// subquery — we want just the matched outer keys, not the
// (unproven) results of the leaf aggregate-count.
let mut shallow_query = grovedb_query::Query::new_with_direction(left_to_right);
shallow_query.items = outer_items;
let shallow_pq = PathQuery::new_unsized(path_query.path.clone(), shallow_query);

let (matched, _skipped) = cost_return_on_error!(
&mut cost,
self.query_raw(
&shallow_pq,
true, // allow_cache
false, // decrease_limit_on_range_with_no_sub_elements
true, // error_if_intermediate_path_tree_not_present
QueryResultType::QueryKeyElementPairResultType,
transaction,
grove_version,
)
);

let key_elements = matched.to_key_elements();
let mut results: Vec<(Vec<u8>, u64)> = Vec::with_capacity(key_elements.len());
let tx = TxRef::new(&self.db, transaction);

for (key, element) in key_elements {
// Refuse non-tree matches: aggregate-count requires
// descending into the matched element to find the leaf
// count subtree.
if !element.is_any_tree() {
return Err(Error::InvalidQuery(
"carrier aggregate-count matched a non-tree element; outer items must \
resolve to tree elements",
))
.wrap_with_cost(cost);
}

// Build the path to the leaf count subtree:
// `path_query.path / outer_key / subquery_path...`.
let mut leaf_path_owned: Vec<Vec<u8>> = path_query.path.clone();
leaf_path_owned.push(key.clone());
leaf_path_owned.extend(subquery_path.iter().cloned());
let leaf_path: Vec<&[u8]> = leaf_path_owned.iter().map(|p| p.as_slice()).collect();

let leaf_subtree = cost_return_on_error!(
&mut cost,
self.open_transactional_merk_at_path(
SubtreePath::from(leaf_path.as_slice()),
tx.as_ref(),
None,
grove_version,
)
);

let count = cost_return_on_error!(
&mut cost,
leaf_subtree
.count_aggregate_on_range(&inner_range, grove_version)
.map_err(Error::MerkError)
);

results.push((key, count));
}

Ok(results).wrap_with_cost(cost)
}

/// Retrieves SumItem values that match a regular [`PathQuery`], returning
/// a `Vec<i64>` of the raw sum values and the number of skipped elements.
///
Expand Down
Loading
Loading