Skip to main content

type_bridge/
aggregate.rs

1#![deny(missing_docs)]
2//! Typed reduction terms and tuple decoding for the query facade
3//! (Flight 3, F3-04b-3).
4//!
5//! Aggregates use the same query lineage and bound-field handles as row
6//! terminals and lower onto the canonical typed reduce operation; they
7//! never route through legacy string-keyed aggregate maps.
8
9use std::marker::PhantomData;
10
11use type_bridge_orm::match_request::{ReducedValue, Reduction};
12
13use crate::__codegen::Model;
14use crate::__codegen::NumericValued;
15use crate::Result;
16use crate::error::{Error, ModelValidationPhase};
17use crate::query::{BindingKey, BoundField};
18use crate::schema::Schema;
19
20pub(crate) fn wrong_reduction_value() -> Error {
21    Error::model_validation(
22        ModelValidationPhase::Hydration,
23        "wrong_result_shape",
24        vec![],
25        "provider reduction value does not fit its requested reducer",
26        None,
27    )
28}
29
30/// One typed reducer term over the query's distinct selected stream.
31pub struct Agg<S: Schema, Out> {
32    pub(crate) reduction: Reduction,
33    pub(crate) input: Option<(BindingKey, &'static str)>,
34    marker: PhantomData<fn() -> (S, Out)>,
35}
36
37impl<S: Schema, Out> Copy for Agg<S, Out> {}
38impl<S: Schema, Out> Clone for Agg<S, Out> {
39    fn clone(&self) -> Self {
40        *self
41    }
42}
43
44impl<S: Schema, Out> Agg<S, Out> {
45    pub(crate) fn new(reduction: Reduction, input: Option<(BindingKey, &'static str)>) -> Self {
46        Self {
47            reduction,
48            input,
49            marker: PhantomData,
50        }
51    }
52}
53
54/// Count distinct selected identities; zero on an empty ungrouped stream.
55#[must_use]
56pub fn count<S: Schema>() -> Agg<S, u64> {
57    Agg::new(Reduction::Count, None)
58}
59
60/// Sealed decoding of one typed reduced scalar into its facade output.
61pub trait ReducedOutput: reduced_sealed::Sealed + Sized {
62    #[doc(hidden)]
63    fn decode(value: &ReducedValue) -> Result<Self>;
64}
65
66mod reduced_sealed {
67    pub trait Sealed {}
68}
69
70impl reduced_sealed::Sealed for u64 {}
71impl ReducedOutput for u64 {
72    fn decode(value: &ReducedValue) -> Result<Self> {
73        match value {
74            ReducedValue::Count(value) => Ok(*value),
75            _ => Err(wrong_reduction_value()),
76        }
77    }
78}
79
80impl reduced_sealed::Sealed for i64 {}
81impl ReducedOutput for i64 {
82    fn decode(value: &ReducedValue) -> Result<Self> {
83        match value {
84            ReducedValue::Long(Some(value)) => Ok(*value),
85            _ => Err(wrong_reduction_value()),
86        }
87    }
88}
89
90impl reduced_sealed::Sealed for f64 {}
91impl ReducedOutput for f64 {
92    fn decode(value: &ReducedValue) -> Result<Self> {
93        match value {
94            ReducedValue::Double(Some(value)) => Ok(*value),
95            _ => Err(wrong_reduction_value()),
96        }
97    }
98}
99
100impl reduced_sealed::Sealed for Option<i64> {}
101impl ReducedOutput for Option<i64> {
102    fn decode(value: &ReducedValue) -> Result<Self> {
103        match value {
104            ReducedValue::Long(value) => Ok(*value),
105            _ => Err(wrong_reduction_value()),
106        }
107    }
108}
109
110impl reduced_sealed::Sealed for Option<f64> {}
111impl ReducedOutput for Option<f64> {
112    fn decode(value: &ReducedValue) -> Result<Self> {
113        match value {
114            ReducedValue::Double(value) => Ok(*value),
115            _ => Err(wrong_reduction_value()),
116        }
117    }
118}
119
120impl<S, Owner, V> BoundField<S, Owner, V>
121where
122    S: Schema,
123    Owner: Model<Schema = S>,
124    V: NumericValued,
125{
126    fn aggregate<Out>(self, reduction: Reduction) -> Agg<S, Out> {
127        let (key, owns_id_json) = self.reduction_input();
128        Agg::new(reduction, Some((key, owns_id_json)))
129    }
130
131    /// The total of this field's values; domain-preserving and total on an
132    /// empty ungrouped stream (zero). A provider reporting an absent sum
133    /// fails closed at decode.
134    #[must_use]
135    pub fn sum(self) -> Agg<S, V::Reduced> {
136        self.aggregate(Reduction::Sum)
137    }
138
139    /// The smallest of this field's values; absent on an empty ungrouped
140    /// stream.
141    #[must_use]
142    pub fn min(self) -> Agg<S, Option<V::Reduced>> {
143        self.aggregate(Reduction::Min)
144    }
145
146    /// The largest of this field's values; absent on an empty ungrouped
147    /// stream.
148    #[must_use]
149    pub fn max(self) -> Agg<S, Option<V::Reduced>> {
150        self.aggregate(Reduction::Max)
151    }
152
153    /// The arithmetic mean of this field's values; absent on an empty
154    /// ungrouped stream.
155    #[must_use]
156    pub fn mean(self) -> Agg<S, Option<f64>> {
157        self.aggregate(Reduction::Mean)
158    }
159
160    /// The statistical median of this field's values; absent on an empty
161    /// ungrouped stream.
162    #[must_use]
163    pub fn median(self) -> Agg<S, Option<f64>> {
164        self.aggregate(Reduction::Median)
165    }
166
167    /// The sample standard deviation of this field's values; absent when
168    /// fewer than two values are witnessed on an ungrouped stream.
169    #[must_use]
170    pub fn stddev(self) -> Agg<S, Option<f64>> {
171        self.aggregate(Reduction::Std)
172    }
173}
174
175/// Sealed tuple of typed reducer terms through the sixteen-slot ceiling.
176pub trait AggregateTuple<S: Schema>: tuple_sealed::Sealed<S> {
177    /// The typed tuple output decoded from one reduction row.
178    type Output;
179    #[doc(hidden)]
180    fn terms(&self) -> Vec<(Reduction, Option<(BindingKey, &'static str)>)>;
181    #[doc(hidden)]
182    fn decode(values: &[ReducedValue]) -> Result<Self::Output>;
183}
184
185mod tuple_sealed {
186    pub trait Sealed<S> {}
187}
188
189macro_rules! aggregate_tuple {
190    ($(($name:ident, $out:ident, $index:tt)),+) => {
191        impl<S: Schema, $($out: ReducedOutput),+> tuple_sealed::Sealed<S>
192            for ($(Agg<S, $out>,)+)
193        {
194        }
195        impl<S: Schema, $($out: ReducedOutput),+> AggregateTuple<S> for ($(Agg<S, $out>,)+) {
196            type Output = ($($out,)+);
197            fn terms(&self) -> Vec<(Reduction, Option<(BindingKey, &'static str)>)> {
198                vec![$((self.$index.reduction, self.$index.input)),+]
199            }
200            fn decode(values: &[ReducedValue]) -> Result<Self::Output> {
201                let expected = [$(stringify!($name)),+].len();
202                if values.len() != expected {
203                    return Err(wrong_reduction_value());
204                }
205                Ok(($($out::decode(&values[$index])?,)+))
206            }
207        }
208    };
209}
210
211aggregate_tuple!((a, A, 0));
212aggregate_tuple!((a, A, 0), (b, B, 1));
213aggregate_tuple!((a, A, 0), (b, B, 1), (c, C, 2));
214aggregate_tuple!((a, A, 0), (b, B, 1), (c, C, 2), (d, D, 3));
215aggregate_tuple!((a, A, 0), (b, B, 1), (c, C, 2), (d, D, 3), (e, E, 4));
216aggregate_tuple!(
217    (a, A, 0),
218    (b, B, 1),
219    (c, C, 2),
220    (d, D, 3),
221    (e, E, 4),
222    (f, F, 5)
223);
224aggregate_tuple!(
225    (a, A, 0),
226    (b, B, 1),
227    (c, C, 2),
228    (d, D, 3),
229    (e, E, 4),
230    (f, F, 5),
231    (g, G, 6)
232);
233aggregate_tuple!(
234    (a, A, 0),
235    (b, B, 1),
236    (c, C, 2),
237    (d, D, 3),
238    (e, E, 4),
239    (f, F, 5),
240    (g, G, 6),
241    (h, H, 7)
242);