Skip to main content

mz_compute/render/
reduce.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Reduction dataflow construction.
11//!
12//! Consult [ReducePlan] documentation for details.
13
14use std::collections::BTreeMap;
15
16use columnation::{Columnation, CopyRegion};
17use dec::OrderedDecimal;
18use differential_dataflow::Diff as _;
19use differential_dataflow::collection::AsCollection;
20use differential_dataflow::consolidation::ConsolidatingContainerBuilder;
21use differential_dataflow::difference::{IsZero, Multiply, Semigroup};
22use differential_dataflow::hashable::Hashable;
23use differential_dataflow::operators::arrange::{Arranged, TraceAgent};
24use differential_dataflow::trace::implementations::BatchContainer;
25use differential_dataflow::trace::{Builder, Trace};
26use differential_dataflow::{Data, VecCollection};
27use itertools::Itertools;
28use mz_compute_types::dyncfgs::{ENABLE_COMPUTE_TEMPORAL_BUCKETING, TEMPORAL_BUCKETING_SUMMARY};
29use mz_compute_types::plan::ArrangementStrategy;
30use mz_compute_types::plan::reduce::{
31    AccumulablePlan, BasicPlan, BucketedPlan, HierarchicalPlan, KeyValPlan, LirAggregateExpr,
32    MonotonicPlan, ReducePlan, ReductionType, SingleBasicPlan, reduction_type,
33};
34use mz_compute_types::plan::scalar::LirScalarExpr;
35use mz_expr::{AggregateFunc, EvalError, SafeMfpPlan};
36use mz_ore::cast::CastLossy;
37use mz_repr::adt::numeric::{self, Numeric, NumericAgg};
38use mz_repr::fixed_length::ExtendDatums;
39use mz_repr::{Datum, DatumVec, Diff, Row, RowArena, SharedRow};
40use mz_timely_util::columnation::ColumnationChunker;
41use mz_timely_util::operator::CollectionExt;
42use num_traits::Float;
43use serde::{Deserialize, Serialize};
44use timely::Container;
45use timely::container::{CapacityContainerBuilder, PushInto};
46use tracing::warn;
47
48use crate::extensions::arrange::{ArrangementSize, KeyCollection, MzArrange};
49use crate::extensions::reduce::{ClearContainer, MzReduce};
50use crate::render::context::{CollectionBundle, Context};
51use crate::render::errors::DataflowErrorSer;
52use crate::render::errors::MaybeValidatingRow;
53use crate::render::reduce::monoids::{ReductionMonoid, get_monoid};
54use crate::render::{ArrangementFlavor, Pairer, RenderTimestamp};
55use crate::typedefs::{
56    ErrBatcher, ErrBuilder, KeyBatcher, RowErrBuilder, RowErrSpine, RowRowAgent, RowRowArrangement,
57    RowRowSpine, RowSpine, RowValSpine,
58};
59use mz_row_spine::{
60    DatumSeq, RowBatcher, RowBuilder, RowRowBatcher, RowRowBuilder, RowValBatcher, RowValBuilder,
61};
62
63impl<'scope, T: RenderTimestamp> Context<'scope, T> {
64    /// Renders a `MirRelationExpr::Reduce` using various non-obvious techniques to
65    /// minimize worst-case incremental update times and memory footprint.
66    pub fn render_reduce(
67        &self,
68        input_key: Option<Vec<LirScalarExpr>>,
69        input: CollectionBundle<'scope, T>,
70        key_val_plan: KeyValPlan,
71        reduce_plan: ReducePlan,
72        mfp_after: Option<SafeMfpPlan<LirScalarExpr>>,
73        temporal_bucketing_strategy: ArrangementStrategy,
74    ) -> CollectionBundle<'scope, T>
75    where
76        T: crate::render::MaybeBucketByTime,
77    {
78        input.scope().region_named("Reduce", |inner| {
79            let KeyValPlan {
80                mut key_plan,
81                mut val_plan,
82            } = key_val_plan;
83            let key_arity = key_plan.projection.len();
84            let mut datums = DatumVec::new();
85
86            // Determine the columns we'll need from the row.
87            let mut demand = Vec::new();
88            demand.extend(key_plan.demand());
89            demand.extend(val_plan.demand());
90            demand.sort();
91            demand.dedup();
92
93            // remap column references to the subset we use.
94            let mut demand_map = BTreeMap::new();
95            for column in demand.iter() {
96                demand_map.insert(*column, demand_map.len());
97            }
98            let demand_map_len = demand_map.len();
99            key_plan.permute_fn(|c| demand_map[&c], demand_map_len);
100            val_plan.permute_fn(|c| demand_map[&c], demand_map_len);
101            let max_demand = demand.iter().max().map(|x| *x + 1).unwrap_or(0);
102            let skips = mz_compute_types::plan::reduce::convert_indexes_to_skips(demand);
103
104            let (key_val_input, err) = input
105                .enter_region(inner)
106                .flat_map::<_, ConsolidatingContainerBuilder<Vec<((Row, Row), T, Diff)>>, _>(
107                    input_key.map(|k| (k, None)),
108                    max_demand,
109                    move |row_datums, time, diff, ok_session, err_session| {
110                        let mut row_builder = SharedRow::get();
111                        let temp_storage = RowArena::new();
112
113                        let mut row_iter = row_datums.drain(..);
114                        let mut datums_local = datums.borrow();
115                        // Unpack only the demanded columns.
116                        for skip in skips.iter() {
117                            datums_local.push(row_iter.nth(*skip).unwrap());
118                        }
119
120                        // Evaluate the key expressions.
121                        let key = key_plan.evaluate_into(
122                            &mut datums_local,
123                            &temp_storage,
124                            &mut row_builder,
125                        );
126                        let key = match key {
127                            Err(e) => {
128                                err_session.give((e.into(), time, diff));
129                                return 1;
130                            }
131                            Ok(Some(key)) => key.clone(),
132                            Ok(None) => panic!("Row expected as no predicate was used"),
133                        };
134
135                        // Evaluate the value expressions.
136                        // The prior evaluation may have left additional columns we should delete.
137                        datums_local.truncate(skips.len());
138                        let val = val_plan.evaluate_into(
139                            &mut datums_local,
140                            &temp_storage,
141                            &mut row_builder,
142                        );
143                        let val = match val {
144                            Err(e) => {
145                                err_session.give((e.into(), time, diff));
146                                return 1;
147                            }
148                            Ok(Some(val)) => val.clone(),
149                            Ok(None) => panic!("Row expected as no predicate was used"),
150                        };
151
152                        ok_session.give(((key, val), time, diff));
153                        1
154                    },
155                );
156
157            // Bucket the keyed `(key, val)` stream when lowering chose `TemporalBucketing`.
158            // `Reduce` builds its own arrangement via `KeyValPlan`, bypassing
159            // `ensure_collections`, so the strategy is plumbed through `PlanNode::Reduce`
160            // rather than inferred at the arrangement site. No-op for `Direct`.
161            let key_val_collection = key_val_input.as_collection();
162            let key_val_collection = if matches!(
163                temporal_bucketing_strategy,
164                ArrangementStrategy::TemporalBucketing
165            ) && ENABLE_COMPUTE_TEMPORAL_BUCKETING.get(&self.config_set)
166            {
167                let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
168                    .get(&self.config_set)
169                    .try_into()
170                    .expect("must fit");
171                T::maybe_apply_temporal_bucketing(
172                    key_val_collection.inner,
173                    self.as_of_frontier.clone(),
174                    summary,
175                )
176            } else {
177                key_val_collection
178            };
179
180            // Render the reduce plan
181            self.render_reduce_plan(reduce_plan, key_val_collection, err, key_arity, mfp_after)
182                .leave_region(self.scope)
183        })
184    }
185
186    /// Render a dataflow based on the provided plan.
187    ///
188    /// The output will be an arrangements that looks the same as if
189    /// we just had a single reduce operator computing everything together, and
190    /// this arrangement can also be re-used.
191    fn render_reduce_plan<'s>(
192        &self,
193        plan: ReducePlan,
194        collection: VecCollection<'s, T, (Row, Row), Diff>,
195        err_input: VecCollection<'s, T, DataflowErrorSer, Diff>,
196        key_arity: usize,
197        mfp_after: Option<SafeMfpPlan<LirScalarExpr>>,
198    ) -> CollectionBundle<'s, T> {
199        let mut errors = Default::default();
200        let arrangement =
201            self.render_reduce_plan_inner(plan, collection, &mut errors, key_arity, mfp_after);
202        let errs: KeyCollection<_, _, _> = err_input.concatenate(errors).into();
203        CollectionBundle::from_columns(
204            0..key_arity,
205            ArrangementFlavor::Local(
206                arrangement,
207                errs.mz_arrange::<ColumnationChunker<_>, ErrBatcher<_, _>, ErrBuilder<_, _>, _>(
208                    "Arrange bundle err",
209                ),
210            ),
211        )
212    }
213
214    fn render_reduce_plan_inner<'s>(
215        &self,
216        plan: ReducePlan,
217        collection: VecCollection<'s, T, (Row, Row), Diff>,
218        errors: &mut Vec<VecCollection<'s, T, DataflowErrorSer, Diff>>,
219        key_arity: usize,
220        mfp_after: Option<SafeMfpPlan<LirScalarExpr>>,
221    ) -> Arranged<'s, RowRowAgent<T, Diff>> {
222        // TODO(vmarcos): Arrangement specialization here could eventually be extended to keys,
223        // not only values (database-issues#6658).
224        let arrangement = match plan {
225            // If we have no aggregations or just a single type of reduction, we
226            // can go ahead and render them directly.
227            ReducePlan::Distinct => {
228                let (arranged_output, errs) = self.build_distinct(collection, mfp_after);
229                errors.push(errs);
230                arranged_output
231            }
232            ReducePlan::Accumulable(expr) => {
233                let (arranged_output, errs) =
234                    self.build_accumulable(collection, expr, key_arity, mfp_after);
235                errors.push(errs);
236                arranged_output
237            }
238            ReducePlan::Hierarchical(HierarchicalPlan::Monotonic(expr)) => {
239                let (output, errs) = self.build_monotonic(collection, expr, mfp_after);
240                errors.push(errs);
241                output
242            }
243            ReducePlan::Hierarchical(HierarchicalPlan::Bucketed(expr)) => {
244                let (output, errs) = self.build_bucketed(collection, expr, key_arity, mfp_after);
245                errors.push(errs);
246                output
247            }
248            ReducePlan::Basic(BasicPlan::Single(SingleBasicPlan {
249                expr,
250                fused_unnest_list,
251            })) => {
252                // Note that we skip validating for negative diffs when we have a fused unnest list,
253                // because this is already a CPU-intensive situation due to the non-incrementalness
254                // of window functions.
255                let validating = !fused_unnest_list;
256                let (output, errs) = self.build_basic_aggregate(
257                    collection,
258                    0,
259                    &expr,
260                    validating,
261                    key_arity,
262                    mfp_after,
263                    fused_unnest_list,
264                );
265                if validating {
266                    errors.push(errs.expect("validation should have occurred as it was requested"));
267                }
268                output
269            }
270            ReducePlan::Basic(BasicPlan::Multiple(aggrs)) => {
271                let (output, errs) =
272                    self.build_basic_aggregates(collection, aggrs, key_arity, mfp_after);
273                errors.push(errs);
274                output
275            }
276        };
277        arrangement
278    }
279
280    /// Build the dataflow to compute the set of distinct keys.
281    fn build_distinct<'s>(
282        &self,
283        collection: VecCollection<'s, T, (Row, Row), Diff>,
284        mfp_after: Option<SafeMfpPlan<LirScalarExpr>>,
285    ) -> (
286        Arranged<'s, TraceAgent<RowRowSpine<T, Diff>>>,
287        VecCollection<'s, T, DataflowErrorSer, Diff>,
288    ) {
289        let error_logger = self.error_logger();
290
291        // Allocations for the two closures.
292        let mut datums1 = DatumVec::new();
293        let mut datums2 = DatumVec::new();
294        let mfp_after1 = mfp_after.clone();
295        let mfp_after2 = mfp_after.filter(|mfp| mfp.could_error());
296
297        let arranged = collection
298            .mz_arrange::<
299                ColumnationChunker<_>,
300                RowRowBatcher<_, _>,
301                RowRowBuilder<_, _>,
302                RowRowSpine<_, _>,
303            >(
304                "Arranged DistinctBy",
305            );
306        let output = arranged
307            .clone()
308            .mz_reduce_abelian::<_, RowRowBuilder<_, _>, RowRowSpine<_, _>>(
309                "DistinctBy",
310                move |key, _input, output| {
311                    let temp_storage = RowArena::new();
312                    let mut datums_local = datums1.borrow();
313                    key.extend_datums(&temp_storage, &mut datums_local, None);
314
315                    // Note that the key contains all the columns in a `Distinct` and that `mfp_after` is
316                    // required to preserve the key. Therefore, if `mfp_after` maps, then it must project
317                    // back to the key. As a consequence, we can treat `mfp_after` as a filter here.
318                    if mfp_after1
319                        .as_ref()
320                        .map(|mfp| mfp.evaluate_inner(&mut datums_local, &temp_storage))
321                        .unwrap_or(Ok(true))
322                        == Ok(true)
323                    {
324                        // We're pushing a unit value here because the key is implicitly added by the
325                        // arrangement, and the permutation logic takes care of using the key part of the
326                        // output.
327                        output.push((Row::default(), Diff::ONE));
328                    }
329                },
330            );
331        let errors = arranged.mz_reduce_abelian::<_, RowErrBuilder<_, _>, RowErrSpine<_, _>>(
332            "DistinctByErrorCheck",
333            move |key, input: &[(_, Diff)], output: &mut Vec<(DataflowErrorSer, _)>| {
334                for (_, count) in input.iter() {
335                    if count.is_positive() {
336                        continue;
337                    }
338                    let message = "Non-positive multiplicity in DistinctBy";
339                    error_logger.log(message, &format!("row={key:?}, count={count}"));
340                    output.push((EvalError::Internal(message.into()).into(), Diff::ONE));
341                    return;
342                }
343                // If `mfp_after` can error, then evaluate it here.
344                let Some(mfp) = &mfp_after2 else { return };
345                let temp_storage = RowArena::new();
346                let mut datums_local = datums2.borrow();
347                key.extend_datums(&temp_storage, &mut datums_local, None);
348
349                if let Err(e) = mfp.evaluate_inner(&mut datums_local, &temp_storage) {
350                    output.push((e.into(), Diff::ONE));
351                }
352            },
353        );
354        (output, errors.as_collection(|_k, v| v.clone()))
355    }
356
357    /// Build the dataflow to compute and arrange multiple non-accumulable,
358    /// non-hierarchical aggregations on `input`.
359    ///
360    /// This function assumes that we are explicitly rendering multiple basic aggregations.
361    /// For each aggregate, we render a different reduce operator, and then fuse
362    /// results together into a final arrangement that presents all the results
363    /// in the order specified by `aggrs`.
364    fn build_basic_aggregates<'s>(
365        &self,
366        input: VecCollection<'s, T, (Row, Row), Diff>,
367        aggrs: Vec<LirAggregateExpr>,
368        key_arity: usize,
369        mfp_after: Option<SafeMfpPlan<LirScalarExpr>>,
370    ) -> (
371        RowRowArrangement<'s, T>,
372        VecCollection<'s, T, DataflowErrorSer, Diff>,
373    ) {
374        // We are only using this function to render multiple basic aggregates and
375        // stitch them together. If that's not true we should complain.
376        if aggrs.len() <= 1 {
377            self.error_logger().soft_panic_or_log(
378                "Too few aggregations when building basic aggregates",
379                &format!("len={}", aggrs.len()),
380            )
381        }
382        let mut err_output = None;
383        let mut to_collect = Vec::new();
384        for (index, aggr) in aggrs.into_iter().enumerate() {
385            let (result, errs) = self.build_basic_aggregate(
386                input.clone(),
387                index,
388                &aggr,
389                err_output.is_none(),
390                key_arity,
391                None,
392                false,
393            );
394            if errs.is_some() {
395                err_output = errs
396            }
397            to_collect
398                .push(result.as_collection(move |key, val| (key.to_row(), (index, val.to_row()))));
399        }
400
401        // Allocations for the two closures.
402        let mut datums1 = DatumVec::new();
403        let mut datums2 = DatumVec::new();
404        let mfp_after1 = mfp_after.clone();
405        let mfp_after2 = mfp_after.filter(|mfp| mfp.could_error());
406
407        let arranged = differential_dataflow::collection::concatenate(input.scope(), to_collect)
408            .mz_arrange::<
409                ColumnationChunker<_>,
410                RowValBatcher<_, _, _>,
411                RowValBuilder<_, _, _>,
412                RowValSpine<_, _, _>,
413            >(
414            "Arranged ReduceFuseBasic input",
415        );
416
417        let output = arranged
418            .clone()
419            .mz_reduce_abelian::<_, RowRowBuilder<_, _>, RowRowSpine<_, _>>("ReduceFuseBasic", {
420                move |key, input, output| {
421                    let temp_storage = RowArena::new();
422                    let mut datums_local = datums1.borrow();
423                    key.extend_datums(&temp_storage, &mut datums_local, None);
424                    let key_len = datums_local.len();
425
426                    for ((_, row), _) in input.iter() {
427                        datums_local.push(row.unpack_first());
428                    }
429
430                    if let Some(row) =
431                        evaluate_mfp_after(&mfp_after1, &mut datums_local, &temp_storage, key_len)
432                    {
433                        output.push((row, Diff::ONE));
434                    }
435                }
436            });
437        // If `mfp_after` can error, then we need to render a paired reduction
438        // to scan for these potential errors. Note that we cannot directly use
439        // `mz_timely_util::reduce::ReduceExt::reduce_pair` here because we only
440        // conditionally render the second component of the reduction pair.
441        let validation_errs = err_output.expect("expected to validate in at least one aggregate");
442        if let Some(mfp) = mfp_after2 {
443            let mfp_errs = arranged
444                .mz_reduce_abelian::<_, RowErrBuilder<_, _>, RowErrSpine<_, _>>(
445                    "ReduceFuseBasic Error Check",
446                    move |key, input, output| {
447                        // Since negative accumulations are checked in at least one component
448                        // aggregate, we only need to look for MFP errors here.
449                        let temp_storage = RowArena::new();
450                        let mut datums_local = datums2.borrow();
451                        key.extend_datums(&temp_storage, &mut datums_local, None);
452
453                        for ((_, row), _) in input.iter() {
454                            datums_local.push(row.unpack_first());
455                        }
456
457                        if let Err(e) = mfp.evaluate_inner(&mut datums_local, &temp_storage) {
458                            output.push((e.into(), Diff::ONE));
459                        }
460                    },
461                )
462                .as_collection(|_, v| v.clone());
463            (output, validation_errs.concat(mfp_errs))
464        } else {
465            (output, validation_errs)
466        }
467    }
468
469    /// Build the dataflow to compute a single basic aggregation.
470    ///
471    /// This method also applies distinctness if required.
472    fn build_basic_aggregate<'s>(
473        &self,
474        input: VecCollection<'s, T, (Row, Row), Diff>,
475        index: usize,
476        aggr: &LirAggregateExpr,
477        validating: bool,
478        key_arity: usize,
479        mfp_after: Option<SafeMfpPlan<LirScalarExpr>>,
480        fused_unnest_list: bool,
481    ) -> (
482        RowRowArrangement<'s, T>,
483        Option<VecCollection<'s, T, DataflowErrorSer, Diff>>,
484    ) {
485        let LirAggregateExpr {
486            func,
487            expr: _,
488            distinct,
489        } = aggr.clone();
490
491        // Extract the value we were asked to aggregate over.
492        let mut partial = input.map(move |(key, row)| {
493            let mut row_builder = SharedRow::get();
494            let value = row.iter().nth(index).unwrap();
495            row_builder.packer().push(value);
496            (key, row_builder.clone())
497        });
498
499        let mut err_output = None;
500
501        // If `distinct` is set, we restrict ourselves to the distinct `(key, val)`.
502        if distinct {
503            // We map `(Row, Row)` to `Row` to take advantage of `Row*Spine` types.
504            let pairer = Pairer::new(key_arity);
505            let keyed = partial.map(move |(key, val)| pairer.merge(&key, &val));
506            if validating {
507                let (oks, errs) = self
508                    .build_reduce_inaccumulable_distinct::<
509                        RowValBuilder<Result<(), String>, _, _>,
510                        RowValSpine<Result<(), String>, _, _>,
511                    >(keyed, None)
512                    .as_collection(|k, v| {
513                        (
514                            k.to_row(),
515                            v.as_ref()
516                                .map(|&()| ())
517                                .map_err(|m| m.as_str().into()),
518                        )
519                    })
520                    .map_fallible::<
521                        CapacityContainerBuilder<_>,
522                        CapacityContainerBuilder<_>,
523                        _,
524                        _,
525                        _,
526                    >(
527                        "Demux Errors",
528                        move |(key_val, result)| match result {
529                            Ok(()) => Ok(pairer.split(&key_val)),
530                            Err(m) => {
531                                Err(EvalError::Internal(m).into())
532                            }
533                        },
534                    );
535                err_output = Some(errs);
536                partial = oks;
537            } else {
538                partial = self
539                    .build_reduce_inaccumulable_distinct::<RowBuilder<_, _>, RowSpine<_, _>>(
540                        keyed,
541                        Some(" [val: empty]"),
542                    )
543                    .as_collection(move |key_val_iter, _| pairer.split(key_val_iter));
544            }
545        }
546
547        // Allocations for the two closures.
548        let mut datums1 = DatumVec::new();
549        let mut datums2 = DatumVec::new();
550        let mut datums_key_1 = DatumVec::new();
551        let mut datums_key_2 = DatumVec::new();
552        // Scratch buffers for decoding each input value's (single) datum into the
553        // arena, so the aggregates iterate arena-resident datums rather than the
554        // packed value bytes — a prerequisite for compressed value representations.
555        let mut vals1 = DatumVec::new();
556        let mut vals2 = DatumVec::new();
557        let mut vals_key_1 = DatumVec::new();
558        let mut vals_key_2 = DatumVec::new();
559        let mfp_after1 = mfp_after.clone();
560        let func2 = func.clone();
561
562        let name = if !fused_unnest_list {
563            "ReduceInaccumulable"
564        } else {
565            "FusedReduceUnnestList"
566        };
567        let arranged = partial
568            .mz_arrange::<
569                ColumnationChunker<_>,
570                RowRowBatcher<_, _>,
571                RowRowBuilder<_, _>,
572                RowRowSpine<_, _>,
573            >(&format!(
574                "Arranged {name}"
575            ));
576        let oks = if !fused_unnest_list {
577            arranged
578                .clone()
579                .mz_reduce_abelian::<_, RowRowBuilder<_, _>, RowRowSpine<_, _>>(name, {
580                    move |key, source, target| {
581                        let temp_storage = RowArena::new();
582                        // Decode each input value's single datum into the arena, reusing one
583                        // scratch buffer; the datum is `Copy` and is copied out before the
584                        // buffer is overwritten on the next row. We pass the multiplicity
585                        // through (unlike in hierarchical aggregation) because we don't know
586                        // that the aggregation method is not sensitive to the number of
587                        // records. The aggregate decides how to consume it.
588                        let mut val_scratch = vals1.borrow();
589                        let iter = source.iter().map(|(v, w)| {
590                            val_scratch.clear();
591                            v.extend_datums(&temp_storage, &mut val_scratch, Some(1));
592                            (val_scratch[0], *w)
593                        });
594
595                        let mut datums_local = datums1.borrow();
596                        key.extend_datums(&temp_storage, &mut datums_local, None);
597                        let key_len = datums_local.len();
598                        datums_local.push(
599                        // Note that this is not necessarily a window aggregation, in which case
600                        // `eval_with_fast_window_agg` delegates to the normal `eval`.
601                        func.eval_with_fast_window_agg::<_, window_agg_helpers::OneByOneAggrImpls>(
602                            iter,
603                            &temp_storage,
604                        ),
605                    );
606
607                        if let Some(row) = evaluate_mfp_after(
608                            &mfp_after1,
609                            &mut datums_local,
610                            &temp_storage,
611                            key_len,
612                        ) {
613                            target.push((row, Diff::ONE));
614                        }
615                    }
616                })
617        } else {
618            arranged
619                .clone()
620                .mz_reduce_abelian::<_, RowRowBuilder<_, _>, RowRowSpine<_, _>>(name, {
621                    move |key, source, target| {
622                        // This part is the same as in the `!fused_unnest_list` if branch above.
623                        let temp_storage = RowArena::new();
624                        let mut val_scratch = vals_key_1.borrow();
625                        let iter = source.iter().map(|(v, w)| {
626                            val_scratch.clear();
627                            v.extend_datums(&temp_storage, &mut val_scratch, Some(1));
628                            (val_scratch[0], *w)
629                        });
630
631                        // This is the part that is specific to the `fused_unnest_list` branch.
632                        let mut datums_local = datums_key_1.borrow();
633                        key.extend_datums(&temp_storage, &mut datums_local, None);
634                        let key_len = datums_local.len();
635                        for datum in func
636                            .eval_with_unnest_list::<_, window_agg_helpers::OneByOneAggrImpls>(
637                                iter,
638                                &temp_storage,
639                            )
640                        {
641                            datums_local.truncate(key_len);
642                            datums_local.push(datum);
643                            if let Some(row) = evaluate_mfp_after(
644                                &mfp_after1,
645                                &mut datums_local,
646                                &temp_storage,
647                                key_len,
648                            ) {
649                                target.push((row, Diff::ONE));
650                            }
651                        }
652                    }
653                })
654        };
655
656        // Note that we would prefer to use `mz_timely_util::reduce::ReduceExt::reduce_pair` here, but
657        // we then wouldn't be able to do this error check conditionally.  See its documentation for the
658        // rationale around using a second reduction here.
659        let must_validate = validating && err_output.is_none();
660        let mfp_after2 = mfp_after.filter(|mfp| mfp.could_error());
661        if must_validate || mfp_after2.is_some() {
662            let error_logger = self.error_logger();
663
664            let errs = if !fused_unnest_list {
665                arranged
666                    .mz_reduce_abelian::<_, RowErrBuilder<_, _>, RowErrSpine<_, _>>(
667                        &format!("{name} Error Check"),
668                        move |key, source, target| {
669                            // Negative counts would be surprising, but until we are 100% certain we won't
670                            // see them, we should report when we do. We may want to bake even more info
671                            // in here in the future.
672                            if must_validate {
673                                for (value, count) in source.iter() {
674                                    if count.is_positive() {
675                                        continue;
676                                    }
677                                    let value = value.to_row();
678                                    let message =
679                                        "Non-positive accumulation in ReduceInaccumulable";
680                                    error_logger
681                                        .log(message, &format!("value={value:?}, count={count}"));
682                                    let err = EvalError::Internal(message.into());
683                                    target.push((err.into(), Diff::ONE));
684                                    return;
685                                }
686                            }
687
688                            // We know that `mfp_after` can error if it exists, so try to evaluate it here.
689                            let Some(mfp) = &mfp_after2 else { return };
690                            let temp_storage = RowArena::new();
691                            let mut val_scratch = vals2.borrow();
692                            let iter = source.iter().map(|(v, w)| {
693                                val_scratch.clear();
694                                v.extend_datums(&temp_storage, &mut val_scratch, Some(1));
695                                (val_scratch[0], *w)
696                            });
697
698                            let mut datums_local = datums2.borrow();
699                            key.extend_datums(&temp_storage, &mut datums_local, None);
700                            datums_local.push(
701                                func2.eval_with_fast_window_agg::<
702                                    _,
703                                    window_agg_helpers::OneByOneAggrImpls,
704                                >(
705                                    iter, &temp_storage
706                                ),
707                            );
708                            if let Err(e) = mfp.evaluate_inner(&mut datums_local, &temp_storage) {
709                                target.push((e.into(), Diff::ONE));
710                            }
711                        },
712                    )
713                    .as_collection(|_, v| v.clone())
714            } else {
715                // `render_reduce_plan_inner` doesn't request validation when `fused_unnest_list`.
716                assert!(!must_validate);
717                // We couldn't have got into this if branch due to `must_validate`, so it must be
718                // because of the `mfp_after2.is_some()`.
719                let Some(mfp) = mfp_after2 else {
720                    unreachable!()
721                };
722                arranged
723                    .mz_reduce_abelian::<_, RowErrBuilder<_, _>, RowErrSpine<_, _>>(
724                        &format!("{name} Error Check"),
725                        move |key, source, target| {
726                            let temp_storage = RowArena::new();
727                            let mut val_scratch = vals_key_2.borrow();
728                            let iter = source.iter().map(|(v, w)| {
729                                val_scratch.clear();
730                                v.extend_datums(&temp_storage, &mut val_scratch, Some(1));
731                                (val_scratch[0], *w)
732                            });
733
734                            let mut datums_local = datums_key_2.borrow();
735                            key.extend_datums(&temp_storage, &mut datums_local, None);
736                            let key_len = datums_local.len();
737                            for datum in func2
738                                .eval_with_unnest_list::<_, window_agg_helpers::OneByOneAggrImpls>(
739                                    iter,
740                                    &temp_storage,
741                                )
742                            {
743                                datums_local.truncate(key_len);
744                                datums_local.push(datum);
745                                // We know that `mfp` can error (because of the `could_error` call
746                                // above), so try to evaluate it here.
747                                if let Err(e) = mfp.evaluate_inner(&mut datums_local, &temp_storage)
748                                {
749                                    target.push((e.into(), Diff::ONE));
750                                }
751                            }
752                        },
753                    )
754                    .as_collection(|_, v| v.clone())
755            };
756
757            if let Some(e) = err_output {
758                err_output = Some(e.concat(errs));
759            } else {
760                err_output = Some(errs);
761            }
762        }
763        (oks, err_output)
764    }
765
766    fn build_reduce_inaccumulable_distinct<'s, Bu, Tr>(
767        &self,
768        input: VecCollection<'s, T, Row, Diff>,
769        name_tag: Option<&str>,
770    ) -> Arranged<'s, TraceAgent<Tr>>
771    where
772        Tr: for<'a> Trace<
773                Key<'a> = DatumSeq<'a>,
774                KeyContainer: BatchContainer<Owned = Row>,
775                Time = T,
776                Diff = Diff,
777                ValOwn: Data + MaybeValidatingRow<(), String>,
778            > + 'static,
779        Bu: Builder<
780                Time = T,
781                Input: Container
782                           + ClearContainer
783                           + PushInto<((Row, Tr::ValOwn), Tr::Time, Tr::Diff)>,
784                Output = Tr::Batch,
785            >,
786        Arranged<'s, TraceAgent<Tr>>: ArrangementSize,
787    {
788        let error_logger = self.error_logger();
789
790        let output_name = format!(
791            "ReduceInaccumulable Distinct{}",
792            name_tag.unwrap_or_default()
793        );
794
795        let input: KeyCollection<_, _, _> = input.into();
796        input
797            .mz_arrange::<
798                ColumnationChunker<_>,
799                RowBatcher<_, _>,
800                RowBuilder<_, _>,
801                RowSpine<_, _>,
802            >(
803                "Arranged ReduceInaccumulable Distinct [val: empty]",
804            )
805            .mz_reduce_abelian::<_, Bu, Tr>(&output_name, move |_, source, t| {
806                if let Some(err) = Tr::ValOwn::into_error() {
807                    for (value, count) in source.iter() {
808                        if count.is_positive() {
809                            continue;
810                        }
811
812                        let message = "Non-positive accumulation in ReduceInaccumulable DISTINCT";
813                        error_logger.log(message, &format!("value={value:?}, count={count}"));
814                        t.push((err(message.to_string()), Diff::ONE));
815                        return;
816                    }
817                }
818                t.push((Tr::ValOwn::ok(()), Diff::ONE))
819            })
820    }
821
822    /// Build the dataflow to compute and arrange multiple hierarchical aggregations
823    /// on non-monotonic inputs.
824    ///
825    /// This function renders a single reduction tree that computes aggregations with
826    /// a priority queue implemented with a series of reduce operators that partition
827    /// the input into buckets, and compute the aggregation over very small buckets
828    /// and feed the results up to larger buckets.
829    ///
830    /// Note that this implementation currently ignores the distinct bit because we
831    /// currently only perform min / max hierarchically and the reduction tree
832    /// efficiently suppresses non-distinct updates.
833    ///
834    /// `buckets` indicates the number of buckets in this stage. We do some non-obvious
835    /// trickery here to limit the memory usage per layer by internally
836    /// holding only the elements that were rejected by this stage. However, the
837    /// output collection maintains the `((key, bucket), (passing value)` for this
838    /// stage.
839    fn build_bucketed<'s>(
840        &self,
841        input: VecCollection<'s, T, (Row, Row), Diff>,
842        BucketedPlan {
843            aggr_funcs,
844            buckets,
845        }: BucketedPlan,
846        key_arity: usize,
847        mfp_after: Option<SafeMfpPlan<LirScalarExpr>>,
848    ) -> (
849        RowRowArrangement<'s, T>,
850        VecCollection<'s, T, DataflowErrorSer, Diff>,
851    ) {
852        let mut err_output: Option<VecCollection<'s, T, _, _>> = None;
853        let outer_scope = input.scope();
854        let arranged_output = outer_scope
855            .clone()
856            .region_named("ReduceHierarchical", |inner| {
857                let input = input.enter(inner);
858
859                // The first mod to apply to the hash.
860                let first_mod = buckets.get(0).copied().unwrap_or(1);
861                let aggregations = aggr_funcs.len();
862
863                // Gather the relevant keys with their hashes along with values ordered by aggregation_index.
864                let mut stage = input.map(move |(key, row)| {
865                    let mut row_builder = SharedRow::get();
866                    let mut row_packer = row_builder.packer();
867                    row_packer.extend(row.iter().take(aggregations));
868                    let values = row_builder.clone();
869
870                    // Apply the initial mod here.
871                    let hash = values.hashed() % first_mod;
872                    let hash_key =
873                        row_builder.pack_using(std::iter::once(Datum::from(hash)).chain(&key));
874                    (hash_key, values)
875                });
876
877                // Repeatedly apply hierarchical reduction with a progressively coarser key.
878                for (index, b) in buckets.into_iter().enumerate() {
879                    // Apply subsequent bucket mods for all but the first round.
880                    let input = if index == 0 {
881                        stage
882                    } else {
883                        stage.map(move |(hash_key, values)| {
884                            let mut hash_key_iter = hash_key.iter();
885                            let hash = hash_key_iter.next().unwrap().unwrap_uint64() % b;
886                            // TODO: Convert the `chain(hash_key_iter...)` into a memcpy.
887                            let hash_key = SharedRow::pack(
888                                std::iter::once(Datum::from(hash))
889                                    .chain(hash_key_iter.take(key_arity)),
890                            );
891                            (hash_key, values)
892                        })
893                    };
894
895                    // We only want the first stage to perform validation of whether invalid accumulations
896                    // were observed in the input. Subsequently, we will either produce an error in the error
897                    // stream or produce correct data in the output stream.
898                    let validating = err_output.is_none();
899
900                    let (oks, errs) = self.build_bucketed_stage(&aggr_funcs, input, validating);
901                    if let Some(errs) = errs {
902                        err_output = Some(errs.leave_region(outer_scope));
903                    }
904                    stage = oks
905                }
906
907                // Discard the hash from the key and return to the format of the input data.
908                let partial = stage.map(move |(hash_key, values)| {
909                    let mut hash_key_iter = hash_key.iter();
910                    let _hash = hash_key_iter.next();
911                    (SharedRow::pack(hash_key_iter.take(key_arity)), values)
912                });
913
914                // Allocations for the two closures.
915                let mut datums1 = DatumVec::new();
916                let mut datums2 = DatumVec::new();
917                // Scratch buffers for decoding the input values (one column per aggregate)
918                // into the arena, so the aggregates iterate arena-resident datums rather
919                // than the packed value bytes.
920                let mut vals1 = DatumVec::new();
921                let mut vals2 = DatumVec::new();
922                let mfp_after1 = mfp_after.clone();
923                let mfp_after2 = mfp_after.filter(|mfp| mfp.could_error());
924                let aggr_funcs2 = aggr_funcs.clone();
925
926                // Build a series of stages for the reduction
927                // Arrange the final result into (key, Row)
928                let error_logger = self.error_logger();
929                // NOTE(vmarcos): The input operator name below is used in the tuning advice built-in
930                // view mz_introspection.mz_expected_group_size_advice.
931                let arranged = partial
932                    .mz_arrange::<
933                        ColumnationChunker<_>,
934                        RowRowBatcher<_, _>,
935                        RowRowBuilder<_, _>,
936                        RowRowSpine<_, _>,
937                    >(
938                        "Arrange ReduceMinsMaxes",
939                    );
940                // Note that we would prefer to use `mz_timely_util::reduce::ReduceExt::reduce_pair` here,
941                // but we then wouldn't be able to do this error check conditionally.  See its documentation
942                // for the rationale around using a second reduction here.
943                let must_validate = err_output.is_none();
944                if must_validate || mfp_after2.is_some() {
945                    let errs = arranged
946                        .clone()
947                        .mz_reduce_abelian::<_, RowErrBuilder<_, _>, RowErrSpine<_, _>>(
948                            "ReduceMinsMaxes Error Check",
949                            move |key, source, target| {
950                                // Negative counts would be surprising, but until we are 100% certain we wont
951                                // see them, we should report when we do. We may want to bake even more info
952                                // in here in the future.
953                                if must_validate {
954                                    for (val, count) in source.iter() {
955                                        if count.is_positive() {
956                                            continue;
957                                        }
958                                        let val = val.to_row();
959                                        let message =
960                                            "Non-positive accumulation in ReduceMinsMaxes";
961                                        error_logger
962                                            .log(message, &format!("val={val:?}, count={count}"));
963                                        target.push((
964                                            EvalError::Internal(message.into()).into(),
965                                            Diff::ONE,
966                                        ));
967                                        return;
968                                    }
969                                }
970
971                                // We know that `mfp_after` can error if it exists, so try to evaluate it here.
972                                let Some(mfp) = &mfp_after2 else { return };
973                                let temp_storage = RowArena::new();
974                                let mut datums_local = datums2.borrow();
975                                key.extend_datums(&temp_storage, &mut datums_local, None);
976
977                                // Decode every value row's datums into the arena, one column
978                                // per aggregate, then iterate them column-major below. Min/max
979                                // hierarchical aggregates are multiplicity-insensitive, so each
980                                // row contributes once (`Diff::ONE`) regardless of `_cnt`.
981                                let arity = aggr_funcs2.len();
982                                let mut decoded = vals2.borrow();
983                                for (values, _cnt) in source.iter() {
984                                    values.extend_datums(&temp_storage, &mut decoded, None);
985                                }
986                                assert_eq!(decoded.len(), source.len() * arity);
987                                for (col, func) in aggr_funcs2.iter().enumerate() {
988                                    let column_iter = (0..source.len())
989                                        .map(|r| (decoded[r * arity + col], Diff::ONE));
990                                    datums_local.push(func.eval(column_iter, &temp_storage));
991                                }
992                                if let Result::Err(e) =
993                                    mfp.evaluate_inner(&mut datums_local, &temp_storage)
994                                {
995                                    target.push((e.into(), Diff::ONE));
996                                }
997                            },
998                        )
999                        .as_collection(|_, v| v.clone())
1000                        .leave_region(outer_scope);
1001                    if let Some(e) = err_output.take() {
1002                        err_output = Some(e.concat(errs));
1003                    } else {
1004                        err_output = Some(errs);
1005                    }
1006                }
1007                arranged
1008                    .mz_reduce_abelian::<_, RowRowBuilder<_, _>, RowRowSpine<_, _>>(
1009                        "ReduceMinsMaxes",
1010                        move |key, source, target| {
1011                            let temp_storage = RowArena::new();
1012                            let mut datums_local = datums1.borrow();
1013                            key.extend_datums(&temp_storage, &mut datums_local, None);
1014                            let key_len = datums_local.len();
1015
1016                            // Decode every value row's datums into the arena, one column
1017                            // per aggregate, then iterate them column-major below. Min/max
1018                            // hierarchical aggregates are multiplicity-insensitive, so each
1019                            // row contributes once (`Diff::ONE`) regardless of `_cnt`.
1020                            let arity = aggr_funcs.len();
1021                            let mut decoded = vals1.borrow();
1022                            for (values, _cnt) in source.iter() {
1023                                values.extend_datums(&temp_storage, &mut decoded, None);
1024                            }
1025                            assert_eq!(decoded.len(), source.len() * arity);
1026                            for (col, func) in aggr_funcs.iter().enumerate() {
1027                                let column_iter = (0..source.len())
1028                                    .map(|r| (decoded[r * arity + col], Diff::ONE));
1029                                datums_local.push(func.eval(column_iter, &temp_storage));
1030                            }
1031
1032                            if let Some(row) = evaluate_mfp_after(
1033                                &mfp_after1,
1034                                &mut datums_local,
1035                                &temp_storage,
1036                                key_len,
1037                            ) {
1038                                target.push((row, Diff::ONE));
1039                            }
1040                        },
1041                    )
1042                    .leave_region(outer_scope)
1043            });
1044        (
1045            arranged_output,
1046            err_output.expect("expected to validate in one level of the hierarchy"),
1047        )
1048    }
1049
1050    /// Build a bucketed stage fragment that wraps [`Self::build_bucketed_negated_output`], and
1051    /// adds validation if `validating` is true. It returns the consolidated inputs concatenated
1052    /// with the negation of what's produced by the reduction.
1053    /// `validating` indicates whether we want this stage to perform error detection
1054    /// for invalid accumulations. Once a stage is clean of such errors, subsequent
1055    /// stages can skip validation.
1056    fn build_bucketed_stage<'s>(
1057        &self,
1058        aggr_funcs: &Vec<AggregateFunc>,
1059        input: VecCollection<'s, T, (Row, Row), Diff>,
1060        validating: bool,
1061    ) -> (
1062        VecCollection<'s, T, (Row, Row), Diff>,
1063        Option<VecCollection<'s, T, DataflowErrorSer, Diff>>,
1064    ) {
1065        let (input, negated_output, errs) = if validating {
1066            let (input, reduced) = self
1067                .build_bucketed_negated_output::<
1068                    RowValBuilder<_, _, _>,
1069                    RowValSpine<Result<Row, Row>, _, _>,
1070                >(
1071                    input.clone(),
1072                    aggr_funcs.clone(),
1073                );
1074            let (oks, errs) = reduced
1075                .as_collection(|k, v| (k.to_row(), v.clone()))
1076                .map_fallible::<CapacityContainerBuilder<_>, CapacityContainerBuilder<_>, _, _, _>(
1077                "Checked Invalid Accumulations",
1078                |(hash_key, result)| match result {
1079                    Err(hash_key) => {
1080                        let mut hash_key_iter = hash_key.iter();
1081                        let _hash = hash_key_iter.next();
1082                        let key = SharedRow::pack(hash_key_iter);
1083                        let message = format!(
1084                            "Invalid data in source, saw non-positive accumulation \
1085                                         for key {key:?} in hierarchical mins-maxes aggregate"
1086                        );
1087                        Err(EvalError::Internal(message.into()).into())
1088                    }
1089                    Ok(values) => Ok((hash_key, values)),
1090                },
1091            );
1092            (input, oks, Some(errs))
1093        } else {
1094            let (input, reduced) = self
1095                .build_bucketed_negated_output::<RowRowBuilder<_, _>, RowRowSpine<_, _>>(
1096                    input,
1097                    aggr_funcs.clone(),
1098                );
1099            // TODO: Here is a good moment where we could apply the next `mod` calculation. Note
1100            // that we need to apply the mod on both input and oks.
1101            let oks = reduced.as_collection(|k, v| (k.to_row(), v.to_row()));
1102            (input, oks, None)
1103        };
1104
1105        let input = input.as_collection(|k, v| (k.to_row(), v.to_row()));
1106        let oks = negated_output.concat(input);
1107        (oks, errs)
1108    }
1109
1110    /// Build a dataflow fragment for one stage of a reduction tree for multiple hierarchical
1111    /// aggregates to arrange and reduce the inputs. Returns the arranged input and the reduction,
1112    /// with all diffs in the reduction's output negated.
1113    fn build_bucketed_negated_output<'s, Bu, Tr>(
1114        &self,
1115        input: VecCollection<'s, T, (Row, Row), Diff>,
1116        aggrs: Vec<AggregateFunc>,
1117    ) -> (
1118        Arranged<'s, TraceAgent<RowRowSpine<T, Diff>>>,
1119        Arranged<'s, TraceAgent<Tr>>,
1120    )
1121    where
1122        Tr: for<'a> Trace<
1123                Key<'a> = DatumSeq<'a>,
1124                KeyContainer: BatchContainer<Owned = Row>,
1125                ValOwn: Data + MaybeValidatingRow<Row, Row>,
1126                Time = T,
1127                Diff = Diff,
1128            > + 'static,
1129        Bu: Builder<
1130                Time = T,
1131                Input: Container
1132                           + ClearContainer
1133                           + PushInto<((Row, Tr::ValOwn), Tr::Time, Tr::Diff)>,
1134                Output = Tr::Batch,
1135            >,
1136        Arranged<'s, TraceAgent<Tr>>: ArrangementSize,
1137    {
1138        let error_logger = self.error_logger();
1139        // NOTE(vmarcos): The input operator name below is used in the tuning advice built-in
1140        // view mz_introspection.mz_expected_group_size_advice.
1141        let arranged_input = input
1142            .mz_arrange::<
1143                ColumnationChunker<_>,
1144                RowRowBatcher<_, _>,
1145                RowRowBuilder<_, _>,
1146                RowRowSpine<_, _>,
1147            >(
1148                "Arranged MinsMaxesHierarchical input",
1149            );
1150
1151        // Scratch buffer for decoding the input values (one column per aggregate) into the
1152        // arena, so the aggregates iterate arena-resident datums rather than the packed bytes.
1153        let mut value_datums = DatumVec::new();
1154        let reduced = arranged_input.clone().mz_reduce_abelian::<_, Bu, Tr>(
1155            "Reduced Fallibly MinsMaxesHierarchical",
1156            move |key, source, target| {
1157                if let Some(err) = Tr::ValOwn::into_error() {
1158                    // Should negative accumulations reach us, we should loudly complain.
1159                    for (value, count) in source.iter() {
1160                        if count.is_positive() {
1161                            continue;
1162                        }
1163                        error_logger.log(
1164                            "Non-positive accumulation in MinsMaxesHierarchical",
1165                            &format!("key={key:?}, value={value:?}, count={count}"),
1166                        );
1167                        // After complaining, output an error here so that we can eventually
1168                        // report it in an error stream.
1169                        target.push((
1170                            err(<Tr::KeyContainer as BatchContainer>::into_owned(key)),
1171                            Diff::ONE,
1172                        ));
1173                        return;
1174                    }
1175                }
1176
1177                // Decode every value row's datums into the arena, one column per aggregate,
1178                // then iterate them column-major below.
1179                let temp_storage = RowArena::new();
1180                let arity = aggrs.len();
1181                let mut decoded = value_datums.borrow();
1182                for (values, _cnt) in source.iter() {
1183                    values.extend_datums(&temp_storage, &mut decoded, None);
1184                }
1185                assert_eq!(decoded.len(), source.len() * arity);
1186
1187                let mut row_builder = SharedRow::get();
1188                let mut row_packer = row_builder.packer();
1189                for (col, func) in aggrs.iter().enumerate() {
1190                    // Min/max hierarchical aggregates are multiplicity-insensitive, so each
1191                    // row contributes once (`Diff::ONE`) regardless of `_cnt`.
1192                    let column_iter =
1193                        (0..source.len()).map(|r| (decoded[r * arity + col], Diff::ONE));
1194                    row_packer.push(func.eval(column_iter, &temp_storage));
1195                }
1196                // We only want to arrange the parts of the input that are not part of the output.
1197                // More specifically, we want to arrange it so that `input.concat(&output.negate())`
1198                // gives us the intended value of this aggregate function. Also we assume that regardless
1199                // of the multiplicity of the final result in the input, we only want to have one copy
1200                // in the output.
1201                target.reserve(source.len().saturating_add(1));
1202                target.push((Tr::ValOwn::ok(row_builder.clone()), Diff::MINUS_ONE));
1203                target.extend(source.iter().map(|(values, cnt)| {
1204                    let mut cnt = *cnt;
1205                    cnt.negate();
1206                    (Tr::ValOwn::ok(values.to_row()), cnt)
1207                }));
1208            },
1209        );
1210        (arranged_input, reduced)
1211    }
1212
1213    /// Build the dataflow to compute and arrange multiple hierarchical aggregations
1214    /// on monotonic inputs.
1215    fn build_monotonic<'s>(
1216        &self,
1217        collection: VecCollection<'s, T, (Row, Row), Diff>,
1218        MonotonicPlan {
1219            aggr_funcs,
1220            must_consolidate,
1221        }: MonotonicPlan,
1222        mfp_after: Option<SafeMfpPlan<LirScalarExpr>>,
1223    ) -> (
1224        RowRowArrangement<'s, T>,
1225        VecCollection<'s, T, DataflowErrorSer, Diff>,
1226    ) {
1227        let aggregations = aggr_funcs.len();
1228        // Gather the relevant values into a vec of rows ordered by aggregation_index
1229        let collection = collection
1230            .map(move |(key, row)| {
1231                let mut row_builder = SharedRow::get();
1232                let mut values = Vec::with_capacity(aggregations);
1233                values.extend(
1234                    row.iter()
1235                        .take(aggregations)
1236                        .map(|v| row_builder.pack_using(std::iter::once(v))),
1237                );
1238
1239                (key, values)
1240            })
1241            .consolidate_named_if::<KeyBatcher<_, _, _>>(
1242                must_consolidate,
1243                "Consolidated ReduceMonotonic input",
1244            );
1245
1246        // It should be now possible to ensure that we have a monotonic collection.
1247        let error_logger = self.error_logger();
1248        let (partial, validation_errs) = collection.ensure_monotonic(move |data, diff| {
1249            error_logger.log(
1250                "Non-monotonic input to ReduceMonotonic",
1251                &format!("data={data:?}, diff={diff}"),
1252            );
1253            let m = "tried to build a monotonic reduction on non-monotonic input".into();
1254            (EvalError::Internal(m).into(), Diff::ONE)
1255        });
1256        // We can place our rows directly into the diff field, and
1257        // only keep the relevant one corresponding to evaluating our
1258        // aggregate, instead of having to do a hierarchical reduction.
1259        let partial = partial.explode_one(move |(key, values)| {
1260            let mut output = Vec::new();
1261            for (row, func) in values.into_iter().zip_eq(aggr_funcs.iter()) {
1262                output.push(monoids::get_monoid(row, func).expect(
1263                    "hierarchical aggregations are expected to have monoid implementations",
1264                ));
1265            }
1266            (key, output)
1267        });
1268
1269        // Allocations for the two closures.
1270        let mut datums1 = DatumVec::new();
1271        let mut datums2 = DatumVec::new();
1272        let mfp_after1 = mfp_after.clone();
1273        let mfp_after2 = mfp_after.filter(|mfp| mfp.could_error());
1274
1275        let partial: KeyCollection<_, _, _> = partial.into();
1276        let arranged = partial
1277            .mz_arrange::<
1278                ColumnationChunker<_>,
1279                RowBatcher<_, _>,
1280                RowBuilder<_, _>,
1281                RowSpine<_, Vec<ReductionMonoid>>,
1282            >(
1283                "ArrangeMonotonic [val: empty]",
1284            );
1285        let output = arranged
1286            .clone()
1287            .mz_reduce_abelian::<_, RowRowBuilder<_, _>, RowRowSpine<_, _>>("ReduceMonotonic", {
1288                move |key, input, output| {
1289                    let temp_storage = RowArena::new();
1290                    let mut datums_local = datums1.borrow();
1291                    key.extend_datums(&temp_storage, &mut datums_local, None);
1292                    let key_len = datums_local.len();
1293                    let accum = &input[0].1;
1294                    for monoid in accum.iter() {
1295                        datums_local.extend(monoid.finalize().iter());
1296                    }
1297
1298                    if let Some(row) =
1299                        evaluate_mfp_after(&mfp_after1, &mut datums_local, &temp_storage, key_len)
1300                    {
1301                        output.push((row, Diff::ONE));
1302                    }
1303                }
1304            });
1305
1306        // If `mfp_after` can error, then we need to render a paired reduction
1307        // to scan for these potential errors. Note that we cannot directly use
1308        // `mz_timely_util::reduce::ReduceExt::reduce_pair` here because we only
1309        // conditionally render the second component of the reduction pair.
1310        if let Some(mfp) = mfp_after2 {
1311            let mfp_errs = arranged
1312                .mz_reduce_abelian::<_, RowErrBuilder<_, _>, RowErrSpine<_, _>>(
1313                    "ReduceMonotonic Error Check",
1314                    move |key, input, output| {
1315                        let temp_storage = RowArena::new();
1316                        let mut datums_local = datums2.borrow();
1317                        key.extend_datums(&temp_storage, &mut datums_local, None);
1318                        let accum = &input[0].1;
1319                        for monoid in accum.iter() {
1320                            datums_local.extend(monoid.finalize().iter());
1321                        }
1322                        if let Result::Err(e) = mfp.evaluate_inner(&mut datums_local, &temp_storage)
1323                        {
1324                            output.push((e.into(), Diff::ONE));
1325                        }
1326                    },
1327                )
1328                .as_collection(|_k, v| v.clone());
1329            (output, validation_errs.concat(mfp_errs))
1330        } else {
1331            (output, validation_errs)
1332        }
1333    }
1334
1335    /// Build the dataflow to compute and arrange multiple accumulable aggregations.
1336    ///
1337    /// The incoming values are moved to the update's "difference" field, at which point
1338    /// they can be accumulated in place. The `count` operator promotes the accumulated
1339    /// values to data, at which point a final map applies operator-specific logic to
1340    /// yield the final aggregate.
1341    fn build_accumulable<'s>(
1342        &self,
1343        collection: VecCollection<'s, T, (Row, Row), Diff>,
1344        AccumulablePlan {
1345            full_aggrs,
1346            simple_aggrs,
1347            distinct_aggrs,
1348        }: AccumulablePlan,
1349        key_arity: usize,
1350        mfp_after: Option<SafeMfpPlan<LirScalarExpr>>,
1351    ) -> (
1352        RowRowArrangement<'s, T>,
1353        VecCollection<'s, T, DataflowErrorSer, Diff>,
1354    ) {
1355        let collection_scope = collection.scope();
1356
1357        // we must have called this function with something to reduce
1358        if full_aggrs.len() == 0 || simple_aggrs.len() + distinct_aggrs.len() != full_aggrs.len() {
1359            self.error_logger().soft_panic_or_log(
1360                "Incorrect numbers of aggregates in accummulable reduction rendering",
1361                &format!(
1362                    "full_aggrs={}, simple_aggrs={}, distinct_aggrs={}",
1363                    full_aggrs.len(),
1364                    simple_aggrs.len(),
1365                    distinct_aggrs.len(),
1366                ),
1367            );
1368        }
1369
1370        // Some of the aggregations may have the `distinct` bit set, which means that they'll
1371        // need to be extracted from `collection` and be subjected to `distinct` with `key`.
1372        // Other aggregations can be directly moved in to the `diff` field.
1373        //
1374        // In each case, the resulting collection should have `data` shaped as `(key, ())`
1375        // and a `diff` that is a vector with length `3 * aggrs.len()`. The three values are
1376        // generally the count, and then two aggregation-specific values. The size could be
1377        // reduced if we want to specialize for the aggregations.
1378
1379        // Instantiate a default vector for diffs with the correct types at each
1380        // position.
1381        let zero_diffs: (Vec<_>, Diff) = (
1382            full_aggrs
1383                .iter()
1384                .map(|f| accumulable_zero(&f.func))
1385                .collect(),
1386            Diff::ZERO,
1387        );
1388
1389        let mut to_aggregate = Vec::new();
1390        if simple_aggrs.len() > 0 {
1391            // First, collect all non-distinct aggregations in one pass.
1392            let collection = collection.clone();
1393            let easy_cases = collection.explode_one({
1394                let zero_diffs = zero_diffs.clone();
1395                move |(key, row)| {
1396                    let mut diffs = zero_diffs.clone();
1397                    // Try to unpack only the datums we need. Unfortunately, since we
1398                    // can't random access into a Row, we have to iterate through one by one.
1399                    // TODO: Even though we don't have random access, we could still avoid unpacking
1400                    // everything that we don't care about, and it might be worth it to extend the
1401                    // Row API to do that.
1402                    let mut row_iter = row.iter().enumerate();
1403                    for (datum_index, aggr) in simple_aggrs.iter() {
1404                        let mut datum = row_iter.next().unwrap();
1405                        while datum_index != &datum.0 {
1406                            datum = row_iter.next().unwrap();
1407                        }
1408                        let datum = datum.1;
1409                        diffs.0[*datum_index] = datum_to_accumulator(&aggr.func, datum);
1410                        diffs.1 = Diff::ONE;
1411                    }
1412                    ((key, ()), diffs)
1413                }
1414            });
1415            to_aggregate.push(easy_cases);
1416        }
1417
1418        // Next, collect all aggregations that require distinctness.
1419        for (datum_index, aggr) in distinct_aggrs.into_iter() {
1420            let pairer = Pairer::new(key_arity);
1421            let collection = collection
1422                .clone()
1423                .map(move |(key, row)| {
1424                    let value = row.iter().nth(datum_index).unwrap();
1425                    (pairer.merge(&key, std::iter::once(value)), ())
1426                })
1427                .mz_arrange::<
1428                    ColumnationChunker<_>,
1429                    RowBatcher<_, _>,
1430                    RowBuilder<_, _>,
1431                    RowSpine<_, _>,
1432                >(
1433                    "Arranged Accumulable Distinct [val: empty]",
1434                )
1435                .mz_reduce_abelian::<_, RowBuilder<_, _>, RowSpine<_, _>>(
1436                    "Reduced Accumulable Distinct [val: empty]",
1437                    move |_k, _s, t| t.push(((), Diff::ONE)),
1438                )
1439                .as_collection(move |key_val_iter, _| pairer.split(key_val_iter))
1440                .explode_one({
1441                    let zero_diffs = zero_diffs.clone();
1442                    move |(key, row)| {
1443                        let datum = row.iter().next().unwrap();
1444                        let mut diffs = zero_diffs.clone();
1445                        diffs.0[datum_index] = datum_to_accumulator(&aggr.func, datum);
1446                        diffs.1 = Diff::ONE;
1447                        ((key, ()), diffs)
1448                    }
1449                });
1450            to_aggregate.push(collection);
1451        }
1452
1453        // now concatenate, if necessary, multiple aggregations
1454        let collection = if to_aggregate.len() == 1 {
1455            to_aggregate.remove(0)
1456        } else {
1457            differential_dataflow::collection::concatenate(collection_scope, to_aggregate)
1458        };
1459
1460        // Allocations for the two closures.
1461        let mut datums1 = DatumVec::new();
1462        let mut datums2 = DatumVec::new();
1463        let mfp_after1 = mfp_after.clone();
1464        let mfp_after2 = mfp_after.filter(|mfp| mfp.could_error());
1465        let full_aggrs2 = full_aggrs.clone();
1466
1467        let error_logger = self.error_logger();
1468        let err_full_aggrs = full_aggrs.clone();
1469        let arranged = collection
1470            .mz_arrange::<
1471                ColumnationChunker<_>,
1472                RowBatcher<_, _>,
1473                RowBuilder<_, _>,
1474                RowSpine<_, (Vec<Accum>, Diff)>,
1475            >(
1476                "ArrangeAccumulable [val: empty]",
1477            );
1478        let arranged_output = arranged
1479            .clone()
1480            .mz_reduce_abelian::<_, RowRowBuilder<_, _>, RowRowSpine<_, _>>("ReduceAccumulable", {
1481                move |key, input, output| {
1482                    let (ref accums, total) = input[0].1;
1483
1484                    let temp_storage = RowArena::new();
1485                    let mut datums_local = datums1.borrow();
1486                    key.extend_datums(&temp_storage, &mut datums_local, None);
1487                    let key_len = datums_local.len();
1488                    for (aggr, accum) in full_aggrs.iter().zip_eq(accums) {
1489                        datums_local.push(finalize_accum(&aggr.func, accum, total));
1490                    }
1491
1492                    if let Some(row) =
1493                        evaluate_mfp_after(&mfp_after1, &mut datums_local, &temp_storage, key_len)
1494                    {
1495                        output.push((row, Diff::ONE));
1496                    }
1497                }
1498            });
1499        let arranged_errs = arranged
1500            .mz_reduce_abelian::<_, RowErrBuilder<_, _>, RowErrSpine<_, _>>(
1501                "AccumulableErrorCheck",
1502                move |key, input, output| {
1503                    let (ref accums, total) = input[0].1;
1504                    for (aggr, accum) in err_full_aggrs.iter().zip_eq(accums) {
1505                        // We first test here if inputs without net-positive records are present,
1506                        // producing an error to the logs and to the query output if that is the case.
1507                        if total == Diff::ZERO && !accum.is_zero() {
1508                            error_logger.log(
1509                                "Net-zero records with non-zero accumulation in ReduceAccumulable",
1510                                &format!("aggr={aggr:?}, accum={accum:?}"),
1511                            );
1512                            let key = key.to_row();
1513                            let message = format!(
1514                                "Invalid data in source, saw net-zero records for key {key} \
1515                                 with non-zero accumulation in accumulable aggregate"
1516                            );
1517                            output.push((EvalError::Internal(message.into()).into(), Diff::ONE));
1518                        }
1519                        match (&aggr.func, &accum) {
1520                            (AggregateFunc::SumUInt16, Accum::SimpleNumber { accum, .. })
1521                            | (AggregateFunc::SumUInt32, Accum::SimpleNumber { accum, .. })
1522                            | (AggregateFunc::SumUInt64, Accum::SimpleNumber { accum, .. }) => {
1523                                if accum.is_negative() {
1524                                    error_logger.log(
1525                                    "Invalid negative unsigned aggregation in ReduceAccumulable",
1526                                    &format!("aggr={aggr:?}, accum={accum:?}"),
1527                                );
1528                                    let key = key.to_row();
1529                                    let message = format!(
1530                                        "Invalid data in source, saw negative accumulation with \
1531                                         unsigned type for key {key}"
1532                                    );
1533                                    let err = EvalError::Internal(message.into());
1534                                    output.push((err.into(), Diff::ONE));
1535                                }
1536                            }
1537                            _ => (), // no more errors to check for at this point!
1538                        }
1539                    }
1540
1541                    // If `mfp_after` can error, then evaluate it here.
1542                    let Some(mfp) = &mfp_after2 else { return };
1543                    let temp_storage = RowArena::new();
1544                    let mut datums_local = datums2.borrow();
1545                    key.extend_datums(&temp_storage, &mut datums_local, None);
1546                    for (aggr, accum) in full_aggrs2.iter().zip_eq(accums) {
1547                        datums_local.push(finalize_accum(&aggr.func, accum, total));
1548                    }
1549
1550                    if let Result::Err(e) = mfp.evaluate_inner(&mut datums_local, &temp_storage) {
1551                        output.push((e.into(), Diff::ONE));
1552                    }
1553                },
1554            );
1555        (
1556            arranged_output,
1557            arranged_errs.as_collection(|_key, error| error.clone()),
1558        )
1559    }
1560}
1561
1562/// Evaluates the fused MFP, if one exists, on a reconstructed `DatumVecBorrow`
1563/// containing key and aggregate values, then returns a result `Row` or `None`
1564/// if the MFP filters the result out.
1565fn evaluate_mfp_after<'a, 'b>(
1566    mfp_after: &'a Option<SafeMfpPlan<LirScalarExpr>>,
1567    datums_local: &'b mut mz_repr::DatumVecBorrow<'a>,
1568    temp_storage: &'a RowArena,
1569    key_len: usize,
1570) -> Option<Row> {
1571    let mut row_builder = SharedRow::get();
1572    // Apply MFP if it exists and pack a Row of
1573    // aggregate values from `datums_local`.
1574    if let Some(mfp) = mfp_after {
1575        // It must ignore errors here, but they are scanned
1576        // for elsewhere if the MFP can error.
1577        if let Ok(Some(iter)) = mfp.evaluate_iter(datums_local, temp_storage) {
1578            // The `mfp_after` must preserve the key columns,
1579            // so we can skip them to form aggregation results.
1580            Some(row_builder.pack_using(iter.skip(key_len)))
1581        } else {
1582            None
1583        }
1584    } else {
1585        Some(row_builder.pack_using(&datums_local[key_len..]))
1586    }
1587}
1588
1589fn accumulable_zero(aggr_func: &AggregateFunc) -> Accum {
1590    match aggr_func {
1591        AggregateFunc::Any | AggregateFunc::All => Accum::Bool {
1592            trues: Diff::ZERO,
1593            falses: Diff::ZERO,
1594        },
1595        AggregateFunc::SumFloat32 | AggregateFunc::SumFloat64 => Accum::Float {
1596            accum: AccumCount::ZERO,
1597            pos_infs: Diff::ZERO,
1598            neg_infs: Diff::ZERO,
1599            nans: Diff::ZERO,
1600            non_nulls: Diff::ZERO,
1601        },
1602        AggregateFunc::SumNumeric => Accum::Numeric {
1603            accum: OrderedDecimal(NumericAgg::zero()),
1604            pos_infs: Diff::ZERO,
1605            neg_infs: Diff::ZERO,
1606            nans: Diff::ZERO,
1607            non_nulls: Diff::ZERO,
1608        },
1609        _ => Accum::SimpleNumber {
1610            accum: AccumCount::ZERO,
1611            non_nulls: Diff::ZERO,
1612        },
1613    }
1614}
1615
1616/// The number of fractional bits of binary precision retained by the
1617/// fixed-point representation used to accumulate float sums. The fixed-point
1618/// scale is `FLOAT_SCALE == 2^FLOAT_SCALE_EXP`.
1619const FLOAT_SCALE_EXP: u32 = 24;
1620
1621/// The fixed-point scale applied to float sums, i.e. `2^FLOAT_SCALE_EXP`.
1622#[allow(clippy::as_conversions)] // Integer-to-float cast, exact and const-evaluable.
1623const FLOAT_SCALE: f64 = (1_u64 << FLOAT_SCALE_EXP) as f64;
1624
1625/// Maps a finite `f64` onto the fixed-point `i128` domain used to accumulate
1626/// float sums, i.e. computes `trunc(n * FLOAT_SCALE)` reduced modulo `2^128`.
1627///
1628/// Conceptually this multiplies `n` by `FLOAT_SCALE` and truncates towards zero,
1629/// but it does so using *wrapping* (modulo `2^128`) rather than *saturating*
1630/// semantics, and it never forms the intermediate product `n * FLOAT_SCALE` as
1631/// an `f64` (which could itself overflow to infinity for very large `n`).
1632///
1633/// Wrapping is what makes this conversion a group homomorphism into the additive
1634/// group of `i128` (mod `2^128`), matching the wrapping arithmetic used when
1635/// accumulators are combined and retracted. As a result, a set of large finite
1636/// values whose *sum* is representable produces the correct result even when the
1637/// individual values fall outside the representable fixed-point range. Saturating
1638/// instead breaks this: e.g. `1.1e31` and `-1.1e31` both overflow the domain and
1639/// would saturate to `i128::MAX` and `i128::MIN`, which sum to `-1` rather than
1640/// `0` (see database-issues#11265).
1641fn float_to_fixed_point(n: f64) -> i128 {
1642    debug_assert!(n.is_finite());
1643
1644    // Decompose `n` into integer parts such that `n == sign * mantissa *
1645    // 2^exponent`. Folding in the `* 2^FLOAT_SCALE_EXP` scaling then amounts to
1646    // shifting `mantissa` left by `exponent + FLOAT_SCALE_EXP` bits.
1647    let (mantissa, exponent, sign) = Float::integer_decode(n);
1648    let significand = u128::from(mantissa);
1649    let exp = i64::from(exponent) + i64::from(FLOAT_SCALE_EXP);
1650
1651    let magnitude: u128 = if exp >= 0 {
1652        // Left shifts of 128 or more bits leave nothing within the 128-bit
1653        // window; smaller shifts keep only the low 128 bits (i.e. mod `2^128`).
1654        match u32::try_from(exp) {
1655            Ok(shift) if shift < 128 => significand << shift,
1656            _ => 0,
1657        }
1658    } else {
1659        // Right shift truncates the fractional part towards zero. Subnormals
1660        // (and zero) shift entirely out of the window and become zero.
1661        match u32::try_from(-exp) {
1662            Ok(shift) if shift < 128 => significand >> shift,
1663            _ => 0,
1664        }
1665    };
1666
1667    // Reinterpret the magnitude as a signed `i128` (wrapping into the signed
1668    // domain) and apply the sign of `n`.
1669    let magnitude = magnitude.cast_signed();
1670    if sign < 0 {
1671        magnitude.wrapping_neg()
1672    } else {
1673        magnitude
1674    }
1675}
1676
1677fn datum_to_accumulator(aggregate_func: &AggregateFunc, datum: Datum) -> Accum {
1678    match aggregate_func {
1679        AggregateFunc::Count => Accum::SimpleNumber {
1680            accum: AccumCount::ZERO, // unused for AggregateFunc::Count
1681            non_nulls: if datum.is_null() {
1682                Diff::ZERO
1683            } else {
1684                Diff::ONE
1685            },
1686        },
1687        AggregateFunc::Any | AggregateFunc::All => match datum {
1688            Datum::True => Accum::Bool {
1689                trues: Diff::ONE,
1690                falses: Diff::ZERO,
1691            },
1692            Datum::Null => Accum::Bool {
1693                trues: Diff::ZERO,
1694                falses: Diff::ZERO,
1695            },
1696            Datum::False => Accum::Bool {
1697                trues: Diff::ZERO,
1698                falses: Diff::ONE,
1699            },
1700            x => panic!("Invalid argument to AggregateFunc::Any: {x:?}"),
1701        },
1702        AggregateFunc::Dummy => match datum {
1703            Datum::Dummy => Accum::SimpleNumber {
1704                accum: AccumCount::ZERO,
1705                non_nulls: Diff::ZERO,
1706            },
1707            x => panic!("Invalid argument to AggregateFunc::Dummy: {x:?}"),
1708        },
1709        AggregateFunc::SumFloat32 | AggregateFunc::SumFloat64 => {
1710            let n = match datum {
1711                Datum::Float32(n) => f64::from(*n),
1712                Datum::Float64(n) => *n,
1713                Datum::Null => 0f64,
1714                x => panic!("Invalid argument to AggregateFunc::{aggregate_func:?}: {x:?}"),
1715            };
1716
1717            let nans = Diff::from(n.is_nan());
1718            let pos_infs = Diff::from(n == f64::INFINITY);
1719            let neg_infs = Diff::from(n == f64::NEG_INFINITY);
1720            let non_nulls = Diff::from(datum != Datum::Null);
1721
1722            // Map the floating point value onto a fixed precision domain
1723            // All special values should map to zero, since they are tracked separately
1724            let accum = if nans.is_positive() || pos_infs.is_positive() || neg_infs.is_positive() {
1725                AccumCount::ZERO
1726            } else {
1727                // Wrap (rather than saturate) on overflow, so that the mapping is
1728                // a group homomorphism and large finite values whose sum is in
1729                // range still produce correct results (database-issues#11265).
1730                float_to_fixed_point(n).into()
1731            };
1732
1733            Accum::Float {
1734                accum,
1735                pos_infs,
1736                neg_infs,
1737                nans,
1738                non_nulls,
1739            }
1740        }
1741        AggregateFunc::SumNumeric => match datum {
1742            Datum::Numeric(n) => {
1743                let (accum, pos_infs, neg_infs, nans) = if n.0.is_infinite() {
1744                    if n.0.is_negative() {
1745                        (NumericAgg::zero(), Diff::ZERO, Diff::ONE, Diff::ZERO)
1746                    } else {
1747                        (NumericAgg::zero(), Diff::ONE, Diff::ZERO, Diff::ZERO)
1748                    }
1749                } else if n.0.is_nan() {
1750                    (NumericAgg::zero(), Diff::ZERO, Diff::ZERO, Diff::ONE)
1751                } else {
1752                    // Take a narrow decimal (datum) into a wide decimal
1753                    // (aggregator).
1754                    let mut cx_agg = numeric::cx_agg();
1755                    (cx_agg.to_width(n.0), Diff::ZERO, Diff::ZERO, Diff::ZERO)
1756                };
1757
1758                Accum::Numeric {
1759                    accum: OrderedDecimal(accum),
1760                    pos_infs,
1761                    neg_infs,
1762                    nans,
1763                    non_nulls: Diff::ONE,
1764                }
1765            }
1766            Datum::Null => Accum::Numeric {
1767                accum: OrderedDecimal(NumericAgg::zero()),
1768                pos_infs: Diff::ZERO,
1769                neg_infs: Diff::ZERO,
1770                nans: Diff::ZERO,
1771                non_nulls: Diff::ZERO,
1772            },
1773            x => panic!("Invalid argument to AggregateFunc::SumNumeric: {x:?}"),
1774        },
1775        _ => {
1776            // Other accumulations need to disentangle the accumulable
1777            // value from its NULL-ness, which is not quite as easily
1778            // accumulated.
1779            match datum {
1780                Datum::Int16(i) => Accum::SimpleNumber {
1781                    accum: i.into(),
1782                    non_nulls: Diff::ONE,
1783                },
1784                Datum::Int32(i) => Accum::SimpleNumber {
1785                    accum: i.into(),
1786                    non_nulls: Diff::ONE,
1787                },
1788                Datum::Int64(i) => Accum::SimpleNumber {
1789                    accum: i.into(),
1790                    non_nulls: Diff::ONE,
1791                },
1792                Datum::UInt16(u) => Accum::SimpleNumber {
1793                    accum: u.into(),
1794                    non_nulls: Diff::ONE,
1795                },
1796                Datum::UInt32(u) => Accum::SimpleNumber {
1797                    accum: u.into(),
1798                    non_nulls: Diff::ONE,
1799                },
1800                Datum::UInt64(u) => Accum::SimpleNumber {
1801                    accum: u.into(),
1802                    non_nulls: Diff::ONE,
1803                },
1804                Datum::MzTimestamp(t) => Accum::SimpleNumber {
1805                    accum: u64::from(t).into(),
1806                    non_nulls: Diff::ONE,
1807                },
1808                Datum::Null => Accum::SimpleNumber {
1809                    accum: AccumCount::ZERO,
1810                    non_nulls: Diff::ZERO,
1811                },
1812                x => panic!("Accumulating non-integer data: {x:?}"),
1813            }
1814        }
1815    }
1816}
1817
1818fn finalize_accum<'a>(aggr_func: &'a AggregateFunc, accum: &'a Accum, total: Diff) -> Datum<'a> {
1819    // The finished value depends on the aggregation function in a variety of ways.
1820    // For all aggregates but count, if only null values were
1821    // accumulated, then the output is null.
1822    if total.is_positive() && accum.is_zero() && *aggr_func != AggregateFunc::Count {
1823        Datum::Null
1824    } else {
1825        match (&aggr_func, &accum) {
1826            (AggregateFunc::Count, Accum::SimpleNumber { non_nulls, .. }) => {
1827                Datum::Int64(non_nulls.into_inner())
1828            }
1829            (AggregateFunc::All, Accum::Bool { falses, trues }) => {
1830                // If any false, else if all true, else must be no false and some nulls.
1831                if falses.is_positive() {
1832                    Datum::False
1833                } else if *trues == total {
1834                    Datum::True
1835                } else {
1836                    Datum::Null
1837                }
1838            }
1839            (AggregateFunc::Any, Accum::Bool { falses, trues }) => {
1840                // If any true, else if all false, else must be no true and some nulls.
1841                if trues.is_positive() {
1842                    Datum::True
1843                } else if *falses == total {
1844                    Datum::False
1845                } else {
1846                    Datum::Null
1847                }
1848            }
1849            (AggregateFunc::Dummy, _) => Datum::Dummy,
1850            // If any non-nulls, just report the aggregate.
1851            (AggregateFunc::SumInt16, Accum::SimpleNumber { accum, .. })
1852            | (AggregateFunc::SumInt32, Accum::SimpleNumber { accum, .. }) => {
1853                // This conversion is safe, as long as we have less than 2^32
1854                // summands.
1855                // TODO(benesch): are we guaranteed to have less than 2^32 summands?
1856                // If so, rewrite to avoid `as`.
1857                #[allow(clippy::as_conversions)]
1858                Datum::Int64(accum.into_inner() as i64)
1859            }
1860            (AggregateFunc::SumInt64, Accum::SimpleNumber { accum, .. }) => Datum::from(*accum),
1861            (AggregateFunc::SumUInt16, Accum::SimpleNumber { accum, .. })
1862            | (AggregateFunc::SumUInt32, Accum::SimpleNumber { accum, .. }) => {
1863                if !accum.is_negative() {
1864                    // Our semantics of overflow are not clearly articulated wrt.
1865                    // unsigned vs. signed types (database-issues#5172). We adopt an
1866                    // unsigned wrapping behavior to match what we do above for
1867                    // signed types.
1868                    // TODO(vmarcos): remove potentially dangerous usage of `as`.
1869                    #[allow(clippy::as_conversions)]
1870                    Datum::UInt64(accum.into_inner() as u64)
1871                } else {
1872                    // Note that we return a value here, but an error in the other
1873                    // operator of the reduce_pair. Therefore, we expect that this
1874                    // value will never be exposed as an output.
1875                    Datum::Null
1876                }
1877            }
1878            (AggregateFunc::SumUInt64, Accum::SimpleNumber { accum, .. }) => {
1879                if !accum.is_negative() {
1880                    Datum::from(*accum)
1881                } else {
1882                    // Note that we return a value here, but an error in the other
1883                    // operator of the reduce_pair. Therefore, we expect that this
1884                    // value will never be exposed as an output.
1885                    Datum::Null
1886                }
1887            }
1888            (
1889                AggregateFunc::SumFloat32,
1890                Accum::Float {
1891                    accum,
1892                    pos_infs,
1893                    neg_infs,
1894                    nans,
1895                    non_nulls: _,
1896                },
1897            ) => {
1898                if nans.is_positive() || (pos_infs.is_positive() && neg_infs.is_positive()) {
1899                    // NaNs are NaNs and cases where we've seen a
1900                    // mixture of positive and negative infinities.
1901                    Datum::from(f32::NAN)
1902                } else if pos_infs.is_positive() {
1903                    Datum::from(f32::INFINITY)
1904                } else if neg_infs.is_positive() {
1905                    Datum::from(f32::NEG_INFINITY)
1906                } else {
1907                    let sum = f64::cast_lossy(accum.into_inner()) / FLOAT_SCALE;
1908                    Datum::from(f32::cast_lossy(sum))
1909                }
1910            }
1911            (
1912                AggregateFunc::SumFloat64,
1913                Accum::Float {
1914                    accum,
1915                    pos_infs,
1916                    neg_infs,
1917                    nans,
1918                    non_nulls: _,
1919                },
1920            ) => {
1921                if nans.is_positive() || (pos_infs.is_positive() && neg_infs.is_positive()) {
1922                    // NaNs are NaNs and cases where we've seen a
1923                    // mixture of positive and negative infinities.
1924                    Datum::from(f64::NAN)
1925                } else if pos_infs.is_positive() {
1926                    Datum::from(f64::INFINITY)
1927                } else if neg_infs.is_positive() {
1928                    Datum::from(f64::NEG_INFINITY)
1929                } else {
1930                    Datum::from(f64::cast_lossy(accum.into_inner()) / FLOAT_SCALE)
1931                }
1932            }
1933            (
1934                AggregateFunc::SumNumeric,
1935                Accum::Numeric {
1936                    accum,
1937                    pos_infs,
1938                    neg_infs,
1939                    nans,
1940                    non_nulls: _,
1941                },
1942            ) => {
1943                let mut cx_datum = numeric::cx_datum();
1944                let d = cx_datum.to_width(accum.0);
1945                // Take a wide decimal (aggregator) into a
1946                // narrow decimal (datum). If this operation
1947                // overflows the datum, this new value will be
1948                // +/- infinity. However, the aggregator tracks
1949                // the amount of overflow, making it invertible.
1950                let inf_d = d.is_infinite();
1951                let neg_d = d.is_negative();
1952                let pos_inf = pos_infs.is_positive() || (inf_d && !neg_d);
1953                let neg_inf = neg_infs.is_positive() || (inf_d && neg_d);
1954                if nans.is_positive() || (pos_inf && neg_inf) {
1955                    // NaNs are NaNs and cases where we've seen a
1956                    // mixture of positive and negative infinities.
1957                    Datum::from(Numeric::nan())
1958                } else if pos_inf {
1959                    Datum::from(Numeric::infinity())
1960                } else if neg_inf {
1961                    let mut cx = numeric::cx_datum();
1962                    let mut d = Numeric::infinity();
1963                    cx.neg(&mut d);
1964                    Datum::from(d)
1965                } else {
1966                    Datum::from(d)
1967                }
1968            }
1969            _ => panic!(
1970                "Unexpected accumulation (aggr={:?}, accum={accum:?})",
1971                aggr_func
1972            ),
1973        }
1974    }
1975}
1976
1977/// The type for accumulator counting. Set to [`Overflowing<u128>`](mz_ore::Overflowing).
1978type AccumCount = mz_ore::Overflowing<i128>;
1979
1980/// Accumulates values for the various types of accumulable aggregations.
1981///
1982/// We assume that there are not more than 2^32 elements for the aggregation.
1983/// Thus we can perform a summation over i32 in an i64 accumulator
1984/// and not worry about exceeding its bounds.
1985///
1986/// The float accumulator performs accumulation in fixed point arithmetic. The fixed
1987/// point representation has less precision than a double. It is entirely possible
1988/// that the values of the accumulator overflow, thus we have to use wrapping arithmetic
1989/// to preserve group guarantees.
1990#[derive(
1991    Debug,
1992    Clone,
1993    Copy,
1994    PartialEq,
1995    Eq,
1996    PartialOrd,
1997    Ord,
1998    Serialize,
1999    Deserialize
2000)]
2001enum Accum {
2002    /// Accumulates boolean values.
2003    Bool {
2004        /// The number of `true` values observed.
2005        trues: Diff,
2006        /// The number of `false` values observed.
2007        falses: Diff,
2008    },
2009    /// Accumulates simple numeric values.
2010    SimpleNumber {
2011        /// The accumulation of all non-NULL values observed.
2012        accum: AccumCount,
2013        /// The number of non-NULL values observed.
2014        non_nulls: Diff,
2015    },
2016    /// Accumulates float values.
2017    Float {
2018        /// Accumulates non-special float values, mapped to a fixed precision i128 domain to
2019        /// preserve associativity and commutativity
2020        accum: AccumCount,
2021        /// Counts +inf
2022        pos_infs: Diff,
2023        /// Counts -inf
2024        neg_infs: Diff,
2025        /// Counts NaNs
2026        nans: Diff,
2027        /// Counts non-NULL values
2028        non_nulls: Diff,
2029    },
2030    /// Accumulates arbitrary precision decimals.
2031    Numeric {
2032        /// Accumulates non-special values
2033        accum: OrderedDecimal<NumericAgg>,
2034        /// Counts +inf
2035        pos_infs: Diff,
2036        /// Counts -inf
2037        neg_infs: Diff,
2038        /// Counts NaNs
2039        nans: Diff,
2040        /// Counts non-NULL values
2041        non_nulls: Diff,
2042    },
2043}
2044
2045impl IsZero for Accum {
2046    fn is_zero(&self) -> bool {
2047        match self {
2048            Accum::Bool { trues, falses } => trues.is_zero() && falses.is_zero(),
2049            Accum::SimpleNumber { accum, non_nulls } => accum.is_zero() && non_nulls.is_zero(),
2050            Accum::Float {
2051                accum,
2052                pos_infs,
2053                neg_infs,
2054                nans,
2055                non_nulls,
2056            } => {
2057                accum.is_zero()
2058                    && pos_infs.is_zero()
2059                    && neg_infs.is_zero()
2060                    && nans.is_zero()
2061                    && non_nulls.is_zero()
2062            }
2063            Accum::Numeric {
2064                accum,
2065                pos_infs,
2066                neg_infs,
2067                nans,
2068                non_nulls,
2069            } => {
2070                accum.0.is_zero()
2071                    && pos_infs.is_zero()
2072                    && neg_infs.is_zero()
2073                    && nans.is_zero()
2074                    && non_nulls.is_zero()
2075            }
2076        }
2077    }
2078}
2079
2080impl Semigroup for Accum {
2081    fn plus_equals(&mut self, other: &Accum) {
2082        match (&mut *self, other) {
2083            (
2084                Accum::Bool { trues, falses },
2085                Accum::Bool {
2086                    trues: other_trues,
2087                    falses: other_falses,
2088                },
2089            ) => {
2090                *trues += other_trues;
2091                *falses += other_falses;
2092            }
2093            (
2094                Accum::SimpleNumber { accum, non_nulls },
2095                Accum::SimpleNumber {
2096                    accum: other_accum,
2097                    non_nulls: other_non_nulls,
2098                },
2099            ) => {
2100                *accum += other_accum;
2101                *non_nulls += other_non_nulls;
2102            }
2103            (
2104                Accum::Float {
2105                    accum,
2106                    pos_infs,
2107                    neg_infs,
2108                    nans,
2109                    non_nulls,
2110                },
2111                Accum::Float {
2112                    accum: other_accum,
2113                    pos_infs: other_pos_infs,
2114                    neg_infs: other_neg_infs,
2115                    nans: other_nans,
2116                    non_nulls: other_non_nulls,
2117                },
2118            ) => {
2119                *accum = accum.checked_add(*other_accum).unwrap_or_else(|| {
2120                    warn!("Float accumulator overflow. Incorrect results possible");
2121                    accum.wrapping_add(*other_accum)
2122                });
2123                *pos_infs += other_pos_infs;
2124                *neg_infs += other_neg_infs;
2125                *nans += other_nans;
2126                *non_nulls += other_non_nulls;
2127            }
2128            (
2129                Accum::Numeric {
2130                    accum,
2131                    pos_infs,
2132                    neg_infs,
2133                    nans,
2134                    non_nulls,
2135                },
2136                Accum::Numeric {
2137                    accum: other_accum,
2138                    pos_infs: other_pos_infs,
2139                    neg_infs: other_neg_infs,
2140                    nans: other_nans,
2141                    non_nulls: other_non_nulls,
2142                },
2143            ) => {
2144                let mut cx_agg = numeric::cx_agg();
2145                cx_agg.add(&mut accum.0, &other_accum.0);
2146                // `rounded` signals we have exceeded the aggregator's max
2147                // precision, which means we've lost commutativity and
2148                // associativity; nothing to be done here, so panic. For more
2149                // context, see the DEC_Rounded definition at
2150                // http://speleotrove.com/decimal/dncont.html
2151                assert!(!cx_agg.status().rounded(), "Accum::Numeric overflow");
2152                // Reduce to reclaim unused decimal precision. Note that this
2153                // reduction must happen somewhere to make the following
2154                // invertible:
2155                // ```
2156                // CREATE TABLE a (a numeric);
2157                // CREATE MATERIALIZED VIEW t as SELECT sum(a) FROM a;
2158                // INSERT INTO a VALUES ('9e39'), ('9e-39');
2159                // ```
2160                // This will now return infinity. However, we can retract the
2161                // value that blew up its precision:
2162                // ```
2163                // INSERT INTO a VALUES ('-9e-39');
2164                // ```
2165                // This leaves `t`'s aggregator with a value of 9e39. However,
2166                // without doing a reduction, `libdecnum` will store the value
2167                // as 9e39+0e-39, which still exceeds the narrower context's
2168                // precision. By doing the reduction, we can "reclaim" the 39
2169                // digits of precision.
2170                cx_agg.reduce(&mut accum.0);
2171                *pos_infs += other_pos_infs;
2172                *neg_infs += other_neg_infs;
2173                *nans += other_nans;
2174                *non_nulls += other_non_nulls;
2175            }
2176            (l, r) => unreachable!(
2177                "Accumulator::plus_equals called with non-matching variants: {l:?} vs {r:?}"
2178            ),
2179        }
2180    }
2181}
2182
2183impl Multiply<Diff> for Accum {
2184    type Output = Accum;
2185
2186    fn multiply(self, factor: &Diff) -> Accum {
2187        let factor = *factor;
2188        match self {
2189            Accum::Bool { trues, falses } => Accum::Bool {
2190                trues: trues * factor,
2191                falses: falses * factor,
2192            },
2193            Accum::SimpleNumber { accum, non_nulls } => Accum::SimpleNumber {
2194                accum: accum * AccumCount::from(factor),
2195                non_nulls: non_nulls * factor,
2196            },
2197            Accum::Float {
2198                accum,
2199                pos_infs,
2200                neg_infs,
2201                nans,
2202                non_nulls,
2203            } => Accum::Float {
2204                accum: accum
2205                    .checked_mul(AccumCount::from(factor))
2206                    .unwrap_or_else(|| {
2207                        warn!("Float accumulator overflow. Incorrect results possible");
2208                        accum.wrapping_mul(AccumCount::from(factor))
2209                    }),
2210                pos_infs: pos_infs * factor,
2211                neg_infs: neg_infs * factor,
2212                nans: nans * factor,
2213                non_nulls: non_nulls * factor,
2214            },
2215            Accum::Numeric {
2216                accum,
2217                pos_infs,
2218                neg_infs,
2219                nans,
2220                non_nulls,
2221            } => {
2222                let mut cx = numeric::cx_agg();
2223                let mut f = NumericAgg::from(factor.into_inner());
2224                // Unlike `plus_equals`, not necessary to reduce after this operation because `f` will
2225                // always be an integer, i.e. we are never increasing the
2226                // values' scale.
2227                cx.mul(&mut f, &accum.0);
2228                // `rounded` signals we have exceeded the aggregator's max
2229                // precision, which means we've lost commutativity and
2230                // associativity; nothing to be done here, so panic. For more
2231                // context, see the DEC_Rounded definition at
2232                // http://speleotrove.com/decimal/dncont.html
2233                assert!(!cx.status().rounded(), "Accum::Numeric multiply overflow");
2234                Accum::Numeric {
2235                    accum: OrderedDecimal(f),
2236                    pos_infs: pos_infs * factor,
2237                    neg_infs: neg_infs * factor,
2238                    nans: nans * factor,
2239                    non_nulls: non_nulls * factor,
2240                }
2241            }
2242        }
2243    }
2244}
2245
2246impl Columnation for Accum {
2247    type InnerRegion = CopyRegion<Self>;
2248}
2249
2250/// Monoids for in-place compaction of monotonic streams.
2251mod monoids {
2252
2253    // We can improve the performance of some aggregations through the use of algebra.
2254    // In particular, we can move some of the aggregations in to the `diff` field of
2255    // updates, by changing `diff` from integers to a different algebraic structure.
2256    //
2257    // The one we use is called a "semigroup", and it means that the structure has a
2258    // symmetric addition operator. The trait we use also allows the semigroup elements
2259    // to present as "zero", meaning they always act as the identity under +. Here,
2260    // `Datum::Null` acts as the identity under +, _but_ we don't want to make this
2261    // known to DD by the `is_zero` method, see comment there. So, from the point of view
2262    // of DD, this Semigroup should _not_ have a zero.
2263    //
2264    // WARNING: `Datum::Null` should continue to act as the identity of our + (even if we
2265    // add a new enum variant here), because other code (e.g., `HierarchicalOneByOneAggr`)
2266    // assumes this.
2267
2268    use columnation::{Columnation, Region};
2269    use differential_dataflow::difference::{IsZero, Multiply, Semigroup};
2270    use mz_expr::AggregateFunc;
2271    use mz_ore::soft_panic_or_log;
2272    use mz_repr::{Datum, Diff, Row};
2273    use serde::{Deserialize, Serialize};
2274
2275    /// A monoid containing a single-datum row.
2276    #[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Serialize, Deserialize, Hash)]
2277    pub enum ReductionMonoid {
2278        Min(Row),
2279        Max(Row),
2280    }
2281
2282    impl ReductionMonoid {
2283        pub fn finalize(&self) -> &Row {
2284            use ReductionMonoid::*;
2285            match self {
2286                Min(row) | Max(row) => row,
2287            }
2288        }
2289    }
2290
2291    impl Clone for ReductionMonoid {
2292        fn clone(&self) -> Self {
2293            use ReductionMonoid::*;
2294            match self {
2295                Min(row) => Min(row.clone()),
2296                Max(row) => Max(row.clone()),
2297            }
2298        }
2299
2300        fn clone_from(&mut self, source: &Self) {
2301            use ReductionMonoid::*;
2302
2303            let mut row = std::mem::take(match self {
2304                Min(row) | Max(row) => row,
2305            });
2306
2307            let source_row = match source {
2308                Min(row) | Max(row) => row,
2309            };
2310
2311            row.clone_from(source_row);
2312
2313            match source {
2314                Min(_) => *self = Min(row),
2315                Max(_) => *self = Max(row),
2316            }
2317        }
2318    }
2319
2320    impl Multiply<Diff> for ReductionMonoid {
2321        type Output = Self;
2322
2323        fn multiply(self, factor: &Diff) -> Self {
2324            // Multiplication in ReductionMonoid is idempotent, and
2325            // its users must ascertain its monotonicity beforehand
2326            // (typically with ensure_monotonic) since it has no zero
2327            // value for us to use here.
2328            assert!(factor.is_positive());
2329            self
2330        }
2331    }
2332
2333    impl Semigroup for ReductionMonoid {
2334        fn plus_equals(&mut self, rhs: &Self) {
2335            match (self, rhs) {
2336                (ReductionMonoid::Min(lhs), ReductionMonoid::Min(rhs)) => {
2337                    let swap = {
2338                        let lhs_val = lhs.unpack_first();
2339                        let rhs_val = rhs.unpack_first();
2340                        // Datum::Null is the identity, not a small element.
2341                        match (lhs_val, rhs_val) {
2342                            (_, Datum::Null) => false,
2343                            (Datum::Null, _) => true,
2344                            (lhs, rhs) => rhs < lhs,
2345                        }
2346                    };
2347                    if swap {
2348                        lhs.clone_from(rhs);
2349                    }
2350                }
2351                (ReductionMonoid::Max(lhs), ReductionMonoid::Max(rhs)) => {
2352                    let swap = {
2353                        let lhs_val = lhs.unpack_first();
2354                        let rhs_val = rhs.unpack_first();
2355                        // Datum::Null is the identity, not a large element.
2356                        match (lhs_val, rhs_val) {
2357                            (_, Datum::Null) => false,
2358                            (Datum::Null, _) => true,
2359                            (lhs, rhs) => rhs > lhs,
2360                        }
2361                    };
2362                    if swap {
2363                        lhs.clone_from(rhs);
2364                    }
2365                }
2366                (lhs, rhs) => {
2367                    soft_panic_or_log!(
2368                        "Mismatched monoid variants in reduction! lhs: {lhs:?} rhs: {rhs:?}"
2369                    );
2370                }
2371            }
2372        }
2373    }
2374
2375    impl IsZero for ReductionMonoid {
2376        fn is_zero(&self) -> bool {
2377            // It totally looks like we could return true here for `Datum::Null`, but don't do this!
2378            // DD uses true results of this method to make stuff disappear. This makes sense when
2379            // diffs mean really just diffs, but for `ReductionMonoid` diffs hold reduction results.
2380            // We don't want funny stuff, like disappearing, happening to reduction results even
2381            // when they are null. (This would confuse, e.g., `ReduceCollation` for null inputs.)
2382            false
2383        }
2384    }
2385
2386    impl Columnation for ReductionMonoid {
2387        type InnerRegion = ReductionMonoidRegion;
2388    }
2389
2390    /// Region for [`ReductionMonoid`]. This region is special in that it stores both enum variants
2391    /// in the same backing region. Alternatively, it could store it in two regions, but we select
2392    /// the former for simplicity reasons.
2393    #[derive(Default)]
2394    pub struct ReductionMonoidRegion {
2395        inner: <Row as Columnation>::InnerRegion,
2396    }
2397
2398    impl Region for ReductionMonoidRegion {
2399        type Item = ReductionMonoid;
2400
2401        unsafe fn copy(&mut self, item: &Self::Item) -> Self::Item {
2402            use ReductionMonoid::*;
2403            match item {
2404                Min(row) => Min(unsafe { self.inner.copy(row) }),
2405                Max(row) => Max(unsafe { self.inner.copy(row) }),
2406            }
2407        }
2408
2409        fn clear(&mut self) {
2410            self.inner.clear();
2411        }
2412
2413        fn reserve_items<'a, I>(&mut self, items: I)
2414        where
2415            Self: 'a,
2416            I: Iterator<Item = &'a Self::Item> + Clone,
2417        {
2418            self.inner
2419                .reserve_items(items.map(ReductionMonoid::finalize));
2420        }
2421
2422        fn reserve_regions<'a, I>(&mut self, regions: I)
2423        where
2424            Self: 'a,
2425            I: Iterator<Item = &'a Self> + Clone,
2426        {
2427            self.inner.reserve_regions(regions.map(|r| &r.inner));
2428        }
2429
2430        fn heap_size(&self, callback: impl FnMut(usize, usize)) {
2431            self.inner.heap_size(callback);
2432        }
2433    }
2434
2435    /// Get the correct monoid implementation for a given aggregation function. Note that
2436    /// all hierarchical aggregation functions need to supply a monoid implementation.
2437    pub fn get_monoid(row: Row, func: &AggregateFunc) -> Option<ReductionMonoid> {
2438        match func {
2439            AggregateFunc::MaxNumeric
2440            | AggregateFunc::MaxInt16
2441            | AggregateFunc::MaxInt32
2442            | AggregateFunc::MaxInt64
2443            | AggregateFunc::MaxUInt16
2444            | AggregateFunc::MaxUInt32
2445            | AggregateFunc::MaxUInt64
2446            | AggregateFunc::MaxMzTimestamp
2447            | AggregateFunc::MaxFloat32
2448            | AggregateFunc::MaxFloat64
2449            | AggregateFunc::MaxBool
2450            | AggregateFunc::MaxString
2451            | AggregateFunc::MaxDate
2452            | AggregateFunc::MaxTimestamp
2453            | AggregateFunc::MaxTimestampTz
2454            | AggregateFunc::MaxInterval
2455            | AggregateFunc::MaxTime => Some(ReductionMonoid::Max(row)),
2456            AggregateFunc::MinNumeric
2457            | AggregateFunc::MinInt16
2458            | AggregateFunc::MinInt32
2459            | AggregateFunc::MinInt64
2460            | AggregateFunc::MinUInt16
2461            | AggregateFunc::MinUInt32
2462            | AggregateFunc::MinUInt64
2463            | AggregateFunc::MinMzTimestamp
2464            | AggregateFunc::MinFloat32
2465            | AggregateFunc::MinFloat64
2466            | AggregateFunc::MinBool
2467            | AggregateFunc::MinString
2468            | AggregateFunc::MinDate
2469            | AggregateFunc::MinTimestamp
2470            | AggregateFunc::MinTimestampTz
2471            | AggregateFunc::MinInterval
2472            | AggregateFunc::MinTime => Some(ReductionMonoid::Min(row)),
2473            AggregateFunc::SumInt16
2474            | AggregateFunc::SumInt32
2475            | AggregateFunc::SumInt64
2476            | AggregateFunc::SumUInt16
2477            | AggregateFunc::SumUInt32
2478            | AggregateFunc::SumUInt64
2479            | AggregateFunc::SumFloat32
2480            | AggregateFunc::SumFloat64
2481            | AggregateFunc::SumNumeric
2482            | AggregateFunc::Count
2483            | AggregateFunc::Any
2484            | AggregateFunc::All
2485            | AggregateFunc::Dummy
2486            | AggregateFunc::JsonbAgg { .. }
2487            | AggregateFunc::JsonbObjectAgg { .. }
2488            | AggregateFunc::MapAgg { .. }
2489            | AggregateFunc::ArrayConcat { .. }
2490            | AggregateFunc::ListConcat { .. }
2491            | AggregateFunc::StringAgg { .. }
2492            | AggregateFunc::RowNumber { .. }
2493            | AggregateFunc::Rank { .. }
2494            | AggregateFunc::DenseRank { .. }
2495            | AggregateFunc::LagLead { .. }
2496            | AggregateFunc::FirstValue { .. }
2497            | AggregateFunc::LastValue { .. }
2498            | AggregateFunc::WindowAggregate { .. }
2499            | AggregateFunc::FusedValueWindowFunc { .. }
2500            | AggregateFunc::FusedWindowAggregate { .. } => None,
2501        }
2502    }
2503}
2504
2505mod window_agg_helpers {
2506    use crate::render::reduce::*;
2507
2508    /// TODO: It would be better for performance to do the branching that is in the methods of this
2509    /// enum at the place where we are calling `eval_fast_window_agg`. Then we wouldn't need an enum
2510    /// here, and would parameterize `eval_fast_window_agg` with one of the implementations
2511    /// directly.
2512    pub enum OneByOneAggrImpls {
2513        Accumulable(AccumulableOneByOneAggr),
2514        Hierarchical(HierarchicalOneByOneAggr),
2515        Basic(mz_expr::NaiveOneByOneAggr),
2516    }
2517
2518    impl mz_expr::OneByOneAggr for OneByOneAggrImpls {
2519        fn new(agg: &AggregateFunc, reverse: bool) -> Self {
2520            match reduction_type(agg) {
2521                ReductionType::Basic => {
2522                    OneByOneAggrImpls::Basic(mz_expr::NaiveOneByOneAggr::new(agg, reverse))
2523                }
2524                ReductionType::Accumulable => {
2525                    OneByOneAggrImpls::Accumulable(AccumulableOneByOneAggr::new(agg))
2526                }
2527                ReductionType::Hierarchical => {
2528                    OneByOneAggrImpls::Hierarchical(HierarchicalOneByOneAggr::new(agg))
2529                }
2530            }
2531        }
2532
2533        fn give(&mut self, d: &Datum) {
2534            match self {
2535                OneByOneAggrImpls::Basic(i) => i.give(d),
2536                OneByOneAggrImpls::Accumulable(i) => i.give(d),
2537                OneByOneAggrImpls::Hierarchical(i) => i.give(d),
2538            }
2539        }
2540
2541        fn get_current_aggregate<'a>(&self, temp_storage: &'a RowArena) -> Datum<'a> {
2542            // Note that the `reverse` parameter is currently forwarded only for Basic aggregations.
2543            match self {
2544                OneByOneAggrImpls::Basic(i) => i.get_current_aggregate(temp_storage),
2545                OneByOneAggrImpls::Accumulable(i) => i.get_current_aggregate(temp_storage),
2546                OneByOneAggrImpls::Hierarchical(i) => i.get_current_aggregate(temp_storage),
2547            }
2548        }
2549    }
2550
2551    pub struct AccumulableOneByOneAggr {
2552        aggr_func: AggregateFunc,
2553        accum: Accum,
2554        total: Diff,
2555    }
2556
2557    impl AccumulableOneByOneAggr {
2558        fn new(aggr_func: &AggregateFunc) -> Self {
2559            AccumulableOneByOneAggr {
2560                aggr_func: aggr_func.clone(),
2561                accum: accumulable_zero(aggr_func),
2562                total: Diff::ZERO,
2563            }
2564        }
2565
2566        fn give(&mut self, d: &Datum) {
2567            self.accum
2568                .plus_equals(&datum_to_accumulator(&self.aggr_func, d.clone()));
2569            self.total += Diff::ONE;
2570        }
2571
2572        fn get_current_aggregate<'a>(&self, temp_storage: &'a RowArena) -> Datum<'a> {
2573            temp_storage.make_datum(|packer| {
2574                packer.push(finalize_accum(&self.aggr_func, &self.accum, self.total));
2575            })
2576        }
2577    }
2578
2579    pub struct HierarchicalOneByOneAggr {
2580        aggr_func: AggregateFunc,
2581        // Warning: We are assuming that `Datum::Null` acts as the identity for `ReductionMonoid`'s
2582        // `plus_equals`. (But _not_ relying here on `ReductionMonoid::is_zero`.)
2583        monoid: ReductionMonoid,
2584    }
2585
2586    impl HierarchicalOneByOneAggr {
2587        fn new(aggr_func: &AggregateFunc) -> Self {
2588            let mut row_buf = Row::default();
2589            row_buf.packer().push(Datum::Null);
2590            HierarchicalOneByOneAggr {
2591                aggr_func: aggr_func.clone(),
2592                monoid: get_monoid(row_buf, aggr_func)
2593                    .expect("aggr_func should be a hierarchical aggregation function"),
2594            }
2595        }
2596
2597        fn give(&mut self, d: &Datum) {
2598            let mut row_buf = Row::default();
2599            row_buf.packer().push(d);
2600            let m = get_monoid(row_buf, &self.aggr_func)
2601                .expect("aggr_func should be a hierarchical aggregation function");
2602            self.monoid.plus_equals(&m);
2603        }
2604
2605        fn get_current_aggregate<'a>(&self, temp_storage: &'a RowArena) -> Datum<'a> {
2606            temp_storage.make_datum(|packer| packer.extend(self.monoid.finalize().iter()))
2607        }
2608    }
2609}
2610
2611#[cfg(test)]
2612mod tests {
2613    use super::*;
2614
2615    /// The saturating conversion that `float_to_fixed_point` replaces. Used to
2616    /// assert that the new wrapping conversion agrees on the in-range values
2617    /// where the old conversion was already correct.
2618    #[allow(clippy::as_conversions)]
2619    fn saturating_convert(n: f64) -> i128 {
2620        (n * FLOAT_SCALE) as i128
2621    }
2622
2623    #[mz_ore::test]
2624    fn float_to_fixed_point_matches_saturating_in_range() {
2625        // For values whose scaled magnitude comfortably fits in an `i128`, the
2626        // wrapping conversion must produce exactly the same result the previous
2627        // saturating cast did.
2628        let cases = [
2629            0.0,
2630            -0.0,
2631            1.0,
2632            -1.0,
2633            0.1,
2634            -0.1,
2635            0.5,
2636            -0.5,
2637            3.25,
2638            -3.25,
2639            123456.789,
2640            -123456.789,
2641            1e10,
2642            -1e10,
2643            1e20,
2644            -1e20,
2645            5e30, // large, but scaled magnitude still fits comfortably in i128
2646            -5e30,
2647        ];
2648        for n in cases {
2649            assert_eq!(
2650                float_to_fixed_point(n),
2651                saturating_convert(n),
2652                "mismatch for n = {n}"
2653            );
2654        }
2655    }
2656
2657    #[mz_ore::test]
2658    fn float_to_fixed_point_truncates_toward_zero() {
2659        // 1.75 * 2^24 = 29360128, exactly representable.
2660        assert_eq!(float_to_fixed_point(1.75), 29_360_128);
2661        assert_eq!(float_to_fixed_point(-1.75), -29_360_128);
2662
2663        // Fractional results truncate toward zero, matching the previous cast.
2664        let frac = 0.123_456_7_f64;
2665        assert_eq!(float_to_fixed_point(frac), saturating_convert(frac));
2666        assert_eq!(float_to_fixed_point(-frac), saturating_convert(-frac));
2667        assert_eq!(float_to_fixed_point(-frac), -float_to_fixed_point(frac));
2668    }
2669
2670    #[mz_ore::test]
2671    fn float_to_fixed_point_subnormals_round_to_zero() {
2672        assert_eq!(float_to_fixed_point(0.0), 0);
2673        assert_eq!(float_to_fixed_point(-0.0), 0);
2674        assert_eq!(float_to_fixed_point(f64::MIN_POSITIVE / 2.0), 0);
2675        assert_eq!(float_to_fixed_point(5e-324), 0); // smallest subnormal
2676    }
2677
2678    #[mz_ore::test]
2679    fn float_to_fixed_point_cancels_large_finite_values() {
2680        // Regression test for database-issues#11265: large finite values that
2681        // individually overflow the fixed-point domain must still sum to the
2682        // correct result when their mathematical sum is representable. The
2683        // previous saturating conversion produced `i128::MAX + i128::MIN == -1`.
2684        for &n in &[1.1e31_f64, 1e32, 5e33, 1e284] {
2685            assert_eq!(
2686                float_to_fixed_point(n).wrapping_add(float_to_fixed_point(-n)),
2687                0,
2688                "n = {n} did not cancel with -n"
2689            );
2690        }
2691    }
2692
2693    #[mz_ore::test]
2694    fn float_to_fixed_point_sum_via_accumulator() {
2695        // Exercise the full accumulate-then-finalize path for the reported case.
2696        let func = AggregateFunc::SumFloat64;
2697        let mut acc = accumulable_zero(&func);
2698        acc.plus_equals(&datum_to_accumulator(&func, Datum::from(1.1e31_f64)));
2699        acc.plus_equals(&datum_to_accumulator(&func, Datum::from(-1.1e31_f64)));
2700        let datum = finalize_accum(&func, &acc, Diff::from(2_i64));
2701        assert_eq!(datum, Datum::from(0.0_f64));
2702    }
2703}