Skip to main content

icydb_core/db/response/
grouped.rs

1//! Module: response::grouped
2//! Responsibility: grouped paged response payload contracts.
3//! Does not own: grouped execution evaluation, route policy, or cursor token protocol.
4//! Boundary: grouped DTOs returned by session/query execution APIs.
5
6use crate::value::OutputValue;
7use candid::CandidType;
8use serde::Deserialize;
9
10///
11/// GroupedRow
12///
13/// One grouped public output row: ordered grouping key values plus ordered
14/// aggregate outputs. Group and aggregate vectors preserve query declaration
15/// order at the outward API boundary.
16///
17
18#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
19pub struct GroupedRow {
20    group_key: Vec<OutputValue>,
21    aggregate_values: Vec<OutputValue>,
22}
23
24impl GroupedRow {
25    /// Construct one grouped output row payload.
26    #[must_use]
27    pub fn new<I, J, K, L>(group_key: I, aggregate_values: J) -> Self
28    where
29        I: IntoIterator<Item = K>,
30        J: IntoIterator<Item = L>,
31        K: Into<OutputValue>,
32        L: Into<OutputValue>,
33    {
34        Self {
35            group_key: group_key.into_iter().map(Into::into).collect(),
36            aggregate_values: aggregate_values.into_iter().map(Into::into).collect(),
37        }
38    }
39
40    /// Borrow grouped key values.
41    #[must_use]
42    pub const fn group_key(&self) -> &[OutputValue] {
43        self.group_key.as_slice()
44    }
45
46    /// Borrow aggregate output values.
47    #[must_use]
48    pub const fn aggregate_values(&self) -> &[OutputValue] {
49        self.aggregate_values.as_slice()
50    }
51}
52
53/// One bounded grouped-query page produced by the engine-neutral query lane.
54#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
55pub struct GroupedQueryOutput {
56    /// Accepted entity name used for the read.
57    pub entity: String,
58    /// Ordered grouped rows. Key and aggregate value order follows declaration order.
59    pub rows: Vec<GroupedRow>,
60    /// Number of rows returned in this page.
61    pub row_count: u32,
62    /// Opaque continuation cursor for the next page, when one exists.
63    pub next_cursor: Option<String>,
64}
65
66#[cfg(test)]
67mod tests {
68    use candid::CandidType;
69
70    use super::{GroupedQueryOutput, GroupedRow};
71    use crate::value::OutputValue;
72
73    #[derive(CandidType)]
74    struct FrozenGroupedRowWire {
75        group_key: Vec<OutputValue>,
76        aggregate_values: Vec<OutputValue>,
77    }
78
79    #[derive(CandidType)]
80    struct FrozenGroupedQueryOutputWire {
81        entity: String,
82        rows: Vec<FrozenGroupedRowWire>,
83        row_count: u32,
84        next_cursor: Option<String>,
85    }
86
87    #[test]
88    fn grouped_query_output_preserves_its_initial_candid_record_shape() {
89        let current = GroupedQueryOutput {
90            entity: "Example".to_string(),
91            rows: vec![GroupedRow::new(
92                [OutputValue::Nat64(7)],
93                [OutputValue::Nat64(1)],
94            )],
95            row_count: 1,
96            next_cursor: Some("abcd".to_string()),
97        };
98        let frozen = FrozenGroupedQueryOutputWire {
99            entity: current.entity.clone(),
100            rows: vec![FrozenGroupedRowWire {
101                group_key: vec![OutputValue::Nat64(7)],
102                aggregate_values: vec![OutputValue::Nat64(1)],
103            }],
104            row_count: current.row_count,
105            next_cursor: current.next_cursor.clone(),
106        };
107
108        assert_eq!(
109            candid::encode_one(&current).expect("current grouped output should encode"),
110            candid::encode_one(&frozen).expect("frozen grouped output should encode"),
111        );
112    }
113}