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