Skip to main content

harmos_storage/query/transform/
aggregate.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10// Storage Arrow Query Contract
11
12/* === Modules === */
13
14/* === Re-Exports === */
15
16pub use harmos_domain::model::storage::query::transform::aggregate::{Aggregate, Measure};
17
18/* === Imports === */
19
20use crate::storage::query::transform::FieldPathExecution;
21// Standard Library Imports
22use std::cmp::Ordering;
23use std::sync::Arc;
24// Third Party Imports
25use arrow_array::RecordBatch;
26use arrow_schema::{DataType, Field, Schema};
27// Local Imports
28use super::{exact_type, invalid, numeric_type, scalar_type, values_array};
29use crate::Result;
30use crate::storage::query::accumulate::Accumulator;
31use harmos_domain::model::storage::query::scalar::{Agg, ScalarValue};
32
33/* === Constants === */
34
35/* === Macros === */
36
37/* === Type Aliases === */
38
39/* === Structs === */
40
41pub struct Groups {
42    plan: Aggregate,
43    schema: Arc<Schema>,
44    groups: Vec<Group>,
45    capture: u64,
46}
47
48struct Group {
49    keys: Vec<ScalarValue>,
50    accumulators: Vec<Accumulator>,
51}
52
53/* === Enums === */
54
55/* === Traits === */
56
57pub trait AggregateExecution {
58    fn output_schema(&self, input: &Schema) -> Result<Schema>;
59
60    fn start(&self, input: &Schema) -> Result<Groups>;
61
62    fn apply(&self, input: &RecordBatch, check: &mut impl FnMut() -> Result<()>) -> Result<RecordBatch>;
63}
64
65/* === Implementations === */
66
67impl AggregateExecution for Aggregate {
68    fn output_schema(&self, input: &Schema) -> Result<Schema> {
69        if self.groups.len() > 64 || self.measures.is_empty() || self.measures.len() > 64 {
70            return Err(invalid("aggregate requires at most 64 groups and 1..64 measures"));
71        }
72        let mut fields = Vec::new();
73        for group in &self.groups {
74            let field = group.field.field(input)?;
75            if !scalar_type(field.data_type()) {
76                return Err(invalid("aggregate grouping requires scalar keys"));
77            }
78            fields.push(field.with_name(&group.name).with_nullable(true));
79        }
80        for measure in &self.measures {
81            let field = measure.field.field(input)?;
82            if !scalar_type(field.data_type()) {
83                return Err(invalid("aggregate requires scalar values"));
84            }
85            let statistic = measure.statistic;
86            if !matches!(statistic, crate::Aggregate::Count | crate::Aggregate::Min | crate::Aggregate::Max)
87                && !numeric_type(field.data_type())
88            {
89                return Err(invalid("numeric aggregate requires numeric field"));
90            }
91            if let crate::Aggregate::Percentile(rank) = statistic {
92                // The shared vocabulary validates the rank through its normal codec.
93                if rank > 100 {
94                    return Err(invalid("invalid percentile rank"));
95                }
96            }
97            let output = match statistic {
98                crate::Aggregate::Count => DataType::UInt64,
99                crate::Aggregate::Min | crate::Aggregate::Max => field.data_type().clone(),
100                crate::Aggregate::Sum | crate::Aggregate::Mean | crate::Aggregate::Median
101                    if !matches!(field.data_type(), DataType::Float32 | DataType::Float64) =>
102                {
103                    exact_type()
104                }
105                _ => DataType::Float64,
106            };
107            let mut field = Field::new(&measure.name, output, !matches!(statistic, crate::Aggregate::Count));
108            if field.data_type() == &exact_type() {
109                field = field
110                    .with_metadata(std::collections::HashMap::from([("harmos.logical_type".to_owned(), "exact-rational-v1".to_owned())]));
111            }
112            fields.push(field);
113        }
114        for field in &fields {
115            if field.name().is_empty() || fields.iter().filter(|other| other.name() == field.name()).count() != 1 {
116                return Err(invalid("aggregate output names must be nonempty and unique"));
117            }
118        }
119        Ok(Schema::new(fields))
120    }
121
122    fn start(&self, input: &Schema) -> Result<Groups> {
123        let schema = Arc::new(self.output_schema(input)?);
124        let mut groups = Groups {
125            plan: self.clone(),
126            schema,
127            groups: Vec::new(),
128            capture: 0,
129        };
130        // An ungrouped reduction always yields its identity row, even for no input.
131        if self.groups.is_empty() {
132            groups.groups.push(new_group(self, Vec::new()));
133        }
134        Ok(groups)
135    }
136
137    fn apply(&self, input: &RecordBatch, check: &mut impl FnMut() -> Result<()>) -> Result<RecordBatch> {
138        let mut groups = self.start(input.schema().as_ref())?;
139        groups.push(input, check)?;
140        groups.finish(check)
141    }
142}
143
144impl Groups {
145    pub fn push(&mut self, input: &RecordBatch, check: &mut impl FnMut() -> Result<()>) -> Result<()> {
146        // Fold One Row At A Time Without A Page-Sized Scalar Copy.
147        for row in 0..input.num_rows() {
148            check()?;
149            let keys = self.plan.groups.iter().map(|group| group.field.value(input, row).map(normalized)).collect::<Result<Vec<_>>>()?;
150            let values =
151                self.plan.measures.iter().map(|measure| measure.field.value(input, row).map(normalized)).collect::<Result<Vec<_>>>()?;
152            let index = match self.find(&keys)? {
153                Ok(index) => index,
154                Err(index) => {
155                    self.groups.insert(index, new_group(&self.plan, keys));
156                    index
157                }
158            };
159            let group = &mut self.groups[index];
160            for (accumulator, value) in group.accumulators.iter_mut().zip(values) {
161                accumulator.push(self.capture, &value)?;
162            }
163            self.capture = self.capture.checked_add(1).ok_or_else(|| invalid("query capture count overflow"))?;
164        }
165        Ok(())
166    }
167
168    pub fn finish(self, check: &mut impl FnMut() -> Result<()>) -> Result<RecordBatch> {
169        check()?;
170        let width = self.schema.fields().len();
171        let mut values = vec![Vec::with_capacity(self.groups.len()); width];
172        for group in self.groups {
173            check()?;
174            for (column, key) in group.keys.into_iter().enumerate() {
175                values[column].push(key);
176            }
177            for (column, accumulator) in group.accumulators.into_iter().enumerate() {
178                values[self.plan.groups.len() + column].push(accumulator.finish(check)?);
179            }
180        }
181        let columns = self
182            .schema
183            .fields()
184            .iter()
185            .zip(values)
186            .map(|(field, values)| values_array(field.data_type(), &values))
187            .collect::<Result<Vec<_>>>()?;
188        RecordBatch::try_new(self.schema, columns).map_err(|_| invalid("aggregate output batch is invalid"))
189    }
190
191    // Binary Search Over Groups Kept In Key Order.
192    fn find(&self, keys: &[ScalarValue]) -> Result<std::result::Result<usize, usize>> {
193        let (mut low, mut high) = (0usize, self.groups.len());
194        while low < high {
195            let middle = low + (high - low) / 2;
196            match compare_keys(&self.groups[middle].keys, keys)? {
197                Ordering::Less => low = middle + 1,
198                Ordering::Greater => high = middle,
199                Ordering::Equal => return Ok(Ok(middle)),
200            }
201        }
202        Ok(Err(low))
203    }
204}
205
206/* === Public Functions === */
207
208/* === Private Functions === */
209
210fn new_group(plan: &Aggregate, keys: Vec<ScalarValue>) -> Group {
211    Group {
212        keys,
213        accumulators: plan.measures.iter().map(|measure| Accumulator::new(Agg::Of(measure.statistic))).collect(),
214    }
215}
216
217fn compare_keys(left: &[ScalarValue], right: &[ScalarValue]) -> Result<Ordering> {
218    for (left, right) in left.iter().zip(right) {
219        let comparison = left.compare(right)?;
220        if !comparison.is_eq() {
221            return Ok(comparison);
222        }
223    }
224    Ok(left.len().cmp(&right.len()))
225}
226
227fn normalized(value: ScalarValue) -> ScalarValue {
228    match value {
229        ScalarValue::Float(number) if !number.is_finite() => ScalarValue::Null,
230        value => value,
231    }
232}
233
234/* === Tests === */