1use crate::documents::document_query::{
17 order_clause_to_proto, where_clause_to_proto, DocumentQuery,
18};
19use crate::error::Error;
20use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::{
21 sub_query, SubQuery as ProtoSubQuery,
22};
23use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata};
24use dapi_grpc::platform::VersionedGrpcResponse;
25use dash_context_provider::ContextProvider;
26use dpp::dashcore::Network;
27use dpp::data_contract::accessors::v0::DataContractV0Getters;
28use dpp::data_contract::DataContract;
29use dpp::version::PlatformVersion;
30use dpp::ProtocolError;
31use drive::config::DEFAULT_QUERY_LIMIT;
32use drive::error::query::QuerySyntaxError;
33use drive::query::{
34 BindingSource, DriveDocumentQuery, DriveSubQuery, OrderClause, SelectProjection,
35 SubQueryBinding, SubQueryKind, WhereClause, MAX_SUB_QUERIES,
36};
37use drive_proof_verifier::{
38 verify_composite_documents_tenderdash_proof, CompositeDocuments, FromProof,
39};
40use std::sync::Arc;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
45pub enum CompositeBindingSource {
46 Page,
48 SubQuery(usize),
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
56#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
57pub struct CompositeBinding {
58 pub source: CompositeBindingSource,
60 pub source_property: String,
64 pub field: String,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
74pub enum CompositeSubQueryKind {
75 Documents,
77 Count,
80}
81
82#[derive(Debug, Clone, PartialEq)]
84#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
85pub struct CompositeSubQuery {
86 pub data_contract: Arc<DataContract>,
89 pub document_type_name: String,
91 pub kind: CompositeSubQueryKind,
93 pub where_clauses: Vec<WhereClause>,
96 pub order_by_clauses: Vec<OrderClause>,
102 pub limit: Option<u32>,
107 pub binding: Option<CompositeBinding>,
110}
111
112impl CompositeSubQuery {
113 fn new(
114 data_contract: Arc<DataContract>,
115 document_type_name: &str,
116 kind: CompositeSubQueryKind,
117 ) -> Result<Self, Error> {
118 data_contract
119 .document_type_for_name(document_type_name)
120 .map_err(|e| Error::Protocol(ProtocolError::DataContractError(e)))?;
121 Ok(Self {
122 data_contract,
123 document_type_name: document_type_name.to_string(),
124 kind,
125 where_clauses: Vec::new(),
126 order_by_clauses: Vec::new(),
127 limit: None,
128 binding: None,
129 })
130 }
131
132 pub fn documents<C: Into<Arc<DataContract>>>(
136 data_contract: C,
137 document_type_name: &str,
138 ) -> Result<Self, Error> {
139 Self::new(
140 data_contract.into(),
141 document_type_name,
142 CompositeSubQueryKind::Documents,
143 )
144 }
145
146 pub fn count<C: Into<Arc<DataContract>>>(
149 data_contract: C,
150 document_type_name: &str,
151 ) -> Result<Self, Error> {
152 Self::new(
153 data_contract.into(),
154 document_type_name,
155 CompositeSubQueryKind::Count,
156 )
157 }
158
159 pub fn bound_to(
162 mut self,
163 source: CompositeBindingSource,
164 source_property: impl Into<String>,
165 field: impl Into<String>,
166 ) -> Self {
167 self.binding = Some(CompositeBinding {
168 source,
169 source_property: source_property.into(),
170 field: field.into(),
171 });
172 self
173 }
174
175 pub fn bound_to_page(
178 self,
179 source_property: impl Into<String>,
180 field: impl Into<String>,
181 ) -> Self {
182 self.bound_to(CompositeBindingSource::Page, source_property, field)
183 }
184
185 pub fn with_where(mut self, clause: WhereClause) -> Self {
187 self.where_clauses.push(clause);
188 self
189 }
190
191 pub fn with_order_by(mut self, clause: OrderClause) -> Self {
193 self.order_by_clauses.push(clause);
194 self
195 }
196
197 pub fn with_limit(mut self, limit: u32) -> Self {
199 self.limit = Some(limit);
200 self
201 }
202}
203
204impl From<&DriveSubQuery<'_>> for CompositeSubQuery {
205 fn from(sub: &DriveSubQuery<'_>) -> Self {
206 use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
207 Self {
208 data_contract: Arc::new(sub.contract.clone()),
209 document_type_name: sub.document_type.name().to_string(),
210 kind: match sub.kind {
211 SubQueryKind::Documents => CompositeSubQueryKind::Documents,
212 SubQueryKind::Count => CompositeSubQueryKind::Count,
213 },
214 where_clauses: sub.where_clauses.clone(),
215 order_by_clauses: sub.order_by.clone(),
216 limit: sub.limit.map(u32::from),
217 binding: sub.binding.as_ref().map(|binding| CompositeBinding {
218 source: match binding.source {
219 BindingSource::Page => CompositeBindingSource::Page,
220 BindingSource::SubQuery(index) => CompositeBindingSource::SubQuery(index),
221 },
222 source_property: binding.source_property.clone(),
223 field: binding.field.clone(),
224 }),
225 }
226 }
227}
228
229impl DocumentQuery {
230 pub fn with_sub_query(mut self, sub_query: CompositeSubQuery) -> Self {
233 self.sub_queries.push(sub_query);
234 self
235 }
236
237 pub fn with_sub_queries(mut self, sub_queries: Vec<CompositeSubQuery>) -> Self {
239 self.sub_queries = sub_queries;
240 self
241 }
242
243 pub(super) fn check_composite_shape(&self) -> Result<(), Error> {
245 if self.sub_queries.is_empty() || self.sub_queries.len() > MAX_SUB_QUERIES {
246 return Err(Error::Config(format!(
247 "a composite document query requires between 1 and {MAX_SUB_QUERIES} sub-queries"
248 )));
249 }
250 check_page_shape(self)?;
251 for (index, sub) in self.sub_queries.iter().enumerate() {
252 if let Some(limit) = sub.limit {
253 if limit == 0 || limit > u32::from(DEFAULT_QUERY_LIMIT) {
254 return Err(Error::Drive(drive::error::Error::Query(
255 QuerySyntaxError::InvalidLimit(format!(
256 "sub-query {index}: limit must be in [1, {DEFAULT_QUERY_LIMIT}], got {limit}"
257 )),
258 )));
259 }
260 }
261 if let Some(CompositeBinding {
262 source: CompositeBindingSource::SubQuery(source),
263 ..
264 }) = &sub.binding
265 {
266 if *source >= index
267 || self.sub_queries[*source].kind != CompositeSubQueryKind::Documents
268 {
269 return Err(Error::Config(format!(
270 "sub-query {index}: a binding must name an earlier documents sub-query"
271 )));
272 }
273 }
274 }
275 Ok(())
276 }
277}
278
279fn check_page_shape(page: &DocumentQuery) -> Result<(), Error> {
283 if page.limit == 0 || page.limit > u32::from(DEFAULT_QUERY_LIMIT) {
284 return Err(Error::Config(format!(
285 "a composite document query requires an explicit page limit between 1 and {DEFAULT_QUERY_LIMIT}: it \
286 bounds every derived sub-query clause, so there is no server-default sentinel"
287 )));
288 }
289 if page.select != SelectProjection::documents() {
290 return Err(Error::Config(
291 "a composite page supports the DOCUMENTS projection only".to_string(),
292 ));
293 }
294 if !page.time_range_clauses.is_empty()
295 || page.start.is_some()
296 || page.offset.is_some()
297 || !page.group_by.is_empty()
298 || !page.having.is_empty()
299 {
300 return Err(Error::Config(
301 "a composite page supports where/order_by/limit only: no time-range \
302 selections, cursors, offsets, group_by, or having (paginate with a range \
303 clause on the page's ordering property)"
304 .to_string(),
305 ));
306 }
307 Ok(())
308}
309
310pub(super) fn sub_queries_to_proto(
313 sub_queries: Vec<CompositeSubQuery>,
314) -> Result<Vec<ProtoSubQuery>, Error> {
315 sub_queries
316 .into_iter()
317 .map(|sub_query| {
318 let CompositeSubQuery {
319 data_contract,
320 document_type_name,
321 kind,
322 where_clauses,
323 order_by_clauses,
324 limit,
325 binding,
326 } = sub_query;
327 let kind = match kind {
328 CompositeSubQueryKind::Documents => sub_query::Kind::Documents,
329 CompositeSubQueryKind::Count => sub_query::Kind::Count,
330 };
331 Ok(ProtoSubQuery {
332 data_contract_id: data_contract.id().to_vec(),
337 document_type: document_type_name,
338 where_clauses: where_clauses
339 .into_iter()
340 .map(where_clause_to_proto)
341 .collect::<Result<Vec<_>, _>>()?,
342 order_by: order_by_clauses
343 .into_iter()
344 .map(order_clause_to_proto)
345 .collect(),
346 limit,
347 kind: kind as i32,
348 bind: binding.map(|binding| sub_query::Binding {
349 source: match binding.source {
350 CompositeBindingSource::Page => 0,
351 CompositeBindingSource::SubQuery(index) => index as u32 + 1,
352 },
353 source_property: binding.source_property,
354 field: binding.field,
355 }),
356 })
357 })
358 .collect::<Result<Vec<_>, Error>>()
359}
360
361pub(super) fn drive_sub_queries<'a>(
364 request: &'a DocumentQuery,
365) -> Result<Vec<DriveSubQuery<'a>>, Error> {
366 request
367 .sub_queries
368 .iter()
369 .map(|sub_query| {
370 let contract: &'a DataContract = &sub_query.data_contract;
371 let document_type = contract
372 .document_type_for_name(&sub_query.document_type_name)
373 .map_err(|e| Error::Protocol(ProtocolError::DataContractError(e)))?;
374 Ok(DriveSubQuery {
375 contract,
376 document_type,
377 kind: match sub_query.kind {
378 CompositeSubQueryKind::Documents => SubQueryKind::Documents,
379 CompositeSubQueryKind::Count => SubQueryKind::Count,
380 },
381 where_clauses: sub_query.where_clauses.clone(),
382 order_by: sub_query.order_by_clauses.clone(),
383 limit: sub_query.limit.map(|limit| limit as u16),
384 binding: sub_query.binding.as_ref().map(|binding| SubQueryBinding {
385 source: match binding.source {
386 CompositeBindingSource::Page => BindingSource::Page,
387 CompositeBindingSource::SubQuery(index) => BindingSource::SubQuery(index),
388 },
389 source_property: binding.source_property.clone(),
390 field: binding.field.clone(),
391 }),
392 })
393 })
394 .collect::<Result<Vec<_>, Error>>()
395}
396
397impl FromProof<DocumentQuery> for CompositeDocuments {
398 type Request = DocumentQuery;
399 type Response = GetDocumentsResponse;
400
401 fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
402 request: I,
403 response: O,
404 _network: Network,
405 platform_version: &PlatformVersion,
406 provider: &'a dyn ContextProvider,
407 ) -> Result<(Option<Self>, ResponseMetadata, Proof), drive_proof_verifier::Error>
408 where
409 Self: 'a,
410 {
411 let request: Self::Request = request.into();
412 request
413 .check_composite_shape()
414 .map_err(|e| drive_proof_verifier::Error::RequestError {
415 error: e.to_string(),
416 })?;
417 let response: Self::Response = response.into();
418
419 let query: DriveDocumentQuery = (&request).try_into().map_err(|e: Error| {
420 drive_proof_verifier::Error::RequestError {
421 error: e.to_string(),
422 }
423 })?;
424
425 let proof = response
429 .proof()
430 .or(Err(drive_proof_verifier::Error::NoProofInResult))?;
431 let mtd = response
432 .metadata()
433 .or(Err(drive_proof_verifier::Error::EmptyResponseMetadata))?;
434
435 let (_root_hash, composite) = verify_composite_documents_tenderdash_proof(
436 &query,
437 proof,
438 mtd,
439 platform_version,
440 provider,
441 )?;
442
443 Ok((Some(composite), mtd.clone(), proof.clone()))
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
461 use dapi_grpc::platform::v0::get_documents_request::Version as RequestVersion;
462 use dapi_grpc::platform::v0::GetDocumentsRequest;
463 use dpp::platform_value::Value;
464 use dpp::tests::json_document::json_document_to_contract;
465 use dpp::version::TryFromPlatformVersioned;
466 use drive::query::WhereOperator;
467
468 const FEED_CONTRACT_PATH: &str =
469 "../rs-drive/tests/supporting_files/contract/yappr-feed/yappr-feed-contract.json";
470 const DASHPAY_CONTRACT_PATH: &str =
471 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json";
472
473 fn platform_version() -> &'static PlatformVersion {
474 PlatformVersion::latest()
475 }
476
477 fn contract(path: &str) -> Arc<DataContract> {
478 Arc::new(
479 json_document_to_contract(path, false, platform_version())
480 .expect("expected to parse the fixture contract"),
481 )
482 }
483
484 fn feed_page(limit: u32) -> DocumentQuery {
487 let feed = contract(FEED_CONTRACT_PATH);
488 let dashpay = contract(DASHPAY_CONTRACT_PATH);
489 let page = DocumentQuery::new(feed.clone(), "post")
490 .expect("post doctype exists")
491 .with_where(WhereClause {
492 field: "hashtag".to_string(),
493 operator: WhereOperator::Equal,
494 value: Value::Text("dash".to_string()),
495 })
496 .with_limit(limit);
497 page.with_sub_query(
498 CompositeSubQuery::count(feed.clone(), "like")
499 .expect("like doctype exists")
500 .bound_to_page("$id", "postId"),
501 )
502 .with_sub_query(
503 CompositeSubQuery::documents(feed, "post")
504 .expect("post doctype exists")
505 .bound_to_page("quotedPostId", "$id"),
506 )
507 .with_sub_query(
508 CompositeSubQuery::documents(dashpay, "profile")
509 .expect("profile doctype exists")
510 .bound_to_page("$ownerId", "$ownerId"),
511 )
512 }
513
514 #[test]
515 fn encodes_the_v1_wire_shape() {
516 let query = feed_page(10);
517 let dashpay_id = query.sub_queries[2].data_contract.id().to_vec();
518 let request = GetDocumentsRequest::try_from_platform_versioned(query, platform_version())
519 .expect("encodes");
520 let Some(RequestVersion::V1(v1)) = request.version else {
521 panic!("expected a V1 request");
522 };
523 assert_eq!(v1.document_type, "post");
524 assert_eq!(v1.limit, Some(10));
525 assert!(v1.prove, "composite fetch always proves");
526 assert!(v1.chained.is_none(), "composite and chained are exclusive");
527 assert_eq!(v1.where_clauses.len(), 1);
528 assert_eq!(v1.sub_queries.len(), 3);
529
530 let counts = &v1.sub_queries[0];
531 assert_eq!(counts.document_type, "like");
532 assert_eq!(counts.kind, sub_query::Kind::Count as i32);
533 assert_eq!(counts.limit, None);
534 let bind = counts.bind.as_ref().expect("bound");
535 assert_eq!(bind.source, 0, "the page is source 0");
536 assert_eq!(bind.source_property, "$id");
537 assert_eq!(bind.field, "postId");
538
539 let quoted = &v1.sub_queries[1];
540 assert_eq!(quoted.kind, sub_query::Kind::Documents as i32);
541 assert_eq!(quoted.bind.as_ref().expect("bound").field, "$id");
542
543 let profiles = &v1.sub_queries[2];
544 assert_eq!(profiles.data_contract_id, dashpay_id);
545 assert_eq!(profiles.document_type, "profile");
546 }
547
548 #[test]
549 fn numbers_sub_query_sources_from_one() {
550 let feed = contract(FEED_CONTRACT_PATH);
551 let query = feed_page(10).with_sub_query(
552 CompositeSubQuery::count(feed, "like")
553 .expect("like doctype exists")
554 .bound_to(CompositeBindingSource::SubQuery(1), "$id", "postId"),
555 );
556 let request = GetDocumentsRequest::try_from_platform_versioned(query, platform_version())
557 .expect("encodes");
558 let Some(RequestVersion::V1(v1)) = request.version else {
559 panic!("expected a V1 request");
560 };
561 assert_eq!(
562 v1.sub_queries[3].bind.as_ref().expect("bound").source,
563 2,
564 "sub-query 1 is wire source 2"
565 );
566 }
567
568 #[test]
569 fn should_preserve_the_full_composition_through_drive_conversion() {
570 let feed = contract(FEED_CONTRACT_PATH);
571 let query = feed_page(10).with_sub_query(
572 CompositeSubQuery::documents(feed, "repost")
573 .expect("repost doctype exists")
574 .bound_to(CompositeBindingSource::SubQuery(1), "$id", "postId")
575 .with_where(WhereClause {
576 field: "hashtag".to_string(),
577 operator: WhereOperator::Equal,
578 value: Value::Text("dash".to_string()),
579 })
580 .with_order_by(OrderClause {
581 field: "postId".to_string(),
582 ascending: true,
583 })
584 .with_limit(7),
585 );
586 let drive_query: DriveDocumentQuery = (&query).try_into().expect("converts");
587 for restored in [
588 DocumentQuery::try_from(&drive_query),
589 DocumentQuery::try_from(drive_query.clone()),
590 DocumentQuery::new_with_drive_query(&drive_query),
591 ] {
592 assert_eq!(restored.expect("preserves the composition"), query);
593 }
594 }
595
596 #[test]
597 fn should_reject_compositions_on_v0_without_affecting_ordinary_queries() {
598 let mut v0 = platform_version().clone();
599 v0.drive_abci.query.document_query.default_current_version = 0;
600 let query = feed_page(10);
601 let refused = GetDocumentsRequest::try_from_platform_versioned(query.clone(), &v0);
602 assert!(matches!(refused, Err(Error::Config(message)) if message.contains("V1")));
603
604 let ordinary = query.with_sub_queries(vec![]);
605 let request = GetDocumentsRequest::try_from_platform_versioned(ordinary.clone(), &v0)
606 .expect("ordinary queries still encode as V0");
607 assert!(matches!(request.version, Some(RequestVersion::V0(_))));
608 let request =
609 GetDocumentsRequest::try_from_platform_versioned(ordinary, platform_version())
610 .expect("ordinary queries still encode as V1");
611 let Some(RequestVersion::V1(v1)) = request.version else {
612 panic!("expected V1");
613 };
614 assert!(v1.sub_queries.is_empty());
615 }
616
617 #[test]
618 fn should_enforce_sub_query_limits_before_encoding_or_conversion() {
619 let query = feed_page(10);
620 let sub = query.sub_queries[0].clone();
621 let maximum = query
622 .clone()
623 .with_sub_queries(vec![sub.clone(); MAX_SUB_QUERIES]);
624 GetDocumentsRequest::try_from_platform_versioned(maximum.clone(), platform_version())
625 .expect("the maximum sub-query count encodes");
626 DriveDocumentQuery::try_from(&maximum).expect("the maximum sub-query count converts");
627
628 let excessive = query.with_sub_queries(vec![sub; MAX_SUB_QUERIES + 1]);
629 assert!(GetDocumentsRequest::try_from_platform_versioned(
630 excessive.clone(),
631 platform_version()
632 )
633 .is_err());
634 assert!(DriveDocumentQuery::try_from(&excessive).is_err());
635
636 for limit in [0, 101, u32::MAX] {
637 let mut query = feed_page(10);
638 query.sub_queries[2].limit = Some(limit);
639 assert!(GetDocumentsRequest::try_from_platform_versioned(
640 query.clone(),
641 platform_version()
642 )
643 .is_err());
644 assert!(DriveDocumentQuery::try_from(&query).is_err());
645 }
646 }
647
648 #[test]
649 fn should_reject_invalid_binding_sources_before_encoding_or_conversion() {
650 for source in [0, 2, 3, usize::MAX] {
653 let mut query = feed_page(10);
654 query.sub_queries[2].binding.as_mut().expect("bound").source =
655 CompositeBindingSource::SubQuery(source);
656 assert!(GetDocumentsRequest::try_from_platform_versioned(
657 query.clone(),
658 platform_version()
659 )
660 .is_err());
661 assert!(DriveDocumentQuery::try_from(&query).is_err());
662 }
663 }
664
665 #[cfg(feature = "mocks")]
666 #[test]
667 fn should_preserve_mock_compositions_and_read_older_ordinary_queries() {
668 let query = feed_page(10);
669 let encoded = serde_json::to_value(&query).expect("serializes");
670 let restored: DocumentQuery =
671 serde_json::from_value(encoded.clone()).expect("deserializes");
672 assert_eq!(restored, query);
673
674 let mut legacy = encoded;
675 legacy
676 .as_object_mut()
677 .expect("query object")
678 .remove("sub_queries");
679 let restored: DocumentQuery = serde_json::from_value(legacy).expect("reads older vectors");
680 assert_eq!(restored, query.with_sub_queries(vec![]));
681 }
682
683 #[test]
684 fn should_reject_invalid_page_limits() {
685 for limit in [0, 101, u32::MAX] {
686 let query = feed_page(limit);
687 let refused =
688 GetDocumentsRequest::try_from_platform_versioned(query.clone(), platform_version());
689 assert!(
690 matches!(refused, Err(Error::Config(_))),
691 "an invalid page limit must be refused, got {refused:?}"
692 );
693 assert!(DriveDocumentQuery::try_from(&query).is_err());
694 }
695 }
696
697 #[test]
698 fn refuses_unsupported_page_features() {
699 let mut query = feed_page(10);
700 query.offset = Some(4);
701 let refused = GetDocumentsRequest::try_from_platform_versioned(query, platform_version());
702 assert!(
703 matches!(refused, Err(Error::Config(_))),
704 "a page offset must be refused, got {refused:?}"
705 );
706 }
707
708 #[test]
709 fn converts_to_a_valid_drive_query() {
710 let query = feed_page(10);
711 let drive_query: DriveDocumentQuery =
712 (&query).try_into().expect("converts to a drive query");
713 drive_query
714 .validate_composite(platform_version())
715 .expect("the feed card shape validates");
716 assert_eq!(drive_query.limit, Some(10));
717 assert_eq!(drive_query.sub_queries.len(), 3);
718 assert_eq!(drive_query.sub_queries[0].kind, SubQueryKind::Count);
719 assert_eq!(
720 drive_query.sub_queries[2]
721 .binding
722 .as_ref()
723 .expect("bound")
724 .source,
725 BindingSource::Page
726 );
727 }
728
729 #[test]
730 fn conversion_refuses_an_out_of_range_sub_query_limit() {
731 let feed = contract(FEED_CONTRACT_PATH);
732 let query = feed_page(10).with_sub_query(
733 CompositeSubQuery::documents(feed, "repost")
734 .expect("repost doctype exists")
735 .bound_to_page("$id", "postId")
736 .with_limit(101),
737 );
738 let refused: Result<DriveDocumentQuery, _> = (&query).try_into();
739 assert!(
740 matches!(refused, Err(Error::Drive(_))),
741 "a sub-query limit above the server maximum must be refused, got {refused:?}"
742 );
743 }
744
745 #[test]
746 fn conversion_surfaces_shape_errors() {
747 let feed = contract(FEED_CONTRACT_PATH);
748 let query = feed_page(10).with_sub_query(
749 CompositeSubQuery::documents(feed, "post")
750 .expect("post doctype exists")
751 .bound_to_page("hashtag", "$id"),
752 );
753 let drive_query: DriveDocumentQuery =
754 (&query).try_into().expect("conversion itself succeeds");
755 assert!(
756 drive_query.validate_composite(platform_version()).is_err(),
757 "a by-id join off a non-refersTo property must fail validation"
758 );
759 }
760}