Skip to main content

uqa_operators/
aggregation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Aggregation monoids and posting-list aggregate operators.
8//!
9//! Aggregation functions
10//! form a monoid (Section 5.1, Paper 1) so a parallel executor can
11//! split the input, fold each shard with [`AggregationMonoid::accumulate`],
12//! merge the partial states with [`AggregationMonoid::combine`], and
13//! emit the final value via [`AggregationMonoid::finalize`].
14//!
15//! # Concrete monoids
16//!
17//! * [`CountMonoid`] -- `(0, +)` over `u64`.
18//! * [`SumMonoid`] -- `(0.0, +)` over `f64`.
19//! * [`AvgMonoid`] -- `((0.0, 0), pair-wise +)` over `(sum, count)`.
20//! * [`MinMonoid`], [`MaxMonoid`] -- `(infinity, min)` / `(-infinity, max)`
21//!   over `f64`.
22//! * [`QuantileMonoid`] -- collect-then-finalize quantile estimator.
23//!
24//! # Operators
25//!
26//! * [`AggregateOperator`] folds a posting list with the supplied
27//!   monoid and emits a single-entry posting list whose payload
28//!   carries `_aggregate_field` / `_aggregate` metadata.
29//! * [`GroupByOperator`] groups by a field, folds a second field with
30//!   the monoid per group, and emits one entry per group.
31
32#![allow(
33    clippy::redundant_field_names,
34    clippy::similar_names,
35    clippy::manual_let_else,
36    clippy::explicit_iter_loop
37)]
38
39use std::collections::BTreeMap;
40use std::sync::Arc;
41
42use uqa_core::{Payload, PostingEntry, PostingList, Value};
43use uqa_storage::{StorageBackendError, StorageBackendResult};
44
45use crate::base::{missing_backend, ExecutionContext, Operator, OperatorResult};
46
47/// Pair-wise additive state used by [`AvgMonoid`].
48#[derive(Debug, Clone, Copy, Default, PartialEq)]
49pub struct AvgState {
50    pub sum: f64,
51    pub count: u64,
52}
53
54/// Generic aggregation state. Concrete monoids consume / emit the
55/// variants they care about; a mismatched variant is an execution
56/// error rather than a silently accepted partial fold.
57#[derive(Debug, Clone, PartialEq)]
58pub enum AggState {
59    Count(u64),
60    Sum(f64),
61    Avg(AvgState),
62    Min(f64),
63    Max(f64),
64    Values(Vec<f64>),
65}
66
67impl AggState {
68    pub fn as_f64(&self) -> Option<f64> {
69        match self {
70            AggState::Count(n) => Some(*n as f64),
71            AggState::Sum(v) | AggState::Min(v) | AggState::Max(v) => Some(*v),
72            AggState::Avg(s) => {
73                if s.count == 0 {
74                    Some(0.0)
75                } else {
76                    Some(s.sum / s.count as f64)
77                }
78            }
79            AggState::Values(_) => None,
80        }
81    }
82}
83
84/// Aggregation function with monoid structure for parallel
85/// decomposition. See Section 5.1, Paper 1.
86pub trait AggregationMonoid: Send + Sync {
87    fn identity(&self) -> AggState;
88    fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState>;
89    fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState>;
90    fn finalize(&self, state: AggState) -> StorageBackendResult<Value>;
91}
92
93fn invalid_state(operation: &str, expected: &str, actual: &AggState) -> StorageBackendError {
94    StorageBackendError::Other(format!(
95        "{operation} aggregation expected {expected} state, got {actual:?}"
96    ))
97}
98
99fn numeric_value(operation: &str, value: &Value) -> StorageBackendResult<Option<f64>> {
100    let numeric = match value {
101        Value::Null => return Ok(None),
102        Value::Int(integer) => *integer as f64,
103        Value::Float(float) => *float,
104        Value::Bool(true) => 1.0,
105        Value::Bool(false) => 0.0,
106        _ => {
107            return Err(StorageBackendError::Other(format!(
108                "{operation} aggregation requires a numeric value, got {value:?}"
109            )))
110        }
111    };
112    if !numeric.is_finite() {
113        return Err(StorageBackendError::Other(format!(
114            "{operation} aggregation requires a finite numeric value, got {numeric}"
115        )));
116    }
117    Ok(Some(numeric))
118}
119
120fn finite_result(operation: &str, value: f64) -> StorageBackendResult<f64> {
121    if value.is_finite() {
122        Ok(value)
123    } else {
124        Err(StorageBackendError::Other(format!(
125            "{operation} aggregation overflowed the finite numeric range"
126        )))
127    }
128}
129
130#[derive(Debug, Clone, Copy, Default)]
131pub struct CountMonoid;
132
133impl AggregationMonoid for CountMonoid {
134    fn identity(&self) -> AggState {
135        AggState::Count(0)
136    }
137    fn accumulate(&self, state: AggState, _value: &Value) -> StorageBackendResult<AggState> {
138        let AggState::Count(count) = state else {
139            return Err(invalid_state("count", "Count", &state));
140        };
141        Ok(AggState::Count(count.checked_add(1).ok_or_else(|| {
142            StorageBackendError::Other("count aggregation overflowed u64".to_string())
143        })?))
144    }
145    fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
146        let (AggState::Count(left), AggState::Count(right)) = (&a, &b) else {
147            return Err(StorageBackendError::Other(format!(
148                "count aggregation expected Count states, got {a:?} and {b:?}"
149            )));
150        };
151        Ok(AggState::Count(left.checked_add(*right).ok_or_else(
152            || StorageBackendError::Other("count aggregation overflowed u64".to_string()),
153        )?))
154    }
155    fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
156        let AggState::Count(count) = state else {
157            return Err(invalid_state("count", "Count", &state));
158        };
159        let count = i64::try_from(count).map_err(|_| {
160            StorageBackendError::Other(format!(
161                "count aggregation result {count} exceeds the Value::Int range"
162            ))
163        })?;
164        Ok(Value::Int(count))
165    }
166}
167
168#[derive(Debug, Clone, Copy, Default)]
169pub struct SumMonoid;
170
171impl AggregationMonoid for SumMonoid {
172    fn identity(&self) -> AggState {
173        AggState::Sum(0.0)
174    }
175    fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
176        let Some(delta) = numeric_value("sum", value)? else {
177            return Ok(state);
178        };
179        let AggState::Sum(sum) = state else {
180            return Err(invalid_state("sum", "Sum", &state));
181        };
182        Ok(AggState::Sum(finite_result("sum", sum + delta)?))
183    }
184    fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
185        let (AggState::Sum(left), AggState::Sum(right)) = (&a, &b) else {
186            return Err(StorageBackendError::Other(format!(
187                "sum aggregation expected Sum states, got {a:?} and {b:?}"
188            )));
189        };
190        Ok(AggState::Sum(finite_result("sum", left + right)?))
191    }
192    fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
193        let AggState::Sum(sum) = state else {
194            return Err(invalid_state("sum", "Sum", &state));
195        };
196        Ok(Value::Float(finite_result("sum", sum)?))
197    }
198}
199
200#[derive(Debug, Clone, Copy, Default)]
201pub struct AvgMonoid;
202
203impl AggregationMonoid for AvgMonoid {
204    fn identity(&self) -> AggState {
205        AggState::Avg(AvgState::default())
206    }
207    fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
208        let Some(delta) = numeric_value("avg", value)? else {
209            return Ok(state);
210        };
211        let AggState::Avg(average) = state else {
212            return Err(invalid_state("avg", "Avg", &state));
213        };
214        Ok(AggState::Avg(AvgState {
215            sum: finite_result("avg", average.sum + delta)?,
216            count: average.count.checked_add(1).ok_or_else(|| {
217                StorageBackendError::Other("avg aggregation count overflowed u64".to_string())
218            })?,
219        }))
220    }
221    fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
222        let (AggState::Avg(left), AggState::Avg(right)) = (&a, &b) else {
223            return Err(StorageBackendError::Other(format!(
224                "avg aggregation expected Avg states, got {a:?} and {b:?}"
225            )));
226        };
227        Ok(AggState::Avg(AvgState {
228            sum: finite_result("avg", left.sum + right.sum)?,
229            count: left.count.checked_add(right.count).ok_or_else(|| {
230                StorageBackendError::Other("avg aggregation count overflowed u64".to_string())
231            })?,
232        }))
233    }
234    fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
235        let AggState::Avg(average) = state else {
236            return Err(invalid_state("avg", "Avg", &state));
237        };
238        if average.count == 0 {
239            Ok(Value::Float(0.0))
240        } else {
241            Ok(Value::Float(finite_result(
242                "avg",
243                average.sum / average.count as f64,
244            )?))
245        }
246    }
247}
248
249#[derive(Debug, Clone, Copy, Default)]
250pub struct MinMonoid;
251
252impl AggregationMonoid for MinMonoid {
253    fn identity(&self) -> AggState {
254        AggState::Min(f64::INFINITY)
255    }
256    fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
257        let Some(value) = numeric_value("min", value)? else {
258            return Ok(state);
259        };
260        let AggState::Min(minimum) = state else {
261            return Err(invalid_state("min", "Min", &state));
262        };
263        Ok(AggState::Min(minimum.min(value)))
264    }
265    fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
266        let (AggState::Min(left), AggState::Min(right)) = (&a, &b) else {
267            return Err(StorageBackendError::Other(format!(
268                "min aggregation expected Min states, got {a:?} and {b:?}"
269            )));
270        };
271        Ok(AggState::Min(left.min(*right)))
272    }
273    fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
274        let AggState::Min(minimum) = state else {
275            return Err(invalid_state("min", "Min", &state));
276        };
277        if minimum == f64::INFINITY {
278            Ok(Value::Null)
279        } else {
280            Ok(Value::Float(finite_result("min", minimum)?))
281        }
282    }
283}
284
285#[derive(Debug, Clone, Copy, Default)]
286pub struct MaxMonoid;
287
288impl AggregationMonoid for MaxMonoid {
289    fn identity(&self) -> AggState {
290        AggState::Max(f64::NEG_INFINITY)
291    }
292    fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
293        let Some(value) = numeric_value("max", value)? else {
294            return Ok(state);
295        };
296        let AggState::Max(maximum) = state else {
297            return Err(invalid_state("max", "Max", &state));
298        };
299        Ok(AggState::Max(maximum.max(value)))
300    }
301    fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
302        let (AggState::Max(left), AggState::Max(right)) = (&a, &b) else {
303            return Err(StorageBackendError::Other(format!(
304                "max aggregation expected Max states, got {a:?} and {b:?}"
305            )));
306        };
307        Ok(AggState::Max(left.max(*right)))
308    }
309    fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
310        let AggState::Max(maximum) = state else {
311            return Err(invalid_state("max", "Max", &state));
312        };
313        if maximum == f64::NEG_INFINITY {
314            Ok(Value::Null)
315        } else {
316            Ok(Value::Float(finite_result("max", maximum)?))
317        }
318    }
319}
320
321/// Quantile aggregation: collects observed values and computes the
322/// requested quantile at finalize. `quantile = 0.5` is the median.
323#[derive(Debug, Clone, Copy)]
324pub struct QuantileMonoid {
325    quantile: f64,
326}
327
328impl QuantileMonoid {
329    pub fn new(quantile: f64) -> StorageBackendResult<Self> {
330        if !quantile.is_finite() || !(0.0..=1.0).contains(&quantile) {
331            return Err(StorageBackendError::Other(format!(
332                "quantile must be finite and in [0, 1], got {quantile}"
333            )));
334        }
335        Ok(Self { quantile })
336    }
337}
338
339impl AggregationMonoid for QuantileMonoid {
340    fn identity(&self) -> AggState {
341        AggState::Values(Vec::new())
342    }
343    fn accumulate(&self, state: AggState, value: &Value) -> StorageBackendResult<AggState> {
344        let Some(value) = numeric_value("quantile", value)? else {
345            return Ok(state);
346        };
347        let AggState::Values(mut values) = state else {
348            return Err(invalid_state("quantile", "Values", &state));
349        };
350        values.push(value);
351        Ok(AggState::Values(values))
352    }
353    fn combine(&self, a: AggState, b: AggState) -> StorageBackendResult<AggState> {
354        let (AggState::Values(mut left), AggState::Values(right)) = (a, b) else {
355            return Err(StorageBackendError::Other(
356                "quantile aggregation expected Values states".to_string(),
357            ));
358        };
359        left.extend(right);
360        Ok(AggState::Values(left))
361    }
362    fn finalize(&self, state: AggState) -> StorageBackendResult<Value> {
363        let mut buf = match state {
364            AggState::Values(v) => v,
365            other => return Err(invalid_state("quantile", "Values", &other)),
366        };
367        if buf.is_empty() {
368            return Ok(Value::Null);
369        }
370        if buf.iter().any(|value| !value.is_finite()) {
371            return Err(StorageBackendError::Other(
372                "quantile aggregation state contains a non-finite value".to_string(),
373            ));
374        }
375        buf.sort_by(f64::total_cmp);
376        let n = buf.len();
377        let idx = self.quantile * (n - 1) as f64;
378        let lower = idx.floor() as usize;
379        let upper = (lower + 1).min(n - 1);
380        let frac = idx - lower as f64;
381        Ok(Value::Float(buf[lower] * (1.0 - frac) + buf[upper] * frac))
382    }
383}
384
385// -------------------------------------------------------------------------
386// Operators
387// -------------------------------------------------------------------------
388
389/// Apply a monoid over a single field across the rows the source
390/// operator produces. Emits a one-entry posting list whose payload
391/// carries `_aggregate_field` / `_aggregate` so downstream callers
392/// can pick the result by name.
393pub struct AggregateOperator {
394    pub source: Option<Arc<dyn Operator>>,
395    pub field: String,
396    pub monoid: Arc<dyn AggregationMonoid>,
397}
398
399impl AggregateOperator {
400    pub fn new(
401        source: Option<Arc<dyn Operator>>,
402        field: impl Into<String>,
403        monoid: Arc<dyn AggregationMonoid>,
404    ) -> Self {
405        Self {
406            source,
407            field: field.into(),
408            monoid,
409        }
410    }
411}
412
413impl Operator for AggregateOperator {
414    fn execute(&self, ctx: &ExecutionContext) -> OperatorResult {
415        let doc_ids: Vec<u64> = if let Some(op) = &self.source {
416            op.execute(ctx)?.iter().map(|e| e.doc_id).collect()
417        } else {
418            let store = ctx
419                .document_store
420                .as_ref()
421                .ok_or_else(|| missing_backend("document-store", "field aggregation"))?;
422            let mut ids = store.doc_ids()?;
423            ids.sort_unstable();
424            ids
425        };
426
427        let mut state = self.monoid.identity();
428        let store = ctx
429            .document_store
430            .as_ref()
431            .ok_or_else(|| missing_backend("document-store", "field aggregation"))?;
432        for doc_id in doc_ids {
433            if store.get(doc_id)?.is_none() {
434                return Err(StorageBackendError::Other(format!(
435                    "field aggregate candidate {doc_id} is missing from the document store"
436                )));
437            }
438            if let Some(value) = store.get_field(doc_id, &self.field)? {
439                state = self.monoid.accumulate(state, &value)?;
440            }
441        }
442
443        let result = self.monoid.finalize(state)?;
444        let score = match &result {
445            Value::Int(i) => *i as f64,
446            Value::Float(f) => *f,
447            _ => 0.0,
448        };
449        let mut fields: BTreeMap<String, Value> = BTreeMap::new();
450        fields.insert("_aggregate_field".into(), Value::Str(self.field.clone()));
451        fields.insert("_aggregate".into(), result);
452        Ok(PostingList::from_sorted_unchecked(vec![PostingEntry::new(
453            0,
454            Payload {
455                score,
456                fields,
457                ..Default::default()
458            },
459        )]))
460    }
461}
462
463/// Group documents by `group_field` and fold `agg_field` per group.
464pub struct GroupByOperator {
465    pub source: Arc<dyn Operator>,
466    pub group_field: String,
467    pub agg_field: String,
468    pub monoid: Arc<dyn AggregationMonoid>,
469}
470
471impl GroupByOperator {
472    pub fn new(
473        source: Arc<dyn Operator>,
474        group_field: impl Into<String>,
475        agg_field: impl Into<String>,
476        monoid: Arc<dyn AggregationMonoid>,
477    ) -> Self {
478        Self {
479            source,
480            group_field: group_field.into(),
481            agg_field: agg_field.into(),
482            monoid,
483        }
484    }
485}
486
487impl Operator for GroupByOperator {
488    fn execute(&self, ctx: &ExecutionContext) -> OperatorResult {
489        let source_pl = self.source.execute(ctx)?;
490        let store = ctx
491            .document_store
492            .as_ref()
493            .ok_or_else(|| missing_backend("document-store", "group-by aggregation"))?;
494
495        let mut groups: BTreeMap<String, AggState> = BTreeMap::new();
496        for entry in source_pl.iter() {
497            if store.get(entry.doc_id)?.is_none() {
498                return Err(StorageBackendError::Other(format!(
499                    "group-by candidate {} is missing from the document store",
500                    entry.doc_id
501                )));
502            }
503            let Some(group_val) = store.get_field(entry.doc_id, &self.group_field)? else {
504                continue;
505            };
506            let key = value_to_key(&group_val);
507            let state = groups.entry(key).or_insert_with(|| self.monoid.identity());
508            if let Some(agg_val) = store.get_field(entry.doc_id, &self.agg_field)? {
509                let new_state = self
510                    .monoid
511                    .accumulate(std::mem::replace(state, self.monoid.identity()), &agg_val)?;
512                *state = new_state;
513            }
514        }
515
516        let mut entries: Vec<PostingEntry> = Vec::with_capacity(groups.len());
517        for (i, (group_key, state)) in groups.into_iter().enumerate() {
518            let result = self.monoid.finalize(state)?;
519            let score = match &result {
520                Value::Int(i) => *i as f64,
521                Value::Float(f) => *f,
522                _ => 0.0,
523            };
524            let mut fields: BTreeMap<String, Value> = BTreeMap::new();
525            fields.insert("_group_key".into(), Value::Str(group_key));
526            fields.insert("_group_field".into(), Value::Str(self.group_field.clone()));
527            fields.insert("_aggregate_result".into(), result);
528            entries.push(PostingEntry::new(
529                u64::try_from(i).map_err(|_| {
530                    StorageBackendError::Other(format!(
531                        "group-by bucket index {i} exceeds the document-id range"
532                    ))
533                })?,
534                Payload {
535                    score,
536                    fields,
537                    ..Default::default()
538                },
539            ));
540        }
541        Ok(PostingList::from_sorted_unchecked(entries))
542    }
543}
544
545fn value_to_key(v: &Value) -> String {
546    match v {
547        Value::Null => "\x00".into(),
548        Value::Int(i) => i.to_string(),
549        Value::Float(f) => format!("{f:.17}"),
550        Value::Str(s) => s.clone(),
551        Value::Bool(b) => b.to_string(),
552        other => format!("{other:?}"),
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559
560    #[test]
561    fn count_monoid_combines_partial_folds() {
562        let m = CountMonoid;
563        let mut a = m.identity();
564        let mut b = m.identity();
565        a = m.accumulate(a, &Value::Null).unwrap();
566        a = m.accumulate(a, &Value::Null).unwrap();
567        b = m.accumulate(b, &Value::Null).unwrap();
568        let merged = m.combine(a, b).unwrap();
569        assert_eq!(m.finalize(merged).unwrap(), Value::Int(3));
570    }
571
572    #[test]
573    fn sum_monoid_rejects_non_numeric() {
574        let m = SumMonoid;
575        let mut s = m.identity();
576        s = m.accumulate(s, &Value::Float(1.5)).unwrap();
577        s = m.accumulate(s, &Value::Int(2)).unwrap();
578        assert_eq!(m.finalize(s).unwrap(), Value::Float(3.5));
579
580        let error = m
581            .accumulate(m.identity(), &Value::Str("nope".into()))
582            .unwrap_err();
583        assert!(error.to_string().contains("requires a numeric value"));
584    }
585
586    #[test]
587    fn avg_monoid_divides_by_count() {
588        let m = AvgMonoid;
589        let mut s = m.identity();
590        for v in [Value::Int(2), Value::Int(4), Value::Int(6)] {
591            s = m.accumulate(s, &v).unwrap();
592        }
593        assert_eq!(m.finalize(s).unwrap(), Value::Float(4.0));
594    }
595
596    #[test]
597    fn min_max_track_extremes() {
598        let mn = MinMonoid;
599        let mx = MaxMonoid;
600        let mut a = mn.identity();
601        let mut b = mx.identity();
602        for v in [Value::Float(3.0), Value::Float(1.0), Value::Float(2.0)] {
603            a = mn.accumulate(a, &v).unwrap();
604            b = mx.accumulate(b, &v).unwrap();
605        }
606        assert_eq!(mn.finalize(a).unwrap(), Value::Float(1.0));
607        assert_eq!(mx.finalize(b).unwrap(), Value::Float(3.0));
608    }
609
610    #[test]
611    fn quantile_median_interpolates() {
612        let q = QuantileMonoid::new(0.5).unwrap();
613        let mut s = q.identity();
614        for v in [
615            Value::Float(1.0),
616            Value::Float(2.0),
617            Value::Float(3.0),
618            Value::Float(4.0),
619        ] {
620            s = q.accumulate(s, &v).unwrap();
621        }
622        // median of [1,2,3,4] interpolates between idx 1 and 2 = (2+3)/2 = 2.5
623        assert_eq!(q.finalize(s).unwrap(), Value::Float(2.5));
624    }
625
626    #[test]
627    fn quantile_constructor_rejects_invalid_values() {
628        for quantile in [f64::NAN, -0.1, 1.1] {
629            let error = QuantileMonoid::new(quantile).unwrap_err();
630            assert!(error.to_string().contains("finite and in [0, 1]"));
631        }
632    }
633
634    #[test]
635    fn aggregation_state_mismatches_are_errors() {
636        let count = CountMonoid;
637        assert!(count
638            .accumulate(AggState::Sum(0.0), &Value::Int(1))
639            .unwrap_err()
640            .to_string()
641            .contains("expected Count state"));
642        assert!(count
643            .combine(AggState::Count(1), AggState::Sum(2.0))
644            .unwrap_err()
645            .to_string()
646            .contains("expected Count states"));
647        assert!(count
648            .finalize(AggState::Values(Vec::new()))
649            .unwrap_err()
650            .to_string()
651            .contains("expected Count state"));
652    }
653
654    #[test]
655    fn aggregation_counters_and_value_widths_are_checked() {
656        let count = CountMonoid;
657        assert!(count
658            .accumulate(AggState::Count(u64::MAX), &Value::Null)
659            .unwrap_err()
660            .to_string()
661            .contains("overflowed u64"));
662        assert!(count
663            .combine(AggState::Count(u64::MAX), AggState::Count(1))
664            .unwrap_err()
665            .to_string()
666            .contains("overflowed u64"));
667        assert!(count
668            .finalize(AggState::Count(i64::MAX as u64 + 1))
669            .unwrap_err()
670            .to_string()
671            .contains("Value::Int range"));
672
673        let avg = AvgMonoid;
674        assert!(avg
675            .accumulate(
676                AggState::Avg(AvgState {
677                    sum: 1.0,
678                    count: u64::MAX,
679                }),
680                &Value::Int(1),
681            )
682            .unwrap_err()
683            .to_string()
684            .contains("count overflowed"));
685    }
686
687    #[test]
688    fn numeric_aggregations_reject_invalid_types_and_non_finite_values() {
689        for result in [
690            AvgMonoid.accumulate(AvgMonoid.identity(), &Value::Str("bad".into())),
691            MinMonoid.accumulate(MinMonoid.identity(), &Value::Float(f64::NAN)),
692            MaxMonoid.accumulate(MaxMonoid.identity(), &Value::Float(f64::INFINITY)),
693        ] {
694            assert!(result.is_err());
695        }
696        assert_eq!(
697            MinMonoid.finalize(MinMonoid.identity()).unwrap(),
698            Value::Null
699        );
700        assert_eq!(
701            MaxMonoid.finalize(MaxMonoid.identity()).unwrap(),
702            Value::Null
703        );
704        let quantile = QuantileMonoid::new(0.5).unwrap();
705        assert_eq!(quantile.finalize(quantile.identity()).unwrap(), Value::Null);
706    }
707}