1use std::time::Duration;
9
10use riffdb_proto::app::v1 as app_v1;
11use riffdb_proto::{aggregate_sum_from_proto, canonical_value_from_proto};
12use riffdb_types::{
13 CanonicalValue, CommitToken, FreshnessPolicy, ProjectionFrontier, decode_canonical_value,
14};
15
16use crate::application::{
17 ApplicationClientError, ApplicationContract, ApplicationValue, lower_value, validate_contract,
18};
19use crate::{CallMetadata, RiffDbClient, generate_request_id};
20
21#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23pub enum ProjectedResponseEncoding {
24 #[default]
26 Row,
27 Packed,
29}
30
31#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
33pub enum ProjectedSortDirection {
34 #[default]
36 Asc,
37 Desc,
39}
40
41#[derive(Clone, Debug, Eq, PartialEq)]
43pub enum ProjectedPredicate {
44 Eq {
46 field: String,
48 value: ApplicationValue,
50 },
51 Range {
53 field: String,
55 low: Option<ApplicationValue>,
57 high: Option<ApplicationValue>,
59 },
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct ProjectedOrder {
65 pub field: String,
67 pub direction: ProjectedSortDirection,
69}
70
71#[derive(Clone, Debug, Eq, PartialEq)]
76pub enum ProjectedAggregate {
77 Count,
79 CountPresent {
81 field: String,
83 },
84 CountDistinct {
86 field: String,
88 },
89 CountDistinctPresent {
91 field: String,
93 },
94 Sum {
96 field: String,
98 },
99 Mean {
101 field: String,
103 },
104 Min {
106 field: String,
108 },
109 Max {
111 field: String,
113 },
114 Any {
116 field: String,
118 },
119 All {
121 field: String,
123 },
124}
125
126#[derive(Clone, Debug, Eq, PartialEq)]
128pub enum ProjectedAggregateValue {
129 Count(u64),
132 Sum(i128),
134 ExactMean {
136 total: i128,
138 count: u64,
140 },
141 Scalar(Option<CanonicalValue>),
147 Bool(bool),
149}
150
151#[derive(Clone, Debug, Eq, PartialEq)]
153pub struct ProjectedAggregateGroup {
154 pub keys: Vec<CanonicalValue>,
158 pub values: Vec<ProjectedAggregateValue>,
160}
161
162#[derive(Clone, Debug, Eq, PartialEq)]
164pub struct ProjectedQuery {
165 contract: ApplicationContract,
166 projection_name: String,
167 org_scope: ApplicationValue,
168 select: Vec<String>,
169 predicates: Vec<ProjectedPredicate>,
170 order: Vec<ProjectedOrder>,
171 limit: Option<u32>,
172 group_by: Vec<String>,
173 aggregates: Vec<ProjectedAggregate>,
174 freshness: FreshnessPolicy,
175 encoding: ProjectedResponseEncoding,
176}
177
178impl ProjectedQuery {
179 pub fn new(
181 contract: ApplicationContract,
182 projection_name: impl Into<String>,
183 org_scope: ApplicationValue,
184 ) -> Result<Self, ApplicationClientError> {
185 let projection_name = projection_name.into();
186 if projection_name.is_empty() || projection_name.len() > 256 {
187 return Err(ApplicationClientError::InvalidInput);
188 }
189 validate_contract(&contract)?;
190 Ok(Self {
191 contract,
192 projection_name,
193 org_scope,
194 select: Vec::new(),
195 predicates: Vec::new(),
196 order: Vec::new(),
197 limit: None,
198 group_by: Vec::new(),
199 aggregates: Vec::new(),
200 freshness: FreshnessPolicy::Available,
201 encoding: ProjectedResponseEncoding::Row,
202 })
203 }
204
205 #[must_use]
207 pub fn select(mut self, select: Vec<String>) -> Self {
208 self.select = select;
209 self
210 }
211
212 #[must_use]
214 pub fn predicates(mut self, predicates: Vec<ProjectedPredicate>) -> Self {
215 self.predicates = predicates;
216 self
217 }
218
219 #[must_use]
221 pub fn order(mut self, order: Vec<ProjectedOrder>) -> Self {
222 self.order = order;
223 self
224 }
225
226 #[must_use]
236 pub fn limit(mut self, limit: Option<u32>) -> Self {
237 self.limit = limit;
238 self
239 }
240
241 #[must_use]
250 pub fn aggregate(mut self, aggregate: ProjectedAggregate) -> Self {
251 self.aggregates.push(aggregate);
252 self
253 }
254
255 #[must_use]
257 pub fn group_by(mut self, keys: Vec<String>) -> Self {
258 self.group_by = keys;
259 self
260 }
261
262 #[must_use]
264 pub fn freshness(mut self, freshness: FreshnessPolicy) -> Self {
265 self.freshness = freshness;
266 self
267 }
268
269 #[must_use]
271 pub const fn encoding(mut self, encoding: ProjectedResponseEncoding) -> Self {
272 self.encoding = encoding;
273 self
274 }
275
276 #[must_use]
278 pub fn projection_name(&self) -> &str {
279 &self.projection_name
280 }
281
282 #[must_use]
284 pub const fn response_encoding(&self) -> ProjectedResponseEncoding {
285 self.encoding
286 }
287
288 fn response_shape(&self) -> ProjectedResponseShape {
294 if self.aggregates.is_empty() && self.group_by.is_empty() {
295 ProjectedResponseShape::Rows(self.encoding)
296 } else {
297 ProjectedResponseShape::Aggregates(ProjectedAggregateShape {
298 group_by: self.group_by.clone(),
299 aggregates: self.aggregates.clone(),
300 order: self.order.clone(),
301 })
302 }
303 }
304
305 pub fn into_wire_request(
307 self,
308 ) -> Result<app_v1::ExecuteProjectedQueryRequest, ApplicationClientError> {
309 let request_id = Vec::from(
310 generate_request_id()
311 .map_err(|_| ApplicationClientError::IdentifierUnavailable)?
312 .into_bytes(),
313 );
314 let mut predicates = Vec::with_capacity(self.predicates.len());
315 for predicate in self.predicates {
316 predicates.push(lower_predicate(predicate)?);
317 }
318 if !self.group_by.is_empty() {
319 for item in &self.order {
320 if !self.group_by.contains(&item.field) {
321 return Err(ApplicationClientError::InvalidInput);
322 }
323 }
324 }
325 let order = self
326 .order
327 .into_iter()
328 .map(|item| {
329 if item.field.is_empty() || item.field.len() > 256 {
330 return Err(ApplicationClientError::InvalidInput);
331 }
332 Ok(app_v1::ProjectedOrder {
333 field: item.field,
334 descending: matches!(item.direction, ProjectedSortDirection::Desc),
335 })
336 })
337 .collect::<Result<Vec<_>, _>>()?;
338 for name in &self.select {
339 if name.is_empty() || name.len() > 256 {
340 return Err(ApplicationClientError::InvalidInput);
341 }
342 }
343 for name in &self.group_by {
344 if name.is_empty() || name.len() > 256 {
345 return Err(ApplicationClientError::InvalidInput);
346 }
347 }
348 if self.group_by.is_empty() && self.aggregates.len() > 1 {
351 return Err(ApplicationClientError::InvalidInput);
352 }
353 let aggregates = self
354 .aggregates
355 .into_iter()
356 .map(lower_aggregate)
357 .collect::<Result<Vec<_>, _>>()?;
358 Ok(app_v1::ExecuteProjectedQueryRequest {
359 contract: lower_contract_selector(self.contract),
360 projection_name: self.projection_name,
361 request: Some(app_v1::ProjectedQueryBody {
362 select: self.select,
363 org_scope: Some(lower_value(self.org_scope)?),
364 predicates,
365 order,
366 limit: self.limit,
367 group_by: self.group_by,
368 aggregates,
369 }),
370 freshness: Some(lower_freshness(self.freshness)),
371 response_encoding: match self.encoding {
372 ProjectedResponseEncoding::Row => {
373 Some(app_v1::ProjectedResponseEncoding::Row as i32)
374 }
375 ProjectedResponseEncoding::Packed => {
376 Some(app_v1::ProjectedResponseEncoding::Packed as i32)
377 }
378 },
379 request_id,
380 })
381 }
382}
383
384#[derive(Clone, Debug, Eq, PartialEq)]
386pub struct ProjectedReadyRow {
387 pub cells: Vec<CanonicalValue>,
389 pub primary_key: Vec<CanonicalValue>,
391}
392
393#[derive(Clone, Copy, Debug, Eq, PartialEq)]
395pub enum ProjectedRebuildingReason {
396 ReplayBudgetExceeded,
398 ExplicitRebuild,
400 StateIntegrityFailure,
402}
403
404#[derive(Clone, Copy, Debug, Eq, PartialEq)]
406pub enum ProjectedDegradedReason {
407 ApplyLagSlo,
409 MaintenanceBacklog,
411 PartialInventory,
413}
414
415#[derive(Clone, Debug, Eq, PartialEq)]
417pub enum ProjectedQueryOutcome {
418 Ready {
432 fields: Vec<String>,
437 primary_key_fields: Vec<String>,
441 rows: Vec<ProjectedReadyRow>,
443 frontier: ProjectionFrontier,
445 head: ProjectionFrontier,
447 commit_token: Option<CommitToken>,
449 },
450 ReadyAggregates {
461 group_key_fields: Vec<String>,
463 aggregates: Vec<ProjectedAggregate>,
466 groups: Vec<ProjectedAggregateGroup>,
468 frontier: ProjectionFrontier,
470 head: ProjectionFrontier,
472 commit_token: Option<CommitToken>,
474 },
475 Lagging {
477 required: ProjectionFrontier,
479 current: ProjectionFrontier,
481 head: ProjectionFrontier,
483 lag_sequences: Option<u64>,
485 retry_after: Option<Duration>,
487 },
488 Building {
490 applied_through: ProjectionFrontier,
492 head: ProjectionFrontier,
494 },
495 Rebuilding {
497 reason: ProjectedRebuildingReason,
499 progress_applied: u64,
501 progress_total: u64,
503 },
504 Degraded {
506 reason: ProjectedDegradedReason,
508 current: ProjectionFrontier,
510 },
511 Invalid {
513 expected_fingerprint: Vec<u8>,
515 found_fingerprint: Vec<u8>,
517 },
518}
519
520impl RiffDbClient {
521 pub async fn execute_projected_query(
523 &mut self,
524 query: ProjectedQuery,
525 metadata: &CallMetadata,
526 ) -> Result<ProjectedQueryOutcome, ApplicationClientError> {
527 let shape = query.response_shape();
528 let request = query.into_wire_request()?;
529 let response = self
530 .execute_projected_query_raw(request, metadata)
531 .await
532 .map_err(ApplicationClientError::from)?;
533 raise_projected_outcome(response, shape)
534 }
535}
536
537impl crate::StableApplicationClient {
538 pub async fn execute_projected_query(
540 &mut self,
541 query: ProjectedQuery,
542 metadata: &CallMetadata,
543 ) -> Result<ProjectedQueryOutcome, ApplicationClientError> {
544 self.inner.execute_projected_query(query, metadata).await
545 }
546}
547
548#[doc(hidden)]
556pub fn raise_projected_response_for_query(
557 response: app_v1::ExecuteProjectedQueryResponse,
558 query: &ProjectedQuery,
559) -> Result<ProjectedQueryOutcome, ApplicationClientError> {
560 raise_projected_outcome(response, query.response_shape())
561}
562
563fn lower_contract_selector(contract: ApplicationContract) -> Option<app_v1::ContractSelector> {
564 match contract {
565 ApplicationContract::Active => None,
566 ApplicationContract::Exact {
567 lineage,
568 version,
569 bundle_hash,
570 } => Some(app_v1::ContractSelector {
571 lineage,
572 version,
573 bundle_hash: bundle_hash.map_or_else(Vec::new, |hash| hash.to_vec()),
574 }),
575 }
576}
577
578fn lower_predicate(
579 predicate: ProjectedPredicate,
580) -> Result<app_v1::ProjectedPredicate, ApplicationClientError> {
581 match predicate {
582 ProjectedPredicate::Eq { field, value } => {
583 if field.is_empty() || field.len() > 256 {
584 return Err(ApplicationClientError::InvalidInput);
585 }
586 Ok(app_v1::ProjectedPredicate {
587 field,
588 kind: Some(app_v1::projected_predicate::Kind::Eq(lower_value(value)?)),
589 })
590 }
591 ProjectedPredicate::Range { field, low, high } => {
592 if field.is_empty() || field.len() > 256 {
593 return Err(ApplicationClientError::InvalidInput);
594 }
595 Ok(app_v1::ProjectedPredicate {
596 field,
597 kind: Some(app_v1::projected_predicate::Kind::Range(
598 app_v1::ProjectedRange {
599 lower: low.map(lower_value).transpose()?,
600 upper: high.map(lower_value).transpose()?,
601 lower_inclusive: true,
603 upper_inclusive: false,
604 },
605 )),
606 })
607 }
608 }
609}
610
611fn lower_aggregate(
612 aggregate: ProjectedAggregate,
613) -> Result<app_v1::ProjectedAggregate, ApplicationClientError> {
614 let (op, field) = match aggregate {
615 ProjectedAggregate::Count => (app_v1::ProjectedAggregateOp::Count, String::new()),
616 ProjectedAggregate::CountPresent { field } => {
617 (app_v1::ProjectedAggregateOp::CountPresent, field)
618 }
619 ProjectedAggregate::CountDistinct { field } => {
620 (app_v1::ProjectedAggregateOp::CountDistinct, field)
621 }
622 ProjectedAggregate::CountDistinctPresent { field } => {
623 (app_v1::ProjectedAggregateOp::CountDistinctPresent, field)
624 }
625 ProjectedAggregate::Sum { field } => (app_v1::ProjectedAggregateOp::Sum, field),
626 ProjectedAggregate::Mean { field } => (app_v1::ProjectedAggregateOp::Mean, field),
627 ProjectedAggregate::Min { field } => (app_v1::ProjectedAggregateOp::Min, field),
628 ProjectedAggregate::Max { field } => (app_v1::ProjectedAggregateOp::Max, field),
629 ProjectedAggregate::Any { field } => (app_v1::ProjectedAggregateOp::Any, field),
630 ProjectedAggregate::All { field } => (app_v1::ProjectedAggregateOp::All, field),
631 };
632 if op != app_v1::ProjectedAggregateOp::Count && (field.is_empty() || field.len() > 256) {
633 return Err(ApplicationClientError::InvalidInput);
634 }
635 Ok(app_v1::ProjectedAggregate {
636 field,
637 op: op as i32,
638 })
639}
640
641fn lower_freshness(freshness: FreshnessPolicy) -> app_v1::FreshnessPolicyProto {
642 let policy = match freshness {
643 FreshnessPolicy::Available => {
644 app_v1::freshness_policy_proto::Policy::Available(app_v1::FreshnessAvailable {})
645 }
646 FreshnessPolicy::Bounded { max_lag_sequences } => {
647 app_v1::freshness_policy_proto::Policy::Bounded(app_v1::FreshnessBounded {
648 max_lag_sequences,
649 })
650 }
651 FreshnessPolicy::Causal { token, max_wait } => {
652 app_v1::freshness_policy_proto::Policy::Causal(app_v1::FreshnessCausal {
653 commit_token: token.into_bytes(),
654 max_wait_nanos: u64::try_from(max_wait.as_nanos()).unwrap_or(u64::MAX),
655 })
656 }
657 };
658 app_v1::FreshnessPolicyProto {
659 policy: Some(policy),
660 }
661}
662
663#[derive(Clone, Debug, Eq, PartialEq)]
670struct ProjectedAggregateShape {
671 group_by: Vec<String>,
673 aggregates: Vec<ProjectedAggregate>,
675 order: Vec<ProjectedOrder>,
677}
678
679#[derive(Clone, Debug, Eq, PartialEq)]
681enum ProjectedResponseShape {
682 Rows(ProjectedResponseEncoding),
684 Aggregates(ProjectedAggregateShape),
686}
687
688fn raise_projected_outcome(
689 response: app_v1::ExecuteProjectedQueryResponse,
690 shape: ProjectedResponseShape,
691) -> Result<ProjectedQueryOutcome, ApplicationClientError> {
692 match response.outcome {
693 Some(app_v1::execute_projected_query_response::Outcome::Ready(ready)) => {
694 if shape != ProjectedResponseShape::Rows(ProjectedResponseEncoding::Row) {
695 return Err(ApplicationClientError::InvalidResponse);
698 }
699 raise_ready_row(ready)
700 }
701 Some(app_v1::execute_projected_query_response::Outcome::ReadyPacked(packed)) => {
702 if shape != ProjectedResponseShape::Rows(ProjectedResponseEncoding::Packed) {
703 return Err(ApplicationClientError::InvalidResponse);
706 }
707 raise_ready_packed(packed)
708 }
709 Some(app_v1::execute_projected_query_response::Outcome::ReadyAggregates(aggregates)) => {
710 let ProjectedResponseShape::Aggregates(requested) = shape else {
711 return Err(ApplicationClientError::InvalidResponse);
714 };
715 raise_ready_aggregates(aggregates, &requested)
716 }
717 Some(app_v1::execute_projected_query_response::Outcome::Lagging(lagging)) => {
718 Ok(ProjectedQueryOutcome::Lagging {
719 required: raise_frontier(lagging.required)?,
720 current: raise_frontier(lagging.current)?,
721 head: raise_frontier(lagging.head)?,
722 lag_sequences: lagging.lag_sequences,
723 retry_after: lagging.retry_after_nanos.map(Duration::from_nanos),
724 })
725 }
726 Some(app_v1::execute_projected_query_response::Outcome::Building(building)) => {
727 Ok(ProjectedQueryOutcome::Building {
728 applied_through: raise_frontier(building.applied_through)?,
729 head: raise_frontier(building.head)?,
730 })
731 }
732 Some(app_v1::execute_projected_query_response::Outcome::Rebuilding(rebuilding)) => {
733 Ok(ProjectedQueryOutcome::Rebuilding {
734 reason: raise_rebuilding_reason(rebuilding.reason)?,
735 progress_applied: rebuilding.progress_applied,
736 progress_total: rebuilding.progress_total,
737 })
738 }
739 Some(app_v1::execute_projected_query_response::Outcome::Degraded(degraded)) => {
740 Ok(ProjectedQueryOutcome::Degraded {
741 reason: raise_degraded_reason(degraded.reason)?,
742 current: raise_frontier(degraded.current)?,
743 })
744 }
745 Some(app_v1::execute_projected_query_response::Outcome::Invalid(invalid)) => {
746 Ok(ProjectedQueryOutcome::Invalid {
747 expected_fingerprint: invalid.expected_fingerprint,
748 found_fingerprint: invalid.found_fingerprint,
749 })
750 }
751 None => Err(ApplicationClientError::InvalidResponse),
752 }
753}
754
755fn raise_ready_row(
756 ready: app_v1::ProjectedQueryReady,
757) -> Result<ProjectedQueryOutcome, ApplicationClientError> {
758 let fields = ready.fields;
762 let mut rows = Vec::with_capacity(ready.rows.len());
763 for row in ready.rows {
764 let mut cells = Vec::with_capacity(fields.len());
765 let mut by_name = std::collections::BTreeMap::new();
767 for parameter in row.fields {
768 let value = parameter
769 .value
770 .ok_or(ApplicationClientError::InvalidResponse)?;
771 let canonical = canonical_value_from_proto(value)
772 .map_err(|_| ApplicationClientError::InvalidResponse)?;
773 if by_name.insert(parameter.name, canonical).is_some() {
774 return Err(ApplicationClientError::InvalidResponse);
775 }
776 }
777 for name in &fields {
778 let value = by_name
779 .remove(name)
780 .ok_or(ApplicationClientError::InvalidResponse)?;
781 cells.push(value);
782 }
783 if !by_name.is_empty() {
784 return Err(ApplicationClientError::InvalidResponse);
785 }
786 rows.push(ProjectedReadyRow {
787 cells,
788 primary_key: Vec::new(),
789 });
790 }
791 Ok(ProjectedQueryOutcome::Ready {
792 fields,
793 primary_key_fields: Vec::new(),
794 rows,
795 frontier: raise_frontier(ready.frontier)?,
796 head: raise_frontier(ready.head)?,
797 commit_token: raise_optional_token(ready.commit_token)?,
798 })
799}
800
801fn raise_ready_packed(
811 packed: app_v1::ProjectedReadyPacked,
812) -> Result<ProjectedQueryOutcome, ApplicationClientError> {
813 let expected_columns = packed
814 .primary_key_fields
815 .len()
816 .saturating_add(packed.fields.len());
817 if packed.columns.len() != expected_columns {
818 return Err(ApplicationClientError::InvalidResponse);
819 }
820 let row_count = packed.row_count as usize;
821 if packed.columns.is_empty() && row_count != 0 {
822 return Err(ApplicationClientError::InvalidResponse);
823 }
824 for column in &packed.columns {
825 validate_packed_column_offsets(column, row_count)?;
826 }
827
828 let mut rows = Vec::with_capacity(row_count);
829 for row_index in 0..row_count {
830 let mut primary_key = Vec::with_capacity(packed.primary_key_fields.len());
831 let mut cells = Vec::with_capacity(packed.fields.len());
832 for (col_index, column) in packed.columns.iter().enumerate() {
833 let start = column.offsets[row_index] as usize;
834 let end = column.offsets[row_index + 1] as usize;
835 let value = decode_canonical_value(&column.data[start..end])
836 .map_err(|_| ApplicationClientError::InvalidResponse)?;
837 if col_index < packed.primary_key_fields.len() {
838 primary_key.push(value);
839 } else {
840 cells.push(value);
841 }
842 }
843 rows.push(ProjectedReadyRow { cells, primary_key });
844 }
845 Ok(ProjectedQueryOutcome::Ready {
846 fields: packed.fields,
847 primary_key_fields: packed.primary_key_fields,
848 rows,
849 frontier: raise_frontier(packed.frontier)?,
850 head: raise_frontier(packed.head)?,
851 commit_token: raise_optional_token(packed.commit_token)?,
852 })
853}
854
855fn raise_ready_aggregates(
878 aggregates: app_v1::ProjectedReadyAggregates,
879 requested: &ProjectedAggregateShape,
880) -> Result<ProjectedQueryOutcome, ApplicationClientError> {
881 if aggregates.group_key_fields != requested.group_by {
882 return Err(ApplicationClientError::InvalidResponse);
883 }
884 let order_positions: Vec<(usize, ProjectedSortDirection)> = requested
885 .order
886 .iter()
887 .map(|spec| {
888 requested
889 .group_by
890 .iter()
891 .position(|key| key == &spec.field)
892 .map(|pos| (pos, spec.direction))
893 .ok_or(ApplicationClientError::InvalidResponse)
894 })
895 .collect::<Result<_, _>>()?;
896 let descriptors = aggregates
897 .aggregates
898 .into_iter()
899 .map(raise_aggregate_descriptor)
900 .collect::<Result<Vec<_>, _>>()?;
901 if descriptors != requested.aggregates {
902 return Err(ApplicationClientError::InvalidResponse);
903 }
904 if requested.group_by.is_empty() && aggregates.groups.len() != 1 {
908 return Err(ApplicationClientError::InvalidResponse);
909 }
910 let mut groups = Vec::with_capacity(aggregates.groups.len());
911 let mut previous_keys: Option<(Vec<CanonicalValue>, Vec<u8>)> = None;
912 let mut canonical_ascending = true;
913 let mut encoded_ascending = true;
914 for group in aggregates.groups {
915 if group.keys.len() != aggregates.group_key_fields.len() {
916 return Err(ApplicationClientError::InvalidResponse);
917 }
918 if group.values.len() != descriptors.len() {
919 return Err(ApplicationClientError::InvalidResponse);
920 }
921 let mut keys = Vec::with_capacity(group.keys.len());
922 for key in group.keys {
923 keys.push(
924 canonical_value_from_proto(key)
925 .map_err(|_| ApplicationClientError::InvalidResponse)?,
926 );
927 }
928 let encoded = encoded_group_key(&keys)?;
929 if let Some((previous, previous_encoded)) = &previous_keys {
930 if !order_positions.is_empty() {
931 if compare_group_keys(previous, &keys, &order_positions) != std::cmp::Ordering::Less
932 {
933 return Err(ApplicationClientError::InvalidResponse);
934 }
935 } else {
936 if compare_group_keys(previous, &keys, &[]) != std::cmp::Ordering::Less {
937 canonical_ascending = false;
938 }
939 if *previous_encoded >= encoded {
940 encoded_ascending = false;
941 }
942 if !canonical_ascending && !encoded_ascending {
943 return Err(ApplicationClientError::InvalidResponse);
944 }
945 }
946 }
947 previous_keys = Some((keys.clone(), encoded));
948 let mut values = Vec::with_capacity(group.values.len());
949 for (value, descriptor) in group.values.into_iter().zip(&descriptors) {
950 values.push(raise_aggregate_value(value, descriptor)?);
951 }
952 groups.push(ProjectedAggregateGroup { keys, values });
953 }
954 Ok(ProjectedQueryOutcome::ReadyAggregates {
955 group_key_fields: aggregates.group_key_fields,
956 aggregates: descriptors,
957 groups,
958 frontier: raise_frontier(aggregates.frontier)?,
959 head: raise_frontier(aggregates.head)?,
960 commit_token: raise_optional_token(aggregates.commit_token)?,
961 })
962}
963
964fn encoded_group_key(keys: &[CanonicalValue]) -> Result<Vec<u8>, ApplicationClientError> {
965 let mut encoded = Vec::new();
966 for key in keys {
967 let cell = riffdb_types::encode_canonical_value(key)
968 .map_err(|_| ApplicationClientError::InvalidResponse)?;
969 let length =
970 u32::try_from(cell.len()).map_err(|_| ApplicationClientError::InvalidResponse)?;
971 encoded.extend_from_slice(&length.to_be_bytes());
972 encoded.extend_from_slice(&cell);
973 }
974 Ok(encoded)
975}
976
977fn compare_group_keys(
978 left_keys: &[CanonicalValue],
979 right_keys: &[CanonicalValue],
980 order_positions: &[(usize, ProjectedSortDirection)],
981) -> std::cmp::Ordering {
982 for (pos, direction) in order_positions {
983 let cmp = compare_canonical_values(&left_keys[*pos], &right_keys[*pos]);
984 let cmp = match direction {
985 ProjectedSortDirection::Asc => cmp,
986 ProjectedSortDirection::Desc => cmp.reverse(),
987 };
988 if cmp != std::cmp::Ordering::Equal {
989 return cmp;
990 }
991 }
992 for (left, right) in left_keys.iter().zip(right_keys.iter()) {
993 let cmp = compare_canonical_values(left, right);
994 if cmp != std::cmp::Ordering::Equal {
995 return cmp;
996 }
997 }
998 left_keys.len().cmp(&right_keys.len())
999}
1000
1001fn compare_canonical_values(left: &CanonicalValue, right: &CanonicalValue) -> std::cmp::Ordering {
1002 match (left, right) {
1003 (CanonicalValue::Bool(a), CanonicalValue::Bool(b)) => a.cmp(b),
1004 (CanonicalValue::I64(a), CanonicalValue::I64(b)) => a.cmp(b),
1005 (CanonicalValue::U64(a), CanonicalValue::U64(b)) => a.cmp(b),
1006 (CanonicalValue::Uuid(a), CanonicalValue::Uuid(b)) => a.cmp(b),
1007 (CanonicalValue::String(a), CanonicalValue::String(b)) => a.as_str().cmp(b.as_str()),
1008 (CanonicalValue::Bytes(a), CanonicalValue::Bytes(b)) => a.as_bytes().cmp(b.as_bytes()),
1009 (CanonicalValue::Timestamp(a), CanonicalValue::Timestamp(b)) => a.cmp(b),
1010 (CanonicalValue::Date(a), CanonicalValue::Date(b)) => a.cmp(b),
1011 (CanonicalValue::Null, CanonicalValue::Null) => std::cmp::Ordering::Equal,
1012 (CanonicalValue::Null, _) => std::cmp::Ordering::Less,
1013 (_, CanonicalValue::Null) => std::cmp::Ordering::Greater,
1014 _ => {
1015 let left_bytes = riffdb_types::encode_canonical_value(left).unwrap_or_default();
1016 let right_bytes = riffdb_types::encode_canonical_value(right).unwrap_or_default();
1017 left_bytes.cmp(&right_bytes)
1018 }
1019 }
1020}
1021
1022fn raise_aggregate_descriptor(
1023 descriptor: app_v1::ProjectedAggregate,
1024) -> Result<ProjectedAggregate, ApplicationClientError> {
1025 let op = app_v1::ProjectedAggregateOp::try_from(descriptor.op)
1026 .map_err(|_| ApplicationClientError::InvalidResponse)?;
1027 match op {
1028 app_v1::ProjectedAggregateOp::Count => {
1029 if descriptor.field.is_empty() {
1030 Ok(ProjectedAggregate::Count)
1031 } else {
1032 Err(ApplicationClientError::InvalidResponse)
1033 }
1034 }
1035 app_v1::ProjectedAggregateOp::CountPresent => Ok(ProjectedAggregate::CountPresent {
1036 field: named_column(descriptor.field)?,
1037 }),
1038 app_v1::ProjectedAggregateOp::CountDistinct => Ok(ProjectedAggregate::CountDistinct {
1039 field: named_column(descriptor.field)?,
1040 }),
1041 app_v1::ProjectedAggregateOp::CountDistinctPresent => {
1042 Ok(ProjectedAggregate::CountDistinctPresent {
1043 field: named_column(descriptor.field)?,
1044 })
1045 }
1046 app_v1::ProjectedAggregateOp::Sum => Ok(ProjectedAggregate::Sum {
1047 field: named_column(descriptor.field)?,
1048 }),
1049 app_v1::ProjectedAggregateOp::Mean => Ok(ProjectedAggregate::Mean {
1050 field: named_column(descriptor.field)?,
1051 }),
1052 app_v1::ProjectedAggregateOp::Min => Ok(ProjectedAggregate::Min {
1053 field: named_column(descriptor.field)?,
1054 }),
1055 app_v1::ProjectedAggregateOp::Max => Ok(ProjectedAggregate::Max {
1056 field: named_column(descriptor.field)?,
1057 }),
1058 app_v1::ProjectedAggregateOp::Any => Ok(ProjectedAggregate::Any {
1059 field: named_column(descriptor.field)?,
1060 }),
1061 app_v1::ProjectedAggregateOp::All => Ok(ProjectedAggregate::All {
1062 field: named_column(descriptor.field)?,
1063 }),
1064 app_v1::ProjectedAggregateOp::Unspecified => Err(ApplicationClientError::InvalidResponse),
1065 }
1066}
1067
1068fn named_column(field: String) -> Result<String, ApplicationClientError> {
1069 if field.is_empty() {
1070 return Err(ApplicationClientError::InvalidResponse);
1071 }
1072 Ok(field)
1073}
1074
1075fn raise_aggregate_value(
1080 value: app_v1::ProjectedAggregateValue,
1081 descriptor: &ProjectedAggregate,
1082) -> Result<ProjectedAggregateValue, ApplicationClientError> {
1083 use app_v1::projected_aggregate_value::Value as WireValue;
1084 let arm = value.value.ok_or(ApplicationClientError::InvalidResponse)?;
1085 match (arm, descriptor) {
1086 (
1087 WireValue::Count(count),
1088 ProjectedAggregate::Count
1089 | ProjectedAggregate::CountPresent { .. }
1090 | ProjectedAggregate::CountDistinct { .. }
1091 | ProjectedAggregate::CountDistinctPresent { .. },
1092 ) => Ok(ProjectedAggregateValue::Count(count)),
1093 (WireValue::Sum(decimal), ProjectedAggregate::Sum { .. }) => {
1094 let sum = aggregate_sum_from_proto(&decimal)
1096 .map_err(|_| ApplicationClientError::InvalidResponse)?;
1097 Ok(ProjectedAggregateValue::Sum(sum))
1098 }
1099 (WireValue::ExactMean(exact_mean), ProjectedAggregate::Mean { .. }) => {
1100 let decimal = exact_mean
1101 .total
1102 .ok_or(ApplicationClientError::InvalidResponse)?;
1103 let total = aggregate_sum_from_proto(&decimal)
1104 .map_err(|_| ApplicationClientError::InvalidResponse)?;
1105 if exact_mean.count == 0 && total != 0 {
1106 return Err(ApplicationClientError::InvalidResponse);
1107 }
1108 Ok(ProjectedAggregateValue::ExactMean {
1109 total,
1110 count: exact_mean.count,
1111 })
1112 }
1113 (
1114 WireValue::Scalar(scalar),
1115 ProjectedAggregate::Min { .. } | ProjectedAggregate::Max { .. },
1116 ) => {
1117 let value = scalar
1119 .value
1120 .map(canonical_value_from_proto)
1121 .transpose()
1122 .map_err(|_| ApplicationClientError::InvalidResponse)?;
1123 Ok(ProjectedAggregateValue::Scalar(value))
1124 }
1125 (
1126 WireValue::Boolean(boolean),
1127 ProjectedAggregate::Any { .. } | ProjectedAggregate::All { .. },
1128 ) => Ok(ProjectedAggregateValue::Bool(boolean)),
1129 _ => Err(ApplicationClientError::InvalidResponse),
1130 }
1131}
1132
1133fn validate_packed_column_offsets(
1134 column: &app_v1::PackedColumn,
1135 row_count: usize,
1136) -> Result<(), ApplicationClientError> {
1137 if column.offsets.len() != row_count.saturating_add(1) {
1138 return Err(ApplicationClientError::InvalidResponse);
1139 }
1140 if column.offsets.first().copied() != Some(0) {
1141 return Err(ApplicationClientError::InvalidResponse);
1142 }
1143 let last = *column.offsets.last().unwrap_or(&0) as usize;
1144 if last != column.data.len() {
1145 return Err(ApplicationClientError::InvalidResponse);
1146 }
1147 for window in column.offsets.windows(2) {
1148 if window[0] > window[1] || window[1] as usize > column.data.len() {
1149 return Err(ApplicationClientError::InvalidResponse);
1150 }
1151 }
1152 Ok(())
1153}
1154
1155fn raise_frontier(bytes: Vec<u8>) -> Result<ProjectionFrontier, ApplicationClientError> {
1156 ProjectionFrontier::from_bytes(bytes).map_err(|_| ApplicationClientError::InvalidResponse)
1157}
1158
1159fn raise_optional_token(bytes: Vec<u8>) -> Result<Option<CommitToken>, ApplicationClientError> {
1160 if bytes.is_empty() {
1161 return Ok(None);
1162 }
1163 CommitToken::from_bytes(bytes)
1164 .map(Some)
1165 .map_err(|_| ApplicationClientError::InvalidResponse)
1166}
1167
1168fn raise_rebuilding_reason(
1169 reason: i32,
1170) -> Result<ProjectedRebuildingReason, ApplicationClientError> {
1171 match app_v1::ProjectedRebuildingReason::try_from(reason) {
1172 Ok(app_v1::ProjectedRebuildingReason::ReplayBudgetExceeded) => {
1173 Ok(ProjectedRebuildingReason::ReplayBudgetExceeded)
1174 }
1175 Ok(app_v1::ProjectedRebuildingReason::ExplicitRebuild) => {
1176 Ok(ProjectedRebuildingReason::ExplicitRebuild)
1177 }
1178 Ok(app_v1::ProjectedRebuildingReason::StateIntegrityFailure) => {
1179 Ok(ProjectedRebuildingReason::StateIntegrityFailure)
1180 }
1181 Ok(app_v1::ProjectedRebuildingReason::Unspecified) | Err(_) => {
1182 Err(ApplicationClientError::InvalidResponse)
1183 }
1184 }
1185}
1186
1187fn raise_degraded_reason(reason: i32) -> Result<ProjectedDegradedReason, ApplicationClientError> {
1188 match app_v1::ProjectedDegradedReason::try_from(reason) {
1189 Ok(app_v1::ProjectedDegradedReason::ApplyLagSlo) => {
1190 Ok(ProjectedDegradedReason::ApplyLagSlo)
1191 }
1192 Ok(app_v1::ProjectedDegradedReason::MaintenanceBacklog) => {
1193 Ok(ProjectedDegradedReason::MaintenanceBacklog)
1194 }
1195 Ok(app_v1::ProjectedDegradedReason::PartialInventory) => {
1196 Ok(ProjectedDegradedReason::PartialInventory)
1197 }
1198 Ok(app_v1::ProjectedDegradedReason::Unspecified) | Err(_) => {
1199 Err(ApplicationClientError::InvalidResponse)
1200 }
1201 }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206 use super::*;
1207 use riffdb_types::{CommitSequence, encode_canonical_value};
1208
1209 fn pack_one(value: &CanonicalValue) -> app_v1::PackedColumn {
1210 let data = encode_canonical_value(value).expect("enc");
1211 let end = data.len() as u32;
1212 app_v1::PackedColumn {
1213 data,
1214 offsets: vec![0, end],
1215 }
1216 }
1217
1218 fn valid_packed() -> app_v1::ProjectedReadyPacked {
1219 app_v1::ProjectedReadyPacked {
1220 fields: vec!["title".to_owned()],
1221 primary_key_fields: vec!["ticket_id".to_owned()],
1222 row_count: 1,
1223 columns: vec![
1224 pack_one(&CanonicalValue::Uuid([1; 16])),
1225 pack_one(&CanonicalValue::string("hello").expect("s")),
1226 ],
1227 frontier: ProjectionFrontier::new(
1228 1,
1229 riffdb_types::FrontierPosition::AppliedThrough(
1230 CommitSequence::new(7).expect("seq"),
1231 ),
1232 )
1233 .into_bytes(),
1234 head: ProjectionFrontier::new(
1235 1,
1236 riffdb_types::FrontierPosition::AppliedThrough(
1237 CommitSequence::new(9).expect("seq"),
1238 ),
1239 )
1240 .into_bytes(),
1241 commit_token: CommitToken::new(1, CommitSequence::new(7).expect("seq")).into_bytes(),
1242 }
1243 }
1244
1245 #[test]
1246 fn packed_decoder_accepts_baseline_and_rejects_hostile_shapes() {
1247 let ok = raise_ready_packed(valid_packed()).expect("valid");
1248 match ok {
1249 ProjectedQueryOutcome::Ready {
1250 fields,
1251 primary_key_fields,
1252 rows,
1253 ..
1254 } => {
1255 assert_eq!(fields, vec!["title".to_owned()]);
1256 assert_eq!(primary_key_fields, vec!["ticket_id".to_owned()]);
1257 assert_eq!(rows.len(), 1);
1258 assert_eq!(rows[0].primary_key, vec![CanonicalValue::Uuid([1; 16])]);
1259 assert_eq!(
1260 rows[0].cells,
1261 vec![CanonicalValue::string("hello").expect("s")]
1262 );
1263 }
1264 other => panic!("expected Ready, got {other:?}"),
1265 }
1266
1267 let mut truncated = valid_packed();
1269 truncated.columns[0].data.pop();
1270 assert!(raise_ready_packed(truncated).is_err());
1271
1272 let mut nonmono = valid_packed();
1274 nonmono.columns[1].offsets = vec![1, 0];
1275 assert!(
1276 raise_ready_packed(nonmono).is_err(),
1277 "non-monotone offsets must fail closed"
1278 );
1279
1280 let mut garbage = valid_packed();
1282 garbage.columns[1].data.push(0xFF);
1283 let end = garbage.columns[1].data.len() as u32;
1284 garbage.columns[1].offsets = vec![0, end];
1285 assert!(raise_ready_packed(garbage).is_err());
1286
1287 let mut mismatch = valid_packed();
1289 mismatch.row_count = 2;
1290 assert!(raise_ready_packed(mismatch).is_err());
1291 }
1292
1293 #[test]
1297 fn packed_decoder_rejects_rows_without_columns() {
1298 let mut fabricated = valid_packed();
1299 fabricated.fields = Vec::new();
1300 fabricated.primary_key_fields = Vec::new();
1301 fabricated.columns = Vec::new();
1302 fabricated.row_count = 100_000;
1303 let err = raise_ready_packed(fabricated)
1304 .expect_err("nonzero row_count with no columns must fail closed");
1305 assert!(matches!(err, ApplicationClientError::InvalidResponse));
1306
1307 let mut empty = valid_packed();
1309 empty.fields = Vec::new();
1310 empty.primary_key_fields = Vec::new();
1311 empty.columns = Vec::new();
1312 empty.row_count = 0;
1313 match raise_ready_packed(empty).expect("zero rows and zero columns") {
1314 ProjectedQueryOutcome::Ready { rows, fields, .. } => {
1315 assert!(rows.is_empty());
1316 assert!(fields.is_empty());
1317 }
1318 other => panic!("expected Ready, got {other:?}"),
1319 }
1320 }
1321
1322 #[test]
1328 fn packed_decoder_rejects_mid_column_offset_decrease() {
1329 let cell = encode_canonical_value(&CanonicalValue::Uuid([2; 16])).expect("enc");
1330 let span = u32::try_from(cell.len()).expect("span");
1331 let mut data = Vec::with_capacity(cell.len() * 3);
1332 for _ in 0..3 {
1333 data.extend_from_slice(&cell);
1334 }
1335 let mut hostile = valid_packed();
1336 hostile.fields = vec!["title".to_owned()];
1337 hostile.primary_key_fields = Vec::new();
1338 hostile.row_count = 3;
1339 hostile.columns = vec![app_v1::PackedColumn {
1340 data,
1341 offsets: vec![0, span, 0, span * 3],
1342 }];
1343 let err = raise_ready_packed(hostile)
1344 .expect_err("mid-column offset decrease must fail closed, not panic");
1345 assert!(matches!(err, ApplicationClientError::InvalidResponse));
1346 }
1347
1348 #[test]
1349 fn packed_request_builder_sets_packed_encoding() {
1350 let query = ProjectedQuery::new(
1351 ApplicationContract::Exact {
1352 lineage: "TicketDesk".to_owned(),
1353 version: 1,
1354 bundle_hash: None,
1355 },
1356 "board",
1357 ApplicationValue::Uuid(crate::ApplicationUuid::from_bytes([9; 16])),
1358 )
1359 .expect("query")
1360 .encoding(ProjectedResponseEncoding::Packed)
1361 .limit(Some(50));
1362 let wire = query.into_wire_request().expect("wire");
1363 assert_eq!(
1364 wire.response_encoding,
1365 Some(app_v1::ProjectedResponseEncoding::Packed as i32)
1366 );
1367 assert_eq!(wire.projection_name, "board");
1368 assert_eq!(wire.request.as_ref().expect("body").limit, Some(50));
1369 }
1370
1371 #[test]
1372 fn packed_encoding_rejects_ready_row_arm() {
1373 let ready = app_v1::ExecuteProjectedQueryResponse {
1374 outcome: Some(app_v1::execute_projected_query_response::Outcome::Ready(
1375 app_v1::ProjectedQueryReady {
1376 fields: vec!["title".to_owned()],
1377 rows: Vec::new(),
1378 frontier: ProjectionFrontier::new(
1379 1,
1380 riffdb_types::FrontierPosition::BeforeFirst,
1381 )
1382 .into_bytes(),
1383 head: ProjectionFrontier::new(1, riffdb_types::FrontierPosition::BeforeFirst)
1384 .into_bytes(),
1385 commit_token: Vec::new(),
1386 },
1387 )),
1388 };
1389 let err = raise_projected_outcome(
1390 ready,
1391 ProjectedResponseShape::Rows(ProjectedResponseEncoding::Packed),
1392 )
1393 .expect_err("row arm under packed must fail");
1394 assert!(matches!(err, ApplicationClientError::InvalidResponse));
1395 }
1396
1397 #[test]
1398 fn row_encoding_rejects_ready_packed_arm() {
1399 let packed = app_v1::ExecuteProjectedQueryResponse {
1400 outcome: Some(
1401 app_v1::execute_projected_query_response::Outcome::ReadyPacked(valid_packed()),
1402 ),
1403 };
1404 let err = raise_projected_outcome(
1405 packed,
1406 ProjectedResponseShape::Rows(ProjectedResponseEncoding::Row),
1407 )
1408 .expect_err("packed arm under row must fail");
1409 assert!(matches!(err, ApplicationClientError::InvalidResponse));
1410
1411 let packed = app_v1::ExecuteProjectedQueryResponse {
1413 outcome: Some(
1414 app_v1::execute_projected_query_response::Outcome::ReadyPacked(valid_packed()),
1415 ),
1416 };
1417 assert!(
1418 raise_projected_outcome(
1419 packed,
1420 ProjectedResponseShape::Rows(ProjectedResponseEncoding::Packed)
1421 )
1422 .is_ok()
1423 );
1424 }
1425}
1426
1427#[cfg(test)]
1428mod aggregate_tests {
1429 use super::*;
1430 use riffdb_proto::aggregate_sum_to_proto;
1431 use riffdb_types::CommitSequence;
1432
1433 fn frontier_bytes(sequence: u64) -> Vec<u8> {
1434 ProjectionFrontier::new(
1435 1,
1436 riffdb_types::FrontierPosition::AppliedThrough(
1437 CommitSequence::new(sequence).expect("seq"),
1438 ),
1439 )
1440 .into_bytes()
1441 }
1442
1443 fn descriptor(op: app_v1::ProjectedAggregateOp, field: &str) -> app_v1::ProjectedAggregate {
1444 app_v1::ProjectedAggregate {
1445 op: op as i32,
1446 field: field.to_owned(),
1447 }
1448 }
1449
1450 fn count_cell(count: u64) -> app_v1::ProjectedAggregateValue {
1451 app_v1::ProjectedAggregateValue {
1452 value: Some(app_v1::projected_aggregate_value::Value::Count(count)),
1453 }
1454 }
1455
1456 fn sum_cell(sum: i128) -> app_v1::ProjectedAggregateValue {
1457 app_v1::ProjectedAggregateValue {
1458 value: Some(app_v1::projected_aggregate_value::Value::Sum(
1459 aggregate_sum_to_proto(sum),
1460 )),
1461 }
1462 }
1463
1464 fn scalar_cell(value: Option<CanonicalValue>) -> app_v1::ProjectedAggregateValue {
1465 app_v1::ProjectedAggregateValue {
1466 value: Some(app_v1::projected_aggregate_value::Value::Scalar(
1467 app_v1::ProjectedAggregateScalar {
1468 value: value
1469 .as_ref()
1470 .map(|value| riffdb_proto::canonical_value_to_proto(value).expect("value")),
1471 },
1472 )),
1473 }
1474 }
1475
1476 fn valid_aggregates() -> app_v1::ProjectedReadyAggregates {
1478 app_v1::ProjectedReadyAggregates {
1479 group_key_fields: vec!["status".to_owned()],
1480 aggregates: vec![
1481 descriptor(app_v1::ProjectedAggregateOp::Count, ""),
1482 descriptor(app_v1::ProjectedAggregateOp::Sum, "story_points"),
1483 descriptor(app_v1::ProjectedAggregateOp::Min, "title"),
1484 ],
1485 groups: vec![
1486 app_v1::ProjectedAggregateGroup {
1487 keys: vec![
1488 riffdb_proto::canonical_value_to_proto(
1489 &CanonicalValue::string("closed").expect("s"),
1490 )
1491 .expect("value"),
1492 ],
1493 values: vec![
1494 count_cell(2),
1495 sum_cell(11),
1496 scalar_cell(Some(CanonicalValue::string("a").expect("s"))),
1497 ],
1498 },
1499 app_v1::ProjectedAggregateGroup {
1500 keys: vec![
1501 riffdb_proto::canonical_value_to_proto(
1502 &CanonicalValue::string("open").expect("s"),
1503 )
1504 .expect("value"),
1505 ],
1506 values: vec![count_cell(0), sum_cell(0), scalar_cell(None)],
1507 },
1508 ],
1509 frontier: frontier_bytes(7),
1510 head: frontier_bytes(9),
1511 commit_token: CommitToken::new(1, CommitSequence::new(7).expect("seq")).into_bytes(),
1512 }
1513 }
1514
1515 fn requested() -> ProjectedAggregateShape {
1517 ProjectedAggregateShape {
1518 group_by: vec!["status".to_owned()],
1519 aggregates: vec![
1520 ProjectedAggregate::Count,
1521 ProjectedAggregate::Sum {
1522 field: "story_points".to_owned(),
1523 },
1524 ProjectedAggregate::Min {
1525 field: "title".to_owned(),
1526 },
1527 ],
1528 order: Vec::new(),
1529 }
1530 }
1531
1532 fn requested_whole_set() -> ProjectedAggregateShape {
1534 ProjectedAggregateShape {
1535 group_by: Vec::new(),
1536 aggregates: vec![ProjectedAggregate::Count],
1537 order: Vec::new(),
1538 }
1539 }
1540
1541 fn valid_whole_set() -> app_v1::ProjectedReadyAggregates {
1543 app_v1::ProjectedReadyAggregates {
1544 group_key_fields: Vec::new(),
1545 aggregates: vec![descriptor(app_v1::ProjectedAggregateOp::Count, "")],
1546 groups: vec![app_v1::ProjectedAggregateGroup {
1547 keys: Vec::new(),
1548 values: vec![count_cell(6)],
1549 }],
1550 frontier: frontier_bytes(7),
1551 head: frontier_bytes(9),
1552 commit_token: Vec::new(),
1553 }
1554 }
1555
1556 fn aggregates_response(
1557 payload: app_v1::ProjectedReadyAggregates,
1558 ) -> app_v1::ExecuteProjectedQueryResponse {
1559 app_v1::ExecuteProjectedQueryResponse {
1560 outcome: Some(
1561 app_v1::execute_projected_query_response::Outcome::ReadyAggregates(payload),
1562 ),
1563 }
1564 }
1565
1566 #[test]
1567 fn aggregate_decoder_accepts_baseline_payload_in_server_order() {
1568 let outcome = raise_ready_aggregates(valid_aggregates(), &requested()).expect("valid");
1569 let ProjectedQueryOutcome::ReadyAggregates {
1570 group_key_fields,
1571 aggregates,
1572 groups,
1573 commit_token,
1574 ..
1575 } = outcome
1576 else {
1577 panic!("expected ReadyAggregates");
1578 };
1579 assert_eq!(group_key_fields, vec!["status".to_owned()]);
1580 assert_eq!(
1581 aggregates,
1582 vec![
1583 ProjectedAggregate::Count,
1584 ProjectedAggregate::Sum {
1585 field: "story_points".to_owned()
1586 },
1587 ProjectedAggregate::Min {
1588 field: "title".to_owned()
1589 },
1590 ]
1591 );
1592 assert_eq!(groups.len(), 2);
1593 assert_eq!(
1595 groups[0].keys,
1596 vec![CanonicalValue::string("closed").expect("s")]
1597 );
1598 assert_eq!(groups[0].values[0], ProjectedAggregateValue::Count(2));
1599 assert_eq!(groups[0].values[1], ProjectedAggregateValue::Sum(11));
1600 assert_eq!(
1601 groups[0].values[2],
1602 ProjectedAggregateValue::Scalar(Some(CanonicalValue::string("a").expect("s")))
1603 );
1604 assert_eq!(groups[1].values[2], ProjectedAggregateValue::Scalar(None));
1605 assert!(commit_token.is_some());
1606 }
1607
1608 #[test]
1611 fn aggregate_decoder_rejects_length_disagreements() {
1612 let mut short_values = valid_aggregates();
1614 short_values.groups[0].values.pop();
1615 assert!(
1616 raise_ready_aggregates(short_values, &requested()).is_err(),
1617 "a group with fewer values than descriptors must fail closed"
1618 );
1619
1620 let mut long_values = valid_aggregates();
1622 long_values.groups[1].values.push(count_cell(9));
1623 assert!(raise_ready_aggregates(long_values, &requested()).is_err());
1624
1625 let mut short_keys = valid_aggregates();
1627 short_keys.groups[0].keys.clear();
1628 assert!(
1629 raise_ready_aggregates(short_keys, &requested()).is_err(),
1630 "a group missing a key cell must fail closed"
1631 );
1632
1633 let mut long_keys = valid_aggregates();
1635 long_keys.groups[1]
1636 .keys
1637 .push(riffdb_proto::canonical_value_to_proto(&CanonicalValue::U64(1)).expect("value"));
1638 assert!(raise_ready_aggregates(long_keys, &requested()).is_err());
1639 }
1640
1641 #[test]
1644 fn aggregate_decoder_rejects_arm_descriptor_mismatch() {
1645 let mut swapped = valid_aggregates();
1646 swapped.groups[0].values[0] = sum_cell(2);
1647 assert!(
1648 raise_ready_aggregates(swapped, &requested()).is_err(),
1649 "a sum under a count descriptor must fail closed"
1650 );
1651
1652 let mut scalar_for_sum = valid_aggregates();
1653 scalar_for_sum.groups[0].values[1] = scalar_cell(Some(CanonicalValue::U64(1)));
1654 assert!(raise_ready_aggregates(scalar_for_sum, &requested()).is_err());
1655
1656 let mut count_for_min = valid_aggregates();
1657 count_for_min.groups[0].values[2] = count_cell(1);
1658 assert!(raise_ready_aggregates(count_for_min, &requested()).is_err());
1659
1660 let mut unset = valid_aggregates();
1662 unset.groups[0].values[0] = app_v1::ProjectedAggregateValue { value: None };
1663 assert!(raise_ready_aggregates(unset, &requested()).is_err());
1664 }
1665
1666 #[test]
1667 fn aggregate_decoder_rejects_malformed_descriptors() {
1668 let mut unknown_op = valid_aggregates();
1669 unknown_op.aggregates[0].op = 99;
1670 assert!(
1671 raise_ready_aggregates(unknown_op, &requested()).is_err(),
1672 "an unknown op enum must fail closed"
1673 );
1674
1675 let mut unspecified = valid_aggregates();
1676 unspecified.aggregates[0].op = app_v1::ProjectedAggregateOp::Unspecified as i32;
1677 assert!(raise_ready_aggregates(unspecified, &requested()).is_err());
1678
1679 let mut counted_column = valid_aggregates();
1680 counted_column.aggregates[0].field = "title".to_owned();
1681 assert!(raise_ready_aggregates(counted_column, &requested()).is_err());
1682
1683 let mut columnless_sum = valid_aggregates();
1684 columnless_sum.aggregates[1].field = String::new();
1685 assert!(raise_ready_aggregates(columnless_sum, &requested()).is_err());
1686 }
1687
1688 #[test]
1690 fn aggregate_decoder_rejects_hostile_sum_decimals() {
1691 let oversized = app_v1::ProjectedAggregateValue {
1692 value: Some(app_v1::projected_aggregate_value::Value::Sum(
1693 riffdb_proto::v1::Decimal {
1694 coefficient_twos_complement: vec![1; 17],
1695 scale: 0,
1696 precision: None,
1697 },
1698 )),
1699 };
1700 let mut payload = valid_aggregates();
1701 payload.groups[0].values[1] = oversized;
1702 assert!(
1703 raise_ready_aggregates(payload, &requested()).is_err(),
1704 "a 17-byte coefficient exceeds i128 and must fail closed"
1705 );
1706
1707 let mut scaled = valid_aggregates();
1708 scaled.groups[0].values[1] = app_v1::ProjectedAggregateValue {
1709 value: Some(app_v1::projected_aggregate_value::Value::Sum(
1710 riffdb_proto::v1::Decimal {
1711 coefficient_twos_complement: vec![1],
1712 scale: 2,
1713 precision: None,
1714 },
1715 )),
1716 };
1717 assert!(
1718 raise_ready_aggregates(scaled, &requested()).is_err(),
1719 "a non-zero scale is not this carriage"
1720 );
1721
1722 let mut asserted_precision = valid_aggregates();
1723 asserted_precision.groups[0].values[1] = app_v1::ProjectedAggregateValue {
1724 value: Some(app_v1::projected_aggregate_value::Value::Sum(
1725 riffdb_proto::v1::Decimal {
1726 coefficient_twos_complement: vec![1],
1727 scale: 0,
1728 precision: Some(38),
1729 },
1730 )),
1731 };
1732 assert!(raise_ready_aggregates(asserted_precision, &requested()).is_err());
1733
1734 let mut empty_coefficient = valid_aggregates();
1735 empty_coefficient.groups[0].values[1] = app_v1::ProjectedAggregateValue {
1736 value: Some(app_v1::projected_aggregate_value::Value::Sum(
1737 riffdb_proto::v1::Decimal {
1738 coefficient_twos_complement: Vec::new(),
1739 scale: 0,
1740 precision: None,
1741 },
1742 )),
1743 };
1744 assert!(raise_ready_aggregates(empty_coefficient, &requested()).is_err());
1745
1746 let mut padded = valid_aggregates();
1748 padded.groups[0].values[1] = app_v1::ProjectedAggregateValue {
1749 value: Some(app_v1::projected_aggregate_value::Value::Sum(
1750 riffdb_proto::v1::Decimal {
1751 coefficient_twos_complement: vec![0, 1],
1752 scale: 0,
1753 precision: None,
1754 },
1755 )),
1756 };
1757 assert!(raise_ready_aggregates(padded, &requested()).is_err());
1758 }
1759
1760 #[test]
1762 fn sum_round_trips_across_the_full_i128_range() {
1763 for sum in [
1764 0_i128,
1765 1,
1766 -1,
1767 i64::MAX as i128,
1768 i64::MIN as i128,
1769 i64::MAX as i128 + 1,
1770 i128::MAX,
1771 i128::MIN,
1772 ] {
1773 let mut payload = valid_aggregates();
1774 payload.groups[0].values[1] = sum_cell(sum);
1775 let ProjectedQueryOutcome::ReadyAggregates { groups, .. } =
1776 raise_ready_aggregates(payload, &requested()).expect("valid")
1777 else {
1778 panic!("expected ReadyAggregates");
1779 };
1780 assert_eq!(groups[0].values[1], ProjectedAggregateValue::Sum(sum));
1781 }
1782 }
1783
1784 #[test]
1787 fn empty_group_scalar_is_distinct_from_a_null_extreme() {
1788 let mut absent = valid_aggregates();
1789 absent.groups[0].values[2] = scalar_cell(None);
1790 let mut null_extreme = valid_aggregates();
1791 null_extreme.groups[0].values[2] = scalar_cell(Some(CanonicalValue::Null));
1792
1793 let value_of = |payload| {
1794 let ProjectedQueryOutcome::ReadyAggregates { groups, .. } =
1795 raise_ready_aggregates(payload, &requested()).expect("valid")
1796 else {
1797 panic!("expected ReadyAggregates");
1798 };
1799 groups[0].values[2].clone()
1800 };
1801 assert_eq!(value_of(absent), ProjectedAggregateValue::Scalar(None));
1802 assert_eq!(
1803 value_of(null_extreme),
1804 ProjectedAggregateValue::Scalar(Some(CanonicalValue::Null))
1805 );
1806 }
1807
1808 #[test]
1810 fn aggregate_arm_and_request_shape_must_agree() {
1811 for shape in [
1812 ProjectedResponseShape::Rows(ProjectedResponseEncoding::Row),
1813 ProjectedResponseShape::Rows(ProjectedResponseEncoding::Packed),
1814 ] {
1815 let err = raise_projected_outcome(aggregates_response(valid_aggregates()), shape)
1816 .expect_err("a row request must refuse folded values");
1817 assert!(matches!(err, ApplicationClientError::InvalidResponse));
1818 }
1819 assert!(
1820 raise_projected_outcome(
1821 aggregates_response(valid_aggregates()),
1822 ProjectedResponseShape::Aggregates(requested())
1823 )
1824 .is_ok()
1825 );
1826
1827 let ready_row = app_v1::ExecuteProjectedQueryResponse {
1829 outcome: Some(app_v1::execute_projected_query_response::Outcome::Ready(
1830 app_v1::ProjectedQueryReady {
1831 fields: Vec::new(),
1832 rows: Vec::new(),
1833 frontier: frontier_bytes(7),
1834 head: frontier_bytes(7),
1835 commit_token: Vec::new(),
1836 },
1837 )),
1838 };
1839 assert!(matches!(
1840 raise_projected_outcome(ready_row, ProjectedResponseShape::Aggregates(requested())),
1841 Err(ApplicationClientError::InvalidResponse)
1842 ));
1843 let ready_packed = app_v1::ExecuteProjectedQueryResponse {
1844 outcome: Some(
1845 app_v1::execute_projected_query_response::Outcome::ReadyPacked(
1846 app_v1::ProjectedReadyPacked {
1847 fields: Vec::new(),
1848 primary_key_fields: Vec::new(),
1849 row_count: 0,
1850 columns: Vec::new(),
1851 frontier: frontier_bytes(7),
1852 head: frontier_bytes(7),
1853 commit_token: Vec::new(),
1854 },
1855 ),
1856 ),
1857 };
1858 assert!(matches!(
1859 raise_projected_outcome(
1860 ready_packed,
1861 ProjectedResponseShape::Aggregates(requested())
1862 ),
1863 Err(ApplicationClientError::InvalidResponse)
1864 ));
1865 }
1866
1867 #[test]
1874 fn a_self_consistent_answer_to_a_different_question_is_rejected() {
1875 let mislabelled = app_v1::ProjectedReadyAggregates {
1876 group_key_fields: Vec::new(),
1877 aggregates: vec![descriptor(app_v1::ProjectedAggregateOp::Count, "")],
1878 groups: vec![app_v1::ProjectedAggregateGroup {
1879 keys: Vec::new(),
1880 values: vec![count_cell(9999)],
1881 }],
1882 frontier: frontier_bytes(7),
1883 head: frontier_bytes(9),
1884 commit_token: Vec::new(),
1885 };
1886 let asked_for_a_sum = ProjectedAggregateShape {
1887 group_by: Vec::new(),
1888 aggregates: vec![ProjectedAggregate::Sum {
1889 field: "story_points".to_owned(),
1890 }],
1891 order: Vec::new(),
1892 };
1893 assert!(
1894 raise_ready_aggregates(mislabelled.clone(), &asked_for_a_sum).is_err(),
1895 "a count echoed where a sum was requested must fail closed"
1896 );
1897 assert!(raise_ready_aggregates(mislabelled, &requested_whole_set()).is_ok());
1899
1900 let mut reordered = valid_aggregates();
1902 reordered.aggregates.swap(0, 1);
1903 for group in &mut reordered.groups {
1904 group.values.swap(0, 1);
1905 }
1906 assert!(
1907 raise_ready_aggregates(reordered, &requested()).is_err(),
1908 "descriptors reordered against the request must fail closed"
1909 );
1910
1911 let mut renamed = valid_aggregates();
1913 renamed.group_key_fields = vec!["project_id".to_owned()];
1914 assert!(
1915 raise_ready_aggregates(renamed, &requested()).is_err(),
1916 "a different key column than the one requested must fail closed"
1917 );
1918 }
1919
1920 #[test]
1926 fn whole_set_requests_require_exactly_one_empty_key_group() {
1927 assert!(
1928 raise_ready_aggregates(valid_whole_set(), &requested_whole_set()).is_ok(),
1929 "the baseline whole-set answer must still decode"
1930 );
1931
1932 let mut none = valid_whole_set();
1933 none.groups.clear();
1934 assert!(
1935 raise_ready_aggregates(none, &requested_whole_set()).is_err(),
1936 "a whole-set COUNT must never arrive as no result at all"
1937 );
1938
1939 let mut several = valid_whole_set();
1940 several.groups = vec![
1941 app_v1::ProjectedAggregateGroup {
1942 keys: Vec::new(),
1943 values: vec![count_cell(1)],
1944 },
1945 app_v1::ProjectedAggregateGroup {
1946 keys: Vec::new(),
1947 values: vec![count_cell(2)],
1948 },
1949 app_v1::ProjectedAggregateGroup {
1950 keys: Vec::new(),
1951 values: vec![count_cell(3)],
1952 },
1953 ];
1954 assert!(
1955 raise_ready_aggregates(several, &requested_whole_set()).is_err(),
1956 "three conflicting whole-set answers must fail closed"
1957 );
1958
1959 let mut empty_grouped = valid_aggregates();
1962 empty_grouped.groups.clear();
1963 assert!(
1964 raise_ready_aggregates(empty_grouped, &requested()).is_ok(),
1965 "a grouped request may legitimately match nothing"
1966 );
1967 }
1968
1969 #[test]
1975 fn group_keys_must_be_strictly_ascending_in_encoded_order() {
1976 let group = |key: &str, count: u64| app_v1::ProjectedAggregateGroup {
1977 keys: vec![
1978 riffdb_proto::canonical_value_to_proto(
1979 &CanonicalValue::string(key).expect("bounded"),
1980 )
1981 .expect("value"),
1982 ],
1983 values: vec![count_cell(count)],
1984 };
1985 let payload =
1986 |groups: Vec<app_v1::ProjectedAggregateGroup>| app_v1::ProjectedReadyAggregates {
1987 group_key_fields: vec!["title".to_owned()],
1988 aggregates: vec![descriptor(app_v1::ProjectedAggregateOp::Count, "")],
1989 groups,
1990 frontier: frontier_bytes(7),
1991 head: frontier_bytes(9),
1992 commit_token: Vec::new(),
1993 };
1994 let asked = ProjectedAggregateShape {
1995 group_by: vec!["title".to_owned()],
1996 aggregates: vec![ProjectedAggregate::Count],
1997 order: Vec::new(),
1998 };
1999
2000 assert!(
2002 raise_ready_aggregates(payload(vec![group("aaa", 7), group("zzz", 5)]), &asked).is_ok(),
2003 "ascending keys must decode"
2004 );
2005
2006 assert!(
2008 raise_ready_aggregates(
2009 payload(vec![group("zzz", 5), group("aaa", 7), group("zzz", 11)]),
2010 &asked
2011 )
2012 .is_err(),
2013 "a repeated group key would double-count in any caller folding groups"
2014 );
2015
2016 assert!(
2018 raise_ready_aggregates(payload(vec![group("zzz", 5), group("aaa", 7)]), &asked)
2019 .is_err(),
2020 "descending groups break the default ascending emission order"
2021 );
2022
2023 assert!(
2025 raise_ready_aggregates(payload(vec![group("aaa", 7), group("aaa", 7)]), &asked)
2026 .is_err()
2027 );
2028
2029 assert!(
2032 raise_ready_aggregates(payload(vec![group("aaa", 2), group("zz", 1)]), &asked).is_ok(),
2033 "canonical value order sorts 'aaa' before 'zz'"
2034 );
2035 assert!(
2036 raise_ready_aggregates(payload(vec![group("zz", 1), group("aaa", 2)]), &asked).is_ok(),
2037 "length-prefixed order sorts 'zz' before 'aaa'"
2038 );
2039 assert!(
2040 raise_ready_aggregates(
2041 payload(vec![group("aaa", 2), group("zz", 1), group("b", 3)]),
2042 &asked
2043 )
2044 .is_err(),
2045 "mixed disorder is neither canonical ascending nor encoded ascending"
2046 );
2047
2048 let asked_desc = ProjectedAggregateShape {
2051 group_by: vec!["title".to_owned()],
2052 aggregates: vec![ProjectedAggregate::Count],
2053 order: vec![ProjectedOrder {
2054 field: "title".to_owned(),
2055 direction: ProjectedSortDirection::Desc,
2056 }],
2057 };
2058 assert!(
2059 raise_ready_aggregates(payload(vec![group("zz", 1), group("aaa", 2)]), &asked_desc)
2060 .is_ok(),
2061 "descending group order must decode when requested"
2062 );
2063 assert!(
2064 raise_ready_aggregates(payload(vec![group("aaa", 2), group("zz", 1)]), &asked_desc)
2065 .is_err(),
2066 "ascending group order must be rejected when descending was requested"
2067 );
2068 }
2069
2070 fn query() -> ProjectedQuery {
2075 ProjectedQuery::new(
2076 ApplicationContract::Exact {
2077 lineage: "TicketDesk".to_owned(),
2078 version: 1,
2079 bundle_hash: None,
2080 },
2081 "board",
2082 ApplicationValue::Uuid(crate::ApplicationUuid::from_bytes([9; 16])),
2083 )
2084 .expect("query")
2085 }
2086
2087 #[test]
2089 fn aggregate_builder_lowers_every_op_and_keeps_request_order() {
2090 let wire = query()
2091 .group_by(vec!["status".to_owned()])
2092 .aggregate(ProjectedAggregate::Count)
2093 .aggregate(ProjectedAggregate::CountPresent {
2094 field: "assignee".to_owned(),
2095 })
2096 .aggregate(ProjectedAggregate::CountDistinct {
2097 field: "status".to_owned(),
2098 })
2099 .aggregate(ProjectedAggregate::CountDistinctPresent {
2100 field: "assignee".to_owned(),
2101 })
2102 .aggregate(ProjectedAggregate::Sum {
2103 field: "story_points".to_owned(),
2104 })
2105 .aggregate(ProjectedAggregate::Mean {
2106 field: "story_points".to_owned(),
2107 })
2108 .aggregate(ProjectedAggregate::Min {
2109 field: "title".to_owned(),
2110 })
2111 .aggregate(ProjectedAggregate::Max {
2112 field: "title".to_owned(),
2113 })
2114 .aggregate(ProjectedAggregate::Any {
2115 field: "is_open".to_owned(),
2116 })
2117 .aggregate(ProjectedAggregate::All {
2118 field: "is_open".to_owned(),
2119 })
2120 .into_wire_request()
2121 .expect("wire");
2122 let body = wire.request.expect("body");
2123 assert_eq!(body.group_by, vec!["status".to_owned()]);
2124 assert_eq!(
2125 body.aggregates
2126 .iter()
2127 .map(|item| (item.op, item.field.clone()))
2128 .collect::<Vec<_>>(),
2129 vec![
2130 (app_v1::ProjectedAggregateOp::Count as i32, String::new()),
2131 (
2132 app_v1::ProjectedAggregateOp::CountPresent as i32,
2133 "assignee".to_owned()
2134 ),
2135 (
2136 app_v1::ProjectedAggregateOp::CountDistinct as i32,
2137 "status".to_owned()
2138 ),
2139 (
2140 app_v1::ProjectedAggregateOp::CountDistinctPresent as i32,
2141 "assignee".to_owned()
2142 ),
2143 (
2144 app_v1::ProjectedAggregateOp::Sum as i32,
2145 "story_points".to_owned()
2146 ),
2147 (
2148 app_v1::ProjectedAggregateOp::Mean as i32,
2149 "story_points".to_owned()
2150 ),
2151 (app_v1::ProjectedAggregateOp::Min as i32, "title".to_owned()),
2152 (app_v1::ProjectedAggregateOp::Max as i32, "title".to_owned()),
2153 (
2154 app_v1::ProjectedAggregateOp::Any as i32,
2155 "is_open".to_owned()
2156 ),
2157 (
2158 app_v1::ProjectedAggregateOp::All as i32,
2159 "is_open".to_owned()
2160 ),
2161 ]
2162 );
2163 }
2164
2165 #[test]
2173 fn group_keys_without_functions_expect_the_aggregate_arm() {
2174 let distinct = query().group_by(vec!["status".to_owned()]);
2175 assert_eq!(
2176 distinct.clone().response_shape(),
2177 ProjectedResponseShape::Aggregates(ProjectedAggregateShape {
2178 group_by: vec!["status".to_owned()],
2179 aggregates: Vec::new(),
2180 order: Vec::new(),
2181 }),
2182 "a grouped request must not declare itself row-shaped"
2183 );
2184
2185 let body = distinct
2187 .clone()
2188 .into_wire_request()
2189 .expect("wire")
2190 .request
2191 .expect("body");
2192 assert_eq!(body.group_by, vec!["status".to_owned()]);
2193 assert!(body.aggregates.is_empty());
2194
2195 let key_only = app_v1::ProjectedReadyAggregates {
2197 group_key_fields: vec!["status".to_owned()],
2198 aggregates: Vec::new(),
2199 groups: vec![
2200 app_v1::ProjectedAggregateGroup {
2201 keys: vec![
2202 riffdb_proto::canonical_value_to_proto(
2203 &CanonicalValue::string("closed").expect("bounded"),
2204 )
2205 .expect("value"),
2206 ],
2207 values: Vec::new(),
2208 },
2209 app_v1::ProjectedAggregateGroup {
2210 keys: vec![
2211 riffdb_proto::canonical_value_to_proto(
2212 &CanonicalValue::string("open___").expect("bounded"),
2213 )
2214 .expect("value"),
2215 ],
2216 values: Vec::new(),
2217 },
2218 ],
2219 frontier: frontier_bytes(7),
2220 head: frontier_bytes(9),
2221 commit_token: Vec::new(),
2222 };
2223 let outcome = raise_projected_response_for_query(aggregates_response(key_only), &distinct)
2224 .expect("a grouped request must accept the grouped answer it asked for");
2225 let ProjectedQueryOutcome::ReadyAggregates {
2226 group_key_fields,
2227 aggregates,
2228 groups,
2229 ..
2230 } = outcome
2231 else {
2232 panic!("expected ReadyAggregates");
2233 };
2234 assert_eq!(group_key_fields, vec!["status".to_owned()]);
2235 assert!(aggregates.is_empty(), "no functions were requested");
2236 assert_eq!(groups.len(), 2);
2237 assert!(groups.iter().all(|group| group.values.is_empty()));
2238 }
2239
2240 #[test]
2241 fn row_requests_still_carry_no_aggregate_vocabulary() {
2242 let body = query().into_wire_request().expect("wire").request;
2243 let body = body.expect("body");
2244 assert!(body.aggregates.is_empty());
2245 assert!(body.group_by.is_empty());
2246 }
2247
2248 #[test]
2251 fn multiple_aggregates_without_group_keys_are_refused_locally() {
2252 let err = query()
2253 .aggregate(ProjectedAggregate::Count)
2254 .aggregate(ProjectedAggregate::Sum {
2255 field: "story_points".to_owned(),
2256 })
2257 .into_wire_request()
2258 .expect_err("two whole-set functions have no server path");
2259 assert!(matches!(err, ApplicationClientError::InvalidInput));
2260
2261 assert!(
2263 query()
2264 .aggregate(ProjectedAggregate::Count)
2265 .into_wire_request()
2266 .is_ok()
2267 );
2268 assert!(
2269 query()
2270 .group_by(vec!["status".to_owned()])
2271 .aggregate(ProjectedAggregate::Count)
2272 .aggregate(ProjectedAggregate::Sum {
2273 field: "story_points".to_owned(),
2274 })
2275 .into_wire_request()
2276 .is_ok()
2277 );
2278 }
2279
2280 #[test]
2281 fn unbounded_names_are_refused_before_the_wire() {
2282 let long = "x".repeat(257);
2283 assert!(matches!(
2284 query()
2285 .aggregate(ProjectedAggregate::Sum {
2286 field: long.clone()
2287 })
2288 .into_wire_request(),
2289 Err(ApplicationClientError::InvalidInput)
2290 ));
2291 assert!(matches!(
2292 query()
2293 .group_by(vec![long])
2294 .aggregate(ProjectedAggregate::Count)
2295 .into_wire_request(),
2296 Err(ApplicationClientError::InvalidInput)
2297 ));
2298 assert!(matches!(
2299 query()
2300 .group_by(vec![String::new()])
2301 .aggregate(ProjectedAggregate::Count)
2302 .into_wire_request(),
2303 Err(ApplicationClientError::InvalidInput)
2304 ));
2305 }
2306
2307 #[test]
2310 fn aggregate_requests_expect_the_aggregate_arm_under_any_encoding() {
2311 for encoding in [
2312 ProjectedResponseEncoding::Row,
2313 ProjectedResponseEncoding::Packed,
2314 ] {
2315 let query = query()
2316 .encoding(encoding)
2317 .aggregate(ProjectedAggregate::Count);
2318 assert_eq!(
2319 query.response_shape(),
2320 ProjectedResponseShape::Aggregates(ProjectedAggregateShape {
2321 group_by: Vec::new(),
2322 aggregates: vec![ProjectedAggregate::Count],
2323 order: Vec::new(),
2324 })
2325 );
2326 }
2327 assert_eq!(
2328 query().response_shape(),
2329 ProjectedResponseShape::Rows(ProjectedResponseEncoding::Row)
2330 );
2331 }
2332}