Skip to main content

drive/query/chained_document_query/
mod.rs

1//! Chained document queries: a provable semi-join.
2//!
3//! `SELECT * FROM post WHERE $id IN (SELECT postId FROM like WHERE
4//! $ownerId = <me>)` — the INNER query runs against an indexOnly document
5//! type and projects a `refersTo: permanentDocument` property (the JOIN
6//! property); its proven values are reinjected as the OUTER query's
7//! primary keys. Both halves are proven as ONE merged grovedb proof —
8//! `prove_query_many` merges the limited inner query with the derived
9//! outer by-ids query, the inner limit carried as a per-instance cap on
10//! its root query (the form grovedb's merge lifts a global limit into),
11//! so a single root binds the whole composition by construction; the
12//! surrounding tenderdash layer then binds that root to the
13//! quorum-signed app hash (see `rs-drive-proof-verifier`).
14//!
15//! There is no separate chained query type: a chained query is a
16//! [`DriveDocumentQuery`] — the inner half — whose
17//! [`sub_queries`](DriveDocumentQuery::sub_queries) carry exactly one
18//! by-id join bound to it, the shape
19//! [`DriveDocumentQuery::with_by_id_join`] builds (the same shape the
20//! composite surface generalizes). This module holds the chained
21//! behaviour of `DriveDocumentQuery`: shape validation, join-value
22//! derivation, the outer by-ids builder, proof merging, and the
23//! server-side executors behind `Drive::query_chained_documents` /
24//! `query_chained_documents_with_proof` (the verifier half lives in
25//! `verify::chained_document`).
26//!
27//! Soundness never rests on the server's join: the verifier re-derives
28//! the outer query from the INNER proof's results
29//! ([`DriveDocumentQuery::chained_join_values`] →
30//! [`DriveDocumentQuery::derive_chained_outer_query`], the same functions
31//! the server executes), so a server cannot substitute, omit, or inject
32//! outer documents. When the join property's `refersTo` targets a
33//! `permanentDocument` type (non-deletable, enforced at write time),
34//! every proven join value MUST resolve to a document — a missing outer
35//! document is an invalid proof, not an absence. A `moderatedDocument`
36//! target leaves state only through a moderator's recorded removal, so
37//! the merged proof also covers the removal records of the join values
38//! (see [`moderated_join`](crate::query::moderated_join)): a join value
39//! with no outer document is reported with its proven record, and one
40//! with neither is an invalid proof. A `deletableDocument` target
41//! promises only that the document existed when the inner one was
42//! written, so there a join value with no outer document is left out.
43//! That omission is proven, not trusted: every derived `$id` is a queried
44//! key of the merged query, and grovedb refuses a proof without the
45//! coverage to show a queried key present or absent, so a prover cannot
46//! pass an existing document off as deleted.
47//!
48//! Guardrails (v1): the inner query must resolve to an indexOnly index
49//! that carries the join property (as terminal or prefix property, so
50//! every synthesized projection provably carries its value); the join
51//! edge must be a same-contract `refersTo: permanentDocument`,
52//! `refersTo: moderatedDocument` or `refersTo: deletableDocument` whose
53//! target is the outer type; the
54//! inner limit is required (it is what
55//! bounds the outer fan-out); the outer half takes no clauses, no
56//! limit and no cursor — it is purely the derived by-ids fetch, and
57//! pagination lives on the inner query alone.
58
59use crate::drive::contract::moderation::types::ContractDocumentRemovalEntry;
60use crate::error::drive::DriveError;
61use crate::error::proof::ProofError;
62use crate::error::query::QuerySyntaxError;
63use crate::error::Error;
64#[cfg(feature = "server")]
65use crate::query::moderated_join::fetch_removals;
66use crate::query::moderated_join::{pair_missing_with_removals, removals_path_query};
67use crate::query::{
68    BindingSource, DriveDocumentQuery, InternalClauses, SubQueryBinding, SubQueryKind, WhereClause,
69    WhereOperator,
70};
71use dpp::data_contract::accessors::v0::DataContractV0Getters;
72use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters};
73use dpp::data_contract::document_type::{
74    DocumentPropertyReferenceTarget, DocumentPropertyType, DocumentReferenceDeclaration,
75    DocumentReferenceKind, DocumentTypeRef,
76};
77use dpp::data_contract::DataContract;
78use dpp::document::{Document, DocumentV0Getters};
79use dpp::identifier::Identifier;
80use dpp::platform_value::Value;
81use dpp::version::PlatformVersion;
82use std::collections::BTreeMap;
83
84/// The most join values one chained query can carry — the derived
85/// outer query is a single `$id IN [...]` clause, and `in` clauses
86/// admit at most 100 values (`WhereClause::in_values`).
87/// [`DriveDocumentQuery::validate_chained`] caps the inner limit here so
88/// every reachable page fits, and
89/// [`DriveDocumentQuery::chained_proof_path_queries`] enforces it on the
90/// (untrusted, verifier-supplied) join-value list itself.
91pub const MAX_CHAINED_JOIN_VALUES: usize = 100;
92
93/// The materialized result of a chained query, in inner-proof order.
94#[derive(Debug, Default)]
95pub struct ChainedDocumentsResult {
96    /// The join values that have NO outer document, in first-appearance
97    /// order. Only a join off a `refersTo: deletableDocument` property
98    /// can report any (a referenced document deleted after the inner one
99    /// was written); off a `permanentDocument` property a missing document
100    /// is refused instead. On the proof path each is a proven absence.
101    pub missing_outer_ids: Vec<Identifier>,
102    /// The join values whose outer document the contract's moderators
103    /// removed, each with its removal record, in first-appearance order.
104    /// Only a join off a `refersTo: moderatedDocument` property can report
105    /// any; there a join value with neither a document nor a record is
106    /// refused. On the proof path each is a proven absence of the document
107    /// and a proven record.
108    pub removed_outer_documents: Vec<ContractDocumentRemovalEntry>,
109    /// The inner projections (synthesized indexOnly documents), exactly
110    /// as the inner query alone would return them — the caller reads its
111    /// pagination cursor (the last join value) from here.
112    pub inner_documents: Vec<Document>,
113    /// The referenced outer documents, ordered by FIRST APPEARANCE of
114    /// their id in `inner_documents` (deduplicated).
115    pub outer_documents: Vec<Document>,
116}
117
118/// The outer half of a chained query, assembled against its join values by
119/// [`DriveDocumentQuery::assemble_chained_outer_documents`].
120#[derive(Debug, Default)]
121pub struct ChainedOuterDocuments {
122    /// The outer documents, in first-appearance order of their ids.
123    pub documents: Vec<Document>,
124    /// The join values with no outer document, off a `deletableDocument`
125    /// join property.
126    pub missing: Vec<Identifier>,
127    /// The removal records of the join values with no outer document, off a
128    /// `moderatedDocument` join property.
129    pub removed: Vec<ContractDocumentRemovalEntry>,
130}
131
132impl<'a> DriveDocumentQuery<'a> {
133    /// The join edge of a chained query. There is no separate chained
134    /// query type: a chained query is this query (the inner half) whose
135    /// [`sub_queries`](Self::sub_queries) carry EXACTLY ONE by-id join
136    /// bound to it — the shape [`Self::with_by_id_join`] builds. Returns
137    /// the join's source property (the inner property whose proven values
138    /// become the outer `$id`s) and the outer document type with its
139    /// contract; refuses any other sub-query shape.
140    pub(crate) fn chained_join(
141        &self,
142    ) -> Result<(&str, DocumentTypeRef<'a>, &'a DataContract), Error> {
143        let unsupported =
144            |message: &str| Error::Query(QuerySyntaxError::Unsupported(message.to_string()));
145        let [join] = self.sub_queries.as_slice() else {
146            return Err(unsupported(
147                "a chained query carries exactly one sub-query: the by-id join whose source \
148                 property's proven values become the outer `$id`s (build it with \
149                 with_by_id_join); a query with more sub-queries belongs on the composite \
150                 surface",
151            ));
152        };
153        let Some(SubQueryBinding {
154            source: BindingSource::Page,
155            source_property,
156            field,
157        }) = &join.binding
158        else {
159            return Err(unsupported(
160                "a chained query's sub-query must be bound to the inner query itself",
161            ));
162        };
163        if field.as_str() != dpp::document::property_names::ID {
164            return Err(unsupported(
165                "a chained query's sub-query must be a by-id join (bound field `$id`); other \
166                 bindings live on the composite surface",
167            ));
168        }
169        if join.kind != SubQueryKind::Documents {
170            return Err(unsupported(
171                "a chained join returns documents; counts live on the composite surface",
172            ));
173        }
174        if !join.where_clauses.is_empty() || !join.order_by.is_empty() || join.limit.is_some() {
175            return Err(unsupported(
176                "a chained by-id join takes no fixed clauses, no ordering and no limit: the \
177                 outer half is purely the derived by-ids fetch, complete by set equality",
178            ));
179        }
180        Ok((source_property.as_str(), join.document_type, join.contract))
181    }
182
183    /// Validates the chained shape: this query as the inner indexOnly
184    /// half plus the single by-id join its
185    /// [`sub_queries`](Self::sub_queries) carry (see
186    /// [`Self::chained_join`]). Called by the server before executing and
187    /// by the verifier before verifying, so an invalid spec fails
188    /// identically on both sides.
189    pub fn validate_chained(&self, platform_version: &PlatformVersion) -> Result<(), Error> {
190        let unsupported = |message: String| Error::Query(QuerySyntaxError::Unsupported(message));
191
192        let (join_property, outer_document_type, outer_contract) = self.chained_join()?;
193        // Chained joins are same-contract (v1): the join sub-query's
194        // contract must be the inner query's own.
195        if outer_contract.id() != self.contract.id() {
196            return Err(unsupported(
197                "chained document queries support same-contract joins only: the join \
198                 sub-query targets another contract"
199                    .to_string(),
200            ));
201        }
202        if !self.document_type.index_only() {
203            return Err(unsupported(
204                "chained document queries require an indexOnly inner document type: only \
205                 indexOnly projections prove their values positionally"
206                    .to_string(),
207            ));
208        }
209        if outer_document_type.index_only() {
210            return Err(unsupported(
211                "the outer document type of a chained query cannot be indexOnly: outer \
212                 documents are fetched by id from primary storage, which indexOnly types \
213                 do not have"
214                    .to_string(),
215            ));
216        }
217        match self.limit {
218            None => {
219                return Err(unsupported(
220                    "chained document queries require an explicit limit on the inner query: \
221                     the inner page size is what bounds the derived outer query"
222                        .to_string(),
223                ));
224            }
225            Some(limit) if limit as usize > MAX_CHAINED_JOIN_VALUES => {
226                return Err(unsupported(format!(
227                    "a chained inner limit of {} exceeds {}: the derived outer query is a \
228                     single `$id IN` clause, which admits at most that many values",
229                    limit, MAX_CHAINED_JOIN_VALUES,
230                )));
231            }
232            Some(_) => {}
233        }
234        if self.offset.is_some() {
235            return Err(unsupported(
236                "chained document queries do not support an inner offset; paginate with a \
237                 range clause on the join property"
238                    .to_string(),
239            ));
240        }
241
242        // The join property must be a same-contract document reference
243        // targeting the outer type. `refersTo` writes are
244        // existence-validated; a permanentDocument target can never be
245        // deleted, so every proven join value MUST resolve and a missing
246        // outer document is an invalid proof, while a deletableDocument
247        // target may be gone and is then left out (see
248        // `assemble_chained_outer_documents`).
249        let Some(join_document_property) =
250            self.document_type.flattened_properties().get(join_property)
251        else {
252            return Err(unsupported(format!(
253                "chained query join property \"{}\" does not name a property of inner \
254                 document type \"{}\"",
255                join_property,
256                self.document_type.name(),
257            )));
258        };
259        // A typed array of references is no join property: it is not
260        // indexable, and a join value is one identifier
261        let document_reference = match &join_document_property.property_type {
262            DocumentPropertyType::IdentifierWithReference(reference_target) => {
263                reference_target.as_document_reference()
264            }
265            _ => None,
266        };
267        // A reference found by `findBy` is a document reference whose value
268        // is not the outer document's id, so `as_document_reference` leaves it
269        // out; it is named here so the refusal says why
270        if let DocumentPropertyType::IdentifierWithReference(
271            DocumentPropertyReferenceTarget::PermanentDocumentLookup { lookup, .. }
272            | DocumentPropertyReferenceTarget::DeletableDocumentLookup { lookup, .. },
273        ) = &join_document_property.property_type
274        {
275            return Err(unsupported(format!(
276                "chained query join property \"{}\" refers to its document by findBy ({}), \
277                 so its value is not the outer document's id: a join needs a reference whose \
278                 value is the referenced document's $id",
279                join_property,
280                lookup.find_by_names(),
281            )));
282        }
283        // A reference expression is no single document reference either: an
284        // `anyOf` value may be the id of any of its leaves, and an `allOf`
285        // names no one outer document type to join through
286        if let DocumentPropertyType::IdentifierWithReference(
287            DocumentPropertyReferenceTarget::AnyOf(_) | DocumentPropertyReferenceTarget::AllOf(_),
288        ) = &join_document_property.property_type
289        {
290            return Err(unsupported(format!(
291                "chained query join property \"{}\" declares a refersTo anyOf or allOf \
292                 expression: a join needs a reference to one document type",
293                join_property,
294            )));
295        }
296        match document_reference {
297            Some(DocumentReferenceDeclaration {
298                contract_id,
299                document_type_name,
300                ..
301            }) => {
302                if let Some(referenced_contract_id) = contract_id {
303                    if referenced_contract_id != self.contract.id() {
304                        return Err(unsupported(
305                            "chained document queries support same-contract joins only: \
306                             the join property's refersTo names another contract"
307                                .to_string(),
308                        ));
309                    }
310                }
311                if document_type_name != outer_document_type.name() {
312                    return Err(unsupported(format!(
313                        "chained query outer document type \"{}\" does not match the join \
314                         property's refersTo target \"{}\"",
315                        outer_document_type.name(),
316                        document_type_name,
317                    )));
318                }
319            }
320            None => {
321                return Err(unsupported(format!(
322                    "chained query join property \"{}\" must carry a `refersTo: \
323                     permanentDocument`, `refersTo: moderatedDocument` or `refersTo: \
324                     deletableDocument` declaration: it is what names the outer document type \
325                     the proven join values resolve in",
326                    join_property,
327                )));
328            }
329        }
330
331        // The resolved index must carry the join property, so every
332        // synthesized inner projection provably carries its value.
333        let index = self.index_only_query_index(platform_version)?;
334        let index_carries_join_property = index.terminal_contains(join_property)
335            || index
336                .properties
337                .iter()
338                .any(|property| property.name == join_property);
339        if !index_carries_join_property {
340            return Err(unsupported(format!(
341                "the inner query resolves to index \"{}\", which does not carry the join \
342                 property \"{}\"; constrain the query so an index carrying it serves it",
343                index.name, join_property,
344            )));
345        }
346
347        Ok(())
348    }
349
350    /// Extracts the join values from the inner documents in their proof
351    /// order, deduplicated to first appearance. ONE extraction both the
352    /// server and the verifier run — the single-builder rule that keeps
353    /// the derived outer query identical on both sides.
354    pub fn chained_join_values(
355        &self,
356        inner_documents: &[Document],
357    ) -> Result<Vec<Identifier>, Error> {
358        use dpp::platform_value::btreemap_extensions::BTreeValueMapPathHelper;
359
360        let (join_property, _, _) = self.chained_join()?;
361        let mut seen: std::collections::BTreeSet<Identifier> = std::collections::BTreeSet::new();
362        let mut join_values = Vec::with_capacity(inner_documents.len());
363        for document in inner_documents {
364            // Path-aware read: `validate_chained` admits any property
365            // `flattened_properties()` names — dotted (nested) keys
366            // included — and the synthesis builder stores those nested
367            // (`insert_at_path`), so a flat `.get` would miss them.
368            let value = document
369                .properties()
370                .get_optional_at_path(join_property)
371                .ok()
372                .flatten()
373                .ok_or(Error::Drive(DriveError::CorruptedCodeExecution(
374                    "an inner projection is missing the join property: validate_chained() \
375                     guarantees the resolved index carries it",
376                )))?;
377            let identifier = value.to_identifier().map_err(|_| {
378                Error::Drive(DriveError::CorruptedCodeExecution(
379                    "a chained join property must decode as an identifier: the parser \
380                     only admits identifier-typed refersTo properties",
381                ))
382            })?;
383            if seen.insert(identifier) {
384                join_values.push(identifier);
385            }
386        }
387        Ok(join_values)
388    }
389
390    /// The derived outer query: a pure by-ids fetch of the join values
391    /// from the outer type's primary storage. No clauses, no limit, no
392    /// cursor — completeness is set-equality against `join_values`,
393    /// checked by the verifier.
394    pub fn derive_chained_outer_query(
395        &self,
396        join_values: &[Identifier],
397    ) -> Result<DriveDocumentQuery<'a>, Error> {
398        let (_, outer_document_type, outer_contract) = self.chained_join()?;
399        // Canonical value order: byte-ascending. Grove sorts query keys
400        // internally either way; sorting here keeps the built query —
401        // and therefore the proof — byte-identical between the server
402        // and a verifier that extracted the ids in any order.
403        let mut ids: Vec<Identifier> = join_values.to_vec();
404        ids.sort();
405        Ok(DriveDocumentQuery {
406            contract: outer_contract,
407            document_type: outer_document_type,
408            internal_clauses: InternalClauses {
409                primary_key_in_clause: Some(WhereClause {
410                    field: dpp::document::property_names::ID.to_string(),
411                    operator: WhereOperator::In,
412                    value: Value::Array(
413                        ids.into_iter()
414                            .map(|id| Value::Identifier(id.to_buffer()))
415                            .collect(),
416                    ),
417                }),
418                primary_key_equal_clause: None,
419                in_clauses: Vec::new(),
420                range_clause: None,
421                equal_clauses: Default::default(),
422            },
423            offset: None,
424            limit: None,
425            order_by: Default::default(),
426            start_at: None,
427            start_at_included: false,
428            block_time_ms: None,
429            resolved_time_ranges: Vec::new(),
430            sub_queries: vec![],
431        })
432    }
433
434    /// What the join property's reference guarantees of its target: that
435    /// it stays in state (`permanentDocument`), leaves it only on a
436    /// moderator's record (`moderatedDocument`), or nothing
437    /// (`deletableDocument`). A join property that is none of them, which
438    /// `validate_chained` refuses, is held to the strict rule.
439    fn chained_join_kind(&self) -> Result<DocumentReferenceKind, Error> {
440        let (join_property, _, _) = self.chained_join()?;
441        Ok(self
442            .document_type
443            .flattened_properties()
444            .get(join_property)
445            .and_then(|property| match &property.property_type {
446                DocumentPropertyType::IdentifierWithReference(reference_target) => {
447                    reference_target.as_document_reference()
448                }
449                _ => None,
450            })
451            .map_or(DocumentReferenceKind::Permanent, |declaration| {
452                declaration.kind
453            }))
454    }
455
456    /// The removal records component of the chained proof: for a join off
457    /// a `moderatedDocument` property with join values, the records of
458    /// every join value (see [`removals_path_query`]), walking as the outer
459    /// by-ids query does; `None` for every other join.
460    fn chained_removals_path_query(
461        &self,
462        join_values: &[Identifier],
463        outer_left_to_right: bool,
464    ) -> Result<Option<grovedb::PathQuery>, Error> {
465        if join_values.is_empty() || self.chained_join_kind()? != DocumentReferenceKind::Moderated {
466            return Ok(None);
467        }
468        let (_, outer_document_type, outer_contract) = self.chained_join()?;
469        Ok(Some(removals_path_query(
470            outer_contract.id(),
471            outer_document_type.name(),
472            join_values,
473            outer_left_to_right,
474        )))
475    }
476
477    /// Reorders the outer documents (returned in key order by the by-ids
478    /// query) into first-appearance join order, and checks them against
479    /// the derived join values. An outer document carried twice, or one
480    /// no join value references, is always refused. A join value with no
481    /// outer document is refused when the join property is a
482    /// `permanentDocument` reference (it cannot dangle: corrupted state
483    /// on the server, an invalid proof in the verifier), reported with its
484    /// removal record, taken from `removals`, when it is a
485    /// `moderatedDocument` reference (one without a record is refused as a
486    /// permanent one is), and left out when it is a `deletableDocument`
487    /// reference (the target was deleted after the inner document was
488    /// written, and the by-ids query that found nothing under its `$id` is
489    /// the very query the proof covers); the join values left out and the
490    /// records are returned alongside, each in first-appearance order.
491    /// Shared by the server and the verifier.
492    pub fn assemble_chained_outer_documents(
493        &self,
494        join_values: &[Identifier],
495        outer_documents: Vec<Document>,
496        removals: &BTreeMap<Identifier, ContractDocumentRemovalEntry>,
497    ) -> Result<ChainedOuterDocuments, Error> {
498        let mut by_id: BTreeMap<Identifier, Document> = BTreeMap::new();
499        for document in outer_documents {
500            let id = document.id();
501            if by_id.insert(id, document).is_some() {
502                return Err(Error::Proof(ProofError::CorruptedProof(format!(
503                    "chained outer results carry document {} twice",
504                    id
505                ))));
506            }
507        }
508        let kind = self.chained_join_kind()?;
509        let mut ordered = Vec::with_capacity(join_values.len());
510        let mut missing = Vec::new();
511        for join_value in join_values {
512            match by_id.remove(join_value) {
513                Some(document) => ordered.push(document),
514                None if kind.is_permanent() => {
515                    return Err(Error::Proof(ProofError::CorruptedProof(format!(
516                        "chained outer results are missing referenced document {}: a \
517                             permanentDocument reference cannot dangle, so the outer half \
518                             does not prove the derived query",
519                        join_value
520                    ))));
521                }
522                // A moderatedDocument or deletableDocument target that is no
523                // longer in state.
524                None => missing.push(*join_value),
525            }
526        }
527        if let Some((extra_id, _)) = by_id.into_iter().next() {
528            return Err(Error::Proof(ProofError::CorruptedProof(format!(
529                "chained outer results carry document {} that no proven join value \
530                     references",
531                extra_id
532            ))));
533        }
534        // A moderated target leaves state on a moderator's record only
535        if kind == DocumentReferenceKind::Moderated {
536            return Ok(ChainedOuterDocuments {
537                removed: pair_missing_with_removals(&missing, removals)?,
538                documents: ordered,
539                missing: Vec::new(),
540            });
541        }
542        Ok(ChainedOuterDocuments {
543            documents: ordered,
544            missing,
545            removed: Vec::new(),
546        })
547    }
548
549    /// The component path queries the chained proof covers: the inner
550    /// query's own path query, plus — for a non-empty join — the outer
551    /// by-ids path query derived from `join_values`, and for a join off a
552    /// `moderatedDocument` property the removal records of the same ids. ONE builder both
553    /// the prover (`prove_query_many` merges these) and the verifier
554    /// (`PathQuery::merge` on the same inputs at the same grove
555    /// version) call, so the merged query is byte-identical on both
556    /// sides.
557    ///
558    /// The inner component is the composite page
559    /// ([`Self::page_path_query`]): a chained query is that shape with one
560    /// by-id join, and the page carries its limit as its root query's
561    /// per-instance cap, exactly what grovedb's merge lifts a global limit
562    /// into (exact here: the branch instance executes once). Authoring the
563    /// cap up front gives the inner half one form whether or not anything
564    /// merges with it, so the server's read of the inner page, the proof
565    /// and the verifier's bootstrap pass select the same rows. Under a
566    /// global limit they would not: grovedb cuts a layer that fans out
567    /// across an index's prefix values to that many branches and charges
568    /// an empty branch against it, while the cap budgets only the rows
569    /// below.
570    pub fn chained_proof_path_queries(
571        &self,
572        join_values: &[Identifier],
573        platform_version: &PlatformVersion,
574    ) -> Result<Vec<grovedb::PathQuery>, Error> {
575        // `join_values` may be an UNTRUSTED verifier-side hint; cap it
576        // before deriving, so an oversized list fails here with a clear
577        // message instead of deep in the `in`-clause lowering. An
578        // honest list cannot exceed this: it is deduplicated from an
579        // inner page whose limit `validate_chained` bounds to the same
580        // cap.
581        if join_values.len() > MAX_CHAINED_JOIN_VALUES {
582            return Err(Error::Query(QuerySyntaxError::Unsupported(format!(
583                "{} chained join values exceed the {} an outer `$id IN` clause admits",
584                join_values.len(),
585                MAX_CHAINED_JOIN_VALUES,
586            ))));
587        }
588        // Edited in place, as are the inner page reads of both executors
589        // below: a chained query needs an indexOnly inner type
590        // (`validate_chained`), which only protocol version 14 parses.
591        let inner = self.page_path_query(platform_version)?;
592        if join_values.is_empty() {
593            return Ok(vec![inner]);
594        }
595        let outer = self
596            .derive_chained_outer_query(join_values)?
597            .construct_path_query(None, platform_version)?;
598        match self.chained_removals_path_query(join_values, outer.query.query.left_to_right)? {
599            Some(removals) => Ok(vec![inner, outer, removals]),
600            None => Ok(vec![inner, outer]),
601        }
602    }
603}
604
605#[cfg(feature = "server")]
606impl DriveDocumentQuery<'_> {
607    /// Executes the chained query without proofs.
608    pub(crate) fn execute_chained_no_proof_internal(
609        &self,
610        drive: &crate::drive::Drive,
611        transaction: grovedb::TransactionArg,
612        drive_operations: &mut Vec<crate::fees::op::LowLevelDriveOperation>,
613        platform_version: &PlatformVersion,
614    ) -> Result<ChainedDocumentsResult, Error> {
615        use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0;
616
617        self.validate_chained(platform_version)?;
618        // The inner documents are serialized whole, as a documents query's
619        // are: refused before any read when the inner index lacks a property.
620        self.refuse_an_uncovered_index_only_projection(platform_version)?;
621
622        // Read from the inner component the proof covers, so a read with and
623        // without a proof return the same page (see `chained_proof_path_queries`)
624        let inner_documents = Self::materialize_component(
625            self,
626            &self.page_path_query(platform_version)?,
627            drive,
628            transaction,
629            drive_operations,
630            platform_version,
631        )?;
632        let join_values = self.chained_join_values(&inner_documents)?;
633        if join_values.is_empty() {
634            return Ok(ChainedDocumentsResult {
635                inner_documents,
636                outer_documents: Vec::new(),
637                missing_outer_ids: Vec::new(),
638                removed_outer_documents: Vec::new(),
639            });
640        }
641
642        let outer_query = self.derive_chained_outer_query(&join_values)?;
643        let outer_document_type = outer_query.document_type;
644        let (serialized_outer, _outer_skipped) = outer_query
645            .execute_raw_results_no_proof_internal(
646                drive,
647                transaction,
648                drive_operations,
649                platform_version,
650            )?;
651        let outer_documents = serialized_outer
652            .into_iter()
653            .map(|serialized| {
654                Document::from_bytes(serialized.as_slice(), outer_document_type, platform_version)
655                    .map_err(|e| Error::Protocol(Box::new(e)))
656            })
657            .collect::<Result<Vec<Document>, Error>>()?;
658        // The records component the proof covers; a fetch by keys selects the same records
659        // whichever way it walks
660        let removals = match self.chained_removals_path_query(&join_values, true)? {
661            Some(path_query) => fetch_removals(
662                drive,
663                &path_query,
664                transaction,
665                drive_operations,
666                platform_version,
667            )?,
668            None => BTreeMap::new(),
669        };
670        let ChainedOuterDocuments {
671            documents: outer_documents,
672            missing: missing_outer_ids,
673            removed: removed_outer_documents,
674        } = self.assemble_chained_outer_documents(&join_values, outer_documents, &removals)?;
675
676        Ok(ChainedDocumentsResult {
677            inner_documents,
678            outer_documents,
679            missing_outer_ids,
680            removed_outer_documents,
681        })
682    }
683
684    /// Executes the chained query AND generates its single merged
685    /// proof.
686    ///
687    /// The inner page and the derived outer by-ids fetch are proven as
688    /// ONE grovedb proof: [`Self::chained_proof_path_queries`] builds the
689    /// component path queries and `prove_query_many` merges them, the
690    /// inner query's limit riding as its branch's per-instance
691    /// `Query::limit`. One proof means one root by construction.
692    ///
693    /// The materialize pass reads the inner page from that very inner
694    /// component, so the join values the outer component derives from
695    /// are the ones the proof's inner branch shows. The materialize pass
696    /// and the prove pass still both read
697    /// committed state — grovedb proves committed state only — so the
698    /// sequence is BRACKETED by root-hash reads and retried if a block
699    /// commit interleaved; otherwise the proof's inner branch could
700    /// disagree with the outer branch derived from the stale
701    /// materialization, and every verifier would reject the
702    /// composition.
703    ///
704    /// Returns the proof and the materialized INNER projections (the
705    /// join values, and with them the caller's response hint and
706    /// pagination cursor, derive from these). The outer documents are
707    /// deliberately NOT materialized here — the proof pass covers them,
708    /// so reading their bodies a second time would double the state
709    /// reads for data the proved response never carries inline.
710    pub(crate) fn execute_chained_with_proof_internal(
711        &self,
712        drive: &crate::drive::Drive,
713        drive_operations: &mut Vec<crate::fees::op::LowLevelDriveOperation>,
714        platform_version: &PlatformVersion,
715    ) -> Result<(Vec<u8>, Vec<Document>), Error> {
716        self.validate_chained(platform_version)?;
717        let inner_path_query = self.page_path_query(platform_version)?;
718
719        // Block commits are seconds apart while an attempt is
720        // milliseconds, so a bracket collision is rare and two in a row
721        // vanishingly so; three attempts is generosity, not need.
722        const MAX_ATTEMPTS: usize = 3;
723        for _ in 0..MAX_ATTEMPTS {
724            let root_before = drive
725                .grove
726                .root_hash(None, &platform_version.drive.grove_version)
727                .unwrap()?;
728
729            // Materialize the INNER half only — the join values the
730            // outer component derives from live in its projections.
731            let inner_documents = Self::materialize_component(
732                self,
733                &inner_path_query,
734                drive,
735                None,
736                drive_operations,
737                platform_version,
738            )?;
739            let join_values = self.chained_join_values(&inner_documents)?;
740
741            let path_queries = self.chained_proof_path_queries(&join_values, platform_version)?;
742            let path_query_refs: Vec<&grovedb::PathQuery> = path_queries.iter().collect();
743            let proof = drive
744                .grove
745                .prove_query_many(path_query_refs, None, &platform_version.drive.grove_version)
746                .unwrap()?;
747
748            let root_after = drive
749                .grove
750                .root_hash(None, &platform_version.drive.grove_version)
751                .unwrap()?;
752            if root_before != root_after {
753                continue;
754            }
755
756            return Ok((proof, inner_documents));
757        }
758        Err(Error::Drive(DriveError::NotSupported(
759            "chained proof generation raced a block commit on every attempt; \
760             transient — retry the request",
761        )))
762    }
763}