Skip to content

feat(grovedb,query): allow SizedQuery::limit on carrier AggregateCountOnRange#664

Merged
QuantumExplorer merged 3 commits into
developfrom
claude/modest-hofstadter-493d92
May 15, 2026
Merged

feat(grovedb,query): allow SizedQuery::limit on carrier AggregateCountOnRange#664
QuantumExplorer merged 3 commits into
developfrom
claude/modest-hofstadter-493d92

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 15, 2026

Issue being fixed or feature implemented

Follow-up to #663 (allow AggregateCountOnRange as carrier subquery). That change deliberately accepted Range* outer items on carriers, but the SizedQuery::limit / offset constraint inherited from leaf-ACOR was still applied uniformly — which makes carrier ACOR with Range* outer items practically unusable above a certain dataset size (the proof grows linearly in the number of in-range outer keys, with no way for the caller to cap it).

The concrete upstream use case from dashpay/platform is a "Q8 with outer Range" GROUP BY shape:

SELECT brand, COUNT(*) FROM widgets
WHERE brand > 'brand_050' AND color > 'color_00000500'
GROUP BY brand
LIMIT 20

which maps to a carrier ACOR: outer item RangeAfter("brand_050"..), subquery_path = ["color"], subquery AggregateCountOnRange(RangeAfter("color_00000500"..)), capped at 20 outer matches via SizedQuery::limit. Today this PathQuery is rejected at validation with InvalidQuery("AggregateCountOnRange queries may not set SizedQuery::limit") even though the semantic is well-defined.

What was done?

Split the SizedQuery-level size-constraint check by shape:

  • Leaf (single AggregateCountOnRange(_) item, no subqueries): keep rejecting both limit and offset. A leaf returns a single u64; pagination would silently change the answer.
  • Carrier (outer Key/Range* items routing to a leaf-ACOR subquery): accept SizedQuery::limit — it caps the number of outer-key matches the carrier walks. Each matched outer key still produces a complete leaf-ACOR u64 (the inner range is not capped). SizedQuery::offset is still rejected on carriers; the error message documents that skipping outer matches changes which (outer_key, u64) pairs end up in the proof and the use case for that hasn't been designed yet.

Changes:

  • grovedb/src/query/mod.rs: SizedQuery::check_aggregate_count_size_constraints is replaced by check_leaf_aggregate_count_size_constraints (strict) and check_carrier_aggregate_count_size_constraints (offset-only). validate_aggregate_count_on_range classifies the inner query first and then dispatches the appropriate check. validate_leaf_aggregate_count_on_range still uses the leaf-strict check.
  • grovedb/src/operations/proof/aggregate_count/{helpers,per_key}.rs: threaded SizedQuery::limit through to the carrier verifier so the merk walker's execute_proof(... limit, ...) stops at the same boundary the prover's truncation produced. Without this the verifier rejects truncated proofs with "Proof is missing data for query."
  • grovedb/src/operations/get/query.rs: query_aggregate_count_per_key (no-proof path) propagates SizedQuery::limit to the shallow outer-walk query.

The merk-level hash composition is unchanged; the prover's existing Merk::prove_unchecked_query_items(query, limit, ...) call already truncates correctly when the carrier merk's limit arrives non-None.

How Has This Been Tested?

cargo test --workspace --features 'minimal verify' passes (all 1543 grovedb tests + the rest of the workspace).

Replaced carrier_pagination_is_rejected_at_entry with carrier_aggregate_count_rejects_offset (limit is no longer rejected; offset still is). Added five new tests in grovedb/src/tests/aggregate_count_query_tests.rs:

  • leaf_aggregate_count_still_rejects_limit — the leaf strict path is byte-identical to before this PR.
  • carrier_keys_outer_with_limit_caps_results — outer Keys + limit=2 against 4 brands returns exactly 2 (brand, u64) pairs in lex-asc order, proof verifies.
  • carrier_range_outer_with_limit_caps_results — outer RangeAfter + limit=2 against 4 in-range brands behaves identically (the upstream Q8-with-Range shape).
  • carrier_range_outer_with_limit_zero_returns_no_resultslimit=0 walks zero outer matches; exercised via the no-proof per-key API because the existing prove_query_non_serialized entry-point gate has a long-standing unconditional rejection of limit == 0 for proved queries ("proved path queries can not be for limit 0"), which is outside the scope of this PR.
  • carrier_range_outer_with_limit_exceeding_available_walks_alllimit=100 against 2 in-range brands returns all 2, matching the no-proof per-key result byte-for-byte.

Also added two SizedQuery-level unit tests in grovedb/src/query/mod.rs: sized_query_validate_leaf_acor_rejects_limit_and_offset and sized_query_validate_carrier_acor_accepts_limit_rejects_offset.

cargo clippy -p grovedb --lib --features minimal,verify -- -D warnings is clean. cargo fmt --check is clean.

Breaking Changes

None. Wire format is unchanged — SizedQuery::limit was already part of the serialized PathQuery and just rejected client-side at validation. Any previously-valid query keeps its byte-identical behavior. The relaxed rule applies unconditionally (no per-feature grove-version gate added); if maintainers want a version gate for the new carrier-with-limit acceptance so older verifiers continue to reject these queries, happy to add one — flagging here per the original spec.

Out of scope (intentionally deferred):

  • SizedQuery::offset on carriers — the rationale is in the validator's error message; adding it later is a separate, well-scoped change.
  • Limit on leaf ACOR queries (still rejected — a leaf returns one u64 and pagination doesn't apply).
  • The drive-side wire-up in dashpay/platform — a separate PR there consumes the new grovedb behavior.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

Summary by CodeRabbit

  • Bug Fixes

    • Fixed aggregate count proof verification to properly respect limit boundaries for carrier-shaped queries.
  • Enhancements

    • Carrier-shaped aggregate count queries now support pagination limits to cap results.
    • Improved validation to distinguish between leaf and carrier query shapes with appropriate constraint rules.
    • Expanded test coverage for aggregate count query pagination behavior.

Review Change Stack

…tOnRange

Split the SizedQuery-level size-constraint check by shape so the leaf
form still rejects pagination (a leaf returns a single u64; pagination
would silently change the answer) but the carrier form accepts
`SizedQuery::limit` to cap the number of outer-key matches the walk
returns. Each matched outer key still produces a complete leaf-ACOR
u64 — the inner range isn't capped. `SizedQuery::offset` remains
rejected on carriers pending a separate design pass on outer-dimension
pagination semantics.

Threaded the limit through the carrier verifier (`execute_carrier_layer_proof`
now takes `outer_limit` so its merk walker stops at the same boundary
the prover's truncation produced) and through the no-proof
`query_aggregate_count_per_key` (shallow outer query carries the limit).

Concrete upstream use case: dashpay/platform "Q8 with outer Range"
(SELECT ... GROUP BY brand WHERE brand > X AND color > Y LIMIT 20),
which maps to a carrier ACOR with `SizedQuery::limit = Some(20)`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Warning

Rate limit exceeded

@QuantumExplorer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 35 minutes and 26 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7500b67-50de-4f6c-9a88-3403f4bff752

📥 Commits

Reviewing files that changed from the base of the PR and between 52f44bb and 52be0da.

📒 Files selected for processing (3)
  • grovedb/src/operations/get/query.rs
  • grovedb/src/operations/proof/aggregate_count/classification.rs
  • grovedb/src/query/mod.rs
📝 Walkthrough

Walkthrough

This PR implements sized query limit pagination for carrier-shaped AggregateCountOnRange queries by distinguishing validation constraints (leaf vs carrier), threading limits through proof verification, constructing queries with those limits, and validating the behavior end-to-end.

Changes

Carrier limit pagination

Layer / File(s) Summary
Query validation leaf vs carrier constraints
grovedb/src/query/mod.rs, grovedb/src/tests/*
SizedQuery::validate_aggregate_count_on_range now classifies ACOR shape (leaf or carrier) and applies different pagination rules: leaf queries reject both limit and offset; carrier queries allow limit but reject offset. New public validate_leaf_aggregate_count_on_range enforces leaf constraints upfront. Unit tests verify both constraint branches with shape-specific error messages.
Carrier outer limit threading through proof verification
grovedb/src/operations/proof/aggregate_count/helpers.rs, grovedb/src/operations/proof/aggregate_count/per_key.rs
execute_carrier_layer_proof signature now accepts outer_limit: Option<u16> parameter. Verification chain threads the limit from verify_v1_per_keyverify_v1_carrier_layerexecute_carrier_layer_proofexecute_proof, aligning verifier truncation with prover truncation at the limit boundary.
Shallow query construction with sized limits
grovedb/src/operations/get/query.rs
SizedQuery import added. query_aggregate_count_per_key now constructs the shallow carrier-key enumeration PathQuery via PathQuery::new with SizedQuery::new, propagating the original query's limit into the shallow walk.
End-to-end pagination test coverage
grovedb/src/tests/aggregate_count_query_tests.rs
New tests validate carrier pagination: offset rejection at per-key verification, limit acceptance and result capping for both Keys and Range* outer queries, limit=0 producing empty results, and oversized limits returning all matches with agreement between proof and no-proof walkers. Leaf tests confirm that limit is still rejected at both legacy and per-key entry points.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • dashpay/grovedb#663: Introduces the carrier-shaped AggregateCountOnRange classification and proof verification path; this PR threads the outer_limit through that path and updates leaf vs carrier validation semantics to control limit/offset usage by shape.

Poem

🐰 Carriers now limit their outer walk with grace,
Leaves reject pagination in their leaf-wise place,
Proof and verifier march in sync side-by-side,
Each shallow query carries forward its guide!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: enabling SizedQuery::limit on carrier-shaped AggregateCountOnRange queries, which is the central feature across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/modest-hofstadter-493d92

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

❌ Patch coverage is 98.55072% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.77%. Comparing base (8755418) to head (52be0da).

Files with missing lines Patch % Lines
grovedb/src/query/mod.rs 98.36% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #664   +/-   ##
========================================
  Coverage    90.76%   90.77%           
========================================
  Files          189      189           
  Lines        56407    56466   +59     
========================================
+ Hits         51196    51255   +59     
  Misses        5211     5211           
Components Coverage Δ
grovedb-core 88.52% <98.55%> (+0.02%) ⬆️
merk 92.32% <ø> (ø)
storage 86.36% <ø> (ø)
commitment-tree 96.43% <ø> (ø)
mmr 96.76% <ø> (ø)
bulk-append-tree 89.14% <ø> (ø)
element 95.75% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Add a direct unit test for the `query_validation_error_to_static_str`
helper that exercises both the normal `InvalidOperation` projection
and the defensive catch-all for other QueryError variants. The
catch-all is unreachable from real `Query::validate_aggregate_count_on_range`
results (its comment explicitly says so) but is kept as a defensive
guard against future bugs; this test pins the projection behavior so
the guard isn't silently broken later.

Lifts patch coverage on this PR from 98.36% to 100%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed

Both spots still said "pagination is rejected" but this PR makes
carrier `SizedQuery::limit` valid and propagates it through the
no-proof path and the proof verifier. The public `query_aggregate_count_per_key`
doc and the internal `classify_aggregate_count_path_query` doc both
now describe the shape-specific rules: leaf still rejects limit and
offset; carrier accepts limit and rejects offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer merged commit 7a64938 into develop May 15, 2026
10 of 11 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/modest-hofstadter-493d92 branch May 15, 2026 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant