Skip to content

Commit 8c20866

Browse files
feat(dapi): composite document queries on the getDocuments V1 wire
`GetDocumentsRequestV1.sub_queries` (field 14): each entry is a typed sub-query (contract, document type, fixed where/order clauses, limit, DOCUMENTS or COUNT) with an optional binding `<field> IN <values>` whose values the node derives from the page's, or an earlier sub-query's, proven documents. Presence selects composite mode: the request's own clauses describe the page; `chained` and `sub_queries` are mutually exclusive. `ResultData.composite` (variant 7) carries the page and one result per sub-query (documents, or count entries keyed by the bound value) for the no-proof path; the proof path is the single merged proof in the standard envelope, verified client-side by re-deriving the whole composition from the proven page. drive-abci intercepts composite requests before select routing (`dispatch/composite.rs`): the page limit is required (1..=max), every SQL-shaped knob, cursor and time-range clause is refused, each distinct sub-query contract is fetched once, and drive's shape validation surfaces as client-attributable query errors. Old nodes ignore the field and serve a page-only proof, which the verifier refuses. Existing V1 request literals gain the new field; the serde default keeps mock vectors captured before it wire-compatible. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent ff233f4 commit 8c20866

10 files changed

Lines changed: 963 additions & 1 deletion

File tree

packages/dapi-grpc/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig {
341341
// absent (same pattern DocumentQuery's own serde defaults follow
342342
// for pre-SQL-surface fixtures).
343343
.field_attribute("GetDocumentsRequestV1.chained", SERDE_DEFAULT)
344+
.field_attribute("GetDocumentsRequestV1.sub_queries", SERDE_DEFAULT)
344345
// Same compat rule for the typed IN_TIME_RANGE operand: mock
345346
// vectors captured while the operand still rode `value` carry no
346347
// `time_range` key.

packages/dapi-grpc/protos/platform/v0/platform.proto

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,81 @@ message GetDocumentsRequest {
12531253
string outer_document_type = 2;
12541254
}
12551255
ChainedJoin chained = 13;
1256+
1257+
// Composite mode — a page plus sub-queries DERIVED from its
1258+
// results, answered as ONE merged proof over one state root.
1259+
//
1260+
// Presence of any `sub_queries` selects composite mode: this
1261+
// request's own `data_contract_id` / `document_type` /
1262+
// `where_clauses` / `order_by` / `limit` describe the PAGE, and
1263+
// every sub-query's `IN` clause is derived by the node from the
1264+
// page's (or an earlier sub-query's) proven documents. The
1265+
// verifier re-derives every sub-query from the proven page with
1266+
// the same builders, re-merges, and verifies the whole
1267+
// composition — so the composition cannot be steered by the
1268+
// responding node, and a node that predates this field (proto3
1269+
// unknown field) serves a page-only proof that FAILS CLOSED
1270+
// client-side.
1271+
//
1272+
// Mode gates (rejected otherwise): `limit` is REQUIRED on the
1273+
// page (at most 100 — it bounds every derived clause); `selects`
1274+
// must be empty or a single DOCUMENTS projection; `group_by`,
1275+
// `having`, time-range clauses, cursors and `offset` are
1276+
// rejected (paginate with a range clause on the page's ordering
1277+
// property); `chained` and `sub_queries` are mutually exclusive.
1278+
// See `SubQuery` for the per-sub-query rules.
1279+
message SubQuery {
1280+
// The contract this sub-query targets. Empty = the page's own
1281+
// contract; otherwise any contract (profiles keyed by owner,
1282+
// names keyed by identity).
1283+
bytes data_contract_id = 1;
1284+
string document_type = 2;
1285+
// The FIXED clauses — everything but the derived `IN`, which
1286+
// must not be named here.
1287+
repeated WhereClause where_clauses = 3;
1288+
// Ordering (documents only). A bound field missing from it is
1289+
// appended ascending by the node and the verifier alike.
1290+
repeated OrderClause order_by = 4;
1291+
// Documents lookups on a non-unique index REQUIRE a limit (it
1292+
// bounds the walk under each derived value, at most 100);
1293+
// lookups already bounded by their values (a unique index, or
1294+
// an indexOnly terminal with every prefix fixed), by-id joins
1295+
// (completeness is set equality) and counts take none.
1296+
optional uint32 limit = 5;
1297+
enum Kind {
1298+
// The matching documents.
1299+
DOCUMENTS = 0;
1300+
// One count per derived value from the `countable` index
1301+
// covering the fixed clauses plus the bound field. Must be
1302+
// bound, and must not share its index path with a documents
1303+
// component (the count reads the value trees the documents
1304+
// query descends past).
1305+
COUNT = 1;
1306+
}
1307+
Kind kind = 6;
1308+
// The derived clause `<field> IN <values>`. Absent = a SIBLING:
1309+
// an independent documents query proven under the same root.
1310+
message Binding {
1311+
// Whose proven documents supply the values: `0` = the page,
1312+
// `n` = `sub_queries[n - 1]` (which must precede this one and
1313+
// be a DOCUMENTS sub-query).
1314+
uint32 source = 1;
1315+
// The source property read off each document: `$id`,
1316+
// `$ownerId`, or an identifier-typed property (dotted paths
1317+
// reach nested properties). Documents without it contribute
1318+
// nothing.
1319+
string source_property = 2;
1320+
// The sub-query field receiving the `IN` clause. `$id` makes
1321+
// this a by-id JOIN: the source property must then declare
1322+
// `refersTo: permanentDocument` targeting this document type,
1323+
// so every derived id resolves and a missing document is an
1324+
// invalid proof. Otherwise `$ownerId` or an indexed property
1325+
// (a LOOKUP, where absence is a proven fact).
1326+
string field = 3;
1327+
}
1328+
Binding bind = 7;
1329+
}
1330+
repeated SubQuery sub_queries = 14;
12561331
}
12571332

12581333
oneof version {
@@ -1605,6 +1680,10 @@ message GetDocumentsResponse {
16051680
// among the inner projections, deduplicated). Routed when
16061681
// the request's `chained` message is present.
16071682
ChainedDocuments chained = 6;
1683+
// Composite-mode result: the page plus one result per
1684+
// sub-query, in request order. Routed when the request
1685+
// carries `sub_queries`.
1686+
CompositeDocuments composite = 7;
16081687
}
16091688
}
16101689

@@ -1615,6 +1694,25 @@ message GetDocumentsResponse {
16151694
repeated bytes outer_documents = 2;
16161695
}
16171696

1697+
// A composite query's page and per-sub-query results, documents
1698+
// serialized with their own document type.
1699+
message CompositeDocuments {
1700+
// The page, exactly as the page query alone would return it.
1701+
repeated bytes page_documents = 1;
1702+
message SubQueryResult {
1703+
oneof result {
1704+
// DOCUMENTS: a by-id join in first-appearance order of the
1705+
// derived ids; a lookup or sibling in query order.
1706+
Documents documents = 1;
1707+
// COUNT: one entry per derived value that has a count tree
1708+
// (a value with no entry counts zero), keyed by the
1709+
// value's index-key bytes.
1710+
CountEntries counts = 2;
1711+
}
1712+
}
1713+
repeated SubQueryResult sub_results = 2;
1714+
}
1715+
16181716
oneof result {
16191717
ResultData data = 1;
16201718
Proof proof = 2;

packages/dash-platform-queries/src/documents/document_query.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,7 @@ fn encode_v1(
872872
// a second copy of that rule in the SDK.
873873
offset,
874874
chained: None,
875+
sub_queries: Vec::new(),
875876
})),
876877
})
877878
}

packages/rs-drive-abci/src/query/document_query/v1/dispatch/chained.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ mod tests {
348348
group_by: Vec::new(),
349349
having: Vec::new(),
350350
offset: None,
351+
sub_queries: Vec::new(),
351352
chained: Some(ChainedJoin {
352353
join_property: "postId".to_string(),
353354
outer_document_type: "post".to_string(),

0 commit comments

Comments
 (0)