@dashevo/dapi-grpc 4.1.1 → 4.2.0-dev.10

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.
@@ -594,6 +594,18 @@ message GetDocumentsRequest {
594
594
  BETWEEN_EXCLUDE_RIGHT = 8;
595
595
  IN = 9;
596
596
  STARTS_WITH = 10;
597
+ // Time-range bucket selection (v1 only; the v0 CBOR surface is
598
+ // unaffected). `field` names a timestamp covered by a `timeRange`
599
+ // index. The operand is `WhereClause.time_range` (a
600
+ // `TimeRangeSelection`); `WhereClause.value` must be unset. For the
601
+ // relative selectors (`NEWEST` / `OLDEST`) the server resolves the
602
+ // selection to a bucket-start equality from current block time and
603
+ // the verifier re-derives the same bucket from the quorum-signed
604
+ // metadata time; `BY_START` names the window absolutely, so both
605
+ // sides read the start straight from the query — an ordinary
606
+ // index/count proof either way. See `timeRange` in the document
607
+ // meta-schema and `drive::query::resolve_time_range_bucket_clause`.
608
+ IN_TIME_RANGE = 11;
597
609
  }
598
610
 
599
611
  // Tagged scalar (or list) operand for a `WhereClause`. The
@@ -646,6 +658,54 @@ message GetDocumentsRequest {
646
658
  }
647
659
  }
648
660
 
661
+ // Operand of an `IN_TIME_RANGE` where clause: which window of a
662
+ // `timeRange` grid the query selects. Typed rather than riding
663
+ // `DocumentFieldValue` — the selection is not a field value, and a
664
+ // structured message keeps the selector an enum instead of a
665
+ // magic string.
666
+ //
667
+ // The relative selectors are resolved server-side: `NEWEST` is the
668
+ // freshest started window (largest grid start <= block time; the
669
+ // latest partial slice of history), `OLDEST` the oldest window still
670
+ // active at block time (a near-full trailing window of ~`range` —
671
+ // best for "trending over the last window"). The proof verifier
672
+ // re-derives the same window from the quorum-signed response
673
+ // metadata time, so neither side trusts the other's clock.
674
+ //
675
+ // `BY_START` names a window absolutely — any window, current or
676
+ // historic — by its start. `start_ms` is then required and must lie
677
+ // on the grid (`start_ms == phase + k * step`, in milliseconds);
678
+ // an unaligned start is rejected rather than snapped. A window with
679
+ // no documents (including one that has not started yet) is a
680
+ // provable empty answer, not an error. The relative selectors must
681
+ // NOT carry `start_ms` — one wire spelling per meaning.
682
+ message TimeRangeSelection {
683
+ enum Selector {
684
+ NEWEST = 0;
685
+ OLDEST = 1;
686
+ BY_START = 2;
687
+ }
688
+ // Names one of the field's declared grids, in the contract's own
689
+ // seconds — verbatim from the contract's `timeRange` declaration.
690
+ // Required when more than one `timeRange` grid buckets the field
691
+ // (the bare selector is ambiguous there and rejected); optional
692
+ // while exactly one grid does. A zero `phase` is the proto3
693
+ // default, matching the contract grammar where `phase` is an
694
+ // omittable key — every grid has exactly one wire spelling by
695
+ // construction.
696
+ message Grid {
697
+ uint64 range = 1 [jstype = JS_STRING];
698
+ uint64 step = 2 [jstype = JS_STRING];
699
+ uint64 phase = 3 [jstype = JS_STRING];
700
+ }
701
+ Selector selector = 1;
702
+ // `BY_START` only: the selected window's start, as a millisecond
703
+ // timestamp on the grid (see the message docstring). Rejected on
704
+ // the relative selectors.
705
+ optional uint64 start_ms = 2 [jstype = JS_STRING];
706
+ Grid grid = 3;
707
+ }
708
+
649
709
  // Single `field <op> value` clause. The server reassembles a
650
710
  // `Vec<WhereClause>` from the request's `where_clauses` field,
651
711
  // runs the same `WhereClause::group_clauses` validator (rejects
@@ -654,16 +714,26 @@ message GetDocumentsRequest {
654
714
  // then hands the structured clauses to the executor. Wire
655
715
  // semantics are identical to v0's CBOR `[field, op, value]`
656
716
  // triples — only the envelope differs.
717
+ //
718
+ // Exactly one operand field is set, keyed by the operator:
719
+ // `operator = IN_TIME_RANGE` carries its operand in `time_range`
720
+ // (`value` must be unset); every other operator carries `value`
721
+ // (`time_range` must be unset). Either mismatch is rejected.
657
722
  message WhereClause {
658
723
  string field = 1;
659
724
  WhereOperator operator = 2;
660
725
  DocumentFieldValue value = 3;
726
+ TimeRangeSelection time_range = 4;
661
727
  }
662
728
 
663
729
  // 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`.
730
+ // `HavingClause`, and the aggregate-function target of an
731
+ // `OrderClause`. Only the per-group aggregates live here
732
+ // `HAVING` is a boolean predicate over one group's own aggregate,
733
+ // and nothing on this message reaches across groups. Cross-group
734
+ // ranking ("which groups score highest?") is expressed with SQL's
735
+ // ordering surface instead: `ORDER BY <the selected aggregate>
736
+ // DESC LIMIT n [OFFSET m]`. See `GetDocumentsRequestV1.order_by`.
667
737
  //
668
738
  // **Field semantics by function**:
669
739
  // - `COUNT`: empty `field` means `COUNT(*)` (group cardinality);
@@ -682,58 +752,51 @@ message GetDocumentsRequest {
682
752
  string field = 2;
683
753
  }
684
754
 
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
755
+ // Single `HAVING <aggregate> <op> <value>` clause. Multiple
720
756
  // entries in `GetDocumentsRequestV1.having` combine with
721
757
  // implicit AND — same semantics as multiple `where_clauses`
722
758
  // entries. `HAVING COUNT(*) > 5 AND SUM(amount) > 100` is two
723
759
  // `HavingClause` rows, not a tree.
724
760
  //
761
+ // **`HAVING` is a boolean per-group predicate and nothing else.**
762
+ // Its right operand is always a literal `DocumentFieldValue`; it
763
+ // never names another group or the set of groups. Cross-group
764
+ // ranking — "the 5 highest-scoring groups" — is `ORDER BY <the
765
+ // selected aggregate> DESC LIMIT 5`, exactly as in SQL, and is
766
+ // served by the ranked executor (protocol v14+). An earlier draft
767
+ // of this surface carried the ranking on the right of a `HAVING`
768
+ // (`HAVING AVG(grade) IN TOP(5)`); that spelling was removed
769
+ // before release rather than deprecated, because it invented
770
+ // non-SQL grammar for something SQL already expresses.
771
+ //
772
+ // **From protocol v14 a single `HAVING` clause is served as a
773
+ // bounded range read** (having-range mode): `SELECT <agg> GROUP BY
774
+ // p HAVING <agg> <op> <value> [ORDER BY <order-key> ASC|DESC]
775
+ // LIMIT n` answers from the same per-axis secondary as ranked
776
+ // mode, on an index declaring the matching ranked axis. The
777
+ // clause's aggregate must be the selected aggregate, the operator
778
+ // must describe one contiguous range (`NOT_EQUAL` / `IN` are
779
+ // rejected), and the optional `ORDER BY` picks the walk direction
780
+ // using the same order-key spelling as ranked mode: `f` for
781
+ // `SUM(f)` / `AVG(f)`, the `$count` sentinel for `COUNT(*)` —
782
+ // never an explicit `OrderClause.aggregate` target, which is
783
+ // rejected. See the supported-shape table on
784
+ // `GetDocumentsRequestV1`. On protocol v13 and earlier every
785
+ // non-empty `having` stays rejected with `Unsupported`, exactly as
786
+ // before.
787
+ //
725
788
  // The operator set mirrors `WhereOperator` minus `STARTS_WITH`
726
789
  // (prefix matching has no natural meaning against a scalar
727
790
  // aggregate result, even a string-typed one). `BETWEEN*` and
728
791
  // `IN` operand semantics match `WhereOperator`: `BETWEEN*`
729
792
  // expects a 2-element `DocumentFieldValue.list` carrying
730
793
  // `[lower, upper]`, and `IN` expects a `list` of candidate
731
- // values (or a ranking set via `right.ranking`).
794
+ // values.
732
795
  //
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`.
796
+ // The `right` oneof exists (rather than a bare
797
+ // `DocumentFieldValue` field) so the "unset right operand" case
798
+ // stays distinguishable from "the literal null value"; the wire
799
+ // rejects an unset `right`.
737
800
  message HavingClause {
738
801
  enum Operator {
739
802
  EQUAL = 0;
@@ -752,33 +815,65 @@ message GetDocumentsRequest {
752
815
  Operator operator = 2;
753
816
  oneof right {
754
817
  DocumentFieldValue value = 3;
755
- HavingRanking ranking = 4;
756
818
  }
757
819
  }
758
820
 
759
- // Single `ORDER BY field <direction>` clause. Multi-field
821
+ // Single `ORDER BY <target> <direction>` clause. Multi-field
760
822
  // ordering is expressed by repeating this message at the
761
823
  // request level (`repeated OrderClause order_by = 4`), matching
762
824
  // 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
825
  //
766
826
  // The `target` oneof carries either a plain field name
767
827
  // (`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).
828
+ // field (`ORDER BY COUNT(*)`, `ORDER BY SUM(amount)`).
829
+ //
830
+ // **Two distinct roles ride the `field` target.**
831
+ //
832
+ // 1. *Row ordering* `select = DOCUMENTS`. `field` names a
833
+ // document property and the matched rows come back in that
834
+ // order. This is v0's behaviour, unchanged.
835
+ //
836
+ // 2. *Aggregate ordering* — the **ranked** surface (protocol
837
+ // v14+). With a `GROUP BY` and a single aggregate `select`,
838
+ // exactly one `order_by` clause naming that select's
839
+ // aggregate orders the *groups* by their aggregate value and
840
+ // routes the request to the ranked executor:
774
841
  //
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.
842
+ // ```text
843
+ // SELECT AVG(grade) GROUP BY restaurantId
844
+ // ORDER BY grade DESC LIMIT 3 -- 3 best restaurants
845
+ // SELECT COUNT(*) GROUP BY restaurantId
846
+ // ORDER BY $count DESC LIMIT 10 OFFSET 10 -- busiest, page 2
847
+ // ```
848
+ //
849
+ // `SUM(f)` / `AVG(f)` are named by `f` — the same property the
850
+ // projection aggregates, which is how `ORDER BY avg(grade)`
851
+ // reads once `SELECT` has already fixed the function.
852
+ // `COUNT(*)` aggregates no property, so it is named by the
853
+ // reserved sentinel **`$count`**. The `$` prefix is what keeps
854
+ // the sentinel from colliding with a real document property:
855
+ // document properties cannot start with `$` (that namespace is
856
+ // the system fields' `$id` / `$ownerId` / …), so `$count` can
857
+ // never be mistaken for a column. `DESC` is the "top n"
858
+ // reading, `ASC` the "bottom n" reading.
859
+ //
860
+ // An `order_by` naming anything other than the selected
861
+ // aggregate — a second clause, the `GROUP BY` property, an
862
+ // unrelated field — is rejected rather than normalized: it
863
+ // asks for an ordering the ranked secondary cannot produce.
864
+ //
865
+ // **Aggregate target still rejected** with
866
+ // `Unsupported("ORDER BY on aggregate keys is not yet
867
+ // implemented")`. It is the *explicit* spelling of role 2
868
+ // (`ORDER BY AVG(grade)` rather than `ORDER BY grade` under a
869
+ // `SELECT AVG(grade)`) and is wire-stable so it can start being
870
+ // evaluated without another version bump; today the field-target
871
+ // spelling above is the one the ranked executor reads.
779
872
  message OrderClause {
780
873
  oneof target {
781
- // Plain field name. Today's evaluated form.
874
+ // Plain field name. Today's evaluated form — a document
875
+ // property for row ordering, or the selected aggregate's
876
+ // property (`$count` for `COUNT(*)`) for aggregate ordering.
782
877
  string field = 1;
783
878
  // Aggregate function applied to a field, sorted by the
784
879
  // per-group result. `function = DOCUMENTS` is invalid
@@ -823,12 +918,22 @@ message GetDocumentsRequest {
823
918
  // other shapes return `Unsupported` (see supported-shape table
824
919
  // below).
825
920
  //
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.
921
+ // **Ranked mode** is served from protocol v14 and is selected by
922
+ // `group_by` + a single `order_by` naming the selected aggregate —
923
+ // SQL's own top-n spelling, `ORDER BY <agg> DESC LIMIT n OFFSET m`.
924
+ // It returns `ResultData.ranked`. See `order_by` and the
925
+ // supported-shape table below.
926
+ //
927
+ // **Having-range mode** is served from protocol v14: a single
928
+ // `having` clause whose aggregate is the selected aggregate turns
929
+ // the request into a bounded range read over the same per-axis
930
+ // secondary ranked mode walks, answered in `ResultData.ranked`.
931
+ // On protocol v13 and earlier every non-empty `having` is rejected
932
+ // (`"HAVING clause is not yet implemented"`). `having` carries no
933
+ // ranking spelling: an earlier draft put cross-group ranking on the
934
+ // right of a `HAVING` (`HAVING AVG(grade) IN TOP(5)`) and that
935
+ // grammar was removed before release in favour of `ORDER BY` +
936
+ // `LIMIT`. See the supported-shape table below.
832
937
  //
833
938
  // **Supported shapes** (everything else rejects with a typed
834
939
  // `QuerySyntaxError::Unsupported` so callers can detect un-wired
@@ -853,8 +958,17 @@ message GetDocumentsRequest {
853
958
  // `select=COUNT, group_by=[a, b]`:
854
959
  // - 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
960
  //
961
+ // `select=<COUNT(*)|SUM(f)|AVG(f)>, group_by=[p], order_by=[<the selected aggregate>]` (protocol v14+) — **ranked mode**:
962
+ // - 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).
963
+ // - `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.
964
+ //
965
+ // `select=<COUNT(*)|SUM(f)|AVG(f)>, group_by=[p], having=[<the selected aggregate> <op> <value>]` (protocol v14+) — **having-range mode**:
966
+ // - 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`.
967
+ // - 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.
968
+ //
856
969
  // **Rejected shapes** (return `Unsupported`):
857
- // - any non-empty `having` (alwayspending future server capability).
970
+ // - 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).
971
+ // - 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
972
  // - `select=DOCUMENTS` with non-empty `group_by`.
859
973
  // - `select=COUNT` with `group_by` on a field that is not constrained by an `In` or range where clause.
860
974
  // - `select=COUNT` with `group_by.len() > 2`.
@@ -927,11 +1041,11 @@ message GetDocumentsRequest {
927
1041
  AVG = 3;
928
1042
  // Per-group MIN / MAX — `SELECT MIN(field) GROUP BY
929
1043
  // 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.
1044
+ // category. These operate over the row values *within*
1045
+ // each group, the same way `SUM` and `AVG` do; they are
1046
+ // not a cross-group ranking. To pick out the extreme
1047
+ // *group*, order by the aggregate instead:
1048
+ // `ORDER BY <agg> ASC|DESC LIMIT 1` (see `order_by`).
935
1049
  MIN = 4;
936
1050
  MAX = 5;
937
1051
  }
@@ -1044,34 +1158,181 @@ message GetDocumentsRequest {
1044
1158
  // message-level docstring for the supported-shape table.
1045
1159
  repeated string group_by = 10;
1046
1160
 
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.
1161
+ // SQL `HAVING` clauses — **boolean** aggregate filters that
1162
+ // apply to the grouped rows produced by `select=<COUNT|SUM|AVG>,
1163
+ // group_by=[…]`. The wire shape is `HavingClause`, not
1164
+ // `WhereClause`, because HAVING evaluates against per-group
1165
+ // aggregates (`COUNT` / `SUM` / `AVG`) rather than row field
1166
+ // values. Multiple entries combine with implicit AND. See
1167
+ // `HavingClause` / `HavingAggregate` for the operator and
1168
+ // aggregate-function catalogs.
1169
+ //
1170
+ // **From protocol v14 a single clause is served** as a bounded
1171
+ // range read — having-range mode; see the message-level
1172
+ // supported-shape table. On v13 and earlier every non-empty
1173
+ // `having` is rejected with `Unsupported("HAVING clause is not
1174
+ // yet implemented")`. Multi-clause `HAVING COUNT(*) > 5 AND
1175
+ // SUM(amount) > 100` requests can still be constructed on the
1176
+ // wire, but stay rejected until a multi-clause evaluator lands.
1055
1177
  //
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.
1178
+ // **`having` does not express ranking.** "The n highest-scoring
1179
+ // groups" is `ORDER BY <the selected aggregate> DESC LIMIT n`
1180
+ // (see `order_by`), which *is* served, from protocol v14. An
1181
+ // earlier draft of this surface spelled it
1182
+ // `HAVING AVG(grade) IN TOP(5)`; that grammar was removed
1183
+ // before release rather than deprecated.
1063
1184
  repeated HavingClause having = 11;
1064
1185
 
1065
1186
  // Row-based pagination offset, on top of the cursor-based
1066
1187
  // `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.
1188
+ // first `N` result rows before applying `limit`.
1189
+ //
1190
+ // **Consumed in ranked mode** (protocol v14+): on a request that
1191
+ // routes to the ranked executor (`group_by` + a single `order_by`
1192
+ // naming the selected aggregate), `offset` skips that many ranks
1193
+ // before the returned page, so `ORDER BY avg(grade) DESC LIMIT 1
1194
+ // OFFSET 4` is the 5th-best group. The skip is **counted, not
1195
+ // walked**: grovedb descends on each subtree's aggregate count and
1196
+ // collapses whole subtrees that fit inside the remaining offset, so
1197
+ // the work stays `O(log n + k)` at any offset and the response
1198
+ // reports the skip it performed in `RankedEntries.skipped`. On a
1199
+ // proved request that count is additionally *attested* — committed
1200
+ // to by the proof and re-derived by the verifier; on an unproved
1201
+ // one it is the node's own report. See `RankedEntries.skipped`. There is deliberately no ceiling — an
1202
+ // offset of 4 and an offset of four billion cost the same *order*
1203
+ // of work — neither walks the region it skips — so there is no
1204
+ // denial-of-service lever a cap would close. An offset
1205
+ // past the end of the ranking is a provable answer rather than an
1206
+ // error: `entries` comes back empty and `skipped` is the ranking's
1207
+ // whole population.
1208
+ //
1209
+ // **Rejected everywhere else** with
1210
+ // `Unsupported("OFFSET pagination is not yet implemented")` —
1211
+ // including on every path at protocol v13 and earlier, which has
1212
+ // no ranked executor. Cursor pagination via `start_after` /
1213
+ // `start_at` remains the supported way to page through documents.
1074
1214
  optional uint32 offset = 12;
1215
+
1216
+ // Chained mode — a provable semi-join:
1217
+ // `SELECT * FROM <outer_document_type> WHERE $id IN
1218
+ // (SELECT <join_property> FROM <document_type> WHERE ...)`.
1219
+ //
1220
+ // Presence of this message selects chained mode: this request's
1221
+ // own `document_type` / `where_clauses` / `order_by` / `limit`
1222
+ // describe the INNER indexOnly query, and the outer half is
1223
+ // DERIVED from its results — the request carries no outer
1224
+ // clauses by design, and the verifier re-derives the outer
1225
+ // query from the proven inner values, so the join cannot be
1226
+ // steered by the responding node.
1227
+ //
1228
+ // Mode gates (rejected otherwise): the inner type must be
1229
+ // indexOnly and resolve to an index carrying `join_property`;
1230
+ // `join_property` must declare a same-contract
1231
+ // `refersTo: permanentDocument` targeting
1232
+ // `outer_document_type`; `limit` is REQUIRED (it bounds the
1233
+ // derived outer query — no server-default fallback) and capped
1234
+ // by the outer `$id IN` clause's 100-value limit; `selects`
1235
+ // must be empty or a single DOCUMENTS projection; `group_by`,
1236
+ // `having`, time-range clauses, cursors, and `offset` are all
1237
+ // rejected. Pagination is a range clause on `join_property`.
1238
+ //
1239
+ // The verifier needs nothing beyond the proof itself: it
1240
+ // subset-verifies the inner query against the merged proof to
1241
+ // extract the join values, re-derives the outer component, and
1242
+ // verifies the whole composition. A node that predates this
1243
+ // field ignores it (proto3 unknown field) and serves the plain
1244
+ // inner query — which FAILS CLOSED client-side: an inner-only
1245
+ // proof cannot satisfy the re-derived merged query for a
1246
+ // non-empty page, and an unproven response carries the wrong
1247
+ // ResultData variant.
1248
+ message ChainedJoin {
1249
+ // The inner property whose proven values become the outer
1250
+ // documents' `$id`s.
1251
+ string join_property = 1;
1252
+ // The joined document type — the `refersTo` target.
1253
+ string outer_document_type = 2;
1254
+ }
1255
+ 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). Every component of the merged proof
1289
+ // walks in the page's direction: a bound field missing from here
1290
+ // is appended in that direction by the node and the verifier
1291
+ // alike, and an ordering that disagrees with the page's direction
1292
+ // is refused (turning a limited lookup around would change the
1293
+ // rows it returns).
1294
+ repeated OrderClause order_by = 4;
1295
+ // Documents lookups on a non-unique index REQUIRE a limit: it
1296
+ // caps the rows the lookup returns in total, in walk order, like
1297
+ // an ordinary IN query's limit (at most 100). Lookups already
1298
+ // bounded by their values (a unique index, or an indexOnly
1299
+ // terminal with every prefix fixed), by-id joins (completeness is
1300
+ // set equality) and counts take none.
1301
+ optional uint32 limit = 5;
1302
+ enum Kind {
1303
+ // The matching documents.
1304
+ DOCUMENTS = 0;
1305
+ // One count per derived value from the `countable` index
1306
+ // covering the fixed clauses plus the bound field. Must be
1307
+ // bound, and must not share its index path with a documents
1308
+ // component (the count reads the value trees the documents
1309
+ // query descends past).
1310
+ COUNT = 1;
1311
+ }
1312
+ Kind kind = 6;
1313
+ // The derived clause `<field> IN <values>`. Absent = a SIBLING:
1314
+ // an independent documents query proven under the same root.
1315
+ message Binding {
1316
+ // Whose proven documents supply the values: `0` = the page,
1317
+ // `n` = `sub_queries[n - 1]` (which must precede this one and
1318
+ // be a DOCUMENTS sub-query).
1319
+ uint32 source = 1;
1320
+ // The source property read off each document: `$id`,
1321
+ // `$ownerId`, or an identifier-typed property (dotted paths
1322
+ // reach nested properties). Documents without it contribute
1323
+ // nothing.
1324
+ string source_property = 2;
1325
+ // The sub-query field receiving the `IN` clause. `$id` makes
1326
+ // this a by-id JOIN: the source property must then declare
1327
+ // `refersTo: permanentDocument` targeting this document type,
1328
+ // so every derived id resolves and a missing document is an
1329
+ // invalid proof. Otherwise `$ownerId` or an indexed property
1330
+ // (a LOOKUP, where absence is a proven fact).
1331
+ string field = 3;
1332
+ }
1333
+ Binding bind = 7;
1334
+ }
1335
+ repeated SubQuery sub_queries = 14;
1075
1336
  }
1076
1337
 
1077
1338
  oneof version {
@@ -1258,9 +1519,134 @@ message GetDocumentsResponse {
1258
1519
  }
1259
1520
  }
1260
1521
 
1522
+ // One group in a ranked (`GROUP BY … ORDER BY <agg> LIMIT n`)
1523
+ // result: the group's index key plus the aggregate it was ranked
1524
+ // by.
1525
+ //
1526
+ // `key` is the raw index-key bytes of the GROUP BY property's
1527
+ // value — the same bytes that name the group's value tree under
1528
+ // the index (for a `string` property, its UTF-8 bytes). Clients
1529
+ // that want the typed value decode it with the document type's
1530
+ // key deserialization; the wire carries bytes so prover and
1531
+ // verifier agree without a schema round-trip.
1532
+ //
1533
+ // Exactly one `value` variant is set, determined by the
1534
+ // request's SELECT function:
1535
+ // * `count` — `SELECT COUNT(*)`, ranked on the index's
1536
+ // `rankedCountable` axis.
1537
+ // * `sum` — `SELECT SUM(field)`, `rankedSummable` axis.
1538
+ // Signed for the same reason `SumEntry.sum` is.
1539
+ // * `avg` — `SELECT AVG(field)`,
1540
+ // `rankedAverageable` axis.
1541
+ message RankedEntry {
1542
+ bytes key = 1;
1543
+ oneof value {
1544
+ // `jstype = JS_STRING` so JS/Web clients receive a string
1545
+ // and don't round counts > 2^53−1 to the nearest
1546
+ // representable Number — same choice as `CountEntry.count`.
1547
+ uint64 count = 2 [jstype = JS_STRING];
1548
+ // `jstype = JS_STRING` for the same precision reason as
1549
+ // `SumEntry.sum`.
1550
+ sint64 sum = 3 [jstype = JS_STRING];
1551
+ // The group's average, as a **`double` approximation** of the
1552
+ // exact value the Avg axis is ordered by.
1553
+ //
1554
+ // What grovedb actually commits to and sorts by is an `i128`
1555
+ // fixed-point integer: `floor(sum * SCALE / count)` with
1556
+ // euclidean (toward −∞) division, where SCALE is grovedb's
1557
+ // `AVG_FIXED_POINT_SCALE` (currently 10^19). This field is
1558
+ // that integer divided by SCALE in `f64`, i.e.
1559
+ // `fixed_point as f64 / SCALE as f64`.
1560
+ //
1561
+ // A `double` is honest here because `RankedEntry` is only ever
1562
+ // populated on the **no-proof ("quick answer") path**, where
1563
+ // the client has already chosen to trust the server's reply.
1564
+ // A proof-verifying client never reads this field: it
1565
+ // reconstructs each entry from the grovedb proof itself, where
1566
+ // the exact fixed-point `i128` lives, so nothing about proof
1567
+ // verification depends on this number's precision.
1568
+ //
1569
+ // Precision bound: `f64` carries ~15–16 significant decimal
1570
+ // digits, so two groups whose exact fixed-point averages differ
1571
+ // only beyond that can compare equal here. Do not use this
1572
+ // value for equality checks, tie-breaking, or any
1573
+ // reconstruction of the committed integer — request the proof
1574
+ // and read the fixed point from it instead. Entry *order* is
1575
+ // still exact: the server ranks on the i128 before converting.
1576
+ //
1577
+ // SCALE is a grovedb constant, not a wire constant: it moved
1578
+ // from 10^15 to 10^19 before release. Clients that need it
1579
+ // (e.g. to go back to fixed point on the proof path) should
1580
+ // read it from the SDK's re-export (`RANKED_AVG_SCALE`) rather
1581
+ // than hardcoding the literal.
1582
+ double avg = 4;
1583
+ }
1584
+ // The prefix branch this entry came from, set **only** on an
1585
+ // `IN`-pinned request (see the supported-shape table): the
1586
+ // encoded index-key bytes of the `IN` property's pinned value —
1587
+ // empty bytes for the `null` (absent-value) branch. Absent on
1588
+ // single-prefix responses. The same group key can legally appear
1589
+ // under two prefixes, so `(in_key, key)` is the entry's identity
1590
+ // on a merged page, exactly as on `CountEntry`.
1591
+ optional bytes in_key = 5;
1592
+ }
1593
+
1594
+ // Ranked result entries. **Entry order IS the ranking order** —
1595
+ // best-first for `ORDER BY <agg> DESC`, worst-first for `ASC`.
1596
+ // Clients must not re-sort; ties (equal aggregates) come back in
1597
+ // group-key order in the direction of the walk, which is
1598
+ // descending group-key order for `DESC`.
1599
+ //
1600
+ // Fewer than `limit` entries is normal — the index simply has
1601
+ // fewer groups than requested — and is not an error.
1602
+ message RankedEntries {
1603
+ repeated RankedEntry entries = 1;
1604
+
1605
+ // How many groups were skipped before the first entry — the
1606
+ // page's **starting rank base**. Entry `i` of `entries` is the
1607
+ // group at rank `skipped + i` (0-based).
1608
+ //
1609
+ // `0` for an `OFFSET 0` (or offset-less) query, which is what
1610
+ // makes this field additive: a caller that never paginates sees
1611
+ // the value it would have assumed. For `OFFSET m` it is
1612
+ // normally `m`, and it is what turns a page back into a
1613
+ // *ranking* — without it, a caller who asked for
1614
+ // `ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4` receives one
1615
+ // entry with no way to tell that it really is the 5th-best
1616
+ // group rather than the best.
1617
+ //
1618
+ // **When a requested offset exceeds the population**, `entries`
1619
+ // is empty and `skipped` is the ranking's *total* reported
1620
+ // population — a positive, useful answer ("there are only 12
1621
+ // groups") rather than a bare empty list.
1622
+ //
1623
+ // Both paths report the same quantity: the offset you asked for
1624
+ // when the skip succeeded, and the ranking's total population
1625
+ // when the walk ran out of groups first. They no longer disagree
1626
+ // anywhere, including past the end.
1627
+ //
1628
+ // What differs is the *warrant*, not the value. On the proved
1629
+ // path the number is cryptographically attested — re-derived by
1630
+ // the verifier from the counted subtree commitments in the proof
1631
+ // bytes rather than trusted from this field — so a proving client
1632
+ // should use the verified value and ignore this one. On the
1633
+ // unproven path it is an **unverified claim**, exactly like the
1634
+ // entries beside it: it equals the attested value on an honest
1635
+ // node, and nothing forces a node to be honest. Read "the true
1636
+ // population" as "what this node says the population is".
1637
+ // Callers who need to trust it, rather than merely receive it,
1638
+ // must still prove.
1639
+ //
1640
+ // Do not assume this field equals the offset you requested. It
1641
+ // equals the offset only when the skip succeeded; when the walk
1642
+ // ran out of groups first it is smaller, and that is the answer
1643
+ // rather than an inconsistency.
1644
+ optional uint64 skipped = 2 [jstype = JS_STRING];
1645
+ }
1646
+
1261
1647
  // Non-proof result wrapper. The outer `oneof result` switches
1262
1648
  // between this and `proof`; this inner oneof switches between
1263
- // the four non-proof shapes the v1 surface can return.
1649
+ // the non-proof shapes the v1 surface can return.
1264
1650
  message ResultData {
1265
1651
  oneof variant {
1266
1652
  Documents documents = 1;
@@ -1280,7 +1666,56 @@ message GetDocumentsResponse {
1280
1666
  // `book/src/drive/average-index-examples.md` for the design
1281
1667
  // and the grades-contract worked example.
1282
1668
  AverageResults averages = 4;
1669
+ // Ranked-aggregate result. Routed when the request pairs a
1670
+ // single-property `group_by` with a single `order_by` clause
1671
+ // naming the single aggregate `select` (the `$count` sentinel
1672
+ // for `COUNT(*)`) and a `limit` — SQL's `ORDER BY <agg>
1673
+ // DESC LIMIT n [OFFSET m]`. Answered from the per-axis
1674
+ // secondary of an indexed tree, so the index must declare the
1675
+ // matching `rankedCountable` / `rankedSummable` /
1676
+ // `rankedAverageable` keyword (meta-schema v3, protocol
1677
+ // version 14+). Entry order is the ranking order, and
1678
+ // `skipped` carries the page's starting rank — see
1679
+ // `RankedEntries`.
1680
+ RankedEntries ranked = 5;
1681
+ // Chained-mode result: both halves of the provable
1682
+ // semi-join, in inner order (the last inner projection's
1683
+ // join-property value is the pagination cursor; outer
1684
+ // documents are ordered by first appearance of their id
1685
+ // among the inner projections, deduplicated). Routed when
1686
+ // the request's `chained` message is present.
1687
+ ChainedDocuments chained = 6;
1688
+ // Composite-mode result: the page plus one result per
1689
+ // sub-query, in request order. Routed when the request
1690
+ // carries `sub_queries`.
1691
+ CompositeDocuments composite = 7;
1692
+ }
1693
+ }
1694
+
1695
+ // Both halves of a chained (semi-join) query, each serialized
1696
+ // with its own document type.
1697
+ message ChainedDocuments {
1698
+ repeated bytes inner_documents = 1;
1699
+ repeated bytes outer_documents = 2;
1700
+ }
1701
+
1702
+ // A composite query's page and per-sub-query results, documents
1703
+ // serialized with their own document type.
1704
+ message CompositeDocuments {
1705
+ // The page, exactly as the page query alone would return it.
1706
+ repeated bytes page_documents = 1;
1707
+ message SubQueryResult {
1708
+ oneof result {
1709
+ // DOCUMENTS: a by-id join in first-appearance order of the
1710
+ // derived ids; a lookup or sibling in query order.
1711
+ Documents documents = 1;
1712
+ // COUNT: one entry per derived value that has a count tree
1713
+ // (a value with no entry counts zero), keyed by the
1714
+ // value's index-key bytes.
1715
+ CountEntries counts = 2;
1716
+ }
1283
1717
  }
1718
+ repeated SubQueryResult sub_results = 2;
1284
1719
  }
1285
1720
 
1286
1721
  oneof result {