Skip to main content

mz_compute/render/
context.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//! Management of dataflow-local state, like arrangements, while building a
11//! dataflow.
12
13use std::collections::BTreeMap;
14use std::rc::Rc;
15
16use columnar::{Columnar, Index};
17use differential_dataflow::consolidation::ConsolidatingContainerBuilder;
18use differential_dataflow::operators::arrange::Arranged;
19use differential_dataflow::trace::cursor::{BatchCursor, BatchKey, BatchVal};
20use differential_dataflow::trace::implementations::BatchContainer;
21use differential_dataflow::trace::{Cursor, Navigable, TraceReader};
22use differential_dataflow::{AsCollection, VecCollection};
23use mz_compute_types::dataflows::DataflowDescription;
24use mz_compute_types::dyncfgs::TEMPORAL_BUCKETING_SUMMARY;
25use mz_compute_types::plan::scalar::LirScalarExpr;
26use mz_compute_types::plan::{ArrangementStrategy, AvailableCollections};
27use mz_dyncfg::ConfigSet;
28use mz_expr::{Eval, Id, MfpPlan};
29use mz_ore::soft_assert_or_log;
30use mz_repr::fixed_length::ExtendDatums;
31use mz_repr::{DatumVec, DatumVecBorrow, Diff, GlobalId, Row, RowArena, SharedRow, StableRow};
32use mz_storage_types::controller::CollectionMetadata;
33use mz_timely_util::columnar::Column;
34use mz_timely_util::columnar::batcher;
35use mz_timely_util::columnar::builder::ColumnBuilder;
36use mz_timely_util::columnar::chunk::{
37    AccountedChunkBatcher, ChunkChunker, ComputeSpill, UnchunkBuilder,
38};
39use mz_timely_util::columnar::consolidate::ConsolidatingColumnBuilder;
40use mz_timely_util::columnar::{Col2ValBatcher, Col2ValColBatcher, columnar_exchange};
41use mz_timely_util::columnation::ColumnationChunker;
42use timely::ContainerBuilder;
43use timely::container::NoopBuilder;
44use timely::dataflow::channels::pact::{ExchangeCore, Pipeline};
45use timely::dataflow::operators::Capability;
46use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
47use timely::dataflow::operators::generic::{OutputBuilder, OutputBuilderSession};
48use timely::dataflow::{Scope, Stream};
49use timely::progress::operate::FrontierInterest;
50use timely::progress::{Antichain, Timestamp};
51
52use crate::compute_state::ComputeState;
53use crate::extensions::arrange::{ArrangementBatcher, KeyCollection, MzArrange, MzArrangeCore};
54use crate::extensions::reduce::MzReduce;
55use crate::render::columnar::{ColCollection, flat_map_datums};
56use crate::render::errors::{DataflowErrorSer, ErrorLogger};
57use crate::render::{LinearJoinSpec, MaybeBucketByTime, RenderTimestamp};
58use crate::typedefs::{
59    ErrAgent, ErrBatcher, ErrBuilder, ErrEnter, ErrSpine, RowRowAgent, RowRowEnter, RowRowSpine,
60};
61use mz_row_spine::{RowRowBuilder, RowRowColPagedBuilder};
62
63/// Dataflow-local collections and arrangements.
64///
65/// A context means to wrap available data assets and present them in an easy-to-use manner.
66/// These assets include dataflow-local collections and arrangements, as well as imported
67/// arrangements from outside the dataflow.
68///
69/// Context has a timestamp type `T`, which is the timestamp used by the scope in question.
70pub struct Context<'scope, T: RenderTimestamp> {
71    /// The scope within which all managed collections exist.
72    ///
73    /// It is an error to add any collections not contained in this scope.
74    pub(crate) scope: Scope<'scope, T>,
75    /// The debug name of the dataflow associated with this context.
76    pub debug_name: String,
77    /// The Timely ID of the dataflow associated with this context.
78    pub dataflow_id: usize,
79    /// The collection IDs of exports of the dataflow associated with this context.
80    pub export_ids: Vec<GlobalId>,
81    /// Frontier before which updates should not be emitted.
82    ///
83    /// We *must* apply it to sinks, to ensure correct outputs.
84    /// We *should* apply it to sources and imported traces, because it improves performance.
85    pub as_of_frontier: Antichain<mz_repr::Timestamp>,
86    /// Frontier after which updates should not be emitted.
87    /// Used to limit the amount of work done when appropriate.
88    pub until: Antichain<mz_repr::Timestamp>,
89    /// Bindings of identifiers to collections.
90    pub bindings: BTreeMap<Id, CollectionBundle<'scope, T>>,
91    /// The logger, from Timely's logging framework, if logs are enabled.
92    pub(super) compute_logger: Option<crate::logging::compute::Logger>,
93    /// Specification for rendering linear joins.
94    pub(super) linear_join_spec: LinearJoinSpec,
95    /// The expiration time for dataflows in this context. The output's frontier should never advance
96    /// past this frontier, except the empty frontier.
97    pub dataflow_expiration: Antichain<mz_repr::Timestamp>,
98    /// The config set for this context.
99    pub config_set: Rc<ConfigSet>,
100}
101
102impl<'scope, T: RenderTimestamp> Context<'scope, T> {
103    /// Creates a new empty Context.
104    pub fn for_dataflow_in<Plan, O>(
105        dataflow: &DataflowDescription<Plan, CollectionMetadata, O>,
106        scope: Scope<'scope, T>,
107        compute_state: &ComputeState,
108        until: Antichain<mz_repr::Timestamp>,
109        dataflow_expiration: Antichain<mz_repr::Timestamp>,
110    ) -> Self {
111        use mz_ore::collections::CollectionExt as IteratorExt;
112        let dataflow_id = *scope.addr().into_first();
113        let as_of_frontier = dataflow
114            .as_of
115            .clone()
116            .unwrap_or_else(|| Antichain::from_elem(Timestamp::minimum()));
117
118        let export_ids = dataflow.export_ids().collect();
119
120        // Skip compute event logging for transient dataflows. We do this to avoid overhead for
121        // slow-path peeks, but it also affects subscribes. For now that seems fine, but we may
122        // want to reconsider in the future.
123        let compute_logger = if dataflow.is_transient() {
124            None
125        } else {
126            compute_state.compute_logger.clone()
127        };
128
129        Self {
130            scope,
131            debug_name: dataflow.debug_name.clone(),
132            dataflow_id,
133            export_ids,
134            as_of_frontier,
135            until,
136            bindings: BTreeMap::new(),
137            compute_logger,
138            linear_join_spec: compute_state.linear_join_spec,
139            dataflow_expiration,
140            config_set: Rc::clone(&compute_state.worker_config),
141        }
142    }
143}
144
145impl<'scope, T: RenderTimestamp> Context<'scope, T> {
146    /// Insert a collection bundle by an identifier.
147    ///
148    /// This is expected to be used to install external collections (sources, indexes, other views),
149    /// as well as for `Let` bindings of local collections.
150    pub fn insert_id(
151        &mut self,
152        id: Id,
153        collection: CollectionBundle<'scope, T>,
154    ) -> Option<CollectionBundle<'scope, T>> {
155        self.bindings.insert(id, collection)
156    }
157    /// Remove a collection bundle by an identifier.
158    ///
159    /// The primary use of this method is uninstalling `Let` bindings.
160    pub fn remove_id(&mut self, id: Id) -> Option<CollectionBundle<'scope, T>> {
161        self.bindings.remove(&id)
162    }
163    /// Melds a collection bundle to whatever exists.
164    pub fn update_id(&mut self, id: Id, collection: CollectionBundle<'scope, T>) {
165        if !self.bindings.contains_key(&id) {
166            self.bindings.insert(id, collection);
167        } else {
168            let binding = self
169                .bindings
170                .get_mut(&id)
171                .expect("Binding verified to exist");
172            if collection.collection.is_some() {
173                binding.collection = collection.collection;
174            }
175            for (key, flavor) in collection.arranged.into_iter() {
176                binding.arranged.insert(key, flavor);
177            }
178        }
179    }
180    /// Look up a collection bundle by an identifier.
181    pub fn lookup_id(&self, id: Id) -> Option<CollectionBundle<'scope, T>> {
182        self.bindings.get(&id).cloned()
183    }
184
185    pub(super) fn error_logger(&self) -> ErrorLogger {
186        ErrorLogger::new(self.debug_name.clone())
187    }
188}
189
190impl<'scope, T: RenderTimestamp> Context<'scope, T> {
191    /// Brings the underlying arrangements and collections into a region.
192    pub fn enter_region<'a>(
193        &self,
194        region: Scope<'a, T>,
195        bindings: Option<&std::collections::BTreeSet<Id>>,
196    ) -> Context<'a, T> {
197        let bindings = self
198            .bindings
199            .iter()
200            .filter(|(key, _)| bindings.as_ref().map(|b| b.contains(key)).unwrap_or(true))
201            .map(|(key, bundle)| (*key, bundle.enter_region(region)))
202            .collect();
203
204        Context {
205            scope: region,
206            debug_name: self.debug_name.clone(),
207            dataflow_id: self.dataflow_id.clone(),
208            export_ids: self.export_ids.clone(),
209            as_of_frontier: self.as_of_frontier.clone(),
210            until: self.until.clone(),
211            compute_logger: self.compute_logger.clone(),
212            linear_join_spec: self.linear_join_spec.clone(),
213            bindings,
214            dataflow_expiration: self.dataflow_expiration.clone(),
215            config_set: Rc::clone(&self.config_set),
216        }
217    }
218}
219
220/// Describes flavor of arrangement: local or imported trace.
221#[derive(Clone)]
222pub enum ArrangementFlavor<'scope, T: RenderTimestamp> {
223    /// A dataflow-local arrangement.
224    Local(
225        Arranged<'scope, RowRowAgent<T, Diff>>,
226        Arranged<'scope, ErrAgent<T, Diff>>,
227    ),
228    /// An imported trace from outside the dataflow.
229    ///
230    /// The `GlobalId` identifier exists so that exports of this same trace
231    /// can refer back to and depend on the original instance.
232    Trace(
233        GlobalId,
234        Arranged<'scope, RowRowEnter<mz_repr::Timestamp, Diff, T>>,
235        Arranged<'scope, ErrEnter<mz_repr::Timestamp, T>>,
236    ),
237}
238
239impl<'scope, T: RenderTimestamp> ArrangementFlavor<'scope, T> {
240    /// Constructs and applies logic to elements of `self` and returns the results.
241    ///
242    /// The `logic` callback receives a borrow of the decoded datum vector, a timestamp, a
243    /// diff, and two output sessions: one for `ok` updates of type `(D, T, Diff)` and one for
244    /// MFP-style `DataflowErrorSer` updates. It must return the number of records *produced*
245    /// (written to either session), not the number of input tuples consumed.
246    ///
247    /// # Fuel
248    ///
249    /// The operator accumulates the returned counts as fuel and yields when the total reaches
250    /// an internal refuel threshold. The metric is output-produced (not input-consumed) on
251    /// purpose: it regulates two asymmetric pressures.
252    ///
253    /// * **Drain inputs.** The operator holds a clone of each pending `Batch` until its work
254    ///   item pops; we want to release that memory back to the upstream arrangement as soon
255    ///   as possible. A `filter(false)` MFP returns 0 for every tuple, so fuel never trips
256    ///   and the cursor runs to end-of-batch in one activation.
257    /// * **Throttle outputs.** A `map("1KB-string")` MFP produces large records per input;
258    ///   stopping when emit count hits the threshold caps how much data a single activation
259    ///   dumps on the next operator.
260    ///
261    /// The refuel constant is a pragmatic compromise: large enough to be a non-event in
262    /// steady-state, small enough that one activation can't flood downstream. There is no
263    /// universal value across MFP shapes.
264    ///
265    /// If `key` is set, this is a promise that `logic` will produce no results on
266    /// records for which the key does not evaluate to the value. This is used to
267    /// leap directly to exactly those records.
268    ///
269    /// The `max_demand` parameter limits the number of columns decoded from the
270    /// input. Only the first `max_demand` columns are decoded. Pass `usize::MAX` to
271    /// decode all columns.
272    pub fn flat_map<DCB, L>(
273        &self,
274        key: Option<&Row>,
275        max_demand: usize,
276        logic: L,
277    ) -> (
278        Stream<'scope, T, DCB::Container>,
279        VecCollection<'scope, T, DataflowErrorSer, Diff>,
280    )
281    where
282        DCB: ContainerBuilder,
283        L: for<'a, 'b> FnMut(
284                &'a mut DatumVecBorrow<'b>,
285                T,
286                Diff,
287                &mut Session<T, DCB>,
288                &mut Session<T, ECB<T>>,
289            ) -> usize
290            + 'static,
291    {
292        // `logic` is passed straight through to `flat_map_core_fallible`, which owns the per-row
293        // decode (and the activation-scoped arena it decodes into).
294        match &self {
295            ArrangementFlavor::Local(oks, errs) => {
296                let (oks, mfp_errs) = CollectionBundle::<T>::flat_map_core_fallible::<_, DCB, _>(
297                    oks.clone(),
298                    key,
299                    max_demand,
300                    logic,
301                    REFUEL,
302                );
303                let errs = errs.clone().as_collection(|k, &()| k.clone());
304                let errs = errs.concat(mfp_errs.as_collection());
305                (oks, errs)
306            }
307            ArrangementFlavor::Trace(_, oks, errs) => {
308                let (oks, mfp_errs) = CollectionBundle::<T>::flat_map_core_fallible::<_, DCB, _>(
309                    oks.clone(),
310                    key,
311                    max_demand,
312                    logic,
313                    REFUEL,
314                );
315                let errs = errs.clone().as_collection(|k, &()| k.clone());
316                let errs = errs.concat(mfp_errs.as_collection());
317                (oks, errs)
318            }
319        }
320    }
321
322    /// Ok-only variant of [`Self::flat_map`]. The `logic` callback receives a single output
323    /// session, cannot produce errors, and returns the number of records produced (see
324    /// [`Self::flat_map`] for fuel semantics). The returned err collection comes solely from
325    /// the arrangement; no extra operator is built to carry an empty MFP-error stream.
326    pub fn flat_map_ok<DCB, L>(
327        &self,
328        key: Option<&Row>,
329        max_demand: usize,
330        logic: L,
331    ) -> (
332        Stream<'scope, T, DCB::Container>,
333        VecCollection<'scope, T, DataflowErrorSer, Diff>,
334    )
335    where
336        // No push bound here: it lives at `logic`'s `give` call site, so a caller can push
337        // borrowed records into a columnar builder that has no owned-tuple `Push`.
338        DCB: ContainerBuilder,
339        L: for<'a, 'b> FnMut(&'a mut DatumVecBorrow<'b>, T, Diff, &mut Session<T, DCB>) -> usize
340            + 'static,
341    {
342        match &self {
343            ArrangementFlavor::Local(oks, errs) => {
344                let oks = CollectionBundle::<T>::flat_map_core_ok::<_, DCB, _>(
345                    oks.clone(),
346                    key,
347                    max_demand,
348                    logic,
349                    REFUEL,
350                );
351                let errs = errs.clone().as_collection(|k, &()| k.clone());
352                (oks, errs)
353            }
354            ArrangementFlavor::Trace(_, oks, errs) => {
355                let oks = CollectionBundle::<T>::flat_map_core_ok::<_, DCB, _>(
356                    oks.clone(),
357                    key,
358                    max_demand,
359                    logic,
360                    REFUEL,
361                );
362                let errs = errs.clone().as_collection(|k, &()| k.clone());
363                (oks, errs)
364            }
365        }
366    }
367}
368impl<'scope, T: RenderTimestamp> ArrangementFlavor<'scope, T> {
369    /// The scope containing the collection bundle.
370    pub fn scope(&self) -> Scope<'scope, T> {
371        match self {
372            ArrangementFlavor::Local(oks, _errs) => oks.stream.scope(),
373            ArrangementFlavor::Trace(_gid, oks, _errs) => oks.stream.scope(),
374        }
375    }
376
377    /// Brings the arrangement flavor into a region.
378    pub fn enter_region<'a>(&self, region: Scope<'a, T>) -> ArrangementFlavor<'a, T> {
379        match self {
380            ArrangementFlavor::Local(oks, errs) => ArrangementFlavor::Local(
381                oks.clone().enter_region(region),
382                errs.clone().enter_region(region),
383            ),
384            ArrangementFlavor::Trace(gid, oks, errs) => ArrangementFlavor::Trace(
385                *gid,
386                oks.clone().enter_region(region),
387                errs.clone().enter_region(region),
388            ),
389        }
390    }
391}
392impl<'scope, T: RenderTimestamp> ArrangementFlavor<'scope, T> {
393    /// Extracts the arrangement flavor from a region.
394    pub fn leave_region<'outer>(&self, outer: Scope<'outer, T>) -> ArrangementFlavor<'outer, T> {
395        match self {
396            ArrangementFlavor::Local(oks, errs) => ArrangementFlavor::Local(
397                oks.clone().leave_region(outer),
398                errs.clone().leave_region(outer),
399            ),
400            ArrangementFlavor::Trace(gid, oks, errs) => ArrangementFlavor::Trace(
401                *gid,
402                oks.clone().leave_region(outer),
403                errs.clone().leave_region(outer),
404            ),
405        }
406    }
407}
408/// Rewrites an arranged error collection to hold each of its errors once.
409///
410/// Sound because query semantics depend only on whether an error is present, and correct under
411/// retraction only because it reads the accumulated collection: no pointwise function of the input
412/// diffs (a saturating add, a sign) can collapse multiplicity and still cancel when the errors
413/// retract.
414///
415/// NOTE: One consumer does read the multiplicity. Error-count introspection reports it as the
416/// number of failing rows, so `log_dataflow_errors` must see the collection before this collapses
417/// it, or a dataflow reports one error however many rows failed. Collapse after the logging, never
418/// before.
419pub(crate) fn distinct_arranged_errs<'a, T: RenderTimestamp>(
420    errs: Arranged<'a, ErrAgent<T, Diff>>,
421    name: &str,
422) -> Arranged<'a, ErrAgent<T, Diff>> {
423    errs.mz_reduce_abelian::<_, ErrBuilder<_, _>, ErrSpine<_, _>, _>(
424        name,
425        |_err, _input, output| output.push(((), Diff::ONE)),
426    )
427}
428
429/// Rewrites an error collection to hold each of its errors once.
430///
431/// Costs an arrangement more than [`distinct_arranged_errs`], which reuses the arrangement its
432/// input already has.
433pub(crate) fn distinct_errs_collection<'a, T: RenderTimestamp>(
434    errs: VecCollection<'a, T, DataflowErrorSer, Diff>,
435) -> VecCollection<'a, T, DataflowErrorSer, Diff> {
436    let errs: KeyCollection<_, _, _> = errs.into();
437    let errs = errs
438        .mz_arrange::<ColumnationChunker<_>, ErrBatcher<_, _>, ErrBuilder<_, _>, ErrSpine<_, _>>(
439            "Arrange errors",
440        );
441    distinct_arranged_errs(errs, "Distinct errors").as_collection(|err, _| err.clone())
442}
443
444/// A bundle of the various ways a collection can be represented.
445///
446/// This type maintains the invariant that it does contain at least one valid
447/// source of data, either a collection or at least one arrangement.
448#[derive(Clone)]
449pub struct CollectionBundle<'scope, T: RenderTimestamp> {
450    pub collection: Option<(
451        ColCollection<'scope, T>,
452        VecCollection<'scope, T, DataflowErrorSer, Diff>,
453    )>,
454    pub arranged: BTreeMap<Vec<LirScalarExpr>, ArrangementFlavor<'scope, T>>,
455}
456
457impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
458    /// Construct a new collection bundle from a [`ColCollection`] and an error stream.
459    pub fn from_edge(
460        oks: ColCollection<'scope, T>,
461        errs: VecCollection<'scope, T, DataflowErrorSer, Diff>,
462    ) -> Self {
463        Self {
464            collection: Some((oks, errs)),
465            arranged: BTreeMap::default(),
466        }
467    }
468
469    /// Inserts arrangements by the expressions on which they are keyed.
470    pub fn from_expressions(
471        exprs: Vec<LirScalarExpr>,
472        arrangements: ArrangementFlavor<'scope, T>,
473    ) -> Self {
474        let mut arranged = BTreeMap::new();
475        arranged.insert(exprs, arrangements);
476        Self {
477            collection: None,
478            arranged,
479        }
480    }
481
482    /// Inserts arrangements by the columns on which they are keyed.
483    pub fn from_columns<I: IntoIterator<Item = usize>>(
484        columns: I,
485        arrangements: ArrangementFlavor<'scope, T>,
486    ) -> Self {
487        let mut keys = Vec::new();
488        for column in columns {
489            keys.push(LirScalarExpr::column(column));
490        }
491        Self::from_expressions(keys, arrangements)
492    }
493
494    /// The scope containing the collection bundle.
495    pub fn scope(&self) -> Scope<'scope, T> {
496        if let Some((oks, _errs)) = &self.collection {
497            oks.scope()
498        } else {
499            self.arranged
500                .values()
501                .next()
502                .expect("Must contain a valid collection")
503                .scope()
504        }
505    }
506
507    /// Collapses the multiplicity of every error this bundle carries to one.
508    ///
509    /// Belongs at the definition of a binding that more than one `Get` reads. Each reader
510    /// propagates the binding's errors independently, so a binding read `f` times contributes its
511    /// errors `f` times to the dataflow's error output, and those factors apply again at every
512    /// further level of sharing: a chain of diamonds multiplies rather than adds, and reaches
513    /// `Diff` overflow at a depth plans really do have. Collapsing at each definition holds the
514    /// dataflow's error multiplicity to the fan-out of a single level.
515    ///
516    /// Sound because error semantics depend only on whether an error is present, and correct under
517    /// retraction only because it reads the accumulated collection: no pointwise function of the
518    /// input diffs (a saturating add, a sign) can collapse multiplicity and still cancel when the
519    /// errors retract.
520    ///
521    /// Collapses every form the bundle offers, not just one. Each form carries its own error
522    /// stream, and those streams differ in content as well as identity: an arrangement's errors
523    /// include the key-formation errors that the raw collection's do not. Which form a consumer
524    /// reads is the consumer's choice, and a delta join reads both within one operator, so a
525    /// binding's definition cannot know which form to collapse.
526    ///
527    /// NOTE: Leaves imported arrangements (`ArrangementFlavor::Trace`) alone, whose error traces
528    /// this dataflow cannot rewrite in place. Their errors arrive bounded by the exporting
529    /// dataflow's last level of sharing rather than collapsed to one, since nothing collapses at an
530    /// export. A global read more than once within one dataflow is not collapsed either, because
531    /// only local bindings reach this.
532    pub fn distinct_errs(mut self) -> Self {
533        if let Some((oks, errs)) = self.collection.take() {
534            self.collection = Some((oks, distinct_errs_collection(errs)));
535        }
536        for (key, flavor) in std::mem::take(&mut self.arranged) {
537            let flavor = match flavor {
538                ArrangementFlavor::Local(oks, errs) => {
539                    // Names the key, not the binding: an operator name carrying a `LocalId` would
540                    // churn the introspection goldens every time the optimizer renumbers locals.
541                    let name = format!("Distinct errors[{key:?}]");
542                    ArrangementFlavor::Local(oks, distinct_arranged_errs(errs, &name))
543                }
544                flavor @ ArrangementFlavor::Trace(..) => flavor,
545            };
546            self.arranged.insert(key, flavor);
547        }
548        self
549    }
550
551    /// Brings the collection bundle into a region.
552    pub fn enter_region<'inner>(&self, region: Scope<'inner, T>) -> CollectionBundle<'inner, T> {
553        CollectionBundle {
554            collection: self.collection.as_ref().map(|(oks, errs)| {
555                (
556                    oks.clone().enter_region(region),
557                    errs.clone().enter_region(region),
558                )
559            }),
560            arranged: self
561                .arranged
562                .iter()
563                .map(|(key, bundle)| (key.clone(), bundle.enter_region(region)))
564                .collect(),
565        }
566    }
567}
568
569impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
570    /// Extracts the collection bundle from a region.
571    pub fn leave_region<'outer>(&self, outer: Scope<'outer, T>) -> CollectionBundle<'outer, T> {
572        CollectionBundle {
573            collection: self.collection.as_ref().map(|(oks, errs)| {
574                (
575                    oks.clone().leave_region(outer),
576                    errs.clone().leave_region(outer),
577                )
578            }),
579            arranged: self
580                .arranged
581                .iter()
582                .map(|(key, bundle)| (key.clone(), bundle.leave_region(outer)))
583                .collect(),
584        }
585    }
586}
587
588impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
589    /// Asserts that the arrangement for a specific key
590    /// (or the raw collection for no key) exists,
591    /// and returns the corresponding collection.
592    ///
593    /// This returns the collection as-is, without
594    /// doing any unthinning transformation.
595    /// Therefore, it should be used when the appropriate transformation
596    /// was planned as part of a following MFP.
597    ///
598    /// If `key` is specified, the function converts the arrangement to a collection using a
599    /// fueled `flat_map` operator.
600    ///
601    /// The keyed path materializes the arrangement as the columnar edge. The unkeyed path
602    /// returns the unarranged `.collection` edge with its variant intact.
603    pub fn as_specific_collection(
604        &self,
605        key: Option<&[LirScalarExpr]>,
606    ) -> (
607        ColCollection<'scope, T>,
608        VecCollection<'scope, T, DataflowErrorSer, Diff>,
609    ) {
610        // Any operator that uses this method was told to use a particular
611        // collection during LIR planning, where we should have made
612        // sure that that collection exists.
613        //
614        // If it doesn't, we panic.
615        match key {
616            None => self
617                .collection
618                .clone()
619                .expect("The unarranged collection doesn't exist."),
620            Some(key) => {
621                let arranged = self.arranged.get(key).unwrap_or_else(|| {
622                    panic!("The collection arranged by {:?} doesn't exist.", key)
623                });
624                // Output is 1:1 from the already-consolidated cursor, so a
625                // non-consolidating `ColumnBuilder` suffices. `max_demand` is
626                // `usize::MAX` because the materialized collection carries every column.
627                let (ok, err) =
628                    arranged.flat_map_ok::<ColumnBuilder<(Row, T, Diff)>, _>(None, usize::MAX, {
629                        // `give` copies the bytes into the column, so one buffer
630                        // serves every record.
631                        let mut row_buf = Row::default();
632                        move |borrow, t, r, ok_session| {
633                            row_buf.packer().extend(borrow.iter());
634                            ok_session.give((&row_buf, &t, &r));
635                            1
636                        }
637                    });
638                (ok.as_collection(), err)
639            }
640        }
641    }
642
643    /// Constructs and applies logic to elements of a collection and returns the results.
644    ///
645    /// The function applies `logic` on elements. The logic conceptually receives
646    /// `(&Row, &Row)` pairs in the form of a datum vec in the expected order.
647    ///
648    /// If `key_val` is set, this is a promise that `logic` will produce no results on
649    /// records for which the key does not evaluate to the value. This is used when we
650    /// have an arrangement by that key to leap directly to exactly those records.
651    /// It is important that `logic` still guard against data that does not satisfy
652    /// this constraint, as this method does not statically know that it will have
653    /// that arrangement.
654    ///
655    /// The `max_demand` parameter limits the number of columns decoded from the
656    /// input. Only the first `max_demand` columns are decoded. Pass `usize::MAX` to
657    /// decode all columns.
658    pub fn flat_map<DCB, L>(
659        &self,
660        key_val: Option<(Vec<LirScalarExpr>, Option<Row>)>,
661        max_demand: usize,
662        logic: L,
663    ) -> (
664        Stream<'scope, T, DCB::Container>,
665        VecCollection<'scope, T, DataflowErrorSer, Diff>,
666    )
667    where
668        DCB: ContainerBuilder,
669        L: for<'a> FnMut(
670                &'a mut DatumVecBorrow<'_>,
671                T,
672                Diff,
673                &mut Session<T, DCB>,
674                &mut Session<T, ECB<T>>,
675            ) -> usize
676            + 'static,
677    {
678        // If `key_val` is set, we should have to use the corresponding arrangement.
679        // If there isn't one, that implies an error in the contract between
680        // key-production and available arrangements.
681        if let Some((key, val)) = key_val {
682            self.arrangement(&key)
683                .expect("Should have ensured during planning that this arrangement exists.")
684                .flat_map::<DCB, _>(val.as_ref(), max_demand, logic)
685        } else {
686            let (oks, errs) = self
687                .collection
688                .clone()
689                .expect("Invariant violated: CollectionBundle contains no collection.");
690            let (ok_stream, err_stream) =
691                flat_map_datums::<_, DCB, _>(oks, "CollectionFlatMap", max_demand, logic);
692            let errs = errs.concat(err_stream.as_collection());
693            (ok_stream, errs)
694        }
695    }
696
697    /// Factored out common logic for using literal keys in general traces.
698    ///
699    /// This logic is sufficiently interesting that we want to write it only
700    /// once, and thereby avoid any skew in the two uses of the logic.
701    ///
702    /// The function presents the contents of the trace as `(key, value, time, delta)` tuples,
703    /// where key and value are potentially specialized, but convertible into rows. The `logic`
704    /// callback writes ok results into the first session and errors into the second, returning
705    /// the number of records produced. See [`ArrangementFlavor::flat_map`] for the fuel
706    /// rationale.
707    fn flat_map_core_fallible<Tr, DCB, L>(
708        trace: Arranged<'scope, Tr>,
709        key: Option<&<<BatchCursor<Tr> as Cursor>::KeyContainer as BatchContainer>::Owned>,
710        max_demand: usize,
711        mut logic: L,
712        refuel: usize,
713    ) -> (
714        Stream<'scope, T, DCB::Container>,
715        Stream<'scope, T, Vec<(DataflowErrorSer, T, Diff)>>,
716    )
717    where
718        Tr: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
719        for<'a> BatchCursor<Tr>:
720            Cursor<Key<'a>: ExtendDatums, Val<'a>: ExtendDatums, Time = T, Diff = mz_repr::Diff>,
721        <<BatchCursor<Tr> as Cursor>::KeyContainer as BatchContainer>::Owned: PartialEq,
722        DCB: ContainerBuilder,
723        // `logic` receives the key and value already decoded into a `DatumVecBorrow`. The decode
724        // (and its arena/`DatumVec`) lives in the per-activation closure below, so it is scoped to
725        // a single scheduling invocation rather than to the operator.
726        L: for<'a, 'b> FnMut(
727                &'a mut DatumVecBorrow<'b>,
728                T,
729                mz_repr::Diff,
730                &mut Session<T, DCB>,
731                &mut Session<T, ECB<T>>,
732            ) -> usize
733            + 'static,
734    {
735        let scope = trace.stream.scope();
736
737        let mut key_con = <BatchCursor<Tr> as Cursor>::KeyContainer::with_capacity(1);
738        if let Some(key) = &key {
739            key_con.push_own(key);
740        }
741        let mode = if key.is_some() { "index" } else { "scan" };
742        let name = format!("ArrangementFlatMap({})", mode);
743
744        let mut builder = OperatorBuilder::new(name, scope.clone());
745        let (ok_output, ok_stream) = builder.new_output();
746        let mut ok_output = OutputBuilder::<_, DCB>::from(ok_output);
747        let (err_output, err_stream) = builder.new_output();
748        let mut err_output = OutputBuilder::<_, ECB<T>>::from(err_output);
749        let mut input = builder.new_input(trace.stream.clone(), Pipeline);
750        let operator_info = builder.operator_info();
751
752        builder.build(move |_capabilities| {
753            // Acquire an activator to reschedule the operator when it has unfinished work.
754            let activator = scope.activator_for(operator_info.address);
755            // Maintain a list of work to do, cursor to navigate and process.
756            let mut todo = std::collections::VecDeque::new();
757            move |_frontiers| {
758                let key = key_con.get(0);
759                let mut ok_output = ok_output.activate();
760                let mut err_output = err_output.activate();
761
762                // First, dequeue all batches.
763                input.for_each(|time, data| {
764                    // Retain a capability for each output, as the work may complete across
765                    // multiple activations.
766                    let ok_cap = time.retain(0);
767                    let err_cap = time.retain(1);
768                    for batch in data.iter() {
769                        todo.push_back(PendingWork::new(
770                            ok_cap.clone(),
771                            err_cap.clone(),
772                            batch.cursor(),
773                            batch.clone(),
774                        ));
775                    }
776                });
777
778                // Decode the key/value of each record into datums for `logic`. The arena and datum
779                // buffer are created here, so they are scoped to this activation (dropped when it
780                // returns) rather than retained for the operator's lifetime; both are reused across
781                // the records processed within the activation.
782                let mut temp_storage = RowArena::new();
783                let mut datums = DatumVec::new();
784                let mut decode_logic =
785                    |k: BatchKey<'_, Tr>,
786                     v: BatchVal<'_, Tr>,
787                     t: T,
788                     d: mz_repr::Diff,
789                     ok_session: &mut Session<T, DCB>,
790                     err_session: &mut Session<T, ECB<T>>| {
791                        temp_storage.clear();
792                        let mut datums_borrow = datums.borrow();
793                        k.extend_datums(&temp_storage, &mut datums_borrow, Some(max_demand));
794                        let remaining = max_demand.saturating_sub(datums_borrow.len());
795                        v.extend_datums(&temp_storage, &mut datums_borrow, Some(remaining));
796                        logic(&mut datums_borrow, t, d, ok_session, err_session)
797                    };
798
799                // Second, make progress on `todo`.
800                let mut fuel = refuel;
801                while !todo.is_empty() && fuel > 0 {
802                    todo.front_mut().unwrap().do_work(
803                        key.as_ref(),
804                        &mut decode_logic,
805                        &mut fuel,
806                        &mut ok_output,
807                        &mut err_output,
808                    );
809                    if fuel > 0 {
810                        todo.pop_front();
811                    }
812                }
813                // If we have not finished all work, re-activate the operator.
814                if !todo.is_empty() {
815                    activator.activate();
816                }
817            }
818        });
819
820        (ok_stream, err_stream)
821    }
822
823    /// Ok-only variant of [`Self::flat_map_core_fallible`]. The `logic` callback writes results
824    /// into a single output session and returns the number of records produced (see the
825    /// fallible variant for fuel semantics). Use this when the caller statically knows it
826    /// will never produce `DataflowErrorSer` records, to avoid building a second output port
827    /// and the empty err stream that would follow it.
828    fn flat_map_core_ok<Tr, DCB, L>(
829        trace: Arranged<'scope, Tr>,
830        key: Option<&<<BatchCursor<Tr> as Cursor>::KeyContainer as BatchContainer>::Owned>,
831        max_demand: usize,
832        mut logic: L,
833        refuel: usize,
834    ) -> Stream<'scope, T, DCB::Container>
835    where
836        Tr: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
837        for<'a> BatchCursor<Tr>:
838            Cursor<Key<'a>: ExtendDatums, Val<'a>: ExtendDatums, Time = T, Diff = mz_repr::Diff>,
839        <<BatchCursor<Tr> as Cursor>::KeyContainer as BatchContainer>::Owned: PartialEq,
840        DCB: ContainerBuilder,
841        L: for<'a, 'b> FnMut(
842                &'a mut DatumVecBorrow<'b>,
843                T,
844                mz_repr::Diff,
845                &mut Session<T, DCB>,
846            ) -> usize
847            + 'static,
848    {
849        let scope = trace.stream.scope();
850
851        let mut key_con = <BatchCursor<Tr> as Cursor>::KeyContainer::with_capacity(1);
852        if let Some(key) = &key {
853            key_con.push_own(key);
854        }
855        let mode = if key.is_some() { "index" } else { "scan" };
856        let name = format!("ArrangementFlatMapOk({})", mode);
857
858        let mut builder = OperatorBuilder::new(name, scope.clone());
859        let (ok_output, ok_stream) = builder.new_output();
860        let mut ok_output = OutputBuilder::<_, DCB>::from(ok_output);
861        let mut input = builder.new_input(trace.stream.clone(), Pipeline);
862        let operator_info = builder.operator_info();
863
864        builder.build(move |_capabilities| {
865            let activator = scope.activator_for(operator_info.address);
866            let mut todo = std::collections::VecDeque::new();
867            move |_frontiers| {
868                let key = key_con.get(0);
869                let mut ok_output = ok_output.activate();
870
871                input.for_each(|time, data| {
872                    let cap = time.retain(0);
873                    for batch in data.iter() {
874                        todo.push_back(PendingWorkOk::new(
875                            cap.clone(),
876                            batch.cursor(),
877                            batch.clone(),
878                        ));
879                    }
880                });
881
882                // Activation-scoped decode storage; see `flat_map_core_fallible`.
883                let mut temp_storage = RowArena::new();
884                let mut datums = DatumVec::new();
885                let mut decode_logic =
886                    |k: BatchKey<'_, Tr>,
887                     v: BatchVal<'_, Tr>,
888                     t: T,
889                     d: mz_repr::Diff,
890                     ok_session: &mut Session<T, DCB>| {
891                        temp_storage.clear();
892                        let mut datums_borrow = datums.borrow();
893                        k.extend_datums(&temp_storage, &mut datums_borrow, Some(max_demand));
894                        let remaining = max_demand.saturating_sub(datums_borrow.len());
895                        v.extend_datums(&temp_storage, &mut datums_borrow, Some(remaining));
896                        logic(&mut datums_borrow, t, d, ok_session)
897                    };
898
899                let mut fuel = refuel;
900                while !todo.is_empty() && fuel > 0 {
901                    todo.front_mut().unwrap().do_work(
902                        key.as_ref(),
903                        &mut decode_logic,
904                        &mut fuel,
905                        &mut ok_output,
906                    );
907                    if fuel > 0 {
908                        todo.pop_front();
909                    }
910                }
911                if !todo.is_empty() {
912                    activator.activate();
913                }
914            }
915        });
916
917        ok_stream
918    }
919
920    /// Look up an arrangement by the expressions that form the key.
921    ///
922    /// The result may be `None` if no such arrangement exists, or it may be one of many
923    /// "arrangement flavors" that represent the types of arranged data we might have.
924    pub fn arrangement(&self, key: &[LirScalarExpr]) -> Option<ArrangementFlavor<'scope, T>> {
925        self.arranged.get(key).map(|x| x.clone())
926    }
927}
928
929impl<'scope, T: RenderTimestamp> CollectionBundle<'scope, T> {
930    /// Presents `self` as a stream of updates, having been subjected to `mfp`.
931    ///
932    /// This operator is able to apply the logic of `mfp` early, which can substantially
933    /// reduce the amount of data produced when `mfp` is non-trivial.
934    ///
935    /// The `key_val` argument, when present, indicates that a specific arrangement should
936    /// be used, and if, in addition, the `val` component is present,
937    /// that we can seek to the supplied row.
938    pub fn as_collection_core(
939        &self,
940        mut mfp_plan: MfpPlan<LirScalarExpr>,
941        key_val: Option<(Vec<LirScalarExpr>, Option<StableRow>)>,
942        until: Antichain<mz_repr::Timestamp>,
943    ) -> (
944        ColCollection<'scope, T>,
945        VecCollection<'scope, T, DataflowErrorSer, Diff>,
946    ) {
947        // Unwrap the stable-serialization row wrapper, seeking works on
948        // plain rows.
949        let key_val = key_val.map(|(key, val)| (key, val.map(|val| val.0)));
950        // If the MFP is trivial, we can just return the collection.
951        // In the case that we weren't going to apply the `key_val` optimization,
952        // this path results in a slightly smaller and faster
953        // dataflow graph, and is intended to fix
954        // https://github.com/MaterializeInc/database-issues/issues/3111
955        let has_key_val = if let Some((_key, Some(_val))) = &key_val {
956            true
957        } else {
958            false
959        };
960
961        if mfp_plan.is_identity() && !has_key_val {
962            let key = key_val.map(|(k, _v)| k);
963            return match key {
964                // Unarranged identity hands the edge straight through, so a columnar
965                // producer stays columnar.
966                None => self
967                    .collection
968                    .clone()
969                    .expect("The unarranged collection doesn't exist."),
970                Some(key) => self.as_specific_collection(Some(&key)),
971            };
972        }
973
974        let max_demand = mfp_plan.demand().last().map(|x| *x + 1).unwrap_or(0);
975        mfp_plan.permute_fn(|c| c, max_demand);
976
977        let mut datum_vec = DatumVec::new();
978        // Wrap in an `Rc` so that lifetimes work out.
979        let until = std::rc::Rc::new(until);
980
981        // `ConsolidatingColumnBuilder` folds within-batch duplicates. It consolidates in
982        // place, so records are given owned, which costs nothing here because
983        // `mfp_plan.evaluate` already produces a fresh `Row` per result.
984        let (stream, errors) = self.flat_map::<ConsolidatingColumnBuilder<Row, T, Diff>, _>(
985            key_val,
986            max_demand,
987            move |row_datums, time, diff, ok_session, err_session| {
988                let mut row_builder = SharedRow::get();
989                let until = std::rc::Rc::clone(&until);
990                let temp_storage = RowArena::new();
991                let row_iter = row_datums.iter();
992                let mut datums_local = datum_vec.borrow();
993                datums_local.extend(row_iter);
994                let event_time = time.event_time();
995                let mut work: usize = 0;
996                for result in mfp_plan.evaluate(
997                    &mut datums_local,
998                    &temp_storage,
999                    event_time,
1000                    diff.clone(),
1001                    move |time| !until.less_equal(time),
1002                    &mut row_builder,
1003                ) {
1004                    work += 1;
1005                    match result {
1006                        Ok((row, event_time, diff)) => {
1007                            // Copy the whole time, and re-populate event time.
1008                            let mut time: T = time.clone();
1009                            *time.event_time_mut() = event_time;
1010                            ok_session.give((row, time, diff));
1011                        }
1012                        Err((e, event_time, diff)) => {
1013                            // Copy the whole time, and re-populate event time.
1014                            let mut time: T = time.clone();
1015                            *time.event_time_mut() = event_time;
1016                            err_session.give((e, time, diff));
1017                        }
1018                    }
1019                }
1020                work
1021            },
1022        );
1023
1024        (stream.as_collection(), errors)
1025    }
1026    pub fn ensure_collections(
1027        mut self,
1028        collections: AvailableCollections,
1029        input_key: Option<Vec<LirScalarExpr>>,
1030        input_mfp: MfpPlan<LirScalarExpr>,
1031        as_of: Antichain<mz_repr::Timestamp>,
1032        until: Antichain<mz_repr::Timestamp>,
1033        config_set: &ConfigSet,
1034        strategy: ArrangementStrategy,
1035    ) -> Self
1036    where
1037        T: MaybeBucketByTime,
1038    {
1039        if collections == Default::default() {
1040            return self;
1041        }
1042        // Cache collection to avoid reforming it each time.
1043        //
1044        // TODO(mcsherry): In theory this could be faster run out of another arrangement,
1045        // as the `map_fallible` that follows could be run against an arrangement itself.
1046        //
1047        // Note(btv): If we ever do that, we would then only need to make the raw collection here
1048        // if `collections.raw` is true.
1049
1050        for (key, _, _) in collections.arranged.iter() {
1051            soft_assert_or_log!(
1052                !self.arranged.contains_key(key),
1053                "LIR ArrangeBy tried to create an existing arrangement"
1054            );
1055        }
1056
1057        // Track whether we already applied temporal bucketing in this call, to
1058        // avoid bucketing the same updates twice.
1059        let mut bucketed = false;
1060
1061        // True iff at least one new arrangement will actually be built below. Bucketing only
1062        // pays off when something downstream merges/compacts the future-stamped updates; on a
1063        // pure raw collection (no new arrangement) the work is wasted.
1064        let will_create_arrangement = collections
1065            .arranged
1066            .iter()
1067            .any(|(key, _, _)| !self.arranged.contains_key(key));
1068
1069        // We need the collection if either (1) it is explicitly demanded, or (2) we are going to render any arrangement
1070        let form_raw_collection = collections.raw || will_create_arrangement;
1071        if form_raw_collection && self.collection.is_none() {
1072            let (oks, errs) =
1073                self.as_collection_core(input_mfp, input_key.map(|k| (k, None)), until);
1074            // Apply temporal bucketing when the lowering selected `TemporalBucketing` and
1075            // we will build at least one arrangement. This path fires when the collection
1076            // must be formed from scratch (e.g., from an arrangement via as_collection_core).
1077            let effective_strategy = if will_create_arrangement {
1078                strategy
1079            } else {
1080                ArrangementStrategy::Direct
1081            };
1082            let oks = if matches!(effective_strategy, ArrangementStrategy::TemporalBucketing) {
1083                let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
1084                    .get(config_set)
1085                    .try_into()
1086                    .expect("must fit");
1087                bucketed = true;
1088                // Temporal bucketing is columnar throughout, so no round trip here.
1089                T::maybe_apply_temporal_bucketing(oks.inner, as_of.clone(), summary)
1090            } else {
1091                oks
1092            };
1093            self.collection = Some((oks, errs));
1094        }
1095        for (key, _, thinning) in collections.arranged {
1096            if !self.arranged.contains_key(&key) {
1097                // TODO: Consider allowing more expressive names.
1098                let name = format!("ArrangeBy[{:?}]", key);
1099
1100                let (oks, errs) = self
1101                    .collection
1102                    .take()
1103                    .expect("Collection constructed above");
1104                // Apply temporal bucketing if the collection already existed on
1105                // the bundle (e.g., from an upstream temporal Mfp or Get) and we
1106                // haven't bucketed yet. This is the common path for temporal-MFP
1107                // → ArrangeBy flows.
1108                let effective_strategy = if bucketed {
1109                    ArrangementStrategy::Direct
1110                } else {
1111                    strategy
1112                };
1113                let oks = if matches!(effective_strategy, ArrangementStrategy::TemporalBucketing) {
1114                    let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
1115                        .get(config_set)
1116                        .try_into()
1117                        .expect("must fit");
1118                    bucketed = true;
1119                    // Temporal bucketing is columnar throughout, so no round trip here.
1120                    T::maybe_apply_temporal_bucketing(oks.inner, as_of.clone(), summary)
1121                } else {
1122                    oks
1123                };
1124                let batcher = ArrangementBatcher::from_config(config_set);
1125                let (oks, errs_keyed, passthrough) =
1126                    Self::arrange_collection(&name, oks, key.clone(), thinning.clone(), batcher);
1127                let errs_concat: KeyCollection<_, _, _> = errs.clone().concat(errs_keyed).into();
1128                self.collection = Some((passthrough, errs));
1129                let errs =
1130                    errs_concat.mz_arrange::<
1131                        ColumnationChunker<_>,
1132                        ErrBatcher<_, _>,
1133                        ErrBuilder<_, _>,
1134                        ErrSpine<_, _>,
1135                    >(
1136                        &format!("{}-errors", name),
1137                    );
1138                self.arranged
1139                    .insert(key, ArrangementFlavor::Local(oks, errs));
1140            }
1141        }
1142        self
1143    }
1144
1145    /// Builds an arrangement from a collection, using the specified key and value thinning.
1146    ///
1147    /// The arrangement's key is based on the `key` expressions, and the value the input with
1148    /// the `thinning` applied to it. It selects which of the input columns are included in the
1149    /// value of the arrangement. The thinning is in support of permuting arrangements such that
1150    /// columns in the key are not included in the value.
1151    ///
1152    /// In addition to the ok and err streams, we produce a passthrough stream that forwards
1153    /// the input as-is, which allows downstream consumers to reuse the collection without
1154    /// teeing the stream.
1155    fn arrange_collection(
1156        name: &String,
1157        oks: ColCollection<'scope, T>,
1158        key: Vec<LirScalarExpr>,
1159        thinning: Vec<usize>,
1160        batcher: ArrangementBatcher,
1161    ) -> (
1162        Arranged<'scope, RowRowAgent<T, Diff>>,
1163        VecCollection<'scope, T, DataflowErrorSer, Diff>,
1164        ColCollection<'scope, T>,
1165    ) {
1166        // Spelled out rather than `map_fallible`, whose closure cannot return the references
1167        // a columnar stream is pushed from.
1168        //
1169        // The arena is per-activation and cleared per row, so the capacity it retains never
1170        // outlives a scheduling invocation.
1171        let (ok_stream, err_stream, passthrough) = {
1172            let mut builder =
1173                OperatorBuilder::new("FormArrangementKey".to_string(), oks.inner.scope());
1174            let (ok_output, ok_stream) = builder.new_output();
1175            let mut ok_output =
1176                OutputBuilder::<_, ColumnBuilder<((Row, Row), T, Diff)>>::from(ok_output);
1177            let (err_output, err_stream) = builder.new_output();
1178            let mut err_output = OutputBuilder::from(err_output);
1179            let (passthrough_output, passthrough_stream) = builder.new_output();
1180            let mut passthrough_output =
1181                OutputBuilder::<_, NoopBuilder<Column<(Row, T, Diff)>>>::from(passthrough_output);
1182            let mut input = builder.new_input(oks.inner, Pipeline);
1183            builder.set_notify_for(0, FrontierInterest::Never);
1184            builder.build(move |_capabilities| {
1185                let mut key_buf = Row::default();
1186                let mut val_buf = Row::default();
1187                let mut datums = DatumVec::new();
1188                move |_frontiers| {
1189                    // Scoped to the activation so the arena's retained capacity does not
1190                    // outlive a single scheduling invocation; cleared per row to reuse it
1191                    // within the batch.
1192                    let mut temp_storage = RowArena::new();
1193                    let mut ok_output = ok_output.activate();
1194                    let mut err_output = err_output.activate();
1195                    let mut passthrough_output = passthrough_output.activate();
1196                    input.for_each(|time, data| {
1197                        let mut ok_session = ok_output.session_with_builder(&time);
1198                        let mut err_session = err_output.session(&time);
1199                        // Rows are read from the borrowed column, never materialized as
1200                        // owned `Row`s. Times and diffs are owned only on the error path.
1201                        for (row, t, d) in data.borrow().into_index_iter() {
1202                            temp_storage.clear();
1203                            let datums = datums.borrow_with(row);
1204                            let key_iter = key.iter().map(|k| k.eval(&datums, &temp_storage));
1205                            match key_buf.packer().try_extend(key_iter) {
1206                                Ok(()) => {
1207                                    let val_datum_iter = thinning.iter().map(|c| datums[*c]);
1208                                    val_buf.packer().extend(val_datum_iter);
1209                                    ok_session.give(((&*key_buf, &*val_buf), t, d));
1210                                }
1211                                Err(e) => {
1212                                    err_session.give((
1213                                        e.into(),
1214                                        Columnar::into_owned(t),
1215                                        Columnar::into_owned(d),
1216                                    ));
1217                                }
1218                            }
1219                        }
1220                        passthrough_output
1221                            .session_with_builder(&time)
1222                            .give_container(data);
1223                    });
1224                }
1225            });
1226            (ok_stream, err_stream, passthrough_stream.as_collection())
1227        };
1228
1229        let exchange =
1230            ExchangeCore::<ColumnBuilder<_>, _>::new_core(columnar_exchange::<Row, Row, T, Diff>);
1231        let oks = match batcher {
1232            ArrangementBatcher::Chunked => ok_stream.mz_arrange_core::<
1233                _,
1234                ChunkChunker<(Row, Row), T, Diff, ComputeSpill>,
1235                AccountedChunkBatcher<(Row, Row), T, Diff, ComputeSpill>,
1236                UnchunkBuilder<RowRowColPagedBuilder<T, Diff>, (Row, Row), T, Diff, ComputeSpill>,
1237                RowRowSpine<_, _>,
1238            >(exchange, name),
1239            ArrangementBatcher::Columnar => ok_stream.mz_arrange_core::<
1240                _,
1241                batcher::ColumnChunker<_>,
1242                Col2ValColBatcher<_, _, _, _>,
1243                RowRowColPagedBuilder<_, _>,
1244                RowRowSpine<_, _>,
1245            >(exchange, name),
1246            ArrangementBatcher::Columnation => ok_stream.mz_arrange_core::<
1247                _,
1248                batcher::Chunker<_>,
1249                Col2ValBatcher<_, _, _, _>,
1250                RowRowBuilder<_, _>,
1251                RowRowSpine<_, _>,
1252            >(exchange, name),
1253        };
1254        (oks, err_stream.as_collection(), passthrough)
1255    }
1256}
1257
1258/// Type alias for a timely output `Session` whose capability is a `Capability<T>`. The container
1259/// builder `CB` is left to the caller; sessions can therefore drive consolidating, capacity, or
1260/// (in the future) columnar output builders without changing call sites.
1261pub(crate) type Session<'a, 'b, T, CB> =
1262    timely::dataflow::operators::generic::Session<'a, 'b, T, CB, Capability<T>>;
1263
1264/// Container builder used for the err output of every flat_map variant. Pre-refactor the
1265/// merged Ok/Err stream flowed through a [`ConsolidatingContainerBuilder`] before the
1266/// `map_fallible` demux split it; we preserve that consolidation here so errors with the
1267/// same `(error, time)` cancel within a batch rather than propagating to downstream.
1268pub(crate) type ECB<T> = ConsolidatingContainerBuilder<Vec<(DataflowErrorSer, T, Diff)>>;
1269
1270/// Number of output records the arrangement flat_map operators may produce before yielding.
1271/// See [`ArrangementFlavor::flat_map`] for the fuel rationale; the constant is a pragmatic
1272/// compromise and not tuned empirically.
1273const REFUEL: usize = 1_000_000;
1274
1275struct PendingWork<C>
1276where
1277    C: Cursor,
1278{
1279    /// Capability for the `ok` output (output port 0).
1280    ok_capability: Capability<C::Time>,
1281    /// Capability for the `err` output (output port 1).
1282    err_capability: Capability<C::Time>,
1283    cursor: C,
1284    batch: C::Storage,
1285}
1286
1287impl<C> PendingWork<C>
1288where
1289    C: Cursor<KeyContainer: BatchContainer<Owned: PartialEq + Sized>>,
1290{
1291    /// Create a new bundle of pending work, from a pair of capabilities (one per output),
1292    /// a cursor, and backing storage.
1293    fn new(
1294        ok_capability: Capability<C::Time>,
1295        err_capability: Capability<C::Time>,
1296        cursor: C,
1297        batch: C::Storage,
1298    ) -> Self {
1299        Self {
1300            ok_capability,
1301            err_capability,
1302            cursor,
1303            batch,
1304        }
1305    }
1306    /// Perform roughly `fuel` work through the cursor, applying `logic` and sending results to
1307    /// the two output sessions.
1308    fn do_work<DCB, L>(
1309        &mut self,
1310        key: Option<&C::Key<'_>>,
1311        logic: &mut L,
1312        fuel: &mut usize,
1313        ok_output: &mut OutputBuilderSession<'_, C::Time, DCB>,
1314        err_output: &mut OutputBuilderSession<'_, C::Time, ECB<C::Time>>,
1315    ) where
1316        DCB: ContainerBuilder,
1317        L: FnMut(
1318            C::Key<'_>,
1319            C::Val<'_>,
1320            C::Time,
1321            C::Diff,
1322            &mut Session<C::Time, DCB>,
1323            &mut Session<C::Time, ECB<C::Time>>,
1324        ) -> usize,
1325    {
1326        let mut ok_session = ok_output.session_with_builder(&self.ok_capability);
1327        let mut err_session = err_output.session_with_builder(&self.err_capability);
1328        walk_cursor(&mut self.cursor, &self.batch, key, fuel, |k, v, t, d| {
1329            logic(k, v, t, d, &mut ok_session, &mut err_session)
1330        });
1331    }
1332}
1333
1334/// Pending work for the Ok-only variant of `flat_map_core_fallible`. Holds a single capability since
1335/// the operator has only one output port.
1336struct PendingWorkOk<C>
1337where
1338    C: Cursor,
1339{
1340    capability: Capability<C::Time>,
1341    cursor: C,
1342    batch: C::Storage,
1343}
1344
1345impl<C> PendingWorkOk<C>
1346where
1347    C: Cursor<KeyContainer: BatchContainer<Owned: PartialEq + Sized>>,
1348{
1349    fn new(capability: Capability<C::Time>, cursor: C, batch: C::Storage) -> Self {
1350        Self {
1351            capability,
1352            cursor,
1353            batch,
1354        }
1355    }
1356
1357    /// Perform roughly `fuel` work through the cursor, applying `logic` and sending results to
1358    /// the single output session.
1359    fn do_work<DCB, L>(
1360        &mut self,
1361        key: Option<&C::Key<'_>>,
1362        logic: &mut L,
1363        fuel: &mut usize,
1364        ok_output: &mut OutputBuilderSession<'_, C::Time, DCB>,
1365    ) where
1366        DCB: ContainerBuilder,
1367        L: FnMut(C::Key<'_>, C::Val<'_>, C::Time, C::Diff, &mut Session<C::Time, DCB>) -> usize,
1368    {
1369        let mut ok_session = ok_output.session_with_builder(&self.capability);
1370        walk_cursor(&mut self.cursor, &self.batch, key, fuel, |k, v, t, d| {
1371            logic(k, v, t, d, &mut ok_session)
1372        });
1373    }
1374}
1375
1376/// Walk a cursor, calling `emit` for each consolidated `(key, val, time, diff)` tuple. If
1377/// `key` is set, the cursor is seeked to it and only values for that key are produced.
1378///
1379/// `emit` returns the number of records it produced for the given input tuple. The cursor
1380/// stops as soon as the accumulated emit count reaches `*fuel`, leaving the cursor in place
1381/// so work can resume on a later call. Within a batch, both the inner val loop and the
1382/// outer key loop are bounded only by emit count, so selective filters (`emit` returns 0)
1383/// run to batch completion in a single activation — see [`ArrangementFlavor::flat_map`]
1384/// for why fuel counts output rather than input.
1385fn walk_cursor<C, F>(
1386    cursor: &mut C,
1387    batch: &C::Storage,
1388    key: Option<&C::Key<'_>>,
1389    fuel: &mut usize,
1390    mut emit: F,
1391) where
1392    C: Cursor<KeyContainer: BatchContainer<Owned: PartialEq + Sized>>,
1393    F: FnMut(C::Key<'_>, C::Val<'_>, C::Time, C::Diff) -> usize,
1394{
1395    use differential_dataflow::consolidation::consolidate;
1396
1397    let mut work: usize = 0;
1398    let mut buffer = Vec::new();
1399    if let Some(key) = key {
1400        let key = C::KeyContainer::reborrow(*key);
1401        if cursor.get_key(batch).map(|k| k == key) != Some(true) {
1402            cursor.seek_key(batch, key);
1403        }
1404        if cursor.get_key(batch).map(|k| k == key) == Some(true) {
1405            let key = cursor.key(batch);
1406            while let Some(val) = cursor.get_val(batch) {
1407                cursor.map_times(batch, |time, diff| {
1408                    buffer.push((C::owned_time(time), C::owned_diff(diff)));
1409                });
1410                consolidate(&mut buffer);
1411                for (time, diff) in buffer.drain(..) {
1412                    work += emit(key, val, time, diff);
1413                }
1414                cursor.step_val(batch);
1415                if work >= *fuel {
1416                    *fuel = 0;
1417                    return;
1418                }
1419            }
1420        }
1421    } else {
1422        while let Some(key) = cursor.get_key(batch) {
1423            while let Some(val) = cursor.get_val(batch) {
1424                cursor.map_times(batch, |time, diff| {
1425                    buffer.push((C::owned_time(time), C::owned_diff(diff)));
1426                });
1427                consolidate(&mut buffer);
1428                for (time, diff) in buffer.drain(..) {
1429                    work += emit(key, val, time, diff);
1430                }
1431                cursor.step_val(batch);
1432                if work >= *fuel {
1433                    *fuel = 0;
1434                    return;
1435                }
1436            }
1437            cursor.step_key(batch);
1438        }
1439    }
1440    *fuel -= work;
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445    use differential_dataflow::input::Input;
1446    use mz_expr::{EvalError, MapFilterProject};
1447    use mz_repr::{Datum, ReprScalarType, Timestamp};
1448    use timely::dataflow::operators::Capture;
1449    use timely::dataflow::operators::capture::{Event, Extract};
1450
1451    use super::*;
1452    use crate::render::columnar::{columnar_to_vec, vec_to_columnar};
1453
1454    type OkUpdate = ((Row, Row), Timestamp, Diff);
1455    type ErrUpdate = (DataflowErrorSer, Timestamp, Diff);
1456    type Captured<D> = std::sync::mpsc::Receiver<Event<Timestamp, Vec<D>>>;
1457
1458    fn extract_ok(captured: Captured<OkUpdate>) -> Vec<OkUpdate> {
1459        let mut updates: Vec<_> = captured
1460            .extract()
1461            .into_iter()
1462            .flat_map(|(_, data)| data)
1463            .collect();
1464        updates.sort();
1465        updates
1466    }
1467
1468    // `DataflowErrorSer` is not `Ord`, so order by the error's debug string.
1469    fn extract_err(captured: Captured<ErrUpdate>) -> Vec<(String, Timestamp, Diff)> {
1470        let mut updates: Vec<_> = captured
1471            .extract()
1472            .into_iter()
1473            .flat_map(|(_, data)| data)
1474            .map(|(e, t, d)| (format!("{e:?}"), t, d))
1475            .collect();
1476        updates.sort();
1477        updates
1478    }
1479
1480    /// Arranges `rows`, returning the sorted ok and err output.
1481    fn arrange_columnar(
1482        rows: Vec<(Row, u64)>,
1483        key: Vec<LirScalarExpr>,
1484    ) -> (Vec<OkUpdate>, Vec<(String, Timestamp, Diff)>) {
1485        let thinning = vec![0, 1];
1486        let (ok, err) = timely::execute_directly(move |worker| {
1487            worker.dataflow::<Timestamp, _, _>(|scope| {
1488                let (mut input, collection) = scope.new_collection();
1489                let (arranged, errs, _passthrough) =
1490                    CollectionBundle::<Timestamp>::arrange_collection(
1491                        &"col".to_string(),
1492                        vec_to_columnar(collection),
1493                        key,
1494                        thinning,
1495                        ArrangementBatcher::Columnation,
1496                    );
1497                let ok = arranged
1498                    .as_collection(|k, v| (k.to_row(), v.to_row()))
1499                    .inner
1500                    .capture();
1501                let err = errs.inner.capture();
1502
1503                let max_time = rows.iter().map(|(_, t)| *t).max().unwrap_or(0);
1504                for (row, time) in rows {
1505                    input.update_at(row, Timestamp::from(time), Diff::ONE);
1506                }
1507                input.advance_to(Timestamp::from(max_time + 1));
1508                input.flush();
1509                (ok, err)
1510            })
1511        });
1512        (extract_ok(ok), extract_err(err))
1513    }
1514
1515    // Uniform two columns so `Column(0)` and full-row thinning are in bounds, over three
1516    // distinct times so per-record time handling is exercised.
1517    fn test_rows() -> Vec<(Row, u64)> {
1518        vec![
1519            (Row::pack_slice(&[Datum::Int32(1), Datum::String("a")]), 0),
1520            (Row::pack_slice(&[Datum::Int32(2), Datum::String("b")]), 1),
1521            (Row::pack_slice(&[Datum::Int32(1), Datum::String("a")]), 1),
1522            (Row::pack_slice(&[Datum::Int32(3), Datum::Null]), 2),
1523        ]
1524    }
1525
1526    /// Agreeing contents do not rule out a silent `columnar_to_vec` on the ok path. That the
1527    /// operator never decodes holds by inspection, not by this test.
1528    #[mz_ore::test]
1529    fn arrange_collection_keys_correctly() {
1530        let rows = test_rows();
1531        let mut expected: Vec<OkUpdate> = rows
1532            .iter()
1533            .map(|(row, t)| {
1534                let key = Row::pack_slice(&[row.iter().next().unwrap()]);
1535                ((key, row.clone()), Timestamp::from(*t), Diff::ONE)
1536            })
1537            .collect();
1538        expected.sort();
1539
1540        let (ok, err) = arrange_columnar(rows, vec![LirScalarExpr::column(0)]);
1541        assert_eq!(ok, expected);
1542        assert!(err.is_empty());
1543    }
1544
1545    /// A key expression that always errors drives every record onto the error path.
1546    #[mz_ore::test]
1547    fn arrange_collection_error_path() {
1548        let key = vec![LirScalarExpr::literal(
1549            Err(EvalError::DivisionByZero),
1550            ReprScalarType::Int32,
1551        )];
1552        let (ok, err) = arrange_columnar(test_rows(), key);
1553
1554        assert!(ok.is_empty());
1555        assert!(!err.is_empty());
1556    }
1557
1558    /// The passthrough forwards every input record, including those whose key
1559    /// evaluation errored, so a consumer of the bundle's collection sees the
1560    /// unarranged input rather than the ok side of the arrangement.
1561    #[mz_ore::test]
1562    fn arrange_collection_passthrough_forwards_input() {
1563        let rows = test_rows();
1564        let mut expected: Vec<(Row, Timestamp, Diff)> = rows
1565            .iter()
1566            .map(|(row, t)| (row.clone(), Timestamp::from(*t), Diff::ONE))
1567            .collect();
1568        expected.sort();
1569
1570        let key = vec![LirScalarExpr::literal(
1571            Err(EvalError::DivisionByZero),
1572            ReprScalarType::Int32,
1573        )];
1574        let captured = timely::execute_directly(move |worker| {
1575            worker.dataflow::<Timestamp, _, _>(|scope| {
1576                let (mut input, collection) = scope.new_collection();
1577                let (_arranged, _errs, passthrough) =
1578                    CollectionBundle::<Timestamp>::arrange_collection(
1579                        &"col".to_string(),
1580                        vec_to_columnar(collection),
1581                        key,
1582                        vec![0, 1],
1583                        ArrangementBatcher::Columnation,
1584                    );
1585                let captured = columnar_to_vec(passthrough).inner.capture();
1586
1587                let max_time = rows.iter().map(|(_, t)| *t).max().unwrap_or(0);
1588                for (row, time) in rows {
1589                    input.update_at(row, Timestamp::from(time), Diff::ONE);
1590                }
1591                input.advance_to(Timestamp::from(max_time + 1));
1592                input.flush();
1593                captured
1594            })
1595        });
1596
1597        assert_eq!(extract_row_updates(captured), expected);
1598    }
1599
1600    fn extract_row_updates(
1601        captured: Captured<(Row, Timestamp, Diff)>,
1602    ) -> Vec<(Row, Timestamp, Diff)> {
1603        let mut updates: Vec<_> = captured
1604            .extract()
1605            .into_iter()
1606            .flat_map(|(_, data)| data)
1607            .collect();
1608        updates.sort();
1609        updates
1610    }
1611
1612    /// Arrange correctness itself is covered by `arrange_collection_arms_agree`; this only
1613    /// asserts the columnar variant survives the hand-off.
1614    #[mz_ore::test]
1615    fn get_arrange_by_produces_projected_rows() {
1616        let rows = vec![
1617            (Row::pack_slice(&[Datum::Int64(1), Datum::Int64(10)]), 0u64),
1618            (Row::pack_slice(&[Datum::Int64(2), Datum::Int64(20)]), 1),
1619            (Row::pack_slice(&[Datum::Int64(1), Datum::Int64(10)]), 1),
1620        ];
1621        // A projection is non-identity, so `as_collection_core` takes the producer path
1622        // rather than the identity passthrough.
1623        let mfp = MapFilterProject::<LirScalarExpr>::new(2)
1624            .project(vec![0])
1625            .into_plan()
1626            .expect("mfp");
1627        let mut expected: Vec<(Row, Timestamp, Diff)> = rows
1628            .iter()
1629            .map(|(r, t)| {
1630                let col0 = r.iter().next().unwrap();
1631                (Row::pack_slice(&[col0]), Timestamp::from(*t), Diff::ONE)
1632            })
1633            .collect();
1634        expected.sort();
1635
1636        let produced = timely::execute_directly(move |worker| {
1637            worker.dataflow::<Timestamp, _, _>(|scope| {
1638                let (mut input, collection) = scope.new_collection();
1639                let (_err_input, errs) = scope.new_collection::<DataflowErrorSer, Diff>();
1640                let bundle = CollectionBundle::from_edge(vec_to_columnar(collection), errs);
1641                let (edge, _errs) = bundle.as_collection_core(mfp, None, Antichain::new());
1642                let produced = columnar_to_vec(edge.clone()).inner.capture();
1643                let (_arranged, _arrange_errs, _passthrough) =
1644                    CollectionBundle::<Timestamp>::arrange_collection(
1645                        &"arrange".to_string(),
1646                        edge,
1647                        vec![LirScalarExpr::column(0)],
1648                        vec![0],
1649                        ArrangementBatcher::Columnation,
1650                    );
1651
1652                let max_time = rows.iter().map(|(_, t)| *t).max().unwrap();
1653                for (row, time) in rows {
1654                    input.update_at(row, Timestamp::from(time), Diff::ONE);
1655                }
1656                input.advance_to(Timestamp::from(max_time + 1));
1657                input.flush();
1658                produced
1659            })
1660        });
1661
1662        assert_eq!(extract_row_updates(produced), expected);
1663    }
1664
1665    /// The identity fast-path returns the unarranged input edge unchanged, with no
1666    /// repack, so its contents pass straight through.
1667    #[mz_ore::test]
1668    fn as_collection_core_identity_passes_edge_through() {
1669        let expected = vec![(
1670            Row::pack_slice(&[Datum::Int64(1)]),
1671            Timestamp::from(0u64),
1672            Diff::ONE,
1673        )];
1674        let captured = timely::execute_directly(move |worker| {
1675            worker.dataflow::<Timestamp, _, _>(|scope| {
1676                let (mut input, collection) = scope.new_collection::<Row, Diff>();
1677                let (_err_input, errs) = scope.new_collection::<DataflowErrorSer, Diff>();
1678                let bundle = CollectionBundle::from_edge(vec_to_columnar(collection), errs);
1679                let identity = MapFilterProject::<LirScalarExpr>::new(1)
1680                    .into_plan()
1681                    .expect("identity mfp");
1682                let (out, _errs) = bundle.as_collection_core(identity, None, Antichain::new());
1683                let captured = columnar_to_vec(out).inner.capture();
1684                input.update_at(
1685                    Row::pack_slice(&[Datum::Int64(1)]),
1686                    Timestamp::from(0u64),
1687                    Diff::ONE,
1688                );
1689                input.advance_to(Timestamp::from(1u64));
1690                input.flush();
1691                captured
1692            })
1693        });
1694        assert_eq!(extract_row_updates(captured), expected);
1695    }
1696
1697    /// Input rows that project to the same output row at the same time collapse to one
1698    /// record with summed diff. A plain `ColumnBuilder` would emit both.
1699    #[mz_ore::test]
1700    fn as_collection_core_consolidates_within_batch() {
1701        // `[1, 10]` and `[1, 20]` both project (dropping column 1) to `[1]` at
1702        // t=0, so their `+1` diffs must fold to `+2`. `[2, 30]` projects to a
1703        // distinct record.
1704        let rows = vec![
1705            Row::pack_slice(&[Datum::Int64(1), Datum::Int64(10)]),
1706            Row::pack_slice(&[Datum::Int64(1), Datum::Int64(20)]),
1707            Row::pack_slice(&[Datum::Int64(2), Datum::Int64(30)]),
1708        ];
1709        let mfp = MapFilterProject::<LirScalarExpr>::new(2)
1710            .project(vec![0])
1711            .into_plan()
1712            .expect("mfp");
1713        let expected = vec![
1714            (
1715                Row::pack_slice(&[Datum::Int64(1)]),
1716                Timestamp::from(0u64),
1717                Diff::from(2),
1718            ),
1719            (
1720                Row::pack_slice(&[Datum::Int64(2)]),
1721                Timestamp::from(0u64),
1722                Diff::ONE,
1723            ),
1724        ];
1725
1726        let captured = timely::execute_directly(move |worker| {
1727            worker.dataflow::<Timestamp, _, _>(|scope| {
1728                let (mut input, collection) = scope.new_collection();
1729                let (_err_input, errs) = scope.new_collection::<DataflowErrorSer, Diff>();
1730                let bundle = CollectionBundle::from_edge(vec_to_columnar(collection), errs);
1731                let (edge, _errs) = bundle.as_collection_core(mfp, None, Antichain::new());
1732                let captured = columnar_to_vec(edge).inner.capture();
1733                // Feed all rows at the same time in one batch so the fold is
1734                // within-batch, not a downstream re-consolidation.
1735                for row in rows {
1736                    input.update_at(row, Timestamp::from(0u64), Diff::ONE);
1737                }
1738                input.advance_to(Timestamp::from(1u64));
1739                input.flush();
1740                captured
1741            })
1742        });
1743
1744        assert_eq!(extract_row_updates(captured), expected);
1745    }
1746
1747    /// Keying by column 0 and thinning the value to column 1 reconstructs the original
1748    /// two-column row. The decode below belongs to the capture harness.
1749    #[mz_ore::test]
1750    fn as_specific_collection_materializes_columnar() {
1751        let rows = test_rows();
1752        let key = vec![LirScalarExpr::column(0)];
1753        let mut expected: Vec<(Row, Timestamp, Diff)> = rows
1754            .iter()
1755            .map(|(r, t)| (r.clone(), Timestamp::from(*t), Diff::ONE))
1756            .collect();
1757        expected.sort();
1758
1759        let captured = timely::execute_directly(move |worker| {
1760            worker.dataflow::<Timestamp, _, _>(|scope| {
1761                let (mut input, collection) = scope.new_collection();
1762                let (arranged, arr_errs, _passthrough) =
1763                    CollectionBundle::<Timestamp>::arrange_collection(
1764                        &"agg".to_string(),
1765                        vec_to_columnar(collection),
1766                        key.clone(),
1767                        vec![1],
1768                        ArrangementBatcher::Columnation,
1769                    );
1770                let err_arranged = {
1771                    let kc: KeyCollection<_, _, _> = arr_errs.into();
1772                    kc.mz_arrange::<
1773                        ColumnationChunker<_>,
1774                        ErrBatcher<_, _>,
1775                        ErrBuilder<_, _>,
1776                        ErrSpine<_, _>,
1777                    >("agg-errs")
1778                };
1779                // An arrangement-only bundle, as Reduce/Threshold/TopK produce.
1780                let bundle = CollectionBundle::from_columns(
1781                    0..1,
1782                    ArrangementFlavor::Local(arranged, err_arranged),
1783                );
1784                let (edge, _errs) = bundle.as_specific_collection(Some(&key));
1785                let captured = columnar_to_vec(edge).inner.capture();
1786
1787                let max_time = rows.iter().map(|(_, t)| *t).max().unwrap();
1788                for (row, time) in rows {
1789                    input.update_at(row, Timestamp::from(time), Diff::ONE);
1790                }
1791                input.advance_to(Timestamp::from(max_time + 1));
1792                input.flush();
1793                captured
1794            })
1795        });
1796
1797        assert_eq!(extract_row_updates(captured), expected);
1798    }
1799}