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;
7
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
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
69    use super::{GroupedQueryOutput, GroupedRow};
70    use crate::value::OutputValue;
71
72    #[derive(serde::Serialize)]
73    struct FrozenGroupedRowWire {
74        group_key: Vec<OutputValue>,
75        aggregate_values: Vec<OutputValue>,
76    }
77
78    #[derive(serde::Serialize)]
79    struct FrozenGroupedQueryOutputWire {
80        entity: String,
81        rows: Vec<FrozenGroupedRowWire>,
82        row_count: u32,
83        next_cursor: Option<String>,
84    }
85
86    #[test]
87    fn grouped_query_output_preserves_its_initial_wire_record_shape() {
88        let current = GroupedQueryOutput {
89            entity: "Example".to_string(),
90            rows: vec![GroupedRow::new(
91                [OutputValue::nat64(7)],
92                [OutputValue::nat64(1)],
93            )],
94            row_count: 1,
95            next_cursor: Some("abcd".to_string()),
96        };
97        let frozen = FrozenGroupedQueryOutputWire {
98            entity: current.entity.clone(),
99            rows: vec![FrozenGroupedRowWire {
100                group_key: vec![OutputValue::nat64(7)],
101                aggregate_values: vec![OutputValue::nat64(1)],
102            }],
103            row_count: current.row_count,
104            next_cursor: current.next_cursor.clone(),
105        };
106
107        assert_eq!(
108            icydb_schema::wire::encode_one(&current).expect("current grouped output should encode"),
109            icydb_schema::wire::encode_one(&frozen).expect("frozen grouped output should encode"),
110        );
111    }
112}