Skip to main content

mz_compute/render/
top_k.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//! TopK execution logic.
11//!
12//! Consult [TopKPlan] documentation for details.
13
14use std::cell::RefCell;
15use std::collections::BTreeMap;
16use std::rc::Rc;
17
18use columnar::{Columnar, Index};
19use differential_dataflow::AsCollection;
20use differential_dataflow::hashable::Hashable;
21use differential_dataflow::lattice::Lattice;
22use differential_dataflow::operators::arrange::{Arranged, TraceAgent};
23use differential_dataflow::operators::iterate::Variable as SemigroupVariable;
24use differential_dataflow::trace::cursor::{BatchCursor, BatchValOwn};
25use differential_dataflow::trace::{Builder, Cursor, Navigable, Trace};
26use differential_dataflow::{Data, VecCollection};
27use mz_compute_types::dyncfgs::{ENABLE_COMPUTE_TEMPORAL_BUCKETING, TEMPORAL_BUCKETING_SUMMARY};
28use mz_compute_types::plan::ArrangementStrategy;
29use mz_compute_types::plan::scalar::LirScalarExpr;
30use mz_compute_types::plan::top_k::{
31    BasicTopKPlan, MonotonicTop1Plan, MonotonicTopKPlan, TopKPlan,
32};
33use mz_expr::func::CastUint64ToInt64;
34use mz_expr::{BinaryFunc, Columns, Eval, EvalError, UnaryFunc, func, permutation_for_arrangement};
35use mz_ore::cast::CastFrom;
36use mz_ore::soft_assert_or_log;
37use mz_repr::fixed_length::ExtendDatums;
38use mz_repr::{Datum, DatumVec, Diff, ReprScalarType, Row, SharedRow};
39use mz_timely_util::columnar::builder::ColumnBuilder;
40use mz_timely_util::columnation::ColumnationChunker;
41use mz_timely_util::operator::CollectionExt;
42use timely::Container;
43use timely::container::{CapacityContainerBuilder, PushInto};
44use timely::dataflow::channels::pact::Pipeline;
45use timely::dataflow::operators::Operator;
46use timely::dataflow::operators::generic::OutputBuilder;
47use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
48
49use crate::extensions::arrange::{ArrangementSize, KeyCollection, MzArrange};
50use crate::extensions::reduce::{ClearContainer, MzReduce};
51use crate::render::Pairer;
52use crate::render::columnar::{ColCollection, flat_map_datums};
53use crate::render::context::{ArrangementFlavor, CollectionBundle, Context};
54use crate::render::errors::DataflowErrorSer;
55use crate::render::errors::MaybeValidatingRow;
56use crate::typedefs::{ErrBatcher, ErrBuilder, KeyBatcher, MzTimestamp, RowRowSpine, RowSpine};
57use mz_row_spine::{
58    DatumContainer, DatumSeq, RowBatcher, RowBuilder, RowRowBatcher, RowRowBuilder, RowValBuilder,
59    RowValSpine,
60};
61
62// The implementation requires integer timestamps to be able to delay feedback for monotonic inputs.
63impl<'scope, T: crate::render::RenderTimestamp + crate::render::MaybeBucketByTime>
64    Context<'scope, T>
65{
66    pub(crate) fn render_topk(
67        &self,
68        input: CollectionBundle<'scope, T>,
69        top_k_plan: TopKPlan,
70        temporal_bucketing_strategy: ArrangementStrategy,
71    ) -> CollectionBundle<'scope, T> {
72        // `map_topk_key` forms the arrangement key off the edge, so the common input path
73        // carries no `ColumnarToVec`.
74        let (ok_input, err_input) = input
75            .collection
76            .clone()
77            .expect("The unarranged collection doesn't exist.");
78
79        // Bucket the per-row input stream when lowering chose `TemporalBucketing`.
80        // `TopK` builds its own arrangement(s) inside the variants below, bypassing
81        // `ensure_collections`, so the strategy is plumbed through `LirRelationNode::TopK`
82        // rather than inferred at the arrangement site. `apply_bucketing_strategy`
83        // is a no-op for `Direct`.
84        //
85        // Note: a `MonotonicTop1Plan`/`MonotonicTopKPlan` with `must_consolidate =
86        // false` together with `TemporalBucketing` here would mean we install a
87        // bucket operator with no downstream consolidator -- pure overhead. That
88        // combination cannot actually occur: `RelaxMustConsolidate` (which is the
89        // only writer of `must_consolidate = false`) runs only on single-time
90        // dataflows (one-shot peeks / `COPY TO`), and in single-time dataflows
91        // `ExprPrepOneShot` constant-folds `mz_now()` to the dataflow `as_of`
92        // before lowering, so no temporal predicates survive into LIR and
93        // `has_future_updates` is `false` everywhere -- meaning no operator (TopK
94        // included) is ever lowered with `TemporalBucketing`. The assertion below
95        // pins down this invariant.
96        if matches!(
97            temporal_bucketing_strategy,
98            ArrangementStrategy::TemporalBucketing
99        ) {
100            let must_consolidate = match &top_k_plan {
101                TopKPlan::MonotonicTop1(p) => p.must_consolidate,
102                TopKPlan::MonotonicTopK(p) => p.must_consolidate,
103                TopKPlan::Basic(_) => true,
104            };
105            soft_assert_or_log!(
106                must_consolidate,
107                "TopK with `TemporalBucketing` should not have `must_consolidate = false`; \
108                 `RelaxMustConsolidate` only runs on single-time dataflows where \
109                 `mz_now()` has been const-folded and no temporal bucketing is set",
110            );
111        }
112        // Temporal bucketing fires only under `ENABLE_COMPUTE_TEMPORAL_BUCKETING`
113        // and the `TemporalBucketing` strategy.
114        let ok_input = if matches!(
115            temporal_bucketing_strategy,
116            ArrangementStrategy::TemporalBucketing
117        ) && ENABLE_COMPUTE_TEMPORAL_BUCKETING.get(&self.config_set)
118        {
119            let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
120                .get(&self.config_set)
121                .try_into()
122                .expect("must fit");
123            T::maybe_apply_temporal_bucketing(ok_input.inner, self.as_of_frontier.clone(), summary)
124        } else {
125            ok_input
126        };
127
128        // We create a new region to compartmentalize the topk logic.
129        let outer_scope = ok_input.scope();
130        let bundle = outer_scope.clone().region_named("TopK", |inner| {
131            let ok_input = ok_input.enter_region(inner);
132            let mut err_collection = err_input.enter_region(inner);
133
134            // Determine if there should be errors due to limit evaluation; update `err_collection`.
135            // TODO(vmarcos): We evaluate the limit expression below for each input update. There
136            // is an opportunity to do so for every group key instead if the error handling is
137            // integrated with: 1. The intra-timestamp thinning step in monotonic top-k, e.g., by
138            // adding an error output there; 2. The validating reduction on basic top-k
139            // (database-issues#7108).
140
141            match top_k_plan.limit().map(|l| (l.as_literal(), l)) {
142                None => {}
143                Some((Some(Ok(literal)), _))
144                    if literal == Datum::Null || literal.unwrap_int64() >= 0 => {}
145                Some((_, expr)) => {
146                    // Produce errors from limit selectors that error or are
147                    // negative, and nothing from limit selectors that do
148                    // not. Note that even if expr.could_error() is false,
149                    // the expression might still return a negative limit and
150                    // thus needs to be checked.
151                    let expr = expr.clone();
152                    // A literal, non-negative limit skips this branch, so the evaluation
153                    // only runs for column or otherwise fallible limits. It reads datums
154                    // off the borrowed column, so no owned `Row` is built, and it emits
155                    // errors only.
156                    let (_, errors) = flat_map_datums::<
157                        _,
158                        CapacityContainerBuilder<Vec<(Row, T, Diff)>>,
159                        _,
160                    >(
161                        ok_input.clone(),
162                        "TopKLimitCheck",
163                        usize::MAX,
164                        {
165                            let mut datum_vec = mz_repr::DatumVec::new();
166                            move |row_datums, time, diff, _ok_session, err_session| {
167                                let temp_storage = mz_repr::RowArena::new();
168                                // `eval` unifies the lifetimes of the expression, the
169                                // datums, and the arena. Copying the datums into a local
170                                // vec lets that lifetime shrink to this call.
171                                let mut datums = datum_vec.borrow();
172                                datums.extend(row_datums.iter());
173                                match expr.eval(&datums[..], &temp_storage) {
174                                    Ok(l) if l != Datum::Null && l.unwrap_int64() < 0 => {
175                                        err_session.give((EvalError::NegLimit.into(), time, diff));
176                                        1
177                                    }
178                                    Ok(_) => 0,
179                                    Err(e) => {
180                                        err_session.give((e.into(), time, diff));
181                                        1
182                                    }
183                                }
184                            }
185                        },
186                    );
187                    err_collection = err_collection.concat(errors.as_collection());
188                }
189            }
190
191            let bundle = match top_k_plan {
192                TopKPlan::MonotonicTop1(MonotonicTop1Plan {
193                    group_key,
194                    order_key,
195                    arity,
196                    must_consolidate,
197                }) => {
198                    let (arrangement, errs) = self.render_top1_monotonic(
199                        ok_input,
200                        group_key.clone(),
201                        order_key,
202                        arity,
203                        must_consolidate,
204                    );
205                    err_collection = err_collection.concat(errs);
206
207                    // Lowering advertises this group-key arrangement (see the
208                    // `MirRelationExpr::TopK` arm in `lowering.rs`), so deliver it alone,
209                    // mirroring `render_reduce_plan`'s `ArrangementFlavor::Local`. A consumer
210                    // that needs the raw collection reconstructs it from the arrangement via
211                    // the advertised permutation, exactly as for an index arrangement.
212                    let errs: KeyCollection<_, _, _> = err_collection.clone().into();
213                    let err_arrangement = errs
214                        .mz_arrange::<ColumnationChunker<_>, ErrBatcher<_, _>, ErrBuilder<_, _>, _>(
215                            "Arrange bundle err",
216                        );
217                    CollectionBundle::from_columns(
218                        group_key.iter().copied(),
219                        ArrangementFlavor::Local(arrangement, err_arrangement),
220                    )
221                }
222                TopKPlan::MonotonicTopK(MonotonicTopKPlan {
223                    order_key,
224                    group_key,
225                    arity,
226                    mut limit,
227                    must_consolidate,
228                }) => {
229                    // Must permute `limit` to reference `group_key` elements as if in order.
230                    if let Some(expr) = limit.as_mut() {
231                        let mut map = BTreeMap::new();
232                        for (index, column) in group_key.iter().enumerate() {
233                            map.insert(*column, index);
234                        }
235                        expr.permute_map(&map);
236                    }
237
238                    // Map the group key along with the row and consolidate if required to do so.
239                    let ok_scope = ok_input.scope();
240                    let collection =
241                        map_topk_key(ok_input, "MonotonicTopK input", move |datums, _row| {
242                            SharedRow::pack(group_key.iter().map(|i| datums[*i]))
243                        })
244                        .consolidate_named_if::<KeyBatcher<_, _, _>>(
245                            must_consolidate,
246                            "Consolidated MonotonicTopK input",
247                        );
248
249                    // It should be now possible to ensure that we have a monotonic collection.
250                    let error_logger = self.error_logger();
251                    let (collection, errs) = collection.ensure_monotonic(move |data, diff| {
252                        error_logger.log(
253                            "Non-monotonic input to MonotonicTopK",
254                            &format!("data={data:?}, diff={diff}"),
255                        );
256                        let m = "tried to build monotonic top-k on non-monotonic input".into();
257                        (DataflowErrorSer::from(EvalError::Internal(m)), Diff::ONE)
258                    });
259                    err_collection = err_collection.concat(errs);
260
261                    // For monotonic inputs, we are able to thin the input relation in two stages:
262                    // 1. First, we can do an intra-timestamp thinning which has the advantage of
263                    //    being computed in a streaming fashion, even for the initial snapshot.
264                    // 2. Then, we can do inter-timestamp thinning by feeding back negations for
265                    //    any records that have been invalidated.
266                    let collection = if let Some(limit) = limit.clone() {
267                        render_intra_ts_thinning(collection, order_key.clone(), limit)
268                    } else {
269                        collection
270                    };
271
272                    let pairer = Pairer::new(1);
273                    let collection = collection.map(move |(group_row, row)| {
274                        let hash = row.hashed();
275                        let hash_key = pairer.merge(std::iter::once(Datum::from(hash)), &group_row);
276                        (hash_key, row)
277                    });
278
279                    // For monotonic inputs, we are able to retract inputs that can no longer be produced
280                    // as outputs. Any inputs beyond `offset + limit` will never again be produced as
281                    // outputs, and can be removed. The simplest form of this is when `offset == 0` and
282                    // these removable records are those in the input not produced in the output.
283                    // TODO: consider broadening this optimization to `offset > 0` by first filtering
284                    // down to `offset = 0` and `limit = offset + limit`, followed by a finishing act
285                    // of `offset` and `limit`, discarding only the records not produced in the intermediate
286                    // stage.
287                    let delay = std::time::Duration::from_secs(10);
288                    let (retractions_var, retractions) = SemigroupVariable::new(
289                        ok_scope,
290                        <T as crate::render::RenderTimestamp>::system_delay(
291                            delay.try_into().expect("must fit"),
292                        ),
293                    );
294                    let thinned = collection.clone().concat(retractions.negate());
295
296                    // As an additional optimization, we can skip creating the full topk hierachy
297                    // here since we now have an upper bound on the number records due to the
298                    // intra-ts thinning. The maximum number of records per timestamp is
299                    // (num_workers * limit), which we expect to be a small number and so we render
300                    // a single topk stage.
301                    let (result, errs) =
302                        self.build_topk_stage(thinned, order_key, 1u64, 0, limit, arity, false);
303                    // Consolidate the output of `build_topk_stage` because it's not guaranteed to be.
304                    let result = CollectionExt::consolidate_named::<KeyBatcher<_, _, _>>(
305                        result,
306                        "Monotonic TopK final consolidate",
307                    );
308                    retractions_var.set(collection.concat(result.clone().negate()));
309                    soft_assert_or_log!(
310                        errs.is_none(),
311                        "requested no validation, but received error collection"
312                    );
313
314                    CollectionBundle::from_edge(topk_result_to_columnar(result), err_collection)
315                }
316                TopKPlan::Basic(BasicTopKPlan {
317                    group_key,
318                    order_key,
319                    offset,
320                    mut limit,
321                    arity,
322                    buckets,
323                }) => {
324                    // Must permute `limit` to reference `group_key` elements as if in order.
325                    if let Some(expr) = limit.as_mut() {
326                        let mut map = BTreeMap::new();
327                        for (index, column) in group_key.iter().enumerate() {
328                            map.insert(*column, index);
329                        }
330                        expr.permute_map(&map);
331                    }
332
333                    let (oks, errs) = self.build_topk(
334                        ok_input, group_key, order_key, offset, limit, arity, buckets,
335                    );
336                    err_collection = err_collection.concat(errs);
337                    CollectionBundle::from_edge(oks, err_collection)
338                }
339            };
340
341            // Extract the results from the region.
342            bundle.leave_region(outer_scope)
343        });
344
345        bundle
346    }
347
348    /// Constructs a TopK dataflow subgraph.
349    fn build_topk<'s>(
350        &self,
351        collection: ColCollection<'s, T>,
352        group_key: Vec<usize>,
353        order_key: Vec<mz_expr::ColumnOrder>,
354        offset: usize,
355        limit: Option<LirScalarExpr>,
356        arity: usize,
357        buckets: Vec<u64>,
358    ) -> (
359        ColCollection<'s, T>,
360        VecCollection<'s, T, DataflowErrorSer, Diff>,
361    ) {
362        let pairer = Pairer::new(1);
363        let mut collection = map_topk_key(collection, "TopK input", move |datums, row| {
364            let row_hash = row.hashed();
365            let iterator = group_key.iter().map(|i| datums[*i]);
366            pairer.merge(std::iter::once(Datum::from(row_hash)), iterator)
367        });
368
369        let mut validating = true;
370        let mut err_collection: Option<VecCollection<'s, T, _, _>> = None;
371
372        if let Some(mut limit) = limit.clone() {
373            // We may need a new `limit` that reflects the addition of `offset`.
374            // Ideally we compile it down to a literal if at all possible.
375            if offset > 0 {
376                let new_limit = (|| {
377                    let limit = limit.as_literal_int64()?;
378                    let offset = i64::try_from(offset).ok()?;
379                    limit.checked_add(offset)
380                })();
381
382                if let Some(new_limit) = new_limit {
383                    limit =
384                        LirScalarExpr::literal_ok(Datum::Int64(new_limit), ReprScalarType::Int64);
385                } else {
386                    limit = limit.call_binary(
387                        LirScalarExpr::literal_ok(
388                            Datum::UInt64(u64::cast_from(offset)),
389                            ReprScalarType::UInt64,
390                        )
391                        .call_unary(UnaryFunc::CastUint64ToInt64(CastUint64ToInt64)),
392                        BinaryFunc::AddInt64(func::AddInt64),
393                    );
394                }
395            }
396
397            // These bucket values define the shifts that happen to the 64 bit hash of the
398            // record, and should have the properties that 1. there are not too many of them,
399            // and 2. each has a modest difference to the next.
400            for bucket in buckets.into_iter() {
401                // here we do not apply `offset`, but instead restrict ourself with a limit
402                // that includes the offset. We cannot apply `offset` until we perform the
403                // final, complete reduction.
404                let (oks, errs) = self.build_topk_stage(
405                    collection,
406                    order_key.clone(),
407                    bucket,
408                    0,
409                    Some(limit.clone()),
410                    arity,
411                    validating,
412                );
413                collection = oks;
414                if validating {
415                    err_collection = errs;
416                    validating = false;
417                }
418            }
419        }
420
421        // We do a final step, both to make sure that we complete the reduction, and to correctly
422        // apply `offset` to the final group, as we have not yet been applying it to the partially
423        // formed groups.
424        let (oks, errs) = self.build_topk_stage(
425            collection, order_key, 1u64, offset, limit, arity, validating,
426        );
427        // Consolidate the output of `build_topk_stage` because it's not guaranteed to be.
428        let oks =
429            CollectionExt::consolidate_named::<KeyBatcher<_, _, _>>(oks, "TopK final consolidate");
430        collection = oks;
431        if validating {
432            err_collection = errs;
433        }
434        (
435            topk_result_to_columnar(collection),
436            err_collection.expect("at least one stage validated its inputs"),
437        )
438    }
439
440    /// To provide a robust incremental orderby-limit experience, we want to avoid grouping *all*
441    /// records (or even large groups) and then applying the ordering and limit. Instead, a more
442    /// robust approach forms groups of bounded size and applies the offset and limit to each,
443    /// and then increases the sizes of the groups.
444    ///
445    /// Builds a "stage", which uses a finer grouping than is required to reduce the volume of
446    /// updates, and to reduce the amount of work on the critical path for updates. The cost is
447    /// a larger number of arrangements when this optimization does nothing beneficial.
448    ///
449    /// The function accepts a collection of the form `(hash_key, row)`, a modulus it applies to the
450    /// `hash_key`'s hash datum, an `offset` for returning results, and a `limit` to restrict the
451    /// output size. `arity` represents the number of columns in the input data, and
452    /// if `validating` is true, we check for negative multiplicities, which indicate
453    /// an error in the input data.
454    ///
455    /// The output of this function is _not consolidated_.
456    ///
457    /// The dataflow fragment has the following shape:
458    /// ```text
459    ///     | input
460    ///     |
461    ///   arrange
462    ///     |\
463    ///     | \
464    ///     |  reduce
465    ///     |  |
466    ///     concat
467    ///     |
468    ///     | output
469    /// ```
470    /// There are additional map/flat_map operators as well as error demuxing operators, but we're
471    /// omitting them here for the sake of simplicity.
472    fn build_topk_stage<'s>(
473        &self,
474        collection: VecCollection<'s, T, (Row, Row), Diff>,
475        order_key: Vec<mz_expr::ColumnOrder>,
476        modulus: u64,
477        offset: usize,
478        limit: Option<LirScalarExpr>,
479        arity: usize,
480        validating: bool,
481    ) -> (
482        VecCollection<'s, T, (Row, Row), Diff>,
483        Option<VecCollection<'s, T, DataflowErrorSer, Diff>>,
484    ) {
485        // Form appropriate input by updating the `hash` column (first datum in `hash_key`) by
486        // applying `modulus`.
487        let input = collection.map(move |(hash_key, row)| {
488            let mut hash_key_iter = hash_key.iter();
489            let hash = hash_key_iter.next().unwrap().unwrap_uint64() % modulus;
490            let hash_key = SharedRow::pack(std::iter::once(hash.into()).chain(hash_key_iter));
491            (hash_key, row)
492        });
493
494        // If validating: demux errors, otherwise we cannot produce errors.
495        let (input, oks, errs) = if validating {
496            // Build topk stage, produce errors for invalid multiplicities.
497            let (input, stage) = build_topk_negated_stage::<
498                T,
499                RowValBuilder<_, _, _>,
500                RowValSpine<Result<Row, Row>, _, _>,
501            >(&input, order_key, offset, limit, arity);
502            let stage = stage.as_collection(|k, v| (k.to_row(), v.clone()));
503
504            // Demux oks and errors.
505            let error_logger = self.error_logger();
506            type CB<C> = CapacityContainerBuilder<C>;
507            let (oks, errs) = stage.map_fallible::<CB<_>, CB<_>, _, _, _>(
508                "Demuxing Errors",
509                move |(hk, result)| match result {
510                    Err(v) => {
511                        let mut hk_iter = hk.iter();
512                        let h = hk_iter.next().unwrap().unwrap_uint64();
513                        let k = SharedRow::pack(hk_iter);
514                        let message = "Negative multiplicities in TopK";
515                        error_logger.log(message, &format!("k={k:?}, h={h}, v={v:?}"));
516                        Err(EvalError::Internal(message.into()).into())
517                    }
518                    Ok(t) => Ok((hk, t)),
519                },
520            );
521            (input, oks, Some(errs))
522        } else {
523            // Build non-validating topk stage.
524            let (input, stage) =
525                build_topk_negated_stage::<T, RowRowBuilder<_, _>, RowRowSpine<_, _>>(
526                    &input, order_key, offset, limit, arity,
527                );
528            // Turn arrangement into collection.
529            let stage = stage.as_collection(|k, v| (k.to_row(), v.to_row()));
530
531            (input, stage, None)
532        };
533        let input = input.as_collection(|k, v| (k.to_row(), v.to_row()));
534        (oks.concat(input), errs)
535    }
536
537    fn render_top1_monotonic<'s>(
538        &self,
539        collection: ColCollection<'s, T>,
540        group_key: Vec<usize>,
541        order_key: Vec<mz_expr::ColumnOrder>,
542        arity: usize,
543        must_consolidate: bool,
544    ) -> (
545        Arranged<'s, TraceAgent<RowRowSpine<T, Diff>>>,
546        VecCollection<'s, T, DataflowErrorSer, Diff>,
547    ) {
548        // The arrangement we build below is keyed by `group_key` and its value is the winning
549        // row thinned to `thinning`, following the layout `permutation_for_arrangement`
550        // dictates for `Reduce`-style group-key arrangements. A top-1 winner's group-key
551        // columns equal the key by construction, so dropping them from the value is lossless;
552        // consumers reconstruct the full row from key and value via the (unused here)
553        // permutation.
554        let key: Vec<LirScalarExpr> = group_key
555            .iter()
556            .map(|c| LirScalarExpr::column(*c))
557            .collect();
558        let (_permutation, thinning) = permutation_for_arrangement(&key, arity);
559
560        // We can place our rows directly into the diff field, and only keep the relevant one
561        // corresponding to evaluating our aggregate, instead of having to do a hierarchical
562        // reduction. We start by mapping the group key along with the row and consolidating
563        // if required to do so.
564        let collection = map_topk_key(collection, "MonotonicTop1 input", move |datums, _row| {
565            SharedRow::pack(group_key.iter().map(|i| datums[*i]))
566        })
567        .consolidate_named_if::<KeyBatcher<_, _, _>>(
568            must_consolidate,
569            "Consolidated MonotonicTop1 input",
570        );
571
572        // It should be now possible to ensure that we have a monotonic collection and process it.
573        let error_logger = self.error_logger();
574        let (partial, errs) = collection.ensure_monotonic(move |data, diff| {
575            error_logger.log(
576                "Non-monotonic input to MonotonicTop1",
577                &format!("data={data:?}, diff={diff}"),
578            );
579            let m = "tried to build monotonic top-1 on non-monotonic input".into();
580            (EvalError::Internal(m).into(), Diff::ONE)
581        });
582        let partial: KeyCollection<_, _, _> = partial
583            .explode_one(move |(group_key, row)| {
584                (
585                    group_key,
586                    monoids::Top1Monoid {
587                        row,
588                        order_key: order_key.clone(),
589                    },
590                )
591            })
592            .into();
593        let result = partial
594            .mz_arrange::<
595                ColumnationChunker<_>,
596                RowBatcher<_, _>,
597                RowBuilder<_, _>,
598                RowSpine<_, _>,
599            >(
600                "Arranged MonotonicTop1 partial [val: empty]",
601            )
602            .mz_reduce_abelian::<_, RowRowBuilder<_, _>, RowRowSpine<_, _>, _>(
603                "MonotonicTop1",
604                {
605                    let mut datum_vec = mz_repr::DatumVec::new();
606                    move |_key, input, output| {
607                        let accum: &monoids::Top1Monoid = &input[0].1;
608                        let datums = datum_vec.borrow_with(&accum.row);
609                        let value = SharedRow::pack(thinning.iter().map(|i| datums[*i]));
610                        output.push((value, Diff::ONE));
611                    }
612                },
613            );
614        (result, errs)
615    }
616}
617
618/// Forms the `(key, value)` arrangement input the TopK stages consume.
619///
620/// `key` receives the borrowed datums of an input row and the owned row. The value is the
621/// full input row, because every TopK stage carries it through to its output.
622///
623/// The output is a `VecCollection`, since the TopK stages are `Vec`-based, so a `Row` is
624/// decoded per record. That saves the separate `ColumnarToVec` operator and its
625/// intermediate container, not the decode itself, which needs a columnar batcher to push
626/// borrowed rows into. The key is formed from the borrowed datums.
627fn map_topk_key<'s, T, L>(
628    edge: ColCollection<'s, T>,
629    name: &str,
630    mut key: L,
631) -> VecCollection<'s, T, (Row, Row), Diff>
632where
633    T: crate::render::RenderTimestamp,
634    L: FnMut(&[Datum], &Row) -> Row + 'static,
635{
636    let mut builder = OperatorBuilder::new(name.to_string(), edge.inner.scope());
637    let (output, stream) = builder.new_output();
638    let mut output =
639        OutputBuilder::<_, CapacityContainerBuilder<Vec<((Row, Row), T, Diff)>>>::from(output);
640    let mut input = builder.new_input(edge.inner, Pipeline);
641    builder.build(move |_capabilities| {
642        let mut datum_vec = mz_repr::DatumVec::new();
643        move |_frontiers| {
644            let mut output = output.activate();
645            input.for_each(|time, data| {
646                let mut session = output.session_with_builder(&time);
647                for (row, t, d) in data.borrow().into_index_iter() {
648                    let value_row: Row = Columnar::into_owned(row);
649                    let key_row = {
650                        let datums = datum_vec.borrow_with(&value_row);
651                        key(&datums, &value_row)
652                    };
653                    session.give((
654                        (key_row, value_row),
655                        Columnar::into_owned(t),
656                        Columnar::into_owned(d),
657                    ));
658                }
659            });
660        }
661    });
662    stream.as_collection()
663}
664
665/// Drops the hash-key pairing from a consolidated `(hash_key, row)` TopK result.
666///
667/// The hash key is a function of the row and the input is consolidated, so dropping the
668/// key is injective and the output has no within-batch duplicates for a consolidating
669/// builder to fold. Rows are pushed borrowed.
670///
671/// TODO: TopK renders its stages over `Vec` containers, so this encode sits at the very
672/// end of the plan. Pushing columnar containers down through `build_topk` and the
673/// monotonic path would remove it, leaving a projection that drops the hash.
674fn topk_result_to_columnar<'s, T>(
675    collection: VecCollection<'s, T, (Row, Row), Diff>,
676) -> ColCollection<'s, T>
677where
678    T: crate::render::RenderTimestamp,
679{
680    let stream = collection
681        .inner
682        .unary::<ColumnBuilder<(Row, T, Diff)>, _, _, _>(Pipeline, "TopKUnkey", |_cap, _info| {
683            move |input, output| {
684                input.for_each(|time, data| {
685                    let mut session = output.session_with_builder(&time);
686                    for ((_key_hash, row), t, d) in data.drain(..) {
687                        session.give((&row, &t, &d));
688                    }
689                });
690            }
691        });
692    stream.as_collection()
693}
694
695/// Build a stage of a topk reduction. Maintains the _retractions_ of the output instead of emitted
696/// rows. This has the benefit that we have to maintain state proportionally to size of the output
697/// instead of the size of the input.
698///
699/// Returns two arrangements:
700/// * The arranged input data without modifications, and
701/// * the maintained negated output data.
702fn build_topk_negated_stage<'s, T, Bu, Tr>(
703    input: &VecCollection<'s, T, (Row, Row), Diff>,
704    order_key: Vec<mz_expr::ColumnOrder>,
705    offset: usize,
706    limit: Option<LirScalarExpr>,
707    arity: usize,
708) -> (
709    Arranged<'s, TraceAgent<RowRowSpine<T, Diff>>>,
710    Arranged<'s, TraceAgent<Tr>>,
711)
712where
713    T: MzTimestamp,
714    Bu: Builder<
715            Time = T,
716            Input: Container + ClearContainer + PushInto<((Row, BatchValOwn<Tr>), T, Diff)>,
717            Output = Tr::Batch,
718        > + 'static,
719    Tr: Trace<Batch: Navigable, Time = T> + 'static,
720    for<'a> BatchCursor<Tr>: Cursor<
721            Key<'a> = DatumSeq<'a>,
722            KeyContainer = DatumContainer,
723            ValOwn: Data + MaybeValidatingRow<Row, Row>,
724            Time = T,
725            Diff = Diff,
726        >,
727    Arranged<'s, TraceAgent<Tr>>: ArrangementSize,
728{
729    let mut datum_vec = mz_repr::DatumVec::new();
730
731    // We only want to arrange parts of the input that are not part of the actual output
732    // such that `input.concat(&negated_output)` yields the correct TopK
733    // NOTE(vmarcos): The arranged input operator name below is used in the tuning advice
734    // built-in view mz_introspection.mz_expected_group_size_advice.
735    let arranged = input
736        .clone()
737        .mz_arrange::<
738            ColumnationChunker<_>,
739            RowRowBatcher<_, _>,
740            RowRowBuilder<_, _>,
741            RowRowSpine<_, _>,
742        >(
743            "Arranged TopK input",
744        );
745
746    // Eagerly evaluate literal limits.
747    let limit = limit.map(|l| match l.as_literal() {
748        Some(Ok(Datum::Null)) => Ok(Diff::MAX),
749        Some(Ok(d)) => Ok(Diff::from(d.unwrap_int64())),
750        _ => Err(l),
751    });
752
753    let reduced = arranged
754        .clone()
755        .mz_reduce_abelian::<_, Bu, Tr, _>("Reduced TopK input", {
756            move |hash_key, source, target: &mut Vec<(BatchValOwn<Tr>, Diff)>| {
757                // Unpack the limit, either into an integer literal or an expression to evaluate.
758                let limit = match &limit {
759                    Some(Ok(lit)) => Some(*lit),
760                    Some(Err(expr)) => {
761                        // Unpack `key` after skipping the hash and determine the limit.
762                        // If the limit errors, use a zero limit; errors are surfaced elsewhere.
763                        let temp_storage = mz_repr::RowArena::new();
764                        let mut key_datums = datum_vec.borrow();
765                        hash_key.extend_datums(&temp_storage, &mut key_datums, None);
766                        // `key_datums[0]` is the hash; the key columns follow it.
767                        let datum_limit = expr
768                            .eval(&key_datums[1..], &temp_storage)
769                            .unwrap_or(Datum::Int64(0));
770                        Some(match datum_limit {
771                            Datum::Null => Diff::MAX,
772                            d => Diff::from(d.unwrap_int64()),
773                        })
774                    }
775                    None => None,
776                };
777
778                if let Some(err) = BatchValOwn::<Tr>::into_error() {
779                    for (datums, diff) in source.iter() {
780                        if diff.is_positive() {
781                            continue;
782                        }
783                        target.push((err((*datums).to_row()), Diff::ONE));
784                        return;
785                    }
786                }
787
788                // Determine if we must actually shrink the result set.
789                let must_shrink = offset > 0
790                    || limit
791                        .map(|l| source.iter().map(|(_, d)| *d).sum::<Diff>() > l)
792                        .unwrap_or(false);
793                if !must_shrink {
794                    return;
795                }
796
797                // First go ahead and emit all records. Note that we ensure target
798                // has the capacity to hold at least these records, and avoid any
799                // dependencies on the user-provided (potentially unbounded) limit.
800                target.reserve(source.len());
801                for (datums, diff) in source.iter() {
802                    target.push((BatchValOwn::<Tr>::ok((*datums).to_row()), -diff));
803                }
804                // local copies that may count down to zero.
805                let mut offset = offset;
806                let mut limit = limit;
807
808                // The order in which we should produce rows.
809                let mut indexes = (0..source.len()).collect::<Vec<_>>();
810                // We decode the datums once, into a common buffer for efficiency.
811                // Each row should contain `arity` columns; we should check that.
812                let temp_storage = mz_repr::RowArena::new();
813                let mut buffer = Vec::with_capacity(arity * source.len());
814                for (index, (datums, _)) in source.iter().enumerate() {
815                    datums.extend_datums(&temp_storage, &mut buffer, None);
816                    assert_eq!(buffer.len(), arity * (index + 1));
817                }
818                let width = buffer.len() / source.len();
819
820                //todo: use arrangements or otherwise make the sort more performant?
821                indexes.sort_by(|left, right| {
822                    let left = &buffer[left * width..][..width];
823                    let right = &buffer[right * width..][..width];
824                    // Note: source was originally ordered by the u8 array representation
825                    // of rows, but left.cmp(right) uses Datum::cmp.
826                    mz_expr::compare_columns(&order_key, left, right, || left.cmp(right))
827                });
828
829                // We now need to lay out the data in order of `buffer`, but respecting
830                // the `offset` and `limit` constraints.
831                for index in indexes.into_iter() {
832                    let (datums, mut diff) = source[index];
833                    if !diff.is_positive() {
834                        continue;
835                    }
836                    // If we are still skipping early records ...
837                    if offset > 0 {
838                        let to_skip =
839                            std::cmp::min(offset, usize::try_from(diff.into_inner()).unwrap());
840                        offset -= to_skip;
841                        diff -= Diff::try_from(to_skip).unwrap();
842                    }
843                    // We should produce at most `limit` records.
844                    if let Some(limit) = &mut limit {
845                        diff = std::cmp::min(diff, Diff::from(*limit));
846                        *limit -= diff;
847                    }
848                    // Output the indicated number of rows.
849                    if diff.is_positive() {
850                        // Emit retractions for the elements actually part of
851                        // the set of TopK elements.
852                        target.push((BatchValOwn::<Tr>::ok(datums.to_row()), diff));
853                    }
854                }
855            }
856        });
857    (arranged, reduced)
858}
859
860fn render_intra_ts_thinning<'s, T>(
861    collection: VecCollection<'s, T, (Row, Row), Diff>,
862    order_key: Vec<mz_expr::ColumnOrder>,
863    limit: LirScalarExpr,
864) -> VecCollection<'s, T, (Row, Row), Diff>
865where
866    T: timely::progress::Timestamp + Lattice,
867{
868    let mut datum_vec = mz_repr::DatumVec::new();
869
870    let mut aggregates = BTreeMap::new();
871    let shared = Rc::new(RefCell::new(monoids::Top1MonoidShared {
872        order_key,
873        left: DatumVec::new(),
874        right: DatumVec::new(),
875    }));
876    collection
877        .inner
878        .unary_notify(
879            Pipeline,
880            "TopKIntraTimeThinning",
881            [],
882            move |input, output, notificator| {
883                input.for_each_time(|time, data| {
884                    let agg_time = aggregates
885                        .entry(time.time().clone())
886                        .or_insert_with(BTreeMap::new);
887                    for ((grp_row, row), record_time, diff) in data.flat_map(|data| data.drain(..))
888                    {
889                        let monoid = monoids::Top1MonoidLocal {
890                            row,
891                            shared: Rc::clone(&shared),
892                        };
893
894                        // Evalute the limit, first as a constant and then against the key if needed.
895                        let limit = if let Some(l) = limit.as_literal_int64() {
896                            l
897                        } else {
898                            let temp_storage = mz_repr::RowArena::new();
899                            let key_datums = datum_vec.borrow_with(&grp_row);
900                            // Unpack `key` and determine the limit.
901                            // If the limit errors, use a zero limit; errors are surfaced elsewhere.
902                            let datum_limit = limit
903                                .eval(&key_datums, &temp_storage)
904                                .unwrap_or(mz_repr::Datum::Int64(0));
905                            if datum_limit == Datum::Null {
906                                i64::MAX
907                            } else {
908                                datum_limit.unwrap_int64()
909                            }
910                        };
911
912                        let topk = agg_time
913                            .entry((grp_row, record_time))
914                            .or_insert_with(move || topk_agg::TopKBatch::new(limit));
915                        topk.update(monoid, diff.into_inner());
916                    }
917                    notificator.notify_at(time.retain(0));
918                });
919
920                notificator.for_each(|time, _, _| {
921                    if let Some(aggs) = aggregates.remove(time.time()) {
922                        let mut session = output.session(&time);
923                        for ((grp_row, record_time), topk) in aggs {
924                            session.give_iterator(topk.into_iter().map(|(monoid, diff)| {
925                                (
926                                    (grp_row.clone(), monoid.into_row()),
927                                    record_time.clone(),
928                                    diff.into(),
929                                )
930                            }))
931                        }
932                    }
933                });
934            },
935        )
936        .as_collection()
937}
938
939/// Types for in-place intra-ts aggregation of monotonic streams.
940pub mod topk_agg {
941    use differential_dataflow::consolidation;
942    use smallvec::SmallVec;
943
944    // TODO: This struct looks a lot like ChangeBatch and indeed its code is a modified version of
945    // that. It would be nice to find a way to reuse some or all of the code from there.
946    //
947    // Additionally, because we're calling into DD's consolidate method we are forced to work with
948    // the `Ord` trait which for the usage we do above means that we need to clone the `order_key`
949    // for each record. It would be nice to also remove the need for cloning that piece of data
950    pub struct TopKBatch<T> {
951        updates: SmallVec<[(T, i64); 16]>,
952        clean: usize,
953        limit: i64,
954    }
955
956    impl<T: Ord> TopKBatch<T> {
957        pub fn new(limit: i64) -> Self {
958            Self {
959                updates: SmallVec::new(),
960                clean: 0,
961                limit,
962            }
963        }
964
965        /// Adds a new update, for `item` with `value`.
966        ///
967        /// This could be optimized to perform compaction when the number of "dirty" elements exceeds
968        /// half the length of the list, which would keep the total footprint within reasonable bounds
969        /// even under an arbitrary number of updates. This has a cost, and it isn't clear whether it
970        /// is worth paying without some experimentation.
971        #[inline]
972        pub fn update(&mut self, item: T, value: i64) {
973            self.updates.push((item, value));
974            self.maintain_bounds();
975        }
976
977        /// Compact the internal representation.
978        ///
979        /// This method sort `self.updates` and consolidates elements with equal item, discarding
980        /// any whose accumulation is zero. It is optimized to only do this if the number of dirty
981        /// elements is non-zero.
982        #[inline]
983        pub fn compact(&mut self) {
984            if self.clean < self.updates.len() && self.updates.len() > 1 {
985                let len = consolidation::consolidate_slice(&mut self.updates);
986                self.updates.truncate(len);
987
988                // We can now retain only the first K records and throw away everything else
989                let mut limit = self.limit;
990                self.updates.retain(|x| {
991                    if limit > 0 {
992                        limit -= x.1;
993                        true
994                    } else {
995                        false
996                    }
997                });
998                // By the end of the loop above `limit` will either be:
999                // (a) Positive, in which case all updates were retained;
1000                // (b) Zero, in which case we discarded all updates after limit became zero;
1001                // (c) Negative, in which case the last record we retained had more copies
1002                // than necessary. In this latter case, we need to do one final adjustment
1003                // of the diff field of the last record so that the total sum of the diffs
1004                // in the batch is K.
1005                if limit < 0 {
1006                    if let Some(item) = self.updates.last_mut() {
1007                        // We are subtracting the limit *negated*, therefore we are subtracting a value
1008                        // that is *greater* than or equal to zero, which represents the excess.
1009                        item.1 -= -limit;
1010                    }
1011                }
1012            }
1013            self.clean = self.updates.len();
1014        }
1015
1016        /// Maintain the bounds of pending (non-compacted) updates versus clean (compacted) data.
1017        /// This function tries to minimize work by only compacting if enough work has accumulated.
1018        fn maintain_bounds(&mut self) {
1019            // if we have more than 32 elements and at least half of them are not clean, compact
1020            if self.updates.len() > 32 && self.updates.len() >> 1 >= self.clean {
1021                self.compact()
1022            }
1023        }
1024    }
1025
1026    impl<T: Ord> IntoIterator for TopKBatch<T> {
1027        type Item = (T, i64);
1028        type IntoIter = smallvec::IntoIter<[(T, i64); 16]>;
1029
1030        fn into_iter(mut self) -> Self::IntoIter {
1031            self.compact();
1032            self.updates.into_iter()
1033        }
1034    }
1035}
1036
1037/// Monoids for in-place compaction of monotonic streams.
1038pub mod monoids {
1039    use std::cell::RefCell;
1040    use std::cmp::Ordering;
1041    use std::hash::{Hash, Hasher};
1042    use std::rc::Rc;
1043
1044    use columnation::{Columnation, Region};
1045    use differential_dataflow::difference::{IsZero, Multiply, Semigroup};
1046    use mz_expr::ColumnOrder;
1047    use mz_repr::{DatumVec, Diff, Row};
1048    use serde::{Deserialize, Serialize};
1049
1050    /// A monoid containing a row and an ordering.
1051    #[derive(Eq, PartialEq, Debug, Serialize, Deserialize, Hash, Default)]
1052    pub struct Top1Monoid {
1053        pub row: Row,
1054        pub order_key: Vec<ColumnOrder>,
1055    }
1056
1057    impl Clone for Top1Monoid {
1058        #[inline]
1059        fn clone(&self) -> Self {
1060            Self {
1061                row: self.row.clone(),
1062                order_key: self.order_key.clone(),
1063            }
1064        }
1065
1066        #[inline]
1067        fn clone_from(&mut self, source: &Self) {
1068            self.row.clone_from(&source.row);
1069            self.order_key.clone_from(&source.order_key);
1070        }
1071    }
1072
1073    impl Multiply<Diff> for Top1Monoid {
1074        type Output = Self;
1075
1076        fn multiply(self, factor: &Diff) -> Self {
1077            // Multiplication in Top1Monoid is idempotent, and its
1078            // users must ascertain its monotonicity beforehand
1079            // (typically with ensure_monotonic) since it has no zero
1080            // value for us to use here.
1081            assert!(factor.is_positive());
1082            self
1083        }
1084    }
1085
1086    impl Ord for Top1Monoid {
1087        fn cmp(&self, other: &Self) -> Ordering {
1088            mz_ore::soft_assert_eq_no_log!(self.order_key, other.order_key);
1089
1090            // It might be nice to cache this row decoding like the non-monotonic codepath, but we'd
1091            // have to store the decoded Datums in the same struct as the Row, which gets tricky.
1092            let left: Vec<_> = self.row.unpack();
1093            let right: Vec<_> = other.row.unpack();
1094            mz_expr::compare_columns(&self.order_key, &left, &right, || left.cmp(&right))
1095        }
1096    }
1097    impl PartialOrd for Top1Monoid {
1098        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1099            Some(self.cmp(other))
1100        }
1101    }
1102
1103    impl Semigroup for Top1Monoid {
1104        fn plus_equals(&mut self, rhs: &Self) {
1105            let cmp = (*self).cmp(rhs);
1106            // NB: Reminder that TopK returns the _minimum_ K items.
1107            if cmp == Ordering::Greater {
1108                self.clone_from(rhs);
1109            }
1110        }
1111    }
1112
1113    impl IsZero for Top1Monoid {
1114        fn is_zero(&self) -> bool {
1115            false
1116        }
1117    }
1118
1119    impl Columnation for Top1Monoid {
1120        type InnerRegion = Top1MonoidRegion;
1121    }
1122
1123    #[derive(Default)]
1124    pub struct Top1MonoidRegion {
1125        row_region: <Row as Columnation>::InnerRegion,
1126        order_key_region: <Vec<ColumnOrder> as Columnation>::InnerRegion,
1127    }
1128
1129    impl Region for Top1MonoidRegion {
1130        type Item = Top1Monoid;
1131
1132        unsafe fn copy(&mut self, item: &Self::Item) -> Self::Item {
1133            let row = unsafe { self.row_region.copy(&item.row) };
1134            let order_key = unsafe { self.order_key_region.copy(&item.order_key) };
1135            Self::Item { row, order_key }
1136        }
1137
1138        fn clear(&mut self) {
1139            self.row_region.clear();
1140            self.order_key_region.clear();
1141        }
1142
1143        fn reserve_items<'a, I>(&mut self, items1: I)
1144        where
1145            Self: 'a,
1146            I: Iterator<Item = &'a Self::Item> + Clone,
1147        {
1148            let items2 = items1.clone();
1149            self.row_region
1150                .reserve_items(items1.into_iter().map(|s| &s.row));
1151            self.order_key_region
1152                .reserve_items(items2.into_iter().map(|s| &s.order_key));
1153        }
1154
1155        fn reserve_regions<'a, I>(&mut self, regions1: I)
1156        where
1157            Self: 'a,
1158            I: Iterator<Item = &'a Self> + Clone,
1159        {
1160            let regions2 = regions1.clone();
1161            self.row_region
1162                .reserve_regions(regions1.into_iter().map(|s| &s.row_region));
1163            self.order_key_region
1164                .reserve_regions(regions2.into_iter().map(|s| &s.order_key_region));
1165        }
1166
1167        fn heap_size(&self, mut callback: impl FnMut(usize, usize)) {
1168            self.row_region.heap_size(&mut callback);
1169            self.order_key_region.heap_size(callback);
1170        }
1171    }
1172
1173    /// A shared portion of a thread-local top-1 monoid implementation.
1174    #[derive(Debug)]
1175    pub struct Top1MonoidShared {
1176        pub order_key: Vec<ColumnOrder>,
1177        pub left: DatumVec,
1178        pub right: DatumVec,
1179    }
1180
1181    /// A monoid containing a row and a shared pointer to a shared structure.
1182    /// Only suitable for thread-local aggregations.
1183    #[derive(Debug, Clone)]
1184    pub struct Top1MonoidLocal {
1185        pub row: Row,
1186        pub shared: Rc<RefCell<Top1MonoidShared>>,
1187    }
1188
1189    impl Top1MonoidLocal {
1190        pub fn into_row(self) -> Row {
1191            self.row
1192        }
1193    }
1194
1195    impl PartialEq for Top1MonoidLocal {
1196        fn eq(&self, other: &Self) -> bool {
1197            self.row.eq(&other.row)
1198        }
1199    }
1200
1201    impl Eq for Top1MonoidLocal {}
1202
1203    impl Hash for Top1MonoidLocal {
1204        fn hash<H: Hasher>(&self, state: &mut H) {
1205            self.row.hash(state);
1206        }
1207    }
1208
1209    impl Ord for Top1MonoidLocal {
1210        fn cmp(&self, other: &Self) -> Ordering {
1211            mz_ore::soft_assert_no_log!(Rc::ptr_eq(&self.shared, &other.shared));
1212            let Top1MonoidShared {
1213                left,
1214                right,
1215                order_key,
1216            } = &mut *self.shared.borrow_mut();
1217
1218            let left = left.borrow_with(&self.row);
1219            let right = right.borrow_with(&other.row);
1220            mz_expr::compare_columns(order_key, &left, &right, || left.cmp(&right))
1221        }
1222    }
1223
1224    impl PartialOrd for Top1MonoidLocal {
1225        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1226            Some(self.cmp(other))
1227        }
1228    }
1229
1230    impl Semigroup for Top1MonoidLocal {
1231        fn plus_equals(&mut self, rhs: &Self) {
1232            let cmp = (*self).cmp(rhs);
1233            // NB: Reminder that TopK returns the _minimum_ K items.
1234            if cmp == Ordering::Greater {
1235                self.clone_from(rhs);
1236            }
1237        }
1238    }
1239
1240    impl IsZero for Top1MonoidLocal {
1241        fn is_zero(&self) -> bool {
1242            false
1243        }
1244    }
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249    use differential_dataflow::input::Input;
1250    use mz_repr::{Datum, Timestamp};
1251    use timely::dataflow::operators::Capture;
1252    use timely::dataflow::operators::capture::{Event, Extract};
1253
1254    use super::*;
1255    use crate::render::columnar::{columnar_to_vec, vec_to_columnar};
1256
1257    type KeyedUpdate = ((Row, Row), Timestamp, Diff);
1258    type Captured = std::sync::mpsc::Receiver<Event<Timestamp, Vec<KeyedUpdate>>>;
1259
1260    fn extract_sorted(captured: Captured) -> Vec<KeyedUpdate> {
1261        let mut updates: Vec<_> = captured
1262            .extract()
1263            .into_iter()
1264            .flat_map(|(_, data)| data)
1265            .collect();
1266        updates.sort();
1267        updates
1268    }
1269
1270    /// Rows across several timestamps, including two `-1` diffs so a negative diff is
1271    /// decoded. Those retract at a `(row, time)` with no matching insertion, so the
1272    /// `InputSession`'s pre-send consolidation does not cancel them out.
1273    fn test_input() -> Vec<(Row, u64, Diff)> {
1274        vec![
1275            (
1276                Row::pack_slice(&[Datum::Int32(1), Datum::String("a")]),
1277                0,
1278                Diff::ONE,
1279            ),
1280            (
1281                Row::pack_slice(&[Datum::Int32(2), Datum::String("b")]),
1282                1,
1283                Diff::ONE,
1284            ),
1285            (
1286                Row::pack_slice(&[Datum::Int32(1), Datum::String("a")]),
1287                2,
1288                Diff::ONE,
1289            ),
1290            (
1291                Row::pack_slice(&[Datum::Int32(3), Datum::Null]),
1292                2,
1293                Diff::ONE,
1294            ),
1295            (
1296                Row::pack_slice(&[Datum::Int32(2), Datum::String("b")]),
1297                2,
1298                -Diff::ONE,
1299            ),
1300            (
1301                Row::pack_slice(&[Datum::Int32(4), Datum::String("d")]),
1302                1,
1303                -Diff::ONE,
1304            ),
1305        ]
1306    }
1307
1308    /// Runs `map_topk_key` with the hash-and-group key `build_topk` forms, returning the
1309    /// sorted `(key, value)` updates.
1310    fn run_columnar(input: Vec<(Row, u64, Diff)>) -> Vec<KeyedUpdate> {
1311        let captured = timely::execute_directly(move |worker| {
1312            worker.dataflow::<Timestamp, _, _>(|scope| {
1313                let (mut handle, collection) = scope.new_collection();
1314                let pairer = Pairer::new(1);
1315                let group_key = [0usize];
1316                let keyed =
1317                    map_topk_key(vec_to_columnar(collection), "test", move |datums, row| {
1318                        let hash = row.hashed();
1319                        let iterator = group_key.iter().map(|i| datums[*i]);
1320                        pairer.merge(std::iter::once(Datum::from(hash)), iterator)
1321                    });
1322                let captured = keyed.inner.capture();
1323                for (row, time, diff) in input {
1324                    handle.update_at(row, Timestamp::from(time), diff);
1325                }
1326                handle.advance_to(Timestamp::from(3_u64));
1327                handle.flush();
1328                captured
1329            })
1330        });
1331        extract_sorted(captured)
1332    }
1333
1334    /// The key closure is infallible, so there is no fallible-key path here.
1335    #[mz_ore::test]
1336    fn map_topk_key_forms_key() {
1337        let updates = run_columnar(test_input());
1338        assert!(!updates.is_empty());
1339        // Retractions reach the operator, so a negative diff was decoded via
1340        // `Columnar::into_owned`.
1341        assert!(updates.iter().any(|(_, _, d)| *d < Diff::ZERO));
1342        // The value is the full input row. The key is `(hash, group_column)`, so
1343        // the group component mirrors column 0 of the value row.
1344        for ((key, value), _t, _d) in &updates {
1345            let key_datums: Vec<_> = key.iter().collect();
1346            let value_datums: Vec<_> = value.iter().collect();
1347            assert_eq!(key_datums.len(), 2);
1348            assert_eq!(key_datums[1], value_datums[0]);
1349        }
1350    }
1351
1352    #[mz_ore::test]
1353    fn topk_result_to_columnar_drops_key() {
1354        let key = Row::pack_slice(&[Datum::Int64(7)]);
1355        let rows = vec![
1356            (
1357                (key.clone(), Row::pack_slice(&[Datum::Int32(1)])),
1358                0u64,
1359                Diff::ONE,
1360            ),
1361            (
1362                (key.clone(), Row::pack_slice(&[Datum::Int32(2)])),
1363                1u64,
1364                Diff::ONE,
1365            ),
1366            // Retracts at a `(row, time)` with no insertion, so the `InputSession`'s
1367            // pre-send consolidation does not cancel it out.
1368            (
1369                (key.clone(), Row::pack_slice(&[Datum::Int32(1)])),
1370                2u64,
1371                -Diff::ONE,
1372            ),
1373        ];
1374        let mut expected: Vec<(Row, Timestamp, Diff)> = rows
1375            .iter()
1376            .map(|((_, v), t, d)| (v.clone(), Timestamp::from(*t), *d))
1377            .collect();
1378        expected.sort();
1379
1380        let captured = timely::execute_directly(move |worker| {
1381            worker.dataflow::<Timestamp, _, _>(|scope| {
1382                let (mut handle, collection) = scope.new_collection();
1383                let edge = topk_result_to_columnar(collection);
1384                let captured = columnar_to_vec(edge).inner.capture();
1385                for (kv, time, diff) in rows {
1386                    handle.update_at(kv, Timestamp::from(time), diff);
1387                }
1388                handle.advance_to(Timestamp::from(3u64));
1389                handle.flush();
1390                captured
1391            })
1392        });
1393
1394        let mut got: Vec<(Row, Timestamp, Diff)> = captured
1395            .extract()
1396            .into_iter()
1397            .flat_map(|(_, data)| data)
1398            .collect();
1399        got.sort();
1400        assert_eq!(got, expected);
1401    }
1402}