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