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