feat: add time-range indexes for trending/leaderboard queries#3740
feat: add time-range indexes for trending/leaderboard queries#3740QuantumExplorer wants to merge 1 commit into
Conversation
Introduces a `timeRange` index transform that buckets a timestamp index property (e.g. $createdAt) into fixed-length, regularly-spaced, optionally overlapping ranges. A document is indexed under every range whose window contains its timestamp, enabling provable "trending"/leaderboard queries (ORDER BY COUNT(*) within the most-recent range). - rs-dpp: TimeRangeTransform on Index/IndexLevel, JSON parsing + meta-schema, validation (range % step, overlap cap, first-property, timestamp source, non-unique/non-contested), protocol v12 gate, update-immutability, bucket math - rs-drive: insert/delete/update index fan-out (one document -> N overlapping bucket entries); time-range query resolution to a concrete bucket equality - dapi-grpc: new v1 IN_TIME_RANGE where operator (v0 wire unchanged) - drive-abci: v1 handler resolves IN_TIME_RANGE from authoritative block time - rs-sdk/wasm-sdk: with_time_range / timeRange query builders; proof verifier re-derives the bucket from the quorum-signed response metadata time Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR introduces time-range index bucketing for Dash Platform v1 document queries. It adds a ChangesTime-Range Index Bucketing for Document Queries
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v3.1-dev #3740 +/- ##
============================================
- Coverage 87.16% 87.15% -0.01%
============================================
Files 2607 2606 -1
Lines 319420 319221 -199
============================================
- Hits 278413 278216 -197
+ Misses 41007 41005 -2
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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-dpp/src/data_contract/document_type/index/time_range.rs`:
- Around line 85-97: The method containing_buckets currently uses
most_recent_start which saturates to origin_ms, causing t < origin_ms to
incorrectly return origin_ms; to fix, add an early check at the start of
containing_buckets: if t < self.origin_ms return an empty Vec, then proceed as
before (using overlap_factor, most_recent_start, step_ms) so only timestamps >=
origin_ms are considered for bucket computation; reference functions/fields:
containing_buckets, most_recent_start, origin_ms, overlap_factor, step_ms,
range_ms.
In `@packages/rs-drive-abci/src/query/document_query/v1/mod.rs`:
- Around line 468-536: The test helper validate_and_route_for_tests() no longer
mirrors query_documents_v1() because it decodes proto where-clauses without
stripping/resolving IN_TIME_RANGE clauses; update validate_and_route_for_tests()
to partition proto_where_clauses with conversions::is_time_range_clause, decode
normal_proto via conversions::where_clauses_from_proto into where_clauses, and
then handle time_range_proto the same way as query_documents_v1(): obtain
block_time_ms from platform_state.last_committed_block_time_ms(), resolve
contract_id and contract_fetch_info, get doc_type, and for each proto_wc call
conversions::time_range_clause_from_proto and
drive::query::resolve_time_range_bucket_clause to push resolved clauses into
where_clauses (or extract this shared logic into a helper used by both
query_documents_v1() and validate_and_route_for_tests()).
In
`@packages/rs-drive/src/drive/document/update/internal/update_document_for_contract_operations/v0/mod.rs`:
- Around line 775-806: The current encode_buckets closure maps None to an empty
Vec, losing the single empty-index-key convention used by insert/delete paths;
change the logic so that when the raw decoded timestamp is None (or the raw
encoded bytes represent an empty key) encode_buckets returns a Vec containing
one empty Vec (i.e., vec![vec![]]) so old_buckets/new_buckets preserve the
empty-key; update the use sites around get_raw_for_document_type,
DocumentPropertyType::decode_date_timestamp, transform.containing_buckets and
DocumentPropertyType::encode_date_timestamp accordingly and add a regression
test covering null ↔ non-null updates to ensure delete/insert of the empty-key
behaves correctly.
In `@packages/rs-drive/src/query/mod.rs`:
- Around line 539-571: resolve_time_range_bucket_clause currently returns only a
synthetic WhereClause (field == bucket_start) which loses the fact that this
came from a time-range index; change it to return the matched index/transform as
well (e.g. return a tuple or new struct like (WhereClause, IndexRef) or
(WhereClause, TimeRangeTransform)) so downstream index pickers can see the
original time-range index and avoid choosing a non-time-range index;
specifically, in resolve_time_range_bucket_clause locate the matched
index/transform (the variable transform found via
DocumentTypeRef::indexes().values().find_map), include that transform or the
index identifier in the function return value, and update all callers to accept
and thread that hint into the query planner so index selection uses the provided
time-range index rather than falling back to coverage-based selection.
In `@packages/rs-sdk/src/platform/documents/document_query.rs`:
- Around line 77-84: The conversion impl TryFrom<&DocumentQuery> for
DriveDocumentQuery currently ignores DocumentQuery::time_range_clauses; update
the impl(s) that build DriveDocumentQuery from a DocumentQuery (the
TryFrom<&DocumentQuery> for DriveDocumentQuery and the analogous conversion used
elsewhere) to detect non-empty time_range_clauses and return an Err immediately
instead of silently dropping them. Specifically, check
DocumentQuery::time_range_clauses at the start of the conversion, and if not
empty return a clear error (e.g., UnsupportedTimeRangeInDriveQuery) referencing
that the caller must use Self::with_time_range or a block-time-aware conversion
path; do this for every conversion path that currently only uses where_clauses
so the time-range filters are not lost.
🪄 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: 0585dcb0-39b1-41af-af09-9ff3e81814c8
📒 Files selected for processing (23)
packages/dapi-grpc/protos/platform/v0/platform.protopackages/rs-dpp/schema/meta_schemas/document/v1/document-meta.jsonpackages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v1/mod.rspackages/rs-dpp/src/data_contract/document_type/index/mod.rspackages/rs-dpp/src/data_contract/document_type/index/random_index.rspackages/rs-dpp/src/data_contract/document_type/index/time_range.rspackages/rs-dpp/src/data_contract/document_type/index_level/find_first_change.rspackages/rs-dpp/src/data_contract/document_type/index_level/mod.rspackages/rs-drive-abci/src/query/document_query/v1/conversions.rspackages/rs-drive-abci/src/query/document_query/v1/mod.rspackages/rs-drive/src/drive/document/delete/remove_indices_for_top_index_level_for_contract_operations/v1/mod.rspackages/rs-drive/src/drive/document/insert/add_document_for_contract/mod.rspackages/rs-drive/src/drive/document/insert/add_indices_for_top_index_level_for_contract_operations/v1/mod.rspackages/rs-drive/src/drive/document/update/internal/update_document_for_contract_operations/v0/mod.rspackages/rs-drive/src/query/drive_document_count_query/tests.rspackages/rs-drive/src/query/drive_document_sum_query/tests.rspackages/rs-drive/src/query/mod.rspackages/rs-sdk/src/platform/dashpay/contact_request_queries.rspackages/rs-sdk/src/platform/documents/document_query.rspackages/rs-sdk/src/platform/dpns_usernames/mod.rspackages/rs-sdk/src/platform/dpns_usernames/queries.rspackages/wasm-sdk/src/dpns.rspackages/wasm-sdk/src/queries/document.rs
| pub fn containing_buckets(&self, t: u64) -> Vec<u64> { | ||
| let overlap = self.overlap_factor(); | ||
| if overlap == 0 { | ||
| return Vec::new(); | ||
| } | ||
| let newest = self.most_recent_start(t); | ||
| (0..overlap) | ||
| .filter_map(|j| { | ||
| let offset = j.checked_mul(self.step_ms)?; | ||
| newest.checked_sub(offset) | ||
| }) | ||
| .filter(|start| *start >= self.origin_ms) | ||
| .collect() |
There was a problem hiding this comment.
Return no buckets before origin_ms.
For t < origin_ms, most_recent_start() saturates to origin_ms, so this method returns [origin_ms] even though the interval [origin_ms, origin_ms + range_ms) does not contain t. A contract with a positive origin will therefore index pre-origin documents into a bucket they do not belong to.
Proposed fix
pub fn containing_buckets(&self, t: u64) -> Vec<u64> {
+ if t < self.origin_ms {
+ return Vec::new();
+ }
let overlap = self.overlap_factor();
if overlap == 0 {
return Vec::new();
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pub fn containing_buckets(&self, t: u64) -> Vec<u64> { | |
| let overlap = self.overlap_factor(); | |
| if overlap == 0 { | |
| return Vec::new(); | |
| } | |
| let newest = self.most_recent_start(t); | |
| (0..overlap) | |
| .filter_map(|j| { | |
| let offset = j.checked_mul(self.step_ms)?; | |
| newest.checked_sub(offset) | |
| }) | |
| .filter(|start| *start >= self.origin_ms) | |
| .collect() | |
| pub fn containing_buckets(&self, t: u64) -> Vec<u64> { | |
| if t < self.origin_ms { | |
| return Vec::new(); | |
| } | |
| let overlap = self.overlap_factor(); | |
| if overlap == 0 { | |
| return Vec::new(); | |
| } | |
| let newest = self.most_recent_start(t); | |
| (0..overlap) | |
| .filter_map(|j| { | |
| let offset = j.checked_mul(self.step_ms)?; | |
| newest.checked_sub(offset) | |
| }) | |
| .filter(|start| *start >= self.origin_ms) | |
| .collect() | |
| } |
🤖 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-dpp/src/data_contract/document_type/index/time_range.rs` around
lines 85 - 97, The method containing_buckets currently uses most_recent_start
which saturates to origin_ms, causing t < origin_ms to incorrectly return
origin_ms; to fix, add an early check at the start of containing_buckets: if t <
self.origin_ms return an empty Vec, then proceed as before (using
overlap_factor, most_recent_start, step_ms) so only timestamps >= origin_ms are
considered for bucket computation; reference functions/fields:
containing_buckets, most_recent_start, origin_ms, overlap_factor, step_ms,
range_ms.
| // Partition out time-range (IN_TIME_RANGE) clauses. They are resolved | ||
| // to concrete equality clauses on the bucketed source field using the | ||
| // authoritative committed block time, so the rest of the v1 pipeline | ||
| // (routing, executors, proofs) treats them as ordinary equality | ||
| // lookups. The verifier re-derives the same bucket from the | ||
| // quorum-signed response metadata time, so the proof matches. | ||
| let (time_range_proto, normal_proto): (Vec<_>, Vec<_>) = proto_where_clauses | ||
| .into_iter() | ||
| .partition(conversions::is_time_range_clause); | ||
|
|
||
| let mut where_clauses = match conversions::where_clauses_from_proto(normal_proto) { | ||
| Ok(c) => c, | ||
| Err(e) => return Ok(QueryValidationResult::new_with_error(e)), | ||
| }; | ||
|
|
||
| if !time_range_proto.is_empty() { | ||
| let block_time_ms = | ||
| match platform_state.last_committed_block_time_ms() { | ||
| Some(t) => t, | ||
| None => return Ok(QueryValidationResult::new_with_error(QueryError::Query( | ||
| QuerySyntaxError::Unsupported( | ||
| "a time range (IN_TIME_RANGE) query requires a committed block time" | ||
| .to_string(), | ||
| ), | ||
| ))), | ||
| }; | ||
| let contract_id: Identifier = check_validation_result_with_data!(data_contract_id | ||
| .clone() | ||
| .try_into() | ||
| .map_err(|_| QueryError::InvalidArgument( | ||
| "id must be a valid identifier (32 bytes long)".to_string() | ||
| ))); | ||
| let (_, contract_fetch_info) = self.drive.get_contract_with_fetch_info_and_fee( | ||
| contract_id.to_buffer(), | ||
| None, | ||
| true, | ||
| None, | ||
| platform_version, | ||
| )?; | ||
| let contract_fetch_info = check_validation_result_with_data!(contract_fetch_info | ||
| .ok_or(QueryError::Query(QuerySyntaxError::DataContractNotFound( | ||
| "contract not found when resolving a time range query", | ||
| )))); | ||
| let contract_ref = &contract_fetch_info.contract; | ||
| let doc_type = check_validation_result_with_data!(contract_ref | ||
| .document_type_for_name(document_type.as_str()) | ||
| .map_err(|_| QueryError::InvalidArgument(format!( | ||
| "document type {} not found for contract {}", | ||
| document_type, contract_id | ||
| )))); | ||
| for proto_wc in time_range_proto { | ||
| let (field, selector) = match conversions::time_range_clause_from_proto(proto_wc) { | ||
| Ok(parsed) => parsed, | ||
| Err(e) => return Ok(QueryValidationResult::new_with_error(e)), | ||
| }; | ||
| match drive::query::resolve_time_range_bucket_clause( | ||
| &field, | ||
| selector, | ||
| doc_type, | ||
| block_time_ms, | ||
| ) { | ||
| Ok(resolved) => where_clauses.push(resolved), | ||
| Err(drive::error::Error::Query(qe)) => { | ||
| return Ok(QueryValidationResult::new_with_error(QueryError::Query(qe))) | ||
| } | ||
| Err(e) => return Err(e.into()), | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Keep the test-only routing mirror aligned with the new IN_TIME_RANGE flow.
query_documents_v1() now strips and resolves IN_TIME_RANGE clauses before normal where-clause decoding, but validate_and_route_for_tests() still decodes the full proto where-clause list directly. Any test request that includes a time-range clause will now fail in the helper with InvalidArgument even though the real handler accepts and resolves it, so the “same gate ordering” contract in this file is no longer true.
🤖 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-drive-abci/src/query/document_query/v1/mod.rs` around lines 468 -
536, The test helper validate_and_route_for_tests() no longer mirrors
query_documents_v1() because it decodes proto where-clauses without
stripping/resolving IN_TIME_RANGE clauses; update validate_and_route_for_tests()
to partition proto_where_clauses with conversions::is_time_range_clause, decode
normal_proto via conversions::where_clauses_from_proto into where_clauses, and
then handle time_range_proto the same way as query_documents_v1(): obtain
block_time_ms from platform_state.last_committed_block_time_ms(), resolve
contract_id and contract_fetch_info, get doc_type, and for each proto_wc call
conversions::time_range_clause_from_proto and
drive::query::resolve_time_range_bucket_clause to push resolved clauses into
where_clauses (or extract this shared logic into a helper used by both
query_documents_v1() and validate_and_route_for_tests()).
| let new_ts = document | ||
| .get_raw_for_document_type( | ||
| &transform.source, | ||
| document_type, | ||
| owner_id, | ||
| platform_version, | ||
| )? | ||
| .and_then(|bytes| DocumentPropertyType::decode_date_timestamp(&bytes)); | ||
| let old_ts = match old_document_info.get_raw_for_document_type( | ||
| &transform.source, | ||
| document_type, | ||
| None, | ||
| None, | ||
| platform_version, | ||
| )? { | ||
| Some(DriveKeyInfo::Key(k)) => DocumentPropertyType::decode_date_timestamp(&k), | ||
| Some(DriveKeyInfo::KeyRef(k)) => DocumentPropertyType::decode_date_timestamp(k), | ||
| _ => None, | ||
| }; | ||
|
|
||
| let encode_buckets = |ts: Option<u64>| -> Vec<Vec<u8>> { | ||
| ts.map(|t| { | ||
| transform | ||
| .containing_buckets(t) | ||
| .into_iter() | ||
| .map(DocumentPropertyType::encode_date_timestamp) | ||
| .collect() | ||
| }) | ||
| .unwrap_or_default() | ||
| }; | ||
| let new_buckets = encode_buckets(new_ts); | ||
| let old_buckets = encode_buckets(old_ts); |
There was a problem hiding this comment.
Preserve the empty-key case when diffing time-range buckets.
Insert/delete treat a missing time-range source as a single empty index key, but this helper turns None into []. That means an optional Date/$updatedAt moving null → timestamp never deletes the old empty-key entry, and timestamp → null never inserts it back. Please derive the old/new key sets from the raw encoded value using the same empty-value convention as the insert/delete paths, and add a regression for null ↔ non-null updates.
🤖 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-drive/src/drive/document/update/internal/update_document_for_contract_operations/v0/mod.rs`
around lines 775 - 806, The current encode_buckets closure maps None to an empty
Vec, losing the single empty-index-key convention used by insert/delete paths;
change the logic so that when the raw decoded timestamp is None (or the raw
encoded bytes represent an empty key) encode_buckets returns a Vec containing
one empty Vec (i.e., vec![vec![]]) so old_buckets/new_buckets preserve the
empty-key; update the use sites around get_raw_for_document_type,
DocumentPropertyType::decode_date_timestamp, transform.containing_buckets and
DocumentPropertyType::encode_date_timestamp accordingly and add a regression
test covering null ↔ non-null updates to ensure delete/insert of the empty-key
behaves correctly.
| pub fn resolve_time_range_bucket_clause( | ||
| field: &str, | ||
| selector: TimeRangeSelector, | ||
| document_type: DocumentTypeRef, | ||
| block_time_ms: u64, | ||
| ) -> Result<WhereClause, Error> { | ||
| let transform = document_type | ||
| .indexes() | ||
| .values() | ||
| .find_map(|index| { | ||
| index | ||
| .time_range | ||
| .as_ref() | ||
| .filter(|transform| transform.source == field) | ||
| }) | ||
| .ok_or(Error::Query( | ||
| QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( | ||
| "no time-range index is defined on field \"{}\"", | ||
| field | ||
| )), | ||
| ))?; | ||
|
|
||
| let bucket_start = match selector { | ||
| TimeRangeSelector::Newest => transform.newest_active_start(block_time_ms), | ||
| TimeRangeSelector::Oldest => transform.oldest_active_start(block_time_ms), | ||
| }; | ||
|
|
||
| Ok(WhereClause { | ||
| field: field.to_string(), | ||
| operator: WhereOperator::Equal, | ||
| value: Value::U64(bucket_start), | ||
| }) | ||
| } |
There was a problem hiding this comment.
Preserve the matched time-range index, not just the bucketed clause.
This helper collapses the resolution down to field == bucket_start, but later planning in this module still chooses an index by field coverage alone. That means a contract with both a normal index and a time-range index on the same first property can desugar into an ordinary equality query and hit the wrong tree, returning incorrect results/proofs. Please carry the matched index (or an equivalent hard hint like index name/transform) alongside the synthetic clause so downstream pickers cannot fall back to a non-time-range index.
🤖 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-drive/src/query/mod.rs` around lines 539 - 571,
resolve_time_range_bucket_clause currently returns only a synthetic WhereClause
(field == bucket_start) which loses the fact that this came from a time-range
index; change it to return the matched index/transform as well (e.g. return a
tuple or new struct like (WhereClause, IndexRef) or (WhereClause,
TimeRangeTransform)) so downstream index pickers can see the original time-range
index and avoid choosing a non-time-range index; specifically, in
resolve_time_range_bucket_clause locate the matched index/transform (the
variable transform found via DocumentTypeRef::indexes().values().find_map),
include that transform or the index identifier in the function return value, and
update all callers to accept and thread that hint into the query planner so
index selection uses the provided time-range index rather than falling back to
coverage-based selection.
| /// Time-range (`IN_TIME_RANGE`) selections — `(field, selector)` pairs on | ||
| /// a timestamp field covered by a `timeRange` index. These are emitted as | ||
| /// `IN_TIME_RANGE` clauses on the v1 wire and resolved server-side from | ||
| /// the current block time; the verifier re-derives the same bucket from | ||
| /// the quorum-signed response metadata time. v1-only (the v0 wire has no | ||
| /// `IN_TIME_RANGE` operator). See [`Self::with_time_range`]. | ||
| #[cfg_attr(feature = "mocks", serde(default))] | ||
| pub time_range_clauses: Vec<(String, TimeRangeSelector)>, |
There was a problem hiding this comment.
Don't let DriveDocumentQuery conversion drop time-range filters.
DocumentQuery now exposes time_range_clauses, but TryFrom<&DocumentQuery> for DriveDocumentQuery still builds from where_clauses only. Any internal caller that converts directly to a drive query will silently run the broader unbucketed query instead of the requested time-range query. Please make that conversion fail fast on non-empty time_range_clauses (or add a block-time-aware conversion path) so the filter can't be lost.
Also applies to: 196-203
🤖 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 77 -
84, The conversion impl TryFrom<&DocumentQuery> for DriveDocumentQuery currently
ignores DocumentQuery::time_range_clauses; update the impl(s) that build
DriveDocumentQuery from a DocumentQuery (the TryFrom<&DocumentQuery> for
DriveDocumentQuery and the analogous conversion used elsewhere) to detect
non-empty time_range_clauses and return an Err immediately instead of silently
dropping them. Specifically, check DocumentQuery::time_range_clauses at the
start of the conversion, and if not empty return a clear error (e.g.,
UnsupportedTimeRangeInDriveQuery) referencing that the caller must use
Self::with_time_range or a block-time-aware conversion path; do this for every
conversion path that currently only uses where_clauses so the time-range filters
are not lost.
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
I verified the reported issues against the checked-out SHA and confirmed four blocking problems. The time-range indexing and proof-verification changes are not internally consistent yet, and the protobuf wire addition was not propagated into the shipped generated platform clients.
🔴 4 blocking
4 finding(s)
blocking: Time-range updates drop null-key index entries instead of preserving the existing layout
packages/rs-drive/src/drive/document/update/internal/update_document_for_contract_operations/v0/mod.rs (line 795)
update_time_range_index_for_contract_operations_v0() turns a missing/undecodable source timestamp into Vec::new() for both new_buckets and old_buckets. That is inconsistent with the insert and delete implementations, which explicitly keep a single ordinary key when the first indexed timestamp is null (None => vec![document_top_field.clone()] in add_indices_for_top_index_level_for_contract_operations_v1 and remove_indices_for_top_index_level_for_contract_operations_v1). Because the caller always routes time-range indexes into this helper, updates from null -> value, value -> null, or null -> null with later suffix changes skip the delete/reinsert work needed to maintain that null entry. The result is stale index state for valid documents with nullable timestamp fields.
blocking: Pre-origin timestamps are assigned to buckets that do not contain them
packages/rs-dpp/src/data_contract/document_type/index/time_range.rs (line 68)
most_recent_start() saturates t < origin_ms to origin_ms, and containing_buckets() then emits that start as long as it is >= origin_ms. For any contract with a nonzero origin_ms, a document timestamp earlier than the origin is therefore indexed into the origin_ms bucket even though the documented bucket interval is [start, start + range_ms) and does not contain that timestamp. The same saturation also makes newest_active_start() and oldest_active_start() report an active bucket before any range has actually started. Contract validation does not reject nonzero origins, so this is a reachable correctness bug for valid contracts.
blocking: Aggregate proof verification never resolves time-range selectors before mode and index selection
packages/rs-sdk/src/platform/documents/count_proof_helpers.rs (line 140)
verify_count_query() reads request.where_clauses directly when it computes the count mode and picks the covering index, but DocumentQuery::with_time_range() stores its selector in request.time_range_clauses until verification time. The document proof path already resolves those selectors into concrete equality clauses using the quorum-signed metadata time before rebuilding the drive query, but this helper does not do that, and the same omission is duplicated in sum_proof_helpers.rs and average_proof_helpers.rs. As a result, aggregate COUNT/SUM/AVG proof verification for with_time_range(...) queries is rebuilt from a different query shape than the prover used, so valid proofs can be rejected or verified against the wrong path/query layout.
blocking: The new `IN_TIME_RANGE` enum value was not regenerated into shipped platform clients
packages/dapi-grpc/protos/platform/v0/platform.proto (line 601)
The proto adds IN_TIME_RANGE = 11, and the Rust SDK/server code already uses it, but the checked-in generated platform clients still stop at STARTS_WITH = 10. This is visible in packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js:19601-19614, packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js:24583-24595, packages/dapi-grpc/clients/platform/v0/web/platform_pb.js:24583-24594, and packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h:381-393. packages/dapi-grpc/node.js exports those generated bindings, so consumers of the published JS/web/Objective-C platform clients cannot construct or recognize the new enum through the typed API at this SHA.
Inline posting hit GitHub HTTP 422, so I posted the same verified findings as a top-level review body.
Issue being fixed or feature implemented
Adds time-range indexes — the foundation for "trending"/leaderboard queries (e.g. trending hashtags). A contract can declare an index whose first property is a timestamp bucketed into fixed-length, regularly-spaced, optionally overlapping ranges. A document is indexed under every range whose window contains its timestamp, so
ORDER BY COUNT(*)within the most-recent range becomes a provable, indexed query.Index definition (JSON):
{ "name": "trending", "properties": [{ "$createdAt": "asc" }, { "hashtag": "asc" }], "timeRange": { "on": "$createdAt", "range": 21600000, "step": 7200000 }, "countable": true }range=window length,step=interval between range starts. Withrange > stepthe ranges overlap (here 6h windows every 2h → 3 buckets/doc), so there is always an active range covering a near-full window.What was done?
TimeRangeTransformonIndex/IndexLevel;timeRange:{on,range,step,origin?}parsing + document meta-schema entry; validation (range % step == 0, overlap factor ≤ 256, source must be the first index property and a millisecond timestamp$createdAt/$updatedAt/$transferredAtor aDateproperty, non-unique + non-contested, cross-index config consistency); protocol v12 gate; update-immutability; bucket math (containing_buckets,newest_active_start,oldest_active_start).resolve_time_range_bucket_clause(...)desugars a time-range selection into a concrete bucket-equality clause, so existing index/count proofs apply unchanged.IN_TIME_RANGEoperator on the v1WhereOperator(operand"newest"/"oldest"). The v0 query wire is untouched.IN_TIME_RANGEclauses from the authoritative committed block time before routing (documents + count/sum/avg).query.with_time_range(field, selector)(Rust) andtimeRange: [{ field, selector }](JS/WASM) builders; the proof verifier re-derives the identical bucket from the quorum-signed response metadata time, so light-client proofs verify deterministically.How Has This Been Tested?
timeRangeparsing/validation (rejects non-multiple range, overlap > cap, non-first source, zero step).(timeRange($createdAt, 6h/2h), hashtag)index writes it under all 3 overlapping buckets (queryable by exact bucket start, not under the raw timestamp), and deletion removes every bucket entry.cargo fmtapplied;cargo clippyclean on the touched crates;dpp/drive/drive-abci/dash-sdk/wasm-sdkall compile.IN_TIME_RANGEquery path (server resolve → verifier re-derive) needs a running node; the resolution logic is shared/deterministic and the underlying proofs are the standard index/count proofs.Breaking Changes
None. The feature is gated to the unreleased protocol v12, and the v0 query wire is unchanged — hence
feat:rather thanfeat!:.Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code
Summary by CodeRabbit