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