@dashevo/dapi-grpc 4.1.1 → 4.2.0-beta.1

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.
@@ -18,6 +18,8 @@ service Platform {
18
18
  returns (GetIdentityNonceResponse);
19
19
  rpc getIdentityContractNonce(GetIdentityContractNonceRequest)
20
20
  returns (GetIdentityContractNonceResponse);
21
+ rpc getIdentityKeysRemainingBudgets(GetIdentityKeysRemainingBudgetsRequest)
22
+ returns (GetIdentityKeysRemainingBudgetsResponse);
21
23
  rpc getIdentityBalance(GetIdentityBalanceRequest)
22
24
  returns (GetIdentityBalanceResponse);
23
25
  rpc getIdentitiesBalances(GetIdentitiesBalancesRequest)
@@ -33,8 +35,18 @@ service Platform {
33
35
  rpc getDataContract(GetDataContractRequest) returns (GetDataContractResponse);
34
36
  rpc getDataContractHistory(GetDataContractHistoryRequest)
35
37
  returns (GetDataContractHistoryResponse);
38
+ rpc getDataContractsLatestVersions(GetDataContractsLatestVersionsRequest)
39
+ returns (GetDataContractsLatestVersionsResponse);
36
40
  rpc getDataContracts(GetDataContractsRequest)
37
41
  returns (GetDataContractsResponse);
42
+ rpc getDataContractsByRange(GetDataContractsByRangeRequest)
43
+ returns (GetDataContractsResponse);
44
+ rpc getContractGroupInfo(GetContractGroupInfoRequest)
45
+ returns (GetContractGroupInfoResponse);
46
+ rpc getContractGroupMembers(GetContractGroupMembersRequest)
47
+ returns (GetContractGroupMembersResponse);
48
+ rpc getContractGroupsForContract(GetContractGroupsForContractRequest)
49
+ returns (GetContractGroupsForContractResponse);
38
50
  rpc getDocumentHistory(GetDocumentHistoryRequest)
39
51
  returns (GetDocumentHistoryResponse);
40
52
  rpc getDocuments(GetDocumentsRequest) returns (GetDocumentsResponse);
@@ -263,6 +275,40 @@ message GetIdentityContractNonceResponse {
263
275
  oneof version { GetIdentityContractNonceResponseV0 v0 = 1; }
264
276
  }
265
277
 
278
+ // What is left of the budgets of keys of one identity. A key may carry a
279
+ // total budget (protocol version 14); the key itself never changes, so what
280
+ // remains of it is tracked next to it and read with this query.
281
+ message GetIdentityKeysRemainingBudgetsRequest {
282
+ message GetIdentityKeysRemainingBudgetsRequestV0 {
283
+ bytes identity_id = 1; // ID of the identity
284
+ repeated uint32 key_ids = 2; // IDs of the keys to look up, at least one
285
+ bool prove = 3; // Flag to request a proof as the response
286
+ }
287
+ oneof version { GetIdentityKeysRemainingBudgetsRequestV0 v0 = 1; }
288
+ }
289
+
290
+ message GetIdentityKeysRemainingBudgetsResponse {
291
+ message GetIdentityKeysRemainingBudgetsResponseV0 {
292
+ message KeyRemainingBudgetEntry {
293
+ uint32 key_id = 1; // ID of the key
294
+ // Credits left of the key's total budget. Absent when the key has no
295
+ // budget (or does not exist). Zero means the key can no longer sign.
296
+ optional uint64 remaining_budget = 2 [ jstype = JS_STRING ];
297
+ }
298
+
299
+ message KeysRemainingBudgets {
300
+ repeated KeyRemainingBudgetEntry entries = 1; // One entry per requested key
301
+ }
302
+
303
+ oneof result {
304
+ KeysRemainingBudgets keys_remaining_budgets = 1; // The remaining budgets
305
+ Proof proof = 2; // Proof of the remaining budgets, if requested
306
+ }
307
+ ResponseMetadata metadata = 3; // Metadata about the blockchain state
308
+ }
309
+ oneof version { GetIdentityKeysRemainingBudgetsResponseV0 v0 = 1; }
310
+ }
311
+
266
312
  message GetIdentityBalanceResponse {
267
313
 
268
314
  message GetIdentityBalanceResponseV0 {
@@ -497,6 +543,201 @@ message GetDataContractResponse {
497
543
  oneof version { GetDataContractResponseV0 v0 = 1; }
498
544
  }
499
545
 
546
+ // Returns the current version number of each requested data contract: the
547
+ // cheap way for a client to check that the contracts it already holds are
548
+ // still current. Every distinct requested id gets an entry; an id no contract has gets
549
+ // an entry without `version`. Serialized contracts are added only when
550
+ // `include_contracts` is set.
551
+ //
552
+ // From protocol version 14 every contract carries a four-byte version item
553
+ // beside it in state. Without `include_contracts`, the unproved form answers
554
+ // from Drive's contract cache or that item without loading a contract, and
555
+ // the proof covers the items, a few hundred bytes of hash path per contract.
556
+ // With `include_contracts`, and on earlier protocol versions, the unproved
557
+ // form reads the contracts through the cache and the proof is the
558
+ // multi-contract proof `getDataContracts` returns, which carries the
559
+ // contracts.
560
+ message GetDataContractsLatestVersionsRequest {
561
+ message GetDataContractsLatestVersionsRequestV0 {
562
+ repeated bytes ids =
563
+ 1; // The IDs of the data contracts, at least one and at most 100
564
+ bool include_contracts =
565
+ 2; // When set, found entries also carry the serialized contract
566
+ bool prove = 3; // Flag to request a proof as the response
567
+ }
568
+ oneof version { GetDataContractsLatestVersionsRequestV0 v0 = 1; }
569
+ }
570
+
571
+ message GetDataContractsLatestVersionsResponse {
572
+ message DataContractLatestVersionEntry {
573
+ bytes identifier = 1; // The requested contract id
574
+ optional uint32 version =
575
+ 2; // The contract's current version number; absent when no contract has this id
576
+ optional bytes data_contract =
577
+ 3; // The serialized contract, only when `include_contracts` was set and the contract exists
578
+ }
579
+
580
+ message DataContractsLatestVersions {
581
+ repeated DataContractLatestVersionEntry entries =
582
+ 1; // One entry per requested contract id
583
+ }
584
+
585
+ message GetDataContractsLatestVersionsResponseV0 {
586
+ oneof result {
587
+ DataContractsLatestVersions data_contracts_latest_versions =
588
+ 1; // The current versions, and the contracts if requested
589
+ Proof proof =
590
+ 2; // Cryptographic proof of the data contracts, if requested
591
+ }
592
+ ResponseMetadata metadata = 3; // Metadata about the blockchain state
593
+ }
594
+ oneof version { GetDataContractsLatestVersionsResponseV0 v0 = 1; }
595
+ }
596
+
597
+ // A document type that belongs to a contract group: the contract and the
598
+ // document type name.
599
+ message ContractGroupDocumentTypeMember {
600
+ bytes contract_id = 1; // The contract the document type belongs to
601
+ string document_type_name = 2; // The document type name within the contract
602
+ }
603
+
604
+ // A token that belongs to a contract group: the contract and the token's
605
+ // position within it.
606
+ message ContractGroupTokenMember {
607
+ bytes contract_id = 1; // The contract the token belongs to
608
+ uint32 token_position = 2; // The token's position in the contract (u16)
609
+ }
610
+
611
+ message GetContractGroupInfoRequest {
612
+ message GetContractGroupInfoRequestV0 {
613
+ bytes contract_group_id = 1; // The 32-byte id of the contract group
614
+ bool prove = 2; // Flag to request a proof as the response
615
+ }
616
+ oneof version { GetContractGroupInfoRequestV0 v0 = 1; }
617
+ }
618
+
619
+ message GetContractGroupInfoResponse {
620
+ message ContractGroupInfo {
621
+ bytes owner_id = 1; // The identity that registered the group and owns it
622
+ repeated bytes admin_ids =
623
+ 2; // Identities that may add members besides the owner; empty for a
624
+ // single owner
625
+ optional string name = 3; // The group's name, when it has one
626
+ optional string description = 4; // The group's description, when it has one
627
+ }
628
+
629
+ message GetContractGroupInfoResponseV0 {
630
+ oneof result {
631
+ ContractGroupInfo contract_group_info =
632
+ 1; // The group's stored information; absent when no such group exists
633
+ Proof proof = 2; // Cryptographic proof of the information, if requested
634
+ }
635
+ ResponseMetadata metadata = 3; // Metadata about the blockchain state
636
+ }
637
+ oneof version { GetContractGroupInfoResponseV0 v0 = 1; }
638
+ }
639
+
640
+ message GetContractGroupMembersRequest {
641
+ // Contracts that belong to the group as a whole, in contract id order.
642
+ message ContractMembersQuery {
643
+ optional bytes start_after =
644
+ 1; // 32-byte contract id; the page starts after it
645
+ }
646
+
647
+ // Document types that belong to the group, in contract id then document
648
+ // type name order.
649
+ message DocumentTypeMembersQuery {
650
+ optional ContractGroupDocumentTypeMember start_after =
651
+ 1; // The page starts after this document type
652
+ }
653
+
654
+ // Tokens that belong to the group, in contract id then token position order.
655
+ message TokenMembersQuery {
656
+ optional ContractGroupTokenMember start_after =
657
+ 1; // The page starts after this token
658
+ }
659
+
660
+ message GetContractGroupMembersRequestV0 {
661
+ bytes contract_group_id = 1; // The 32-byte id of the contract group
662
+ oneof members {
663
+ ContractMembersQuery contracts = 2; // Read the member contracts
664
+ DocumentTypeMembersQuery document_types = 3; // Read the member document types
665
+ TokenMembersQuery tokens = 4; // Read the member tokens
666
+ }
667
+ optional uint32 limit =
668
+ 5; // Maximum number of members to return, 1..=100; absent means 100
669
+ bool prove = 6; // Flag to request a proof as the response
670
+ }
671
+ oneof version { GetContractGroupMembersRequestV0 v0 = 1; }
672
+ }
673
+
674
+ message GetContractGroupMembersResponse {
675
+ message ContractMembers {
676
+ repeated bytes contract_ids = 1; // Member contracts, in contract id order
677
+ }
678
+
679
+ message DocumentTypeMembers {
680
+ repeated ContractGroupDocumentTypeMember document_types =
681
+ 1; // Member document types, in contract id then name order
682
+ }
683
+
684
+ message TokenMembers {
685
+ repeated ContractGroupTokenMember tokens =
686
+ 1; // Member tokens, in contract id then position order
687
+ }
688
+
689
+ message GetContractGroupMembersResponseV0 {
690
+ oneof result {
691
+ ContractMembers contracts = 1; // One page of member contracts
692
+ DocumentTypeMembers document_types = 2; // One page of member document types
693
+ TokenMembers tokens = 3; // One page of member tokens
694
+ Proof proof = 4; // Cryptographic proof of the page, if requested
695
+ }
696
+ ResponseMetadata metadata = 5; // Metadata about the blockchain state
697
+ }
698
+ oneof version { GetContractGroupMembersResponseV0 v0 = 1; }
699
+ }
700
+
701
+ message GetContractGroupsForContractRequest {
702
+ message GetContractGroupsForContractRequestV0 {
703
+ bytes contract_id = 1; // The 32-byte id of the contract
704
+ bool prove = 2; // Flag to request a proof as the response
705
+ }
706
+ oneof version { GetContractGroupsForContractRequestV0 v0 = 1; }
707
+ }
708
+
709
+ message GetContractGroupsForContractResponse {
710
+ message DocumentTypeMemberships {
711
+ string document_type_name = 1; // The document type within the contract
712
+ repeated bytes contract_group_ids =
713
+ 2; // The groups the document type belongs to
714
+ }
715
+
716
+ message TokenMemberships {
717
+ uint32 token_position = 1; // The token's position in the contract (u16)
718
+ repeated bytes contract_group_ids = 2; // The groups the token belongs to
719
+ }
720
+
721
+ message ContractGroupMemberships {
722
+ repeated bytes contract_group_ids =
723
+ 1; // The groups the whole contract belongs to
724
+ repeated DocumentTypeMemberships document_types =
725
+ 2; // The groups each document type belongs to
726
+ repeated TokenMemberships tokens = 3; // The groups each token belongs to
727
+ }
728
+
729
+ message GetContractGroupsForContractResponseV0 {
730
+ oneof result {
731
+ ContractGroupMemberships contract_group_memberships =
732
+ 1; // The memberships; every list is empty when the contract belongs
733
+ // to no group
734
+ Proof proof = 2; // Cryptographic proof of the memberships, if requested
735
+ }
736
+ ResponseMetadata metadata = 3; // Metadata about the blockchain state
737
+ }
738
+ oneof version { GetContractGroupsForContractResponseV0 v0 = 1; }
739
+ }
740
+
500
741
  message GetDataContractsRequest {
501
742
  message GetDataContractsRequestV0 {
502
743
  repeated bytes ids =
@@ -506,6 +747,28 @@ message GetDataContractsRequest {
506
747
  oneof version { GetDataContractsRequestV0 v0 = 1; }
507
748
  }
508
749
 
750
+ // Enumerates every data contract on Platform, one page at a time, in
751
+ // ascending contract id order. Answered with `GetDataContractsResponse`:
752
+ // each `DataContractEntry` carries the contract id and, unless `ids_only`
753
+ // is set, the serialized contract. An empty page (no more contracts) is an
754
+ // empty `data_contract_entries` list, never NotFound. A page shorter than
755
+ // `limit` is the last page; otherwise pass the last entry's identifier as
756
+ // `start_after` to fetch the next one.
757
+ message GetDataContractsByRangeRequest {
758
+ message GetDataContractsByRangeRequestV0 {
759
+ optional uint32 limit =
760
+ 1; // Maximum number of contracts to return, 1..=100; absent means 100
761
+ oneof start {
762
+ bytes start_after = 2; // 32-byte contract id; the page starts after it
763
+ bytes start_at = 3; // 32-byte contract id; the page starts at it
764
+ }
765
+ bool ids_only =
766
+ 4; // When set, entries carry only `identifier`; `data_contract` is unset
767
+ bool prove = 5; // Flag to request a proof as the response
768
+ }
769
+ oneof version { GetDataContractsByRangeRequestV0 v0 = 1; }
770
+ }
771
+
509
772
  message GetDataContractsResponse {
510
773
  message DataContractEntry {
511
774
  bytes identifier = 1; // The unique identifier of the data contract
@@ -594,6 +857,18 @@ message GetDocumentsRequest {
594
857
  BETWEEN_EXCLUDE_RIGHT = 8;
595
858
  IN = 9;
596
859
  STARTS_WITH = 10;
860
+ // Time-range bucket selection (v1 only; the v0 CBOR surface is
861
+ // unaffected). `field` names a timestamp covered by a `timeRange`
862
+ // index. The operand is `WhereClause.time_range` (a
863
+ // `TimeRangeSelection`); `WhereClause.value` must be unset. For the
864
+ // relative selectors (`NEWEST` / `OLDEST`) the server resolves the
865
+ // selection to a bucket-start equality from current block time and
866
+ // the verifier re-derives the same bucket from the quorum-signed
867
+ // metadata time; `BY_START` names the window absolutely, so both
868
+ // sides read the start straight from the query — an ordinary
869
+ // index/count proof either way. See `timeRange` in the document
870
+ // meta-schema and `drive::query::resolve_time_range_bucket_clause`.
871
+ IN_TIME_RANGE = 11;
597
872
  }
598
873
 
599
874
  // Tagged scalar (or list) operand for a `WhereClause`. The
@@ -646,6 +921,54 @@ message GetDocumentsRequest {
646
921
  }
647
922
  }
648
923
 
924
+ // Operand of an `IN_TIME_RANGE` where clause: which window of a
925
+ // `timeRange` grid the query selects. Typed rather than riding
926
+ // `DocumentFieldValue` — the selection is not a field value, and a
927
+ // structured message keeps the selector an enum instead of a
928
+ // magic string.
929
+ //
930
+ // The relative selectors are resolved server-side: `NEWEST` is the
931
+ // freshest started window (largest grid start <= block time; the
932
+ // latest partial slice of history), `OLDEST` the oldest window still
933
+ // active at block time (a near-full trailing window of ~`range` —
934
+ // best for "trending over the last window"). The proof verifier
935
+ // re-derives the same window from the quorum-signed response
936
+ // metadata time, so neither side trusts the other's clock.
937
+ //
938
+ // `BY_START` names a window absolutely — any window, current or
939
+ // historic — by its start. `start_ms` is then required and must lie
940
+ // on the grid (`start_ms == phase + k * step`, in milliseconds);
941
+ // an unaligned start is rejected rather than snapped. A window with
942
+ // no documents (including one that has not started yet) is a
943
+ // provable empty answer, not an error. The relative selectors must
944
+ // NOT carry `start_ms` — one wire spelling per meaning.
945
+ message TimeRangeSelection {
946
+ enum Selector {
947
+ NEWEST = 0;
948
+ OLDEST = 1;
949
+ BY_START = 2;
950
+ }
951
+ // Names one of the field's declared grids, in the contract's own
952
+ // seconds — verbatim from the contract's `timeRange` declaration.
953
+ // Required when more than one `timeRange` grid buckets the field
954
+ // (the bare selector is ambiguous there and rejected); optional
955
+ // while exactly one grid does. A zero `phase` is the proto3
956
+ // default, matching the contract grammar where `phase` is an
957
+ // omittable key — every grid has exactly one wire spelling by
958
+ // construction.
959
+ message Grid {
960
+ uint64 range = 1 [jstype = JS_STRING];
961
+ uint64 step = 2 [jstype = JS_STRING];
962
+ uint64 phase = 3 [jstype = JS_STRING];
963
+ }
964
+ Selector selector = 1;
965
+ // `BY_START` only: the selected window's start, as a millisecond
966
+ // timestamp on the grid (see the message docstring). Rejected on
967
+ // the relative selectors.
968
+ optional uint64 start_ms = 2 [jstype = JS_STRING];
969
+ Grid grid = 3;
970
+ }
971
+
649
972
  // Single `field <op> value` clause. The server reassembles a
650
973
  // `Vec<WhereClause>` from the request's `where_clauses` field,
651
974
  // runs the same `WhereClause::group_clauses` validator (rejects
@@ -654,16 +977,26 @@ message GetDocumentsRequest {
654
977
  // then hands the structured clauses to the executor. Wire
655
978
  // semantics are identical to v0's CBOR `[field, op, value]`
656
979
  // triples — only the envelope differs.
980
+ //
981
+ // Exactly one operand field is set, keyed by the operator:
982
+ // `operator = IN_TIME_RANGE` carries its operand in `time_range`
983
+ // (`value` must be unset); every other operator carries `value`
984
+ // (`time_range` must be unset). Either mismatch is rejected.
657
985
  message WhereClause {
658
986
  string field = 1;
659
987
  WhereOperator operator = 2;
660
988
  DocumentFieldValue value = 3;
989
+ TimeRangeSelection time_range = 4;
661
990
  }
662
991
 
663
992
  // Per-group aggregate operand for the left side of a
664
- // `HavingClause`. Only the per-group aggregates live here:
665
- // `MIN` / `MAX` / `TOP` / `BOTTOM` are **cross-group** ranking
666
- // primitives and appear on the right side via `HavingRanking`.
993
+ // `HavingClause`, and the aggregate-function target of an
994
+ // `OrderClause`. Only the per-group aggregates live here
995
+ // `HAVING` is a boolean predicate over one group's own aggregate,
996
+ // and nothing on this message reaches across groups. Cross-group
997
+ // ranking ("which groups score highest?") is expressed with SQL's
998
+ // ordering surface instead: `ORDER BY <the selected aggregate>
999
+ // DESC LIMIT n [OFFSET m]`. See `GetDocumentsRequestV1.order_by`.
667
1000
  //
668
1001
  // **Field semantics by function**:
669
1002
  // - `COUNT`: empty `field` means `COUNT(*)` (group cardinality);
@@ -682,58 +1015,51 @@ message GetDocumentsRequest {
682
1015
  string field = 2;
683
1016
  }
684
1017
 
685
- // Cross-group ranking primitive on the right side of a
686
- // `HavingClause`. The ranking is computed over the set of
687
- // group-aggregate results (one per `GROUP BY` row), so
688
- // `HAVING COUNT(*) EQ MAX` selects groups whose count equals
689
- // the maximum count across all groups, and
690
- // `HAVING COUNT(*) IN TOP(5)` selects groups whose count is
691
- // among the five largest. Concise way to express top-N /
692
- // bottom-N selection without window functions or
693
- // `ORDER BY` + `LIMIT`.
694
- //
695
- // **Operator compatibility**:
696
- // - Scalar operators (`=`, `!=`, `<`, `<=`, `>`, `>=`) work
697
- // with `MIN` / `MAX`. `TOP` / `BOTTOM` with scalar operators
698
- // only make sense when `n=1` (the single largest / smallest);
699
- // evaluation rejects other combinations as ambiguous.
700
- // - `IN` works with `TOP(n)` / `BOTTOM(n)` for set membership.
701
- // - `BETWEEN*` doesn't compose meaningfully with rankings and
702
- // is rejected at evaluation time.
703
- message HavingRanking {
704
- enum Kind {
705
- MIN = 0;
706
- MAX = 1;
707
- TOP = 2;
708
- BOTTOM = 3;
709
- }
710
- Kind kind = 1;
711
- // N-th rank for `TOP` / `BOTTOM` (1-indexed: `n=1` is the
712
- // single largest / smallest). Required for those two kinds;
713
- // must be unset for `MIN` / `MAX`. The wire allows setting
714
- // it on `MIN` / `MAX` for forward compatibility, but
715
- // evaluation rejects it as a malformed ranking.
716
- optional uint64 n = 2 [jstype = JS_STRING];
717
- }
718
-
719
- // Single `HAVING <aggregate> <op> <right>` clause. Multiple
1018
+ // Single `HAVING <aggregate> <op> <value>` clause. Multiple
720
1019
  // entries in `GetDocumentsRequestV1.having` combine with
721
1020
  // implicit AND — same semantics as multiple `where_clauses`
722
1021
  // entries. `HAVING COUNT(*) > 5 AND SUM(amount) > 100` is two
723
1022
  // `HavingClause` rows, not a tree.
724
1023
  //
1024
+ // **`HAVING` is a boolean per-group predicate and nothing else.**
1025
+ // Its right operand is always a literal `DocumentFieldValue`; it
1026
+ // never names another group or the set of groups. Cross-group
1027
+ // ranking — "the 5 highest-scoring groups" — is `ORDER BY <the
1028
+ // selected aggregate> DESC LIMIT 5`, exactly as in SQL, and is
1029
+ // served by the ranked executor (protocol v14+). An earlier draft
1030
+ // of this surface carried the ranking on the right of a `HAVING`
1031
+ // (`HAVING AVG(grade) IN TOP(5)`); that spelling was removed
1032
+ // before release rather than deprecated, because it invented
1033
+ // non-SQL grammar for something SQL already expresses.
1034
+ //
1035
+ // **From protocol v14 a single `HAVING` clause is served as a
1036
+ // bounded range read** (having-range mode): `SELECT <agg> GROUP BY
1037
+ // p HAVING <agg> <op> <value> [ORDER BY <order-key> ASC|DESC]
1038
+ // LIMIT n` answers from the same per-axis secondary as ranked
1039
+ // mode, on an index declaring the matching ranked axis. The
1040
+ // clause's aggregate must be the selected aggregate, the operator
1041
+ // must describe one contiguous range (`NOT_EQUAL` / `IN` are
1042
+ // rejected), and the optional `ORDER BY` picks the walk direction
1043
+ // using the same order-key spelling as ranked mode: `f` for
1044
+ // `SUM(f)` / `AVG(f)`, the `$count` sentinel for `COUNT(*)` —
1045
+ // never an explicit `OrderClause.aggregate` target, which is
1046
+ // rejected. See the supported-shape table on
1047
+ // `GetDocumentsRequestV1`. On protocol v13 and earlier every
1048
+ // non-empty `having` stays rejected with `Unsupported`, exactly as
1049
+ // before.
1050
+ //
725
1051
  // The operator set mirrors `WhereOperator` minus `STARTS_WITH`
726
1052
  // (prefix matching has no natural meaning against a scalar
727
1053
  // aggregate result, even a string-typed one). `BETWEEN*` and
728
1054
  // `IN` operand semantics match `WhereOperator`: `BETWEEN*`
729
1055
  // expects a 2-element `DocumentFieldValue.list` carrying
730
1056
  // `[lower, upper]`, and `IN` expects a `list` of candidate
731
- // values (or a ranking set via `right.ranking`).
1057
+ // values.
732
1058
  //
733
- // The `right` oneof carries either a concrete
734
- // `DocumentFieldValue` (literal comparison target) or a
735
- // `HavingRanking` (cross-group reference). Exactly one is set;
736
- // the wire rejects an unset `right`.
1059
+ // The `right` oneof exists (rather than a bare
1060
+ // `DocumentFieldValue` field) so the "unset right operand" case
1061
+ // stays distinguishable from "the literal null value"; the wire
1062
+ // rejects an unset `right`.
737
1063
  message HavingClause {
738
1064
  enum Operator {
739
1065
  EQUAL = 0;
@@ -752,33 +1078,65 @@ message GetDocumentsRequest {
752
1078
  Operator operator = 2;
753
1079
  oneof right {
754
1080
  DocumentFieldValue value = 3;
755
- HavingRanking ranking = 4;
756
1081
  }
757
1082
  }
758
1083
 
759
- // Single `ORDER BY field <direction>` clause. Multi-field
1084
+ // Single `ORDER BY <target> <direction>` clause. Multi-field
760
1085
  // ordering is expressed by repeating this message at the
761
1086
  // request level (`repeated OrderClause order_by = 4`), matching
762
1087
  // SQL's `ORDER BY a ASC, b DESC` shape.
763
- // Single ORDER BY entry. Multi-entry ordering is expressed by
764
- // repeating this message at the request level.
765
1088
  //
766
1089
  // The `target` oneof carries either a plain field name
767
1090
  // (`ORDER BY field`) or an aggregate function applied to a
768
- // field (`ORDER BY COUNT(*)`, `ORDER BY SUM(amount)`) — the
769
- // latter sorts per-group result rows produced by `GROUP BY`,
770
- // useful with `LIMIT` for top-N / bottom-N selection at the
771
- // routing layer (overlapping `HavingRanking::Top` / `Bottom`
772
- // but more general because the ranking field can be any
773
- // aggregate, not just count).
1091
+ // field (`ORDER BY COUNT(*)`, `ORDER BY SUM(amount)`).
774
1092
  //
775
- // **Aggregate target currently rejected** with
776
- // `Unsupported("ORDER BY on aggregate is not yet implemented")`.
777
- // The wire surface is shipped now so callers can encode the
778
- // shape ahead of server support landing.
1093
+ // **Two distinct roles ride the `field` target.**
1094
+ //
1095
+ // 1. *Row ordering* `select = DOCUMENTS`. `field` names a
1096
+ // document property and the matched rows come back in that
1097
+ // order. This is v0's behaviour, unchanged.
1098
+ //
1099
+ // 2. *Aggregate ordering* — the **ranked** surface (protocol
1100
+ // v14+). With a `GROUP BY` and a single aggregate `select`,
1101
+ // exactly one `order_by` clause naming that select's
1102
+ // aggregate orders the *groups* by their aggregate value and
1103
+ // routes the request to the ranked executor:
1104
+ //
1105
+ // ```text
1106
+ // SELECT AVG(grade) GROUP BY restaurantId
1107
+ // ORDER BY grade DESC LIMIT 3 -- 3 best restaurants
1108
+ // SELECT COUNT(*) GROUP BY restaurantId
1109
+ // ORDER BY $count DESC LIMIT 10 OFFSET 10 -- busiest, page 2
1110
+ // ```
1111
+ //
1112
+ // `SUM(f)` / `AVG(f)` are named by `f` — the same property the
1113
+ // projection aggregates, which is how `ORDER BY avg(grade)`
1114
+ // reads once `SELECT` has already fixed the function.
1115
+ // `COUNT(*)` aggregates no property, so it is named by the
1116
+ // reserved sentinel **`$count`**. The `$` prefix is what keeps
1117
+ // the sentinel from colliding with a real document property:
1118
+ // document properties cannot start with `$` (that namespace is
1119
+ // the system fields' `$id` / `$ownerId` / …), so `$count` can
1120
+ // never be mistaken for a column. `DESC` is the "top n"
1121
+ // reading, `ASC` the "bottom n" reading.
1122
+ //
1123
+ // An `order_by` naming anything other than the selected
1124
+ // aggregate — a second clause, the `GROUP BY` property, an
1125
+ // unrelated field — is rejected rather than normalized: it
1126
+ // asks for an ordering the ranked secondary cannot produce.
1127
+ //
1128
+ // **Aggregate target still rejected** with
1129
+ // `Unsupported("ORDER BY on aggregate keys is not yet
1130
+ // implemented")`. It is the *explicit* spelling of role 2
1131
+ // (`ORDER BY AVG(grade)` rather than `ORDER BY grade` under a
1132
+ // `SELECT AVG(grade)`) and is wire-stable so it can start being
1133
+ // evaluated without another version bump; today the field-target
1134
+ // spelling above is the one the ranked executor reads.
779
1135
  message OrderClause {
780
1136
  oneof target {
781
- // Plain field name. Today's evaluated form.
1137
+ // Plain field name. Today's evaluated form — a document
1138
+ // property for row ordering, or the selected aggregate's
1139
+ // property (`$count` for `COUNT(*)`) for aggregate ordering.
782
1140
  string field = 1;
783
1141
  // Aggregate function applied to a field, sorted by the
784
1142
  // per-group result. `function = DOCUMENTS` is invalid
@@ -823,12 +1181,22 @@ message GetDocumentsRequest {
823
1181
  // other shapes return `Unsupported` (see supported-shape table
824
1182
  // below).
825
1183
  //
826
- // `having` is wire-reserved for a future server capability. Any
827
- // non-empty `having` list currently returns
828
- // `Unsupported("HAVING clause is not yet implemented")`
829
- // regardless of `select` / `group_by`. The wire shape is
830
- // `repeated WhereClause` so when execution lands the surface is
831
- // already typed end-to-end and callers don't need to re-encode.
1184
+ // **Ranked mode** is served from protocol v14 and is selected by
1185
+ // `group_by` + a single `order_by` naming the selected aggregate —
1186
+ // SQL's own top-n spelling, `ORDER BY <agg> DESC LIMIT n OFFSET m`.
1187
+ // It returns `ResultData.ranked`. See `order_by` and the
1188
+ // supported-shape table below.
1189
+ //
1190
+ // **Having-range mode** is served from protocol v14: a single
1191
+ // `having` clause whose aggregate is the selected aggregate turns
1192
+ // the request into a bounded range read over the same per-axis
1193
+ // secondary ranked mode walks, answered in `ResultData.ranked`.
1194
+ // On protocol v13 and earlier every non-empty `having` is rejected
1195
+ // (`"HAVING clause is not yet implemented"`). `having` carries no
1196
+ // ranking spelling: an earlier draft put cross-group ranking on the
1197
+ // right of a `HAVING` (`HAVING AVG(grade) IN TOP(5)`) and that
1198
+ // grammar was removed before release in favour of `ORDER BY` +
1199
+ // `LIMIT`. See the supported-shape table below.
832
1200
  //
833
1201
  // **Supported shapes** (everything else rejects with a typed
834
1202
  // `QuerySyntaxError::Unsupported` so callers can detect un-wired
@@ -853,8 +1221,17 @@ message GetDocumentsRequest {
853
1221
  // `select=COUNT, group_by=[a, b]`:
854
1222
  // - 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).
855
1223
  //
1224
+ // `select=<COUNT(*)|SUM(f)|AVG(f)>, group_by=[p], order_by=[<the selected aggregate>]` (protocol v14+) — **ranked mode**:
1225
+ // - 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).
1226
+ // - `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.
1227
+ //
1228
+ // `select=<COUNT(*)|SUM(f)|AVG(f)>, group_by=[p], having=[<the selected aggregate> <op> <value>]` (protocol v14+) — **having-range mode**:
1229
+ // - 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`.
1230
+ // - 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.
1231
+ //
856
1232
  // **Rejected shapes** (return `Unsupported`):
857
- // - any non-empty `having` (alwayspending future server capability).
1233
+ // - 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).
1234
+ // - 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.
858
1235
  // - `select=DOCUMENTS` with non-empty `group_by`.
859
1236
  // - `select=COUNT` with `group_by` on a field that is not constrained by an `In` or range where clause.
860
1237
  // - `select=COUNT` with `group_by.len() > 2`.
@@ -927,11 +1304,11 @@ message GetDocumentsRequest {
927
1304
  AVG = 3;
928
1305
  // Per-group MIN / MAX — `SELECT MIN(field) GROUP BY
929
1306
  // category` returns the smallest `field` value in each
930
- // category. Semantically distinct from
931
- // `HavingRanking::Min` / `Max` (which are cross-group
932
- // meta-aggregates over group results). MIN/MAX here
933
- // operate over the row values within each group, the
934
- // same way `SUM` and `AVG` do.
1307
+ // category. These operate over the row values *within*
1308
+ // each group, the same way `SUM` and `AVG` do; they are
1309
+ // not a cross-group ranking. To pick out the extreme
1310
+ // *group*, order by the aggregate instead:
1311
+ // `ORDER BY <agg> ASC|DESC LIMIT 1` (see `order_by`).
935
1312
  MIN = 4;
936
1313
  MAX = 5;
937
1314
  }
@@ -1044,34 +1421,181 @@ message GetDocumentsRequest {
1044
1421
  // message-level docstring for the supported-shape table.
1045
1422
  repeated string group_by = 10;
1046
1423
 
1047
- // SQL `HAVING` clauses — aggregate filters that apply to the
1048
- // grouped rows produced by `select=COUNT, group_by=[…]`. The
1049
- // wire shape is `HavingClause`, not `WhereClause`, because
1050
- // HAVING evaluates against per-group aggregates
1051
- // (`COUNT`/`SUM`/`AVG`/`MIN`/`MAX`/`TOP`/`BOTTOM`) rather than
1052
- // row field values. Multiple entries combine with implicit
1053
- // AND. See `HavingClause` / `HavingAggregate` for the
1054
- // operator and aggregate-function catalogs.
1424
+ // SQL `HAVING` clauses — **boolean** aggregate filters that
1425
+ // apply to the grouped rows produced by `select=<COUNT|SUM|AVG>,
1426
+ // group_by=[…]`. The wire shape is `HavingClause`, not
1427
+ // `WhereClause`, because HAVING evaluates against per-group
1428
+ // aggregates (`COUNT` / `SUM` / `AVG`) rather than row field
1429
+ // values. Multiple entries combine with implicit AND. See
1430
+ // `HavingClause` / `HavingAggregate` for the operator and
1431
+ // aggregate-function catalogs.
1055
1432
  //
1056
- // **Always rejected when non-empty** today with
1057
- // `Unsupported("HAVING clause is not yet implemented")`. The
1058
- // wire shape is shipped now so the future server capability
1059
- // can land without another version bump and so callers can
1060
- // construct full `HAVING COUNT(*) > 5 AND SUM(amount) > 100`
1061
- // requests in their builders even before the server evaluates
1062
- // them.
1433
+ // **From protocol v14 a single clause is served** as a bounded
1434
+ // range read having-range mode; see the message-level
1435
+ // supported-shape table. On v13 and earlier every non-empty
1436
+ // `having` is rejected with `Unsupported("HAVING clause is not
1437
+ // yet implemented")`. Multi-clause `HAVING COUNT(*) > 5 AND
1438
+ // SUM(amount) > 100` requests can still be constructed on the
1439
+ // wire, but stay rejected until a multi-clause evaluator lands.
1440
+ //
1441
+ // **`having` does not express ranking.** "The n highest-scoring
1442
+ // groups" is `ORDER BY <the selected aggregate> DESC LIMIT n`
1443
+ // (see `order_by`), which *is* served, from protocol v14. An
1444
+ // earlier draft of this surface spelled it
1445
+ // `HAVING AVG(grade) IN TOP(5)`; that grammar was removed
1446
+ // before release rather than deprecated.
1063
1447
  repeated HavingClause having = 11;
1064
1448
 
1065
1449
  // Row-based pagination offset, on top of the cursor-based
1066
1450
  // `start_after` / `start_at` pagination. `OFFSET N` skips the
1067
- // first `N` matching rows before applying `limit`. Currently
1068
- // **always rejected when non-`None`** with
1069
- // `Unsupported("OFFSET pagination is not yet implemented")`
1070
- // the wire surface is shipped now so callers can encode it
1071
- // ahead of server support landing without another version
1072
- // bump. Cursor pagination via `start_after` / `start_at`
1073
- // remains the supported way to page through results.
1451
+ // first `N` result rows before applying `limit`.
1452
+ //
1453
+ // **Consumed in ranked mode** (protocol v14+): on a request that
1454
+ // routes to the ranked executor (`group_by` + a single `order_by`
1455
+ // naming the selected aggregate), `offset` skips that many ranks
1456
+ // before the returned page, so `ORDER BY avg(grade) DESC LIMIT 1
1457
+ // OFFSET 4` is the 5th-best group. The skip is **counted, not
1458
+ // walked**: grovedb descends on each subtree's aggregate count and
1459
+ // collapses whole subtrees that fit inside the remaining offset, so
1460
+ // the work stays `O(log n + k)` at any offset and the response
1461
+ // reports the skip it performed in `RankedEntries.skipped`. On a
1462
+ // proved request that count is additionally *attested* — committed
1463
+ // to by the proof and re-derived by the verifier; on an unproved
1464
+ // one it is the node's own report. See `RankedEntries.skipped`. There is deliberately no ceiling — an
1465
+ // offset of 4 and an offset of four billion cost the same *order*
1466
+ // of work — neither walks the region it skips — so there is no
1467
+ // denial-of-service lever a cap would close. An offset
1468
+ // past the end of the ranking is a provable answer rather than an
1469
+ // error: `entries` comes back empty and `skipped` is the ranking's
1470
+ // whole population.
1471
+ //
1472
+ // **Rejected everywhere else** with
1473
+ // `Unsupported("OFFSET pagination is not yet implemented")` —
1474
+ // including on every path at protocol v13 and earlier, which has
1475
+ // no ranked executor. Cursor pagination via `start_after` /
1476
+ // `start_at` remains the supported way to page through documents.
1074
1477
  optional uint32 offset = 12;
1478
+
1479
+ // Chained mode — a provable semi-join:
1480
+ // `SELECT * FROM <outer_document_type> WHERE $id IN
1481
+ // (SELECT <join_property> FROM <document_type> WHERE ...)`.
1482
+ //
1483
+ // Presence of this message selects chained mode: this request's
1484
+ // own `document_type` / `where_clauses` / `order_by` / `limit`
1485
+ // describe the INNER indexOnly query, and the outer half is
1486
+ // DERIVED from its results — the request carries no outer
1487
+ // clauses by design, and the verifier re-derives the outer
1488
+ // query from the proven inner values, so the join cannot be
1489
+ // steered by the responding node.
1490
+ //
1491
+ // Mode gates (rejected otherwise): the inner type must be
1492
+ // indexOnly and resolve to an index carrying `join_property`;
1493
+ // `join_property` must declare a same-contract
1494
+ // `refersTo: permanentDocument` targeting
1495
+ // `outer_document_type`; `limit` is REQUIRED (it bounds the
1496
+ // derived outer query — no server-default fallback) and capped
1497
+ // by the outer `$id IN` clause's 100-value limit; `selects`
1498
+ // must be empty or a single DOCUMENTS projection; `group_by`,
1499
+ // `having`, time-range clauses, cursors, and `offset` are all
1500
+ // rejected. Pagination is a range clause on `join_property`.
1501
+ //
1502
+ // The verifier needs nothing beyond the proof itself: it
1503
+ // subset-verifies the inner query against the merged proof to
1504
+ // extract the join values, re-derives the outer component, and
1505
+ // verifies the whole composition. A node that predates this
1506
+ // field ignores it (proto3 unknown field) and serves the plain
1507
+ // inner query — which FAILS CLOSED client-side: an inner-only
1508
+ // proof cannot satisfy the re-derived merged query for a
1509
+ // non-empty page, and an unproven response carries the wrong
1510
+ // ResultData variant.
1511
+ message ChainedJoin {
1512
+ // The inner property whose proven values become the outer
1513
+ // documents' `$id`s.
1514
+ string join_property = 1;
1515
+ // The joined document type — the `refersTo` target.
1516
+ string outer_document_type = 2;
1517
+ }
1518
+ ChainedJoin chained = 13;
1519
+
1520
+ // Composite mode — a page plus sub-queries DERIVED from its
1521
+ // results, answered as ONE merged proof over one state root.
1522
+ //
1523
+ // Presence of any `sub_queries` selects composite mode: this
1524
+ // request's own `data_contract_id` / `document_type` /
1525
+ // `where_clauses` / `order_by` / `limit` describe the PAGE, and
1526
+ // every sub-query's `IN` clause is derived by the node from the
1527
+ // page's (or an earlier sub-query's) proven documents. The
1528
+ // verifier re-derives every sub-query from the proven page with
1529
+ // the same builders, re-merges, and verifies the whole
1530
+ // composition — so the composition cannot be steered by the
1531
+ // responding node, and a node that predates this field (proto3
1532
+ // unknown field) serves a page-only proof that FAILS CLOSED
1533
+ // client-side.
1534
+ //
1535
+ // Mode gates (rejected otherwise): `limit` is REQUIRED on the
1536
+ // page (at most 100 — it bounds every derived clause); `selects`
1537
+ // must be empty or a single DOCUMENTS projection; `group_by`,
1538
+ // `having`, time-range clauses, cursors and `offset` are
1539
+ // rejected (paginate with a range clause on the page's ordering
1540
+ // property); `chained` and `sub_queries` are mutually exclusive.
1541
+ // See `SubQuery` for the per-sub-query rules.
1542
+ message SubQuery {
1543
+ // The contract this sub-query targets. Empty = the page's own
1544
+ // contract; otherwise any contract (profiles keyed by owner,
1545
+ // names keyed by identity).
1546
+ bytes data_contract_id = 1;
1547
+ string document_type = 2;
1548
+ // The FIXED clauses — everything but the derived `IN`, which
1549
+ // must not be named here.
1550
+ repeated WhereClause where_clauses = 3;
1551
+ // Ordering (documents only). Every component of the merged proof
1552
+ // walks in the page's direction: a bound field missing from here
1553
+ // is appended in that direction by the node and the verifier
1554
+ // alike, and an ordering that disagrees with the page's direction
1555
+ // is refused (turning a limited lookup around would change the
1556
+ // rows it returns).
1557
+ repeated OrderClause order_by = 4;
1558
+ // Documents lookups on a non-unique index REQUIRE a limit: it
1559
+ // caps the rows the lookup returns in total, in walk order, like
1560
+ // an ordinary IN query's limit (at most 100). Lookups already
1561
+ // bounded by their values (a unique index, or an indexOnly
1562
+ // terminal with every prefix fixed), by-id joins (completeness is
1563
+ // set equality) and counts take none.
1564
+ optional uint32 limit = 5;
1565
+ enum Kind {
1566
+ // The matching documents.
1567
+ DOCUMENTS = 0;
1568
+ // One count per derived value from the `countable` index
1569
+ // covering the fixed clauses plus the bound field. Must be
1570
+ // bound, and must not share its index path with a documents
1571
+ // component (the count reads the value trees the documents
1572
+ // query descends past).
1573
+ COUNT = 1;
1574
+ }
1575
+ Kind kind = 6;
1576
+ // The derived clause `<field> IN <values>`. Absent = a SIBLING:
1577
+ // an independent documents query proven under the same root.
1578
+ message Binding {
1579
+ // Whose proven documents supply the values: `0` = the page,
1580
+ // `n` = `sub_queries[n - 1]` (which must precede this one and
1581
+ // be a DOCUMENTS sub-query).
1582
+ uint32 source = 1;
1583
+ // The source property read off each document: `$id`,
1584
+ // `$ownerId`, or an identifier-typed property (dotted paths
1585
+ // reach nested properties). Documents without it contribute
1586
+ // nothing.
1587
+ string source_property = 2;
1588
+ // The sub-query field receiving the `IN` clause. `$id` makes
1589
+ // this a by-id JOIN: the source property must then declare
1590
+ // `refersTo: permanentDocument` targeting this document type,
1591
+ // so every derived id resolves and a missing document is an
1592
+ // invalid proof. Otherwise `$ownerId` or an indexed property
1593
+ // (a LOOKUP, where absence is a proven fact).
1594
+ string field = 3;
1595
+ }
1596
+ Binding bind = 7;
1597
+ }
1598
+ repeated SubQuery sub_queries = 14;
1075
1599
  }
1076
1600
 
1077
1601
  oneof version {
@@ -1258,9 +1782,134 @@ message GetDocumentsResponse {
1258
1782
  }
1259
1783
  }
1260
1784
 
1785
+ // One group in a ranked (`GROUP BY … ORDER BY <agg> LIMIT n`)
1786
+ // result: the group's index key plus the aggregate it was ranked
1787
+ // by.
1788
+ //
1789
+ // `key` is the raw index-key bytes of the GROUP BY property's
1790
+ // value — the same bytes that name the group's value tree under
1791
+ // the index (for a `string` property, its UTF-8 bytes). Clients
1792
+ // that want the typed value decode it with the document type's
1793
+ // key deserialization; the wire carries bytes so prover and
1794
+ // verifier agree without a schema round-trip.
1795
+ //
1796
+ // Exactly one `value` variant is set, determined by the
1797
+ // request's SELECT function:
1798
+ // * `count` — `SELECT COUNT(*)`, ranked on the index's
1799
+ // `rankedCountable` axis.
1800
+ // * `sum` — `SELECT SUM(field)`, `rankedSummable` axis.
1801
+ // Signed for the same reason `SumEntry.sum` is.
1802
+ // * `avg` — `SELECT AVG(field)`,
1803
+ // `rankedAverageable` axis.
1804
+ message RankedEntry {
1805
+ bytes key = 1;
1806
+ oneof value {
1807
+ // `jstype = JS_STRING` so JS/Web clients receive a string
1808
+ // and don't round counts > 2^53−1 to the nearest
1809
+ // representable Number — same choice as `CountEntry.count`.
1810
+ uint64 count = 2 [jstype = JS_STRING];
1811
+ // `jstype = JS_STRING` for the same precision reason as
1812
+ // `SumEntry.sum`.
1813
+ sint64 sum = 3 [jstype = JS_STRING];
1814
+ // The group's average, as a **`double` approximation** of the
1815
+ // exact value the Avg axis is ordered by.
1816
+ //
1817
+ // What grovedb actually commits to and sorts by is an `i128`
1818
+ // fixed-point integer: `floor(sum * SCALE / count)` with
1819
+ // euclidean (toward −∞) division, where SCALE is grovedb's
1820
+ // `AVG_FIXED_POINT_SCALE` (currently 10^19). This field is
1821
+ // that integer divided by SCALE in `f64`, i.e.
1822
+ // `fixed_point as f64 / SCALE as f64`.
1823
+ //
1824
+ // A `double` is honest here because `RankedEntry` is only ever
1825
+ // populated on the **no-proof ("quick answer") path**, where
1826
+ // the client has already chosen to trust the server's reply.
1827
+ // A proof-verifying client never reads this field: it
1828
+ // reconstructs each entry from the grovedb proof itself, where
1829
+ // the exact fixed-point `i128` lives, so nothing about proof
1830
+ // verification depends on this number's precision.
1831
+ //
1832
+ // Precision bound: `f64` carries ~15–16 significant decimal
1833
+ // digits, so two groups whose exact fixed-point averages differ
1834
+ // only beyond that can compare equal here. Do not use this
1835
+ // value for equality checks, tie-breaking, or any
1836
+ // reconstruction of the committed integer — request the proof
1837
+ // and read the fixed point from it instead. Entry *order* is
1838
+ // still exact: the server ranks on the i128 before converting.
1839
+ //
1840
+ // SCALE is a grovedb constant, not a wire constant: it moved
1841
+ // from 10^15 to 10^19 before release. Clients that need it
1842
+ // (e.g. to go back to fixed point on the proof path) should
1843
+ // read it from the SDK's re-export (`RANKED_AVG_SCALE`) rather
1844
+ // than hardcoding the literal.
1845
+ double avg = 4;
1846
+ }
1847
+ // The prefix branch this entry came from, set **only** on an
1848
+ // `IN`-pinned request (see the supported-shape table): the
1849
+ // encoded index-key bytes of the `IN` property's pinned value —
1850
+ // empty bytes for the `null` (absent-value) branch. Absent on
1851
+ // single-prefix responses. The same group key can legally appear
1852
+ // under two prefixes, so `(in_key, key)` is the entry's identity
1853
+ // on a merged page, exactly as on `CountEntry`.
1854
+ optional bytes in_key = 5;
1855
+ }
1856
+
1857
+ // Ranked result entries. **Entry order IS the ranking order** —
1858
+ // best-first for `ORDER BY <agg> DESC`, worst-first for `ASC`.
1859
+ // Clients must not re-sort; ties (equal aggregates) come back in
1860
+ // group-key order in the direction of the walk, which is
1861
+ // descending group-key order for `DESC`.
1862
+ //
1863
+ // Fewer than `limit` entries is normal — the index simply has
1864
+ // fewer groups than requested — and is not an error.
1865
+ message RankedEntries {
1866
+ repeated RankedEntry entries = 1;
1867
+
1868
+ // How many groups were skipped before the first entry — the
1869
+ // page's **starting rank base**. Entry `i` of `entries` is the
1870
+ // group at rank `skipped + i` (0-based).
1871
+ //
1872
+ // `0` for an `OFFSET 0` (or offset-less) query, which is what
1873
+ // makes this field additive: a caller that never paginates sees
1874
+ // the value it would have assumed. For `OFFSET m` it is
1875
+ // normally `m`, and it is what turns a page back into a
1876
+ // *ranking* — without it, a caller who asked for
1877
+ // `ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4` receives one
1878
+ // entry with no way to tell that it really is the 5th-best
1879
+ // group rather than the best.
1880
+ //
1881
+ // **When a requested offset exceeds the population**, `entries`
1882
+ // is empty and `skipped` is the ranking's *total* reported
1883
+ // population — a positive, useful answer ("there are only 12
1884
+ // groups") rather than a bare empty list.
1885
+ //
1886
+ // Both paths report the same quantity: the offset you asked for
1887
+ // when the skip succeeded, and the ranking's total population
1888
+ // when the walk ran out of groups first. They no longer disagree
1889
+ // anywhere, including past the end.
1890
+ //
1891
+ // What differs is the *warrant*, not the value. On the proved
1892
+ // path the number is cryptographically attested — re-derived by
1893
+ // the verifier from the counted subtree commitments in the proof
1894
+ // bytes rather than trusted from this field — so a proving client
1895
+ // should use the verified value and ignore this one. On the
1896
+ // unproven path it is an **unverified claim**, exactly like the
1897
+ // entries beside it: it equals the attested value on an honest
1898
+ // node, and nothing forces a node to be honest. Read "the true
1899
+ // population" as "what this node says the population is".
1900
+ // Callers who need to trust it, rather than merely receive it,
1901
+ // must still prove.
1902
+ //
1903
+ // Do not assume this field equals the offset you requested. It
1904
+ // equals the offset only when the skip succeeded; when the walk
1905
+ // ran out of groups first it is smaller, and that is the answer
1906
+ // rather than an inconsistency.
1907
+ optional uint64 skipped = 2 [jstype = JS_STRING];
1908
+ }
1909
+
1261
1910
  // Non-proof result wrapper. The outer `oneof result` switches
1262
1911
  // between this and `proof`; this inner oneof switches between
1263
- // the four non-proof shapes the v1 surface can return.
1912
+ // the non-proof shapes the v1 surface can return.
1264
1913
  message ResultData {
1265
1914
  oneof variant {
1266
1915
  Documents documents = 1;
@@ -1280,7 +1929,56 @@ message GetDocumentsResponse {
1280
1929
  // `book/src/drive/average-index-examples.md` for the design
1281
1930
  // and the grades-contract worked example.
1282
1931
  AverageResults averages = 4;
1932
+ // Ranked-aggregate result. Routed when the request pairs a
1933
+ // single-property `group_by` with a single `order_by` clause
1934
+ // naming the single aggregate `select` (the `$count` sentinel
1935
+ // for `COUNT(*)`) and a `limit` — SQL's `ORDER BY <agg>
1936
+ // DESC LIMIT n [OFFSET m]`. Answered from the per-axis
1937
+ // secondary of an indexed tree, so the index must declare the
1938
+ // matching `rankedCountable` / `rankedSummable` /
1939
+ // `rankedAverageable` keyword (meta-schema v3, protocol
1940
+ // version 14+). Entry order is the ranking order, and
1941
+ // `skipped` carries the page's starting rank — see
1942
+ // `RankedEntries`.
1943
+ RankedEntries ranked = 5;
1944
+ // Chained-mode result: both halves of the provable
1945
+ // semi-join, in inner order (the last inner projection's
1946
+ // join-property value is the pagination cursor; outer
1947
+ // documents are ordered by first appearance of their id
1948
+ // among the inner projections, deduplicated). Routed when
1949
+ // the request's `chained` message is present.
1950
+ ChainedDocuments chained = 6;
1951
+ // Composite-mode result: the page plus one result per
1952
+ // sub-query, in request order. Routed when the request
1953
+ // carries `sub_queries`.
1954
+ CompositeDocuments composite = 7;
1955
+ }
1956
+ }
1957
+
1958
+ // Both halves of a chained (semi-join) query, each serialized
1959
+ // with its own document type.
1960
+ message ChainedDocuments {
1961
+ repeated bytes inner_documents = 1;
1962
+ repeated bytes outer_documents = 2;
1963
+ }
1964
+
1965
+ // A composite query's page and per-sub-query results, documents
1966
+ // serialized with their own document type.
1967
+ message CompositeDocuments {
1968
+ // The page, exactly as the page query alone would return it.
1969
+ repeated bytes page_documents = 1;
1970
+ message SubQueryResult {
1971
+ oneof result {
1972
+ // DOCUMENTS: a by-id join in first-appearance order of the
1973
+ // derived ids; a lookup or sibling in query order.
1974
+ Documents documents = 1;
1975
+ // COUNT: one entry per derived value that has a count tree
1976
+ // (a value with no entry counts zero), keyed by the
1977
+ // value's index-key bytes.
1978
+ CountEntries counts = 2;
1979
+ }
1283
1980
  }
1981
+ repeated SubQueryResult sub_results = 2;
1284
1982
  }
1285
1983
 
1286
1984
  oneof result {