Skip to content

feat(platform): getDocuments v1 — SQL-shaped select + count surface (1/3)#3633

Open
QuantumExplorer wants to merge 50 commits into
v3.1-devfrom
feat/get-documents-v1-select-group-by
Open

feat(platform): getDocuments v1 — SQL-shaped select + count surface (1/3)#3633
QuantumExplorer wants to merge 50 commits into
v3.1-devfrom
feat/get-documents-v1-select-group-by

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 12, 2026

Issue being fixed or feature implemented

PR 1 of 3 in the v1 unification track. Wire-format change adding a SQL-shaped `GetDocumentsRequestV1` that unifies `getDocuments` and `getDocumentsCount` under a single request type with typed `select`, `group_by`, and `having` clauses. After all three PRs land and a deprecation cycle, callers can drop their dual-endpoint plumbing in favor of one unified surface.

This PR is pure rewiring — no new server-side execution capability. Every supported request shape translates to an existing v0 (`query_documents_v0`) or v0-count (`query_documents_count_v0`) handler invocation and produces byte-identical proof bytes / response data. The SDK-side wiring (PR 2) and FFI/wasm/Swift surface (PR 3) ship separately.

What was done?

Wire format (`platform.proto`):

  • `GetDocumentsRequestV1` joins V0 in the existing `oneof version`.
  • `Select { DOCUMENTS, COUNT }` projection enum.
  • `repeated string group_by` for explicit grouping.
  • `bytes having` reserved on the wire (always rejected in Phase 1).
  • `GetDocumentsResponseV1` with `oneof result` carrying `Documents`, `CountResults` (referenced from `GetDocumentsCountResponse`), or `Proof`.

Dispatcher (`drive-abci/src/query/document_query/v1/mod.rs`):

  • `query_documents_v1` validates the SQL-shape combination, decides which v0 handler to forward to, and re-wraps the v0 response into the v1 envelope.
  • For `SELECT COUNT, group_by=[]` with `In + no range + no prove`, v0-count's PerInValue returns entries; v1 sums them server-side into a single aggregate before emitting on the wire.
  • Every unsupported shape returns `QuerySyntaxError::Unsupported("… is not yet implemented")` so callers can detect un-wired capabilities without prose-parsing the error.

Rejection table (the only "new" logic in v1):

Request shape Rejection
Any non-empty `having` `HAVING clause is not yet implemented`
`SELECT DOCUMENTS` + non-empty `group_by` `GROUP BY with SELECT DOCUMENTS is not yet implemented`
`SELECT COUNT` + `group_by=[f]` where f isn't an `In`/range field `GROUP BY on field 'f' which is not constrained by an `In` or range where clause is not yet implemented`
`SELECT COUNT` + `group_by.len() > 2` `GROUP BY with more than two fields is not yet implemented`
`SELECT COUNT` + 2-field `group_by` outside `(In, range)` shape `two-field GROUP BY outside the `(In, range)` compound shape is not yet implemented`
`SELECT COUNT` + `start_after`/`start_at` `start_after / start_at with SELECT COUNT is not yet implemented`

Platform-version: `document_query.max_version` bumped to 1; default stays at 0. v0 callers unchanged.

Supported routing:

v1 request Forwards to
`SELECT DOCUMENTS, group_by=[]` v0 documents handler
`SELECT COUNT, group_by=[]` v0-count, aggregate (sums PerInValue entries server-side for In + no range)
`SELECT COUNT, group_by=[in_field]` v0-count's PerInValue (entries)
`SELECT COUNT, group_by=[range_field]` v0-count's RangeDistinct (`return_distinct_counts_in_range=true`)
`SELECT COUNT, group_by=[in_field, range_field]` v0-count's existing compound shape

How Has This Been Tested?

12 new tests in `query::document_query::v1::tests`:

  • 7 rejection-table unit tests covering every `Unsupported` arm.
  • 4 routing-decision unit tests pinning each supported shape's routing decision.
  • 2 end-to-end parity tests: `SELECT DOCUMENTS` returns the same docs as v0, and the HAVING rejection surfaces cleanly in the response via `QueryValidationResult.errors`.

Existing v0 (27 tests) and v0-count (9 tests) suites unaffected — both continue to pass unchanged.

Breaking Changes

None. v0 callers continue to work; v1 callers opt in via the request's `version` oneof. The dual-endpoint shape stays alive during the deprecation cycle.

Out of scope (tracked as follow-ups)

  • PR 2: SDK `DocumentQuery` builder + `Fetch` wiring for v1.
  • PR 3: FFI / wasm / Swift unified entry points threading the new knobs.
  • Phase 2 capabilities (each unblocks a corresponding rejection arm — same wire format, just adds server-side execution):
    • GROUP BY on a non-where-constrained field.
    • Multi-field GROUP BY beyond the existing compound `(In, range)` shape.
    • HAVING.
  • Non-Rust client regen: this commit doesn't run the docker-based `scripts/build.sh` for java / nodejs / python / objc / web. Will land as a separate commit once the proto is reviewer-approved.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
    • PRs 2 + 3 depend on this; will reference once opened.

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Unified getDocuments v1: single request surface for documents and counts (select/group_by/having), with pagination and proof; generated clients/bindings now expose V1 request/response variants across languages.
    • SDK: proof verification and count/split-count support updated for the unified v1 flow.
  • Breaking Changes

    • Removed getDocumentsCount RPC — use getDocuments v1 with select=COUNT.
    • SDK/FFI/WASM: groupBy replaces returnDistinctCountsInRange; native FFI signature and DocumentQuery shape changed.

…nt surface

PR 1 of 3 in the v1 unification track. Adds the wire format +
server dispatcher that unifies \`getDocuments\` and
\`getDocumentsCount\` under a single SQL-shaped request type with
\`select\`, \`group_by\`, and \`having\` clauses. **Pure rewiring** —
no new server-side capability ships here; every supported request
shape translates to an existing v0 (\`query_documents_v0\`) or
v0-count (\`query_documents_count_v0\`) handler invocation and
produces byte-identical proof bytes / response data. The v1
surface just makes the SQL semantics explicit on the wire.

**Wire format** (\`platform.proto\`):
- \`GetDocumentsRequestV1\` joins V0 in the existing oneof.
- \`Select { DOCUMENTS, COUNT }\` projection enum.
- \`repeated string group_by\` for explicit grouping.
- \`bytes having\` reserved for future capability.
- \`GetDocumentsResponseV1\` with \`oneof result\` carrying
  \`Documents\`, \`CountResults\` (referenced from
  \`GetDocumentsCountResponse\`), or \`Proof\`.

**Dispatcher rejection table** — every request shape outside the
v0 / v0-count capability surface returns
\`QuerySyntaxError::Unsupported("… is not yet implemented")\`:

- HAVING with any payload (always rejected, no exceptions).
- GROUP BY with \`SELECT DOCUMENTS\` (no aggregate to group on).
- GROUP BY on a field that's not constrained by an \`In\` or range
  where clause (would need a new server primitive — walking a
  property-name \`ProvableCountTree\` without a covering prefix).
- GROUP BY with more than two fields (Phase 1 cap).
- Two-field GROUP BY outside the existing \`(In, range)\` compound
  shape (the server emits \`(in_key, key)\` entries in that order;
  other orderings would need a new merk walk).
- \`start_after\` / \`start_at\` with \`SELECT COUNT\` (no concept
  of "skip past this aggregate" — paginate by narrowing the range).

The wording "… is not yet implemented" is deliberate: it signals
future capability, not malformed requests. Clients can keep
these request shapes in code and they'll start working once the
capability lands without a wire-format change.

**Supported routing**:
- \`SELECT DOCUMENTS, group_by=[]\` → v0 documents handler.
- \`SELECT COUNT, group_by=[]\` → v0-count, aggregate; for
  \`In + no range + no prove\` mode, v1 sums the PerInValue entries
  server-side into a single aggregate.
- \`SELECT COUNT, group_by=[f]\` where f matches the In or range
  field → v0-count's PerInValue / RangeDistinct (entries).
- \`SELECT COUNT, group_by=[in_field, range_field]\` → v0-count's
  existing compound \`(In + range + distinct)\` shape.

**Platform-version**: \`document_query.max_version\` bumped to 1
(default still 0; v1 callers opt in via the request's \`version\`
oneof). v0 callers continue working unchanged.

**Tests** (12 in \`query::document_query::v1::tests\`):
- 7 rejection-table unit tests covering every \`Unsupported\` arm.
- 4 routing-decision unit tests pinning each supported shape.
- 2 end-to-end parity tests: \`SELECT DOCUMENTS\` returns the
  same docs as v0, and HAVING rejection surfaces cleanly in
  the response (\`Unsupported("HAVING clause is not yet
  implemented")\`).

Existing v0 (27 tests) and v0-count (9 tests) suites unaffected.

**Out of scope** (follow-up PRs):
- PR 2: SDK \`DocumentQuery\` builder + \`Fetch\` wiring for v1.
- PR 3: FFI / wasm / Swift unified entry points.
- Phase 2: explicit GROUP BY on non-where-constrained fields,
  multi-field GROUP BY, HAVING.

**Non-Rust client regeneration**: this commit doesn't regenerate
java / nodejs / python / objc / web clients — the docker-based
\`scripts/build.sh\` produces those. Will land as a separate
commit once the proto is reviewer-approved.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR removes the standalone getDocumentsCount RPC and migrates counting into GetDocuments v1. It adds GetDocumentsRequestV1/GetDocumentsResponseV1 (select/group_by/having/start/limit/prove + result oneof counts/documents/proof), updates generated clients and service descriptors, deletes legacy count modules/handlers, adds a v1 document-query handler that routes documents vs counts, and updates SDK/FFI/WASM and proof verification to the unified DocumentQuery/Select flow.

Changes

Unified GetDocuments v1 (single cohort)

Layer / File(s) Summary
Proto & codegen configuration
packages/dapi-grpc/protos/platform/v0/platform.proto, packages/dapi-grpc/build.rs
Removed getDocumentsCount RPC; added GetDocumentsRequestV1/GetDocumentsResponseV1 (select/group_by/having/start/limit/prove + result oneof with counts/documents/proof). build.rs updated to emit grpc_versions(1) for selected messages and keep others at grpc_versions(0).
Generated JS/TS/Node bindings
packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js, packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js, packages/dapi-grpc/clients/platform/v0/web/platform_pb.js, packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts, packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js
Added GetDocumentsRequestV1/GetDocumentsResponseV1 message classes/types; expanded version oneofs to include V1; added full encode/decode/verify/fromObject/toObject/toJSON and enums (Select, StartCase, ResultCase).
Generated Objective‑C / Java / Python bindings & service descriptors
packages/dapi-grpc/clients/platform/v0/objective-c/*, packages/dapi-grpc/clients/platform/v0/java/*/PlatformGrpc.java, packages/dapi-grpc/clients/platform/v0/python/platform_pb2_grpc.py, packages/dapi-grpc/clients/platform/v0/web/platform_pb_service.{js,d.ts}
Removed getDocumentsCount method from generated service clients/headers/stubs; added v1 message symbols and accessors; adjusted method ID ordering and service descriptors; inserted doc comments pointing callers to getDocuments v1 select=COUNT.
Server RPC wiring & transport
packages/rs-dapi/src/services/platform_service/mod.rs, packages/rs-dapi-client/src/transport/grpc.rs, packages/rs-drive-abci/src/query/service.rs
Deleted get_documents_count handler and transport registration/imports; service dispatch and transport mappings now skip the removed RPC.
Drive/ABCI query layer: remove old count module, add v1 handler
packages/rs-drive-abci/src/query/document_count_query/* (deleted), packages/rs-drive-abci/src/query/mod.rs, packages/rs-drive-abci/src/query/document_query/mod.rs, packages/rs-drive-abci/src/query/document_query/v1/mod.rs
Deleted separate document_count_query module and v0 count implementation; extended query_documents to dispatch on request version and added query_documents_v1 which validates select/group_by/having, decodes CBOR where/order_by, routes to documents (wraps v0 result) or to count dispatch (constructs DocumentCountRequest, maps per-key vs aggregate counts, wraps proofs). Includes unit and ported integration tests.
SDK: DocumentQuery changes & count proof wiring
packages/rs-sdk/src/platform/documents/document_query.rs, packages/rs-sdk/src/platform/documents/document_count.rs, packages/rs-sdk/src/platform/documents/mod.rs, packages/rs-sdk/src/platform/documents/document_count_query.rs (removed)
DocumentQuery extended with select, group_by, having and builders; TryFrom<DocumentQuery> now emits GetDocumentsRequestV1. Removed legacy DocumentCountQuery. Added FromProof<DocumentQuery> impls and Fetch wiring for DocumentCount and DocumentSplitCounts.
Proof verifier & response type migration
packages/rs-drive-proof-verifier/src/proof/document_count.rs, packages/rs-drive-proof-verifier/src/proof/document_split_count.rs
Changed verifier FromProof associated Response types from GetDocumentsCountResponseGetDocumentsResponse to match unified wire shape.
FFI / WASM / tests / mocks / callers
packages/rs-sdk-ffi/src/document/queries/count.rs, packages/wasm-sdk/src/queries/document.rs, packages/rs-sdk/tests/fetch/document_count.rs, packages/rs-sdk/src/mock/sdk.rs, various callsites (packages/rs-platform-wallet/*, packages/rs-sdk/src/platform/*, packages/wasm-sdk/src/dpns.rs)
FFI dash_sdk_document_count signature changed (removed distinct-range boolean, added group_by_json); builds DocumentQuery with Select::Count and parsed group_by. WASM API now exposes groupBy?: string[]. Tests and mocks updated to use DocumentQuery.with_select(Select::Count) / .with_group_by(...). Caller sites explicitly set select=Documents and empty group_by/having where appropriate.
Platform version & feature bounds
packages/rs-platform-version/src/version/drive_abci_versions/*, packages/rs-platform-version/src/version/mocks/v2_test.rs
Removed document_count_query and document_split_count_query feature bounds; bumped document_query bounds in v1 to accept max_version/default_current_version = 1 and updated mocks.
Minor formatting & small fixes
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs, packages/rs-sdk/src/platform/query.rs, packages/rs-drive/src/error/query.rs, packages/rs-drive/*dispatchers*
Cosmetic reformat and replacement of an unimplemented! panic with a configuration Error::Config for non-proven queries; adjusted some error variants to use owned String.
sequenceDiagram
    autonumber
    participant Client as Client (SDK/JS)
    participant Server as Platform gRPC Server
    participant Drive as Drive executor
    participant Verifier as Proof Verifier

    Client->>Server: Send GetDocumentsRequestV1 (select=DOCUMENTS|COUNT, group_by?, having?, start/limit, prove?)
    alt select = DOCUMENTS
        Server->>Drive: Call documents executor (v0 path) / translate start/limit
        Drive-->>Server: Documents or proof
    else select = COUNT
        Server->>Drive: Build DocumentCountRequest / execute count path
        Drive-->>Server: Counts or proof (per-key or aggregate)
    end
    alt proof present
        Server->>Verifier: Verify GetDocumentsResponse (proof)
        Verifier-->>Server: Verified result
    end
    Server-->>Client: Return GetDocumentsResponseV1 (documents | counts | proof + metadata)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Suggested reviewers

  • shumkov

"🐰
I hop through code and fields anew,
One query now counts and fetches too,
v1 speaks SQL-shaped, tidy and bright,
Docs and counts bundled just right."

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/get-documents-v1-select-group-by

@github-actions github-actions Bot added this to the v3.1.0 milestone May 12, 2026
QuantumExplorer and others added 5 commits May 12, 2026 17:32
Runs the docker-based `scripts/build.sh` codegen to produce the
java / nodejs / python / objc / web client bindings for the
v1 wire format introduced in the preceding commit
(`feat(platform): GetDocumentsRequestV1 …`).

Regenerated files:
- `packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h`
- `packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m`
- `packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py`
- `packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts`
- `packages/dapi-grpc/clients/platform/v0/web/platform_pb.js`
- `packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js`
- `packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js`
- `packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js`

The last one (drive's nodejs bindings) regenerates because the
drive proto imports platform message types via the shared protos
path; any platform proto change ripples through.

The Rust client is generated by `build.rs` at compile time and
needs no checked-in update.

The Java client doesn't regenerate here because no fixtures or
in-repo tests exercise it; downstream consumers re-run their own
codegen against the proto.
Two consolidation changes on top of the prior v1 commit.

**Response shape**: `GetDocumentsResponseV1` now mirrors every
other `Get*Response` in the proto — a two-variant outer `oneof`
with `result.data` at position 1 and `proof` at position 2. The
non-proof result wraps an inner `oneof` (`ResultData.variant`)
that switches between `Documents` (for `select=DOCUMENTS`) and
`CountResults` (for `select=COUNT`). Keeps the canonical
result-or-proof shape callers expect without flattening to a
three-variant outer oneof.

**`getDocumentsCount` endpoint removed entirely**:
- `rpc getDocumentsCount` deleted from the proto service.
- `GetDocumentsCountRequest` / `GetDocumentsCountResponse`
  messages deleted from the proto.
- `CountResults` / `CountEntry` / `CountEntries` types moved
  into `GetDocumentsResponseV1` (the new and only home for the
  count wire shape).
- `query_documents_count` + `query_documents_count_v0` handlers
  deleted from drive-abci; v1 dispatches to drive's
  `execute_document_count_request` directly without delegating
  through the now-gone v0-count layer.
- `query/document_count_query/` directory removed from drive-abci.
- `document_count_query` + `document_split_count_query` fields
  removed from `DriveAbciQueryVersions` (rs-platform-version).
- `get_documents_count` trait method removed from rs-dapi /
  rs-dapi-client / rs-drive-abci's query service impl.
- dapi-grpc `build.rs`: count messages dropped from
  `VERSIONED_REQUESTS` / `VERSIONED_RESPONSES`.
- rs-sdk: `DocumentCountQuery` still presents the same surface
  but now builds a `GetDocumentsRequest::V1` with the right
  `select=COUNT` + computed `group_by` based on where-clause
  shape (preserves v0-count's implicit grouping behavior at the
  SDK seam). FromProof impls updated to consume
  `GetDocumentsResponse`.
- rs-drive-proof-verifier: response type aliases updated.
- rs-sdk mock harness: `GetDocumentsCountRequest` mock-loader arm
  removed; existing dumps for that request type need to be
  re-recorded against `GetDocumentsRequest` v1.

The `getDocumentsCount` endpoint shipped briefly in #3623 and
never had stable callers, so this consolidation is a clean
pre-release removal rather than a deprecation cycle. v0 of the
documents endpoint stays alive unchanged.

Tests:
- drive-abci `document_query` (v0 + v1): 38/38 (1 ignored).
- SDK `fetch::document_count`: 6/6 (the count fetch path now
  exercises the v1 wire bytes end-to-end through the mock
  transport).
- Workspace compiles cleanly across all touched crates.

**Non-Rust client regen** lands in a follow-up commit (user
runs the docker-based `scripts/build.sh`).
When `document_count_query/v0/mod.rs` was deleted in the prior
commit, its 9-test integration suite went with it — a real
coverage gap, since the v1 dispatcher inherits the entire count
execution surface those tests pinned (Total, PerInValue,
RangeNoProof summed + distinct, PointLookupProof, RangeProof,
RangeDistinctProof, plus the no-covering-index rejection paths).

Mechanical 1:1 port into a new `ported_v0_count_tests` submodule:

- Request type: `GetDocumentsCountRequestV0 { … }` →
  `GetDocumentsRequestV1 { select: COUNT, group_by: <derived>, … }`
  via a `count_v1_request` helper.
- `return_distinct_counts_in_range: true/false` → explicit
  `group_by` field (empty for aggregate, `[in_field]` for In,
  `[range_field]` for distinct range, `[in_field, range_field]`
  for compound — matching the SDK's `compute_group_by` shape).
- Response pattern: `GetDocumentsCountResponseV0`'s
  `Counts(CountResults { … })` envelope → v1's nested
  `Data(ResultData { variant: Counts(CountResults { … }) })`.
  Two helpers (`unwrap_aggregate` / `unwrap_entries`) keep the
  per-test assertions focused.
- `setup_platform`, `store_data_contract`, `store_document`,
  `family-contract-countable.json` fixture, and the
  `store_person_document` / `serialize_where_clauses_to_cbor`
  helpers all carry over verbatim.

The 9 ported tests + the existing 12 v1 unit/e2e tests now form
the complete v1 test surface — 21 tests total in the v1 module.
Combined with v0's 27 unchanged tests, `query::document_query`
has 47 passing tests (1 ignored, pre-existing).

No execution-path differences vs. the deleted v0-count tests —
the v1 handler dispatches into the same drive executor (`Drive::
execute_document_count_request`) those tests originally
exercised; only the wire shape on each end changed.
v1 is the canonical surface — it's a superset of v0 (matched
documents via SELECT DOCUMENTS) plus the count surface that
replaces the removed `getDocumentsCount` endpoint. Bump
`default_current_version` from 0 to 1 to signal that new code
should target v1; v0 remains accepted (`max_version: 1`,
`min_version: 0`) so existing v0 callers continue working until
they re-pin their request version.

`default_current_version` is metadata only — not consumed by
the dispatcher's version-check logic, which gates exclusively on
`min_version` / `max_version`. Callers (handlers, SDKs)
inspect it to choose which request shape to build when they have
no other guidance.
PR 2 of the v1 GetDocuments migration: collapse the SDK-side
DocumentCountQuery wrapper into DocumentQuery itself, exposing
the count surface via builders (.with_select(Select::Count),
.with_group_by(...), .with_having(...)) rather than a separate
type.

- DocumentQuery gains v1 fields (select, group_by, having) and
  builders; TryFrom switches to GetDocumentsRequest::V1. The
  u32-with-0-sentinel limit translates to Option<u32> at the
  wire boundary; V0::Start translates to V1::Start.
- New document_count.rs hosts FromProof<DocumentQuery> + Fetch
  for DocumentCount and DocumentSplitCounts; validates
  select == Count at the SDK boundary so callers who forget
  .with_select(Count) fail loudly rather than via an opaque
  verifier error.
- document_count_query.rs deleted (-825 LoC).
- FFI (rs-sdk-ffi) and wasm-sdk count shims keep the legacy
  `return_distinct_counts_in_range: bool` parameter on their
  public C ABI / JS surface; they translate to v1 group_by
  internally via mirrored derive_group_by helpers, preserving
  binary back-compat for existing iOS and browser callers.
- 9 in-tree DocumentQuery struct-literal callsites patched
  with the 3 new default fields (Documents, vec![], vec![]).
- 6 SDK fetch tests rewritten against the unified surface;
  expect_fetch carries explicit turbofish since DocumentQuery
  is now the Request type for 3 separate Fetch impls (Document,
  DocumentCount, DocumentSplitCounts).

All 47 drive-abci document_query tests still pass; all 6
rewritten SDK fetch tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer marked this pull request as ready for review May 12, 2026 13:10
@QuantumExplorer QuantumExplorer requested a review from shumkov as a code owner May 12, 2026 13:10
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented May 12, 2026

Review Gate

Commit: 8a16a942

  • Debounce: 259m ago (need 30m)

  • CI checks: build failure: Rust workspace tests / Tests (macOS)

  • CodeRabbit review: comment found

  • Off-peak hours: off-peak (12:24 AM PT Friday)

  • Run review now (check to override)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
packages/wasm-sdk/src/dpns.rs (1)

282-284: 💤 Low value

Optional: alias the long enum path.

The fully-qualified dash_sdk::dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::Select path is repeated across SDK files. Consider adding a use import at the top of this file (or re-exporting it from dash_sdk::platform) to improve readability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/wasm-sdk/src/dpns.rs` around lines 282 - 284, The long
fully-qualified enum path
dash_sdk::dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::Select
is repeated and harms readability; add a local alias (e.g. use
dash_sdk::dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::Select;)
at the top of packages/wasm-sdk/src/dpns.rs (or re-export it from
dash_sdk::platform) and then replace occurrences like select:
dash_sdk::dapi_grpc::...::Select::Documents with select: Select::Documents to
simplify the code.
packages/rs-sdk/src/platform/documents/document_count.rs (1)

102-291: 💤 Low value

Optional: extract shared dispatch helpers.

DocumentCount::maybe_from_proof_with_metadata and DocumentSplitCounts::maybe_from_proof_with_metadata share substantial structure (range-countable index lookup + DriveDocumentCountQuery construction, documents_countable fast path, non-range countable index lookup, proof/metadata extraction). Consider extracting helpers like build_range_count_query(&request) -> Result<(DriveDocumentCountQuery, ...), _> and build_point_count_query(&request) -> Result<DriveDocumentCountQuery, _> to keep the two impls aligned as the surface evolves.

Also applies to: 320-523

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-sdk/src/platform/documents/document_count.rs` around lines 102 -
291, The two big impls (DocumentCount::maybe_from_proof_with_metadata and
DocumentSplitCounts::maybe_from_proof_with_metadata) duplicate logic for
range-vs-point dispatch, DriveDocumentCountQuery construction, index lookup,
documents_countable fast-path and proof/metadata extraction; extract shared
helpers such as build_range_count_query(request: &DocumentQuery) ->
Result<(DriveDocumentCountQuery, index, document_type),
drive_proof_verifier::Error> and build_point_count_query(request:
&DocumentQuery) -> Result<DriveDocumentCountQuery, drive_proof_verifier::Error>,
plus a helper extract_proof_and_metadata(response: &GetDocumentsResponse) ->
Result<(Proof, ResponseMetadata), ...>, then replace the duplicated blocks in
both impls to call these helpers (preserving existing error messages and all
uses of DriveDocumentCountQuery, verify_distinct_count_proof,
verify_aggregate_count_proof, verify_primary_key_count_tree_proof,
verify_point_lookup_count_proof) so behavior stays identical while keeping the
two impls aligned as the API evolves.
packages/rs-platform-wallet/src/wallet/identity/network/profile.rs (1)

156-171: 💤 Low value

Optional: extract a helper for the duplicated profile-query construction.

fetch_profile_document and update_profile_with_external_signer build essentially the same DocumentQuery (profile doctype, $ownerId == identity_id, limit: 1). Extracting a build_profile_query(contract, identity_id) -> DocumentQuery helper would keep the V1 fields in sync as the surface evolves.

Also applies to: 430-444

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/wallet/identity/network/profile.rs` around
lines 156 - 171, Both fetch_profile_document and
update_profile_with_external_signer duplicate building the same DocumentQuery
for the "profile" doctype ($ownerId == identity_id, limit: 1); extract a helper
function like build_profile_query(dashpay_contract: Arc<...>, identity_id:
<type>) -> dash_sdk::platform::DocumentQuery that constructs and returns the
DocumentQuery (set data_contract, document_type_name="profile", where_clauses
with field "$ownerId" and WhereOperator::Equal with
platform_value!(identity_id), limit:1, select: Documents, and empty
order_by/group_by/having), then replace the inline query constructions in
fetch_profile_document and update_profile_with_external_signer with calls to
build_profile_query to keep the V1 fields consistent.
packages/rs-sdk/src/platform/documents/document_query.rs (2)

378-444: 💤 Low value

Optional: deduplicate the two From<DriveDocumentQuery> impls.

From<&DriveDocumentQuery> and From<DriveDocumentQuery> have identical bodies (and both clone the contract). Consider having the by-value impl delegate to the by-reference impl to keep them in sync as the v1 surface evolves.

♻️ Proposed refactor
 impl<'a> From<DriveDocumentQuery<'a>> for DocumentQuery {
     fn from(value: DriveDocumentQuery<'a>) -> Self {
-        let data_contract = value.contract.clone();
-        // ...duplicated body...
+        Self::from(&value)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-sdk/src/platform/documents/document_query.rs` around lines 378 -
444, The two identical impls From<&'a DriveDocumentQuery<'a>> for DocumentQuery
and From<DriveDocumentQuery<'a>> for DocumentQuery should be deduplicated: keep
the existing by-reference impl and change the by-value impl for
DriveDocumentQuery to delegate to it (call DocumentQuery::from(&value)) so the
by-value conversion reuses the by-reference logic (preserving the current
cloning behavior of contract, where_clauses, etc.) and prevents drift between
the two implementations; update the impl block for From<DriveDocumentQuery<'a>>
accordingly and remove the duplicated body.

330-375: 💤 Low value

Optional: drop unnecessary .clone() calls on consumed fields.

dapi_request is taken by value, so start, document_type_name, group_by, and having can be moved rather than cloned. This is a minor allocation win.

♻️ Proposed refactor
-        let start_v1 = dapi_request.start.clone().map(|s| match s {
+        let start_v1 = dapi_request.start.map(|s| match s {
             Start::StartAfter(b) => V1Start::StartAfter(b),
             Start::StartAt(b) => V1Start::StartAt(b),
         });

         //todo: transform this into PlatformVersionedTryFrom
         Ok(GetDocumentsRequest {
             version: Some(V1(GetDocumentsRequestV1 {
                 data_contract_id: dapi_request.data_contract.id().to_vec(),
-                document_type: dapi_request.document_type_name.clone(),
+                document_type: dapi_request.document_type_name,
                 r#where: where_clauses,
                 order_by,
                 limit,
                 // ...existing comment...
                 prove: true,
                 start: start_v1,
                 select: dapi_request.select as i32,
-                group_by: dapi_request.group_by.clone(),
-                having: dapi_request.having.clone(),
+                group_by: dapi_request.group_by,
+                having: dapi_request.having,
             })),
         })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-sdk/src/platform/documents/document_query.rs` around lines 330 -
375, dapi_request is owned so avoid needless clones: remove .clone() on
dapi_request.start, dapi_request.document_type_name, dapi_request.group_by, and
dapi_request.having and move those fields by value into the GetDocumentsRequest
(e.g., use dapi_request.start.map(...), dapi_request.document_type_name,
dapi_request.group_by, dapi_request.having). Keep existing serialization of
where_clauses/order_by unchanged if they still require cloning or borrowing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/dapi-grpc/protos/platform/v0/platform.proto`:
- Around line 696-700: Update the pagination cursor docblock (the comments
describing start_after/start_at) to match runtime behavior: explicitly state
that start (start_after/start_at) is rejected for all queries with select=COUNT
(regardless of group_by), and only supported for select=DOCUMENTS; remove or
change the existing sentence that allows start for select=COUNT with non-empty
group_by to avoid implying it's accepted. Reference the comment for the
pagination cursor and the symbols start_after, start_at, select=DOCUMENTS, and
select=COUNT when making the edit.

In `@packages/rs-drive-abci/src/query/document_query/v1/mod.rs`:
- Around line 106-109: The current code leaks memory by using Box::leak when
mapping WhereClause::from_components errors; change the approach so the error
variant owns the formatted message instead of requiring a 'static str: update
QuerySyntaxError::InvalidFormatWhereClause to accept an owned String or Box<str>
(rather than &'static str), then construct the error with
QueryError::Query(QuerySyntaxError::InvalidFormatWhereClause(format!("invalid
where clause components: {e}"))) in the WhereClause::from_components error path;
update any other sites that construct or match on InvalidFormatWhereClause
accordingly to use the owned string type.

In `@packages/rs-sdk-ffi/src/document/queries/count.rs`:
- Around line 336-338: The code currently maps any negative `limit` to 0 via the
`limit_u32` conversion; change this to honor only -1 as the "unset" sentinel: if
`limit == -1` map to 0, if `limit < -1` return an error (e.g. Err(...) or an
InvalidArgument/Parse error consistent with this module's error handling) and
otherwise cast `limit` to `u32`; update the conversion at the `limit_u32`
binding and ensure callers of this function propagate or handle the new error
path.

In `@packages/rs-sdk/src/platform/documents/document_count.rs`:
- Around line 320-523: The function
DocumentSplitCounts::maybe_from_proof_with_metadata incorrectly falls through
for range queries with an empty group_by; add an early branch when has_range &&
request.group_by.is_empty() that mirrors the DocumentCount aggregate-path:
extract proof/metadata from response, call verify_aggregate_count_proof (or the
equivalent helper used at DocumentCount) to get the total count, then build a
single SplitCountEntry { in_key: None, key: Vec::new(), count } and return
Some(DocumentSplitCounts::from_verified(vec![...])) with the cloned metadata and
proof; if you prefer to keep unsupported semantics instead, add an explicit
early Err(drive_proof_verifier::Error::RequestError { error: "...range + empty
group_by not supported by DocumentSplitCounts..." }) to fail fast and avoid the
misleading index lookup error.

---

Nitpick comments:
In `@packages/rs-platform-wallet/src/wallet/identity/network/profile.rs`:
- Around line 156-171: Both fetch_profile_document and
update_profile_with_external_signer duplicate building the same DocumentQuery
for the "profile" doctype ($ownerId == identity_id, limit: 1); extract a helper
function like build_profile_query(dashpay_contract: Arc<...>, identity_id:
<type>) -> dash_sdk::platform::DocumentQuery that constructs and returns the
DocumentQuery (set data_contract, document_type_name="profile", where_clauses
with field "$ownerId" and WhereOperator::Equal with
platform_value!(identity_id), limit:1, select: Documents, and empty
order_by/group_by/having), then replace the inline query constructions in
fetch_profile_document and update_profile_with_external_signer with calls to
build_profile_query to keep the V1 fields consistent.

In `@packages/rs-sdk/src/platform/documents/document_count.rs`:
- Around line 102-291: The two big impls
(DocumentCount::maybe_from_proof_with_metadata and
DocumentSplitCounts::maybe_from_proof_with_metadata) duplicate logic for
range-vs-point dispatch, DriveDocumentCountQuery construction, index lookup,
documents_countable fast-path and proof/metadata extraction; extract shared
helpers such as build_range_count_query(request: &DocumentQuery) ->
Result<(DriveDocumentCountQuery, index, document_type),
drive_proof_verifier::Error> and build_point_count_query(request:
&DocumentQuery) -> Result<DriveDocumentCountQuery, drive_proof_verifier::Error>,
plus a helper extract_proof_and_metadata(response: &GetDocumentsResponse) ->
Result<(Proof, ResponseMetadata), ...>, then replace the duplicated blocks in
both impls to call these helpers (preserving existing error messages and all
uses of DriveDocumentCountQuery, verify_distinct_count_proof,
verify_aggregate_count_proof, verify_primary_key_count_tree_proof,
verify_point_lookup_count_proof) so behavior stays identical while keeping the
two impls aligned as the API evolves.

In `@packages/rs-sdk/src/platform/documents/document_query.rs`:
- Around line 378-444: The two identical impls From<&'a DriveDocumentQuery<'a>>
for DocumentQuery and From<DriveDocumentQuery<'a>> for DocumentQuery should be
deduplicated: keep the existing by-reference impl and change the by-value impl
for DriveDocumentQuery to delegate to it (call DocumentQuery::from(&value)) so
the by-value conversion reuses the by-reference logic (preserving the current
cloning behavior of contract, where_clauses, etc.) and prevents drift between
the two implementations; update the impl block for From<DriveDocumentQuery<'a>>
accordingly and remove the duplicated body.
- Around line 330-375: dapi_request is owned so avoid needless clones: remove
.clone() on dapi_request.start, dapi_request.document_type_name,
dapi_request.group_by, and dapi_request.having and move those fields by value
into the GetDocumentsRequest (e.g., use dapi_request.start.map(...),
dapi_request.document_type_name, dapi_request.group_by, dapi_request.having).
Keep existing serialization of where_clauses/order_by unchanged if they still
require cloning or borrowing.

In `@packages/wasm-sdk/src/dpns.rs`:
- Around line 282-284: The long fully-qualified enum path
dash_sdk::dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::Select
is repeated and harms readability; add a local alias (e.g. use
dash_sdk::dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::Select;)
at the top of packages/wasm-sdk/src/dpns.rs (or re-export it from
dash_sdk::platform) and then replace occurrences like select:
dash_sdk::dapi_grpc::...::Select::Documents with select: Select::Documents to
simplify the code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9765acee-3764-4979-b2a7-f167481a97ef

📥 Commits

Reviewing files that changed from the base of the PR and between fa1e492 and 0dcabac.

📒 Files selected for processing (37)
  • packages/dapi-grpc/build.rs
  • packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js
  • packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js
  • packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m
  • packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py
  • packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts
  • packages/dapi-grpc/clients/platform/v0/web/platform_pb.js
  • packages/dapi-grpc/protos/platform/v0/platform.proto
  • packages/rs-dapi-client/src/transport/grpc.rs
  • packages/rs-dapi/src/services/platform_service/mod.rs
  • packages/rs-drive-abci/src/query/document_count_query/mod.rs
  • packages/rs-drive-abci/src/query/document_count_query/v0/mod.rs
  • packages/rs-drive-abci/src/query/document_query/mod.rs
  • packages/rs-drive-abci/src/query/document_query/v1/mod.rs
  • packages/rs-drive-abci/src/query/mod.rs
  • packages/rs-drive-abci/src/query/service.rs
  • packages/rs-drive-proof-verifier/src/proof/document_count.rs
  • packages/rs-drive-proof-verifier/src/proof/document_split_count.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs
  • packages/rs-platform-version/src/version/mocks/v2_test.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/profile.rs
  • packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs
  • packages/rs-sdk-ffi/src/document/queries/count.rs
  • packages/rs-sdk/src/mock/sdk.rs
  • packages/rs-sdk/src/platform/dashpay/contact_request_queries.rs
  • packages/rs-sdk/src/platform/documents/document_count.rs
  • packages/rs-sdk/src/platform/documents/document_count_query.rs
  • packages/rs-sdk/src/platform/documents/document_query.rs
  • packages/rs-sdk/src/platform/documents/mod.rs
  • packages/rs-sdk/src/platform/dpns_usernames/mod.rs
  • packages/rs-sdk/src/platform/dpns_usernames/queries.rs
  • packages/rs-sdk/tests/fetch/document_count.rs
  • packages/wasm-sdk/src/dpns.rs
  • packages/wasm-sdk/src/queries/document.rs
💤 Files with no reviewable changes (8)
  • packages/rs-dapi/src/services/platform_service/mod.rs
  • packages/rs-drive-abci/src/query/document_count_query/mod.rs
  • packages/rs-drive-abci/src/query/mod.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs
  • packages/rs-sdk/src/platform/documents/document_count_query.rs
  • packages/rs-drive-abci/src/query/document_count_query/v0/mod.rs
  • packages/rs-platform-version/src/version/mocks/v2_test.rs
  • packages/rs-dapi-client/src/transport/grpc.rs

Comment thread packages/dapi-grpc/protos/platform/v0/platform.proto Outdated
Comment thread packages/rs-drive-abci/src/query/document_query/v1/mod.rs
Comment thread packages/rs-sdk-ffi/src/document/queries/count.rs Outdated
Comment thread packages/rs-sdk/src/platform/documents/document_count.rs Outdated
…bool

The `return_distinct_counts_in_range` knob only ever lived on the
v0 `GetDocumentsCountRequest` endpoint, which shipped in #3623,
never had stable callers, and was fully removed (not deprecated)
from the proto in PR 1 of this work. PR 2 preserved the bool on
the FFI and wasm-sdk count surfaces "for back-compat" — but there
was nothing to be back-compatible with, so the in-shim
implicit-grouping translation is dead weight.

This commit removes it.

- rs-sdk-ffi `dash_sdk_document_count`: replace the
  `return_distinct_counts_in_range: bool` parameter with
  `group_by_json: *const c_char` (NUL-terminated JSON array of
  field names; null/empty → aggregate). Mirrors the v1 wire's
  `group_by: repeated string` field one-to-one.
- wasm-sdk `DocumentsQuery`: replace
  `returnDistinctCountsInRange?: boolean` with
  `groupBy?: string[]`. Same one-to-one wire mirror on the JS
  side.
- Delete `derive_group_by` helper in rs-sdk-ffi and the inline
  copy in wasm-sdk. No SDK-side translation table; no second
  source of truth for "which operators are range operators";
  callsites become trivial pass-throughs.
- Comment / docstring cleanup in document_count.rs and the
  count-fetch test to drop residual references to the legacy
  flag's narrative.

No external callers — verified by grep against Swift / Kotlin /
native / wasm-demo sources; only the auto-generated FFI header
mentioned the old signature. Existing tests all still pass.

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-sdk/src/platform/documents/document_count.rs`:
- Around line 500-517: Return a single aggregate empty-key entry when the
request has no grouping: after calling verify_point_lookup_count_proof (which
yields entries: Vec<SplitCountEntry>), if request.group_by.is_empty() collapse
all per-In entries into one SplitCountEntry with in_key: None, key: Vec::new(),
and count equal to the sum of the counts from entries (replace entries with that
single entry); preserve the existing zero-case logic that pushes an empty entry
when !has_in && entries.is_empty(); keep using
DocumentSplitCounts::from_verified and the same mtd/proof return values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8e6a45b2-9ea5-43a9-97b6-3a46a4300e56

📥 Commits

Reviewing files that changed from the base of the PR and between 0dcabac and 26f9512.

📒 Files selected for processing (4)
  • packages/rs-sdk-ffi/src/document/queries/count.rs
  • packages/rs-sdk/src/platform/documents/document_count.rs
  • packages/rs-sdk/tests/fetch/document_count.rs
  • packages/wasm-sdk/src/queries/document.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/rs-sdk/tests/fetch/document_count.rs

Comment thread packages/rs-sdk/src/platform/documents/document_count.rs Outdated
QuantumExplorer and others added 5 commits May 12, 2026 23:47
`select` was tacked onto the end of the struct when it was
added, which reads awkwardly given the type is the SQL-shaped
query surface. Reorder the field declaration (and every struct
literal, the `new()` constructor, and both `From<DriveDocumentQuery>`
impls) to canonical SQL clause order:

    SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET

i.e.:

    select, data_contract, document_type_name, where_clauses,
    group_by, having, order_by_clauses, limit, start

The struct uses named fields throughout so this is purely
stylistic; behavior, wire format, serde shape, and the Mockable
derive are all unaffected. 6 SDK fetch tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `Select as V1Select` rename was load-bearing back when the
DocumentCountQuery wrapper coexisted with a `Select` type from
somewhere else; with the wrapper gone there is no other `Select`
in scope inside document_query.rs or document_count.rs, so the
alias is pure noise.

`Start as V1Start` stays — that one is real, since `Start` is
also imported from `get_documents_request_v0` for the
DocumentQuery.start field.

No behavior change. SDK fetch tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The non-Rust gRPC clients (nodejs / web / objective-c / python /
java) still referenced the pre-reshape `GetDocumentsResponseV1`
fields (`documents`, `counts`) and the removed
`GetDocumentsCountResponse` types. Regenerate against the current
`platform.proto` so they pick up the inner `ResultData` wrapper
that the v1 response carries (`result.data.documents` for
SELECT=DOCUMENTS, `result.data.counts` for SELECT=COUNT).

Generated output only — no hand-edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace comments that referenced commit-time context ("removed
in PR 2", "Pre-v1", "Phase 1", "prior regression", "v0-style",
"the v0 wrapper carried...") with comments that describe what
the code does and why, without anchoring to a transition that a
future reader has no context for.

The most egregious offender was a multi-line note in
`mock/sdk.rs` explaining why the `DocumentCountQuery` arm
"was removed in PR 2 of the v1 migration"; deleted entirely
since the match expression with one `DocumentQuery` arm
speaks for itself. Similar rewrites in `document_count.rs`,
`document_query.rs`, the FFI `count.rs`, the wasm-sdk
`document.rs`, and the SDK fetch tests.

Stable wire-version identifiers like "V1 wire" and
"GetDocumentsRequestV1" are kept — those name a proto version
that exists in the codebase, not a transition.

No behavior changes. Tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…yet"

`unimplemented!("queries without proofs are not supported yet")`
implied this was a feature waiting to land. It isn't: dash-sdk
intentionally only serves proof-verified responses, and that's
the permanent architectural contract — non-proven gRPC is a
direct-client concern (rs-dapi-client), not an SDK fetch-path
concern.

Replace the `unimplemented!` panic with a typed
`Err(Error::Config(...))` return so callers get a clean error
they can match on rather than a runtime crash, and reword the
message as a contract statement instead of a TODO.

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/rs-sdk-ffi/src/document/queries/count.rs (1)

321-330: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Honor only -1 as the limit sentinel.

Line 321 still maps any negative value to 0 (the "unset" sentinel), but the documented contract at line 276 defines only -1 as valid. Other negatives (e.g., -2, i64::MIN) are silently coerced to "use server default" instead of being rejected — this lets invalid input through and contradicts the doc.

🛡️ Proposed fix
-        let limit_u32: u32 = if limit < 0 {
-            0
+        let limit_u32: u32 = if limit == -1 {
+            0
+        } else if limit < -1 {
+            return Err(FFIError::InternalError(format!(
+                "limit {} is invalid; use -1 for server default or a non-negative value",
+                limit
+            )));
         } else if limit > u32::MAX as i64 {
             return Err(FFIError::InternalError(format!(
                 "limit {} exceeds u32::MAX",
                 limit
             )));
         } else {
             limit as u32
         };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-sdk-ffi/src/document/queries/count.rs` around lines 321 - 330,
The current conversion of `limit` to `limit_u32` treats any negative `limit` as
the sentinel for "use server default"; change this so only `-1` is accepted as
that sentinel and any other negative values produce an error: check `limit`
first for equality to `-1` and map that to `0u32`, then if `limit < -1` return
an `FFIError::InternalError` (or appropriate error) explaining the invalid
negative value, and finally handle the `limit > u32::MAX as i64` overflow case
and the normal `limit as u32` conversion; update the code that sets `limit_u32`
to follow this order and keep references to `FFIError::InternalError` and the
`u32::MAX` check.
🧹 Nitpick comments (1)
packages/rs-sdk/src/platform/query.rs (1)

325-341: 🏗️ Heavy lift

Consider applying this error-handling pattern to other Query implementations for consistency.

Currently, other Query trait implementations (e.g., lines 130, 144, 158, 180, 255, 282, 298, 314, etc.) still use unimplemented!() when prove == false, which panics. For a consistent API surface and better user experience, consider refactoring them to return Error::Config as well, since the SDK is designed to only support proven queries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-sdk/src/platform/query.rs` around lines 325 - 341, Several other
impl Query<...> blocks currently call unimplemented!() when prove == false,
which panics; replace those with the same error-return pattern used in the
DriveDocumentQuery impl. Locate each impl Query implementation that branches on
prove (the ones currently invoking unimplemented!()), and change the branch to
return Err(Error::Config("dash-sdk does not support non-proven queries; proof
verification is mandatory on the SDK fetch path".to_string())); keep the rest of
the impl (conversion to the query type, e.g., let q: DocumentQuery =
(&self).into(); Ok(q)) unchanged so all Query implementations consistently
return a Config error instead of panicking.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbrpc.h`:
- Around line 235-241: The migration note about `getDocumentsCount` is
incorrectly attached to the `getIdentityByPublicKeyHash` Objective-C RPC docs;
remove that doc block from the `getIdentityByPublicKeyHash` method declarations
(`getIdentityByPublicKeyHash` / `getIdentityByPublicKeyHashV0` symbols) and
relocate it to the `getDocuments` API surface (or the proto/codegen source that
defines `GetDocumentsRequestV1` / `getDocuments`), updating the comment there to
describe that `getDocumentsCount` was removed in v1 and callers should use
`GetDocumentsRequestV1` with `version.v1.select = COUNT` (and optional
`group_by`); ensure the internal issue reference (`#3623`) is omitted from shipped
client docs per guidance.

---

Duplicate comments:
In `@packages/rs-sdk-ffi/src/document/queries/count.rs`:
- Around line 321-330: The current conversion of `limit` to `limit_u32` treats
any negative `limit` as the sentinel for "use server default"; change this so
only `-1` is accepted as that sentinel and any other negative values produce an
error: check `limit` first for equality to `-1` and map that to `0u32`, then if
`limit < -1` return an `FFIError::InternalError` (or appropriate error)
explaining the invalid negative value, and finally handle the `limit > u32::MAX
as i64` overflow case and the normal `limit as u32` conversion; update the code
that sets `limit_u32` to follow this order and keep references to
`FFIError::InternalError` and the `u32::MAX` check.

---

Nitpick comments:
In `@packages/rs-sdk/src/platform/query.rs`:
- Around line 325-341: Several other impl Query<...> blocks currently call
unimplemented!() when prove == false, which panics; replace those with the same
error-return pattern used in the DriveDocumentQuery impl. Locate each impl Query
implementation that branches on prove (the ones currently invoking
unimplemented!()), and change the branch to return Err(Error::Config("dash-sdk
does not support non-proven queries; proof verification is mandatory on the SDK
fetch path".to_string())); keep the rest of the impl (conversion to the query
type, e.g., let q: DocumentQuery = (&self).into(); Ok(q)) unchanged so all Query
implementations consistently return a Config error instead of panicking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e030b6f-6ccd-4808-a520-c87519997e8f

📥 Commits

Reviewing files that changed from the base of the PR and between db4cf40 and 4e6d040.

📒 Files selected for processing (21)
  • packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js
  • packages/dapi-grpc/clients/platform/v0/java/org/dash/platform/dapi/v0/PlatformGrpc.java
  • packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js
  • packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbrpc.h
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbrpc.m
  • packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py
  • packages/dapi-grpc/clients/platform/v0/python/platform_pb2_grpc.py
  • packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts
  • packages/dapi-grpc/clients/platform/v0/web/platform_pb.js
  • packages/dapi-grpc/clients/platform/v0/web/platform_pb_service.d.ts
  • packages/dapi-grpc/clients/platform/v0/web/platform_pb_service.js
  • packages/rs-sdk-ffi/src/document/queries/count.rs
  • packages/rs-sdk/src/mock/sdk.rs
  • packages/rs-sdk/src/platform/documents/document_count.rs
  • packages/rs-sdk/src/platform/documents/document_query.rs
  • packages/rs-sdk/src/platform/query.rs
  • packages/rs-sdk/tests/fetch/document_count.rs
  • packages/wasm-sdk/src/queries/document.rs
💤 Files with no reviewable changes (3)
  • packages/rs-sdk/src/mock/sdk.rs
  • packages/dapi-grpc/clients/platform/v0/web/platform_pb_service.js
  • packages/dapi-grpc/clients/platform/v0/web/platform_pb_service.d.ts
✅ Files skipped from review due to trivial changes (6)
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbrpc.m
  • packages/dapi-grpc/clients/platform/v0/python/platform_pb2_grpc.py
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m
  • packages/dapi-grpc/clients/platform/v0/java/org/dash/platform/dapi/v0/PlatformGrpc.java
  • packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h
  • packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/rs-sdk/src/platform/documents/document_count.rs
  • packages/wasm-sdk/src/queries/document.rs
  • packages/rs-sdk/src/platform/documents/document_query.rs
  • packages/rs-sdk/tests/fetch/document_count.rs

Comment on lines +235 to +241
/**
* `getDocumentsCount` removed in v1: callers express counts via
* `getDocuments` with `version.v1.select = COUNT` (optionally
* with `group_by`). See `GetDocumentsRequestV1` for the unified
* SQL-shaped surface. The v0-count endpoint shipped briefly in
* #3623 and never had stable callers; v1 supersedes it entirely.
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move this migration note off the getIdentityByPublicKeyHash methods.

These doc blocks are now attached to the getIdentityByPublicKeyHash APIs, so generated Objective-C help text for that RPC becomes unrelated and misleading. It also bakes internal rollout history (#3623) into shipped client docs. Please move the note to the getDocuments surface or the proto/codegen source that owns the migration guidance instead of annotating an unrelated method.

Also applies to: 571-579, 582-590

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbrpc.h` around
lines 235 - 241, The migration note about `getDocumentsCount` is incorrectly
attached to the `getIdentityByPublicKeyHash` Objective-C RPC docs; remove that
doc block from the `getIdentityByPublicKeyHash` method declarations
(`getIdentityByPublicKeyHash` / `getIdentityByPublicKeyHashV0` symbols) and
relocate it to the `getDocuments` API surface (or the proto/codegen source that
defines `GetDocumentsRequestV1` / `getDocuments`), updating the comment there to
describe that `getDocumentsCount` was removed in v1 and callers should use
`GetDocumentsRequestV1` with `version.v1.select = COUNT` (and optional
`group_by`); ensure the internal issue reference (`#3623`) is omitted from shipped
client docs per guidance.

@QuantumExplorer QuantumExplorer changed the title feat(platform): GetDocumentsRequestV1 — SQL-shaped getDocuments + count surface (1/3) feat(platform): getDocuments v1 — SQL-shaped select + count surface (1/3) May 12, 2026
QuantumExplorer and others added 5 commits May 13, 2026 17:12
Four independent fixes raised in review of 4e6d040:

[P1] Reject `SELECT COUNT, group_by=[]` with non-None `limit`.
The handler previously forwarded `request_v1.limit` to Drive even
on the aggregate path, so Drive's PerInValue fan-out would honor
the limit and return a partial sum that looked like a total. Now
validated up front in `validate_and_route` and rejected with
`QuerySyntaxError::InvalidLimit`, matching the proto contract
that aggregate count is structurally a single row.

[P1] Reject single-field `group_by` when both `In` and range
clauses are constrained. `group_by=[in_field]` (or
`[range_field]`) routes through `dispatch_count_v1` with
`return_distinct_counts_in_range` toggled, but Drive's compound
walk emits unmerged `(in_key, key)` rows that don't match a
single-field grouping. Now both single-field branches require
the other clause to be absent; callers wanting the compound
shape must spell it out with `[in_field, range_field]`.

[P2] Drop cursor-on-grouped-count claim from the proto docs.
The handler at `dispatch_count_v1` rejects every count cursor
permanently, but `start.proto`'s docstring still claimed they
were valid for `select=COUNT` with non-empty `group_by`. Update
the comment to reflect reality: cursors are documents-only, and
count pagination happens by narrowing the where-clause range.

[P2] Drop `Box::leak` from the malformed-where-clause path.
`QuerySyntaxError::InvalidFormatWhereClause(&'static str)` was
forcing a permanent leak on every malformed external request —
a slow unbounded DoS vector. Variant now takes `String`; 12
callsites updated to `.to_string()` (or `format!()` at the
formerly-leaky site).

Three new validation tests cover the new rejections; all 50
v1 document_query tests, 38 rs-drive count query tests, and 6
SDK fetch tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ool with CountMode enum

The boolean flag on `DocumentCountRequest` conflated two distinct
caller intents — `(In + no-distinct + aggregate-sum)` and
`(In + no-distinct + per-In-entries)` shared the same `false`
value despite producing very different responses. That overlap
was the root cause of Codex review Finding 1 (aggregate
silently honoring `limit` via PerInValue fan-out).

Introduce `enum CountMode { Aggregate, GroupByIn, GroupByRange,
GroupByCompound }` as the SQL-shape contract the caller asserts
on the wire via `(select, group_by)`. The dispatcher uses it
directly instead of re-deriving from booleans; `detect_mode`
takes `mode: CountMode` instead of the bool; the dispatcher's
range-no-proof arm asks `mode.is_aggregate()` instead of
inverting a flag; the v1 handler's `RoutingDecision` collapses
to `enum { Documents, Count(CountMode) }` so the count branch
of dispatch carries the mode literally.

Existing `DocumentCountMode` (executor-strategy enum: Total /
PerInValue / RangeNoProof / RangeProof / RangeDistinctProof /
PointLookupProof) stays — it's a different abstraction layer
(which proof primitive / which walk), derived from
`(CountMode, where_clauses, prove)`. The two coexist with a
clarifying docstring at `CountMode`'s definition.

Test fixtures in rs-drive/tests.rs and the lone v0
contract-insert test updated from `return_distinct_counts_in_range:
bool` to `mode: CountMode::*`. detect_mode_tests bulk-updated
via regex (false→Aggregate, true→GroupByRange — both
behaviorally equivalent since detect_mode only branches on
`mode.requires_distinct_walk()`).

All test suites still pass: 50 drive-abci v1 tests, 38 rs-drive
count tests, 6 SDK fetch tests, 0 regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review caught that `point_lookup_count_path_query` builds
its outer `SizedQuery::new(_, None, None)` and the
PointLookupProof dispatch never threaded `request.limit` into
it. Effect: proof-backed `select=COUNT, group_by=[in_field],
limit=N` queries silently returned all In branches in the
proof, ignoring N.

The right fix isn't to thread limit through to the path query —
the In array is already capped at 100 entries by
`WhereClause::in_values()`, so result size is bounded by
construction and a separate limit is either redundant (≤ 100)
or would require partial-In SizedQuery semantics the verifier
can't reconstruct symmetrically. Reject limit upstream instead.

Add `CountMode::accepts_limit()` so the contract lives on the
mode itself (`GroupByRange` / `GroupByCompound` accept; the
other two reject), restructure `validate_and_route` to compute
the mode first and check `limit` once at the bottom, and add a
breadcrumb comment on the `SizedQuery::new(_, None, None)` site
so a future reader knows the `None` limit is the deliberate
contract, not an oversight.

New test `reject_count_group_by_in_with_limit` companion to the
existing `reject_count_aggregate_with_limit`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ating Some(0) and None

Codex review flagged that proof-backed `select=COUNT,
group_by=[in_field]` queries omit zero-count entries for In
values with no matching documents, despite proto docs promising
them. The underlying issue is structural: the PointLookupProof
shape only materializes existing CountTree elements (zero-count
branches aren't stored in the merk tree), so "absent from proof"
collapses with "verified zero" if both are encoded as `count: 0`.

Three-value contract instead:
- `Some(n)` with `n > 0` — verified count for an existing
  CountTree element.
- `Some(0)` — caller queried this branch and the executor /
  verifier confirmed zero (no-proof path, or documents_countable
  fast path where the empty tree IS cryptographically committed).
- `None` — caller asked but the verifier was silent. Distinct
  from `Some(0)` so callers don't conflate "verified zero" with
  "proof didn't cover this."

Implementation:
- `SplitCountEntry.count` type changes from `u64` to
  `Option<u64>`. ~20 production callsites updated.
- Drive executors emit `Some(n)` (the no-proof path knows what
  it queried).
- Drive verifiers emit `Some(n)` for proof-materialized entries.
- SDK's `FromProof<DocumentQuery>` for `DocumentSplitCounts`
  appends `count: None` entries for In values in the request
  that the proof was silent on (new helper
  `synthesize_missing_in_entries`, keyed off
  `document_type.serialize_value_for_key` so the synthesized
  keys byte-match the verified ones).
- `DocumentCount` summing uses `filter_map(|e| e.count)` so
  `None` entries don't contaminate the aggregate.
- `into_flat_map` treats `None` as 0 for the sum (caller can
  iterate `self.0` directly for the full three-valued view).
- Wire format unchanged — wire `CountEntry.count` stays `uint64`;
  server-side never emits `None` so the conversion is lossless.
  `None` synthesis lives SDK-only because the In array context
  lives on the request, not the wire response.

Proto docstring rewritten: the "Zero-count entries on
`In`-grouped queries" section now describes the actual behavior
(wire emits only existing CountTree entries; SDK synthesizes
`None` for absent ones).

New test `test_mock_fetch_document_split_counts_preserves_none_for_absent_in_values`
pins the wire/mock shape round-trip for mixed `Some(_)` / `None`
fixtures.

Counts: 38/38 rs-drive count + 51/51 drive-abci v1 + 225/225
drive-proof-verifier + 7/7 SDK fetch (up from 6) — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The proto docstring on `GetDocumentsRequestV1.limit` still said
aggregate count "ignores" `limit`, but the v1 handler now
rejects every `select=COUNT, group_by=[]` request that sets one
(landed earlier in this PR after Codex flagged it). Generated
client docs (nodejs / web / objective-c / python / java) pointed
integrators at behavior the server no longer honors.

Update the comment to spell out the per-shape contract:
- `Aggregate`: rejected with `InvalidLimit` when set.
- `GroupByIn`: rejected with `InvalidLimit` when set (In array
  already capped at 100 by `WhereClause::in_values()`; partial-
  In SizedQuery selection isn't representable).
- `GroupByRange` / `GroupByCompound`: accepted, validate-don't-
  clamp on the prove path.

Generated comments only — no code or wire format change.

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

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 53.91304% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.24%. Comparing base (3b9fe6b) to head (396d9f3).
⚠️ Report is 5 commits behind head on v3.1-dev.

Files with missing lines Patch % Lines
...ery/drive_document_count_query/drive_dispatcher.rs 44.11% 19 Missing ⚠️
...ages/rs-drive-abci/src/query/document_query/mod.rs 0.00% 10 Missing ⚠️
...s-drive-proof-verifier/src/proof/document_count.rs 0.00% 6 Missing ⚠️
...e-proof-verifier/src/proof/document_split_count.rs 0.00% 5 Missing ⚠️
packages/rs-drive/src/query/mod.rs 0.00% 4 Missing ⚠️
...cument_count/verify_distinct_count_proof/v0/mod.rs 0.00% 4 Missing ⚠️
...rc/drive/contract/insert/insert_contract/v0/mod.rs 86.66% 2 Missing ⚠️
...src/query/drive_document_count_query/path_query.rs 66.66% 2 Missing ⚠️
...query/drive_document_count_query/mode_detection.rs 91.66% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3633      +/-   ##
============================================
- Coverage     88.25%   88.24%   -0.02%     
============================================
  Files          2494     2504      +10     
  Lines        304580   307236    +2656     
============================================
+ Hits         268812   271106    +2294     
- Misses        35768    36130     +362     
Components Coverage Δ
dpp 88.00% <92.17%> (+0.01%) ⬆️
drive 87.33% <76.05%> (-0.04%) ⬇️
drive-abci 90.24% <0.00%> (+0.06%) ⬆️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 53.62% <4.10%> (-0.60%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…s grovedb's None

The SDK's `synthesize_missing_in_entries` was re-deriving
information the verifier already had and was throwing away.
`GroveDb::verify_query` returns `(path, key, Option<Element>)`
triples for every key the path query enumerates — `Some(element)`
for present branches, `None` for absent ones — but
`verify_point_lookup_count_proof_v0` discarded the `None` triples
via `let Some(e) = elem else { continue };`, then the SDK
reconstructed them client-side by comparing the request's In
array against the surviving entries.

Pass through what grovedb already gives us:

- `verify_point_lookup_count_proof_v0` now maps `elem.map(|e|
  e.count_value_or_default())` directly onto
  `SplitCountEntry::count`, so absent branches surface as
  `count: None` from the verifier itself.
- `synthesize_missing_in_entries` deleted from
  `dash-sdk/.../document_count.rs` along with its call site and
  the unused `has_in` derivation.
- The Equal-only fully-covered re-emit-zero hack is also gone —
  the verifier now emits the entry with `count: None` instead of
  silently dropping it, so the caller always sees one entry per
  queried key regardless of presence.

Wire format unchanged; the change is purely in how
verifier-internal data is shaped. Proto doc + SDK doc updated to
describe the simpler model.

All 38 rs-drive count + 51 drive-abci v1 + 7 SDK fetch tests
still pass.

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

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

The core rewiring is sound: the new v1 document-query surface routes into the existing executors and I did not confirm any consensus or panic-safety blocker in the reviewed paths. The remaining issues are targeted quality gaps: one real request-validation mismatch, one misleading error classification, two compatibility/UX issues at language boundaries, and missing tests around newly introduced behavior.

Reviewed commit: 55e3526

🟡 4 suggestion(s) | 💬 2 nitpick(s)

2 additional findings

🟡 suggestion: The verifier's new `None` propagation is untested on the real proof path

packages/rs-drive/src/verify/document_count/verify_point_lookup_count_proof/v0/mod.rs (lines 89-111)

This verifier now preserves None from GroveDb::verify_query instead of skipping missing branches. That is a behavioral change in the proof decoder itself, but the test coverage only exercises a mock DocumentSplitCounts round-trip in rs-sdk/tests/fetch/document_count.rs; it does not invoke verify_point_lookup_count_proof_v0 against an actual proof containing an absent In branch. A regression back to continue on None would still pass the current tests. Add a proof-backed test that verifies an In request with a missing branch yields a SplitCountEntry { count: None }.

🟡 suggestion: The WASM query input silently ignores the removed `returnDistinctCountsInRange` option

packages/wasm-sdk/src/queries/document.rs (lines 129-155)

DocumentsQueryInput accepts unknown fields, and the legacy returnDistinctCountsInRange property is no longer part of the struct. A plain JavaScript caller that still sends returnDistinctCountsInRange: true will not get an error; serde drops the field and the request falls back to empty groupBy, which changes the response shape from distinct entries to an aggregate count. For a boundary-layer deprecation, silent reinterpretation is the wrong failure mode. Reject unknown fields or keep a shim field that throws a migration error when the deprecated option is supplied.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`:
- [SUGGESTION] lines 470-500: The new aggregate `In` no-proof collapse has no dedicated regression test
  `dispatch_count_v1` has a special-case branch for `select=COUNT`, `group_by=[]`, `prove=false` when Drive returns `DocumentCountResponse::Entries`: it folds the per-`In` entries back into a single aggregate result. None of the ported count tests exercise that exact wire shape. `ported_documents_count_with_in_operator` covers `group_by=["age"]`, which expects entries on the wire, not the aggregate collapse path. Because this is the only response-shape transformation introduced in the v1 handler, it needs a direct test to lock the contract and catch future refactors that accidentally return entry rows instead of an aggregate.
- [SUGGESTION] lines 260-280: `limit: Some(0)` has inconsistent semantics between document and count requests
  The v1 handler treats `limit: Some(0)` as an explicit, invalid limit for count modes that do not accept `limit`, because it rejects any `request_v1.limit.is_some()`. The documents path treats the same wire value as unset by forwarding `request_v1.limit.unwrap_or(0)` into the v0 sentinel semantics. That means the same encoded request value is accepted as "use server default" for `SELECT DOCUMENTS` but rejected for aggregate and `GROUP BY In` count requests. Raw gRPC, WASM, or FFI callers can legitimately send `Some(0)`, so this is observable behavior, not just an internal SDK detail.

In `packages/rs-drive/src/verify/document_count/verify_point_lookup_count_proof/v0/mod.rs`:
- [SUGGESTION] lines 89-111: The verifier's new `None` propagation is untested on the real proof path
  This verifier now preserves `None` from `GroveDb::verify_query` instead of skipping missing branches. That is a behavioral change in the proof decoder itself, but the test coverage only exercises a mock `DocumentSplitCounts` round-trip in `rs-sdk/tests/fetch/document_count.rs`; it does not invoke `verify_point_lookup_count_proof_v0` against an actual proof containing an absent `In` branch. A regression back to `continue` on `None` would still pass the current tests. Add a proof-backed test that verifies an `In` request with a missing branch yields a `SplitCountEntry { count: None }`.

In `packages/wasm-sdk/src/queries/document.rs`:
- [SUGGESTION] lines 129-155: The WASM query input silently ignores the removed `returnDistinctCountsInRange` option
  `DocumentsQueryInput` accepts unknown fields, and the legacy `returnDistinctCountsInRange` property is no longer part of the struct. A plain JavaScript caller that still sends `returnDistinctCountsInRange: true` will not get an error; serde drops the field and the request falls back to empty `groupBy`, which changes the response shape from distinct entries to an aggregate count. For a boundary-layer deprecation, silent reinterpretation is the wrong failure mode. Reject unknown fields or keep a shim field that throws a migration error when the deprecated option is supplied.

Comment on lines +470 to +500
DocumentCountResponse::Entries(entries) => {
if mode.is_aggregate() {
// `select=COUNT, group_by=[]` against a request
// that drove a PerInValue execution (In + no
// range + no prove). Sum entries into a single
// aggregate before emission. `saturating_add`
// on the off-chance an operator-misconfigured
// count tree exceeds u64; realistic ceiling is
// `|In| × max_per-branch-count`, well under u64.
let total: u64 = entries
.iter()
// `count.unwrap_or(0)` here is safe: this
// arm is server-side, summing entries the
// executor emitted. Executor never emits
// `None` (that's an SDK-side
// synthesis-for-missing concept). The
// `unwrap_or(0)` is a belt-and-suspenders
// guard against any future executor that
// forgets the contract.
.map(|e| e.count.unwrap_or(0))
.fold(0u64, |a, b| a.saturating_add(b));
GetDocumentsResponseV1 {
result: Some(get_documents_response_v1::Result::Data(ResultData {
variant: Some(result_data::Variant::Counts(CountResults {
variant: Some(count_results::Variant::AggregateCount(total)),
})),
})),
metadata: Some(
self.response_metadata_v0(platform_state, CheckpointUsed::Current),
),
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: The new aggregate In no-proof collapse has no dedicated regression test

dispatch_count_v1 has a special-case branch for select=COUNT, group_by=[], prove=false when Drive returns DocumentCountResponse::Entries: it folds the per-In entries back into a single aggregate result. None of the ported count tests exercise that exact wire shape. ported_documents_count_with_in_operator covers group_by=["age"], which expects entries on the wire, not the aggregate collapse path. Because this is the only response-shape transformation introduced in the v1 handler, it needs a direct test to lock the contract and catch future refactors that accidentally return entry rows instead of an aggregate.

source: ['claude']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`:
- [SUGGESTION] lines 470-500: The new aggregate `In` no-proof collapse has no dedicated regression test
  `dispatch_count_v1` has a special-case branch for `select=COUNT`, `group_by=[]`, `prove=false` when Drive returns `DocumentCountResponse::Entries`: it folds the per-`In` entries back into a single aggregate result. None of the ported count tests exercise that exact wire shape. `ported_documents_count_with_in_operator` covers `group_by=["age"]`, which expects entries on the wire, not the aggregate collapse path. Because this is the only response-shape transformation introduced in the v1 handler, it needs a direct test to lock the contract and catch future refactors that accidentally return entry rows instead of an aggregate.

Comment on lines +260 to +280
if request_v1.limit.is_some() && !mode.accepts_limit() {
let reason = match mode {
CountMode::Aggregate => {
"`limit` is not valid for SELECT COUNT with empty GROUP BY \
(aggregate count is a single row; omit `limit` to fix)"
}
CountMode::GroupByIn => {
"`limit` is not valid for SELECT COUNT with GROUP BY on an \
`In` field (result is bounded by the In array — capped at \
100 entries; narrow the In array directly to reduce the \
result set)"
}
CountMode::GroupByRange | CountMode::GroupByCompound => unreachable!(
"`accepts_limit()` returns true for these variants; \
outer guard already filtered them out"
),
};
return Err(QueryError::Query(QuerySyntaxError::InvalidLimit(
reason.to_string(),
)));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: limit: Some(0) has inconsistent semantics between document and count requests

The v1 handler treats limit: Some(0) as an explicit, invalid limit for count modes that do not accept limit, because it rejects any request_v1.limit.is_some(). The documents path treats the same wire value as unset by forwarding request_v1.limit.unwrap_or(0) into the v0 sentinel semantics. That means the same encoded request value is accepted as "use server default" for SELECT DOCUMENTS but rejected for aggregate and GROUP BY In count requests. Raw gRPC, WASM, or FFI callers can legitimately send Some(0), so this is observable behavior, not just an internal SDK detail.

source: ['claude']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`:
- [SUGGESTION] lines 260-280: `limit: Some(0)` has inconsistent semantics between document and count requests
  The v1 handler treats `limit: Some(0)` as an explicit, invalid limit for count modes that do not accept `limit`, because it rejects any `request_v1.limit.is_some()`. The documents path treats the same wire value as unset by forwarding `request_v1.limit.unwrap_or(0)` into the v0 sentinel semantics. That means the same encoded request value is accepted as "use server default" for `SELECT DOCUMENTS` but rejected for aggregate and `GROUP BY In` count requests. Raw gRPC, WASM, or FFI callers can legitimately send `Some(0)`, so this is observable behavior, not just an internal SDK detail.

Comment on lines +145 to +150
let select = Select::try_from(request_v1.select).map_err(|_| {
not_yet_implemented(&format!(
"select value {} (not in the Select enum)",
request_v1.select
))
})?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Nitpick: An unknown Select enum value is reported as "not yet implemented" instead of invalid input

Select::try_from(request_v1.select) maps an unknown integer to QuerySyntaxError::Unsupported("... not yet implemented"). That message is appropriate for reserved-but-planned query shapes like HAVING, but not for a malformed enum value such as 42. Returning the same "future capability" error for garbage input makes client-side error handling less precise and misclassifies a bad request as a feature gap.

source: ['claude']

Comment on lines 316 to 330
// Sentinel decoding for the C ABI. `-1` means "unset; use
// server-side default". The Rust-side request field is
// `Option<u32>` so `None` here is the same as the request
// omitting the field on the wire.
let limit_opt = if limit < 0 {
None
// server-side default". The DocumentQuery `limit` field is
// a `u32` with `0` as its "unset" sentinel (translated to
// `None` on the V1 wire's `optional uint32`), so the FFI
// `-1` maps to `0`.
let limit_u32: u32 = if limit < 0 {
0
} else if limit > u32::MAX as i64 {
return Err(FFIError::InternalError(format!(
"limit {} exceeds u32::MAX",
limit
)));
} else {
Some(limit as u32)
limit as u32
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Nitpick: The FFI limit documentation does not match the sentinel handling

The C ABI docs say -1 means unset and any value >= 0 is an explicit cap. The implementation maps every negative value to 0, and 0 is then translated into the Rust-side unset sentinel as well. In practice, -2, -100, and 0 all mean "use server default", which is not what the function contract documents. Either tighten the decoding to match the documented contract or update the docs to state that all values <= 0 are treated as unset.

source: ['claude']

…MAX_LIMIT_AS_FAILSAFE)

The PerInValue dispatcher arm and the Aggregate sub-case of
RangeNoProof both unwrapped `request.limit` to
`drive_config.default_query_limit`. `CountMode::Aggregate` and
`CountMode::GroupByIn` reject explicit `limit` upstream in
`validate_and_route`, so `request.limit` was always None at
those sites — and `default_query_limit` is a documents-fetch
knob that doesn't belong on count fan-out. Result: under
operator-tuned `default_query_limit < |In|`, the per-In fan-out
was silently truncated and aggregate sums were wrong.

Introduce `MAX_LIMIT_AS_FAILSAFE: u32 = 1024` in rs-drive as
the executor-level cap for fan-out arms. The `In` array is
structurally capped at 100 by `WhereClause::in_values()`, so
1024 sits well above the real bound — it's not load-bearing,
just keeps the dispatcher's correctness independent of operator
config. If it ever fires that's a signal to revisit the bound
before raising the constant.

- PerInValue: cap at `MAX_LIMIT_AS_FAILSAFE` (was
  `default_query_limit`).
- RangeNoProof: split on `request.mode.is_aggregate()`.
  Aggregate uses `MAX_LIMIT_AS_FAILSAFE`; distinct-walk modes
  (GroupByRange / GroupByCompound) keep the existing
  `default_query_limit` / `max_query_limit` clamping since
  range-distinct is genuinely unbounded.

Regression test inserts 8 docs across 8 distinct ages, sets
`default_query_limit = 3`, asks for an Aggregate over the full
8-element In array, and asserts both the entry count (8) and
the summed total (8). Pre-fix this returned 3 / 3.

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

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Verified all six convergent findings against SHA 75e220e. No blocking issues. The codex 'security' blockers conflate request-shape validation with proof soundness: the verifier deterministically reconstructs a path-query from where_clauses + index, and merk verification binds the proof to that path-query — a malicious server cannot substitute a proof for a different path-query. The real gap is that the SDK doesn't pre-reject unsupported (select, group_by, having, start_after) shapes the way the server does, so a malicious node could return a cryptographically valid count for a request the server would have rejected; the count is still numerically correct for the reconstructed path-query. Reframing as defense-in-depth (suggestion) rather than blocking. Remaining valid findings: two test-coverage gaps (aggregate-collapse path, verifier None propagation against real proof), one limit:Some(0) cross-mode asymmetry, one error classification miss for unknown Select enums, one WASM silent-drop of removed legacy fields, and one FFI limit-sentinel doc/impl mismatch.

Reviewed commit: 75e220e

🟡 4 suggestion(s) | 💬 2 nitpick(s)

2 additional findings

🟡 suggestion: Verifier's `None` propagation on absent In branch is not tested against a real proof

packages/rs-drive/src/verify/document_count/verify_point_lookup_count_proof/v0/mod.rs (lines 89-111)

This verifier preserves Option<Element> from GroveDb::verify_query and maps it onto SplitCountEntry::count so SDK callers can distinguish Some(0) (verified zero) from None (proof was silent on this branch). The contract is load-bearing — it's documented in the proto comment and is the stated reason the SDK no longer synthesizes missing entries — but coverage exists only via the mock round-trip test_mock_fetch_document_split_counts_preserves_none_for_absent_in_values in rs-sdk/tests/fetch/document_count.rs, which hand-builds the DocumentSplitCounts and never invokes verify_point_lookup_count_proof_v0 on real proof bytes. The two integration tests in rs-drive that do call this verifier (around lines 728 and 852) collapse the result through entries.iter().map(|e| e.count.unwrap_or(0)).sum() and only assert the sum, so a regression to continue on None or to Some(0) would still pass. Add a proof-backed test where an In array contains a value with no matching CountTree element, run prove+verify, and assert the resulting entry has count: None (not Some(0)).

🟡 suggestion: WASM `DocumentsQueryInput` silently ignores removed legacy fields

packages/wasm-sdk/src/queries/document.rs (lines 129-155)

DocumentsQueryInput derives Deserialize without #[serde(deny_unknown_fields)], so unknown JS properties are dropped silently. The legacy returnDistinctCountsInRange knob (which toggled distinct-per-value vs. aggregate response shape under the v0 count path) is no longer present and no shim catches it. A JS caller migrating from the previous binding who still passes returnDistinctCountsInRange: true gets no error: serde drops the field, groupBy defaults to [], and the request degrades from per-distinct entries to a single aggregate count without any signal. At a language boundary, this kind of lossy decode is worse than an explicit error — it turns a migration failure into a silent semantic change. Either add #[serde(deny_unknown_fields)], or temporarily reintroduce the field as Option<bool> and reject with a typed migration error when set.

💡 Suggested change
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct DocumentsQueryInput {
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`:
- [SUGGESTION] lines 470-515: Aggregate-collapse-of-Entries branch in `dispatch_count_v1` has no dedicated test
  `dispatch_count_v1` rewrites `DocumentCountResponse::Entries` into a single `AggregateCount` server-side (lines 470–500) when `mode.is_aggregate()` but Drive routed through the per-In executor — reachable only via `select=COUNT, group_by=[], where=[In(...)], prove=false, no range` (Drive's `detect_mode` maps that to `DocumentCountMode::PerInValue` → emits `Entries`). This is the only response-shape transformation introduced by the v1 handler. None of the ported tests hit this exact wire shape: `ported_documents_count_with_in_operator` uses `group_by=["age"]` (routes to `GroupByIn`, emits Entries unchanged), `ported_documents_count_no_prove` has no In clause (Drive returns Aggregate directly via the `documents_countable` fast path), and `ported_documents_count_range_query_no_prove` exercises the drive-side `RangeNoProof → Aggregate` collapse in `drive_dispatcher.rs`, not this handler-level fold. A regression that leaks per-In rows on the wire (or produces the wrong `saturating_add` total) would still pass. Add an end-to-end test that seeds documents, issues `select=COUNT, group_by=[], where=[(field, in, [...])], prove=false`, and asserts the response is `AggregateCount(N)`.
- [SUGGESTION] lines 260-370: `limit: Some(0)` has inconsistent semantics across v1 SELECT modes
  `limit` is `optional uint32` on the wire, so `Some(0)` is a distinct value any raw-gRPC, WASM, or FFI caller can encode. The same wire bytes fan out into three behaviors: (1) `select=DOCUMENTS` (line 365) normalizes `Some(0)` via `unwrap_or(0)` and forwards it to v0, which reads `0` as 'use server default'; (2) `select=COUNT` with `mode ∈ {Aggregate, GroupByIn}` rejects `Some(0)` with `QuerySyntaxError::InvalidLimit` (line 260, `limit.is_some()` check); (3) `select=COUNT` with `mode ∈ {GroupByRange, GroupByCompound}` forwards `Some(0)` to drive's dispatcher, where it produces an empty/0-limit proof. Pick one rule consistently — either normalize `Some(0)` to `None` at handler entry, or reject `Some(0)` explicitly on the documents path — so identical encoded requests don't get three different interpretations across modes.

In `packages/rs-drive/src/verify/document_count/verify_point_lookup_count_proof/v0/mod.rs`:
- [SUGGESTION] lines 89-111: Verifier's `None` propagation on absent In branch is not tested against a real proof
  This verifier preserves `Option<Element>` from `GroveDb::verify_query` and maps it onto `SplitCountEntry::count` so SDK callers can distinguish `Some(0)` (verified zero) from `None` (proof was silent on this branch). The contract is load-bearing — it's documented in the proto comment and is the stated reason the SDK no longer synthesizes missing entries — but coverage exists only via the mock round-trip `test_mock_fetch_document_split_counts_preserves_none_for_absent_in_values` in `rs-sdk/tests/fetch/document_count.rs`, which hand-builds the `DocumentSplitCounts` and never invokes `verify_point_lookup_count_proof_v0` on real proof bytes. The two integration tests in `rs-drive` that do call this verifier (around lines 728 and 852) collapse the result through `entries.iter().map(|e| e.count.unwrap_or(0)).sum()` and only assert the sum, so a regression to `continue` on `None` or to `Some(0)` would still pass. Add a proof-backed test where an In array contains a value with no matching CountTree element, run prove+verify, and assert the resulting entry has `count: None` (not `Some(0)`).

In `packages/wasm-sdk/src/queries/document.rs`:
- [SUGGESTION] lines 129-155: WASM `DocumentsQueryInput` silently ignores removed legacy fields
  `DocumentsQueryInput` derives `Deserialize` without `#[serde(deny_unknown_fields)]`, so unknown JS properties are dropped silently. The legacy `returnDistinctCountsInRange` knob (which toggled distinct-per-value vs. aggregate response shape under the v0 count path) is no longer present and no shim catches it. A JS caller migrating from the previous binding who still passes `returnDistinctCountsInRange: true` gets no error: serde drops the field, `groupBy` defaults to `[]`, and the request degrades from per-distinct entries to a single aggregate count without any signal. At a language boundary, this kind of lossy decode is worse than an explicit error — it turns a migration failure into a silent semantic change. Either add `#[serde(deny_unknown_fields)]`, or temporarily reintroduce the field as `Option<bool>` and reject with a typed migration error when set.

Comment on lines +470 to +515
DocumentCountResponse::Entries(entries) => {
if mode.is_aggregate() {
// `select=COUNT, group_by=[]` against a request
// that drove a PerInValue execution (In + no
// range + no prove). Sum entries into a single
// aggregate before emission. `saturating_add`
// on the off-chance an operator-misconfigured
// count tree exceeds u64; realistic ceiling is
// `|In| × max_per-branch-count`, well under u64.
let total: u64 = entries
.iter()
// `count.unwrap_or(0)` here is safe: this
// arm is server-side, summing entries the
// executor emitted. Executor never emits
// `None` (that's an SDK-side
// synthesis-for-missing concept). The
// `unwrap_or(0)` is a belt-and-suspenders
// guard against any future executor that
// forgets the contract.
.map(|e| e.count.unwrap_or(0))
.fold(0u64, |a, b| a.saturating_add(b));
GetDocumentsResponseV1 {
result: Some(get_documents_response_v1::Result::Data(ResultData {
variant: Some(result_data::Variant::Counts(CountResults {
variant: Some(count_results::Variant::AggregateCount(total)),
})),
})),
metadata: Some(
self.response_metadata_v0(platform_state, CheckpointUsed::Current),
),
}
} else {
GetDocumentsResponseV1 {
result: Some(get_documents_response_v1::Result::Data(ResultData {
variant: Some(result_data::Variant::Counts(CountResults {
variant: Some(count_results::Variant::Entries(CountEntries {
entries: entries.into_iter().map(into_v1_entry).collect(),
})),
})),
})),
metadata: Some(
self.response_metadata_v0(platform_state, CheckpointUsed::Current),
),
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Aggregate-collapse-of-Entries branch in dispatch_count_v1 has no dedicated test

dispatch_count_v1 rewrites DocumentCountResponse::Entries into a single AggregateCount server-side (lines 470–500) when mode.is_aggregate() but Drive routed through the per-In executor — reachable only via select=COUNT, group_by=[], where=[In(...)], prove=false, no range (Drive's detect_mode maps that to DocumentCountMode::PerInValue → emits Entries). This is the only response-shape transformation introduced by the v1 handler. None of the ported tests hit this exact wire shape: ported_documents_count_with_in_operator uses group_by=["age"] (routes to GroupByIn, emits Entries unchanged), ported_documents_count_no_prove has no In clause (Drive returns Aggregate directly via the documents_countable fast path), and ported_documents_count_range_query_no_prove exercises the drive-side RangeNoProof → Aggregate collapse in drive_dispatcher.rs, not this handler-level fold. A regression that leaks per-In rows on the wire (or produces the wrong saturating_add total) would still pass. Add an end-to-end test that seeds documents, issues select=COUNT, group_by=[], where=[(field, in, [...])], prove=false, and asserts the response is AggregateCount(N).

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`:
- [SUGGESTION] lines 470-515: Aggregate-collapse-of-Entries branch in `dispatch_count_v1` has no dedicated test
  `dispatch_count_v1` rewrites `DocumentCountResponse::Entries` into a single `AggregateCount` server-side (lines 470–500) when `mode.is_aggregate()` but Drive routed through the per-In executor — reachable only via `select=COUNT, group_by=[], where=[In(...)], prove=false, no range` (Drive's `detect_mode` maps that to `DocumentCountMode::PerInValue` → emits `Entries`). This is the only response-shape transformation introduced by the v1 handler. None of the ported tests hit this exact wire shape: `ported_documents_count_with_in_operator` uses `group_by=["age"]` (routes to `GroupByIn`, emits Entries unchanged), `ported_documents_count_no_prove` has no In clause (Drive returns Aggregate directly via the `documents_countable` fast path), and `ported_documents_count_range_query_no_prove` exercises the drive-side `RangeNoProof → Aggregate` collapse in `drive_dispatcher.rs`, not this handler-level fold. A regression that leaks per-In rows on the wire (or produces the wrong `saturating_add` total) would still pass. Add an end-to-end test that seeds documents, issues `select=COUNT, group_by=[], where=[(field, in, [...])], prove=false`, and asserts the response is `AggregateCount(N)`.

Comment on lines +260 to +370
if request_v1.limit.is_some() && !mode.accepts_limit() {
let reason = match mode {
CountMode::Aggregate => {
"`limit` is not valid for SELECT COUNT with empty GROUP BY \
(aggregate count is a single row; omit `limit` to fix)"
}
CountMode::GroupByIn => {
"`limit` is not valid for SELECT COUNT with GROUP BY on an \
`In` field (result is bounded by the In array — capped at \
100 entries; narrow the In array directly to reduce the \
result set)"
}
CountMode::GroupByRange | CountMode::GroupByCompound => unreachable!(
"`accepts_limit()` returns true for these variants; \
outer guard already filtered them out"
),
};
return Err(QueryError::Query(QuerySyntaxError::InvalidLimit(
reason.to_string(),
)));
}

Ok(RoutingDecision::Count(mode))
}
}
}

/// Outcome of `validate_and_route` — names the path the v1 request
/// will dispatch to.
///
/// `Count(CountMode)` carries the SQL-shape contract (`Aggregate` /
/// `GroupByIn` / `GroupByRange` / `GroupByCompound`) directly; the
/// dispatcher passes it through to [`DocumentCountRequest::mode`]
/// without further translation.
enum RoutingDecision {
Documents,
Count(CountMode),
}

/// Test-only: expose the routing decision for unit tests without
/// needing a full `Platform` setup.
#[cfg(test)]
pub(super) fn validate_and_route_for_tests(
request_v1: &GetDocumentsRequestV1,
where_clauses: &[WhereClause],
) -> Result<&'static str, QueryError> {
validate_and_route(request_v1, where_clauses).map(|d| match d {
RoutingDecision::Documents => "documents",
RoutingDecision::Count(CountMode::Aggregate) => "count_aggregate",
RoutingDecision::Count(CountMode::GroupByIn) => "count_entries_via_in_field",
RoutingDecision::Count(CountMode::GroupByRange) => "count_entries_via_range_field",
RoutingDecision::Count(CountMode::GroupByCompound) => "count_entries_via_compound",
})
}

impl<C> Platform<C> {
pub(super) fn query_documents_v1(
&self,
request_v1: GetDocumentsRequestV1,
platform_state: &PlatformState,
platform_version: &PlatformVersion,
) -> Result<QueryValidationResult<GetDocumentsResponseV1>, Error> {
let where_clauses = match decode_where_clauses(&request_v1.r#where) {
Ok(c) => c,
Err(e) => return Ok(QueryValidationResult::new_with_error(e)),
};

let routing = match validate_and_route(&request_v1, &where_clauses) {
Ok(r) => r,
Err(e) => return Ok(QueryValidationResult::new_with_error(e)),
};

match routing {
RoutingDecision::Documents => {
self.dispatch_documents_v1(request_v1, platform_state, platform_version)
}
RoutingDecision::Count(mode) => {
self.dispatch_count_v1(request_v1, mode, platform_state, platform_version)
}
}
}

/// Forward a `select = DOCUMENTS` request through the v0
/// handler. v1 doesn't add any documents-side capability — the
/// SQL-shaped fields (`select`, `group_by`, `having`) are all
/// validated as documents-compatible above (empty `group_by`,
/// empty `having`, etc.) before reaching here.
fn dispatch_documents_v1(
&self,
request_v1: GetDocumentsRequestV1,
platform_state: &PlatformState,
platform_version: &PlatformVersion,
) -> Result<QueryValidationResult<GetDocumentsResponseV1>, Error> {
let start = request_v1.start.map(|s| match s {
RequestV1Start::StartAfter(b) => RequestV0Start::StartAfter(b),
RequestV1Start::StartAt(b) => RequestV0Start::StartAt(b),
});
// `limit` is `optional uint32` on v1 vs unwrapped `uint32`
// (default 0) on v0. Unset on v1 → 0 on v0 (v0 reads `0`
// as "use the server's `default_query_limit`").
let request_v0 = GetDocumentsRequestV0 {
data_contract_id: request_v1.data_contract_id,
document_type: request_v1.document_type,
r#where: request_v1.r#where,
order_by: request_v1.order_by,
limit: request_v1.limit.unwrap_or(0),
prove: request_v1.prove,
start,
};
let result = self.query_documents_v0(request_v0, platform_state, platform_version)?;
Ok(result.map(translate_documents_v0_to_v1))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: limit: Some(0) has inconsistent semantics across v1 SELECT modes

limit is optional uint32 on the wire, so Some(0) is a distinct value any raw-gRPC, WASM, or FFI caller can encode. The same wire bytes fan out into three behaviors: (1) select=DOCUMENTS (line 365) normalizes Some(0) via unwrap_or(0) and forwards it to v0, which reads 0 as 'use server default'; (2) select=COUNT with mode ∈ {Aggregate, GroupByIn} rejects Some(0) with QuerySyntaxError::InvalidLimit (line 260, limit.is_some() check); (3) select=COUNT with mode ∈ {GroupByRange, GroupByCompound} forwards Some(0) to drive's dispatcher, where it produces an empty/0-limit proof. Pick one rule consistently — either normalize Some(0) to None at handler entry, or reject Some(0) explicitly on the documents path — so identical encoded requests don't get three different interpretations across modes.

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-drive-abci/src/query/document_query/v1/mod.rs`:
- [SUGGESTION] lines 260-370: `limit: Some(0)` has inconsistent semantics across v1 SELECT modes
  `limit` is `optional uint32` on the wire, so `Some(0)` is a distinct value any raw-gRPC, WASM, or FFI caller can encode. The same wire bytes fan out into three behaviors: (1) `select=DOCUMENTS` (line 365) normalizes `Some(0)` via `unwrap_or(0)` and forwards it to v0, which reads `0` as 'use server default'; (2) `select=COUNT` with `mode ∈ {Aggregate, GroupByIn}` rejects `Some(0)` with `QuerySyntaxError::InvalidLimit` (line 260, `limit.is_some()` check); (3) `select=COUNT` with `mode ∈ {GroupByRange, GroupByCompound}` forwards `Some(0)` to drive's dispatcher, where it produces an empty/0-limit proof. Pick one rule consistently — either normalize `Some(0)` to `None` at handler entry, or reject `Some(0)` explicitly on the documents path — so identical encoded requests don't get three different interpretations across modes.

Comment on lines +145 to +150
let select = Select::try_from(request_v1.select).map_err(|_| {
not_yet_implemented(&format!(
"select value {} (not in the Select enum)",
request_v1.select
))
})?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Nitpick: Unknown Select enum integer is classified as 'not yet implemented' instead of malformed input

Select::try_from(request_v1.select).map_err(|_| not_yet_implemented("select value N (not in the Select enum)")) reuses the future-capability error class for what is structurally a malformed wire value (e.g. select = 42). not_yet_implemented carries the documented contract (line 63) that 'the request structure is fine and callers can keep it unchanged when the capability lands' — which is wrong for a garbage enum discriminant that has no future-protocol meaning. QuerySyntaxError::InvalidParameter exists and is a better fit; client error handling can then distinguish 'my request shape is wrong' from 'this server doesn't do this yet.'

💡 Suggested change
Suggested change
let select = Select::try_from(request_v1.select).map_err(|_| {
not_yet_implemented(&format!(
"select value {} (not in the Select enum)",
request_v1.select
))
})?;
let select = Select::try_from(request_v1.select).map_err(|_| {
QueryError::Query(QuerySyntaxError::InvalidParameter(format!(
"select value {} is not in the Select enum",
request_v1.select
)))
})?;

source: ['claude', 'codex']

Comment on lines 316 to 330
// Sentinel decoding for the C ABI. `-1` means "unset; use
// server-side default". The Rust-side request field is
// `Option<u32>` so `None` here is the same as the request
// omitting the field on the wire.
let limit_opt = if limit < 0 {
None
// server-side default". The DocumentQuery `limit` field is
// a `u32` with `0` as its "unset" sentinel (translated to
// `None` on the V1 wire's `optional uint32`), so the FFI
// `-1` maps to `0`.
let limit_u32: u32 = if limit < 0 {
0
} else if limit > u32::MAX as i64 {
return Err(FFIError::InternalError(format!(
"limit {} exceeds u32::MAX",
limit
)));
} else {
Some(limit as u32)
limit as u32
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 Nitpick: FFI dash_sdk_document_count limit decoding doesn't match its documented sentinel contract

The doc comment (lines 276–284) states -1 = use server default, ≥ 0 = explicit cap. The implementation (lines 321–330) maps every negative value to 0, then passes 0 to DocumentQuery::with_limit(0) — where 0 is itself the SDK's 'unset' sentinel (translated to None on the V1 wire). Net effect: -1, -2, -100, and 0 all mean 'use server default,' and 0 cannot express 'explicit cap of zero' even though the doc says it can. Either tighten the decode (-1 → unset, < -1InvalidParameter error, 0 → explicit zero cap that gets sent as Some(0)) or update the doc to state that values ≤ 0 are all treated as unset.

source: ['claude']

QuantumExplorer and others added 14 commits May 14, 2026 15:35
…de variant

`executors.rs` had one `impl Drive { ... }` block holding all
six per-mode executor methods (plus a private helper) at ~480
LoC. Each method is independent of the others — they share no
state, only the `impl Drive` block. Splitting along mode
boundaries makes "find the X-mode executor" a directory lookup
rather than a grep through 480 lines.

New layout:

    drive_document_count_query/
    └── executors/
        ├── mod.rs                    (32 LoC, module decls + overview)
        ├── total.rs                  (114 LoC) — DocumentCountMode::Total + read_primary_key_count_tree helper
        ├── per_in_value.rs           (158 LoC) — DocumentCountMode::PerInValue
        ├── range_no_proof.rs         (54 LoC)  — DocumentCountMode::RangeNoProof
        ├── range_proof.rs            (50 LoC)  — DocumentCountMode::RangeProof
        ├── range_distinct_proof.rs   (70 LoC)  — DocumentCountMode::RangeDistinctProof
        └── point_lookup_proof.rs     (91 LoC)  — DocumentCountMode::PointLookupProof

The `read_primary_key_count_tree` private helper stays in
`total.rs` (its only caller); it's `pub(super)` so a future
PointLookupProof variant that needs it can pull it in without
re-declaring.

`executors.rs` file deleted, replaced by the directory module.
No re-exports needed — each file adds methods directly to
`impl Drive`, so the dispatcher sees them on the `Drive` type
unchanged.

39 + 51 + 7 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-and-count fallback

Codex spotted Drive-side comments still describing the
pre-refactor SDK and proof architecture. None of these are
behavioral bugs — they just contradict the architecture/readability
work this PR landed.

Stale `FromProof<DocumentCountQuery>` references (the SDK
`DocumentCountQuery` type was deleted; the impl is now
`FromProof<DocumentQuery>`):

- `path_query.rs`: `aggregate_count_path_query` docstring.
- `mode_detection.rs`: the `(false, true, true, _) → PointLookupProof`
  match-arm comment.
- `drive_dispatcher.rs`: the RangeDistinctProof arm's left_to_right
  comment.

Stale "materialize-and-count" framing on PointLookupProof
(point-lookup is a CountTree-element proof primitive, not a
materialize-and-count fallback):

- `mode_detection.rs:detect_mode` docstring: replaced the
  catch-all "Equal/In point lookup, range walk, range proof,
  materialize-and-count proof, etc." list with the actual
  six-variant enumeration.
- `tests.rs:in_with_prove_routes_to_point_lookup_proof`
  docstring: rewrote to describe what PointLookupProof actually
  does (one `Element::CountTree` per In branch, verifier reads
  `count_value_or_default()` directly; O(|In| × log n), no doc
  materialization). The old "materialize path is the only
  correct route until grovedb gains a per-key count proof" was
  doubly wrong — grovedb already has CountTree-element proofs
  AND that's what this path uses.

Time-locked "pre-this-PR" framing in
`execute_point_lookup.rs`: rephrased as a timeless comparison
against the regular doc-query's materialize-and-count path.

No code change; 39 rs-drive count tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…impls become wrappers

The previous extraction (b8974a8) pulled the per-shape proof
verification into a `verify_aggregate_count` helper that returned
`Option<u64>`. `DocumentCount`'s `FromProof` shrank to a 5-line
wrapper, but `DocumentSplitCounts`'s non-aggregate branches still
carried ~150 LoC of their own dispatch: range path looked up
document_type / picked range_countable_index / built count_query /
extracted proof+metadata / called `verify_distinct_count_proof`;
no-range path did the same with point-lookup. That logic
overlapped with `verify_aggregate_count`'s internals but used
different verifier calls and a different return shape.

Collapse all four count-proof dispatch paths into a single
`verify_count_query` helper returning
`Result<(Option<Vec<SplitCountEntry>>, ResponseMetadata, Proof), _>`:

- range + non-empty group_by → `RangeDistinctProof` →
  per-distinct-value entries.
- range + empty group_by → `AggregateCountOnRange` → single u64
  wrapped as one empty-key entry.
- no range + empty where + documents_countable → primary-key
  CountTree → single u64 wrapped as one empty-key entry.
- no range + covering countable index → `PointLookupProof` →
  per-branch entries.

Wrapping the two u64-returning primitives as single empty-key
entries is the only shape massage; the verifier calls are
unchanged. Both consumers reduce to one-call wrappers:

- DocumentCount: sums entries via `filter_map(|e| e.count)`,
  wraps in `DocumentCount(u64)`.
- DocumentSplitCounts: passes entries through
  `DocumentSplitCounts::from_verified`.

File sizes:
- count_proof_helpers.rs: 240 LoC (was ~225; +15 from one extra
  helper `single_empty_key_entry` and unified case structure)
- document_count.rs: 53 LoC (was 55, basically unchanged)
- document_split_counts.rs: 65 LoC (was 225 — 71% reduction)

Net: -107 LoC across the three files. The dispatch logic now
lives in exactly one place; adding a fifth proof shape touches
`verify_count_query` and nothing else.

All 7 SDK fetch + 39 rs-drive count + 51 drive-abci v1 tests
still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tted, |In|=100 boundary accepted

Two real-proof tests catch behaviors the existing mocks/sum-only tests
miss, and one of them surfaced a misleading docstring contract that's
fixed here.

1. `test_point_lookup_proof_omits_absent_in_branches_from_entries` —
   queries `age IN [30, 40, 99, 50]` against `byAge` (99 absent), gets
   real proof bytes, and asserts the verifier emits 3 entries, not 4.
   The `point_lookup_count_path_query` doesn't set
   `absence_proofs_for_non_existing_searched_keys: true`, so grovedb's
   `verify_query` silently omits absent-Key branches from the elements
   stream rather than surfacing them as `(path, key, None)`. This
   makes the test the semantic anchor for any future flip of that
   flag — the failure message tells the next maintainer exactly what
   to do.

2. `test_count_query_in_operator_accepts_max_sized_array` — pins the
   `|In| = 100` boundary on `WhereClause::in_values()`'s cap.
   Existing tests cover < 100 (happy) and 101 (rejection); off-by-one
   in the cap would silently reject all max-sized queries while
   passing every smaller test. Asserts per-entry `Some(1)` not just
   the sum, so a verifier bug that splits counts unevenly across
   branches fails loudly.

The first test exposed that the verifier's docstring claim "grovedb
already enumerates [missing keys]" and `SplitCountEntry::count`'s
`None`-emission claim were both aspirational — `None` is reserved for
a future absence-proof variant and isn't produced today. Updated
docstrings across rs-drive (verifier + struct), rs-drive-proof-verifier
(public API), and rs-sdk (count_proof_helpers + document_split_counts)
to describe the actual contract: present branches surface as `Some(n)`
entries; absent branches are omitted; callers diff the In array
against returned `key`s to detect "queried but absent."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stContext destructure, extract v1 tests file

Three review-driven cleanups, no behavior change:

**Drop stale `removed in v1` comments from platform.proto.** The two
comments documenting that `getDocumentsCount` (rpc) and
`GetDocumentsCountRequest`/`GetDocumentsCountResponse` (messages) were
removed in v1 served the in-flight reviewer; once this PR lands they're
just commit-archeology noise that any future reader can recover from
git log. The removal is structurally evident from the absence of the
RPC and message names.

**Replace `.unwrap()` on `CostContext` with explicit destructure (5
sites).** `CostContext::unwrap()` is infallible (it discards the cost
field, not panic-on-None), so there was no DoS risk, but the visual
pattern collides with `Option/Result::unwrap` and reviewers (rightly)
flag every one. Replaced with the canonical
`let CostContext { value, cost: _ } = …` pattern the codebase already
uses in `grove_get_proved_path_query_v0`. Sites:
- `execute_range_count.rs`: In fan-out, flat summed,
  `execute_aggregate_count_with_proof`, `execute_distinct_count_with_proof`
- `execute_point_lookup.rs`: `execute_point_lookup_count_with_proof`

The destructure documents intent (we're deliberately discarding cost
because the per-mode dispatcher in `drive_dispatcher` wraps these
executors with its own fee accounting) and doesn't read as a panic-
prone unwrap to a future reviewer.

**Extract v1 `getDocuments` tests into `v1/tests.rs`.** The inline test
modules in `query/document_query/v1/mod.rs` had grown to ~1250 lines
across two `#[cfg(test)]` blocks (validate-and-route routing tests +
the full ported v0-count integration suite), pushing the file to 1827
lines and burying the 569-line production handler. Moved both blocks
into `v1/tests.rs` as a single `mod tests` with `ported_v0_count_tests`
preserved as a nested submodule. The nested module's `use super::*;`
becomes `use super::super::*;` so it still resolves to `v1/mod.rs`
items (since `super` now refers to `tests.rs`'s top level, not v1).

`mod.rs` shrinks from 1827 → 572 LoC (production-only); tests.rs is
1255 LoC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…item lint errors

Pre-existing CI failure: every macOS Tests run on this branch since
the first PR commit has been failing the `dapi-grpc` build with 9
`error: doc list item without indentation` errors at lines 1508-1525
of the generated `org.dash.platform.dapi.v0.rs` (server-side codegen).

Root cause: prost-build converts proto `//` line comments to `///`
doc comments and strips common leading whitespace. The multi-line
markdown bullets in `GetDocumentsRequestV1`'s "Phase 1 supported
shapes" section relied on a 7-space continuation indent to make each
second line visually attached to its parent `-` bullet — but the
prost-stripped output landed both lines at the same column. rustdoc
then flagged each continuation as a top-level `///` line that broke
out of the list context, which fails the build under the CI's
all-features compile path (likely via `cargo llvm-cov nextest` doctest
extraction; doesn't reproduce on the local `cargo build`-only path).

Fix: every bullet in the supported/rejected shape tables now fits on
a single source line. Comment lines wrap freely at the paragraph
level (one bullet per `//` line, no continuation lines under bullets).
Generated `dapi_grpc/platform/server/org.dash.platform.dapi.v0.rs`
lines 1508-1525 now have no continuation `///` lines under any list
item — rustdoc accepts the doc comments cleanly.

Also corrected the absent-In-branch contract paragraph at the same
spot to match the actual verifier behavior (which my last commit's
new test, `test_point_lookup_proof_omits_absent_in_branches_from_entries`,
pins). The proto comment previously claimed grovedb's `verify_query`
enumerates every queried key as `(path, key, Option<Element>)` triples
including `None` for absent ones — but the current
`point_lookup_count_path_query` doesn't set
`absence_proofs_for_non_existing_searched_keys: true`, so grovedb
silently omits absent keys from the stream. Callers detect "queried
but absent" by diffing their request's In array against the returned
entries' `key` field. Same correction I applied to rs-drive /
rs-drive-proof-verifier / rs-sdk docstrings in 396d9f3, now mirrored
onto the wire-format source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…0) uniformity, FFI limit decode

Three independent review findings from thepastaclaw / coderabbitai
addressed together because the proto contract change in #2 is the
single source of truth for both server (#2) and client (#4).

**#1: Unknown `Select` enum discriminant** classified as
`QueryError::InvalidArgument` instead of `not_yet_implemented`.
`Select::try_from(42)` is structurally malformed wire input — there's
no future protocol value that would make `42` a valid `Select`
behavior, so the future-capability error class (with its "valid
request structure, callers can keep it unchanged when capability
lands" contract from `not_yet_implemented`'s docstring) is the
wrong class. New test `reject_unknown_select_enum_value_as_invalid_argument`
pins the discriminator so a future refactor that re-collapses the
two error classes for "consistency" fails loudly.

**#2: `limit: Some(0)` uniformly invalid across SELECT modes.**
Pre-fix, three legacy behaviors collided on the same wire value:
- `SELECT DOCUMENTS` `unwrap_or(0)` and forwarded to v0, where
  `limit=0` is "use server default" (accept-as-default).
- `SELECT COUNT, {Aggregate, GroupByIn}` rejected via `is_some()`
  with mode-specific message (reject-as-invalid).
- `SELECT COUNT, {GroupByRange, GroupByCompound}` passed `Some(0)`
  to drive (accept-as-zero).

Three semantics for the same wire bytes is bad contract. The v1
wire's whole point of switching to `optional uint32` was to make
"unset" explicit (`None`), so `Some(0)` only makes sense as an
*explicit* zero — structurally meaningless regardless of mode.
Centralized `limit == Some(0)` rejection at the top of
`validate_and_route`; the existing per-mode `is_some()` checks
still catch `Some(N>0)` correctly. Updated the documents-path
`unwrap_or(0)` comment to note `Some(0)` can't reach it. Proto
docstring on `optional uint32 limit` calls out the cross-mode
contract explicitly. New test
`reject_limit_some_zero_uniformly_across_select_modes` exercises
all 5 mode combinations and asserts the centralized message fires.

**#4: FFI `dash_sdk_document_count` limit decode matches docs.**
Pre-fix doc said `-1` = unset, `≥0` = explicit cap. Implementation
mapped every negative value to `0` (SDK's unset sentinel), and `0`
was also the SDK's unset sentinel — so `-1`, `-2`, `-100`, AND `0`
all silently meant "use server default", masking caller bugs from
uninitialized memory / arithmetic underflow / etc.

Extracted the decode into `decode_ffi_limit(i64) -> Result<u32, _>`
so it's unit-testable in isolation. New contract is single-valued
per input: `-1` → unset sentinel; `> 0` → explicit cap; `0` and
`< -1` → rejected at the FFI boundary with messages directing
callers to valid alternatives. Server-side #2 rejection happens
anyway, but surfacing the rejection at the FFI is faster and
mode-independent. 5 new tests cover each sentinel category
(minus_one_is_unset, zero_is_rejected, negative_other_than_minus_one_is_rejected,
positive_decodes_verbatim, over_u32_max_is_rejected).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's macOS Tests job runs \`cargo clippy --workspace --all-features
--locked -- --no-deps -D warnings\` before the test step. Every Tests
run on this branch has been failing on clippy errors — partly from
this PR's own code, partly from pre-existing issues in unrelated
crates whose fix had landed on \`v3.1-dev\` after our branch's base
(unrelated-histories git state prevents a clean merge).

**This PR's code** (two findings introduced by the count surface):
- \`rs-sdk/src/mock/requests.rs\`: extracted the
  \`(Option<Vec<u8>>, Vec<u8>, Option<u64>)\` triple type into a
  module-level \`DocumentSplitCountTriples\` alias used by both
  \`mock_serialize\`/\`mock_deserialize\` (clippy::type_complexity).
- \`rs-sdk/src/platform/documents/count_proof_helpers.rs\`: dropped
  the explicit \`'a\` lifetime on \`verify_count_query\` since
  elision handles it (clippy::needless_lifetimes).

**Drive-by fixes** for pre-existing clippy errors that blocked CI
on every recent run (matching the fixes landed on \`v3.1-dev\` via
#3638 — applied directly because cherry-pick / merge fails on
unrelated histories):
- \`rs-platform-wallet-ffi/src/tokens/group_info.rs\`: replaced
  \`matches!(result, Err(_))\` with \`result.is_err()\` (×2 sites;
  clippy::redundant_pattern_matching).
- \`rs-platform-wallet-ffi/tests/integration_tests.rs\`: dropped
  the stale \`std::ptr::null()\` arg from
  \`platform_wallet_info_create_from_mnemonic\` test calls (×2
  sites; the function takes 3 args but tests were passing 4 —
  E0061).
- \`rs-dpp/src/withdrawal/mod.rs\`: dropped the unnecessary \`&\`
  on \`Wrap(variant)\` argument to \`bincode::serde::encode_to_vec\`
  (clippy::needless_borrows_for_generic_args). \`Pooling\` is
  \`Copy\`, so \`Wrap(variant)\` is movable inline.

Workspace clippy now clean under \`-D warnings\`:
\`RUSTFLAGS="-D warnings" cargo clippy --workspace --all-features
--locked --tests\` → \`Finished\`. All 41 drive count-query tests +
27 drive-abci v1 tests + 5 rs-sdk-ffi limit-decode tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ntable terminators

For rangeCountable indexes the terminator's value tree IS a CountTree
— its `count_value_or_default()` already equals the per-branch doc
count, because continuation property-name subtrees beneath are wrapped
`Element::NonCounted` and don't pollute the value tree's count
(see `add_indices_for_index_level_for_contract_operations_v0`'s
docstring). The point-lookup count proof's legacy
`base_path + Key([0])` shape was correct for normal `countable: true`
indexes (NormalTree value trees with `[0]`-child CountTrees) but ran
one merk layer too deep on rangeCountable, inflating proof bytes
linearly with the number of resolved branches.

This change adapts `point_lookup_count_path_query` (the single source
of truth shared by the prover and the SDK verifier) to target the
terminator's value tree directly when `self.index.range_countable`:

- **Equal-only**: pop the trailing `last_value` off `base_path` and
  use it as the query's `Key(last_value)`. Path now ends at
  `[..., last_field]`; resolved element is the value-tree CountTree.
- **In on terminator**: outer `Key(in_value)` items resolve directly
  to value-tree CountTrees; no subquery is set. grovedb returns one
  element per matched outer Key without descending another layer.
- **In + trailing Equals (terminator is a trailing Equal)**: hoist
  the trailing `termval` from `subquery_path_extension` into the
  subquery's `Key(termval)`. `subquery_path` ends at the terminator's
  property-name segment only.

Normal `countable: true` (NOT rangeCountable) keeps `Key([0])`
unchanged — load-bearing because `Element::count_value_or_default()`
returns 1 for `NormalTree`, so applying this optimization there would
silently break counts.

Verifier (`verify_point_lookup_count_proof_v0`) updates only its
per-branch key extraction: when `path.len() == base_path_len`
(reachable only via the rangeCountable In-on-terminator shape), the
In value lives in `grove_key` instead of `path[base_path_len]`. The
shared builder guarantees byte-identical `PathQuery` reconstruction
across prover and verifier — no merk-root mismatch risk.

Tests in `range_countable_point_lookup_tests` (new submodule):

- `equal_only_rangecountable_path_query_targets_value_tree_directly`
  — asserts path ends at `[..., "brand"]`, query item is
  `Key(serialize("acme"))`, no subquery; end-to-end count agreement
  between no-proof and prove.
- `in_on_rangecountable_terminator_path_query_has_no_subquery` —
  asserts no subquery set, outer Keys are the serialized In values;
  verifier demuxes back via `grove_key`. Absent In branches still
  silently omitted (semantic preserved).
- `compound_in_prefix_plus_trailing_equal_on_rangecountable_terminator`
  — compound `byBrandColor`: asserts `subquery_path = ["color"]`
  (name only, no value) and subquery's `Key` is the serialized
  terminator value. End-to-end count check.
- `normal_countable_path_query_still_targets_zero_child` —
  regression for a non-rangeCountable `byCategory`: asserts the
  legacy `Key([0])` selector is preserved (the inverse pin — a
  regression that accidentally generalized the optimization would
  fail loudly here, not silently mis-count via NormalTree's
  `count_value_or_default() = 1`).

All 45 tests in `query::drive_document_count_query::tests::` pass
(41 pre-existing + 4 new). Drive-abci v1 (27 tests) and rs-sdk-ffi
count (5 tests) suites unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g, default 100k rows

Three complementary additions to the document-count worst-case bench:

1. **`group_by_color_in_proof_100_rangecountable_branches`** —
   new bench shape that targets the existing `byColor` index
   (1-property, `rangeCountable: true`) with `color IN(100)` and
   `prove=true`. Pairs with the existing `byBrand` variant
   (`group_by_in_proof_100_count_tree_branches`) which uses the
   non-rangeCountable `byBrand` index, so the two together surface
   the byte-savings delta of the rangeCountable point-lookup
   optimization (this PR's `perf` commit). Reuses the fixture's
   100-color prefix from `color_label(0..100)` so all branches are
   *present* in the merk tree — absent branches are silently
   omitted from the verified-elements stream and would trivially
   shrink the proof without exercising the optimization.

2. **One-shot proof-size logging** — `report_proof_sizes` runs each
   proof-emitting shape once at bench setup and prints
   `[proof-size] rows=N <shape>: <bytes>` lines to stderr.
   Criterion measures wall-clock time; for count-proof work the
   load-bearing number is bytes-per-proof (smaller proofs save
   network/disk linearly with the number of resolved branches,
   while per-request wall-clock is dominated by setup and merk-
   traversal CPU). Surfacing the byte size directly removes the
   need to wire `proof.len()` into criterion's HTML output or
   re-run with ad-hoc instrumentation when reviewing a perf change.

3. **`DEFAULT_ROW_COUNT: 2_000_000 → 100_000`** — the 100k fixture
   takes ~1 minute to build vs. the 2M fixture's ~20+ minutes,
   while still being large enough that the per-branch merk paths
   exercise multiple tree layers. Callers who want the heavier
   worst-case run can set `DASH_PLATFORM_COUNT_BENCH_ROWS=2000000`
   on the command line; the default now matches the routine-
   development case rather than the worst-case-baseline case.

Measured impact of the rangeCountable optimization on the new
shape (`group_by_color_in_proof_100_rangecountable_branches`,
100k fixture):

- Pre-optimization (HEAD~1, normal `[0]`-child descent):
  **20,212 bytes**.
- Post-optimization (HEAD, value-tree-direct target):
  **10,512 bytes** — −9,700 B, **−48%**.

The non-rangeCountable control (`group_by_in_proof_100_count_tree_branches`,
byBrand) is unchanged at 22,438 bytes before and after the
optimization, confirming the optimization is correctly gated on
`Index::range_countable` and doesn't leak to indexes whose value
trees are `NormalTree`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prints `[matrix]` lines reporting drive-level no-proof + prove
outcomes for every meaningful combination of `group_by` (over the
contract's `[brand, color]` properties) and where-clause shape,
with each case annotated with the platform-level
`validate_and_route` verdict. Useful for understanding which
combinations the v1 dispatcher accepts, which the drive
dispatcher accepts but the platform layer rejects (drive's
CountMode mapping is more permissive than the platform's
`group_by-field ↔ In/range-field` alignment check), and what
proof bytes each accepted shape emits on the current fixture.

Runs once at bench setup. Output is grep-able via `[matrix]` —
each case spans four lines (label + no-proof + prove + platform).

19 cases cover:
- `group_by = []` (8 where shapes — total, single-prop ==, In,
  range, compound)
- `group_by = [color]` (4 shapes — In, range, `==`, In+range)
- `group_by = [brand]` (4 shapes — In, In+`==`, In+range, `==`)
- `group_by = [brand, color]` (2 shapes — `(In, range)` and `(In, ==)`)
- `group_by = [color, brand]` (1 shape — illustrates picker rejection
  when no rangeCountable index has `brand` as terminator)

Notable findings the matrix surfaces:
- The rangeCountable optimization activates for
  `[color] / color IN[2]` and `[color] / color IN[100]`; with the
  2-value test set the per-branch savings are visible but small,
  but scale linearly with In array length (the 100-value variant
  measured at 10,512 B post-opt vs 20,212 B pre-opt).
- Some combinations the drive layer accepts (e.g.
  `[brand] / brand==X`, `[color] / color==X`) are rejected at the
  platform layer because the `group_by` field isn't constrained
  by `In` or range — surfaced as the "Aggregate(_) but
  platform: no" rows.
- `[brand] / brand IN AND color > floor` is the most subtle case:
  drive returns `Entries(len=1)` (semantically wrong — caller
  expected per-brand entries), while the platform layer correctly
  rejects it with `single-field GROUP BY when both In and range
  clauses are present`. The matrix shows both verdicts so the
  rejection's value (vs silent shape mismatch) is concrete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Emits `[proof-bytes]` lines for each of the seven Aggregate proof
shapes — `(empty)`, `field == X` (×3), `field IN [...]` (×2),
and `range_field > floor`. Each case prints the proof's byte
length and a hex view formatted 32 bytes per row so reviewers
can inspect the actual grovedb merk-proof bytes the dispatcher
signs without rebuilding a separate verifier harness.

Useful for understanding which primitive the drive dispatcher
routes to per shape (every case shares the same ~320-byte
preamble that descends from the GroveDB root through
`[DataContractDocuments, contract_id, 1, "widget"]`; the
per-shape divergence starts after that) and for visually
correlating proof sizes with the structural differences:

- `(empty)` (585 B): doctype-level CountTree proof; just the
  primary-key fast-path descent.
- `brand == X` (1,165 B): byBrand point-lookup → trailing
  `[..., "brand", "brand_050"]` + `Key([0])`.
- `color == X` (1,327 B): byColor rangeCountable point-lookup
  → trailing `[..., "color"]` + `Key("color_00000500")`
  (post-optimization shape, no `[0]` descent).
- `brand == X AND color == Y` (1,907 B): byBrandColor
  compound rangeCountable point-lookup.
- `brand IN[2]` (1,350 B): two parallel byBrand branches.
- `color IN[2]` (1,381 B): two parallel byColor branches
  (rangeCountable).
- `color > floor` (2,072 B): AggregateCountOnRange — distinct
  primitive byte layout (range boundaries + signed u64).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed-element view

Hex bytes aren't legible for understanding what a count proof
actually proves — most of the bytes are merk hashes and per-node
metadata. Swap the hex dump for a structured display that, for
each `group_by = []` case:

1. Prints the **path query** (the prover-side spec): the path
   segments, the outer query items, and any subquery_path /
   subquery items, with byte segments decoded as quoted UTF-8
   when printable and hex otherwise.

2. Reconstructs the **same `PathQuery`** the prover used by
   calling the appropriate builder on `DriveDocumentCountQuery`
   (the single source of truth shared by prover + verifier).
   For point-lookup and primary-key shapes this routes through
   `point_lookup_count_path_query` / `primary_key_count_tree_path_query`;
   for the range-aggregate primitive it routes through
   `aggregate_count_path_query`.

3. Runs the matching grovedb verifier (`verify_query` for
   point-lookup / primary-key; `verify_aggregate_count_query` for
   the range-aggregate primitive) and prints the **verified
   payload**: the merk root hash plus, depending on shape,
   either the verified element list (path, key, CountTree's
   `count_value_or_default`) or the single aggregated count.

The output makes the rangeCountable optimization (this PR's
`perf` commit) visible at a glance: the byColor / byBrandColor
cases show `subquery items: (none)` and the outer key holding
the serialized terminator value (or, for the compound case, a
path that stops one segment short of the legacy `[0]` descent),
while the byBrand case still shows `subquery items: [Key(0x00)]`
(the unchanged normal-countable selector).

Sample output (100k-row fixture):

  [proof] [] / where=color==X (1327 bytes)
  [proof]   path: ["@", 0x...id, 0x01, "widget", "color"]
  [proof]   query items: [Key("color_00000500")]
  [proof]   verified:
  [proof]     elements (1):
  [proof]       path: ["@", ..., "widget", "color"]
  [proof]       key:  "color_00000500"
  [proof]       element: CountTree { count_value_or_default: 100 }

Compared to the byBrand control on the same fixture:

  [proof] [] / where=brand==X (1165 bytes)
  [proof]   path: ["@", ..., "widget", "brand", "brand_050"]
  [proof]   query items: [Key(0x00)]    ← `[0]` descent (legacy shape)
  [proof]   verified:
  [proof]       element: CountTree { count_value_or_default: 1000 }

Helpers added: `display_segment` (UTF-8/hex auto-detection),
`display_query_items` (all `QueryItem` variants), `display_element`
(shows `count_value_or_default` for count proofs), `hex_bytes`.

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

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "8dda08930bb98e8425630351cb198b74f84d3f5d95ecf42a2082091d9a753b6c"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

QuantumExplorer and others added 4 commits May 14, 2026 22:04
Add explicit match arms for `Element::ProvableCountTree` and
`Element::ProvableCountSumTree` (previously fell through the `_`
catch-all and would have been mislabelled as "(other)"), and
append the element's full `Debug` to the per-case output so the
variant tag is unambiguous on inspection.

This addresses a reviewer question of the form "isn't this a
ProvableCountTree?" — the existing output read e.g.
`CountTree { count_value_or_default: 100000 }` for the `(empty)`
case, and the variant choice (`CountTree` vs `ProvableCountTree`)
matters because:

- The bench fixture's widget doctype has only
  `documentsCountable: true` set at the document-type level
  (not `rangeCountable: true`); per
  `DocumentTypeRef::primary_key_tree_type`, that rule maps to
  `TreeType::CountTree`. The doctype's primary-key tree at
  `[..., "widget", 0]` is therefore inserted as
  `Element::CountTree`, not `Element::ProvableCountTree`. Setting
  `rangeCountable: true` at the doctype root level would flip
  that.

- The per-index `rangeCountable: true` flag on byColor /
  byBrandColor makes only those specific index trees use
  `ProvableCountTree` at the property-name level (e.g.
  `widget/color`) and `CountTree` at the value-tree level (e.g.
  `widget/color/color_00000500`). For the seven `group_by = []`
  proof cases dumped here, the verified element is always the
  leaf count-bearing tree, which is `Element::CountTree` in
  every case; the property-name `ProvableCountTree` is walked
  over (its merk-path is part of the proof) but it isn't
  *returned* as a verified element — instead `verify_query`
  emits the leaves the path query asked for.

The full `Debug` output now confirms this directly: every
case shows `count_tree: [hex: …] N`, the serialized form of
`Element::CountTree`. A `ProvableCountTree` would show
`provable_count_tree: …`.

The `color > floor` case is also unchanged: it uses
`verify_aggregate_count_query` (not `verify_query`), which
returns just `(root_hash, count)` — the `ProvableCountTree` at
`widget/color` is the *source* the aggregate primitive sums
over, but the verifier doesn't expose individual elements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rminators

Empirically surfaces the structural reason the rangeCountable
proof optimization can't be applied to non-rangeCountable
indexes: the optimization stops the descent at the property-name
level and uses `Key(serialized_value)` as the selector, which
means the *verified element* IS whatever's stored at the value
key. For that to yield the correct doc count, that element must
be a count-bearing variant (CountTree / ProvableCountTree). For
non-rangeCountable indexes it's `Element::Tree` (NormalTree),
whose `count_value_or_default()` returns `1` regardless of how
many docs sit underneath.

The new `probe_value_tree_types` directly inspects what grovedb
has stored at the two single-property index terminators:

  [probe] byBrand: widget/brand/brand_050 →
      Tree (NormalTree) { count_value_or_default: 1,
      debug: tree: [hex: 636f6c6f72, str: color][...] }

  [probe] byColor: widget/color/color_00000500 →
      CountTree { count_value_or_default: 100,
      debug: count_tree: [hex: 00, str: ] 100[...] }

The byBrand NormalTree's stored value is `"color"` — a pointer
to the byBrandColor continuation child living under the same
value tree. Because the parent is a NormalTree, the continuation
child contributes the default `1` to the parent's
`count_value_or_default()` rather than its own count, which is
exactly why the prove-count path has to descend one more layer
to the `[0]` CountTree to find the real doc count for byBrand.

For byColor (rangeCountable), the value tree is itself a
CountTree with count = 100; the byBrandColor continuation child
beneath is wrapped `NonCounted` so it doesn't pollute the
parent's count. That's what makes the
`path=[..., "color"], Key("color_*")` shape sound for byColor
and unsound for byBrand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rminators

The point-lookup count proof's value-tree-direct optimization
(previous commit `d9b3d3078f`) was gated on `Index::range_countable`,
which made it activate only for indexes that ALSO opted into
`AggregateCountOnRange` support. For plain `countable: true` indexes
(the common case — e.g. `byBrand` in the bench contract), the
optimization sat on the table while proofs still descended through
a redundant `[0]` merk layer per resolved branch.

This commit generalizes the rule so every countable terminator's
value tree is a `CountTree` (with sibling continuations wrapped
`NonCounted` so they don't pollute the parent count). The
optimization now activates uniformly for any
`Countable | CountableAllowingOffset` index — `range_countable` is
preserved strictly for the orthogonal `AggregateCountOnRange`
property-name-tree upgrade (`NormalTree` → `ProvableCountTree`).

## Insertion-side changes

`add_indices_for_index_level_for_contract_operations_v0` and
`add_indices_for_top_index_level_for_contract_operations_v0`:

- Split the single `sub_level_range_countable` flag into two:
  - `sub_level_is_countable_terminator` (any countability tier) —
    drives the value-tree type (`CountTree` vs `NormalTree`).
  - `sub_level_range_countable` (range-aggregate opt-in only) —
    drives the property-name tree type (`ProvableCountTree` vs
    `NormalTree`).
- Pure prefix levels (e.g. `brand` in a contract with only
  `[brand, color]` and no standalone `[brand]` index) stay
  `NormalTree`; there's no count surface to materialize at a
  prefix level.
- Renamed the propagation flag `parent_value_tree_is_range_countable`
  → `parent_value_tree_is_count_tree` to match the new semantics —
  the `NonCounted` continuation wrapping is now driven by "parent
  value tree is a CountTree" (true for every countable terminator),
  not by "range_countable specifically".

## Read-side changes

`DriveDocumentCountQuery::point_lookup_count_path_query` gates the
value-tree-direct shape on `self.index.countable.is_countable()`
(was: `self.index.range_countable`). The shape decision (path
stops one segment short of the legacy `[0]` descent; `Key(serialized_value)`
as selector; In-on-terminator skips the subquery) is unchanged.

`verify_point_lookup_count_proof_v0`'s docstring updated to reflect
the single uniform terminator shape; the in-value extraction logic
(`path.len() == base_path_len` ↔ grove_key carries In value, else
`path[base_path_len]`) is unchanged.

## Tests

- All 45 `query::drive_document_count_query::tests::` pass under
  the new layout.
- All 41 `drive::contract::insert::insert_contract::` tests pass
  (the rangeCountable e2e suite which pinned the
  `ProvableCountTree` primary-key shape continues to work; that
  invariant is independent of this change).
- `normal_countable_path_query_still_targets_zero_child` was an
  inverse pin claiming non-rangeCountable countable indexes
  *retain* the legacy `[0]` selector. That contract is now wrong —
  every countable index uses the value-tree-direct shape. Replaced
  with `plain_countable_path_query_targets_value_tree_directly`,
  which now positively asserts the optimization activates uniformly
  across countability tiers AND that `range_countable` is no longer
  the gating axis.

## Measured impact (100k-row bench fixture, byBrand = plain countable)

| Shape                    | Before (legacy) | After (uniform) | Δ          |
|--------------------------|-----------------|-----------------|------------|
| `brand == X`             | 1,165 B         | **1,041 B**     | −124 (−11%) |
| `brand IN[2]`            | 1,350 B         | **1,102 B**     | −248 (−18%) |
| `brand IN[100]`          | 22,438 B        | **10,038 B**    | **−12,400 (−55%)** |
| `color IN[100]` (control)| 10,512 B        | 10,512 B        | 0 (already optimized) |

byBrand now matches byColor's compact shape on every point-lookup
shape — they're indistinguishable to the path-query builder beyond
their per-index merk depth.

## Bench fixture schema bump

`FIXTURE_SCHEMA_VERSION: 1 → 2` so cached `/tmp/dash-platform-document-count-bench-v1-rows-*`
directories from prior runs are rebuilt automatically. Old proofs
verified against the new code would fail (different element
variants at `widget/brand/<v>`).

## Safety: pre-v12 chain replay unaffected

Count indexes (any tier) are gated to `protocol_version >= 12` per
`packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rs:356`.
Pre-v12 contracts never reach the countable-terminator branches in
the insertion code, so chain replay against the new code produces
the same merk roots as before for any block predating v12. Per
direct user confirmation, v12 has not been activated on mainnet
yet, so modifying `_v0` in place is safe rather than requiring a
versioned `_v1` migration path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iagrams

New chapter under "Drive" that walks through the bench fixture's
widget contract and shows what each count-query proof actually
proves — both the path query the prover signs AND the verified
element the verifier extracts. Every example is reproducible
against the existing bench at
`packages/rs-drive/benches/document_count_worst_case.rs`.

Seven worked examples (the `group_by = []` proof surface):

1. `(empty)` — primary-key CountTree fast path, 585 B.
2. `brand == X` — PointLookup over byBrand (plain countable),
   1,041 B post-v12 (the value-tree-direct shape now activates
   for any countable terminator, not just rangeCountable).
3. `color == X` — PointLookup over byColor (rangeCountable),
   1,327 B. Same shape as #2 — the rangeCountable flag is only
   relevant for #7.
4. `brand == X AND color == Y` — PointLookup over byBrandColor;
   the proof descends through the byBrand value tree's
   `NonCounted`-wrapped `color` continuation to the byBrandColor
   terminator, 1,911 B.
5. `brand IN [b0, b1]` — outer Keys per In value, no subquery
   (the value-tree-direct shape on the In axis), 1,102 B.
6. `color IN [c0, c1]` — identical shape to #5, surfacing the
   v12 generalization at a glance: byBrand (plain countable)
   and byColor (rangeCountable) produce structurally identical
   proofs, 1,381 B.
7. `color > floor` — AggregateCountOnRange over byColor's
   ProvableCountTree; different verifier
   (`verify_aggregate_count_query`), single u64 result, 2,072 B.

Each section has three parts:

- **Path query** (decoded path + items + subquery), the prover's
  spec.
- **Verified element / payload** (the structured output of
  `verify_query`/`verify_aggregate_count_query`).
- **Mermaid diagram** with:
  - The tree element wrapper for the relevant GroveDB subtree.
  - Blue arrows tracing the descent.
  - A cyan target node for the verified element.
  - Faded nodes for context.

A "GroveDB Layout" diagram up front shows the storage shape for
the whole contract (doctype CountTree primary key, byBrand
NormalTree property-name with CountTree value trees and
NonCounted-wrapped byBrandColor continuations, byColor
ProvableCountTree property-name with CountTree value trees).
Each per-query diagram is a focused slice of this overview.

An at-a-glance comparison table at the end summarizes primitive
choice, verified shape, and proof size across all seven examples
to make the structural symmetry between #2/#3 and #5/#6 (post-v12
generalization) and the asymmetry of #7 (AggregateCountOnRange)
visually obvious.

Registered under `Drive` in `SUMMARY.md` immediately after the
existing `Document Count Trees` chapter, which it complements
(that chapter explains the tree variants in the abstract; this
one shows how queries traverse the resulting layout). Builds
cleanly via `mdbook build` against the existing
`mdbook-mermaid` preprocessor configuration.

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

github-actions Bot commented May 14, 2026

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-05-15T03:05:38.917Z

QuantumExplorer and others added 9 commits May 14, 2026 23:26
…xamples

Count indexes (any countability tier) are a v12 feature — they
didn't exist in earlier protocol versions, so there's no
"pre-v12 layout" to contrast with. Earlier framing in Query 2
("Pre-v12 this would have descended one more layer to Key(0x00)
under brand_050") implied byBrand had a different storage shape
in an earlier protocol version, which is wrong — that
`[0]`-child descent was a transient development iteration
during v12, never a deployed protocol behavior.

Rewrote the affected passages to describe the design statically
(every countable terminator's value tree is a CountTree;
`rangeCountable` is orthogonal, opt-in for `AggregateCountOnRange`
support only) without implying a pre/post-v12 axis:

- Contract section: dropped "from protocol v12 onward" before
  the value-tree-CountTree claim; added a direct reference to
  the insertion code so the rule's source is one click away.
- Query 2 ("brand == X"): replaced the pre-v12 contrast with a
  static statement of why `brand_050` carries the count directly.
- GroveDB Layout section: changed "the rule generalizes to every
  countability tier as of v12" to "the rule applies uniformly to
  every countability tier".
- At-a-Glance: changed "The v12 generalization made the
  value-tree-direct shape uniform" to "The value-tree-direct
  shape is uniform across countability tiers".

No content / numbers / diagrams changed — only the prose framing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dex Example

Each Query section in Count Index Examples now contains the
literal output of the bench's `display_proofs` helper —
`path` + `query items` + optional `subquery items` on the
prover side, plus the verifier's `root_hash` + element list (or
aggregate `count: N` for Query 7) on the verified side. The
blocks are byte-for-byte what `cargo bench -p drive --bench
document_count_worst_case -- __nonexistent` prints to stderr
when run against the 100 000-row fixture, with only the
`[proof]` line prefix stripped.

Why this matters: the prior version of the chapter had hand-
trimmed "Path query" / "Verified element" summaries that
abstracted away the actual on-the-wire shape (no merk
`root_hash`, no `Element::Debug` content showing the
continuation pointer in `count_tree: [hex: 636f6c6f72, str:
color] 1000`, no `0x4ed22624…(32 bytes)` truncation marker for
the contract id). Showing the verbatim output makes:

- The `root_hash` reproducible — a reader can run the bench
  and confirm they get the same hash, confirming layout +
  insertion order.
- The `Element::Debug` content (`count_tree:` vs
  `provable_count_tree:`, the value-slot hex/str pair, the
  trailing count) visible — that's what proves the variant is
  `Element::CountTree`, not `Element::ProvableCountTree`, on
  every byBrand / byColor / byBrandColor terminator.
- The byBrand vs byColor continuation-pointer asymmetry
  visible (`[hex: 636f6c6f72, str: color]` on byBrand, `[hex:
  00, str:  ]` on byColor) — which is what makes byBrand's
  CountTree count correct despite the `NonCounted` wrapper
  not being directly visible in `grove.get`'s output.
- The Query 7 verifier-divergence concrete: `verified:`
  contains `count: 49900` (no `elements (N):` block), making
  the `verify_aggregate_count_query` vs `verify_query` split
  visible in the output, not just stated in prose.

Rewrote the "How To Read The Proofs" preamble to:

- Describe the new two-section structure (verbatim display +
  diagram).
- Explain the `display_segment` rendering conventions: `"@"`
  is the printable form of byte `0x40`
  (`RootTree::DataContractDocuments`); `0x...(N bytes)`
  truncation; `count_tree:` / `provable_count_tree:` variant
  markers; the `[hex: …, str: …]` value-slot field.
- Reference the bench helper directly so the reproducibility
  path ("re-run the bench") is one click away.

Per-section commentary tightened to call out exactly what the
displayed `debug:` field reveals — e.g. Query 2's continuation
pointer on byBrand's `brand_050` CountTree, Query 3's bare-`[0]`
value slot on byColor's leaf CountTree (no continuation since
byColor is single-property), Query 6's same-asymmetry note.

No diagrams changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restored the chapter's previous concise structure (per-query
Path query + Verified element + Diagram) and added a fourth
section per query: the structured proof Display.

This is the same view dash-evo-tool's Proof Log screen ([src/ui/
tools/proof_log_screen.rs in dash-evo-tool](https://github.com/
dashpay/dash-evo-tool/blob/master/src/ui/tools/proof_log_screen.rs))
shows when its display mode is set to "JSON" — bincode-decode
the proof bytes into a `GroveDBProof`, then `format!("{}",
proof)` runs the type's `Display` impl, which renders the
layered merk-proof structure inside.

To make this reproducible the bench's `display_proofs` helper
gained a fifth step that decodes each query's proof bytes via
`bincode::config::standard().with_big_endian().with_no_limit()`
(matching dash-evo-tool's config) and prints
`format!("{}", grovedb_proof)` indented under a
`[proof]   proof-display:` header. Grep `[proof]` from the
bench's stderr after running it against the 100k-row fixture
and you get the exact text inlined below in the chapter.

Each per-query Proof Display block is wrapped in a `<details>`
collapsible because the merk-path through 4-6 grovedb layers is
long (Query 4 alone is 6 layers deep — the bench's deepest
descent). Common upper layers (root → contract id → 0x01 prefix
→ widget doctype) are abbreviated `...` for the latter queries
since they're identical to Query 1's full rendering; the
bottom layers — where the actual count surface lives — are
shown verbatim.

The most informative blocks:

- Query 2 (`brand == X`): visible at the bottom is
  `Push(KVValueHashFeatureTypeWithChildHash(brand_050,
  CountTree(636f6c6f72, 1000, flags: [0, 0, 0]), ...))` —
  the `636f6c6f72` value slot is the ASCII for `"color"`
  (the byBrandColor continuation pointer), `NonCounted`-wrapped
  at the storage layer so it doesn't pollute the parent count.
- Query 3 (`color == X`): bottom layer carries `KVHashCount(
  HASH[...], N)` ops with the per-subtree counts (51 100 +
  25 500 + 12 700 + 6 300 + 3 100 + 700 + 300 + ...) summing to
  the 100 000 doc total — `ProvableCountTree`'s
  count-at-every-node property visible in the proof itself.
- Query 4 (`brand == X AND color == Y`): 6 layers deep, showing
  the descent through byBrand's value tree (which carries
  `CountTree(636f6c6f72, 1000, ...)` as an intermediate stop)
  into the byBrandColor continuation and finally to
  `CountTree(00, 1, ...)` — the single doc per `(brand, color)`
  pair the bench's deterministic schedule guarantees.
- Query 7 (`color > floor`): uses entirely different bottom-
  layer ops than the other six — `HashWithCount(kv_hash, left,
  right, count)` and `KVDigestCount(key, kv_hash, count)` —
  showing the `AggregateCountOnRange` boundary walk over the
  byColor `ProvableCountTree`. The verified payload is the
  summed `count: 49900` rather than an element list, and no
  individual `CountTree(…)` appears anywhere — the running
  totals inside the boundary ops *are* the proof's count
  surface.

The "How To Read The Proofs" preamble now describes the four
sections and links the dash-evo-tool reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-by, visualizer links

Chapter 29 (Count Index Examples) additions:
- Query 8 section (brand == X AND color > floor) — verbatim proof, conceptual
  flowchart, per-layer Layer-5+ diagram, Layer-8 binary merk-tree diagram
- Hash composition worked example — Blake3 formulas + step-by-step recomposition
  of color_00000511's node_hash from its KVDigestCount op and HashWithCount right
  child
- Top navigation table with Filter, Complexity, Avg time, Proof size columns
- Per-query GroveDB Proof Visualizer links on every Q1-Q8 details summary

Chapter 30 (Count Index Group By Examples) — new chapter:
- "When group_by changes the proof (and when it doesn't)" framing — group_by as
  both result-shaping (SDK) and proof-shaping (prover) directive
- Six G* sections (G1-G6) following the Q-section template
- "Group-By Shapes That Are Not Allowed" — four buckets with reasons, including
  the brand IN + color > floor + group_by=[brand] aggregate-style case marked as
  "incoming" pending grovedb's AggregateCountOnRange-as-subquery extension
- Per-query GroveDB Proof Visualizer links on every G* section

Bench (packages/rs-drive/benches/document_count_worst_case.rs):
- query_1 through query_8 criterion bench_function calls (per-query timings)
- query_g1 through query_g6 criterion bench_function calls
- display_group_by_proofs helper that emits [gproof]-tagged verbatim merk proofs
  for G3-G6
- Matrix case for brand == X AND color > floor (Aggregate prove path, 2656 B)

Visualizer links use the gzip+base64url URL fragment scheme documented at
https://github.com/dashpay/grovedb-proof-visualizer-widget/blob/master/prompts/link-from-platform-book.md

All 14 proofs verified to round-trip byte-identical through the encode pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ bench probe

Pulls in dashpay/grovedb#663 ("feat(grovedb,query): allow AggregateCountOnRange
as carrier subquery"), merge commit 87554188ad — bumps every pinned
`grovedb*` dep across the workspace from a917d92d to 87554188.

The new grovedb APIs:
- `Query::validate_carrier_aggregate_count_on_range`
- `Query::validate_leaf_aggregate_count_on_range`
- `GroveDb::query_aggregate_count_per_key`
- `GroveDb::verify_aggregate_count_query_per_key`

These unblock the chapter 30 G7 shape (`brand IN[...] AND color > floor` with
`group_by = [brand]`) at the grovedb layer. Drive wire-up is the next step
(lift mode_detection.rs:140 rejection + new path-query builder + new
DocumentCountMode variant + per-key verifier wrapper).

This commit also adds `probe_carrier_acor` to document_count_worst_case bench
as a feasibility smoke test against the existing 100 000-row widget fixture:

  [carrier-acor] no-proof entries (2):
    ("brand_000", 499)
    ("brand_001", 499)
  [carrier-acor] proof bytes: 4332 B
  [carrier-acor] verified root_hash: 0x62ee7348f4d28dd9d7cf86a6c725fa8276...
  [carrier-acor] verified entries (2):
    ("brand_000", 499)
    ("brand_001", 499)

4 332 B for k=2, matching the complexity estimate documented in chapter 30
(`O(k · (log B + log C'))`) and ~17 % smaller than the concatenated-leaf-ACOR
alternative.

Verification:
- cargo check --workspace clean
- cargo test -p drive --lib drive_document_count_query → 45 tests passing
- cargo test -p drive --lib verify → 240 tests passing
- All chapter 29 / chapter 30 proof sizes (585 / 1041 / 1327 / 1911 / 1102 /
  1381 / 2072 / 2656 B for Q1..Q8) unchanged — bump is purely additive
- Carrier-ACOR end-to-end: query → prove → verify all round-trip with root
  hash matching the chapter's known 0x62ee7348... fixture root

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r range)

Lifts the `mode_detection.rs:140` rejection for the `GroupByIn + In + range +
prove` case, now that grovedb PR #663 exposes `AggregateCountOnRange` as a
carrier subquery under outer `Keys`. The new shape returns one
`(in_key, u64)` per resolved In branch in a single proof:

  brand IN ["brand_000", "brand_001"] AND color > "color_00000500"
  group_by = [brand]   →  Entries([("brand_000", 499), ("brand_001", 499)])
                          Proof(4 332 bytes), median 255.9 µs

Drive-side wire-up
- New `DocumentCountMode::RangeAggregateCarrierProof` variant.
- `mode_detection::detect_mode`: rejection lifted for `CountMode::GroupByIn`
  carrying `(In + range)`; routes to the new mode. Aggregate-mode `In +
  range + prove` still rejected (no group_by means caller asks for a
  single sum, which carrier-ACOR can't safely produce without verifier
  trust in the SDK's summation).
- New `DriveDocumentCountQuery::carrier_aggregate_count_path_query` builder
  in `path_query.rs` — outer Keys per In value, subquery_path through the
  index's middle properties (`==` clauses) plus the terminator name,
  subquery is `Query::new_aggregate_count_on_range(range_item)`. Mirrors
  the In-fan-out pattern from `distinct_count_path_query` but with the
  carrier-ACOR subquery composition.
- New executor `Drive::execute_document_count_range_aggregate_carrier_proof`
  in `executors/range_aggregate_carrier_proof.rs`, plus inner
  `DriveDocumentCountQuery::execute_carrier_aggregate_count_with_proof`
  in `execute_range_count.rs`.
- New verifier `DriveDocumentCountQuery::verify_carrier_aggregate_count_proof`
  (`v0`) wrapping `GroveDb::verify_aggregate_count_query_per_key`. Returns
  `(RootHash, Vec<(Vec<u8>, u64)>)`.
- Added the new method version to
  `DriveVerifyDocumentCountMethodVersions` (defaults to `0` in `v1.rs`).
- Dispatcher arm in `drive_dispatcher.rs` matches the new mode and
  returns `DocumentCountResponse::Proof(...)`.

Bench
- Matrix entry `[brand] / where=brand IN[2] AND color > floor` flipped
  from rejected ("no — single-field GROUP BY with both `In` and range")
  to allowed ("yes (RangeAggregateCarrierProof — carrier ACOR per In
  branch)") with `prove: Proof(4 332 bytes)`.
- New `query_g7_brand_in_color_gt_grouped_by_brand` criterion bench —
  10 samples × 9 295 iters, median 255.87 µs (~4× Q8's 71 µs because
  it's two parallel Q8-shaped descents).
- New `G7` case in `display_group_by_proofs` emits the full 186-line
  carrier-ACOR proof verbatim as `[gproof] G7 [brand] / where=...`.

Chapter 30 (`book/src/drive/count-index-group-by-examples.md`)
- G7 added to the navigation table (`O(k · (log B + log C'))`, 255.9 µs,
  4 332 B, `Entries(2 groups, sum = 998)`).
- New "G7 — Carrier `In` + Range, Grouped By `brand`" section with the
  same template as G1..G6: path query, verified payload, proof size,
  proof display (schematic + interactive visualizer link), narrative,
  conceptual flowchart, per-layer (Layer-5+) merk-tree diagram.
- "Group-By Shapes That Are Not Allowed" section's bucket #4 removed
  (the "incoming" placeholder), replaced with a historical note
  pointing forward to G7.

Tests + verification
- `cargo test -p drive --lib drive_document_count_query` → 45 passing
- `cargo test -p drive --lib verify` → 240 passing
- All chapter 29 / chapter 30 documented proof sizes (Q1..Q8, G3..G6)
  unchanged — wire-up is purely additive on top of grovedb PR #663.
- mdBook builds clean.

Closes (in this PR): chapter 30 G7 placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `probe_carrier_acor_range_outer` — the natural extension of
`probe_carrier_acor` from `outer In` to `outer Range`. Confirms the
grovedb feature works for our widget fixture:

  [carrier-acor-range] probing: widget/brand RangeAfter(brand_050..)
                                subquery_path=color
                                subquery=AggregateCountOnRange(RangeAfter(color_00000500..))
  [carrier-acor-range] no-proof entries (49):
    ("brand_051", 499) … ("brand_099", 499)
  [carrier-acor-range] proof bytes: 84576 B
  [carrier-acor-range] verified root_hash: 0x62ee7348… (matches chapter fixture)
  [carrier-acor-range] verified entries (49):
    ("brand_051", 499) … ("brand_099", 499)

Findings worth recording:

1. The outer-Range carrier-ACOR shape WORKS at the grovedb layer
   (grovedb PR #663's `validate_carrier_aggregate_count_accepts_range_outer_items`
   covers this). The widget fixture's 49 brands > "brand_050" each
   carry the expected per-brand count of 499 colors > "color_00000500"
   (the bench's fixture has 1 doc per (brand, color) pair).

2. **`SizedQuery::limit` is rejected on any ACOR-bearing query**
   (carrier or leaf) — grovedb's validator returns
   `InvalidQuery("AggregateCountOnRange queries may not set SizedQuery::limit")`.
   So "limit the outer Range walk to N matches" can't be expressed
   today; this probe walks the full 49 brands and pays the resulting
   ~83 KB proof size. The natural drive-level workaround is to
   compute an explicit upper bound for the outer Range from the
   requested limit (e.g. rewrite `brand > X` with `limit = 20` to
   `brand > X AND brand <= caller_supplied_or_precomputed_Y`) — but
   that pushes the upper-bound responsibility to the caller or
   requires an extra grovedb read.

3. Drive doesn't wire this through yet: `mode_detection::detect_mode`
   rejects ≥2 range clauses up front ("count query supports at most
   one range where-clause"), independent of the limit issue. The
   shape is logged here as future work for chapter 30; lifting it
   requires both the multi-range-rejection in mode_detection AND a
   grovedb-level extension to support `SizedQuery::limit` on carrier
   ACOR (or a drive-level upper-bound computation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Targets grovedb PR #664 head (4e20c338) — the follow-up to PR #663 that
relaxes the leaf-strict `SizedQuery::limit` rule for carrier ACOR queries.

The new shape unblocks the `Q8`-with-outer-range case:

  brand > "brand_050" AND color > "color_00000500"
  group_by = [brand]
  limit = 20

  →  Entries([("brand_051", 499), …, ("brand_070", 499)])
     Proof(35 122 bytes), median 1.07 ms

The proof bytes scale linearly with `limit`: 20 outer matches × ~1 700 B
per per-brand ACOR boundary walk ≈ 35 KB, matching the per-In slope
established by Q8 vs G7.

Drive-side wire-up extending the existing `RangeAggregateCarrierProof`
mode:

- `mode_detection`: lifts `range_count > 1` for the specific shape
  (CountMode::GroupByRange + prove + exactly two range clauses on
  distinct fields + no In). Routes to the existing
  `DocumentCountMode::RangeAggregateCarrierProof` arm.
- `path_query::carrier_aggregate_count_path_query`: generalized to
  accept either an `In` clause (G7 shape) OR a range clause (G8
  shape) at the carrier position. The terminator's range becomes
  the inner ACOR `QueryItem`. Builder now takes `limit: Option<u16>`
  and threads it to `SizedQuery::new`.
- `execute_carrier_aggregate_count_with_proof`: accepts `limit`,
  passes through to the builder.
- `Drive::execute_document_count_range_aggregate_carrier_proof`:
  accepts `limit`, plumbs through.
- `drive_dispatcher`: extracts `request.limit`, validates against
  `max_query_limit` (same validate-don't-clamp policy as
  `RangeDistinctProof`), passes through.
- `verify_carrier_aggregate_count_proof[_v0]`: accepts `limit`,
  rebuilds the same `PathQuery` byte-for-byte.
- `index_picker::find_range_countable_index_for_where_clauses`:
  extended to accept the two-range case — finds an index whose
  first property carries one range (the outer carrier) and whose
  terminator carries the other (the inner ACOR). Single-range case
  is unchanged.
- `drive_dispatcher::where_clauses_from_value`: catches the system-
  wide parser's `MultipleRangeClauses` error for count queries.
  Structural validation lives in `detect_mode`; the regular-query
  parser's "all ranges must be on same field" rule was rejecting
  the G8 shape upstream.

Bench:
- Matrix entry `[brand] / where=brand > floor AND color > floor
  (limit 20)` flipped from rejected to `Proof(35 122 bytes)`.
- New `query_g8_brand_gt_color_gt_grouped_by_brand_limit_20`
  criterion bench — 10 samples × 5 060 iters, median 1.07 ms
  (~4× G7's 256 µs because the outer walk is 10× longer).
- New `G8` case in `display_group_by_proofs`.
- `probe_carrier_acor_range_outer` now uses `limit = 20` (was
  unbounded in the previous commit's feasibility probe). Proof
  shrinks from ~83 KB to 35 122 B.

Chapter 30:
- G8 row in the nav table (`O(L · (log B + log C'))`, 1 072 µs,
  35 122 B, `Entries(20 groups, sum = 9 980)`).
- New "G8 — Carrier outer Range + Range, Grouped By `brand`"
  section with the same template as G1..G7: path query, verified
  payload, schematic proof display + interactive visualizer link,
  conceptual flowchart, per-layer (Layer-5+) merk-tree diagram.
- Complexity variables note extends `k` (|IN|) with `L` (the
  caller's `limit` for the Range-outer carrier shape).

Tests + verification:
- cargo test -p drive --lib drive_document_count_query → 45 passing
- cargo test -p drive --lib verify → 240 passing
- All previously documented proof sizes unchanged (Q1..Q8 / G3..G7
  byte-identical to before)
- G8 end-to-end via the dispatcher matches the standalone grovedb-
  level probe (35 122 B; root hash 0x62ee7348… matches chapter
  fixture)
- mdBook builds clean

The grovedb rev points at PR #664's open head (4e20c338); will be
rebased to the merge commit once the PR lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…db to PR #664 merge

Bumps grovedb to the merged PR #664 commit (7a649386) and adopts the
platform-wide hardcoded outer-walk cap for G8's range+ACOR carrier
shape:

  brand > "brand_050" AND color > "color_00000500"
  group_by = [brand]
  prove = true                # caller MUST NOT pass `limit`

  →  Entries([("brand_051", 499), …, ("brand_075", 499)])
     Proof(43 638 bytes), median 1.26 ms

The hardcoded constant `CARRIER_AGGREGATE_OUTER_RANGE_LIMIT = 25`
lives in `query/drive_document_count_query/mod.rs` alongside
`MAX_LIMIT_AS_FAILSAFE`. The dispatcher rejects any caller-supplied
`limit` on this shape — the cap is part of the per-shape structural
contract, not a runtime knob. Two reasons:

1. **Prover/verifier byte-for-byte agreement.** `SizedQuery::limit`
   feeds the merk-root reconstruction; if the caller could pick the
   value, the verifier would need a side channel to know which one
   was used. Hardcoding keeps proof bytes deterministic across
   callers.
2. **Proof-size bounding.** Linear in the cap (~1 700 B per outer
   match). 25 keeps the worst case under 50 KB (Tier-2 in the
   visualizer's shareable-link guidance) while covering typical
   "top-N brands by outer range" queries. Callers wanting fewer
   results narrow the where-clause range; callers wanting more
   results call repeatedly with disjoint outer-range windows.

For the In-outer (G7) shape, the dispatcher also rejects any
caller-supplied `limit` now — the In array's length already bounds
the result, and accepting a sub-|In| limit would silently change
which In-branches appear in the proof.

Drive:
- New constant `CARRIER_AGGREGATE_OUTER_RANGE_LIMIT: u16 = 25` in
  `drive_document_count_query/mod.rs`.
- Dispatcher: split the `RangeAggregateCarrierProof` arm into the
  In-outer case (`limit` must be `None`) and the Range-outer case
  (`limit` must be `None` from the caller, hardcoded to 25 internally).
  Rejects caller-supplied `limit` on both with a clear error message.

Bench:
- `query_g8_brand_gt_color_gt_grouped_by_brand_limit_20` →
  `query_g8_brand_gt_color_gt_grouped_by_brand` (caller passes `None`
  now; the cap is platform-set).
- Matrix entry label drops "(limit 20)"; verdict updated to mention
  the platform-cap.
- `display_group_by_proofs` G8 case: caller passes `None`.
- `probe_carrier_acor_range_outer` switched from `limit = 20` to
  `limit = 25` to match the platform's hardcoded value (the probe
  calls grovedb directly, so it sets the SizedQuery limit itself).

Empirical numbers from the new bench run (100 000-row warmed fixture):
- Q8         (brand == X  AND color > floor)                : 71 µs  / 2 656 B
- G7  (k=2)  (brand IN[2] AND color > floor)                : 256 µs / 4 332 B
- G8 (L=25)  (brand > X   AND color > floor, group_by [brand]): 1.26 ms / 43 638 B

Per-outer-match slope of ~1 700 B and ~50 µs holds across all three rows.

Chapter 30:
- G8 nav-table row updated to the new numbers (25 entries, 43 638 B,
  1 260 µs, sum = 12 475).
- New "Why the limit is hardcoded" subsection with the two-reason
  rationale.
- All G8 prose, flowchart, and per-layer diagram references to
  "20" / "brand_070" / "9 980" / "35 122 B" replaced with "25" /
  "brand_075" / "12 475" / "43 638 B".
- New visualizer link for the 25-entry proof (encoded payload size
  ≈ 56 KB — Tier-2; works in modern browsers, may truncate in some
  link-preview surfaces).
- Complexity-variables note now describes `L` as "platform-wide
  outer-walk cap" rather than "caller's limit".

Tests:
- cargo test -p drive --lib → 3 141 passing
- All previously documented proof sizes unchanged for Q1..Q8 / G1..G7
- mdBook build clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants