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
18 changes: 13 additions & 5 deletions docs/book/src/count-indexed-tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,11 +542,19 @@ which is what `indexed_count_top_k(path, k, descending = true, ..)` produces.
Ascending traversal is also supported for "smallest counts first" /
"items with the lowest counts in [a, b]" patterns.

### Lookup of count for a key

`indexed_count_range_aggregate(path, lo, hi, ..)` returns the count without returning the
value. It can be answered by reading the primary node's feature type
(which carries `count_value`). No secondary lookup needed; one Merk read.
### How many entries fall in a count band

`indexed_count_range_aggregate(path, lo, hi, ..)` answers **how many
entries have a `count_value` in `[lo, hi]`** — a bucket population, in
which each matching entry contributes 1. It is *not* the total of those
entries' counts: over counts `[3, 1, 5]`, the band `[2, 10]` selects the
`3` and the `5` and answers `2`, not `8`. If you want the total, use
`indexed_count_range(path, lo, hi, ..)` to list the selected entries
with their counts and sum them caller-side.

The walk folds each fully-contained subtree's stored aggregate in one
step and descends only along the two range boundaries, so the cost is
`O(log n)` with no term in the number of matching entries.

### Subqueries

Expand Down
38 changes: 30 additions & 8 deletions grovedb-query/src/axis_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,21 +177,43 @@ pub enum AxisTraversal {
/// The original (primary) key whose rank is requested.
key: Vec<u8>,
},
/// A single aggregate over every entry whose aggregate value lies in
/// the inclusive `[lo, hi]` range. Count and Sum axes only — the
/// Avg axis has no meaningful sum-of-averages.
/// `[lo, hi]` selects the entries; the axis's own secondary
/// aggregate over exactly those entries is the answer. Count and
/// Sum axes only — the Avg axis has no meaningful
/// average-of-averages.
///
/// **The aggregation differs per axis, and the count axis is the
/// one that reads wrong.** Each secondary aggregates the way its
/// own tree type does, so:
///
/// * **Sum axis** — the answer is the TOTAL of the selected
/// entries' sums. `RangeAggregate { lo: 0, hi: 100 }` over sums
/// `[40, -10, 25]` selects `40` and `25` and answers `65`.
/// * **Count axis** — the answer is HOW MANY entries were
/// selected, each contributing 1. It is a bucket population, NOT
/// the total of their counts. `RangeAggregate { lo: 2, hi: 10 }`
/// over counts `[3, 1, 5]` selects the `3` and the `5` and
/// answers **2**, not 8.
///
/// Reach for the count axis here to ask "how many entries fall in
/// this band?". There is no traversal that totals the counts in a
/// band — [`Self::Bounded`] lists the selected entries with their
/// values, and the caller sums them.
///
/// **Cost** — `O(log n)` in every case. The walk classifies each
/// subtree as fully Contained, Disjoint, or Partial and folds a
/// Contained subtree's stored aggregate in one step, descending
/// only along the two range boundaries. **No term in the number of
/// matched entries** — summing a million in-range entries costs
/// what summing one costs, which is what makes this preferable to
/// [`Self::Bounded`] whenever only the total is wanted.
/// matched entries** — aggregating a million in-range entries costs
/// what aggregating one costs, which is what makes this preferable
/// to [`Self::Bounded`] whenever only the total is wanted.
RangeAggregate {
/// Inclusive lower bound on the aggregate.
/// Inclusive lower bound on the entry's own axis VALUE (its
/// count on the count axis, its sum on the sum axis) — not on
/// the aggregate this traversal returns.
lo: i128,
/// Inclusive upper bound on the aggregate.
/// Inclusive upper bound on the entry's own axis value. See
/// [`Self::RangeAggregate::lo`].
hi: i128,
},
}
Expand Down
24 changes: 24 additions & 0 deletions grovedb-version/src/version/grovedb_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,30 @@ pub struct GroveDBOperationsProofVersions {
/// non-empty **Merk** trees have required the child hash since V3 and
/// stay bound at every version.
pub terminal_non_merk_tree_child_hash: FeatureVersion,
/// Whether the V1 proof envelope carries **axis-ordered descents**
/// into indexed trees (`ProofBytes::IndexedTreeAxisDescent`),
/// serving `PathQuery`s whose query node holds `ReadMode::Axis`.
///
/// - `0` (V1..V3): the prover refuses axis-shaped queries with
/// `NotSupported` and the verifier rejects any proof/query pair
/// involving one. These versions also reject the version-2
/// `Query` wire encoding outright, so the slot's `0` value is the
/// in-process mirror of that fail-closed decode.
/// - `1` (V4+): the prover emits the axis-descent layer — a proof
/// over the queried per-axis **secondary** in place of the primary
/// descent — and the verifier accepts it, recomputing the
/// secondary-root attestation from the carried secondary proof
/// (never trusting 32 raw bytes) before performing the same
/// `combine_hash_three` parent binding as the other indexed
/// shapes.
///
/// Gated because it adds an acceptance rule to the live V1
/// envelope: an upgraded verifier accepts proof shapes a released
/// one rejects. Prover and verifier read the same slot, so there is
/// no version at which they disagree about whether the shape
/// exists. Indexed trees themselves cannot exist in pre-V4
/// production data, so `0` never rejects anything real.
pub axis_descent_in_v1_envelope: FeatureVersion,
}

#[derive(Clone, Debug, Default)]
Expand Down
1 change: 1 addition & 0 deletions grovedb-version/src/version/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ pub const GROVE_V1: GroveVersion = GroveVersion {
verify_query_with_chained_path_queries: 0,
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 0,
axis_descent_in_v1_envelope: 0,
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
1 change: 1 addition & 0 deletions grovedb-version/src/version/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ pub const GROVE_V2: GroveVersion = GroveVersion {
verify_query_with_chained_path_queries: 0,
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 0,
axis_descent_in_v1_envelope: 0,
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
1 change: 1 addition & 0 deletions grovedb-version/src/version/v3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ pub const GROVE_V3: GroveVersion = GroveVersion {
verify_query_with_chained_path_queries: 0,
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 0,
axis_descent_in_v1_envelope: 0,
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
9 changes: 9 additions & 0 deletions grovedb-version/src/version/v4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
//! flips a rejected/accepted outcome and because deriving the state root
//! costs the prover extra storage reads and hash calls.
//!
//! - `proof.axis_descent_in_v1_envelope: 1` — the V1 proof envelope carries
//! axis-ordered descents into indexed trees
//! (`ProofBytes::IndexedTreeAxisDescent`): a proof over the queried
//! per-axis secondary in place of the primary descent, with the
//! secondary-root attestation recomputed by the verifier rather than
//! supplied raw. V1..V3 refuse the shape on both sides. Gated because it
//! adds an acceptance rule to the live V1 envelope.
//!
//! - `path_query_methods.unified_read_mode: 1` — `PathQuery` read modes
//! (axis-ordered and sum-budget reads carried in `Query::read_mode`) are
//! served by the unified dispatch (`run_path_query`, and the unified
Expand Down Expand Up @@ -230,6 +238,7 @@ pub const GROVE_V4: GroveVersion = GroveVersion {
verify_query_with_chained_path_queries: 0,
verify_query_get_parent_tree_info_with_options: 0,
terminal_non_merk_tree_child_hash: 1, // bind terminal non-Merk tree element bytes to the parent value_hash
axis_descent_in_v1_envelope: 1, // axis-ordered descents in the V1 envelope (ReadMode::Axis)
},
average_case: GroveDBOperationsAverageCaseVersions {
add_average_case_get_merk_at_path: 0,
Expand Down
51 changes: 32 additions & 19 deletions grovedb/src/operations/get/run_path_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,28 +240,41 @@ impl GroveDb {
))
.wrap_with_cost(cost);
};
// Mirror the branched proof's absence slots: a branch
// key missing at the branching level yields None
// rather than an error, so partially-populated
// branch sets read the same way they prove.
let present = cost_return_on_error!(
&mut cost,
self.get_raw_optional(
SubtreePath::from(prefix_refs.as_slice()),
branch_key,
transaction,
grove_version,
)
);
if present.is_none() {
// Mirror the branched proof's absence slots: a
// branch key — or any suffix segment under it —
// missing yields None rather than an error, so
// partially-populated branch sets read exactly the
// way they prove (the proof authenticates the
// absence at whichever level the chain breaks).
//
// Seeded from the hoisted `prefix_refs`: the prefix
// is built once, and only the per-branch descent
// pushes onto its own copy.
let mut resolved: Vec<&[u8]> = prefix_refs.clone();
let mut chain_broken = false;
for segment in std::iter::once(branch_key.as_slice())
.chain(suffix.iter().map(|segment| segment.as_slice()))
{
let present = cost_return_on_error!(
&mut cost,
self.get_raw_optional(
SubtreePath::from(resolved.as_slice()),
segment,
transaction,
grove_version,
)
);
if present.is_none() {
chain_broken = true;
break;
}
resolved.push(segment);
}
if chain_broken {
branches.push((branch_key.clone(), None));
continue;
}
let mut full_path: Vec<&[u8]> =
Vec::with_capacity(path_query.path.len() + 1 + suffix.len());
full_path.extend(path_query.path.iter().map(|segment| segment.as_slice()));
full_path.push(branch_key.as_slice());
full_path.extend(suffix.iter().map(|segment| segment.as_slice()));
let full_path = resolved;
let run = cost_return_on_error!(
&mut cost,
self.run_axis_read(full_path.as_slice(), axis, transaction, grove_version)
Expand Down
Loading
Loading