@dashevo/dapi-grpc 4.2.0-dev.1 → 4.2.0-dev.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,8 +33,12 @@ service Platform {
33
33
  rpc getDataContract(GetDataContractRequest) returns (GetDataContractResponse);
34
34
  rpc getDataContractHistory(GetDataContractHistoryRequest)
35
35
  returns (GetDataContractHistoryResponse);
36
+ rpc getDataContractsLatestVersions(GetDataContractsLatestVersionsRequest)
37
+ returns (GetDataContractsLatestVersionsResponse);
36
38
  rpc getDataContracts(GetDataContractsRequest)
37
39
  returns (GetDataContractsResponse);
40
+ rpc getDataContractsByRange(GetDataContractsByRangeRequest)
41
+ returns (GetDataContractsResponse);
38
42
  rpc getDocumentHistory(GetDocumentHistoryRequest)
39
43
  returns (GetDocumentHistoryResponse);
40
44
  rpc getDocuments(GetDocumentsRequest) returns (GetDocumentsResponse);
@@ -497,6 +501,57 @@ message GetDataContractResponse {
497
501
  oneof version { GetDataContractResponseV0 v0 = 1; }
498
502
  }
499
503
 
504
+ // Returns the current version number of each requested data contract: the
505
+ // cheap way for a client to check that the contracts it already holds are
506
+ // still current. Every distinct requested id gets an entry; an id no contract has gets
507
+ // an entry without `version`. Serialized contracts are added only when
508
+ // `include_contracts` is set.
509
+ //
510
+ // From protocol version 14 every contract carries a four-byte version item
511
+ // beside it in state. Without `include_contracts`, the unproved form answers
512
+ // from Drive's contract cache or that item without loading a contract, and
513
+ // the proof covers the items, a few hundred bytes of hash path per contract.
514
+ // With `include_contracts`, and on earlier protocol versions, the unproved
515
+ // form reads the contracts through the cache and the proof is the
516
+ // multi-contract proof `getDataContracts` returns, which carries the
517
+ // contracts.
518
+ message GetDataContractsLatestVersionsRequest {
519
+ message GetDataContractsLatestVersionsRequestV0 {
520
+ repeated bytes ids =
521
+ 1; // The IDs of the data contracts, at least one and at most 100
522
+ bool include_contracts =
523
+ 2; // When set, found entries also carry the serialized contract
524
+ bool prove = 3; // Flag to request a proof as the response
525
+ }
526
+ oneof version { GetDataContractsLatestVersionsRequestV0 v0 = 1; }
527
+ }
528
+
529
+ message GetDataContractsLatestVersionsResponse {
530
+ message DataContractLatestVersionEntry {
531
+ bytes identifier = 1; // The requested contract id
532
+ optional uint32 version =
533
+ 2; // The contract's current version number; absent when no contract has this id
534
+ optional bytes data_contract =
535
+ 3; // The serialized contract, only when `include_contracts` was set and the contract exists
536
+ }
537
+
538
+ message DataContractsLatestVersions {
539
+ repeated DataContractLatestVersionEntry entries =
540
+ 1; // One entry per requested contract id
541
+ }
542
+
543
+ message GetDataContractsLatestVersionsResponseV0 {
544
+ oneof result {
545
+ DataContractsLatestVersions data_contracts_latest_versions =
546
+ 1; // The current versions, and the contracts if requested
547
+ Proof proof =
548
+ 2; // Cryptographic proof of the data contracts, if requested
549
+ }
550
+ ResponseMetadata metadata = 3; // Metadata about the blockchain state
551
+ }
552
+ oneof version { GetDataContractsLatestVersionsResponseV0 v0 = 1; }
553
+ }
554
+
500
555
  message GetDataContractsRequest {
501
556
  message GetDataContractsRequestV0 {
502
557
  repeated bytes ids =
@@ -506,6 +561,28 @@ message GetDataContractsRequest {
506
561
  oneof version { GetDataContractsRequestV0 v0 = 1; }
507
562
  }
508
563
 
564
+ // Enumerates every data contract on Platform, one page at a time, in
565
+ // ascending contract id order. Answered with `GetDataContractsResponse`:
566
+ // each `DataContractEntry` carries the contract id and, unless `ids_only`
567
+ // is set, the serialized contract. An empty page (no more contracts) is an
568
+ // empty `data_contract_entries` list, never NotFound. A page shorter than
569
+ // `limit` is the last page; otherwise pass the last entry's identifier as
570
+ // `start_after` to fetch the next one.
571
+ message GetDataContractsByRangeRequest {
572
+ message GetDataContractsByRangeRequestV0 {
573
+ optional uint32 limit =
574
+ 1; // Maximum number of contracts to return, 1..=100; absent means 100
575
+ oneof start {
576
+ bytes start_after = 2; // 32-byte contract id; the page starts after it
577
+ bytes start_at = 3; // 32-byte contract id; the page starts at it
578
+ }
579
+ bool ids_only =
580
+ 4; // When set, entries carry only `identifier`; `data_contract` is unset
581
+ bool prove = 5; // Flag to request a proof as the response
582
+ }
583
+ oneof version { GetDataContractsByRangeRequestV0 v0 = 1; }
584
+ }
585
+
509
586
  message GetDataContractsResponse {
510
587
  message DataContractEntry {
511
588
  bytes identifier = 1; // The unique identifier of the data contract
@@ -594,6 +671,18 @@ message GetDocumentsRequest {
594
671
  BETWEEN_EXCLUDE_RIGHT = 8;
595
672
  IN = 9;
596
673
  STARTS_WITH = 10;
674
+ // Time-range bucket selection (v1 only; the v0 CBOR surface is
675
+ // unaffected). `field` names a timestamp covered by a `timeRange`
676
+ // index. The operand is `WhereClause.time_range` (a
677
+ // `TimeRangeSelection`); `WhereClause.value` must be unset. For the
678
+ // relative selectors (`NEWEST` / `OLDEST`) the server resolves the
679
+ // selection to a bucket-start equality from current block time and
680
+ // the verifier re-derives the same bucket from the quorum-signed
681
+ // metadata time; `BY_START` names the window absolutely, so both
682
+ // sides read the start straight from the query — an ordinary
683
+ // index/count proof either way. See `timeRange` in the document
684
+ // meta-schema and `drive::query::resolve_time_range_bucket_clause`.
685
+ IN_TIME_RANGE = 11;
597
686
  }
598
687
 
599
688
  // Tagged scalar (or list) operand for a `WhereClause`. The
@@ -646,6 +735,54 @@ message GetDocumentsRequest {
646
735
  }
647
736
  }
648
737
 
738
+ // Operand of an `IN_TIME_RANGE` where clause: which window of a
739
+ // `timeRange` grid the query selects. Typed rather than riding
740
+ // `DocumentFieldValue` — the selection is not a field value, and a
741
+ // structured message keeps the selector an enum instead of a
742
+ // magic string.
743
+ //
744
+ // The relative selectors are resolved server-side: `NEWEST` is the
745
+ // freshest started window (largest grid start <= block time; the
746
+ // latest partial slice of history), `OLDEST` the oldest window still
747
+ // active at block time (a near-full trailing window of ~`range` —
748
+ // best for "trending over the last window"). The proof verifier
749
+ // re-derives the same window from the quorum-signed response
750
+ // metadata time, so neither side trusts the other's clock.
751
+ //
752
+ // `BY_START` names a window absolutely — any window, current or
753
+ // historic — by its start. `start_ms` is then required and must lie
754
+ // on the grid (`start_ms == phase + k * step`, in milliseconds);
755
+ // an unaligned start is rejected rather than snapped. A window with
756
+ // no documents (including one that has not started yet) is a
757
+ // provable empty answer, not an error. The relative selectors must
758
+ // NOT carry `start_ms` — one wire spelling per meaning.
759
+ message TimeRangeSelection {
760
+ enum Selector {
761
+ NEWEST = 0;
762
+ OLDEST = 1;
763
+ BY_START = 2;
764
+ }
765
+ // Names one of the field's declared grids, in the contract's own
766
+ // seconds — verbatim from the contract's `timeRange` declaration.
767
+ // Required when more than one `timeRange` grid buckets the field
768
+ // (the bare selector is ambiguous there and rejected); optional
769
+ // while exactly one grid does. A zero `phase` is the proto3
770
+ // default, matching the contract grammar where `phase` is an
771
+ // omittable key — every grid has exactly one wire spelling by
772
+ // construction.
773
+ message Grid {
774
+ uint64 range = 1 [jstype = JS_STRING];
775
+ uint64 step = 2 [jstype = JS_STRING];
776
+ uint64 phase = 3 [jstype = JS_STRING];
777
+ }
778
+ Selector selector = 1;
779
+ // `BY_START` only: the selected window's start, as a millisecond
780
+ // timestamp on the grid (see the message docstring). Rejected on
781
+ // the relative selectors.
782
+ optional uint64 start_ms = 2 [jstype = JS_STRING];
783
+ Grid grid = 3;
784
+ }
785
+
649
786
  // Single `field <op> value` clause. The server reassembles a
650
787
  // `Vec<WhereClause>` from the request's `where_clauses` field,
651
788
  // runs the same `WhereClause::group_clauses` validator (rejects
@@ -654,10 +791,16 @@ message GetDocumentsRequest {
654
791
  // then hands the structured clauses to the executor. Wire
655
792
  // semantics are identical to v0's CBOR `[field, op, value]`
656
793
  // triples — only the envelope differs.
794
+ //
795
+ // Exactly one operand field is set, keyed by the operator:
796
+ // `operator = IN_TIME_RANGE` carries its operand in `time_range`
797
+ // (`value` must be unset); every other operator carries `value`
798
+ // (`time_range` must be unset). Either mismatch is rejected.
657
799
  message WhereClause {
658
800
  string field = 1;
659
801
  WhereOperator operator = 2;
660
802
  DocumentFieldValue value = 3;
803
+ TimeRangeSelection time_range = 4;
661
804
  }
662
805
 
663
806
  // Per-group aggregate operand for the left side of a
@@ -703,12 +846,21 @@ message GetDocumentsRequest {
703
846
  // before release rather than deprecated, because it invented
704
847
  // non-SQL grammar for something SQL already expresses.
705
848
  //
706
- // **`HAVING` cannot yet combine with an aggregate `ORDER BY`.**
707
- // The ranked executor reads a pre-sorted per-axis secondary and
708
- // has no way to drop groups from the middle of that walk, so a
709
- // request carrying both a non-empty `having` and a ranking
710
- // `order_by` is rejected with `Unsupported` rather than served
711
- // with one of the two silently ignored.
849
+ // **From protocol v14 a single `HAVING` clause is served as a
850
+ // bounded range read** (having-range mode): `SELECT <agg> GROUP BY
851
+ // p HAVING <agg> <op> <value> [ORDER BY <order-key> ASC|DESC]
852
+ // LIMIT n` answers from the same per-axis secondary as ranked
853
+ // mode, on an index declaring the matching ranked axis. The
854
+ // clause's aggregate must be the selected aggregate, the operator
855
+ // must describe one contiguous range (`NOT_EQUAL` / `IN` are
856
+ // rejected), and the optional `ORDER BY` picks the walk direction
857
+ // using the same order-key spelling as ranked mode: `f` for
858
+ // `SUM(f)` / `AVG(f)`, the `$count` sentinel for `COUNT(*)` —
859
+ // never an explicit `OrderClause.aggregate` target, which is
860
+ // rejected. See the supported-shape table on
861
+ // `GetDocumentsRequestV1`. On protocol v13 and earlier every
862
+ // non-empty `having` stays rejected with `Unsupported`, exactly as
863
+ // before.
712
864
  //
713
865
  // The operator set mirrors `WhereOperator` minus `STARTS_WITH`
714
866
  // (prefix matching has no natural meaning against a scalar
@@ -849,12 +1001,16 @@ message GetDocumentsRequest {
849
1001
  // It returns `ResultData.ranked`. See `order_by` and the
850
1002
  // supported-shape table below.
851
1003
  //
852
- // `having` is a boolean per-group predicate and is **still**
853
- // `Unsupported` at every protocol version, ranked mode or not
854
- // (`"HAVING clause is not yet implemented"`). It carries no ranking
855
- // spelling: an earlier draft put cross-group ranking on the right of
856
- // a `HAVING` (`HAVING AVG(grade) IN TOP(5)`) and that grammar was
857
- // removed before release in favour of `ORDER BY` + `LIMIT`.
1004
+ // **Having-range mode** is served from protocol v14: a single
1005
+ // `having` clause whose aggregate is the selected aggregate turns
1006
+ // the request into a bounded range read over the same per-axis
1007
+ // secondary ranked mode walks, answered in `ResultData.ranked`.
1008
+ // On protocol v13 and earlier every non-empty `having` is rejected
1009
+ // (`"HAVING clause is not yet implemented"`). `having` carries no
1010
+ // ranking spelling: an earlier draft put cross-group ranking on the
1011
+ // right of a `HAVING` (`HAVING AVG(grade) IN TOP(5)`) and that
1012
+ // grammar was removed before release in favour of `ORDER BY` +
1013
+ // `LIMIT`. See the supported-shape table below.
858
1014
  //
859
1015
  // **Supported shapes** (everything else rejects with a typed
860
1016
  // `QuerySyntaxError::Unsupported` so callers can detect un-wired
@@ -880,12 +1036,16 @@ message GetDocumentsRequest {
880
1036
  // - a is the In field AND b is the range field, in that order → existing compound distinct shape; entries carry both `in_key` (= a's value) and `key` (= b's value).
881
1037
  //
882
1038
  // `select=<COUNT(*)|SUM(f)|AVG(f)>, group_by=[p], order_by=[<the selected aggregate>]` (protocol v14+) — **ranked mode**:
883
- // - exactly one `group_by` property, exactly one `order_by` clause naming the select's aggregate (`f` for `SUM(f)` / `AVG(f)`, the `$count` sentinel for `COUNT(*)`), a `limit` in `1 ..= 100`, an optional `offset`, and no `where` / `having` / `start_at`, on an index declaring the matching `rankedCountable` / `rankedSummable` / `rankedAverageable` axis → ranked executor, answered in `ResultData.ranked`.
1039
+ // - exactly one `group_by` property, exactly one `order_by` clause naming the select's aggregate (`f` for `SUM(f)` / `AVG(f)`, the `$count` sentinel for `COUNT(*)`), a `limit` in `1 ..= 100`, an optional `offset`, and no `having` / `start_at`, on an index declaring the matching `rankedCountable` / `rankedSummable` / `rankedAverageable` axis → ranked executor, answered in `ResultData.ranked`. On a single-property ranked index no `where` is accepted; on a compound ranked index every leading index property must be pinned (one clause per property, `group_by` names the trailing property) — `EQUAL` pins one prefix, and **at most one** clause may be `IN` (2..=10 distinct elements, `null` legal; a single-element `IN` normalizes to the equality pin; an element whose prefix was never written — at any depth of its pinned chain — contributes an empty branch, union semantics), fanning the walk out across one prefix branch per element and merging by `(aggregate, encoded prefix, group key)`; merged entries carry `in_key`. A non-zero `offset` is rejected together with `IN` (rank-skip is per-secondary; `OFFSET 0` is the offset-free request).
884
1040
  // - `DESC` is the "top n" reading (walk the axis from the largest aggregate down), `ASC` the "bottom n" reading. Worked example: `SELECT AVG(grade) GROUP BY restaurantId ORDER BY grade DESC LIMIT 1 OFFSET 4` is the 5th-best restaurant.
885
1041
  //
1042
+ // `select=<COUNT(*)|SUM(f)|AVG(f)>, group_by=[p], having=[<the selected aggregate> <op> <value>]` (protocol v14+) — **having-range mode**:
1043
+ // - exactly one `group_by` property, exactly one `having` clause whose aggregate is the select's aggregate, an operator describing one contiguous range (`EQUAL`, `GREATER_THAN[_OR_EQUALS]`, `LESS_THAN[_OR_EQUALS]`, `BETWEEN*`; `NOT_EQUAL` / `IN` rejected), a `limit` in `1 ..= 100`, an optional `order_by` naming the same aggregate (walk direction; ascending by default), and no `offset` / `start_at` / `start_after`, on an index declaring the matching ranked axis → having-range executor, answered in `ResultData.ranked`. `where` follows the same rule as ranked mode: none on a single-property ranked index; exactly one pin per leading index property on a compound ranked index, at most one of them an `IN` (2..=10 distinct elements; a never-written element contributes an empty branch) that fans the bound out across prefix branches and merges, entries carrying `in_key`.
1044
+ // - no offset or cursor pagination: a page cut at `limit` continues only by tightening the bound past the last *distinct* aggregate value seen; a cut inside a tie (several groups sharing the boundary aggregate) cannot be continued, so size `limit` above the widest expected tie.
1045
+ //
886
1046
  // **Rejected shapes** (return `Unsupported`):
887
- // - any non-empty `having`, at every protocol version.
888
- // - at v14+: a ranked-shaped request carrying a `where` clause, a `start_at` / `start_after` cursor, more than one `order_by`, or an `order_by` naming anything but the selected aggregate.
1047
+ // - any non-empty `having` on protocol v13 and earlier; at v14+, any `having` shape outside having-range mode above (multiple clauses, an aggregate other than the select's, `NOT_EQUAL` / `IN` as the having operator, a `where` shape other than the compound-index prefix pins above — equality pins plus at most one bounded `IN` — or a carried `offset` / cursor).
1048
+ // - at v14+: a ranked-shaped request carrying a `where` shape other than the compound-index prefix pins above (an operator other than `EQUAL` or the one permitted `IN`, more than one `IN`, a `null` pin combined with an `IN`, a repeated or non-leading property, or a missing pin), a `start_at` / `start_after` cursor, more than one `order_by`, or an `order_by` naming anything but the selected aggregate.
889
1049
  // - `select=DOCUMENTS` with non-empty `group_by`.
890
1050
  // - `select=COUNT` with `group_by` on a field that is not constrained by an `In` or range where clause.
891
1051
  // - `select=COUNT` with `group_by.len() > 2`.
@@ -1084,12 +1244,13 @@ message GetDocumentsRequest {
1084
1244
  // `HavingClause` / `HavingAggregate` for the operator and
1085
1245
  // aggregate-function catalogs.
1086
1246
  //
1087
- // **Every non-empty `having` is rejected**, at every protocol
1088
- // version, with `Unsupported("HAVING clause is not yet
1089
- // implemented")`. The wire shape ships ahead of evaluation so
1090
- // callers can construct full `HAVING COUNT(*) > 5 AND
1091
- // SUM(amount) > 100` requests in their builders, and so the
1092
- // capability can land without another version bump.
1247
+ // **From protocol v14 a single clause is served** as a bounded
1248
+ // range read having-range mode; see the message-level
1249
+ // supported-shape table. On v13 and earlier every non-empty
1250
+ // `having` is rejected with `Unsupported("HAVING clause is not
1251
+ // yet implemented")`. Multi-clause `HAVING COUNT(*) > 5 AND
1252
+ // SUM(amount) > 100` requests can still be constructed on the
1253
+ // wire, but stay rejected until a multi-clause evaluator lands.
1093
1254
  //
1094
1255
  // **`having` does not express ranking.** "The n highest-scoring
1095
1256
  // groups" is `ORDER BY <the selected aggregate> DESC LIMIT n`
@@ -1107,13 +1268,17 @@ message GetDocumentsRequest {
1107
1268
  // routes to the ranked executor (`group_by` + a single `order_by`
1108
1269
  // naming the selected aggregate), `offset` skips that many ranks
1109
1270
  // before the returned page, so `ORDER BY avg(grade) DESC LIMIT 1
1110
- // OFFSET 4` is the 5th-best group. The skip is **count-attested**,
1111
- // not walked: grovedb proves it from the counted subtree
1112
- // commitments, so the proof stays `O(log n + k)` at any offset and
1113
- // the response echoes the attested number in
1114
- // `RankedEntries.skipped`. There is deliberately no ceiling — an
1115
- // offset of 4 and an offset of four billion cost the same, so
1116
- // there is no denial-of-service lever a cap would close. An offset
1271
+ // OFFSET 4` is the 5th-best group. The skip is **counted, not
1272
+ // walked**: grovedb descends on each subtree's aggregate count and
1273
+ // collapses whole subtrees that fit inside the remaining offset, so
1274
+ // the work stays `O(log n + k)` at any offset and the response
1275
+ // reports the skip it performed in `RankedEntries.skipped`. On a
1276
+ // proved request that count is additionally *attested* committed
1277
+ // to by the proof and re-derived by the verifier; on an unproved
1278
+ // one it is the node's own report. See `RankedEntries.skipped`. There is deliberately no ceiling — an
1279
+ // offset of 4 and an offset of four billion cost the same *order*
1280
+ // of work — neither walks the region it skips — so there is no
1281
+ // denial-of-service lever a cap would close. An offset
1117
1282
  // past the end of the ranking is a provable answer rather than an
1118
1283
  // error: `entries` comes back empty and `skipped` is the ranking's
1119
1284
  // whole population.
@@ -1124,6 +1289,127 @@ message GetDocumentsRequest {
1124
1289
  // no ranked executor. Cursor pagination via `start_after` /
1125
1290
  // `start_at` remains the supported way to page through documents.
1126
1291
  optional uint32 offset = 12;
1292
+
1293
+ // Chained mode — a provable semi-join:
1294
+ // `SELECT * FROM <outer_document_type> WHERE $id IN
1295
+ // (SELECT <join_property> FROM <document_type> WHERE ...)`.
1296
+ //
1297
+ // Presence of this message selects chained mode: this request's
1298
+ // own `document_type` / `where_clauses` / `order_by` / `limit`
1299
+ // describe the INNER indexOnly query, and the outer half is
1300
+ // DERIVED from its results — the request carries no outer
1301
+ // clauses by design, and the verifier re-derives the outer
1302
+ // query from the proven inner values, so the join cannot be
1303
+ // steered by the responding node.
1304
+ //
1305
+ // Mode gates (rejected otherwise): the inner type must be
1306
+ // indexOnly and resolve to an index carrying `join_property`;
1307
+ // `join_property` must declare a same-contract
1308
+ // `refersTo: permanentDocument` targeting
1309
+ // `outer_document_type`; `limit` is REQUIRED (it bounds the
1310
+ // derived outer query — no server-default fallback) and capped
1311
+ // by the outer `$id IN` clause's 100-value limit; `selects`
1312
+ // must be empty or a single DOCUMENTS projection; `group_by`,
1313
+ // `having`, time-range clauses, cursors, and `offset` are all
1314
+ // rejected. Pagination is a range clause on `join_property`.
1315
+ //
1316
+ // The verifier needs nothing beyond the proof itself: it
1317
+ // subset-verifies the inner query against the merged proof to
1318
+ // extract the join values, re-derives the outer component, and
1319
+ // verifies the whole composition. A node that predates this
1320
+ // field ignores it (proto3 unknown field) and serves the plain
1321
+ // inner query — which FAILS CLOSED client-side: an inner-only
1322
+ // proof cannot satisfy the re-derived merged query for a
1323
+ // non-empty page, and an unproven response carries the wrong
1324
+ // ResultData variant.
1325
+ message ChainedJoin {
1326
+ // The inner property whose proven values become the outer
1327
+ // documents' `$id`s.
1328
+ string join_property = 1;
1329
+ // The joined document type — the `refersTo` target.
1330
+ string outer_document_type = 2;
1331
+ }
1332
+ ChainedJoin chained = 13;
1333
+
1334
+ // Composite mode — a page plus sub-queries DERIVED from its
1335
+ // results, answered as ONE merged proof over one state root.
1336
+ //
1337
+ // Presence of any `sub_queries` selects composite mode: this
1338
+ // request's own `data_contract_id` / `document_type` /
1339
+ // `where_clauses` / `order_by` / `limit` describe the PAGE, and
1340
+ // every sub-query's `IN` clause is derived by the node from the
1341
+ // page's (or an earlier sub-query's) proven documents. The
1342
+ // verifier re-derives every sub-query from the proven page with
1343
+ // the same builders, re-merges, and verifies the whole
1344
+ // composition — so the composition cannot be steered by the
1345
+ // responding node, and a node that predates this field (proto3
1346
+ // unknown field) serves a page-only proof that FAILS CLOSED
1347
+ // client-side.
1348
+ //
1349
+ // Mode gates (rejected otherwise): `limit` is REQUIRED on the
1350
+ // page (at most 100 — it bounds every derived clause); `selects`
1351
+ // must be empty or a single DOCUMENTS projection; `group_by`,
1352
+ // `having`, time-range clauses, cursors and `offset` are
1353
+ // rejected (paginate with a range clause on the page's ordering
1354
+ // property); `chained` and `sub_queries` are mutually exclusive.
1355
+ // See `SubQuery` for the per-sub-query rules.
1356
+ message SubQuery {
1357
+ // The contract this sub-query targets. Empty = the page's own
1358
+ // contract; otherwise any contract (profiles keyed by owner,
1359
+ // names keyed by identity).
1360
+ bytes data_contract_id = 1;
1361
+ string document_type = 2;
1362
+ // The FIXED clauses — everything but the derived `IN`, which
1363
+ // must not be named here.
1364
+ repeated WhereClause where_clauses = 3;
1365
+ // Ordering (documents only). Every component of the merged proof
1366
+ // walks in the page's direction: a bound field missing from here
1367
+ // is appended in that direction by the node and the verifier
1368
+ // alike, and an ordering that disagrees with the page's direction
1369
+ // is refused (turning a limited lookup around would change the
1370
+ // rows it returns).
1371
+ repeated OrderClause order_by = 4;
1372
+ // Documents lookups on a non-unique index REQUIRE a limit: it
1373
+ // caps the rows the lookup returns in total, in walk order, like
1374
+ // an ordinary IN query's limit (at most 100). Lookups already
1375
+ // bounded by their values (a unique index, or an indexOnly
1376
+ // terminal with every prefix fixed), by-id joins (completeness is
1377
+ // set equality) and counts take none.
1378
+ optional uint32 limit = 5;
1379
+ enum Kind {
1380
+ // The matching documents.
1381
+ DOCUMENTS = 0;
1382
+ // One count per derived value from the `countable` index
1383
+ // covering the fixed clauses plus the bound field. Must be
1384
+ // bound, and must not share its index path with a documents
1385
+ // component (the count reads the value trees the documents
1386
+ // query descends past).
1387
+ COUNT = 1;
1388
+ }
1389
+ Kind kind = 6;
1390
+ // The derived clause `<field> IN <values>`. Absent = a SIBLING:
1391
+ // an independent documents query proven under the same root.
1392
+ message Binding {
1393
+ // Whose proven documents supply the values: `0` = the page,
1394
+ // `n` = `sub_queries[n - 1]` (which must precede this one and
1395
+ // be a DOCUMENTS sub-query).
1396
+ uint32 source = 1;
1397
+ // The source property read off each document: `$id`,
1398
+ // `$ownerId`, or an identifier-typed property (dotted paths
1399
+ // reach nested properties). Documents without it contribute
1400
+ // nothing.
1401
+ string source_property = 2;
1402
+ // The sub-query field receiving the `IN` clause. `$id` makes
1403
+ // this a by-id JOIN: the source property must then declare
1404
+ // `refersTo: permanentDocument` targeting this document type,
1405
+ // so every derived id resolves and a missing document is an
1406
+ // invalid proof. Otherwise `$ownerId` or an indexed property
1407
+ // (a LOOKUP, where absence is a proven fact).
1408
+ string field = 3;
1409
+ }
1410
+ Binding bind = 7;
1411
+ }
1412
+ repeated SubQuery sub_queries = 14;
1127
1413
  }
1128
1414
 
1129
1415
  oneof version {
@@ -1372,6 +1658,14 @@ message GetDocumentsResponse {
1372
1658
  // than hardcoding the literal.
1373
1659
  double avg = 4;
1374
1660
  }
1661
+ // The prefix branch this entry came from, set **only** on an
1662
+ // `IN`-pinned request (see the supported-shape table): the
1663
+ // encoded index-key bytes of the `IN` property's pinned value —
1664
+ // empty bytes for the `null` (absent-value) branch. Absent on
1665
+ // single-prefix responses. The same group key can legally appear
1666
+ // under two prefixes, so `(in_key, key)` is the entry's identity
1667
+ // on a merged page, exactly as on `CountEntry`.
1668
+ optional bytes in_key = 5;
1375
1669
  }
1376
1670
 
1377
1671
  // Ranked result entries. **Entry order IS the ranking order** —
@@ -1399,20 +1693,31 @@ message GetDocumentsResponse {
1399
1693
  // group rather than the best.
1400
1694
  //
1401
1695
  // **When a requested offset exceeds the population**, `entries`
1402
- // is empty and `skipped` is the ranking's attested *total*
1696
+ // is empty and `skipped` is the ranking's *total* reported
1403
1697
  // population — a positive, useful answer ("there are only 12
1404
1698
  // groups") rather than a bare empty list.
1405
1699
  //
1406
- // On the proved path the number is grovedb's cryptographically
1407
- // attested count, re-derived by the verifier from the counted
1408
- // subtree commitments in the proof bytes rather than trusted
1409
- // from this field; a proving client should use the verified
1410
- // value. On the unproven read there is nothing to attest and
1411
- // grovedb's read API does not report a short walk, so the server
1412
- // echoes the requested offset. The two therefore disagree in
1413
- // exactly one case an offset past the end, where the unproven
1414
- // read reports the request and the proved one reports the truth.
1415
- // Callers who need the population must prove.
1700
+ // Both paths report the same quantity: the offset you asked for
1701
+ // when the skip succeeded, and the ranking's total population
1702
+ // when the walk ran out of groups first. They no longer disagree
1703
+ // anywhere, including past the end.
1704
+ //
1705
+ // What differs is the *warrant*, not the value. On the proved
1706
+ // path the number is cryptographically attested re-derived by
1707
+ // the verifier from the counted subtree commitments in the proof
1708
+ // bytes rather than trusted from this field so a proving client
1709
+ // should use the verified value and ignore this one. On the
1710
+ // unproven path it is an **unverified claim**, exactly like the
1711
+ // entries beside it: it equals the attested value on an honest
1712
+ // node, and nothing forces a node to be honest. Read "the true
1713
+ // population" as "what this node says the population is".
1714
+ // Callers who need to trust it, rather than merely receive it,
1715
+ // must still prove.
1716
+ //
1717
+ // Do not assume this field equals the offset you requested. It
1718
+ // equals the offset only when the skip succeeded; when the walk
1719
+ // ran out of groups first it is smaller, and that is the answer
1720
+ // rather than an inconsistency.
1416
1721
  optional uint64 skipped = 2 [jstype = JS_STRING];
1417
1722
  }
1418
1723
 
@@ -1450,7 +1755,44 @@ message GetDocumentsResponse {
1450
1755
  // `skipped` carries the page's starting rank — see
1451
1756
  // `RankedEntries`.
1452
1757
  RankedEntries ranked = 5;
1758
+ // Chained-mode result: both halves of the provable
1759
+ // semi-join, in inner order (the last inner projection's
1760
+ // join-property value is the pagination cursor; outer
1761
+ // documents are ordered by first appearance of their id
1762
+ // among the inner projections, deduplicated). Routed when
1763
+ // the request's `chained` message is present.
1764
+ ChainedDocuments chained = 6;
1765
+ // Composite-mode result: the page plus one result per
1766
+ // sub-query, in request order. Routed when the request
1767
+ // carries `sub_queries`.
1768
+ CompositeDocuments composite = 7;
1769
+ }
1770
+ }
1771
+
1772
+ // Both halves of a chained (semi-join) query, each serialized
1773
+ // with its own document type.
1774
+ message ChainedDocuments {
1775
+ repeated bytes inner_documents = 1;
1776
+ repeated bytes outer_documents = 2;
1777
+ }
1778
+
1779
+ // A composite query's page and per-sub-query results, documents
1780
+ // serialized with their own document type.
1781
+ message CompositeDocuments {
1782
+ // The page, exactly as the page query alone would return it.
1783
+ repeated bytes page_documents = 1;
1784
+ message SubQueryResult {
1785
+ oneof result {
1786
+ // DOCUMENTS: a by-id join in first-appearance order of the
1787
+ // derived ids; a lookup or sibling in query order.
1788
+ Documents documents = 1;
1789
+ // COUNT: one entry per derived value that has a count tree
1790
+ // (a value with no entry counts zero), keyed by the
1791
+ // value's index-key bytes.
1792
+ CountEntries counts = 2;
1793
+ }
1453
1794
  }
1795
+ repeated SubQueryResult sub_results = 2;
1454
1796
  }
1455
1797
 
1456
1798
  oneof result {